Pass everything down as props

This commit is contained in:
lukasmartinelli 2016-09-09 23:25:06 +02:00
parent 416cf7e0af
commit b6dff4aa58
8 changed files with 214 additions and 167 deletions

View file

@ -5,6 +5,7 @@ import { Drawer, Container, Block, Fixed } from 'rebass'
import {Map} from './map.jsx' import {Map} from './map.jsx'
import {Toolbar} from './toolbar.jsx' import {Toolbar} from './toolbar.jsx'
import { StyleManager } from './style.js' import { StyleManager } from './style.js'
import { StyleStore } from './stylestore.js'
import { WorkspaceDrawer } from './workspace.jsx' import { WorkspaceDrawer } from './workspace.jsx'
import theme from './theme.js' import theme from './theme.js'
@ -20,40 +21,38 @@ export default class App extends React.Component {
constructor(props) { constructor(props) {
super(props) super(props)
this.state = { this.state = {
styleManager: new StyleManager(), styleStore: new StyleStore(),
workContext: "layers", workContext: "layers",
} }
} }
onStyleDownload() {
const mapStyle = this.state.styleManager.exportStyle()
const blob = new Blob([mapStyle], {type: "application/json;charset=utf-8"});
saveAs(blob, "glstyle.json");
}
onStyleUpload(newStyle) {
this.setState({ styleManager: new StyleManager(newStyle) })
}
onOpenSettings() {
this.setState({
workContext: "settings",
})
}
onOpenLayers() {
this.setState({
workContext: "layers",
})
}
getChildContext() { getChildContext() {
return { return {
rebass: theme, rebass: theme,
reactIconBase: { reactIconBase: { size: 20 }
size: 20,
} }
} }
onStyleDownload() {
const mapStyle = JSON.stringify(this.state.styleStore.currentStyle, null, 4)
const blob = new Blob([mapStyle], {type: "application/json;charset=utf-8"});
saveAs(blob, mapStyle.id + ".json");
}
onStyleUpload(newStyle) {
this.setState({ styleStore: new StyleStore(newStyle) })
}
onStyleChanged(newStyle) {
this.setState({ styleStore: new StyleStore(newStyle) })
}
onOpenSettings() {
this.setState({ workContext: "settings", })
}
onOpenLayers() {
this.setState({ workContext: "layers", })
} }
render() { render() {
@ -64,9 +63,13 @@ export default class App extends React.Component {
onOpenSettings={this.onOpenSettings.bind(this)} onOpenSettings={this.onOpenSettings.bind(this)}
onOpenLayers={this.onOpenLayers.bind(this)} onOpenLayers={this.onOpenLayers.bind(this)}
/> />
<WorkspaceDrawer workContext={this.state.workContext} styleManager={this.state.styleManager}/> <WorkspaceDrawer
onStyleChanged={this.onStyleChanged.bind(this)}
workContext={this.state.workContext}
mapStyle={this.state.styleStore.currentStyle}
/>
<div className={layout.map}> <div className={layout.map}>
<Map styleManager={this.state.styleManager} /> <Map mapStyle={this.state.styleStore.currentStyle} />
</div> </div>
</div> </div>
} }

View file

@ -79,8 +79,8 @@ export class NoLayer extends React.Component {
export class LayerPanel extends React.Component { export class LayerPanel extends React.Component {
static propTypes = { static propTypes = {
layer: React.PropTypes.object.isRequired, layer: React.PropTypes.object.isRequired,
styleManager: React.PropTypes.object.isRequired, onLayerChanged: React.PropTypes.func.isRequired,
destroyLayer: React.PropTypes.func.isRequired, onLayerDestroyed: React.PropTypes.func.isRequired,
} }
static childContextTypes = { static childContextTypes = {
@ -91,10 +91,6 @@ export class LayerPanel extends React.Component {
super(props); super(props);
this.state = { this.state = {
isOpened: false, isOpened: false,
//TODO: Is that bad practice?
//however I want to keep the layer state local herere
//otherwise the style always would, propagate around?
layer: this.props.layer
} }
} }
@ -108,25 +104,15 @@ export class LayerPanel extends React.Component {
} }
onPaintChanged(property, newValue) { onPaintChanged(property, newValue) {
let layer = this.state.layer const layer = this.props.layer
layer.paint[property] = newValue; layer.paint[property] = newValue;
this.props.onLayerChanged(layer)
this.props.styleManager.changeStyle({
command: 'setPaintProperty',
args: [layer.id, property, newValue]
})
this.setState({ layer });
} }
onLayoutChanged(property, newValue) { onLayoutChanged(property, newValue) {
let layer = this.state.layer const layer = this.props.layer
layer.layout[property] = newValue; layer.layout[property] = newValue;
this.props.styleManager.changeStyle({ this.props.onLayerChanged(layer)
command: 'setLayoutProperty',
args: [layer.id, property, newValue]
})
this.setState({ layer });
} }
toggleLayer() { toggleLayer() {
@ -135,11 +121,17 @@ export class LayerPanel extends React.Component {
layerFromType(type) { layerFromType(type) {
if (type === "fill") { if (type === "fill") {
return <FillLayer layer={this.state.layer} onPaintChanged={this.onPaintChanged.bind(this)} /> return <FillLayer
layer={this.props.layer}
onPaintChanged={this.onPaintChanged.bind(this)}
/>
} }
if (type === "background") { if (type === "background") {
return <BackgroundLayer layer={this.state.layer} onPaintChanged={this.onPaintChanged.bind(this)} /> return <BackgroundLayer
layer={this.props.layer}
onPaintChanged={this.onPaintChanged.bind(this)}
/>
} }
if (type === "line") { if (type === "line") {
@ -149,12 +141,12 @@ export class LayerPanel extends React.Component {
if (type === "symbol") { if (type === "symbol") {
return <SymbolLayer /> return <SymbolLayer />
} }
return <NoLayer /> return <NoLayer />
} }
toggleVisibility() { toggleVisibility() {
console.log(this.state.layer) if(this.props.layer.layout.visibility === 'none') {
if(this.state.layer.layout.visibility === 'none') {
this.onLayoutChanged('visibility', 'visible') this.onLayoutChanged('visibility', 'visible')
} else { } else {
this.onLayoutChanged('visibility', 'none') this.onLayoutChanged('visibility', 'none')
@ -163,7 +155,7 @@ export class LayerPanel extends React.Component {
render() { render() {
let visibleIcon = <MdVisibilityOff /> let visibleIcon = <MdVisibilityOff />
if(this.state.layer.layout && this.state.layer.layout.visibility === 'none') { if(this.props.layer.layout && this.props.layer.layout.visibility === 'none') {
visibleIcon = <MdVisibility /> visibleIcon = <MdVisibility />
} }
@ -175,23 +167,23 @@ export class LayerPanel extends React.Component {
borderRight: 0, borderRight: 0,
borderStyle: "solid", borderStyle: "solid",
borderColor: theme.borderColor, borderColor: theme.borderColor,
borderLeftColor: this.state.layer.metadata["mapolo:color"], borderLeftColor: this.props.layer.metadata["mapolo:color"],
}}> }}>
<Toolbar onClick={this.toggleLayer.bind(this)}> <Toolbar onClick={this.toggleLayer.bind(this)}>
<NavItem style={{fontWeight: 400}}> <NavItem style={{fontWeight: 400}}>
#{this.state.layer.id} #{this.props.layer.id}
</NavItem> </NavItem>
<Space auto x={1} /> <Space auto x={1} />
<NavItem onClick={this.toggleVisibility.bind(this)}> <NavItem onClick={this.toggleVisibility.bind(this)}>
{visibleIcon} {visibleIcon}
</NavItem> </NavItem>
<NavItem onClick={(e) => this.props.destroyLayer(this.state.layer.id)}> <NavItem onClick={(e) => this.props.onLayerDestroyed(this.props.layer)}>
<MdDelete /> <MdDelete />
</NavItem> </NavItem>
</Toolbar> </Toolbar>
<Collapse isOpened={this.state.isOpened}> <Collapse isOpened={this.state.isOpened}>
<div style={{padding: theme.scale[2], paddingRight: 0, backgroundColor: theme.colors.black}}> <div style={{padding: theme.scale[2], paddingRight: 0, backgroundColor: theme.colors.black}}>
{this.layerFromType(this.state.layer.type)} {this.layerFromType(this.props.layer.type)}
</div> </div>
</Collapse> </Collapse>
</div> </div>
@ -200,24 +192,47 @@ export class LayerPanel extends React.Component {
export class LayerEditor extends React.Component { export class LayerEditor extends React.Component {
static propTypes = { static propTypes = {
styleManager: React.PropTypes.object.isRequired layers: React.PropTypes.array.isRequired,
onLayersChanged: React.PropTypes.func.isRequired
} }
destroyLayer(layerId) { constructor(props) {
this.props.styleManager.changeStyle({ super(props)
command: 'removeLayer', }
args: [layerId]
}) onLayerDestroyed(deletedLayer) {
let deleteIdx = -1
for (let i = 0; i < this.props.layers.length; i++) {
if(this.props.layers[i].id == deletedLayer.id) {
deleteIdx = i
}
}
const remainingLayers = this.props.layers
const removedLayers = remainingLayers.splice(deleteIdx, 1)
this.props.onLayersChanged(remainingLayers)
}
onLayerChanged(changedLayer) {
let changedIdx = -1
for (let i = 0; i < this.props.layers.length; i++) {
if(this.props.layers[i].id == changedLayer.id) {
changedIdx = i
}
}
const changedLayers = this.props.layers
changedLayers[changedIdx] = changedLayer
this.props.onLayersChanged(changedLayers)
} }
render() { render() {
const layers = this.props.styleManager.layers() const layerPanels = this.props.layers.map(layer => {
const layerPanels = layers.map(layer => {
return <LayerPanel return <LayerPanel
key={layer.id} key={layer.id}
layer={layer} layer={layer}
destroyLayer={this.destroyLayer.bind(this)} onLayerDestroyed={this.onLayerDestroyed.bind(this)}
styleManager={this.props.styleManager} onLayerChanged={this.onLayerChanged.bind(this)}
/> />
}); });

View file

@ -5,32 +5,14 @@ import theme from './theme.js'
export class Map extends React.Component { export class Map extends React.Component {
static propTypes = { static propTypes = {
styleManager: React.PropTypes.object.isRequired mapStyle: React.PropTypes.object.isRequired
}
constructor(props) {
super(props)
this.map = null
}
onStyleChange(change) {
this.map[change.command].apply(this.map, change.args);
}
onMapLoaded(map) {
this.map = map;
this.props.styleManager.onStyleChange(this.onStyleChange.bind(this))
} }
render() { render() {
if (this.props.styleManager.mapStyle) {
return <ReactMapboxGl return <ReactMapboxGl
onStyleLoad={this.onMapLoaded.bind(this)} style={this.props.mapStyle}
style={this.props.styleManager.mapStyle}
accessToken="pk.eyJ1IjoibW9yZ2Vua2FmZmVlIiwiYSI6IjIzcmN0NlkifQ.0LRTNgCc-envt9d5MzR75w"> accessToken="pk.eyJ1IjoibW9yZ2Vua2FmZmVlIiwiYSI6IjIzcmN0NlkifQ.0LRTNgCc-envt9d5MzR75w">
<ZoomControl/> <ZoomControl/>
</ReactMapboxGl> </ReactMapboxGl>
} }
return <div style={{backgroundColor: theme.colors.black}}/>
}
} }

View file

@ -5,7 +5,7 @@ import { Heading, Container, Input, Toolbar, NavItem, Space } from 'rebass'
/** Edit global settings within a style such as the name */ /** Edit global settings within a style such as the name */
export class SettingsEditor extends React.Component { export class SettingsEditor extends React.Component {
static propTypes = { static propTypes = {
styleManager: React.PropTypes.object.isRequired mapStyle: React.PropTypes.object.isRequired
} }
constructor(props) { constructor(props) {

View file

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import randomColor from 'randomcolor' import randomColor from 'randomcolor'
function assignColorsToLayers(layers) { export function colorizeLayers(layers) {
return layers.map(layer => { return layers.map(layer => {
if(!layer.metadata) { if(!layer.metadata) {
layer.metadata = {} layer.metadata = {}
@ -12,69 +12,3 @@ function assignColorsToLayers(layers) {
return layer return layer
}) })
} }
// A wrapper around Mapbox GL style to publish
// and subscribe to map changes
export class StyleManager {
constructor(mapStyle) {
this.commandHistory = [];
this.subscribers = [];
this.mapStyle = mapStyle;
if(this.mapStyle) {
this.mapStyle.layers = assignColorsToLayers(this.mapStyle.layers)
}
}
onStyleChange(cb) {
this.subscribers.push(cb);
}
changeStyle(change) {
this.commandHistory.push(change)
this.subscribers.forEach(f => f(change))
console.log(change)
}
exportStyle() {
return JSON.stringify(this.mapStyle, null, 4)
}
settings() {
const { name, sprite, glyphs, owner } = this.mapStyle
return { name, sprite, glyphs, owner }
}
set name(val) {
this.mapStyle.name = val
}
set owner(val) {
this.mapStyle.owner = val
}
set glyphs(val) {
this.mapStyle.glyphs = val
this.changeStyle({
command: 'setStyle',
args: [this.mapStyle]
})
}
set sprite(val) {
this.mapStyle.sprite = val
this.changeStyle({
command: 'setStyle',
args: [this.mapStyle]
})
}
layer(layerId) {
return this.mapStyle.layers[layerId]
}
layers() {
if(this.mapStyle) return this.mapStyle.layers
return []
}
}

100
src/stylestore.js Normal file
View file

@ -0,0 +1,100 @@
import { colorizeLayers } from './style.js'
const emptyStyle = {
version: 8,
sources: {},
layers: []
}
// Manages many possible styles that are stored in the local storage
export class StyleStore {
// By default the style store will use the last edited style
// as current working style if no explicit style is set
constructor(mapStyle) {
if(mapStyle) {
this.load(mapStyle)
} else {
try {
const latestStyle = this.latestStyle()
console.log("Loading latest stlye " + latestStyle.id + " from " + latestStyle.modified)
this.load(latestStyle)
} catch(err) {
console.log(err)
this.load(emptyStyle)
}
}
}
// Find the last edited style
latestStyle() {
const styles = this.loadStoredStyles()
if(styles.length == 0) {
throw "No existing style found"
}
let maxStyle = styles[0]
styles.forEach(s => {
if(s.date > maxStyle.date) {
maxStyle = s
}
})
return JSON.parse(window.localStorage.getItem(this.styleKey(maxStyle.styleId, maxStyle.date)))
}
// Return style ids and dates of all styles stored in local storage
loadStoredStyles() {
const styles = []
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i)
if(this.isStyleKey(key)) {
styles.push(this.fromKey(key))
}
}
return styles
}
isStyleKey(key) {
const parts = key.split(":")
return parts.length >= 3 && parts[0] === "mapolo"
}
// Load style from local storage by key
fromKey(key) {
if(!this.isStyleKey(key)) {
throw "Key is not a valid style key"
}
const parts = key.split(":")
const styleId = parts[1]
const date = new Date(parts.slice(2).join(":"))
return {styleId, date}
}
// Calculate key that identifies the style with a version
styleKey(styleId, modifiedDate) {
return ["mapolo", styleId, modifiedDate.toJSON()].join(":")
}
// Take snapshot of current style and load it
backup(mapStyle) {
mapStyle.modified = new Date()
const key = this.styleKey(mapStyle.id, mapStyle.modified)
window.localStorage.setItem(key, JSON.stringify(mapStyle))
}
// Load a style from external into the store
// replacing the previous version
load(mapStyle) {
if(!("id" in mapStyle)) {
mapStyle.id = Math.random().toString(36).substr(2, 9)
}
if(!("created" in mapStyle)) {
mapStyle.created = new Date()
}
mapStyle.layers = colorizeLayers(mapStyle.layers)
this.backup(mapStyle)
this.currentStyle = mapStyle
}
}

View file

@ -3,23 +3,36 @@ import { LayerEditor } from './layers.jsx'
import { SettingsEditor } from './settings.jsx' import { SettingsEditor } from './settings.jsx'
import theme from './theme.js' import theme from './theme.js'
/** The workspace drawer contains the editor components depending on the context /** The workspace drawer contains the editor components depending on the edit
* chosen in the toolbar. */ * context chosen in the toolbar. It holds the state of the layers.*/
export class WorkspaceDrawer extends React.Component { export class WorkspaceDrawer extends React.Component {
static propTypes = { static propTypes = {
mapStyle: React.PropTypes.object.isRequired,
onStyleChanged: React.PropTypes.func.isRequired,
workContext: React.PropTypes.oneOf(['layers', 'settings']).isRequired, workContext: React.PropTypes.oneOf(['layers', 'settings']).isRequired,
styleManager: React.PropTypes.object.isRequired }
onLayersChanged(layers) {
const changedStyle = this.props.mapStyle
changedStyle.layers = layers
this.props.onStyleChanged(changedStyle)
} }
render() { render() {
let workspaceContent = null let workspaceContent = null
if(this.props.workContext === "layers" && this.props.styleManager.mapStyle) { if(this.props.workContext === "layers") {
workspaceContent = <LayerEditor styleManager={this.props.styleManager}/> workspaceContent = <LayerEditor
onLayersChanged={this.onLayersChanged.bind(this)}
layers={this.props.mapStyle.layers}
/>
} }
if(this.props.workContext === "settings" && this.props.styleManager.mapStyle) { if(this.props.workContext === "settings") {
workspaceContent = <SettingsEditor styleManager={this.props.styleManager}/> workspaceContent = <SettingsEditor
onStyleChanged={this.props.onStyleChanged}
mapStyle={this.props.mapStyle}
/>
} }
return <div style={{ return <div style={{

View file

@ -58,7 +58,7 @@ module.exports = {
}), }),
new HtmlWebpackPlugin({ new HtmlWebpackPlugin({
template: './src/template.html', template: './src/template.html',
title: 'Webpack App' title: 'Mapolo'
}), }),
new webpack.optimize.DedupePlugin() new webpack.optimize.DedupePlugin()
] ]