App style is now single source of truth

This commit is contained in:
lukasmartinelli 2016-09-10 13:42:23 +02:00
parent c84318e6fe
commit e0a8b0a8e9
5 changed files with 117 additions and 122 deletions

View file

@ -20,9 +20,10 @@ export default class App extends React.Component {
constructor(props) { constructor(props) {
super(props) super(props)
this.styleStore = new StyleStore()
this.state = { this.state = {
styleStore: new StyleStore(),
workContext: "layers", workContext: "layers",
currentStyle: this.styleStore.latestStyle(),
} }
} }
@ -34,17 +35,19 @@ export default class App extends React.Component {
} }
onStyleDownload() { onStyleDownload() {
const mapStyle = JSON.stringify(this.state.styleStore.currentStyle, null, 4) this.styleStore.save(newStyle)
const mapStyle = JSON.stringify(this.state.currentStyle.toJS(), null, 4)
const blob = new Blob([mapStyle], {type: "application/json;charset=utf-8"}); const blob = new Blob([mapStyle], {type: "application/json;charset=utf-8"});
saveAs(blob, mapStyle.id + ".json"); saveAs(blob, mapStyle.id + ".json");
} }
onStyleUpload(newStyle) { onStyleUpload(newStyle) {
this.setState({ styleStore: new StyleStore(newStyle) }) const savedStyle = this.styleStore.save(newStyle)
this.setState({ currentStyle: savedStyle })
} }
onStyleChanged(newStyle) { onStyleChanged(newStyle) {
this.setState({ styleStore: new StyleStore(newStyle) }) this.setState({ currentStyle: newStyle })
} }
onOpenSettings() { onOpenSettings() {
@ -66,10 +69,10 @@ export default class App extends React.Component {
<WorkspaceDrawer <WorkspaceDrawer
onStyleChanged={this.onStyleChanged.bind(this)} onStyleChanged={this.onStyleChanged.bind(this)}
workContext={this.state.workContext} workContext={this.state.workContext}
mapStyle={this.state.styleStore.currentStyle} mapStyle={this.state.currentStyle}
/> />
<div className={layout.map}> <div className={layout.map}>
<Map mapStyle={this.state.styleStore.currentStyle} /> <Map mapStyle={this.state.currentStyle} />
</div> </div>
</div> </div>
} }

View file

@ -7,6 +7,7 @@ import Collapse from 'react-collapse'
import theme from './theme.js' import theme from './theme.js'
import scrollbars from './scrollbars.scss' import scrollbars from './scrollbars.scss'
import _ from 'lodash' import _ from 'lodash'
import Immutable from 'immutable'
export class FillLayer extends React.Component { export class FillLayer extends React.Component {
static propTypes = { static propTypes = {
@ -24,14 +25,14 @@ export class FillLayer extends React.Component {
} }
render() { render() {
const paint = this.props.layer.paint const paint = this.props.layer.get('paint')
return <div> return <div>
<Input name="fill-color" label="Fill color" onChange={this.onPaintChanged.bind(this, "fill-color")} value={paint["fill-color"]} /> <Input name="fill-color" label="Fill color" onChange={this.onPaintChanged.bind(this, "fill-color")} value={paint.get("fill-color")} />
<Input name="fill-outline-color" label="Fill outline color" onChange={this.onPaintChanged.bind(this, "fill-outline-color")} value={paint["fill-outline-color"]} /> <Input name="fill-outline-color" label="Fill outline color" onChange={this.onPaintChanged.bind(this, "fill-outline-color")} value={paint.get("fill-outline-color")} />
<Input name="fill-translate" label="Fill translate" onChange={this.onPaintChanged.bind(this, "fill-translate")} value={paint["fill-translate"]} /> <Input name="fill-translate" label="Fill translate" onChange={this.onPaintChanged.bind(this, "fill-translate")} value={paint.get("fill-translate")} />
<Input name="fill-translate-anchor" label="Fill translate anchor" onChange={this.onPaintChanged.bind(this, "fill-translate-anchor")} value={paint["fill-translate-anchor"]} /> <Input name="fill-translate-anchor" label="Fill translate anchor" onChange={this.onPaintChanged.bind(this, "fill-translate-anchor")} value={paint.get("fill-translate-anchor")} />
<Checkbox name="fill-antialias" label="Antialias" onChange={this.onPaintChanged.bind(this, "fill-antialias")} checked={paint["fill-antialias"]} /> <Checkbox name="fill-antialias" label="Antialias" onChange={this.onPaintChanged.bind(this, "fill-antialias")} checked={paint.get("fill-antialias")} />
<Input name="fill-opacity" label="Opacity" onChange={this.onPaintChanged.bind(this, "fill-opacity")} value={paint["fill-opacity"]} /> <Input name="fill-opacity" label="Opacity" onChange={this.onPaintChanged.bind(this, "fill-opacity")} value={paint.get("fill-opacity")} />
</div> </div>
} }
} }
@ -51,10 +52,10 @@ export class BackgroundLayer extends React.Component {
} }
render() { render() {
const paint = this.props.layer.paint const paint = this.props.layer.get('paint')
return <div> return <div>
<Input name="background-color" label="Background color" onChange={this.onPaintChanged.bind(this, "background-color")} value={paint["background-color"]} /> <Input name="background-color" label="Background color" onChange={this.onPaintChanged.bind(this, "background-color")} value={paint.get("background-color")} />
<Input name="background-opacity" label="Background opacity" onChange={this.onPaintChanged.bind(this, "background-opacity")} value={paint["background-opacity"]} /> <Input name="background-opacity" label="Background opacity" onChange={this.onPaintChanged.bind(this, "background-opacity")} value={paint.get("background-opacity")} />
</div> </div>
} }
} }
@ -105,15 +106,13 @@ export class LayerPanel extends React.Component {
} }
onPaintChanged(property, newValue) { onPaintChanged(property, newValue) {
const layer = _.cloneDeep(this.props.layer) const changedLayer = this.props.layer.setIn(['paint', property], newValue)
layer.paint[property] = newValue; this.props.onLayerChanged(changedLayer)
this.props.onLayerChanged(layer)
} }
onLayoutChanged(property, newValue) { onLayoutChanged(property, newValue) {
const layer = _.cloneDeep(this.props.layer) const changedLayer = this.props.layer.setIn(['layout', property], newValue)
layer.layout[property] = newValue; this.props.onLayerChanged(changedLayer)
this.props.onLayerChanged(layer)
} }
toggleLayer() { toggleLayer() {
@ -168,11 +167,11 @@ export class LayerPanel extends React.Component {
borderRight: 0, borderRight: 0,
borderStyle: "solid", borderStyle: "solid",
borderColor: theme.borderColor, borderColor: theme.borderColor,
borderLeftColor: this.props.layer.metadata["mapolo:color"], borderLeftColor: this.props.layer.getIn(['metadata', 'mapolo:color'])
}}> }}>
<Toolbar onClick={this.toggleLayer.bind(this)}> <Toolbar onClick={this.toggleLayer.bind(this)}>
<NavItem style={{fontWeight: 400}}> <NavItem style={{fontWeight: 400}}>
#{this.props.layer.id} #{this.props.layer.get('id')}
</NavItem> </NavItem>
<Space auto x={1} /> <Space auto x={1} />
<NavItem onClick={this.toggleVisibility.bind(this)}> <NavItem onClick={this.toggleVisibility.bind(this)}>
@ -184,7 +183,7 @@ export class LayerPanel extends React.Component {
</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.props.layer.type)} {this.layerFromType(this.props.layer.get('type'))}
</div> </div>
</Collapse> </Collapse>
</div> </div>
@ -193,7 +192,7 @@ export class LayerPanel extends React.Component {
export class LayerEditor extends React.Component { export class LayerEditor extends React.Component {
static propTypes = { static propTypes = {
layers: React.PropTypes.array.isRequired, layers: React.PropTypes.instanceOf(Immutable.List),
onLayersChanged: React.PropTypes.func.isRequired onLayersChanged: React.PropTypes.func.isRequired
} }
@ -208,6 +207,7 @@ export class LayerEditor extends React.Component {
for (let i = 0; i < this.props.layers.length; i++) { for (let i = 0; i < this.props.layers.length; i++) {
if(this.props.layers[i].id == deletedLayer.id) { if(this.props.layers[i].id == deletedLayer.id) {
deleteIdx = i deleteIdx = i
break
} }
} }
@ -219,25 +219,29 @@ export class LayerEditor extends React.Component {
onLayerChanged(changedLayer) { onLayerChanged(changedLayer) {
//TODO: That's just horrible... //TODO: That's just horrible...
let changeIdx = -1 let changeIdx = -1
for (let i = 0; i < this.props.layers.length; i++) { for (let entry of this.props.layers.entries()) {
if(this.props.layers[i].id == changedLayer.id) { let [i, layer] = entry
if(layer.get('id') == changedLayer.get('id')) {
changeIdx = i changeIdx = i
break
} }
} }
const changedLayers = _.cloneDeep(this.props.layers)
changedLayers[changeIdx] = changedLayer const changedLayers = this.props.layers.set(changeIdx, changedLayer)
this.props.onLayersChanged(changedLayers) this.props.onLayersChanged(changedLayers)
} }
render() { render() {
const layerPanels = this.props.layers.map(layer => { var layerPanels = []
return <LayerPanel
key={layer.id} for(let layer of this.props.layers) {
layerPanels.push(<LayerPanel
key={layer.get('id')}
layer={layer} layer={layer}
onLayerDestroyed={this.onLayerDestroyed.bind(this)} onLayerDestroyed={this.onLayerDestroyed.bind(this)}
onLayerChanged={this.onLayerChanged.bind(this)} onLayerChanged={this.onLayerChanged.bind(this)}
/> />)
}); }
return <div> return <div>
<Toolbar style={{marginRight: 20}}> <Toolbar style={{marginRight: 20}}>

View file

@ -11,7 +11,7 @@ export class Map extends React.Component {
componentWillReceiveProps(nextProps) { componentWillReceiveProps(nextProps) {
const map = this.state.map const map = this.state.map
if(map) { if(map) {
const changes = diffStyles(this.props.mapStyle, nextProps.mapStyle) const changes = diffStyles(this.props.mapStyle.toJS(), nextProps.mapStyle.toJS())
changes.forEach(change => { changes.forEach(change => {
map[change.command].apply(map, change.args); map[change.command].apply(map, change.args);
}); });
@ -26,7 +26,7 @@ export class Map extends React.Component {
MapboxGl.accessToken = "pk.eyJ1IjoibW9yZ2Vua2FmZmVlIiwiYSI6IjIzcmN0NlkifQ.0LRTNgCc-envt9d5MzR75w"; MapboxGl.accessToken = "pk.eyJ1IjoibW9yZ2Vua2FmZmVlIiwiYSI6IjIzcmN0NlkifQ.0LRTNgCc-envt9d5MzR75w";
const map = new MapboxGl.Map({ const map = new MapboxGl.Map({
container: this.container, container: this.container,
style: this.props.mapStyle, style: this.props.mapStyle.toJS(),
}); });
map.on("style.load", (...args) => { map.on("style.load", (...args) => {

View file

@ -1,4 +1,12 @@
import { colorizeLayers } from './style.js' import { colorizeLayers } from './style.js'
import Immutable from 'immutable'
const storage = {
prefix: 'mapolo',
keys: {
latest: 'mapolo:latest_style'
}
}
const emptyStyle = { const emptyStyle = {
version: 8, version: 8,
@ -6,96 +14,77 @@ const emptyStyle = {
layers: [] layers: []
} }
// Return style ids and dates of all styles stored in local storage
function loadStoredStyles() {
const styles = []
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i)
if(isStyleKey(key)) {
styles.push(fromKey(key))
}
}
return styles
}
function isStyleKey(key) {
const parts = key.split(":")
return parts.length == 2 && parts[0] === storage.prefix
}
// Load style id from key
function fromKey(key) {
if(!isStyleKey(key)) {
throw "Key is not a valid style key"
}
const parts = key.split(":")
const styleId = parts[1]
return styleId
}
// Calculate key that identifies the style with a version
function styleKey(styleId) {
return [storage.prefix, styleId].join(":")
}
// Ensure a style has a unique id and a created date
function ensureOptionalStyleProps(mapStyle) {
if(!('id' in mapStyle)) {
mapStyle = mapStyle.set('id', Math.random().toString(36).substr(2, 9))
}
if(!("created" in mapStyle)) {
mapStyle = mapStyle.set('created', new Date())
}
return mapStyle
}
// Manages many possible styles that are stored in the local storage // Manages many possible styles that are stored in the local storage
export class StyleStore { export class StyleStore {
// By default the style store will use the last edited style // Tile store will load all items from local storage and
// as current working style if no explicit style is set // assume they do not change will working on it
constructor(mapStyle) { constructor() {
if(mapStyle) { this.mapStyles = loadStoredStyles()
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 // Find the last edited style
latestStyle() { latestStyle() {
const styles = this.loadStoredStyles() if(this.mapStyles.length == 0) {
return Immutable.fromJS(emptyStyle)
if(styles.length == 0) {
throw "No existing style found"
} }
const styleId = window.localStorage.getItem(storage.keys.latest)
let maxStyle = styles[0] const styleItem = window.localStorage.getItem(styleKey(styleId))
styles.forEach(s => { return Immutable.fromJS(JSON.parse(styleItem))
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 // Save current style replacing previous version
loadStoredStyles() { save(mapStyle) {
const styles = [] if(!(mapStyle instanceof Immutable.Map)) {
for (let i = 0; i < localStorage.length; i++) { mapStyle = Immutable.fromJS(mapStyle)
const key = localStorage.key(i)
if(this.isStyleKey(key)) {
styles.push(this.fromKey(key))
}
} }
return styles mapStyle = ensureOptionalStyleProps(mapStyle)
} const key = styleKey(mapStyle.get('id'))
window.localStorage.setItem(key, JSON.stringify(mapStyle.toJS()))
isStyleKey(key) { window.localStorage.setItem(storage.keys.latest, mapStyle.get('id'))
const parts = key.split(":") return mapStyle
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

@ -12,9 +12,8 @@ export class WorkspaceDrawer extends React.Component {
workContext: React.PropTypes.oneOf(['layers', 'settings']).isRequired, workContext: React.PropTypes.oneOf(['layers', 'settings']).isRequired,
} }
onLayersChanged(layers) { onLayersChanged(changedLayers) {
const changedStyle = this.props.mapStyle const changedStyle = this.props.mapStyle.set('layers', changedLayers)
changedStyle.layers = layers
this.props.onStyleChanged(changedStyle) this.props.onStyleChanged(changedStyle)
} }
@ -24,7 +23,7 @@ export class WorkspaceDrawer extends React.Component {
if(this.props.workContext === "layers") { if(this.props.workContext === "layers") {
workspaceContent = <LayerEditor workspaceContent = <LayerEditor
onLayersChanged={this.onLayersChanged.bind(this)} onLayersChanged={this.onLayersChanged.bind(this)}
layers={this.props.mapStyle.layers} layers={this.props.mapStyle.get('layers')}
/> />
} }