mirror of
https://github.com/a-nyx/maputnik-with-pmtiles.git
synced 2024-11-10 03:41:55 +01:00
Pass everything down as props
This commit is contained in:
parent
416cf7e0af
commit
b6dff4aa58
8 changed files with 214 additions and 167 deletions
59
src/app.jsx
59
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 <div style={{ fontFamily: theme.fontFamily, color: theme.color, fontWeight: 300 }}>
|
||||
<Toolbar
|
||||
|
@ -64,9 +63,13 @@ export default class App extends React.Component {
|
|||
onOpenSettings={this.onOpenSettings.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}>
|
||||
<Map styleManager={this.state.styleManager} />
|
||||
<Map mapStyle={this.state.styleStore.currentStyle} />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
|
|
@ -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 <FillLayer layer={this.state.layer} onPaintChanged={this.onPaintChanged.bind(this)} />
|
||||
return <FillLayer
|
||||
layer={this.props.layer}
|
||||
onPaintChanged={this.onPaintChanged.bind(this)}
|
||||
/>
|
||||
}
|
||||
|
||||
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") {
|
||||
|
@ -149,12 +141,12 @@ export class LayerPanel extends React.Component {
|
|||
if (type === "symbol") {
|
||||
return <SymbolLayer />
|
||||
}
|
||||
|
||||
return <NoLayer />
|
||||
}
|
||||
|
||||
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 = <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 />
|
||||
}
|
||||
|
||||
|
@ -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"],
|
||||
}}>
|
||||
<Toolbar onClick={this.toggleLayer.bind(this)}>
|
||||
<NavItem style={{fontWeight: 400}}>
|
||||
#{this.state.layer.id}
|
||||
#{this.props.layer.id}
|
||||
</NavItem>
|
||||
<Space auto x={1} />
|
||||
<NavItem onClick={this.toggleVisibility.bind(this)}>
|
||||
{visibleIcon}
|
||||
</NavItem>
|
||||
<NavItem onClick={(e) => this.props.destroyLayer(this.state.layer.id)}>
|
||||
<NavItem onClick={(e) => this.props.onLayerDestroyed(this.props.layer)}>
|
||||
<MdDelete />
|
||||
</NavItem>
|
||||
</Toolbar>
|
||||
<Collapse isOpened={this.state.isOpened}>
|
||||
<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>
|
||||
</Collapse>
|
||||
</div>
|
||||
|
@ -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 <LayerPanel
|
||||
key={layer.id}
|
||||
layer={layer}
|
||||
destroyLayer={this.destroyLayer.bind(this)}
|
||||
styleManager={this.props.styleManager}
|
||||
onLayerDestroyed={this.onLayerDestroyed.bind(this)}
|
||||
onLayerChanged={this.onLayerChanged.bind(this)}
|
||||
/>
|
||||
});
|
||||
|
||||
|
|
30
src/map.jsx
30
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 <ReactMapboxGl
|
||||
onStyleLoad={this.onMapLoaded.bind(this)}
|
||||
style={this.props.styleManager.mapStyle}
|
||||
accessToken="pk.eyJ1IjoibW9yZ2Vua2FmZmVlIiwiYSI6IjIzcmN0NlkifQ.0LRTNgCc-envt9d5MzR75w">
|
||||
<ZoomControl/>
|
||||
</ReactMapboxGl>
|
||||
}
|
||||
return <div style={{backgroundColor: theme.colors.black}}/>
|
||||
return <ReactMapboxGl
|
||||
style={this.props.mapStyle}
|
||||
accessToken="pk.eyJ1IjoibW9yZ2Vua2FmZmVlIiwiYSI6IjIzcmN0NlkifQ.0LRTNgCc-envt9d5MzR75w">
|
||||
<ZoomControl/>
|
||||
</ReactMapboxGl>
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
68
src/style.js
68
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 []
|
||||
}
|
||||
}
|
||||
|
|
100
src/stylestore.js
Normal file
100
src/stylestore.js
Normal 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
|
||||
}
|
||||
}
|
|
@ -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 = <LayerEditor styleManager={this.props.styleManager}/>
|
||||
if(this.props.workContext === "layers") {
|
||||
workspaceContent = <LayerEditor
|
||||
onLayersChanged={this.onLayersChanged.bind(this)}
|
||||
layers={this.props.mapStyle.layers}
|
||||
/>
|
||||
}
|
||||
|
||||
if(this.props.workContext === "settings" && this.props.styleManager.mapStyle) {
|
||||
workspaceContent = <SettingsEditor styleManager={this.props.styleManager}/>
|
||||
if(this.props.workContext === "settings") {
|
||||
workspaceContent = <SettingsEditor
|
||||
onStyleChanged={this.props.onStyleChanged}
|
||||
mapStyle={this.props.mapStyle}
|
||||
/>
|
||||
}
|
||||
|
||||
return <div style={{
|
||||
|
|
|
@ -58,7 +58,7 @@ module.exports = {
|
|||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
template: './src/template.html',
|
||||
title: 'Webpack App'
|
||||
title: 'Mapolo'
|
||||
}),
|
||||
new webpack.optimize.DedupePlugin()
|
||||
]
|
||||
|
|
Loading…
Reference in a new issue