Merge branch 'master' into master

This commit is contained in:
ziveo
2018-03-18 20:02:20 -04:00
committed by GitHub
98 changed files with 16151 additions and 858 deletions

View File

@@ -10,9 +10,7 @@ import AppLayout from './AppLayout'
import MessagePanel from './MessagePanel'
import { downloadGlyphsMetadata, downloadSpriteMetadata } from '../libs/metadata'
import GlSpec from 'mapbox-gl-style-spec/reference/latest.js'
import validateStyleMin from 'mapbox-gl-style-spec/lib/validate_style.min'
import formatStyle from 'mapbox-gl-style-spec/lib/format'
import styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
import style from '../libs/style.js'
import { initialStyleUrl, loadStyleUrl } from '../libs/urlopen'
import { undoMessages, redoMessages } from '../libs/diffmessage'
@@ -21,6 +19,11 @@ import { ApiStyleStore } from '../libs/apistore'
import { RevisionStore } from '../libs/revisions'
import LayerWatcher from '../libs/layerwatcher'
import tokens from '../config/tokens.json'
import isEqual from 'lodash.isequal'
import MapboxGl from 'mapbox-gl'
import mapboxUtil from 'mapbox-gl/src/util/mapbox'
function updateRootSpec(spec, fieldName, newValues) {
return {
@@ -65,11 +68,10 @@ export default class App extends React.Component {
sources: {},
vectorLayers: {},
inspectModeEnabled: false,
spec: GlSpec,
spec: styleSpec.latest,
}
this.layerWatcher = new LayerWatcher({
onSourcesChange: v => this.setState({ sources: v }),
onVectorLayersChange: v => this.setState({ vectorLayers: v })
})
}
@@ -96,7 +98,9 @@ export default class App extends React.Component {
updateFonts(urlTemplate) {
const metadata = this.state.mapStyle.metadata || {}
const accessToken = metadata['maputnik:openmaptiles_access_token'] || tokens.openmaptiles
downloadGlyphsMetadata(urlTemplate.replace('{key}', accessToken), fonts => {
let glyphUrl = (typeof urlTemplate === 'string')? urlTemplate.replace('{key}', accessToken): urlTemplate;
downloadGlyphsMetadata(glyphUrl, fonts => {
this.setState({ spec: updateRootSpec(this.state.spec, 'glyphs', fonts)})
})
}
@@ -108,15 +112,17 @@ export default class App extends React.Component {
}
onStyleChanged(newStyle, save=true) {
if(newStyle.glyphs !== this.state.mapStyle.glyphs) {
this.updateFonts(newStyle.glyphs)
}
if(newStyle.sprite !== this.state.mapStyle.sprite) {
this.updateIcons(newStyle.sprite)
}
const errors = validateStyleMin(newStyle, GlSpec)
const errors = styleSpec.validate(newStyle, styleSpec.latest)
if(errors.length === 0) {
if(newStyle.glyphs !== this.state.mapStyle.glyphs) {
this.updateFonts(newStyle.glyphs)
}
if(newStyle.sprite !== this.state.mapStyle.sprite) {
this.updateIcons(newStyle.sprite)
}
this.revisionStore.addRevision(newStyle)
if(save) this.saveStyle(newStyle)
this.setState({
@@ -128,6 +134,8 @@ export default class App extends React.Component {
errors: errors.map(err => err.message)
})
}
this.fetchSources();
}
onUndo() {
@@ -184,11 +192,68 @@ export default class App extends React.Component {
})
}
fetchSources() {
const sourceList = {...this.state.sources};
for(let [key, val] of Object.entries(this.state.mapStyle.sources)) {
if(sourceList.hasOwnProperty(key)) {
continue;
}
sourceList[key] = {
type: val.type,
layers: []
};
if(!this.state.sources.hasOwnProperty(key) && val.type === "vector" && val.hasOwnProperty("url")) {
let url = val.url;
try {
url = mapboxUtil.normalizeSourceURL(url, MapboxGl.accessToken);
} catch(err) {
console.warn("Failed to normalizeSourceURL: ", err);
}
fetch(url)
.then((response) => {
return response.json();
})
.then((json) => {
if(!json.hasOwnProperty("vector_layers")) {
return;
}
// Create new objects before setState
const sources = Object.assign({}, this.state.sources);
for(let layer of json.vector_layers) {
sources[key].layers.push(layer.id)
}
console.debug("Updating source: "+key);
this.setState({
sources: sources
});
})
.catch((err) => {
console.error("Failed to process sources for '%s'", url, err);
})
}
}
if(!isEqual(this.state.sources, sourceList)) {
console.debug("Setting sources");
this.setState({
sources: sourceList
})
}
}
mapRenderer() {
const mapProps = {
mapStyle: style.replaceAccessToken(this.state.mapStyle),
mapStyle: style.replaceAccessToken(this.state.mapStyle, {allowFallback: true}),
onDataChange: (e) => {
this.layerWatcher.analyzeMap(e.map)
this.fetchSources();
},
}
@@ -201,7 +266,8 @@ export default class App extends React.Component {
} else {
return <MapboxGlMap {...mapProps}
inspectModeEnabled={this.state.inspectModeEnabled}
highlightedLayer={this.state.mapStyle.layers[this.state.selectedLayerIndex]} />
highlightedLayer={this.state.mapStyle.layers[this.state.selectedLayerIndex]}
onLayerSelect={this.onLayerSelect.bind(this)} />
}
}

View File

@@ -1,17 +1,18 @@
import React from 'react'
import PropTypes from 'prop-types'
import ScrollContainer from './ScrollContainer'
class AppLayout extends React.Component {
static propTypes = {
toolbar: React.PropTypes.element.isRequired,
layerList: React.PropTypes.element.isRequired,
layerEditor: React.PropTypes.element,
map: React.PropTypes.element.isRequired,
bottom: React.PropTypes.element,
toolbar: PropTypes.element.isRequired,
layerList: PropTypes.element.isRequired,
layerEditor: PropTypes.element,
map: PropTypes.element.isRequired,
bottom: PropTypes.element,
}
static childContextTypes = {
reactIconBase: React.PropTypes.object
reactIconBase: PropTypes.object
}
getChildContext() {

View File

@@ -1,11 +1,13 @@
import React from 'react'
import PropTypes from 'prop-types'
import classnames from 'classnames'
class Button extends React.Component {
static propTypes = {
onClick: React.PropTypes.func,
style: React.PropTypes.object,
className: React.PropTypes.string,
onClick: PropTypes.func,
style: PropTypes.object,
className: PropTypes.string,
children: PropTypes.node
}
render() {

View File

@@ -1,18 +1,19 @@
import React from 'react'
import PropTypes from 'prop-types'
class MessagePanel extends React.Component {
static propTypes = {
errors: React.PropTypes.array,
infos: React.PropTypes.array,
errors: PropTypes.array,
infos: PropTypes.array,
}
render() {
const errors = this.props.errors.map((m, i) => {
return <p className="maputnik-message-panel-error">{m}</p>
return <p key={"error-"+i} className="maputnik-message-panel-error">{m}</p>
})
const infos = this.props.infos.map((m, i) => {
return <p key={i}>{m}</p>
return <p key={"info-"+i}>{m}</p>
})
return <div className="maputnik-message-panel">

View File

@@ -1,9 +1,16 @@
import React from 'react'
import PropTypes from 'prop-types'
const ScrollContainer = (props) => {
return <div className="maputnik-scroll-container">
{props.children}
</div>
class ScrollContainer extends React.Component {
static propTypes = {
children: PropTypes.node
}
render() {
return <div className="maputnik-scroll-container">
{this.props.children}
</div>
}
}
export default ScrollContainer

View File

@@ -1,4 +1,5 @@
import React from 'react'
import PropTypes from 'prop-types'
import FileReaderInput from 'react-file-reader-input'
import classnames from 'classnames'
@@ -16,47 +17,71 @@ import MdFontDownload from 'react-icons/lib/md/font-download'
import HelpIcon from 'react-icons/lib/md/help-outline'
import InspectionIcon from 'react-icons/lib/md/find-in-page'
import logoImage from '../img/maputnik.png'
import logoImage from 'maputnik-design/logos/logo-color.svg'
import SettingsModal from './modals/SettingsModal'
import ExportModal from './modals/ExportModal'
import SourcesModal from './modals/SourcesModal'
import OpenModal from './modals/OpenModal'
import pkgJson from '../../package.json'
import style from '../libs/style'
function IconText(props) {
return <span className="maputnik-icon-text">{props.children}</span>
class IconText extends React.Component {
static propTypes = {
children: PropTypes.node,
}
render() {
return <span className="maputnik-icon-text">{this.props.children}</span>
}
}
function ToolbarLink(props) {
return <a
className={classnames('maputnik-toolbar-link', props.className)}
href={props.href}
target={"blank"}
>
{props.children}
</a>
class ToolbarLink extends React.Component {
static propTypes = {
className: PropTypes.string,
children: PropTypes.node,
href: PropTypes.string,
}
render() {
return <a
className={classnames('maputnik-toolbar-link', this.props.className)}
href={this.props.href}
rel="noopener noreferrer"
target="_blank"
>
{this.props.children}
</a>
}
}
function ToolbarAction(props) {
return <a
className='maputnik-toolbar-action'
onClick={props.onClick}
>
{props.children}
</a>
class ToolbarAction extends React.Component {
static propTypes = {
children: PropTypes.node,
onClick: PropTypes.func
}
render() {
return <a
className='maputnik-toolbar-action'
onClick={this.props.onClick}
>
{this.props.children}
</a>
}
}
export default class Toolbar extends React.Component {
static propTypes = {
mapStyle: React.PropTypes.object.isRequired,
inspectModeEnabled: React.PropTypes.bool.isRequired,
onStyleChanged: React.PropTypes.func.isRequired,
mapStyle: PropTypes.object.isRequired,
inspectModeEnabled: PropTypes.bool.isRequired,
onStyleChanged: PropTypes.func.isRequired,
// A new style has been uploaded
onStyleOpen: React.PropTypes.func.isRequired,
onStyleOpen: PropTypes.func.isRequired,
// A dict of source id's and the available source layers
sources: React.PropTypes.object.isRequired,
onInspectModeToggle: React.PropTypes.func.isRequired
sources: PropTypes.object.isRequired,
onInspectModeToggle: PropTypes.func.isRequired,
children: PropTypes.node
}
constructor(props) {
@@ -106,40 +131,46 @@ export default class Toolbar extends React.Component {
isOpen={this.state.isOpen.sources}
onOpenToggle={this.toggleModal.bind(this, 'sources')}
/>
<ToolbarLink
href={"https://github.com/maputnik/editor"}
className="maputnik-toolbar-logo"
>
<img src={logoImage} alt="Maputnik" />
<h1>Maputnik</h1>
</ToolbarLink>
<ToolbarAction onClick={this.toggleModal.bind(this, 'open')}>
<OpenIcon />
<IconText>Open</IconText>
</ToolbarAction>
<ToolbarAction onClick={this.toggleModal.bind(this, 'export')}>
<MdFileDownload />
<IconText>Export</IconText>
</ToolbarAction>
<ToolbarAction onClick={this.toggleModal.bind(this, 'sources')}>
<SourcesIcon />
<IconText>Sources</IconText>
</ToolbarAction>
<ToolbarAction onClick={this.toggleModal.bind(this, 'settings')}>
<SettingsIcon />
<IconText>Style Settings</IconText>
</ToolbarAction>
<ToolbarAction onClick={this.props.onInspectModeToggle}>
<InspectionIcon />
<IconText>
{ this.props.inspectModeEnabled && <span>Map Mode</span> }
{ !this.props.inspectModeEnabled && <span>Inspect Mode</span> }
</IconText>
</ToolbarAction>
<ToolbarLink href={"https://github.com/maputnik/editor/wiki"}>
<HelpIcon />
<IconText>Help</IconText>
</ToolbarLink>
<div className="maputnik-toolbar__inner">
<ToolbarLink
href={"https://github.com/maputnik/editor"}
className="maputnik-toolbar-logo"
>
<img src={logoImage} alt="Maputnik" />
<h1>Maputnik
<span className="maputnik-toolbar-version">v{pkgJson.version}</span>
</h1>
</ToolbarLink>
<div className="maputnik-toolbar__actions">
<ToolbarAction onClick={this.toggleModal.bind(this, 'open')}>
<OpenIcon />
<IconText>Open</IconText>
</ToolbarAction>
<ToolbarAction onClick={this.toggleModal.bind(this, 'export')}>
<MdFileDownload />
<IconText>Export</IconText>
</ToolbarAction>
<ToolbarAction onClick={this.toggleModal.bind(this, 'sources')}>
<SourcesIcon />
<IconText>Sources</IconText>
</ToolbarAction>
<ToolbarAction onClick={this.toggleModal.bind(this, 'settings')}>
<SettingsIcon />
<IconText>Style Settings</IconText>
</ToolbarAction>
<ToolbarAction onClick={this.props.onInspectModeToggle}>
<InspectionIcon />
<IconText>
{ this.props.inspectModeEnabled && <span>Map Mode</span> }
{ !this.props.inspectModeEnabled && <span>Inspect Mode</span> }
</IconText>
</ToolbarAction>
<ToolbarLink href={"https://github.com/maputnik/editor/wiki"}>
<HelpIcon />
<IconText>Help</IconText>
</ToolbarLink>
</div>
</div>
</div>
}
}

View File

@@ -1,6 +1,7 @@
import React from 'react'
import Color from 'color'
import ChromePicker from 'react-color/lib/components/chrome/Chrome'
import PropTypes from 'prop-types'
function formatColor(color) {
const rgb = color.rgb
@@ -10,12 +11,12 @@ function formatColor(color) {
/*** Number fields with support for min, max and units and documentation*/
class ColorField extends React.Component {
static propTypes = {
onChange: React.PropTypes.func.isRequired,
name: React.PropTypes.string.isRequired,
value: React.PropTypes.string,
doc: React.PropTypes.string,
style: React.PropTypes.object,
default: React.PropTypes.string,
onChange: PropTypes.func.isRequired,
name: PropTypes.string.isRequired,
value: PropTypes.string,
doc: PropTypes.string,
style: PropTypes.object,
default: PropTypes.string,
}
constructor(props) {
@@ -29,7 +30,7 @@ class ColorField extends React.Component {
//but I am too stupid to get it to work together with fixed position
//and scrollbars so I have to fallback to JavaScript
calcPickerOffset() {
const elem = this.refs.colorInput
const elem = this.colorInput
if(elem) {
const pos = elem.getBoundingClientRect()
return {
@@ -37,7 +38,6 @@ class ColorField extends React.Component {
left: pos.left + 196,
}
} else {
console.warn('Color field has no element to adjust position')
return {
top: 160,
left: 555,
@@ -50,7 +50,14 @@ class ColorField extends React.Component {
}
get color() {
return Color(this.props.value || '#fff').rgb()
// Catch invalid color.
try {
return Color(this.props.value).rgb()
}
catch(err) {
console.warn("Error parsing color: ", err);
return Color("rgb(255,255,255)");
}
}
render() {
@@ -91,7 +98,7 @@ class ColorField extends React.Component {
</div>
var swatchStyle = {
"background-color": this.props.value
backgroundColor: this.props.value
};
return <div className="maputnik-color-wrapper">
@@ -99,7 +106,7 @@ class ColorField extends React.Component {
<div className="maputnik-color-swatch" style={swatchStyle}></div>
<input
className="maputnik-color"
ref="colorInput"
ref={(input) => this.colorInput = input}
onClick={this.togglePicker.bind(this)}
style={this.props.style}
name={this.props.name}

View File

@@ -1,12 +1,13 @@
import React from 'react'
import PropTypes from 'prop-types'
export default class DocLabel extends React.Component {
static propTypes = {
label: React.PropTypes.oneOfType([
React.PropTypes.object,
React.PropTypes.string
label: PropTypes.oneOfType([
PropTypes.object,
PropTypes.string
]).isRequired,
doc: React.PropTypes.string.isRequired,
doc: PropTypes.string.isRequired,
}
render() {

View File

@@ -0,0 +1,140 @@
import React from 'react'
import PropTypes from 'prop-types'
import SpecProperty from './_SpecProperty'
import DataProperty from './_DataProperty'
import ZoomProperty from './_ZoomProperty'
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: PropTypes.func.isRequired,
fieldName: PropTypes.string.isRequired,
fieldSpec: PropTypes.object.isRequired,
value: PropTypes.oneOfType([
PropTypes.object,
PropTypes.string,
PropTypes.number,
PropTypes.bool,
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)
}
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)
}
render() {
const propClass = this.props.fieldSpec.default === this.props.value ? "maputnik-default-property" : "maputnik-modified-property"
let specField;
if (isZoomField(this.props.value)) {
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)) {
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 {
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}>
{specField}
</div>
}
}

View File

@@ -1,6 +1,7 @@
import React from 'react'
import PropTypes from 'prop-types'
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
@@ -35,10 +36,10 @@ function getGroupName(spec, layerType, fieldName) {
export default class PropertyGroup extends React.Component {
static propTypes = {
layer: React.PropTypes.object.isRequired,
groupFields: React.PropTypes.array.isRequired,
onChange: React.PropTypes.func.isRequired,
spec: React.PropTypes.object.isRequired,
layer: PropTypes.object.isRequired,
groupFields: PropTypes.array.isRequired,
onChange: PropTypes.func.isRequired,
spec: PropTypes.object.isRequired,
}
onPropertyChange(property, newValue) {
@@ -54,7 +55,7 @@ export default class PropertyGroup extends React.Component {
const layout = this.props.layer.layout || {}
const fieldValue = fieldName in paint ? paint[fieldName] : layout[fieldName]
return <ZoomSpecField
return <FunctionSpecField
onChange={this.onPropertyChange.bind(this)}
key={fieldName}
fieldName={fieldName}

View File

@@ -1,4 +1,5 @@
import React from 'react'
import PropTypes from 'prop-types'
import color from 'color'
import ColorField from './ColorField'
@@ -8,6 +9,7 @@ import StringInput from '../inputs/StringInput'
import SelectInput from '../inputs/SelectInput'
import MultiButtonInput from '../inputs/MultiButtonInput'
import ArrayInput from '../inputs/ArrayInput'
import DynamicArrayInput from '../inputs/DynamicArrayInput'
import FontInput from '../inputs/FontInput'
import IconInput from '../inputs/IconInput'
import capitalize from 'lodash.capitalize'
@@ -35,16 +37,17 @@ function optionsLabelLength(options) {
* to display @{value}. */
export default class SpecField 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.string,
React.PropTypes.number,
React.PropTypes.array,
onChange: PropTypes.func.isRequired,
fieldName: PropTypes.string.isRequired,
fieldSpec: PropTypes.object.isRequired,
value: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
PropTypes.array,
PropTypes.bool
]),
/** Override the style of the field */
style: React.PropTypes.object,
style: PropTypes.object,
}
render() {
@@ -105,11 +108,18 @@ export default class SpecField extends React.Component {
fonts={this.props.fieldSpec.values}
/>
} else {
return <ArrayInput
{...commonProps}
type={this.props.fieldSpec.value}
length={this.props.fieldSpec.length}
/>
if (this.props.fieldSpec.length) {
return <ArrayInput
{...commonProps}
type={this.props.fieldSpec.value}
length={this.props.fieldSpec.length}
/>
} else {
return <DynamicArrayInput
{...commonProps}
type={this.props.fieldSpec.value}
/>
}
}
default: return null
}

View File

@@ -1,180 +0,0 @@
import React from 'react'
import Color from 'color'
import Button from '../Button'
import SpecField from './SpecField'
import NumberInput from '../inputs/NumberInput'
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 capitalize from 'lodash.capitalize'
function isZoomField(value) {
return typeof value === 'object' && value.stops
}
/** Supports displaying spec field for zoom function objects
* https://www.mapbox.com/mapbox-gl-style-spec/#types-function-zoom-property
*/
export default class ZoomSpecProperty 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,
]),
}
addStop() {
const stops = this.props.value.stops.slice(0)
const lastStop = stops[stops.length - 1]
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)
}
changeStop(changeIdx, zoomLevel, value) {
const stops = this.props.value.stops.slice(0)
stops[changeIdx] = [zoomLevel, value]
const changedValue = {
...this.props.value,
stops: stops,
}
this.props.onChange(this.props.fieldName, changedValue)
}
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() {
let zoomBtn = null
if(this.props.fieldSpec['zoom-function']) {
zoomBtn = <MakeZoomFunctionButton onClick={this.makeZoomFunction.bind(this)} />
}
return <InputBlock
doc={this.props.fieldSpec.doc}
label={labelFromFieldName(this.props.fieldName)}
action={zoomBtn}
>
<SpecField {...this.props} />
</InputBlock>
}
render() {
const propClass = this.props.fieldSpec.default === this.props.value ? "maputnik-default-property" : "maputnik-modified-property"
return <div className={propClass}>
{isZoomField(this.props.value) ? this.renderZoomProperty() : this.renderProperty()}
</div>
}
}
function MakeZoomFunctionButton(props) {
return <Button
className="maputnik-make-zoom-function"
onClick={props.onClick}
>
<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>
}
function DeleteStopButton(props) {
return <Button
className="maputnik-delete-stop"
onClick={props.onClick}
>
<DocLabel
label={<DeleteIcon />}
doc={"Remove zoom level stop."}
/>
</Button>
}
function labelFromFieldName(fieldName) {
let label = fieldName.split('-').slice(1).join(' ')
return capitalize(label)
}

View 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>
}
}

View 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>
}
}

View 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
}
}
}

View 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>
}
}

View 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>
}
}

View 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)
}

View File

@@ -1,7 +1,8 @@
import React from 'react'
import PropTypes from 'prop-types'
import { combiningFilterOps } from '../../libs/filterops.js'
import GlSpec from 'mapbox-gl-style-spec/reference/latest.js'
import styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
import DocLabel from '../fields/DocLabel'
import SelectInput from '../inputs/SelectInput'
import SingleFilterEditor from './SingleFilterEditor'
@@ -26,9 +27,9 @@ function hasNestedCombiningFilter(filter) {
export default class CombiningFilterEditor extends React.Component {
static propTypes = {
/** Properties of the vector layer and the available fields */
properties: React.PropTypes.object,
filter: React.PropTypes.array,
onChange: React.PropTypes.func.isRequired,
properties: PropTypes.object,
filter: PropTypes.array,
onChange: PropTypes.func.isRequired,
}
// Convert filter to combining filter
@@ -91,7 +92,7 @@ export default class CombiningFilterEditor extends React.Component {
<div className="maputnik-filter-editor-compound-select">
<DocLabel
label={"Compound Filter"}
doc={GlSpec.layer.filter.doc + " Combine multiple filters together by using a compound filter."}
doc={styleSpec.latest.layer.filter.doc + " Combine multiple filters together by using a compound filter."}
/>
<SelectInput
value={combiningOp}

View File

@@ -1,11 +1,12 @@
import React from 'react'
import PropTypes from 'prop-types'
import Button from '../Button'
import DeleteIcon from 'react-icons/lib/md/delete'
class FilterEditorBlock extends React.Component {
static propTypes = {
onDelete: React.PropTypes.func.isRequired,
children: React.PropTypes.element.isRequired,
onDelete: PropTypes.func.isRequired,
children: PropTypes.element.isRequired,
}
render() {

View File

@@ -1,4 +1,5 @@
import React from 'react'
import PropTypes from 'prop-types'
import { otherFilterOps } from '../../libs/filterops.js'
import StringInput from '../inputs/StringInput'
@@ -11,11 +12,34 @@ function tryParseInt(v) {
return parseFloat(v)
}
function tryParseBool(v) {
const isString = (typeof(v) === "string");
if(!isString) {
return v;
}
if(v.match(/^\s*true\s*$/)) {
return true;
}
else if(v.match(/^\s*false\s*$/)) {
return false;
}
else {
return v;
}
}
function parseFilter(v) {
v = tryParseInt(v);
v = tryParseBool(v);
return v;
}
class SingleFilterEditor extends React.Component {
static propTypes = {
filter: React.PropTypes.array.isRequired,
onChange: React.PropTypes.func.isRequired,
properties: React.PropTypes.object,
filter: PropTypes.array.isRequired,
onChange: PropTypes.func.isRequired,
properties: PropTypes.object,
}
static defaultProps = {
@@ -23,7 +47,7 @@ class SingleFilterEditor extends React.Component {
}
onFilterPartChanged(filterOp, propertyName, filterArgs) {
let newFilter = [filterOp, propertyName, ...filterArgs.map(tryParseInt)]
let newFilter = [filterOp, propertyName, ...filterArgs.map(parseFilter)]
if(filterOp === 'has' || filterOp === '!has') {
newFilter = [filterOp, propertyName]
} else if(filterArgs.length === 0) {

View File

@@ -1,4 +1,5 @@
import React from 'react'
import PropTypes from 'prop-types'
import LineIcon from './LineIcon.jsx'
import FillIcon from './FillIcon.jsx'
@@ -8,8 +9,8 @@ import CircleIcon from './CircleIcon.jsx'
class LayerIcon extends React.Component {
static propTypes = {
type: React.PropTypes.string.isRequired,
style: React.PropTypes.object,
type: PropTypes.string.isRequired,
style: PropTypes.object,
}
render() {
@@ -17,6 +18,8 @@ class LayerIcon extends React.Component {
switch(this.props.type) {
case 'fill-extrusion': return <BackgroundIcon {...iconProps} />
case 'raster': return <FillIcon {...iconProps} />
case 'hillshade': return <FillIcon {...iconProps} />
case 'heatmap': return <FillIcon {...iconProps} />
case 'fill': return <FillIcon {...iconProps} />
case 'background': return <BackgroundIcon {...iconProps} />
case 'line': return <LineIcon {...iconProps} />

View File

@@ -6,7 +6,7 @@ export default class FillIcon extends React.Component {
render() {
return (
<IconBase viewBox="0 0 20 20" {...this.props}>
<path id="path8" d="M 12.34,1.29 C 12.5114,1.1076 12.7497,1.0029 13,1 13.5523,1 14,1.4477 14,2 14.0047,2.2478 13.907,2.4866 13.73,2.66 9.785626,6.5516986 6.6148407,9.7551593 2.65,13.72 2.4793,13.8963 2.2453,13.9971 2,14 1.4477,14 1,13.5523 1,13 0.9953,12.7522 1.093,12.5134 1.27,12.34 4.9761967,8.7018093 9.0356422,4.5930579 12.34,1.29 Z" transform="translate(2,2)" />
<path d="M 12.34,1.29 C 12.5114,1.1076 12.7497,1.0029 13,1 13.5523,1 14,1.4477 14,2 14.0047,2.2478 13.907,2.4866 13.73,2.66 9.785626,6.5516986 6.6148407,9.7551593 2.65,13.72 2.4793,13.8963 2.2453,13.9971 2,14 1.4477,14 1,13.5523 1,13 0.9953,12.7522 1.093,12.5134 1.27,12.34 4.9761967,8.7018093 9.0356422,4.5930579 12.34,1.29 Z" transform="translate(2,2)" />
</IconBase>
)
}

View File

@@ -6,8 +6,8 @@ export default class SymbolIcon extends React.Component {
render() {
return (
<IconBase viewBox="0 0 20 20" {...this.props}>
<g id="svg_1" transform="matrix(1.2718518,0,0,1.2601269,16.559526,-7.4065264)">
<path id="svg_2" d="m -9.7959773,11.060163 c -0.3734787,-0.724437 -0.3580577,-1.2147051 -0.00547,-1.8767873 l 8.6034029,-0.019416 c 0.39670292,0.6865644 0.38365934,1.4750693 -0.011097,1.8864953 l -3.1359613,-0.0033 -0.013695,7.1305 c -0.4055357,0.397083 -1.3146432,0.397083 -1.7769191,-0.02274 l 0.030226,-7.104422 z" />
<g transform="matrix(1.2718518,0,0,1.2601269,16.559526,-7.4065264)">
<path d="m -9.7959773,11.060163 c -0.3734787,-0.724437 -0.3580577,-1.2147051 -0.00547,-1.8767873 l 8.6034029,-0.019416 c 0.39670292,0.6865644 0.38365934,1.4750693 -0.011097,1.8864953 l -3.1359613,-0.0033 -0.013695,7.1305 c -0.4055357,0.397083 -1.3146432,0.397083 -1.7769191,-0.02274 l 0.030226,-7.104422 z" />
</g>
</IconBase>
)

View File

@@ -1,14 +1,15 @@
import React from 'react'
import PropTypes from 'prop-types'
import StringInput from './StringInput'
import NumberInput from './NumberInput'
class ArrayInput extends React.Component {
static propTypes = {
value: React.PropTypes.array,
type: React.PropTypes.string,
length: React.PropTypes.number,
default: React.PropTypes.array,
onChange: React.PropTypes.func,
value: PropTypes.array,
type: PropTypes.string,
length: PropTypes.number,
default: PropTypes.array,
onChange: PropTypes.func,
}
changeValue(idx, newValue) {

View File

@@ -1,13 +1,17 @@
import React from 'react'
import PropTypes from 'prop-types'
import classnames from 'classnames'
import Autocomplete from 'react-autocomplete'
const MAX_HEIGHT = 140;
class AutocompleteInput extends React.Component {
static propTypes = {
value: React.PropTypes.string,
options: React.PropTypes.array,
onChange: React.PropTypes.func,
value: PropTypes.string,
options: PropTypes.array,
onChange: PropTypes.func,
keepMenuWithinWindowBounds: PropTypes.bool
}
static defaultProps = {
@@ -15,35 +19,73 @@ class AutocompleteInput extends React.Component {
options: [],
}
render() {
const AutocompleteMenu = (items, value, style) => <div className={"maputnik-autocomplete-menu"} children={items} />
constructor(props) {
super(props);
this.state = {
maxHeight: MAX_HEIGHT
};
}
return <Autocomplete
wrapperProps={{
className: "maputnik-autocomplete",
style: null
calcMaxHeight() {
if(this.props.keepMenuWithinWindowBounds) {
const maxHeight = window.innerHeight - this.autocompleteMenuEl.getBoundingClientRect().top;
const limitedMaxHeight = Math.min(maxHeight, MAX_HEIGHT);
if(limitedMaxHeight != this.state.maxHeight) {
this.setState({
maxHeight: limitedMaxHeight
})
}
}
}
componentDidMount() {
this.calcMaxHeight();
}
componentDidUpdate() {
this.calcMaxHeight();
}
render() {
return <div
ref={(el) => {
this.autocompleteMenuEl = el;
}}
renderMenu={AutocompleteMenu}
inputProps={{
className: "maputnik-string"
}}
value={this.props.value}
items={this.props.options}
getItemValue={(item) => item[0]}
onSelect={v => this.props.onChange(v)}
onChange={(e, v) => this.props.onChange(v)}
renderItem={(item, isHighlighted) => (
<div
key={item[0]}
className={classnames({
"maputnik-autocomplete-menu-item": true,
"maputnik-autocomplete-menu-item-selected": isHighlighted,
})}
>
{item[1]}
</div>
)}
/>
>
<Autocomplete
menuStyle={{
position: "absolute",
overflow: "auto",
maxHeight: this.state.maxHeight
}}
wrapperProps={{
className: "maputnik-autocomplete",
style: null
}}
inputProps={{
className: "maputnik-string"
}}
value={this.props.value}
items={this.props.options}
getItemValue={(item) => item[0]}
onSelect={v => this.props.onChange(v)}
onChange={(e, v) => this.props.onChange(v)}
shouldItemRender={(item, value) => {
return item[0].toLowerCase().indexOf(value.toLowerCase()) > -1
}}
renderItem={(item, isHighlighted) => (
<div
key={item[0]}
className={classnames({
"maputnik-autocomplete-menu-item": true,
"maputnik-autocomplete-menu-item-selected": isHighlighted,
})}
>
{item[1]}
</div>
)}
/>
</div>
}
}

View File

@@ -1,10 +1,11 @@
import React from 'react'
import PropTypes from 'prop-types'
class CheckboxInput extends React.Component {
static propTypes = {
value: React.PropTypes.bool.isRequired,
style: React.PropTypes.object,
onChange: React.PropTypes.func,
value: PropTypes.bool.isRequired,
style: PropTypes.object,
onChange: PropTypes.func,
}
render() {

View File

@@ -0,0 +1,106 @@
import React from 'react'
import PropTypes from 'prop-types'
import StringInput from './StringInput'
import NumberInput from './NumberInput'
import Button from '../Button'
import DeleteIcon from 'react-icons/lib/md/delete'
import DocLabel from '../fields/DocLabel'
class DynamicArrayInput extends React.Component {
static propTypes = {
value: PropTypes.array,
type: PropTypes.string,
default: PropTypes.array,
onChange: PropTypes.func,
style: PropTypes.object,
}
changeValue(idx, newValue) {
console.log(idx, newValue)
const values = this.values.slice(0)
values[idx] = newValue
this.props.onChange(values)
}
get values() {
return this.props.value || this.props.default || []
}
addValue() {
const values = this.values.slice(0)
if (this.props.type === 'number') {
values.push(0)
} else {
values.push("")
}
this.props.onChange(values)
}
deleteValue(valueIdx) {
const values = this.values.slice(0)
values.splice(valueIdx, 1)
this.props.onChange(values)
}
render() {
const inputs = this.values.map((v, i) => {
const deleteValueBtn= <DeleteValueButton onClick={this.deleteValue.bind(this, i)} />
const input = this.props.type === 'number'
? <NumberInput
value={v}
onChange={this.changeValue.bind(this, i)}
/>
: <StringInput
value={v}
onChange={this.changeValue.bind(this, i)}
/>
return <div
style={this.props.style}
key={i}
className="maputnik-array-block"
>
<div className="maputnik-array-block-action">
{deleteValueBtn}
</div>
<div className="maputnik-array-block-content">
{input}
</div>
</div>
})
return <div className="maputnik-array">
{inputs}
<Button
className="maputnik-array-add-value"
onClick={this.addValue.bind(this)}
>
Add value
</Button>
</div>
}
}
class DeleteValueButton extends React.Component {
static propTypes = {
onClick: PropTypes.func,
}
render() {
return <Button
className="maputnik-delete-stop"
onClick={this.props.onClick}
>
<DocLabel
label={<DeleteIcon />}
doc={"Remove array entry."}
/>
</Button>
}
}
export default DynamicArrayInput

View File

@@ -1,12 +1,14 @@
import React from 'react'
import PropTypes from 'prop-types'
import AutocompleteInput from './AutocompleteInput'
class FontInput extends React.Component {
static propTypes = {
value: React.PropTypes.array.isRequired,
fonts: React.PropTypes.array,
style: React.PropTypes.object,
onChange: React.PropTypes.func.isRequired,
value: PropTypes.array.isRequired,
default: PropTypes.array,
fonts: PropTypes.array,
style: PropTypes.object,
onChange: PropTypes.func.isRequired,
}
static defaultProps = {

View File

@@ -1,13 +1,14 @@
import React from 'react'
import PropTypes from 'prop-types'
import AutocompleteInput from './AutocompleteInput'
class IconInput extends React.Component {
static propTypes = {
value: React.PropTypes.array,
icons: React.PropTypes.array,
style: React.PropTypes.object,
onChange: React.PropTypes.func.isRequired,
value: PropTypes.array,
icons: PropTypes.array,
style: PropTypes.object,
onChange: PropTypes.func.isRequired,
}
static defaultProps = {

View File

@@ -1,18 +1,20 @@
import React from 'react'
import PropTypes from 'prop-types'
import classnames from 'classnames'
import DocLabel from '../fields/DocLabel'
/** Wrap a component with a label */
class InputBlock extends React.Component {
static propTypes = {
label: React.PropTypes.oneOfType([
React.PropTypes.string,
React.PropTypes.element,
label: PropTypes.oneOfType([
PropTypes.string,
PropTypes.element,
]).isRequired,
doc: React.PropTypes.string,
action: React.PropTypes.element,
children: React.PropTypes.element.isRequired,
style: React.PropTypes.object,
doc: PropTypes.string,
action: PropTypes.element,
children: PropTypes.node.isRequired,
style: PropTypes.object,
onChange: PropTypes.func,
}
onChange(e) {

View File

@@ -1,12 +1,13 @@
import React from 'react'
import PropTypes from 'prop-types'
import classnames from 'classnames'
import Button from '../Button'
class MultiButtonInput extends React.Component {
static propTypes = {
value: React.PropTypes.string.isRequired,
options: React.PropTypes.array.isRequired,
onChange: React.PropTypes.func.isRequired,
value: PropTypes.string.isRequired,
options: PropTypes.array.isRequired,
onChange: PropTypes.func.isRequired,
}
render() {

View File

@@ -1,12 +1,13 @@
import React from 'react'
import PropTypes from 'prop-types'
class NumberInput extends React.Component {
static propTypes = {
value: React.PropTypes.number,
default: React.PropTypes.number,
min: React.PropTypes.number,
max: React.PropTypes.number,
onChange: React.PropTypes.func,
value: PropTypes.number,
default: PropTypes.number,
min: PropTypes.number,
max: PropTypes.number,
onChange: PropTypes.func,
}
constructor(props) {

View File

@@ -1,11 +1,12 @@
import React from 'react'
import PropTypes from 'prop-types'
class SelectInput extends React.Component {
static propTypes = {
value: React.PropTypes.string.isRequired,
options: React.PropTypes.array.isRequired,
style: React.PropTypes.object,
onChange: React.PropTypes.func.isRequired,
value: PropTypes.string.isRequired,
options: PropTypes.array.isRequired,
style: PropTypes.object,
onChange: PropTypes.func.isRequired,
}

View File

@@ -1,11 +1,13 @@
import React from 'react'
import PropTypes from 'prop-types'
class StringInput extends React.Component {
static propTypes = {
value: React.PropTypes.string,
style: React.PropTypes.object,
default: React.PropTypes.string,
onChange: React.PropTypes.func,
value: PropTypes.string,
style: PropTypes.object,
default: PropTypes.string,
onChange: PropTypes.func,
multi: PropTypes.bool,
}
constructor(props) {

View File

@@ -1,11 +1,12 @@
import React from 'react'
import PropTypes from 'prop-types'
import CollapseOpenIcon from 'react-icons/lib/md/arrow-drop-down'
import CollapseCloseIcon from 'react-icons/lib/md/arrow-drop-up'
export default class Collapser extends React.Component {
static propTypes = {
isCollapsed: React.PropTypes.bool.isRequired,
style: React.PropTypes.object,
isCollapsed: PropTypes.bool.isRequired,
style: PropTypes.object,
}
render() {

View File

@@ -1,16 +1,17 @@
import React from 'react'
import PropTypes from 'prop-types'
import InputBlock from '../inputs/InputBlock'
import StringInput from '../inputs/StringInput'
class MetadataBlock extends React.Component {
static propTypes = {
value: React.PropTypes.string,
onChange: React.PropTypes.func.isRequired,
value: PropTypes.string,
onChange: PropTypes.func.isRequired,
}
render() {
return <InputBlock label={"Comments"}>
return <InputBlock label={"Comments"} doc={"Comments for the current layer. This is non-standard and not in the spec."}>
<StringInput
multi={true}
value={this.props.value}

View File

@@ -1,6 +1,7 @@
import React from 'react'
import PropTypes from 'prop-types'
import CodeMirror from 'react-codemirror'
import {Controlled as CodeMirror} from 'react-codemirror2'
import InputBlock from '../inputs/InputBlock'
import StringInput from '../inputs/StringInput'
import SelectInput from '../inputs/SelectInput'
@@ -18,8 +19,8 @@ import '../../vendor/codemirror/addon/lint/json-lint'
class JSONEditor extends React.Component {
static propTypes = {
layer: React.PropTypes.object.isRequired,
onChange: React.PropTypes.func,
layer: PropTypes.object.isRequired,
onChange: PropTypes.func,
}
constructor(props) {
@@ -80,7 +81,7 @@ class JSONEditor extends React.Component {
return <CodeMirror
value={this.state.code}
onChange={this.onCodeUpdate.bind(this)}
onBeforeChange={(editor, data, value) => this.onCodeUpdate(value)}
onFocusChange={focused => focused ? true : this.resetValue()}
options={codeMirrorOptions}
/>

View File

@@ -1,4 +1,5 @@
import React from 'react'
import PropTypes from 'prop-types'
import JSONEditor from './JSONEditor'
import FilterEditor from '../filter/FilterEditor'
@@ -43,12 +44,12 @@ function layoutGroups(layerType) {
/** Layer editor supporting multiple types of layers. */
export default class LayerEditor extends React.Component {
static propTypes = {
layer: React.PropTypes.object.isRequired,
sources: React.PropTypes.object,
vectorLayers: React.PropTypes.object,
spec: React.PropTypes.object.isRequired,
onLayerChanged: React.PropTypes.func,
onLayerIdChange: React.PropTypes.func,
layer: PropTypes.object.isRequired,
sources: PropTypes.object,
vectorLayers: PropTypes.object,
spec: PropTypes.object.isRequired,
onLayerChanged: PropTypes.func,
onLayerIdChange: PropTypes.func,
}
static defaultProps = {
@@ -58,7 +59,7 @@ export default class LayerEditor extends React.Component {
}
static childContextTypes = {
reactIconBase: React.PropTypes.object
reactIconBase: PropTypes.object
}
constructor(props) {
@@ -116,6 +117,11 @@ export default class LayerEditor extends React.Component {
comment = this.props.layer.metadata['maputnik:comment']
}
let sourceLayerIds;
if(this.props.sources.hasOwnProperty(this.props.layer.source)) {
sourceLayerIds = this.props.sources[this.props.layer.source].layers;
}
switch(type) {
case 'layer': return <div>
<LayerIdBlock
@@ -132,8 +138,9 @@ export default class LayerEditor extends React.Component {
onChange={v => this.changeProperty(null, 'source', v)}
/>
}
{this.props.layer.type !== 'raster' && this.props.layer.type !== 'background' && <LayerSourceLayerBlock
sourceLayerIds={this.props.sources[this.props.layer.source]}
{['background', 'raster', 'hillshade', 'heatmap'].indexOf(this.state.type) < 0 &&
<LayerSourceLayerBlock
sourceLayerIds={sourceLayerIds}
value={this.props.layer['source-layer']}
onChange={v => this.changeProperty(null, 'source-layer', v)}
/>

View File

@@ -1,13 +1,14 @@
import React from 'react'
import PropTypes from 'prop-types'
import Collapse from 'react-collapse'
import Collapser from './Collapser'
export default class LayerEditorGroup extends React.Component {
static propTypes = {
title: React.PropTypes.string.isRequired,
isActive: React.PropTypes.bool.isRequired,
children: React.PropTypes.element.isRequired,
onActiveToggle: React.PropTypes.func.isRequired
title: PropTypes.string.isRequired,
isActive: PropTypes.bool.isRequired,
children: PropTypes.element.isRequired,
onActiveToggle: PropTypes.func.isRequired
}
render() {

View File

@@ -1,17 +1,18 @@
import React from 'react'
import PropTypes from 'prop-types'
import GlSpec from 'mapbox-gl-style-spec/reference/latest.js'
import styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
import InputBlock from '../inputs/InputBlock'
import StringInput from '../inputs/StringInput'
class LayerIdBlock extends React.Component {
static propTypes = {
value: React.PropTypes.string.isRequired,
onChange: React.PropTypes.func.isRequired,
value: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired,
}
render() {
return <InputBlock label={"ID"} doc={GlSpec.layer.id.doc}>
return <InputBlock label={"ID"} doc={styleSpec.latest.layer.id.doc}>
<StringInput
value={this.props.value}
onChange={this.props.onChange}

View File

@@ -1,4 +1,5 @@
import React from 'react'
import PropTypes from 'prop-types'
import classnames from 'classnames'
import cloneDeep from 'lodash.clonedeep'
@@ -12,11 +13,11 @@ import style from '../../libs/style.js'
import {SortableContainer, SortableHandle, arrayMove} from 'react-sortable-hoc';
const layerListPropTypes = {
layers: React.PropTypes.array.isRequired,
selectedLayerIndex: React.PropTypes.number.isRequired,
onLayersChange: React.PropTypes.func.isRequired,
onLayerSelect: React.PropTypes.func,
sources: React.PropTypes.object.isRequired,
layers: PropTypes.array.isRequired,
selectedLayerIndex: PropTypes.number.isRequired,
onLayersChange: PropTypes.func.isRequired,
onLayerSelect: PropTypes.func,
sources: PropTypes.object.isRequired,
}
function layerPrefix(name) {
@@ -49,6 +50,7 @@ class LayerListContainer extends React.Component {
super(props)
this.state = {
collapsedGroups: {},
areAllGroupsExpanded: false,
isOpen: {
add: false,
}
@@ -94,6 +96,31 @@ class LayerListContainer extends React.Component {
})
}
toggleLayers() {
let idx=0
let newGroups=[]
this.groupedLayers().forEach(layers => {
const groupPrefix = layerPrefix(layers[0].id)
const lookupKey = [groupPrefix, idx].join('-')
if (layers.length > 1) {
newGroups[lookupKey] = this.state.areAllGroupsExpanded
}
layers.forEach((layer) => {
idx += 1
})
});
this.setState({
collapsedGroups: newGroups,
areAllGroupsExpanded: !this.state.areAllGroupsExpanded
})
}
groupedLayers() {
const groups = []
for (let i = 0; i < this.props.layers.length; i++) {
@@ -137,7 +164,7 @@ class LayerListContainer extends React.Component {
const grp = <LayerListGroup
key={[groupPrefix, idx].join('-')}
title={groupPrefix}
isActive={!this.isCollapsed(groupPrefix, idx)}
isActive={!this.isCollapsed(groupPrefix, idx) || idx === this.props.selectedLayerIndex}
onActiveToggle={this.toggleLayerGroup.bind(this, groupPrefix, idx)}
/>
listItems.push(grp)
@@ -145,9 +172,10 @@ class LayerListContainer extends React.Component {
layers.forEach((layer, idxInGroup) => {
const groupIdx = findClosestCommonPrefix(this.props.layers, idx)
const listItem = <LayerListItem
className={classnames({
'maputnik-layer-list-item-collapsed': layers.length > 1 && this.isCollapsed(groupPrefix, groupIdx),
'maputnik-layer-list-item-collapsed': layers.length > 1 && this.isCollapsed(groupPrefix, groupIdx) && idx !== this.props.selectedLayerIndex,
'maputnik-layer-list-item-group-last': idxInGroup == layers.length - 1 && layers.length > 1
})}
index={idx}
@@ -177,11 +205,24 @@ class LayerListContainer extends React.Component {
<header className="maputnik-layer-list-header">
<span className="maputnik-layer-list-header-title">Layers</span>
<span className="maputnik-space" />
<Button
onClick={this.toggleModal.bind(this, 'add')}
className="maputnik-add-layer">
Add Layer
</Button>
<div className="maputnik-default-property">
<div className="maputnik-multibutton">
<a
onClick={this.toggleLayers.bind(this)}
className="maputnik-button">
{this.state.areAllGroupsExpanded === true ? "Collapse" : "Expand"}
</a>
</div>
</div>
<div className="maputnik-default-property">
<div className="maputnik-multibutton">
<a
onClick={this.toggleModal.bind(this, 'add')}
className="maputnik-button maputnik-button-selected">
Add Layer
</a>
</div>
</div>
</header>
<ul className="maputnik-layer-list-container">
{listItems}

View File

@@ -1,16 +1,16 @@
import React from 'react'
import PropTypes from 'prop-types'
import Collapser from './Collapser'
export default class LayerEditorGroup extends React.Component {
export default class LayerListGroup extends React.Component {
static propTypes = {
title: React.PropTypes.string.isRequired,
children: React.PropTypes.element.isRequired,
isActive: React.PropTypes.bool.isRequired,
onActiveToggle: React.PropTypes.func.isRequired
title: PropTypes.string.isRequired,
isActive: PropTypes.bool.isRequired,
onActiveToggle: PropTypes.func.isRequired
}
render() {
return <div className="maputnik-layer-list-group">
return <li className="maputnik-layer-list-group">
<div className="maputnik-layer-list-group-header"
onClick={e => this.props.onActiveToggle(!this.props.isActive)}
>
@@ -21,6 +21,6 @@ export default class LayerEditorGroup extends React.Component {
isCollapsed={this.props.isActive}
/>
</div>
</div>
</li>
}
}

View File

@@ -1,4 +1,5 @@
import React from 'react'
import PropTypes from 'prop-types'
import Color from 'color'
import classnames from 'classnames'
@@ -30,8 +31,8 @@ class LayerTypeDragHandle extends React.Component {
class IconAction extends React.Component {
static propTypes = {
action: React.PropTypes.string.isRequired,
onClick: React.PropTypes.func.isRequired,
action: PropTypes.string.isRequired,
onClick: PropTypes.func.isRequired,
}
renderIcon() {
@@ -57,16 +58,16 @@ class IconAction extends React.Component {
@SortableElement
class LayerListItem extends React.Component {
static propTypes = {
layerId: React.PropTypes.string.isRequired,
layerType: React.PropTypes.string.isRequired,
isSelected: React.PropTypes.bool,
visibility: React.PropTypes.string,
className: React.PropTypes.string,
layerId: PropTypes.string.isRequired,
layerType: PropTypes.string.isRequired,
isSelected: PropTypes.bool,
visibility: PropTypes.string,
className: PropTypes.string,
onLayerSelect: React.PropTypes.func.isRequired,
onLayerCopy: React.PropTypes.func,
onLayerDestroy: React.PropTypes.func,
onLayerVisibilityToggle: React.PropTypes.func,
onLayerSelect: PropTypes.func.isRequired,
onLayerCopy: PropTypes.func,
onLayerDestroy: PropTypes.func,
onLayerVisibilityToggle: PropTypes.func,
}
static defaultProps = {
@@ -78,7 +79,7 @@ class LayerListItem extends React.Component {
}
static childContextTypes = {
reactIconBase: React.PropTypes.object
reactIconBase: PropTypes.object
}
getChildContext() {

View File

@@ -1,6 +1,7 @@
import React from 'react'
import PropTypes from 'prop-types'
import GlSpec from 'mapbox-gl-style-spec/reference/latest.js'
import styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
import InputBlock from '../inputs/InputBlock'
import StringInput from '../inputs/StringInput'
import SelectInput from '../inputs/SelectInput'
@@ -8,9 +9,9 @@ import AutocompleteInput from '../inputs/AutocompleteInput'
class LayerSourceBlock extends React.Component {
static propTypes = {
value: React.PropTypes.string,
onChange: React.PropTypes.func,
sourceIds: React.PropTypes.array,
value: PropTypes.string,
onChange: PropTypes.func,
sourceIds: PropTypes.array,
}
static defaultProps = {
@@ -19,7 +20,7 @@ class LayerSourceBlock extends React.Component {
}
render() {
return <InputBlock label={"Source"} doc={GlSpec.layer.source.doc}>
return <InputBlock label={"Source"} doc={styleSpec.latest.layer.source.doc}>
<AutocompleteInput
value={this.props.value}
onChange={this.props.onChange}

View File

@@ -1,6 +1,7 @@
import React from 'react'
import PropTypes from 'prop-types'
import GlSpec from 'mapbox-gl-style-spec/reference/latest.js'
import styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
import InputBlock from '../inputs/InputBlock'
import StringInput from '../inputs/StringInput'
import SelectInput from '../inputs/SelectInput'
@@ -8,19 +9,22 @@ import AutocompleteInput from '../inputs/AutocompleteInput'
class LayerSourceLayer extends React.Component {
static propTypes = {
value: React.PropTypes.string,
onChange: React.PropTypes.func,
sourceLayerIds: React.PropTypes.array,
value: PropTypes.string,
onChange: PropTypes.func,
sourceLayerIds: PropTypes.array,
isFixed: PropTypes.bool,
}
static defaultProps = {
onChange: () => {},
sourceLayerIds: [],
isFixed: false
}
render() {
return <InputBlock label={"Source Layer"} doc={GlSpec.layer['source-layer'].doc}>
return <InputBlock label={"Source Layer"} doc={styleSpec.latest.layer['source-layer'].doc}>
<AutocompleteInput
keepMenuWithinWindowBounds={!!this.props.isFixed}
value={this.props.value}
onChange={this.props.onChange}
options={this.props.sourceLayerIds.map(l => [l, l])}

View File

@@ -1,17 +1,18 @@
import React from 'react'
import PropTypes from 'prop-types'
import GlSpec from 'mapbox-gl-style-spec/reference/latest.js'
import styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
import InputBlock from '../inputs/InputBlock'
import SelectInput from '../inputs/SelectInput'
class LayerTypeBlock extends React.Component {
static propTypes = {
value: React.PropTypes.string.isRequired,
onChange: React.PropTypes.func.isRequired,
value: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired,
}
render() {
return <InputBlock label={"Type"} doc={GlSpec.layer.type.doc}>
return <InputBlock label={"Type"} doc={styleSpec.latest.layer.type.doc}>
<SelectInput
options={[
['background', 'Background'],
@@ -21,6 +22,8 @@ class LayerTypeBlock extends React.Component {
['raster', 'Raster'],
['circle', 'Circle'],
['fill-extrusion', 'Fill Extrusion'],
['hillshade', 'Hillshade'],
['heatmap', 'Heatmap'],
]}
onChange={this.props.onChange}
value={this.props.value}

View File

@@ -1,23 +1,24 @@
import React from 'react'
import PropTypes from 'prop-types'
import GlSpec from 'mapbox-gl-style-spec/reference/latest.js'
import styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
import InputBlock from '../inputs/InputBlock'
import NumberInput from '../inputs/NumberInput'
class MaxZoomBlock extends React.Component {
static propTypes = {
value: React.PropTypes.number.isRequired,
onChange: React.PropTypes.func.isRequired,
value: PropTypes.number,
onChange: PropTypes.func.isRequired,
}
render() {
return <InputBlock label={"Max Zoom"} doc={GlSpec.layer.maxzoom.doc}>
return <InputBlock label={"Max Zoom"} doc={styleSpec.latest.layer.maxzoom.doc}>
<NumberInput
value={this.props.value}
onChange={this.props.onChange}
min={GlSpec.layer.maxzoom.minimum}
max={GlSpec.layer.maxzoom.maximum}
default={GlSpec.layer.maxzoom.maximum}
min={styleSpec.latest.layer.maxzoom.minimum}
max={styleSpec.latest.layer.maxzoom.maximum}
default={styleSpec.latest.layer.maxzoom.maximum}
/>
</InputBlock>
}

View File

@@ -1,23 +1,24 @@
import React from 'react'
import PropTypes from 'prop-types'
import GlSpec from 'mapbox-gl-style-spec/reference/latest.js'
import styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
import InputBlock from '../inputs/InputBlock'
import NumberInput from '../inputs/NumberInput'
class MinZoomBlock extends React.Component {
static propTypes = {
value: React.PropTypes.number.isRequired,
onChange: React.PropTypes.func.isRequired,
value: PropTypes.number,
onChange: PropTypes.func.isRequired,
}
render() {
return <InputBlock label={"Min Zoom"} doc={GlSpec.layer.minzoom.doc}>
return <InputBlock label={"Min Zoom"} doc={styleSpec.latest.layer.minzoom.doc}>
<NumberInput
value={this.props.value}
onChange={this.props.onChange}
min={GlSpec.layer.minzoom.minimum}
max={GlSpec.layer.minzoom.maximum}
default={GlSpec.layer.minzoom.minimum}
min={styleSpec.latest.layer.minzoom.minimum}
max={styleSpec.latest.layer.minzoom.maximum}
default={styleSpec.latest.layer.minzoom.minimum}
/>
</InputBlock>
}

View File

@@ -1,19 +1,38 @@
import React from 'react'
import PropTypes from 'prop-types'
import InputBlock from '../inputs/InputBlock'
import StringInput from '../inputs/StringInput'
import LayerIcon from '../icons/LayerIcon'
function groupFeaturesBySourceLayer(features) {
const sources = {}
let returnedFeatures = {};
features.forEach(feature => {
sources[feature.layer['source-layer']] = sources[feature.layer['source-layer']] || []
sources[feature.layer['source-layer']].push(feature)
if(returnedFeatures.hasOwnProperty(feature.layer.id)) {
returnedFeatures[feature.layer.id]++
const featureObject = sources[feature.layer['source-layer']].find(f => f.layer.id === feature.layer.id)
featureObject.counter = returnedFeatures[feature.layer.id]
} else {
sources[feature.layer['source-layer']] = sources[feature.layer['source-layer']] || []
sources[feature.layer['source-layer']].push(feature)
returnedFeatures[feature.layer.id] = 1
}
})
return sources
}
class FeatureLayerPopup extends React.Component {
static propTypes = {
onLayerSelect: PropTypes.func.isRequired,
features: PropTypes.array
}
render() {
const sources = groupFeaturesBySourceLayer(this.props.features)
@@ -22,6 +41,9 @@ class FeatureLayerPopup extends React.Component {
return <label
key={idx}
className="maputnik-popup-layer"
onClick={() => {
this.props.onLayerSelect(feature.layer.id)
}}
>
<LayerIcon type={feature.layer.type} style={{
width: 14,
@@ -29,6 +51,7 @@ class FeatureLayerPopup extends React.Component {
paddingRight: 3
}}/>
{feature.layer.id}
{feature.counter && <span> × {feature.counter}</span>}
</label>
})
return <div key={vectorLayerId}>

View File

@@ -1,4 +1,5 @@
import React from 'react'
import PropTypes from 'prop-types'
import InputBlock from '../inputs/InputBlock'
import StringInput from '../inputs/StringInput'
@@ -22,7 +23,7 @@ function renderProperties(feature) {
function renderFeature(feature) {
return <div key={feature.id}>
<div className="maputnik-popup-layer-id">{feature.layer['source-layer']}</div>
<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>
@@ -30,10 +31,36 @@ function renderFeature(feature) {
</div>
}
function removeDuplicatedFeatures(features) {
let uniqueFeatures = [];
features.forEach(feature => {
const featureIndex = uniqueFeatures.findIndex(feature2 => {
return feature.layer['source-layer'] === feature2.layer['source-layer']
&& JSON.stringify(feature.properties) === JSON.stringify(feature2.properties)
})
if(featureIndex === -1) {
uniqueFeatures.push(feature)
} else {
if(uniqueFeatures[featureIndex].hasOwnProperty('counter')) {
uniqueFeatures[featureIndex].inspectModeCounter++
} else {
uniqueFeatures[featureIndex].inspectModeCounter = 2
}
}
})
return uniqueFeatures
}
class FeaturePropertyPopup extends React.Component {
static propTypes = {
features: PropTypes.array
}
render() {
const features = this.props.features
const features = removeDuplicatedFeatures(this.props.features)
return <div className="maputnik-feature-property-popup">
{features.map(renderFeature)}
</div>

View File

@@ -1,25 +1,20 @@
import React from 'react'
import PropTypes from 'prop-types'
import ReactDOM from 'react-dom'
import MapboxGl from 'mapbox-gl/dist/mapbox-gl.js'
import MapboxGl from 'mapbox-gl'
import MapboxInspect from 'mapbox-gl-inspect'
import FeatureLayerPopup from './FeatureLayerPopup'
import FeaturePropertyPopup from './FeaturePropertyPopup'
import validateColor from 'mapbox-gl-style-spec/lib/validate/validate_color'
import style from '../../libs/style.js'
import tokens from '../../config/tokens.json'
import colors from 'mapbox-gl-inspect/lib/colors'
import Color from 'color'
import ZoomControl from '../../libs/zoomcontrol'
import { colorHighlightedLayer } from '../../libs/highlight'
import 'mapbox-gl/dist/mapbox-gl.css'
import '../../mapboxgl.css'
import '../../libs/mapbox-rtl'
function renderLayerPopup(features) {
var mountNode = document.createElement('div');
ReactDOM.render(<FeatureLayerPopup features={features} />, mountNode)
return mountNode.innerHTML;
}
function renderPropertyPopup(features) {
var mountNode = document.createElement('div');
ReactDOM.render(<FeaturePropertyPopup features={features} />, mountNode)
@@ -43,7 +38,7 @@ function buildInspectStyle(originalMapStyle, coloredLayers, highlightedLayer) {
const sources = {}
Object.keys(originalMapStyle.sources).forEach(sourceId => {
const source = originalMapStyle.sources[sourceId]
if(source.type !== 'raster') {
if(source.type !== 'raster' && source.type !== 'raster-dem') {
sources[sourceId] = source
}
})
@@ -58,15 +53,17 @@ function buildInspectStyle(originalMapStyle, coloredLayers, highlightedLayer) {
export default class MapboxGlMap extends React.Component {
static propTypes = {
onDataChange: React.PropTypes.func,
mapStyle: React.PropTypes.object.isRequired,
inspectModeEnabled: React.PropTypes.bool.isRequired,
highlightedLayer: React.PropTypes.object,
onDataChange: PropTypes.func,
onLayerSelect: PropTypes.func.isRequired,
mapStyle: PropTypes.object.isRequired,
inspectModeEnabled: PropTypes.bool.isRequired,
highlightedLayer: PropTypes.object,
}
static defaultProps = {
onMapLoaded: () => {},
onDataChange: () => {},
onLayerSelect: () => {},
mapboxAccessToken: tokens.mapbox,
}
@@ -110,6 +107,9 @@ export default class MapboxGlMap extends React.Component {
hash: true,
})
const zoom = new ZoomControl;
map.addControl(zoom, 'top-right');
const nav = new MapboxGl.NavigationControl();
map.addControl(nav, 'top-right');
@@ -129,7 +129,9 @@ export default class MapboxGlMap extends React.Component {
if(this.props.inspectModeEnabled) {
return renderPropertyPopup(features)
} else {
return renderLayerPopup(features)
var mountNode = document.createElement('div');
ReactDOM.render(<FeatureLayerPopup features={features} onLayerSelect={this.props.onLayerSelect} />, mountNode)
return mountNode
}
}
})

View File

@@ -1,59 +1,17 @@
import React from 'react'
import PropTypes from 'prop-types'
import style from '../../libs/style.js'
import isEqual from 'lodash.isequal'
import { loadJSON } from '../../libs/urlopen'
import 'ol/ol.css'
function suitableVectorSource(mapStyle) {
const sources = Object.keys(mapStyle.sources)
.map(sourceId => {
return {
id: sourceId,
source: mapStyle.sources[sourceId]
}
})
.filter(({source}) => source.type === 'vector')
return sources[0]
}
function toVectorLayer(source, tilegrid, cb) {
function newMVTLayer(tileUrl) {
const ol = require('openlayers')
return new ol.layer.VectorTile({
source: new ol.source.VectorTile({
format: new ol.format.MVT(),
tileGrid: tilegrid,
tilePixelRatio: 8,
url: tileUrl
})
})
}
if(!source.tiles) {
sourceFromTileJSON(source.url, tileSource => {
cb(newMVTLayer(tileSource.tiles[0]))
})
} else {
cb(newMVTLayer(source.tiles[0]))
}
}
function sourceFromTileJSON(url, cb) {
loadJSON(url, null, tilejson => {
if(!tilejson) return
cb({
type: 'vector',
tiles: tilejson.tiles,
minzoom: tilejson.minzoom,
maxzoom: tilejson.maxzoom,
})
})
}
class OpenLayers3Map extends React.Component {
static propTypes = {
onDataChange: React.PropTypes.func,
mapStyle: React.PropTypes.object.isRequired,
accessToken: React.PropTypes.string,
onDataChange: PropTypes.func,
mapStyle: PropTypes.object.isRequired,
accessToken: PropTypes.string,
style: PropTypes.object,
}
static defaultProps = {
@@ -63,48 +21,17 @@ class OpenLayers3Map extends React.Component {
constructor(props) {
super(props)
this.tilegrid = null
this.resolutions = null
this.layer = null
this.map = null
}
updateStyle(newMapStyle) {
const oldSource = suitableVectorSource(this.props.mapStyle)
const newSource = suitableVectorSource(newMapStyle)
const resolutions = this.resolutions
function setStyleFunc(map, layer) {
const olms = require('ol-mapbox-style')
const styleFunc = olms.getStyleFunction(newMapStyle, newSource.id, resolutions)
layer.setStyle(styleFunc)
//NOTE: We need to mark the source as changed in order
//to trigger a rerender
layer.getSource().changed()
map.render()
}
if(newSource) {
if(this.layer && !isEqual(oldSource, newSource)) {
this.map.removeLayer(this.layer)
this.layer = null
}
if(!this.layer) {
toVectorLayer(newSource.source, this.tilegrid, vectorLayer => {
this.layer = vectorLayer
this.map.addLayer(this.layer)
setStyleFunc(this.map, this.layer)
})
} else {
setStyleFunc(this.map, this.layer)
}
}
const olms = require('ol-mapbox-style');
const styleFunc = olms.apply(this.map, newMapStyle)
}
componentWillReceiveProps(nextProps) {
require.ensure(["openlayers", "ol-mapbox-style"], () => {
if(!this.map || !this.resolutions) return
require.ensure(["ol", "ol-mapbox-style"], () => {
if(!this.map) return
this.updateStyle(nextProps.mapStyle)
})
}
@@ -112,24 +39,22 @@ class OpenLayers3Map extends React.Component {
componentDidMount() {
//Load OpenLayers dynamically once we need it
//TODO: Make this more convenient
require.ensure(["openlayers", "ol-mapbox-style"], ()=> {
require.ensure(["ol", "ol/map", "ol/view", "ol/control/zoom", "ol-mapbox-style"], ()=> {
console.log('Loaded OpenLayers3 renderer')
const ol = require('openlayers')
const olms = require('ol-mapbox-style')
const olMap = require('ol/map').default
const olView = require('ol/view').default
const olZoom = require('ol/control/zoom').default
this.tilegrid = ol.tilegrid.createXYZ({tileSize: 512, maxZoom: 22})
this.resolutions = this.tilegrid.getResolutions()
const map = new ol.Map({
const map = new olMap({
target: this.container,
layers: [],
view: new ol.View({
view: new olView({
zoom: 2,
center: [52.5, -78.4]
})
})
map.addControl(new ol.control.Zoom())
map.addControl(new olZoom())
this.map = map
this.updateStyle(this.props.mapStyle)
})
@@ -140,10 +65,10 @@ class OpenLayers3Map extends React.Component {
ref={x => this.container = x}
style={{
position: "fixed",
top: 0,
top: 40,
right: 0,
bottom: 0,
height: "100%",
height: 'calc(100% - 40px)',
width: "75%",
backgroundColor: '#fff',
...this.props.style,

View File

@@ -1,4 +1,5 @@
import React from 'react'
import PropTypes from 'prop-types'
import Button from '../Button'
import InputBlock from '../inputs/InputBlock'
@@ -13,13 +14,13 @@ import LayerSourceLayerBlock from '../layers/LayerSourceLayerBlock'
class AddModal extends React.Component {
static propTypes = {
layers: React.PropTypes.array.isRequired,
onLayersChange: React.PropTypes.func.isRequired,
isOpen: React.PropTypes.bool.isRequired,
onOpenToggle: React.PropTypes.func.isRequired,
layers: PropTypes.array.isRequired,
onLayersChange: PropTypes.func.isRequired,
isOpen: PropTypes.bool.isRequired,
onOpenToggle: PropTypes.func.isRequired,
// A dict of source id's and the available source layers
sources: React.PropTypes.object.isRequired,
sources: PropTypes.object.isRequired,
}
addLayer() {
@@ -55,18 +56,65 @@ class AddModal extends React.Component {
}
}
componentWillReceiveProps(nextProps) {
const sourceIds = Object.keys(nextProps.sources)
if(!this.state.source && sourceIds.length > 0) {
componentWillUpdate(nextProps, nextState) {
// Check if source is valid for new type
const oldType = this.state.type;
const newType = nextState.type;
const availableSourcesOld = this.getSources(oldType);
const availableSourcesNew = this.getSources(newType);
if(
// Type has changed
oldType !== newType
&& this.state.source !== ""
// Was a valid source previously
&& availableSourcesOld.indexOf(this.state.source) > -1
// And is not a valid source now
&& availableSourcesNew.indexOf(nextState.source) < 0
) {
// Clear the source
this.setState({
source: sourceIds[0],
'source-layer': this.state['source-layer'] || (nextProps.sources[sourceIds[0]] || [])[0]
})
source: ""
});
}
}
getLayersForSource(source) {
const sourceObj = this.props.sources[source] || {};
return sourceObj.layers || [];
}
getSources(type) {
const sources = [];
const types = {
vector: [
"fill",
"line",
"symbol",
"circle",
"fill-extrusion"
],
raster: [
"raster"
]
}
for(let [key, val] of Object.entries(this.props.sources)) {
if(types[val.type] && types[val.type].indexOf(type) > -1) {
sources.push(key);
}
}
return sources;
}
render() {
const sources = this.getSources(this.state.type);
const layers = this.getLayersForSource(this.state.source);
return <Modal
isOpen={this.props.isOpen}
onOpenToggle={this.props.onOpenToggle}
@@ -83,14 +131,15 @@ class AddModal extends React.Component {
/>
{this.state.type !== 'background' &&
<LayerSourceBlock
sourceIds={Object.keys(this.props.sources)}
sourceIds={sources}
value={this.state.source}
onChange={v => this.setState({ source: v })}
/>
}
{this.state.type !== 'background' && this.state.type !== 'raster' &&
{['background', 'raster', 'hillshade', 'heatmap'].indexOf(this.state.type) < 0 &&
<LayerSourceLayerBlock
sourceLayerIds={this.props.sources[this.state.source] || []}
isFixed={true}
sourceLayerIds={layers}
value={this.state['source-layer']}
onChange={v => this.setState({ 'source-layer': v })}
/>

View File

@@ -1,7 +1,8 @@
import React from 'react'
import PropTypes from 'prop-types'
import { saveAs } from 'file-saver'
import GlSpec from 'mapbox-gl-style-spec/reference/latest.js'
import styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
import InputBlock from '../inputs/InputBlock'
import StringInput from '../inputs/StringInput'
import SelectInput from '../inputs/SelectInput'
@@ -9,21 +10,23 @@ import CheckboxInput from '../inputs/CheckboxInput'
import Button from '../Button'
import Modal from './Modal'
import MdFileDownload from 'react-icons/lib/md/file-download'
import TiClipboard from 'react-icons/lib/ti/clipboard'
import style from '../../libs/style.js'
import formatStyle from 'mapbox-gl-style-spec/lib/format'
import GitHub from 'github-api'
import { CopyToClipboard } from 'react-copy-to-clipboard'
class Gist extends React.Component {
static propTypes = {
mapStyle: React.PropTypes.object.isRequired,
onStyleChanged: React.PropTypes.func.isRequired,
mapStyle: PropTypes.object.isRequired,
onStyleChanged: PropTypes.func.isRequired,
}
constructor(props) {
super(props);
this.state = {
preview: false,
public: false,
saving: false,
latestGist: null,
}
@@ -41,11 +44,14 @@ class Gist extends React.Component {
...this.state,
saving: true
});
const preview = this.state.preview && (this.props.mapStyle.metadata || {})['maputnik:openmaptiles_access_token'];
const preview = this.state.preview;
const mapboxToken = (this.props.mapStyle.metadata || {})['maputnik:mapbox_access_token'];
const mapStyleStr = preview ?
formatStyle(stripAccessTokens(style.replaceAccessToken(this.props.mapStyle))) :
formatStyle(stripAccessTokens(this.props.mapStyle));
styleSpec.format(stripAccessTokens(style.replaceAccessToken(this.props.mapStyle))) :
styleSpec.format(stripAccessTokens(this.props.mapStyle));
const styleTitle = this.props.mapStyle.name || 'Style';
const htmlStr = `
<!DOCTYPE html>
@@ -54,8 +60,8 @@ class Gist extends React.Component {
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>`+styleTitle+` Preview</title>
<link rel="stylesheet" type="text/css" href="https://api.mapbox.com/mapbox-gl-js/v0.28.0/mapbox-gl.css" />
<script src="https://api.mapbox.com/mapbox-gl-js/v0.28.0/mapbox-gl.js"></script>
<link rel="stylesheet" type="text/css" href="https://api.mapbox.com/mapbox-gl-js/v0.44.0/mapbox-gl.css" />
<script src="https://api.mapbox.com/mapbox-gl-js/v0.44.0/mapbox-gl.js"></script>
<style>
body { margin:0; padding:0; }
#map { position:absolute; top:0; bottom:0; width:100%; }
@@ -64,6 +70,7 @@ class Gist extends React.Component {
<body>
<div id='map'></div>
<script>
mapboxgl.accessToken = '${mapboxToken}';
var map = new mapboxgl.Map({
container: 'map',
style: 'style.json',
@@ -88,7 +95,7 @@ class Gist extends React.Component {
const gh = new GitHub();
let gist = gh.getGist(); // not a gist yet
gist.create({
public: true,
public: this.state.public,
description: styleTitle,
files: files
}).then(function({data}) {
@@ -109,6 +116,13 @@ class Gist extends React.Component {
})
}
onPublicChange(value) {
this.setState({
...this.state,
public: value
})
}
changeMetadataProperty(property, value) {
const changedStyle = {
...this.props.mapStyle,
@@ -125,7 +139,7 @@ class Gist extends React.Component {
const user = gist.user || 'anonymous';
const preview = !!gist.files['index.html'];
if(preview) {
return <span><a target="_blank" href={"https://bl.ocks.org/"+user+"/"+gist.id}>Preview</a>,{' '}</span>
return <span><a target="_blank" rel="noopener noreferrer" href={"https://bl.ocks.org/"+user+"/"+gist.id}>Preview</a>,{' '}</span>
}
return null;
}
@@ -137,11 +151,21 @@ class Gist extends React.Component {
return <p>Saving...</p>
} else if(gist) {
const user = gist.user || 'anonymous';
return <p>
Latest saved gist:{' '}
{this.renderPreviewLink(this)}
<a target="_blank" href={"https://gist.github.com/"+user+"/"+gist.id}>Source</a>
</p>
const rawGistLink = "https://gist.githubusercontent.com/" + user + "/" + gist.id + "/raw/" + gist.history[0].version + "/style.json"
const maputnikStyleLink = "https://maputnik.github.io/editor/?style=" + rawGistLink
return <div className="maputnik-render-gist">
<p>
Latest saved gist:{' '}
{this.renderPreviewLink(this)}
<a target="_blank" rel="noopener noreferrer" href={"https://gist.github.com/" + user + "/" + gist.id}>Source</a>
</p>
<p>
<CopyToClipboard text={maputnikStyleLink}>
<span>Share this style: <Button><TiClipboard size={18} /></Button></span>
</CopyToClipboard>
<StringInput value={maputnikStyleLink} />
</p>
</div>
}
}
@@ -151,13 +175,22 @@ class Gist extends React.Component {
<MdFileDownload />
Save to Gist (anonymous)
</Button>
{' '}
<CheckboxInput
value={this.state.preview}
name='gist-style-preview'
onChange={this.onPreviewChange.bind(this)}
/>
<span> Include preview</span>
<div className="maputnik-modal-sub-section">
<CheckboxInput
value={this.state.public}
name='gist-style-public'
onChange={this.onPublicChange.bind(this)}
/>
<span> Public gist</span>
</div>
<div className="maputnik-modal-sub-section">
<CheckboxInput
value={this.state.preview}
name='gist-style-preview'
onChange={this.onPreviewChange.bind(this)}
/>
<span> Include preview</span>
</div>
{this.state.preview ?
<div>
<InputBlock
@@ -166,7 +199,13 @@ class Gist extends React.Component {
value={(this.props.mapStyle.metadata || {})['maputnik:openmaptiles_access_token']}
onChange={this.changeMetadataProperty.bind(this, "maputnik:openmaptiles_access_token")}/>
</InputBlock>
<a target="_blank" href="https://openmaptiles.com/hosting/">Get your free access token</a>
<InputBlock
label={"Mapbox Access Token: "}>
<StringInput
value={(this.props.mapStyle.metadata || {})['maputnik:mapbox_access_token']}
onChange={this.changeMetadataProperty.bind(this, "maputnik:mapbox_access_token")}/>
</InputBlock>
<a target="_blank" rel="noopener noreferrer" href="https://openmaptiles.com/hosting/">Get your free access token</a>
</div>
: null}
{this.renderLatestGist()}
@@ -186,10 +225,10 @@ function stripAccessTokens(mapStyle) {
class ExportModal extends React.Component {
static propTypes = {
mapStyle: React.PropTypes.object.isRequired,
onStyleChanged: React.PropTypes.func.isRequired,
isOpen: React.PropTypes.bool.isRequired,
onOpenToggle: React.PropTypes.func.isRequired,
mapStyle: PropTypes.object.isRequired,
onStyleChanged: PropTypes.func.isRequired,
isOpen: PropTypes.bool.isRequired,
onOpenToggle: PropTypes.func.isRequired,
}
constructor(props) {
@@ -197,7 +236,7 @@ class ExportModal extends React.Component {
}
downloadStyle() {
const blob = new Blob([formatStyle(stripAccessTokens(this.props.mapStyle))], {type: "application/json;charset=utf-8"});
const blob = new Blob([styleSpec.format(stripAccessTokens(this.props.mapStyle))], {type: "application/json;charset=utf-8"});
saveAs(blob, this.props.mapStyle.id + ".json");
}

View File

@@ -1,12 +1,14 @@
import React from 'react'
import PropTypes from 'prop-types'
import CloseIcon from 'react-icons/lib/md/close'
import Overlay from './Overlay'
class Modal extends React.Component {
static propTypes = {
isOpen: React.PropTypes.bool.isRequired,
title: React.PropTypes.string.isRequired,
onOpenToggle: React.PropTypes.func.isRequired,
isOpen: PropTypes.bool.isRequired,
title: PropTypes.string.isRequired,
onOpenToggle: PropTypes.func.isRequired,
children: PropTypes.node,
}
render() {
@@ -21,7 +23,9 @@ class Modal extends React.Component {
<CloseIcon />
</a>
</header>
<div className="maputnik-modal-content">{this.props.children}</div>
<div className="maputnik-modal-scroller">
<div className="maputnik-modal-content">{this.props.children}</div>
</div>
</div>
</Overlay>
}

View File

@@ -1,4 +1,5 @@
import React from 'react'
import PropTypes from 'prop-types'
import Modal from './Modal'
import Button from '../Button'
import FileReaderInput from 'react-file-reader-input'
@@ -12,10 +13,10 @@ import publicStyles from '../../config/styles.json'
class PublicStyle extends React.Component {
static propTypes = {
url: React.PropTypes.string.isRequired,
thumbnailUrl: React.PropTypes.string.isRequired,
title: React.PropTypes.string.isRequired,
onSelect: React.PropTypes.func.isRequired,
url: PropTypes.string.isRequired,
thumbnailUrl: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
onSelect: PropTypes.func.isRequired,
}
render() {
@@ -41,9 +42,9 @@ class PublicStyle extends React.Component {
class OpenModal extends React.Component {
static propTypes = {
isOpen: React.PropTypes.bool.isRequired,
onOpenToggle: React.PropTypes.func.isRequired,
onStyleOpen: React.PropTypes.func.isRequired,
isOpen: PropTypes.bool.isRequired,
onOpenToggle: PropTypes.func.isRequired,
onStyleOpen: PropTypes.func.isRequired,
}
constructor(props) {
@@ -68,12 +69,18 @@ class OpenModal extends React.Component {
const mapStyle = style.ensureStyleValidity(JSON.parse(body))
console.log('Loaded style ', mapStyle.id)
this.props.onStyleOpen(mapStyle)
this.onOpenToggle()
} else {
console.warn('Could not open the style URL', styleUrl)
}
})
}
onOpenUrl() {
const url = this.styleUrlElement.value;
this.onStyleSelect(url);
}
onUpload(_, files) {
const [e, file] = files[0];
const reader = new FileReader();
@@ -94,6 +101,7 @@ class OpenModal extends React.Component {
}
mapStyle = style.ensureStyleValidity(mapStyle)
this.props.onStyleOpen(mapStyle);
this.onOpenToggle();
}
reader.onerror = e => console.log(e.target);
}
@@ -137,6 +145,18 @@ class OpenModal extends React.Component {
<Button className="maputnik-upload-button"><FileUploadIcon /> Upload</Button>
</FileReaderInput>
</section>
<section className="maputnik-modal-section">
<h2>Load from URL</h2>
<p>
Load from a URL. Note that the URL must have <a href="https://enable-cors.org" target="_blank" rel="noopener noreferrer">CORS enabled</a>.
</p>
<input type="text" ref={(input) => this.styleUrlElement = input} className="maputnik-input" placeholder="Enter URL..."/>
<div>
<Button className="maputnik-big-button" onClick={this.onOpenUrl.bind(this)}>Open URL</Button>
</div>
</section>
<section className="maputnik-modal-section maputnik-modal-section--shrink">
<h2>Gallery Styles</h2>
<p>

View File

@@ -1,10 +1,11 @@
import React from 'react'
import PropTypes from 'prop-types'
class Overlay extends React.Component {
static propTypes = {
isOpen: React.PropTypes.bool.isRequired,
children: React.PropTypes.element.isRequired
isOpen: PropTypes.bool.isRequired,
children: PropTypes.element.isRequired
}
render() {

View File

@@ -1,6 +1,7 @@
import React from 'react'
import PropTypes from 'prop-types'
import GlSpec from 'mapbox-gl-style-spec/reference/latest.js'
import styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
import InputBlock from '../inputs/InputBlock'
import StringInput from '../inputs/StringInput'
import SelectInput from '../inputs/SelectInput'
@@ -8,10 +9,10 @@ import Modal from './Modal'
class SettingsModal extends React.Component {
static propTypes = {
mapStyle: React.PropTypes.object.isRequired,
onStyleChanged: React.PropTypes.func.isRequired,
isOpen: React.PropTypes.bool.isRequired,
onOpenToggle: React.PropTypes.func.isRequired,
mapStyle: PropTypes.object.isRequired,
onStyleChanged: PropTypes.func.isRequired,
isOpen: PropTypes.bool.isRequired,
onOpenToggle: PropTypes.func.isRequired,
}
constructor(props) {
@@ -46,7 +47,7 @@ class SettingsModal extends React.Component {
title={'Style Settings'}
>
<div style={{minWidth: 350}}>
<InputBlock label={"Name"} doc={GlSpec.$root.name.doc}>
<InputBlock label={"Name"} doc={styleSpec.latest.$root.name.doc}>
<StringInput {...inputProps}
value={this.props.mapStyle.name}
onChange={this.changeStyleProperty.bind(this, "name")}
@@ -58,14 +59,14 @@ class SettingsModal extends React.Component {
onChange={this.changeStyleProperty.bind(this, "owner")}
/>
</InputBlock>
<InputBlock label={"Sprite URL"} doc={GlSpec.$root.sprite.doc}>
<InputBlock label={"Sprite URL"} doc={styleSpec.latest.$root.sprite.doc}>
<StringInput {...inputProps}
value={this.props.mapStyle.sprite}
onChange={this.changeStyleProperty.bind(this, "sprite")}
/>
</InputBlock>
<InputBlock label={"Glyphs URL"} doc={GlSpec.$root.glyphs.doc}>
<InputBlock label={"Glyphs URL"} doc={styleSpec.latest.$root.glyphs.doc}>
<StringInput {...inputProps}
value={this.props.mapStyle.glyphs}
onChange={this.changeStyleProperty.bind(this, "glyphs")}

View File

@@ -1,5 +1,6 @@
import React from 'react'
import GlSpec from 'mapbox-gl-style-spec/reference/latest.js'
import PropTypes from 'prop-types'
import styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
import Modal from './Modal'
import Button from '../Button'
import InputBlock from '../inputs/InputBlock'
@@ -16,10 +17,10 @@ import DeleteIcon from 'react-icons/lib/md/delete'
class PublicSource extends React.Component {
static propTypes = {
id: React.PropTypes.string.isRequired,
type: React.PropTypes.string.isRequired,
title: React.PropTypes.string.isRequired,
onSelect: React.PropTypes.func.isRequired,
id: PropTypes.string.isRequired,
type: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
onSelect: PropTypes.func.isRequired,
}
render() {
@@ -44,6 +45,10 @@ function editorMode(source) {
if(source.tiles) return 'tilexyz_raster'
return 'tilejson_raster'
}
if(source.type === 'raster-dem') {
if(source.tiles) return 'tilexyz_raster-dem'
return 'tilejson_raster-dem'
}
if(source.type === 'vector') {
if(source.tiles) return 'tilexyz_vector'
return 'tilejson_vector'
@@ -54,10 +59,10 @@ function editorMode(source) {
class ActiveSourceTypeEditor extends React.Component {
static propTypes = {
sourceId: React.PropTypes.string.isRequired,
source: React.PropTypes.object.isRequired,
onDelete: React.PropTypes.func.isRequired,
onChange: React.PropTypes.func.isRequired,
sourceId: PropTypes.string.isRequired,
source: PropTypes.object.isRequired,
onDelete: PropTypes.func.isRequired,
onChange: PropTypes.func.isRequired,
}
render() {
@@ -87,7 +92,7 @@ class ActiveSourceTypeEditor extends React.Component {
class AddSource extends React.Component {
static propTypes = {
onAdd: React.PropTypes.func.isRequired,
onAdd: PropTypes.func.isRequired,
}
constructor(props) {
@@ -126,6 +131,16 @@ class AddSource extends React.Component {
minzoom: source.minzoom || 0,
maxzoom: source.maxzoom || 14
}
case 'tilejson_raster-dem': return {
type: 'raster-dem',
url: source.url || 'http://localhost:3000/tilejson.json'
}
case 'tilexyz_raster-dem': return {
type: 'raster-dem',
tiles: source.tiles || ['http://localhost:3000/{x}/{y}/{z}.pbf'],
minzoom: source.minzoom || 0,
maxzoom: source.maxzoom || 14
}
default: return {}
}
}
@@ -138,7 +153,7 @@ class AddSource extends React.Component {
onChange={v => this.setState({ sourceId: v})}
/>
</InputBlock>
<InputBlock label={"Source Type"} doc={GlSpec.source_tile.type.doc}>
<InputBlock label={"Source Type"} doc={styleSpec.latest.source_vector.type.doc}>
<SelectInput
options={[
['geojson', 'GeoJSON'],
@@ -146,6 +161,8 @@ class AddSource extends React.Component {
['tilexyz_vector', 'Vector (XYZ URLs)'],
['tilejson_raster', 'Raster (TileJSON URL)'],
['tilexyz_raster', 'Raster (XYZ URL)'],
['tilejson_raster-dem', 'Raster DEM (TileJSON URL)'],
['tilexyz_raster-dem', 'Raster DEM (XYZ URLs)'],
]}
onChange={mode => this.setState({mode: mode, source: this.defaultSource(mode)})}
value={this.state.mode}
@@ -167,10 +184,10 @@ class AddSource extends React.Component {
class SourcesModal extends React.Component {
static propTypes = {
mapStyle: React.PropTypes.object.isRequired,
isOpen: React.PropTypes.bool.isRequired,
onOpenToggle: React.PropTypes.func.isRequired,
onStyleChanged: React.PropTypes.func.isRequired,
mapStyle: PropTypes.object.isRequired,
isOpen: PropTypes.bool.isRequired,
onOpenToggle: PropTypes.func.isRequired,
onStyleChanged: PropTypes.func.isRequired,
}
stripTitle(source) {

View File

@@ -1,17 +1,18 @@
import React from 'react'
import GlSpec from 'mapbox-gl-style-spec/reference/latest.js'
import PropTypes from 'prop-types'
import styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
import InputBlock from '../inputs/InputBlock'
import StringInput from '../inputs/StringInput'
import NumberInput from '../inputs/NumberInput'
class TileJSONSourceEditor extends React.Component {
static propTypes = {
source: React.PropTypes.object.isRequired,
onChange: React.PropTypes.func.isRequired,
source: PropTypes.object.isRequired,
onChange: PropTypes.func.isRequired,
}
render() {
return <InputBlock label={"TileJSON URL"} doc={GlSpec.source_tile.url.doc}>
return <InputBlock label={"TileJSON URL"} doc={styleSpec.latest.source_vector.url.doc}>
<StringInput
value={this.props.source.url}
onChange={url => this.props.onChange({
@@ -25,8 +26,8 @@ class TileJSONSourceEditor extends React.Component {
class TileURLSourceEditor extends React.Component {
static propTypes = {
source: React.PropTypes.object.isRequired,
onChange: React.PropTypes.func.isRequired,
source: PropTypes.object.isRequired,
onChange: PropTypes.func.isRequired,
}
changeTileUrl(idx, value) {
@@ -42,7 +43,7 @@ class TileURLSourceEditor extends React.Component {
const prefix = ['1st', '2nd', '3rd', '4th', '5th', '6th', '7th']
const tiles = this.props.source.tiles || []
return tiles.map((tileUrl, tileIndex) => {
return <InputBlock key={tileIndex} label={prefix[tileIndex] + " Tile URL"} doc={GlSpec.source_tile.tiles.doc}>
return <InputBlock key={tileIndex} label={prefix[tileIndex] + " Tile URL"} doc={styleSpec.latest.source_vector.tiles.doc}>
<StringInput
value={tileUrl}
onChange={this.changeTileUrl.bind(this, tileIndex)}
@@ -54,7 +55,7 @@ class TileURLSourceEditor extends React.Component {
render() {
return <div>
{this.renderTileUrls()}
<InputBlock label={"Min Zoom"} doc={GlSpec.source_tile.minzoom.doc}>
<InputBlock label={"Min Zoom"} doc={styleSpec.latest.source_vector.minzoom.doc}>
<NumberInput
value={this.props.source.minzoom || 0}
onChange={minzoom => this.props.onChange({
@@ -63,7 +64,7 @@ class TileURLSourceEditor extends React.Component {
})}
/>
</InputBlock>
<InputBlock label={"Max Zoom"} doc={GlSpec.source_tile.maxzoom.doc}>
<InputBlock label={"Max Zoom"} doc={styleSpec.latest.source_vector.maxzoom.doc}>
<NumberInput
value={this.props.source.maxzoom || 22}
onChange={maxzoom => this.props.onChange({
@@ -79,12 +80,12 @@ class TileURLSourceEditor extends React.Component {
class GeoJSONSourceEditor extends React.Component {
static propTypes = {
source: React.PropTypes.object.isRequired,
onChange: React.PropTypes.func.isRequired,
source: PropTypes.object.isRequired,
onChange: PropTypes.func.isRequired,
}
render() {
return <InputBlock label={"GeoJSON Data"} doc={GlSpec.source_geojson.data.doc}>
return <InputBlock label={"GeoJSON Data"} doc={styleSpec.latest.source_geojson.data.doc}>
<StringInput
value={this.props.source.data}
onChange={data => this.props.onChange({
@@ -98,9 +99,9 @@ class GeoJSONSourceEditor extends React.Component {
class SourceTypeEditor extends React.Component {
static propTypes = {
mode: React.PropTypes.string.isRequired,
source: React.PropTypes.object.isRequired,
onChange: React.PropTypes.func.isRequired,
mode: PropTypes.string.isRequired,
source: PropTypes.object.isRequired,
onChange: PropTypes.func.isRequired,
}
render() {
@@ -114,6 +115,8 @@ class SourceTypeEditor extends React.Component {
case 'tilexyz_vector': return <TileURLSourceEditor {...commonProps} />
case 'tilejson_raster': return <TileJSONSourceEditor {...commonProps} />
case 'tilexyz_raster': return <TileURLSourceEditor {...commonProps} />
case 'tilejson_raster-dem': return <TileJSONSourceEditor {...commonProps} />
case 'tilexyz_raster-dem': return <TileURLSourceEditor {...commonProps} />
default: return null
}
}