mirror of
https://github.com/a-nyx/maputnik-with-pmtiles.git
synced 2024-12-27 09:15:29 +01:00
App style is now single source of truth
This commit is contained in:
parent
c84318e6fe
commit
e0a8b0a8e9
5 changed files with 117 additions and 122 deletions
15
src/app.jsx
15
src/app.jsx
|
@ -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>
|
||||||
}
|
}
|
||||||
|
|
|
@ -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}}>
|
||||||
|
|
|
@ -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) => {
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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')}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue