Merge branch 'feature/layout'

This commit is contained in:
Lukas Martinelli 2016-12-16 14:53:52 +01:00
commit 20788a0234
14 changed files with 573 additions and 507 deletions

View file

@ -5,11 +5,10 @@ root = true
end_of_line = lf end_of_line = lf
insert_final_newline = true insert_final_newline = true
# Matches multiple files with brace expansion notation # Matches multiple files with brace expansion notation
# Set default charset # Set default charset
[*.{js,jsx,html,sass}] [*.{js,jsx,html,sass}]
charset = utf-8 charset = utf-8
indent_style = tab indent_style = space
indent_size = 2 indent_size = 2
trim_trailing_whitespace = true trim_trailing_whitespace = true

View file

@ -15,6 +15,7 @@ before_install:
install: install:
- npm install - npm install
script: script:
- mkdir public
- npm run build - npm run build
- npm run lint - npm run lint
- npm run test - npm run test

View file

@ -8,7 +8,7 @@
"build": "webpack --config webpack.production.config.js --progress --profile --colors", "build": "webpack --config webpack.production.config.js --progress --profile --colors",
"test": "karma start --single-run", "test": "karma start --single-run",
"test-watch": "karma start", "test-watch": "karma start",
"start": "webpack-dev-server --progress --profile --colors", "start": "webpack-dev-server --progress --profile --colors --watch-poll",
"lint": "eslint --ext js --ext jsx {src,test}" "lint": "eslint --ext js --ext jsx {src,test}"
}, },
"repository": { "repository": {
@ -32,7 +32,8 @@
"react-height": "^2.1.1", "react-height": "^2.1.1",
"react-icons": "^2.2.1", "react-icons": "^2.2.1",
"react-motion": "^0.4.4", "react-motion": "^0.4.4",
"rebass": "^0.3.1" "rebass": "^0.3.1",
"request": "^2.79.0"
}, },
"babel": { "babel": {
"presets": [ "presets": [

41
src/apistore.js Normal file
View file

@ -0,0 +1,41 @@
import request from 'request'
import style from './style.js'
export class ApiStyleStore {
supported(cb) {
request('http://localhost:8000/styles', (error, response, body) => {
cb(error === undefined)
})
}
latestStyle(cb) {
if(this.latestStyleId) {
request('http://localhost:8000/styles/' + this.latestStyleId, (error, response, body) => {
cb(JSON.parse(body))
})
} else {
request('http://localhost:8000/styles', (error, response, body) => {
if (!error && response.statusCode == 200) {
const styleIds = JSON.parse(body);
this.latestStyleId = styleIds[0];
request('http://localhost:8000/styles/' + this.latestStyleId, (error, response, body) => {
cb(style.fromJSON(JSON.parse(body)))
})
}
})
}
}
// Save current style replacing previous version
save(mapStyle) {
const id = mapStyle.get('id')
request.put({
url: 'http://localhost:8000/styles/' + id,
json: true,
body: style.toJSON(mapStyle)
}, (error, response, body) => {
console.log('Saved style');
})
return mapStyle
}
}

View file

@ -10,111 +10,117 @@ import { Map } from './map.jsx'
import {Toolbar} from './toolbar.jsx' import {Toolbar} from './toolbar.jsx'
import style from './style.js' import style from './style.js'
import { loadDefaultStyle, SettingsStore, StyleStore } from './stylestore.js' import { loadDefaultStyle, SettingsStore, StyleStore } from './stylestore.js'
import { ApiStyleStore } from './apistore.js'
import { WorkspaceDrawer } from './workspace.jsx' import { WorkspaceDrawer } from './workspace.jsx'
import theme from './theme.js' import theme from './theme.js'
import './index.scss' import './index.scss'
export default class App extends React.Component { export default class App extends React.Component {
static childContextTypes = { static childContextTypes = {
rebass: React.PropTypes.object, rebass: React.PropTypes.object,
reactIconBase: React.PropTypes.object reactIconBase: React.PropTypes.object
} }
constructor(props) { constructor(props) {
super(props) super(props)
this.styleStore = new StyleStore()
this.settingsStore = new SettingsStore()
this.state = {
accessToken: this.settingsStore.accessToken,
workContext: "layers",
currentStyle: this.styleStore.latestStyle(),
}
if(this.state.currentStyle.get('layers').size === 0) {
loadDefaultStyle(mapStyle => this.onStyleUpload(mapStyle))
}
}
onReset() { this.styleStore = new ApiStyleStore()
this.styleStore.purge() this.styleStore.supported(isSupported => {
loadDefaultStyle(mapStyle => this.onStyleUpload(mapStyle)) if(!isSupported) {
} console.log('Falling back to local storage for storing styles')
this.styleStore = new StyleStore()
}
this.styleStore.latestStyle(mapStyle => this.onStyleUpload(mapStyle))
})
getChildContext() { this.settingsStore = new SettingsStore()
return { this.state = {
rebass: theme, accessToken: this.settingsStore.accessToken,
reactIconBase: { size: 20 } workContext: "layers",
} currentStyle: style.emptyStyle
} }
}
onStyleDownload() { onReset() {
const mapStyle = style.toJSON(this.state.currentStyle) this.styleStore.purge()
const blob = new Blob([JSON.stringify(mapStyle, null, 4)], {type: "application/json;charset=utf-8"}); loadDefaultStyle(mapStyle => this.onStyleUpload(mapStyle))
saveAs(blob, mapStyle.id + ".json"); }
this.onStyleSave()
}
onStyleUpload(newStyle) { getChildContext() {
const savedStyle = this.styleStore.save(newStyle) return {
this.setState({ currentStyle: savedStyle }) rebass: theme,
} reactIconBase: { size: 20 }
}
}
onStyleSave() { onStyleDownload() {
const snapshotStyle = this.state.currentStyle.set('modified', new Date().toJSON()) const mapStyle = style.toJSON(this.state.currentStyle)
this.setState({ currentStyle: snapshotStyle }) const blob = new Blob([JSON.stringify(mapStyle, null, 4)], {type: "application/json;charset=utf-8"});
this.styleStore.save(snapshotStyle) saveAs(blob, mapStyle.id + ".json");
} this.onStyleSave()
}
onStyleChanged(newStyle) { onStyleUpload(newStyle) {
this.setState({ currentStyle: newStyle }) const savedStyle = this.styleStore.save(newStyle)
} this.setState({ currentStyle: savedStyle })
}
onOpenSettings() { onStyleSave() {
this.setState({ workContext: "settings" }) const snapshotStyle = this.state.currentStyle.set('modified', new Date().toJSON())
} this.setState({ currentStyle: snapshotStyle })
console.log('Save')
this.styleStore.save(snapshotStyle)
}
onOpenAbout() { onStyleChanged(newStyle) {
this.setState({ workContext: "about" }) this.setState({ currentStyle: newStyle })
} }
onOpenLayers() { onOpenSettings() {
this.setState({ workContext: "layers", }) //TODO: open settings modal
} //this.setState({ workContext: "settings" })
}
onOpenSources() { onOpenAbout() {
this.setState({ workContext: "sources", }) //TODO: open about modal
} //this.setState({ workContext: "about" })
}
onAccessTokenChanged(newToken) { onOpenSources() {
this.settingsStore.accessToken = newToken //TODO: open sources modal
this.setState({ accessToken: newToken }) //this.setState({ workContext: "sources", })
} }
render() { onAccessTokenChanged(newToken) {
return <div style={{ fontFamily: theme.fontFamily, color: theme.color, fontWeight: 300 }}> this.settingsStore.accessToken = newToken
<Toolbar this.setState({ accessToken: newToken })
styleAvailable={this.state.currentStyle.get('layers').size > 0} }
onStyleSave={this.onStyleSave.bind(this)}
onStyleUpload={this.onStyleUpload.bind(this)} render() {
onStyleDownload={this.onStyleDownload.bind(this)} return <div style={{ fontFamily: theme.fontFamily, color: theme.color, fontWeight: 300 }}>
onOpenSettings={this.onOpenSettings.bind(this)} <Toolbar
onOpenAbout={this.onOpenAbout.bind(this)} styleAvailable={this.state.currentStyle.get('layers').size > 0}
onOpenLayers={this.onOpenLayers.bind(this)} onStyleSave={this.onStyleSave.bind(this)}
onOpenSources={this.onOpenSources.bind(this)} onStyleUpload={this.onStyleUpload.bind(this)}
/> onStyleDownload={this.onStyleDownload.bind(this)}
<WorkspaceDrawer onOpenSettings={this.onOpenSettings.bind(this)}
onStyleChanged={this.onStyleChanged.bind(this)} onOpenAbout={this.onOpenAbout.bind(this)}
onReset={this.onReset.bind(this)} onOpenSources={this.onOpenSources.bind(this)}
workContext={this.state.workContext} />
mapStyle={this.state.currentStyle} <WorkspaceDrawer
accessToken={this.state.accessToken} onStyleChanged={this.onStyleChanged.bind(this)}
onAccessTokenChanged={this.onAccessTokenChanged.bind(this)} onReset={this.onReset.bind(this)}
/> workContext={this.state.workContext}
<Map mapStyle={this.state.currentStyle}
mapStyle={this.state.currentStyle} accessToken={this.state.accessToken}
accessToken={this.state.accessToken} onAccessTokenChanged={this.onAccessTokenChanged.bind(this)}
/> />
</div> <Map
} mapStyle={this.state.currentStyle}
accessToken={this.state.accessToken}
/>
</div>
}
} }

View file

@ -12,55 +12,48 @@ import PureRenderMixin from 'react-addons-pure-render-mixin';
// List of collapsible layer editors // List of collapsible layer editors
export class LayerList extends React.Component { export class LayerList extends React.Component {
static propTypes = { static propTypes = {
layers: React.PropTypes.instanceOf(Immutable.OrderedMap), layers: React.PropTypes.instanceOf(Immutable.OrderedMap),
onLayersChanged: React.PropTypes.func.isRequired onLayersChanged: React.PropTypes.func.isRequired
} }
constructor(props) { constructor(props) {
super(props) super(props)
this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this); this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this);
} }
onLayerDestroyed(deletedLayer) { onLayerDestroyed(deletedLayer) {
const remainingLayers = this.props.layers.delete(deletedLayer.get('id')) const remainingLayers = this.props.layers.delete(deletedLayer.get('id'))
this.props.onLayersChanged(remainingLayers) this.props.onLayersChanged(remainingLayers)
} }
onLayerChanged(layer) { onLayerChanged(layer) {
const changedLayers = this.props.layers.set(layer.get('id'), layer) const changedLayers = this.props.layers.set(layer.get('id'), layer)
this.props.onLayersChanged(changedLayers) this.props.onLayersChanged(changedLayers)
} }
render() { render() {
var layerPanels = [] var layerPanels = []
layerPanels = this.props.layers.map(layer => { layerPanels = this.props.layers.map(layer => {
return <LayerEditor return <LayerEditor
key={layer.get('id')} 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)}
/> />
}).toIndexedSeq() }).toIndexedSeq()
return <div> return <div>
<Toolbar style={{marginRight: 20}}> <div className={scrollbars.darkScrollbar} style={{
<NavItem> overflowY: "scroll",
<Heading>Layers</Heading> bottom:0,
</NavItem> left:0,
<Space auto x={1} /> right:0,
</Toolbar> top:1,
position: "absolute",
<div className={scrollbars.darkScrollbar} style={{ }}>
overflowY: "scroll", {layerPanels}
bottom:0, </div>
left:0, </div>
right:0, }
top:40,
position: "absolute",
}}>
{layerPanels}
</div>
</div>
}
} }

View file

@ -12,14 +12,15 @@ export class Map extends React.Component {
} }
componentWillReceiveProps(nextProps) { componentWillReceiveProps(nextProps) {
const tokenChanged = nextProps.accessToken !== MapboxGl.accessToken const hasTokenChanged = nextProps.accessToken !== MapboxGl.accessToken
MapboxGl.accessToken = nextProps.accessToken
// If the id has changed a new style has been uplaoded and // If the id has changed a new style has been uplaoded and
// it is safer to do a full new render // it is safer to do a full new render
// TODO: might already be handled in diff algorithm? // TODO: might already be handled in diff algorithm?
const mapIdChanged = this.props.mapStyle.get('id') !== nextProps.mapStyle.get('id') const mapIdChanged = this.props.mapStyle.get('id') !== nextProps.mapStyle.get('id')
if(mapIdChanged || tokenChanged) { if(mapIdChanged || hasTokenChanged) {
this.state.map.setStyle(style.toJSON(nextProps.mapStyle)) this.state.map.setStyle(style.toJSON(nextProps.mapStyle))
return return
} }

View file

@ -1,12 +1,12 @@
.darkScrollbar::-webkit-scrollbar { .darkScrollbar::-webkit-scrollbar {
background-color: #313131; background-color: #26282e;
width: 10px; width: 10px;
} }
.darkScrollbar::-webkit-scrollbar-thumb { .darkScrollbar::-webkit-scrollbar-thumb {
border-radius: 6px; border-radius: 6px;
-webkit-box-shadow: inset 0 0 6px rgba(0,0,0,.3); -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,.3);
background-color: #555; background-color: #40444e;
padding-left: 2px; padding-left: 2px;
padding-right: 2px; padding-right: 2px;
} }

View file

@ -9,34 +9,41 @@ import randomColor from 'randomcolor'
// It also ensures that every style has an id and // It also ensures that every style has an id and
// a created date for future reference // a created date for future reference
function fromJSON(jsonStyle) { function fromJSON(jsonStyle) {
if (typeof jsonStyle === 'string' || jsonStyle instanceof String) { if (typeof jsonStyle === 'string' || jsonStyle instanceof String) {
jsonStyle = JSON.parse(jsonStyle) jsonStyle = JSON.parse(jsonStyle)
} }
return Immutable.Map(Object.keys(jsonStyle).map(key => { return Immutable.Map(Object.keys(jsonStyle).map(key => {
const val = jsonStyle[key] const val = jsonStyle[key]
if(key === "layers") { if(key === "layers") {
return [key, Immutable.OrderedMap(val.map(l => [l.id, Immutable.fromJS(l)]))] return [key, Immutable.OrderedMap(val.map(l => [l.id, Immutable.fromJS(l)]))]
} else if(key === "sources" || key === "metadata" || key === "transition") { } else if(key === "sources" || key === "metadata" || key === "transition") {
return [key, Immutable.fromJS(val)] return [key, Immutable.fromJS(val)]
} else { } else {
return [key, val] return [key, val]
} }
})) }))
} }
// Empty style is always used if no style could be restored or fetched
const emptyStyle = ensureMetadataExists(fromJSON({
version: 8,
sources: {},
layers: [],
}))
function ensureHasId(style) { function ensureHasId(style) {
if(style.has('id')) return style if(style.has('id')) return style
return style.set('id', Math.random().toString(36).substr(2, 9)) return style.set('id', Math.random().toString(36).substr(2, 9))
} }
function ensureHasTimestamp(style) { function ensureHasTimestamp(style) {
if(style.has('id')) return style if(style.has('id')) return style
return style.set('created', new Date().toJSON()) return style.set('created', new Date().toJSON())
} }
function ensureMetadataExists(style) { function ensureMetadataExists(style) {
return ensureHasId(ensureHasTimestamp(style)) return ensureHasId(ensureHasTimestamp(style))
} }
// Compare style with other style and return changes // Compare style with other style and return changes
@ -44,41 +51,42 @@ function ensureMetadataExists(style) {
// Should be able to improve performance since we can only compare // Should be able to improve performance since we can only compare
// by reference // by reference
function diffStyles(before, after) { function diffStyles(before, after) {
return diffJSONStyles(toJSON(before), toJSON(after)) return diffJSONStyles(toJSON(before), toJSON(after))
} }
// Turns immutable style back into JSON with the original order of the // Turns immutable style back into JSON with the original order of the
// layers preserved // layers preserved
function toJSON(mapStyle) { function toJSON(mapStyle) {
const jsonStyle = {} const jsonStyle = {}
for(let [key, value] of mapStyle.entries()) { for(let [key, value] of mapStyle.entries()) {
if(key === "layers") { if(key === "layers") {
jsonStyle[key] = value.toIndexedSeq().toJS() jsonStyle[key] = value.toIndexedSeq().toJS()
} else if(key === 'sources' || key === "metadata" || key === "transition") { } else if(key === 'sources' || key === "metadata" || key === "transition") {
jsonStyle[key] = value.toJS() jsonStyle[key] = value.toJS()
} else { } else {
jsonStyle[key] = value jsonStyle[key] = value
} }
} }
return jsonStyle return jsonStyle
} }
export function colorizeLayers(layers) { export function colorizeLayers(layers) {
return layers.map(layer => { return layers.map(layer => {
if(!layer.metadata) { if(!layer.metadata) {
layer.metadata = {} layer.metadata = {}
} }
if(!"maputnik:color" in layer.metadata) { if(!"maputnik:color" in layer.metadata) {
layer.metadata["maputnik:color"] = randomColor() layer.metadata["maputnik:color"] = randomColor()
} }
return layer return layer
}) })
} }
export default { export default {
colorizeLayers, colorizeLayers,
toJSON, toJSON,
fromJSON, fromJSON,
diffStyles, diffStyles,
ensureMetadataExists, ensureMetadataExists,
emptyStyle,
} }

View file

@ -5,117 +5,114 @@ import style from './style.js'
const storagePrefix = "maputnik" const storagePrefix = "maputnik"
const stylePrefix = 'style' const stylePrefix = 'style'
const storageKeys = { const storageKeys = {
latest: [storagePrefix, 'latest_style'].join(':'), latest: [storagePrefix, 'latest_style'].join(':'),
accessToken: [storagePrefix, 'access_token'].join(':') accessToken: [storagePrefix, 'access_token'].join(':')
} }
// Empty style is always used if no style could be restored or fetched
const emptyStyle = style.ensureMetadataExists(style.fromJSON({
version: 8,
sources: {},
layers: [],
}))
const defaultStyleUrl = "https://raw.githubusercontent.com/osm2vectortiles/mapbox-gl-styles/master/styles/basic-v9-cdn.json" const defaultStyleUrl = "https://raw.githubusercontent.com/osm2vectortiles/mapbox-gl-styles/master/styles/basic-v9-cdn.json"
// Fetch a default style via URL and return it or a fallback style via callback // Fetch a default style via URL and return it or a fallback style via callback
export function loadDefaultStyle(cb) { export function loadDefaultStyle(cb) {
console.log('Load default style') console.log('Load default style')
var request = new XMLHttpRequest() var request = new XMLHttpRequest()
request.open('GET', defaultStyleUrl, true) request.open('GET', defaultStyleUrl, true)
request.onload = () => { request.onload = () => {
if (request.status >= 200 && request.status < 400) { if (request.status >= 200 && request.status < 400) {
cb(style.ensureMetadataExists(style.fromJSON(request.responseText))) cb(style.ensureMetadataExists(style.fromJSON(request.responseText)))
} else { } else {
cb(emptyStyle) cb(style.emptyStyle)
} }
} }
request.onerror = function() { request.onerror = function() {
console.log('Could not fetch default style') console.log('Could not fetch default style')
cb(emptyStyle) cb(style.emptyStyle)
} }
request.send() request.send()
} }
// Return style ids and dates of all styles stored in local storage // Return style ids and dates of all styles stored in local storage
function loadStoredStyles() { function loadStoredStyles() {
const styles = [] const styles = []
for (let i = 0; i < window.localStorage.length; i++) { for (let i = 0; i < window.localStorage.length; i++) {
const key = window.localStorage.key(i) const key = window.localStorage.key(i)
if(isStyleKey(key)) { if(isStyleKey(key)) {
styles.push(fromKey(key)) styles.push(fromKey(key))
} }
} }
return styles return styles
} }
function isStyleKey(key) { function isStyleKey(key) {
const parts = key.split(":") const parts = key.split(":")
return parts.length == 3 && parts[0] === storagePrefix && parts[1] === stylePrefix return parts.length == 3 && parts[0] === storagePrefix && parts[1] === stylePrefix
} }
// Load style id from key // Load style id from key
function fromKey(key) { function fromKey(key) {
if(!isStyleKey(key)) { if(!isStyleKey(key)) {
throw "Key is not a valid style key" throw "Key is not a valid style key"
} }
const parts = key.split(":") const parts = key.split(":")
const styleId = parts[2] const styleId = parts[2]
return styleId return styleId
} }
// Calculate key that identifies the style with a version // Calculate key that identifies the style with a version
function styleKey(styleId) { function styleKey(styleId) {
return [storagePrefix, stylePrefix, styleId].join(":") return [storagePrefix, stylePrefix, styleId].join(":")
} }
// Store style independent settings // Store style independent settings
export class SettingsStore { export class SettingsStore {
get accessToken() { get accessToken() {
const token = window.localStorage.getItem(storageKeys.accessToken) const token = window.localStorage.getItem(storageKeys.accessToken)
return token ? token : "" return token ? token : ""
} }
set accessToken(val) { set accessToken(val) {
window.localStorage.setItem(storageKeys.accessToken, val) window.localStorage.setItem(storageKeys.accessToken, val)
} }
} }
// 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 {
// Tile store will load all items from local storage and // Tile store will load all items from local storage and
// assume they do not change will working on it // assume they do not change will working on it
constructor() { constructor() {
this.mapStyles = loadStoredStyles() this.mapStyles = loadStoredStyles()
} }
// Delete entire style history supported(cb) {
purge() { cb(window.localStorage !== undefined)
for (let i = 0; i < window.localStorage.length; i++) { }
const key = window.localStorage.key(i)
if(key.startsWith(storagePrefix)) {
window.localStorage.removeItem(key)
}
}
}
// Find the last edited style // Delete entire style history
latestStyle() { purge() {
if(this.mapStyles.length === 0) return emptyStyle for (let i = 0; i < window.localStorage.length; i++) {
const styleId = window.localStorage.getItem(storageKeys.latest) const key = window.localStorage.key(i)
const styleItem = window.localStorage.getItem(styleKey(styleId)) if(key.startsWith(storagePrefix)) {
window.localStorage.removeItem(key)
}
}
}
if(styleItem) return style.fromJSON(styleItem) // Find the last edited style
return emptyStyle latestStyle(cb) {
} if(this.mapStyles.length === 0) return cb(style.emptyStyle)
const styleId = window.localStorage.getItem(storageKeys.latest)
const styleItem = window.localStorage.getItem(styleKey(styleId))
// Save current style replacing previous version if(styleItem) return cb(style.fromJSON(styleItem))
save(mapStyle) { cb(style.emptyStyle)
const key = styleKey(mapStyle.get('id')) }
window.localStorage.setItem(key, JSON.stringify(style.toJSON(mapStyle)))
window.localStorage.setItem(storageKeys.latest, mapStyle.get('id')) // Save current style replacing previous version
return mapStyle save(mapStyle) {
} const key = styleKey(mapStyle.get('id'))
window.localStorage.setItem(key, JSON.stringify(style.toJSON(mapStyle)))
window.localStorage.setItem(storageKeys.latest, mapStyle.get('id'))
return mapStyle
}
} }

View file

@ -1,112 +1,112 @@
const caps = { const caps = {
textTransform: 'uppercase', textTransform: 'uppercase',
letterSpacing: '.2em' letterSpacing: '.2em'
} }
export const fullHeight = { export const fullHeight = {
position: "fixed", position: "fixed",
top: 0, top: 0,
bottom: 0, bottom: 0,
height: "100%", height: "100%",
} }
const baseColors = { const baseColors = {
black: '#242424', black: '#1c1f24',
gray: '#313131', gray: '#26282e',
midgray: '#778', midgray: '#36383e',
lowgray: '#dcdcdc', lowgray: '#8e8e8e',
white: '#fff', white: '#fff',
blue: '#00d9f7', blue: '#00d9f7',
green: '#B4C7AD', green: '#B4C7AD',
orange: '#fb3', orange: '#fb3',
red: '#f04', red: '#f04',
} }
const themeColors = { const themeColors = {
primary: baseColors.gray, primary: baseColors.gray,
secondary: baseColors.midgray, secondary: baseColors.midgray,
default: baseColors.gray, default: baseColors.gray,
info: baseColors.blue, info: baseColors.blue,
success: baseColors.green, success: baseColors.green,
warning: baseColors.orange, warning: baseColors.orange,
error: baseColors.red error: baseColors.red
} }
export const colors = { export const colors = {
...baseColors, ...baseColors,
...themeColors ...themeColors
} }
export const inputBase = { export const inputBase = {
display: 'block', display: 'block',
border: '1px solid rgb(36, 36, 36)', border: '1px solid rgb(36, 36, 36)',
height: 30, height: 30,
width: '100%', width: '100%',
paddingLeft: 5, paddingLeft: 5,
paddingRight: 5, paddingRight: 5,
backgroundColor: colors.gray, backgroundColor: colors.gray,
} }
const scale = [3, 5, 10, 30, 40] const scale = [3, 5, 10, 30, 40]
const fontSizes = [28, 24, 20, 16, 14, 12, 10] const fontSizes = [28, 24, 20, 16, 14, 12, 10]
const border = { const border = {
borderColor: colors.black, borderColor: colors.black,
borderRadius: 0, borderRadius: 0,
} }
const dark = { const dark = {
name: 'Dark', name: 'Dark',
color: colors.white, color: colors.white,
fontFamily: 'Roboto, sans-serif', fontFamily: 'Roboto, sans-serif',
scale, scale,
fontSizes, fontSizes,
colors, colors,
inverted: colors.midGray, inverted: colors.midGray,
...border, ...border,
Block: { Block: {
backgroundColor: colors.gray, backgroundColor: colors.gray,
...border, ...border,
borderLeft: 0, borderLeft: 0,
borderRight: 0, borderRight: 0,
marginBottom: 0, marginBottom: 0,
paddingBottom: 0, paddingBottom: 0,
}, },
PanelHeader: { PanelHeader: {
marginRight: -10, marginRight: -10,
marginBottom: 0, marginBottom: 0,
fontSize: fontSizes[5], fontSize: fontSizes[5],
fontWeight: 400, fontWeight: 400,
color: colors.white, color: colors.white,
}, },
Button: { Button: {
color: '#00d9f7', color: '#00d9f7',
}, },
Menu: { Menu: {
color: '#00d9f7', color: '#00d9f7',
backgroundColor: '#000' backgroundColor: '#000'
}, },
Message: { Message: {
color: '#111', color: '#111',
opacity: 15/16 opacity: 15/16
}, },
Header: { Header: {
fontWeight: 400, fontWeight: 400,
}, },
ButtonCircle : { ButtonCircle : {
}, },
Toolbar: { Toolbar: {
fontWeight: 400, fontWeight: 400,
minHeight: scale[3] minHeight: scale[3]
}, },
Label: { Label: {
fontWeight: 300, fontWeight: 300,
}, },
Input: { Input: {
fontWeight: 300, fontWeight: 300,
fontSize: fontSizes[5], fontSize: fontSizes[5],
}, },
} }
export default dark export default dark

View file

@ -12,122 +12,138 @@ import Fixed from 'rebass/dist/Fixed'
import MdFileDownload from 'react-icons/lib/md/file-download' import MdFileDownload from 'react-icons/lib/md/file-download'
import MdFileUpload from 'react-icons/lib/md/file-upload' import MdFileUpload from 'react-icons/lib/md/file-upload'
import MdOpenInBrowser from 'react-icons/lib/md/open-in-browser'
import MdSettings from 'react-icons/lib/md/settings' import MdSettings from 'react-icons/lib/md/settings'
import MdInfo from 'react-icons/lib/md/info' import MdInfo from 'react-icons/lib/md/info'
import MdLayers from 'react-icons/lib/md/layers' import MdLayers from 'react-icons/lib/md/layers'
import MdSave from 'react-icons/lib/md/save' import MdSave from 'react-icons/lib/md/save'
import MdStyle from 'react-icons/lib/md/style'
import MdMap from 'react-icons/lib/md/map' import MdMap from 'react-icons/lib/md/map'
import MdInsertEmoticon from 'react-icons/lib/md/insert-emoticon'
import MdFontDownload from 'react-icons/lib/md/font-download'
import MdHelpOutline from 'react-icons/lib/md/help-outline'
import MdFindInPage from 'react-icons/lib/md/find-in-page'
import style from './style.js' import style from './style.js'
import { fullHeight } from './theme.js' import { fullHeight } from './theme.js'
import theme from './theme.js'; import theme from './theme.js';
const InlineBlock = props => <div style={{display: "inline-block", ...props.style}}>
{props.children}
</div>
export class Toolbar extends React.Component { export class Toolbar extends React.Component {
static propTypes = { static propTypes = {
// A new style has been uploaded // A new style has been uploaded
onStyleUpload: React.PropTypes.func.isRequired, onStyleUpload: React.PropTypes.func.isRequired,
// Current style is requested for download // Current style is requested for download
onStyleDownload: React.PropTypes.func.isRequired, onStyleDownload: React.PropTypes.func.isRequired,
// Style is explicitely saved to local cache // Style is explicitely saved to local cache
onStyleSave: React.PropTypes.func, onStyleSave: React.PropTypes.func,
// Open settings drawer // Open settings drawer
onOpenSettings: React.PropTypes.func, onOpenSettings: React.PropTypes.func,
// Open about page // Open about page
onOpenAbout: React.PropTypes.func, onOpenAbout: React.PropTypes.func,
// Open sources drawer // Open sources drawer
onOpenSources: React.PropTypes.func, onOpenSources: React.PropTypes.func,
// Open layers drawer // Whether a style is available for download or saving
onOpenLayers: React.PropTypes.func, // A style with no layers should not be available
// Whether a style is available for download or saving styleAvailable: React.PropTypes.bool,
// A style with no layers should not be available }
styleAvailable: React.PropTypes.bool,
}
onUpload(_, files) { onUpload(_, files) {
const [e, file] = files[0]; const [e, file] = files[0];
const reader = new FileReader(); const reader = new FileReader();
reader.readAsText(file, "UTF-8"); reader.readAsText(file, "UTF-8");
reader.onload = e => { reader.onload = e => {
let mapStyle = style.fromJSON(JSON.parse(e.target.result)) let mapStyle = style.fromJSON(JSON.parse(e.target.result))
mapStyle = style.ensureMetadataExists(mapStyle) mapStyle = style.ensureMetadataExists(mapStyle)
this.props.onStyleUpload(mapStyle); this.props.onStyleUpload(mapStyle);
} }
reader.onerror = e => console.log(e.target); reader.onerror = e => console.log(e.target);
} }
saveButton() { saveButton() {
if(this.props.styleAvailable) { if(this.props.styleAvailable) {
return <Block> return <InlineBlock>
<Button onClick={this.props.onStyleSave} big={true}> <Button onClick={this.props.onStyleSave} big={true}>
<Tooltip inverted rounded title="Save style"> <MdSave />
<MdSave /> Save
</Tooltip> </Button>
</Button> </InlineBlock>
</Block> }
} return null
return null }
}
downloadButton() { downloadButton() {
if(this.props.styleAvailable) { if(this.props.styleAvailable) {
return <Block> return <InlineBlock>
<Button onClick={this.props.onStyleDownload} big={true}> <Button onClick={this.props.onStyleDownload} big={true}>
<Tooltip inverted rounded title="Download style"> <MdFileDownload />
<MdFileDownload /> Download
</Tooltip> </Button>
</Button> </InlineBlock>
</Block> }
} return null
return null }
}
render() { render() {
return <Container style={{
...fullHeight, return <div style={{
zIndex: 100, position: "fixed",
left: 0, height: 50,
top: 0, width: '100%',
backgroundColor: theme.colors.black } zIndex: 100,
}> left: 0,
<Block> top: 0,
<FileReaderInput onChange={this.onUpload.bind(this)}> backgroundColor: theme.colors.black
<Button big={true} theme={this.props.styleAvailable ? "default" : "success"}> }}>
<Tooltip inverted rounded title="Upload style"> <InlineBlock>
<MdFileUpload /> <Button style={{width: 300, textAlign: 'left'}}>
</Tooltip> <img src="https://github.com/maputnik/editor/raw/master/media/maputnik.png" alt="Maputnik" style={{width: 40, height: 40, paddingRight: 5, verticalAlign: 'middle'}}/>
</Button> <span style={{fontSize: 20 }}>Maputnik</span>
</FileReaderInput> </Button>
</Block> </InlineBlock>
{this.downloadButton()} <InlineBlock>
{this.saveButton()} <FileReaderInput onChange={this.onUpload.bind(this)}>
<Block> <Button big={true} theme={this.props.styleAvailable ? "default" : "success"}>
<Button big={true} onClick={this.props.onOpenLayers}> <MdOpenInBrowser />
<Tooltip inverted rounded title="Layers"> Open
<MdLayers /> </Button>
</Tooltip> </FileReaderInput>
</Button> </InlineBlock>
</Block> {this.downloadButton()}
<Block> {this.saveButton()}
<Button big={true} onClick={this.props.onOpenSources}> <InlineBlock>
<Tooltip inverted rounded title="Sources"> <Button big={true} onClick={this.props.onOpenSettings}>
<MdMap /> <MdLayers />
</Tooltip> Tilesets
</Button> </Button>
</Block> </InlineBlock>
<Block> <InlineBlock>
<Button big={true} onClick={this.props.onOpenSettings}> <Button big={true} onClick={this.props.onOpenSettings}>
<Tooltip inverted rounded title="Settings"> <MdFontDownload />
<MdSettings /> Fonts
</Tooltip> </Button>
</Button> </InlineBlock>
</Block> <InlineBlock>
<Block> <Button big={true} onClick={this.props.onOpenSettings}>
<Button big={true} onClick={this.props.onOpenAbout}> <MdInsertEmoticon/>
<Tooltip inverted rounded title="About"> Icons
<MdInfo /> </Button>
</Tooltip> </InlineBlock>
</Button> <InlineBlock>
</Block> <Button big={true} onClick={this.props.onOpenSettings}>
</Container> <MdFindInPage />
} Inspect
</Button>
</InlineBlock>
<InlineBlock>
<Button big={true} onClick={this.props.onOpenAbout}>
<MdHelpOutline />
Help
</Button>
</InlineBlock>
</div>
}
} }

View file

@ -8,65 +8,66 @@ import { colors, fullHeight } from './theme.js'
/** The workspace drawer contains the editor components depending on the edit /** The workspace drawer contains the editor components depending on the edit
* context chosen in the toolbar. It holds the state of the layers.*/ * context chosen in the toolbar. It holds the state of the layers.*/
export class WorkspaceDrawer extends React.Component { export class WorkspaceDrawer extends React.Component {
static propTypes = { static propTypes = {
mapStyle: React.PropTypes.object.isRequired, mapStyle: React.PropTypes.object.isRequired,
onStyleChanged: React.PropTypes.func.isRequired, onStyleChanged: React.PropTypes.func.isRequired,
workContext: React.PropTypes.oneOf(['layers', 'settings', 'sources']).isRequired, workContext: React.PropTypes.oneOf(['layers', 'settings', 'sources']).isRequired,
accessToken: React.PropTypes.string, accessToken: React.PropTypes.string,
onAccessTokenChanged: React.PropTypes.func, onAccessTokenChanged: React.PropTypes.func,
onReset: React.PropTypes.func, onReset: React.PropTypes.func,
} }
onLayersChanged(changedLayers) { onLayersChanged(changedLayers) {
const changedStyle = this.props.mapStyle.set('layers', changedLayers) const changedStyle = this.props.mapStyle.set('layers', changedLayers)
this.props.onStyleChanged(changedStyle) this.props.onStyleChanged(changedStyle)
} }
onSourcesChanged(changedSources) { onSourcesChanged(changedSources) {
const changedStyle = this.props.mapStyle.set('sources', changedSources) const changedStyle = this.props.mapStyle.set('sources', changedSources)
this.props.onStyleChanged(changedStyle) this.props.onStyleChanged(changedStyle)
} }
render() { render() {
let workspaceContent = null let workspaceContent = null
if(this.props.workContext === "sources") { if(this.props.workContext === "sources") {
workspaceContent = <SourceList workspaceContent = <SourceList
onSourcesChanged={this.onSourcesChanged.bind(this)} onSourcesChanged={this.onSourcesChanged.bind(this)}
sources={this.props.mapStyle.get('sources')} sources={this.props.mapStyle.get('sources')}
/> />
} }
if(this.props.workContext === "layers") { if(this.props.workContext === "layers") {
workspaceContent = <LayerList workspaceContent = <LayerList
onLayersChanged={this.onLayersChanged.bind(this)} onLayersChanged={this.onLayersChanged.bind(this)}
layers={this.props.mapStyle.get('layers')} layers={this.props.mapStyle.get('layers')}
/> />
} }
if(this.props.workContext === "settings") { if(this.props.workContext === "settings") {
workspaceContent = <SettingsEditor workspaceContent = <SettingsEditor
onReset={this.props.onReset} onReset={this.props.onReset}
onStyleChanged={this.props.onStyleChanged} onStyleChanged={this.props.onStyleChanged}
mapStyle={this.props.mapStyle} mapStyle={this.props.mapStyle}
accessToken={this.props.accessToken} accessToken={this.props.accessToken}
onAccessTokenChanged={this.props.onAccessTokenChanged} onAccessTokenChanged={this.props.onAccessTokenChanged}
/> />
} }
if(this.props.workContext === "about") { if(this.props.workContext === "about") {
workspaceContent = <About /> workspaceContent = <About />
} }
return <div style={{ return <div style={{
...fullHeight, ...fullHeight,
zIndex: 100, top: 50,
left: 60, left: 0,
width: 300, zIndex: 100,
overflow: "hidden", width: 300,
backgroundColor: colors.gray} overflow: "hidden",
}> backgroundColor: colors.gray}
{workspaceContent} }>
</div> {workspaceContent}
} </div>
}
} }

View file

@ -62,7 +62,9 @@ module.exports = {
}] }]
}, },
node: { node: {
fs: "empty" fs: "empty",
net: 'empty',
tls: 'empty'
}, },
devServer: { devServer: {
contentBase: "./public", contentBase: "./public",