Compare commits
218 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5c286f8d96 | ||
|
|
404b53587f | ||
|
|
e5fbe3b74a | ||
|
|
3f262885ca | ||
|
|
c837179f71 | ||
|
|
9a947658e2 | ||
|
|
2458d4b637 | ||
|
|
e4850805fb | ||
|
|
3a15a3bb06 | ||
|
|
75ca1fa930 | ||
|
|
377840ca24 | ||
|
|
48e9589b58 | ||
|
|
11e9cef834 | ||
|
|
7e3aa09d3e | ||
|
|
e3b7e002b4 | ||
|
|
3b7fb7ae75 | ||
|
|
fab004cdfe | ||
|
|
07523c00f0 | ||
|
|
c15ac14f88 | ||
|
|
8f6006c19f | ||
|
|
16bedcf5b1 | ||
|
|
05349d8ffe | ||
|
|
a1e1895651 | ||
|
|
a111599850 | ||
|
|
121a95cee8 | ||
|
|
decd1f3ea2 | ||
|
|
c632718324 | ||
|
|
9509b59696 | ||
|
|
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 | ||
|
|
99acbd4d92 | ||
|
|
b0e9790382 | ||
|
|
e00cdde3af | ||
|
|
c3a634b216 | ||
|
|
4f26a521a0 | ||
|
|
ca6b48843c | ||
|
|
0eb00312f4 | ||
|
|
e7709dae15 | ||
|
|
03796c963b | ||
|
|
b50855a4a9 | ||
|
|
24a90b3c57 | ||
|
|
cf80e80025 | ||
|
|
48f10bcb73 | ||
|
|
7bc2323401 | ||
|
|
a71ac502d6 | ||
|
|
f2dd785e7b | ||
|
|
0b99e571c4 | ||
|
|
cfc6085718 | ||
|
|
384b2d4bea | ||
|
|
1058dbfb5a | ||
|
|
bda7ce7390 | ||
|
|
7b631b0510 | ||
|
|
1d7768e37c | ||
|
|
89d497c73f | ||
|
|
886c87f231 | ||
|
|
d567a4f98b | ||
|
|
5eb0e36faf | ||
|
|
51a2eabc91 | ||
|
|
007bdad70a | ||
|
|
1f1a919c77 | ||
|
|
3be3a716d4 | ||
|
|
ae9afdd8d9 | ||
|
|
a5307054b3 | ||
|
|
d16c3f4356 | ||
|
|
853361ace7 | ||
|
|
e41e1eb2f1 | ||
|
|
e36c233b49 | ||
|
|
d1b8f8d63e | ||
|
|
29cfb58a56 | ||
|
|
bf5131cadd | ||
|
|
ccc39b87db | ||
|
|
604be38b7c | ||
|
|
160bd9563b | ||
|
|
488fdf2bd5 | ||
|
|
a0e1e6152b | ||
|
|
58897f1856 | ||
|
|
80678af691 | ||
|
|
ba271e1fc6 | ||
|
|
c7ac90ba15 | ||
|
|
0dc335ea5f | ||
|
|
acac314d27 | ||
|
|
916c1dc9fc | ||
|
|
c159f7041f | ||
|
|
a3d586a75d | ||
|
|
6b0b29d1da | ||
|
|
8afda2fe28 | ||
|
|
beb1a2a8d1 | ||
|
|
436e0c2095 | ||
|
|
e1bc2a321a | ||
|
|
720c8f108b | ||
|
|
4db5c7cf68 | ||
|
|
8f561d8a27 | ||
|
|
0c483cffe3 | ||
|
|
def5ebb587 | ||
|
|
6e9e66b147 | ||
|
|
f332d517f3 | ||
|
|
04eab70e27 | ||
|
|
cfbcdc7fa1 | ||
|
|
c95dd75e2a | ||
|
|
4408f3ab3b |
2
.gitignore
vendored
@@ -1,6 +1,8 @@
|
|||||||
# Logs
|
# Logs
|
||||||
logs
|
logs
|
||||||
*.log
|
*.log
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
# Runtime data
|
# Runtime data
|
||||||
pids
|
pids
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ install:
|
|||||||
- npm install
|
- npm install
|
||||||
script:
|
script:
|
||||||
- mkdir public
|
- mkdir public
|
||||||
- npm run build
|
- node --stack_size=100000 $(which npm) run build
|
||||||
- npm run lint
|
- npm run lint
|
||||||
|
- npm run lint-styles
|
||||||
- npm run test
|
- npm run test
|
||||||
|
|||||||
122
README.md
@@ -1,28 +1,27 @@
|
|||||||
# Maputnik [](https://travis-ci.org/maputnik/editor) [](https://ci.appveyor.com/project/lukasmartinelli/editor) [](https://tldrlegal.com/license/mit-license)
|
# Maputnik [](https://travis-ci.org/maputnik/editor) [](https://ci.appveyor.com/project/lukasmartinelli/editor) [](https://tldrlegal.com/license/mit-license)
|
||||||
|
|
||||||
<img width="200" align="right" alt="Maputnik" src="media/maputnik.png" />
|
<img width="200" align="right" alt="Maputnik" src="src/img/maputnik.png" />
|
||||||
|
|
||||||
A free and open visual editor for the [Mapbox GL styles](https://www.mapbox.com/mapbox-gl-style-spec/)
|
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.
|
||||||
|
|
||||||
*Maputnik is an early prototype and is under development.
|
- :link: Design your maps online at **http://maputnik.com/editor/** (all in local storage)
|
||||||
[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)*.
|
- :link: Use the [Maputnik CLI](https://github.com/maputnik/editor/wiki/Maputnik-CLI) for local style development
|
||||||
|
|
||||||
## Features
|
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.
|
||||||
|
|
||||||
- [x] Completely free and open source
|
## Documentation
|
||||||
- [x] Visual interface for designing maps
|
|
||||||
- [x] Immediate feedback (thanks to [style diffs](https://github.com/mapbox/mapbox-gl-style-spec/blob/mb-pages/lib/diff.js))
|
|
||||||
- [x] Edit layers
|
|
||||||
- [x] Easy to deploy as single HTML file
|
|
||||||
- [ ] Support for Open Layers 3
|
|
||||||
|
|
||||||

|
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)
|
||||||
|
|
||||||
## Develop
|
## 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
|
We ensure building and developing Maputnik works with
|
||||||
|
|
||||||
@@ -47,103 +46,64 @@ npm run build
|
|||||||
Lint the JavaScript code.
|
Lint the JavaScript code.
|
||||||
|
|
||||||
```
|
```
|
||||||
# install lint dependencies
|
|
||||||
npm install --save-dev eslint eslint-plugin-react
|
|
||||||
# run linter
|
# run linter
|
||||||
npm run lint
|
npm run lint
|
||||||
```
|
npm run lint-styles
|
||||||
|
|
||||||
## 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
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Sponsors
|
## 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
|
### 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
|
### Silver
|
||||||
|
|
||||||
|
- [Klokan Technologies](https://www.klokantech.com/)
|
||||||
|
- [Geofabrik](http://www.geofabrik.de/)
|
||||||
|
- [Dreipol](https://www.dreipol.ch/)
|
||||||
|
|
||||||
<a href="https://www.klokantech.com/">
|
<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>
|
||||||
<a href="https://www.dreipol.ch/">
|
<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>
|
</a>
|
||||||
|
|
||||||
<br/>
|
<br/>
|
||||||
|
|
||||||
### Individuals
|
### Individuals
|
||||||
|
|
||||||
**Influential Stakeholder**
|
**Influential Stakeholder**
|
||||||
|
|
||||||
- Alan McConchie
|
Alan McConchie, Odi, Mats Norén, Uli [geOps](http://geops.ch/), Helge Fahrnberger ([Toursprung](http://www.toursprung.com/)), Kirusanth Poopalasingam
|
||||||
- Odi
|
|
||||||
- Mats Norén
|
|
||||||
- Uli [geOps](http://geops.ch/)
|
|
||||||
- Helge Fahrnberger
|
|
||||||
Kirusanth Poopalasingam
|
|
||||||
|
|
||||||
**Stakeholder**
|
**Stakeholder**
|
||||||
|
|
||||||
- Brian Flood
|
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
|
||||||
- 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**
|
**Supporter**
|
||||||
|
|
||||||
- Sina Martinelli
|
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
|
||||||
- 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
|
## License
|
||||||
|
|
||||||
|
|||||||
BIN
media/demo.gif
|
Before Width: | Height: | Size: 3.2 MiB |
|
Before Width: | Height: | Size: 410 KiB |
BIN
media/sponsors/geofabrik.png
Normal file
|
After Width: | Height: | Size: 8.2 KiB |
BIN
media/sponsors/orbicon_informatik.png
Normal file
|
After Width: | Height: | Size: 65 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 37 KiB |
53
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "maputnik",
|
"name": "maputnik",
|
||||||
"version": "0.0.1",
|
"version": "1.0.1",
|
||||||
"description": "A MapboxGL visual style editor",
|
"description": "A MapboxGL visual style editor",
|
||||||
"main": "''",
|
"main": "''",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -9,7 +9,8 @@
|
|||||||
"test": "karma start --single-run",
|
"test": "karma start --single-run",
|
||||||
"test-watch": "karma start",
|
"test-watch": "karma start",
|
||||||
"start": "webpack-dev-server --progress --profile --colors --watch-poll",
|
"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": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@@ -19,18 +20,24 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"homepage": "https://github.com/maputnik/editor#readme",
|
"homepage": "https://github.com/maputnik/editor#readme",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"classnames": "^2.2.5",
|
||||||
|
"codemirror": "^5.18.2",
|
||||||
"color": "^1.0.3",
|
"color": "^1.0.3",
|
||||||
"file-saver": "^1.3.2",
|
"file-saver": "^1.3.2",
|
||||||
|
"github-api": "^3.0.0",
|
||||||
|
"lodash.capitalize": "^4.2.1",
|
||||||
"lodash.clonedeep": "^4.5.0",
|
"lodash.clonedeep": "^4.5.0",
|
||||||
|
"lodash.isequal": "^4.4.0",
|
||||||
"lodash.throttle": "^4.1.1",
|
"lodash.throttle": "^4.1.1",
|
||||||
"lodash.topairs": "^4.3.0",
|
"mapbox-gl": "^0.31.0",
|
||||||
"mapbox-gl": "mapbox/mapbox-gl-js#6c24b9621d2aa770eda67fb5638b4d78087b5624",
|
"mapbox-gl-inspect": "^1.2.1",
|
||||||
"mapbox-gl-style-spec": "mapbox/mapbox-gl-style-spec#e85407a377510acb647161de6be6357ab4f606dd",
|
"mapbox-gl-style-spec": "^8.11.0",
|
||||||
"ol-mapbox-style": "0.0.11",
|
"mousetrap": "^1.6.0",
|
||||||
|
"ol-mapbox-style": "1.0.1",
|
||||||
"openlayers": "^3.19.1",
|
"openlayers": "^3.19.1",
|
||||||
"randomcolor": "^0.4.4",
|
|
||||||
"react": "^15.4.0",
|
"react": "^15.4.0",
|
||||||
"react-addons-pure-render-mixin": "^15.4.0",
|
"react-addons-pure-render-mixin": "^15.4.0",
|
||||||
|
"react-autocomplete": "^1.4.0",
|
||||||
"react-codemirror": "^0.3.0",
|
"react-codemirror": "^0.3.0",
|
||||||
"react-collapse": "^2.3.3",
|
"react-collapse": "^2.3.3",
|
||||||
"react-color": "^2.10.0",
|
"react-color": "^2.10.0",
|
||||||
@@ -41,7 +48,9 @@
|
|||||||
"react-icons": "^2.2.1",
|
"react-icons": "^2.2.1",
|
||||||
"react-motion": "^0.4.7",
|
"react-motion": "^0.4.7",
|
||||||
"react-sortable-hoc": "^0.4.5",
|
"react-sortable-hoc": "^0.4.5",
|
||||||
"request": "^2.79.0"
|
"reconnecting-websocket": "^3.0.3",
|
||||||
|
"request": "^2.79.0",
|
||||||
|
"url": "^0.11.0"
|
||||||
},
|
},
|
||||||
"babel": {
|
"babel": {
|
||||||
"presets": [
|
"presets": [
|
||||||
@@ -55,6 +64,9 @@
|
|||||||
"jshintConfig": {
|
"jshintConfig": {
|
||||||
"esversion": 6
|
"esversion": 6
|
||||||
},
|
},
|
||||||
|
"stylelint": {
|
||||||
|
"extends": "stylelint-config-standard"
|
||||||
|
},
|
||||||
"eslintConfig": {
|
"eslintConfig": {
|
||||||
"plugins": [
|
"plugins": [
|
||||||
"react"
|
"react"
|
||||||
@@ -79,18 +91,18 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"babel-core": "6.14.0",
|
"babel-core": "6.21.0",
|
||||||
"babel-eslint": "^6.1.2",
|
"babel-eslint": "^7.1.1",
|
||||||
"babel-loader": "6.2.4",
|
"babel-loader": "6.2.10",
|
||||||
"babel-plugin-transform-class-properties": "^6.11.5",
|
"babel-plugin-transform-class-properties": "^6.11.5",
|
||||||
"babel-plugin-transform-decorators-legacy": "^1.3.4",
|
"babel-plugin-transform-decorators-legacy": "^1.3.4",
|
||||||
"babel-plugin-transform-flow-strip-types": "^6.21.0",
|
"babel-plugin-transform-flow-strip-types": "^6.21.0",
|
||||||
"babel-plugin-transform-object-rest-spread": "^6.8.0",
|
"babel-plugin-transform-object-rest-spread": "^6.8.0",
|
||||||
"babel-plugin-transform-runtime": "^6.15.0",
|
"babel-plugin-transform-runtime": "^6.15.0",
|
||||||
"babel-preset-es2015": "6.14.0",
|
"babel-preset-es2015": "6.18.0",
|
||||||
"babel-preset-react": "6.11.1",
|
"babel-preset-react": "6.16.0",
|
||||||
"babel-runtime": "^6.11.6",
|
"babel-runtime": "^6.11.6",
|
||||||
"css-loader": "0.25.0",
|
"css-loader": "0.26.1",
|
||||||
"eslint": "^3.5.0",
|
"eslint": "^3.5.0",
|
||||||
"eslint-plugin-react": "^6.2.0",
|
"eslint-plugin-react": "^6.2.0",
|
||||||
"extract-text-webpack-plugin": "^1.0.1",
|
"extract-text-webpack-plugin": "^1.0.1",
|
||||||
@@ -101,18 +113,19 @@
|
|||||||
"karma-chrome-launcher": "^2.0.0",
|
"karma-chrome-launcher": "^2.0.0",
|
||||||
"karma-firefox-launcher": "^1.0.0",
|
"karma-firefox-launcher": "^1.0.0",
|
||||||
"karma-mocha": "^1.3.0",
|
"karma-mocha": "^1.3.0",
|
||||||
"karma-webpack": "^1.8.0",
|
"karma-webpack": "^2.0.1",
|
||||||
"mocha": "^3.1.2",
|
"mocha": "^3.1.2",
|
||||||
"mocha-loader": "^1.0.0",
|
"mocha-loader": "^1.0.0",
|
||||||
"node-sass": "^3.9.2",
|
"node-sass": "^4.2.0",
|
||||||
"react-hot-loader": "^3.0.0-beta.6",
|
"react-hot-loader": "^3.0.0-beta.6",
|
||||||
"sass-loader": "^4.0.1",
|
"sass-loader": "^4.0.1",
|
||||||
"style-loader": "0.13.1",
|
"style-loader": "0.13.1",
|
||||||
|
"stylelint": "^7.7.1",
|
||||||
|
"stylelint-config-standard": "^15.0.1",
|
||||||
"transform-loader": "^0.2.3",
|
"transform-loader": "^0.2.3",
|
||||||
"url-loader": "0.5.7",
|
"url-loader": "0.5.7",
|
||||||
"webpack": "1.13.2",
|
"webpack": "1.14.0",
|
||||||
"webpack-cleanup-plugin": "^0.3.0",
|
"webpack-cleanup-plugin": "^0.4.1",
|
||||||
"webpack-dev-server": "1.15.1",
|
"webpack-dev-server": "1.16.2"
|
||||||
"webworkify-webpack": "^1.1.3"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
.cm-s-maputnik.CodeMirror {
|
.cm-s-maputnik.CodeMirror {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
font-size: 10px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cm-s-maputnik.CodeMirror, .cm-s-maputnik .CodeMirror-gutters {
|
.cm-s-maputnik.CodeMirror, .cm-s-maputnik .CodeMirror-gutters {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { saveAs } from 'file-saver'
|
import Mousetrap from 'mousetrap'
|
||||||
|
|
||||||
import MapboxGlMap from './map/MapboxGlMap'
|
import MapboxGlMap from './map/MapboxGlMap'
|
||||||
import OpenLayers3Map from './map/OpenLayers3Map'
|
import OpenLayers3Map from './map/OpenLayers3Map'
|
||||||
@@ -7,33 +7,81 @@ import LayerList from './layers/LayerList'
|
|||||||
import LayerEditor from './layers/LayerEditor'
|
import LayerEditor from './layers/LayerEditor'
|
||||||
import Toolbar from './Toolbar'
|
import Toolbar from './Toolbar'
|
||||||
import AppLayout from './AppLayout'
|
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 style from '../libs/style.js'
|
||||||
import { loadDefaultStyle, SettingsStore, StyleStore } from '../libs/stylestore'
|
import { initialStyleUrl, loadStyleUrl } from '../libs/urlopen'
|
||||||
|
import { undoMessages, redoMessages } from '../libs/diffmessage'
|
||||||
|
import { loadDefaultStyle, StyleStore } from '../libs/stylestore'
|
||||||
import { ApiStyleStore } from '../libs/apistore'
|
import { ApiStyleStore } from '../libs/apistore'
|
||||||
|
import { RevisionStore } from '../libs/revisions'
|
||||||
import LayerWatcher from '../libs/layerwatcher'
|
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 {
|
export default class App extends React.Component {
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props)
|
super(props)
|
||||||
this.layerWatcher = new LayerWatcher()
|
this.revisionStore = new RevisionStore()
|
||||||
this.styleStore = new ApiStyleStore()
|
this.styleStore = new ApiStyleStore({
|
||||||
this.styleStore.supported(isSupported => {
|
onLocalStyleChange: mapStyle => this.onStyleChanged(mapStyle, false)
|
||||||
if(!isSupported) {
|
})
|
||||||
|
|
||||||
|
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')
|
console.log('Falling back to local storage for storing styles')
|
||||||
this.styleStore = new StyleStore()
|
this.styleStore = new StyleStore()
|
||||||
}
|
}
|
||||||
this.styleStore.latestStyle(mapStyle => this.onStyleChanged(mapStyle))
|
this.styleStore.latestStyle(mapStyle => this.onStyleChanged(mapStyle))
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
this.settingsStore = new SettingsStore()
|
|
||||||
this.state = {
|
this.state = {
|
||||||
accessToken: this.settingsStore.accessToken,
|
errors: [],
|
||||||
|
infos: [],
|
||||||
mapStyle: style.emptyStyle,
|
mapStyle: style.emptyStyle,
|
||||||
selectedLayerIndex: 0,
|
selectedLayerIndex: 0,
|
||||||
|
sources: {},
|
||||||
|
vectorLayers: {},
|
||||||
|
inspectModeEnabled: false,
|
||||||
|
spec: GlSpec,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.layerWatcher = new LayerWatcher({
|
||||||
|
onSourcesChange: v => this.setState({ sources: v }),
|
||||||
|
onVectorLayersChange: v => this.setState({ vectorLayers: v })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
Mousetrap.bind(['ctrl+z'], this.onUndo.bind(this));
|
||||||
|
Mousetrap.bind(['ctrl+y'], this.onRedo.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
Mousetrap.unbind(['ctrl+z'], this.onUndo.bind(this));
|
||||||
|
Mousetrap.unbind(['ctrl+y'], this.onRedo.bind(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
onReset() {
|
onReset() {
|
||||||
@@ -41,25 +89,65 @@ export default class App extends React.Component {
|
|||||||
loadDefaultStyle(mapStyle => this.onStyleOpen(mapStyle))
|
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) {
|
saveStyle(snapshotStyle) {
|
||||||
snapshotStyle.modified = new Date().toJSON()
|
|
||||||
this.styleStore.save(snapshotStyle)
|
this.styleStore.save(snapshotStyle)
|
||||||
}
|
}
|
||||||
|
|
||||||
onStyleChanged(newStyle) {
|
updateFonts(urlTemplate) {
|
||||||
this.saveStyle(newStyle)
|
const metadata = this.state.mapStyle.metadata || {}
|
||||||
this.setState({ mapStyle: newStyle })
|
const accessToken = metadata['maputnik:openmaptiles_access_token'] || tokens.openmaptiles
|
||||||
|
downloadGlyphsMetadata(urlTemplate.replace('{key}', accessToken), fonts => {
|
||||||
|
this.setState({ spec: updateRootSpec(this.state.spec, 'glyphs', fonts)})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
onAccessTokenChanged(newToken) {
|
updateIcons(baseUrl) {
|
||||||
this.settingsStore.accessToken = newToken
|
downloadSpriteMetadata(baseUrl, icons => {
|
||||||
this.setState({ accessToken: newToken })
|
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)
|
||||||
|
if(save) this.saveStyle(newStyle)
|
||||||
|
this.setState({
|
||||||
|
mapStyle: newStyle,
|
||||||
|
errors: [],
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
this.setState({
|
||||||
|
errors: errors.map(err => err.message)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onUndo() {
|
||||||
|
const activeStyle = this.revisionStore.undo()
|
||||||
|
const messages = undoMessages(this.state.mapStyle, activeStyle)
|
||||||
|
this.saveStyle(activeStyle)
|
||||||
|
this.setState({
|
||||||
|
mapStyle: activeStyle,
|
||||||
|
infos: messages,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onRedo() {
|
||||||
|
const activeStyle = this.revisionStore.redo()
|
||||||
|
const messages = redoMessages(this.state.mapStyle, activeStyle)
|
||||||
|
this.saveStyle(activeStyle)
|
||||||
|
this.setState({
|
||||||
|
mapStyle: activeStyle,
|
||||||
|
infos: messages,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
onLayersChange(changedLayers) {
|
onLayersChange(changedLayers) {
|
||||||
@@ -90,13 +178,18 @@ export default class App extends React.Component {
|
|||||||
this.onLayersChange(changedLayers)
|
this.onLayersChange(changedLayers)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
changeInspectMode() {
|
||||||
|
this.setState({
|
||||||
|
inspectModeEnabled: !this.state.inspectModeEnabled
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
mapRenderer() {
|
mapRenderer() {
|
||||||
const mapProps = {
|
const mapProps = {
|
||||||
mapStyle: this.state.mapStyle,
|
mapStyle: style.replaceAccessToken(this.state.mapStyle),
|
||||||
accessToken: this.state.accessToken,
|
onDataChange: (e) => {
|
||||||
onMapLoaded: (map) => {
|
this.layerWatcher.analyzeMap(e.map)
|
||||||
this.layerWatcher.map = map
|
},
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const metadata = this.state.mapStyle.metadata || {}
|
const metadata = this.state.mapStyle.metadata || {}
|
||||||
@@ -106,7 +199,9 @@ export default class App extends React.Component {
|
|||||||
if(renderer === 'ol3') {
|
if(renderer === 'ol3') {
|
||||||
return <OpenLayers3Map {...mapProps} />
|
return <OpenLayers3Map {...mapProps} />
|
||||||
} else {
|
} else {
|
||||||
return <MapboxGlMap {...mapProps} />
|
return <MapboxGlMap {...mapProps}
|
||||||
|
inspectModeEnabled={this.state.inspectModeEnabled}
|
||||||
|
highlightedLayer={this.state.mapStyle.layers[this.state.selectedLayerIndex]} />
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,12 +213,15 @@ export default class App extends React.Component {
|
|||||||
render() {
|
render() {
|
||||||
const layers = this.state.mapStyle.layers || []
|
const layers = this.state.mapStyle.layers || []
|
||||||
const selectedLayer = layers.length > 0 ? layers[this.state.selectedLayerIndex] : null
|
const selectedLayer = layers.length > 0 ? layers[this.state.selectedLayerIndex] : null
|
||||||
|
const metadata = this.state.mapStyle.metadata || {}
|
||||||
|
|
||||||
const toolbar = <Toolbar
|
const toolbar = <Toolbar
|
||||||
mapStyle={this.state.mapStyle}
|
mapStyle={this.state.mapStyle}
|
||||||
|
inspectModeEnabled={this.state.inspectModeEnabled}
|
||||||
|
sources={this.state.sources}
|
||||||
onStyleChanged={this.onStyleChanged.bind(this)}
|
onStyleChanged={this.onStyleChanged.bind(this)}
|
||||||
onStyleOpen={this.onStyleChanged.bind(this)}
|
onStyleOpen={this.onStyleChanged.bind(this)}
|
||||||
onStyleDownload={this.onStyleDownload.bind(this)}
|
onInspectModeToggle={this.changeInspectMode.bind(this)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
const layerList = <LayerList
|
const layerList = <LayerList
|
||||||
@@ -131,22 +229,29 @@ export default class App extends React.Component {
|
|||||||
onLayerSelect={this.onLayerSelect.bind(this)}
|
onLayerSelect={this.onLayerSelect.bind(this)}
|
||||||
selectedLayerIndex={this.state.selectedLayerIndex}
|
selectedLayerIndex={this.state.selectedLayerIndex}
|
||||||
layers={layers}
|
layers={layers}
|
||||||
|
sources={this.state.sources}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
const layerEditor = selectedLayer ? <LayerEditor
|
const layerEditor = selectedLayer ? <LayerEditor
|
||||||
layer={selectedLayer}
|
layer={selectedLayer}
|
||||||
sources={this.layerWatcher.sources}
|
sources={this.state.sources}
|
||||||
vectorLayers={this.layerWatcher.vectorLayers}
|
vectorLayers={this.state.vectorLayers}
|
||||||
|
spec={this.state.spec}
|
||||||
onLayerChanged={this.onLayerChanged.bind(this)}
|
onLayerChanged={this.onLayerChanged.bind(this)}
|
||||||
onLayerIdChange={this.onLayerIdChange.bind(this)}
|
onLayerIdChange={this.onLayerIdChange.bind(this)}
|
||||||
/> : null
|
/> : null
|
||||||
|
|
||||||
|
const bottomPanel = (this.state.errors.length + this.state.infos.length) > 0 ? <MessagePanel
|
||||||
|
errors={this.state.errors}
|
||||||
|
infos={this.state.infos}
|
||||||
|
/> : null
|
||||||
|
|
||||||
return <AppLayout
|
return <AppLayout
|
||||||
toolbar={toolbar}
|
toolbar={toolbar}
|
||||||
layerList={layerList}
|
layerList={layerList}
|
||||||
layerEditor={layerEditor}
|
layerEditor={layerEditor}
|
||||||
map={this.mapRenderer()}
|
map={this.mapRenderer()}
|
||||||
|
bottom={bottomPanel}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import ScrollContainer from './ScrollContainer'
|
import ScrollContainer from './ScrollContainer'
|
||||||
|
|
||||||
import theme from '../config/theme'
|
|
||||||
import colors from '../config/colors'
|
|
||||||
|
|
||||||
class AppLayout extends React.Component {
|
class AppLayout extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
toolbar: React.PropTypes.element.isRequired,
|
toolbar: React.PropTypes.element.isRequired,
|
||||||
layerList: React.PropTypes.element.isRequired,
|
layerList: React.PropTypes.element.isRequired,
|
||||||
layerEditor: React.PropTypes.element,
|
layerEditor: React.PropTypes.element,
|
||||||
map: React.PropTypes.element.isRequired,
|
map: React.PropTypes.element.isRequired,
|
||||||
|
bottom: React.PropTypes.element,
|
||||||
}
|
}
|
||||||
|
|
||||||
static childContextTypes = {
|
static childContextTypes = {
|
||||||
@@ -23,42 +21,23 @@ class AppLayout extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return <div style={{
|
return <div className="maputnik-layout">
|
||||||
fontFamily: theme.fontFamily,
|
|
||||||
color: theme.color,
|
|
||||||
fontWeight: 300
|
|
||||||
}}>
|
|
||||||
{this.props.toolbar}
|
{this.props.toolbar}
|
||||||
<div style={{
|
<div className="maputnik-layout-list">
|
||||||
position: 'fixed',
|
|
||||||
bottom: 0,
|
|
||||||
height: "100%",
|
|
||||||
top: 40,
|
|
||||||
left: 0,
|
|
||||||
zIndex: 1,
|
|
||||||
width: 200,
|
|
||||||
overflow: "hidden",
|
|
||||||
backgroundColor: colors.black
|
|
||||||
}}>
|
|
||||||
<ScrollContainer>
|
<ScrollContainer>
|
||||||
{this.props.layerList}
|
{this.props.layerList}
|
||||||
</ScrollContainer>
|
</ScrollContainer>
|
||||||
</div>
|
</div>
|
||||||
<div style={{
|
<div className="maputnik-layout-drawer">
|
||||||
position: 'fixed',
|
|
||||||
bottom: 0,
|
|
||||||
height: "100%",
|
|
||||||
top: 40,
|
|
||||||
left: 200,
|
|
||||||
zIndex: 1,
|
|
||||||
width: 300,
|
|
||||||
backgroundColor: colors.black
|
|
||||||
}}>
|
|
||||||
<ScrollContainer>
|
<ScrollContainer>
|
||||||
{this.props.layerEditor}
|
{this.props.layerEditor}
|
||||||
</ScrollContainer>
|
</ScrollContainer>
|
||||||
</div>
|
</div>
|
||||||
{this.props.map}
|
{this.props.map}
|
||||||
|
{this.props.bottom && <div className="maputnik-layout-bottom">
|
||||||
|
{this.props.bottom}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,26 +1,18 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import colors from '../config/colors'
|
import classnames from 'classnames'
|
||||||
import { margins, fontSizes } from '../config/scales'
|
|
||||||
|
|
||||||
class Button extends React.Component {
|
class Button extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
onClick: React.PropTypes.func,
|
onClick: React.PropTypes.func,
|
||||||
style: React.PropTypes.object,
|
style: React.PropTypes.object,
|
||||||
|
className: React.PropTypes.string,
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return <a
|
return <a
|
||||||
onClick={this.props.onClick}
|
onClick={this.props.onClick}
|
||||||
style={{
|
className={classnames("maputnik-button", this.props.className)}
|
||||||
cursor: 'pointer',
|
style={this.props.style}>
|
||||||
backgroundColor: colors.midgray,
|
|
||||||
color: colors.lowgray,
|
|
||||||
fontSize: fontSizes[4],
|
|
||||||
padding: margins[1],
|
|
||||||
userSelect: 'none',
|
|
||||||
borderRadius: 2,
|
|
||||||
...this.props.style,
|
|
||||||
}}>
|
|
||||||
{this.props.children}
|
{this.props.children}
|
||||||
</a>
|
</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
|
|
||||||
26
src/components/MessagePanel.jsx
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
class MessagePanel extends React.Component {
|
||||||
|
static propTypes = {
|
||||||
|
errors: React.PropTypes.array,
|
||||||
|
infos: React.PropTypes.array,
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const errors = this.props.errors.map((m, i) => {
|
||||||
|
return <p className="maputnik-message-panel-error">{m}</p>
|
||||||
|
})
|
||||||
|
|
||||||
|
const infos = this.props.infos.map((m, i) => {
|
||||||
|
return <p key={i}>{m}</p>
|
||||||
|
})
|
||||||
|
|
||||||
|
return <div className="maputnik-message-panel">
|
||||||
|
{errors}
|
||||||
|
{infos}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export default MessagePanel
|
||||||
@@ -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,16 +1,7 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import scrollbars from './scrollbars.scss'
|
|
||||||
|
|
||||||
const ScrollContainer = (props) => {
|
const ScrollContainer = (props) => {
|
||||||
return <div className={scrollbars.darkScrollbar} style={{
|
return <div className="maputnik-scroll-container">
|
||||||
overflowX: "visible",
|
|
||||||
overflowY: "scroll",
|
|
||||||
bottom:0,
|
|
||||||
left:0,
|
|
||||||
right:0,
|
|
||||||
top:1,
|
|
||||||
position: "absolute",
|
|
||||||
}}>
|
|
||||||
{props.children}
|
{props.children}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,84 +1,62 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import FileReaderInput from 'react-file-reader-input'
|
import FileReaderInput from 'react-file-reader-input'
|
||||||
|
import classnames from 'classnames'
|
||||||
|
|
||||||
import MdFileDownload from 'react-icons/lib/md/file-download'
|
import MdFileDownload from 'react-icons/lib/md/file-download'
|
||||||
import MdFileUpload from 'react-icons/lib/md/file-upload'
|
import MdFileUpload from 'react-icons/lib/md/file-upload'
|
||||||
import MdOpenInBrowser from 'react-icons/lib/md/open-in-browser'
|
import OpenIcon from 'react-icons/lib/md/open-in-browser'
|
||||||
import MdSettings from 'react-icons/lib/md/settings'
|
import SettingsIcon from 'react-icons/lib/md/settings'
|
||||||
import MdInfo from 'react-icons/lib/md/info'
|
import MdInfo from 'react-icons/lib/md/info'
|
||||||
import MdLayers from 'react-icons/lib/md/layers'
|
import SourcesIcon from 'react-icons/lib/md/layers'
|
||||||
import MdSave from 'react-icons/lib/md/save'
|
import MdSave from 'react-icons/lib/md/save'
|
||||||
import MdStyle from 'react-icons/lib/md/style'
|
import MdStyle from 'react-icons/lib/md/style'
|
||||||
import MdMap from 'react-icons/lib/md/map'
|
import MdMap from 'react-icons/lib/md/map'
|
||||||
import MdInsertEmoticon from 'react-icons/lib/md/insert-emoticon'
|
import MdInsertEmoticon from 'react-icons/lib/md/insert-emoticon'
|
||||||
import MdFontDownload from 'react-icons/lib/md/font-download'
|
import MdFontDownload from 'react-icons/lib/md/font-download'
|
||||||
import MdHelpOutline from 'react-icons/lib/md/help-outline'
|
import HelpIcon from 'react-icons/lib/md/help-outline'
|
||||||
import MdFindInPage from 'react-icons/lib/md/find-in-page'
|
import InspectionIcon from 'react-icons/lib/md/find-in-page'
|
||||||
|
|
||||||
|
import logoImage from '../img/maputnik.png'
|
||||||
import SettingsModal from './modals/SettingsModal'
|
import SettingsModal from './modals/SettingsModal'
|
||||||
|
import ExportModal from './modals/ExportModal'
|
||||||
import SourcesModal from './modals/SourcesModal'
|
import SourcesModal from './modals/SourcesModal'
|
||||||
import OpenModal from './modals/OpenModal'
|
import OpenModal from './modals/OpenModal'
|
||||||
|
|
||||||
import style from '../libs/style'
|
import style from '../libs/style'
|
||||||
import colors from '../config/colors'
|
|
||||||
import { margins, fontSizes } from '../config/scales'
|
|
||||||
|
|
||||||
const IconText = props => <span style={{ paddingLeft: margins[0] }}>
|
function IconText(props) {
|
||||||
{props.children}
|
return <span className="maputnik-icon-text">{props.children}</span>
|
||||||
</span>
|
|
||||||
|
|
||||||
const actionStyle = {
|
|
||||||
display: "inline-block",
|
|
||||||
padding: 12.5,
|
|
||||||
fontSize: fontSizes[4],
|
|
||||||
cursor: "pointer",
|
|
||||||
color: colors.white,
|
|
||||||
textDecoration: 'none',
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const ToolbarLink = props => <a
|
function ToolbarLink(props) {
|
||||||
|
return <a
|
||||||
|
className={classnames('maputnik-toolbar-link', props.className)}
|
||||||
href={props.href}
|
href={props.href}
|
||||||
target={"blank"}
|
target={"blank"}
|
||||||
style={{
|
>
|
||||||
...actionStyle,
|
|
||||||
...props.style,
|
|
||||||
}}>
|
|
||||||
{props.children}
|
{props.children}
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
class ToolbarAction extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
onClick: React.PropTypes.func.isRequired,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(props) {
|
function ToolbarAction(props) {
|
||||||
super(props)
|
|
||||||
this.state = { hover: false }
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return <a
|
return <a
|
||||||
onClick={this.props.onClick}
|
className='maputnik-toolbar-action'
|
||||||
onMouseOver={e => this.setState({hover: true})}
|
onClick={props.onClick}
|
||||||
onMouseOut={e => this.setState({hover: false})}
|
>
|
||||||
style={{
|
{props.children}
|
||||||
...actionStyle,
|
|
||||||
...this.props.style,
|
|
||||||
backgroundColor: this.state.hover ? colors.gray : null,
|
|
||||||
}}>
|
|
||||||
{this.props.children}
|
|
||||||
</a>
|
</a>
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
export default class Toolbar extends React.Component {
|
export default class Toolbar extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
mapStyle: React.PropTypes.object.isRequired,
|
mapStyle: React.PropTypes.object.isRequired,
|
||||||
|
inspectModeEnabled: React.PropTypes.bool.isRequired,
|
||||||
onStyleChanged: React.PropTypes.func.isRequired,
|
onStyleChanged: React.PropTypes.func.isRequired,
|
||||||
// A new style has been uploaded
|
// A new style has been uploaded
|
||||||
onStyleOpen: React.PropTypes.func.isRequired,
|
onStyleOpen: React.PropTypes.func.isRequired,
|
||||||
// Current style is requested for download
|
// A dict of source id's and the available source layers
|
||||||
onStyleDownload: React.PropTypes.func.isRequired,
|
sources: React.PropTypes.object.isRequired,
|
||||||
|
onInspectModeToggle: React.PropTypes.func.isRequired
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
@@ -88,17 +66,12 @@ export default class Toolbar extends React.Component {
|
|||||||
settings: false,
|
settings: false,
|
||||||
sources: false,
|
sources: false,
|
||||||
open: false,
|
open: false,
|
||||||
|
add: false,
|
||||||
|
export: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
downloadButton() {
|
|
||||||
return <ToolbarAction onClick={this.props.onStyleDownload}>
|
|
||||||
<MdFileDownload />
|
|
||||||
<IconText>Download</IconText>
|
|
||||||
</ToolbarAction>
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleModal(modalName) {
|
toggleModal(modalName) {
|
||||||
this.setState({
|
this.setState({
|
||||||
isOpen: {
|
isOpen: {
|
||||||
@@ -109,21 +82,19 @@ export default class Toolbar extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return <div style={{
|
return <div className='maputnik-toolbar'>
|
||||||
position: "fixed",
|
|
||||||
height: 40,
|
|
||||||
width: '100%',
|
|
||||||
zIndex: 100,
|
|
||||||
left: 0,
|
|
||||||
top: 0,
|
|
||||||
backgroundColor: colors.black
|
|
||||||
}}>
|
|
||||||
<SettingsModal
|
<SettingsModal
|
||||||
mapStyle={this.props.mapStyle}
|
mapStyle={this.props.mapStyle}
|
||||||
onStyleChanged={this.props.onStyleChanged}
|
onStyleChanged={this.props.onStyleChanged}
|
||||||
isOpen={this.state.isOpen.settings}
|
isOpen={this.state.isOpen.settings}
|
||||||
onOpenToggle={this.toggleModal.bind(this, 'settings')}
|
onOpenToggle={this.toggleModal.bind(this, 'settings')}
|
||||||
/>
|
/>
|
||||||
|
<ExportModal
|
||||||
|
mapStyle={this.props.mapStyle}
|
||||||
|
onStyleChanged={this.props.onStyleChanged}
|
||||||
|
isOpen={this.state.isOpen.export}
|
||||||
|
onOpenToggle={this.toggleModal.bind(this, 'export')}
|
||||||
|
/>
|
||||||
<OpenModal
|
<OpenModal
|
||||||
isOpen={this.state.isOpen.open}
|
isOpen={this.state.isOpen.open}
|
||||||
onStyleOpen={this.props.onStyleOpen}
|
onStyleOpen={this.props.onStyleOpen}
|
||||||
@@ -137,35 +108,36 @@ export default class Toolbar extends React.Component {
|
|||||||
/>
|
/>
|
||||||
<ToolbarLink
|
<ToolbarLink
|
||||||
href={"https://github.com/maputnik/editor"}
|
href={"https://github.com/maputnik/editor"}
|
||||||
style={{
|
className="maputnik-toolbar-logo"
|
||||||
width: 180,
|
|
||||||
textAlign: 'left',
|
|
||||||
backgroundColor: colors.black,
|
|
||||||
padding: 5,
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<img src="https://github.com/maputnik/editor/raw/master/media/maputnik.png" alt="Maputnik" style={{width: 30, height: 30, paddingRight: 5, verticalAlign: 'middle'}}/>
|
<img src={logoImage} alt="Maputnik" />
|
||||||
<span style={{fontSize: 20, verticalAlign: 'middle' }}>Maputnik</span>
|
<h1>Maputnik</h1>
|
||||||
</ToolbarLink>
|
</ToolbarLink>
|
||||||
<ToolbarAction onClick={this.toggleModal.bind(this, 'open')}>
|
<ToolbarAction onClick={this.toggleModal.bind(this, 'open')}>
|
||||||
<MdOpenInBrowser />
|
<OpenIcon />
|
||||||
<IconText>Open</IconText>
|
<IconText>Open</IconText>
|
||||||
</ToolbarAction>
|
</ToolbarAction>
|
||||||
{this.downloadButton()}
|
<ToolbarAction onClick={this.toggleModal.bind(this, 'export')}>
|
||||||
|
<MdFileDownload />
|
||||||
|
<IconText>Export</IconText>
|
||||||
|
</ToolbarAction>
|
||||||
<ToolbarAction onClick={this.toggleModal.bind(this, 'sources')}>
|
<ToolbarAction onClick={this.toggleModal.bind(this, 'sources')}>
|
||||||
<MdLayers />
|
<SourcesIcon />
|
||||||
<IconText>Sources</IconText>
|
<IconText>Sources</IconText>
|
||||||
</ToolbarAction>
|
</ToolbarAction>
|
||||||
<ToolbarAction onClick={this.toggleModal.bind(this, 'settings')}>
|
<ToolbarAction onClick={this.toggleModal.bind(this, 'settings')}>
|
||||||
<MdSettings />
|
<SettingsIcon />
|
||||||
<IconText>Style Settings</IconText>
|
<IconText>Style Settings</IconText>
|
||||||
</ToolbarAction>
|
</ToolbarAction>
|
||||||
<ToolbarAction onClick={this.toggleModal.bind(this, 'settings')}>
|
<ToolbarAction onClick={this.props.onInspectModeToggle}>
|
||||||
<MdFindInPage />
|
<InspectionIcon />
|
||||||
<IconText>Inspect</IconText>
|
<IconText>
|
||||||
|
{ this.props.inspectModeEnabled && <span>Map Mode</span> }
|
||||||
|
{ !this.props.inspectModeEnabled && <span>Inspect Mode</span> }
|
||||||
|
</IconText>
|
||||||
</ToolbarAction>
|
</ToolbarAction>
|
||||||
<ToolbarLink href={"https://github.com/maputnik/editor/wiki"}>
|
<ToolbarLink href={"https://github.com/maputnik/editor/wiki"}>
|
||||||
<MdHelpOutline />
|
<HelpIcon />
|
||||||
<IconText>Help</IconText>
|
<IconText>Help</IconText>
|
||||||
</ToolbarLink>
|
</ToolbarLink>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,75 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
|
|
||||||
import input from '../../config/input'
|
|
||||||
import colors from '../../config/colors'
|
|
||||||
import { margins } from '../../config/scales'
|
|
||||||
|
|
||||||
class BooleanField extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
onChange: React.PropTypes.func.isRequired,
|
|
||||||
name: React.PropTypes.string.isRequired,
|
|
||||||
value: React.PropTypes.bool,
|
|
||||||
doc: React.PropTypes.string,
|
|
||||||
style: React.PropTypes.object,
|
|
||||||
}
|
|
||||||
|
|
||||||
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}>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
style={{
|
|
||||||
...styles.input,
|
|
||||||
...this.props.style,
|
|
||||||
}}
|
|
||||||
value={this.props.value}
|
|
||||||
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}>
|
|
||||||
<path d='M1 14 L5 10 L13 18 L27 4 L31 8 L13 26 z' />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default BooleanField
|
|
||||||
@@ -2,8 +2,6 @@ import React from 'react'
|
|||||||
import Color from 'color'
|
import Color from 'color'
|
||||||
import ChromePicker from 'react-color/lib/components/chrome/Chrome'
|
import ChromePicker from 'react-color/lib/components/chrome/Chrome'
|
||||||
|
|
||||||
import input from '../../config/input.js'
|
|
||||||
|
|
||||||
function formatColor(color) {
|
function formatColor(color) {
|
||||||
const rgb = color.rgb
|
const rgb = color.rgb
|
||||||
return `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${rgb.a})`
|
return `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${rgb.a})`
|
||||||
@@ -17,6 +15,7 @@ class ColorField extends React.Component {
|
|||||||
value: React.PropTypes.string,
|
value: React.PropTypes.string,
|
||||||
doc: React.PropTypes.string,
|
doc: React.PropTypes.string,
|
||||||
style: React.PropTypes.object,
|
style: React.PropTypes.object,
|
||||||
|
default: React.PropTypes.string,
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
@@ -35,13 +34,13 @@ class ColorField extends React.Component {
|
|||||||
const pos = elem.getBoundingClientRect()
|
const pos = elem.getBoundingClientRect()
|
||||||
return {
|
return {
|
||||||
top: pos.top,
|
top: pos.top,
|
||||||
left: pos.left + 165,
|
left: pos.left + 196,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.warn('Color field has no element to adjust position')
|
console.warn('Color field has no element to adjust position')
|
||||||
return {
|
return {
|
||||||
top: 160,
|
top: 160,
|
||||||
left: 500,
|
left: 555,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -57,6 +56,7 @@ class ColorField extends React.Component {
|
|||||||
render() {
|
render() {
|
||||||
const offset = this.calcPickerOffset()
|
const offset = this.calcPickerOffset()
|
||||||
const picker = <div
|
const picker = <div
|
||||||
|
className="maputnik-color-picker-offset"
|
||||||
style={{
|
style={{
|
||||||
position: 'fixed',
|
position: 'fixed',
|
||||||
zIndex: 1,
|
zIndex: 1,
|
||||||
@@ -68,6 +68,7 @@ class ColorField extends React.Component {
|
|||||||
onChange={c => this.props.onChange(formatColor(c))}
|
onChange={c => this.props.onChange(formatColor(c))}
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
|
className="maputnik-color-picker-offset"
|
||||||
onClick={this.togglePicker.bind(this)}
|
onClick={this.togglePicker.bind(this)}
|
||||||
style={{
|
style={{
|
||||||
zIndex: -1,
|
zIndex: -1,
|
||||||
@@ -80,19 +81,13 @@ class ColorField extends React.Component {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
return <div style={{
|
return <div className="maputnik-color-wrapper">
|
||||||
...input.property,
|
|
||||||
position: 'relative',
|
|
||||||
display: 'inline',
|
|
||||||
}}>
|
|
||||||
{this.state.pickerOpened && picker}
|
{this.state.pickerOpened && picker}
|
||||||
<input
|
<input
|
||||||
|
className="maputnik-color"
|
||||||
ref="colorInput"
|
ref="colorInput"
|
||||||
onClick={this.togglePicker.bind(this)}
|
onClick={this.togglePicker.bind(this)}
|
||||||
style={{
|
style={this.props.style}
|
||||||
...input.select,
|
|
||||||
...this.props.style
|
|
||||||
}}
|
|
||||||
name={this.props.name}
|
name={this.props.name}
|
||||||
placeholder={this.props.default}
|
placeholder={this.props.default}
|
||||||
value={this.props.value ? this.props.value : ""}
|
value={this.props.value ? this.props.value : ""}
|
||||||
|
|||||||
22
src/components/fields/DocLabel.jsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
export default class DocLabel extends React.Component {
|
||||||
|
static propTypes = {
|
||||||
|
label: React.PropTypes.oneOfType([
|
||||||
|
React.PropTypes.object,
|
||||||
|
React.PropTypes.string
|
||||||
|
]).isRequired,
|
||||||
|
doc: React.PropTypes.string.isRequired,
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
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,36 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import input from '../../config/input.js'
|
|
||||||
|
|
||||||
class EnumField extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
onChange: React.PropTypes.func.isRequired,
|
|
||||||
name: React.PropTypes.string.isRequired,
|
|
||||||
value: React.PropTypes.string,
|
|
||||||
allowedValues: React.PropTypes.array.isRequired,
|
|
||||||
doc: React.PropTypes.string,
|
|
||||||
style: React.PropTypes.object,
|
|
||||||
}
|
|
||||||
|
|
||||||
onChange(e) {
|
|
||||||
return this.props.onChange(e.target.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const options = this.props.allowedValues.map(val => {
|
|
||||||
return <option key={val} value={val}>{val}</option>
|
|
||||||
})
|
|
||||||
|
|
||||||
return <select
|
|
||||||
style={{
|
|
||||||
...input.select,
|
|
||||||
...this.props.style
|
|
||||||
}}
|
|
||||||
value={this.props.value}
|
|
||||||
onChange={this.onChange.bind(this)}
|
|
||||||
>
|
|
||||||
{options}
|
|
||||||
</select>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default EnumField
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import input from '../../config/input.js'
|
|
||||||
|
|
||||||
/*** Number fields with support for min, max and units and documentation*/
|
|
||||||
class NumberField extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
onChange: React.PropTypes.func.isRequired,
|
|
||||||
name: React.PropTypes.string.isRequired,
|
|
||||||
value: React.PropTypes.number,
|
|
||||||
default: React.PropTypes.number,
|
|
||||||
unit: React.PropTypes.string,
|
|
||||||
min: React.PropTypes.number,
|
|
||||||
max: React.PropTypes.number,
|
|
||||||
doc: React.PropTypes.string,
|
|
||||||
style: React.PropTypes.object,
|
|
||||||
}
|
|
||||||
|
|
||||||
onChange(e) {
|
|
||||||
const value = parseFloat(e.target.value)
|
|
||||||
/*TODO: we can do range validation already here?
|
|
||||||
if(this.props.min && value < this.props.min) return
|
|
||||||
if(this.props.max && value > this.props.max) return
|
|
||||||
*/
|
|
||||||
this.props.onChange(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
let stepSize = null
|
|
||||||
if(this.props.max && this.props.min) {
|
|
||||||
stepSize = (this.props.max - this.props.min) / 10
|
|
||||||
}
|
|
||||||
|
|
||||||
return <input
|
|
||||||
style={{
|
|
||||||
...input.input,
|
|
||||||
...this.props.style
|
|
||||||
}}
|
|
||||||
type="number"
|
|
||||||
min={this.props.min}
|
|
||||||
max={this.props.max}
|
|
||||||
step={stepSize}
|
|
||||||
name={this.props.name}
|
|
||||||
placeholder={this.props.default}
|
|
||||||
value={this.props.value}
|
|
||||||
onChange={this.onChange.bind(this)}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default NumberField
|
|
||||||
@@ -1,20 +1,31 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import GlSpec from 'mapbox-gl-style-spec/reference/latest.min.js'
|
|
||||||
|
|
||||||
import ZoomSpecField from './ZoomSpecField'
|
import ZoomSpecField from './ZoomSpecField'
|
||||||
import colors from '../../config/colors'
|
const iconProperties = ['background-pattern', 'fill-pattern', 'line-pattern', 'fill-extrusion-pattern', 'icon-image']
|
||||||
import { margins } from '../../config/scales'
|
|
||||||
|
|
||||||
/** Extract field spec by {@fieldName} from the {@layerType} in the
|
/** Extract field spec by {@fieldName} from the {@layerType} in the
|
||||||
* style specification from either the paint or layout group */
|
* style specification from either the paint or layout group */
|
||||||
function getFieldSpec(layerType, fieldName) {
|
function getFieldSpec(spec, layerType, fieldName) {
|
||||||
const groupName = getGroupName(layerType, fieldName)
|
const groupName = getGroupName(spec, layerType, fieldName)
|
||||||
const group = GlSpec[groupName + '_' + layerType]
|
const group = spec[groupName + '_' + layerType]
|
||||||
return group[fieldName]
|
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) {
|
function getGroupName(spec, layerType, fieldName) {
|
||||||
const paint = GlSpec['paint_' + layerType] || {}
|
const paint = spec['paint_' + layerType] || {}
|
||||||
if (fieldName in paint) {
|
if (fieldName in paint) {
|
||||||
return 'paint'
|
return 'paint'
|
||||||
} else {
|
} else {
|
||||||
@@ -27,35 +38,32 @@ export default class PropertyGroup extends React.Component {
|
|||||||
layer: React.PropTypes.object.isRequired,
|
layer: React.PropTypes.object.isRequired,
|
||||||
groupFields: React.PropTypes.array.isRequired,
|
groupFields: React.PropTypes.array.isRequired,
|
||||||
onChange: React.PropTypes.func.isRequired,
|
onChange: React.PropTypes.func.isRequired,
|
||||||
|
spec: React.PropTypes.object.isRequired,
|
||||||
}
|
}
|
||||||
|
|
||||||
onPropertyChange(property, newValue) {
|
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)
|
this.props.onChange(group , property, newValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const fields = this.props.groupFields.map(fieldName => {
|
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 paint = this.props.layer.paint || {}
|
||||||
const layout = this.props.layer.layout || {}
|
const layout = this.props.layer.layout || {}
|
||||||
const fieldValue = paint[fieldName] || layout[fieldName]
|
const fieldValue = fieldName in paint ? paint[fieldName] : layout[fieldName]
|
||||||
|
|
||||||
return <ZoomSpecField
|
return <ZoomSpecField
|
||||||
onChange={this.onPropertyChange.bind(this)}
|
onChange={this.onPropertyChange.bind(this)}
|
||||||
key={fieldName}
|
key={fieldName}
|
||||||
fieldName={fieldName}
|
fieldName={fieldName}
|
||||||
value={fieldValue}
|
value={fieldValue === undefined ? fieldSpec.default : fieldValue}
|
||||||
fieldSpec={fieldSpec}
|
fieldSpec={fieldSpec}
|
||||||
/>
|
/>
|
||||||
})
|
})
|
||||||
|
|
||||||
return <div style={{
|
return <div className="maputnik-property-group">
|
||||||
padding: margins[2],
|
|
||||||
paddingRight: 0,
|
|
||||||
backgroundColor: colors.black,
|
|
||||||
}}>
|
|
||||||
{fields}
|
{fields}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,18 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import color from 'color'
|
import color from 'color'
|
||||||
|
|
||||||
import GlSpec from 'mapbox-gl-style-spec/reference/latest.min.js'
|
|
||||||
import NumberField from './NumberField'
|
|
||||||
import EnumField from './EnumField'
|
|
||||||
import BooleanField from './BooleanField'
|
|
||||||
import ColorField from './ColorField'
|
import ColorField from './ColorField'
|
||||||
import StringField from './StringField'
|
import NumberInput from '../inputs/NumberInput'
|
||||||
|
import CheckboxInput from '../inputs/CheckboxInput'
|
||||||
|
import StringInput from '../inputs/StringInput'
|
||||||
|
import SelectInput from '../inputs/SelectInput'
|
||||||
|
import MultiButtonInput from '../inputs/MultiButtonInput'
|
||||||
|
import ArrayInput from '../inputs/ArrayInput'
|
||||||
|
import FontInput from '../inputs/FontInput'
|
||||||
|
import 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) {
|
function labelFromFieldName(fieldName) {
|
||||||
let label = fieldName.split('-').slice(1).join(' ')
|
let label = fieldName.split('-').slice(1).join(' ')
|
||||||
@@ -18,6 +22,14 @@ function labelFromFieldName(fieldName) {
|
|||||||
return label
|
return label
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function optionsLabelLength(options) {
|
||||||
|
let sum = 0;
|
||||||
|
options.forEach(([_, label]) => {
|
||||||
|
sum += label.length
|
||||||
|
})
|
||||||
|
return sum
|
||||||
|
}
|
||||||
|
|
||||||
/** Display any field from the Mapbox GL style spec and
|
/** Display any field from the Mapbox GL style spec and
|
||||||
* choose the correct field component based on the @{fieldSpec}
|
* choose the correct field component based on the @{fieldSpec}
|
||||||
* to display @{value}. */
|
* to display @{value}. */
|
||||||
@@ -26,10 +38,10 @@ export default class SpecField extends React.Component {
|
|||||||
onChange: React.PropTypes.func.isRequired,
|
onChange: React.PropTypes.func.isRequired,
|
||||||
fieldName: React.PropTypes.string.isRequired,
|
fieldName: React.PropTypes.string.isRequired,
|
||||||
fieldSpec: React.PropTypes.object.isRequired,
|
fieldSpec: React.PropTypes.object.isRequired,
|
||||||
|
|
||||||
value: React.PropTypes.oneOfType([
|
value: React.PropTypes.oneOfType([
|
||||||
React.PropTypes.string,
|
React.PropTypes.string,
|
||||||
React.PropTypes.number,
|
React.PropTypes.number,
|
||||||
|
React.PropTypes.array,
|
||||||
]),
|
]),
|
||||||
/** Override the style of the field */
|
/** Override the style of the field */
|
||||||
style: React.PropTypes.object,
|
style: React.PropTypes.object,
|
||||||
@@ -37,43 +49,68 @@ export default class SpecField extends React.Component {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const commonProps = {
|
const commonProps = {
|
||||||
doc: this.props.fieldSpec.doc,
|
|
||||||
style: this.props.style,
|
style: this.props.style,
|
||||||
value: this.props.value,
|
value: this.props.value,
|
||||||
|
default: this.props.fieldSpec.default,
|
||||||
name: this.props.fieldName,
|
name: this.props.fieldName,
|
||||||
onChange: newValue => this.props.onChange(this.props.fieldName, newValue)
|
onChange: newValue => this.props.onChange(this.props.fieldName, newValue)
|
||||||
}
|
}
|
||||||
switch(this.props.fieldSpec.type) {
|
switch(this.props.fieldSpec.type) {
|
||||||
case 'number': return (
|
case 'number': return (
|
||||||
<NumberField
|
<NumberInput
|
||||||
{...commonProps}
|
{...commonProps}
|
||||||
default={this.props.fieldSpec.default}
|
|
||||||
min={this.props.fieldSpec.minimum}
|
min={this.props.fieldSpec.minimum}
|
||||||
max={this.props.fieldSpec.maximum}
|
max={this.props.fieldSpec.maximum}
|
||||||
unit={this.props.fieldSpec.unit}
|
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
case 'enum': return (
|
case 'enum':
|
||||||
<EnumField
|
const options = Object.keys(this.props.fieldSpec.values).map(v => [v, capitalize(v)])
|
||||||
|
|
||||||
|
if(options.length <= 3 && optionsLabelLength(options) <= 20) {
|
||||||
|
return <MultiButtonInput
|
||||||
{...commonProps}
|
{...commonProps}
|
||||||
allowedValues={Object.keys(this.props.fieldSpec.values)}
|
options={options}
|
||||||
/>
|
/>
|
||||||
)
|
} else {
|
||||||
case 'string': return (
|
return <SelectInput
|
||||||
<StringField
|
{...commonProps}
|
||||||
|
options={options}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
case 'string':
|
||||||
|
if(iconProperties.indexOf(this.props.fieldName) >= 0) {
|
||||||
|
return <IconInput
|
||||||
|
{...commonProps}
|
||||||
|
icons={this.props.fieldSpec.values}
|
||||||
|
/>
|
||||||
|
} else {
|
||||||
|
return <StringInput
|
||||||
{...commonProps}
|
{...commonProps}
|
||||||
/>
|
/>
|
||||||
)
|
}
|
||||||
case 'color': return (
|
case 'color': return (
|
||||||
<ColorField
|
<ColorField
|
||||||
{...commonProps}
|
{...commonProps}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
case 'boolean': return (
|
case 'boolean': return (
|
||||||
<BooleanField
|
<CheckboxInput
|
||||||
{...commonProps}
|
{...commonProps}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
case 'array':
|
||||||
|
if(this.props.fieldName === 'text-font') {
|
||||||
|
return <FontInput
|
||||||
|
{...commonProps}
|
||||||
|
fonts={this.props.fieldSpec.values}
|
||||||
|
/>
|
||||||
|
} else {
|
||||||
|
return <ArrayInput
|
||||||
|
{...commonProps}
|
||||||
|
type={this.props.fieldSpec.value}
|
||||||
|
length={this.props.fieldSpec.length}
|
||||||
|
/>
|
||||||
|
}
|
||||||
default: return null
|
default: return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import input from '../../config/input.js'
|
|
||||||
|
|
||||||
/*** Number fields with support for min, max and units and documentation*/
|
|
||||||
class StringField extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
onChange: React.PropTypes.func.isRequired,
|
|
||||||
name: React.PropTypes.string.isRequired,
|
|
||||||
value: React.PropTypes.string,
|
|
||||||
default: React.PropTypes.number,
|
|
||||||
doc: React.PropTypes.string,
|
|
||||||
style: React.PropTypes.object,
|
|
||||||
}
|
|
||||||
|
|
||||||
onChange(e) {
|
|
||||||
const value = e.target.value
|
|
||||||
return this.props.onChange(value === "" ? null: value)
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return <input
|
|
||||||
style={{
|
|
||||||
...input.input,
|
|
||||||
...this.props.style
|
|
||||||
}}
|
|
||||||
name={this.props.name}
|
|
||||||
placeholder={this.props.default}
|
|
||||||
value={this.props.value ? this.props.value : ""}
|
|
||||||
onChange={this.onChange.bind(this)}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default StringField
|
|
||||||
@@ -1,31 +1,26 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import Color from 'color'
|
import Color from 'color'
|
||||||
|
|
||||||
import NumberField from './NumberField'
|
import Button from '../Button'
|
||||||
import EnumField from './EnumField'
|
|
||||||
import BooleanField from './BooleanField'
|
|
||||||
import ColorField from './ColorField'
|
|
||||||
import StringField from './StringField'
|
|
||||||
import SpecField from './SpecField'
|
import SpecField from './SpecField'
|
||||||
|
import NumberInput from '../inputs/NumberInput'
|
||||||
|
import DocLabel from './DocLabel'
|
||||||
|
import InputBlock from '../inputs/InputBlock'
|
||||||
|
|
||||||
import input from '../../config/input.js'
|
import AddIcon from 'react-icons/lib/md/add-circle-outline'
|
||||||
import colors from '../../config/colors.js'
|
import DeleteIcon from 'react-icons/lib/md/delete'
|
||||||
import { margins } from '../../config/scales.js'
|
import FunctionIcon from 'react-icons/lib/md/functions'
|
||||||
|
|
||||||
|
import capitalize from 'lodash.capitalize'
|
||||||
|
|
||||||
function isZoomField(value) {
|
function isZoomField(value) {
|
||||||
return typeof value === 'object' && value.stops
|
return typeof value === 'object' && value.stops
|
||||||
}
|
}
|
||||||
|
|
||||||
const specFieldProps = {
|
|
||||||
onChange: React.PropTypes.func.isRequired,
|
|
||||||
fieldName: React.PropTypes.string.isRequired,
|
|
||||||
fieldSpec: React.PropTypes.object.isRequired,
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Supports displaying spec field for zoom function objects
|
/** Supports displaying spec field for zoom function objects
|
||||||
* https://www.mapbox.com/mapbox-gl-style-spec/#types-function-zoom-property
|
* 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 = {
|
static propTypes = {
|
||||||
onChange: React.PropTypes.func.isRequired,
|
onChange: React.PropTypes.func.isRequired,
|
||||||
fieldName: React.PropTypes.string.isRequired,
|
fieldName: React.PropTypes.string.isRequired,
|
||||||
@@ -39,59 +34,147 @@ export default class ZoomSpecField extends React.Component {
|
|||||||
]),
|
]),
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
addStop() {
|
||||||
const label = <label style={input.label}>
|
const stops = this.props.value.stops.slice(0)
|
||||||
{labelFromFieldName(this.props.fieldName)}
|
const lastStop = stops[stops.length - 1]
|
||||||
</label>
|
stops.push([lastStop[0] + 1, lastStop[1]])
|
||||||
|
|
||||||
if(isZoomField(this.props.value)) {
|
const changedValue = {
|
||||||
const zoomFields = this.props.value.stops.map(stop => {
|
...this.props.value,
|
||||||
|
stops: stops,
|
||||||
|
}
|
||||||
|
|
||||||
|
this.props.onChange(this.props.fieldName, changedValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteStop(stopIdx) {
|
||||||
|
const stops = this.props.value.stops.slice(0)
|
||||||
|
stops.splice(stopIdx, 1)
|
||||||
|
|
||||||
|
let changedValue = {
|
||||||
|
...this.props.value,
|
||||||
|
stops: stops,
|
||||||
|
}
|
||||||
|
|
||||||
|
if(stops.length === 1) {
|
||||||
|
changedValue = stops[0][1]
|
||||||
|
}
|
||||||
|
|
||||||
|
this.props.onChange(this.props.fieldName, changedValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
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]
|
||||||
|
const changedValue = {
|
||||||
|
...this.props.value,
|
||||||
|
stops: stops,
|
||||||
|
}
|
||||||
|
this.props.onChange(this.props.fieldName, changedValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
renderZoomProperty() {
|
||||||
|
const zoomFields = this.props.value.stops.map((stop, idx) => {
|
||||||
const zoomLevel = stop[0]
|
const zoomLevel = stop[0]
|
||||||
const value = stop[1]
|
const value = stop[1]
|
||||||
|
const deleteStopBtn= <DeleteStopButton onClick={this.deleteStop.bind(this, idx)} />
|
||||||
|
|
||||||
return <div style={input.property} key={zoomLevel}>
|
return <InputBlock
|
||||||
{label}
|
key={zoomLevel}
|
||||||
<SpecField {...this.props}
|
doc={this.props.fieldSpec.doc}
|
||||||
value={value}
|
label={labelFromFieldName(this.props.fieldName)}
|
||||||
style={{
|
action={deleteStopBtn}
|
||||||
width: '33%'
|
>
|
||||||
}}
|
<div>
|
||||||
/>
|
<div className="maputnik-zoom-spec-property-stop-edit">
|
||||||
|
<NumberInput
|
||||||
<input
|
|
||||||
style={{
|
|
||||||
...input.input,
|
|
||||||
width: '10%',
|
|
||||||
marginLeft: margins[0],
|
|
||||||
}}
|
|
||||||
type="number"
|
|
||||||
value={zoomLevel}
|
value={zoomLevel}
|
||||||
|
onChange={changedStop => this.changeStop(idx, changedStop, value)}
|
||||||
min={0}
|
min={0}
|
||||||
max={22}
|
max={22}
|
||||||
/>
|
/>
|
||||||
</div>
|
</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={{
|
|
||||||
border: 1,
|
return <div className="maputnik-zoom-spec-property">
|
||||||
borderStyle: 'solid',
|
|
||||||
borderColor: Color(colors.gray).lighten(0.1).string(),
|
|
||||||
padding: margins[1],
|
|
||||||
}}>
|
|
||||||
{zoomFields}
|
{zoomFields}
|
||||||
|
<Button
|
||||||
|
className="maputnik-add-stop"
|
||||||
|
onClick={this.addStop.bind(this)}
|
||||||
|
>
|
||||||
|
Add stop
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
} else {
|
}
|
||||||
return <div style={input.property}>
|
|
||||||
{label}
|
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} />
|
<SpecField {...this.props} />
|
||||||
|
</InputBlock>
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const propClass = this.props.fieldSpec.default === this.props.value ? "maputnik-default-property" : "maputnik-modified-property"
|
||||||
|
return <div className={propClass}>
|
||||||
|
{isZoomField(this.props.value) ? this.renderZoomProperty() : this.renderProperty()}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
function labelFromFieldName(fieldName) {
|
||||||
let label = fieldName.split('-').slice(1).join(' ')
|
let label = fieldName.split('-').slice(1).join(' ')
|
||||||
if(label.length > 0) {
|
return capitalize(label)
|
||||||
label = label.charAt(0).toUpperCase() + label.slice(1);
|
|
||||||
}
|
|
||||||
return label
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,145 +1,46 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import GlSpec from 'mapbox-gl-style-spec/reference/latest.min.js'
|
import { combiningFilterOps } from '../../libs/filterops.js'
|
||||||
|
|
||||||
import input from '../../config/input.js'
|
import GlSpec from 'mapbox-gl-style-spec/reference/latest.js'
|
||||||
import colors from '../../config/colors.js'
|
import DocLabel from '../fields/DocLabel'
|
||||||
import { margins } from '../../config/scales.js'
|
import SelectInput from '../inputs/SelectInput'
|
||||||
|
import SingleFilterEditor from './SingleFilterEditor'
|
||||||
|
import FilterEditorBlock from './FilterEditorBlock'
|
||||||
|
import Button from '../Button'
|
||||||
|
|
||||||
const combiningFilterOps = ['all', 'any', 'none']
|
import DeleteIcon from 'react-icons/lib/md/delete'
|
||||||
const setFilterOps = ['in', '!in']
|
import AddIcon from 'react-icons/lib/fa/plus'
|
||||||
const otherFilterOps = Object
|
|
||||||
.keys(GlSpec.filter_operator.values)
|
|
||||||
.filter(op => combiningFilterOps.indexOf(op) < 0)
|
|
||||||
|
|
||||||
class CombiningOperatorSelect extends React.Component {
|
function hasCombiningFilter(filter) {
|
||||||
static propTypes = {
|
return combiningFilterOps.indexOf(filter[0]) >= 0
|
||||||
value: React.PropTypes.string.isRequired,
|
|
||||||
onChange: React.PropTypes.func.isRequired,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
function hasNestedCombiningFilter(filter) {
|
||||||
const options = combiningFilterOps.map(op => {
|
if(hasCombiningFilter(filter)) {
|
||||||
return <option key={op} value={op}>{op}</option>
|
const combinedFilters = filter.slice(1)
|
||||||
})
|
return filter.slice(1).map(f => hasCombiningFilter(f)).filter(f => f == true).length > 0
|
||||||
|
|
||||||
return <div>
|
|
||||||
<select
|
|
||||||
style={{
|
|
||||||
...input.select,
|
|
||||||
width: '20.5%',
|
|
||||||
margin: margins[0],
|
|
||||||
}}
|
|
||||||
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>
|
|
||||||
}
|
}
|
||||||
}
|
return false
|
||||||
|
|
||||||
class OperatorSelect extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
value: React.PropTypes.string.isRequired,
|
|
||||||
onChange: React.PropTypes.func.isRequired,
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const options = otherFilterOps.map(op => {
|
|
||||||
return <option key={op} value={op}>{op}</option>
|
|
||||||
})
|
|
||||||
return <select
|
|
||||||
style={{
|
|
||||||
...input.select,
|
|
||||||
width: '15%',
|
|
||||||
margin: margins[0]
|
|
||||||
}}
|
|
||||||
value={this.props.value}
|
|
||||||
onChange={e => this.props.onChange(e.target.value)}
|
|
||||||
>
|
|
||||||
{options}
|
|
||||||
</select>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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>
|
|
||||||
<select
|
|
||||||
style={{
|
|
||||||
...input.select,
|
|
||||||
width: '17%',
|
|
||||||
margin: margins[0]
|
|
||||||
}}
|
|
||||||
value={propertyName}
|
|
||||||
onChange={newPropertyName => this.onFilterPartChanged(filterOp, newPropertyName, filterArgs)}
|
|
||||||
>
|
|
||||||
{Object.keys(this.props.properties).map(propName => {
|
|
||||||
return <option key={propName} value={propName}>{propName}</option>
|
|
||||||
})}
|
|
||||||
</select>
|
|
||||||
<OperatorSelect
|
|
||||||
value={filterOp}
|
|
||||||
onChange={newFilterOp => this.onFilterPartChanged(newFilterOp, propertyName, filterArgs)}
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
style={{
|
|
||||||
...input.input,
|
|
||||||
width: '53%',
|
|
||||||
margin: margins[0]
|
|
||||||
}}
|
|
||||||
value={filterArgs.join(',')}
|
|
||||||
onChange={e => {
|
|
||||||
this.onFilterPartChanged(filterOp, propertyName, e.target.value.split(','))}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class CombiningFilterEditor extends React.Component {
|
export default class CombiningFilterEditor extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
/** Properties of the vector layer and the available fields */
|
/** Properties of the vector layer and the available fields */
|
||||||
properties: React.PropTypes.object.isRequired,
|
properties: React.PropTypes.object,
|
||||||
filter: React.PropTypes.array.isRequired,
|
filter: React.PropTypes.array,
|
||||||
onChange: React.PropTypes.func.isRequired,
|
onChange: React.PropTypes.func.isRequired,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert filter to combining filter
|
// Convert filter to combining filter
|
||||||
combiningFilter() {
|
combiningFilter() {
|
||||||
let combiningOp = this.props.filter[0]
|
let filter = this.props.filter || ['all']
|
||||||
let filters = this.props.filter.slice(1)
|
|
||||||
|
let combiningOp = filter[0]
|
||||||
|
let filters = filter.slice(1)
|
||||||
|
|
||||||
if(combiningFilterOps.indexOf(combiningOp) < 0) {
|
if(combiningFilterOps.indexOf(combiningOp) < 0) {
|
||||||
combiningOp = 'all'
|
combiningOp = 'all'
|
||||||
filters = [this.props.filter.slice(0)]
|
filters = [filter.slice(0)]
|
||||||
}
|
}
|
||||||
|
|
||||||
return [combiningOp, ...filters]
|
return [combiningOp, ...filters]
|
||||||
@@ -151,30 +52,61 @@ export default class CombiningFilterEditor extends React.Component {
|
|||||||
this.props.onChange(newFilter)
|
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() {
|
render() {
|
||||||
const filter = this.combiningFilter()
|
const filter = this.combiningFilter()
|
||||||
let combiningOp = filter[0]
|
let combiningOp = filter[0]
|
||||||
let filters = filter.slice(1)
|
let filters = filter.slice(1)
|
||||||
|
|
||||||
const filterEditors = filters.map((f, idx) => {
|
const editorBlocks = filters.map((f, idx) => {
|
||||||
return <SingleFilterEditor
|
return <FilterEditorBlock key={idx} onDelete={this.deleteFilterItem.bind(this, idx)}>
|
||||||
key={idx}
|
<SingleFilterEditor
|
||||||
properties={this.props.properties}
|
properties={this.props.properties}
|
||||||
filter={f}
|
filter={f}
|
||||||
onChange={this.onFilterPartChanged.bind(this, idx + 1)}
|
onChange={this.onFilterPartChanged.bind(this, idx + 1)}
|
||||||
/>
|
/>
|
||||||
|
</FilterEditorBlock>
|
||||||
})
|
})
|
||||||
|
|
||||||
return <div style={{
|
//TODO: Implement support for nested filter
|
||||||
padding: margins[2],
|
if(hasNestedCombiningFilter(filter)) {
|
||||||
paddingRight: 0,
|
return <div className="maputnik-filter-editor-unsupported">
|
||||||
backgroundColor: colors.black
|
Nested filters are not supported.
|
||||||
}}>
|
</div>
|
||||||
<CombiningOperatorSelect
|
}
|
||||||
|
|
||||||
|
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}
|
value={combiningOp}
|
||||||
onChange={this.onFilterPartChanged.bind(this, 0)}
|
onChange={this.onFilterPartChanged.bind(this, 0)}
|
||||||
|
options={[["all", "every filter matches"], ["none", "no filter matches"], ["any", "any filter matches"]]}
|
||||||
/>
|
/>
|
||||||
{filterEditors}
|
</div>
|
||||||
|
{editorBlocks}
|
||||||
|
<div className="maputnik-filter-editor-add-wrapper">
|
||||||
|
<Button
|
||||||
|
className="maputnik-add-filter"
|
||||||
|
onClick={this.addFilterItem.bind(this)}>
|
||||||
|
Add filter
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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
|
||||||
69
src/components/filter/SingleFilterEditor.jsx
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
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'
|
||||||
|
|
||||||
|
function tryParseInt(v) {
|
||||||
|
if (v === '') return v
|
||||||
|
if (isNaN(v)) return v
|
||||||
|
return parseFloat(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
let newFilter = [filterOp, propertyName, ...filterArgs.map(tryParseInt)]
|
||||||
|
if(filterOp === 'has' || filterOp === '!has') {
|
||||||
|
newFilter = [filterOp, propertyName]
|
||||||
|
} else if(filterArgs.length === 0) {
|
||||||
|
newFilter = [filterOp, propertyName, '']
|
||||||
|
}
|
||||||
|
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>
|
||||||
|
{filterArgs.length > 0 &&
|
||||||
|
<div className="maputnik-filter-editor-args">
|
||||||
|
<StringInput
|
||||||
|
value={filterArgs.join(',')}
|
||||||
|
onChange={ v=> this.onFilterPartChanged(filterOp, propertyName, v.split(','))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SingleFilterEditor
|
||||||
15
src/components/icons/CircleIcon.jsx
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import IconBase from 'react-icon-base'
|
||||||
|
|
||||||
|
|
||||||
|
export default class FillIcon extends React.Component {
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<IconBase viewBox="0 0 20 20" {...this.props}>
|
||||||
|
<path transform="translate(2 2)" d="M7.5,0C11.6422,0,15,3.3578,15,7.5S11.6422,15,7.5,15 S0,11.6422,0,7.5S3.3578,0,7.5,0z M7.5,1.6666c-3.2217,0-5.8333,2.6117-5.8333,5.8334S4.2783,13.3334,7.5,13.3334 s5.8333-2.6117,5.8333-5.8334S10.7217,1.6666,7.5,1.6666z"></path>
|
||||||
|
</IconBase>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -4,6 +4,7 @@ import LineIcon from './LineIcon.jsx'
|
|||||||
import FillIcon from './FillIcon.jsx'
|
import FillIcon from './FillIcon.jsx'
|
||||||
import SymbolIcon from './SymbolIcon.jsx'
|
import SymbolIcon from './SymbolIcon.jsx'
|
||||||
import BackgroundIcon from './BackgroundIcon.jsx'
|
import BackgroundIcon from './BackgroundIcon.jsx'
|
||||||
|
import CircleIcon from './CircleIcon.jsx'
|
||||||
|
|
||||||
class LayerIcon extends React.Component {
|
class LayerIcon extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
@@ -14,10 +15,13 @@ class LayerIcon extends React.Component {
|
|||||||
render() {
|
render() {
|
||||||
const iconProps = { style: this.props.style }
|
const iconProps = { style: this.props.style }
|
||||||
switch(this.props.type) {
|
switch(this.props.type) {
|
||||||
|
case 'fill-extrusion': return <BackgroundIcon {...iconProps} />
|
||||||
|
case 'raster': return <FillIcon {...iconProps} />
|
||||||
case 'fill': return <FillIcon {...iconProps} />
|
case 'fill': return <FillIcon {...iconProps} />
|
||||||
case 'background': return <BackgroundIcon {...iconProps} />
|
case 'background': return <BackgroundIcon {...iconProps} />
|
||||||
case 'line': return <LineIcon {...iconProps} />
|
case 'line': return <LineIcon {...iconProps} />
|
||||||
case 'symbol': return <SymbolIcon {...iconProps} />
|
case 'symbol': return <SymbolIcon {...iconProps} />
|
||||||
|
case 'circle': return <CircleIcon {...iconProps} />
|
||||||
default: return null
|
default: return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
48
src/components/inputs/ArrayInput.jsx
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import StringInput from './StringInput'
|
||||||
|
import NumberInput from './NumberInput'
|
||||||
|
|
||||||
|
class ArrayInput extends React.Component {
|
||||||
|
static propTypes = {
|
||||||
|
value: React.PropTypes.array,
|
||||||
|
type: React.PropTypes.string,
|
||||||
|
length: React.PropTypes.number,
|
||||||
|
default: React.PropTypes.array,
|
||||||
|
onChange: React.PropTypes.func,
|
||||||
|
}
|
||||||
|
|
||||||
|
changeValue(idx, newValue) {
|
||||||
|
console.log(idx, newValue)
|
||||||
|
const values = this.values.slice(0)
|
||||||
|
values[idx] = newValue
|
||||||
|
this.props.onChange(values)
|
||||||
|
}
|
||||||
|
|
||||||
|
get values() {
|
||||||
|
return this.props.value || this.props.default || []
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const inputs = this.values.map((v, i) => {
|
||||||
|
if(this.props.type === 'number') {
|
||||||
|
return <NumberInput
|
||||||
|
key={i}
|
||||||
|
value={v}
|
||||||
|
onChange={this.changeValue.bind(this, i)}
|
||||||
|
/>
|
||||||
|
} else {
|
||||||
|
return <StringInput
|
||||||
|
key={i}
|
||||||
|
value={v}
|
||||||
|
onChange={this.changeValue.bind(this, i)}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return <div className="maputnik-array">
|
||||||
|
{inputs}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ArrayInput
|
||||||
50
src/components/inputs/AutocompleteInput.jsx
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import classnames from 'classnames'
|
||||||
|
import Autocomplete from 'react-autocomplete'
|
||||||
|
|
||||||
|
|
||||||
|
class AutocompleteInput extends React.Component {
|
||||||
|
static propTypes = {
|
||||||
|
value: React.PropTypes.string,
|
||||||
|
options: React.PropTypes.array,
|
||||||
|
onChange: React.PropTypes.func,
|
||||||
|
}
|
||||||
|
|
||||||
|
static defaultProps = {
|
||||||
|
onChange: () => {},
|
||||||
|
options: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const AutocompleteMenu = (items, value, style) => <div className={"maputnik-autocomplete-menu"} children={items} />
|
||||||
|
|
||||||
|
return <Autocomplete
|
||||||
|
wrapperProps={{
|
||||||
|
className: "maputnik-autocomplete",
|
||||||
|
style: null
|
||||||
|
}}
|
||||||
|
renderMenu={AutocompleteMenu}
|
||||||
|
inputProps={{
|
||||||
|
className: "maputnik-string"
|
||||||
|
}}
|
||||||
|
value={this.props.value}
|
||||||
|
items={this.props.options}
|
||||||
|
getItemValue={(item) => item[0]}
|
||||||
|
onSelect={v => this.props.onChange(v)}
|
||||||
|
onChange={(e, v) => this.props.onChange(v)}
|
||||||
|
renderItem={(item, isHighlighted) => (
|
||||||
|
<div
|
||||||
|
key={item[0]}
|
||||||
|
className={classnames({
|
||||||
|
"maputnik-autocomplete-menu-item": true,
|
||||||
|
"maputnik-autocomplete-menu-item-selected": isHighlighted,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{item[1]}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AutocompleteInput
|
||||||
30
src/components/inputs/CheckboxInput.jsx
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
class CheckboxInput extends React.Component {
|
||||||
|
static propTypes = {
|
||||||
|
value: React.PropTypes.bool.isRequired,
|
||||||
|
style: React.PropTypes.object,
|
||||||
|
onChange: React.PropTypes.func,
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return <label className="maputnik-checkbox-wrapper">
|
||||||
|
<input
|
||||||
|
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 style={{
|
||||||
|
display: this.props.value ? 'inline' : 'none'
|
||||||
|
}} 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>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CheckboxInput
|
||||||
42
src/components/inputs/FontInput.jsx
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import AutocompleteInput from './AutocompleteInput'
|
||||||
|
|
||||||
|
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) || []
|
||||||
|
}
|
||||||
|
|
||||||
|
changeFont(idx, newValue) {
|
||||||
|
const changedValues = this.values.slice(0)
|
||||||
|
changedValues[idx] = newValue
|
||||||
|
this.props.onChange(changedValues)
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const inputs = this.values.map((value, i) => {
|
||||||
|
return <AutocompleteInput
|
||||||
|
key={i}
|
||||||
|
value={value}
|
||||||
|
options={this.props.fonts.map(f => [f, f])}
|
||||||
|
onChange={this.changeFont.bind(this, i)}
|
||||||
|
/>
|
||||||
|
})
|
||||||
|
|
||||||
|
return <div className="maputnik-font">
|
||||||
|
{inputs}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FontInput
|
||||||
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,12 +1,18 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import input from '../../config/input'
|
import classnames from 'classnames'
|
||||||
import { margins } from '../../config/scales'
|
import DocLabel from '../fields/DocLabel'
|
||||||
|
|
||||||
/** Wrap a component with a label */
|
/** Wrap a component with a label */
|
||||||
class InputBlock extends React.Component {
|
class InputBlock extends React.Component {
|
||||||
static propTypes = {
|
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,
|
children: React.PropTypes.element.isRequired,
|
||||||
|
style: React.PropTypes.object,
|
||||||
}
|
}
|
||||||
|
|
||||||
onChange(e) {
|
onChange(e) {
|
||||||
@@ -15,14 +21,34 @@ class InputBlock extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return <div style={{
|
return <div style={this.props.style}
|
||||||
display: 'block',
|
className={classnames({
|
||||||
marginTop: margins[2],
|
"maputnik-input-block": true,
|
||||||
marginBottom: margins[2],
|
"maputnik-action-block": this.props.action
|
||||||
}}>
|
})}
|
||||||
<label style={input.label}>{this.props.label}</label>
|
>
|
||||||
|
{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.props.action &&
|
||||||
|
<div className="maputnik-input-block-action">
|
||||||
|
{this.props.action}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<div className="maputnik-input-block-content">
|
||||||
{this.props.children}
|
{this.props.children}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
35
src/components/inputs/MultiButtonInput.jsx
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import classnames from 'classnames'
|
||||||
|
import Button from '../Button'
|
||||||
|
|
||||||
|
class MultiButtonInput extends React.Component {
|
||||||
|
static propTypes = {
|
||||||
|
value: React.PropTypes.string.isRequired,
|
||||||
|
options: React.PropTypes.array.isRequired,
|
||||||
|
onChange: React.PropTypes.func.isRequired,
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
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}
|
||||||
|
onClick={e => this.props.onChange(val)}
|
||||||
|
className={classnames({"maputnik-button-selected": val === selectedValue})}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</Button>
|
||||||
|
})
|
||||||
|
|
||||||
|
return <div className="maputnik-multibutton">
|
||||||
|
{buttons}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MultiButtonInput
|
||||||
78
src/components/inputs/NumberInput.jsx
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
class NumberInput extends React.Component {
|
||||||
|
static propTypes = {
|
||||||
|
value: React.PropTypes.number,
|
||||||
|
default: React.PropTypes.number,
|
||||||
|
min: React.PropTypes.number,
|
||||||
|
max: React.PropTypes.number,
|
||||||
|
onChange: React.PropTypes.func,
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props)
|
||||||
|
this.state = {
|
||||||
|
value: props.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillReceiveProps(nextProps) {
|
||||||
|
this.setState({ value: nextProps.value })
|
||||||
|
}
|
||||||
|
|
||||||
|
changeValue(newValue) {
|
||||||
|
const value = parseFloat(newValue)
|
||||||
|
|
||||||
|
const hasChanged = this.state.value !== value
|
||||||
|
if(this.isValid(value) && hasChanged) {
|
||||||
|
this.props.onChange(value)
|
||||||
|
} else {
|
||||||
|
this.setState({ value: newValue })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isValid(v) {
|
||||||
|
const value = parseFloat(v)
|
||||||
|
if(isNaN(value)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!isNaN(this.props.min) && value < this.props.min) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!isNaN(this.props.max) && value > this.props.max) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
resetValue() {
|
||||||
|
// Reset explicitly to default value if value has been cleared
|
||||||
|
if(this.state.value === "") {
|
||||||
|
return this.changeValue(this.props.default)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If set value is invalid fall back to the last valid value from props or at last resort the default value
|
||||||
|
if(!this.isValid(this.state.value)) {
|
||||||
|
if(this.isValid(this.props.value)) {
|
||||||
|
this.changeValue(this.props.value)
|
||||||
|
} else {
|
||||||
|
this.changeValue(this.props.default)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return <input
|
||||||
|
className="maputnik-number"
|
||||||
|
placeholder={this.props.default}
|
||||||
|
value={this.state.value}
|
||||||
|
onChange={e => this.changeValue(e.target.value)}
|
||||||
|
onBlur={this.resetValue.bind(this)}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default NumberInput
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import input from '../../config/input.js'
|
|
||||||
|
|
||||||
class SelectInput extends React.Component {
|
class SelectInput extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
@@ -11,19 +10,18 @@ class SelectInput extends React.Component {
|
|||||||
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const options = this.props.options.map(([val, label])=> {
|
let options = this.props.options
|
||||||
return <option key={val} value={val}>{label}</option>
|
if(options.length > 0 && !Array.isArray(options[0])) {
|
||||||
})
|
options = options.map(v => [v, v])
|
||||||
|
}
|
||||||
|
|
||||||
return <select
|
return <select
|
||||||
style={{
|
className="maputnik-select"
|
||||||
...input.select,
|
style={this.props.style}
|
||||||
...this.props.style
|
|
||||||
}}
|
|
||||||
value={this.props.value}
|
value={this.props.value}
|
||||||
onChange={e => this.props.onChange(e.target.value)}
|
onChange={e => this.props.onChange(e.target.value)}
|
||||||
>
|
>
|
||||||
{options}
|
{ options.map(([val, label]) => <option key={val} value={val}>{label}</option>) }
|
||||||
</select>
|
</select>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,34 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import input from '../../config/input.js'
|
|
||||||
|
|
||||||
class StringInput extends React.Component {
|
class StringInput extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
value: React.PropTypes.string,
|
value: React.PropTypes.string,
|
||||||
style: React.PropTypes.object,
|
style: React.PropTypes.object,
|
||||||
|
default: React.PropTypes.string,
|
||||||
onChange: React.PropTypes.func,
|
onChange: React.PropTypes.func,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props)
|
||||||
|
this.state = {
|
||||||
|
value: props.value || ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillReceiveProps(nextProps) {
|
||||||
|
this.setState({ value: nextProps.value || '' })
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return <input
|
return <input
|
||||||
style={{
|
className="maputnik-string"
|
||||||
...input.input,
|
style={this.props.style}
|
||||||
...this.props.style
|
value={this.state.value}
|
||||||
|
placeholder={this.props.default}
|
||||||
|
onChange={e => this.setState({ value: e.target.value })}
|
||||||
|
onBlur={() => {
|
||||||
|
if(this.state.value!==this.props.value) this.props.onChange(this.state.value)
|
||||||
}}
|
}}
|
||||||
value={this.props.value}
|
|
||||||
onChange={e => this.props.onChange(e.target.value)}
|
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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 StringInput from '../inputs/StringInput'
|
||||||
import SelectInput from '../inputs/SelectInput'
|
import SelectInput from '../inputs/SelectInput'
|
||||||
|
|
||||||
import colors from '../../config/colors'
|
|
||||||
import { margins } from '../../config/scales'
|
|
||||||
|
|
||||||
import 'codemirror/mode/javascript/javascript'
|
import 'codemirror/mode/javascript/javascript'
|
||||||
import 'codemirror/lib/codemirror.css'
|
import 'codemirror/lib/codemirror.css'
|
||||||
import '../../codemirror-maputnik.css'
|
import '../../codemirror-maputnik.css'
|
||||||
@@ -32,18 +29,37 @@ class JSONEditor extends React.Component {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
shouldComponentUpdate(nextProps, nextState) {
|
||||||
|
try {
|
||||||
|
const parsedLayer = JSON.parse(this.state.code)
|
||||||
|
// If the structure is still the same do not update
|
||||||
|
// because it affects editing experience by reformatting all the time
|
||||||
|
return nextState.code !== JSON.stringify(parsedLayer, null, 2)
|
||||||
|
} catch(err) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onCodeUpdate(newCode) {
|
onCodeUpdate(newCode) {
|
||||||
try {
|
try {
|
||||||
const parsedLayer = JSON.parse(newCode)
|
const parsedLayer = JSON.parse(newCode)
|
||||||
this.props.onChange(parsedLayer)
|
this.props.onChange(parsedLayer)
|
||||||
} catch(err) {
|
} catch(err) {
|
||||||
console.warn(err)
|
console.warn(err)
|
||||||
|
} finally {
|
||||||
this.setState({
|
this.setState({
|
||||||
code: newCode
|
code: newCode
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
resetValue() {
|
||||||
|
console.log('reset')
|
||||||
|
this.setState({
|
||||||
|
code: JSON.stringify(this.props.layer, null, 2)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const codeMirrorOptions = {
|
const codeMirrorOptions = {
|
||||||
mode: {name: "javascript", json: true},
|
mode: {name: "javascript", json: true},
|
||||||
@@ -51,11 +67,13 @@ class JSONEditor extends React.Component {
|
|||||||
theme: 'maputnik',
|
theme: 'maputnik',
|
||||||
viewportMargin: Infinity,
|
viewportMargin: Infinity,
|
||||||
lineNumbers: false,
|
lineNumbers: false,
|
||||||
|
scrollbarStyle: "null",
|
||||||
}
|
}
|
||||||
|
|
||||||
return <CodeMirror
|
return <CodeMirror
|
||||||
value={this.state.code}
|
value={this.state.code}
|
||||||
onChange={this.onCodeUpdate.bind(this)}
|
onChange={this.onCodeUpdate.bind(this)}
|
||||||
|
onFocusChange={focused => focused ? true : this.resetValue()}
|
||||||
options={codeMirrorOptions}
|
options={codeMirrorOptions}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,21 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
import JSONEditor from './JSONEditor'
|
import JSONEditor from './JSONEditor'
|
||||||
import SourceEditor from './SourceEditor'
|
|
||||||
import FilterEditor from '../filter/FilterEditor'
|
import FilterEditor from '../filter/FilterEditor'
|
||||||
import PropertyGroup from '../fields/PropertyGroup'
|
import PropertyGroup from '../fields/PropertyGroup'
|
||||||
import LayerEditorGroup from './LayerEditorGroup'
|
import LayerEditorGroup from './LayerEditorGroup'
|
||||||
import LayerSettings from './LayerSettings'
|
import LayerTypeBlock from './LayerTypeBlock'
|
||||||
|
import LayerIdBlock from './LayerIdBlock'
|
||||||
|
import MinZoomBlock from './MinZoomBlock'
|
||||||
|
import MaxZoomBlock from './MaxZoomBlock'
|
||||||
|
import LayerSourceBlock from './LayerSourceBlock'
|
||||||
|
import LayerSourceLayerBlock from './LayerSourceLayerBlock'
|
||||||
|
|
||||||
|
import InputBlock from '../inputs/InputBlock'
|
||||||
|
import MultiButtonInput from '../inputs/MultiButtonInput'
|
||||||
|
|
||||||
|
import { changeType, changeProperty } from '../../libs/layer'
|
||||||
import layout from '../../config/layout.json'
|
import layout from '../../config/layout.json'
|
||||||
import { margins, fontSizes } from '../../config/scales'
|
|
||||||
import colors from '../../config/colors'
|
|
||||||
|
|
||||||
class UnsupportedLayer extends React.Component {
|
class UnsupportedLayer extends React.Component {
|
||||||
render() {
|
render() {
|
||||||
@@ -17,12 +23,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. */
|
/** Layer editor supporting multiple types of layers. */
|
||||||
export default class LayerEditor extends React.Component {
|
export default class LayerEditor extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
layer: React.PropTypes.object.isRequired,
|
layer: React.PropTypes.object.isRequired,
|
||||||
sources: React.PropTypes.object,
|
sources: React.PropTypes.object,
|
||||||
vectorLayers: React.PropTypes.object,
|
vectorLayers: React.PropTypes.object,
|
||||||
|
spec: React.PropTypes.object.isRequired,
|
||||||
onLayerChanged: React.PropTypes.func,
|
onLayerChanged: React.PropTypes.func,
|
||||||
onLayerIdChange: React.PropTypes.func,
|
onLayerIdChange: React.PropTypes.func,
|
||||||
}
|
}
|
||||||
@@ -42,7 +65,7 @@ export default class LayerEditor extends React.Component {
|
|||||||
|
|
||||||
//TODO: Clean this up and refactor into function
|
//TODO: Clean this up and refactor into function
|
||||||
const editorGroups = {}
|
const editorGroups = {}
|
||||||
layout[this.props.layer.type].groups.forEach(group => {
|
layoutGroups(this.props.layer.type).forEach(group => {
|
||||||
editorGroups[group.title] = true
|
editorGroups[group.title] = true
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -66,38 +89,14 @@ export default class LayerEditor extends React.Component {
|
|||||||
getChildContext () {
|
getChildContext () {
|
||||||
return {
|
return {
|
||||||
reactIconBase: {
|
reactIconBase: {
|
||||||
size: fontSizes[4],
|
size: 14,
|
||||||
color: colors.lowgray,
|
color: '#8e8e8e',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** A {@property} in either the paint our layout {@group} has changed
|
changeProperty(group, property, newValue) {
|
||||||
* to a {@newValue}.
|
this.props.onLayerChanged(changeProperty(this.props.layer, group, property, newValue))
|
||||||
*/
|
|
||||||
onPropertyChange(group, property, newValue) {
|
|
||||||
if(group) {
|
|
||||||
this.props.onLayerChanged({
|
|
||||||
...this.props.layer,
|
|
||||||
[group]: {
|
|
||||||
...this.props.layer[group],
|
|
||||||
[property]: newValue
|
|
||||||
}
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
this.props.onLayerChanged({
|
|
||||||
...this.props.layer,
|
|
||||||
[property]: newValue
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onFilterChange(newValue) {
|
|
||||||
const changedLayer = {
|
|
||||||
...this.props.layer,
|
|
||||||
filter: newValue
|
|
||||||
}
|
|
||||||
this.props.onLayerChanged(changedLayer)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onGroupToggle(groupTitle, active) {
|
onGroupToggle(groupTitle, active) {
|
||||||
@@ -112,30 +111,50 @@ export default class LayerEditor extends React.Component {
|
|||||||
|
|
||||||
renderGroupType(type, fields) {
|
renderGroupType(type, fields) {
|
||||||
switch(type) {
|
switch(type) {
|
||||||
case 'settings': return <LayerSettings
|
case 'layer': return <div>
|
||||||
id={this.props.layer.id}
|
<LayerIdBlock
|
||||||
type={this.props.layer.type}
|
value={this.props.layer.id}
|
||||||
onTypeChange={v => this.onPropertyChange(null, 'type', v)}
|
onChange={newId => this.props.onLayerIdChange(this.props.layer.id, newId)}
|
||||||
onIdChange={newId => this.props.onLayerIdChange(this.props.layer.id, newId)}
|
|
||||||
/>
|
/>
|
||||||
case 'source': return <div>
|
<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)}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
{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)}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
<MinZoomBlock
|
||||||
|
value={this.props.layer.minzoom}
|
||||||
|
onChange={v => this.changeProperty(null, 'minzoom', v)}
|
||||||
|
/>
|
||||||
|
<MaxZoomBlock
|
||||||
|
value={this.props.layer.maxzoom}
|
||||||
|
onChange={v => this.changeProperty(null, 'maxzoom', v)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
case 'filter': return <div>
|
||||||
|
<div className="maputnik-filter-editor-wrapper">
|
||||||
<FilterEditor
|
<FilterEditor
|
||||||
filter={this.props.layer.filter}
|
filter={this.props.layer.filter}
|
||||||
properties={this.props.vectorLayers[this.props.layer['source-layer']]}
|
properties={this.props.vectorLayers[this.props.layer['source-layer']]}
|
||||||
onChange={f => this.onFilterChange(f)}
|
onChange={f => this.changeProperty(null, 'filter', f)}
|
||||||
/>
|
|
||||||
<SourceEditor
|
|
||||||
source={this.props.layer.source}
|
|
||||||
sourceLayer={this.props.layer['source-layer']}
|
|
||||||
sources={this.props.sources}
|
|
||||||
onSourceChange={console.log}
|
|
||||||
onSourceLayerChange={console.log}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
case 'properties': return <PropertyGroup
|
case 'properties': return <PropertyGroup
|
||||||
layer={this.props.layer}
|
layer={this.props.layer}
|
||||||
groupFields={fields}
|
groupFields={fields}
|
||||||
onChange={this.onPropertyChange.bind(this)}
|
spec={this.props.spec}
|
||||||
|
onChange={this.changeProperty.bind(this)}
|
||||||
/>
|
/>
|
||||||
case 'jsoneditor': return <JSONEditor
|
case 'jsoneditor': return <JSONEditor
|
||||||
layer={this.props.layer}
|
layer={this.props.layer}
|
||||||
@@ -147,8 +166,8 @@ export default class LayerEditor extends React.Component {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const layerType = this.props.layer.type
|
const layerType = this.props.layer.type
|
||||||
const layoutGroups = layout[layerType].groups.filter(group => {
|
const groups = layoutGroups(layerType).filter(group => {
|
||||||
return !(this.props.layer.type === 'background' && group.type === 'source')
|
return !(layerType === 'background' && group.type === 'source')
|
||||||
}).map(group => {
|
}).map(group => {
|
||||||
return <LayerEditorGroup
|
return <LayerEditorGroup
|
||||||
key={group.title}
|
key={group.title}
|
||||||
@@ -160,8 +179,9 @@ export default class LayerEditor extends React.Component {
|
|||||||
</LayerEditorGroup>
|
</LayerEditorGroup>
|
||||||
})
|
})
|
||||||
|
|
||||||
return <div>
|
return <div className="maputnik-layer-editor"
|
||||||
{layoutGroups}
|
>
|
||||||
|
{groups}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,25 +1,6 @@
|
|||||||
import React from 'react'
|
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 Collapse from 'react-collapse'
|
||||||
import CollapseOpenIcon from 'react-icons/lib/md/arrow-drop-down'
|
import Collapser from './Collapser'
|
||||||
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} />
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class LayerEditorGroup extends React.Component {
|
export default class LayerEditorGroup extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
@@ -29,26 +10,9 @@ export default class LayerEditorGroup extends React.Component {
|
|||||||
onActiveToggle: React.PropTypes.func.isRequired
|
onActiveToggle: React.PropTypes.func.isRequired
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
super(props)
|
|
||||||
this.state = { hover: false }
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return <div>
|
return <div>
|
||||||
<div style={{
|
<div className="maputnik-layer-editor-group"
|
||||||
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})}
|
|
||||||
onClick={e => this.props.onActiveToggle(!this.props.isActive)}
|
onClick={e => this.props.onActiveToggle(!this.props.isActive)}
|
||||||
>
|
>
|
||||||
<span>{this.props.title}</span>
|
<span>{this.props.title}</span>
|
||||||
|
|||||||
23
src/components/layers/LayerIdBlock.jsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
import GlSpec from 'mapbox-gl-style-spec/reference/latest.js'
|
||||||
|
import InputBlock from '../inputs/InputBlock'
|
||||||
|
import StringInput from '../inputs/StringInput'
|
||||||
|
|
||||||
|
class LayerIdBlock extends React.Component {
|
||||||
|
static propTypes = {
|
||||||
|
value: React.PropTypes.string.isRequired,
|
||||||
|
onChange: React.PropTypes.func.isRequired,
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return <InputBlock label={"ID"} doc={GlSpec.layer.id.doc}>
|
||||||
|
<StringInput
|
||||||
|
value={this.props.value}
|
||||||
|
onChange={this.props.onChange}
|
||||||
|
/>
|
||||||
|
</InputBlock>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LayerIdBlock
|
||||||
@@ -1,11 +1,14 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import classnames from 'classnames'
|
||||||
import cloneDeep from 'lodash.clonedeep'
|
import cloneDeep from 'lodash.clonedeep'
|
||||||
|
|
||||||
|
import Button from '../Button'
|
||||||
|
import LayerListGroup from './LayerListGroup'
|
||||||
import LayerListItem from './LayerListItem'
|
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 style from '../../libs/style.js'
|
||||||
import { margins } from '../../config/scales.js'
|
|
||||||
|
|
||||||
import {SortableContainer, SortableHandle, arrayMove} from 'react-sortable-hoc';
|
import {SortableContainer, SortableHandle, arrayMove} from 'react-sortable-hoc';
|
||||||
|
|
||||||
const layerListPropTypes = {
|
const layerListPropTypes = {
|
||||||
@@ -13,6 +16,25 @@ const layerListPropTypes = {
|
|||||||
selectedLayerIndex: React.PropTypes.number.isRequired,
|
selectedLayerIndex: React.PropTypes.number.isRequired,
|
||||||
onLayersChange: React.PropTypes.func.isRequired,
|
onLayersChange: React.PropTypes.func.isRequired,
|
||||||
onLayerSelect: React.PropTypes.func,
|
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
|
// List of collapsible layer editors
|
||||||
@@ -23,6 +45,16 @@ class LayerListContainer extends React.Component {
|
|||||||
onLayerSelect: () => {},
|
onLayerSelect: () => {},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props)
|
||||||
|
this.state = {
|
||||||
|
collapsedGroups: {},
|
||||||
|
isOpen: {
|
||||||
|
add: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onLayerDestroy(layerId) {
|
onLayerDestroy(layerId) {
|
||||||
const remainingLayers = this.props.layers.slice(0)
|
const remainingLayers = this.props.layers.slice(0)
|
||||||
const idx = style.indexOfLayer(remainingLayers, layerId)
|
const idx = style.indexOfLayer(remainingLayers, layerId)
|
||||||
@@ -53,28 +85,108 @@ class LayerListContainer extends React.Component {
|
|||||||
this.props.onLayersChange(changedLayers)
|
this.props.onLayersChange(changedLayers)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toggleModal(modalName) {
|
||||||
|
this.setState({
|
||||||
|
isOpen: {
|
||||||
|
...this.state.isOpen,
|
||||||
|
[modalName]: !this.state.isOpen[modalName]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
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] = false
|
||||||
|
}
|
||||||
|
this.setState({
|
||||||
|
collapsedGroups: newGroups
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
isCollapsed(groupPrefix, idx) {
|
||||||
|
const collapsed = this.state.collapsedGroups[[groupPrefix, idx].join('-')]
|
||||||
|
return collapsed === undefined ? true : collapsed
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const layerPanels = this.props.layers.map((layer, index) => {
|
|
||||||
const layerId = layer.id
|
const listItems = []
|
||||||
return <LayerListItem
|
let idx = 0
|
||||||
index={index}
|
this.groupedLayers().forEach(layers => {
|
||||||
key={layerId}
|
const groupPrefix = layerPrefix(layers[0].id)
|
||||||
layerId={layerId}
|
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': layers.length > 1 && 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}
|
layerType={layer.type}
|
||||||
visibility={(layer.layout || {}).visibility}
|
visibility={(layer.layout || {}).visibility}
|
||||||
isSelected={index === this.props.selectedLayerIndex}
|
isSelected={idx === this.props.selectedLayerIndex}
|
||||||
onLayerSelect={this.props.onLayerSelect}
|
onLayerSelect={this.props.onLayerSelect}
|
||||||
onLayerDestroy={this.onLayerDestroy.bind(this)}
|
onLayerDestroy={this.onLayerDestroy.bind(this)}
|
||||||
onLayerCopy={this.onLayerCopy.bind(this)}
|
onLayerCopy={this.onLayerCopy.bind(this)}
|
||||||
onLayerVisibilityToggle={this.onLayerVisibilityToggle.bind(this)}
|
onLayerVisibilityToggle={this.onLayerVisibilityToggle.bind(this)}
|
||||||
/>
|
/>
|
||||||
|
listItems.push(listItem)
|
||||||
|
idx += 1
|
||||||
})
|
})
|
||||||
return <ul style={{
|
})
|
||||||
padding: 0,
|
|
||||||
margin: 0
|
return <div className="maputnik-layer-list">
|
||||||
}}>
|
<AddModal
|
||||||
{layerPanels}
|
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>
|
</ul>
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
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 React from 'react'
|
||||||
import Color from 'color'
|
import Color from 'color'
|
||||||
|
import classnames from 'classnames'
|
||||||
|
|
||||||
import CopyIcon from 'react-icons/lib/md/content-copy'
|
import CopyIcon from 'react-icons/lib/md/content-copy'
|
||||||
import VisibilityIcon from 'react-icons/lib/md/visibility'
|
import VisibilityIcon from 'react-icons/lib/md/visibility'
|
||||||
@@ -10,10 +11,6 @@ import LayerIcon from '../icons/LayerIcon'
|
|||||||
import LayerEditor from './LayerEditor'
|
import LayerEditor from './LayerEditor'
|
||||||
import {SortableElement, SortableHandle} from 'react-sortable-hoc'
|
import {SortableElement, SortableHandle} from 'react-sortable-hoc'
|
||||||
|
|
||||||
import colors from '../../config/colors.js'
|
|
||||||
import { fontSizes, margins } from '../../config/scales.js'
|
|
||||||
|
|
||||||
|
|
||||||
@SortableHandle
|
@SortableHandle
|
||||||
class LayerTypeDragHandle extends React.Component {
|
class LayerTypeDragHandle extends React.Component {
|
||||||
static propTypes = LayerIcon.propTypes
|
static propTypes = LayerIcon.propTypes
|
||||||
@@ -23,9 +20,9 @@ class LayerTypeDragHandle extends React.Component {
|
|||||||
{...this.props}
|
{...this.props}
|
||||||
style={{
|
style={{
|
||||||
cursor: 'move',
|
cursor: 'move',
|
||||||
width: 15,
|
width: 14,
|
||||||
height: 15,
|
height: 14,
|
||||||
paddingRight: margins[0],
|
paddingRight: 3,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
@@ -34,46 +31,23 @@ class LayerTypeDragHandle extends React.Component {
|
|||||||
class IconAction extends React.Component {
|
class IconAction extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
action: React.PropTypes.string.isRequired,
|
action: React.PropTypes.string.isRequired,
|
||||||
active: React.PropTypes.bool,
|
|
||||||
onClick: React.PropTypes.func.isRequired,
|
onClick: React.PropTypes.func.isRequired,
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
super(props)
|
|
||||||
this.state = { hover: false }
|
|
||||||
}
|
|
||||||
|
|
||||||
renderIcon() {
|
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) {
|
switch(this.props.action) {
|
||||||
case 'copy': return <CopyIcon style={iconStyle} />
|
case 'copy': return <CopyIcon />
|
||||||
case 'show': return <VisibilityIcon style={iconStyle} />
|
case 'show': return <VisibilityIcon />
|
||||||
case 'hide': return <VisibilityOffIcon style={iconStyle} />
|
case 'hide': return <VisibilityOffIcon />
|
||||||
case 'delete': return <DeleteIcon style={iconStyle} />
|
case 'delete': return <DeleteIcon />
|
||||||
default: return null
|
default: return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return <a
|
return <a
|
||||||
style={{
|
className="maputnik-layer-list-icon-action"
|
||||||
display: "inline",
|
|
||||||
marginLeft: margins[0],
|
|
||||||
...this.props.style
|
|
||||||
}}
|
|
||||||
onClick={this.props.onClick}
|
onClick={this.props.onClick}
|
||||||
onMouseOver={e => this.setState({hover: true})}
|
|
||||||
onMouseOut={e => this.setState({hover: false})}
|
|
||||||
>
|
>
|
||||||
{this.renderIcon()}
|
{this.renderIcon()}
|
||||||
</a>
|
</a>
|
||||||
@@ -87,6 +61,7 @@ class LayerListItem extends React.Component {
|
|||||||
layerType: React.PropTypes.string.isRequired,
|
layerType: React.PropTypes.string.isRequired,
|
||||||
isSelected: React.PropTypes.bool,
|
isSelected: React.PropTypes.bool,
|
||||||
visibility: React.PropTypes.string,
|
visibility: React.PropTypes.string,
|
||||||
|
className: React.PropTypes.string,
|
||||||
|
|
||||||
onLayerSelect: React.PropTypes.func.isRequired,
|
onLayerSelect: React.PropTypes.func.isRequired,
|
||||||
onLayerCopy: React.PropTypes.func,
|
onLayerCopy: React.PropTypes.func,
|
||||||
@@ -106,83 +81,36 @@ class LayerListItem extends React.Component {
|
|||||||
reactIconBase: React.PropTypes.object
|
reactIconBase: React.PropTypes.object
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
super(props)
|
|
||||||
this.state = {
|
|
||||||
hover: false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getChildContext() {
|
getChildContext() {
|
||||||
return {
|
return {
|
||||||
reactIconBase: { size: 12 }
|
reactIconBase: { size: 14 }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
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,
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
return <li
|
||||||
key={this.props.layerId}
|
key={this.props.layerId}
|
||||||
onClick={e => this.props.onLayerSelect(this.props.layerId)}
|
onClick={e => this.props.onLayerSelect(this.props.layerId)}
|
||||||
onMouseOver={e => this.setState({hover: true})}
|
className={classnames({
|
||||||
onMouseOut={e => this.setState({hover: false})}
|
"maputnik-layer-list-item": true,
|
||||||
style={itemStyle}>
|
"maputnik-layer-list-item-selected": this.props.isSelected,
|
||||||
<div style={{
|
[this.props.className]: true,
|
||||||
display: 'flex',
|
})}>
|
||||||
flexDirection: 'row'
|
|
||||||
}}>
|
|
||||||
<LayerTypeDragHandle type={this.props.layerType} />
|
<LayerTypeDragHandle type={this.props.layerType} />
|
||||||
<span style={{
|
<span className="maputnik-layer-list-item-id">{this.props.layerId}</span>
|
||||||
width: 115,
|
|
||||||
whiteSpace: 'nowrap',
|
|
||||||
overflow: 'hidden',
|
|
||||||
textOverflow: 'ellipsis'
|
|
||||||
}}>{this.props.layerId}</span>
|
|
||||||
<span style={{flexGrow: 1}} />
|
<span style={{flexGrow: 1}} />
|
||||||
<IconAction {...iconProps}
|
<IconAction
|
||||||
action={'delete'}
|
action={'delete'}
|
||||||
onClick={e => this.props.onLayerDestroy(this.props.layerId)}
|
onClick={e => this.props.onLayerDestroy(this.props.layerId)}
|
||||||
/>
|
/>
|
||||||
<IconAction {...iconProps}
|
<IconAction
|
||||||
action={'copy'}
|
action={'copy'}
|
||||||
onClick={e => this.props.onLayerCopy(this.props.layerId)}
|
onClick={e => this.props.onLayerCopy(this.props.layerId)}
|
||||||
/>
|
/>
|
||||||
<IconAction {...iconProps}
|
<IconAction
|
||||||
active={this.state.hover || this.props.isSelected || this.props.visibility === 'none'}
|
|
||||||
action={this.props.visibility === 'visible' ? 'hide' : 'show'}
|
action={this.props.visibility === 'visible' ? 'hide' : 'show'}
|
||||||
onClick={e => this.props.onLayerVisibilityToggle(this.props.layerId)}
|
onClick={e => this.props.onLayerVisibilityToggle(this.props.layerId)}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
</li>
|
</li>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,50 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import GlSpec from 'mapbox-gl-style-spec/reference/latest.min.js'
|
|
||||||
import InputBlock from '../inputs/InputBlock'
|
|
||||||
import StringInput from '../inputs/StringInput'
|
|
||||||
import SelectInput from '../inputs/SelectInput'
|
|
||||||
|
|
||||||
import colors from '../../config/colors'
|
|
||||||
import { margins } from '../../config/scales'
|
|
||||||
|
|
||||||
|
|
||||||
class LayerSettings extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
id: React.PropTypes.string.isRequired,
|
|
||||||
type: React.PropTypes.oneOf(Object.keys(GlSpec.layer.type.values)).isRequired,
|
|
||||||
onIdChange: React.PropTypes.func.isRequired,
|
|
||||||
onTypeChange: React.PropTypes.func.isRequired,
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return <div style={{
|
|
||||||
padding: margins[2],
|
|
||||||
paddingRight: 0,
|
|
||||||
backgroundColor: colors.black,
|
|
||||||
}}>
|
|
||||||
<InputBlock label={"Layer ID"}>
|
|
||||||
<StringInput
|
|
||||||
value={this.props.id}
|
|
||||||
onChange={this.props.onIdChange}
|
|
||||||
/>
|
|
||||||
</InputBlock>
|
|
||||||
<InputBlock label={"Layer Type"}>
|
|
||||||
<SelectInput
|
|
||||||
options={[
|
|
||||||
['background', 'Background'],
|
|
||||||
['fill', 'Fill'],
|
|
||||||
['line', 'Line'],
|
|
||||||
['symbol', 'Symbol'],
|
|
||||||
['raster', 'Raster'],
|
|
||||||
['circle', 'Circle'],
|
|
||||||
['fill-extrusion', 'Fill Extrusion'],
|
|
||||||
]}
|
|
||||||
onChange={this.props.onTypeChange}
|
|
||||||
value={this.props.type}
|
|
||||||
/>
|
|
||||||
</InputBlock>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default LayerSettings
|
|
||||||
32
src/components/layers/LayerSourceBlock.jsx
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
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 AutocompleteInput from '../inputs/AutocompleteInput'
|
||||||
|
|
||||||
|
class LayerSourceBlock extends React.Component {
|
||||||
|
static propTypes = {
|
||||||
|
value: React.PropTypes.string,
|
||||||
|
onChange: React.PropTypes.func,
|
||||||
|
sourceIds: React.PropTypes.array,
|
||||||
|
}
|
||||||
|
|
||||||
|
static defaultProps = {
|
||||||
|
onChange: () => {},
|
||||||
|
sourceIds: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return <InputBlock label={"Source"} doc={GlSpec.layer.source.doc}>
|
||||||
|
<AutocompleteInput
|
||||||
|
value={this.props.value}
|
||||||
|
onChange={this.props.onChange}
|
||||||
|
options={this.props.sourceIds.map(src => [src, src])}
|
||||||
|
/>
|
||||||
|
</InputBlock>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LayerSourceBlock
|
||||||
32
src/components/layers/LayerSourceLayerBlock.jsx
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
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 AutocompleteInput from '../inputs/AutocompleteInput'
|
||||||
|
|
||||||
|
class LayerSourceLayer extends React.Component {
|
||||||
|
static propTypes = {
|
||||||
|
value: React.PropTypes.string,
|
||||||
|
onChange: React.PropTypes.func,
|
||||||
|
sourceLayerIds: React.PropTypes.array,
|
||||||
|
}
|
||||||
|
|
||||||
|
static defaultProps = {
|
||||||
|
onChange: () => {},
|
||||||
|
sourceLayerIds: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return <InputBlock label={"Source Layer"} doc={GlSpec.layer['source-layer'].doc}>
|
||||||
|
<AutocompleteInput
|
||||||
|
value={this.props.value}
|
||||||
|
onChange={this.props.onChange}
|
||||||
|
options={this.props.sourceLayerIds.map(l => [l, l])}
|
||||||
|
/>
|
||||||
|
</InputBlock>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LayerSourceLayer
|
||||||
32
src/components/layers/LayerTypeBlock.jsx
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
import GlSpec from 'mapbox-gl-style-spec/reference/latest.js'
|
||||||
|
import InputBlock from '../inputs/InputBlock'
|
||||||
|
import SelectInput from '../inputs/SelectInput'
|
||||||
|
|
||||||
|
class LayerTypeBlock extends React.Component {
|
||||||
|
static propTypes = {
|
||||||
|
value: React.PropTypes.string.isRequired,
|
||||||
|
onChange: React.PropTypes.func.isRequired,
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return <InputBlock label={"Type"} doc={GlSpec.layer.type.doc}>
|
||||||
|
<SelectInput
|
||||||
|
options={[
|
||||||
|
['background', 'Background'],
|
||||||
|
['fill', 'Fill'],
|
||||||
|
['line', 'Line'],
|
||||||
|
['symbol', 'Symbol'],
|
||||||
|
['raster', 'Raster'],
|
||||||
|
['circle', 'Circle'],
|
||||||
|
['fill-extrusion', 'Fill Extrusion'],
|
||||||
|
]}
|
||||||
|
onChange={this.props.onChange}
|
||||||
|
value={this.props.value}
|
||||||
|
/>
|
||||||
|
</InputBlock>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LayerTypeBlock
|
||||||
26
src/components/layers/MaxZoomBlock.jsx
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
import GlSpec from 'mapbox-gl-style-spec/reference/latest.js'
|
||||||
|
import InputBlock from '../inputs/InputBlock'
|
||||||
|
import NumberInput from '../inputs/NumberInput'
|
||||||
|
|
||||||
|
class MaxZoomBlock extends React.Component {
|
||||||
|
static propTypes = {
|
||||||
|
value: React.PropTypes.number.isRequired,
|
||||||
|
onChange: React.PropTypes.func.isRequired,
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return <InputBlock label={"Max Zoom"} doc={GlSpec.layer.maxzoom.doc}>
|
||||||
|
<NumberInput
|
||||||
|
value={this.props.value}
|
||||||
|
onChange={this.props.onChange}
|
||||||
|
min={GlSpec.layer.maxzoom.minimum}
|
||||||
|
max={GlSpec.layer.maxzoom.maximum}
|
||||||
|
default={GlSpec.layer.maxzoom.maximum}
|
||||||
|
/>
|
||||||
|
</InputBlock>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MaxZoomBlock
|
||||||
26
src/components/layers/MinZoomBlock.jsx
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
import GlSpec from 'mapbox-gl-style-spec/reference/latest.js'
|
||||||
|
import InputBlock from '../inputs/InputBlock'
|
||||||
|
import NumberInput from '../inputs/NumberInput'
|
||||||
|
|
||||||
|
class MinZoomBlock extends React.Component {
|
||||||
|
static propTypes = {
|
||||||
|
value: React.PropTypes.number.isRequired,
|
||||||
|
onChange: React.PropTypes.func.isRequired,
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return <InputBlock label={"Min Zoom"} doc={GlSpec.layer.minzoom.doc}>
|
||||||
|
<NumberInput
|
||||||
|
value={this.props.value}
|
||||||
|
onChange={this.props.onChange}
|
||||||
|
min={GlSpec.layer.minzoom.minimum}
|
||||||
|
max={GlSpec.layer.minzoom.maximum}
|
||||||
|
default={GlSpec.layer.minzoom.minimum}
|
||||||
|
/>
|
||||||
|
</InputBlock>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MinZoomBlock
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
|
|
||||||
import PropertyGroup from '../fields/PropertyGroup'
|
|
||||||
import input from '../../config/input.js'
|
|
||||||
|
|
||||||
/** Choose tileset (source) and the source layer */
|
|
||||||
export default class SourceEditor extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
source: React.PropTypes.string.isRequired,
|
|
||||||
sourceLayer: React.PropTypes.string.isRequired,
|
|
||||||
|
|
||||||
onSourceChange: React.PropTypes.func.isRequired,
|
|
||||||
onSourceLayerChange: React.PropTypes.func.isRequired,
|
|
||||||
|
|
||||||
/** List of available sources in the style
|
|
||||||
* https://www.mapbox.com/mapbox-gl-style-spec/#root-sources */
|
|
||||||
sources: React.PropTypes.object.isRequired,
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const options = Object.keys(this.props.sources).map(sourceId => {
|
|
||||||
return <option key={sourceId} value={sourceId}>{sourceId}</option>
|
|
||||||
})
|
|
||||||
|
|
||||||
const layerOptions = this.props.sources[this.props.source].map(vectorLayerId => {
|
|
||||||
const id = vectorLayerId
|
|
||||||
return <option key={id} value={id}>{id}</option>
|
|
||||||
})
|
|
||||||
|
|
||||||
return <div>
|
|
||||||
<div style={input.property}>
|
|
||||||
<label style={input.label}>Source</label>
|
|
||||||
<select
|
|
||||||
style={input.select}
|
|
||||||
value={this.props.source}
|
|
||||||
onChange={(e) => this.onSourceChange(e.target.value)}
|
|
||||||
>
|
|
||||||
{options}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div style={input.property}>
|
|
||||||
<label style={input.label}>Source Layer</label>
|
|
||||||
<select
|
|
||||||
style={input.select}
|
|
||||||
value={this.props.sourceLayer}
|
|
||||||
onChange={(e) => this.onSourceLayerChange(e.target.value)}
|
|
||||||
>
|
|
||||||
{layerOptions}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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
|
||||||
44
src/components/map/FeaturePropertyPopup.jsx
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import InputBlock from '../inputs/InputBlock'
|
||||||
|
import StringInput from '../inputs/StringInput'
|
||||||
|
|
||||||
|
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 key={propertyName} label={propertyName}>
|
||||||
|
<StringInput value={displayValue(property)} style={{backgroundColor: 'transparent'}}/>
|
||||||
|
</InputBlock>
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderFeature(feature) {
|
||||||
|
return <div key={feature.id}>
|
||||||
|
<div className="maputnik-popup-layer-id">{feature.layer['source-layer']}</div>
|
||||||
|
<InputBlock key={"property-type"} label={"$type"}>
|
||||||
|
<StringInput value={feature.geometry.type} style={{backgroundColor: 'transparent'}} />
|
||||||
|
</InputBlock>
|
||||||
|
{renderProperties(feature)}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
class FeaturePropertyPopup extends React.Component {
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const features = this.props.features
|
||||||
|
return <div className="maputnik-feature-property-popup">
|
||||||
|
{features.map(renderFeature)}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export default FeaturePropertyPopup
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
|
|
||||||
export default class Map extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
mapStyle: React.PropTypes.object.isRequired,
|
|
||||||
accessToken: React.PropTypes.string,
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return <div
|
|
||||||
ref={x => this.container = x}
|
|
||||||
style={{
|
|
||||||
position: "fixed",
|
|
||||||
top: 0,
|
|
||||||
bottom: 0,
|
|
||||||
height: "100%",
|
|
||||||
width: "100%",
|
|
||||||
}}></div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,42 +1,155 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import MapboxGl from 'mapbox-gl'
|
import ReactDOM from 'react-dom'
|
||||||
|
import MapboxGl from 'mapbox-gl/dist/mapbox-gl.js'
|
||||||
|
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 validateColor from 'mapbox-gl-style-spec/lib/validate/validate_color'
|
||||||
|
|
||||||
import Map from './Map.jsx'
|
|
||||||
import style from '../../libs/style.js'
|
import style from '../../libs/style.js'
|
||||||
|
import tokens from '../../config/tokens.json'
|
||||||
|
import colors from 'mapbox-gl-inspect/lib/colors'
|
||||||
|
import Color from 'color'
|
||||||
|
import { colorHighlightedLayer } from '../../libs/highlight'
|
||||||
|
import 'mapbox-gl/dist/mapbox-gl.css'
|
||||||
|
import '../../mapboxgl.css'
|
||||||
|
|
||||||
export default class MapboxGlMap extends Map {
|
function renderLayerPopup(features) {
|
||||||
|
var mountNode = document.createElement('div');
|
||||||
|
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 = {
|
static propTypes = {
|
||||||
onMapLoaded: React.PropTypes.func,
|
onDataChange: React.PropTypes.func,
|
||||||
|
mapStyle: React.PropTypes.object.isRequired,
|
||||||
|
inspectModeEnabled: React.PropTypes.bool.isRequired,
|
||||||
|
highlightedLayer: React.PropTypes.object,
|
||||||
}
|
}
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
onMapLoaded: () => {}
|
onMapLoaded: () => {},
|
||||||
|
onDataChange: () => {},
|
||||||
|
mapboxAccessToken: tokens.mapbox,
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props)
|
super(props)
|
||||||
this.state = { map: null }
|
MapboxGl.accessToken = tokens.mapbox
|
||||||
|
this.state = {
|
||||||
|
map: null,
|
||||||
|
inspect: null,
|
||||||
|
isPopupOpen: false,
|
||||||
|
popupX: 0,
|
||||||
|
popupY: 0,
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
componentWillReceiveProps(nextProps) {
|
componentWillReceiveProps(nextProps) {
|
||||||
if(!this.state.map) return
|
if(!this.state.map) return
|
||||||
|
const metadata = nextProps.mapStyle.metadata || {}
|
||||||
|
MapboxGl.accessToken = metadata['maputnik:mapbox_access_token'] || tokens.mapbox
|
||||||
|
|
||||||
|
if(!nextProps.inspectModeEnabled) {
|
||||||
//Mapbox GL now does diffing natively so we don't need to calculate
|
//Mapbox GL now does diffing natively so we don't need to calculate
|
||||||
//the necessary operations ourselves!
|
//the necessary operations ourselves!
|
||||||
this.state.map.setStyle(nextProps.mapStyle, { diff: true})
|
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() {
|
componentDidMount() {
|
||||||
MapboxGl.accessToken = this.props.accessToken
|
|
||||||
|
|
||||||
const map = new MapboxGl.Map({
|
const map = new MapboxGl.Map({
|
||||||
container: this.container,
|
container: this.container,
|
||||||
style: this.props.mapStyle,
|
style: this.props.mapStyle,
|
||||||
});
|
hash: true,
|
||||||
|
})
|
||||||
|
|
||||||
map.on("style.load", (...args) => {
|
const nav = new MapboxGl.NavigationControl();
|
||||||
this.props.onMapLoaded(map)
|
map.addControl(nav, 'top-right');
|
||||||
this.setState({ map });
|
|
||||||
});
|
const inspect = new MapboxInspect({
|
||||||
|
popup: new MapboxGl.Popup({
|
||||||
|
closeOnClick: false
|
||||||
|
}),
|
||||||
|
showMapPopup: true,
|
||||||
|
showMapPopupOnHover: false,
|
||||||
|
showInspectMapPopupOnHover: true,
|
||||||
|
showInspectButton: false,
|
||||||
|
assignLayerColor: (layerId, alpha) => {
|
||||||
|
return Color(colors.brightColor(layerId, alpha)).desaturate(0.5).string()
|
||||||
|
},
|
||||||
|
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, inspect });
|
||||||
|
})
|
||||||
|
|
||||||
|
map.on("data", e => {
|
||||||
|
if(e.dataType !== 'tile') return
|
||||||
|
this.props.onDataChange({
|
||||||
|
map: this.state.map
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return <div
|
||||||
|
className="maputnik-map"
|
||||||
|
ref={x => this.container = x}
|
||||||
|
></div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,27 +1,111 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import Map from './Map'
|
|
||||||
import style from '../../libs/style.js'
|
import style from '../../libs/style.js'
|
||||||
|
import isEqual from 'lodash.isequal'
|
||||||
|
import { loadJSON } from '../../libs/urlopen'
|
||||||
|
|
||||||
class OpenLayers3Map extends Map {
|
function suitableVectorSource(mapStyle) {
|
||||||
constructor(props) {
|
const sources = Object.keys(mapStyle.sources)
|
||||||
super(props)
|
.map(sourceId => {
|
||||||
|
return {
|
||||||
|
id: sourceId,
|
||||||
|
source: mapStyle.sources[sourceId]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter(({source}) => source.type === 'vector')
|
||||||
|
return sources[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillReceiveProps(nextProps) {
|
function toVectorLayer(source, tilegrid, cb) {
|
||||||
require.ensure(["openlayers", "ol-mapbox-style"], ()=> {
|
function newMVTLayer(tileUrl) {
|
||||||
const ol = require('openlayers')
|
const ol = require('openlayers')
|
||||||
const olms = require('ol-mapbox-style')
|
return new ol.layer.VectorTile({
|
||||||
const jsonStyle = nextProps.mapStyle
|
source: new ol.source.VectorTile({
|
||||||
const styleFunc = olms.getStyleFunction(jsonStyle, 'openmaptiles', this.resolutions)
|
format: new ol.format.MVT(),
|
||||||
console.log('New style babee')
|
tileGrid: tilegrid,
|
||||||
|
tilePixelRatio: 8,
|
||||||
|
url: tileUrl
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const layer = this.layer
|
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 = {
|
||||||
|
onDataChange: React.PropTypes.func,
|
||||||
|
mapStyle: React.PropTypes.object.isRequired,
|
||||||
|
accessToken: React.PropTypes.string,
|
||||||
|
}
|
||||||
|
|
||||||
|
static defaultProps = {
|
||||||
|
onMapLoaded: () => {},
|
||||||
|
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)
|
layer.setStyle(styleFunc)
|
||||||
//NOTE: We need to mark the source as changed in order
|
//NOTE: We need to mark the source as changed in order
|
||||||
//to trigger a rerender
|
//to trigger a rerender
|
||||||
layer.getSource().changed()
|
layer.getSource().changed()
|
||||||
|
map.render()
|
||||||
|
}
|
||||||
|
|
||||||
this.state.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"], () => {
|
||||||
|
if(!this.map || !this.resolutions) return
|
||||||
|
this.updateStyle(nextProps.mapStyle)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,35 +118,38 @@ class OpenLayers3Map extends Map {
|
|||||||
const ol = require('openlayers')
|
const ol = require('openlayers')
|
||||||
const olms = require('ol-mapbox-style')
|
const olms = require('ol-mapbox-style')
|
||||||
|
|
||||||
const tilegrid = ol.tilegrid.createXYZ({tileSize: 512, maxZoom: 22})
|
this.tilegrid = ol.tilegrid.createXYZ({tileSize: 512, maxZoom: 22})
|
||||||
this.resolutions = tilegrid.getResolutions()
|
this.resolutions = this.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)
|
|
||||||
|
|
||||||
const map = new ol.Map({
|
const map = new ol.Map({
|
||||||
target: this.container,
|
target: this.container,
|
||||||
layers: [this.layer],
|
layers: [],
|
||||||
view: new ol.View({
|
view: new ol.View({
|
||||||
center: jsonStyle.center,
|
|
||||||
zoom: 2,
|
zoom: 2,
|
||||||
//zoom: jsonStyle.zoom,
|
center: [52.5, -78.4]
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
map.addControl(new ol.control.Zoom());
|
map.addControl(new ol.control.Zoom())
|
||||||
this.setState({ map });
|
this.map = map
|
||||||
|
this.updateStyle(this.props.mapStyle)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return <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>
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default OpenLayers3Map
|
export default OpenLayers3Map
|
||||||
|
|||||||
106
src/components/modals/AddModal.jsx
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
import Button from '../Button'
|
||||||
|
import InputBlock from '../inputs/InputBlock'
|
||||||
|
import StringInput from '../inputs/StringInput'
|
||||||
|
import SelectInput from '../inputs/SelectInput'
|
||||||
|
import Modal from './Modal'
|
||||||
|
|
||||||
|
import LayerTypeBlock from '../layers/LayerTypeBlock'
|
||||||
|
import LayerIdBlock from '../layers/LayerIdBlock'
|
||||||
|
import LayerSourceBlock from '../layers/LayerSourceBlock'
|
||||||
|
import LayerSourceLayerBlock from '../layers/LayerSourceLayerBlock'
|
||||||
|
|
||||||
|
class AddModal extends React.Component {
|
||||||
|
static propTypes = {
|
||||||
|
layers: React.PropTypes.array.isRequired,
|
||||||
|
onLayersChange: React.PropTypes.func.isRequired,
|
||||||
|
isOpen: React.PropTypes.bool.isRequired,
|
||||||
|
onOpenToggle: React.PropTypes.func.isRequired,
|
||||||
|
|
||||||
|
// A dict of source id's and the available source layers
|
||||||
|
sources: React.PropTypes.object.isRequired,
|
||||||
|
}
|
||||||
|
|
||||||
|
addLayer() {
|
||||||
|
const changedLayers = this.props.layers.slice(0)
|
||||||
|
const layer = {
|
||||||
|
id: this.state.id,
|
||||||
|
type: this.state.type,
|
||||||
|
}
|
||||||
|
|
||||||
|
if(this.state.type !== 'background') {
|
||||||
|
layer.source = this.state.source
|
||||||
|
if(this.state.type !== 'raster' && this.state['source-layer']) {
|
||||||
|
layer['source-layer'] = this.state['source-layer']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
changedLayers.push(layer)
|
||||||
|
|
||||||
|
this.props.onLayersChange(changedLayers)
|
||||||
|
this.props.onOpenToggle(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props)
|
||||||
|
this.state = {
|
||||||
|
type: 'fill',
|
||||||
|
id: '',
|
||||||
|
}
|
||||||
|
|
||||||
|
if(props.sources.length > 0) {
|
||||||
|
this.state.source = Object.keys(this.props.sources)[0]
|
||||||
|
this.state['source-layer'] = this.props.sources[this.state.source][0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillReceiveProps(nextProps) {
|
||||||
|
const sourceIds = Object.keys(nextProps.sources)
|
||||||
|
if(!this.state.source && sourceIds.length > 0) {
|
||||||
|
this.setState({
|
||||||
|
source: sourceIds[0],
|
||||||
|
'source-layer': this.state['source-layer'] || (nextProps.sources[sourceIds[0]] || [])[0]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return <Modal
|
||||||
|
isOpen={this.props.isOpen}
|
||||||
|
onOpenToggle={this.props.onOpenToggle}
|
||||||
|
title={'Add Layer'}
|
||||||
|
>
|
||||||
|
<div className="maputnik-add-layer">
|
||||||
|
<LayerIdBlock
|
||||||
|
value={this.state.id}
|
||||||
|
onChange={v => this.setState({ id: v })}
|
||||||
|
/>
|
||||||
|
<LayerTypeBlock
|
||||||
|
value={this.state.type}
|
||||||
|
onChange={v => this.setState({ type: v })}
|
||||||
|
/>
|
||||||
|
{this.state.type !== 'background' &&
|
||||||
|
<LayerSourceBlock
|
||||||
|
sourceIds={Object.keys(this.props.sources)}
|
||||||
|
value={this.state.source}
|
||||||
|
onChange={v => this.setState({ source: v })}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
{this.state.type !== 'background' && 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 className="maputnik-add-layer-button" onClick={this.addLayer.bind(this)}>
|
||||||
|
Add Layer
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AddModal
|
||||||
230
src/components/modals/ExportModal.jsx
Normal file
@@ -0,0 +1,230 @@
|
|||||||
|
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 CheckboxInput from '../inputs/CheckboxInput'
|
||||||
|
import Button from '../Button'
|
||||||
|
import Modal from './Modal'
|
||||||
|
import MdFileDownload from 'react-icons/lib/md/file-download'
|
||||||
|
import style from '../../libs/style.js'
|
||||||
|
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,
|
||||||
|
onStyleChanged: React.PropTypes.func.isRequired,
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
preview: false,
|
||||||
|
saving: false,
|
||||||
|
latestGist: null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillReceiveProps(nextProps) {
|
||||||
|
this.setState({
|
||||||
|
...this.state,
|
||||||
|
preview: !!(nextProps.mapStyle.metadata || {})['maputnik:openmaptiles_access_token']
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onSave() {
|
||||||
|
this.setState({
|
||||||
|
...this.state,
|
||||||
|
saving: true
|
||||||
|
});
|
||||||
|
const preview = this.state.preview && (this.props.mapStyle.metadata || {})['maputnik:openmaptiles_access_token'];
|
||||||
|
|
||||||
|
const mapStyleStr = preview ?
|
||||||
|
formatStyle(stripAccessTokens(style.replaceAccessToken(this.props.mapStyle))) :
|
||||||
|
formatStyle(stripAccessTokens(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 files = {
|
||||||
|
"style.json": {
|
||||||
|
content: mapStyleStr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(preview) {
|
||||||
|
files["index.html"] = {
|
||||||
|
content: htmlStr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const gh = new GitHub();
|
||||||
|
let gist = gh.getGist(); // not a gist yet
|
||||||
|
gist.create({
|
||||||
|
public: true,
|
||||||
|
description: styleTitle,
|
||||||
|
files: files
|
||||||
|
}).then(function({data}) {
|
||||||
|
return gist.read();
|
||||||
|
}).then(function({data}) {
|
||||||
|
this.setState({
|
||||||
|
...this.state,
|
||||||
|
latestGist: data,
|
||||||
|
saving: false,
|
||||||
|
});
|
||||||
|
}.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
onPreviewChange(value) {
|
||||||
|
this.setState({
|
||||||
|
...this.state,
|
||||||
|
preview: value
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
changeMetadataProperty(property, value) {
|
||||||
|
const changedStyle = {
|
||||||
|
...this.props.mapStyle,
|
||||||
|
metadata: {
|
||||||
|
...this.props.mapStyle.metadata,
|
||||||
|
[property]: value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.props.onStyleChanged(changedStyle)
|
||||||
|
}
|
||||||
|
|
||||||
|
renderPreviewLink() {
|
||||||
|
const gist = this.state.latestGist;
|
||||||
|
const user = gist.user || 'anonymous';
|
||||||
|
const preview = !!gist.files['index.html'];
|
||||||
|
if(preview) {
|
||||||
|
return <span><a target="_blank" href={"https://bl.ocks.org/"+user+"/"+gist.id}>Preview</a>,{' '}</span>
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderLatestGist() {
|
||||||
|
const gist = this.state.latestGist;
|
||||||
|
const saving = this.state.saving;
|
||||||
|
if(saving) {
|
||||||
|
return <p>Saving...</p>
|
||||||
|
} else if(gist) {
|
||||||
|
const user = gist.user || 'anonymous';
|
||||||
|
return <p>
|
||||||
|
Latest saved gist:{' '}
|
||||||
|
{this.renderPreviewLink(this)}
|
||||||
|
<a target="_blank" href={"https://gist.github.com/"+user+"/"+gist.id}>Source</a>
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return <div className="maputnik-export-gist">
|
||||||
|
<Button onClick={this.onSave.bind(this)}>
|
||||||
|
<MdFileDownload />
|
||||||
|
Save to Gist (anonymous)
|
||||||
|
</Button>
|
||||||
|
{' '}
|
||||||
|
<CheckboxInput
|
||||||
|
value={this.state.preview}
|
||||||
|
name='gist-style-preview'
|
||||||
|
onChange={this.onPreviewChange.bind(this)}
|
||||||
|
/>
|
||||||
|
<span> Include preview</span>
|
||||||
|
{this.state.preview ?
|
||||||
|
<div>
|
||||||
|
<InputBlock
|
||||||
|
label={"OpenMapTiles Access Token: "}>
|
||||||
|
<StringInput
|
||||||
|
value={(this.props.mapStyle.metadata || {})['maputnik:openmaptiles_access_token']}
|
||||||
|
onChange={this.changeMetadataProperty.bind(this, "maputnik:openmaptiles_access_token")}/>
|
||||||
|
</InputBlock>
|
||||||
|
<a target="_blank" href="https://openmaptiles.com/hosting/">Get your free access token</a>
|
||||||
|
</div>
|
||||||
|
: null}
|
||||||
|
{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,
|
||||||
|
onStyleChanged: React.PropTypes.func.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} onStyleChanged={this.props.onStyleChanged}/>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ExportModal
|
||||||
@@ -1,10 +1,6 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
import CloseIcon from 'react-icons/lib/md/close'
|
import CloseIcon from 'react-icons/lib/md/close'
|
||||||
|
|
||||||
import Overlay from './Overlay'
|
import Overlay from './Overlay'
|
||||||
import colors from '../../config/colors'
|
|
||||||
import { margins, fontSizes } from '../../config/scales'
|
|
||||||
|
|
||||||
class Modal extends React.Component {
|
class Modal extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
@@ -15,32 +11,17 @@ class Modal extends React.Component {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
return <Overlay isOpen={this.props.isOpen}>
|
return <Overlay isOpen={this.props.isOpen}>
|
||||||
<div style={{
|
<div className="maputnik-modal">
|
||||||
minWidth: 350,
|
<header className="maputnik-modal-header">
|
||||||
maxWidth: 600,
|
<h1 className="maputnik-modal-header-title">{this.props.title}</h1>
|
||||||
backgroundColor: colors.black,
|
<span className="maputnik-modal-header-space"></span>
|
||||||
boxShadow: '0px 0px 5px 0px rgba(0,0,0,0.3)',
|
<a className="maputnik-modal-header-toggle"
|
||||||
}}>
|
|
||||||
<div style={{
|
|
||||||
backgroundColor: colors.gray,
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'row',
|
|
||||||
padding: margins[2],
|
|
||||||
fontSize: fontSizes[4],
|
|
||||||
}}>
|
|
||||||
{this.props.title}
|
|
||||||
<span style={{flexGrow: 1}} />
|
|
||||||
<a
|
|
||||||
onClick={() => this.props.onOpenToggle(false)}
|
onClick={() => this.props.onOpenToggle(false)}
|
||||||
style={{ cursor: 'pointer' }} >
|
>
|
||||||
<CloseIcon />
|
<CloseIcon />
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</header>
|
||||||
<div style={{
|
<div className="maputnik-modal-content">{this.props.children}</div>
|
||||||
padding: margins[2],
|
|
||||||
}}>
|
|
||||||
{this.props.children}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</Overlay>
|
</Overlay>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import Modal from './Modal'
|
import Modal from './Modal'
|
||||||
import Heading from '../Heading'
|
|
||||||
import Button from '../Button'
|
import Button from '../Button'
|
||||||
import Paragraph from '../Paragraph'
|
|
||||||
import FileReaderInput from 'react-file-reader-input'
|
import FileReaderInput from 'react-file-reader-input'
|
||||||
import request from 'request'
|
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 AddIcon from 'react-icons/lib/md/add-circle-outline'
|
||||||
|
|
||||||
import style from '../../libs/style.js'
|
import style from '../../libs/style.js'
|
||||||
import colors from '../../config/colors'
|
|
||||||
import { margins, fontSizes } from '../../config/scales'
|
|
||||||
import publicStyles from '../../config/styles.json'
|
import publicStyles from '../../config/styles.json'
|
||||||
|
|
||||||
class PublicStyle extends React.Component {
|
class PublicStyle extends React.Component {
|
||||||
@@ -23,38 +19,18 @@ class PublicStyle extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return <div style={{
|
return <div className="maputnik-public-style">
|
||||||
verticalAlign: 'top',
|
|
||||||
marginTop: margins[2],
|
|
||||||
marginRight: margins[2],
|
|
||||||
backgroundColor: colors.gray,
|
|
||||||
display: 'inline-block',
|
|
||||||
width: 180,
|
|
||||||
fontSize: fontSizes[4],
|
|
||||||
color: colors.lowgray,
|
|
||||||
}}>
|
|
||||||
<Button
|
<Button
|
||||||
|
className="maputnik-public-style-button"
|
||||||
onClick={() => this.props.onSelect(this.props.url)}
|
onClick={() => this.props.onSelect(this.props.url)}
|
||||||
style={{
|
|
||||||
backgroundColor: 'transparent',
|
|
||||||
padding: margins[2],
|
|
||||||
display: 'block',
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<div style={{
|
<header className="maputnik-public-style-header">
|
||||||
display: 'flex',
|
<h4>{this.props.title}</h4>
|
||||||
flexDirection: 'row',
|
<span className="maputnik-space" />
|
||||||
}}>
|
|
||||||
<span style={{fontWeight: 700}}>{this.props.title}</span>
|
|
||||||
<span style={{flexGrow: 1}} />
|
|
||||||
<AddIcon />
|
<AddIcon />
|
||||||
</div>
|
</header>
|
||||||
<img
|
<img
|
||||||
style={{
|
className="maputnik-public-style-thumbnail"
|
||||||
display: 'block',
|
|
||||||
marginTop: margins[1],
|
|
||||||
maxWidth: '100%',
|
|
||||||
}}
|
|
||||||
src={this.props.thumbnailUrl}
|
src={this.props.thumbnailUrl}
|
||||||
alt={this.props.title}
|
alt={this.props.title}
|
||||||
/>
|
/>
|
||||||
@@ -76,7 +52,7 @@ class OpenModal extends React.Component {
|
|||||||
withCredentials: false,
|
withCredentials: false,
|
||||||
}, (error, response, body) => {
|
}, (error, response, body) => {
|
||||||
if (!error && response.statusCode == 200) {
|
if (!error && response.statusCode == 200) {
|
||||||
const mapStyle = style.ensureMetadataExists(JSON.parse(body))
|
const mapStyle = style.ensureStyleValidity(JSON.parse(body))
|
||||||
console.log('Loaded style ', mapStyle.id)
|
console.log('Loaded style ', mapStyle.id)
|
||||||
this.props.onStyleOpen(mapStyle)
|
this.props.onStyleOpen(mapStyle)
|
||||||
} else {
|
} else {
|
||||||
@@ -91,7 +67,7 @@ class OpenModal extends React.Component {
|
|||||||
reader.readAsText(file, "UTF-8");
|
reader.readAsText(file, "UTF-8");
|
||||||
reader.onload = e => {
|
reader.onload = e => {
|
||||||
let mapStyle = JSON.parse(e.target.result)
|
let mapStyle = JSON.parse(e.target.result)
|
||||||
mapStyle = style.ensureMetadataExists(mapStyle)
|
mapStyle = style.ensureStyleValidity(mapStyle)
|
||||||
this.props.onStyleOpen(mapStyle);
|
this.props.onStyleOpen(mapStyle);
|
||||||
}
|
}
|
||||||
reader.onerror = e => console.log(e.target);
|
reader.onerror = e => console.log(e.target);
|
||||||
@@ -113,22 +89,22 @@ class OpenModal extends React.Component {
|
|||||||
onOpenToggle={this.props.onOpenToggle}
|
onOpenToggle={this.props.onOpenToggle}
|
||||||
title={'Open Style'}
|
title={'Open Style'}
|
||||||
>
|
>
|
||||||
<Heading level={4}>Upload Style</Heading>
|
<section className="maputnik-modal-section">
|
||||||
<Paragraph>
|
<h2>Upload Style</h2>
|
||||||
Upload a JSON style from your computer.
|
<p>Upload a JSON style from your computer.</p>
|
||||||
</Paragraph>
|
|
||||||
<FileReaderInput onChange={this.onUpload.bind(this)}>
|
<FileReaderInput onChange={this.onUpload.bind(this)}>
|
||||||
<Button>
|
<Button className="maputnik-upload-button"><FileUploadIcon /> Upload</Button>
|
||||||
<FileUploadIcon />
|
|
||||||
Upload
|
|
||||||
</Button>
|
|
||||||
</FileReaderInput>
|
</FileReaderInput>
|
||||||
|
</section>
|
||||||
<Heading level={4}>Gallery Styles</Heading>
|
<section className="maputnik-modal-section">
|
||||||
<Paragraph>
|
<h2>Gallery Styles</h2>
|
||||||
|
<p>
|
||||||
Open one of the publicly available styles to start from.
|
Open one of the publicly available styles to start from.
|
||||||
</Paragraph>
|
</p>
|
||||||
|
<div className="maputnik-style-gallery-container">
|
||||||
{styleOptions}
|
{styleOptions}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
</Modal>
|
</Modal>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,26 +1,5 @@
|
|||||||
import React from 'react'
|
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 {
|
class Overlay extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
@@ -29,24 +8,15 @@ class Overlay extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return <div style={{
|
let overlayStyle = {}
|
||||||
top: 0,
|
if(!this.props.isOpen) {
|
||||||
right: 0,
|
overlayStyle['display'] = 'none';
|
||||||
bottom: 0,
|
}
|
||||||
left: 0,
|
|
||||||
position: 'fixed',
|
return <div className={"maputnik-overlay"} style={overlayStyle}>
|
||||||
display: this.props.isOpen ? 'flex' : 'none',
|
<div className={"maputnik-overlay-viewport"} />
|
||||||
flexDirection: 'column',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center'
|
|
||||||
}}>
|
|
||||||
<ViewportOverlay />
|
|
||||||
<div style={{
|
|
||||||
zIndex: 3,
|
|
||||||
}}>
|
|
||||||
{this.props.children}
|
{this.props.children}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
|
import GlSpec from 'mapbox-gl-style-spec/reference/latest.js'
|
||||||
import InputBlock from '../inputs/InputBlock'
|
import InputBlock from '../inputs/InputBlock'
|
||||||
import StringInput from '../inputs/StringInput'
|
import StringInput from '../inputs/StringInput'
|
||||||
import SelectInput from '../inputs/SelectInput'
|
import SelectInput from '../inputs/SelectInput'
|
||||||
import Modal from './Modal'
|
import Modal from './Modal'
|
||||||
import colors from '../../config/colors'
|
|
||||||
|
|
||||||
class SettingsModal extends React.Component {
|
class SettingsModal extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
@@ -18,66 +18,85 @@ class SettingsModal extends React.Component {
|
|||||||
super(props);
|
super(props);
|
||||||
}
|
}
|
||||||
|
|
||||||
onChange(property, e) {
|
changeStyleProperty(property, value) {
|
||||||
const changedStyle = this.props.mapStyle.set(property, e.target.value)
|
const changedStyle = {
|
||||||
|
...this.props.mapStyle,
|
||||||
|
[property]: value
|
||||||
|
}
|
||||||
this.props.onStyleChanged(changedStyle)
|
this.props.onStyleChanged(changedStyle)
|
||||||
}
|
}
|
||||||
|
|
||||||
onRendererChange(renderer) {
|
changeMetadataProperty(property, value) {
|
||||||
const changedStyle = {
|
const changedStyle = {
|
||||||
...this.props.mapStyle,
|
...this.props.mapStyle,
|
||||||
metadata: {
|
metadata: {
|
||||||
...this.props.mapStyle.metadata,
|
...this.props.mapStyle.metadata,
|
||||||
'maputnik:renderer': renderer,
|
[property]: value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.props.onStyleChanged(changedStyle)
|
this.props.onStyleChanged(changedStyle)
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
const metadata = this.props.mapStyle.metadata || {}
|
||||||
const inputProps = { }
|
const inputProps = { }
|
||||||
return <Modal
|
return <Modal
|
||||||
isOpen={this.props.isOpen}
|
isOpen={this.props.isOpen}
|
||||||
onOpenToggle={this.props.onOpenToggle}
|
onOpenToggle={this.props.onOpenToggle}
|
||||||
title={'Style Settings'}
|
title={'Style Settings'}
|
||||||
>
|
>
|
||||||
<InputBlock label={"Name"}>
|
<div style={{minWidth: 350}}>
|
||||||
|
<InputBlock label={"Name"} doc={GlSpec.$root.name.doc}>
|
||||||
<StringInput {...inputProps}
|
<StringInput {...inputProps}
|
||||||
value={this.props.mapStyle.name}
|
value={this.props.mapStyle.name}
|
||||||
onChange={this.onChange.bind(this, "name")}
|
onChange={this.changeStyleProperty.bind(this, "name")}
|
||||||
/>
|
/>
|
||||||
</InputBlock>
|
</InputBlock>
|
||||||
<InputBlock label={"Owner"}>
|
<InputBlock label={"Owner"} doc={"Owner ID of the style. Used by Mapbox or future style APIs."}>
|
||||||
<StringInput {...inputProps}
|
<StringInput {...inputProps}
|
||||||
value={this.props.mapStyle.owner}
|
value={this.props.mapStyle.owner}
|
||||||
onChange={this.onChange.bind(this, "owner")}
|
onChange={this.changeStyleProperty.bind(this, "owner")}
|
||||||
/>
|
/>
|
||||||
</InputBlock>
|
</InputBlock>
|
||||||
<InputBlock label={"Sprite URL"}>
|
<InputBlock label={"Sprite URL"} doc={GlSpec.$root.sprite.doc}>
|
||||||
<StringInput {...inputProps}
|
<StringInput {...inputProps}
|
||||||
value={this.props.mapStyle.sprite}
|
value={this.props.mapStyle.sprite}
|
||||||
onChange={this.onChange.bind(this, "sprite")}
|
onChange={this.changeStyleProperty.bind(this, "sprite")}
|
||||||
/>
|
/>
|
||||||
</InputBlock>
|
</InputBlock>
|
||||||
|
|
||||||
<InputBlock label={"Glyphs URL"}>
|
<InputBlock label={"Glyphs URL"} doc={GlSpec.$root.glyphs.doc}>
|
||||||
<StringInput {...inputProps}
|
<StringInput {...inputProps}
|
||||||
value={this.props.mapStyle.glyphs}
|
value={this.props.mapStyle.glyphs}
|
||||||
onChange={this.onChange.bind(this, "glyphs")}
|
onChange={this.changeStyleProperty.bind(this, "glyphs")}
|
||||||
/>
|
/>
|
||||||
</InputBlock>
|
</InputBlock>
|
||||||
|
|
||||||
|
<InputBlock label={"Mapbox Access Token"} doc={"Public access token for Mapbox services."}>
|
||||||
|
<StringInput {...inputProps}
|
||||||
|
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}
|
<SelectInput {...inputProps}
|
||||||
options={[
|
options={[
|
||||||
['mbgljs', 'MapboxGL JS'],
|
['mbgljs', 'MapboxGL JS'],
|
||||||
['ol3', 'Open Layers 3']
|
['ol3', 'Open Layers 3'],
|
||||||
]}
|
]}
|
||||||
value={(this.props.mapStyle.metadata || {})['maputnik:renderer'] || 'mbgljs'}
|
value={metadata['maputnik:renderer'] || 'mbgljs'}
|
||||||
onChange={this.onRendererChange.bind(this)}
|
onChange={this.changeMetadataProperty.bind(this, 'maputnik:renderer')}
|
||||||
/>
|
/>
|
||||||
</InputBlock>
|
</InputBlock>
|
||||||
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,15 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import GlSpec from 'mapbox-gl-style-spec/reference/latest.js'
|
||||||
import Modal from './Modal'
|
import Modal from './Modal'
|
||||||
import Heading from '../Heading'
|
|
||||||
import Button from '../Button'
|
import Button from '../Button'
|
||||||
import Paragraph from '../Paragraph'
|
|
||||||
import InputBlock from '../inputs/InputBlock'
|
import InputBlock from '../inputs/InputBlock'
|
||||||
import StringInput from '../inputs/StringInput'
|
import StringInput from '../inputs/StringInput'
|
||||||
import SelectInput from '../inputs/SelectInput'
|
import SelectInput from '../inputs/SelectInput'
|
||||||
import SourceTypeEditor from '../sources/SourceTypeEditor'
|
import SourceTypeEditor from '../sources/SourceTypeEditor'
|
||||||
|
|
||||||
import style from '../../libs/style'
|
import style from '../../libs/style'
|
||||||
|
import { deleteSource, addSource, changeSource } from '../../libs/source'
|
||||||
import publicSources from '../../config/tilesets.json'
|
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 AddIcon from 'react-icons/lib/md/add-circle-outline'
|
||||||
import DeleteIcon from 'react-icons/lib/md/delete'
|
import DeleteIcon from 'react-icons/lib/md/delete'
|
||||||
@@ -25,31 +23,16 @@ class PublicSource extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return <div style={{
|
return <div className="maputnik-public-source">
|
||||||
verticalAlign: 'top',
|
|
||||||
marginTop: margins[2],
|
|
||||||
marginRight: margins[2],
|
|
||||||
backgroundColor: colors.gray,
|
|
||||||
display: 'inline-block',
|
|
||||||
width: 240,
|
|
||||||
fontSize: fontSizes[4],
|
|
||||||
color: colors.lowgray,
|
|
||||||
}}>
|
|
||||||
<Button
|
<Button
|
||||||
|
className="maputnik-public-source-select"
|
||||||
onClick={() => this.props.onSelect(this.props.id)}
|
onClick={() => this.props.onSelect(this.props.id)}
|
||||||
style={{
|
|
||||||
backgroundColor: 'transparent',
|
|
||||||
padding: margins[2],
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'row',
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<div>
|
<div className="maputnik-public-source-info">
|
||||||
<span style={{fontWeight: 700}}>{this.props.title}</span>
|
<p className="maputnik-public-source-name">{this.props.title}</p>
|
||||||
<br/>
|
<p className="maputnik-public-source-id">#{this.props.id}</p>
|
||||||
<span style={{fontSize: fontSizes[5]}}>#{this.props.id}</span>
|
|
||||||
</div>
|
</div>
|
||||||
<span style={{flexGrow: 1}} />
|
<span className="maputnik-space" />
|
||||||
<AddIcon />
|
<AddIcon />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -57,50 +40,43 @@ class PublicSource extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function editorMode(source) {
|
function editorMode(source) {
|
||||||
if(source.type === 'geojson') return ' geojson'
|
if(source.type === 'raster') {
|
||||||
if(source.type === 'vector' && source.tiles) {
|
if(source.tiles) return 'tilexyz_raster'
|
||||||
return 'tilexyz'
|
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 SourceEditorLayout extends React.Component {
|
class ActiveSourceTypeEditor extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
sourceId: React.PropTypes.string.isRequired,
|
sourceId: React.PropTypes.string.isRequired,
|
||||||
source: React.PropTypes.object.isRequired,
|
source: React.PropTypes.object.isRequired,
|
||||||
onSourceDelete: React.PropTypes.func.isRequired,
|
onDelete: React.PropTypes.func.isRequired,
|
||||||
onSourceChange: React.PropTypes.func.isRequired,
|
onChange: React.PropTypes.func.isRequired,
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const inputProps = { }
|
const inputProps = { }
|
||||||
return <div style={{
|
return <div className="maputnik-active-source-type-editor">
|
||||||
}}>
|
<div className="maputnik-active-source-type-editor-header">
|
||||||
<div style={{
|
<span className="maputnik-active-source-type-editor-header-id">#{this.props.sourceId}</span>
|
||||||
backgroundColor: colors.gray,
|
<span className="maputnik-space" />
|
||||||
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}} />
|
|
||||||
<Button
|
<Button
|
||||||
onClick={this.props.onSourceDelete}
|
className="maputnik-active-source-type-editor-header-delete"
|
||||||
|
onClick={()=> this.props.onDelete(this.props.sourceId)}
|
||||||
style={{backgroundColor: 'transparent'}}
|
style={{backgroundColor: 'transparent'}}
|
||||||
>
|
>
|
||||||
<DeleteIcon />
|
<DeleteIcon />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div style={{
|
<div className="maputnik-active-source-type-editor-content">
|
||||||
borderColor: colors.gray,
|
|
||||||
borderWidth: 2,
|
|
||||||
borderStyle: 'solid',
|
|
||||||
padding: margins[1],
|
|
||||||
}}>
|
|
||||||
<SourceTypeEditor
|
<SourceTypeEditor
|
||||||
onChange={this.props.onSourceChange}
|
onChange={this.props.onChange}
|
||||||
mode={editorMode(this.props.source)}
|
mode={editorMode(this.props.source)}
|
||||||
source={this.props.source}
|
source={this.props.source}
|
||||||
/>
|
/>
|
||||||
@@ -111,59 +87,78 @@ class SourceEditorLayout extends React.Component {
|
|||||||
|
|
||||||
class AddSource extends React.Component {
|
class AddSource extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
onSourceAdd: React.PropTypes.func.isRequired,
|
onAdd: React.PropTypes.func.isRequired,
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props)
|
super(props)
|
||||||
this.state = {
|
this.state = {
|
||||||
mode: 'tilejson',
|
mode: 'tilejson_vector',
|
||||||
source: {
|
sourceId: style.generateId(),
|
||||||
id: style.generateId(),
|
source: this.defaultSource('tilejson_vector'),
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onSourceIdChange(newId) {
|
defaultSource(mode) {
|
||||||
this.setState({
|
const source = (this.state || {}).source || {}
|
||||||
source: {
|
switch(mode) {
|
||||||
...this.state.source,
|
case 'geojson': return {
|
||||||
id: newId,
|
type: 'geojson',
|
||||||
|
data: source.data || 'http://localhost:3000/geojson.json'
|
||||||
}
|
}
|
||||||
})
|
case 'tilejson_vector': return {
|
||||||
|
type: 'vector',
|
||||||
|
url: source.url || 'http://localhost:3000/tilejson.json'
|
||||||
|
}
|
||||||
|
case 'tilexyz_vector': return {
|
||||||
|
type: 'vector',
|
||||||
|
tiles: source.tiles || ['http://localhost:3000/{x}/{y}/{z}.pbf'],
|
||||||
|
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() {
|
render() {
|
||||||
return <div>
|
return <div className="maputnik-add-source">
|
||||||
<InputBlock label={"Source ID"}>
|
<InputBlock label={"Source ID"} doc={"Unique ID that identifies the source and is used in the layer to reference the source."}>
|
||||||
<StringInput
|
<StringInput
|
||||||
value={this.state.source.id}
|
value={this.state.sourceId}
|
||||||
onChange={this.onSourceIdChange.bind(this)}
|
onChange={v => this.setState({ sourceId: v})}
|
||||||
/>
|
/>
|
||||||
</InputBlock>
|
</InputBlock>
|
||||||
<InputBlock label={"Source Type"}>
|
<InputBlock label={"Source Type"} doc={GlSpec.source_tile.type.doc}>
|
||||||
<SelectInput
|
<SelectInput
|
||||||
options={[
|
options={[
|
||||||
['geojson', 'GeoJSON'],
|
['geojson', 'GeoJSON'],
|
||||||
['tilejson', 'Vector (TileJSON URL)'],
|
['tilejson_vector', 'Vector (TileJSON URL)'],
|
||||||
['tilexyz', 'Vector (XYZ URLs)'],
|
['tilexyz_vector', 'Vector (XYZ URLs)'],
|
||||||
|
['tilejson_raster', 'Raster (TileJSON URL)'],
|
||||||
|
['tilexyz_raster', 'Raster (XYZ URL)'],
|
||||||
]}
|
]}
|
||||||
onChange={v => this.setState({mode: v})}
|
onChange={mode => this.setState({mode: mode, source: this.defaultSource(mode)})}
|
||||||
value={this.state.mode}
|
value={this.state.mode}
|
||||||
/>
|
/>
|
||||||
</InputBlock>
|
</InputBlock>
|
||||||
<SourceTypeEditor
|
<SourceTypeEditor
|
||||||
onChange={this.onSourceChange.bind(this)}
|
onChange={src => this.setState({ source: src })}
|
||||||
mode={this.state.mode}
|
mode={this.state.mode}
|
||||||
source={this.state.source}
|
source={this.state.source}
|
||||||
/>
|
/>
|
||||||
<Button onClick={() => this.onSourceAdd(this.state.source)}>
|
<Button
|
||||||
|
className="maputnik-add-source-button"
|
||||||
|
onClick={() => this.props.onAdd(this.state.sourceId, this.state.source)}>
|
||||||
Add Source
|
Add Source
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -178,38 +173,33 @@ class SourcesModal extends React.Component {
|
|||||||
onStyleChanged: React.PropTypes.func.isRequired,
|
onStyleChanged: React.PropTypes.func.isRequired,
|
||||||
}
|
}
|
||||||
|
|
||||||
onSourceAdd(source) {
|
stripTitle(source) {
|
||||||
const changedSources = {
|
const strippedSource = {...source}
|
||||||
...this.props.mapStyle.sources,
|
delete strippedSource['title']
|
||||||
[source.id]: source
|
return strippedSource
|
||||||
}
|
|
||||||
|
|
||||||
const changedStyle = {
|
|
||||||
...this.props.mapStyle,
|
|
||||||
sources: changedSources
|
|
||||||
}
|
|
||||||
|
|
||||||
this.props.onStyleChanged(changedStyle)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const activeSources = Object.keys(this.props.mapStyle.sources).map(sourceId => {
|
const mapStyle = this.props.mapStyle
|
||||||
const source = this.props.mapStyle.sources[sourceId]
|
const activeSources = Object.keys(mapStyle.sources).map(sourceId => {
|
||||||
return <SourceEditorLayout
|
const source = mapStyle.sources[sourceId]
|
||||||
|
return <ActiveSourceTypeEditor
|
||||||
key={sourceId}
|
key={sourceId}
|
||||||
sourceId={sourceId}
|
sourceId={sourceId}
|
||||||
source={source}
|
source={source}
|
||||||
|
onChange={src => this.props.onStyleChanged(changeSource(mapStyle, sourceId, src))}
|
||||||
|
onDelete={() => this.props.onStyleChanged(deleteSource(mapStyle, sourceId))}
|
||||||
/>
|
/>
|
||||||
})
|
})
|
||||||
|
|
||||||
const tilesetOptions = publicSources.filter(source => !(source.id in this.props.mapStyle.sources)).map(source => {
|
const tilesetOptions = Object.keys(publicSources).filter(sourceId => !(sourceId in mapStyle.sources)).map(sourceId => {
|
||||||
|
const source = publicSources[sourceId]
|
||||||
return <PublicSource
|
return <PublicSource
|
||||||
key={source.id}
|
key={sourceId}
|
||||||
id={source.id}
|
id={sourceId}
|
||||||
type={source.type}
|
type={source.type}
|
||||||
title={source.title}
|
title={source.title}
|
||||||
description={source.description}
|
onSelect={() => this.props.onStyleChanged(addSource(mapStyle, sourceId, this.stripTitle(source)))}
|
||||||
onSelect={() => this.onSourceAdd(source)}
|
|
||||||
/>
|
/>
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -219,22 +209,28 @@ class SourcesModal extends React.Component {
|
|||||||
onOpenToggle={this.props.onOpenToggle}
|
onOpenToggle={this.props.onOpenToggle}
|
||||||
title={'Sources'}
|
title={'Sources'}
|
||||||
>
|
>
|
||||||
<Heading level={4}>Active Sources</Heading>
|
<div className="maputnik-modal-section">
|
||||||
|
<h4>Active Sources</h4>
|
||||||
{activeSources}
|
{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>
|
</div>
|
||||||
|
|
||||||
<Heading level={4}>Choose Public Source</Heading>
|
<div className="maputnik-modal-section">
|
||||||
<Paragraph>
|
<h4>Choose Public Source</h4>
|
||||||
|
<p>
|
||||||
Add one of the publicly availble sources to your style.
|
Add one of the publicly availble sources to your style.
|
||||||
</Paragraph>
|
</p>
|
||||||
<div style={{maxwidth: 500}}>
|
<div style={{maxwidth: 500}}>
|
||||||
{tilesetOptions}
|
{tilesetOptions}
|
||||||
</div>
|
</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>
|
</Modal>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
.darkScrollbar::-webkit-scrollbar {
|
|
||||||
background-color: #26282e;
|
|
||||||
width: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.darkScrollbar::-webkit-scrollbar-thumb {
|
|
||||||
border-radius: 6px;
|
|
||||||
-webkit-box-shadow: inset 0 0 6px rgba(0,0,0,.3);
|
|
||||||
background-color: #40444e;
|
|
||||||
padding-left: 2px;
|
|
||||||
padding-right: 2px;
|
|
||||||
}
|
|
||||||
@@ -1,18 +1,23 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import GlSpec from 'mapbox-gl-style-spec/reference/latest.js'
|
||||||
import InputBlock from '../inputs/InputBlock'
|
import InputBlock from '../inputs/InputBlock'
|
||||||
import StringInput from '../inputs/StringInput'
|
import StringInput from '../inputs/StringInput'
|
||||||
|
import NumberInput from '../inputs/NumberInput'
|
||||||
|
|
||||||
class TileJSONSourceEditor extends React.Component {
|
class TileJSONSourceEditor extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
url: React.PropTypes.string.isRequired,
|
source: React.PropTypes.object.isRequired,
|
||||||
onChange: React.PropTypes.func,
|
onChange: React.PropTypes.func.isRequired,
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return <InputBlock label={"TileJSON URL"}>
|
return <InputBlock label={"TileJSON URL"} doc={GlSpec.source_tile.url.doc}>
|
||||||
<StringInput
|
<StringInput
|
||||||
value={this.props.url}
|
value={this.props.source.url}
|
||||||
onChange={this.props.onChange}
|
onChange={url => this.props.onChange({
|
||||||
|
...this.props.source,
|
||||||
|
url: url
|
||||||
|
})}
|
||||||
/>
|
/>
|
||||||
</InputBlock>
|
</InputBlock>
|
||||||
}
|
}
|
||||||
@@ -20,35 +25,51 @@ class TileJSONSourceEditor extends React.Component {
|
|||||||
|
|
||||||
class TileURLSourceEditor extends React.Component {
|
class TileURLSourceEditor extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
tiles: React.PropTypes.array.isRequired,
|
source: React.PropTypes.object.isRequired,
|
||||||
minZoom: React.PropTypes.number.isRequired,
|
onChange: React.PropTypes.func.isRequired,
|
||||||
maxZoom: React.PropTypes.number.isRequired,
|
}
|
||||||
onChange: React.PropTypes.func,
|
|
||||||
|
changeTileUrl(idx, value) {
|
||||||
|
const tiles = this.props.source.tiles.slice(0)
|
||||||
|
tiles[idx] = value
|
||||||
|
this.props.onChange({
|
||||||
|
...this.props.source,
|
||||||
|
tiles: tiles
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
renderTileUrls() {
|
renderTileUrls() {
|
||||||
const prefix = ['1st', '2nd', '3rd', '4th', '5th', '6th', '7th']
|
const prefix = ['1st', '2nd', '3rd', '4th', '5th', '6th', '7th']
|
||||||
return this.props.tiles.map((tileUrl, tileIndex) => {
|
const tiles = this.props.source.tiles || []
|
||||||
return <InputBlock key={tileIndex} label={prefix[tileIndex] + " Tile URL"}>
|
return tiles.map((tileUrl, tileIndex) => {
|
||||||
|
return <InputBlock key={tileIndex} label={prefix[tileIndex] + " Tile URL"} doc={GlSpec.source_tile.tiles.doc}>
|
||||||
<StringInput
|
<StringInput
|
||||||
value={tileUrl}
|
value={tileUrl}
|
||||||
|
onChange={this.changeTileUrl.bind(this, tileIndex)}
|
||||||
/>
|
/>
|
||||||
</InputBlock>
|
</InputBlock>
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
console.log(this.props.tiles)
|
|
||||||
return <div>
|
return <div>
|
||||||
{this.renderTileUrls()}
|
{this.renderTileUrls()}
|
||||||
<InputBlock label={"Min Zoom"}>
|
<InputBlock label={"Min Zoom"} doc={GlSpec.source_tile.minzoom.doc}>
|
||||||
<StringInput
|
<NumberInput
|
||||||
value={this.props.minZoom}
|
value={this.props.source.minzoom || 0}
|
||||||
|
onChange={minzoom => this.props.onChange({
|
||||||
|
...this.props.source,
|
||||||
|
minzoom: minzoom
|
||||||
|
})}
|
||||||
/>
|
/>
|
||||||
</InputBlock>
|
</InputBlock>
|
||||||
<InputBlock label={"Max Zoom"}>
|
<InputBlock label={"Max Zoom"} doc={GlSpec.source_tile.maxzoom.doc}>
|
||||||
<StringInput
|
<NumberInput
|
||||||
value={this.props.maxZoom}
|
value={this.props.source.maxzoom || 22}
|
||||||
|
onChange={maxzoom => this.props.onChange({
|
||||||
|
...this.props.source,
|
||||||
|
maxzoom: maxzoom
|
||||||
|
})}
|
||||||
/>
|
/>
|
||||||
</InputBlock>
|
</InputBlock>
|
||||||
</div>
|
</div>
|
||||||
@@ -58,15 +79,18 @@ class TileURLSourceEditor extends React.Component {
|
|||||||
|
|
||||||
class GeoJSONSourceEditor extends React.Component {
|
class GeoJSONSourceEditor extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
data: React.PropTypes.string.isRequired,
|
source: React.PropTypes.object.isRequired,
|
||||||
onChange: React.PropTypes.func,
|
onChange: React.PropTypes.func.isRequired,
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return <InputBlock label={"GeoJSON Data"}>
|
return <InputBlock label={"GeoJSON Data"} doc={GlSpec.source_geojson.data.doc}>
|
||||||
<StringInput
|
<StringInput
|
||||||
value={this.props.data}
|
value={this.props.source.data}
|
||||||
onChange={this.props.onChange}
|
onChange={data => this.props.onChange({
|
||||||
|
...this.props.source,
|
||||||
|
data: data
|
||||||
|
})}
|
||||||
/>
|
/>
|
||||||
</InputBlock>
|
</InputBlock>
|
||||||
}
|
}
|
||||||
@@ -80,11 +104,16 @@ class SourceTypeEditor extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const source = this.props.source
|
const commonProps = {
|
||||||
|
source: this.props.source,
|
||||||
|
onChange: this.props.onChange,
|
||||||
|
}
|
||||||
switch(this.props.mode) {
|
switch(this.props.mode) {
|
||||||
case 'geojson': return <GeoJSONSourceEditor data={source.data || 'http://localhost:3000/mygeojson.json'} />
|
case 'geojson': return <GeoJSONSourceEditor {...commonProps} />
|
||||||
case 'tilejson': return <TileJSONSourceEditor url={source.url || 'http://localhost:3000/tiles.json'}/>
|
case 'tilejson_vector': return <TileJSONSourceEditor {...commonProps} />
|
||||||
case 'tilexyz': return <TileURLSourceEditor tiles={source.tiles || ['http://localhost:3000/{x}/{y}/{z}.pbf']} minZoom={source.minZoom || 0} maxZoom={source.maxZoom || 14}/>
|
case 'tilexyz_vector': return <TileURLSourceEditor {...commonProps} />
|
||||||
|
case 'tilejson_raster': return <TileJSONSourceEditor {...commonProps} />
|
||||||
|
case 'tilexyz_raster': return <TileURLSourceEditor {...commonProps} />
|
||||||
default: return null
|
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: '#f04',
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
14
src/config/empty-style.json
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"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": []
|
||||||
|
}
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
import colors from './colors'
|
|
||||||
import { margins, fontSizes } from './scales'
|
|
||||||
|
|
||||||
const base = {
|
|
||||||
display: 'inline-block',
|
|
||||||
fontSize: fontSizes[5],
|
|
||||||
lineHeight: 2,
|
|
||||||
paddingLeft: 5,
|
|
||||||
paddingRight: 5,
|
|
||||||
}
|
|
||||||
|
|
||||||
const label = {
|
|
||||||
...base,
|
|
||||||
width: '40%',
|
|
||||||
color: colors.lowgray,
|
|
||||||
userSelect: 'none',
|
|
||||||
}
|
|
||||||
|
|
||||||
const property = {
|
|
||||||
marginTop: margins[2],
|
|
||||||
marginBottom: margins[2],
|
|
||||||
}
|
|
||||||
|
|
||||||
const input = {
|
|
||||||
...base,
|
|
||||||
border: 'none',
|
|
||||||
width: '47%',
|
|
||||||
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,
|
|
||||||
width: '51%',
|
|
||||||
height: '2.3em',
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
|
||||||
base,
|
|
||||||
label,
|
|
||||||
select,
|
|
||||||
input,
|
|
||||||
property,
|
|
||||||
checkbox,
|
|
||||||
}
|
|
||||||
@@ -2,15 +2,7 @@
|
|||||||
"line": {
|
"line": {
|
||||||
"groups": [
|
"groups": [
|
||||||
{
|
{
|
||||||
"title": "Settings",
|
"title": "Paint properties",
|
||||||
"type": "settings"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Source",
|
|
||||||
"type": "source"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Paint",
|
|
||||||
"type": "properties",
|
"type": "properties",
|
||||||
"fields": [
|
"fields": [
|
||||||
"line-opacity",
|
"line-opacity",
|
||||||
@@ -18,66 +10,42 @@
|
|||||||
"line-width",
|
"line-width",
|
||||||
"line-offset",
|
"line-offset",
|
||||||
"line-blur",
|
"line-blur",
|
||||||
"line-pattern"
|
"line-dasharray",
|
||||||
]
|
"line-pattern",
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Secondary",
|
|
||||||
"type": "properties",
|
|
||||||
"fields": [
|
|
||||||
"line-translate",
|
"line-translate",
|
||||||
"line-translate-anchor",
|
"line-translate-anchor",
|
||||||
"line-cap",
|
|
||||||
"line-join",
|
|
||||||
"line-miter-limit",
|
|
||||||
"line-round-limit",
|
|
||||||
"line-dasharray",
|
|
||||||
"line-gap-width"
|
"line-gap-width"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "JSON",
|
"title": "Layout properties",
|
||||||
"type": "jsoneditor"
|
"type": "properties",
|
||||||
|
"fields": [
|
||||||
|
"line-cap",
|
||||||
|
"line-join",
|
||||||
|
"line-miter-limit",
|
||||||
|
"line-round-limit"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"background": {
|
"background": {
|
||||||
"groups": [
|
"groups": [
|
||||||
{
|
{
|
||||||
"title": "Settings",
|
"title": "Paint properties",
|
||||||
"type": "settings"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Source",
|
|
||||||
"type": "source"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Basic",
|
|
||||||
"type": "properties",
|
"type": "properties",
|
||||||
"fields": [
|
"fields": [
|
||||||
"background-color",
|
"background-color",
|
||||||
"background-pattern",
|
"background-pattern",
|
||||||
"background-opacity"
|
"background-opacity"
|
||||||
]
|
]
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "JSON",
|
|
||||||
"type": "jsoneditor"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"fill": {
|
"fill": {
|
||||||
"groups": [
|
"groups": [
|
||||||
{
|
{
|
||||||
"title": "Settings",
|
"title": "Paint properties",
|
||||||
"type": "settings"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Source",
|
|
||||||
"type": "source"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Basic",
|
|
||||||
"type": "properties",
|
"type": "properties",
|
||||||
"fields": [
|
"fields": [
|
||||||
"fill-opacity",
|
"fill-opacity",
|
||||||
@@ -88,49 +56,68 @@
|
|||||||
"fill-translate",
|
"fill-translate",
|
||||||
"fill-translate-anchor"
|
"fill-translate-anchor"
|
||||||
]
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
|
"fill-extrusion": {
|
||||||
|
"groups": [
|
||||||
{
|
{
|
||||||
"title": "JSON",
|
"title": "Paint properties",
|
||||||
"type": "jsoneditor"
|
"type": "properties",
|
||||||
|
"fields": [
|
||||||
|
"fill-extrusion-opacity",
|
||||||
|
"fill-extrusion-color",
|
||||||
|
"fill-extrusion-translate",
|
||||||
|
"fill-extrusion-translate-anchor",
|
||||||
|
"fill-extrusion-pattern",
|
||||||
|
"fill-extrusion-height",
|
||||||
|
"fill-extrusion-base"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"circle": {
|
||||||
|
"groups": [
|
||||||
|
{
|
||||||
|
"title": "Paint properties",
|
||||||
|
"type": "properties",
|
||||||
|
"fields": [
|
||||||
|
"circle-color",
|
||||||
|
"circle-opacity",
|
||||||
|
"circle-stroke-color",
|
||||||
|
"circle-stroke-opacity",
|
||||||
|
"circle-blur",
|
||||||
|
"circle-radius",
|
||||||
|
"circle-stroke-width",
|
||||||
|
"circle-pitch-scale",
|
||||||
|
"circle-translate",
|
||||||
|
"circle-translate-anchor"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"symbol": {
|
"symbol": {
|
||||||
"groups": [
|
"groups": [
|
||||||
{
|
{
|
||||||
"title": "Settings",
|
"title": "General layout properties",
|
||||||
"type": "settings"
|
"type": "properties",
|
||||||
|
"fields": [
|
||||||
|
"symbol-placement",
|
||||||
|
"symbol-spacing",
|
||||||
|
"symbol-avoid-edges"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Source",
|
"title": "Text layout properties",
|
||||||
"type": "source"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Basic",
|
|
||||||
"type": "properties",
|
"type": "properties",
|
||||||
"fields": [
|
"fields": [
|
||||||
"text-field",
|
"text-field",
|
||||||
"text-font",
|
"text-font",
|
||||||
"text-size",
|
"text-size",
|
||||||
"text-line-height"
|
"text-line-height",
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Placement",
|
|
||||||
"type": "properties",
|
|
||||||
"fields": [
|
|
||||||
"symbol-placement",
|
|
||||||
"symbol-spacing",
|
|
||||||
"symbol-avoid-edges",
|
|
||||||
"text-padding",
|
"text-padding",
|
||||||
"text-allow-overlap",
|
"text-allow-overlap",
|
||||||
"text-ignore-placement"
|
"text-ignore-placement",
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Layout",
|
|
||||||
"type": "properties",
|
|
||||||
"fields": [
|
|
||||||
"text-pitch-alignment",
|
"text-pitch-alignment",
|
||||||
"text-rotation-alignment",
|
"text-rotation-alignment",
|
||||||
"text-max-width",
|
"text-max-width",
|
||||||
@@ -146,7 +133,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Icon",
|
"title": "Icon layout properties",
|
||||||
"type": "properties",
|
"type": "properties",
|
||||||
"fields": [
|
"fields": [
|
||||||
"icon-allow-overlap",
|
"icon-allow-overlap",
|
||||||
@@ -164,8 +151,47 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "JSON",
|
"title": "Text paint properties",
|
||||||
"type": "jsoneditor"
|
"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 = [26, 20, 16, 14, 12, 10]
|
|
||||||
@@ -2,37 +2,61 @@
|
|||||||
{
|
{
|
||||||
"id": "klokantech-basic",
|
"id": "klokantech-basic",
|
||||||
"title": "Klokantech Basic",
|
"title": "Klokantech Basic",
|
||||||
"url": "https://rawgit.com/openmaptiles/klokantech-basic-gl-style/gh-pages/style-cdn.json",
|
"url": "https://rawgit.com/openmaptiles/klokantech-basic-gl-style/master/style.json",
|
||||||
"thumbnail": "https://camo.githubusercontent.com/5cf548fdb9fc606f4a452d14fd2a7a959155fd40/68747470733a2f2f6170692e6d6170626f782e636f6d2f7374796c65732f76312f6d6f7267656e6b61666665652f63697578757465726630316135326971716f366b6f6c776b312f7374617469632f382e3534303538372c34372e3337303535352c31342e30382c302e30302c302e30302f363030783430303f6163636573735f746f6b656e3d706b2e65794a31496a6f69625739795a3256756132466d5a6d566c4969776959534936496a497a636d4e304e6c6b6966512e304c52544e6743632d656e76743964354d7a52373577"
|
"thumbnail": "http://maputnik.com/thumbnails/klokantech-basic.png"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "dark-matter",
|
"id": "dark-matter",
|
||||||
"title": "Dark Matter",
|
"title": "Dark Matter",
|
||||||
"url": "https://rawgit.com/openmaptiles/dark-matter-gl-style/gh-pages/style-cdn.json",
|
"url": "https://rawgit.com/openmaptiles/dark-matter-gl-style/master/style.json",
|
||||||
"thumbnail": "https://camo.githubusercontent.com/b73c515d633d2be7368e8e29e3c23e14117fd21b/68747470733a2f2f6170692e6d6170626f782e636f6d2f7374796c65732f76312f6d6f7267656e6b61666665652f6369757878356e37683031396c326870626e396c6970726d6e2f7374617469632f382e3631393138342c34372e3333363230332c392e30372c302e30302c302e30302f363030783430303f6163636573735f746f6b656e3d706b2e65794a31496a6f69625739795a3256756132466d5a6d566c4969776959534936496a497a636d4e304e6c6b6966512e304c52544e6743632d656e76743964354d7a52373577"
|
"thumbnail": "http://maputnik.com/thumbnails/dark-matter.png"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "positron",
|
"id": "positron",
|
||||||
"title": "Positron",
|
"title": "Positron",
|
||||||
"url": "https://rawgit.com/openmaptiles/positron-gl-style/gh-pages/style-cdn.json",
|
"url": "https://rawgit.com/openmaptiles/positron-gl-style/master/style.json",
|
||||||
"thumbnail": "https://camo.githubusercontent.com/0dd866e3fa7b21ada87da69082eac6801e16ec99/68747470733a2f2f6170692e6d6170626f782e636f6d2f7374796c65732f76312f6d6f7267656e6b61666665652f63697578756e37736530313976326a6c387162326a743374662f7374617469632f382e3631393138342c34372e3333363230332c392e30372c302e30302c302e30302f363030783430303f6163636573735f746f6b656e3d706b2e65794a31496a6f69625739795a3256756132466d5a6d566c4969776959534936496a497a636d4e304e6c6b6966512e304c52544e6743632d656e76743964354d7a52373577"
|
"thumbnail": "http://maputnik.com/thumbnails/positron.png"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "osm-bright",
|
"id": "osm-bright",
|
||||||
"title": "OSM Bright",
|
"title": "OSM Bright",
|
||||||
"url": "https://rawgit.com/openmaptiles/osm-bright-gl-style/gh-pages/style-cdn.json",
|
"url": "https://rawgit.com/openmaptiles/osm-bright-gl-style/master/style.json",
|
||||||
"thumbnail": "https://camo.githubusercontent.com/a15e23ab59202c56502e57cde963cb7772ed3bb1/68747470733a2f2f6170692e6d6170626f782e636f6d2f7374796c65732f76312f6f70656e6d617074696c65732f63697736637a7a326e30303234326b6d673668773230626f782f7374617469632f382e3534303538372c34372e3337303535352c31342e30382c302e30302c302e30302f363030783430303f6163636573735f746f6b656e3d706b2e65794a31496a6f696233426c626d3168634852706247567a4969776959534936496d4e70646e593365544a785a7a41774d474d796233427064574a6d616a63784e7a636966512e685031427863786c644968616b4d6350534a4c513151"
|
"thumbnail": "http://maputnik.com/thumbnails/osm-bright.png"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "fiord-color",
|
"id": "osm-liberty",
|
||||||
"title": "Fiord Color",
|
"title": "OSM Liberty",
|
||||||
"url": "https://rawgit.com/openmaptiles/fiord-color-gl-style/gh-pages/style-cdn.json",
|
"url": "https://rawgit.com/lukasmartinelli/osm-liberty/gh-pages/style.json",
|
||||||
"thumbnail": "https://camo.githubusercontent.com/605f2edc30e413b37d16a6ca1d500f265725d76d/68747470733a2f2f6170692e6d6170626f782e636f6d2f7374796c65732f76312f6f70656e6d617074696c65732f6369776775693378353030317732706e7668633063327767302f7374617469632f31302e3938373235382c34362e3435333135302c332e30322c302e30302c302e30302f363030783430303f6163636573735f746f6b656e3d706b2e65794a31496a6f696233426c626d3168634852706247567a4969776959534936496d4e70646e593365544a785a7a41774d474d796233427064574a6d616a63784e7a636966512e685031427863786c644968616b4d6350534a4c513151"
|
"thumbnail": "https://cdn.rawgit.com/lukasmartinelli/osm-liberty/gh-pages/thumbnail.png"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "toner",
|
"id": "empty-style",
|
||||||
"title": "Toner",
|
"title": "Empty Style",
|
||||||
"url": "https://rawgit.com/openmaptiles/toner-color-gl-style/gh-pages/style-cdn.json",
|
"url": "https://rawgit.com/maputnik/editor/master/src/config/empty-style.json",
|
||||||
"thumbnail": "https://cloud.githubusercontent.com/assets/1288339/21422755/86ebe96e-c839-11e6-8337-42742dfe34a2.png"
|
"thumbnail": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAYAAAAECAQAAAAHDYbIAAAAEUlEQVR42mP8/58BDhiJ4wAA974H/U5Xe1oAAAAASUVORK5CYII="
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "mapbox-satellite",
|
||||||
|
"title": "Mapbox Satellite",
|
||||||
|
"url": "https://rawgit.com/mapbox/mapbox-gl-styles/master/styles/satellite-v9.json",
|
||||||
|
"thumbnail": "http://maputnik.com/thumbnails/mapbox-satellite.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "mapbox-bright",
|
||||||
|
"title": "Mapbox Bright",
|
||||||
|
"url": "https://rawgit.com/mapbox/mapbox-gl-styles/master/styles/bright-v9.json",
|
||||||
|
"thumbnail": "http://maputnik.com/thumbnails/mapbox-bright.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "mapbox-basic",
|
||||||
|
"title": "Mapbox Basic",
|
||||||
|
"url": "https://rawgit.com/mapbox/mapbox-gl-styles/master/styles/basic-v9.json",
|
||||||
|
"thumbnail": "http://maputnik.com/thumbnails/mapbox-basic.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "tilezen",
|
||||||
|
"title": "Tilezen",
|
||||||
|
"url": "https://rawgit.com/lukasmartinelli/tilezen-gl-style/master/style.json",
|
||||||
|
"thumbnail": "http://maputnik.com/thumbnails/tilezen.png"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -1,12 +1,15 @@
|
|||||||
[
|
|
||||||
{
|
{
|
||||||
"id": "mapbox-streets",
|
"mapbox-streets": {
|
||||||
"type": "vector",
|
"type": "vector",
|
||||||
"url": "mapbox://mapbox.mapbox-streets-v7",
|
"url": "mapbox://mapbox.mapbox-streets-v7",
|
||||||
"title": "Mapbox Streets"
|
"title": "Mapbox Streets"
|
||||||
},
|
},
|
||||||
{
|
"openmaptiles": {
|
||||||
"id": "tilezen",
|
"type": "vector",
|
||||||
|
"url": "https://free.tilehosting.com/data/v3.json?key={key}",
|
||||||
|
"title": "OpenMapTiles"
|
||||||
|
},
|
||||||
|
"tilezen": {
|
||||||
"type": "vector",
|
"type": "vector",
|
||||||
"tiles": [
|
"tiles": [
|
||||||
"http://tile.mapzen.com/mapzen/vector/v1/{layers}/{z}/{x}/{y}.pbf?api_key=mapzen-RVcyVL7"
|
"http://tile.mapzen.com/mapzen/vector/v1/{layers}/{z}/{x}/{y}.pbf?api_key=mapzen-RVcyVL7"
|
||||||
@@ -14,17 +17,5 @@
|
|||||||
"minZoom": 0,
|
"minZoom": 0,
|
||||||
"maxZoom": 15,
|
"maxZoom": 15,
|
||||||
"title": "Mapzen Vector Tile Service"
|
"title": "Mapzen Vector Tile Service"
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "openmaptiles",
|
|
||||||
"type": "vector",
|
|
||||||
"url": "https://free.tilehosting.com/data/v3.json?key=25ItXg7aI5wurYDtttD",
|
|
||||||
"title": "OpenMapTiles CDN"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "swissnames-landscape",
|
|
||||||
"type": "geojson",
|
|
||||||
"data": "http://swissnames.lukasmartinelli.ch/data/landscape.geojson",
|
|
||||||
"title": "Landscape Names GeoJSON"
|
|
||||||
}
|
}
|
||||||
]
|
}
|
||||||
|
|||||||
4
src/config/tokens.json
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"mapbox": "pk.eyJ1IjoibW9yZ2Vua2FmZmVlIiwiYSI6ImNpeHJmNXNmZTAwNHIycXBid2NqdTJibjMifQ.Dv1-GDpTWi0NP6xW9Fct1w",
|
||||||
|
"openmaptiles": "Og58UhhtiiTaLVlPtPgs"
|
||||||
|
}
|
||||||
BIN
src/favicon.ico
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
src/fonts/Roboto-Medium.ttf
Normal file
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
@@ -1,7 +1,8 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
|
|
||||||
import './index.css'
|
import './favicon.ico'
|
||||||
|
import './styles/index.scss'
|
||||||
import App from './components/App';
|
import App from './components/App';
|
||||||
|
|
||||||
ReactDOM.render(<App/>, document.querySelector("#app"));
|
ReactDOM.render(<App/>, document.querySelector("#app"));
|
||||||
|
|||||||
@@ -1,40 +1,66 @@
|
|||||||
import request from 'request'
|
import request from 'request'
|
||||||
import style from './style.js'
|
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 {
|
export class ApiStyleStore {
|
||||||
supported(cb) {
|
constructor(opts) {
|
||||||
request('http://localhost:8000/styles', (error, response, body) => {
|
this.onLocalStyleChange = opts.onLocalStyleChange || (() => {})
|
||||||
cb(error === undefined)
|
}
|
||||||
|
|
||||||
|
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) {
|
latestStyle(cb) {
|
||||||
if(this.latestStyleId) {
|
if(this.latestStyleId) {
|
||||||
request('http://localhost:8000/styles/' + this.latestStyleId, (error, response, body) => {
|
request(localUrl + '/styles/' + this.latestStyleId, (error, response, body) => {
|
||||||
cb(JSON.parse(body))
|
cb(style.ensureStyleValidity(JSON.parse(body)))
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
request('http://localhost:8000/styles', (error, response, body) => {
|
throw new Error('No latest style available. You need to init the api backend first.')
|
||||||
if (!error && response.statusCode == 200) {
|
|
||||||
const styleIds = JSON.parse(body);
|
|
||||||
this.latestStyleId = styleIds[0];
|
|
||||||
request('http://localhost:8000/styles/' + this.latestStyleId, (error, response, body) => {
|
|
||||||
cb(style.fromJSON(JSON.parse(body)))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save current style replacing previous version
|
// Save current style replacing previous version
|
||||||
save(mapStyle) {
|
save(mapStyle) {
|
||||||
const id = mapStyle.get('id')
|
const id = mapStyle.id
|
||||||
request.put({
|
request.put({
|
||||||
url: 'http://localhost:8000/styles/' + id,
|
url: localUrl + '/styles/' + id,
|
||||||
json: true,
|
json: true,
|
||||||
body: style.toJSON(mapStyle)
|
body: mapStyle
|
||||||
}, (error, response, body) => {
|
}, (error, response, body) => {
|
||||||
console.log('Saved style');
|
if(error) console.error(error)
|
||||||
})
|
})
|
||||||
return mapStyle
|
return mapStyle
|
||||||
}
|
}
|
||||||
|
|||||||
13
src/libs/diffmessage.js
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import diffStyles from 'mapbox-gl-style-spec/lib/diff'
|
||||||
|
|
||||||
|
export function diffMessages(beforeStyle, afterStyle) {
|
||||||
|
const changes = diffStyles(beforeStyle, afterStyle)
|
||||||
|
return changes.map(cmd => cmd.command + ' ' + cmd.args.join(' '))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function undoMessages(beforeStyle, afterStyle) {
|
||||||
|
return diffMessages(beforeStyle, afterStyle).map(m => 'Undo ' + m)
|
||||||
|
}
|
||||||
|
export function redoMessages(beforeStyle, afterStyle) {
|
||||||
|
return diffMessages(beforeStyle, afterStyle).map(m => 'Redo ' + m)
|
||||||
|
}
|
||||||
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)
|
||||||
40
src/libs/highlight.js
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
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(l.type === 'circle') {
|
||||||
|
l.paint['circle-radius'] = 3
|
||||||
|
} else if(l.type === 'line') {
|
||||||
|
l.paint['line-width'] = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
if(layer.filter) {
|
||||||
|
l.filter = layer.filter
|
||||||
|
} else {
|
||||||
|
delete l['filter']
|
||||||
|
}
|
||||||
|
l.id = l.id + '_highlight'
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
|
||||||
|
const sourceLayerId = layer['source-layer'] || ''
|
||||||
|
const color = colors.brightColor(sourceLayerId, 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
|
||||||
|
}
|
||||||
44
src/libs/layer.js
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import GlSpec from 'mapbox-gl-style-spec/reference/latest.js'
|
||||||
|
|
||||||
|
export function changeType(layer, newType) {
|
||||||
|
const changedPaintProps = { ...layer.paint }
|
||||||
|
Object.keys(changedPaintProps).forEach(propertyName => {
|
||||||
|
if(!(propertyName in GlSpec['paint_' + newType])) {
|
||||||
|
delete changedPaintProps[propertyName]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const changedLayoutProps = { ...layer.layout }
|
||||||
|
Object.keys(changedLayoutProps).forEach(propertyName => {
|
||||||
|
if(!(propertyName in GlSpec['layout_' + newType])) {
|
||||||
|
delete changedLayoutProps[propertyName]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
...layer,
|
||||||
|
paint: changedPaintProps,
|
||||||
|
layout: changedLayoutProps,
|
||||||
|
type: newType,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A {@property} in either the paint our layout {@group} has changed
|
||||||
|
* to a {@newValue}.
|
||||||
|
*/
|
||||||
|
export function changeProperty(layer, group, property, newValue) {
|
||||||
|
if(group) {
|
||||||
|
return {
|
||||||
|
...layer,
|
||||||
|
[group]: {
|
||||||
|
...layer[group],
|
||||||
|
[property]: newValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
...layer,
|
||||||
|
[property]: newValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,12 +1,15 @@
|
|||||||
import throttle from 'lodash.throttle'
|
import throttle from 'lodash.throttle'
|
||||||
|
import isEqual from 'lodash.isequal'
|
||||||
|
|
||||||
/** Listens to map events to build up a store of available vector
|
/** Listens to map events to build up a store of available vector
|
||||||
* layers contained in the tiles */
|
* layers contained in the tiles */
|
||||||
export default class LayerWatcher {
|
export default class LayerWatcher {
|
||||||
constructor() {
|
constructor(opts = {}) {
|
||||||
|
this.onSourcesChange = opts.onSourcesChange || (() => {})
|
||||||
|
this.onVectorLayersChange = opts.onVectorLayersChange || (() => {})
|
||||||
|
|
||||||
this._sources = {}
|
this._sources = {}
|
||||||
this._vectorLayers = {}
|
this._vectorLayers = {}
|
||||||
this._map= null
|
|
||||||
|
|
||||||
// Since we scan over all features we want to avoid this as much as
|
// Since we scan over all features we want to avoid this as much as
|
||||||
// possible and only do it after a batch of data has loaded because
|
// possible and only do it after a batch of data has loaded because
|
||||||
@@ -14,27 +17,30 @@ export default class LayerWatcher {
|
|||||||
this.throttledAnalyzeVectorLayerFields = throttle(this.analyzeVectorLayerFields, 5000)
|
this.throttledAnalyzeVectorLayerFields = throttle(this.analyzeVectorLayerFields, 5000)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Set the map as soon as the map is initialized */
|
analyzeMap(map) {
|
||||||
set map(m) {
|
const previousSources = { ...this._sources }
|
||||||
|
|
||||||
this._map = m
|
|
||||||
//TODO: At some point we need to unsubscribe when new map is set
|
|
||||||
this._map.on('data', (e) => {
|
|
||||||
if(e.dataType !== 'tile') return
|
|
||||||
|
|
||||||
|
Object.keys(map.style.sourceCaches).forEach(sourceId => {
|
||||||
//NOTE: This heavily depends on the internal API of Mapbox GL
|
//NOTE: This heavily depends on the internal API of Mapbox GL
|
||||||
//so this breaks between Mapbox GL JS releases
|
//so this breaks between Mapbox GL JS releases
|
||||||
this._sources[e.sourceId] = e.style.sourceCaches[e.sourceId]._source.vectorLayerIds
|
this._sources[sourceId] = map.style.sourceCaches[sourceId]._source.vectorLayerIds
|
||||||
this.throttledAnalyzeVectorLayerFields()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if(!isEqual(previousSources, this._sources)) {
|
||||||
|
this.onSourcesChange(this._sources)
|
||||||
}
|
}
|
||||||
|
|
||||||
analyzeVectorLayerFields() {
|
this.throttledAnalyzeVectorLayerFields(map)
|
||||||
|
}
|
||||||
|
|
||||||
|
analyzeVectorLayerFields(map) {
|
||||||
|
const previousVectorLayers = { ...this._vectorLayers }
|
||||||
|
|
||||||
Object.keys(this._sources).forEach(sourceId => {
|
Object.keys(this._sources).forEach(sourceId => {
|
||||||
this._sources[sourceId].forEach(vectorLayerId => {
|
(this._sources[sourceId] || []).forEach(vectorLayerId => {
|
||||||
const knownProperties = this._vectorLayers[vectorLayerId] || {}
|
const knownProperties = this._vectorLayers[vectorLayerId] || {}
|
||||||
const params = { sourceLayer: vectorLayerId }
|
const params = { sourceLayer: vectorLayerId }
|
||||||
this._map.querySourceFeatures(sourceId, params).forEach(feature => {
|
map.querySourceFeatures(sourceId, params).forEach(feature => {
|
||||||
Object.keys(feature.properties).forEach(propertyName => {
|
Object.keys(feature.properties).forEach(propertyName => {
|
||||||
const knownPropertyValues = knownProperties[propertyName] || {}
|
const knownPropertyValues = knownProperties[propertyName] || {}
|
||||||
knownPropertyValues[feature.properties[propertyName]] = {}
|
knownPropertyValues[feature.properties[propertyName]] = {}
|
||||||
@@ -45,6 +51,11 @@ export default class LayerWatcher {
|
|||||||
this._vectorLayers[vectorLayerId] = knownProperties
|
this._vectorLayers[vectorLayerId] = knownProperties
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if(!isEqual(previousVectorLayers, this._vectorLayers)) {
|
||||||
|
this.onVectorLayersChange(this._vectorLayers)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Access all known sources and their vector tile ids */
|
/** Access all known sources and their vector tile ids */
|
||||||
|
|||||||
37
src/libs/metadata.js
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
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([])
|
||||||
|
|
||||||
|
// Special handling because Tileserver GL serves the fontstacks metadata differently
|
||||||
|
// https://github.com/klokantech/tileserver-gl/pull/104
|
||||||
|
let url = urlTemplate.replace('/fonts/{fontstack}/{range}.pbf', '/fontstacks.json')
|
||||||
|
url = url.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)))
|
||||||
|
}
|
||||||
35
src/libs/revisions.js
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
export class RevisionStore {
|
||||||
|
constructor(initialRevisions=[]) {
|
||||||
|
this.revisions = initialRevisions
|
||||||
|
this.currentIdx = initialRevisions.length - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
get latest() {
|
||||||
|
return this.revisions[this.revisions.length - 1]
|
||||||
|
}
|
||||||
|
|
||||||
|
get current() {
|
||||||
|
return this.revisions[this.currentIdx]
|
||||||
|
}
|
||||||
|
|
||||||
|
addRevision(revision) {
|
||||||
|
//TODO: compare new revision style id with old ones
|
||||||
|
//and ensure that it is always the same id
|
||||||
|
this.revisions.push(revision)
|
||||||
|
this.currentIdx++
|
||||||
|
}
|
||||||
|
|
||||||
|
undo() {
|
||||||
|
if(this.currentIdx > 0) {
|
||||||
|
this.currentIdx--
|
||||||
|
}
|
||||||
|
return this.current
|
||||||
|
}
|
||||||
|
|
||||||
|
redo() {
|
||||||
|
if(this.currentIdx < this.revisions.length - 1) {
|
||||||
|
this.currentIdx++
|
||||||
|
}
|
||||||
|
return this.current
|
||||||
|
}
|
||||||
|
}
|
||||||
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,8 +1,10 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import spec from 'mapbox-gl-style-spec/reference/latest.min.js'
|
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
|
// Empty style is always used if no style could be restored or fetched
|
||||||
const emptyStyle = ensureMetadataExists({
|
const emptyStyle = ensureStyleValidity({
|
||||||
version: 8,
|
version: 8,
|
||||||
sources: {},
|
sources: {},
|
||||||
layers: [],
|
layers: [],
|
||||||
@@ -18,14 +20,30 @@ function ensureHasId(style) {
|
|||||||
return style
|
return style
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureHasTimestamp(style) {
|
function ensureHasNoInteractive(style) {
|
||||||
if('created' in style) return style
|
const changedLayers = style.layers.map(layer => {
|
||||||
style.created = new Date().toJSON()
|
const changedLayer = { ...layer }
|
||||||
return style
|
delete changedLayer.interactive
|
||||||
|
return changedLayer
|
||||||
|
})
|
||||||
|
|
||||||
|
const nonInteractiveStyle = {
|
||||||
|
...style,
|
||||||
|
layers: changedLayers
|
||||||
|
}
|
||||||
|
return nonInteractiveStyle
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureMetadataExists(style) {
|
function ensureHasNoRefs(style) {
|
||||||
return ensureHasId(ensureHasTimestamp(style))
|
const derefedStyle = {
|
||||||
|
...style,
|
||||||
|
layers: derefLayers(style.layers)
|
||||||
|
}
|
||||||
|
return derefedStyle
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureStyleValidity(style) {
|
||||||
|
return ensureHasNoInteractive(ensureHasNoRefs(ensureHasId(style)))
|
||||||
}
|
}
|
||||||
|
|
||||||
function indexOfLayer(layers, layerId) {
|
function indexOfLayer(layers, layerId) {
|
||||||
@@ -37,9 +55,32 @@ function indexOfLayer(layers, layerId) {
|
|||||||
return null
|
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 {
|
export default {
|
||||||
ensureMetadataExists,
|
ensureStyleValidity,
|
||||||
emptyStyle,
|
emptyStyle,
|
||||||
indexOfLayer,
|
indexOfLayer,
|
||||||
generateId,
|
generateId,
|
||||||
|
replaceAccessToken,
|
||||||
}
|
}
|
||||||
|
|||||||
0
src/libs/stylegen.js
Normal file
@@ -1,5 +1,6 @@
|
|||||||
import { colorizeLayers } from './style.js'
|
import { colorizeLayers } from './style.js'
|
||||||
import style from './style.js'
|
import style from './style.js'
|
||||||
|
import { loadStyleUrl } from './urlopen'
|
||||||
import publicSources from '../config/styles.json'
|
import publicSources from '../config/styles.json'
|
||||||
import request from 'request'
|
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
|
// Fetch a default style via URL and return it or a fallback style via callback
|
||||||
export function loadDefaultStyle(cb) {
|
export function loadDefaultStyle(cb) {
|
||||||
console.log('Falling back to default style')
|
loadStyleUrl(defaultStyleUrl, cb)
|
||||||
request({
|
|
||||||
url: defaultStyleUrl,
|
|
||||||
withCredentials: false,
|
|
||||||
}, (error, response, body) => {
|
|
||||||
if (!error && response.statusCode == 200) {
|
|
||||||
cb(style.ensureMetadataExists(JSON.parse(body)))
|
|
||||||
} else {
|
|
||||||
console.warn('Could not fetch default style', styleUrl)
|
|
||||||
cb(style.emptyStyle)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return style ids and dates of all styles stored in local storage
|
// Return style ids and dates of all styles stored in local storage
|
||||||
@@ -61,17 +51,6 @@ function styleKey(styleId) {
|
|||||||
return [storagePrefix, stylePrefix, styleId].join(":")
|
return [storagePrefix, stylePrefix, styleId].join(":")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store style independent settings
|
|
||||||
export class SettingsStore {
|
|
||||||
get accessToken() {
|
|
||||||
const token = window.localStorage.getItem(storageKeys.accessToken)
|
|
||||||
return token ? token : ""
|
|
||||||
}
|
|
||||||
set accessToken(val) {
|
|
||||||
window.localStorage.setItem(storageKeys.accessToken, val)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Manages many possible styles that are stored in the local storage
|
// Manages many possible styles that are stored in the local storage
|
||||||
export class StyleStore {
|
export class StyleStore {
|
||||||
// Tile store will load all items from local storage and
|
// Tile store will load all items from local storage and
|
||||||
@@ -80,8 +59,8 @@ export class StyleStore {
|
|||||||
this.mapStyles = loadStoredStyles()
|
this.mapStyles = loadStoredStyles()
|
||||||
}
|
}
|
||||||
|
|
||||||
supported(cb) {
|
init(cb) {
|
||||||
cb(window.localStorage !== undefined)
|
cb(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete entire style history
|
// Delete entire style history
|
||||||
@@ -106,7 +85,7 @@ export class StyleStore {
|
|||||||
|
|
||||||
// Save current style replacing previous version
|
// Save current style replacing previous version
|
||||||
save(mapStyle) {
|
save(mapStyle) {
|
||||||
mapStyle = style.ensureMetadataExists(mapStyle)
|
mapStyle = style.ensureStyleValidity(mapStyle)
|
||||||
const key = styleKey(mapStyle.id)
|
const key = styleKey(mapStyle.id)
|
||||||
window.localStorage.setItem(key, JSON.stringify(mapStyle))
|
window.localStorage.setItem(key, JSON.stringify(mapStyle))
|
||||||
window.localStorage.setItem(storageKeys.latest, mapStyle.id)
|
window.localStorage.setItem(storageKeys.latest, mapStyle.id)
|
||||||
|
|||||||
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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
60
src/mapboxgl.css
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
.mapboxgl-popup-anchor-top .mapboxgl-popup-tip {
|
||||||
|
border-bottom-color: rgb(28, 31, 36);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mapboxgl-popup-anchor-left .mapboxgl-popup-tip {
|
||||||
|
border-right-color: rgb(28, 31, 36);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mapboxgl-popup-anchor-right .mapboxgl-popup-tip {
|
||||||
|
border-left-color: rgb(28, 31, 36);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mapboxgl-popup-anchor-bottom .mapboxgl-popup-tip {
|
||||||
|
border-top-color: rgb(28, 31, 36);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mapboxgl-popup-content {
|
||||||
|
background-color: rgb(28, 31, 36);
|
||||||
|
border-radius: 0px;
|
||||||
|
box-shadow: rgba(0, 0, 0, 0.298039) 0px 0px 5px 0px;
|
||||||
|
padding: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mapboxgl-popup-close-button {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mapboxgl-ctrl-group {
|
||||||
|
background: rgb(28, 31, 36);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mapboxgl-ctrl-group > button {
|
||||||
|
background-color: rgb(28, 31, 36);
|
||||||
|
border-color: rgb(28, 31, 36);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mapboxgl-ctrl-group > button:hover {
|
||||||
|
background-color: rgb(86, 83, 83);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mapboxgl-ctrl-icon.mapboxgl-ctrl-zoom-in {
|
||||||
|
background-image: url("data:image/svg+xml;charset=utf8,%3Csvg%20viewBox%3D%270%200%2020%2020%27%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%3E%0A%20%20%3Cpath%20style%3D%27fill%3A%238e8e8e%3B%27%20d%3D%27M%2010%206%20C%209.446%206%209%206.4459904%209%207%20L%209%209%20L%207%209%20C%206.446%209%206%209.446%206%2010%20C%206%2010.554%206.446%2011%207%2011%20L%209%2011%20L%209%2013%20C%209%2013.55401%209.446%2014%2010%2014%20C%2010.554%2014%2011%2013.55401%2011%2013%20L%2011%2011%20L%2013%2011%20C%2013.554%2011%2014%2010.554%2014%2010%20C%2014%209.446%2013.554%209%2013%209%20L%2011%209%20L%2011%207%20C%2011%206.4459904%2010.554%206%2010%206%20z%27%20%2F%3E%0A%3C%2Fsvg%3E%0A")
|
||||||
|
}
|
||||||
|
|
||||||
|
.mapboxgl-ctrl-icon.mapboxgl-ctrl-zoom-out {
|
||||||
|
background-image: url("data:image/svg+xml;charset=utf8,%3Csvg%20viewBox%3D%270%200%2020%2020%27%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%3E%0A%20%20%3Cpath%20style%3D%27fill%3A%238e8e8e%3B%27%20d%3D%27m%207%2C9%20c%20-0.554%2C0%20-1%2C0.446%20-1%2C1%200%2C0.554%200.446%2C1%201%2C1%20l%206%2C0%20c%200.554%2C0%201%2C-0.446%201%2C-1%200%2C-0.554%20-0.446%2C-1%20-1%2C-1%20z%27%20%2F%3E%0A%3C%2Fsvg%3E%0A")
|
||||||
|
}
|
||||||
|
|
||||||
|
.mapboxgl-ctrl-icon.mapboxgl-ctrl-compass > span.arrow {
|
||||||
|
background-image: url("data:image/svg+xml;charset=utf8,%3Csvg%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%20viewBox%3D%270%200%2020%2020%27%3E%0A%09%3Cpolygon%20fill%3D%27%238e8e8e%27%20points%3D%276%2C9%2010%2C1%2014%2C9%27%2F%3E%0A%09%3Cpolygon%20fill%3D%27%23CCCCCC%27%20points%3D%276%2C11%2010%2C19%2014%2C11%20%27%2F%3E%0A%3C%2Fsvg%3E")
|
||||||
|
}
|
||||||
|
|
||||||
|
.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');
|
||||||
|
}
|
||||||
|
|
||||||
78
src/styles/_base.scss
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
@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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
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;
|
||||||
|
}
|
||||||
|
}
|
||||||