diff --git a/src/app.jsx b/src/app.jsx index 48067a6..8519c3f 100644 --- a/src/app.jsx +++ b/src/app.jsx @@ -5,6 +5,7 @@ import { Drawer, Container, Block, Fixed } from 'rebass' import {Map} from './map.jsx' import {Toolbar} from './toolbar.jsx' import { StyleManager } from './style.js' +import { StyleStore } from './stylestore.js' import { WorkspaceDrawer } from './workspace.jsx' import theme from './theme.js' @@ -20,42 +21,40 @@ export default class App extends React.Component { constructor(props) { super(props) this.state = { - styleManager: new StyleManager(), + styleStore: new StyleStore(), 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() { return { rebass: theme, - reactIconBase: { - size: 20, - } + reactIconBase: { 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() { return
- +
- +
} diff --git a/src/layers.jsx b/src/layers.jsx index aa07d3e..7e5e47e 100644 --- a/src/layers.jsx +++ b/src/layers.jsx @@ -79,8 +79,8 @@ export class NoLayer extends React.Component { export class LayerPanel extends React.Component { static propTypes = { layer: React.PropTypes.object.isRequired, - styleManager: React.PropTypes.object.isRequired, - destroyLayer: React.PropTypes.func.isRequired, + onLayerChanged: React.PropTypes.func.isRequired, + onLayerDestroyed: React.PropTypes.func.isRequired, } static childContextTypes = { @@ -91,10 +91,6 @@ export class LayerPanel extends React.Component { super(props); this.state = { 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) { - let layer = this.state.layer + const layer = this.props.layer layer.paint[property] = newValue; - - this.props.styleManager.changeStyle({ - command: 'setPaintProperty', - args: [layer.id, property, newValue] - }) - - this.setState({ layer }); + this.props.onLayerChanged(layer) } onLayoutChanged(property, newValue) { - let layer = this.state.layer + const layer = this.props.layer layer.layout[property] = newValue; - this.props.styleManager.changeStyle({ - command: 'setLayoutProperty', - args: [layer.id, property, newValue] - }) - this.setState({ layer }); + this.props.onLayerChanged(layer) } toggleLayer() { @@ -135,11 +121,17 @@ export class LayerPanel extends React.Component { layerFromType(type) { if (type === "fill") { - return + return } if (type === "background") { - return + return } if (type === "line") { @@ -149,12 +141,12 @@ export class LayerPanel extends React.Component { if (type === "symbol") { return } + return } toggleVisibility() { - console.log(this.state.layer) - if(this.state.layer.layout.visibility === 'none') { + if(this.props.layer.layout.visibility === 'none') { this.onLayoutChanged('visibility', 'visible') } else { this.onLayoutChanged('visibility', 'none') @@ -163,7 +155,7 @@ export class LayerPanel extends React.Component { render() { let visibleIcon = - if(this.state.layer.layout && this.state.layer.layout.visibility === 'none') { + if(this.props.layer.layout && this.props.layer.layout.visibility === 'none') { visibleIcon = } @@ -175,23 +167,23 @@ export class LayerPanel extends React.Component { borderRight: 0, borderStyle: "solid", borderColor: theme.borderColor, - borderLeftColor: this.state.layer.metadata["mapolo:color"], + borderLeftColor: this.props.layer.metadata["mapolo:color"], }}> - #{this.state.layer.id} + #{this.props.layer.id} {visibleIcon} - this.props.destroyLayer(this.state.layer.id)}> + this.props.onLayerDestroyed(this.props.layer)}>
- {this.layerFromType(this.state.layer.type)} + {this.layerFromType(this.props.layer.type)}
@@ -200,24 +192,47 @@ export class LayerPanel extends React.Component { export class LayerEditor extends React.Component { static propTypes = { - styleManager: React.PropTypes.object.isRequired + layers: React.PropTypes.array.isRequired, + onLayersChanged: React.PropTypes.func.isRequired } - destroyLayer(layerId) { - this.props.styleManager.changeStyle({ - command: 'removeLayer', - args: [layerId] - }) + constructor(props) { + super(props) + } + + 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() { - const layers = this.props.styleManager.layers() - const layerPanels = layers.map(layer => { + const layerPanels = this.props.layers.map(layer => { return }); diff --git a/src/map.jsx b/src/map.jsx index c2bd229..ebaa45e 100644 --- a/src/map.jsx +++ b/src/map.jsx @@ -5,32 +5,14 @@ import theme from './theme.js' export class Map extends React.Component { 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() { - if (this.props.styleManager.mapStyle) { - return - - - } - return
+ return + + } } diff --git a/src/settings.jsx b/src/settings.jsx index a71b0c0..85986ca 100644 --- a/src/settings.jsx +++ b/src/settings.jsx @@ -5,7 +5,7 @@ import { Heading, Container, Input, Toolbar, NavItem, Space } from 'rebass' /** Edit global settings within a style such as the name */ export class SettingsEditor extends React.Component { static propTypes = { - styleManager: React.PropTypes.object.isRequired + mapStyle: React.PropTypes.object.isRequired } constructor(props) { diff --git a/src/style.js b/src/style.js index e033fc0..9440fba 100644 --- a/src/style.js +++ b/src/style.js @@ -1,7 +1,7 @@ import React from 'react'; import randomColor from 'randomcolor' -function assignColorsToLayers(layers) { +export function colorizeLayers(layers) { return layers.map(layer => { if(!layer.metadata) { layer.metadata = {} @@ -12,69 +12,3 @@ function assignColorsToLayers(layers) { 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 [] - } -} diff --git a/src/stylestore.js b/src/stylestore.js new file mode 100644 index 0000000..c4e2c3a --- /dev/null +++ b/src/stylestore.js @@ -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 + } +} diff --git a/src/workspace.jsx b/src/workspace.jsx index e194cd6..32bd72c 100644 --- a/src/workspace.jsx +++ b/src/workspace.jsx @@ -3,23 +3,36 @@ import { LayerEditor } from './layers.jsx' import { SettingsEditor } from './settings.jsx' import theme from './theme.js' -/** The workspace drawer contains the editor components depending on the context - * chosen in the toolbar. */ +/** The workspace drawer contains the editor components depending on the edit + * context chosen in the toolbar. It holds the state of the layers.*/ export class WorkspaceDrawer extends React.Component { static propTypes = { + mapStyle: React.PropTypes.object.isRequired, + onStyleChanged: React.PropTypes.func.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() { let workspaceContent = null - if(this.props.workContext === "layers" && this.props.styleManager.mapStyle) { - workspaceContent = + if(this.props.workContext === "layers") { + workspaceContent = } - if(this.props.workContext === "settings" && this.props.styleManager.mapStyle) { - workspaceContent = + if(this.props.workContext === "settings") { + workspaceContent = } return