Restructure and rename components

This commit is contained in:
Lukas Martinelli
2016-12-20 11:44:22 +01:00
parent 461a001552
commit fde60ac3e0
46 changed files with 365 additions and 425 deletions

41
src/components/About.jsx Normal file
View 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
View 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>
}
}

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

View 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

View 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

View 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

View 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

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

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

View 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

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

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

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

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

View 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

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

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

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

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

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

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

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

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

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

View 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

View 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

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

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

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