mirror of
https://github.com/maputnik/editor.git
synced 2025-12-08 15:20:02 +00:00
Compare commits
120 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
24dc71344e | ||
|
|
82a11e4b98 | ||
|
|
fc8665ed93 | ||
|
|
ca9424e23d | ||
|
|
99856b1bb3 | ||
|
|
fb518c2be5 | ||
|
|
1248a53029 | ||
|
|
6ce43840e5 | ||
|
|
41d9fb1c44 | ||
|
|
fd9be8f08f | ||
|
|
69a665373f | ||
|
|
8c2b110115 | ||
|
|
5e3b2dd0df | ||
|
|
d045213fa3 | ||
|
|
63bba67750 | ||
|
|
52e8fd2c29 | ||
|
|
5479b240e1 | ||
|
|
f209d8e9a5 | ||
|
|
ac40d7727e | ||
|
|
7bd9d3f5da | ||
|
|
68685dcf42 | ||
|
|
6be6db8f5e | ||
|
|
236dd79b85 | ||
|
|
7d905c5e06 | ||
|
|
6fa2542b56 | ||
|
|
7627b8fb45 | ||
|
|
5901427534 | ||
|
|
a30e57c4d8 | ||
|
|
69f2e12ea0 | ||
|
|
93c7f323fc | ||
|
|
cbe2a4c180 | ||
|
|
2e0cc4511c | ||
|
|
bcab165f97 | ||
|
|
2516fba105 | ||
|
|
9ca8760564 | ||
|
|
df94d9c842 | ||
|
|
abceb457c9 | ||
|
|
26a865bb50 | ||
|
|
d0f047d88a | ||
|
|
76d2d06e77 | ||
|
|
6c56006fbf | ||
|
|
bbe45cf8ee | ||
|
|
82da251218 | ||
|
|
196d9f0a10 | ||
|
|
cb752c0343 | ||
|
|
3917a3e323 | ||
|
|
fed1f09434 | ||
|
|
840778b64f | ||
|
|
0908856b4f | ||
|
|
b51354ae1d | ||
|
|
9ef24428fe | ||
|
|
4a75b0381b | ||
|
|
2426117233 | ||
|
|
d40c704c69 | ||
|
|
cb4fdb0f9f | ||
|
|
f0d04bdb07 | ||
|
|
df61ae8c7a | ||
|
|
2ff8ec07bb | ||
|
|
6021b51385 | ||
|
|
40111e0d8e | ||
|
|
43d9440e05 | ||
|
|
3a3e90c3dc | ||
|
|
104d6311ec | ||
|
|
f5256cf80a | ||
|
|
b470885263 | ||
|
|
7ff0ac9bb5 | ||
|
|
0fb59ca544 | ||
|
|
09b6b2dffe | ||
|
|
a8a3b7a5ad | ||
|
|
766a3e387e | ||
|
|
ec9fc8f6ad | ||
|
|
0f272e233b | ||
|
|
f806e797fa | ||
|
|
cff0a15f7e | ||
|
|
d3276829b2 | ||
|
|
a3caf8499c | ||
|
|
d739ca812c | ||
|
|
cb89ca6ef7 | ||
|
|
c3417241f1 | ||
|
|
5d70de6202 | ||
|
|
c09ffc9d41 | ||
|
|
e19a41d015 | ||
|
|
0a0400a297 | ||
|
|
153232c143 | ||
|
|
7e8813f417 | ||
|
|
b72f86a78d | ||
|
|
fed530f5f2 | ||
|
|
ba0a94f3ad | ||
|
|
d9b458d7fd | ||
|
|
ed9b806143 | ||
|
|
5bb68a38c2 | ||
|
|
cfeaf2cdce | ||
|
|
887b23ce1f | ||
|
|
f227392f9b | ||
|
|
2f7658e245 | ||
|
|
4f0c641eb0 | ||
|
|
1538f2e174 | ||
|
|
580068bf63 | ||
|
|
91604afccb | ||
|
|
c363c88f23 | ||
|
|
e9daee4470 | ||
|
|
118f0360d0 | ||
|
|
7c9dcb3083 | ||
|
|
7c3906fa40 | ||
|
|
7b24cbf39b | ||
|
|
e7b11d8bc9 | ||
|
|
08854cd88f | ||
|
|
cb46ac5421 | ||
|
|
c9fd00e2ed | ||
|
|
7c23fe3646 | ||
|
|
56aacb0149 | ||
|
|
12411ee886 | ||
|
|
85cef2945d | ||
|
|
a1dfeca6e0 | ||
|
|
3be6d14637 | ||
|
|
74b3ef9e88 | ||
|
|
019dfe9f8a | ||
|
|
e92dfd8284 | ||
|
|
fa38667125 | ||
|
|
ce39ae723c |
@@ -16,6 +16,7 @@ install:
|
||||
- npm install
|
||||
script:
|
||||
- mkdir public
|
||||
- npm run build
|
||||
- node --stack_size=100000 $(which npm) run build
|
||||
- npm run lint
|
||||
- npm run lint-styles
|
||||
- npm run test
|
||||
|
||||
116
README.md
116
README.md
@@ -3,20 +3,25 @@
|
||||
<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**.
|
||||
targeted at developers and map designers.
|
||||
|
||||
Check it out at **http://maputnik.com/editor/**
|
||||
- :link: Design your maps online at **http://maputnik.com/editor/** (all in local storage)
|
||||
- :link: Use the [Maputnik CLI](https://github.com/maputnik/editor/wiki/Maputnik-CLI) for local style development
|
||||
|
||||
*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)*.
|
||||
## Documentation
|
||||
|
||||
## Latest Status Update Video
|
||||
The documentation can be found in the [Wiki](https://github.com/maputnik/editor/wiki). You are welcome to collaborate!
|
||||
|
||||

|
||||
- :link: **Study the [Maputnik Wiki](https://github.com/maputnik/editor/wiki)**
|
||||
- :video_camera: Design a map from Scratch https://youtu.be/XoDh0gEnBQo
|
||||
|
||||
[](https://youtu.be/XoDh0gEnBQo)
|
||||
|
||||
Mapbox has built one of the best and most amazing OSS ecosystems. A key component to ensure its longevity and independance is an OSS map designer.
|
||||
|
||||
## Develop
|
||||
|
||||
Maputnik is written in ES6 and is using [React](https://github.com/facebook/react), [Immutable.js](https://facebook.github.io/immutable-js/) and [Mapbox GL JS](https://www.mapbox.com/mapbox-gl-js/api/).
|
||||
Maputnik is written in ES6 and is using [React](https://github.com/facebook/react) and [Mapbox GL JS](https://www.mapbox.com/mapbox-gl-js/api/).
|
||||
|
||||
We ensure building and developing Maputnik works with
|
||||
|
||||
@@ -41,103 +46,64 @@ npm run build
|
||||
Lint the JavaScript code.
|
||||
|
||||
```
|
||||
# install lint dependencies
|
||||
npm install --save-dev eslint eslint-plugin-react
|
||||
# run linter
|
||||
npm run lint
|
||||
```
|
||||
|
||||
## Docker
|
||||
|
||||
Start a container using the official Docker image.
|
||||
```
|
||||
docker run --name maputnik -p 8888:8888 -d maputnik/editor
|
||||
```
|
||||
|
||||
Stop the container
|
||||
|
||||
```
|
||||
docker stop maputnik
|
||||
npm run lint-styles
|
||||
```
|
||||
|
||||
## Sponsors
|
||||
|
||||
This project would not be possible without commercial and individual sponsors.
|
||||
Thanks to the supporters of the **[Kickstarter campaign](https://www.kickstarter.com/projects/174808720/maputnik-visual-map-editor-for-mapbox-gl)**. This project would not be possible without these commercial and individual sponsors.
|
||||
|
||||
### Gold
|
||||
|
||||
[](https://getwemap.com/)
|
||||
- [Wemap](https://getwemap.com/)
|
||||
- [Orbicon Informatik](https://www.orbiconinformatik.dk/)
|
||||
- [Terranodo](http://terranodo.io/)
|
||||
|
||||
[](http://terranodo.io/)
|
||||
<a href="https://getwemap.com/">
|
||||
<img width="33%" alt="Wemap" style="display:inline" src="media/sponsors/wemap.jpg" />
|
||||
</a>
|
||||
<a href="http://terranodo.io/">
|
||||
<img width="33%" alt="Terranodo" style="display:inline" src="media/sponsors/terranodo.png" />
|
||||
</a>
|
||||
<a href="https://www.orbiconinformatik.dk/">
|
||||
<img width="32%" alt="Terranodo" style="display:inline" src="media/sponsors/orbicon_informatik.png" />
|
||||
</a>
|
||||
|
||||
<br/>
|
||||
|
||||
### Silver
|
||||
|
||||
- [Klokan Technologies](https://www.klokantech.com/)
|
||||
- [Geofabrik](http://www.geofabrik.de/)
|
||||
- [Dreipol](https://www.dreipol.ch/)
|
||||
|
||||
<a href="https://www.klokantech.com/">
|
||||
<img alt="Klokan Technologies" style="display:inline" src="media/sponsors/klokantech.png" />
|
||||
<img width="18%" alt="Klokan Technologies" style="display:inline-block" src="media/sponsors/klokantech.png" />
|
||||
</a>
|
||||
<a href="http://www.geofabrik.de/">
|
||||
<img width="18%" alt="Geofabrik" style="display:inline-block" src="media/sponsors/geofabrik.png" />
|
||||
</a>
|
||||
<a href="https://www.dreipol.ch/">
|
||||
<img alt="Dreipol" style="display:inline" src="media/sponsors/dreipol.png" />
|
||||
<img width="18%" alt="Dreipol" style="display:inline-block" src="media/sponsors/dreipol.png" />
|
||||
</a>
|
||||
|
||||
<br/>
|
||||
|
||||
### Individuals
|
||||
|
||||
**Influential Stakeholder**
|
||||
|
||||
- Alan McConchie
|
||||
- Odi
|
||||
- Mats Norén
|
||||
- Uli [geOps](http://geops.ch/)
|
||||
- Helge Fahrnberger
|
||||
Kirusanth Poopalasingam
|
||||
Alan McConchie, Odi, Mats Norén, Uli [geOps](http://geops.ch/), Helge Fahrnberger ([Toursprung](http://www.toursprung.com/)), Kirusanth Poopalasingam
|
||||
|
||||
**Stakeholder**
|
||||
|
||||
- Brian Flood
|
||||
- Vasile Coțovanu
|
||||
- Andreas Kalkbrenner
|
||||
- Christian Mäder
|
||||
- Gregor Wassmann
|
||||
- Lee Armstrong
|
||||
- Rafel
|
||||
- Jon Burgess
|
||||
- Lukas Lehmann
|
||||
- Joachim Ungar
|
||||
- Alois Ackermann
|
||||
- Zsolt Ero
|
||||
- Jordan Meek
|
||||
Brian Flood, Vasile Coțovanu, Andreas Kalkbrenner, Christian Mäder, Gregor Wassmann, Lee Armstrong, Rafel, Jon Burgess, Lukas Lehmann, Joachim Ungar, Alois Ackermann, Zsolt Ero, Jordan Meek
|
||||
|
||||
**Supporter**
|
||||
|
||||
- Sina Martinelli
|
||||
- Nicholas Doiron
|
||||
- Neil Cawse
|
||||
- Urs42
|
||||
- Benedikt Groß
|
||||
- Manuel Roth
|
||||
- Janko Mihelić
|
||||
- Moritz Stefaner
|
||||
- Sebastian Ahoi
|
||||
- Juerg Uhlmann
|
||||
- Tom Wider
|
||||
- Nadia Panchaud
|
||||
- Oliver Snowden
|
||||
- Stephan Heuel
|
||||
- Tobin Bradley
|
||||
- Adrian Herzog
|
||||
- Antti Lehto
|
||||
- Pascal Mages
|
||||
- Marc Gehling
|
||||
- Imre Samu
|
||||
- Lauri K.
|
||||
- Visahavel Parthasarathy
|
||||
- Christophe Waterlot-Buisine
|
||||
- Max Galka
|
||||
- ubahnverleih
|
||||
- Wouter van Dam
|
||||
- Jakob Lobensteiner
|
||||
- Samuel Kurath
|
||||
- Brian Bancroft
|
||||
Sina Martinelli, Nicholas Doiron, Neil Cawse, Urs42, Benedikt Groß, Manuel Roth, Janko Mihelić, Moritz Stefaner, Sebastian Ahoi, Juerg Uhlmann, Tom Wider, Nadia Panchaud, Oliver Snowden, Stephan Heuel, Tobin Bradley, Adrian Herzog, Antti Lehto, Pascal Mages, Marc Gehling, Imre Samu, Lauri K., Visahavel Parthasarathy, Christophe Waterlot-Buisine, Max Galka, ubahnverleih, Wouter van Dam, Jakob Lobensteiner, Samuel Kurath, Brian Bancroft
|
||||
|
||||
## License
|
||||
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 410 KiB |
BIN
media/sponsors/geofabrik.png
Normal file
BIN
media/sponsors/geofabrik.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.2 KiB |
BIN
media/sponsors/orbicon_informatik.png
Normal file
BIN
media/sponsors/orbicon_informatik.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 65 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 37 KiB |
45
package.json
45
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "maputnik",
|
||||
"version": "0.0.1",
|
||||
"version": "1.0.0",
|
||||
"description": "A MapboxGL visual style editor",
|
||||
"main": "''",
|
||||
"scripts": {
|
||||
@@ -9,7 +9,8 @@
|
||||
"test": "karma start --single-run",
|
||||
"test-watch": "karma start",
|
||||
"start": "webpack-dev-server --progress --profile --colors --watch-poll",
|
||||
"lint": "eslint --ext js --ext jsx {src,test}"
|
||||
"lint": "eslint --ext js --ext jsx {src,test}",
|
||||
"lint-styles": "stylelint 'src/styles/*.scss'"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -19,18 +20,20 @@
|
||||
"license": "MIT",
|
||||
"homepage": "https://github.com/maputnik/editor#readme",
|
||||
"dependencies": {
|
||||
"classnames": "^2.2.5",
|
||||
"codemirror": "^5.18.2",
|
||||
"color": "^1.0.3",
|
||||
"file-saver": "^1.3.2",
|
||||
"github-api": "^3.0.0",
|
||||
"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": "^0.29.0",
|
||||
"mapbox-gl": "^0.31.0",
|
||||
"mapbox-gl-inspect": "^1.2.0",
|
||||
"mapbox-gl-style-spec": "^8.11.0",
|
||||
"mousetrap": "^1.6.0",
|
||||
"ol-mapbox-style": "0.0.11",
|
||||
"ol-mapbox-style": "1.0.1",
|
||||
"openlayers": "^3.19.1",
|
||||
"randomcolor": "^0.4.4",
|
||||
"react": "^15.4.0",
|
||||
@@ -46,7 +49,9 @@
|
||||
"react-icons": "^2.2.1",
|
||||
"react-motion": "^0.4.7",
|
||||
"react-sortable-hoc": "^0.4.5",
|
||||
"request": "^2.79.0"
|
||||
"reconnecting-websocket": "^3.0.3",
|
||||
"request": "^2.79.0",
|
||||
"url": "^0.11.0"
|
||||
},
|
||||
"babel": {
|
||||
"presets": [
|
||||
@@ -60,6 +65,9 @@
|
||||
"jshintConfig": {
|
||||
"esversion": 6
|
||||
},
|
||||
"stylelint": {
|
||||
"extends": "stylelint-config-standard"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"plugins": [
|
||||
"react"
|
||||
@@ -84,18 +92,18 @@
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-core": "6.14.0",
|
||||
"babel-eslint": "^6.1.2",
|
||||
"babel-loader": "6.2.4",
|
||||
"babel-core": "6.21.0",
|
||||
"babel-eslint": "^7.1.1",
|
||||
"babel-loader": "6.2.10",
|
||||
"babel-plugin-transform-class-properties": "^6.11.5",
|
||||
"babel-plugin-transform-decorators-legacy": "^1.3.4",
|
||||
"babel-plugin-transform-flow-strip-types": "^6.21.0",
|
||||
"babel-plugin-transform-object-rest-spread": "^6.8.0",
|
||||
"babel-plugin-transform-runtime": "^6.15.0",
|
||||
"babel-preset-es2015": "6.14.0",
|
||||
"babel-preset-react": "6.11.1",
|
||||
"babel-preset-es2015": "6.18.0",
|
||||
"babel-preset-react": "6.16.0",
|
||||
"babel-runtime": "^6.11.6",
|
||||
"css-loader": "0.25.0",
|
||||
"css-loader": "0.26.1",
|
||||
"eslint": "^3.5.0",
|
||||
"eslint-plugin-react": "^6.2.0",
|
||||
"extract-text-webpack-plugin": "^1.0.1",
|
||||
@@ -106,18 +114,19 @@
|
||||
"karma-chrome-launcher": "^2.0.0",
|
||||
"karma-firefox-launcher": "^1.0.0",
|
||||
"karma-mocha": "^1.3.0",
|
||||
"karma-webpack": "^1.8.0",
|
||||
"karma-webpack": "^2.0.1",
|
||||
"mocha": "^3.1.2",
|
||||
"mocha-loader": "^1.0.0",
|
||||
"node-sass": "^3.9.2",
|
||||
"node-sass": "^4.2.0",
|
||||
"react-hot-loader": "^3.0.0-beta.6",
|
||||
"sass-loader": "^4.0.1",
|
||||
"style-loader": "0.13.1",
|
||||
"stylelint": "^7.7.1",
|
||||
"stylelint-config-standard": "^15.0.1",
|
||||
"transform-loader": "^0.2.3",
|
||||
"url-loader": "0.5.7",
|
||||
"webpack": "1.13.2",
|
||||
"webpack-cleanup-plugin": "^0.3.0",
|
||||
"webpack-dev-server": "1.15.1",
|
||||
"webworkify-webpack": "^1.1.3"
|
||||
"webpack": "1.14.0",
|
||||
"webpack-cleanup-plugin": "^0.4.1",
|
||||
"webpack-dev-server": "1.16.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
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'
|
||||
@@ -11,30 +9,54 @@ import Toolbar from './Toolbar'
|
||||
import AppLayout from './AppLayout'
|
||||
import MessagePanel from './MessagePanel'
|
||||
|
||||
import { downloadGlyphsMetadata, downloadSpriteMetadata } from '../libs/metadata'
|
||||
import GlSpec from 'mapbox-gl-style-spec/reference/latest.js'
|
||||
import validateStyleMin from 'mapbox-gl-style-spec/lib/validate_style.min'
|
||||
import formatStyle from 'mapbox-gl-style-spec/lib/format'
|
||||
import style from '../libs/style.js'
|
||||
import { initialStyleUrl, loadStyleUrl } from '../libs/urlopen'
|
||||
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'
|
||||
import tokens from '../config/tokens.json'
|
||||
|
||||
function updateRootSpec(spec, fieldName, newValues) {
|
||||
return {
|
||||
...spec,
|
||||
$root: {
|
||||
...spec.$root,
|
||||
[fieldName]: {
|
||||
...spec.$root[fieldName],
|
||||
values: newValues
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default class App extends React.Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.styleStore = new ApiStyleStore()
|
||||
this.revisionStore = new RevisionStore()
|
||||
|
||||
this.styleStore.supported(isSupported => {
|
||||
if(!isSupported) {
|
||||
console.log('Falling back to local storage for storing styles')
|
||||
this.styleStore = new StyleStore()
|
||||
}
|
||||
this.styleStore.latestStyle(mapStyle => this.onStyleChanged(mapStyle))
|
||||
this.styleStore = new ApiStyleStore({
|
||||
onLocalStyleChange: mapStyle => this.onStyleChanged(mapStyle, false)
|
||||
})
|
||||
|
||||
const styleUrl = initialStyleUrl()
|
||||
if(styleUrl) {
|
||||
this.styleStore = new StyleStore()
|
||||
loadStyleUrl(styleUrl, mapStyle => this.onStyleChanged(mapStyle))
|
||||
} else {
|
||||
this.styleStore.init(err => {
|
||||
if(err) {
|
||||
console.log('Falling back to local storage for storing styles')
|
||||
this.styleStore = new StyleStore()
|
||||
}
|
||||
this.styleStore.latestStyle(mapStyle => this.onStyleChanged(mapStyle))
|
||||
})
|
||||
}
|
||||
|
||||
this.state = {
|
||||
errors: [],
|
||||
infos: [],
|
||||
@@ -42,6 +64,8 @@ export default class App extends React.Component {
|
||||
selectedLayerIndex: 0,
|
||||
sources: {},
|
||||
vectorLayers: {},
|
||||
inspectModeEnabled: false,
|
||||
spec: GlSpec,
|
||||
}
|
||||
|
||||
this.layerWatcher = new LayerWatcher({
|
||||
@@ -65,22 +89,36 @@ export default class App extends React.Component {
|
||||
loadDefaultStyle(mapStyle => this.onStyleOpen(mapStyle))
|
||||
}
|
||||
|
||||
onStyleDownload() {
|
||||
const mapStyle = this.state.mapStyle
|
||||
const blob = new Blob([JSON.stringify(mapStyle, null, 4)], {type: "application/json;charset=utf-8"});
|
||||
saveAs(blob, mapStyle.id + ".json");
|
||||
}
|
||||
|
||||
saveStyle(snapshotStyle) {
|
||||
snapshotStyle.modified = new Date().toJSON()
|
||||
this.styleStore.save(snapshotStyle)
|
||||
}
|
||||
|
||||
onStyleChanged(newStyle) {
|
||||
updateFonts(urlTemplate) {
|
||||
const metadata = this.state.mapStyle.metadata || {}
|
||||
const accessToken = metadata['maputnik:openmaptiles_access_token'] || tokens.openmaptiles
|
||||
downloadGlyphsMetadata(urlTemplate.replace('{key}', accessToken), fonts => {
|
||||
this.setState({ spec: updateRootSpec(this.state.spec, 'glyphs', fonts)})
|
||||
})
|
||||
}
|
||||
|
||||
updateIcons(baseUrl) {
|
||||
downloadSpriteMetadata(baseUrl, icons => {
|
||||
this.setState({ spec: updateRootSpec(this.state.spec, 'sprite', icons)})
|
||||
})
|
||||
}
|
||||
|
||||
onStyleChanged(newStyle, save=true) {
|
||||
if(newStyle.glyphs !== this.state.mapStyle.glyphs) {
|
||||
this.updateFonts(newStyle.glyphs)
|
||||
}
|
||||
if(newStyle.sprite !== this.state.mapStyle.sprite) {
|
||||
this.updateIcons(newStyle.sprite)
|
||||
}
|
||||
|
||||
const errors = validateStyleMin(newStyle, GlSpec)
|
||||
if(errors.length === 0) {
|
||||
this.revisionStore.addRevision(newStyle)
|
||||
this.saveStyle(newStyle)
|
||||
if(save) this.saveStyle(newStyle)
|
||||
this.setState({
|
||||
mapStyle: newStyle,
|
||||
errors: [],
|
||||
@@ -140,32 +178,30 @@ export default class App extends React.Component {
|
||||
this.onLayersChange(changedLayers)
|
||||
}
|
||||
|
||||
changeInspectMode() {
|
||||
this.setState({
|
||||
inspectModeEnabled: !this.state.inspectModeEnabled
|
||||
})
|
||||
}
|
||||
|
||||
mapRenderer() {
|
||||
const metadata = this.state.mapStyle.metadata || {}
|
||||
const mapProps = {
|
||||
mapStyle: this.state.mapStyle,
|
||||
accessToken: metadata['maputnik:access_token'],
|
||||
mapStyle: style.replaceAccessToken(this.state.mapStyle),
|
||||
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} />
|
||||
return <MapboxGlMap {...mapProps}
|
||||
inspectModeEnabled={this.state.inspectModeEnabled}
|
||||
highlightedLayer={this.state.mapStyle.layers[this.state.selectedLayerIndex]} />
|
||||
}
|
||||
}
|
||||
|
||||
@@ -181,10 +217,11 @@ export default class App extends React.Component {
|
||||
|
||||
const toolbar = <Toolbar
|
||||
mapStyle={this.state.mapStyle}
|
||||
inspectModeEnabled={this.state.inspectModeEnabled}
|
||||
sources={this.state.sources}
|
||||
onStyleChanged={this.onStyleChanged.bind(this)}
|
||||
onStyleOpen={this.onStyleChanged.bind(this)}
|
||||
onStyleDownload={this.onStyleDownload.bind(this)}
|
||||
onInspectModeToggle={this.changeInspectMode.bind(this)}
|
||||
/>
|
||||
|
||||
const layerList = <LayerList
|
||||
@@ -192,12 +229,14 @@ export default class App extends React.Component {
|
||||
onLayerSelect={this.onLayerSelect.bind(this)}
|
||||
selectedLayerIndex={this.state.selectedLayerIndex}
|
||||
layers={layers}
|
||||
sources={this.state.sources}
|
||||
/>
|
||||
|
||||
const layerEditor = selectedLayer ? <LayerEditor
|
||||
layer={selectedLayer}
|
||||
sources={this.state.sources}
|
||||
vectorLayers={this.state.vectorLayers}
|
||||
spec={this.state.spec}
|
||||
onLayerChanged={this.onLayerChanged.bind(this)}
|
||||
onLayerIdChange={this.onLayerIdChange.bind(this)}
|
||||
/> : null
|
||||
@@ -216,4 +255,3 @@ export default class App extends React.Component {
|
||||
/>
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import React from 'react'
|
||||
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 = {
|
||||
toolbar: React.PropTypes.element.isRequired,
|
||||
@@ -20,56 +16,25 @@ class AppLayout extends React.Component {
|
||||
|
||||
getChildContext() {
|
||||
return {
|
||||
reactIconBase: { size: fontSizes[3] }
|
||||
reactIconBase: { size: 14 }
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div style={{
|
||||
fontFamily: theme.fontFamily,
|
||||
color: theme.color,
|
||||
fontWeight: 300
|
||||
}}>
|
||||
return <div className="maputnik-layout">
|
||||
{this.props.toolbar}
|
||||
<div style={{
|
||||
position: 'fixed',
|
||||
bottom: 0,
|
||||
height: "100%",
|
||||
top: 40,
|
||||
left: 0,
|
||||
zIndex: 1,
|
||||
width: 200,
|
||||
overflow: "hidden",
|
||||
backgroundColor: colors.black
|
||||
}}>
|
||||
<div className="maputnik-layout-list">
|
||||
<ScrollContainer>
|
||||
{this.props.layerList}
|
||||
</ScrollContainer>
|
||||
</div>
|
||||
<div style={{
|
||||
position: 'fixed',
|
||||
bottom: 0,
|
||||
height: "100%",
|
||||
top: 40,
|
||||
left: 200,
|
||||
zIndex: 1,
|
||||
width: 350,
|
||||
backgroundColor: colors.black
|
||||
}}>
|
||||
<div className="maputnik-layout-drawer">
|
||||
<ScrollContainer>
|
||||
{this.props.layerEditor}
|
||||
</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 className="maputnik-layout-bottom">
|
||||
{this.props.bottom}
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -1,26 +1,18 @@
|
||||
import React from 'react'
|
||||
import colors from '../config/colors'
|
||||
import { margins, fontSizes } from '../config/scales'
|
||||
import classnames from 'classnames'
|
||||
|
||||
class Button extends React.Component {
|
||||
static propTypes = {
|
||||
onClick: React.PropTypes.func,
|
||||
style: React.PropTypes.object,
|
||||
className: React.PropTypes.string,
|
||||
}
|
||||
|
||||
render() {
|
||||
return <a
|
||||
onClick={this.props.onClick}
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
backgroundColor: colors.midgray,
|
||||
color: colors.lowgray,
|
||||
fontSize: fontSizes[5],
|
||||
padding: margins[1],
|
||||
userSelect: 'none',
|
||||
borderRadius: 2,
|
||||
...this.props.style,
|
||||
}}>
|
||||
className={classnames("maputnik-button", this.props.className)}
|
||||
style={this.props.style}>
|
||||
{this.props.children}
|
||||
</a>
|
||||
}
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
import React from 'react'
|
||||
import { fontSizes, margins } from '../config/scales'
|
||||
|
||||
class Heading extends React.Component {
|
||||
static propTypes = {
|
||||
level: React.PropTypes.number.isRequired,
|
||||
style: React.PropTypes.object,
|
||||
}
|
||||
|
||||
render() {
|
||||
const headingProps = {
|
||||
style: {
|
||||
fontWeight: 400,
|
||||
fontSize: fontSizes[this.props.level - 1],
|
||||
marginBottom: margins[1],
|
||||
...this.props.style
|
||||
}
|
||||
}
|
||||
|
||||
switch(this.props.level) {
|
||||
case 1: return <h1 {...headingProps}>{this.props.children}</h1>
|
||||
case 2: return <h2 {...headingProps}>{this.props.children}</h2>
|
||||
case 3: return <h3 {...headingProps}>{this.props.children}</h3>
|
||||
case 4: return <h4 {...headingProps}>{this.props.children}</h4>
|
||||
case 5: return <h5 {...headingProps}>{this.props.children}</h5>
|
||||
default: return <h6 {...headingProps}>{this.props.children}</h6>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default Heading
|
||||
@@ -1,7 +1,4 @@
|
||||
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 = {
|
||||
@@ -10,30 +7,15 @@ class MessagePanel extends React.Component {
|
||||
}
|
||||
|
||||
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>
|
||||
return <p className="maputnik-message-panel-error">{m}</p>
|
||||
})
|
||||
|
||||
const infos = this.props.infos.map((m, i) => {
|
||||
return <Paragraph key={i}
|
||||
style={{
|
||||
...paragraphStyle,
|
||||
color: colors.lowgray,
|
||||
}}>{m}</Paragraph>
|
||||
return <p key={i}>{m}</p>
|
||||
})
|
||||
|
||||
return <div style={{
|
||||
padding: margins[1],
|
||||
}}>
|
||||
return <div className="maputnik-message-panel">
|
||||
{errors}
|
||||
{infos}
|
||||
</div>
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
import React from 'react'
|
||||
import colors from '../config/colors'
|
||||
import { margins, fontSizes } from '../config/scales'
|
||||
|
||||
const Paragraph = (props) => <p style={{
|
||||
color: colors.lowgray,
|
||||
fontSize: fontSizes[5],
|
||||
...props.style
|
||||
}}>
|
||||
{props.children}
|
||||
</p>
|
||||
|
||||
export default Paragraph
|
||||
@@ -1,15 +1,7 @@
|
||||
import React from 'react'
|
||||
|
||||
const ScrollContainer = (props) => {
|
||||
return <div style={{
|
||||
overflowX: "visible",
|
||||
overflowY: "scroll",
|
||||
bottom:0,
|
||||
left:0,
|
||||
right:0,
|
||||
top:1,
|
||||
position: "absolute",
|
||||
}}>
|
||||
return <div className="maputnik-scroll-container">
|
||||
{props.children}
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react'
|
||||
import FileReaderInput from 'react-file-reader-input'
|
||||
import classnames from 'classnames'
|
||||
|
||||
import MdFileDownload from 'react-icons/lib/md/file-download'
|
||||
import MdFileUpload from 'react-icons/lib/md/file-upload'
|
||||
@@ -14,76 +15,48 @@ import MdInsertEmoticon from 'react-icons/lib/md/insert-emoticon'
|
||||
import MdFontDownload from 'react-icons/lib/md/font-download'
|
||||
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 ExportModal from './modals/ExportModal'
|
||||
import SourcesModal from './modals/SourcesModal'
|
||||
import OpenModal from './modals/OpenModal'
|
||||
|
||||
import style from '../libs/style'
|
||||
import colors from '../config/colors'
|
||||
import { margins, fontSizes } from '../config/scales'
|
||||
|
||||
const IconText = props => <span style={{ paddingLeft: margins[0] }}>
|
||||
{props.children}
|
||||
</span>
|
||||
|
||||
const actionStyle = {
|
||||
display: "inline-block",
|
||||
padding: 10,
|
||||
fontSize: fontSizes[4],
|
||||
cursor: "pointer",
|
||||
color: colors.white,
|
||||
textDecoration: 'none',
|
||||
function IconText(props) {
|
||||
return <span className="maputnik-icon-text">{props.children}</span>
|
||||
}
|
||||
|
||||
const ToolbarLink = props => <a
|
||||
href={props.href}
|
||||
target={"blank"}
|
||||
style={{
|
||||
...actionStyle,
|
||||
...props.style,
|
||||
}}>
|
||||
{props.children}
|
||||
</a>
|
||||
function ToolbarLink(props) {
|
||||
return <a
|
||||
className={classnames('maputnik-toolbar-link', props.className)}
|
||||
href={props.href}
|
||||
target={"blank"}
|
||||
>
|
||||
{props.children}
|
||||
</a>
|
||||
}
|
||||
|
||||
class ToolbarAction extends React.Component {
|
||||
static propTypes = {
|
||||
onClick: React.PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = { hover: false }
|
||||
}
|
||||
|
||||
render() {
|
||||
return <a
|
||||
onClick={this.props.onClick}
|
||||
onMouseOver={e => this.setState({hover: true})}
|
||||
onMouseOut={e => this.setState({hover: false})}
|
||||
style={{
|
||||
...actionStyle,
|
||||
...this.props.style,
|
||||
backgroundColor: this.state.hover ? colors.gray : null,
|
||||
}}>
|
||||
{this.props.children}
|
||||
</a>
|
||||
}
|
||||
function ToolbarAction(props) {
|
||||
return <a
|
||||
className='maputnik-toolbar-action'
|
||||
onClick={props.onClick}
|
||||
>
|
||||
{props.children}
|
||||
</a>
|
||||
}
|
||||
|
||||
export default class Toolbar extends React.Component {
|
||||
static propTypes = {
|
||||
mapStyle: React.PropTypes.object.isRequired,
|
||||
inspectModeEnabled: React.PropTypes.bool.isRequired,
|
||||
onStyleChanged: React.PropTypes.func.isRequired,
|
||||
// A new style has been uploaded
|
||||
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,
|
||||
onInspectModeToggle: React.PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
@@ -94,33 +67,11 @@ export default class Toolbar extends React.Component {
|
||||
sources: false,
|
||||
open: false,
|
||||
add: false,
|
||||
export: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
downloadButton() {
|
||||
return <ToolbarAction onClick={this.props.onStyleDownload}>
|
||||
<MdFileDownload />
|
||||
<IconText>Download</IconText>
|
||||
</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: {
|
||||
@@ -131,21 +82,18 @@ export default class Toolbar extends React.Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div style={{
|
||||
position: "fixed",
|
||||
height: 40,
|
||||
width: '100%',
|
||||
zIndex: 100,
|
||||
left: 0,
|
||||
top: 0,
|
||||
backgroundColor: colors.black
|
||||
}}>
|
||||
return <div className='maputnik-toolbar'>
|
||||
<SettingsModal
|
||||
mapStyle={this.props.mapStyle}
|
||||
onStyleChanged={this.props.onStyleChanged}
|
||||
isOpen={this.state.isOpen.settings}
|
||||
onOpenToggle={this.toggleModal.bind(this, 'settings')}
|
||||
/>
|
||||
<ExportModal
|
||||
mapStyle={this.props.mapStyle}
|
||||
isOpen={this.state.isOpen.export}
|
||||
onOpenToggle={this.toggleModal.bind(this, 'export')}
|
||||
/>
|
||||
<OpenModal
|
||||
isOpen={this.state.isOpen.open}
|
||||
onStyleOpen={this.props.onStyleOpen}
|
||||
@@ -157,33 +105,20 @@ 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={{
|
||||
width: 180,
|
||||
textAlign: 'left',
|
||||
backgroundColor: colors.black,
|
||||
padding: 5,
|
||||
}}
|
||||
className="maputnik-toolbar-logo"
|
||||
>
|
||||
<img src={logoImage} alt="Maputnik" style={{width: 30, height: 30, paddingRight: 5, verticalAlign: 'middle'}}/>
|
||||
<span style={{fontSize: 20, verticalAlign: 'middle' }}>Maputnik</span>
|
||||
<img src={logoImage} alt="Maputnik" />
|
||||
<h1>Maputnik</h1>
|
||||
</ToolbarLink>
|
||||
<ToolbarAction onClick={this.toggleModal.bind(this, 'open')}>
|
||||
<OpenIcon />
|
||||
<IconText>Open</IconText>
|
||||
</ToolbarAction>
|
||||
{this.downloadButton()}
|
||||
<ToolbarAction onClick={this.toggleModal.bind(this, 'add')}>
|
||||
<AddIcon />
|
||||
<IconText>Add Layer</IconText>
|
||||
<ToolbarAction onClick={this.toggleModal.bind(this, 'export')}>
|
||||
<MdFileDownload />
|
||||
<IconText>Export</IconText>
|
||||
</ToolbarAction>
|
||||
<ToolbarAction onClick={this.toggleModal.bind(this, 'sources')}>
|
||||
<SourcesIcon />
|
||||
@@ -193,9 +128,12 @@ export default class Toolbar extends React.Component {
|
||||
<SettingsIcon />
|
||||
<IconText>Style Settings</IconText>
|
||||
</ToolbarAction>
|
||||
<ToolbarAction onClick={this.toggleInspectionMode.bind(this)}>
|
||||
<ToolbarAction onClick={this.props.onInspectModeToggle}>
|
||||
<InspectionIcon />
|
||||
<IconText>Inspect</IconText>
|
||||
<IconText>
|
||||
{ this.props.inspectModeEnabled && <span>Map Mode</span> }
|
||||
{ !this.props.inspectModeEnabled && <span>Inspect Mode</span> }
|
||||
</IconText>
|
||||
</ToolbarAction>
|
||||
<ToolbarLink href={"https://github.com/maputnik/editor/wiki"}>
|
||||
<HelpIcon />
|
||||
|
||||
@@ -2,8 +2,6 @@ import React from 'react'
|
||||
import Color from 'color'
|
||||
import ChromePicker from 'react-color/lib/components/chrome/Chrome'
|
||||
|
||||
import input from '../../config/input.js'
|
||||
|
||||
function formatColor(color) {
|
||||
const rgb = color.rgb
|
||||
return `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${rgb.a})`
|
||||
@@ -58,9 +56,10 @@ class ColorField extends React.Component {
|
||||
render() {
|
||||
const offset = this.calcPickerOffset()
|
||||
const picker = <div
|
||||
className="maputnik-color-picker-offset"
|
||||
style={{
|
||||
position: 'fixed',
|
||||
zIndex: 1,
|
||||
position: 'fixed',
|
||||
zIndex: 1,
|
||||
left: offset.left,
|
||||
top: offset.top,
|
||||
}}>
|
||||
@@ -69,6 +68,7 @@ class ColorField extends React.Component {
|
||||
onChange={c => this.props.onChange(formatColor(c))}
|
||||
/>
|
||||
<div
|
||||
className="maputnik-color-picker-offset"
|
||||
onClick={this.togglePicker.bind(this)}
|
||||
style={{
|
||||
zIndex: -1,
|
||||
@@ -81,18 +81,13 @@ class ColorField extends React.Component {
|
||||
/>
|
||||
</div>
|
||||
|
||||
return <div style={{
|
||||
position: 'relative',
|
||||
display: 'inline',
|
||||
}}>
|
||||
return <div className="maputnik-color-wrapper">
|
||||
{this.state.pickerOpened && picker}
|
||||
<input
|
||||
className="maputnik-color"
|
||||
ref="colorInput"
|
||||
onClick={this.togglePicker.bind(this)}
|
||||
style={{
|
||||
...input.input,
|
||||
...this.props.style
|
||||
}}
|
||||
style={this.props.style}
|
||||
name={this.props.name}
|
||||
placeholder={this.props.default}
|
||||
value={this.props.value ? this.props.value : ""}
|
||||
|
||||
@@ -1,50 +1,22 @@
|
||||
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,
|
||||
label: React.PropTypes.oneOfType([
|
||||
React.PropTypes.object,
|
||||
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>
|
||||
return <label className="maputnik-doc-wrapper">
|
||||
<div className="maputnik-doc-target">
|
||||
<span>{this.props.label}</span>
|
||||
<div className="maputnik-doc-popup">
|
||||
{this.props.doc}
|
||||
</div>
|
||||
</div >
|
||||
</label>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,31 @@
|
||||
import React from 'react'
|
||||
import GlSpec from 'mapbox-gl-style-spec/reference/latest.js'
|
||||
|
||||
import ZoomSpecField from './ZoomSpecField'
|
||||
import colors from '../../config/colors'
|
||||
import { margins } from '../../config/scales'
|
||||
const iconProperties = ['background-pattern', 'fill-pattern', 'line-pattern', 'fill-extrusion-pattern', 'icon-image']
|
||||
|
||||
/** Extract field spec by {@fieldName} from the {@layerType} in the
|
||||
* style specification from either the paint or layout group */
|
||||
function getFieldSpec(layerType, fieldName) {
|
||||
const groupName = getGroupName(layerType, fieldName)
|
||||
const group = GlSpec[groupName + '_' + layerType]
|
||||
return group[fieldName]
|
||||
function getFieldSpec(spec, layerType, fieldName) {
|
||||
const groupName = getGroupName(spec, layerType, fieldName)
|
||||
const group = spec[groupName + '_' + layerType]
|
||||
const fieldSpec = group[fieldName]
|
||||
if(iconProperties.indexOf(fieldName) >= 0) {
|
||||
return {
|
||||
...fieldSpec,
|
||||
values: spec.$root.sprite.values
|
||||
}
|
||||
}
|
||||
if(fieldName === 'text-font') {
|
||||
return {
|
||||
...fieldSpec,
|
||||
values: spec.$root.glyphs.values
|
||||
}
|
||||
}
|
||||
return fieldSpec
|
||||
}
|
||||
|
||||
function getGroupName(layerType, fieldName) {
|
||||
const paint = GlSpec['paint_' + layerType] || {}
|
||||
function getGroupName(spec, layerType, fieldName) {
|
||||
const paint = spec['paint_' + layerType] || {}
|
||||
if (fieldName in paint) {
|
||||
return 'paint'
|
||||
} else {
|
||||
@@ -27,16 +38,17 @@ export default class PropertyGroup extends React.Component {
|
||||
layer: React.PropTypes.object.isRequired,
|
||||
groupFields: React.PropTypes.array.isRequired,
|
||||
onChange: React.PropTypes.func.isRequired,
|
||||
spec: React.PropTypes.object.isRequired,
|
||||
}
|
||||
|
||||
onPropertyChange(property, newValue) {
|
||||
const group = getGroupName(this.props.layer.type, property)
|
||||
const group = getGroupName(this.props.spec, this.props.layer.type, property)
|
||||
this.props.onChange(group , property, newValue)
|
||||
}
|
||||
|
||||
render() {
|
||||
const fields = this.props.groupFields.map(fieldName => {
|
||||
const fieldSpec = getFieldSpec(this.props.layer.type, fieldName)
|
||||
const fieldSpec = getFieldSpec(this.props.spec, this.props.layer.type, fieldName)
|
||||
|
||||
const paint = this.props.layer.paint || {}
|
||||
const layout = this.props.layer.layout || {}
|
||||
@@ -46,12 +58,12 @@ export default class PropertyGroup extends React.Component {
|
||||
onChange={this.onPropertyChange.bind(this)}
|
||||
key={fieldName}
|
||||
fieldName={fieldName}
|
||||
value={fieldValue}
|
||||
value={fieldValue === undefined ? fieldSpec.default : fieldValue}
|
||||
fieldSpec={fieldSpec}
|
||||
/>
|
||||
})
|
||||
|
||||
return <div>
|
||||
return <div className="maputnik-property-group">
|
||||
{fields}
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React from 'react'
|
||||
import color from 'color'
|
||||
|
||||
import GlSpec from 'mapbox-gl-style-spec/reference/latest.min.js'
|
||||
import ColorField from './ColorField'
|
||||
import NumberInput from '../inputs/NumberInput'
|
||||
import CheckboxInput from '../inputs/CheckboxInput'
|
||||
@@ -10,10 +9,10 @@ import SelectInput from '../inputs/SelectInput'
|
||||
import MultiButtonInput from '../inputs/MultiButtonInput'
|
||||
import ArrayInput from '../inputs/ArrayInput'
|
||||
import FontInput from '../inputs/FontInput'
|
||||
import IconInput from '../inputs/IconInput'
|
||||
import capitalize from 'lodash.capitalize'
|
||||
|
||||
|
||||
import input from '../../config/input.js'
|
||||
const iconProperties = ['background-pattern', 'fill-pattern', 'line-pattern', 'fill-extrusion-pattern', 'icon-image']
|
||||
|
||||
function labelFromFieldName(fieldName) {
|
||||
let label = fieldName.split('-').slice(1).join(' ')
|
||||
@@ -51,8 +50,8 @@ export default class SpecField extends React.Component {
|
||||
render() {
|
||||
const commonProps = {
|
||||
style: this.props.style,
|
||||
default: this.props.fieldSpec.default,
|
||||
value: this.props.value,
|
||||
default: this.props.fieldSpec.default,
|
||||
name: this.props.fieldName,
|
||||
onChange: newValue => this.props.onChange(this.props.fieldName, newValue)
|
||||
}
|
||||
@@ -78,11 +77,17 @@ export default class SpecField extends React.Component {
|
||||
options={options}
|
||||
/>
|
||||
}
|
||||
case 'string': return (
|
||||
<StringInput
|
||||
{...commonProps}
|
||||
/>
|
||||
)
|
||||
case 'string':
|
||||
if(iconProperties.indexOf(this.props.fieldName) >= 0) {
|
||||
return <IconInput
|
||||
{...commonProps}
|
||||
icons={this.props.fieldSpec.values}
|
||||
/>
|
||||
} else {
|
||||
return <StringInput
|
||||
{...commonProps}
|
||||
/>
|
||||
}
|
||||
case 'color': return (
|
||||
<ColorField
|
||||
{...commonProps}
|
||||
@@ -97,6 +102,7 @@ export default class SpecField extends React.Component {
|
||||
if(this.props.fieldName === 'text-font') {
|
||||
return <FontInput
|
||||
{...commonProps}
|
||||
fonts={this.props.fieldSpec.values}
|
||||
/>
|
||||
} else {
|
||||
return <ArrayInput
|
||||
|
||||
@@ -5,14 +5,13 @@ import Button from '../Button'
|
||||
import SpecField from './SpecField'
|
||||
import NumberInput from '../inputs/NumberInput'
|
||||
import DocLabel from './DocLabel'
|
||||
import InputBlock from '../inputs/InputBlock'
|
||||
|
||||
import AddIcon from 'react-icons/lib/md/add-circle-outline'
|
||||
import DeleteIcon from 'react-icons/lib/md/delete'
|
||||
import FunctionIcon from 'react-icons/lib/md/functions'
|
||||
|
||||
import capitalize from 'lodash.capitalize'
|
||||
import input from '../../config/input.js'
|
||||
import colors from '../../config/colors.js'
|
||||
import { margins, fontSizes } from '../../config/scales.js'
|
||||
|
||||
function isZoomField(value) {
|
||||
return typeof value === 'object' && value.stops
|
||||
@@ -21,7 +20,7 @@ function isZoomField(value) {
|
||||
/** Supports displaying spec field for zoom function objects
|
||||
* https://www.mapbox.com/mapbox-gl-style-spec/#types-function-zoom-property
|
||||
*/
|
||||
export default class ZoomSpecField extends React.Component {
|
||||
export default class ZoomSpecProperty extends React.Component {
|
||||
static propTypes = {
|
||||
onChange: React.PropTypes.func.isRequired,
|
||||
fieldName: React.PropTypes.string.isRequired,
|
||||
@@ -64,6 +63,16 @@ export default class ZoomSpecField extends React.Component {
|
||||
this.props.onChange(this.props.fieldName, changedValue)
|
||||
}
|
||||
|
||||
makeZoomFunction() {
|
||||
const zoomFunc = {
|
||||
stops: [
|
||||
[6, this.props.value],
|
||||
[10, this.props.value]
|
||||
]
|
||||
}
|
||||
this.props.onChange(this.props.fieldName, zoomFunc)
|
||||
}
|
||||
|
||||
changeStop(changeIdx, zoomLevel, value) {
|
||||
const stops = this.props.value.stops.slice(0)
|
||||
stops[changeIdx] = [zoomLevel, value]
|
||||
@@ -74,83 +83,99 @@ export default class ZoomSpecField extends React.Component {
|
||||
this.props.onChange(this.props.fieldName, changedValue)
|
||||
}
|
||||
|
||||
render() {
|
||||
let label = <DocLabel
|
||||
label={labelFromFieldName(this.props.fieldName)}
|
||||
doc={this.props.fieldSpec.doc}
|
||||
style={{
|
||||
width: '50%',
|
||||
}}
|
||||
/>
|
||||
renderZoomProperty() {
|
||||
const zoomFields = this.props.value.stops.map((stop, idx) => {
|
||||
const zoomLevel = stop[0]
|
||||
const value = stop[1]
|
||||
const deleteStopBtn= <DeleteStopButton onClick={this.deleteStop.bind(this, idx)} />
|
||||
|
||||
if(isZoomField(this.props.value)) {
|
||||
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 key={zoomLevel} style={{
|
||||
...input.property,
|
||||
marginLeft: 0,
|
||||
marginRight: 0
|
||||
}}>
|
||||
{label}
|
||||
<Button
|
||||
style={{backgroundColor: null}}
|
||||
onClick={this.deleteStop.bind(this, idx)}
|
||||
>
|
||||
<DeleteIcon />
|
||||
</Button>
|
||||
<NumberInput
|
||||
style={{
|
||||
width: '7%',
|
||||
}}
|
||||
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],
|
||||
}}
|
||||
/>
|
||||
return <InputBlock
|
||||
key={zoomLevel}
|
||||
doc={this.props.fieldSpec.doc}
|
||||
label={labelFromFieldName(this.props.fieldName)}
|
||||
action={deleteStopBtn}
|
||||
>
|
||||
<div>
|
||||
<div className="maputnik-zoom-spec-property-stop-edit">
|
||||
<NumberInput
|
||||
value={zoomLevel}
|
||||
onChange={changedStop => this.changeStop(idx, changedStop, value)}
|
||||
min={0}
|
||||
max={22}
|
||||
/>
|
||||
</div>
|
||||
<div className="maputnik-zoom-spec-property-stop-value">
|
||||
<SpecField
|
||||
fieldName={this.props.fieldName}
|
||||
fieldSpec={this.props.fieldSpec}
|
||||
value={value}
|
||||
onChange={(_, newValue) => this.changeStop(idx, zoomLevel, newValue)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
})
|
||||
</InputBlock>
|
||||
})
|
||||
|
||||
return <div style={input.property}>
|
||||
{zoomFields}
|
||||
</div>
|
||||
return <div className="maputnik-zoom-spec-property">
|
||||
{zoomFields}
|
||||
<Button
|
||||
className="maputnik-add-stop"
|
||||
onClick={this.addStop.bind(this)}
|
||||
>
|
||||
Add stop
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
|
||||
renderProperty() {
|
||||
let zoomBtn = null
|
||||
if(this.props.fieldSpec['zoom-function']) {
|
||||
zoomBtn = <MakeZoomFunctionButton onClick={this.makeZoomFunction.bind(this)} />
|
||||
}
|
||||
|
||||
return <InputBlock
|
||||
doc={this.props.fieldSpec.doc}
|
||||
label={labelFromFieldName(this.props.fieldName)}
|
||||
action={zoomBtn}
|
||||
>
|
||||
<SpecField {...this.props} />
|
||||
</InputBlock>
|
||||
}
|
||||
|
||||
render() {
|
||||
if(isZoomField(this.props.value)) {
|
||||
return this.renderZoomProperty();
|
||||
} else {
|
||||
return <div style={input.property}>
|
||||
{label}
|
||||
<SpecField {...this.props} style={{ width: '50%' } }/>
|
||||
</div>
|
||||
return this.renderProperty();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function MakeZoomFunctionButton(props) {
|
||||
return <Button
|
||||
className="maputnik-make-zoom-function"
|
||||
onClick={props.onClick}
|
||||
>
|
||||
<DocLabel
|
||||
label={<FunctionIcon />}
|
||||
cursorTargetStyle={{ cursor: 'pointer' }}
|
||||
doc={"Turn property into a zoom function to enable a map feature to change with map's zoom level."}
|
||||
/>
|
||||
</Button>
|
||||
}
|
||||
|
||||
function DeleteStopButton(props) {
|
||||
return <Button
|
||||
className="maputnik-delete-stop"
|
||||
onClick={props.onClick}
|
||||
>
|
||||
<DocLabel
|
||||
label={<DeleteIcon />}
|
||||
doc={"Remove zoom level stop."}
|
||||
/>
|
||||
</Button>
|
||||
}
|
||||
|
||||
function labelFromFieldName(fieldName) {
|
||||
let label = fieldName.split('-').slice(1).join(' ')
|
||||
return capitalize(label)
|
||||
|
||||
@@ -1,19 +1,15 @@
|
||||
import React from 'react'
|
||||
import GlSpec from 'mapbox-gl-style-spec/reference/latest.min.js'
|
||||
|
||||
import input from '../../config/input.js'
|
||||
import colors from '../../config/colors.js'
|
||||
import { margins } from '../../config/scales.js'
|
||||
import { combiningFilterOps } from '../../libs/filterops.js'
|
||||
|
||||
import GlSpec from 'mapbox-gl-style-spec/reference/latest.js'
|
||||
import DocLabel from '../fields/DocLabel'
|
||||
import SelectInput from '../inputs/SelectInput'
|
||||
import StringInput from '../inputs/StringInput'
|
||||
import AutocompleteInput from '../inputs/AutocompleteInput'
|
||||
import SingleFilterEditor from './SingleFilterEditor'
|
||||
import FilterEditorBlock from './FilterEditorBlock'
|
||||
import Button from '../Button'
|
||||
|
||||
const combiningFilterOps = ['all', 'any', 'none']
|
||||
const setFilterOps = ['in', '!in']
|
||||
const otherFilterOps = Object
|
||||
.keys(GlSpec.filter_operator.values)
|
||||
.filter(op => combiningFilterOps.indexOf(op) < 0)
|
||||
import DeleteIcon from 'react-icons/lib/md/delete'
|
||||
import AddIcon from 'react-icons/lib/fa/plus'
|
||||
|
||||
function hasCombiningFilter(filter) {
|
||||
return combiningFilterOps.indexOf(filter[0]) >= 0
|
||||
@@ -27,131 +23,24 @@ function hasNestedCombiningFilter(filter) {
|
||||
return false
|
||||
}
|
||||
|
||||
class CombiningOperatorSelect extends React.Component {
|
||||
static propTypes = {
|
||||
value: React.PropTypes.string.isRequired,
|
||||
onChange: React.PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
render() {
|
||||
const options = combiningFilterOps.map(op => {
|
||||
return <option key={op} value={op}>{op}</option>
|
||||
})
|
||||
|
||||
return <div
|
||||
style={{
|
||||
marginTop: margins[1],
|
||||
marginBottom: margins[1],
|
||||
}}
|
||||
>
|
||||
<select
|
||||
style={{
|
||||
...input.select,
|
||||
width: '20.5%',
|
||||
}}
|
||||
value={this.props.value}
|
||||
onChange={e => this.props.onChange(e.target.value)}
|
||||
>
|
||||
{options}
|
||||
</select>
|
||||
<label style={{
|
||||
...input.label,
|
||||
width: '60%',
|
||||
marginLeft: margins[0],
|
||||
}}>
|
||||
of the filters matches
|
||||
</label>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
class OperatorSelect extends React.Component {
|
||||
static propTypes = {
|
||||
value: React.PropTypes.string.isRequired,
|
||||
onChange: React.PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
render() {
|
||||
return <SelectInput
|
||||
style={{
|
||||
width: '15%',
|
||||
margin: margins[0]
|
||||
}}
|
||||
value={this.props.value}
|
||||
onChange={this.props.onChange}
|
||||
options={otherFilterOps.map(op => [op, op])}
|
||||
/>
|
||||
}
|
||||
}
|
||||
|
||||
class SingleFilterEditor extends React.Component {
|
||||
static propTypes = {
|
||||
filter: React.PropTypes.array.isRequired,
|
||||
onChange: React.PropTypes.func.isRequired,
|
||||
properties: React.PropTypes.object,
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
properties: {},
|
||||
}
|
||||
|
||||
onFilterPartChanged(filterOp, propertyName, filterArgs) {
|
||||
const newFilter = [filterOp, propertyName, ...filterArgs]
|
||||
this.props.onChange(newFilter)
|
||||
}
|
||||
|
||||
render() {
|
||||
const f = this.props.filter
|
||||
const filterOp = f[0]
|
||||
const propertyName = f[1]
|
||||
const filterArgs = f.slice(2)
|
||||
|
||||
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)}
|
||||
/>
|
||||
<OperatorSelect
|
||||
value={filterOp}
|
||||
onChange={newFilterOp => this.onFilterPartChanged(newFilterOp, propertyName, filterArgs)}
|
||||
/>
|
||||
<StringInput
|
||||
style={{
|
||||
width: '50%',
|
||||
marginLeft: margins[0]
|
||||
}}
|
||||
value={filterArgs.join(',')}
|
||||
onChange={ v=> this.onFilterPartChanged(filterOp, propertyName, v.split(','))}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default class CombiningFilterEditor extends React.Component {
|
||||
static propTypes = {
|
||||
/** Properties of the vector layer and the available fields */
|
||||
properties: React.PropTypes.object.isRequired,
|
||||
filter: React.PropTypes.array.isRequired,
|
||||
properties: React.PropTypes.object,
|
||||
filter: React.PropTypes.array,
|
||||
onChange: React.PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
// Convert filter to combining filter
|
||||
combiningFilter() {
|
||||
let combiningOp = this.props.filter[0]
|
||||
let filters = this.props.filter.slice(1)
|
||||
let filter = this.props.filter || ['all']
|
||||
|
||||
let combiningOp = filter[0]
|
||||
let filters = filter.slice(1)
|
||||
|
||||
if(combiningFilterOps.indexOf(combiningOp) < 0) {
|
||||
combiningOp = 'all'
|
||||
filters = [this.props.filter.slice(0)]
|
||||
filters = [filter.slice(0)]
|
||||
}
|
||||
|
||||
return [combiningOp, ...filters]
|
||||
@@ -163,31 +52,61 @@ export default class CombiningFilterEditor extends React.Component {
|
||||
this.props.onChange(newFilter)
|
||||
}
|
||||
|
||||
deleteFilterItem(filterIdx) {
|
||||
const newFilter = this.combiningFilter().slice(0)
|
||||
console.log('Delete', filterIdx, newFilter)
|
||||
newFilter.splice(filterIdx + 1, 1)
|
||||
this.props.onChange(newFilter)
|
||||
}
|
||||
|
||||
addFilterItem() {
|
||||
const newFilterItem = this.combiningFilter().slice(0)
|
||||
newFilterItem.push(['==', 'name', ''])
|
||||
this.props.onChange(newFilterItem)
|
||||
}
|
||||
|
||||
render() {
|
||||
const filter = this.combiningFilter()
|
||||
let combiningOp = filter[0]
|
||||
let filters = filter.slice(1)
|
||||
|
||||
const filterEditors = filters.map((f, idx) => {
|
||||
return <SingleFilterEditor
|
||||
key={idx}
|
||||
properties={this.props.properties}
|
||||
filter={f}
|
||||
onChange={this.onFilterPartChanged.bind(this, idx + 1)}
|
||||
/>
|
||||
const editorBlocks = filters.map((f, idx) => {
|
||||
return <FilterEditorBlock key={idx} onDelete={this.deleteFilterItem.bind(this, idx)}>
|
||||
<SingleFilterEditor
|
||||
properties={this.props.properties}
|
||||
filter={f}
|
||||
onChange={this.onFilterPartChanged.bind(this, idx + 1)}
|
||||
/>
|
||||
</FilterEditorBlock>
|
||||
})
|
||||
|
||||
//TODO: Implement support for nested filter
|
||||
if(hasNestedCombiningFilter(filter)) {
|
||||
return null
|
||||
return <div className="maputnik-filter-editor-unsupported">
|
||||
Nested filters are not supported.
|
||||
</div>
|
||||
}
|
||||
|
||||
return <div>
|
||||
<CombiningOperatorSelect
|
||||
value={combiningOp}
|
||||
onChange={this.onFilterPartChanged.bind(this, 0)}
|
||||
/>
|
||||
{filterEditors}
|
||||
return <div className="maputnik-filter-editor">
|
||||
<div className="maputnik-filter-editor-compound-select">
|
||||
<DocLabel
|
||||
label={"Compound Filter"}
|
||||
doc={GlSpec.layer.filter.doc + " Combine multiple filters together by using a compound filter."}
|
||||
/>
|
||||
<SelectInput
|
||||
value={combiningOp}
|
||||
onChange={this.onFilterPartChanged.bind(this, 0)}
|
||||
options={[["all", "every filter matches"], ["none", "no filter matches"], ["any", "any filter matches"]]}
|
||||
/>
|
||||
</div>
|
||||
{editorBlocks}
|
||||
<div className="maputnik-filter-editor-add-wrapper">
|
||||
<Button
|
||||
className="maputnik-add-filter"
|
||||
onClick={this.addFilterItem.bind(this)}>
|
||||
Add filter
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
28
src/components/filter/FilterEditorBlock.jsx
Normal file
28
src/components/filter/FilterEditorBlock.jsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import React from 'react'
|
||||
import Button from '../Button'
|
||||
import DeleteIcon from 'react-icons/lib/md/delete'
|
||||
|
||||
class FilterEditorBlock extends React.Component {
|
||||
static propTypes = {
|
||||
onDelete: React.PropTypes.func.isRequired,
|
||||
children: React.PropTypes.element.isRequired,
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div className="maputnik-filter-editor-block">
|
||||
<div className="maputnik-filter-editor-block-action">
|
||||
<Button
|
||||
className="maputnik-delete-filter"
|
||||
onClick={this.props.onDelete}
|
||||
>
|
||||
<DeleteIcon />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="maputnik-filter-editor-block-content">
|
||||
{this.props.children}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
export default FilterEditorBlock
|
||||
56
src/components/filter/SingleFilterEditor.jsx
Normal file
56
src/components/filter/SingleFilterEditor.jsx
Normal file
@@ -0,0 +1,56 @@
|
||||
import React from 'react'
|
||||
|
||||
import { otherFilterOps } from '../../libs/filterops.js'
|
||||
import StringInput from '../inputs/StringInput'
|
||||
import AutocompleteInput from '../inputs/AutocompleteInput'
|
||||
import SelectInput from '../inputs/SelectInput'
|
||||
|
||||
class SingleFilterEditor extends React.Component {
|
||||
static propTypes = {
|
||||
filter: React.PropTypes.array.isRequired,
|
||||
onChange: React.PropTypes.func.isRequired,
|
||||
properties: React.PropTypes.object,
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
properties: {},
|
||||
}
|
||||
|
||||
onFilterPartChanged(filterOp, propertyName, filterArgs) {
|
||||
const newFilter = [filterOp, propertyName, ...filterArgs]
|
||||
this.props.onChange(newFilter)
|
||||
}
|
||||
|
||||
render() {
|
||||
const f = this.props.filter
|
||||
const filterOp = f[0]
|
||||
const propertyName = f[1]
|
||||
const filterArgs = f.slice(2)
|
||||
|
||||
return <div className="maputnik-filter-editor-single">
|
||||
<div className="maputnik-filter-editor-property">
|
||||
<AutocompleteInput
|
||||
value={propertyName}
|
||||
options={Object.keys(this.props.properties).map(propName => [propName, propName])}
|
||||
onChange={newPropertyName => this.onFilterPartChanged(filterOp, newPropertyName, filterArgs)}
|
||||
/>
|
||||
</div>
|
||||
<div className="maputnik-filter-editor-operator">
|
||||
<SelectInput
|
||||
value={filterOp}
|
||||
onChange={newFilterOp => this.onFilterPartChanged(newFilterOp, propertyName, filterArgs)}
|
||||
options={otherFilterOps}
|
||||
/>
|
||||
</div>
|
||||
<div className="maputnik-filter-editor-args">
|
||||
<StringInput
|
||||
value={filterArgs.join(',')}
|
||||
onChange={ v=> this.onFilterPartChanged(filterOp, propertyName, v.split(','))}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default SingleFilterEditor
|
||||
@@ -16,6 +16,7 @@ class LayerIcon extends React.Component {
|
||||
const iconProps = { style: this.props.style }
|
||||
switch(this.props.type) {
|
||||
case 'fill-extrusion': return <BackgroundIcon {...iconProps} />
|
||||
case 'raster': return <FillIcon {...iconProps} />
|
||||
case 'fill': return <FillIcon {...iconProps} />
|
||||
case 'background': return <BackgroundIcon {...iconProps} />
|
||||
case 'line': return <LineIcon {...iconProps} />
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
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,
|
||||
}
|
||||
|
||||
@@ -27,29 +23,23 @@ class ArrayInput extends React.Component {
|
||||
}
|
||||
|
||||
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%'}}>
|
||||
return <div className="maputnik-array">
|
||||
{inputs}
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
import React from 'react'
|
||||
import classnames from 'classnames'
|
||||
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,
|
||||
}
|
||||
|
||||
@@ -19,26 +16,16 @@ class AutocompleteInput extends React.Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const AutocompleteMenu = (items, value, style) => <div className={"maputnik-autocomplete-menu"} children={items} />
|
||||
|
||||
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
|
||||
wrapperProps={{
|
||||
className: "maputnik-autocomplete",
|
||||
style: null
|
||||
}}
|
||||
renderMenu={AutocompleteMenu}
|
||||
inputProps={{
|
||||
style: {
|
||||
...input.input,
|
||||
width: '100%',
|
||||
...this.props.inputStyle,
|
||||
}
|
||||
className: "maputnik-string"
|
||||
}}
|
||||
value={this.props.value}
|
||||
items={this.props.options}
|
||||
@@ -48,15 +35,10 @@ class AutocompleteInput extends React.Component {
|
||||
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,
|
||||
}}
|
||||
className={classnames({
|
||||
"maputnik-autocomplete-menu-item": true,
|
||||
"maputnik-autocomplete-menu-item-selected": isHighlighted,
|
||||
})}
|
||||
>
|
||||
{item[1]}
|
||||
</div>
|
||||
|
||||
@@ -1,70 +1,27 @@
|
||||
import React from 'react'
|
||||
import input from '../../config/input.js'
|
||||
import colors from '../../config/colors'
|
||||
import { margins } from '../../config/scales'
|
||||
|
||||
class CheckboxInput extends React.Component {
|
||||
static propTypes = {
|
||||
value: React.PropTypes.string,
|
||||
value: React.PropTypes.bool.isRequired,
|
||||
style: React.PropTypes.object,
|
||||
onChange: React.PropTypes.func,
|
||||
}
|
||||
|
||||
render() {
|
||||
const styles = {
|
||||
root: {
|
||||
...input.base,
|
||||
padding: 0,
|
||||
position: 'relative',
|
||||
textAlign: 'center ',
|
||||
cursor: 'pointer'
|
||||
},
|
||||
input: {
|
||||
position: 'absolute',
|
||||
zIndex: -1,
|
||||
opacity: 0
|
||||
},
|
||||
box: {
|
||||
display: 'inline-block',
|
||||
textAlign: 'center ',
|
||||
height: 15,
|
||||
width: 15,
|
||||
marginRight: margins[1],
|
||||
marginBottom: null,
|
||||
backgroundColor: colors.gray,
|
||||
borderRadius: 2,
|
||||
borderStyle: 'solid',
|
||||
borderWidth: 2,
|
||||
borderColor: colors.gray,
|
||||
transition: 'background-color .1s ease-out'
|
||||
},
|
||||
icon: {
|
||||
display: this.props.value ? null : 'none',
|
||||
width: '75%',
|
||||
height: '75%',
|
||||
marginTop: 1,
|
||||
fill: colors.lowgray
|
||||
}
|
||||
}
|
||||
|
||||
return <label style={styles.root}>
|
||||
return <label className="maputnik-checkbox-wrapper">
|
||||
<input
|
||||
type="checkbox"
|
||||
style={{
|
||||
...styles.input,
|
||||
...this.props.style,
|
||||
}}
|
||||
onChange={e => {this.props.onChange(!this.props.value)}}
|
||||
checked={this.props.value}
|
||||
/>
|
||||
<div style={styles.box}>
|
||||
<svg
|
||||
viewBox='0 0 32 32'
|
||||
style={styles.icon}>
|
||||
className="maputnik-checkbox"
|
||||
type="checkbox"
|
||||
style={this.props.style}
|
||||
onChange={e => this.props.onChange(!this.props.value)}
|
||||
checked={this.props.value}
|
||||
/>
|
||||
<div className="maputnik-checkbox-box">
|
||||
<svg className="maputnik-checkbox-icon" viewBox='0 0 32 32'>
|
||||
<path d='M1 14 L5 10 L13 18 L27 4 L31 8 L13 26 z' />
|
||||
</svg>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</label>
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
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'
|
||||
@@ -8,10 +7,15 @@ import fontStacks from '../../config/fontstacks.json'
|
||||
class FontInput extends React.Component {
|
||||
static propTypes = {
|
||||
value: React.PropTypes.array.isRequired,
|
||||
fonts: React.PropTypes.array,
|
||||
style: React.PropTypes.object,
|
||||
onChange: React.PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
fonts: []
|
||||
}
|
||||
|
||||
get values() {
|
||||
return this.props.value || this.props.default.slice(1) || []
|
||||
}
|
||||
@@ -27,12 +31,12 @@ class FontInput extends React.Component {
|
||||
return <AutocompleteInput
|
||||
key={i}
|
||||
value={value}
|
||||
options={fontStacks.map(f => [f, f])}
|
||||
options={this.props.fonts.map(f => [f, f])}
|
||||
onChange={this.changeFont.bind(this, i)}
|
||||
/>
|
||||
})
|
||||
|
||||
return <div style={{display: 'inline-block'}}>
|
||||
return <div className="maputnik-font">
|
||||
{inputs}
|
||||
</div>
|
||||
}
|
||||
|
||||
27
src/components/inputs/IconInput.jsx
Normal file
27
src/components/inputs/IconInput.jsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import React from 'react'
|
||||
import AutocompleteInput from './AutocompleteInput'
|
||||
|
||||
|
||||
class IconInput extends React.Component {
|
||||
static propTypes = {
|
||||
value: React.PropTypes.array,
|
||||
icons: React.PropTypes.array,
|
||||
style: React.PropTypes.object,
|
||||
onChange: React.PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
icons: []
|
||||
}
|
||||
|
||||
render() {
|
||||
return <AutocompleteInput
|
||||
value={this.props.value}
|
||||
options={this.props.icons.map(f => [f, f])}
|
||||
onChange={this.props.onChange}
|
||||
wrapperStyle={this.props.style}
|
||||
/>
|
||||
}
|
||||
}
|
||||
|
||||
export default IconInput
|
||||
@@ -1,11 +1,16 @@
|
||||
import React from 'react'
|
||||
import input from '../../config/input'
|
||||
import { margins } from '../../config/scales'
|
||||
import classnames from 'classnames'
|
||||
import DocLabel from '../fields/DocLabel'
|
||||
|
||||
/** Wrap a component with a label */
|
||||
class InputBlock extends React.Component {
|
||||
static propTypes = {
|
||||
label: React.PropTypes.string.isRequired,
|
||||
label: React.PropTypes.oneOfType([
|
||||
React.PropTypes.string,
|
||||
React.PropTypes.element,
|
||||
]).isRequired,
|
||||
doc: React.PropTypes.string,
|
||||
action: React.PropTypes.element,
|
||||
children: React.PropTypes.element.isRequired,
|
||||
style: React.PropTypes.object,
|
||||
}
|
||||
@@ -15,30 +20,34 @@ 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={{
|
||||
...input.property,
|
||||
...this.props.style,
|
||||
}}>
|
||||
<label
|
||||
style={{
|
||||
...input.label,
|
||||
width: '50%',
|
||||
}}>
|
||||
return <div style={this.props.style}
|
||||
className={classnames({
|
||||
"maputnik-input-block": true,
|
||||
"maputnik-action-block": this.props.action
|
||||
})}
|
||||
>
|
||||
{this.props.doc &&
|
||||
<div className="maputnik-input-block-label">
|
||||
<DocLabel
|
||||
label={this.props.label}
|
||||
doc={this.props.doc}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
{!this.props.doc &&
|
||||
<label className="maputnik-input-block-label">
|
||||
{this.props.label}
|
||||
</label>
|
||||
{this.renderChildren()}
|
||||
}
|
||||
{this.props.action &&
|
||||
<div className="maputnik-input-block-action">
|
||||
{this.props.action}
|
||||
</div>
|
||||
}
|
||||
<div className="maputnik-input-block-content">
|
||||
{this.props.children}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,35 +1,32 @@
|
||||
|
||||
import React from 'react'
|
||||
import classnames from 'classnames'
|
||||
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])=> {
|
||||
let options = this.props.options
|
||||
if(options.length > 0 && !Array.isArray(options[0])) {
|
||||
options = options.map(v => [v, v])
|
||||
}
|
||||
|
||||
const selectedValue = this.props.value || options[0][0]
|
||||
const buttons = 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)}
|
||||
className={classnames({"maputnik-button-selected": val === selectedValue})}
|
||||
>
|
||||
{label}
|
||||
</Button>
|
||||
})
|
||||
|
||||
return <div style={{display: 'inline-block'}}>
|
||||
return <div className="maputnik-multibutton">
|
||||
{buttons}
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
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,
|
||||
@@ -68,10 +66,7 @@ class NumberInput extends React.Component {
|
||||
|
||||
render() {
|
||||
return <input
|
||||
style={{
|
||||
...input.input,
|
||||
...this.props.style
|
||||
}}
|
||||
className="maputnik-number"
|
||||
placeholder={this.props.default}
|
||||
value={this.state.value}
|
||||
onChange={e => this.changeValue(e.target.value)}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import React from 'react'
|
||||
import input from '../../config/input.js'
|
||||
|
||||
class SelectInput extends React.Component {
|
||||
static propTypes = {
|
||||
@@ -11,19 +10,18 @@ class SelectInput extends React.Component {
|
||||
|
||||
|
||||
render() {
|
||||
const options = this.props.options.map(([val, label])=> {
|
||||
return <option key={val} value={val}>{label}</option>
|
||||
})
|
||||
let options = this.props.options
|
||||
if(options.length > 0 && !Array.isArray(options[0])) {
|
||||
options = options.map(v => [v, v])
|
||||
}
|
||||
|
||||
return <select
|
||||
style={{
|
||||
...input.select,
|
||||
...this.props.style
|
||||
}}
|
||||
className="maputnik-select"
|
||||
style={this.props.style}
|
||||
value={this.props.value}
|
||||
onChange={e => this.props.onChange(e.target.value)}
|
||||
>
|
||||
{options}
|
||||
{ options.map(([val, label]) => <option key={val} value={val}>{label}</option>) }
|
||||
</select>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +1,34 @@
|
||||
import React from 'react'
|
||||
import input from '../../config/input.js'
|
||||
|
||||
class StringInput extends React.Component {
|
||||
static propTypes = {
|
||||
value: React.PropTypes.string,
|
||||
style: React.PropTypes.object,
|
||||
default: React.PropTypes.number,
|
||||
default: React.PropTypes.string,
|
||||
onChange: React.PropTypes.func,
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
value: props.value || ''
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
this.setState({ value: nextProps.value || '' })
|
||||
}
|
||||
|
||||
render() {
|
||||
return <input
|
||||
style={{
|
||||
...input.input,
|
||||
...this.props.style
|
||||
}}
|
||||
value={this.props.value}
|
||||
className="maputnik-string"
|
||||
style={this.props.style}
|
||||
value={this.state.value}
|
||||
placeholder={this.props.default}
|
||||
onChange={e => this.props.onChange(e.target.value)}
|
||||
onChange={e => this.setState({ value: e.target.value })}
|
||||
onBlur={() => {
|
||||
if(this.state.value) this.props.onChange(this.state.value)
|
||||
}}
|
||||
/>
|
||||
}
|
||||
}
|
||||
|
||||
20
src/components/layers/Collapser.jsx
Normal file
20
src/components/layers/Collapser.jsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import React from 'react'
|
||||
import CollapseOpenIcon from 'react-icons/lib/md/arrow-drop-down'
|
||||
import CollapseCloseIcon from 'react-icons/lib/md/arrow-drop-up'
|
||||
|
||||
export default class Collapser extends React.Component {
|
||||
static propTypes = {
|
||||
isCollapsed: React.PropTypes.bool.isRequired,
|
||||
style: React.PropTypes.object,
|
||||
}
|
||||
|
||||
render() {
|
||||
const iconStyle = {
|
||||
width: 20,
|
||||
height: 20,
|
||||
...this.props.style,
|
||||
}
|
||||
return this.props.isCollapsed ? <CollapseCloseIcon style={iconStyle}/> : <CollapseOpenIcon style={iconStyle} />
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,9 +5,6 @@ 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'
|
||||
|
||||
import 'codemirror/mode/javascript/javascript'
|
||||
import 'codemirror/lib/codemirror.css'
|
||||
import '../../codemirror-maputnik.css'
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React from 'react'
|
||||
|
||||
import GlSpec from 'mapbox-gl-style-spec/reference/latest.min.js'
|
||||
import JSONEditor from './JSONEditor'
|
||||
import FilterEditor from '../filter/FilterEditor'
|
||||
import PropertyGroup from '../fields/PropertyGroup'
|
||||
@@ -13,11 +12,8 @@ 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'
|
||||
|
||||
class UnsupportedLayer extends React.Component {
|
||||
render() {
|
||||
@@ -25,12 +21,29 @@ class UnsupportedLayer extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
function layoutGroups(layerType) {
|
||||
const layerGroup = {
|
||||
title: 'Layer',
|
||||
type: 'layer'
|
||||
}
|
||||
const filterGroup = {
|
||||
title: 'Filter',
|
||||
type: 'filter'
|
||||
}
|
||||
const editorGroup = {
|
||||
title: 'JSON Editor',
|
||||
type: 'jsoneditor'
|
||||
}
|
||||
return [layerGroup, filterGroup].concat(layout[layerType].groups).concat([editorGroup])
|
||||
}
|
||||
|
||||
/** Layer editor supporting multiple types of layers. */
|
||||
export default class LayerEditor extends React.Component {
|
||||
static propTypes = {
|
||||
layer: React.PropTypes.object.isRequired,
|
||||
sources: React.PropTypes.object,
|
||||
vectorLayers: React.PropTypes.object,
|
||||
spec: React.PropTypes.object.isRequired,
|
||||
onLayerChanged: React.PropTypes.func,
|
||||
onLayerIdChange: React.PropTypes.func,
|
||||
}
|
||||
@@ -50,7 +63,7 @@ export default class LayerEditor extends React.Component {
|
||||
|
||||
//TODO: Clean this up and refactor into function
|
||||
const editorGroups = {}
|
||||
layout[this.props.layer.type].groups.forEach(group => {
|
||||
layoutGroups(this.props.layer.type).forEach(group => {
|
||||
editorGroups[group.title] = true
|
||||
})
|
||||
|
||||
@@ -74,8 +87,8 @@ export default class LayerEditor extends React.Component {
|
||||
getChildContext () {
|
||||
return {
|
||||
reactIconBase: {
|
||||
size: fontSizes[4],
|
||||
color: colors.lowgray,
|
||||
size: 14,
|
||||
color: '#8e8e8e',
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -96,40 +109,41 @@ export default class LayerEditor extends React.Component {
|
||||
|
||||
renderGroupType(type, fields) {
|
||||
switch(type) {
|
||||
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>
|
||||
<LayerSourceBlock
|
||||
case 'layer': 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))}
|
||||
/>
|
||||
{this.props.layer.type !== 'background' && <LayerSourceBlock
|
||||
sourceIds={Object.keys(this.props.sources)}
|
||||
value={this.props.layer.source}
|
||||
onChange={v => this.changeProperty(null, 'source', v)}
|
||||
/>
|
||||
<LayerSourceLayerBlock
|
||||
}
|
||||
{this.props.layer.type !== 'raster' && this.props.layer.type !== 'background' && <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}>
|
||||
}
|
||||
</div>
|
||||
case 'filter': return <div>
|
||||
<div className="maputnik-filter-editor-wrapper">
|
||||
<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}
|
||||
spec={this.props.spec}
|
||||
onChange={this.changeProperty.bind(this)}
|
||||
/>
|
||||
case 'jsoneditor': return <JSONEditor
|
||||
@@ -142,8 +156,8 @@ export default class LayerEditor extends React.Component {
|
||||
|
||||
render() {
|
||||
const layerType = this.props.layer.type
|
||||
const layoutGroups = layout[layerType].groups.filter(group => {
|
||||
return !(this.props.layer.type === 'background' && group.type === 'source')
|
||||
const groups = layoutGroups(layerType).filter(group => {
|
||||
return !(layerType === 'background' && group.type === 'source')
|
||||
}).map(group => {
|
||||
return <LayerEditorGroup
|
||||
key={group.title}
|
||||
@@ -155,8 +169,9 @@ export default class LayerEditor extends React.Component {
|
||||
</LayerEditorGroup>
|
||||
})
|
||||
|
||||
return <div>
|
||||
{layoutGroups}
|
||||
return <div className="maputnik-layer-editor"
|
||||
>
|
||||
{groups}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +1,6 @@
|
||||
import React from 'react'
|
||||
import Color from 'color'
|
||||
import colors from '../../config/colors'
|
||||
import { margins, fontSizes } from '../../config/scales'
|
||||
|
||||
import Collapse from 'react-collapse'
|
||||
import CollapseOpenIcon from 'react-icons/lib/md/arrow-drop-down'
|
||||
import CollapseCloseIcon from 'react-icons/lib/md/arrow-drop-up'
|
||||
|
||||
class Collapser extends React.Component {
|
||||
static propTypes = {
|
||||
isCollapsed: React.PropTypes.bool.isRequired,
|
||||
}
|
||||
|
||||
render() {
|
||||
const iconStyle = {
|
||||
width: 20,
|
||||
height: 20,
|
||||
}
|
||||
return this.props.isCollapsed ? <CollapseCloseIcon style={iconStyle}/> : <CollapseOpenIcon style={iconStyle} />
|
||||
}
|
||||
}
|
||||
import Collapser from './Collapser'
|
||||
|
||||
export default class LayerEditorGroup extends React.Component {
|
||||
static propTypes = {
|
||||
@@ -29,26 +10,9 @@ export default class LayerEditorGroup extends React.Component {
|
||||
onActiveToggle: React.PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = { hover: false }
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div>
|
||||
<div style={{
|
||||
fontSize: fontSizes[4],
|
||||
backgroundColor: this.state.hover ? Color(colors.black).lighten(0.30).string() : Color(colors.black).lighten(0.15).string(),
|
||||
color: colors.lowgray,
|
||||
cursor: 'pointer',
|
||||
userSelect: 'none',
|
||||
padding: margins[1],
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
lineHeight: '20px',
|
||||
}}
|
||||
onMouseOver={e => this.setState({hover: true})}
|
||||
onMouseOut={e => this.setState({hover: false})}
|
||||
<div className="maputnik-layer-editor-group"
|
||||
onClick={e => this.props.onActiveToggle(!this.props.isActive)}
|
||||
>
|
||||
<span>{this.props.title}</span>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react'
|
||||
|
||||
import GlSpec from 'mapbox-gl-style-spec/reference/latest.js'
|
||||
import InputBlock from '../inputs/InputBlock'
|
||||
import StringInput from '../inputs/StringInput'
|
||||
|
||||
@@ -10,7 +11,7 @@ class LayerIdBlock extends React.Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
return <InputBlock label={"Layer ID"}>
|
||||
return <InputBlock label={"ID"} doc={GlSpec.layer.id.doc}>
|
||||
<StringInput
|
||||
value={this.props.value}
|
||||
onChange={this.props.onChange}
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import React from 'react'
|
||||
import classnames from 'classnames'
|
||||
import cloneDeep from 'lodash.clonedeep'
|
||||
|
||||
import Button from '../Button'
|
||||
import LayerListGroup from './LayerListGroup'
|
||||
import LayerListItem from './LayerListItem'
|
||||
import AddIcon from 'react-icons/lib/md/add-circle-outline'
|
||||
import AddModal from '../modals/AddModal'
|
||||
|
||||
import style from '../../libs/style.js'
|
||||
import { margins } from '../../config/scales.js'
|
||||
|
||||
import {SortableContainer, SortableHandle, arrayMove} from 'react-sortable-hoc';
|
||||
|
||||
const layerListPropTypes = {
|
||||
@@ -13,6 +16,25 @@ const layerListPropTypes = {
|
||||
selectedLayerIndex: React.PropTypes.number.isRequired,
|
||||
onLayersChange: React.PropTypes.func.isRequired,
|
||||
onLayerSelect: React.PropTypes.func,
|
||||
sources: React.PropTypes.object.isRequired,
|
||||
}
|
||||
|
||||
function layerPrefix(name) {
|
||||
return name.replace(' ', '-').replace('_', '-').split('-')[0]
|
||||
}
|
||||
|
||||
function findClosestCommonPrefix(layers, idx) {
|
||||
const currentLayerPrefix = layerPrefix(layers[idx].id)
|
||||
let closestIdx = idx
|
||||
for (let i = idx; i > 0; i--) {
|
||||
const previousLayerPrefix = layerPrefix(layers[i-1].id)
|
||||
if(previousLayerPrefix === currentLayerPrefix) {
|
||||
closestIdx = i - 1
|
||||
} else {
|
||||
return closestIdx
|
||||
}
|
||||
}
|
||||
return closestIdx
|
||||
}
|
||||
|
||||
// List of collapsible layer editors
|
||||
@@ -23,6 +45,16 @@ class LayerListContainer extends React.Component {
|
||||
onLayerSelect: () => {},
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
collapsedGroups: {},
|
||||
isOpen: {
|
||||
add: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onLayerDestroy(layerId) {
|
||||
const remainingLayers = this.props.layers.slice(0)
|
||||
const idx = style.indexOfLayer(remainingLayers, layerId)
|
||||
@@ -53,28 +85,108 @@ class LayerListContainer extends React.Component {
|
||||
this.props.onLayersChange(changedLayers)
|
||||
}
|
||||
|
||||
render() {
|
||||
const layerPanels = this.props.layers.map((layer, index) => {
|
||||
const layerId = layer.id
|
||||
return <LayerListItem
|
||||
index={index}
|
||||
key={layerId}
|
||||
layerId={layerId}
|
||||
layerType={layer.type}
|
||||
visibility={(layer.layout || {}).visibility}
|
||||
isSelected={index === this.props.selectedLayerIndex}
|
||||
onLayerSelect={this.props.onLayerSelect}
|
||||
onLayerDestroy={this.onLayerDestroy.bind(this)}
|
||||
onLayerCopy={this.onLayerCopy.bind(this)}
|
||||
onLayerVisibilityToggle={this.onLayerVisibilityToggle.bind(this)}
|
||||
/>
|
||||
toggleModal(modalName) {
|
||||
this.setState({
|
||||
isOpen: {
|
||||
...this.state.isOpen,
|
||||
[modalName]: !this.state.isOpen[modalName]
|
||||
}
|
||||
})
|
||||
return <ul style={{
|
||||
padding: 0,
|
||||
margin: 0
|
||||
}}>
|
||||
{layerPanels}
|
||||
</ul>
|
||||
}
|
||||
|
||||
groupedLayers() {
|
||||
const groups = []
|
||||
for (let i = 0; i < this.props.layers.length; i++) {
|
||||
const previousLayer = this.props.layers[i-1]
|
||||
const layer = this.props.layers[i]
|
||||
if(previousLayer && layerPrefix(previousLayer.id) == layerPrefix(layer.id)) {
|
||||
const lastGroup = groups[groups.length - 1]
|
||||
lastGroup.push(layer)
|
||||
} else {
|
||||
groups.push([layer])
|
||||
}
|
||||
}
|
||||
return groups
|
||||
}
|
||||
|
||||
toggleLayerGroup(groupPrefix, idx) {
|
||||
const lookupKey = [groupPrefix, idx].join('-')
|
||||
const newGroups = { ...this.state.collapsedGroups }
|
||||
if(lookupKey in this.state.collapsedGroups) {
|
||||
newGroups[lookupKey] = !this.state.collapsedGroups[lookupKey]
|
||||
} else {
|
||||
newGroups[lookupKey] = true
|
||||
}
|
||||
this.setState({
|
||||
collapsedGroups: newGroups
|
||||
})
|
||||
}
|
||||
|
||||
isCollapsed(groupPrefix, idx) {
|
||||
const collapsed = this.state.collapsedGroups[[groupPrefix, idx].join('-')]
|
||||
return collapsed === undefined ? false : collapsed
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
const listItems = []
|
||||
let idx = 0
|
||||
this.groupedLayers().forEach(layers => {
|
||||
const groupPrefix = layerPrefix(layers[0].id)
|
||||
if(layers.length > 1) {
|
||||
const grp = <LayerListGroup
|
||||
key={[groupPrefix, idx].join('-')}
|
||||
title={groupPrefix}
|
||||
isActive={!this.isCollapsed(groupPrefix, idx)}
|
||||
onActiveToggle={this.toggleLayerGroup.bind(this, groupPrefix, idx)}
|
||||
/>
|
||||
listItems.push(grp)
|
||||
}
|
||||
|
||||
layers.forEach((layer, idxInGroup) => {
|
||||
const groupIdx = findClosestCommonPrefix(this.props.layers, idx)
|
||||
const listItem = <LayerListItem
|
||||
className={classnames({
|
||||
'maputnik-layer-list-item-collapsed': this.isCollapsed(groupPrefix, groupIdx),
|
||||
'maputnik-layer-list-item-group-last': idxInGroup == layers.length - 1 && layers.length > 1
|
||||
})}
|
||||
index={idx}
|
||||
key={layer.id}
|
||||
layerId={layer.id}
|
||||
layerType={layer.type}
|
||||
visibility={(layer.layout || {}).visibility}
|
||||
isSelected={idx === this.props.selectedLayerIndex}
|
||||
onLayerSelect={this.props.onLayerSelect}
|
||||
onLayerDestroy={this.onLayerDestroy.bind(this)}
|
||||
onLayerCopy={this.onLayerCopy.bind(this)}
|
||||
onLayerVisibilityToggle={this.onLayerVisibilityToggle.bind(this)}
|
||||
/>
|
||||
listItems.push(listItem)
|
||||
idx += 1
|
||||
})
|
||||
})
|
||||
|
||||
return <div className="maputnik-layer-list">
|
||||
<AddModal
|
||||
layers={this.props.layers}
|
||||
sources={this.props.sources}
|
||||
isOpen={this.state.isOpen.add}
|
||||
onOpenToggle={this.toggleModal.bind(this, 'add')}
|
||||
onLayersChange={this.props.onLayersChange}
|
||||
/>
|
||||
<header className="maputnik-layer-list-header">
|
||||
<span className="maputnik-layer-list-header-title">Layers</span>
|
||||
<span className="maputnik-space" />
|
||||
<Button
|
||||
onClick={this.toggleModal.bind(this, 'add')}
|
||||
className="maputnik-add-layer">
|
||||
Add Layer
|
||||
</Button>
|
||||
</header>
|
||||
<ul className="maputnik-layer-list-container">
|
||||
{listItems}
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
26
src/components/layers/LayerListGroup.jsx
Normal file
26
src/components/layers/LayerListGroup.jsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import React from 'react'
|
||||
import Collapser from './Collapser'
|
||||
|
||||
export default class LayerEditorGroup extends React.Component {
|
||||
static propTypes = {
|
||||
title: React.PropTypes.string.isRequired,
|
||||
children: React.PropTypes.element.isRequired,
|
||||
isActive: React.PropTypes.bool.isRequired,
|
||||
onActiveToggle: React.PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div className="maputnik-layer-list-group">
|
||||
<div className="maputnik-layer-list-group-header"
|
||||
onClick={e => this.props.onActiveToggle(!this.props.isActive)}
|
||||
>
|
||||
<span className="maputnik-layer-list-group-title">{this.props.title}</span>
|
||||
<span className="maputnik-space" />
|
||||
<Collapser
|
||||
style={{ height: 14, width: 14 }}
|
||||
isCollapsed={this.props.isActive}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react'
|
||||
import Color from 'color'
|
||||
import classnames from 'classnames'
|
||||
|
||||
import CopyIcon from 'react-icons/lib/md/content-copy'
|
||||
import VisibilityIcon from 'react-icons/lib/md/visibility'
|
||||
@@ -10,10 +11,6 @@ import LayerIcon from '../icons/LayerIcon'
|
||||
import LayerEditor from './LayerEditor'
|
||||
import {SortableElement, SortableHandle} from 'react-sortable-hoc'
|
||||
|
||||
import colors from '../../config/colors.js'
|
||||
import { fontSizes, margins } from '../../config/scales.js'
|
||||
|
||||
|
||||
@SortableHandle
|
||||
class LayerTypeDragHandle extends React.Component {
|
||||
static propTypes = LayerIcon.propTypes
|
||||
@@ -23,9 +20,9 @@ class LayerTypeDragHandle extends React.Component {
|
||||
{...this.props}
|
||||
style={{
|
||||
cursor: 'move',
|
||||
width: fontSizes[4],
|
||||
height: fontSizes[4],
|
||||
paddingRight: margins[0],
|
||||
width: 14,
|
||||
height: 14,
|
||||
paddingRight: 3,
|
||||
}}
|
||||
/>
|
||||
}
|
||||
@@ -34,46 +31,23 @@ class LayerTypeDragHandle extends React.Component {
|
||||
class IconAction extends React.Component {
|
||||
static propTypes = {
|
||||
action: React.PropTypes.string.isRequired,
|
||||
active: React.PropTypes.bool,
|
||||
onClick: React.PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = { hover: false }
|
||||
}
|
||||
|
||||
renderIcon() {
|
||||
const iconStyle = {
|
||||
fill: colors.black
|
||||
}
|
||||
|
||||
if(this.props.active) {
|
||||
iconStyle.fill = colors.midgray
|
||||
}
|
||||
if(this.state.hover) {
|
||||
iconStyle.fill = colors.lowgray
|
||||
}
|
||||
|
||||
switch(this.props.action) {
|
||||
case 'copy': return <CopyIcon style={iconStyle} />
|
||||
case 'show': return <VisibilityIcon style={iconStyle} />
|
||||
case 'hide': return <VisibilityOffIcon style={iconStyle} />
|
||||
case 'delete': return <DeleteIcon style={iconStyle} />
|
||||
case 'copy': return <CopyIcon />
|
||||
case 'show': return <VisibilityIcon />
|
||||
case 'hide': return <VisibilityOffIcon />
|
||||
case 'delete': return <DeleteIcon />
|
||||
default: return null
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return <a
|
||||
style={{
|
||||
display: "inline",
|
||||
marginLeft: margins[0],
|
||||
...this.props.style
|
||||
}}
|
||||
className="maputnik-layer-list-icon-action"
|
||||
onClick={this.props.onClick}
|
||||
onMouseOver={e => this.setState({hover: true})}
|
||||
onMouseOut={e => this.setState({hover: false})}
|
||||
>
|
||||
{this.renderIcon()}
|
||||
</a>
|
||||
@@ -87,6 +61,7 @@ class LayerListItem extends React.Component {
|
||||
layerType: React.PropTypes.string.isRequired,
|
||||
isSelected: React.PropTypes.bool,
|
||||
visibility: React.PropTypes.string,
|
||||
className: React.PropTypes.string,
|
||||
|
||||
onLayerSelect: React.PropTypes.func.isRequired,
|
||||
onLayerCopy: React.PropTypes.func,
|
||||
@@ -106,84 +81,36 @@ class LayerListItem extends React.Component {
|
||||
reactIconBase: React.PropTypes.object
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
hover: false
|
||||
}
|
||||
}
|
||||
|
||||
getChildContext() {
|
||||
return {
|
||||
reactIconBase: { size: fontSizes[4] }
|
||||
reactIconBase: { size: 14 }
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const itemStyle = {
|
||||
fontWeight: 400,
|
||||
color: colors.lowgray,
|
||||
fontSize: fontSizes[5],
|
||||
borderLeft: 0,
|
||||
borderTop: 0,
|
||||
borderBottom: 1,
|
||||
borderRight: 0,
|
||||
borderStyle: "solid",
|
||||
userSelect: 'none',
|
||||
listStyle: 'none',
|
||||
zIndex: 2000,
|
||||
cursor: 'pointer',
|
||||
position: 'relative',
|
||||
padding: margins[1],
|
||||
borderColor: Color(colors.black).lighten(0.10).string(),
|
||||
backgroundColor: colors.black,
|
||||
lineHeight: 1.3,
|
||||
}
|
||||
|
||||
if(this.state.hover) {
|
||||
itemStyle.backgroundColor = Color(colors.black).lighten(0.10).string()
|
||||
}
|
||||
|
||||
if(this.props.isSelected) {
|
||||
itemStyle.backgroundColor = Color(colors.black).lighten(0.15).string()
|
||||
}
|
||||
|
||||
const iconProps = {
|
||||
active: this.state.hover || this.props.isSelected
|
||||
}
|
||||
|
||||
return <li
|
||||
key={this.props.layerId}
|
||||
onClick={e => this.props.onLayerSelect(this.props.layerId)}
|
||||
onMouseOver={e => this.setState({hover: true})}
|
||||
onMouseOut={e => this.setState({hover: false})}
|
||||
style={itemStyle}>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row'
|
||||
}}>
|
||||
className={classnames({
|
||||
"maputnik-layer-list-item": true,
|
||||
"maputnik-layer-list-item-selected": this.props.isSelected,
|
||||
[this.props.className]: true,
|
||||
})}>
|
||||
<LayerTypeDragHandle type={this.props.layerType} />
|
||||
<span style={{
|
||||
width: 115,
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis'
|
||||
}}>{this.props.layerId}</span>
|
||||
<span className="maputnik-layer-list-item-id">{this.props.layerId}</span>
|
||||
<span style={{flexGrow: 1}} />
|
||||
<IconAction {...iconProps}
|
||||
<IconAction
|
||||
action={'delete'}
|
||||
onClick={e => this.props.onLayerDestroy(this.props.layerId)}
|
||||
/>
|
||||
<IconAction {...iconProps}
|
||||
<IconAction
|
||||
action={'copy'}
|
||||
onClick={e => this.props.onLayerCopy(this.props.layerId)}
|
||||
/>
|
||||
<IconAction {...iconProps}
|
||||
active={this.state.hover || this.props.isSelected || this.props.visibility === 'none'}
|
||||
<IconAction
|
||||
action={this.props.visibility === 'visible' ? 'hide' : 'show'}
|
||||
onClick={e => this.props.onLayerVisibilityToggle(this.props.layerId)}
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react'
|
||||
|
||||
import GlSpec from 'mapbox-gl-style-spec/reference/latest.js'
|
||||
import InputBlock from '../inputs/InputBlock'
|
||||
import StringInput from '../inputs/StringInput'
|
||||
import SelectInput from '../inputs/SelectInput'
|
||||
@@ -18,12 +19,11 @@ class LayerSourceBlock extends React.Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
return <InputBlock label={"Source"}>
|
||||
return <InputBlock label={"Source"} doc={GlSpec.layer.source.doc}>
|
||||
<AutocompleteInput
|
||||
value={this.props.value}
|
||||
onChange={this.props.onChange}
|
||||
options={this.props.sourceIds.map(src => [src, src])}
|
||||
wrapperStyle={{ width: '50%' }}
|
||||
/>
|
||||
</InputBlock>
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react'
|
||||
|
||||
import GlSpec from 'mapbox-gl-style-spec/reference/latest.js'
|
||||
import InputBlock from '../inputs/InputBlock'
|
||||
import StringInput from '../inputs/StringInput'
|
||||
import SelectInput from '../inputs/SelectInput'
|
||||
@@ -18,12 +19,11 @@ class LayerSourceLayer extends React.Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
return <InputBlock label={"Source Layer"}>
|
||||
return <InputBlock label={"Source Layer"} doc={GlSpec.layer['source-layer'].doc}>
|
||||
<AutocompleteInput
|
||||
value={this.props.value}
|
||||
onChange={this.props.onChange}
|
||||
options={this.props.sourceLayerIds.map(l => [l, l])}
|
||||
wrapperStyle={{ width: '50%' }}
|
||||
/>
|
||||
</InputBlock>
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react'
|
||||
|
||||
import GlSpec from 'mapbox-gl-style-spec/reference/latest.js'
|
||||
import InputBlock from '../inputs/InputBlock'
|
||||
import SelectInput from '../inputs/SelectInput'
|
||||
|
||||
@@ -10,7 +11,7 @@ class LayerTypeBlock extends React.Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
return <InputBlock label={"Layer Type"}>
|
||||
return <InputBlock label={"Type"} doc={GlSpec.layer.type.doc}>
|
||||
<SelectInput
|
||||
options={[
|
||||
['background', 'Background'],
|
||||
|
||||
47
src/components/map/FeatureLayerPopup.jsx
Normal file
47
src/components/map/FeatureLayerPopup.jsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import React from 'react'
|
||||
import InputBlock from '../inputs/InputBlock'
|
||||
import StringInput from '../inputs/StringInput'
|
||||
import LayerIcon from '../icons/LayerIcon'
|
||||
|
||||
|
||||
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 FeatureLayerPopup extends React.Component {
|
||||
render() {
|
||||
const sources = groupFeaturesBySourceLayer(this.props.features)
|
||||
|
||||
const items = Object.keys(sources).map(vectorLayerId => {
|
||||
const layers = sources[vectorLayerId].map((feature, idx) => {
|
||||
return <label
|
||||
key={idx}
|
||||
className="maputnik-popup-layer"
|
||||
>
|
||||
<LayerIcon type={feature.layer.type} style={{
|
||||
width: 14,
|
||||
height: 14,
|
||||
paddingRight: 3
|
||||
}}/>
|
||||
{feature.layer.id}
|
||||
</label>
|
||||
})
|
||||
return <div key={vectorLayerId}>
|
||||
<div className="maputnik-popup-layer-id">{vectorLayerId}</div>
|
||||
{layers}
|
||||
</div>
|
||||
})
|
||||
|
||||
return <div className="maputnik-feature-layer-popup">
|
||||
{items}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default FeatureLayerPopup
|
||||
@@ -1,66 +0,0 @@
|
||||
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
|
||||
@@ -2,31 +2,27 @@ 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 displayValue(value) {
|
||||
if (typeof value === 'undefined' || value === null) return value;
|
||||
if (value instanceof Date) return value.toLocaleString();
|
||||
if (typeof value === 'object' ||
|
||||
typeof value === 'number' ||
|
||||
typeof value === 'string') return value.toString();
|
||||
return value;
|
||||
}
|
||||
|
||||
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'}}/>
|
||||
return <InputBlock key={propertyName} label={propertyName}>
|
||||
<StringInput value={displayValue(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>
|
||||
return <div key={feature.id}>
|
||||
<div className="maputnik-popup-layer-id">{feature.layer['source-layer']}</div>
|
||||
{renderProperties(feature)}
|
||||
</div>
|
||||
}
|
||||
@@ -35,7 +31,7 @@ class FeaturePropertyPopup extends React.Component {
|
||||
|
||||
render() {
|
||||
const features = this.props.features
|
||||
return <div>
|
||||
return <div className="maputnik-feature-property-popup">
|
||||
{features.map(renderFeature)}
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -1,126 +0,0 @@
|
||||
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,35 +1,78 @@
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import MapboxGl from 'mapbox-gl/dist/mapbox-gl.js'
|
||||
import FeatureLayerTable from './FeatureLayerTable'
|
||||
import MapboxInspect from 'mapbox-gl-inspect'
|
||||
import FeatureLayerPopup from './FeatureLayerPopup'
|
||||
import FeaturePropertyPopup from './FeaturePropertyPopup'
|
||||
import validateColor from 'mapbox-gl-style-spec/lib/validate/validate_color'
|
||||
import style from '../../libs/style.js'
|
||||
import tokens from '../../config/tokens.json'
|
||||
import { colorHighlightedLayer } from '../../libs/highlight'
|
||||
import 'mapbox-gl/dist/mapbox-gl.css'
|
||||
import '../../mapboxgl.css'
|
||||
|
||||
function renderPopup(features) {
|
||||
function renderLayerPopup(features) {
|
||||
var mountNode = document.createElement('div');
|
||||
ReactDOM.render(<FeatureLayerTable features={features} />, mountNode)
|
||||
ReactDOM.render(<FeatureLayerPopup features={features} />, mountNode)
|
||||
return mountNode.innerHTML;
|
||||
}
|
||||
|
||||
function renderPropertyPopup(features) {
|
||||
var mountNode = document.createElement('div');
|
||||
ReactDOM.render(<FeaturePropertyPopup features={features} />, mountNode)
|
||||
return mountNode.innerHTML;
|
||||
}
|
||||
|
||||
function buildInspectStyle(originalMapStyle, coloredLayers, highlightedLayer) {
|
||||
const backgroundLayer = {
|
||||
"id": "background",
|
||||
"type": "background",
|
||||
"paint": {
|
||||
"background-color": '#1c1f24',
|
||||
}
|
||||
}
|
||||
|
||||
const layer = colorHighlightedLayer(highlightedLayer)
|
||||
if(layer) {
|
||||
coloredLayers.push(layer)
|
||||
}
|
||||
|
||||
const sources = {}
|
||||
Object.keys(originalMapStyle.sources).forEach(sourceId => {
|
||||
const source = originalMapStyle.sources[sourceId]
|
||||
if(source.type !== 'raster') {
|
||||
sources[sourceId] = source
|
||||
}
|
||||
})
|
||||
|
||||
const inspectStyle = {
|
||||
...originalMapStyle,
|
||||
sources: sources,
|
||||
layers: [backgroundLayer].concat(coloredLayers)
|
||||
}
|
||||
return inspectStyle
|
||||
}
|
||||
|
||||
export default class MapboxGlMap extends React.Component {
|
||||
static propTypes = {
|
||||
onDataChange: React.PropTypes.func,
|
||||
mapStyle: React.PropTypes.object.isRequired,
|
||||
accessToken: React.PropTypes.string,
|
||||
style: React.PropTypes.object,
|
||||
inspectModeEnabled: React.PropTypes.bool.isRequired,
|
||||
highlightedLayer: React.PropTypes.object,
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
onMapLoaded: () => {},
|
||||
onDataChange: () => {},
|
||||
mapboxAccessToken: tokens.mapbox,
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
MapboxGl.accessToken = tokens.mapbox
|
||||
this.state = {
|
||||
map: null,
|
||||
inspect: null,
|
||||
isPopupOpen: false,
|
||||
popupX: 0,
|
||||
popupY: 0,
|
||||
@@ -38,26 +81,55 @@ export default class MapboxGlMap extends React.Component {
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if(!this.state.map) return
|
||||
const metadata = nextProps.mapStyle.metadata || {}
|
||||
MapboxGl.accessToken = metadata['maputnik:mapbox_access_token'] || tokens.mapbox
|
||||
|
||||
//Mapbox GL now does diffing natively so we don't need to calculate
|
||||
//the necessary operations ourselves!
|
||||
this.state.map.setStyle(nextProps.mapStyle, { diff: true})
|
||||
if(!nextProps.inspectModeEnabled) {
|
||||
//Mapbox GL now does diffing natively so we don't need to calculate
|
||||
//the necessary operations ourselves!
|
||||
this.state.map.setStyle(nextProps.mapStyle, { diff: true})
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if(this.props.inspectModeEnabled !== prevProps.inspectModeEnabled) {
|
||||
this.state.inspect.toggleInspector()
|
||||
}
|
||||
if(this.props.inspectModeEnabled) {
|
||||
this.state.inspect.render()
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
MapboxGl.accessToken = this.props.accessToken
|
||||
|
||||
const map = new MapboxGl.Map({
|
||||
container: this.container,
|
||||
style: this.props.mapStyle,
|
||||
hash: true,
|
||||
})
|
||||
|
||||
const nav = new MapboxGl.NavigationControl();
|
||||
map.addControl(nav, 'top-right');
|
||||
const nav = new MapboxGl.NavigationControl();
|
||||
map.addControl(nav, 'top-right');
|
||||
|
||||
const inspect = new MapboxInspect({
|
||||
popup: new MapboxGl.Popup({
|
||||
closeButton: false,
|
||||
closeOnClick: false
|
||||
}),
|
||||
showMapPopup: true,
|
||||
showInspectButton: false,
|
||||
buildInspectStyle: (originalMapStyle, coloredLayers) => buildInspectStyle(originalMapStyle, coloredLayers, this.props.highlightedLayer),
|
||||
renderPopup: features => {
|
||||
if(this.props.inspectModeEnabled) {
|
||||
return renderPropertyPopup(features)
|
||||
} else {
|
||||
return renderLayerPopup(features)
|
||||
}
|
||||
}
|
||||
})
|
||||
map.addControl(inspect)
|
||||
|
||||
map.on("style.load", () => {
|
||||
this.setState({ map });
|
||||
this.setState({ map, inspect });
|
||||
})
|
||||
|
||||
map.on("data", e => {
|
||||
@@ -66,37 +138,12 @@ export default class MapboxGlMap extends React.Component {
|
||||
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
|
||||
className="maputnik-map"
|
||||
ref={x => this.container = x}
|
||||
style={{
|
||||
position: "fixed",
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
...this.props.style,
|
||||
}}>
|
||||
</div>
|
||||
></div>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,53 @@
|
||||
import React from 'react'
|
||||
import style from '../../libs/style.js'
|
||||
import isEqual from 'lodash.isequal'
|
||||
import { loadJSON } from '../../libs/urlopen'
|
||||
|
||||
function suitableVectorSource(mapStyle) {
|
||||
const sources = Object.keys(mapStyle.sources)
|
||||
.map(sourceId => {
|
||||
return {
|
||||
id: sourceId,
|
||||
source: mapStyle.sources[sourceId]
|
||||
}
|
||||
})
|
||||
.filter(({source}) => source.type === 'vector')
|
||||
return sources[0]
|
||||
}
|
||||
|
||||
function toVectorLayer(source, tilegrid, cb) {
|
||||
function newMVTLayer(tileUrl) {
|
||||
const ol = require('openlayers')
|
||||
return new ol.layer.VectorTile({
|
||||
source: new ol.source.VectorTile({
|
||||
format: new ol.format.MVT(),
|
||||
tileGrid: tilegrid,
|
||||
tilePixelRatio: 8,
|
||||
url: tileUrl
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
if(!source.tiles) {
|
||||
sourceFromTileJSON(source.url, tileSource => {
|
||||
cb(newMVTLayer(tileSource.tiles[0]))
|
||||
})
|
||||
} else {
|
||||
cb(newMVTLayer(source.tiles[0]))
|
||||
}
|
||||
}
|
||||
|
||||
function sourceFromTileJSON(url, cb) {
|
||||
loadJSON(url, null, tilejson => {
|
||||
if(!tilejson) return
|
||||
cb({
|
||||
type: 'vector',
|
||||
tiles: tilejson.tiles,
|
||||
minzoom: tilejson.minzoom,
|
||||
maxzoom: tilejson.maxzoom,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
class OpenLayers3Map extends React.Component {
|
||||
static propTypes = {
|
||||
@@ -13,21 +61,51 @@ class OpenLayers3Map extends React.Component {
|
||||
onDataChange: () => {},
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.tilegrid = null
|
||||
this.resolutions = null
|
||||
this.layer = null
|
||||
this.map = null
|
||||
}
|
||||
|
||||
updateStyle(newMapStyle) {
|
||||
const oldSource = suitableVectorSource(this.props.mapStyle)
|
||||
const newSource = suitableVectorSource(newMapStyle)
|
||||
const resolutions = this.resolutions
|
||||
|
||||
function setStyleFunc(map, layer) {
|
||||
const olms = require('ol-mapbox-style')
|
||||
const styleFunc = olms.getStyleFunction(newMapStyle, newSource.id, resolutions)
|
||||
layer.setStyle(styleFunc)
|
||||
//NOTE: We need to mark the source as changed in order
|
||||
//to trigger a rerender
|
||||
layer.getSource().changed()
|
||||
map.render()
|
||||
}
|
||||
|
||||
if(newSource) {
|
||||
if(this.layer && !isEqual(oldSource, newSource)) {
|
||||
this.map.removeLayer(this.layer)
|
||||
this.layer = null
|
||||
}
|
||||
|
||||
if(!this.layer) {
|
||||
toVectorLayer(newSource.source, this.tilegrid, vectorLayer => {
|
||||
this.layer = vectorLayer
|
||||
this.map.addLayer(this.layer)
|
||||
setStyleFunc(this.map, this.layer)
|
||||
})
|
||||
} else {
|
||||
setStyleFunc(this.map, this.layer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
require.ensure(["openlayers", "ol-mapbox-style"], ()=> {
|
||||
const ol = require('openlayers')
|
||||
const olms = require('ol-mapbox-style')
|
||||
const jsonStyle = nextProps.mapStyle
|
||||
const styleFunc = olms.getStyleFunction(jsonStyle, 'openmaptiles', this.resolutions)
|
||||
console.log('New style babee')
|
||||
|
||||
const layer = this.layer
|
||||
layer.setStyle(styleFunc)
|
||||
//NOTE: We need to mark the source as changed in order
|
||||
//to trigger a rerender
|
||||
layer.getSource().changed()
|
||||
|
||||
this.state.map.render()
|
||||
require.ensure(["openlayers", "ol-mapbox-style"], () => {
|
||||
if(!this.map || !this.resolutions) return
|
||||
this.updateStyle(nextProps.mapStyle)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -40,46 +118,37 @@ class OpenLayers3Map extends React.Component {
|
||||
const ol = require('openlayers')
|
||||
const olms = require('ol-mapbox-style')
|
||||
|
||||
const tilegrid = ol.tilegrid.createXYZ({tileSize: 512, maxZoom: 22})
|
||||
this.resolutions = tilegrid.getResolutions()
|
||||
this.layer = new ol.layer.VectorTile({
|
||||
source: new ol.source.VectorTile({
|
||||
attributions: '© <a href="http://www.openstreetmap.org/copyright">OpenStreetMap contributors</a>',
|
||||
format: new ol.format.MVT(),
|
||||
tileGrid: tilegrid,
|
||||
tilePixelRatio: 8,
|
||||
url: 'https://free-0.tilehosting.com/data/v3/{z}/{x}/{y}.pbf?key=tXiQqN3lIgskyDErJCeY'
|
||||
})
|
||||
})
|
||||
|
||||
const jsonStyle = this.props.mapStyle
|
||||
const styleFunc = olms.getStyleFunction(jsonStyle, 'openmaptiles', this.resolutions)
|
||||
this.layer.setStyle(styleFunc)
|
||||
this.tilegrid = ol.tilegrid.createXYZ({tileSize: 512, maxZoom: 22})
|
||||
this.resolutions = this.tilegrid.getResolutions()
|
||||
|
||||
const map = new ol.Map({
|
||||
target: this.container,
|
||||
layers: [this.layer],
|
||||
layers: [],
|
||||
view: new ol.View({
|
||||
center: jsonStyle.center,
|
||||
zoom: 2,
|
||||
//zoom: jsonStyle.zoom,
|
||||
center: [52.5, -78.4]
|
||||
})
|
||||
})
|
||||
map.addControl(new ol.control.Zoom());
|
||||
this.setState({ map });
|
||||
map.addControl(new ol.control.Zoom())
|
||||
this.map = map
|
||||
this.updateStyle(this.props.mapStyle)
|
||||
})
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div
|
||||
ref={x => this.container = x}
|
||||
style={{
|
||||
position: "fixed",
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
}}></div>
|
||||
ref={x => this.container = x}
|
||||
style={{
|
||||
position: "fixed",
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
height: "100%",
|
||||
width: "75%",
|
||||
backgroundColor: '#fff',
|
||||
...this.props.style,
|
||||
}}>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ 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'
|
||||
@@ -14,8 +13,8 @@ import LayerSourceLayerBlock from '../layers/LayerSourceLayerBlock'
|
||||
|
||||
class AddModal extends React.Component {
|
||||
static propTypes = {
|
||||
mapStyle: React.PropTypes.object.isRequired,
|
||||
onStyleChange: React.PropTypes.func.isRequired,
|
||||
layers: React.PropTypes.array.isRequired,
|
||||
onLayersChange: React.PropTypes.func.isRequired,
|
||||
isOpen: React.PropTypes.bool.isRequired,
|
||||
onOpenToggle: React.PropTypes.func.isRequired,
|
||||
|
||||
@@ -24,7 +23,7 @@ class AddModal extends React.Component {
|
||||
}
|
||||
|
||||
addLayer() {
|
||||
const changedLayers = this.props.mapStyle.layers.slice(0)
|
||||
const changedLayers = this.props.layers.slice(0)
|
||||
const layer = {
|
||||
id: this.state.id,
|
||||
type: this.state.type,
|
||||
@@ -32,17 +31,14 @@ class AddModal extends React.Component {
|
||||
|
||||
if(this.state.type !== 'background') {
|
||||
layer.source = this.state.source
|
||||
layer['source-layer'] = this.state['source-layer']
|
||||
if(this.state.type !== 'raster' && this.state['source-layer']) {
|
||||
layer['source-layer'] = this.state['source-layer']
|
||||
}
|
||||
}
|
||||
|
||||
changedLayers.push(layer)
|
||||
|
||||
const changedStyle = {
|
||||
...this.props.mapStyle,
|
||||
layers: changedLayers,
|
||||
}
|
||||
|
||||
this.props.onStyleChange(changedStyle)
|
||||
this.props.onLayersChange(changedLayers)
|
||||
this.props.onOpenToggle(false)
|
||||
}
|
||||
|
||||
@@ -64,7 +60,7 @@ class AddModal extends React.Component {
|
||||
if(!this.state.source && sourceIds.length > 0) {
|
||||
this.setState({
|
||||
source: sourceIds[0],
|
||||
'source-layer': this.state['source-layer'] || nextProps.sources[sourceIds[0]][0]
|
||||
'source-layer': this.state['source-layer'] || (nextProps.sources[sourceIds[0]] || [])[0]
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -76,6 +72,7 @@ class AddModal extends React.Component {
|
||||
onOpenToggle={this.props.onOpenToggle}
|
||||
title={'Add Layer'}
|
||||
>
|
||||
<div className="maputnik-add-layer">
|
||||
<LayerIdBlock
|
||||
value={this.state.id}
|
||||
onChange={v => this.setState({ id: v })}
|
||||
@@ -91,16 +88,17 @@ class AddModal extends React.Component {
|
||||
onChange={v => this.setState({ source: v })}
|
||||
/>
|
||||
}
|
||||
{this.state.type !== 'background' &&
|
||||
{this.state.type !== 'background' && this.state.type !== 'raster' &&
|
||||
<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)}>
|
||||
<Button className="maputnik-add-layer-button" onClick={this.addLayer.bind(this)}>
|
||||
Add Layer
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
}
|
||||
}
|
||||
|
||||
159
src/components/modals/ExportModal.jsx
Normal file
159
src/components/modals/ExportModal.jsx
Normal file
@@ -0,0 +1,159 @@
|
||||
import React from 'react'
|
||||
import { saveAs } from 'file-saver'
|
||||
|
||||
import GlSpec from 'mapbox-gl-style-spec/reference/latest.js'
|
||||
import InputBlock from '../inputs/InputBlock'
|
||||
import StringInput from '../inputs/StringInput'
|
||||
import SelectInput from '../inputs/SelectInput'
|
||||
import Button from '../Button'
|
||||
import Modal from './Modal'
|
||||
import MdFileDownload from 'react-icons/lib/md/file-download'
|
||||
import formatStyle from 'mapbox-gl-style-spec/lib/format'
|
||||
import GitHub from 'github-api'
|
||||
|
||||
|
||||
class Gist extends React.Component {
|
||||
static propTypes = {
|
||||
mapStyle: React.PropTypes.object.isRequired,
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {}
|
||||
}
|
||||
|
||||
onSave() {
|
||||
this.setState({
|
||||
saving: true
|
||||
});
|
||||
const mapStyleStr = formatStyle(this.props.mapStyle);
|
||||
const styleTitle = this.props.mapStyle.name || 'Style';
|
||||
const htmlStr = `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>`+styleTitle+` Preview</title>
|
||||
<link rel="stylesheet" type="text/css" href="https://api.mapbox.com/mapbox-gl-js/v0.28.0/mapbox-gl.css" />
|
||||
<script src="https://api.mapbox.com/mapbox-gl-js/v0.28.0/mapbox-gl.js"></script>
|
||||
<style>
|
||||
body { margin:0; padding:0; }
|
||||
#map { position:absolute; top:0; bottom:0; width:100%; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id='map'></div>
|
||||
<script>
|
||||
var map = new mapboxgl.Map({
|
||||
container: 'map',
|
||||
style: 'style.json',
|
||||
attributionControl: true,
|
||||
hash: true
|
||||
});
|
||||
map.addControl(new mapboxgl.NavigationControl());
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
`
|
||||
const gh = new GitHub();
|
||||
let gist = gh.getGist(); // not a gist yet
|
||||
gist.create({
|
||||
public: true,
|
||||
description: styleTitle + 'Preview',
|
||||
files: {
|
||||
"style.json": {
|
||||
content: mapStyleStr
|
||||
},
|
||||
"index.html": {
|
||||
content: htmlStr
|
||||
}
|
||||
}
|
||||
}).then(function({data}) {
|
||||
return gist.read();
|
||||
}).then(function({data}) {
|
||||
this.setState({
|
||||
latestGist: data
|
||||
});
|
||||
}.bind(this));
|
||||
}
|
||||
|
||||
renderLatestGist() {
|
||||
const gist = this.state.latestGist;
|
||||
const saving = this.state.saving;
|
||||
if(gist) {
|
||||
const user = gist.user || 'anonymous';
|
||||
return <p>
|
||||
Latest saved gist:{' '}
|
||||
<a target="_blank" href={"https://bl.ocks.org/"+user+"/"+gist.id}>Preview</a>,{' '}
|
||||
<a target="_blank" href={"https://gist.github.com/"+user+"/"+gist.id}>Source</a>
|
||||
</p>
|
||||
} else if(saving) {
|
||||
return <p>Saving...</p>
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div>
|
||||
<Button onClick={this.onSave.bind(this)}>
|
||||
<MdFileDownload />
|
||||
Save to Gist (anonymous)
|
||||
</Button>
|
||||
{this.renderLatestGist()}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
function stripAccessTokens(mapStyle) {
|
||||
const changedMetadata = { ...mapStyle.metadata }
|
||||
delete changedMetadata['maputnik:mapbox_access_token']
|
||||
delete changedMetadata['maputnik:openmaptiles_access_token']
|
||||
return {
|
||||
...mapStyle,
|
||||
metadata: changedMetadata
|
||||
}
|
||||
}
|
||||
|
||||
class ExportModal extends React.Component {
|
||||
static propTypes = {
|
||||
mapStyle: React.PropTypes.object.isRequired,
|
||||
isOpen: React.PropTypes.bool.isRequired,
|
||||
onOpenToggle: React.PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
downloadStyle() {
|
||||
const blob = new Blob([formatStyle(stripAccessTokens(this.props.mapStyle))], {type: "application/json;charset=utf-8"});
|
||||
saveAs(blob, this.props.mapStyle.id + ".json");
|
||||
}
|
||||
|
||||
render() {
|
||||
return <Modal
|
||||
isOpen={this.props.isOpen}
|
||||
onOpenToggle={this.props.onOpenToggle}
|
||||
title={'Export Style'}
|
||||
>
|
||||
|
||||
<div className="maputnik-modal-section">
|
||||
<h4>Download Style</h4>
|
||||
<p>
|
||||
Download a JSON style to your computer.
|
||||
</p>
|
||||
<Button onClick={this.downloadStyle.bind(this)}>
|
||||
<MdFileDownload />
|
||||
Download
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="maputnik-modal-section">
|
||||
<h4>Save style</h4>
|
||||
<Gist mapStyle={this.props.mapStyle} />
|
||||
</div>
|
||||
</Modal>
|
||||
}
|
||||
}
|
||||
|
||||
export default ExportModal
|
||||
@@ -1,10 +1,6 @@
|
||||
import React from 'react'
|
||||
|
||||
import CloseIcon from 'react-icons/lib/md/close'
|
||||
|
||||
import Overlay from './Overlay'
|
||||
import colors from '../../config/colors'
|
||||
import { margins, fontSizes } from '../../config/scales'
|
||||
|
||||
class Modal extends React.Component {
|
||||
static propTypes = {
|
||||
@@ -15,32 +11,17 @@ class Modal extends React.Component {
|
||||
|
||||
render() {
|
||||
return <Overlay isOpen={this.props.isOpen}>
|
||||
<div style={{
|
||||
minWidth: 350,
|
||||
maxWidth: 600,
|
||||
backgroundColor: colors.black,
|
||||
boxShadow: '0px 0px 5px 0px rgba(0,0,0,0.3)',
|
||||
}}>
|
||||
<div style={{
|
||||
backgroundColor: colors.gray,
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
padding: margins[2],
|
||||
fontSize: fontSizes[4],
|
||||
}}>
|
||||
{this.props.title}
|
||||
<span style={{flexGrow: 1}} />
|
||||
<a
|
||||
<div className="maputnik-modal">
|
||||
<header className="maputnik-modal-header">
|
||||
<h1 className="maputnik-modal-header-title">{this.props.title}</h1>
|
||||
<span className="maputnik-modal-header-space"></span>
|
||||
<a className="maputnik-modal-header-toggle"
|
||||
onClick={() => this.props.onOpenToggle(false)}
|
||||
style={{ cursor: 'pointer' }} >
|
||||
>
|
||||
<CloseIcon />
|
||||
</a>
|
||||
</div>
|
||||
<div style={{
|
||||
padding: margins[2],
|
||||
}}>
|
||||
{this.props.children}
|
||||
</div>
|
||||
</header>
|
||||
<div className="maputnik-modal-content">{this.props.children}</div>
|
||||
</div>
|
||||
</Overlay>
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import React from 'react'
|
||||
import Modal from './Modal'
|
||||
import Heading from '../Heading'
|
||||
import Button from '../Button'
|
||||
import Paragraph from '../Paragraph'
|
||||
import FileReaderInput from 'react-file-reader-input'
|
||||
import request from 'request'
|
||||
|
||||
@@ -10,8 +8,6 @@ import FileUploadIcon from 'react-icons/lib/md/file-upload'
|
||||
import AddIcon from 'react-icons/lib/md/add-circle-outline'
|
||||
|
||||
import style from '../../libs/style.js'
|
||||
import colors from '../../config/colors'
|
||||
import { margins, fontSizes } from '../../config/scales'
|
||||
import publicStyles from '../../config/styles.json'
|
||||
|
||||
class PublicStyle extends React.Component {
|
||||
@@ -23,38 +19,18 @@ class PublicStyle extends React.Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div style={{
|
||||
verticalAlign: 'top',
|
||||
marginTop: margins[2],
|
||||
marginRight: margins[2],
|
||||
backgroundColor: colors.gray,
|
||||
display: 'inline-block',
|
||||
width: 180,
|
||||
fontSize: fontSizes[4],
|
||||
color: colors.lowgray,
|
||||
}}>
|
||||
return <div className="maputnik-public-style">
|
||||
<Button
|
||||
className="maputnik-public-style-button"
|
||||
onClick={() => this.props.onSelect(this.props.url)}
|
||||
style={{
|
||||
backgroundColor: 'transparent',
|
||||
padding: margins[2],
|
||||
display: 'block',
|
||||
}}
|
||||
>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
}}>
|
||||
<span style={{fontWeight: 700}}>{this.props.title}</span>
|
||||
<span style={{flexGrow: 1}} />
|
||||
<header className="maputnik-public-style-header">
|
||||
<h4>{this.props.title}</h4>
|
||||
<span className="maputnik-space" />
|
||||
<AddIcon />
|
||||
</div>
|
||||
</header>
|
||||
<img
|
||||
style={{
|
||||
display: 'block',
|
||||
marginTop: margins[1],
|
||||
maxWidth: '100%',
|
||||
}}
|
||||
className="maputnik-public-style-thumbnail"
|
||||
src={this.props.thumbnailUrl}
|
||||
alt={this.props.title}
|
||||
/>
|
||||
@@ -113,22 +89,20 @@ class OpenModal extends React.Component {
|
||||
onOpenToggle={this.props.onOpenToggle}
|
||||
title={'Open Style'}
|
||||
>
|
||||
<Heading level={4}>Upload Style</Heading>
|
||||
<Paragraph>
|
||||
Upload a JSON style from your computer.
|
||||
</Paragraph>
|
||||
<FileReaderInput onChange={this.onUpload.bind(this)}>
|
||||
<Button>
|
||||
<FileUploadIcon />
|
||||
Upload
|
||||
</Button>
|
||||
</FileReaderInput>
|
||||
|
||||
<Heading level={4}>Gallery Styles</Heading>
|
||||
<Paragraph>
|
||||
Open one of the publicly available styles to start from.
|
||||
</Paragraph>
|
||||
{styleOptions}
|
||||
<section className="maputnik-modal-section">
|
||||
<h2>Upload Style</h2>
|
||||
<p>Upload a JSON style from your computer.</p>
|
||||
<FileReaderInput onChange={this.onUpload.bind(this)}>
|
||||
<Button className="maputnik-upload-button"><FileUploadIcon /> Upload</Button>
|
||||
</FileReaderInput>
|
||||
</section>
|
||||
<section className="maputnik-modal-section">
|
||||
<h2>Gallery Styles</h2>
|
||||
<p>
|
||||
Open one of the publicly available styles to start from.
|
||||
</p>
|
||||
{styleOptions}
|
||||
</section>
|
||||
</Modal>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,26 +1,5 @@
|
||||
import React from 'react'
|
||||
|
||||
class ViewportOverlay extends React.Component {
|
||||
static propTypes = {
|
||||
style: React.PropTypes.object
|
||||
}
|
||||
|
||||
render() {
|
||||
const overlayStyle = {
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
zIndex: 2,
|
||||
opacity: 0.875,
|
||||
backgroundColor: 'rgb(28, 31, 36)',
|
||||
...this.props.style
|
||||
}
|
||||
|
||||
return <div style={overlayStyle} />
|
||||
}
|
||||
}
|
||||
|
||||
class Overlay extends React.Component {
|
||||
static propTypes = {
|
||||
@@ -29,23 +8,14 @@ class Overlay extends React.Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div style={{
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
position: 'fixed',
|
||||
display: this.props.isOpen ? 'flex' : 'none',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}>
|
||||
<ViewportOverlay />
|
||||
<div style={{
|
||||
zIndex: 3,
|
||||
}}>
|
||||
let overlayStyle = {}
|
||||
if(!this.props.isOpen) {
|
||||
overlayStyle['display'] = 'none';
|
||||
}
|
||||
|
||||
return <div className={"maputnik-overlay"} style={overlayStyle}>
|
||||
<div className={"maputnik-overlay-viewport"} />
|
||||
{this.props.children}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import React from 'react'
|
||||
|
||||
import GlSpec from 'mapbox-gl-style-spec/reference/latest.js'
|
||||
import InputBlock from '../inputs/InputBlock'
|
||||
import StringInput from '../inputs/StringInput'
|
||||
import SelectInput from '../inputs/SelectInput'
|
||||
import Modal from './Modal'
|
||||
import colors from '../../config/colors'
|
||||
|
||||
class SettingsModal extends React.Component {
|
||||
static propTypes = {
|
||||
@@ -43,52 +43,60 @@ class SettingsModal extends React.Component {
|
||||
return <Modal
|
||||
isOpen={this.props.isOpen}
|
||||
onOpenToggle={this.props.onOpenToggle}
|
||||
title={'StyleSettings'}
|
||||
title={'Style Settings'}
|
||||
>
|
||||
<InputBlock label={"Name"}>
|
||||
<div style={{minWidth: 350}}>
|
||||
<InputBlock label={"Name"} doc={GlSpec.$root.name.doc}>
|
||||
<StringInput {...inputProps}
|
||||
value={this.props.mapStyle.name}
|
||||
onChange={this.changeStyleProperty.bind(this, "name")}
|
||||
/>
|
||||
</InputBlock>
|
||||
<InputBlock label={"Owner"}>
|
||||
<InputBlock label={"Owner"} doc={"Owner ID of the style. Used by Mapbox or future style APIs."}>
|
||||
<StringInput {...inputProps}
|
||||
value={this.props.mapStyle.owner}
|
||||
onChange={this.changeStyleProperty.bind(this, "owner")}
|
||||
/>
|
||||
</InputBlock>
|
||||
<InputBlock label={"Sprite URL"}>
|
||||
<InputBlock label={"Sprite URL"} doc={GlSpec.$root.sprite.doc}>
|
||||
<StringInput {...inputProps}
|
||||
value={this.props.mapStyle.sprite}
|
||||
onChange={this.changeStyleProperty.bind(this, "sprite")}
|
||||
/>
|
||||
</InputBlock>
|
||||
|
||||
<InputBlock label={"Glyphs URL"}>
|
||||
<InputBlock label={"Glyphs URL"} doc={GlSpec.$root.glyphs.doc}>
|
||||
<StringInput {...inputProps}
|
||||
value={this.props.mapStyle.glyphs}
|
||||
onChange={this.changeStyleProperty.bind(this, "glyphs")}
|
||||
/>
|
||||
</InputBlock>
|
||||
|
||||
<InputBlock label={"Access Token"}>
|
||||
<InputBlock label={"Mapbox Access Token"} doc={"Public access token for Mapbox services."}>
|
||||
<StringInput {...inputProps}
|
||||
value={metadata['maputnik:access_token']}
|
||||
onChange={this.changeMetadataProperty.bind(this, "maputnik:access_token")}
|
||||
value={metadata['maputnik:mapbox_access_token']}
|
||||
onChange={this.changeMetadataProperty.bind(this, "maputnik:mapbox_access_token")}
|
||||
/>
|
||||
</InputBlock>
|
||||
|
||||
<InputBlock label={"Style Renderer"}>
|
||||
<InputBlock label={"OpenMapTiles Access Token"} doc={"Public access token for the OpenMapTiles CDN."}>
|
||||
<StringInput {...inputProps}
|
||||
value={metadata['maputnik:openmaptiles_access_token']}
|
||||
onChange={this.changeMetadataProperty.bind(this, "maputnik:openmaptiles_access_token")}
|
||||
/>
|
||||
</InputBlock>
|
||||
|
||||
<InputBlock label={"Style Renderer"} doc={"Choose the default Maputnik renderer for this style."}>
|
||||
<SelectInput {...inputProps}
|
||||
options={[
|
||||
['mbgljs', 'MapboxGL JS'],
|
||||
['ol3', 'Open Layers 3'],
|
||||
['inspection', 'Inspection Mode'],
|
||||
]}
|
||||
value={metadata['maputnik:renderer'] || 'mbgljs'}
|
||||
onChange={this.changeMetadataProperty.bind(this, 'maputnik:renderer')}
|
||||
/>
|
||||
</InputBlock>
|
||||
</div>
|
||||
</Modal>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
import React from 'react'
|
||||
import GlSpec from 'mapbox-gl-style-spec/reference/latest.js'
|
||||
import Modal from './Modal'
|
||||
import Heading from '../Heading'
|
||||
import Button from '../Button'
|
||||
import Paragraph from '../Paragraph'
|
||||
import InputBlock from '../inputs/InputBlock'
|
||||
import StringInput from '../inputs/StringInput'
|
||||
import SelectInput from '../inputs/SelectInput'
|
||||
import SourceTypeEditor from '../sources/SourceTypeEditor'
|
||||
|
||||
import style from '../../libs/style'
|
||||
import { deleteSource, addSource, changeSource } from '../../libs/source'
|
||||
import publicSources from '../../config/tilesets.json'
|
||||
import colors from '../../config/colors'
|
||||
import { margins, fontSizes } from '../../config/scales'
|
||||
|
||||
import AddIcon from 'react-icons/lib/md/add-circle-outline'
|
||||
import DeleteIcon from 'react-icons/lib/md/delete'
|
||||
@@ -25,82 +23,60 @@ class PublicSource extends React.Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div style={{
|
||||
verticalAlign: 'top',
|
||||
marginTop: margins[2],
|
||||
marginRight: margins[2],
|
||||
backgroundColor: colors.gray,
|
||||
display: 'inline-block',
|
||||
width: 240,
|
||||
fontSize: fontSizes[4],
|
||||
color: colors.lowgray,
|
||||
}}>
|
||||
<Button
|
||||
onClick={() => this.props.onSelect(this.props.id)}
|
||||
style={{
|
||||
backgroundColor: 'transparent',
|
||||
padding: margins[2],
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<span style={{fontWeight: 700}}>{this.props.title}</span>
|
||||
<br/>
|
||||
<span style={{fontSize: fontSizes[5]}}>#{this.props.id}</span>
|
||||
</div>
|
||||
<span style={{flexGrow: 1}} />
|
||||
<AddIcon />
|
||||
</Button>
|
||||
return <div className="maputnik-public-source">
|
||||
<Button
|
||||
className="maputnik-public-source-select"
|
||||
onClick={() => this.props.onSelect(this.props.id)}
|
||||
>
|
||||
<div className="maputnik-public-source-info">
|
||||
<p className="maputnik-public-source-name">{this.props.title}</p>
|
||||
<p className="maputnik-public-source-id">#{this.props.id}</p>
|
||||
</div>
|
||||
<span className="maputnik-space" />
|
||||
<AddIcon />
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
function editorMode(source) {
|
||||
if(source.type === 'geojson') return ' geojson'
|
||||
if(source.type === 'vector' && source.tiles) {
|
||||
return 'tilexyz'
|
||||
if(source.type === 'raster') {
|
||||
if(source.tiles) return 'tilexyz_raster'
|
||||
return 'tilejson_raster'
|
||||
}
|
||||
return 'tilejson'
|
||||
if(source.type === 'vector') {
|
||||
if(source.tiles) return 'tilexyz_vector'
|
||||
return 'tilejson_vector'
|
||||
}
|
||||
if(source.type === 'geojson') return 'geojson'
|
||||
return null
|
||||
}
|
||||
|
||||
class ActiveSourceTypeEditor extends React.Component {
|
||||
static propTypes = {
|
||||
sourceId: React.PropTypes.string.isRequired,
|
||||
source: React.PropTypes.object.isRequired,
|
||||
onSourceDelete: React.PropTypes.func.isRequired,
|
||||
onSourceChange: React.PropTypes.func.isRequired,
|
||||
onDelete: React.PropTypes.func.isRequired,
|
||||
onChange: React.PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
render() {
|
||||
const inputProps = { }
|
||||
return <div style={{
|
||||
}}>
|
||||
<div style={{
|
||||
backgroundColor: colors.gray,
|
||||
color: colors.lowgray,
|
||||
padding: margins[1],
|
||||
display: 'flex',
|
||||
fontSize: fontSizes[4],
|
||||
flexDirection: 'row',
|
||||
}}>
|
||||
<span style={{fontWeight: 700, fontSize: fontSizes[4], lineHeight: 2}}>#{this.props.sourceId}</span>
|
||||
<span style={{flexGrow: 1}} />
|
||||
return <div className="maputnik-active-source-type-editor">
|
||||
<div className="maputnik-active-source-type-editor-header">
|
||||
<span className="maputnik-active-source-type-editor-header-id">#{this.props.sourceId}</span>
|
||||
<span className="maputnik-space" />
|
||||
<Button
|
||||
onClick={()=> this.props.onSourceDelete(this.props.sourceId)}
|
||||
className="maputnik-active-source-type-editor-header-delete"
|
||||
onClick={()=> this.props.onDelete(this.props.sourceId)}
|
||||
style={{backgroundColor: 'transparent'}}
|
||||
>
|
||||
<DeleteIcon />
|
||||
</Button>
|
||||
</div>
|
||||
<div style={{
|
||||
borderColor: colors.gray,
|
||||
borderWidth: 2,
|
||||
borderStyle: 'solid',
|
||||
padding: margins[1],
|
||||
}}>
|
||||
<div className="maputnik-active-source-type-editor-content">
|
||||
<SourceTypeEditor
|
||||
onChange={this.props.onSourceChange}
|
||||
onChange={this.props.onChange}
|
||||
mode={editorMode(this.props.source)}
|
||||
source={this.props.source}
|
||||
/>
|
||||
@@ -111,15 +87,15 @@ class ActiveSourceTypeEditor extends React.Component {
|
||||
|
||||
class AddSource extends React.Component {
|
||||
static propTypes = {
|
||||
onSourceAdd: React.PropTypes.func.isRequired,
|
||||
onAdd: React.PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
mode: 'tilejson',
|
||||
mode: 'tilejson_vector',
|
||||
sourceId: style.generateId(),
|
||||
source: this.defaultSource('tilejson'),
|
||||
source: this.defaultSource('tilejson_vector'),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,51 +106,59 @@ class AddSource extends React.Component {
|
||||
type: 'geojson',
|
||||
data: source.data || 'http://localhost:3000/geojson.json'
|
||||
}
|
||||
case 'tilejson': return {
|
||||
case 'tilejson_vector': return {
|
||||
type: 'vector',
|
||||
url: source.url || 'http://localhost:3000/tilejson.json'
|
||||
}
|
||||
case 'tilexyz': return {
|
||||
case 'tilexyz_vector': return {
|
||||
type: 'vector',
|
||||
tiles: source.tiles || ['http://localhost:3000/{x}/{y}/{z}.pbf'],
|
||||
minZoom: source.minZoom || 0,
|
||||
maxZoom: source.maxZoom || 14
|
||||
minZoom: source.minzoom || 0,
|
||||
maxZoom: source.maxzoom || 14
|
||||
}
|
||||
case 'tilejson_raster': return {
|
||||
type: 'raster',
|
||||
url: source.url || 'http://localhost:3000/tilejson.json'
|
||||
}
|
||||
case 'tilexyz_raster': return {
|
||||
type: 'raster',
|
||||
tiles: source.tiles || ['http://localhost:3000/{x}/{y}/{z}.pbf'],
|
||||
minzoom: source.minzoom || 0,
|
||||
maxzoom: source.maxzoom || 14
|
||||
}
|
||||
default: return {}
|
||||
}
|
||||
}
|
||||
|
||||
onSourceChange(source) {
|
||||
this.setState({
|
||||
source: source
|
||||
})
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div>
|
||||
<InputBlock label={"Source ID"}>
|
||||
return <div className="maputnik-add-source">
|
||||
<InputBlock label={"Source ID"} doc={"Unique ID that identifies the source and is used in the layer to reference the source."}>
|
||||
<StringInput
|
||||
value={this.state.sourceId}
|
||||
onChange={v => this.setState({ sourceId: v})}
|
||||
/>
|
||||
</InputBlock>
|
||||
<InputBlock label={"Source Type"}>
|
||||
<InputBlock label={"Source Type"} doc={GlSpec.source_tile.type.doc}>
|
||||
<SelectInput
|
||||
options={[
|
||||
['geojson', 'GeoJSON'],
|
||||
['tilejson', 'Vector (TileJSON URL)'],
|
||||
['tilexyz', 'Vector (XYZ URLs)'],
|
||||
['tilejson_vector', 'Vector (TileJSON URL)'],
|
||||
['tilexyz_vector', 'Vector (XYZ URLs)'],
|
||||
['tilejson_raster', 'Raster (TileJSON URL)'],
|
||||
['tilexyz_raster', 'Raster (XYZ URL)'],
|
||||
]}
|
||||
onChange={mode => this.setState({mode: mode, source: this.defaultSource(mode)})}
|
||||
value={this.state.mode}
|
||||
/>
|
||||
</InputBlock>
|
||||
<SourceTypeEditor
|
||||
onChange={this.onSourceChange.bind(this)}
|
||||
onChange={src => this.setState({ source: src })}
|
||||
mode={this.state.mode}
|
||||
source={this.state.source}
|
||||
/>
|
||||
<Button onClick={() => this.props.onSourceAdd(this.state.sourceId, this.state.source)}>
|
||||
<Button
|
||||
className="maputnik-add-source-button"
|
||||
onClick={() => this.props.onAdd(this.state.sourceId, this.state.source)}>
|
||||
Add Source
|
||||
</Button>
|
||||
</div>
|
||||
@@ -189,31 +173,6 @@ class SourcesModal extends React.Component {
|
||||
onStyleChanged: React.PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
onSourceAdd(sourceId, source) {
|
||||
const changedSources = {
|
||||
...this.props.mapStyle.sources,
|
||||
[sourceId]: source
|
||||
}
|
||||
|
||||
const changedStyle = {
|
||||
...this.props.mapStyle,
|
||||
sources: changedSources
|
||||
}
|
||||
|
||||
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']
|
||||
@@ -221,24 +180,26 @@ class SourcesModal extends React.Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const activeSources = Object.keys(this.props.mapStyle.sources).map(sourceId => {
|
||||
const source = this.props.mapStyle.sources[sourceId]
|
||||
const mapStyle = this.props.mapStyle
|
||||
const activeSources = Object.keys(mapStyle.sources).map(sourceId => {
|
||||
const source = mapStyle.sources[sourceId]
|
||||
return <ActiveSourceTypeEditor
|
||||
key={sourceId}
|
||||
sourceId={sourceId}
|
||||
source={source}
|
||||
onSourceDelete={this.deleteSource.bind(this)}
|
||||
onChange={src => this.props.onStyleChanged(changeSource(mapStyle, sourceId, src))}
|
||||
onDelete={() => this.props.onStyleChanged(deleteSource(mapStyle, sourceId))}
|
||||
/>
|
||||
})
|
||||
|
||||
const tilesetOptions = Object.keys(publicSources).filter(sourceId => !(sourceId in this.props.mapStyle.sources)).map(sourceId => {
|
||||
const tilesetOptions = Object.keys(publicSources).filter(sourceId => !(sourceId in mapStyle.sources)).map(sourceId => {
|
||||
const source = publicSources[sourceId]
|
||||
return <PublicSource
|
||||
key={sourceId}
|
||||
id={sourceId}
|
||||
type={source.type}
|
||||
title={source.title}
|
||||
onSelect={() => this.onSourceAdd(sourceId, this.stripTitle(source))}
|
||||
onSelect={() => this.props.onStyleChanged(addSource(mapStyle, sourceId, this.stripTitle(source)))}
|
||||
/>
|
||||
})
|
||||
|
||||
@@ -248,21 +209,27 @@ class SourcesModal extends React.Component {
|
||||
onOpenToggle={this.props.onOpenToggle}
|
||||
title={'Sources'}
|
||||
>
|
||||
<Heading level={4}>Active Sources</Heading>
|
||||
{activeSources}
|
||||
|
||||
<Heading level={4}>Add New Source</Heading>
|
||||
<div style={{maxWidth: 300}}>
|
||||
<p style={{color: colors.lowgray, fontSize: fontSizes[5]}}>Add a new source to your style. You can only choose the source type and id at creation time!</p>
|
||||
<AddSource onSourceAdd={this.onSourceAdd.bind(this)} />
|
||||
<div className="maputnik-modal-section">
|
||||
<h4>Active Sources</h4>
|
||||
{activeSources}
|
||||
</div>
|
||||
|
||||
<Heading level={4}>Choose Public Source</Heading>
|
||||
<Paragraph>
|
||||
Add one of the publicly availble sources to your style.
|
||||
</Paragraph>
|
||||
<div style={{maxwidth: 500}}>
|
||||
{tilesetOptions}
|
||||
<div className="maputnik-modal-section">
|
||||
<h4>Choose Public Source</h4>
|
||||
<p>
|
||||
Add one of the publicly availble sources to your style.
|
||||
</p>
|
||||
<div style={{maxwidth: 500}}>
|
||||
{tilesetOptions}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="maputnik-modal-section">
|
||||
<h4>Add New Source</h4>
|
||||
<p>Add a new source to your style. You can only choose the source type and id at creation time!</p>
|
||||
<AddSource
|
||||
onAdd={(sourceId, source) => this.props.onStyleChanged(addSource(mapStyle, sourceId, source))}
|
||||
/>
|
||||
</div>
|
||||
</Modal>
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from 'react'
|
||||
import GlSpec from 'mapbox-gl-style-spec/reference/latest.js'
|
||||
import InputBlock from '../inputs/InputBlock'
|
||||
import StringInput from '../inputs/StringInput'
|
||||
import NumberInput from '../inputs/NumberInput'
|
||||
@@ -6,11 +7,11 @@ import NumberInput from '../inputs/NumberInput'
|
||||
class TileJSONSourceEditor extends React.Component {
|
||||
static propTypes = {
|
||||
source: React.PropTypes.object.isRequired,
|
||||
onChange: React.PropTypes.func,
|
||||
onChange: React.PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
render() {
|
||||
return <InputBlock label={"TileJSON URL"}>
|
||||
return <InputBlock label={"TileJSON URL"} doc={GlSpec.source_tile.url.doc}>
|
||||
<StringInput
|
||||
value={this.props.source.url}
|
||||
onChange={url => this.props.onChange({
|
||||
@@ -25,14 +26,14 @@ class TileJSONSourceEditor extends React.Component {
|
||||
class TileURLSourceEditor extends React.Component {
|
||||
static propTypes = {
|
||||
source: React.PropTypes.object.isRequired,
|
||||
onChange: React.PropTypes.func,
|
||||
onChange: React.PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
renderTileUrls() {
|
||||
const prefix = ['1st', '2nd', '3rd', '4th', '5th', '6th', '7th']
|
||||
const tiles = this.props.source.tiles || []
|
||||
return tiles.map((tileUrl, tileIndex) => {
|
||||
return <InputBlock key={tileIndex} label={prefix[tileIndex] + " Tile URL"}>
|
||||
return <InputBlock key={tileIndex} label={prefix[tileIndex] + " Tile URL"} doc={GlSpec.source_tile.tiles.doc}>
|
||||
<StringInput
|
||||
value={tileUrl}
|
||||
/>
|
||||
@@ -43,21 +44,21 @@ class TileURLSourceEditor extends React.Component {
|
||||
render() {
|
||||
return <div>
|
||||
{this.renderTileUrls()}
|
||||
<InputBlock label={"Min Zoom"}>
|
||||
<InputBlock label={"Min Zoom"} doc={GlSpec.source_tile.minzoom.doc}>
|
||||
<NumberInput
|
||||
value={this.props.source.minZoom}
|
||||
onChange={minZoom => this.props.onChange({
|
||||
value={this.props.source.minzoom || 0}
|
||||
onChange={minzoom => this.props.onChange({
|
||||
...this.props.source,
|
||||
minZoom: minZoom
|
||||
minzoom: minzoom
|
||||
})}
|
||||
/>
|
||||
</InputBlock>
|
||||
<InputBlock label={"Max Zoom"}>
|
||||
<InputBlock label={"Max Zoom"} doc={GlSpec.source_tile.maxzoom.doc}>
|
||||
<NumberInput
|
||||
value={this.props.source.maxZoom}
|
||||
onChange={maxZoom => this.props.onChange({
|
||||
value={this.props.source.maxzoom || 22}
|
||||
onChange={maxzoom => this.props.onChange({
|
||||
...this.props.source,
|
||||
maxZoom: maxZoom
|
||||
maxzoom: maxzoom
|
||||
})}
|
||||
/>
|
||||
</InputBlock>
|
||||
@@ -69,11 +70,11 @@ class TileURLSourceEditor extends React.Component {
|
||||
class GeoJSONSourceEditor extends React.Component {
|
||||
static propTypes = {
|
||||
source: React.PropTypes.object.isRequired,
|
||||
onChange: React.PropTypes.func,
|
||||
onChange: React.PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
render() {
|
||||
return <InputBlock label={"GeoJSON Data"}>
|
||||
return <InputBlock label={"GeoJSON Data"} doc={GlSpec.source_geojson.data.doc}>
|
||||
<StringInput
|
||||
value={this.props.source.data}
|
||||
onChange={data => this.props.onChange({
|
||||
@@ -99,8 +100,10 @@ class SourceTypeEditor extends React.Component {
|
||||
}
|
||||
switch(this.props.mode) {
|
||||
case 'geojson': return <GeoJSONSourceEditor {...commonProps} />
|
||||
case 'tilejson': return <TileJSONSourceEditor {...commonProps} />
|
||||
case 'tilexyz': return <TileURLSourceEditor {...commonProps} />
|
||||
case 'tilejson_vector': return <TileJSONSourceEditor {...commonProps} />
|
||||
case 'tilexyz_vector': return <TileURLSourceEditor {...commonProps} />
|
||||
case 'tilejson_raster': return <TileJSONSourceEditor {...commonProps} />
|
||||
case 'tilexyz_raster': return <TileURLSourceEditor {...commonProps} />
|
||||
default: return null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
const baseColors = {
|
||||
black: '#1c1f24',
|
||||
gray: '#26282e',
|
||||
midgray: '#36383e',
|
||||
lowgray: '#8e8e8e',
|
||||
|
||||
white: '#fff',
|
||||
blue: '#00d9f7',
|
||||
green: '#B4C7AD',
|
||||
orange: '#fb3',
|
||||
red: '#cf4a4a',
|
||||
}
|
||||
|
||||
const themeColors = {
|
||||
primary: baseColors.gray,
|
||||
secondary: baseColors.midgray,
|
||||
default: baseColors.gray,
|
||||
info: baseColors.blue,
|
||||
success: baseColors.green,
|
||||
warning: baseColors.orange,
|
||||
error: baseColors.red
|
||||
}
|
||||
|
||||
const colors = {
|
||||
...baseColors,
|
||||
...themeColors
|
||||
}
|
||||
|
||||
export default colors
|
||||
15
src/config/empty-style.json
Normal file
15
src/config/empty-style.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"version": 8,
|
||||
"name": "Empty Style",
|
||||
"metadata": {
|
||||
"mapbox:autocomposite": false,
|
||||
"mapbox:type": "template",
|
||||
"maputnik:renderer": "mbgljs",
|
||||
"openmaptiles:version": "3.x"
|
||||
},
|
||||
"sources": { },
|
||||
"glyphs": "https://demo.tileserver.org/fonts/{fontstack}/{range}.pbf",
|
||||
"sprites": "https://demo.tileserver.org/fonts/{fontstack}/{range}.pbf",
|
||||
"layers": [],
|
||||
"id": "empty-style"
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
import colors from './colors'
|
||||
import { margins, fontSizes } from './scales'
|
||||
|
||||
const base = {
|
||||
display: 'inline-block',
|
||||
boxSizing: 'border-box',
|
||||
fontSize: fontSizes[5],
|
||||
lineHeight: 2,
|
||||
paddingLeft: 5,
|
||||
paddingRight: 5,
|
||||
}
|
||||
|
||||
const label = {
|
||||
...base,
|
||||
padding: null,
|
||||
color: colors.lowgray,
|
||||
userSelect: 'none',
|
||||
}
|
||||
|
||||
const property = {
|
||||
display: 'block',
|
||||
margin: margins[2],
|
||||
}
|
||||
|
||||
const input = {
|
||||
...base,
|
||||
border: 'none',
|
||||
backgroundColor: colors.gray,
|
||||
color: colors.lowgray,
|
||||
}
|
||||
|
||||
const checkbox = {
|
||||
...base,
|
||||
border: '1px solid rgb(36, 36, 36)',
|
||||
backgroundColor: colors.gray,
|
||||
color: colors.lowgray,
|
||||
}
|
||||
|
||||
const select = {
|
||||
...input,
|
||||
height: '2.15em',
|
||||
}
|
||||
|
||||
export default {
|
||||
base,
|
||||
label,
|
||||
select,
|
||||
input,
|
||||
property,
|
||||
checkbox,
|
||||
}
|
||||
@@ -2,15 +2,7 @@
|
||||
"line": {
|
||||
"groups": [
|
||||
{
|
||||
"title": "Settings",
|
||||
"type": "settings"
|
||||
},
|
||||
{
|
||||
"title": "Source",
|
||||
"type": "source"
|
||||
},
|
||||
{
|
||||
"title": "Paint",
|
||||
"title": "Paint properties",
|
||||
"type": "properties",
|
||||
"fields": [
|
||||
"line-opacity",
|
||||
@@ -18,66 +10,42 @@
|
||||
"line-width",
|
||||
"line-offset",
|
||||
"line-blur",
|
||||
"line-pattern"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Secondary",
|
||||
"type": "properties",
|
||||
"fields": [
|
||||
"line-dasharray",
|
||||
"line-pattern",
|
||||
"line-translate",
|
||||
"line-translate-anchor",
|
||||
"line-cap",
|
||||
"line-join",
|
||||
"line-miter-limit",
|
||||
"line-round-limit",
|
||||
"line-dasharray",
|
||||
"line-gap-width"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "JSON",
|
||||
"type": "jsoneditor"
|
||||
"title": "Layout properties",
|
||||
"type": "properties",
|
||||
"fields": [
|
||||
"line-cap",
|
||||
"line-join",
|
||||
"line-miter-limit",
|
||||
"line-round-limit"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"background": {
|
||||
"groups": [
|
||||
{
|
||||
"title": "Settings",
|
||||
"type": "settings"
|
||||
},
|
||||
{
|
||||
"title": "Source",
|
||||
"type": "source"
|
||||
},
|
||||
{
|
||||
"title": "Basic",
|
||||
"title": "Paint properties",
|
||||
"type": "properties",
|
||||
"fields": [
|
||||
"background-color",
|
||||
"background-pattern",
|
||||
"background-opacity"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "JSON",
|
||||
"type": "jsoneditor"
|
||||
}
|
||||
]
|
||||
},
|
||||
"fill": {
|
||||
"groups": [
|
||||
{
|
||||
"title": "Settings",
|
||||
"type": "settings"
|
||||
},
|
||||
{
|
||||
"title": "Source",
|
||||
"type": "source"
|
||||
},
|
||||
{
|
||||
"title": "Basic",
|
||||
"title": "Paint properties",
|
||||
"type": "properties",
|
||||
"fields": [
|
||||
"fill-opacity",
|
||||
@@ -88,25 +56,13 @@
|
||||
"fill-translate",
|
||||
"fill-translate-anchor"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "JSON",
|
||||
"type": "jsoneditor"
|
||||
}
|
||||
]
|
||||
},
|
||||
"fill-extrusion": {
|
||||
"groups": [
|
||||
{
|
||||
"title": "Settings",
|
||||
"type": "settings"
|
||||
},
|
||||
{
|
||||
"title": "Source",
|
||||
"type": "source"
|
||||
},
|
||||
{
|
||||
"title": "Basic",
|
||||
"title": "Paint properties",
|
||||
"type": "properties",
|
||||
"fields": [
|
||||
"fill-extrusion-opacity",
|
||||
@@ -117,99 +73,51 @@
|
||||
"fill-extrusion-height",
|
||||
"fill-extrusion-base"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "JSON",
|
||||
"type": "jsoneditor"
|
||||
}
|
||||
]
|
||||
},
|
||||
"circle": {
|
||||
"groups": [
|
||||
{
|
||||
"title": "Settings",
|
||||
"type": "settings"
|
||||
},
|
||||
{
|
||||
"title": "Source",
|
||||
"type": "source"
|
||||
},
|
||||
{
|
||||
"title": "Basic",
|
||||
"title": "Paint properties",
|
||||
"type": "properties",
|
||||
"fields": [
|
||||
"circle-color",
|
||||
"circle-opacity",
|
||||
"circle-stroke-color",
|
||||
"circle-stroke-opacity",
|
||||
"circle-blur"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Scale",
|
||||
"type": "properties",
|
||||
"fields": [
|
||||
"circle-blur",
|
||||
"circle-radius",
|
||||
"circle-stroke-width",
|
||||
"circle-pitch-scale"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Position",
|
||||
"type": "properties",
|
||||
"fields": [
|
||||
"circle-pitch-scale",
|
||||
"circle-translate",
|
||||
"circle-translate-anchor"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "JSON",
|
||||
"type": "jsoneditor"
|
||||
}
|
||||
]
|
||||
},
|
||||
"symbol": {
|
||||
"groups": [
|
||||
{
|
||||
"title": "Settings",
|
||||
"type": "settings"
|
||||
},
|
||||
{
|
||||
"title": "Source",
|
||||
"type": "source"
|
||||
},
|
||||
{
|
||||
"title": "Basic",
|
||||
"type": "properties",
|
||||
"fields": [
|
||||
"text-field",
|
||||
"text-font",
|
||||
"text-color",
|
||||
"text-size",
|
||||
"text-line-height",
|
||||
"text-halo-color",
|
||||
"text-halo-width",
|
||||
"text-halo-blur"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Placement",
|
||||
"title": "General layout properties",
|
||||
"type": "properties",
|
||||
"fields": [
|
||||
"symbol-placement",
|
||||
"symbol-spacing",
|
||||
"text-padding",
|
||||
"symbol-avoid-edges",
|
||||
"text-allow-overlap",
|
||||
"text-ignore-placement",
|
||||
"text-translate",
|
||||
"text-translate-anchor"
|
||||
"symbol-avoid-edges"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Text",
|
||||
"title": "Text layout properties",
|
||||
"type": "properties",
|
||||
"fields": [
|
||||
"text-field",
|
||||
"text-font",
|
||||
"text-size",
|
||||
"text-line-height",
|
||||
"text-padding",
|
||||
"text-allow-overlap",
|
||||
"text-ignore-placement",
|
||||
"text-pitch-alignment",
|
||||
"text-rotation-alignment",
|
||||
"text-max-width",
|
||||
@@ -225,7 +133,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Icon",
|
||||
"title": "Icon layout properties",
|
||||
"type": "properties",
|
||||
"fields": [
|
||||
"icon-allow-overlap",
|
||||
@@ -239,12 +147,51 @@
|
||||
"icon-rotate",
|
||||
"icon-padding",
|
||||
"icon-keep-upright",
|
||||
"icon-offset"
|
||||
"icon-offset"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "JSON",
|
||||
"type": "jsoneditor"
|
||||
"title": "Text paint properties",
|
||||
"type": "properties",
|
||||
"fields": [
|
||||
"text-color",
|
||||
"text-opacity",
|
||||
"text-halo-color",
|
||||
"text-halo-width",
|
||||
"text-halo-blur",
|
||||
"text-translate",
|
||||
"text-translate-anchor"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Icon paint properties",
|
||||
"type": "properties",
|
||||
"fields": [
|
||||
"icon-color",
|
||||
"icon-opacity",
|
||||
"icon-halo-color",
|
||||
"icon-halo-width",
|
||||
"icon-halo-blur",
|
||||
"icon-translate",
|
||||
"icon-translate-anchor"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"raster": {
|
||||
"groups": [
|
||||
{
|
||||
"title": "Paint properties",
|
||||
"type": "properties",
|
||||
"fields": [
|
||||
"raster-opacity",
|
||||
"raster-hue-rotate",
|
||||
"raster-brightness-min",
|
||||
"raster-brightness-max",
|
||||
"raster-saturation",
|
||||
"raster-contrast",
|
||||
"raster-fade-duration"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
export const margins = [3, 5, 10, 30, 40]
|
||||
export const fontSizes = [24, 20, 18, 16, 14, 12]
|
||||
@@ -3,30 +3,36 @@
|
||||
"id": "klokantech-basic",
|
||||
"title": "Klokantech Basic",
|
||||
"url": "https://rawgit.com/openmaptiles/klokantech-basic-gl-style/gh-pages/style-cdn.json",
|
||||
"thumbnail": "https://camo.githubusercontent.com/5cf548fdb9fc606f4a452d14fd2a7a959155fd40/68747470733a2f2f6170692e6d6170626f782e636f6d2f7374796c65732f76312f6d6f7267656e6b61666665652f63697578757465726630316135326971716f366b6f6c776b312f7374617469632f382e3534303538372c34372e3337303535352c31342e30382c302e30302c302e30302f363030783430303f6163636573735f746f6b656e3d706b2e65794a31496a6f69625739795a3256756132466d5a6d566c4969776959534936496a497a636d4e304e6c6b6966512e304c52544e6743632d656e76743964354d7a52373577"
|
||||
"thumbnail": "https://camo.githubusercontent.com/08dcb3dd384c6083b02e6692c939d68c4114eb33/687474703a2f2f64656d6f2e74696c657365727665722e6f72672f7374796c65732f6b6c6f6b616e746563682d62617369632f7374617469632f382e3631393138342c34372e3333363230332c31302e30372f363030783430304032782e706e67"
|
||||
},
|
||||
{
|
||||
"id": "dark-matter",
|
||||
"title": "Dark Matter",
|
||||
"url": "https://rawgit.com/openmaptiles/dark-matter-gl-style/gh-pages/style-cdn.json",
|
||||
"thumbnail": "https://camo.githubusercontent.com/b73c515d633d2be7368e8e29e3c23e14117fd21b/68747470733a2f2f6170692e6d6170626f782e636f6d2f7374796c65732f76312f6d6f7267656e6b61666665652f6369757878356e37683031396c326870626e396c6970726d6e2f7374617469632f382e3631393138342c34372e3333363230332c392e30372c302e30302c302e30302f363030783430303f6163636573735f746f6b656e3d706b2e65794a31496a6f69625739795a3256756132466d5a6d566c4969776959534936496a497a636d4e304e6c6b6966512e304c52544e6743632d656e76743964354d7a52373577"
|
||||
"thumbnail": "https://camo.githubusercontent.com/258db708523e523782addeecdcc8697368a24df9/687474703a2f2f64656d6f2e74696c657365727665722e6f72672f7374796c65732f6461726b2d6d61747465722f7374617469632f382e3534303538372c34372e3337303535352c31352e30382f363030783430304032782e706e67"
|
||||
},
|
||||
{
|
||||
"id": "positron",
|
||||
"title": "Positron",
|
||||
"url": "https://rawgit.com/openmaptiles/positron-gl-style/gh-pages/style-cdn.json",
|
||||
"thumbnail": "https://camo.githubusercontent.com/0dd866e3fa7b21ada87da69082eac6801e16ec99/68747470733a2f2f6170692e6d6170626f782e636f6d2f7374796c65732f76312f6d6f7267656e6b61666665652f63697578756e37736530313976326a6c387162326a743374662f7374617469632f382e3631393138342c34372e3333363230332c392e30372c302e30302c302e30302f363030783430303f6163636573735f746f6b656e3d706b2e65794a31496a6f69625739795a3256756132466d5a6d566c4969776959534936496a497a636d4e304e6c6b6966512e304c52544e6743632d656e76743964354d7a52373577"
|
||||
"thumbnail": "https://camo.githubusercontent.com/56df86562b6c36b7cc44ee6e8b91eb4d8e593b66/687474703a2f2f64656d6f2e74696c657365727665722e6f72672f7374796c65732f706f736974726f6e2f7374617469632f31302e3938373235382c34362e3435333135302c342e30322f363030783430304032782e706e67"
|
||||
},
|
||||
{
|
||||
"id": "osm-bright",
|
||||
"title": "OSM Bright",
|
||||
"url": "https://rawgit.com/openmaptiles/osm-bright-gl-style/gh-pages/style-cdn.json",
|
||||
"thumbnail": "https://camo.githubusercontent.com/a15e23ab59202c56502e57cde963cb7772ed3bb1/68747470733a2f2f6170692e6d6170626f782e636f6d2f7374796c65732f76312f6f70656e6d617074696c65732f63697736637a7a326e30303234326b6d673668773230626f782f7374617469632f382e3534303538372c34372e3337303535352c31342e30382c302e30302c302e30302f363030783430303f6163636573735f746f6b656e3d706b2e65794a31496a6f696233426c626d3168634852706247567a4969776959534936496d4e70646e593365544a785a7a41774d474d796233427064574a6d616a63784e7a636966512e685031427863786c644968616b4d6350534a4c513151"
|
||||
"thumbnail": "https://camo.githubusercontent.com/0fdf9922c6b632f903e47b3dfbcfb65e62b25046/687474703a2f2f64656d6f2e74696c657365727665722e6f72672f7374796c65732f6f736d2d6272696768742f7374617469632f382e3234333936372c34362e3931363331352c372e32312f363030783430304032782e706e67"
|
||||
},
|
||||
{
|
||||
"id": "fiord-color",
|
||||
"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": "osm-liberty",
|
||||
"title": "OSM Liberty",
|
||||
"url": "https://rawgit.com/lukasmartinelli/osm-liberty/gh-pages/style.json",
|
||||
"thumbnail": "https://cdn.rawgit.com/lukasmartinelli/osm-liberty/gh-pages/thumbnail.png"
|
||||
},
|
||||
{
|
||||
"id": "empty-style",
|
||||
"title": "Empty Style",
|
||||
"url": "https://rawgit.com/maputnik/editor/master/src/config/empty-style.json",
|
||||
"thumbnail": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAYAAAAECAQAAAAHDYbIAAAAEUlEQVR42mP8/58BDhiJ4wAA974H/U5Xe1oAAAAASUVORK5CYII="
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
import colors from './colors'
|
||||
import { margins, fontSizes } from './scales'
|
||||
|
||||
const dark = {
|
||||
color: colors.white,
|
||||
fontFamily: 'Roboto, sans-serif',
|
||||
}
|
||||
|
||||
export default dark
|
||||
@@ -4,6 +4,11 @@
|
||||
"url": "mapbox://mapbox.mapbox-streets-v7",
|
||||
"title": "Mapbox Streets"
|
||||
},
|
||||
"openmaptiles": {
|
||||
"type": "vector",
|
||||
"url": "https://free.tilehosting.com/data/v3.json?key=25ItXg7aI5wurYDtttD",
|
||||
"title": "OpenMapTiles"
|
||||
},
|
||||
"tilezen": {
|
||||
"type": "vector",
|
||||
"tiles": [
|
||||
@@ -12,15 +17,5 @@
|
||||
"minZoom": 0,
|
||||
"maxZoom": 15,
|
||||
"title": "Mapzen Vector Tile Service"
|
||||
},
|
||||
"openmaptiles": {
|
||||
"type": "vector",
|
||||
"url": "https://free.tilehosting.com/data/v3.json?key=25ItXg7aI5wurYDtttD",
|
||||
"title": "OpenMapTiles CDN"
|
||||
},
|
||||
"naturalearth-airports": {
|
||||
"type": "geojson",
|
||||
"data": "https://d2ad6b4ur7yvpq.cloudfront.net/naturalearth-3.3.0/ne_10m_airports.geojson",
|
||||
"title": "NaturalEarth Airports GeoJSON"
|
||||
}
|
||||
}
|
||||
|
||||
4
src/config/tokens.json
Normal file
4
src/config/tokens.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"mapbox": "pk.eyJ1IjoibW9yZ2Vua2FmZmVlIiwiYSI6ImNpeHJmNXNmZTAwNHIycXBid2NqdTJibjMifQ.Dv1-GDpTWi0NP6xW9Fct1w",
|
||||
"openmaptiles": "Og58UhhtiiTaLVlPtPgs"
|
||||
}
|
||||
BIN
src/fonts/Roboto-Medium.ttf
Normal file
BIN
src/fonts/Roboto-Medium.ttf
Normal file
Binary file not shown.
@@ -1,34 +0,0 @@
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
src: url('./fonts/Roboto-Regular.ttf') format('truetype');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
html {
|
||||
background-color: rgb(28, 31, 36);
|
||||
}
|
||||
|
||||
.chrome-picker {
|
||||
background-color: #1c1f24 !important;
|
||||
font-family: inherit !important;
|
||||
}
|
||||
|
||||
.chrome-picker input {
|
||||
background-color: rgb(38, 40, 46) !important;
|
||||
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;
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
import './favicon.ico'
|
||||
import './index.css'
|
||||
import './styles/index.scss'
|
||||
import App from './components/App';
|
||||
|
||||
ReactDOM.render(<App/>, document.querySelector("#app"));
|
||||
|
||||
@@ -1,40 +1,66 @@
|
||||
import request from 'request'
|
||||
import style from './style.js'
|
||||
import ReconnectingWebSocket from 'reconnecting-websocket'
|
||||
|
||||
const host = 'localhost'
|
||||
const port = '8000'
|
||||
const localUrl = `http://${host}:${port}`
|
||||
const websocketUrl = `ws://${host}:${port}/ws`
|
||||
|
||||
|
||||
export class ApiStyleStore {
|
||||
supported(cb) {
|
||||
request('http://localhost:8000/styles', (error, response, body) => {
|
||||
cb(error === undefined)
|
||||
constructor(opts) {
|
||||
this.onLocalStyleChange = opts.onLocalStyleChange || (() => {})
|
||||
}
|
||||
|
||||
init(cb) {
|
||||
request(localUrl + '/styles', (error, response, body) => {
|
||||
if (!error && body && response.statusCode == 200) {
|
||||
const styleIds = JSON.parse(body)
|
||||
this.latestStyleId = styleIds[0]
|
||||
this.notifyLocalChanges()
|
||||
cb(null)
|
||||
} else {
|
||||
cb(new Error('Can not connect to style API'))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
notifyLocalChanges() {
|
||||
const connection = new ReconnectingWebSocket(websocketUrl)
|
||||
connection.onmessage = e => {
|
||||
if(!e.data) return
|
||||
console.log('Received style update from API')
|
||||
let parsedStyle = style.emptyStyle
|
||||
try {
|
||||
parsedStyle = JSON.parse(e.data)
|
||||
} catch(err) {
|
||||
console.error(err)
|
||||
}
|
||||
const updatedStyle = style.ensureStyleValidity(parsedStyle)
|
||||
this.onLocalStyleChange(updatedStyle)
|
||||
}
|
||||
}
|
||||
|
||||
latestStyle(cb) {
|
||||
if(this.latestStyleId) {
|
||||
request('http://localhost:8000/styles/' + this.latestStyleId, (error, response, body) => {
|
||||
cb(JSON.parse(body))
|
||||
request(localUrl + '/styles/' + this.latestStyleId, (error, response, body) => {
|
||||
cb(style.ensureStyleValidity(JSON.parse(body)))
|
||||
})
|
||||
} else {
|
||||
request('http://localhost:8000/styles', (error, response, body) => {
|
||||
if (!error && response.statusCode == 200) {
|
||||
const styleIds = JSON.parse(body);
|
||||
this.latestStyleId = styleIds[0];
|
||||
request('http://localhost:8000/styles/' + this.latestStyleId, (error, response, body) => {
|
||||
cb(style.fromJSON(JSON.parse(body)))
|
||||
})
|
||||
}
|
||||
})
|
||||
throw new Error('No latest style available. You need to init the api backend first.')
|
||||
}
|
||||
}
|
||||
|
||||
// Save current style replacing previous version
|
||||
save(mapStyle) {
|
||||
const id = mapStyle.get('id')
|
||||
const id = mapStyle.id
|
||||
request.put({
|
||||
url: 'http://localhost:8000/styles/' + id,
|
||||
url: localUrl + '/styles/' + id,
|
||||
json: true,
|
||||
body: style.toJSON(mapStyle)
|
||||
body: mapStyle
|
||||
}, (error, response, body) => {
|
||||
console.log('Saved style');
|
||||
if(error) console.error(error)
|
||||
})
|
||||
return mapStyle
|
||||
}
|
||||
|
||||
6
src/libs/filterops.js
Normal file
6
src/libs/filterops.js
Normal file
@@ -0,0 +1,6 @@
|
||||
import GlSpec from 'mapbox-gl-style-spec/reference/latest.js'
|
||||
export const combiningFilterOps = ['all', 'any', 'none']
|
||||
export const setFilterOps = ['in', '!in']
|
||||
export const otherFilterOps = Object
|
||||
.keys(GlSpec.filter_operator.values)
|
||||
.filter(op => combiningFilterOps.indexOf(op) < 0)
|
||||
36
src/libs/highlight.js
Normal file
36
src/libs/highlight.js
Normal file
@@ -0,0 +1,36 @@
|
||||
import randomColor from 'randomcolor'
|
||||
import Color from 'color'
|
||||
|
||||
import stylegen from 'mapbox-gl-inspect/lib/stylegen'
|
||||
import colors from 'mapbox-gl-inspect/lib/colors'
|
||||
|
||||
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 = colors.brightColor(layer.id, 1)
|
||||
const layers = []
|
||||
|
||||
if(layer.type === "fill" || layer.type === 'fill-extrusion') {
|
||||
return changeLayer(stylegen.polygonLayer(color, color, layer.source, layer['source-layer']))
|
||||
}
|
||||
|
||||
if(layer.type === "symbol" || layer.type === 'circle') {
|
||||
return changeLayer(stylegen.circleLayer(color, layer.source, layer['source-layer']))
|
||||
}
|
||||
|
||||
if(layer.type === 'line') {
|
||||
return changeLayer(stylegen.lineLayer(color, layer.source, layer['source-layer']))
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
32
src/libs/metadata.js
Normal file
32
src/libs/metadata.js
Normal file
@@ -0,0 +1,32 @@
|
||||
import request from 'request'
|
||||
|
||||
function loadJSON(url, defaultValue, cb) {
|
||||
request({
|
||||
url: url,
|
||||
withCredentials: false,
|
||||
}, (error, response, body) => {
|
||||
if (!error && body && response.statusCode == 200) {
|
||||
try {
|
||||
cb(JSON.parse(body))
|
||||
} catch(err) {
|
||||
console.error(err)
|
||||
cb(defaultValue)
|
||||
}
|
||||
} else {
|
||||
console.warn('Can not metadata for ' + url)
|
||||
cb(defaultValue)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function downloadGlyphsMetadata(urlTemplate, cb) {
|
||||
if(!urlTemplate) return cb([])
|
||||
const url = urlTemplate.replace('{fontstack}/{range}.pbf', 'fontstacks.json')
|
||||
loadJSON(url, [], cb)
|
||||
}
|
||||
|
||||
export function downloadSpriteMetadata(baseUrl, cb) {
|
||||
if(!baseUrl) return cb([])
|
||||
const url = baseUrl + '.json'
|
||||
loadJSON(url, {}, glyphs => cb(Object.keys(glyphs)))
|
||||
}
|
||||
27
src/libs/source.js
Normal file
27
src/libs/source.js
Normal file
@@ -0,0 +1,27 @@
|
||||
export function deleteSource(mapStyle, sourceId) {
|
||||
const remainingSources = { ...mapStyle.sources}
|
||||
delete remainingSources[sourceId]
|
||||
const changedStyle = {
|
||||
...mapStyle,
|
||||
sources: remainingSources
|
||||
}
|
||||
return changedStyle
|
||||
}
|
||||
|
||||
|
||||
export function addSource(mapStyle, sourceId, source) {
|
||||
return changeSource(mapStyle, sourceId, source)
|
||||
}
|
||||
|
||||
export function changeSource(mapStyle, sourceId, source) {
|
||||
const changedSources = {
|
||||
...mapStyle.sources,
|
||||
[sourceId]: source
|
||||
}
|
||||
const changedStyle = {
|
||||
...mapStyle,
|
||||
sources: changedSources
|
||||
}
|
||||
return changedStyle
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import spec from 'mapbox-gl-style-spec/reference/latest.min.js'
|
||||
import derefLayers from 'mapbox-gl-style-spec/lib/deref'
|
||||
import tokens from '../config/tokens.json'
|
||||
|
||||
// Empty style is always used if no style could be restored or fetched
|
||||
const emptyStyle = ensureStyleValidity({
|
||||
@@ -19,10 +20,18 @@ function ensureHasId(style) {
|
||||
return style
|
||||
}
|
||||
|
||||
function ensureHasTimestamp(style) {
|
||||
if('created' in style) return style
|
||||
style.created = new Date().toJSON()
|
||||
return style
|
||||
function ensureHasNoInteractive(style) {
|
||||
const changedLayers = style.layers.map(layer => {
|
||||
const changedLayer = { ...layer }
|
||||
delete changedLayer.interactive
|
||||
return changedLayer
|
||||
})
|
||||
|
||||
const nonInteractiveStyle = {
|
||||
...style,
|
||||
layers: changedLayers
|
||||
}
|
||||
return nonInteractiveStyle
|
||||
}
|
||||
|
||||
function ensureHasNoRefs(style) {
|
||||
@@ -34,7 +43,7 @@ function ensureHasNoRefs(style) {
|
||||
}
|
||||
|
||||
function ensureStyleValidity(style) {
|
||||
return ensureHasNoRefs(ensureHasId(ensureHasTimestamp(style)))
|
||||
return ensureHasNoInteractive(ensureHasNoRefs(ensureHasId(style)))
|
||||
}
|
||||
|
||||
function indexOfLayer(layers, layerId) {
|
||||
@@ -46,9 +55,32 @@ function indexOfLayer(layers, layerId) {
|
||||
return null
|
||||
}
|
||||
|
||||
function replaceAccessToken(mapStyle) {
|
||||
const omtSource = mapStyle.sources.openmaptiles
|
||||
if(!omtSource) return mapStyle
|
||||
|
||||
const metadata = mapStyle.metadata || {}
|
||||
const accessToken = metadata['maputnik:openmaptiles_access_token'] || tokens.openmaptiles
|
||||
const changedSources = {
|
||||
...mapStyle.sources,
|
||||
openmaptiles: {
|
||||
...omtSource,
|
||||
url: omtSource.url.replace('{key}', accessToken)
|
||||
}
|
||||
}
|
||||
const changedStyle = {
|
||||
...mapStyle,
|
||||
glyphs: mapStyle.glyphs.replace('{key}', accessToken),
|
||||
sources: changedSources
|
||||
}
|
||||
|
||||
return changedStyle
|
||||
}
|
||||
|
||||
export default {
|
||||
ensureStyleValidity,
|
||||
emptyStyle,
|
||||
indexOfLayer,
|
||||
generateId,
|
||||
replaceAccessToken,
|
||||
}
|
||||
|
||||
@@ -1,141 +0,0 @@
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { colorizeLayers } from './style.js'
|
||||
import style from './style.js'
|
||||
import { loadStyleUrl } from './urlopen'
|
||||
import publicSources from '../config/styles.json'
|
||||
import request from 'request'
|
||||
|
||||
@@ -14,18 +15,7 @@ const defaultStyleUrl = publicSources[0].url
|
||||
|
||||
// Fetch a default style via URL and return it or a fallback style via callback
|
||||
export function loadDefaultStyle(cb) {
|
||||
console.log('Falling back to default style')
|
||||
request({
|
||||
url: defaultStyleUrl,
|
||||
withCredentials: false,
|
||||
}, (error, response, body) => {
|
||||
if (!error && response.statusCode == 200) {
|
||||
cb(style.ensureStyleValidity(JSON.parse(body)))
|
||||
} else {
|
||||
console.warn('Could not fetch default style', styleUrl)
|
||||
cb(style.emptyStyle)
|
||||
}
|
||||
})
|
||||
loadStyleUrl(defaultStyleUrl, cb)
|
||||
}
|
||||
|
||||
// Return style ids and dates of all styles stored in local storage
|
||||
@@ -69,8 +59,8 @@ export class StyleStore {
|
||||
this.mapStyles = loadStoredStyles()
|
||||
}
|
||||
|
||||
supported(cb) {
|
||||
cb(window.localStorage !== undefined)
|
||||
init(cb) {
|
||||
cb(null)
|
||||
}
|
||||
|
||||
// Delete entire style history
|
||||
|
||||
42
src/libs/urlopen.js
Normal file
42
src/libs/urlopen.js
Normal file
@@ -0,0 +1,42 @@
|
||||
import request from 'request'
|
||||
import url from 'url'
|
||||
import style from './style.js'
|
||||
|
||||
export function initialStyleUrl() {
|
||||
const initialUrl = url.parse(window.location.href, true)
|
||||
return (initialUrl.query || {}).style
|
||||
}
|
||||
|
||||
export function loadStyleUrl(styleUrl, cb) {
|
||||
console.log('Loading style', styleUrl)
|
||||
request({
|
||||
url: styleUrl,
|
||||
withCredentials: false,
|
||||
}, (error, response, body) => {
|
||||
if (!error && response.statusCode == 200) {
|
||||
cb(style.ensureStyleValidity(JSON.parse(body)))
|
||||
} else {
|
||||
console.warn('Could not fetch default style', styleUrl)
|
||||
cb(style.emptyStyle)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function loadJSON(url, defaultValue, cb) {
|
||||
request({
|
||||
url: url,
|
||||
withCredentials: false,
|
||||
}, (error, response, body) => {
|
||||
if (!error && body && response.statusCode == 200) {
|
||||
try {
|
||||
cb(JSON.parse(body))
|
||||
} catch(err) {
|
||||
console.error(err)
|
||||
cb(defaultValue)
|
||||
}
|
||||
} else {
|
||||
console.error('Can not load JSON from ' + url)
|
||||
cb(defaultValue)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -49,3 +49,12 @@
|
||||
.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")
|
||||
}
|
||||
|
||||
.mapboxgl-ctrl-inspect {
|
||||
background-image: url('data:image/svg+xml;charset=utf8,<svg%20xmlns="http://www.w3.org/2000/svg"%20fill="#8e8e8e%22%20preserveAspectRatio=%22xMidYMid%20meet%22%20viewBox=%22-10%20-10%2060%2060%22%3E%3Cg%3E%3Cpath%20d=%22m15%2021.6q0-2%201.5-3.5t3.5-1.5%203.5%201.5%201.5%203.5-1.5%203.6-3.5%201.4-3.5-1.4-1.5-3.6z%20m18.4%2011.1l-6.4-6.5q1.4-2.1%201.4-4.6%200-3.4-2.5-5.8t-5.9-2.4-5.9%202.4-2.5%205.8%202.5%205.9%205.9%202.5q2.4%200%204.6-1.4l7.4%207.4q-0.9%200.6-2%200.6h-20q-1.3%200-2.3-0.9t-1.1-2.3l0.1-26.8q0-1.3%201-2.3t2.3-0.9h13.4l10%2010v19.3z%22%3E%3C/path%3E%3C/g%3E%3C/svg%3E');
|
||||
}
|
||||
|
||||
.mapboxgl-ctrl-map {
|
||||
background-image: url('data:image/svg+xml;charset=utf8,<svg%20xmlns="http://www.w3.org/2000/svg"%20fill="#8e8e8e%22%20viewBox=%22-10%20-10%2060%2060%22%20preserveAspectRatio=%22xMidYMid%20meet%22%3E%3Cg%3E%3Cpath%20d=%22m25%2031.640000000000004v-19.766666666666673l-10-3.511666666666663v19.766666666666666z%20m9.140000000000008-26.640000000000004q0.8599999999999923%200%200.8599999999999923%200.8600000000000003v25.156666666666666q0%200.625-0.625%200.783333333333335l-9.375%203.1999999999999993-10-3.5133333333333354-8.906666666666668%203.4383333333333326-0.2333333333333334%200.07833333333333314q-0.8616666666666664%200-0.8616666666666664-0.8599999999999994v-25.156666666666663q0-0.625%200.6233333333333331-0.7833333333333332l9.378333333333334-3.198333333333334%2010%203.5133333333333336%208.905000000000001-3.4383333333333344z%22%3E%3C/path%3E%3C/g%3E%3C/svg%3E');
|
||||
}
|
||||
|
||||
|
||||
74
src/styles/_base.scss
Normal file
74
src/styles/_base.scss
Normal file
@@ -0,0 +1,74 @@
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
src: url('../fonts/Roboto-Regular.ttf') format('truetype');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
src: url('../fonts/Roboto-Medium.ttf') format('truetype');
|
||||
font-weight: bold;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
html {
|
||||
color: $color-white;
|
||||
font-size: $font-size-5;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: inherit;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: $font-size-6;
|
||||
margin-top: $margin-2;
|
||||
margin-bottom: $margin-2;
|
||||
color: $color-lowgray;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: $font-size-2;
|
||||
margin-bottom: $margin-3;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: $font-size-3;
|
||||
margin-bottom: $margin-3;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: $font-size-4;
|
||||
margin-bottom: $margin-3;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: $font-size-5;
|
||||
margin-bottom: $margin-3;
|
||||
}
|
||||
|
||||
input:focus,
|
||||
select:focus {
|
||||
color: $color-white !important;
|
||||
outline: #8e8e8e auto 1px !important;
|
||||
}
|
||||
|
||||
label:hover {
|
||||
color: $color-white;
|
||||
}
|
||||
|
||||
.clearfix {
|
||||
&::after {
|
||||
content: "";
|
||||
display: table;
|
||||
clear: both;
|
||||
}
|
||||
}
|
||||
129
src/styles/_components.scss
Normal file
129
src/styles/_components.scss
Normal file
@@ -0,0 +1,129 @@
|
||||
// MAP
|
||||
.maputnik-map {
|
||||
position: fixed !important;
|
||||
top: 40px;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
height: calc(100% - $toolbar-height);
|
||||
width: 75%;
|
||||
}
|
||||
|
||||
// DOC LABEL
|
||||
.maputnik-doc {
|
||||
&-target {
|
||||
cursor: help;
|
||||
}
|
||||
|
||||
&-wrapper {
|
||||
display: inline-block;
|
||||
box-sizing: border-box;
|
||||
font-size: $font-size-6;
|
||||
line-height: 2;
|
||||
user-select: none;
|
||||
position: relative;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
&-popup {
|
||||
display: none;
|
||||
color: $color-lowgray;
|
||||
background-color: $color-gray;
|
||||
padding: $margin-2;
|
||||
font-size: 10px;
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
left: 0;
|
||||
width: 120px;
|
||||
z-index: 3;
|
||||
}
|
||||
}
|
||||
|
||||
.maputnik-doc-target:hover .maputnik-doc-popup {
|
||||
display: block;
|
||||
}
|
||||
|
||||
// BUTTON
|
||||
.maputnik-button {
|
||||
cursor: pointer;
|
||||
background-color: $color-midgray;
|
||||
color: $color-lowgray;
|
||||
font-size: $font-size-6;
|
||||
padding: $margin-2;
|
||||
user-select: none;
|
||||
border-radius: 2px;
|
||||
box-sizing: border-box;
|
||||
|
||||
&:hover {
|
||||
background-color: lighten($color-midgray, 12);
|
||||
color: $color-white;
|
||||
}
|
||||
}
|
||||
|
||||
.maputnik-big-button {
|
||||
margin-top: $margin-3;
|
||||
display: inline-block;
|
||||
padding: $margin-3;
|
||||
font-size: $font-size-5;
|
||||
}
|
||||
|
||||
.maputnik-icon-button {
|
||||
background-color: transparent;
|
||||
|
||||
&:hover {
|
||||
background-color: transparent;
|
||||
|
||||
label,
|
||||
svg {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
svg {
|
||||
fill: $color-white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// INPUT BLOCK
|
||||
.maputnik-input-block {
|
||||
margin: $margin-3;
|
||||
|
||||
&-label {
|
||||
color: $color-lowgray;
|
||||
display: inline-block;
|
||||
user-select: none;
|
||||
width: 50%;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
&-content {
|
||||
display: inline-block;
|
||||
width: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
.maputnik-action-block {
|
||||
.maputnik-input-block-label {
|
||||
display: inline-block;
|
||||
width: 43%;
|
||||
}
|
||||
|
||||
.maputnik-input-block-action {
|
||||
vertical-align: top;
|
||||
display: inline-block;
|
||||
width: 7%;
|
||||
}
|
||||
}
|
||||
|
||||
// SPACE HELPER
|
||||
.maputnik-space {
|
||||
@include vendor-prefix(flex-grow, 1);
|
||||
}
|
||||
|
||||
// MESSAGE PANEL
|
||||
.maputnik-message-panel {
|
||||
padding: $margin-2;
|
||||
|
||||
&-error {
|
||||
color: $color-red;
|
||||
}
|
||||
}
|
||||
96
src/styles/_filtereditor.scss
Normal file
96
src/styles/_filtereditor.scss
Normal file
@@ -0,0 +1,96 @@
|
||||
.maputnik-filter-editor-wrapper {
|
||||
padding: $margin-3;
|
||||
}
|
||||
|
||||
.maputnik-filter-editor {
|
||||
color: $color-lowgray;
|
||||
}
|
||||
|
||||
.maputnik-filter-editor-property {
|
||||
display: inline-block;
|
||||
width: '22%';
|
||||
}
|
||||
|
||||
.maputnik-filter-editor-operator {
|
||||
display: inline-block;
|
||||
width: 19%;
|
||||
margin-left: 2%;
|
||||
}
|
||||
|
||||
.maputnik-filter-editor-args {
|
||||
display: inline-block;
|
||||
width: 54%;
|
||||
margin-left: 2%;
|
||||
}
|
||||
|
||||
.maputnik-filter-editor-compound-select {
|
||||
margin-bottom: $margin-2;
|
||||
|
||||
.maputnik-doc-wrapper {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.maputnik-select {
|
||||
display: inline-block;
|
||||
width: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
.maputnik-filter-editor-unsupported {
|
||||
color: $color-midgray;
|
||||
}
|
||||
|
||||
.maputnik-filter-editor {
|
||||
@extend .clearfix;
|
||||
}
|
||||
|
||||
.maputnik-add-filter {
|
||||
display: inline-block;
|
||||
float: right;
|
||||
margin-top: $margin-3;
|
||||
}
|
||||
|
||||
.maputnik-delete-filter {
|
||||
@extend .maputnik-icon-button;
|
||||
}
|
||||
|
||||
.maputnik-filter-editor-block-action {
|
||||
margin-top: $margin-2;
|
||||
margin-bottom: $margin-2;
|
||||
}
|
||||
|
||||
.maputnik-filter-editor-block-action {
|
||||
display: inline-block;
|
||||
width: 6%;
|
||||
margin-right: 1.5%;
|
||||
}
|
||||
|
||||
.maputnik-filter-editor-block-content {
|
||||
display: inline-block;
|
||||
width: 92.5%;
|
||||
}
|
||||
|
||||
.maputnik-filter-editor-property {
|
||||
display: inline-block;
|
||||
width: 25%;
|
||||
}
|
||||
|
||||
.maputnik-filter-editor-operator {
|
||||
display: inline-block;
|
||||
width: 17%;
|
||||
|
||||
.maputnik-select {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.maputnik-filter-editor-args {
|
||||
display: inline-block;
|
||||
width: 54%;
|
||||
|
||||
.maputnik-string,
|
||||
.maputnik-number {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
139
src/styles/_input.scss
Normal file
139
src/styles/_input.scss
Normal file
@@ -0,0 +1,139 @@
|
||||
//INPUT
|
||||
.maputnik-input {
|
||||
height: 24px;
|
||||
width: 100%;
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
font-size: $font-size-6;
|
||||
line-height: 2;
|
||||
padding-left: $margin-2;
|
||||
padding-right: $margin-2;
|
||||
border: none;
|
||||
background-color: $color-gray;
|
||||
color: lighten($color-lowgray, 12);
|
||||
}
|
||||
|
||||
.maputnik-string {
|
||||
@extend .maputnik-input;
|
||||
}
|
||||
|
||||
.maputnik-number {
|
||||
@extend .maputnik-input;
|
||||
}
|
||||
|
||||
//COLOR PICKER
|
||||
.maputnik-color {
|
||||
@extend .maputnik-input;
|
||||
|
||||
height: 26px;
|
||||
}
|
||||
|
||||
.maputnik-color-wrapper {
|
||||
position: relative;
|
||||
display: inline;
|
||||
}
|
||||
|
||||
// ARRAY
|
||||
|
||||
.maputnik-array {
|
||||
> * {
|
||||
margin-bottom: $margin-3;
|
||||
}
|
||||
}
|
||||
|
||||
// SELECT
|
||||
.maputnik-select {
|
||||
@extend .maputnik-input;
|
||||
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
// MULTIBUTTON
|
||||
.maputnik-multibutton {
|
||||
padding: 0;
|
||||
|
||||
.maputnik-button {
|
||||
margin-right: $margin-1;
|
||||
}
|
||||
}
|
||||
|
||||
.maputnik-button-selected {
|
||||
background-color: lighten($color-midgray, 12);
|
||||
outline: 1px $color-white;
|
||||
color: white;
|
||||
}
|
||||
|
||||
// CHECKBOX
|
||||
.maputnik-checkbox {
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
opacity: 0;
|
||||
width: 50%;
|
||||
|
||||
&-wrapper {
|
||||
@extend .maputnik-input;
|
||||
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
cursor: pointer;
|
||||
max-width: 24px;
|
||||
}
|
||||
|
||||
&-box {
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
margin-right: $margin-2;
|
||||
background-color: $color-gray;
|
||||
border-radius: 2px;
|
||||
border-style: solid;
|
||||
border-width: 2px;
|
||||
border-color: $color-gray;
|
||||
transition: background-color 0.1s ease-out;
|
||||
}
|
||||
|
||||
&-icon {
|
||||
width: 50%;
|
||||
height: 50%;
|
||||
margin-top: 1px;
|
||||
fill: $color-lowgray;
|
||||
}
|
||||
}
|
||||
|
||||
// AUTOCOMPLETE
|
||||
.maputnik-autocomplete {
|
||||
&-menu {
|
||||
border: none;
|
||||
padding: 2px 0;
|
||||
position: fixed;
|
||||
overflow: auto;
|
||||
max-height: 50%;
|
||||
background: $color-gray;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
&-menu-item {
|
||||
user-select: none;
|
||||
color: $color-lowgray;
|
||||
cursor: default;
|
||||
padding: $margin-1;
|
||||
font-size: $font-size-6;
|
||||
z-index: 3;
|
||||
background: $color-gray;
|
||||
}
|
||||
|
||||
&-menu-item-selected {
|
||||
background: $color-midgray;
|
||||
}
|
||||
}
|
||||
|
||||
// FONT
|
||||
.maputnik-font {
|
||||
.maputnik-autocomplete:not(:last-child) {
|
||||
margin-bottom: $margin-3;
|
||||
}
|
||||
}
|
||||
131
src/styles/_layer.scss
Normal file
131
src/styles/_layer.scss
Normal file
@@ -0,0 +1,131 @@
|
||||
// LAYER LIST
|
||||
.maputnik-layer-list {
|
||||
&-header {
|
||||
padding: $margin-2;
|
||||
|
||||
@include flex-row;
|
||||
|
||||
> * {
|
||||
vertical-align: middle;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&-header-title {
|
||||
font-size: $font-size-5;
|
||||
color: $color-white;
|
||||
font-weight: bold;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
&-container {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
padding-bottom: $margin-5;
|
||||
}
|
||||
|
||||
&-item {
|
||||
font-weight: 400;
|
||||
color: $color-lowgray;
|
||||
font-size: $font-size-6;
|
||||
border-width: 0 0 1px;
|
||||
border-style: solid;
|
||||
border-color: lighten($color-black, 0.1);
|
||||
user-select: none;
|
||||
list-style: none;
|
||||
z-index: 2000;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
padding: 5px 10px;
|
||||
line-height: 1.3;
|
||||
max-height: 50px;
|
||||
opacity: 1;
|
||||
-webkit-transition: opacity 600ms, visibility 600ms;
|
||||
transition: opacity 600ms, visibility 600ms;
|
||||
|
||||
@include flex-row;
|
||||
}
|
||||
|
||||
&-icon-action svg {
|
||||
fill: $color-black;
|
||||
}
|
||||
|
||||
.maputnik-layer-list-item:hover,
|
||||
.maputnik-layer-list-item-selected {
|
||||
background-color: lighten($color-black, 2);
|
||||
|
||||
.maputnik-layer-list-icon-action svg {
|
||||
fill: darken($color-lowgray, 0.5);
|
||||
|
||||
&:hover {
|
||||
fill: $color-white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-item-selected {
|
||||
color: $color-white;
|
||||
}
|
||||
|
||||
&-item-collapsed {
|
||||
position: absolute;
|
||||
max-height: 0;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
&-item-group-last {
|
||||
border-bottom: 2px solid $color-gray;
|
||||
}
|
||||
|
||||
&-item-id {
|
||||
width: 115px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
&-group-header {
|
||||
font-size: $font-size-6;
|
||||
color: $color-lowgray;
|
||||
background-color: lighten($color-black, 2);
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
padding: $margin-2;
|
||||
|
||||
@include flex-row;
|
||||
|
||||
svg {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
&-group-title {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
&-group-content {
|
||||
margin-left: $margin-3;
|
||||
}
|
||||
}
|
||||
|
||||
// FILTER EDITOR
|
||||
.maputnik-layer-editor-group {
|
||||
font-weight: bold;
|
||||
font-size: $font-size-5;
|
||||
background-color: lighten($color-black, 2);
|
||||
color: $color-white;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
padding: $margin-2;
|
||||
line-height: 20px;
|
||||
|
||||
@include flex-row;
|
||||
|
||||
&:hover {
|
||||
background-color: $color-gray;
|
||||
}
|
||||
}
|
||||
49
src/styles/_layout.scss
Normal file
49
src/styles/_layout.scss
Normal file
@@ -0,0 +1,49 @@
|
||||
//SCROLLING
|
||||
.maputnik-scroll-container {
|
||||
overflow-x: visible;
|
||||
overflow-y: scroll;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 1px;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
//APP LAYOUT
|
||||
.maputnik-layout {
|
||||
font-family: $font-family;
|
||||
color: $color-white;
|
||||
|
||||
&-list {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
height: calc(100% - $toolbar-height);
|
||||
top: 40px;
|
||||
left: 0;
|
||||
z-index: 3;
|
||||
width: 200px;
|
||||
overflow: hidden;
|
||||
background-color: $color-black;
|
||||
}
|
||||
|
||||
&-drawer {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
height: calc(100% - $toolbar-height);
|
||||
top: 40px;
|
||||
left: 200px;
|
||||
z-index: 1;
|
||||
width: 350px;
|
||||
background-color: $color-black;
|
||||
}
|
||||
|
||||
&-bottom {
|
||||
position: fixed;
|
||||
height: 50px;
|
||||
bottom: 0;
|
||||
left: 550px;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
background-color: $color-black;
|
||||
}
|
||||
}
|
||||
14
src/styles/_mixins.scss
Normal file
14
src/styles/_mixins.scss
Normal file
@@ -0,0 +1,14 @@
|
||||
@mixin vendor-prefix($name, $argument) {
|
||||
-webkit-#{$name}: #{$argument};
|
||||
-ms-#{$name}: #{$argument};
|
||||
-moz-#{$name}: #{$argument};
|
||||
-o-#{$name}: #{$argument};
|
||||
#{$name}: #{$argument};
|
||||
}
|
||||
|
||||
@mixin flex-row {
|
||||
display: flex;
|
||||
display: -ms-flexbox;
|
||||
|
||||
@include vendor-prefix(flex-direction, row);
|
||||
}
|
||||
178
src/styles/_modal.scss
Normal file
178
src/styles/_modal.scss
Normal file
@@ -0,0 +1,178 @@
|
||||
//MODAL
|
||||
.maputnik-modal {
|
||||
min-width: 350px;
|
||||
max-width: 600px;
|
||||
background-color: $color-black;
|
||||
box-shadow: 0 0 5px 0 rgba(0, 0, 0, 0.3);
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.maputnik-modal-section {
|
||||
padding-top: $margin-3;
|
||||
padding-bottom: $margin-3;
|
||||
}
|
||||
|
||||
.maputnik-modal-header {
|
||||
background-color: $color-gray;
|
||||
padding: $margin-3;
|
||||
|
||||
@include flex-row;
|
||||
}
|
||||
|
||||
.maputnik-modal-header-title {
|
||||
font-size: $font-size-5;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.maputnik-modal-header-toggle {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.maputnik-modal-content {
|
||||
padding: $margin-3;
|
||||
}
|
||||
|
||||
.maputnik-modal-header-space {
|
||||
@extend .maputnik-space;
|
||||
}
|
||||
|
||||
//OVERLAY
|
||||
.maputnik-overlay-viewport {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 2;
|
||||
opacity: 0.875;
|
||||
background-color: rgb(28, 31, 36);
|
||||
}
|
||||
|
||||
.maputnik-overlay {
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
position: fixed;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
@include flex-row;
|
||||
}
|
||||
|
||||
//OPEN MODAL
|
||||
.maputnik-upload-button {
|
||||
@extend .maputnik-big-button;
|
||||
}
|
||||
|
||||
.maputnik-public-style {
|
||||
vertical-align: top;
|
||||
margin-top: 10px;
|
||||
margin-right: 10px;
|
||||
background-color: $color-gray;
|
||||
display: inline-block;
|
||||
width: 180px;
|
||||
font-size: $font-size-2;
|
||||
color: $color-lowgray;
|
||||
}
|
||||
|
||||
.maputnik-public-style-button {
|
||||
background-color: $color-gray;
|
||||
padding: $margin-3;
|
||||
display: block;
|
||||
|
||||
&:hover {
|
||||
background-color: $color-midgray;
|
||||
}
|
||||
}
|
||||
|
||||
.maputnik-public-style-header {
|
||||
@include flex-row;
|
||||
}
|
||||
|
||||
.maputnik-public-style-thumbnail {
|
||||
display: block;
|
||||
margin-top: $margin-2;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.maputnik-add-layer {
|
||||
@extend .clearfix;
|
||||
}
|
||||
|
||||
//ADD MODAL
|
||||
.maputnik-add-layer-button {
|
||||
@extend .maputnik-big-button;
|
||||
|
||||
margin-right: $margin-3;
|
||||
float: right;
|
||||
display: inline-block;
|
||||
margin-top: 3;
|
||||
margin-bottom: $margin-3;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
//SOURCE MODAL
|
||||
.maputnik-public-source {
|
||||
vertical-align: top;
|
||||
margin-top: 1.5%;
|
||||
margin-right: 1.5%;
|
||||
background-color: $color-gray;
|
||||
width: 48.5%;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.maputnik-public-source-select {
|
||||
padding: $margin-3;
|
||||
font-size: $font-size-5;
|
||||
color: $color-lowgray;
|
||||
background-color: transparent;
|
||||
|
||||
@include flex-row;
|
||||
}
|
||||
|
||||
.maputnik-public-source-name {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.maputnik-public-source-id {
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.maputnik-active-source-type-editor {
|
||||
min-width: 500px;
|
||||
}
|
||||
|
||||
.maputnik-active-source-type-editor-header {
|
||||
background-color: $color-gray;
|
||||
color: $color-lowgray;
|
||||
padding: $margin-2;
|
||||
|
||||
@include flex-row;
|
||||
}
|
||||
|
||||
.maputnik-active-source-type-editor-header-id {
|
||||
font-weight: 700;
|
||||
line-height: 2;
|
||||
font-size: $font-size-5;
|
||||
}
|
||||
|
||||
.maputnik-active-source-type-editor-content {
|
||||
border-color: $color-gray;
|
||||
border-width: 2px;
|
||||
border-style: solid;
|
||||
padding: $margin-2;
|
||||
}
|
||||
|
||||
.maputnik-add-source {
|
||||
@extend .clearfix;
|
||||
}
|
||||
|
||||
.maputnik-add-source-button {
|
||||
@extend .maputnik-big-button;
|
||||
|
||||
display: inline-block;
|
||||
margin-top: 0;
|
||||
margin-right: $margin-3;
|
||||
float: right;
|
||||
}
|
||||
12
src/styles/_picker.scss
Normal file
12
src/styles/_picker.scss
Normal file
@@ -0,0 +1,12 @@
|
||||
|
||||
|
||||
.chrome-picker {
|
||||
background-color: #1c1f24 !important;
|
||||
font-family: inherit !important;
|
||||
}
|
||||
|
||||
.chrome-picker input {
|
||||
background-color: rgb(38, 40, 46) !important;
|
||||
color: rgb(142, 142, 142) !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
24
src/styles/_popup.scss
Normal file
24
src/styles/_popup.scss
Normal file
@@ -0,0 +1,24 @@
|
||||
.maputnik-popup-layer {
|
||||
display: block;
|
||||
color: $color-lowgray;
|
||||
user-select: none;
|
||||
line-height: 1.2;
|
||||
padding: $margin-2;
|
||||
padding-top: $margin-1;
|
||||
padding-bottom: $margin-1;
|
||||
}
|
||||
|
||||
.maputnik-popup-layer-id {
|
||||
padding-left: $margin-2;
|
||||
padding-right: $margin-2;
|
||||
background-color: $color-midgray;
|
||||
color: $color-white;
|
||||
}
|
||||
|
||||
.maputnik-feature-property-popup {
|
||||
.maputnik-input-block {
|
||||
margin: 0;
|
||||
margin-left: $margin-2;
|
||||
margin-top: $margin-2;
|
||||
}
|
||||
}
|
||||
49
src/styles/_reset.scss
Normal file
49
src/styles/_reset.scss
Normal file
@@ -0,0 +1,49 @@
|
||||
/* stylelint-disable */
|
||||
html {
|
||||
background-color: rgb(28, 31, 36);
|
||||
}
|
||||
|
||||
/* CSS Reset */
|
||||
html, body, div, span, applet, object, iframe,
|
||||
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
|
||||
a, abbr, acronym, address, big, cite, code,
|
||||
del, dfn, em, img, ins, kbd, q, s, samp,
|
||||
small, strike, strong, sub, sup, tt, var,
|
||||
b, u, i, center,
|
||||
dl, dt, dd, ol, ul, li,
|
||||
fieldset, form, label, legend,
|
||||
table, caption, tbody, tfoot, thead, tr, th, td,
|
||||
article, aside, canvas, details, embed,
|
||||
figure, figcaption, footer, header, hgroup,
|
||||
menu, nav, output, ruby, section, summary,
|
||||
time, mark, audio, video {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
font-size: 100%;
|
||||
font: inherit;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
/* HTML5 display-role reset for older browsers */
|
||||
article, aside, details, figcaption, figure,
|
||||
footer, header, hgroup, menu, nav, section {
|
||||
display: block;
|
||||
}
|
||||
body {
|
||||
line-height: 1;
|
||||
}
|
||||
ol, ul {
|
||||
list-style: none;
|
||||
}
|
||||
blockquote, q {
|
||||
quotes: none;
|
||||
}
|
||||
blockquote:before, blockquote:after,
|
||||
q:before, q:after {
|
||||
content: '';
|
||||
content: none;
|
||||
}
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
}
|
||||
12
src/styles/_scrollbar.scss
Normal file
12
src/styles/_scrollbar.scss
Normal file
@@ -0,0 +1,12 @@
|
||||
::-webkit-scrollbar {
|
||||
background-color: #26282e;
|
||||
width: 5px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
border-radius: 6px;
|
||||
-webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
|
||||
background-color: #666;
|
||||
padding-left: 2px;
|
||||
padding-right: 2px;
|
||||
}
|
||||
57
src/styles/_toolbar.scss
Normal file
57
src/styles/_toolbar.scss
Normal file
@@ -0,0 +1,57 @@
|
||||
// TOOLBAR
|
||||
.maputnik-toolbar {
|
||||
position: fixed;
|
||||
height: $toolbar-height;
|
||||
width: 100%;
|
||||
z-index: 100;
|
||||
left: 0;
|
||||
top: 0;
|
||||
background-color: $color-black;
|
||||
}
|
||||
|
||||
.maputnik-toolbar-logo {
|
||||
width: 180px;
|
||||
text-align: left;
|
||||
background-color: $color-black;
|
||||
padding: $margin-2;
|
||||
height: $toolbar-height;
|
||||
|
||||
h1 {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
padding-right: $margin-2;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
|
||||
.maputnik-toolbar-link {
|
||||
vertical-align: top;
|
||||
height: $toolbar-height;
|
||||
display: inline-block;
|
||||
padding: $margin-3;
|
||||
font-size: $font-size-5;
|
||||
cursor: pointer;
|
||||
color: $color-white;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
background-color: $color-midgray;
|
||||
}
|
||||
}
|
||||
|
||||
.maputnik-toolbar-action {
|
||||
@extend .maputnik-toolbar-link;
|
||||
}
|
||||
|
||||
.maputnik-icon-text {
|
||||
padding-left: $margin-1;
|
||||
}
|
||||
|
||||
.maputnik-icon-action {
|
||||
display: inline;
|
||||
margin-left: $margin-1;
|
||||
}
|
||||
69
src/styles/_zoomproperty.scss
Normal file
69
src/styles/_zoomproperty.scss
Normal file
@@ -0,0 +1,69 @@
|
||||
// ZOOM FUNC
|
||||
.maputnik-make-zoom-function {
|
||||
background-color: transparent;
|
||||
display: inline-block;
|
||||
padding-bottom: 0;
|
||||
padding-top: 0;
|
||||
vertical-align: middle;
|
||||
|
||||
@extend .maputnik-icon-button;
|
||||
}
|
||||
|
||||
// ZOOM PROPERTY
|
||||
.maputnik-zoom-spec-property {
|
||||
@extend .clearfix;
|
||||
}
|
||||
|
||||
.maputnik-zoom-spec-property-label {
|
||||
display: inline-block;
|
||||
width: 41%;
|
||||
}
|
||||
|
||||
.maputnik-zoom-spec-property-stop-item {
|
||||
margin-bottom: $margin-2;
|
||||
margin-top: $margin-2;
|
||||
}
|
||||
|
||||
.maputnik-zoom-spec-property-stop-edit {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
width: 16%;
|
||||
margin-right: 3%;
|
||||
|
||||
> * {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.maputnik-zoom-spec-property-stop-value {
|
||||
display: inline-block;
|
||||
width: 81%;
|
||||
|
||||
> * {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.maputnik-delete-stop {
|
||||
@extend .maputnik-icon-button;
|
||||
|
||||
vertical-align: top;
|
||||
|
||||
.maputnik-doc-wrapper {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.maputnik-doc-target {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.maputnik-add-stop {
|
||||
display: inline-block;
|
||||
float: right;
|
||||
margin-right: $margin-3;
|
||||
}
|
||||
|
||||
.maputnik-zoom-spec-property .maputnik-input-block:not(:first-child) .maputnik-input-block-label {
|
||||
visibility: hidden;
|
||||
}
|
||||
35
src/styles/index.scss
Normal file
35
src/styles/index.scss
Normal file
@@ -0,0 +1,35 @@
|
||||
$color-black: #1c1f24;
|
||||
$color-gray: #26282e;
|
||||
$color-midgray: #36383e;
|
||||
$color-lowgray: #8e8e8e;
|
||||
$color-white: #f0f0f0;
|
||||
$color-red: #cf4a4a;
|
||||
$margin-1: 3px;
|
||||
$margin-2: 5px;
|
||||
$margin-3: 10px;
|
||||
$margin-4: 30px;
|
||||
$margin-5: 40px;
|
||||
$font-size-1: 24px;
|
||||
$font-size-2: 20px;
|
||||
$font-size-3: 18px;
|
||||
$font-size-4: 16px;
|
||||
$font-size-5: 14px;
|
||||
$font-size-6: 12px;
|
||||
$font-family: Roboto, sans-serif;
|
||||
|
||||
$toolbar-height: 40px;
|
||||
|
||||
@import 'mixins';
|
||||
@import 'reset';
|
||||
@import 'base';
|
||||
@import 'components';
|
||||
@import 'scrollbar';
|
||||
@import 'picker';
|
||||
@import 'toolbar';
|
||||
@import 'modal';
|
||||
@import 'layout';
|
||||
@import 'layer';
|
||||
@import 'input';
|
||||
@import 'filtereditor';
|
||||
@import 'zoomproperty';
|
||||
@import 'popup';
|
||||
@@ -29,6 +29,10 @@ module.exports = [
|
||||
test: /\.json$/,
|
||||
loader: 'json-loader'
|
||||
},
|
||||
{
|
||||
test: /[\/\\](node_modules|global|src)[\/\\].*\.scss$/,
|
||||
loaders: ["style-loader", "css-loader", "sass-loader"]
|
||||
},
|
||||
{
|
||||
test: /[\/\\](node_modules|global|src)[\/\\].*\.css$/,
|
||||
loaders: [
|
||||
|
||||
@@ -17,7 +17,6 @@ module.exports = {
|
||||
"randomcolor",
|
||||
"lodash.clonedeep",
|
||||
"lodash.throttle",
|
||||
"lodash.topairs",
|
||||
'color',
|
||||
'react',
|
||||
"react-dom",
|
||||
@@ -62,8 +61,6 @@ module.exports = {
|
||||
compress: {
|
||||
warnings: false,
|
||||
screw_ie8: true,
|
||||
drop_console: true,
|
||||
drop_debugger: true
|
||||
}
|
||||
}),
|
||||
new webpack.optimize.OccurenceOrderPlugin(),
|
||||
|
||||
Reference in New Issue
Block a user