mirror of
https://github.com/maputnik/editor.git
synced 2026-02-10 14:40:01 +00:00
Restructure and rename components
This commit is contained in:
41
src/components/About.jsx
Normal file
41
src/components/About.jsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import React from 'react'
|
||||
import theme from './theme.js'
|
||||
|
||||
import Heading from 'rebass/dist/Heading'
|
||||
import Container from 'rebass/dist/Container'
|
||||
import Input from 'rebass/dist/Input'
|
||||
import Toolbar from 'rebass/dist/Toolbar'
|
||||
import NavItem from 'rebass/dist/NavItem'
|
||||
import Space from 'rebass/dist/Space'
|
||||
|
||||
import Immutable from 'immutable'
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||
|
||||
/** About page with basic infos and links to github repo */
|
||||
export class About extends React.Component {
|
||||
static propTypes = {}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this);
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div>
|
||||
<Toolbar style={{marginRight: 20}}>
|
||||
<NavItem>
|
||||
<Heading>About</Heading>
|
||||
</NavItem>
|
||||
</Toolbar>
|
||||
<Container>
|
||||
<h3>Maputnik – Visual Map Editor for Mapbox GL</h3>
|
||||
<p>
|
||||
A free and open visual editor for the Mapbox GL styles targeted at developers and map designers. Creating your own custom map is easy with Maputnik.
|
||||
</p>
|
||||
<p>
|
||||
The source code is openly licensed and available on <a href="https://github.com/maputnik/editor">GitHub</a>.
|
||||
</p>
|
||||
</Container>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
169
src/components/App.jsx
Normal file
169
src/components/App.jsx
Normal file
@@ -0,0 +1,169 @@
|
||||
import React from 'react'
|
||||
import { saveAs } from 'file-saver'
|
||||
|
||||
import Drawer from 'rebass/dist/Drawer'
|
||||
import Container from 'rebass/dist/Container'
|
||||
import Block from 'rebass/dist/Block'
|
||||
import Fixed from 'rebass/dist/Fixed'
|
||||
|
||||
import MapboxGlMap from './map/MapboxGlMap.jsx'
|
||||
import OpenLayers3Map from './map/OpenLayers3Map.jsx'
|
||||
import LayerList from './layers/LayerList.jsx'
|
||||
import LayerEditor from './layers/LayerEditor.jsx'
|
||||
import Toolbar from './Toolbar.jsx'
|
||||
|
||||
import style from '../libs/style.js'
|
||||
import { loadDefaultStyle, SettingsStore, StyleStore } from '../libs/stylestore.js'
|
||||
import { ApiStyleStore } from '../libs/apistore.js'
|
||||
import LayerWatcher from '../libs/layerwatcher.js'
|
||||
|
||||
import theme from '../config/rebass.js'
|
||||
import colors from '../config/colors.js'
|
||||
|
||||
export default class App extends React.Component {
|
||||
static childContextTypes = {
|
||||
rebass: React.PropTypes.object,
|
||||
reactIconBase: React.PropTypes.object
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.layerWatcher = new LayerWatcher()
|
||||
this.styleStore = new ApiStyleStore()
|
||||
this.styleStore.supported(isSupported => {
|
||||
if(!isSupported) {
|
||||
console.log('Falling back to local storage for storing styles')
|
||||
this.styleStore = new StyleStore()
|
||||
}
|
||||
this.styleStore.latestStyle(mapStyle => this.onStyleUpload(mapStyle))
|
||||
})
|
||||
|
||||
this.settingsStore = new SettingsStore()
|
||||
this.state = {
|
||||
accessToken: this.settingsStore.accessToken,
|
||||
mapStyle: style.emptyStyle,
|
||||
selectedLayerId: null,
|
||||
}
|
||||
}
|
||||
|
||||
onReset() {
|
||||
this.styleStore.purge()
|
||||
loadDefaultStyle(mapStyle => this.onStyleUpload(mapStyle))
|
||||
}
|
||||
|
||||
getChildContext() {
|
||||
return {
|
||||
rebass: theme,
|
||||
reactIconBase: { size: 20 }
|
||||
}
|
||||
}
|
||||
|
||||
onStyleDownload() {
|
||||
const mapStyle = style.toJSON(this.state.mapStyle)
|
||||
const blob = new Blob([JSON.stringify(mapStyle, null, 4)], {type: "application/json;charset=utf-8"});
|
||||
saveAs(blob, mapStyle.id + ".json");
|
||||
this.onStyleSave()
|
||||
}
|
||||
|
||||
onStyleUpload(newStyle) {
|
||||
const savedStyle = this.styleStore.save(newStyle)
|
||||
this.setState({ mapStyle: savedStyle })
|
||||
}
|
||||
|
||||
onStyleSave() {
|
||||
const snapshotStyle = this.state.mapStyle.set('modified', new Date().toJSON())
|
||||
this.setState({ mapStyle: snapshotStyle })
|
||||
console.log('Save')
|
||||
this.styleStore.save(snapshotStyle)
|
||||
}
|
||||
|
||||
onStyleChanged(newStyle) {
|
||||
this.setState({ mapStyle: newStyle })
|
||||
}
|
||||
|
||||
onAccessTokenChanged(newToken) {
|
||||
this.settingsStore.accessToken = newToken
|
||||
this.setState({ accessToken: newToken })
|
||||
}
|
||||
|
||||
onLayersChanged(changedLayers) {
|
||||
const changedStyle = this.state.mapStyle.set('layers', changedLayers)
|
||||
this.onStyleChanged(changedStyle)
|
||||
}
|
||||
|
||||
onLayerChanged(layer) {
|
||||
console.log('layer changed', layer)
|
||||
const layers = this.state.mapStyle.get('layers')
|
||||
const changedLayers = layers.set(layer.get('id'), layer)
|
||||
this.onLayersChanged(changedLayers)
|
||||
}
|
||||
|
||||
onLayerChanged(layer) {
|
||||
const changedStyle = this.state.mapStyle.setIn(['layers', layer.get('id')], layer)
|
||||
this.onStyleChanged(changedStyle)
|
||||
}
|
||||
|
||||
mapRenderer() {
|
||||
const mapProps = {
|
||||
mapStyle: this.state.mapStyle,
|
||||
accessToken: this.state.accessToken,
|
||||
onMapLoaded: (map) => {
|
||||
this.layerWatcher.map = map
|
||||
}
|
||||
}
|
||||
const renderer = this.state.mapStyle.getIn(['metadata', 'maputnik:renderer'], 'mbgljs')
|
||||
if(renderer === 'ol3') {
|
||||
return <OpenLayers3Map {...mapProps} />
|
||||
} else {
|
||||
return <MapboxGlMap {...mapProps} />
|
||||
}
|
||||
}
|
||||
|
||||
onLayerSelected(layerId) {
|
||||
this.setState({ selectedLayerId: layerId })
|
||||
}
|
||||
|
||||
render() {
|
||||
const selectedLayer = this.state.mapStyle.getIn(['layers', this.state.selectedLayerId], null)
|
||||
return <div style={{ fontFamily: theme.fontFamily, color: theme.color, fontWeight: 300 }}>
|
||||
<Toolbar
|
||||
mapStyle={this.state.mapStyle}
|
||||
onStyleChanged={this.onStyleChanged.bind(this)}
|
||||
onStyleSave={this.onStyleSave.bind(this)}
|
||||
onStyleUpload={this.onStyleUpload.bind(this)}
|
||||
onStyleDownload={this.onStyleDownload.bind(this)}
|
||||
/>
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
height: "100%",
|
||||
top: 50,
|
||||
left: 0,
|
||||
zIndex: 100,
|
||||
width: 180,
|
||||
overflow: "hidden",
|
||||
backgroundColor: colors.gray
|
||||
}}>
|
||||
<LayerList
|
||||
onLayersChanged={this.onLayersChanged.bind(this)}
|
||||
onLayerSelected={this.onLayerSelected.bind(this)}
|
||||
layers={this.state.mapStyle.get('layers')}
|
||||
/>
|
||||
</div>
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
height: "100%",
|
||||
top: 50,
|
||||
left: 180,
|
||||
zIndex: 100,
|
||||
width: 300,
|
||||
backgroundColor: colors.gray}
|
||||
}>
|
||||
{selectedLayer && <LayerEditor layer={selectedLayer} onLayerChanged={this.onLayerChanged.bind(this)} sources={this.layerWatcher.sources} vectorLayers={this.layerWatcher.vectorLayers}/>}
|
||||
</div>
|
||||
{this.mapRenderer()}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
17
src/components/ScrollContainer.jsx
Normal file
17
src/components/ScrollContainer.jsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import React from 'react'
|
||||
import scrollbars from './scrollbars.scss'
|
||||
|
||||
const ScrollContainer = (props) => {
|
||||
return <div className={scrollbars.darkScrollbar} style={{
|
||||
overflowY: "scroll",
|
||||
bottom:0,
|
||||
left:0,
|
||||
right:0,
|
||||
top:1,
|
||||
position: "absolute",
|
||||
}}>
|
||||
{props.children}
|
||||
</div>
|
||||
}
|
||||
|
||||
export default ScrollContainer
|
||||
166
src/components/Toolbar.jsx
Normal file
166
src/components/Toolbar.jsx
Normal file
@@ -0,0 +1,166 @@
|
||||
import React from 'react'
|
||||
import Immutable from 'immutable'
|
||||
import FileReaderInput from 'react-file-reader-input'
|
||||
|
||||
import Button from 'rebass/dist/Button'
|
||||
import Text from 'rebass/dist/Text'
|
||||
import Menu from 'rebass/dist/Menu'
|
||||
import NavItem from 'rebass/dist/NavItem'
|
||||
import Tooltip from 'rebass/dist/Tooltip'
|
||||
import Container from 'rebass/dist/Container'
|
||||
import Block from 'rebass/dist/Block'
|
||||
import Fixed from 'rebass/dist/Fixed'
|
||||
|
||||
import MdFileDownload from 'react-icons/lib/md/file-download'
|
||||
import MdFileUpload from 'react-icons/lib/md/file-upload'
|
||||
import MdOpenInBrowser from 'react-icons/lib/md/open-in-browser'
|
||||
import MdSettings from 'react-icons/lib/md/settings'
|
||||
import MdInfo from 'react-icons/lib/md/info'
|
||||
import MdLayers from 'react-icons/lib/md/layers'
|
||||
import MdSave from 'react-icons/lib/md/save'
|
||||
import MdStyle from 'react-icons/lib/md/style'
|
||||
import MdMap from 'react-icons/lib/md/map'
|
||||
import MdInsertEmoticon from 'react-icons/lib/md/insert-emoticon'
|
||||
import MdFontDownload from 'react-icons/lib/md/font-download'
|
||||
import MdHelpOutline from 'react-icons/lib/md/help-outline'
|
||||
import MdFindInPage from 'react-icons/lib/md/find-in-page'
|
||||
|
||||
import SettingsModal from './modals/SettingsModal'
|
||||
import TilesetsModal from './modals/TilesetsModal'
|
||||
|
||||
import style from '../libs/style'
|
||||
import colors from '../config/colors';
|
||||
|
||||
const InlineBlock = props => <div style={{display: "inline-block", ...props.style}}>
|
||||
{props.children}
|
||||
</div>
|
||||
|
||||
export default class Toolbar extends React.Component {
|
||||
static propTypes = {
|
||||
mapStyle: React.PropTypes.instanceOf(Immutable.Map).isRequired,
|
||||
onStyleChanged: React.PropTypes.func.isRequired,
|
||||
// A new style has been uploaded
|
||||
onStyleUpload: React.PropTypes.func.isRequired,
|
||||
// Current style is requested for download
|
||||
onStyleDownload: React.PropTypes.func.isRequired,
|
||||
// Style is explicitely saved to local cache
|
||||
onStyleSave: React.PropTypes.func,
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
openSettingsModal: false,
|
||||
openTilesetsModal: false,
|
||||
}
|
||||
}
|
||||
|
||||
onUpload(_, files) {
|
||||
const [e, file] = files[0];
|
||||
const reader = new FileReader();
|
||||
reader.readAsText(file, "UTF-8");
|
||||
reader.onload = e => {
|
||||
let mapStyle = style.fromJSON(JSON.parse(e.target.result))
|
||||
mapStyle = style.ensureMetadataExists(mapStyle)
|
||||
this.props.onStyleUpload(mapStyle);
|
||||
}
|
||||
reader.onerror = e => console.log(e.target);
|
||||
}
|
||||
|
||||
saveButton() {
|
||||
if(this.props.mapStyle.get('layers').size > 0) {
|
||||
return <InlineBlock>
|
||||
<Button onClick={this.props.onStyleSave} big={true}>
|
||||
<MdSave />
|
||||
Save
|
||||
</Button>
|
||||
</InlineBlock>
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
downloadButton() {
|
||||
if(this.props.styleAvailable) {
|
||||
return <InlineBlock>
|
||||
<Button onClick={this.props.onStyleDownload} big={true}>
|
||||
<MdFileDownload />
|
||||
Download
|
||||
</Button>
|
||||
</InlineBlock>
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
toggleSettings() {
|
||||
this.setState({openSettingsModal: !this.state.openSettingsModal})
|
||||
}
|
||||
|
||||
toggleTilesets() {
|
||||
this.setState({openTilesetsModal: !this.state.openTilesetsModal})
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div style={{
|
||||
position: "fixed",
|
||||
height: 50,
|
||||
width: '100%',
|
||||
zIndex: 100,
|
||||
left: 0,
|
||||
top: 0,
|
||||
backgroundColor: colors.black
|
||||
}}>
|
||||
<SettingsModal
|
||||
mapStyle={this.props.mapStyle}
|
||||
onStyleChanged={this.props.onStyleChanged}
|
||||
open={this.state.openSettingsModal}
|
||||
toggle={() => this.toggleSettings.bind(this)}
|
||||
/>
|
||||
<TilesetsModal
|
||||
mapStyle={this.props.mapStyle}
|
||||
onStyleChanged={this.props.onStyleChanged}
|
||||
open={this.state.openTilesetsModal}
|
||||
toggle={() => this.toggleSettings.bind(this)}
|
||||
/>
|
||||
<InlineBlock>
|
||||
<Button style={{width: 180, textAlign: 'left'}}>
|
||||
<img src="https://github.com/maputnik/editor/raw/master/media/maputnik.png" alt="Maputnik" style={{width: 40, height: 40, paddingRight: 5, verticalAlign: 'middle'}}/>
|
||||
<span style={{fontSize: 20 }}>Maputnik</span>
|
||||
</Button>
|
||||
</InlineBlock>
|
||||
<InlineBlock>
|
||||
<FileReaderInput onChange={this.onUpload.bind(this)}>
|
||||
<Button big={true} theme={this.props.styleAvailable ? "default" : "success"}>
|
||||
<MdOpenInBrowser />
|
||||
Open
|
||||
</Button>
|
||||
</FileReaderInput>
|
||||
</InlineBlock>
|
||||
{this.downloadButton()}
|
||||
{this.saveButton()}
|
||||
<InlineBlock>
|
||||
<Button big={true} onClick={this.toggleTilesets.bind(this)}>
|
||||
<MdLayers />
|
||||
Tilesets
|
||||
</Button>
|
||||
</InlineBlock>
|
||||
<InlineBlock>
|
||||
<Button big={true} onClick={this.toggleSettings.bind(this)}>
|
||||
<MdSettings />
|
||||
Style Settings
|
||||
</Button>
|
||||
</InlineBlock>
|
||||
<InlineBlock>
|
||||
<Button big={true} onClick={this.toggleSettings.bind(this)}>
|
||||
<MdFindInPage />
|
||||
Inspect
|
||||
</Button>
|
||||
</InlineBlock>
|
||||
<InlineBlock>
|
||||
<Button big={true} onClick={this.props.onOpenAbout}>
|
||||
<MdHelpOutline />
|
||||
Help
|
||||
</Button>
|
||||
</InlineBlock>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
28
src/components/fields/BooleanField.jsx
Normal file
28
src/components/fields/BooleanField.jsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import React from 'react'
|
||||
import input from '../../config/input.js'
|
||||
|
||||
class BooleanField extends React.Component {
|
||||
static propTypes = {
|
||||
onChange: React.PropTypes.func.isRequired,
|
||||
name: React.PropTypes.string.isRequired,
|
||||
value: React.PropTypes.bool,
|
||||
doc: React.PropTypes.string,
|
||||
style: React.PropTypes.object,
|
||||
}
|
||||
|
||||
render() {
|
||||
return <input
|
||||
type="checkbox"
|
||||
style={{
|
||||
...input.checkbox,
|
||||
...this.props.style
|
||||
}}
|
||||
value={this.props.value}
|
||||
onChange={e => {this.props.onChange(!this.props.value)}}
|
||||
checked={this.props.value}
|
||||
>
|
||||
</input>
|
||||
}
|
||||
}
|
||||
|
||||
export default BooleanField
|
||||
77
src/components/fields/ColorField.jsx
Normal file
77
src/components/fields/ColorField.jsx
Normal file
@@ -0,0 +1,77 @@
|
||||
import React from 'react'
|
||||
import Color from 'color'
|
||||
import ChromePicker from 'react-color/lib/components/chrome/Chrome'
|
||||
|
||||
import input from '../../config/input.js'
|
||||
|
||||
function formatColor(color) {
|
||||
const rgb = color.rgb
|
||||
return `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${rgb.a})`
|
||||
}
|
||||
|
||||
/*** 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,
|
||||
default: React.PropTypes.number,
|
||||
doc: React.PropTypes.string,
|
||||
style: React.PropTypes.object,
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
pickerOpened: false
|
||||
}
|
||||
}
|
||||
|
||||
togglePicker() {
|
||||
this.setState({ pickerOpened: !this.state.pickerOpened })
|
||||
}
|
||||
|
||||
render() {
|
||||
const picker = <div style={{
|
||||
position: 'absolute',
|
||||
left: 163,
|
||||
top: 0,
|
||||
}}>
|
||||
<ChromePicker
|
||||
color={this.props.value ? Color(this.props.value).object() : null}
|
||||
onChange={c => this.props.onChange(formatColor(c))}
|
||||
/>
|
||||
<div
|
||||
onClick={this.togglePicker.bind(this)}
|
||||
style={{
|
||||
zIndex: -1,
|
||||
position: 'fixed',
|
||||
top: '0px',
|
||||
right: '0px',
|
||||
bottom: '0px',
|
||||
left: '0px',
|
||||
}} />
|
||||
</div>
|
||||
|
||||
return <div style={{
|
||||
...input.property,
|
||||
position: 'relative',
|
||||
display: 'inline',
|
||||
}}>
|
||||
{this.state.pickerOpened && picker}
|
||||
<input
|
||||
onClick={this.togglePicker.bind(this)}
|
||||
style={{
|
||||
...input.select,
|
||||
...this.props.style
|
||||
}}
|
||||
name={this.props.name}
|
||||
placeholder={this.props.default}
|
||||
value={this.props.value ? this.props.value : ""}
|
||||
onChange={(e) => this.props.onChange(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
export default ColorField
|
||||
36
src/components/fields/EnumField.jsx
Normal file
36
src/components/fields/EnumField.jsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import React from 'react'
|
||||
import input from '../../config/input.js'
|
||||
|
||||
class EnumField extends React.Component {
|
||||
static propTypes = {
|
||||
onChange: React.PropTypes.func.isRequired,
|
||||
name: React.PropTypes.string.isRequired,
|
||||
value: React.PropTypes.string,
|
||||
allowedValues: React.PropTypes.array.isRequired,
|
||||
doc: React.PropTypes.string,
|
||||
style: React.PropTypes.object,
|
||||
}
|
||||
|
||||
onChange(e) {
|
||||
return this.props.onChange(e.target.value)
|
||||
}
|
||||
|
||||
render() {
|
||||
const options = this.props.allowedValues.map(val => {
|
||||
return <option key={val} value={val}>{val}</option>
|
||||
})
|
||||
|
||||
return <select
|
||||
style={{
|
||||
...input.select,
|
||||
...this.props.style
|
||||
}}
|
||||
value={this.props.value}
|
||||
onChange={this.onChange.bind(this)}
|
||||
>
|
||||
{options}
|
||||
</select>
|
||||
}
|
||||
}
|
||||
|
||||
export default EnumField
|
||||
42
src/components/fields/NumberField.jsx
Normal file
42
src/components/fields/NumberField.jsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import React from 'react'
|
||||
import input from '../../config/input.js'
|
||||
|
||||
/*** Number fields with support for min, max and units and documentation*/
|
||||
class NumberField extends React.Component {
|
||||
static propTypes = {
|
||||
onChange: React.PropTypes.func.isRequired,
|
||||
name: React.PropTypes.string.isRequired,
|
||||
value: React.PropTypes.number,
|
||||
default: React.PropTypes.number,
|
||||
unit: React.PropTypes.string,
|
||||
min: React.PropTypes.number,
|
||||
max: React.PropTypes.number,
|
||||
doc: React.PropTypes.string,
|
||||
style: React.PropTypes.object,
|
||||
}
|
||||
|
||||
onChange(e) {
|
||||
const value = parseFloat(e.target.value)
|
||||
/*TODO: we can do range validation already here?
|
||||
if(this.props.min && value < this.props.min) return
|
||||
if(this.props.max && value > this.props.max) return
|
||||
*/
|
||||
this.props.onChange(value)
|
||||
}
|
||||
|
||||
render() {
|
||||
return <input
|
||||
style={{
|
||||
...input.input,
|
||||
...this.props.style
|
||||
}}
|
||||
type="number"
|
||||
name={this.props.name}
|
||||
placeholder={this.props.default}
|
||||
value={this.props.value}
|
||||
onChange={this.onChange.bind(this)}
|
||||
/>
|
||||
}
|
||||
}
|
||||
|
||||
export default NumberField
|
||||
50
src/components/fields/PropertyGroup.jsx
Normal file
50
src/components/fields/PropertyGroup.jsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import React from 'react'
|
||||
import Immutable from 'immutable'
|
||||
import GlSpec from 'mapbox-gl-style-spec/reference/latest.min.js'
|
||||
|
||||
import ZoomSpecField from './ZoomSpecField'
|
||||
import colors from '../../config/colors'
|
||||
import { margins } from '../../config/scales'
|
||||
|
||||
/** Extract field spec by {@fieldName} from the {@layerType} in the
|
||||
* style specification from either the paint or layout group */
|
||||
function getFieldSpec(layerType, fieldName) {
|
||||
const paint = GlSpec['paint_' + layerType] || {}
|
||||
const layout = GlSpec['layout_' + layerType] || {}
|
||||
if (fieldName in paint) {
|
||||
return paint[fieldName]
|
||||
} else {
|
||||
return layout[fieldName]
|
||||
}
|
||||
}
|
||||
|
||||
export default class PropertyGroup extends React.Component {
|
||||
static propTypes = {
|
||||
layer: React.PropTypes.instanceOf(Immutable.Map).isRequired,
|
||||
groupFields: React.PropTypes.instanceOf(Immutable.OrderedSet).isRequired,
|
||||
onChange: React.PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
render() {
|
||||
const fields = this.props.groupFields.map(fieldName => {
|
||||
const fieldSpec = getFieldSpec(this.props.layer.get('type'), fieldName)
|
||||
const fieldValue = this.props.layer.getIn(['paint', fieldName], this.props.layer.getIn(['layout', fieldName]))
|
||||
return <ZoomSpecField
|
||||
onChange={this.props.onChange}
|
||||
key={fieldName}
|
||||
fieldName={fieldName}
|
||||
value={fieldValue}
|
||||
fieldSpec={fieldSpec}
|
||||
/>
|
||||
}).toIndexedSeq()
|
||||
|
||||
return <div style={{
|
||||
padding: margins[2],
|
||||
paddingRight: 0,
|
||||
backgroundColor: colors.black,
|
||||
marginBottom: margins[2],
|
||||
}}>
|
||||
{fields}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
103
src/components/fields/SpecField.jsx
Normal file
103
src/components/fields/SpecField.jsx
Normal file
@@ -0,0 +1,103 @@
|
||||
import React from 'react'
|
||||
import Immutable from 'immutable'
|
||||
import color from 'color'
|
||||
|
||||
import GlSpec from 'mapbox-gl-style-spec/reference/latest.min.js'
|
||||
import NumberField from './NumberField'
|
||||
import EnumField from './EnumField'
|
||||
import BooleanField from './BooleanField'
|
||||
import ColorField from './ColorField'
|
||||
import StringField from './StringField'
|
||||
|
||||
import input from '../../config/input.js'
|
||||
import theme from '../../config/rebass.js'
|
||||
|
||||
function isZoomField(value) {
|
||||
return Immutable.Map.isMap(value)
|
||||
}
|
||||
|
||||
function labelFromFieldName(fieldName) {
|
||||
let label = fieldName.split('-').slice(1).join(' ')
|
||||
if(label.length > 0) {
|
||||
label = label.charAt(0).toUpperCase() + label.slice(1);
|
||||
}
|
||||
return label
|
||||
}
|
||||
|
||||
/** Display any field from the Mapbox GL style spec and
|
||||
* choose the correct field component based on the @{fieldSpec}
|
||||
* 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,
|
||||
]),
|
||||
/** Override the style of the field */
|
||||
style: React.PropTypes.object,
|
||||
}
|
||||
|
||||
onValueChanged(property, value) {
|
||||
return this.props.onChange(property, value)
|
||||
}
|
||||
|
||||
render() {
|
||||
switch(this.props.fieldSpec.type) {
|
||||
case 'number': return (
|
||||
<NumberField
|
||||
onChange={this.onValueChanged.bind(this, this.props.fieldName)}
|
||||
value={this.props.value}
|
||||
name={this.props.fieldName}
|
||||
default={this.props.fieldSpec.default}
|
||||
min={this.props.fieldSpec.min}
|
||||
max={this.props.fieldSpec.max}
|
||||
unit={this.props.fieldSpec.unit}
|
||||
doc={this.props.fieldSpec.doc}
|
||||
style={this.props.style}
|
||||
/>
|
||||
)
|
||||
case 'enum': return (
|
||||
<EnumField
|
||||
onChange={this.onValueChanged.bind(this, this.props.fieldName)}
|
||||
value={this.props.value}
|
||||
name={this.props.fieldName}
|
||||
allowedValues={Object.keys(this.props.fieldSpec.values)}
|
||||
doc={this.props.fieldSpec.doc}
|
||||
style={this.props.style}
|
||||
/>
|
||||
)
|
||||
case 'string': return (
|
||||
<StringField
|
||||
onChange={this.onValueChanged.bind(this, this.props.fieldName)}
|
||||
value={this.props.value}
|
||||
name={this.props.fieldName}
|
||||
doc={this.props.fieldSpec.doc}
|
||||
style={this.props.style}
|
||||
/>
|
||||
)
|
||||
case 'color': return (
|
||||
<ColorField
|
||||
onChange={this.onValueChanged.bind(this, this.props.fieldName)}
|
||||
value={this.props.value}
|
||||
name={this.props.fieldName}
|
||||
doc={this.props.fieldSpec.doc}
|
||||
style={this.props.style}
|
||||
/>
|
||||
)
|
||||
case 'boolean': return (
|
||||
<BooleanField
|
||||
onChange={this.onValueChanged.bind(this, this.props.fieldName)}
|
||||
value={this.props.value}
|
||||
name={this.props.fieldName}
|
||||
doc={this.props.fieldSpec.doc}
|
||||
style={this.props.style}
|
||||
/>
|
||||
)
|
||||
default: return null
|
||||
}
|
||||
}
|
||||
}
|
||||
34
src/components/fields/StringField.jsx
Normal file
34
src/components/fields/StringField.jsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import React from 'react'
|
||||
import input from '../../config/input.js'
|
||||
|
||||
/*** Number fields with support for min, max and units and documentation*/
|
||||
class StringField extends React.Component {
|
||||
static propTypes = {
|
||||
onChange: React.PropTypes.func.isRequired,
|
||||
name: React.PropTypes.string.isRequired,
|
||||
value: React.PropTypes.string,
|
||||
default: React.PropTypes.number,
|
||||
doc: React.PropTypes.string,
|
||||
style: React.PropTypes.object,
|
||||
}
|
||||
|
||||
onChange(e) {
|
||||
const value = e.target.value
|
||||
return this.props.onChange(value === "" ? null: value)
|
||||
}
|
||||
|
||||
render() {
|
||||
return <input
|
||||
style={{
|
||||
...input.input,
|
||||
...this.props.style
|
||||
}}
|
||||
name={this.props.name}
|
||||
placeholder={this.props.default}
|
||||
value={this.props.value ? this.props.value : ""}
|
||||
onChange={this.onChange.bind(this)}
|
||||
/>
|
||||
}
|
||||
}
|
||||
|
||||
export default StringField
|
||||
98
src/components/fields/ZoomSpecField.jsx
Normal file
98
src/components/fields/ZoomSpecField.jsx
Normal file
@@ -0,0 +1,98 @@
|
||||
import React from 'react'
|
||||
import Immutable from 'immutable'
|
||||
import Color from 'color'
|
||||
|
||||
import NumberField from './NumberField'
|
||||
import EnumField from './EnumField'
|
||||
import BooleanField from './BooleanField'
|
||||
import ColorField from './ColorField'
|
||||
import StringField from './StringField'
|
||||
import SpecField from './SpecField'
|
||||
|
||||
import input from '../../config/input.js'
|
||||
import colors from '../../config/colors.js'
|
||||
import { margins } from '../../config/scales.js'
|
||||
|
||||
function isZoomField(value) {
|
||||
return Immutable.Map.isMap(value)
|
||||
}
|
||||
|
||||
const specFieldProps = {
|
||||
onChange: React.PropTypes.func.isRequired,
|
||||
fieldName: React.PropTypes.string.isRequired,
|
||||
fieldSpec: React.PropTypes.object.isRequired,
|
||||
}
|
||||
|
||||
/** Supports displaying spec field for zoom function objects
|
||||
* https://www.mapbox.com/mapbox-gl-style-spec/#types-function-zoom-property
|
||||
*/
|
||||
export default class ZoomSpecField 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,
|
||||
]),
|
||||
}
|
||||
|
||||
render() {
|
||||
const label = <label style={input.label}>
|
||||
{labelFromFieldName(this.props.fieldName)}
|
||||
</label>
|
||||
|
||||
if(isZoomField(this.props.value)) {
|
||||
const zoomFields = this.props.value.get('stops').map(stop => {
|
||||
const zoomLevel = stop.get(0)
|
||||
const value = stop.get(1)
|
||||
|
||||
return <div style={input.property} key={zoomLevel}>
|
||||
{label}
|
||||
<SpecField {...this.props}
|
||||
value={value}
|
||||
style={{
|
||||
width: '33%'
|
||||
}}
|
||||
/>
|
||||
|
||||
<input
|
||||
style={{
|
||||
...input.input,
|
||||
width: '10%',
|
||||
marginLeft: margins[0],
|
||||
}}
|
||||
type="number"
|
||||
value={zoomLevel}
|
||||
min={0}
|
||||
max={22}
|
||||
/>
|
||||
</div>
|
||||
}).toSeq()
|
||||
return <div style={{
|
||||
border: 1,
|
||||
borderStyle: 'solid',
|
||||
borderColor: Color(colors.gray).lighten(0.1).string(),
|
||||
padding: margins[1],
|
||||
}}>
|
||||
{zoomFields}
|
||||
</div>
|
||||
} else {
|
||||
return <div style={input.property}>
|
||||
{label}
|
||||
<SpecField {...this.props} />
|
||||
</div>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function labelFromFieldName(fieldName) {
|
||||
let label = fieldName.split('-').slice(1).join(' ')
|
||||
if(label.length > 0) {
|
||||
label = label.charAt(0).toUpperCase() + label.slice(1);
|
||||
}
|
||||
return label
|
||||
}
|
||||
183
src/components/filter/FilterEditor.jsx
Normal file
183
src/components/filter/FilterEditor.jsx
Normal file
@@ -0,0 +1,183 @@
|
||||
import React from 'react'
|
||||
import Immutable from 'immutable'
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||
import GlSpec from 'mapbox-gl-style-spec/reference/latest.min.js'
|
||||
|
||||
import input from '../../config/input.js'
|
||||
import colors from '../../config/colors.js'
|
||||
import { margins } from '../../config/scales.js'
|
||||
|
||||
const combiningFilterOps = ['all', 'any', 'none']
|
||||
const setFilterOps = ['in', '!in']
|
||||
const otherFilterOps = Object
|
||||
.keys(GlSpec.filter_operator.values)
|
||||
.filter(op => combiningFilterOps.indexOf(op) < 0)
|
||||
|
||||
class CombiningOperatorSelect extends React.Component {
|
||||
static propTypes = {
|
||||
value: React.PropTypes.string.isRequired,
|
||||
onChange: React.PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
render() {
|
||||
const options = combiningFilterOps.map(op => {
|
||||
return <option key={op} value={op}>{op}</option>
|
||||
})
|
||||
|
||||
return <div>
|
||||
<select
|
||||
style={{
|
||||
...input.select,
|
||||
width: '20.5%',
|
||||
margin: margins[0],
|
||||
}}
|
||||
value={this.props.value}
|
||||
onChange={e => this.props.onChange(e.target.value)}
|
||||
>
|
||||
{options}
|
||||
</select>
|
||||
<label style={{
|
||||
...input.label,
|
||||
width: '60%',
|
||||
marginLeft: margins[0],
|
||||
}}>
|
||||
of the filters matches
|
||||
</label>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
class OperatorSelect extends React.Component {
|
||||
static propTypes = {
|
||||
value: React.PropTypes.string.isRequired,
|
||||
onChange: React.PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
render() {
|
||||
const options = otherFilterOps.map(op => {
|
||||
return <option key={op} value={op}>{op}</option>
|
||||
})
|
||||
return <select
|
||||
style={{
|
||||
...input.select,
|
||||
width: '15%',
|
||||
margin: margins[0]
|
||||
}}
|
||||
value={this.props.value}
|
||||
onChange={e => this.props.onChange(e.target.value)}
|
||||
>
|
||||
{options}
|
||||
</select>
|
||||
}
|
||||
}
|
||||
|
||||
class SingleFilterEditor extends React.Component {
|
||||
static propTypes = {
|
||||
filter: React.PropTypes.array.isRequired,
|
||||
onChange: React.PropTypes.func.isRequired,
|
||||
properties: React.PropTypes.instanceOf(Immutable.Map).isRequired,
|
||||
}
|
||||
|
||||
onFilterPartChanged(filterOp, propertyName, filterArgs) {
|
||||
const newFilter = [filterOp, propertyName, ...filterArgs]
|
||||
this.props.onChange(newFilter)
|
||||
}
|
||||
|
||||
render() {
|
||||
const f = this.props.filter
|
||||
const filterOp = f[0]
|
||||
const propertyName = f[1]
|
||||
const filterArgs = f.slice(2)
|
||||
|
||||
return <div>
|
||||
<select
|
||||
style={{
|
||||
...input.select,
|
||||
width: '17%',
|
||||
margin: margins[0]
|
||||
}}
|
||||
value={propertyName}
|
||||
onChange={newPropertyName => this.onFilterPartChanged(filterOp, newPropertyName, filterArgs)}
|
||||
>
|
||||
{this.props.properties.keySeq().map(propName => {
|
||||
return <option key={propName} value={propName}>{propName}</option>
|
||||
}).toIndexedSeq()}
|
||||
</select>
|
||||
<OperatorSelect
|
||||
value={filterOp}
|
||||
onChange={newFilterOp => this.onFilterPartChanged(newFilterOp, propertyName, filterArgs)}
|
||||
/>
|
||||
<input
|
||||
style={{
|
||||
...input.input,
|
||||
width: '53%',
|
||||
margin: margins[0]
|
||||
}}
|
||||
value={filterArgs.join(',')}
|
||||
onChange={e => {
|
||||
this.onFilterPartChanged(filterOp, propertyName, e.target.value.split(','))}}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default class CombiningFilterEditor extends React.Component {
|
||||
static propTypes = {
|
||||
/** Properties of the vector layer and the available fields */
|
||||
properties: React.PropTypes.instanceOf(Immutable.Map).isRequired,
|
||||
filter: React.PropTypes.array.isRequired,
|
||||
onChange: React.PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this);
|
||||
}
|
||||
|
||||
// Convert filter to combining filter
|
||||
combiningFilter() {
|
||||
let combiningOp = this.props.filter[0]
|
||||
let filters = this.props.filter.slice(1)
|
||||
|
||||
if(combiningFilterOps.indexOf(combiningOp) < 0) {
|
||||
combiningOp = 'all'
|
||||
filters = [this.props.filter.slice(0)]
|
||||
}
|
||||
|
||||
return [combiningOp, ...filters]
|
||||
}
|
||||
|
||||
onFilterPartChanged(filterIdx, newPart) {
|
||||
const newFilter = this.combiningFilter().slice(0)
|
||||
newFilter[filterIdx] = newPart
|
||||
this.props.onChange(newFilter)
|
||||
}
|
||||
|
||||
render() {
|
||||
const filter = this.combiningFilter()
|
||||
let combiningOp = filter[0]
|
||||
let filters = filter.slice(1)
|
||||
|
||||
const filterEditors = filters.map((f, idx) => {
|
||||
return <SingleFilterEditor
|
||||
key={idx}
|
||||
properties={this.props.properties}
|
||||
filter={f}
|
||||
onChange={this.onFilterPartChanged.bind(this, idx + 1)}
|
||||
/>
|
||||
})
|
||||
|
||||
return <div style={{
|
||||
padding: margins[2],
|
||||
paddingRight: 0,
|
||||
backgroundColor: colors.black
|
||||
}}>
|
||||
<CombiningOperatorSelect
|
||||
value={combiningOp}
|
||||
onChange={this.onFilterPartChanged.bind(this, 0)}
|
||||
/>
|
||||
{filterEditors}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
15
src/components/icons/BackgroundIcon.jsx
Normal file
15
src/components/icons/BackgroundIcon.jsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import React from 'react'
|
||||
import IconBase from 'react-icon-base'
|
||||
|
||||
|
||||
export default class BackgroundIcon extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<IconBase viewBox="0 0 20 20" {...this.props}>
|
||||
<path d="m 1.821019,10.255581 7.414535,5.020197 c 0.372277,0.25206 0.958697,0.239771 1.30985,-0.02745 L 17.539255,9.926162 C 17.89041,9.658941 17.873288,9.238006 17.501015,8.985946 L 10.08648,3.9657402 C 9.714204,3.7136802 9.127782,3.7259703 8.776627,3.9931918 L 1.782775,9.315365 c -0.3511551,0.267221 -0.3340331,0.688156 0.03824,0.940216 z" />
|
||||
</IconBase>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
15
src/components/icons/FillIcon.jsx
Normal file
15
src/components/icons/FillIcon.jsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import React from 'react'
|
||||
import IconBase from 'react-icon-base'
|
||||
|
||||
|
||||
export default class FillIcon extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<IconBase viewBox="0 0 20 20" {...this.props}>
|
||||
<path d="M 2.84978,9.763512 9.462149,4.7316391 16.47225,9.478015 9.859886,14.509879 2.84978,9.763512 m -1.028761,0.492069 7.414535,5.020197 c 0.372277,0.25206 0.958697,0.239771 1.30985,-0.02745 L 17.539255,9.926162 C 17.89041,9.658941 17.873288,9.238006 17.501015,8.985946 L 10.08648,3.9657402 C 9.714204,3.7136802 9.127782,3.7259703 8.776627,3.9931918 L 1.782775,9.315365 c -0.3511551,0.267221 -0.3340331,0.688156 0.03824,0.940216 l 0,0 z" />
|
||||
</IconBase>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
26
src/components/icons/LayerIcon.jsx
Normal file
26
src/components/icons/LayerIcon.jsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import React from 'react'
|
||||
|
||||
import LineIcon from './LineIcon.jsx'
|
||||
import FillIcon from './FillIcon.jsx'
|
||||
import SymbolIcon from './SymbolIcon.jsx'
|
||||
import BackgroundIcon from './BackgroundIcon.jsx'
|
||||
|
||||
class LayerIcon extends React.Component {
|
||||
static propTypes = {
|
||||
type: React.PropTypes.string.isRequired,
|
||||
style: React.PropTypes.object,
|
||||
}
|
||||
|
||||
render() {
|
||||
const iconProps = { style: this.props.style }
|
||||
switch(this.props.type) {
|
||||
case 'fill': return <FillIcon {...iconProps} />
|
||||
case 'background': return <BackgroundIcon {...iconProps} />
|
||||
case 'line': return <LineIcon {...iconProps} />
|
||||
case 'symbol': return <SymbolIcon {...iconProps} />
|
||||
default: return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default LayerIcon
|
||||
15
src/components/icons/LineIcon.jsx
Normal file
15
src/components/icons/LineIcon.jsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import React from 'react'
|
||||
import IconBase from 'react-icon-base'
|
||||
|
||||
|
||||
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)" />
|
||||
</IconBase>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
18
src/components/icons/SymbolIcon.jsx
Normal file
18
src/components/icons/SymbolIcon.jsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import React from 'react'
|
||||
import IconBase from 'react-icon-base'
|
||||
|
||||
|
||||
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>
|
||||
</IconBase>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
130
src/components/layers/LayerEditor.jsx
Normal file
130
src/components/layers/LayerEditor.jsx
Normal file
@@ -0,0 +1,130 @@
|
||||
import React from 'react'
|
||||
import Immutable from 'immutable'
|
||||
|
||||
import Toolbar from 'rebass/dist/Toolbar'
|
||||
import NavItem from 'rebass/dist/NavItem'
|
||||
import Space from 'rebass/dist/Space'
|
||||
import Tabs from 'react-simpletabs'
|
||||
|
||||
import SourceEditor from './SourceEditor'
|
||||
import FilterEditor from '../filter/FilterEditor'
|
||||
import PropertyGroup from '../fields/PropertyGroup'
|
||||
|
||||
import MdVisibility from 'react-icons/lib/md/visibility'
|
||||
import MdVisibilityOff from 'react-icons/lib/md/visibility-off'
|
||||
import MdDelete from 'react-icons/lib/md/delete'
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||
|
||||
import ScrollContainer from '../ScrollContainer'
|
||||
|
||||
import layout from '../../config/layout.json'
|
||||
import theme from '../../config/rebass.js'
|
||||
|
||||
class UnsupportedLayer extends React.Component {
|
||||
render() {
|
||||
return <div></div>
|
||||
}
|
||||
}
|
||||
|
||||
/** Layer editor supporting multiple types of layers. */
|
||||
export default class LayerEditor extends React.Component {
|
||||
static propTypes = {
|
||||
layer: React.PropTypes.object.isRequired,
|
||||
sources: React.PropTypes.instanceOf(Immutable.Map),
|
||||
vectorLayers: React.PropTypes.instanceOf(Immutable.Map),
|
||||
onLayerChanged: React.PropTypes.func,
|
||||
onLayerDestroyed: React.PropTypes.func,
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
onLayerChanged: () => {},
|
||||
onLayerDestroyed: () => {},
|
||||
}
|
||||
|
||||
static childContextTypes = {
|
||||
reactIconBase: React.PropTypes.object
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this);
|
||||
}
|
||||
|
||||
getChildContext () {
|
||||
return {
|
||||
reactIconBase: {
|
||||
size: theme.fontSizes[4],
|
||||
color: theme.colors.lowgray,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onPropertyChange(group, property, newValue) {
|
||||
const layer = this.props.layer
|
||||
const changedLayer = layer.setIn([group, property], newValue)
|
||||
this.props.onLayerChanged(changedLayer)
|
||||
}
|
||||
|
||||
onFilterChange(newValue) {
|
||||
let layer = this.props.layer
|
||||
const changedLayer = layer.set('filter', newValue)
|
||||
this.props.onLayerChanged(changedLayer)
|
||||
}
|
||||
|
||||
toggleVisibility() {
|
||||
if(this.props.layer.has('layout') && this.props.layer.getIn(['layout', 'visibility']) === 'none') {
|
||||
this.onLayoutChanged('visibility', 'visible')
|
||||
} else {
|
||||
this.onLayoutChanged('visibility', 'none')
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const layerType = this.props.layer.get('type')
|
||||
const groups = layout[layerType].groups
|
||||
const propertyGroups = groups.map(group => {
|
||||
return <PropertyGroup
|
||||
key={this.props.group}
|
||||
layer={this.props.layer}
|
||||
groupFields={Immutable.OrderedSet(group.fields)}
|
||||
onChange={this.onPropertyChange.bind(this)}
|
||||
/>
|
||||
})
|
||||
|
||||
console.log(this.props.layer.toJSON())
|
||||
let visibleIcon = <MdVisibilityOff />
|
||||
if(this.props.layer.has('layout') && this.props.layer.getIn(['layout', 'visibility']) === 'none') {
|
||||
visibleIcon = <MdVisibility />
|
||||
}
|
||||
return <div style={{
|
||||
padding: theme.scale[0],
|
||||
}}>
|
||||
<Toolbar>
|
||||
<NavItem style={{fontWeight: 400}}>
|
||||
{this.props.layer.get('id')}
|
||||
</NavItem>
|
||||
<Space auto x={1} />
|
||||
<NavItem onClick={this.toggleVisibility.bind(this)}>
|
||||
{visibleIcon}
|
||||
</NavItem>
|
||||
<NavItem onClick={(e) => this.props.onLayerDestroyed(this.props.layer)}>
|
||||
<MdDelete />
|
||||
</NavItem>
|
||||
</Toolbar>
|
||||
{propertyGroups}
|
||||
<FilterEditor
|
||||
filter={this.props.layer.get('filter', Immutable.List()).toJSON()}
|
||||
properties={this.props.vectorLayers.get(this.props.layer.get('source-layer'))}
|
||||
onChange={f => this.onFilterChange(Immutable.fromJS(f))}
|
||||
/>
|
||||
{this.props.layer.get('type') !== 'background'
|
||||
&& <SourceEditor
|
||||
source={this.props.layer.get('source')}
|
||||
sourceLayer={this.props.layer.get('source-layer')}
|
||||
sources={this.props.sources}
|
||||
onSourceChange={console.log}
|
||||
onSourceLayerChange={console.log}
|
||||
/>}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
93
src/components/layers/LayerList.jsx
Normal file
93
src/components/layers/LayerList.jsx
Normal file
@@ -0,0 +1,93 @@
|
||||
import React from 'react'
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||
import Immutable from 'immutable'
|
||||
|
||||
import Heading from 'rebass/dist/Heading'
|
||||
import Toolbar from 'rebass/dist/Toolbar'
|
||||
import NavItem from 'rebass/dist/NavItem'
|
||||
import Space from 'rebass/dist/Space'
|
||||
|
||||
import LayerListItem from './LayerListItem'
|
||||
import ScrollContainer from '../ScrollContainer'
|
||||
|
||||
import { margins } from '../../config/scales.js'
|
||||
|
||||
import {SortableContainer, SortableHandle, arrayMove} from 'react-sortable-hoc';
|
||||
|
||||
const layerListPropTypes = {
|
||||
layers: React.PropTypes.instanceOf(Immutable.OrderedMap),
|
||||
onLayersChanged: React.PropTypes.func.isRequired,
|
||||
onLayerSelected: React.PropTypes.func,
|
||||
}
|
||||
|
||||
// List of collapsible layer editors
|
||||
@SortableContainer
|
||||
class LayerListContainer extends React.Component {
|
||||
static propTypes = {...layerListPropTypes}
|
||||
static defaultProps = {
|
||||
onLayerSelected: () => {},
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this);
|
||||
}
|
||||
|
||||
onLayerDestroyed(deletedLayer) {
|
||||
const remainingLayers = this.props.layers.delete(deletedLayer.get('id'))
|
||||
this.props.onLayersChanged(remainingLayers)
|
||||
}
|
||||
|
||||
onLayerChanged(layer) {
|
||||
const changedLayers = this.props.layers.set(layer.get('id'), layer)
|
||||
this.props.onLayersChanged(changedLayers)
|
||||
}
|
||||
|
||||
render() {
|
||||
const layerPanels = this.props.layers.toIndexedSeq().map((layer, index) => {
|
||||
const layerId = layer.get('id')
|
||||
return <LayerListItem
|
||||
index={index}
|
||||
key={layerId}
|
||||
layerId={layerId}
|
||||
layerType={layer.get('type')}
|
||||
onLayerSelected={this.props.onLayerSelected}
|
||||
/>
|
||||
})
|
||||
return <ScrollContainer>
|
||||
<ul style={{ padding: margins[1], margin: 0 }}>
|
||||
{layerPanels}
|
||||
</ul>
|
||||
</ScrollContainer>
|
||||
}
|
||||
}
|
||||
|
||||
export default class LayerList extends React.Component {
|
||||
static propTypes = {...layerListPropTypes}
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this);
|
||||
}
|
||||
|
||||
onSortEnd(move) {
|
||||
const { oldIndex, newIndex } = move
|
||||
if(oldIndex === newIndex) return
|
||||
|
||||
//TODO: Implement this more performant for immutable collections
|
||||
// instead of converting back and forth
|
||||
let layers = this.props.layers.toArray()
|
||||
layers = arrayMove(layers, oldIndex, newIndex)
|
||||
layers = Immutable.OrderedMap(layers.map(l => [l.get('id'), l]))
|
||||
|
||||
this.props.onLayersChanged(layers)
|
||||
}
|
||||
|
||||
render() {
|
||||
return <LayerListContainer
|
||||
{...this.props}
|
||||
onSortEnd={this.onSortEnd.bind(this)}
|
||||
useDragHandle={true}
|
||||
/>
|
||||
}
|
||||
}
|
||||
76
src/components/layers/LayerListItem.jsx
Normal file
76
src/components/layers/LayerListItem.jsx
Normal file
@@ -0,0 +1,76 @@
|
||||
import React from 'react'
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin'
|
||||
import Radium from 'radium'
|
||||
import Immutable from 'immutable'
|
||||
import Color from 'color'
|
||||
|
||||
import Heading from 'rebass/dist/Heading'
|
||||
import Toolbar from 'rebass/dist/Toolbar'
|
||||
import NavItem from 'rebass/dist/NavItem'
|
||||
import Space from 'rebass/dist/Space'
|
||||
|
||||
import LayerIcon from '../icons/LayerIcon'
|
||||
import LayerEditor from './LayerEditor'
|
||||
import {SortableElement, SortableHandle} from 'react-sortable-hoc'
|
||||
|
||||
import colors from '../../config/colors.js'
|
||||
import { fontSizes, margins } from '../../config/scales.js'
|
||||
|
||||
|
||||
@SortableHandle
|
||||
class LayerTypeDragHandle extends React.Component {
|
||||
static propTypes = LayerIcon.propTypes
|
||||
|
||||
render() {
|
||||
return <LayerIcon
|
||||
{...this.props}
|
||||
style={{width: 15, height: 15, paddingRight: 3}}
|
||||
/>
|
||||
}
|
||||
}
|
||||
|
||||
@SortableElement
|
||||
@Radium
|
||||
class LayerListItem extends React.Component {
|
||||
static propTypes = {
|
||||
layerId: React.PropTypes.string.isRequired,
|
||||
layerType: React.PropTypes.string.isRequired,
|
||||
onLayerSelected: React.PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this);
|
||||
}
|
||||
|
||||
render() {
|
||||
return <li
|
||||
key={this.props.layerId}
|
||||
onClick={() => this.props.onLayerSelected(this.props.layerId)}
|
||||
style={{
|
||||
fontWeight: 400,
|
||||
color: colors.lowgray,
|
||||
fontSize: fontSizes[5],
|
||||
borderBottom: 1,
|
||||
borderLeft: 2,
|
||||
borderRight: 0,
|
||||
borderStyle: "solid",
|
||||
userSelect: 'none',
|
||||
listStyle: 'none',
|
||||
zIndex: 2000,
|
||||
cursor: 'pointer',
|
||||
position: 'relative',
|
||||
padding: margins[1],
|
||||
borderColor: Color(colors.gray).lighten(0.1).string(),
|
||||
backgroundColor: colors.gray,
|
||||
":hover": {
|
||||
backgroundColor: Color(colors.gray).lighten(0.15).string(),
|
||||
}
|
||||
}}>
|
||||
<LayerTypeDragHandle type={this.props.layerType} />
|
||||
<span>{this.props.layerId}</span>
|
||||
</li>
|
||||
}
|
||||
}
|
||||
|
||||
export default Radium(LayerListItem);
|
||||
61
src/components/layers/SourceEditor.jsx
Normal file
61
src/components/layers/SourceEditor.jsx
Normal file
@@ -0,0 +1,61 @@
|
||||
import React from 'react'
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||
import Immutable from 'immutable'
|
||||
|
||||
import PropertyGroup from '../fields/PropertyGroup'
|
||||
import input from '../../config/input.js'
|
||||
|
||||
/** Choose tileset (source) and the source layer */
|
||||
export default class SourceEditor extends React.Component {
|
||||
static propTypes = {
|
||||
source: React.PropTypes.string.isRequired,
|
||||
sourceLayer: React.PropTypes.string.isRequired,
|
||||
|
||||
onSourceChange: React.PropTypes.func.isRequired,
|
||||
onSourceLayerChange: React.PropTypes.func.isRequired,
|
||||
|
||||
/** List of available sources in the style
|
||||
* https://www.mapbox.com/mapbox-gl-style-spec/#root-sources */
|
||||
sources: React.PropTypes.instanceOf(Immutable.Map).isRequired,
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this);
|
||||
}
|
||||
|
||||
render() {
|
||||
const options = this.props.sources.map((source, sourceId)=> {
|
||||
return <option key={sourceId} value={sourceId}>{sourceId}</option>
|
||||
}).toIndexedSeq()
|
||||
|
||||
const layerOptions = this.props.sources.get(this.props.source, Immutable.Set()).map(vectorLayerId => {
|
||||
const id = vectorLayerId
|
||||
return <option key={id} value={id}>{id}</option>
|
||||
}).toIndexedSeq()
|
||||
|
||||
return <div>
|
||||
<div style={input.property}>
|
||||
<label style={input.label}>Source</label>
|
||||
<select
|
||||
style={input.select}
|
||||
value={this.props.source}
|
||||
onChange={(e) => this.onSourceChange(e.target.value)}
|
||||
>
|
||||
{options}
|
||||
</select>
|
||||
</div>
|
||||
<div style={input.property}>
|
||||
<label style={input.label}>Source Layer</label>
|
||||
<select
|
||||
style={input.select}
|
||||
value={this.props.sourceLayer}
|
||||
onChange={(e) => this.onSourceLayerChange(e.target.value)}
|
||||
>
|
||||
{layerOptions}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
}
|
||||
}
|
||||
26
src/components/map/Map.jsx
Normal file
26
src/components/map/Map.jsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import React from 'react'
|
||||
import Immutable from 'immutable'
|
||||
|
||||
export default class Map extends React.Component {
|
||||
static propTypes = {
|
||||
mapStyle: React.PropTypes.instanceOf(Immutable.Map).isRequired,
|
||||
accessToken: React.PropTypes.string,
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
//TODO: If we enable this React mixin for immutable comparison we can remove this?
|
||||
return nextProps.mapStyle !== this.props.mapStyle
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div
|
||||
ref={x => this.container = x}
|
||||
style={{
|
||||
position: "fixed",
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
}}></div>
|
||||
}
|
||||
}
|
||||
65
src/components/map/MapboxGlMap.jsx
Normal file
65
src/components/map/MapboxGlMap.jsx
Normal file
@@ -0,0 +1,65 @@
|
||||
import React from 'react'
|
||||
import MapboxGl from 'mapbox-gl'
|
||||
import validateColor from 'mapbox-gl-style-spec/lib/validate/validate_color'
|
||||
|
||||
import Map from './Map.jsx'
|
||||
import style from '../../libs/style.js'
|
||||
|
||||
export default class MapboxGlMap extends Map {
|
||||
static propTypes = {
|
||||
onMapLoaded: React.PropTypes.func,
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
onMapLoaded: () => {}
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = { map: null }
|
||||
}
|
||||
componentWillReceiveProps(nextProps) {
|
||||
const tokenChanged = nextProps.accessToken !== MapboxGl.accessToken
|
||||
|
||||
// If the id has changed a new style has been uplaoded and
|
||||
// it is safer to do a full new render
|
||||
// TODO: might already be handled in diff algorithm?
|
||||
const mapIdChanged = this.props.mapStyle.get('id') !== nextProps.mapStyle.get('id')
|
||||
|
||||
// TODO: If there is no map yet we need to apply the changes later?
|
||||
if(this.state.map) {
|
||||
if(mapIdChanged || tokenChanged) {
|
||||
this.state.map.setStyle(style.toJSON(nextProps.mapStyle))
|
||||
return
|
||||
}
|
||||
|
||||
style.diffStyles(this.props.mapStyle, nextProps.mapStyle).forEach(change => {
|
||||
|
||||
//TODO: Invalid outline color can cause map to freeze?
|
||||
if(change.command === "setPaintProperty" && change.args[1] === "fill-outline-color" ) {
|
||||
const value = change.args[2]
|
||||
if(validateColor({value}).length > 0) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
console.log(change.command, ...change.args)
|
||||
this.state.map[change.command].apply(this.state.map, change.args);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
MapboxGl.accessToken = this.props.accessToken
|
||||
|
||||
const map = new MapboxGl.Map({
|
||||
container: this.container,
|
||||
style: style.toJSON(this.props.mapStyle),
|
||||
});
|
||||
|
||||
map.on("style.load", (...args) => {
|
||||
this.props.onMapLoaded(map)
|
||||
this.setState({ map });
|
||||
});
|
||||
}
|
||||
}
|
||||
51
src/components/map/OpenLayers3Map.jsx
Normal file
51
src/components/map/OpenLayers3Map.jsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import React from 'react'
|
||||
import ol from 'openlayers'
|
||||
import olms from 'ol-mapbox-style'
|
||||
|
||||
import Map from './Map'
|
||||
import style from '../../libs/style.js'
|
||||
|
||||
export default class OpenLayers3Map extends Map {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
const tilegrid = ol.tilegrid.createXYZ({tileSize: 512, maxZoom: 22})
|
||||
this.resolutions = tilegrid.getResolutions()
|
||||
this.layer = new ol.layer.VectorTile({
|
||||
source: new ol.source.VectorTile({
|
||||
attributions: '© <a href="https://www.mapbox.com/map-feedback/">Mapbox</a> ' +
|
||||
'© <a href="http://www.openstreetmap.org/copyright">' +
|
||||
'OpenStreetMap contributors</a>',
|
||||
format: new ol.format.MVT(),
|
||||
tileGrid: tilegrid,
|
||||
tilePixelRatio: 8,
|
||||
url: 'http://osm2vectortiles-0.tileserver.com/v2/{z}/{x}/{y}.pbf'
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
const jsonStyle = style.toJSON(nextProps.mapStyle)
|
||||
const styleFunc = olms.getStyleFunction(jsonStyle, 'mapbox', this.resolutions)
|
||||
this.layer.setStyle(styleFunc)
|
||||
this.state.map.render()
|
||||
}
|
||||
|
||||
|
||||
componentDidMount() {
|
||||
const jsonStyle = style.toJSON(this.props.mapStyle)
|
||||
const styleFunc = olms.getStyleFunction(jsonStyle, 'mapbox', this.resolutions)
|
||||
this.layer.setStyle(styleFunc)
|
||||
|
||||
const map = new ol.Map({
|
||||
target: this.container,
|
||||
layers: [this.layer],
|
||||
view: new ol.View({
|
||||
center: jsonStyle.center,
|
||||
zoom: jsonStyle.zoom,
|
||||
})
|
||||
})
|
||||
map.addControl(new ol.control.Zoom());
|
||||
this.setState({ map });
|
||||
}
|
||||
}
|
||||
96
src/components/modals/SettingsModal.jsx
Normal file
96
src/components/modals/SettingsModal.jsx
Normal file
@@ -0,0 +1,96 @@
|
||||
import React from 'react'
|
||||
import Immutable from 'immutable'
|
||||
|
||||
import Select from 'rebass/dist/Select'
|
||||
import Overlay from 'rebass/dist/Overlay'
|
||||
import Panel from 'rebass/dist/Panel'
|
||||
import PanelHeader from 'rebass/dist/PanelHeader'
|
||||
import PanelFooter from 'rebass/dist/PanelFooter'
|
||||
import Button from 'rebass/dist/Button'
|
||||
import Text from 'rebass/dist/Text'
|
||||
import Media from 'rebass/dist/Media'
|
||||
import Close from 'rebass/dist/Close'
|
||||
import Space from 'rebass/dist/Space'
|
||||
import Input from 'rebass/dist/Input'
|
||||
|
||||
class SettingsModal extends React.Component {
|
||||
static propTypes = {
|
||||
mapStyle: React.PropTypes.instanceOf(Immutable.Map).isRequired,
|
||||
onStyleChanged: React.PropTypes.func.isRequired,
|
||||
open: React.PropTypes.bool.isRequired,
|
||||
toggle: React.PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
onChange(property, e) {
|
||||
const changedStyle = this.props.mapStyle.set(property, e.target.value)
|
||||
this.props.onStyleChanged(changedStyle)
|
||||
}
|
||||
|
||||
onRendererChange(e) {
|
||||
const changedStyle = this.props.mapStyle.setIn(['metadata', 'maputnik:renderer'], e.target.value)
|
||||
this.props.onStyleChanged(changedStyle)
|
||||
}
|
||||
|
||||
render() {
|
||||
return <Overlay open={this.props.open} >
|
||||
<Panel theme={'secondary'}>
|
||||
<PanelHeader theme={'default'}>
|
||||
Style Settings
|
||||
<Space auto />
|
||||
<Close onClick={this.props.toggle('modalOpen')} />
|
||||
</PanelHeader>
|
||||
<br />
|
||||
<Input
|
||||
name="name"
|
||||
label="Name"
|
||||
value={this.props.mapStyle.get('name')}
|
||||
onChange={this.onChange.bind(this, "name")}
|
||||
/>
|
||||
<Input
|
||||
name="owner"
|
||||
label="Owner"
|
||||
value={this.props.mapStyle.get('owner')}
|
||||
onChange={this.onChange.bind(this, "owner")}
|
||||
/>
|
||||
<Input
|
||||
name="sprite"
|
||||
label="Sprite URL"
|
||||
value={this.props.mapStyle.get('sprite')}
|
||||
onChange={this.onChange.bind(this, "sprite")}
|
||||
/>
|
||||
<Input
|
||||
name="glyphs"
|
||||
label="Glyphs URL"
|
||||
value={this.props.mapStyle.get('glyphs')}
|
||||
onChange={this.onChange.bind(this, "glyphs")}
|
||||
/>
|
||||
<Input
|
||||
name="glyphs"
|
||||
label="Glyphs URL"
|
||||
value={this.props.mapStyle.get('glyphs')}
|
||||
onChange={this.onChange.bind(this, "glyphs")}
|
||||
/>
|
||||
<Select
|
||||
label="Style Renderer"
|
||||
name="renderer"
|
||||
onChange={this.onRendererChange.bind(this)}
|
||||
options={[{children: 'Mapbox GL JS', value: 'mbgljs'}, {children: 'Open Layers 3', value: 'ol3'}]}
|
||||
/>
|
||||
|
||||
<PanelFooter>
|
||||
<Space auto />
|
||||
<Button theme={'default'}
|
||||
onClick={this.props.toggle('modalOpen')}
|
||||
children='Close!'
|
||||
/>
|
||||
</PanelFooter>
|
||||
</Panel>
|
||||
</Overlay>
|
||||
}
|
||||
}
|
||||
|
||||
export default SettingsModal
|
||||
82
src/components/modals/TilesetsModal.jsx
Normal file
82
src/components/modals/TilesetsModal.jsx
Normal file
@@ -0,0 +1,82 @@
|
||||
import React from 'react'
|
||||
import Immutable from 'immutable'
|
||||
|
||||
import Overlay from 'rebass/dist/Overlay'
|
||||
import Panel from 'rebass/dist/Panel'
|
||||
import PanelHeader from 'rebass/dist/PanelHeader'
|
||||
import PanelFooter from 'rebass/dist/PanelFooter'
|
||||
import Button from 'rebass/dist/Button'
|
||||
import Text from 'rebass/dist/Text'
|
||||
import Media from 'rebass/dist/Media'
|
||||
import Close from 'rebass/dist/Close'
|
||||
import Space from 'rebass/dist/Space'
|
||||
import Input from 'rebass/dist/Input'
|
||||
import Toolbar from 'rebass/dist/Toolbar'
|
||||
import NavItem from 'rebass/dist/NavItem'
|
||||
|
||||
import publicTilesets from '../../config/tilesets.json'
|
||||
import theme from '../../config/rebass'
|
||||
|
||||
class TilesetsModal extends React.Component {
|
||||
static propTypes = {
|
||||
mapStyle: React.PropTypes.instanceOf(Immutable.Map).isRequired,
|
||||
onStyleChanged: React.PropTypes.func.isRequired,
|
||||
open: React.PropTypes.bool.isRequired,
|
||||
toggle: React.PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
onChange(property, e) {
|
||||
const changedStyle = this.props.mapStyle.set(property, e.target.value)
|
||||
this.props.onStyleChanged(changedStyle)
|
||||
}
|
||||
|
||||
render() {
|
||||
const tilesetOptions = publicTilesets.map(tileset => {
|
||||
return <div key={tileset.id} style={{
|
||||
padding: theme.scale[0],
|
||||
borderBottom: 1,
|
||||
borderTop: 1,
|
||||
borderLeft: 2,
|
||||
borderRight: 0,
|
||||
borderStyle: "solid",
|
||||
borderColor: theme.borderColor,
|
||||
}}>
|
||||
<Toolbar>
|
||||
<NavItem style={{fontWeight: 400}}>
|
||||
#{tileset.id}
|
||||
</NavItem>
|
||||
<Space auto x={1} />
|
||||
</Toolbar>
|
||||
{tileset.url}
|
||||
</div>
|
||||
})
|
||||
|
||||
return <Overlay open={this.props.open} >
|
||||
<Panel theme={'secondary'}>
|
||||
<PanelHeader theme={'default'}>
|
||||
Tilesets
|
||||
<Space auto />
|
||||
<Close onClick={this.props.toggle('modalOpen')} />
|
||||
</PanelHeader>
|
||||
<br />
|
||||
|
||||
<h2>Choose Public Tileset</h2>
|
||||
{tilesetOptions}
|
||||
|
||||
<PanelFooter>
|
||||
<Space auto />
|
||||
<Button theme={'default'}
|
||||
onClick={this.props.toggle('modalOpen')}
|
||||
children='Close!'
|
||||
/>
|
||||
</PanelFooter>
|
||||
</Panel>
|
||||
</Overlay>
|
||||
}
|
||||
}
|
||||
|
||||
export default TilesetsModal
|
||||
12
src/components/scrollbars.scss
Normal file
12
src/components/scrollbars.scss
Normal file
@@ -0,0 +1,12 @@
|
||||
.darkScrollbar::-webkit-scrollbar {
|
||||
background-color: #26282e;
|
||||
width: 5px;
|
||||
}
|
||||
|
||||
.darkScrollbar::-webkit-scrollbar-thumb {
|
||||
border-radius: 6px;
|
||||
-webkit-box-shadow: inset 0 0 6px rgba(0,0,0,.3);
|
||||
background-color: #40444e;
|
||||
padding-left: 2px;
|
||||
padding-right: 2px;
|
||||
}
|
||||
98
src/components/sources/editor.jsx
Normal file
98
src/components/sources/editor.jsx
Normal file
@@ -0,0 +1,98 @@
|
||||
import React from 'react'
|
||||
import Immutable from 'immutable'
|
||||
|
||||
import Input from 'rebass/dist/Input'
|
||||
import Toolbar from 'rebass/dist/Toolbar'
|
||||
import NavItem from 'rebass/dist/NavItem'
|
||||
import Space from 'rebass/dist/Space'
|
||||
|
||||
import Collapse from 'react-collapse'
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||
|
||||
import theme from '../theme.js'
|
||||
|
||||
|
||||
class UnsupportedSource extends React.Component {
|
||||
render() {
|
||||
return <div></div>
|
||||
}
|
||||
}
|
||||
|
||||
class VectorSource extends React.Component {
|
||||
static propTypes = {
|
||||
source: React.PropTypes.instanceOf(Immutable.Map).isRequired,
|
||||
onSourceChanged: React.PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this);
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div>
|
||||
<Input
|
||||
onChange={e => this.props.onSourceChanged(this.props.source.set('url', e.target.value))}
|
||||
name="url" label="TileJSON url"
|
||||
value={this.props.source.get("url")}
|
||||
/>
|
||||
<Input name="minzoom" label="Minimum zoom level" value={this.props.source.get("minzoom")} />
|
||||
<Input name="maxzoom" label="Maximum zoom level" value={this.props.source.get("maxzoom")} />
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
export class SourceEditor extends React.Component {
|
||||
static propTypes = {
|
||||
sourceId: React.PropTypes.string.isRequired,
|
||||
source: React.PropTypes.instanceOf(Immutable.Map).isRequired,
|
||||
onSourceChanged: React.PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this);
|
||||
this.state = {
|
||||
isOpened: false,
|
||||
}
|
||||
}
|
||||
|
||||
toggleLayer() {
|
||||
this.setState({isOpened: !this.state.isOpened})
|
||||
}
|
||||
|
||||
sourceFromType(type) {
|
||||
if (type === "vector") {
|
||||
return <VectorSource
|
||||
onSourceChanged={s => this.props.onSourceChanged(this.props.sourceId, s)}
|
||||
source={this.props.source}
|
||||
/>
|
||||
}
|
||||
return <UnsupportedSource />
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div style={{
|
||||
padding: theme.scale[0],
|
||||
borderBottom: 1,
|
||||
borderTop: 1,
|
||||
borderLeft: 2,
|
||||
borderRight: 0,
|
||||
borderStyle: "solid",
|
||||
borderColor: theme.borderColor,
|
||||
}}>
|
||||
<Toolbar onClick={this.toggleLayer.bind(this)}>
|
||||
<NavItem style={{fontWeight: 400}}>
|
||||
#{this.props.sourceId}
|
||||
</NavItem>
|
||||
<Space auto x={1} />
|
||||
</Toolbar>
|
||||
<Collapse isOpened={this.state.isOpened}>
|
||||
<div style={{padding: theme.scale[2], paddingRight: 0, backgroundColor: theme.colors.black}}>
|
||||
{this.sourceFromType(this.props.source.get('type'))}
|
||||
</div>
|
||||
</Collapse>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
50
src/components/sources/list.jsx
Normal file
50
src/components/sources/list.jsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import React from 'react'
|
||||
import Immutable from 'immutable'
|
||||
|
||||
import Heading from 'rebass/dist/Heading'
|
||||
import Toolbar from 'rebass/dist/Toolbar'
|
||||
import NavItem from 'rebass/dist/NavItem'
|
||||
import Space from 'rebass/dist/Space'
|
||||
|
||||
import { SourceEditor } from './editor.jsx'
|
||||
import scrollbars from '../scrollbars.scss'
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||
|
||||
// List of collapsible layer editors
|
||||
export class SourceList extends React.Component {
|
||||
static propTypes = {
|
||||
sources: React.PropTypes.instanceOf(Immutable.Map).isRequired,
|
||||
onSourcesChanged: React.PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this);
|
||||
}
|
||||
|
||||
onSourceChanged(sourceId, changedSource) {
|
||||
const changedSources = this.props.sources.set(sourceId, changedSource)
|
||||
this.props.onSourcesChanged(changedSources)
|
||||
}
|
||||
|
||||
render() {
|
||||
const sourceEditors = this.props.sources.map((source, sourceId) => {
|
||||
return <SourceEditor
|
||||
key={sourceId}
|
||||
sourceId={sourceId}
|
||||
source={source}
|
||||
onSourceChanged={this.onSourceChanged.bind(this)}
|
||||
/>
|
||||
}).toIndexedSeq()
|
||||
|
||||
return <div>
|
||||
<Toolbar style={{marginRight: 20}}>
|
||||
<NavItem>
|
||||
<Heading>Sources</Heading>
|
||||
</NavItem>
|
||||
<Space auto x={1} />
|
||||
</Toolbar>
|
||||
{sourceEditors}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user