mirror of
https://github.com/maputnik/editor.git
synced 2025-12-08 23:30:00 +00:00
Compare commits
70 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
99acbd4d92 | ||
|
|
b0e9790382 | ||
|
|
e00cdde3af | ||
|
|
c3a634b216 | ||
|
|
4f26a521a0 | ||
|
|
ca6b48843c | ||
|
|
0eb00312f4 | ||
|
|
e7709dae15 | ||
|
|
03796c963b | ||
|
|
b50855a4a9 | ||
|
|
24a90b3c57 | ||
|
|
cf80e80025 | ||
|
|
48f10bcb73 | ||
|
|
7bc2323401 | ||
|
|
a71ac502d6 | ||
|
|
f2dd785e7b | ||
|
|
0b99e571c4 | ||
|
|
cfc6085718 | ||
|
|
384b2d4bea | ||
|
|
1058dbfb5a | ||
|
|
bda7ce7390 | ||
|
|
7b631b0510 | ||
|
|
1d7768e37c | ||
|
|
89d497c73f | ||
|
|
886c87f231 | ||
|
|
d567a4f98b | ||
|
|
5eb0e36faf | ||
|
|
51a2eabc91 | ||
|
|
007bdad70a | ||
|
|
1f1a919c77 | ||
|
|
3be3a716d4 | ||
|
|
ae9afdd8d9 | ||
|
|
a5307054b3 | ||
|
|
d16c3f4356 | ||
|
|
853361ace7 | ||
|
|
e41e1eb2f1 | ||
|
|
e36c233b49 | ||
|
|
d1b8f8d63e | ||
|
|
29cfb58a56 | ||
|
|
bf5131cadd | ||
|
|
ccc39b87db | ||
|
|
604be38b7c | ||
|
|
160bd9563b | ||
|
|
488fdf2bd5 | ||
|
|
a0e1e6152b | ||
|
|
58897f1856 | ||
|
|
80678af691 | ||
|
|
ba271e1fc6 | ||
|
|
c7ac90ba15 | ||
|
|
0dc335ea5f | ||
|
|
acac314d27 | ||
|
|
916c1dc9fc | ||
|
|
c159f7041f | ||
|
|
a3d586a75d | ||
|
|
6b0b29d1da | ||
|
|
8afda2fe28 | ||
|
|
beb1a2a8d1 | ||
|
|
436e0c2095 | ||
|
|
e1bc2a321a | ||
|
|
720c8f108b | ||
|
|
4db5c7cf68 | ||
|
|
8f561d8a27 | ||
|
|
0c483cffe3 | ||
|
|
def5ebb587 | ||
|
|
6e9e66b147 | ||
|
|
f332d517f3 | ||
|
|
04eab70e27 | ||
|
|
cfbcdc7fa1 | ||
|
|
c95dd75e2a | ||
|
|
4408f3ab3b |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,6 +1,8 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
|
||||
16
README.md
16
README.md
@@ -1,24 +1,18 @@
|
||||
# Maputnik [](https://travis-ci.org/maputnik/editor) [](https://ci.appveyor.com/project/lukasmartinelli/editor) [](https://tldrlegal.com/license/mit-license)
|
||||
|
||||
<img width="200" align="right" alt="Maputnik" src="media/maputnik.png" />
|
||||
<img width="200" align="right" alt="Maputnik" src="src/img/maputnik.png" />
|
||||
|
||||
A free and open visual editor for the [Mapbox GL styles](https://www.mapbox.com/mapbox-gl-style-spec/)
|
||||
targeted at developers and map designers. Creating your own custom map is easy with **Maputnik**.
|
||||
|
||||
Check it out at **http://maputnik.com/editor/**
|
||||
|
||||
*Maputnik is an early prototype and is under development.
|
||||
[Thanks to the supporters of the Kickstarter campaign who made this project possible](https://www.kickstarter.com/projects/174808720/maputnik-visual-map-editor-for-mapbox-gl)*.
|
||||
|
||||
## Features
|
||||
|
||||
- [x] Completely free and open source
|
||||
- [x] Visual interface for designing maps
|
||||
- [x] Immediate feedback (thanks to [style diffs](https://github.com/mapbox/mapbox-gl-style-spec/blob/mb-pages/lib/diff.js))
|
||||
- [x] Edit layers
|
||||
- [x] Easy to deploy as single HTML file
|
||||
- [ ] Support for Open Layers 3
|
||||
|
||||

|
||||
## Latest Status Update Video
|
||||
|
||||

|
||||
|
||||
## Develop
|
||||
|
||||
|
||||
BIN
media/demo.gif
BIN
media/demo.gif
Binary file not shown.
|
Before Width: | Height: | Size: 3.2 MiB |
@@ -19,18 +19,23 @@
|
||||
"license": "MIT",
|
||||
"homepage": "https://github.com/maputnik/editor#readme",
|
||||
"dependencies": {
|
||||
"codemirror": "^5.18.2",
|
||||
"color": "^1.0.3",
|
||||
"file-saver": "^1.3.2",
|
||||
"lodash.capitalize": "^4.2.1",
|
||||
"lodash.clonedeep": "^4.5.0",
|
||||
"lodash.isequal": "^4.4.0",
|
||||
"lodash.throttle": "^4.1.1",
|
||||
"lodash.topairs": "^4.3.0",
|
||||
"mapbox-gl": "mapbox/mapbox-gl-js#6c24b9621d2aa770eda67fb5638b4d78087b5624",
|
||||
"mapbox-gl-style-spec": "mapbox/mapbox-gl-style-spec#e85407a377510acb647161de6be6357ab4f606dd",
|
||||
"mapbox-gl": "^0.29.0",
|
||||
"mapbox-gl-style-spec": "^8.11.0",
|
||||
"mousetrap": "^1.6.0",
|
||||
"ol-mapbox-style": "0.0.11",
|
||||
"openlayers": "^3.19.1",
|
||||
"randomcolor": "^0.4.4",
|
||||
"react": "^15.4.0",
|
||||
"react-addons-pure-render-mixin": "^15.4.0",
|
||||
"react-autocomplete": "^1.4.0",
|
||||
"react-codemirror": "^0.3.0",
|
||||
"react-collapse": "^2.3.3",
|
||||
"react-color": "^2.10.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.cm-s-maputnik.CodeMirror {
|
||||
height: 100%;
|
||||
font-size: 10px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.cm-s-maputnik.CodeMirror, .cm-s-maputnik .CodeMirror-gutters {
|
||||
|
||||
@@ -1,25 +1,32 @@
|
||||
import React from 'react'
|
||||
import { saveAs } from 'file-saver'
|
||||
import Mousetrap from 'mousetrap'
|
||||
|
||||
import InspectionMap from './map/InspectionMap'
|
||||
import MapboxGlMap from './map/MapboxGlMap'
|
||||
import OpenLayers3Map from './map/OpenLayers3Map'
|
||||
import LayerList from './layers/LayerList'
|
||||
import LayerEditor from './layers/LayerEditor'
|
||||
import Toolbar from './Toolbar'
|
||||
import AppLayout from './AppLayout'
|
||||
import MessagePanel from './MessagePanel'
|
||||
|
||||
import GlSpec from 'mapbox-gl-style-spec/reference/latest.js'
|
||||
import validateStyleMin from 'mapbox-gl-style-spec/lib/validate_style.min'
|
||||
import style from '../libs/style.js'
|
||||
import { loadDefaultStyle, SettingsStore, StyleStore } from '../libs/stylestore'
|
||||
import { undoMessages, redoMessages } from '../libs/diffmessage'
|
||||
import { loadDefaultStyle, StyleStore } from '../libs/stylestore'
|
||||
import { ApiStyleStore } from '../libs/apistore'
|
||||
import { RevisionStore } from '../libs/revisions'
|
||||
import LayerWatcher from '../libs/layerwatcher'
|
||||
|
||||
|
||||
export default class App extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.layerWatcher = new LayerWatcher()
|
||||
this.styleStore = new ApiStyleStore()
|
||||
this.revisionStore = new RevisionStore()
|
||||
|
||||
this.styleStore.supported(isSupported => {
|
||||
if(!isSupported) {
|
||||
console.log('Falling back to local storage for storing styles')
|
||||
@@ -28,12 +35,29 @@ export default class App extends React.Component {
|
||||
this.styleStore.latestStyle(mapStyle => this.onStyleChanged(mapStyle))
|
||||
})
|
||||
|
||||
this.settingsStore = new SettingsStore()
|
||||
this.state = {
|
||||
accessToken: this.settingsStore.accessToken,
|
||||
errors: [],
|
||||
infos: [],
|
||||
mapStyle: style.emptyStyle,
|
||||
selectedLayerIndex: 0,
|
||||
sources: {},
|
||||
vectorLayers: {},
|
||||
}
|
||||
|
||||
this.layerWatcher = new LayerWatcher({
|
||||
onSourcesChange: v => this.setState({ sources: v }),
|
||||
onVectorLayersChange: v => this.setState({ vectorLayers: v })
|
||||
})
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
Mousetrap.bind(['ctrl+z'], this.onUndo.bind(this));
|
||||
Mousetrap.bind(['ctrl+y'], this.onRedo.bind(this));
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
Mousetrap.unbind(['ctrl+z'], this.onUndo.bind(this));
|
||||
Mousetrap.unbind(['ctrl+y'], this.onRedo.bind(this));
|
||||
}
|
||||
|
||||
onReset() {
|
||||
@@ -53,13 +77,39 @@ export default class App extends React.Component {
|
||||
}
|
||||
|
||||
onStyleChanged(newStyle) {
|
||||
this.saveStyle(newStyle)
|
||||
this.setState({ mapStyle: newStyle })
|
||||
const errors = validateStyleMin(newStyle, GlSpec)
|
||||
if(errors.length === 0) {
|
||||
this.revisionStore.addRevision(newStyle)
|
||||
this.saveStyle(newStyle)
|
||||
this.setState({
|
||||
mapStyle: newStyle,
|
||||
errors: [],
|
||||
})
|
||||
} else {
|
||||
this.setState({
|
||||
errors: errors.map(err => err.message)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
onAccessTokenChanged(newToken) {
|
||||
this.settingsStore.accessToken = newToken
|
||||
this.setState({ accessToken: newToken })
|
||||
onUndo() {
|
||||
const activeStyle = this.revisionStore.undo()
|
||||
const messages = undoMessages(this.state.mapStyle, activeStyle)
|
||||
this.saveStyle(activeStyle)
|
||||
this.setState({
|
||||
mapStyle: activeStyle,
|
||||
infos: messages,
|
||||
})
|
||||
}
|
||||
|
||||
onRedo() {
|
||||
const activeStyle = this.revisionStore.redo()
|
||||
const messages = redoMessages(this.state.mapStyle, activeStyle)
|
||||
this.saveStyle(activeStyle)
|
||||
this.setState({
|
||||
mapStyle: activeStyle,
|
||||
infos: messages,
|
||||
})
|
||||
}
|
||||
|
||||
onLayersChange(changedLayers) {
|
||||
@@ -91,20 +141,29 @@ export default class App extends React.Component {
|
||||
}
|
||||
|
||||
mapRenderer() {
|
||||
const metadata = this.state.mapStyle.metadata || {}
|
||||
const mapProps = {
|
||||
mapStyle: this.state.mapStyle,
|
||||
accessToken: this.state.accessToken,
|
||||
onMapLoaded: (map) => {
|
||||
this.layerWatcher.map = map
|
||||
}
|
||||
accessToken: metadata['maputnik:access_token'],
|
||||
onDataChange: (e) => {
|
||||
this.layerWatcher.analyzeMap(e.map)
|
||||
},
|
||||
//TODO: This would actually belong to the layout component
|
||||
style:{
|
||||
top: 40,
|
||||
//left: 500,
|
||||
}
|
||||
}
|
||||
|
||||
const metadata = this.state.mapStyle.metadata || {}
|
||||
const renderer = metadata['maputnik:renderer'] || 'mbgljs'
|
||||
|
||||
// Check if OL3 code has been loaded?
|
||||
if(renderer === 'ol3') {
|
||||
return <OpenLayers3Map {...mapProps} />
|
||||
} else if(renderer === 'inspection') {
|
||||
return <InspectionMap {...mapProps}
|
||||
sources={this.state.sources}
|
||||
highlightedLayer={this.state.mapStyle.layers[this.state.selectedLayerIndex]} />
|
||||
} else {
|
||||
return <MapboxGlMap {...mapProps} />
|
||||
}
|
||||
@@ -118,9 +177,11 @@ export default class App extends React.Component {
|
||||
render() {
|
||||
const layers = this.state.mapStyle.layers || []
|
||||
const selectedLayer = layers.length > 0 ? layers[this.state.selectedLayerIndex] : null
|
||||
const metadata = this.state.mapStyle.metadata || {}
|
||||
|
||||
const toolbar = <Toolbar
|
||||
mapStyle={this.state.mapStyle}
|
||||
sources={this.state.sources}
|
||||
onStyleChanged={this.onStyleChanged.bind(this)}
|
||||
onStyleOpen={this.onStyleChanged.bind(this)}
|
||||
onStyleDownload={this.onStyleDownload.bind(this)}
|
||||
@@ -135,17 +196,23 @@ export default class App extends React.Component {
|
||||
|
||||
const layerEditor = selectedLayer ? <LayerEditor
|
||||
layer={selectedLayer}
|
||||
sources={this.layerWatcher.sources}
|
||||
vectorLayers={this.layerWatcher.vectorLayers}
|
||||
sources={this.state.sources}
|
||||
vectorLayers={this.state.vectorLayers}
|
||||
onLayerChanged={this.onLayerChanged.bind(this)}
|
||||
onLayerIdChange={this.onLayerIdChange.bind(this)}
|
||||
/> : null
|
||||
|
||||
const bottomPanel = (this.state.errors.length + this.state.infos.length) > 0 ? <MessagePanel
|
||||
errors={this.state.errors}
|
||||
infos={this.state.infos}
|
||||
/> : null
|
||||
|
||||
return <AppLayout
|
||||
toolbar={toolbar}
|
||||
layerList={layerList}
|
||||
layerEditor={layerEditor}
|
||||
map={this.mapRenderer()}
|
||||
bottom={bottomPanel}
|
||||
/>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import ScrollContainer from './ScrollContainer'
|
||||
|
||||
import theme from '../config/theme'
|
||||
import colors from '../config/colors'
|
||||
import { fontSizes } from '../config/scales'
|
||||
|
||||
class AppLayout extends React.Component {
|
||||
static propTypes = {
|
||||
@@ -10,6 +11,7 @@ class AppLayout extends React.Component {
|
||||
layerList: React.PropTypes.element.isRequired,
|
||||
layerEditor: React.PropTypes.element,
|
||||
map: React.PropTypes.element.isRequired,
|
||||
bottom: React.PropTypes.element,
|
||||
}
|
||||
|
||||
static childContextTypes = {
|
||||
@@ -18,7 +20,7 @@ class AppLayout extends React.Component {
|
||||
|
||||
getChildContext() {
|
||||
return {
|
||||
reactIconBase: { size: 14 }
|
||||
reactIconBase: { size: fontSizes[3] }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,7 +53,7 @@ class AppLayout extends React.Component {
|
||||
top: 40,
|
||||
left: 200,
|
||||
zIndex: 1,
|
||||
width: 300,
|
||||
width: 350,
|
||||
backgroundColor: colors.black
|
||||
}}>
|
||||
<ScrollContainer>
|
||||
@@ -59,6 +61,18 @@ class AppLayout extends React.Component {
|
||||
</ScrollContainer>
|
||||
</div>
|
||||
{this.props.map}
|
||||
{this.props.bottom && <div style={{
|
||||
position: 'fixed',
|
||||
height: 50,
|
||||
bottom: 0,
|
||||
left: 550,
|
||||
zIndex: 1,
|
||||
width: '100%',
|
||||
backgroundColor: colors.black
|
||||
}}>
|
||||
{this.props.bottom}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ class Button extends React.Component {
|
||||
cursor: 'pointer',
|
||||
backgroundColor: colors.midgray,
|
||||
color: colors.lowgray,
|
||||
fontSize: fontSizes[4],
|
||||
fontSize: fontSizes[5],
|
||||
padding: margins[1],
|
||||
userSelect: 'none',
|
||||
borderRadius: 2,
|
||||
|
||||
44
src/components/MessagePanel.jsx
Normal file
44
src/components/MessagePanel.jsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import React from 'react'
|
||||
import Paragraph from './Paragraph'
|
||||
import colors from '../config/colors'
|
||||
import { fontSizes, margins } from '../config/scales'
|
||||
|
||||
class MessagePanel extends React.Component {
|
||||
static propTypes = {
|
||||
errors: React.PropTypes.array,
|
||||
infos: React.PropTypes.array,
|
||||
}
|
||||
|
||||
render() {
|
||||
const paragraphStyle = {
|
||||
margin: 0,
|
||||
lineHeight: 1.2,
|
||||
}
|
||||
|
||||
const errors = this.props.errors.map((m, i) => {
|
||||
return <Paragraph key={i}
|
||||
style={{
|
||||
...paragraphStyle,
|
||||
color: colors.red,
|
||||
}}>{m}</Paragraph>
|
||||
})
|
||||
|
||||
const infos = this.props.infos.map((m, i) => {
|
||||
return <Paragraph key={i}
|
||||
style={{
|
||||
...paragraphStyle,
|
||||
color: colors.lowgray,
|
||||
}}>{m}</Paragraph>
|
||||
})
|
||||
|
||||
return <div style={{
|
||||
padding: margins[1],
|
||||
}}>
|
||||
{errors}
|
||||
{infos}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default MessagePanel
|
||||
@@ -1,8 +1,7 @@
|
||||
import React from 'react'
|
||||
import scrollbars from './scrollbars.scss'
|
||||
|
||||
const ScrollContainer = (props) => {
|
||||
return <div className={scrollbars.darkScrollbar} style={{
|
||||
return <div style={{
|
||||
overflowX: "visible",
|
||||
overflowY: "scroll",
|
||||
bottom:0,
|
||||
|
||||
@@ -3,18 +3,21 @@ import FileReaderInput from 'react-file-reader-input'
|
||||
|
||||
import MdFileDownload from 'react-icons/lib/md/file-download'
|
||||
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 OpenIcon from 'react-icons/lib/md/open-in-browser'
|
||||
import SettingsIcon from 'react-icons/lib/md/settings'
|
||||
import MdInfo from 'react-icons/lib/md/info'
|
||||
import MdLayers from 'react-icons/lib/md/layers'
|
||||
import SourcesIcon from 'react-icons/lib/md/layers'
|
||||
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 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 HelpIcon from 'react-icons/lib/md/help-outline'
|
||||
import InspectionIcon from 'react-icons/lib/md/find-in-page'
|
||||
import AddIcon from 'react-icons/lib/md/add-circle-outline'
|
||||
|
||||
import logoImage from '../img/maputnik.png'
|
||||
import AddModal from './modals/AddModal'
|
||||
import SettingsModal from './modals/SettingsModal'
|
||||
import SourcesModal from './modals/SourcesModal'
|
||||
import OpenModal from './modals/OpenModal'
|
||||
@@ -29,7 +32,7 @@ const IconText = props => <span style={{ paddingLeft: margins[0] }}>
|
||||
|
||||
const actionStyle = {
|
||||
display: "inline-block",
|
||||
padding: 12.5,
|
||||
padding: 10,
|
||||
fontSize: fontSizes[4],
|
||||
cursor: "pointer",
|
||||
color: colors.white,
|
||||
@@ -79,6 +82,8 @@ export default class Toolbar extends React.Component {
|
||||
onStyleOpen: React.PropTypes.func.isRequired,
|
||||
// Current style is requested for download
|
||||
onStyleDownload: React.PropTypes.func.isRequired,
|
||||
// A dict of source id's and the available source layers
|
||||
sources: React.PropTypes.object.isRequired,
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
@@ -88,6 +93,7 @@ export default class Toolbar extends React.Component {
|
||||
settings: false,
|
||||
sources: false,
|
||||
open: false,
|
||||
add: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -99,6 +105,22 @@ export default class Toolbar extends React.Component {
|
||||
</ToolbarAction>
|
||||
}
|
||||
|
||||
toggleInspectionMode() {
|
||||
const metadata = this.props.mapStyle.metadata || {}
|
||||
const currentRenderer = metadata['maputnik:renderer'] || 'mbgljs'
|
||||
|
||||
const changedRenderer = currentRenderer === 'inspection' ? 'mbgljs' : 'inspection'
|
||||
|
||||
const changedStyle = {
|
||||
...this.props.mapStyle,
|
||||
metadata: {
|
||||
'maputnik:renderer': changedRenderer
|
||||
}
|
||||
}
|
||||
|
||||
this.props.onStyleChanged(changedStyle)
|
||||
}
|
||||
|
||||
toggleModal(modalName) {
|
||||
this.setState({
|
||||
isOpen: {
|
||||
@@ -135,6 +157,13 @@ export default class Toolbar extends React.Component {
|
||||
isOpen={this.state.isOpen.sources}
|
||||
onOpenToggle={this.toggleModal.bind(this, 'sources')}
|
||||
/>
|
||||
<AddModal
|
||||
mapStyle={this.props.mapStyle}
|
||||
sources={this.props.sources}
|
||||
isOpen={this.state.isOpen.add}
|
||||
onOpenToggle={this.toggleModal.bind(this, 'add')}
|
||||
onStyleChange={this.props.onStyleChanged}
|
||||
/>
|
||||
<ToolbarLink
|
||||
href={"https://github.com/maputnik/editor"}
|
||||
style={{
|
||||
@@ -144,28 +173,32 @@ export default class Toolbar extends React.Component {
|
||||
padding: 5,
|
||||
}}
|
||||
>
|
||||
<img src="https://github.com/maputnik/editor/raw/master/media/maputnik.png" alt="Maputnik" style={{width: 30, height: 30, paddingRight: 5, verticalAlign: 'middle'}}/>
|
||||
<img src={logoImage} alt="Maputnik" style={{width: 30, height: 30, paddingRight: 5, verticalAlign: 'middle'}}/>
|
||||
<span style={{fontSize: 20, verticalAlign: 'middle' }}>Maputnik</span>
|
||||
</ToolbarLink>
|
||||
<ToolbarAction onClick={this.toggleModal.bind(this, 'open')}>
|
||||
<MdOpenInBrowser />
|
||||
<OpenIcon />
|
||||
<IconText>Open</IconText>
|
||||
</ToolbarAction>
|
||||
{this.downloadButton()}
|
||||
<ToolbarAction onClick={this.toggleModal.bind(this, 'add')}>
|
||||
<AddIcon />
|
||||
<IconText>Add Layer</IconText>
|
||||
</ToolbarAction>
|
||||
<ToolbarAction onClick={this.toggleModal.bind(this, 'sources')}>
|
||||
<MdLayers />
|
||||
<SourcesIcon />
|
||||
<IconText>Sources</IconText>
|
||||
</ToolbarAction>
|
||||
<ToolbarAction onClick={this.toggleModal.bind(this, 'settings')}>
|
||||
<MdSettings />
|
||||
<SettingsIcon />
|
||||
<IconText>Style Settings</IconText>
|
||||
</ToolbarAction>
|
||||
<ToolbarAction onClick={this.toggleModal.bind(this, 'settings')}>
|
||||
<MdFindInPage />
|
||||
<ToolbarAction onClick={this.toggleInspectionMode.bind(this)}>
|
||||
<InspectionIcon />
|
||||
<IconText>Inspect</IconText>
|
||||
</ToolbarAction>
|
||||
<ToolbarLink href={"https://github.com/maputnik/editor/wiki"}>
|
||||
<MdHelpOutline />
|
||||
<HelpIcon />
|
||||
<IconText>Help</IconText>
|
||||
</ToolbarLink>
|
||||
</div>
|
||||
|
||||
@@ -17,6 +17,7 @@ class ColorField extends React.Component {
|
||||
value: React.PropTypes.string,
|
||||
doc: React.PropTypes.string,
|
||||
style: React.PropTypes.object,
|
||||
default: React.PropTypes.string,
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
@@ -35,13 +36,13 @@ class ColorField extends React.Component {
|
||||
const pos = elem.getBoundingClientRect()
|
||||
return {
|
||||
top: pos.top,
|
||||
left: pos.left + 165,
|
||||
left: pos.left + 196,
|
||||
}
|
||||
} else {
|
||||
console.warn('Color field has no element to adjust position')
|
||||
return {
|
||||
top: 160,
|
||||
left: 500,
|
||||
left: 555,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -81,7 +82,6 @@ class ColorField extends React.Component {
|
||||
</div>
|
||||
|
||||
return <div style={{
|
||||
...input.property,
|
||||
position: 'relative',
|
||||
display: 'inline',
|
||||
}}>
|
||||
@@ -90,7 +90,7 @@ class ColorField extends React.Component {
|
||||
ref="colorInput"
|
||||
onClick={this.togglePicker.bind(this)}
|
||||
style={{
|
||||
...input.select,
|
||||
...input.input,
|
||||
...this.props.style
|
||||
}}
|
||||
name={this.props.name}
|
||||
|
||||
50
src/components/fields/DocLabel.jsx
Normal file
50
src/components/fields/DocLabel.jsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import React from 'react'
|
||||
import input from '../../config/input.js'
|
||||
import colors from '../../config/colors.js'
|
||||
import { margins, fontSizes } from '../../config/scales.js'
|
||||
|
||||
export default class DocLabel extends React.Component {
|
||||
static propTypes = {
|
||||
label: React.PropTypes.string.isRequired,
|
||||
doc: React.PropTypes.string.isRequired,
|
||||
style: React.PropTypes.object,
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = { showDoc: false }
|
||||
}
|
||||
|
||||
render() {
|
||||
return <label
|
||||
style={{
|
||||
...input.label,
|
||||
...this.props.style,
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
<span
|
||||
onMouseOver={e => this.setState({showDoc: true})}
|
||||
onMouseOut={e => this.setState({showDoc: false})}
|
||||
style={{
|
||||
cursor: 'help',
|
||||
}}
|
||||
>
|
||||
{this.props.label}
|
||||
</span>
|
||||
<div style={{
|
||||
backgroundColor: colors.gray,
|
||||
padding: margins[1],
|
||||
fontSize: 10,
|
||||
position: 'absolute',
|
||||
top: 20,
|
||||
left: 0,
|
||||
width: 120,
|
||||
display: this.state.showDoc ? null : 'none',
|
||||
zIndex: 3,
|
||||
}}>
|
||||
{this.props.doc}
|
||||
</div>
|
||||
</label>
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
import React from 'react'
|
||||
import input from '../../config/input.js'
|
||||
|
||||
class EnumField extends React.Component {
|
||||
static propTypes = {
|
||||
onChange: React.PropTypes.func.isRequired,
|
||||
name: React.PropTypes.string.isRequired,
|
||||
value: React.PropTypes.string,
|
||||
allowedValues: React.PropTypes.array.isRequired,
|
||||
doc: React.PropTypes.string,
|
||||
style: React.PropTypes.object,
|
||||
}
|
||||
|
||||
onChange(e) {
|
||||
return this.props.onChange(e.target.value)
|
||||
}
|
||||
|
||||
render() {
|
||||
const options = this.props.allowedValues.map(val => {
|
||||
return <option key={val} value={val}>{val}</option>
|
||||
})
|
||||
|
||||
return <select
|
||||
style={{
|
||||
...input.select,
|
||||
...this.props.style
|
||||
}}
|
||||
value={this.props.value}
|
||||
onChange={this.onChange.bind(this)}
|
||||
>
|
||||
{options}
|
||||
</select>
|
||||
}
|
||||
}
|
||||
|
||||
export default EnumField
|
||||
@@ -1,50 +0,0 @@
|
||||
import React from 'react'
|
||||
import input from '../../config/input.js'
|
||||
|
||||
/*** Number fields with support for min, max and units and documentation*/
|
||||
class NumberField extends React.Component {
|
||||
static propTypes = {
|
||||
onChange: React.PropTypes.func.isRequired,
|
||||
name: React.PropTypes.string.isRequired,
|
||||
value: React.PropTypes.number,
|
||||
default: React.PropTypes.number,
|
||||
unit: React.PropTypes.string,
|
||||
min: React.PropTypes.number,
|
||||
max: React.PropTypes.number,
|
||||
doc: React.PropTypes.string,
|
||||
style: React.PropTypes.object,
|
||||
}
|
||||
|
||||
onChange(e) {
|
||||
const value = parseFloat(e.target.value)
|
||||
/*TODO: we can do range validation already here?
|
||||
if(this.props.min && value < this.props.min) return
|
||||
if(this.props.max && value > this.props.max) return
|
||||
*/
|
||||
this.props.onChange(value)
|
||||
}
|
||||
|
||||
render() {
|
||||
let stepSize = null
|
||||
if(this.props.max && this.props.min) {
|
||||
stepSize = (this.props.max - this.props.min) / 10
|
||||
}
|
||||
|
||||
return <input
|
||||
style={{
|
||||
...input.input,
|
||||
...this.props.style
|
||||
}}
|
||||
type="number"
|
||||
min={this.props.min}
|
||||
max={this.props.max}
|
||||
step={stepSize}
|
||||
name={this.props.name}
|
||||
placeholder={this.props.default}
|
||||
value={this.props.value}
|
||||
onChange={this.onChange.bind(this)}
|
||||
/>
|
||||
}
|
||||
}
|
||||
|
||||
export default NumberField
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react'
|
||||
import GlSpec from 'mapbox-gl-style-spec/reference/latest.min.js'
|
||||
import GlSpec from 'mapbox-gl-style-spec/reference/latest.js'
|
||||
|
||||
import ZoomSpecField from './ZoomSpecField'
|
||||
import colors from '../../config/colors'
|
||||
@@ -31,7 +31,7 @@ export default class PropertyGroup extends React.Component {
|
||||
|
||||
onPropertyChange(property, newValue) {
|
||||
const group = getGroupName(this.props.layer.type, property)
|
||||
this.props.onChange(group , property ,newValue)
|
||||
this.props.onChange(group , property, newValue)
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -40,7 +40,7 @@ export default class PropertyGroup extends React.Component {
|
||||
|
||||
const paint = this.props.layer.paint || {}
|
||||
const layout = this.props.layer.layout || {}
|
||||
const fieldValue = paint[fieldName] || layout[fieldName]
|
||||
const fieldValue = fieldName in paint ? paint[fieldName] : layout[fieldName]
|
||||
|
||||
return <ZoomSpecField
|
||||
onChange={this.onPropertyChange.bind(this)}
|
||||
@@ -51,11 +51,7 @@ export default class PropertyGroup extends React.Component {
|
||||
/>
|
||||
})
|
||||
|
||||
return <div style={{
|
||||
padding: margins[2],
|
||||
paddingRight: 0,
|
||||
backgroundColor: colors.black,
|
||||
}}>
|
||||
return <div>
|
||||
{fields}
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -2,11 +2,16 @@ import React from 'react'
|
||||
import color from 'color'
|
||||
|
||||
import GlSpec from 'mapbox-gl-style-spec/reference/latest.min.js'
|
||||
import NumberField from './NumberField'
|
||||
import EnumField from './EnumField'
|
||||
import BooleanField from './BooleanField'
|
||||
import ColorField from './ColorField'
|
||||
import StringField from './StringField'
|
||||
import NumberInput from '../inputs/NumberInput'
|
||||
import CheckboxInput from '../inputs/CheckboxInput'
|
||||
import StringInput from '../inputs/StringInput'
|
||||
import SelectInput from '../inputs/SelectInput'
|
||||
import MultiButtonInput from '../inputs/MultiButtonInput'
|
||||
import ArrayInput from '../inputs/ArrayInput'
|
||||
import FontInput from '../inputs/FontInput'
|
||||
import capitalize from 'lodash.capitalize'
|
||||
|
||||
|
||||
import input from '../../config/input.js'
|
||||
|
||||
@@ -18,6 +23,14 @@ function labelFromFieldName(fieldName) {
|
||||
return label
|
||||
}
|
||||
|
||||
function optionsLabelLength(options) {
|
||||
let sum = 0;
|
||||
options.forEach(([_, label]) => {
|
||||
sum += label.length
|
||||
})
|
||||
return sum
|
||||
}
|
||||
|
||||
/** Display any field from the Mapbox GL style spec and
|
||||
* choose the correct field component based on the @{fieldSpec}
|
||||
* to display @{value}. */
|
||||
@@ -26,10 +39,10 @@ export default class SpecField extends React.Component {
|
||||
onChange: React.PropTypes.func.isRequired,
|
||||
fieldName: React.PropTypes.string.isRequired,
|
||||
fieldSpec: React.PropTypes.object.isRequired,
|
||||
|
||||
value: React.PropTypes.oneOfType([
|
||||
React.PropTypes.string,
|
||||
React.PropTypes.number,
|
||||
React.PropTypes.array,
|
||||
]),
|
||||
/** Override the style of the field */
|
||||
style: React.PropTypes.object,
|
||||
@@ -37,30 +50,36 @@ export default class SpecField extends React.Component {
|
||||
|
||||
render() {
|
||||
const commonProps = {
|
||||
doc: this.props.fieldSpec.doc,
|
||||
style: this.props.style,
|
||||
default: this.props.fieldSpec.default,
|
||||
value: this.props.value,
|
||||
name: this.props.fieldName,
|
||||
onChange: newValue => this.props.onChange(this.props.fieldName, newValue)
|
||||
}
|
||||
switch(this.props.fieldSpec.type) {
|
||||
case 'number': return (
|
||||
<NumberField
|
||||
<NumberInput
|
||||
{...commonProps}
|
||||
default={this.props.fieldSpec.default}
|
||||
min={this.props.fieldSpec.minimum}
|
||||
max={this.props.fieldSpec.maximum}
|
||||
unit={this.props.fieldSpec.unit}
|
||||
/>
|
||||
)
|
||||
case 'enum': return (
|
||||
<EnumField
|
||||
{...commonProps}
|
||||
allowedValues={Object.keys(this.props.fieldSpec.values)}
|
||||
/>
|
||||
)
|
||||
case 'enum':
|
||||
const options = Object.keys(this.props.fieldSpec.values).map(v => [v, capitalize(v)])
|
||||
|
||||
if(options.length <= 3 && optionsLabelLength(options) <= 20) {
|
||||
return <MultiButtonInput
|
||||
{...commonProps}
|
||||
options={options}
|
||||
/>
|
||||
} else {
|
||||
return <SelectInput
|
||||
{...commonProps}
|
||||
options={options}
|
||||
/>
|
||||
}
|
||||
case 'string': return (
|
||||
<StringField
|
||||
<StringInput
|
||||
{...commonProps}
|
||||
/>
|
||||
)
|
||||
@@ -70,10 +89,22 @@ export default class SpecField extends React.Component {
|
||||
/>
|
||||
)
|
||||
case 'boolean': return (
|
||||
<BooleanField
|
||||
<CheckboxInput
|
||||
{...commonProps}
|
||||
/>
|
||||
)
|
||||
case 'array':
|
||||
if(this.props.fieldName === 'text-font') {
|
||||
return <FontInput
|
||||
{...commonProps}
|
||||
/>
|
||||
} else {
|
||||
return <ArrayInput
|
||||
{...commonProps}
|
||||
type={this.props.fieldSpec.value}
|
||||
length={this.props.fieldSpec.length}
|
||||
/>
|
||||
}
|
||||
default: return null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
import React from 'react'
|
||||
import input from '../../config/input.js'
|
||||
|
||||
/*** Number fields with support for min, max and units and documentation*/
|
||||
class StringField extends React.Component {
|
||||
static propTypes = {
|
||||
onChange: React.PropTypes.func.isRequired,
|
||||
name: React.PropTypes.string.isRequired,
|
||||
value: React.PropTypes.string,
|
||||
default: React.PropTypes.number,
|
||||
doc: React.PropTypes.string,
|
||||
style: React.PropTypes.object,
|
||||
}
|
||||
|
||||
onChange(e) {
|
||||
const value = e.target.value
|
||||
return this.props.onChange(value === "" ? null: value)
|
||||
}
|
||||
|
||||
render() {
|
||||
return <input
|
||||
style={{
|
||||
...input.input,
|
||||
...this.props.style
|
||||
}}
|
||||
name={this.props.name}
|
||||
placeholder={this.props.default}
|
||||
value={this.props.value ? this.props.value : ""}
|
||||
onChange={this.onChange.bind(this)}
|
||||
/>
|
||||
}
|
||||
}
|
||||
|
||||
export default StringField
|
||||
@@ -1,27 +1,23 @@
|
||||
import React from 'react'
|
||||
import Color from 'color'
|
||||
|
||||
import NumberField from './NumberField'
|
||||
import EnumField from './EnumField'
|
||||
import BooleanField from './BooleanField'
|
||||
import ColorField from './ColorField'
|
||||
import StringField from './StringField'
|
||||
import Button from '../Button'
|
||||
import SpecField from './SpecField'
|
||||
import NumberInput from '../inputs/NumberInput'
|
||||
import DocLabel from './DocLabel'
|
||||
|
||||
import AddIcon from 'react-icons/lib/md/add-circle-outline'
|
||||
import DeleteIcon from 'react-icons/lib/md/delete'
|
||||
|
||||
import capitalize from 'lodash.capitalize'
|
||||
import input from '../../config/input.js'
|
||||
import colors from '../../config/colors.js'
|
||||
import { margins } from '../../config/scales.js'
|
||||
import { margins, fontSizes } from '../../config/scales.js'
|
||||
|
||||
function isZoomField(value) {
|
||||
return typeof value === 'object' && value.stops
|
||||
}
|
||||
|
||||
const specFieldProps = {
|
||||
onChange: React.PropTypes.func.isRequired,
|
||||
fieldName: React.PropTypes.string.isRequired,
|
||||
fieldSpec: React.PropTypes.object.isRequired,
|
||||
}
|
||||
|
||||
/** Supports displaying spec field for zoom function objects
|
||||
* https://www.mapbox.com/mapbox-gl-style-spec/#types-function-zoom-property
|
||||
*/
|
||||
@@ -39,50 +35,117 @@ export default class ZoomSpecField extends React.Component {
|
||||
]),
|
||||
}
|
||||
|
||||
addStop() {
|
||||
const stops = this.props.value.stops.slice(0)
|
||||
const lastStop = stops[stops.length - 1]
|
||||
stops.push([lastStop[0] + 1, lastStop[1]])
|
||||
|
||||
const changedValue = {
|
||||
...this.props.value,
|
||||
stops: stops,
|
||||
}
|
||||
|
||||
this.props.onChange(this.props.fieldName, changedValue)
|
||||
}
|
||||
|
||||
deleteStop(stopIdx) {
|
||||
const stops = this.props.value.stops.slice(0)
|
||||
stops.splice(stopIdx, 1)
|
||||
|
||||
let changedValue = {
|
||||
...this.props.value,
|
||||
stops: stops,
|
||||
}
|
||||
|
||||
if(stops.length === 1) {
|
||||
changedValue = stops[0][1]
|
||||
}
|
||||
|
||||
this.props.onChange(this.props.fieldName, changedValue)
|
||||
}
|
||||
|
||||
changeStop(changeIdx, zoomLevel, value) {
|
||||
const stops = this.props.value.stops.slice(0)
|
||||
stops[changeIdx] = [zoomLevel, value]
|
||||
const changedValue = {
|
||||
...this.props.value,
|
||||
stops: stops,
|
||||
}
|
||||
this.props.onChange(this.props.fieldName, changedValue)
|
||||
}
|
||||
|
||||
render() {
|
||||
const label = <label style={input.label}>
|
||||
{labelFromFieldName(this.props.fieldName)}
|
||||
</label>
|
||||
let label = <DocLabel
|
||||
label={labelFromFieldName(this.props.fieldName)}
|
||||
doc={this.props.fieldSpec.doc}
|
||||
style={{
|
||||
width: '50%',
|
||||
}}
|
||||
/>
|
||||
|
||||
if(isZoomField(this.props.value)) {
|
||||
const zoomFields = this.props.value.stops.map(stop => {
|
||||
const zoomFields = this.props.value.stops.map((stop, idx) => {
|
||||
label = <DocLabel
|
||||
doc={this.props.fieldSpec.doc}
|
||||
style={{ width: '42.5%'}}
|
||||
label={idx > 0 ? "" : labelFromFieldName(this.props.fieldName)}
|
||||
/>
|
||||
|
||||
if(idx === 1) {
|
||||
label = <label style={{...input.label, width: '42.5%'}}>
|
||||
<Button
|
||||
style={{fontSize: fontSizes[5]}}
|
||||
onClick={this.addStop.bind(this)}
|
||||
>
|
||||
Add stop
|
||||
</Button>
|
||||
</label>
|
||||
}
|
||||
|
||||
const zoomLevel = stop[0]
|
||||
const value = stop[1]
|
||||
|
||||
return <div style={input.property} key={zoomLevel}>
|
||||
return <div key={zoomLevel} style={{
|
||||
...input.property,
|
||||
marginLeft: 0,
|
||||
marginRight: 0
|
||||
}}>
|
||||
{label}
|
||||
<SpecField {...this.props}
|
||||
value={value}
|
||||
<Button
|
||||
style={{backgroundColor: null}}
|
||||
onClick={this.deleteStop.bind(this, idx)}
|
||||
>
|
||||
<DeleteIcon />
|
||||
</Button>
|
||||
<NumberInput
|
||||
style={{
|
||||
width: '33%'
|
||||
width: '7%',
|
||||
}}
|
||||
/>
|
||||
|
||||
<input
|
||||
style={{
|
||||
...input.input,
|
||||
width: '10%',
|
||||
marginLeft: margins[0],
|
||||
}}
|
||||
type="number"
|
||||
value={zoomLevel}
|
||||
onChange={changedStop => this.changeStop(idx, changedStop, value)}
|
||||
min={0}
|
||||
max={22}
|
||||
/>
|
||||
<SpecField
|
||||
fieldName={this.props.fieldName}
|
||||
fieldSpec={this.props.fieldSpec}
|
||||
value={value}
|
||||
onChange={(_, newValue) => this.changeStop(idx, zoomLevel, newValue)}
|
||||
style={{
|
||||
width: '42%',
|
||||
marginLeft: margins[0],
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
})
|
||||
return <div style={{
|
||||
border: 1,
|
||||
borderStyle: 'solid',
|
||||
borderColor: Color(colors.gray).lighten(0.1).string(),
|
||||
padding: margins[1],
|
||||
}}>
|
||||
|
||||
return <div style={input.property}>
|
||||
{zoomFields}
|
||||
</div>
|
||||
} else {
|
||||
return <div style={input.property}>
|
||||
{label}
|
||||
<SpecField {...this.props} />
|
||||
<SpecField {...this.props} style={{ width: '50%' } }/>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -90,8 +153,5 @@ export default class ZoomSpecField extends React.Component {
|
||||
|
||||
function labelFromFieldName(fieldName) {
|
||||
let label = fieldName.split('-').slice(1).join(' ')
|
||||
if(label.length > 0) {
|
||||
label = label.charAt(0).toUpperCase() + label.slice(1);
|
||||
}
|
||||
return label
|
||||
return capitalize(label)
|
||||
}
|
||||
|
||||
@@ -5,12 +5,28 @@ import input from '../../config/input.js'
|
||||
import colors from '../../config/colors.js'
|
||||
import { margins } from '../../config/scales.js'
|
||||
|
||||
import SelectInput from '../inputs/SelectInput'
|
||||
import StringInput from '../inputs/StringInput'
|
||||
import AutocompleteInput from '../inputs/AutocompleteInput'
|
||||
|
||||
const combiningFilterOps = ['all', 'any', 'none']
|
||||
const setFilterOps = ['in', '!in']
|
||||
const otherFilterOps = Object
|
||||
.keys(GlSpec.filter_operator.values)
|
||||
.filter(op => combiningFilterOps.indexOf(op) < 0)
|
||||
|
||||
function hasCombiningFilter(filter) {
|
||||
return combiningFilterOps.indexOf(filter[0]) >= 0
|
||||
}
|
||||
|
||||
function hasNestedCombiningFilter(filter) {
|
||||
if(hasCombiningFilter(filter)) {
|
||||
const combinedFilters = filter.slice(1)
|
||||
return filter.slice(1).map(f => hasCombiningFilter(f)).filter(f => f == true).length > 0
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
class CombiningOperatorSelect extends React.Component {
|
||||
static propTypes = {
|
||||
value: React.PropTypes.string.isRequired,
|
||||
@@ -22,12 +38,16 @@ class CombiningOperatorSelect extends React.Component {
|
||||
return <option key={op} value={op}>{op}</option>
|
||||
})
|
||||
|
||||
return <div>
|
||||
return <div
|
||||
style={{
|
||||
marginTop: margins[1],
|
||||
marginBottom: margins[1],
|
||||
}}
|
||||
>
|
||||
<select
|
||||
style={{
|
||||
...input.select,
|
||||
width: '20.5%',
|
||||
margin: margins[0],
|
||||
}}
|
||||
value={this.props.value}
|
||||
onChange={e => this.props.onChange(e.target.value)}
|
||||
@@ -52,20 +72,15 @@ class OperatorSelect extends React.Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const options = otherFilterOps.map(op => {
|
||||
return <option key={op} value={op}>{op}</option>
|
||||
})
|
||||
return <select
|
||||
return <SelectInput
|
||||
style={{
|
||||
...input.select,
|
||||
width: '15%',
|
||||
margin: margins[0]
|
||||
}}
|
||||
value={this.props.value}
|
||||
onChange={e => this.props.onChange(e.target.value)}
|
||||
>
|
||||
{options}
|
||||
</select>
|
||||
onChange={this.props.onChange}
|
||||
options={otherFilterOps.map(op => [op, op])}
|
||||
/>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,33 +106,30 @@ class SingleFilterEditor extends React.Component {
|
||||
const propertyName = f[1]
|
||||
const filterArgs = f.slice(2)
|
||||
|
||||
return <div>
|
||||
<select
|
||||
style={{
|
||||
...input.select,
|
||||
width: '17%',
|
||||
margin: margins[0]
|
||||
return <div style={{
|
||||
marginTop: margins[1],
|
||||
marginBottom: margins[1],
|
||||
}}>
|
||||
<AutocompleteInput
|
||||
wrapperStyle={{
|
||||
width: '31%',
|
||||
marginRight: margins[0]
|
||||
}}
|
||||
value={propertyName}
|
||||
options={Object.keys(this.props.properties).map(propName => [propName, propName])}
|
||||
onChange={newPropertyName => this.onFilterPartChanged(filterOp, newPropertyName, filterArgs)}
|
||||
>
|
||||
{Object.keys(this.props.properties).map(propName => {
|
||||
return <option key={propName} value={propName}>{propName}</option>
|
||||
})}
|
||||
</select>
|
||||
/>
|
||||
<OperatorSelect
|
||||
value={filterOp}
|
||||
onChange={newFilterOp => this.onFilterPartChanged(newFilterOp, propertyName, filterArgs)}
|
||||
/>
|
||||
<input
|
||||
<StringInput
|
||||
style={{
|
||||
...input.input,
|
||||
width: '53%',
|
||||
margin: margins[0]
|
||||
width: '50%',
|
||||
marginLeft: margins[0]
|
||||
}}
|
||||
value={filterArgs.join(',')}
|
||||
onChange={e => {
|
||||
this.onFilterPartChanged(filterOp, propertyName, e.target.value.split(','))}}
|
||||
onChange={ v=> this.onFilterPartChanged(filterOp, propertyName, v.split(','))}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
@@ -165,11 +177,12 @@ export default class CombiningFilterEditor extends React.Component {
|
||||
/>
|
||||
})
|
||||
|
||||
return <div style={{
|
||||
padding: margins[2],
|
||||
paddingRight: 0,
|
||||
backgroundColor: colors.black
|
||||
}}>
|
||||
//TODO: Implement support for nested filter
|
||||
if(hasNestedCombiningFilter(filter)) {
|
||||
return null
|
||||
}
|
||||
|
||||
return <div>
|
||||
<CombiningOperatorSelect
|
||||
value={combiningOp}
|
||||
onChange={this.onFilterPartChanged.bind(this, 0)}
|
||||
|
||||
15
src/components/icons/CircleIcon.jsx
Normal file
15
src/components/icons/CircleIcon.jsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import React from 'react'
|
||||
import IconBase from 'react-icon-base'
|
||||
|
||||
|
||||
export default class FillIcon extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<IconBase viewBox="0 0 20 20" {...this.props}>
|
||||
<path transform="translate(2 2)" d="M7.5,0C11.6422,0,15,3.3578,15,7.5S11.6422,15,7.5,15 S0,11.6422,0,7.5S3.3578,0,7.5,0z M7.5,1.6666c-3.2217,0-5.8333,2.6117-5.8333,5.8334S4.2783,13.3334,7.5,13.3334 s5.8333-2.6117,5.8333-5.8334S10.7217,1.6666,7.5,1.6666z"></path>
|
||||
</IconBase>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import LineIcon from './LineIcon.jsx'
|
||||
import FillIcon from './FillIcon.jsx'
|
||||
import SymbolIcon from './SymbolIcon.jsx'
|
||||
import BackgroundIcon from './BackgroundIcon.jsx'
|
||||
import CircleIcon from './CircleIcon.jsx'
|
||||
|
||||
class LayerIcon extends React.Component {
|
||||
static propTypes = {
|
||||
@@ -14,10 +15,12 @@ class LayerIcon extends React.Component {
|
||||
render() {
|
||||
const iconProps = { style: this.props.style }
|
||||
switch(this.props.type) {
|
||||
case 'fill-extrusion': return <BackgroundIcon {...iconProps} />
|
||||
case 'fill': return <FillIcon {...iconProps} />
|
||||
case 'background': return <BackgroundIcon {...iconProps} />
|
||||
case 'line': return <LineIcon {...iconProps} />
|
||||
case 'symbol': return <SymbolIcon {...iconProps} />
|
||||
case 'circle': return <CircleIcon {...iconProps} />
|
||||
default: return null
|
||||
}
|
||||
}
|
||||
|
||||
58
src/components/inputs/ArrayInput.jsx
Normal file
58
src/components/inputs/ArrayInput.jsx
Normal file
@@ -0,0 +1,58 @@
|
||||
import React from 'react'
|
||||
import input from '../../config/input.js'
|
||||
import StringInput from './StringInput'
|
||||
import NumberInput from './NumberInput'
|
||||
|
||||
import { margins } from '../../config/scales.js'
|
||||
|
||||
class ArrayInput extends React.Component {
|
||||
static propTypes = {
|
||||
value: React.PropTypes.array,
|
||||
type: React.PropTypes.string,
|
||||
length: React.PropTypes.number,
|
||||
default: React.PropTypes.array,
|
||||
style: React.PropTypes.object,
|
||||
onChange: React.PropTypes.func,
|
||||
}
|
||||
|
||||
changeValue(idx, newValue) {
|
||||
console.log(idx, newValue)
|
||||
const values = this.values.slice(0)
|
||||
values[idx] = newValue
|
||||
this.props.onChange(values)
|
||||
}
|
||||
|
||||
get values() {
|
||||
return this.props.value || this.props.default || []
|
||||
}
|
||||
|
||||
render() {
|
||||
const commonStyle = {
|
||||
width: '49%',
|
||||
marginRight: '1%',
|
||||
}
|
||||
const inputs = this.values.map((v, i) => {
|
||||
if(this.props.type === 'number') {
|
||||
return <NumberInput
|
||||
key={i}
|
||||
value={v}
|
||||
style={commonStyle}
|
||||
onChange={this.changeValue.bind(this, i)}
|
||||
/>
|
||||
} else {
|
||||
return <StringInput
|
||||
key={i}
|
||||
value={v}
|
||||
style={commonStyle}
|
||||
onChange={this.changeValue.bind(this, i)}
|
||||
/>
|
||||
}
|
||||
})
|
||||
|
||||
return <div style={{display: 'inline-block', width: '50%'}}>
|
||||
{inputs}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
export default ArrayInput
|
||||
68
src/components/inputs/AutocompleteInput.jsx
Normal file
68
src/components/inputs/AutocompleteInput.jsx
Normal file
@@ -0,0 +1,68 @@
|
||||
import React from 'react'
|
||||
import Autocomplete from 'react-autocomplete'
|
||||
import input from '../../config/input'
|
||||
import colors from '../../config/colors'
|
||||
import { margins, fontSizes } from '../../config/scales'
|
||||
|
||||
class AutocompleteInput extends React.Component {
|
||||
static propTypes = {
|
||||
value: React.PropTypes.string,
|
||||
options: React.PropTypes.array,
|
||||
wrapperStyle: React.PropTypes.object,
|
||||
inputStyle: React.PropTypes.object,
|
||||
onChange: React.PropTypes.func,
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
onChange: () => {},
|
||||
options: [],
|
||||
}
|
||||
|
||||
render() {
|
||||
return <Autocomplete
|
||||
menuStyle={{
|
||||
border: 'none',
|
||||
padding: '2px 0',
|
||||
position: 'fixed',
|
||||
overflow: 'auto',
|
||||
maxHeight: '50%',
|
||||
background: colors.gray,
|
||||
zIndex: 3,
|
||||
}}
|
||||
wrapperStyle={{
|
||||
display: 'inline-block',
|
||||
...this.props.wrapperStyle
|
||||
}}
|
||||
inputProps={{
|
||||
style: {
|
||||
...input.input,
|
||||
width: '100%',
|
||||
...this.props.inputStyle,
|
||||
}
|
||||
}}
|
||||
value={this.props.value}
|
||||
items={this.props.options}
|
||||
getItemValue={(item) => item[0]}
|
||||
onSelect={v => this.props.onChange(v)}
|
||||
onChange={(e, v) => this.props.onChange(v)}
|
||||
renderItem={(item, isHighlighted) => (
|
||||
<div
|
||||
key={item[0]}
|
||||
style={{
|
||||
userSelect: 'none',
|
||||
color: colors.lowgray,
|
||||
cursor: 'default',
|
||||
background: isHighlighted ? colors.midgray : colors.gray,
|
||||
padding: margins[0],
|
||||
fontSize: fontSizes[5],
|
||||
zIndex: 3,
|
||||
}}
|
||||
>
|
||||
{item[1]}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
}
|
||||
}
|
||||
|
||||
export default AutocompleteInput
|
||||
@@ -1,16 +1,13 @@
|
||||
import React from 'react'
|
||||
|
||||
import input from '../../config/input'
|
||||
import input from '../../config/input.js'
|
||||
import colors from '../../config/colors'
|
||||
import { margins } from '../../config/scales'
|
||||
|
||||
class BooleanField extends React.Component {
|
||||
class CheckboxInput extends React.Component {
|
||||
static propTypes = {
|
||||
onChange: React.PropTypes.func.isRequired,
|
||||
name: React.PropTypes.string.isRequired,
|
||||
value: React.PropTypes.bool,
|
||||
doc: React.PropTypes.string,
|
||||
value: React.PropTypes.string,
|
||||
style: React.PropTypes.object,
|
||||
onChange: React.PropTypes.func,
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -57,7 +54,6 @@ class BooleanField extends React.Component {
|
||||
...styles.input,
|
||||
...this.props.style,
|
||||
}}
|
||||
value={this.props.value}
|
||||
onChange={e => {this.props.onChange(!this.props.value)}}
|
||||
checked={this.props.value}
|
||||
/>
|
||||
@@ -72,4 +68,4 @@ class BooleanField extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
export default BooleanField
|
||||
export default CheckboxInput
|
||||
41
src/components/inputs/FontInput.jsx
Normal file
41
src/components/inputs/FontInput.jsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import React from 'react'
|
||||
import AutocompleteInput from './AutocompleteInput'
|
||||
import input from '../../config/input.js'
|
||||
|
||||
//TODO: Query available font stack dynamically
|
||||
import fontStacks from '../../config/fontstacks.json'
|
||||
|
||||
class FontInput extends React.Component {
|
||||
static propTypes = {
|
||||
value: React.PropTypes.array.isRequired,
|
||||
style: React.PropTypes.object,
|
||||
onChange: React.PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
get values() {
|
||||
return this.props.value || this.props.default.slice(1) || []
|
||||
}
|
||||
|
||||
changeFont(idx, newValue) {
|
||||
const changedValues = this.values.slice(0)
|
||||
changedValues[idx] = newValue
|
||||
this.props.onChange(changedValues)
|
||||
}
|
||||
|
||||
render() {
|
||||
const inputs = this.values.map((value, i) => {
|
||||
return <AutocompleteInput
|
||||
key={i}
|
||||
value={value}
|
||||
options={fontStacks.map(f => [f, f])}
|
||||
onChange={this.changeFont.bind(this, i)}
|
||||
/>
|
||||
})
|
||||
|
||||
return <div style={{display: 'inline-block'}}>
|
||||
{inputs}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
export default FontInput
|
||||
@@ -7,6 +7,7 @@ class InputBlock extends React.Component {
|
||||
static propTypes = {
|
||||
label: React.PropTypes.string.isRequired,
|
||||
children: React.PropTypes.element.isRequired,
|
||||
style: React.PropTypes.object,
|
||||
}
|
||||
|
||||
onChange(e) {
|
||||
@@ -14,14 +15,30 @@ class InputBlock extends React.Component {
|
||||
return this.props.onChange(value === "" ? null: value)
|
||||
}
|
||||
|
||||
renderChildren() {
|
||||
return React.Children.map(this.props.children, child => {
|
||||
return React.cloneElement(child, {
|
||||
style: {
|
||||
...child.props.style,
|
||||
width: '50%',
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div style={{
|
||||
display: 'block',
|
||||
marginTop: margins[2],
|
||||
marginBottom: margins[2],
|
||||
...input.property,
|
||||
...this.props.style,
|
||||
}}>
|
||||
<label style={input.label}>{this.props.label}</label>
|
||||
{this.props.children}
|
||||
<label
|
||||
style={{
|
||||
...input.label,
|
||||
width: '50%',
|
||||
}}>
|
||||
{this.props.label}
|
||||
</label>
|
||||
{this.renderChildren()}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
38
src/components/inputs/MultiButtonInput.jsx
Normal file
38
src/components/inputs/MultiButtonInput.jsx
Normal file
@@ -0,0 +1,38 @@
|
||||
|
||||
import React from 'react'
|
||||
import Button from '../Button'
|
||||
|
||||
import colors from '../../config/colors'
|
||||
import { margins } from '../../config/scales'
|
||||
import input from '../../config/input.js'
|
||||
|
||||
class MultiButtonInput extends React.Component {
|
||||
static propTypes = {
|
||||
value: React.PropTypes.string.isRequired,
|
||||
options: React.PropTypes.array.isRequired,
|
||||
style: React.PropTypes.object,
|
||||
onChange: React.PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
render() {
|
||||
const selectedValue = this.props.value || this.props.options[0][0]
|
||||
const buttons = this.props.options.map(([val, label])=> {
|
||||
return <Button
|
||||
key={val}
|
||||
style={{
|
||||
marginRight: margins[0],
|
||||
backgroundColor: val === selectedValue ? colors.midgray : colors.gray,
|
||||
}}
|
||||
onClick={e => this.props.onChange(val)}
|
||||
>
|
||||
{label}
|
||||
</Button>
|
||||
})
|
||||
|
||||
return <div style={{display: 'inline-block'}}>
|
||||
{buttons}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
export default MultiButtonInput
|
||||
83
src/components/inputs/NumberInput.jsx
Normal file
83
src/components/inputs/NumberInput.jsx
Normal file
@@ -0,0 +1,83 @@
|
||||
import React from 'react'
|
||||
import input from '../../config/input.js'
|
||||
|
||||
class NumberInput extends React.Component {
|
||||
static propTypes = {
|
||||
value: React.PropTypes.number,
|
||||
style: React.PropTypes.object,
|
||||
default: React.PropTypes.number,
|
||||
min: React.PropTypes.number,
|
||||
max: React.PropTypes.number,
|
||||
onChange: React.PropTypes.func,
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
value: props.value
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
this.setState({ value: nextProps.value })
|
||||
}
|
||||
|
||||
changeValue(newValue) {
|
||||
const value = parseFloat(newValue)
|
||||
|
||||
const hasChanged = this.state.value !== value
|
||||
if(this.isValid(value) && hasChanged) {
|
||||
this.props.onChange(value)
|
||||
} else {
|
||||
this.setState({ value: newValue })
|
||||
}
|
||||
}
|
||||
|
||||
isValid(v) {
|
||||
const value = parseFloat(v)
|
||||
if(isNaN(value)) {
|
||||
return false
|
||||
}
|
||||
|
||||
if(!isNaN(this.props.min) && value < this.props.min) {
|
||||
return false
|
||||
}
|
||||
|
||||
if(!isNaN(this.props.max) && value > this.props.max) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
resetValue() {
|
||||
// Reset explicitly to default value if value has been cleared
|
||||
if(this.state.value === "") {
|
||||
return this.changeValue(this.props.default)
|
||||
}
|
||||
|
||||
// If set value is invalid fall back to the last valid value from props or at last resort the default value
|
||||
if(!this.isValid(this.state.value)) {
|
||||
if(this.isValid(this.props.value)) {
|
||||
this.changeValue(this.props.value)
|
||||
} else {
|
||||
this.changeValue(this.props.default)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return <input
|
||||
style={{
|
||||
...input.input,
|
||||
...this.props.style
|
||||
}}
|
||||
placeholder={this.props.default}
|
||||
value={this.state.value}
|
||||
onChange={e => this.changeValue(e.target.value)}
|
||||
onBlur={this.resetValue.bind(this)}
|
||||
/>
|
||||
}
|
||||
}
|
||||
|
||||
export default NumberInput
|
||||
@@ -5,6 +5,7 @@ class StringInput extends React.Component {
|
||||
static propTypes = {
|
||||
value: React.PropTypes.string,
|
||||
style: React.PropTypes.object,
|
||||
default: React.PropTypes.number,
|
||||
onChange: React.PropTypes.func,
|
||||
}
|
||||
|
||||
@@ -15,6 +16,7 @@ class StringInput extends React.Component {
|
||||
...this.props.style
|
||||
}}
|
||||
value={this.props.value}
|
||||
placeholder={this.props.default}
|
||||
onChange={e => this.props.onChange(e.target.value)}
|
||||
/>
|
||||
}
|
||||
|
||||
@@ -32,18 +32,37 @@ class JSONEditor extends React.Component {
|
||||
})
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
try {
|
||||
const parsedLayer = JSON.parse(this.state.code)
|
||||
// If the structure is still the same do not update
|
||||
// because it affects editing experience by reformatting all the time
|
||||
return nextState.code !== JSON.stringify(parsedLayer, null, 2)
|
||||
} catch(err) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
onCodeUpdate(newCode) {
|
||||
try {
|
||||
const parsedLayer = JSON.parse(newCode)
|
||||
this.props.onChange(parsedLayer)
|
||||
} catch(err) {
|
||||
console.warn(err)
|
||||
} finally {
|
||||
this.setState({
|
||||
code: newCode
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
resetValue() {
|
||||
console.log('reset')
|
||||
this.setState({
|
||||
code: JSON.stringify(this.props.layer, null, 2)
|
||||
})
|
||||
}
|
||||
|
||||
render() {
|
||||
const codeMirrorOptions = {
|
||||
mode: {name: "javascript", json: true},
|
||||
@@ -51,11 +70,13 @@ class JSONEditor extends React.Component {
|
||||
theme: 'maputnik',
|
||||
viewportMargin: Infinity,
|
||||
lineNumbers: false,
|
||||
scrollbarStyle: "null",
|
||||
}
|
||||
|
||||
return <CodeMirror
|
||||
value={this.state.code}
|
||||
onChange={this.onCodeUpdate.bind(this)}
|
||||
onFocusChange={focused => focused ? true : this.resetValue()}
|
||||
options={codeMirrorOptions}
|
||||
/>
|
||||
}
|
||||
|
||||
@@ -1,12 +1,20 @@
|
||||
import React from 'react'
|
||||
|
||||
import GlSpec from 'mapbox-gl-style-spec/reference/latest.min.js'
|
||||
import JSONEditor from './JSONEditor'
|
||||
import SourceEditor from './SourceEditor'
|
||||
import FilterEditor from '../filter/FilterEditor'
|
||||
import PropertyGroup from '../fields/PropertyGroup'
|
||||
import LayerEditorGroup from './LayerEditorGroup'
|
||||
import LayerSettings from './LayerSettings'
|
||||
import LayerTypeBlock from './LayerTypeBlock'
|
||||
import LayerIdBlock from './LayerIdBlock'
|
||||
import LayerSourceBlock from './LayerSourceBlock'
|
||||
import LayerSourceLayerBlock from './LayerSourceLayerBlock'
|
||||
|
||||
import InputBlock from '../inputs/InputBlock'
|
||||
import MultiButtonInput from '../inputs/MultiButtonInput'
|
||||
|
||||
import input from '../../config/input.js'
|
||||
import { changeType, changeProperty } from '../../libs/layer'
|
||||
import layout from '../../config/layout.json'
|
||||
import { margins, fontSizes } from '../../config/scales'
|
||||
import colors from '../../config/colors'
|
||||
@@ -72,32 +80,8 @@ export default class LayerEditor extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
/** A {@property} in either the paint our layout {@group} has changed
|
||||
* to a {@newValue}.
|
||||
*/
|
||||
onPropertyChange(group, property, newValue) {
|
||||
if(group) {
|
||||
this.props.onLayerChanged({
|
||||
...this.props.layer,
|
||||
[group]: {
|
||||
...this.props.layer[group],
|
||||
[property]: newValue
|
||||
}
|
||||
})
|
||||
} else {
|
||||
this.props.onLayerChanged({
|
||||
...this.props.layer,
|
||||
[property]: newValue
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
onFilterChange(newValue) {
|
||||
const changedLayer = {
|
||||
...this.props.layer,
|
||||
filter: newValue
|
||||
}
|
||||
this.props.onLayerChanged(changedLayer)
|
||||
changeProperty(group, property, newValue) {
|
||||
this.props.onLayerChanged(changeProperty(this.props.layer, group, property, newValue))
|
||||
}
|
||||
|
||||
onGroupToggle(groupTitle, active) {
|
||||
@@ -112,30 +96,41 @@ export default class LayerEditor extends React.Component {
|
||||
|
||||
renderGroupType(type, fields) {
|
||||
switch(type) {
|
||||
case 'settings': return <LayerSettings
|
||||
id={this.props.layer.id}
|
||||
type={this.props.layer.type}
|
||||
onTypeChange={v => this.onPropertyChange(null, 'type', v)}
|
||||
onIdChange={newId => this.props.onLayerIdChange(this.props.layer.id, newId)}
|
||||
/>
|
||||
case 'settings': return <div>
|
||||
<LayerIdBlock
|
||||
value={this.props.layer.id}
|
||||
onChange={newId => this.props.onLayerIdChange(this.props.layer.id, newId)}
|
||||
/>
|
||||
<LayerTypeBlock
|
||||
value={this.props.layer.type}
|
||||
onChange={newType => this.props.onLayerChanged(changeType(this.props.layer, newType))}
|
||||
/>
|
||||
</div>
|
||||
case 'source': return <div>
|
||||
<FilterEditor
|
||||
filter={this.props.layer.filter}
|
||||
properties={this.props.vectorLayers[this.props.layer['source-layer']]}
|
||||
onChange={f => this.onFilterChange(f)}
|
||||
<LayerSourceBlock
|
||||
sourceIds={Object.keys(this.props.sources)}
|
||||
value={this.props.layer.source}
|
||||
onChange={v => this.changeProperty(null, 'source', v)}
|
||||
/>
|
||||
<SourceEditor
|
||||
source={this.props.layer.source}
|
||||
sourceLayer={this.props.layer['source-layer']}
|
||||
sources={this.props.sources}
|
||||
onSourceChange={console.log}
|
||||
onSourceLayerChange={console.log}
|
||||
<LayerSourceLayerBlock
|
||||
sourceLayerIds={this.props.sources[this.props.layer.source]}
|
||||
value={this.props.layer['source-layer']}
|
||||
onChange={v => this.changeProperty(null, 'source-layer', v)}
|
||||
/>
|
||||
{this.props.layer.filter &&
|
||||
<div style={input.property}>
|
||||
<FilterEditor
|
||||
filter={this.props.layer.filter}
|
||||
properties={this.props.vectorLayers[this.props.layer['source-layer']]}
|
||||
onChange={f => this.changeProperty(null, 'filter', f)}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
case 'properties': return <PropertyGroup
|
||||
layer={this.props.layer}
|
||||
groupFields={fields}
|
||||
onChange={this.onPropertyChange.bind(this)}
|
||||
onChange={this.changeProperty.bind(this)}
|
||||
/>
|
||||
case 'jsoneditor': return <JSONEditor
|
||||
layer={this.props.layer}
|
||||
|
||||
22
src/components/layers/LayerIdBlock.jsx
Normal file
22
src/components/layers/LayerIdBlock.jsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import React from 'react'
|
||||
|
||||
import InputBlock from '../inputs/InputBlock'
|
||||
import StringInput from '../inputs/StringInput'
|
||||
|
||||
class LayerIdBlock extends React.Component {
|
||||
static propTypes = {
|
||||
value: React.PropTypes.string.isRequired,
|
||||
onChange: React.PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
render() {
|
||||
return <InputBlock label={"Layer ID"}>
|
||||
<StringInput
|
||||
value={this.props.value}
|
||||
onChange={this.props.onChange}
|
||||
/>
|
||||
</InputBlock>
|
||||
}
|
||||
}
|
||||
|
||||
export default LayerIdBlock
|
||||
@@ -23,8 +23,8 @@ class LayerTypeDragHandle extends React.Component {
|
||||
{...this.props}
|
||||
style={{
|
||||
cursor: 'move',
|
||||
width: 15,
|
||||
height: 15,
|
||||
width: fontSizes[4],
|
||||
height: fontSizes[4],
|
||||
paddingRight: margins[0],
|
||||
}}
|
||||
/>
|
||||
@@ -115,7 +115,7 @@ class LayerListItem extends React.Component {
|
||||
|
||||
getChildContext() {
|
||||
return {
|
||||
reactIconBase: { size: 12 }
|
||||
reactIconBase: { size: fontSizes[4] }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,6 +137,7 @@ class LayerListItem extends React.Component {
|
||||
padding: margins[1],
|
||||
borderColor: Color(colors.black).lighten(0.10).string(),
|
||||
backgroundColor: colors.black,
|
||||
lineHeight: 1.3,
|
||||
}
|
||||
|
||||
if(this.state.hover) {
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
import React from 'react'
|
||||
import GlSpec from 'mapbox-gl-style-spec/reference/latest.min.js'
|
||||
import InputBlock from '../inputs/InputBlock'
|
||||
import StringInput from '../inputs/StringInput'
|
||||
import SelectInput from '../inputs/SelectInput'
|
||||
|
||||
import colors from '../../config/colors'
|
||||
import { margins } from '../../config/scales'
|
||||
|
||||
|
||||
class LayerSettings extends React.Component {
|
||||
static propTypes = {
|
||||
id: React.PropTypes.string.isRequired,
|
||||
type: React.PropTypes.oneOf(Object.keys(GlSpec.layer.type.values)).isRequired,
|
||||
onIdChange: React.PropTypes.func.isRequired,
|
||||
onTypeChange: React.PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div style={{
|
||||
padding: margins[2],
|
||||
paddingRight: 0,
|
||||
backgroundColor: colors.black,
|
||||
}}>
|
||||
<InputBlock label={"Layer ID"}>
|
||||
<StringInput
|
||||
value={this.props.id}
|
||||
onChange={this.props.onIdChange}
|
||||
/>
|
||||
</InputBlock>
|
||||
<InputBlock label={"Layer Type"}>
|
||||
<SelectInput
|
||||
options={[
|
||||
['background', 'Background'],
|
||||
['fill', 'Fill'],
|
||||
['line', 'Line'],
|
||||
['symbol', 'Symbol'],
|
||||
['raster', 'Raster'],
|
||||
['circle', 'Circle'],
|
||||
['fill-extrusion', 'Fill Extrusion'],
|
||||
]}
|
||||
onChange={this.props.onTypeChange}
|
||||
value={this.props.type}
|
||||
/>
|
||||
</InputBlock>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
export default LayerSettings
|
||||
32
src/components/layers/LayerSourceBlock.jsx
Normal file
32
src/components/layers/LayerSourceBlock.jsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import React from 'react'
|
||||
|
||||
import InputBlock from '../inputs/InputBlock'
|
||||
import StringInput from '../inputs/StringInput'
|
||||
import SelectInput from '../inputs/SelectInput'
|
||||
import AutocompleteInput from '../inputs/AutocompleteInput'
|
||||
|
||||
class LayerSourceBlock extends React.Component {
|
||||
static propTypes = {
|
||||
value: React.PropTypes.string,
|
||||
onChange: React.PropTypes.func,
|
||||
sourceIds: React.PropTypes.array,
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
onChange: () => {},
|
||||
sourceIds: [],
|
||||
}
|
||||
|
||||
render() {
|
||||
return <InputBlock label={"Source"}>
|
||||
<AutocompleteInput
|
||||
value={this.props.value}
|
||||
onChange={this.props.onChange}
|
||||
options={this.props.sourceIds.map(src => [src, src])}
|
||||
wrapperStyle={{ width: '50%' }}
|
||||
/>
|
||||
</InputBlock>
|
||||
}
|
||||
}
|
||||
|
||||
export default LayerSourceBlock
|
||||
32
src/components/layers/LayerSourceLayerBlock.jsx
Normal file
32
src/components/layers/LayerSourceLayerBlock.jsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import React from 'react'
|
||||
|
||||
import InputBlock from '../inputs/InputBlock'
|
||||
import StringInput from '../inputs/StringInput'
|
||||
import SelectInput from '../inputs/SelectInput'
|
||||
import AutocompleteInput from '../inputs/AutocompleteInput'
|
||||
|
||||
class LayerSourceLayer extends React.Component {
|
||||
static propTypes = {
|
||||
value: React.PropTypes.string,
|
||||
onChange: React.PropTypes.func,
|
||||
sourceLayerIds: React.PropTypes.array,
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
onChange: () => {},
|
||||
sourceLayerIds: [],
|
||||
}
|
||||
|
||||
render() {
|
||||
return <InputBlock label={"Source Layer"}>
|
||||
<AutocompleteInput
|
||||
value={this.props.value}
|
||||
onChange={this.props.onChange}
|
||||
options={this.props.sourceLayerIds.map(l => [l, l])}
|
||||
wrapperStyle={{ width: '50%' }}
|
||||
/>
|
||||
</InputBlock>
|
||||
}
|
||||
}
|
||||
|
||||
export default LayerSourceLayer
|
||||
31
src/components/layers/LayerTypeBlock.jsx
Normal file
31
src/components/layers/LayerTypeBlock.jsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import React from 'react'
|
||||
|
||||
import InputBlock from '../inputs/InputBlock'
|
||||
import SelectInput from '../inputs/SelectInput'
|
||||
|
||||
class LayerTypeBlock extends React.Component {
|
||||
static propTypes = {
|
||||
value: React.PropTypes.string.isRequired,
|
||||
onChange: React.PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
render() {
|
||||
return <InputBlock label={"Layer Type"}>
|
||||
<SelectInput
|
||||
options={[
|
||||
['background', 'Background'],
|
||||
['fill', 'Fill'],
|
||||
['line', 'Line'],
|
||||
['symbol', 'Symbol'],
|
||||
['raster', 'Raster'],
|
||||
['circle', 'Circle'],
|
||||
['fill-extrusion', 'Fill Extrusion'],
|
||||
]}
|
||||
onChange={this.props.onChange}
|
||||
value={this.props.value}
|
||||
/>
|
||||
</InputBlock>
|
||||
}
|
||||
}
|
||||
|
||||
export default LayerTypeBlock
|
||||
@@ -1,54 +0,0 @@
|
||||
import React from 'react'
|
||||
|
||||
import PropertyGroup from '../fields/PropertyGroup'
|
||||
import input from '../../config/input.js'
|
||||
|
||||
/** Choose tileset (source) and the source layer */
|
||||
export default class SourceEditor extends React.Component {
|
||||
static propTypes = {
|
||||
source: React.PropTypes.string.isRequired,
|
||||
sourceLayer: React.PropTypes.string.isRequired,
|
||||
|
||||
onSourceChange: React.PropTypes.func.isRequired,
|
||||
onSourceLayerChange: React.PropTypes.func.isRequired,
|
||||
|
||||
/** List of available sources in the style
|
||||
* https://www.mapbox.com/mapbox-gl-style-spec/#root-sources */
|
||||
sources: React.PropTypes.object.isRequired,
|
||||
}
|
||||
|
||||
render() {
|
||||
const options = Object.keys(this.props.sources).map(sourceId => {
|
||||
return <option key={sourceId} value={sourceId}>{sourceId}</option>
|
||||
})
|
||||
|
||||
const layerOptions = this.props.sources[this.props.source].map(vectorLayerId => {
|
||||
const id = vectorLayerId
|
||||
return <option key={id} value={id}>{id}</option>
|
||||
})
|
||||
|
||||
return <div>
|
||||
<div style={input.property}>
|
||||
<label style={input.label}>Source</label>
|
||||
<select
|
||||
style={input.select}
|
||||
value={this.props.source}
|
||||
onChange={(e) => this.onSourceChange(e.target.value)}
|
||||
>
|
||||
{options}
|
||||
</select>
|
||||
</div>
|
||||
<div style={input.property}>
|
||||
<label style={input.label}>Source Layer</label>
|
||||
<select
|
||||
style={input.select}
|
||||
value={this.props.sourceLayer}
|
||||
onChange={(e) => this.onSourceLayerChange(e.target.value)}
|
||||
>
|
||||
{layerOptions}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
}
|
||||
}
|
||||
66
src/components/map/FeatureLayerTable.jsx
Normal file
66
src/components/map/FeatureLayerTable.jsx
Normal file
@@ -0,0 +1,66 @@
|
||||
import React from 'react'
|
||||
import InputBlock from '../inputs/InputBlock'
|
||||
import StringInput from '../inputs/StringInput'
|
||||
import LayerIcon from '../icons/LayerIcon'
|
||||
import input from '../../config/input'
|
||||
|
||||
import colors from '../../config/colors'
|
||||
import { margins, fontSizes } from '../../config/scales'
|
||||
|
||||
const Panel = (props) => {
|
||||
return <div style={{
|
||||
backgroundColor: colors.gray,
|
||||
padding: margins[0],
|
||||
fontSize: fontSizes[5],
|
||||
lineHeight: 1.2,
|
||||
}}>{props.children}</div>
|
||||
}
|
||||
|
||||
function renderFeature(feature) {
|
||||
return <div>
|
||||
<Panel>{feature.layer['source-layer']}</Panel>
|
||||
</div>
|
||||
}
|
||||
|
||||
function groupFeaturesBySourceLayer(features) {
|
||||
const sources = {}
|
||||
features.forEach(feature => {
|
||||
sources[feature.layer['source-layer']] = sources[feature.layer['source-layer']] || []
|
||||
sources[feature.layer['source-layer']].push(feature)
|
||||
})
|
||||
return sources
|
||||
}
|
||||
|
||||
class FeatureLayerTable extends React.Component {
|
||||
render() {
|
||||
const sources = groupFeaturesBySourceLayer(this.props.features)
|
||||
|
||||
const items = Object.keys(sources).map(vectorLayerId => {
|
||||
const layers = sources[vectorLayerId].map(feature => {
|
||||
return <label style={{
|
||||
...input.label,
|
||||
display: 'block',
|
||||
width: 'auto',
|
||||
}}>
|
||||
<LayerIcon type={feature.layer.type} style={{
|
||||
width: fontSizes[4],
|
||||
height: fontSizes[4],
|
||||
paddingRight: margins[0],
|
||||
}}/>
|
||||
{feature.layer.id}
|
||||
</label>
|
||||
})
|
||||
return <div>
|
||||
<Panel>{vectorLayerId}</Panel>
|
||||
{layers}
|
||||
</div>
|
||||
})
|
||||
|
||||
return <div>
|
||||
{items}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default FeatureLayerTable
|
||||
45
src/components/map/FeaturePropertyPopup.jsx
Normal file
45
src/components/map/FeaturePropertyPopup.jsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import React from 'react'
|
||||
import InputBlock from '../inputs/InputBlock'
|
||||
import StringInput from '../inputs/StringInput'
|
||||
|
||||
import colors from '../../config/colors'
|
||||
import { margins, fontSizes } from '../../config/scales'
|
||||
|
||||
function renderProperties(feature) {
|
||||
return Object.keys(feature.properties).map(propertyName => {
|
||||
const property = feature.properties[propertyName]
|
||||
return <InputBlock label={propertyName} style={{marginTop: 0, marginBottom: 0}}>
|
||||
<StringInput value={property} style={{backgroundColor: 'transparent'}}/>
|
||||
</InputBlock>
|
||||
})
|
||||
}
|
||||
|
||||
const Panel = (props) => {
|
||||
return <div style={{
|
||||
backgroundColor: colors.gray,
|
||||
padding: margins[0],
|
||||
fontSize: fontSizes[5],
|
||||
lineHeight: 1.2,
|
||||
}}>{props.children}</div>
|
||||
}
|
||||
|
||||
function renderFeature(feature) {
|
||||
console.log(feature)
|
||||
return <div>
|
||||
<Panel>{feature.layer['source-layer']}</Panel>
|
||||
{renderProperties(feature)}
|
||||
</div>
|
||||
}
|
||||
|
||||
class FeaturePropertyPopup extends React.Component {
|
||||
|
||||
render() {
|
||||
const features = this.props.features
|
||||
return <div>
|
||||
{features.map(renderFeature)}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default FeaturePropertyPopup
|
||||
126
src/components/map/InspectionMap.jsx
Normal file
126
src/components/map/InspectionMap.jsx
Normal file
@@ -0,0 +1,126 @@
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import MapboxGl from 'mapbox-gl/dist/mapbox-gl.js'
|
||||
import validateColor from 'mapbox-gl-style-spec/lib/validate/validate_color'
|
||||
import colors from '../../config/colors'
|
||||
import style from '../../libs/style'
|
||||
import FeaturePropertyPopup from './FeaturePropertyPopup'
|
||||
import { colorHighlightedLayer, generateColoredLayers } from '../../libs/stylegen'
|
||||
import 'mapbox-gl/dist/mapbox-gl.css'
|
||||
import '../../mapboxgl.css'
|
||||
|
||||
function convertInspectStyle(mapStyle, sources, highlightedLayer) {
|
||||
const coloredLayers = generateColoredLayers(sources)
|
||||
|
||||
const layer = colorHighlightedLayer(highlightedLayer)
|
||||
if(layer) {
|
||||
coloredLayers.push(layer)
|
||||
}
|
||||
|
||||
const newStyle = {
|
||||
...mapStyle,
|
||||
layers: [
|
||||
{
|
||||
"id": "background",
|
||||
"type": "background",
|
||||
"paint": {
|
||||
"background-color": colors.black,
|
||||
}
|
||||
},
|
||||
...coloredLayers,
|
||||
]
|
||||
}
|
||||
return newStyle
|
||||
}
|
||||
|
||||
function renderPopup(features) {
|
||||
var mountNode = document.createElement('div');
|
||||
ReactDOM.render(<FeaturePropertyPopup features={features} />, mountNode)
|
||||
return mountNode.innerHTML;
|
||||
}
|
||||
|
||||
export default class InspectionMap extends React.Component {
|
||||
static propTypes = {
|
||||
onDataChange: React.PropTypes.func,
|
||||
sources: React.PropTypes.object,
|
||||
originalStyle: React.PropTypes.object,
|
||||
highlightedLayer: React.PropTypes.object,
|
||||
style: React.PropTypes.object,
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
onMapLoaded: () => {},
|
||||
onTileLoaded: () => {},
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = { map: null }
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if(!this.state.map) return
|
||||
|
||||
this.state.map.setStyle(convertInspectStyle(nextProps.mapStyle, this.props.sources, nextProps.highlightedLayer), { diff: true})
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
MapboxGl.accessToken = this.props.accessToken
|
||||
|
||||
const map = new MapboxGl.Map({
|
||||
container: this.container,
|
||||
style: convertInspectStyle(this.props.mapStyle, this.props.sources, this.props.highlightedLayer),
|
||||
hash: true,
|
||||
})
|
||||
|
||||
const nav = new MapboxGl.NavigationControl();
|
||||
map.addControl(nav, 'top-right');
|
||||
|
||||
map.on("style.load", () => {
|
||||
this.setState({ map });
|
||||
})
|
||||
|
||||
map.on("data", e => {
|
||||
if(e.dataType !== 'tile') return
|
||||
this.props.onDataChange({
|
||||
map: this.state.map
|
||||
})
|
||||
})
|
||||
|
||||
map.on('click', this.displayPopup.bind(this))
|
||||
map.on('mousemove', function(e) {
|
||||
var features = map.queryRenderedFeatures(e.point, { layers: this.layers })
|
||||
map.getCanvas().style.cursor = (features.length) ? 'pointer' : ''
|
||||
})
|
||||
}
|
||||
|
||||
displayPopup(e) {
|
||||
const features = this.state.map.queryRenderedFeatures(e.point, {
|
||||
layers: this.layers
|
||||
});
|
||||
|
||||
if (!features.length) {
|
||||
return
|
||||
}
|
||||
|
||||
// Populate the popup and set its coordinates
|
||||
// based on the feature found.
|
||||
const popup = new MapboxGl.Popup()
|
||||
.setLngLat(e.lngLat)
|
||||
.setHTML(renderPopup(features))
|
||||
.addTo(this.state.map)
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div
|
||||
ref={x => this.container = x}
|
||||
style={{
|
||||
position: "fixed",
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
...this.props.style,
|
||||
}}></div>
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import React from 'react'
|
||||
|
||||
export default class Map extends React.Component {
|
||||
static propTypes = {
|
||||
mapStyle: React.PropTypes.object.isRequired,
|
||||
accessToken: React.PropTypes.string,
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div
|
||||
ref={x => this.container = x}
|
||||
style={{
|
||||
position: "fixed",
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
}}></div>
|
||||
}
|
||||
}
|
||||
@@ -1,23 +1,41 @@
|
||||
import React from 'react'
|
||||
import MapboxGl from 'mapbox-gl'
|
||||
import ReactDOM from 'react-dom'
|
||||
import MapboxGl from 'mapbox-gl/dist/mapbox-gl.js'
|
||||
import FeatureLayerTable from './FeatureLayerTable'
|
||||
import validateColor from 'mapbox-gl-style-spec/lib/validate/validate_color'
|
||||
|
||||
import Map from './Map.jsx'
|
||||
import style from '../../libs/style.js'
|
||||
import 'mapbox-gl/dist/mapbox-gl.css'
|
||||
import '../../mapboxgl.css'
|
||||
|
||||
export default class MapboxGlMap extends Map {
|
||||
function renderPopup(features) {
|
||||
var mountNode = document.createElement('div');
|
||||
ReactDOM.render(<FeatureLayerTable features={features} />, mountNode)
|
||||
return mountNode.innerHTML;
|
||||
}
|
||||
|
||||
export default class MapboxGlMap extends React.Component {
|
||||
static propTypes = {
|
||||
onMapLoaded: React.PropTypes.func,
|
||||
onDataChange: React.PropTypes.func,
|
||||
mapStyle: React.PropTypes.object.isRequired,
|
||||
accessToken: React.PropTypes.string,
|
||||
style: React.PropTypes.object,
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
onMapLoaded: () => {}
|
||||
onMapLoaded: () => {},
|
||||
onDataChange: () => {},
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = { map: null }
|
||||
this.state = {
|
||||
map: null,
|
||||
isPopupOpen: false,
|
||||
popupX: 0,
|
||||
popupY: 0,
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if(!this.state.map) return
|
||||
|
||||
@@ -32,11 +50,53 @@ export default class MapboxGlMap extends Map {
|
||||
const map = new MapboxGl.Map({
|
||||
container: this.container,
|
||||
style: this.props.mapStyle,
|
||||
});
|
||||
hash: true,
|
||||
})
|
||||
|
||||
map.on("style.load", (...args) => {
|
||||
this.props.onMapLoaded(map)
|
||||
const nav = new MapboxGl.NavigationControl();
|
||||
map.addControl(nav, 'top-right');
|
||||
|
||||
map.on("style.load", () => {
|
||||
this.setState({ map });
|
||||
});
|
||||
})
|
||||
|
||||
map.on("data", e => {
|
||||
if(e.dataType !== 'tile') return
|
||||
this.props.onDataChange({
|
||||
map: this.state.map
|
||||
})
|
||||
})
|
||||
|
||||
map.on('click', this.displayPopup.bind(this));
|
||||
map.on('mousemove', function(e) {
|
||||
var features = map.queryRenderedFeatures(e.point, { layers: this.layers })
|
||||
map.getCanvas().style.cursor = (features.length) ? 'pointer' : ''
|
||||
})
|
||||
}
|
||||
|
||||
displayPopup(e) {
|
||||
const features = this.state.map.queryRenderedFeatures(e.point, {
|
||||
layers: this.layers
|
||||
});
|
||||
|
||||
if(features.length < 1) return
|
||||
const popup = new MapboxGl.Popup()
|
||||
.setLngLat(e.lngLat)
|
||||
.setHTML(renderPopup(features))
|
||||
.addTo(this.state.map)
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div
|
||||
ref={x => this.container = x}
|
||||
style={{
|
||||
position: "fixed",
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
...this.props.style,
|
||||
}}>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
import React from 'react'
|
||||
import Map from './Map'
|
||||
import style from '../../libs/style.js'
|
||||
|
||||
class OpenLayers3Map extends Map {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
class OpenLayers3Map extends React.Component {
|
||||
static propTypes = {
|
||||
onDataChange: React.PropTypes.func,
|
||||
mapStyle: React.PropTypes.object.isRequired,
|
||||
accessToken: React.PropTypes.string,
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
onMapLoaded: () => {},
|
||||
onDataChange: () => {},
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
@@ -63,6 +69,18 @@ class OpenLayers3Map extends Map {
|
||||
this.setState({ map });
|
||||
})
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div
|
||||
ref={x => this.container = x}
|
||||
style={{
|
||||
position: "fixed",
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
}}></div>
|
||||
}
|
||||
}
|
||||
|
||||
export default OpenLayers3Map
|
||||
|
||||
108
src/components/modals/AddModal.jsx
Normal file
108
src/components/modals/AddModal.jsx
Normal file
@@ -0,0 +1,108 @@
|
||||
import React from 'react'
|
||||
|
||||
import Button from '../Button'
|
||||
import InputBlock from '../inputs/InputBlock'
|
||||
import StringInput from '../inputs/StringInput'
|
||||
import SelectInput from '../inputs/SelectInput'
|
||||
import Modal from './Modal'
|
||||
import colors from '../../config/colors'
|
||||
|
||||
import LayerTypeBlock from '../layers/LayerTypeBlock'
|
||||
import LayerIdBlock from '../layers/LayerIdBlock'
|
||||
import LayerSourceBlock from '../layers/LayerSourceBlock'
|
||||
import LayerSourceLayerBlock from '../layers/LayerSourceLayerBlock'
|
||||
|
||||
class AddModal extends React.Component {
|
||||
static propTypes = {
|
||||
mapStyle: React.PropTypes.object.isRequired,
|
||||
onStyleChange: React.PropTypes.func.isRequired,
|
||||
isOpen: React.PropTypes.bool.isRequired,
|
||||
onOpenToggle: React.PropTypes.func.isRequired,
|
||||
|
||||
// A dict of source id's and the available source layers
|
||||
sources: React.PropTypes.object.isRequired,
|
||||
}
|
||||
|
||||
addLayer() {
|
||||
const changedLayers = this.props.mapStyle.layers.slice(0)
|
||||
const layer = {
|
||||
id: this.state.id,
|
||||
type: this.state.type,
|
||||
}
|
||||
|
||||
if(this.state.type !== 'background') {
|
||||
layer.source = this.state.source
|
||||
layer['source-layer'] = this.state['source-layer']
|
||||
}
|
||||
|
||||
changedLayers.push(layer)
|
||||
|
||||
const changedStyle = {
|
||||
...this.props.mapStyle,
|
||||
layers: changedLayers,
|
||||
}
|
||||
|
||||
this.props.onStyleChange(changedStyle)
|
||||
this.props.onOpenToggle(false)
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
type: 'fill',
|
||||
id: '',
|
||||
}
|
||||
|
||||
if(props.sources.length > 0) {
|
||||
this.state.source = Object.keys(this.props.sources)[0]
|
||||
this.state['source-layer'] = this.props.sources[this.state.source][0]
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
const sourceIds = Object.keys(nextProps.sources)
|
||||
if(!this.state.source && sourceIds.length > 0) {
|
||||
this.setState({
|
||||
source: sourceIds[0],
|
||||
'source-layer': this.state['source-layer'] || nextProps.sources[sourceIds[0]][0]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
render() {
|
||||
return <Modal
|
||||
isOpen={this.props.isOpen}
|
||||
onOpenToggle={this.props.onOpenToggle}
|
||||
title={'Add Layer'}
|
||||
>
|
||||
<LayerIdBlock
|
||||
value={this.state.id}
|
||||
onChange={v => this.setState({ id: v })}
|
||||
/>
|
||||
<LayerTypeBlock
|
||||
value={this.state.type}
|
||||
onChange={v => this.setState({ type: v })}
|
||||
/>
|
||||
{this.state.type !== 'background' &&
|
||||
<LayerSourceBlock
|
||||
sourceIds={Object.keys(this.props.sources)}
|
||||
value={this.state.source}
|
||||
onChange={v => this.setState({ source: v })}
|
||||
/>
|
||||
}
|
||||
{this.state.type !== 'background' &&
|
||||
<LayerSourceLayerBlock
|
||||
sourceLayerIds={this.props.sources[this.state.source] || []}
|
||||
value={this.state['source-layer']}
|
||||
onChange={v => this.setState({ 'source-layer': v })}
|
||||
/>
|
||||
}
|
||||
<Button onClick={this.addLayer.bind(this)}>
|
||||
Add Layer
|
||||
</Button>
|
||||
</Modal>
|
||||
}
|
||||
}
|
||||
|
||||
export default AddModal
|
||||
@@ -76,7 +76,7 @@ class OpenModal extends React.Component {
|
||||
withCredentials: false,
|
||||
}, (error, response, body) => {
|
||||
if (!error && response.statusCode == 200) {
|
||||
const mapStyle = style.ensureMetadataExists(JSON.parse(body))
|
||||
const mapStyle = style.ensureStyleValidity(JSON.parse(body))
|
||||
console.log('Loaded style ', mapStyle.id)
|
||||
this.props.onStyleOpen(mapStyle)
|
||||
} else {
|
||||
@@ -91,7 +91,7 @@ class OpenModal extends React.Component {
|
||||
reader.readAsText(file, "UTF-8");
|
||||
reader.onload = e => {
|
||||
let mapStyle = JSON.parse(e.target.result)
|
||||
mapStyle = style.ensureMetadataExists(mapStyle)
|
||||
mapStyle = style.ensureStyleValidity(mapStyle)
|
||||
this.props.onStyleOpen(mapStyle);
|
||||
}
|
||||
reader.onerror = e => console.log(e.target);
|
||||
|
||||
@@ -18,23 +18,27 @@ class SettingsModal extends React.Component {
|
||||
super(props);
|
||||
}
|
||||
|
||||
onChange(property, e) {
|
||||
const changedStyle = this.props.mapStyle.set(property, e.target.value)
|
||||
changeStyleProperty(property, value) {
|
||||
const changedStyle = {
|
||||
...this.props.mapStyle,
|
||||
[property]: value
|
||||
}
|
||||
this.props.onStyleChanged(changedStyle)
|
||||
}
|
||||
|
||||
onRendererChange(renderer) {
|
||||
changeMetadataProperty(property, value) {
|
||||
const changedStyle = {
|
||||
...this.props.mapStyle,
|
||||
metadata: {
|
||||
...this.props.mapStyle.metadata,
|
||||
'maputnik:renderer': renderer,
|
||||
[property]: value
|
||||
}
|
||||
}
|
||||
this.props.onStyleChanged(changedStyle)
|
||||
}
|
||||
|
||||
render() {
|
||||
const metadata = this.props.mapStyle.metadata || {}
|
||||
const inputProps = { }
|
||||
return <Modal
|
||||
isOpen={this.props.isOpen}
|
||||
@@ -44,38 +48,45 @@ class SettingsModal extends React.Component {
|
||||
<InputBlock label={"Name"}>
|
||||
<StringInput {...inputProps}
|
||||
value={this.props.mapStyle.name}
|
||||
onChange={this.onChange.bind(this, "name")}
|
||||
onChange={this.changeStyleProperty.bind(this, "name")}
|
||||
/>
|
||||
</InputBlock>
|
||||
<InputBlock label={"Owner"}>
|
||||
<StringInput {...inputProps}
|
||||
value={this.props.mapStyle.owner}
|
||||
onChange={this.onChange.bind(this, "owner")}
|
||||
onChange={this.changeStyleProperty.bind(this, "owner")}
|
||||
/>
|
||||
</InputBlock>
|
||||
<InputBlock label={"Sprite URL"}>
|
||||
<StringInput {...inputProps}
|
||||
value={this.props.mapStyle.sprite}
|
||||
onChange={this.onChange.bind(this, "sprite")}
|
||||
onChange={this.changeStyleProperty.bind(this, "sprite")}
|
||||
/>
|
||||
</InputBlock>
|
||||
|
||||
<InputBlock label={"Glyphs URL"}>
|
||||
<StringInput {...inputProps}
|
||||
value={this.props.mapStyle.glyphs}
|
||||
onChange={this.onChange.bind(this, "glyphs")}
|
||||
onChange={this.changeStyleProperty.bind(this, "glyphs")}
|
||||
/>
|
||||
</InputBlock>
|
||||
|
||||
<InputBlock label={"Access Token"}>
|
||||
<StringInput {...inputProps}
|
||||
value={metadata['maputnik:access_token']}
|
||||
onChange={this.changeMetadataProperty.bind(this, "maputnik:access_token")}
|
||||
/>
|
||||
</InputBlock>
|
||||
|
||||
<InputBlock label={"Style Renderer"}>
|
||||
<SelectInput {...inputProps}
|
||||
options={[
|
||||
['mbgljs', 'MapboxGL JS'],
|
||||
['ol3', 'Open Layers 3']
|
||||
['ol3', 'Open Layers 3'],
|
||||
['inspection', 'Inspection Mode'],
|
||||
]}
|
||||
value={(this.props.mapStyle.metadata || {})['maputnik:renderer'] || 'mbgljs'}
|
||||
onChange={this.onRendererChange.bind(this)}
|
||||
value={metadata['maputnik:renderer'] || 'mbgljs'}
|
||||
onChange={this.changeMetadataProperty.bind(this, 'maputnik:renderer')}
|
||||
/>
|
||||
</InputBlock>
|
||||
</Modal>
|
||||
|
||||
@@ -64,7 +64,7 @@ function editorMode(source) {
|
||||
return 'tilejson'
|
||||
}
|
||||
|
||||
class SourceEditorLayout extends React.Component {
|
||||
class ActiveSourceTypeEditor extends React.Component {
|
||||
static propTypes = {
|
||||
sourceId: React.PropTypes.string.isRequired,
|
||||
source: React.PropTypes.object.isRequired,
|
||||
@@ -87,7 +87,7 @@ class SourceEditorLayout extends React.Component {
|
||||
<span style={{fontWeight: 700, fontSize: fontSizes[4], lineHeight: 2}}>#{this.props.sourceId}</span>
|
||||
<span style={{flexGrow: 1}} />
|
||||
<Button
|
||||
onClick={this.props.onSourceDelete}
|
||||
onClick={()=> this.props.onSourceDelete(this.props.sourceId)}
|
||||
style={{backgroundColor: 'transparent'}}
|
||||
>
|
||||
<DeleteIcon />
|
||||
@@ -118,19 +118,30 @@ class AddSource extends React.Component {
|
||||
super(props)
|
||||
this.state = {
|
||||
mode: 'tilejson',
|
||||
source: {
|
||||
id: style.generateId(),
|
||||
}
|
||||
sourceId: style.generateId(),
|
||||
source: this.defaultSource('tilejson'),
|
||||
}
|
||||
}
|
||||
|
||||
onSourceIdChange(newId) {
|
||||
this.setState({
|
||||
source: {
|
||||
...this.state.source,
|
||||
id: newId,
|
||||
defaultSource(mode) {
|
||||
const source = (this.state || {}).source || {}
|
||||
switch(mode) {
|
||||
case 'geojson': return {
|
||||
type: 'geojson',
|
||||
data: source.data || 'http://localhost:3000/geojson.json'
|
||||
}
|
||||
})
|
||||
case 'tilejson': return {
|
||||
type: 'vector',
|
||||
url: source.url || 'http://localhost:3000/tilejson.json'
|
||||
}
|
||||
case 'tilexyz': return {
|
||||
type: 'vector',
|
||||
tiles: source.tiles || ['http://localhost:3000/{x}/{y}/{z}.pbf'],
|
||||
minZoom: source.minZoom || 0,
|
||||
maxZoom: source.maxZoom || 14
|
||||
}
|
||||
default: return {}
|
||||
}
|
||||
}
|
||||
|
||||
onSourceChange(source) {
|
||||
@@ -143,8 +154,8 @@ class AddSource extends React.Component {
|
||||
return <div>
|
||||
<InputBlock label={"Source ID"}>
|
||||
<StringInput
|
||||
value={this.state.source.id}
|
||||
onChange={this.onSourceIdChange.bind(this)}
|
||||
value={this.state.sourceId}
|
||||
onChange={v => this.setState({ sourceId: v})}
|
||||
/>
|
||||
</InputBlock>
|
||||
<InputBlock label={"Source Type"}>
|
||||
@@ -154,7 +165,7 @@ class AddSource extends React.Component {
|
||||
['tilejson', 'Vector (TileJSON URL)'],
|
||||
['tilexyz', 'Vector (XYZ URLs)'],
|
||||
]}
|
||||
onChange={v => this.setState({mode: v})}
|
||||
onChange={mode => this.setState({mode: mode, source: this.defaultSource(mode)})}
|
||||
value={this.state.mode}
|
||||
/>
|
||||
</InputBlock>
|
||||
@@ -163,7 +174,7 @@ class AddSource extends React.Component {
|
||||
mode={this.state.mode}
|
||||
source={this.state.source}
|
||||
/>
|
||||
<Button onClick={() => this.onSourceAdd(this.state.source)}>
|
||||
<Button onClick={() => this.props.onSourceAdd(this.state.sourceId, this.state.source)}>
|
||||
Add Source
|
||||
</Button>
|
||||
</div>
|
||||
@@ -178,10 +189,10 @@ class SourcesModal extends React.Component {
|
||||
onStyleChanged: React.PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
onSourceAdd(source) {
|
||||
onSourceAdd(sourceId, source) {
|
||||
const changedSources = {
|
||||
...this.props.mapStyle.sources,
|
||||
[source.id]: source
|
||||
[sourceId]: source
|
||||
}
|
||||
|
||||
const changedStyle = {
|
||||
@@ -192,24 +203,42 @@ class SourcesModal extends React.Component {
|
||||
this.props.onStyleChanged(changedStyle)
|
||||
}
|
||||
|
||||
deleteSource(sourceId) {
|
||||
const remainingSources = { ...this.props.mapStyle.sources}
|
||||
delete remainingSources[sourceId]
|
||||
|
||||
const changedStyle = {
|
||||
...this.props.mapStyle,
|
||||
sources: remainingSources
|
||||
}
|
||||
this.props.onStyleChanged(changedStyle)
|
||||
}
|
||||
|
||||
stripTitle(source) {
|
||||
const strippedSource = {...source}
|
||||
delete strippedSource['title']
|
||||
return strippedSource
|
||||
}
|
||||
|
||||
render() {
|
||||
const activeSources = Object.keys(this.props.mapStyle.sources).map(sourceId => {
|
||||
const source = this.props.mapStyle.sources[sourceId]
|
||||
return <SourceEditorLayout
|
||||
return <ActiveSourceTypeEditor
|
||||
key={sourceId}
|
||||
sourceId={sourceId}
|
||||
source={source}
|
||||
onSourceDelete={this.deleteSource.bind(this)}
|
||||
/>
|
||||
})
|
||||
|
||||
const tilesetOptions = publicSources.filter(source => !(source.id in this.props.mapStyle.sources)).map(source => {
|
||||
const tilesetOptions = Object.keys(publicSources).filter(sourceId => !(sourceId in this.props.mapStyle.sources)).map(sourceId => {
|
||||
const source = publicSources[sourceId]
|
||||
return <PublicSource
|
||||
key={source.id}
|
||||
id={source.id}
|
||||
key={sourceId}
|
||||
id={sourceId}
|
||||
type={source.type}
|
||||
title={source.title}
|
||||
description={source.description}
|
||||
onSelect={() => this.onSourceAdd(source)}
|
||||
onSelect={() => this.onSourceAdd(sourceId, this.stripTitle(source))}
|
||||
/>
|
||||
})
|
||||
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
.darkScrollbar::-webkit-scrollbar {
|
||||
background-color: #26282e;
|
||||
width: 5px;
|
||||
}
|
||||
|
||||
.darkScrollbar::-webkit-scrollbar-thumb {
|
||||
border-radius: 6px;
|
||||
-webkit-box-shadow: inset 0 0 6px rgba(0,0,0,.3);
|
||||
background-color: #40444e;
|
||||
padding-left: 2px;
|
||||
padding-right: 2px;
|
||||
}
|
||||
@@ -1,18 +1,22 @@
|
||||
import React from 'react'
|
||||
import InputBlock from '../inputs/InputBlock'
|
||||
import StringInput from '../inputs/StringInput'
|
||||
import NumberInput from '../inputs/NumberInput'
|
||||
|
||||
class TileJSONSourceEditor extends React.Component {
|
||||
static propTypes = {
|
||||
url: React.PropTypes.string.isRequired,
|
||||
source: React.PropTypes.object.isRequired,
|
||||
onChange: React.PropTypes.func,
|
||||
}
|
||||
|
||||
render() {
|
||||
return <InputBlock label={"TileJSON URL"}>
|
||||
<StringInput
|
||||
value={this.props.url}
|
||||
onChange={this.props.onChange}
|
||||
value={this.props.source.url}
|
||||
onChange={url => this.props.onChange({
|
||||
...this.props.source,
|
||||
url: url
|
||||
})}
|
||||
/>
|
||||
</InputBlock>
|
||||
}
|
||||
@@ -20,15 +24,14 @@ class TileJSONSourceEditor extends React.Component {
|
||||
|
||||
class TileURLSourceEditor extends React.Component {
|
||||
static propTypes = {
|
||||
tiles: React.PropTypes.array.isRequired,
|
||||
minZoom: React.PropTypes.number.isRequired,
|
||||
maxZoom: React.PropTypes.number.isRequired,
|
||||
source: React.PropTypes.object.isRequired,
|
||||
onChange: React.PropTypes.func,
|
||||
}
|
||||
|
||||
renderTileUrls() {
|
||||
const prefix = ['1st', '2nd', '3rd', '4th', '5th', '6th', '7th']
|
||||
return this.props.tiles.map((tileUrl, tileIndex) => {
|
||||
const tiles = this.props.source.tiles || []
|
||||
return tiles.map((tileUrl, tileIndex) => {
|
||||
return <InputBlock key={tileIndex} label={prefix[tileIndex] + " Tile URL"}>
|
||||
<StringInput
|
||||
value={tileUrl}
|
||||
@@ -38,17 +41,24 @@ class TileURLSourceEditor extends React.Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
console.log(this.props.tiles)
|
||||
return <div>
|
||||
{this.renderTileUrls()}
|
||||
<InputBlock label={"Min Zoom"}>
|
||||
<StringInput
|
||||
value={this.props.minZoom}
|
||||
<NumberInput
|
||||
value={this.props.source.minZoom}
|
||||
onChange={minZoom => this.props.onChange({
|
||||
...this.props.source,
|
||||
minZoom: minZoom
|
||||
})}
|
||||
/>
|
||||
</InputBlock>
|
||||
<InputBlock label={"Max Zoom"}>
|
||||
<StringInput
|
||||
value={this.props.maxZoom}
|
||||
<NumberInput
|
||||
value={this.props.source.maxZoom}
|
||||
onChange={maxZoom => this.props.onChange({
|
||||
...this.props.source,
|
||||
maxZoom: maxZoom
|
||||
})}
|
||||
/>
|
||||
</InputBlock>
|
||||
</div>
|
||||
@@ -58,15 +68,18 @@ class TileURLSourceEditor extends React.Component {
|
||||
|
||||
class GeoJSONSourceEditor extends React.Component {
|
||||
static propTypes = {
|
||||
data: React.PropTypes.string.isRequired,
|
||||
source: React.PropTypes.object.isRequired,
|
||||
onChange: React.PropTypes.func,
|
||||
}
|
||||
|
||||
render() {
|
||||
return <InputBlock label={"GeoJSON Data"}>
|
||||
<StringInput
|
||||
value={this.props.data}
|
||||
onChange={this.props.onChange}
|
||||
value={this.props.source.data}
|
||||
onChange={data => this.props.onChange({
|
||||
...this.props.source,
|
||||
data: data
|
||||
})}
|
||||
/>
|
||||
</InputBlock>
|
||||
}
|
||||
@@ -80,11 +93,14 @@ class SourceTypeEditor extends React.Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const source = this.props.source
|
||||
const commonProps = {
|
||||
source: this.props.source,
|
||||
onChange: this.props.onChange,
|
||||
}
|
||||
switch(this.props.mode) {
|
||||
case 'geojson': return <GeoJSONSourceEditor data={source.data || 'http://localhost:3000/mygeojson.json'} />
|
||||
case 'tilejson': return <TileJSONSourceEditor url={source.url || 'http://localhost:3000/tiles.json'}/>
|
||||
case 'tilexyz': return <TileURLSourceEditor tiles={source.tiles || ['http://localhost:3000/{x}/{y}/{z}.pbf']} minZoom={source.minZoom || 0} maxZoom={source.maxZoom || 14}/>
|
||||
case 'geojson': return <GeoJSONSourceEditor {...commonProps} />
|
||||
case 'tilejson': return <TileJSONSourceEditor {...commonProps} />
|
||||
case 'tilexyz': return <TileURLSourceEditor {...commonProps} />
|
||||
default: return null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ const baseColors = {
|
||||
blue: '#00d9f7',
|
||||
green: '#B4C7AD',
|
||||
orange: '#fb3',
|
||||
red: '#f04',
|
||||
red: '#cf4a4a',
|
||||
}
|
||||
|
||||
const themeColors = {
|
||||
|
||||
35
src/config/fontstacks.json
Normal file
35
src/config/fontstacks.json
Normal file
@@ -0,0 +1,35 @@
|
||||
[
|
||||
"Metropolis Black Italic",
|
||||
"Metropolis Black",
|
||||
"Metropolis Bold Italic",
|
||||
"Metropolis Bold",
|
||||
"Metropolis Extra Bold Italic",
|
||||
"Metropolis Extra Bold",
|
||||
"Metropolis Extra Light Italic",
|
||||
"Metropolis Extra Light",
|
||||
"Metropolis Light Italic",
|
||||
"Metropolis Light",
|
||||
"Metropolis Medium Italic",
|
||||
"Metropolis Medium",
|
||||
"Metropolis Regular Italic",
|
||||
"Metropolis Regular",
|
||||
"Metropolis Semi Bold Italic",
|
||||
"Metropolis Semi Bold",
|
||||
"Metropolis Thin Italic",
|
||||
"Metropolis Thin",
|
||||
"Open Sans Bold Italic",
|
||||
"Open Sans Bold",
|
||||
"Open Sans Extra Bold Italic",
|
||||
"Open Sans Extra Bold",
|
||||
"Open Sans Italic",
|
||||
"Open Sans Light Italic",
|
||||
"Open Sans Light",
|
||||
"Open Sans Regular",
|
||||
"Open Sans Semibold Italic",
|
||||
"Open Sans Semibold",
|
||||
"Klokantech Noto Sans Bold",
|
||||
"Klokantech Noto Sans CJK Bold",
|
||||
"Klokantech Noto Sans CJK Regular",
|
||||
"Klokantech Noto Sans Italic",
|
||||
"Klokantech Noto Sans Regular"
|
||||
]
|
||||
@@ -3,6 +3,7 @@ import { margins, fontSizes } from './scales'
|
||||
|
||||
const base = {
|
||||
display: 'inline-block',
|
||||
boxSizing: 'border-box',
|
||||
fontSize: fontSizes[5],
|
||||
lineHeight: 2,
|
||||
paddingLeft: 5,
|
||||
@@ -11,20 +12,19 @@ const base = {
|
||||
|
||||
const label = {
|
||||
...base,
|
||||
width: '40%',
|
||||
padding: null,
|
||||
color: colors.lowgray,
|
||||
userSelect: 'none',
|
||||
}
|
||||
|
||||
const property = {
|
||||
marginTop: margins[2],
|
||||
marginBottom: margins[2],
|
||||
display: 'block',
|
||||
margin: margins[2],
|
||||
}
|
||||
|
||||
const input = {
|
||||
...base,
|
||||
border: 'none',
|
||||
width: '47%',
|
||||
backgroundColor: colors.gray,
|
||||
color: colors.lowgray,
|
||||
}
|
||||
@@ -38,8 +38,7 @@ const checkbox = {
|
||||
|
||||
const select = {
|
||||
...input,
|
||||
width: '51%',
|
||||
height: '2.3em',
|
||||
height: '2.15em',
|
||||
}
|
||||
|
||||
export default {
|
||||
|
||||
@@ -95,6 +95,79 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"fill-extrusion": {
|
||||
"groups": [
|
||||
{
|
||||
"title": "Settings",
|
||||
"type": "settings"
|
||||
},
|
||||
{
|
||||
"title": "Source",
|
||||
"type": "source"
|
||||
},
|
||||
{
|
||||
"title": "Basic",
|
||||
"type": "properties",
|
||||
"fields": [
|
||||
"fill-extrusion-opacity",
|
||||
"fill-extrusion-color",
|
||||
"fill-extrusion-translate",
|
||||
"fill-extrusion-translate-anchor",
|
||||
"fill-extrusion-pattern",
|
||||
"fill-extrusion-height",
|
||||
"fill-extrusion-base"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "JSON",
|
||||
"type": "jsoneditor"
|
||||
}
|
||||
]
|
||||
},
|
||||
"circle": {
|
||||
"groups": [
|
||||
{
|
||||
"title": "Settings",
|
||||
"type": "settings"
|
||||
},
|
||||
{
|
||||
"title": "Source",
|
||||
"type": "source"
|
||||
},
|
||||
{
|
||||
"title": "Basic",
|
||||
"type": "properties",
|
||||
"fields": [
|
||||
"circle-color",
|
||||
"circle-opacity",
|
||||
"circle-stroke-color",
|
||||
"circle-stroke-opacity",
|
||||
"circle-blur"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Scale",
|
||||
"type": "properties",
|
||||
"fields": [
|
||||
"circle-radius",
|
||||
"circle-stroke-width",
|
||||
"circle-pitch-scale"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Position",
|
||||
"type": "properties",
|
||||
"fields": [
|
||||
"circle-translate",
|
||||
"circle-translate-anchor"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "JSON",
|
||||
"type": "jsoneditor"
|
||||
}
|
||||
]
|
||||
},
|
||||
"symbol": {
|
||||
"groups": [
|
||||
{
|
||||
@@ -111,8 +184,12 @@
|
||||
"fields": [
|
||||
"text-field",
|
||||
"text-font",
|
||||
"text-color",
|
||||
"text-size",
|
||||
"text-line-height"
|
||||
"text-line-height",
|
||||
"text-halo-color",
|
||||
"text-halo-width",
|
||||
"text-halo-blur"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -121,14 +198,16 @@
|
||||
"fields": [
|
||||
"symbol-placement",
|
||||
"symbol-spacing",
|
||||
"symbol-avoid-edges",
|
||||
"text-padding",
|
||||
"symbol-avoid-edges",
|
||||
"text-allow-overlap",
|
||||
"text-ignore-placement"
|
||||
"text-ignore-placement",
|
||||
"text-translate",
|
||||
"text-translate-anchor"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Layout",
|
||||
"title": "Text",
|
||||
"type": "properties",
|
||||
"fields": [
|
||||
"text-pitch-alignment",
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
export const margins = [3, 5, 10, 30, 40]
|
||||
export const fontSizes = [26, 20, 16, 14, 12, 10]
|
||||
export const fontSizes = [24, 20, 18, 16, 14, 12]
|
||||
|
||||
@@ -28,11 +28,5 @@
|
||||
"title": "Fiord Color",
|
||||
"url": "https://rawgit.com/openmaptiles/fiord-color-gl-style/gh-pages/style-cdn.json",
|
||||
"thumbnail": "https://camo.githubusercontent.com/605f2edc30e413b37d16a6ca1d500f265725d76d/68747470733a2f2f6170692e6d6170626f782e636f6d2f7374796c65732f76312f6f70656e6d617074696c65732f6369776775693378353030317732706e7668633063327767302f7374617469632f31302e3938373235382c34362e3435333135302c332e30322c302e30302c302e30302f363030783430303f6163636573735f746f6b656e3d706b2e65794a31496a6f696233426c626d3168634852706247567a4969776959534936496d4e70646e593365544a785a7a41774d474d796233427064574a6d616a63784e7a636966512e685031427863786c644968616b4d6350534a4c513151"
|
||||
},
|
||||
{
|
||||
"id": "toner",
|
||||
"title": "Toner",
|
||||
"url": "https://rawgit.com/openmaptiles/toner-color-gl-style/gh-pages/style-cdn.json",
|
||||
"thumbnail": "https://cloud.githubusercontent.com/assets/1288339/21422755/86ebe96e-c839-11e6-8337-42742dfe34a2.png"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
[
|
||||
{
|
||||
"id": "mapbox-streets",
|
||||
{
|
||||
"mapbox-streets": {
|
||||
"type": "vector",
|
||||
"url": "mapbox://mapbox.mapbox-streets-v7",
|
||||
"title": "Mapbox Streets"
|
||||
},
|
||||
{
|
||||
"id": "tilezen",
|
||||
"tilezen": {
|
||||
"type": "vector",
|
||||
"tiles": [
|
||||
"http://tile.mapzen.com/mapzen/vector/v1/{layers}/{z}/{x}/{y}.pbf?api_key=mapzen-RVcyVL7"
|
||||
@@ -15,16 +13,14 @@
|
||||
"maxZoom": 15,
|
||||
"title": "Mapzen Vector Tile Service"
|
||||
},
|
||||
{
|
||||
"id": "openmaptiles",
|
||||
"openmaptiles": {
|
||||
"type": "vector",
|
||||
"url": "https://free.tilehosting.com/data/v3.json?key=25ItXg7aI5wurYDtttD",
|
||||
"title": "OpenMapTiles CDN"
|
||||
},
|
||||
{
|
||||
"id": "swissnames-landscape",
|
||||
"naturalearth-airports": {
|
||||
"type": "geojson",
|
||||
"data": "http://swissnames.lukasmartinelli.ch/data/landscape.geojson",
|
||||
"title": "Landscape Names GeoJSON"
|
||||
"data": "https://d2ad6b4ur7yvpq.cloudfront.net/naturalearth-3.3.0/ne_10m_airports.geojson",
|
||||
"title": "NaturalEarth Airports GeoJSON"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
BIN
src/favicon.ico
Normal file
BIN
src/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
@@ -5,6 +5,10 @@
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
html {
|
||||
background-color: rgb(28, 31, 36);
|
||||
}
|
||||
|
||||
.chrome-picker {
|
||||
background-color: #1c1f24 !important;
|
||||
font-family: inherit !important;
|
||||
@@ -15,3 +19,16 @@
|
||||
color: rgb(142, 142, 142) !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
background-color: #26282e;
|
||||
width: 5px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
border-radius: 6px;
|
||||
-webkit-box-shadow: inset 0 0 6px rgba(0,0,0,.3);
|
||||
background-color: #40444e;
|
||||
padding-left: 2px;
|
||||
padding-right: 2px;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
import './favicon.ico'
|
||||
import './index.css'
|
||||
import App from './components/App';
|
||||
|
||||
|
||||
13
src/libs/diffmessage.js
Normal file
13
src/libs/diffmessage.js
Normal file
@@ -0,0 +1,13 @@
|
||||
import diffStyles from 'mapbox-gl-style-spec/lib/diff'
|
||||
|
||||
export function diffMessages(beforeStyle, afterStyle) {
|
||||
const changes = diffStyles(beforeStyle, afterStyle)
|
||||
return changes.map(cmd => cmd.command + ' ' + cmd.args.join(' '))
|
||||
}
|
||||
|
||||
export function undoMessages(beforeStyle, afterStyle) {
|
||||
return diffMessages(beforeStyle, afterStyle).map(m => 'Undo ' + m)
|
||||
}
|
||||
export function redoMessages(beforeStyle, afterStyle) {
|
||||
return diffMessages(beforeStyle, afterStyle).map(m => 'Redo ' + m)
|
||||
}
|
||||
44
src/libs/layer.js
Normal file
44
src/libs/layer.js
Normal file
@@ -0,0 +1,44 @@
|
||||
import GlSpec from 'mapbox-gl-style-spec/reference/latest.js'
|
||||
|
||||
export function changeType(layer, newType) {
|
||||
const changedPaintProps = { ...layer.paint }
|
||||
Object.keys(changedPaintProps).forEach(propertyName => {
|
||||
if(!(propertyName in GlSpec['paint_' + newType])) {
|
||||
delete changedPaintProps[propertyName]
|
||||
}
|
||||
})
|
||||
|
||||
const changedLayoutProps = { ...layer.layout }
|
||||
Object.keys(changedLayoutProps).forEach(propertyName => {
|
||||
if(!(propertyName in GlSpec['layout_' + newType])) {
|
||||
delete changedLayoutProps[propertyName]
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
...layer,
|
||||
paint: changedPaintProps,
|
||||
layout: changedLayoutProps,
|
||||
type: newType,
|
||||
}
|
||||
}
|
||||
|
||||
/** A {@property} in either the paint our layout {@group} has changed
|
||||
* to a {@newValue}.
|
||||
*/
|
||||
export function changeProperty(layer, group, property, newValue) {
|
||||
if(group) {
|
||||
return {
|
||||
...layer,
|
||||
[group]: {
|
||||
...layer[group],
|
||||
[property]: newValue
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
...layer,
|
||||
[property]: newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,15 @@
|
||||
import throttle from 'lodash.throttle'
|
||||
import isEqual from 'lodash.isequal'
|
||||
|
||||
/** Listens to map events to build up a store of available vector
|
||||
* layers contained in the tiles */
|
||||
export default class LayerWatcher {
|
||||
constructor() {
|
||||
constructor(opts = {}) {
|
||||
this.onSourcesChange = opts.onSourcesChange || (() => {})
|
||||
this.onVectorLayersChange = opts.onVectorLayersChange || (() => {})
|
||||
|
||||
this._sources = {}
|
||||
this._vectorLayers = {}
|
||||
this._map= null
|
||||
|
||||
// Since we scan over all features we want to avoid this as much as
|
||||
// possible and only do it after a batch of data has loaded because
|
||||
@@ -14,27 +17,30 @@ export default class LayerWatcher {
|
||||
this.throttledAnalyzeVectorLayerFields = throttle(this.analyzeVectorLayerFields, 5000)
|
||||
}
|
||||
|
||||
/** Set the map as soon as the map is initialized */
|
||||
set map(m) {
|
||||
|
||||
this._map = m
|
||||
//TODO: At some point we need to unsubscribe when new map is set
|
||||
this._map.on('data', (e) => {
|
||||
if(e.dataType !== 'tile') return
|
||||
analyzeMap(map) {
|
||||
const previousSources = { ...this._sources }
|
||||
|
||||
Object.keys(map.style.sourceCaches).forEach(sourceId => {
|
||||
//NOTE: This heavily depends on the internal API of Mapbox GL
|
||||
//so this breaks between Mapbox GL JS releases
|
||||
this._sources[e.sourceId] = e.style.sourceCaches[e.sourceId]._source.vectorLayerIds
|
||||
this.throttledAnalyzeVectorLayerFields()
|
||||
this._sources[sourceId] = map.style.sourceCaches[sourceId]._source.vectorLayerIds
|
||||
})
|
||||
|
||||
if(!isEqual(previousSources, this._sources)) {
|
||||
this.onSourcesChange(this._sources)
|
||||
}
|
||||
|
||||
this.throttledAnalyzeVectorLayerFields(map)
|
||||
}
|
||||
|
||||
analyzeVectorLayerFields() {
|
||||
analyzeVectorLayerFields(map) {
|
||||
const previousVectorLayers = { ...this._vectorLayers }
|
||||
|
||||
Object.keys(this._sources).forEach(sourceId => {
|
||||
this._sources[sourceId].forEach(vectorLayerId => {
|
||||
(this._sources[sourceId] || []).forEach(vectorLayerId => {
|
||||
const knownProperties = this._vectorLayers[vectorLayerId] || {}
|
||||
const params = { sourceLayer: vectorLayerId }
|
||||
this._map.querySourceFeatures(sourceId, params).forEach(feature => {
|
||||
map.querySourceFeatures(sourceId, params).forEach(feature => {
|
||||
Object.keys(feature.properties).forEach(propertyName => {
|
||||
const knownPropertyValues = knownProperties[propertyName] || {}
|
||||
knownPropertyValues[feature.properties[propertyName]] = {}
|
||||
@@ -45,6 +51,11 @@ export default class LayerWatcher {
|
||||
this._vectorLayers[vectorLayerId] = knownProperties
|
||||
})
|
||||
})
|
||||
|
||||
if(!isEqual(previousVectorLayers, this._vectorLayers)) {
|
||||
this.onVectorLayersChange(this._vectorLayers)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/** Access all known sources and their vector tile ids */
|
||||
|
||||
35
src/libs/revisions.js
Normal file
35
src/libs/revisions.js
Normal file
@@ -0,0 +1,35 @@
|
||||
export class RevisionStore {
|
||||
constructor(initialRevisions=[]) {
|
||||
this.revisions = initialRevisions
|
||||
this.currentIdx = initialRevisions.length - 1
|
||||
}
|
||||
|
||||
get latest() {
|
||||
return this.revisions[this.revisions.length - 1]
|
||||
}
|
||||
|
||||
get current() {
|
||||
return this.revisions[this.currentIdx]
|
||||
}
|
||||
|
||||
addRevision(revision) {
|
||||
//TODO: compare new revision style id with old ones
|
||||
//and ensure that it is always the same id
|
||||
this.revisions.push(revision)
|
||||
this.currentIdx++
|
||||
}
|
||||
|
||||
undo() {
|
||||
if(this.currentIdx > 0) {
|
||||
this.currentIdx--
|
||||
}
|
||||
return this.current
|
||||
}
|
||||
|
||||
redo() {
|
||||
if(this.currentIdx < this.revisions.length - 1) {
|
||||
this.currentIdx++
|
||||
}
|
||||
return this.current
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
import React from 'react';
|
||||
import spec from 'mapbox-gl-style-spec/reference/latest.min.js'
|
||||
import derefLayers from 'mapbox-gl-style-spec/lib/deref'
|
||||
|
||||
// Empty style is always used if no style could be restored or fetched
|
||||
const emptyStyle = ensureMetadataExists({
|
||||
const emptyStyle = ensureStyleValidity({
|
||||
version: 8,
|
||||
sources: {},
|
||||
layers: [],
|
||||
@@ -24,8 +25,16 @@ function ensureHasTimestamp(style) {
|
||||
return style
|
||||
}
|
||||
|
||||
function ensureMetadataExists(style) {
|
||||
return ensureHasId(ensureHasTimestamp(style))
|
||||
function ensureHasNoRefs(style) {
|
||||
const derefedStyle = {
|
||||
...style,
|
||||
layers: derefLayers(style.layers)
|
||||
}
|
||||
return derefedStyle
|
||||
}
|
||||
|
||||
function ensureStyleValidity(style) {
|
||||
return ensureHasNoRefs(ensureHasId(ensureHasTimestamp(style)))
|
||||
}
|
||||
|
||||
function indexOfLayer(layers, layerId) {
|
||||
@@ -38,7 +47,7 @@ function indexOfLayer(layers, layerId) {
|
||||
}
|
||||
|
||||
export default {
|
||||
ensureMetadataExists,
|
||||
ensureStyleValidity,
|
||||
emptyStyle,
|
||||
indexOfLayer,
|
||||
generateId,
|
||||
|
||||
141
src/libs/stylegen.js
Normal file
141
src/libs/stylegen.js
Normal file
@@ -0,0 +1,141 @@
|
||||
import randomColor from 'randomcolor'
|
||||
import Color from 'color'
|
||||
|
||||
function assignVectorLayerColor(layerId) {
|
||||
let hue = null
|
||||
if(/water|ocean|lake|sea|river/.test(layerId)) {
|
||||
hue = 'blue'
|
||||
}
|
||||
|
||||
if(/road|highway|transport/.test(layerId)) {
|
||||
hue = 'orange'
|
||||
}
|
||||
|
||||
if(/building/.test(layerId)) {
|
||||
hue = 'yellow'
|
||||
}
|
||||
|
||||
if(/wood|forest|park|landcover|landuse/.test(layerId)) {
|
||||
hue = 'green'
|
||||
}
|
||||
|
||||
return randomColor({
|
||||
luminosity: 'bright',
|
||||
hue: hue,
|
||||
seed: layerId,
|
||||
})
|
||||
}
|
||||
|
||||
function circleLayer(source, vectorLayer, color) {
|
||||
const layer = {
|
||||
id: `${source}_${vectorLayer}_circle`,
|
||||
source: source,
|
||||
type: 'circle',
|
||||
paint: {
|
||||
'circle-color': color,
|
||||
'circle-radius': 2,
|
||||
},
|
||||
filter: ["==", "$type", "Point"]
|
||||
}
|
||||
if(vectorLayer) {
|
||||
layer['source-layer'] = vectorLayer
|
||||
}
|
||||
return layer
|
||||
}
|
||||
|
||||
function polygonLayer(source, vectorLayer, color, fillColor) {
|
||||
const layer = {
|
||||
id: `${source}_${vectorLayer}_polygon`,
|
||||
source: source,
|
||||
type: 'fill',
|
||||
paint: {
|
||||
'fill-color': fillColor,
|
||||
'fill-antialias': true,
|
||||
'fill-outline-color': color,
|
||||
},
|
||||
filter: ["==", "$type", "Polygon"]
|
||||
}
|
||||
if(vectorLayer) {
|
||||
layer['source-layer'] = vectorLayer
|
||||
}
|
||||
return layer
|
||||
}
|
||||
|
||||
function lineLayer(source, vectorLayer, color) {
|
||||
const layer = {
|
||||
id: `${source}_${vectorLayer}_line`,
|
||||
source: source,
|
||||
layout: {
|
||||
'line-join': 'round',
|
||||
'line-cap': 'round'
|
||||
},
|
||||
type: 'line',
|
||||
paint: {
|
||||
'line-color': color,
|
||||
},
|
||||
filter: ["==", "$type", "LineString"]
|
||||
}
|
||||
if(vectorLayer) {
|
||||
layer['source-layer'] = vectorLayer
|
||||
}
|
||||
return layer
|
||||
}
|
||||
|
||||
export function colorHighlightedLayer(layer) {
|
||||
if(!layer || layer.type === 'background' || layer.type === 'raster') return null
|
||||
|
||||
function changeLayer(l) {
|
||||
if(layer.filter) {
|
||||
l.filter = layer.filter
|
||||
} else {
|
||||
delete l['filter']
|
||||
}
|
||||
l.id = l.id + '_highlight'
|
||||
return l
|
||||
}
|
||||
|
||||
const color = assignVectorLayerColor(layer.id)
|
||||
const layers = []
|
||||
|
||||
if(layer.type === "fill" || layer.type === 'fill-extrusion') {
|
||||
return changeLayer(polygonLayer(layer.source, layer['source-layer'], color, Color(color).alpha(0.2).string()))
|
||||
}
|
||||
|
||||
if(layer.type === "symbol" || layer.type === 'circle') {
|
||||
return changeLayer(circleLayer(layer.source, layer['source-layer'], color))
|
||||
}
|
||||
|
||||
if(layer.type === 'line') {
|
||||
return changeLayer(lineLayer(layer.source, layer['source-layer'], color))
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export function generateColoredLayers(sources) {
|
||||
const polyLayers = []
|
||||
const circleLayers = []
|
||||
const lineLayers = []
|
||||
|
||||
Object.keys(sources).forEach(sourceId => {
|
||||
const layers = sources[sourceId]
|
||||
|
||||
// Deal with GeoJSON sources that do not have any source layers
|
||||
if(!layers) {
|
||||
const color = Color(assignVectorLayerColor(sourceId))
|
||||
circleLayers.push(circleLayer(sourceId, null, color.alpha(0.3).string()))
|
||||
lineLayers.push(lineLayer(sourceId, null, color.alpha(0.3).string()))
|
||||
polyLayers.push(polygonLayer(sourceId, null, color.alpha(0.2).string(), color.alpha(0.05).string()))
|
||||
return
|
||||
}
|
||||
|
||||
layers.forEach(layerId => {
|
||||
const color = Color(assignVectorLayerColor(layerId))
|
||||
circleLayers.push(circleLayer(sourceId, layerId, color.alpha(0.3).string()))
|
||||
lineLayers.push(lineLayer(sourceId, layerId, color.alpha(0.3).string()))
|
||||
polyLayers.push(polygonLayer(sourceId, layerId, color.alpha(0.2).string(), color.alpha(0.05).string()))
|
||||
})
|
||||
})
|
||||
|
||||
return polyLayers.concat(lineLayers).concat(circleLayers)
|
||||
}
|
||||
@@ -20,7 +20,7 @@ export function loadDefaultStyle(cb) {
|
||||
withCredentials: false,
|
||||
}, (error, response, body) => {
|
||||
if (!error && response.statusCode == 200) {
|
||||
cb(style.ensureMetadataExists(JSON.parse(body)))
|
||||
cb(style.ensureStyleValidity(JSON.parse(body)))
|
||||
} else {
|
||||
console.warn('Could not fetch default style', styleUrl)
|
||||
cb(style.emptyStyle)
|
||||
@@ -61,17 +61,6 @@ function styleKey(styleId) {
|
||||
return [storagePrefix, stylePrefix, styleId].join(":")
|
||||
}
|
||||
|
||||
// Store style independent settings
|
||||
export class SettingsStore {
|
||||
get accessToken() {
|
||||
const token = window.localStorage.getItem(storageKeys.accessToken)
|
||||
return token ? token : ""
|
||||
}
|
||||
set accessToken(val) {
|
||||
window.localStorage.setItem(storageKeys.accessToken, val)
|
||||
}
|
||||
}
|
||||
|
||||
// Manages many possible styles that are stored in the local storage
|
||||
export class StyleStore {
|
||||
// Tile store will load all items from local storage and
|
||||
@@ -106,7 +95,7 @@ export class StyleStore {
|
||||
|
||||
// Save current style replacing previous version
|
||||
save(mapStyle) {
|
||||
mapStyle = style.ensureMetadataExists(mapStyle)
|
||||
mapStyle = style.ensureStyleValidity(mapStyle)
|
||||
const key = styleKey(mapStyle.id)
|
||||
window.localStorage.setItem(key, JSON.stringify(mapStyle))
|
||||
window.localStorage.setItem(storageKeys.latest, mapStyle.id)
|
||||
|
||||
51
src/mapboxgl.css
Normal file
51
src/mapboxgl.css
Normal file
@@ -0,0 +1,51 @@
|
||||
.mapboxgl-popup-anchor-top .mapboxgl-popup-tip {
|
||||
border-bottom-color: rgb(28, 31, 36);
|
||||
}
|
||||
|
||||
.mapboxgl-popup-anchor-left .mapboxgl-popup-tip {
|
||||
border-right-color: rgb(28, 31, 36);
|
||||
}
|
||||
|
||||
.mapboxgl-popup-anchor-right .mapboxgl-popup-tip {
|
||||
border-left-color: rgb(28, 31, 36);
|
||||
}
|
||||
|
||||
.mapboxgl-popup-anchor-bottom .mapboxgl-popup-tip {
|
||||
border-top-color: rgb(28, 31, 36);
|
||||
}
|
||||
|
||||
.mapboxgl-popup-content {
|
||||
background-color: rgb(28, 31, 36);
|
||||
border-radius: 0px;
|
||||
box-shadow: rgba(0, 0, 0, 0.298039) 0px 0px 5px 0px;
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
.mapboxgl-popup-close-button {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.mapboxgl-ctrl-group {
|
||||
background: rgb(28, 31, 36);
|
||||
}
|
||||
|
||||
.mapboxgl-ctrl-group > button {
|
||||
background-color: rgb(28, 31, 36);
|
||||
border-color: rgb(28, 31, 36);
|
||||
}
|
||||
|
||||
.mapboxgl-ctrl-group > button:hover {
|
||||
background-color: rgb(86, 83, 83);
|
||||
}
|
||||
|
||||
.mapboxgl-ctrl-icon.mapboxgl-ctrl-zoom-in {
|
||||
background-image: url("data:image/svg+xml;charset=utf8,%3Csvg%20viewBox%3D%270%200%2020%2020%27%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%3E%0A%20%20%3Cpath%20style%3D%27fill%3A%238e8e8e%3B%27%20d%3D%27M%2010%206%20C%209.446%206%209%206.4459904%209%207%20L%209%209%20L%207%209%20C%206.446%209%206%209.446%206%2010%20C%206%2010.554%206.446%2011%207%2011%20L%209%2011%20L%209%2013%20C%209%2013.55401%209.446%2014%2010%2014%20C%2010.554%2014%2011%2013.55401%2011%2013%20L%2011%2011%20L%2013%2011%20C%2013.554%2011%2014%2010.554%2014%2010%20C%2014%209.446%2013.554%209%2013%209%20L%2011%209%20L%2011%207%20C%2011%206.4459904%2010.554%206%2010%206%20z%27%20%2F%3E%0A%3C%2Fsvg%3E%0A")
|
||||
}
|
||||
|
||||
.mapboxgl-ctrl-icon.mapboxgl-ctrl-zoom-out {
|
||||
background-image: url("data:image/svg+xml;charset=utf8,%3Csvg%20viewBox%3D%270%200%2020%2020%27%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%3E%0A%20%20%3Cpath%20style%3D%27fill%3A%238e8e8e%3B%27%20d%3D%27m%207%2C9%20c%20-0.554%2C0%20-1%2C0.446%20-1%2C1%200%2C0.554%200.446%2C1%201%2C1%20l%206%2C0%20c%200.554%2C0%201%2C-0.446%201%2C-1%200%2C-0.554%20-0.446%2C-1%20-1%2C-1%20z%27%20%2F%3E%0A%3C%2Fsvg%3E%0A")
|
||||
}
|
||||
|
||||
.mapboxgl-ctrl-icon.mapboxgl-ctrl-compass > span.arrow {
|
||||
background-image: url("data:image/svg+xml;charset=utf8,%3Csvg%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%20viewBox%3D%270%200%2020%2020%27%3E%0A%09%3Cpolygon%20fill%3D%27%238e8e8e%27%20points%3D%276%2C9%2010%2C1%2014%2C9%27%2F%3E%0A%09%3Cpolygon%20fill%3D%27%23CCCCCC%27%20points%3D%276%2C11%2010%2C19%2014%2C11%20%27%2F%3E%0A%3C%2Fsvg%3E")
|
||||
}
|
||||
7
test/sample_test.js
Normal file
7
test/sample_test.js
Normal file
@@ -0,0 +1,7 @@
|
||||
import assert from 'assert'
|
||||
|
||||
describe('Component', () => {
|
||||
it('#always successds', () => {
|
||||
assert.equal(1, 1)
|
||||
})
|
||||
})
|
||||
@@ -1,16 +0,0 @@
|
||||
import { SettingsStore } from '../src/stylestore.js'
|
||||
import assert from 'assert'
|
||||
|
||||
describe('SettingsStore', () => {
|
||||
const store = new SettingsStore()
|
||||
|
||||
it('#get should return access token from local storage', () => {
|
||||
window.localStorage.setItem('maputnik:access_token', 'OLD_TOKEN')
|
||||
assert.equal(store.accessToken, 'OLD_TOKEN')
|
||||
})
|
||||
|
||||
it('#set should set access token in local storage', () => {
|
||||
store.accessToken = 'NEW_TOKEN'
|
||||
assert.equal(window.localStorage.getItem('maputnik:access_token'), 'NEW_TOKEN')
|
||||
})
|
||||
})
|
||||
@@ -7,16 +7,6 @@ var HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
const HOST = process.env.HOST || "127.0.0.1";
|
||||
const PORT = process.env.PORT || "8888";
|
||||
|
||||
// local scss modules
|
||||
loaders.push({
|
||||
test: /[\/\\]src[\/\\].*\.scss/,
|
||||
loaders: [
|
||||
'style?sourceMap',
|
||||
'css?modules&importLoaders=1&localIdentName=[path]___[name]__[local]___[hash:base64:5]',
|
||||
'sass'
|
||||
]
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
target: 'web',
|
||||
entry: [
|
||||
@@ -30,20 +20,10 @@ module.exports = {
|
||||
filename: 'bundle.js'
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'webworkify': 'webworkify-webpack',
|
||||
// TODO: otherwise I get a max call stack error in browser?
|
||||
// 'mapbox-gl': path.resolve('./node_modules/mapbox-gl/dist/mapbox-gl.js')
|
||||
},
|
||||
extensions: ['', '.js', '.jsx']
|
||||
},
|
||||
module: {
|
||||
loaders,
|
||||
postLoaders: [{
|
||||
include: /node_modules\/mapbox-gl\/js\/render\/shaders.js/,
|
||||
loader: 'transform',
|
||||
query: 'brfs'
|
||||
}]
|
||||
loaders
|
||||
},
|
||||
node: {
|
||||
fs: "empty",
|
||||
|
||||
@@ -14,47 +14,21 @@ module.exports = [
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /\.js?$/,
|
||||
include: /node_modules\/mapbox-gl\//,
|
||||
loader: 'babel',
|
||||
query: {
|
||||
presets: ['react'],
|
||||
plugins: ['transform-flow-strip-types'],
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /\.(eot|svg|ttf|woff|woff2)$/,
|
||||
test: /\.(eot|ttf|woff|woff2)$/,
|
||||
loader: 'file?name=fonts/[name].[ext]'
|
||||
},
|
||||
{
|
||||
test: /\.svg(\?v=\d+\.\d+\.\d+)?$/,
|
||||
exclude: /(node_modules|bower_components)/,
|
||||
loader: "url?limit=10000&mimetype=image/svg+xml"
|
||||
test: /\.ico$/,
|
||||
loader: 'file?name=[name].[ext]'
|
||||
},
|
||||
{
|
||||
test: /\.gif/,
|
||||
exclude: /(node_modules|bower_components)/,
|
||||
loader: "url-loader?limit=10000&mimetype=image/gif"
|
||||
},
|
||||
{
|
||||
test: /\.jpg/,
|
||||
exclude: /(node_modules|bower_components)/,
|
||||
loader: "url-loader?limit=10000&mimetype=image/jpg"
|
||||
},
|
||||
{
|
||||
test: /\.png/,
|
||||
exclude: /(node_modules|bower_components)/,
|
||||
loader: "url-loader?limit=10000&mimetype=image/png"
|
||||
test: /\.(svg|gif|jpg|png)$/,
|
||||
loader: 'file?name=img/[name].[ext]'
|
||||
},
|
||||
{
|
||||
test: /\.json$/,
|
||||
loader: 'json-loader'
|
||||
},
|
||||
{
|
||||
test: /\.js$/,
|
||||
include: /node_modules\/mapbox-gl\/js\/render\/shaders.js/,
|
||||
loader: 'transform/cacheable?brfs'
|
||||
},
|
||||
{
|
||||
test: /[\/\\](node_modules|global|src)[\/\\].*\.css$/,
|
||||
loaders: [
|
||||
|
||||
@@ -6,18 +6,12 @@ var ExtractTextPlugin = require('extract-text-webpack-plugin');
|
||||
var HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
var WebpackCleanupPlugin = require('webpack-cleanup-plugin');
|
||||
|
||||
// local scss modules
|
||||
loaders.push({
|
||||
test: /[\/\\]src[\/\\].*\.scss/,
|
||||
loader: ExtractTextPlugin.extract('style', 'css?modules&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]', 'sass')
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
entry: {
|
||||
app: './src/index.jsx',
|
||||
vendor: [
|
||||
'file-saver',
|
||||
'mapbox-gl',
|
||||
'mapbox-gl/dist/mapbox-gl.js',
|
||||
//TODO: Build failure because cannot resolve migrations file
|
||||
//"mapbox-gl-style-spec",
|
||||
"randomcolor",
|
||||
@@ -45,18 +39,10 @@ module.exports = {
|
||||
chunkFilename: '[chunkhash].js'
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'webworkify': 'webworkify-webpack',
|
||||
},
|
||||
extensions: ['', '.js', '.jsx']
|
||||
},
|
||||
module: {
|
||||
loaders,
|
||||
postLoaders: [{
|
||||
include: /node_modules\/mapbox-gl-shaders/,
|
||||
loader: 'transform',
|
||||
query: 'brfs'
|
||||
}]
|
||||
loaders
|
||||
},
|
||||
node: {
|
||||
fs: "empty",
|
||||
|
||||
Reference in New Issue
Block a user