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