Compare commits

..

148 Commits

Author SHA1 Message Date
Lukas Martinelli
5c286f8d96 Remove static fontstacks.json 2017-01-25 13:47:37 +01:00
Lukas Martinelli
404b53587f Special fontstacks.json handling for Tileserver GL 2017-01-25 13:46:46 +01:00
Lukas Martinelli
e5fbe3b74a Collapse layer groups by default #66 2017-01-25 13:34:10 +01:00
Lukas Martinelli
3f262885ca Highlight selected layer more #62 2017-01-25 13:23:54 +01:00
Lukas Martinelli
c837179f71 Clean up layer.scss 2017-01-25 13:23:29 +01:00
Lukas Martinelli
9a947658e2 Improve default property styling #92 2017-01-25 12:54:33 +01:00
Lukas Martinelli
2458d4b637 Show inspect tooltip only on click in map #90 2017-01-22 21:16:11 +01:00
Lukas Martinelli
e4850805fb Fix default tileset of OpenMapTiles #88 2017-01-18 13:06:24 +01:00
Lukas Martinelli
3a15a3bb06 Show type of feature in popup 2017-01-18 10:03:15 +01:00
Lukas Martinelli
75ca1fa930 Deal with no metadata in style 2017-01-16 20:07:21 +01:00
Lukas Martinelli
377840ca24 Fix lint issues in _modal.scss 2017-01-16 16:34:55 +01:00
Lukas Martinelli
48e9589b58 Merge pull request #86 from klokantech/gist-preview
Gist preview & access token
2017-01-16 15:48:12 +01:00
Lukas Martinelli
11e9cef834 Improve styles and text 2017-01-16 15:43:52 +01:00
jirik
7e3aa09d3e Proview & Access Token logic when saving to Gist 2017-01-16 15:13:19 +01:00
jirik
e3b7e002b4 Hypertext links are white instead of blue 2017-01-16 15:13:11 +01:00
jirik
3b7fb7ae75 Fix checkbox not showing status 2017-01-16 15:13:02 +01:00
jirik
fab004cdfe StringInput fires change if state and props values do not match
Now it is also possible to call onChange listener if new value is empty string
2017-01-16 15:12:03 +01:00
Lukas Martinelli
07523c00f0 Point styles to master not gh-pages 2017-01-16 11:08:18 +01:00
Lukas Martinelli
c15ac14f88 Bump version to v1.0.1 2017-01-16 10:14:59 +01:00
Lukas Martinelli
8f6006c19f Less opacity for default values #73 2017-01-15 17:10:38 +01:00
Lukas Martinelli
16bedcf5b1 Add minzoom and maxzoom block #77 2017-01-15 13:46:55 +01:00
Lukas Martinelli
05349d8ffe Convert filter value to number if possible #63 2017-01-15 13:39:40 +01:00
Lukas Martinelli
a1e1895651 Deal specially with has operator #84 2017-01-15 10:42:59 +01:00
Lukas Martinelli
a111599850 Save chang event on XYZ editor #85 2017-01-15 10:36:57 +01:00
Lukas Martinelli
121a95cee8 Move my key message up 2017-01-14 15:09:27 +01:00
Lukas Martinelli
decd1f3ea2 Add tilezen style 2017-01-14 14:45:04 +01:00
Lukas Martinelli
c632718324 Remove id from empty style to generate one 2017-01-14 14:41:13 +01:00
Lukas Martinelli
9509b59696 Add open Mapbox styles to gallery 2017-01-14 14:00:32 +01:00
Lukas Martinelli
24dc71344e Merge pull request #81 from maputnik/default-access-token
Default access token
2017-01-13 17:01:14 +01:00
Lukas Martinelli
82a11e4b98 Fix style download and strip metadata 2017-01-13 15:55:49 +01:00
Lukas Martinelli
fc8665ed93 Support fallback tokens and replace key 2017-01-13 15:33:22 +01:00
Lukas Martinelli
ca9424e23d Remove interactive from style for diffing to work 2017-01-13 15:31:08 +01:00
Lukas Martinelli
99856b1bb3 Update README.md 2017-01-13 14:40:02 +01:00
Lukas Martinelli
fb518c2be5 Add sponsor logos 2017-01-13 14:12:26 +01:00
Lukas Martinelli
1248a53029 Update README.md 2017-01-13 14:08:22 +01:00
Lukas Martinelli
6ce43840e5 Update README.md 2017-01-13 10:18:20 +01:00
Lukas Martinelli
41d9fb1c44 Newest Mapbox GL Inspect plugin for less fidly selecting 2017-01-12 22:54:20 +01:00
Lukas Martinelli
fd9be8f08f Merge pull request #78 from klokantech/export
Export to Gist anonymously, related to maputnik/editor#3
2017-01-12 22:49:49 +01:00
jirik
69a665373f Export to Gist anonymously, related to maputnik/editor#3 2017-01-12 18:27:44 +01:00
Helge Fahrnberger
8c2b110115 Merge pull request #76 from maputnik/property-groups-reorder
Move line-dasharray to correct group
2017-01-12 18:15:49 +01:00
Helge Fahrnberger
5e3b2dd0df Merge branch 'master' into property-groups-reorder 2017-01-12 18:14:25 +01:00
Helge Fahrnberger
d045213fa3 Move line-dasharray to correct group 2017-01-12 18:13:15 +01:00
Lukas Martinelli
63bba67750 Merge pull request #71 from maputnik/property-groups-reorder
Reordered and renamed groups
2017-01-12 17:47:34 +01:00
Helge Fahrnberger
52e8fd2c29 Add missing properties 2017-01-12 17:44:56 +01:00
Lukas Martinelli
5479b240e1 Fix empty field causing exceptions 2017-01-12 17:28:45 +01:00
Lukas Martinelli
f209d8e9a5 Fix layout.json quote tokens 2017-01-12 17:01:30 +01:00
Lukas Martinelli
ac40d7727e Fix popup layer issue 2017-01-12 16:59:38 +01:00
Lukas Martinelli
7bd9d3f5da Remove GeoJSON example from tilesets 2017-01-12 16:59:38 +01:00
Lukas Martinelli
68685dcf42 Only set source layer if not undefined 2017-01-12 16:59:38 +01:00
jirik
6be6db8f5e Fix hidden map attributions (CSS issue) 2017-01-12 15:34:38 +01:00
Helge Fahrnberger
236dd79b85 Reordered and renamed groups
Purpose: Match structure and wording with Mapbox GL style spec.
2017-01-12 14:31:26 +01:00
Lukas Martinelli
7d905c5e06 Update dependencies 2017-01-12 11:50:08 +01:00
Lukas Martinelli
6fa2542b56 Fix color of compound filter 2017-01-12 11:35:11 +01:00
Lukas Martinelli
7627b8fb45 Fix empty style url in config 2017-01-12 11:33:46 +01:00
Lukas Martinelli
5901427534 Move empty style to config dir 2017-01-12 11:32:20 +01:00
Lukas Martinelli
a30e57c4d8 Add empty style option 2017-01-12 11:31:16 +01:00
Lukas Martinelli
69f2e12ea0 Add stylelint and fix lint issues 2017-01-12 11:23:06 +01:00
Lukas Martinelli
93c7f323fc Upgrade Mapbox GL inspect and remove unused lodash 2017-01-12 10:44:44 +01:00
Lukas Martinelli
cbe2a4c180 Fix GeoJSON and default source issue in Sources modal 2017-01-12 10:28:03 +01:00
Lukas Martinelli
2e0cc4511c Improve add layer button visually 2017-01-11 20:48:15 +01:00
Lukas Martinelli
bcab165f97 Select highlighted multibutton 2017-01-11 20:43:40 +01:00
Lukas Martinelli
2516fba105 Animate opacity on layer group collapse 2017-01-11 19:57:12 +01:00
Lukas Martinelli
9ca8760564 Absolute position to not take up space 2017-01-11 19:50:18 +01:00
Lukas Martinelli
df94d9c842 Prevent same prefix from being collapsed 2017-01-11 19:39:09 +01:00
Lukas Martinelli
abceb457c9 Collapsible layer groups #66 2017-01-11 18:18:59 +01:00
Lukas Martinelli
26a865bb50 Add missing mixins 2017-01-11 17:52:29 +01:00
Lukas Martinelli
d0f047d88a Group layers #66 2017-01-11 17:52:21 +01:00
Lukas Martinelli
76d2d06e77 Make section headers white #64 2017-01-11 16:32:31 +01:00
Lukas Martinelli
6c56006fbf Show choose public sources first #64 2017-01-11 16:26:14 +01:00
Lukas Martinelli
bbe45cf8ee Switch text in inspect button #64 2017-01-11 16:23:33 +01:00
Lukas Martinelli
82da251218 Add vendor prefixes 2017-01-11 16:20:10 +01:00
Lukas Martinelli
196d9f0a10 Move add modal to layer list 2017-01-11 15:59:51 +01:00
Lukas Martinelli
cb752c0343 Add layer button and increase contrast 2017-01-11 15:48:15 +01:00
Lukas Martinelli
3917a3e323 Fix multi button style 2017-01-11 14:13:23 +01:00
Lukas Martinelli
fed1f09434 Remove last style configs in JS 2017-01-11 14:11:45 +01:00
Lukas Martinelli
840778b64f Remove JS input config 2017-01-11 14:03:48 +01:00
Lukas Martinelli
0908856b4f Restructure CSS more 2017-01-11 13:34:38 +01:00
Lukas Martinelli
b51354ae1d All important stuff is in CSS now 2017-01-11 11:35:33 +01:00
Lukas Martinelli
9ef24428fe Style open modal 2017-01-11 09:35:48 +01:00
Lukas Martinelli
4a75b0381b Move style code to CSS 2017-01-10 21:28:30 +01:00
Lukas Martinelli
2426117233 Tweaked colors #64 2017-01-10 19:41:39 +01:00
Lukas Martinelli
d40c704c69 Upgrade mapbox-gl-inspect to v1.0.9 2017-01-10 19:27:27 +01:00
Lukas Martinelli
cb4fdb0f9f Remove rasters in inspect style 2017-01-10 19:14:14 +01:00
Lukas Martinelli
f0d04bdb07 Prepare version for release 2017-01-10 19:14:06 +01:00
Lukas Martinelli
df61ae8c7a Add filter button on the right bottom 2017-01-10 19:02:06 +01:00
Lukas Martinelli
2ff8ec07bb Update style thumbs 2017-01-10 18:42:22 +01:00
Lukas Martinelli
6021b51385 Extra padding prevents hidden layers #61 2017-01-10 15:53:22 +01:00
Lukas Martinelli
40111e0d8e Fix minzoom and maxzoom in source modal 2017-01-10 15:35:13 +01:00
Lukas Martinelli
43d9440e05 White background for OL3 2017-01-10 14:32:45 +01:00
Lukas Martinelli
3a3e90c3dc Support TileJSON sources for OL3 2017-01-10 14:24:35 +01:00
Lukas Martinelli
104d6311ec Add missing IconInput 2017-01-10 14:05:46 +01:00
Lukas Martinelli
f5256cf80a Add missing metadata lib 2017-01-10 14:05:36 +01:00
Lukas Martinelli
b470885263 Use first vector source for OL3 2017-01-10 14:05:25 +01:00
Lukas Martinelli
7ff0ac9bb5 Upgrade ol-mapbox-style to v0.14 2017-01-10 12:04:19 +01:00
Lukas Martinelli
0fb59ca544 Load icon and font metadata from endpoint 2017-01-10 11:13:53 +01:00
Lukas Martinelli
09b6b2dffe Add Roboto Medium for groups #51 2017-01-10 10:06:49 +01:00
Lukas Martinelli
a8a3b7a5ad Always have default value if value not set 2017-01-10 09:51:18 +01:00
Lukas Martinelli
766a3e387e Fix many React warnings 2017-01-10 09:38:27 +01:00
Lukas Martinelli
ec9fc8f6ad Allow passing elems to DocLabel 2017-01-09 23:06:55 +01:00
Lukas Martinelli
0f272e233b Rename to FeatureLayerPopup 2017-01-09 23:04:08 +01:00
Lukas Martinelli
f806e797fa Fix non existing value warning 2017-01-09 23:02:17 +01:00
Lukas Martinelli
cff0a15f7e Show hint when hovering over function icon 2017-01-09 22:54:30 +01:00
Lukas Martinelli
d3276829b2 Show hints in the source modal as well 2017-01-09 22:44:22 +01:00
Lukas Martinelli
a3caf8499c Add DocLabel to settings modal 2017-01-09 22:37:21 +01:00
Lukas Martinelli
d739ca812c No source blocks for background layer 2017-01-09 22:27:54 +01:00
Lukas Martinelli
cb89ca6ef7 Show text when nested filter 2017-01-09 22:20:28 +01:00
Lukas Martinelli
c3417241f1 Ensure zoom icon is nice 2017-01-09 22:09:15 +01:00
Lukas Martinelli
5d70de6202 Center checkbox 2017-01-09 21:43:14 +01:00
Lukas Martinelli
c09ffc9d41 Tweak margins to realign 2017-01-09 21:39:35 +01:00
Lukas Martinelli
e19a41d015 Change filter layout again 2017-01-09 21:30:49 +01:00
Lukas Martinelli
0a0400a297 Rearrange and simplify filter layout 2017-01-09 21:07:51 +01:00
Lukas Martinelli
153232c143 Add filter editor block 2017-01-09 20:07:48 +01:00
Lukas Martinelli
7e8813f417 Split filter editor into component per file 2017-01-09 18:56:04 +01:00
Lukas Martinelli
b72f86a78d Improve grouping 2017-01-09 18:43:04 +01:00
Lukas Martinelli
fed530f5f2 Filter out combining operator select 2017-01-09 17:47:35 +01:00
Lukas Martinelli
ba0a94f3ad Use DocLabel in input block 2017-01-09 16:45:59 +01:00
Lukas Martinelli
d9b458d7fd Add label to filter editor 2017-01-09 16:40:09 +01:00
Lukas Martinelli
ed9b806143 Add filter item 2017-01-09 16:33:26 +01:00
Lukas Martinelli
5bb68a38c2 Support delete filter 2017-01-09 16:24:42 +01:00
Lukas Martinelli
cfeaf2cdce Support turning property into zoom func #52 2017-01-09 16:08:22 +01:00
Lukas Martinelli
887b23ce1f Merge pull request #59 from maputnik/switch_gl_inspect
Switch to Mapbox GL Inspect
2017-01-09 12:04:26 +01:00
Lukas Martinelli
f227392f9b Upgrade inspect to v1.0.7 2017-01-09 12:03:47 +01:00
Lukas Martinelli
2f7658e245 Only increase stack size in travis build 2017-01-09 11:39:52 +01:00
Lukas Martinelli
4f0c641eb0 Upgrade inspect 2017-01-09 00:08:50 +01:00
Lukas Martinelli
1538f2e174 Get highlight working 2017-01-08 23:19:21 +01:00
Lukas Martinelli
580068bf63 Show popup also on normal map 2017-01-08 22:44:25 +01:00
Lukas Martinelli
91604afccb Ensure style updates are applied after inspect 2017-01-08 22:16:45 +01:00
Lukas Martinelli
c363c88f23 Use Mapbox GL Inspect 2017-01-08 22:03:21 +01:00
Lukas Martinelli
e9daee4470 Add raster layout group 2017-01-08 19:58:19 +01:00
Lukas Martinelli
118f0360d0 Hide source layer for raster source 2017-01-08 19:47:43 +01:00
Lukas Martinelli
7c9dcb3083 Refactor sources modal 2017-01-08 19:45:44 +01:00
Lukas Martinelli
7c3906fa40 Add raster XYZ and TileJSON options #57 2017-01-08 18:50:32 +01:00
Lukas Martinelli
7b24cbf39b Increase stack size in node 2017-01-08 17:15:35 +01:00
Lukas Martinelli
e7b11d8bc9 Ensure editor does not crash with raster layers 2017-01-08 17:15:35 +01:00
Lukas Martinelli
08854cd88f Merge pull request #55 from klokantech/format
Format style on download
2017-01-06 16:53:20 +01:00
jirik
cb46ac5421 Format style on download 2017-01-06 15:48:57 +01:00
Lukas Martinelli
c9fd00e2ed Update README.md 2017-01-06 09:53:57 +01:00
Lukas Martinelli
7c23fe3646 Open style from url #34 2017-01-05 19:34:53 +01:00
Lukas Martinelli
56aacb0149 Do not generate created timestamp 2017-01-05 19:34:53 +01:00
Helge Fahrnberger
12411ee886 Linked myself 2017-01-04 20:26:04 +01:00
Lukas Martinelli
85cef2945d StringInput triggers change on out of focus #46 2017-01-04 12:06:55 +01:00
Lukas Martinelli
a1dfeca6e0 Keep existing metadata when toggling inspection #45 2017-01-04 12:00:00 +01:00
Lukas Martinelli
3be6d14637 No dropping of console 2017-01-04 12:00:00 +01:00
Lukas Martinelli
74b3ef9e88 Do not set modified date when saving 2017-01-04 12:00:00 +01:00
Lukas Martinelli
019dfe9f8a Update README.md 2017-01-01 22:29:22 +01:00
Lukas Martinelli
e92dfd8284 Fix local update to right this 2017-01-01 15:51:22 +01:00
Lukas Martinelli
fa38667125 Only init websocket if local API 2017-01-01 15:12:46 +01:00
Lukas Martinelli
ce39ae723c Add support local Maputnik 2017-01-01 14:49:32 +01:00
102 changed files with 3160 additions and 2050 deletions

View File

@@ -16,6 +16,7 @@ install:
- npm install
script:
- mkdir public
- npm run build
- node --stack_size=100000 $(which npm) run build
- npm run lint
- npm run lint-styles
- npm run test

116
README.md
View File

@@ -3,20 +3,25 @@
<img width="200" align="right" alt="Maputnik" src="src/img/maputnik.png" />
A free and open visual editor for the [Mapbox GL styles](https://www.mapbox.com/mapbox-gl-style-spec/)
targeted at developers and map designers. Creating your own custom map is easy with **Maputnik**.
targeted at developers and map designers.
Check it out at **http://maputnik.com/editor/**
- :link: Design your maps online at **http://maputnik.com/editor/** (all in local storage)
- :link: Use the [Maputnik CLI](https://github.com/maputnik/editor/wiki/Maputnik-CLI) for local style development
*Maputnik is an early prototype and is under development.
[Thanks to the supporters of the Kickstarter campaign who made this project possible](https://www.kickstarter.com/projects/174808720/maputnik-visual-map-editor-for-mapbox-gl)*.
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.
## Latest Status Update Video
## Documentation
![Latest Status Update for Maputnik v0.2.2](https://j.gifs.com/g5XMgl.gif)
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
[![Design Map from Scratch](https://j.gifs.com/g5XMgl.gif)](https://youtu.be/XoDh0gEnBQo)
## Develop
Maputnik is written in ES6 and is using [React](https://github.com/facebook/react), [Immutable.js](https://facebook.github.io/immutable-js/) and [Mapbox GL JS](https://www.mapbox.com/mapbox-gl-js/api/).
Maputnik is written in ES6 and is using [React](https://github.com/facebook/react) and [Mapbox GL JS](https://www.mapbox.com/mapbox-gl-js/api/).
We ensure building and developing Maputnik works with
@@ -41,103 +46,64 @@ npm run build
Lint the JavaScript code.
```
# install lint dependencies
npm install --save-dev eslint eslint-plugin-react
# run linter
npm run lint
```
## Docker
Start a container using the official Docker image.
```
docker run --name maputnik -p 8888:8888 -d maputnik/editor
```
Stop the container
```
docker stop maputnik
npm run lint-styles
```
## Sponsors
This project would not be possible without commercial and individual sponsors.
Thanks to the supporters of the **[Kickstarter campaign](https://www.kickstarter.com/projects/174808720/maputnik-visual-map-editor-for-mapbox-gl)**. This project would not be possible without these commercial and individual sponsors.
### Gold
[![Wemap](media/sponsors/wemap.jpg)](https://getwemap.com/)
- [Wemap](https://getwemap.com/)
- [Orbicon Informatik](https://www.orbiconinformatik.dk/)
- [Terranodo](http://terranodo.io/)
[![Terranodo](media/sponsors/terranodo.png)](http://terranodo.io/)
<a href="https://getwemap.com/">
<img width="33%" alt="Wemap" style="display:inline" src="media/sponsors/wemap.jpg" />
</a>
<a href="http://terranodo.io/">
<img width="33%" alt="Terranodo" style="display:inline" src="media/sponsors/terranodo.png" />
</a>
<a href="https://www.orbiconinformatik.dk/">
<img width="32%" alt="Terranodo" style="display:inline" src="media/sponsors/orbicon_informatik.png" />
</a>
<br/>
### Silver
- [Klokan Technologies](https://www.klokantech.com/)
- [Geofabrik](http://www.geofabrik.de/)
- [Dreipol](https://www.dreipol.ch/)
<a href="https://www.klokantech.com/">
<img alt="Klokan Technologies" style="display:inline" src="media/sponsors/klokantech.png" />
<img width="18%" alt="Klokan Technologies" style="display:inline-block" src="media/sponsors/klokantech.png" />
</a>
<a href="http://www.geofabrik.de/">
<img width="18%" alt="Geofabrik" style="display:inline-block" src="media/sponsors/geofabrik.png" />
</a>
<a href="https://www.dreipol.ch/">
<img alt="Dreipol" style="display:inline" src="media/sponsors/dreipol.png" />
<img width="18%" alt="Dreipol" style="display:inline-block" src="media/sponsors/dreipol.png" />
</a>
<br/>
### Individuals
**Influential Stakeholder**
- Alan McConchie
- Odi
- Mats Norén
- Uli [geOps](http://geops.ch/)
- Helge Fahrnberger
Kirusanth Poopalasingam
Alan McConchie, Odi, Mats Norén, Uli [geOps](http://geops.ch/), Helge Fahrnberger ([Toursprung](http://www.toursprung.com/)), Kirusanth Poopalasingam
**Stakeholder**
- Brian Flood
- Vasile Coțovanu
- Andreas Kalkbrenner
- Christian Mäder
- Gregor Wassmann
- Lee Armstrong
- Rafel
- Jon Burgess
- Lukas Lehmann
- Joachim Ungar
- Alois Ackermann
- Zsolt Ero
- Jordan Meek
Brian Flood, Vasile Coțovanu, Andreas Kalkbrenner, Christian Mäder, Gregor Wassmann, Lee Armstrong, Rafel, Jon Burgess, Lukas Lehmann, Joachim Ungar, Alois Ackermann, Zsolt Ero, Jordan Meek
**Supporter**
- Sina Martinelli
- Nicholas Doiron
- Neil Cawse
- Urs42
- Benedikt Groß
- Manuel Roth
- Janko Mihelić
- Moritz Stefaner
- Sebastian Ahoi
- Juerg Uhlmann
- Tom Wider
- Nadia Panchaud
- Oliver Snowden
- Stephan Heuel
- Tobin Bradley
- Adrian Herzog
- Antti Lehto
- Pascal Mages
- Marc Gehling
- Imre Samu
- Lauri K.
- Visahavel Parthasarathy
- Christophe Waterlot-Buisine
- Max Galka
- ubahnverleih
- Wouter van Dam
- Jakob Lobensteiner
- Samuel Kurath
- Brian Bancroft
Sina Martinelli, Nicholas Doiron, Neil Cawse, Urs42, Benedikt Groß, Manuel Roth, Janko Mihelić, Moritz Stefaner, Sebastian Ahoi, Juerg Uhlmann, Tom Wider, Nadia Panchaud, Oliver Snowden, Stephan Heuel, Tobin Bradley, Adrian Herzog, Antti Lehto, Pascal Mages, Marc Gehling, Imre Samu, Lauri K., Visahavel Parthasarathy, Christophe Waterlot-Buisine, Max Galka, ubahnverleih, Wouter van Dam, Jakob Lobensteiner, Samuel Kurath, Brian Bancroft
## License

Binary file not shown.

Before

Width:  |  Height:  |  Size: 410 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 37 KiB

View File

@@ -1,6 +1,6 @@
{
"name": "maputnik",
"version": "0.0.1",
"version": "1.0.1",
"description": "A MapboxGL visual style editor",
"main": "''",
"scripts": {
@@ -9,7 +9,8 @@
"test": "karma start --single-run",
"test-watch": "karma start",
"start": "webpack-dev-server --progress --profile --colors --watch-poll",
"lint": "eslint --ext js --ext jsx {src,test}"
"lint": "eslint --ext js --ext jsx {src,test}",
"lint-styles": "stylelint 'src/styles/*.scss'"
},
"repository": {
"type": "git",
@@ -19,20 +20,21 @@
"license": "MIT",
"homepage": "https://github.com/maputnik/editor#readme",
"dependencies": {
"classnames": "^2.2.5",
"codemirror": "^5.18.2",
"color": "^1.0.3",
"file-saver": "^1.3.2",
"github-api": "^3.0.0",
"lodash.capitalize": "^4.2.1",
"lodash.clonedeep": "^4.5.0",
"lodash.isequal": "^4.4.0",
"lodash.throttle": "^4.1.1",
"lodash.topairs": "^4.3.0",
"mapbox-gl": "^0.29.0",
"mapbox-gl": "^0.31.0",
"mapbox-gl-inspect": "^1.2.1",
"mapbox-gl-style-spec": "^8.11.0",
"mousetrap": "^1.6.0",
"ol-mapbox-style": "0.0.11",
"ol-mapbox-style": "1.0.1",
"openlayers": "^3.19.1",
"randomcolor": "^0.4.4",
"react": "^15.4.0",
"react-addons-pure-render-mixin": "^15.4.0",
"react-autocomplete": "^1.4.0",
@@ -46,7 +48,9 @@
"react-icons": "^2.2.1",
"react-motion": "^0.4.7",
"react-sortable-hoc": "^0.4.5",
"request": "^2.79.0"
"reconnecting-websocket": "^3.0.3",
"request": "^2.79.0",
"url": "^0.11.0"
},
"babel": {
"presets": [
@@ -60,6 +64,9 @@
"jshintConfig": {
"esversion": 6
},
"stylelint": {
"extends": "stylelint-config-standard"
},
"eslintConfig": {
"plugins": [
"react"
@@ -84,18 +91,18 @@
}
},
"devDependencies": {
"babel-core": "6.14.0",
"babel-eslint": "^6.1.2",
"babel-loader": "6.2.4",
"babel-core": "6.21.0",
"babel-eslint": "^7.1.1",
"babel-loader": "6.2.10",
"babel-plugin-transform-class-properties": "^6.11.5",
"babel-plugin-transform-decorators-legacy": "^1.3.4",
"babel-plugin-transform-flow-strip-types": "^6.21.0",
"babel-plugin-transform-object-rest-spread": "^6.8.0",
"babel-plugin-transform-runtime": "^6.15.0",
"babel-preset-es2015": "6.14.0",
"babel-preset-react": "6.11.1",
"babel-preset-es2015": "6.18.0",
"babel-preset-react": "6.16.0",
"babel-runtime": "^6.11.6",
"css-loader": "0.25.0",
"css-loader": "0.26.1",
"eslint": "^3.5.0",
"eslint-plugin-react": "^6.2.0",
"extract-text-webpack-plugin": "^1.0.1",
@@ -106,18 +113,19 @@
"karma-chrome-launcher": "^2.0.0",
"karma-firefox-launcher": "^1.0.0",
"karma-mocha": "^1.3.0",
"karma-webpack": "^1.8.0",
"karma-webpack": "^2.0.1",
"mocha": "^3.1.2",
"mocha-loader": "^1.0.0",
"node-sass": "^3.9.2",
"node-sass": "^4.2.0",
"react-hot-loader": "^3.0.0-beta.6",
"sass-loader": "^4.0.1",
"style-loader": "0.13.1",
"stylelint": "^7.7.1",
"stylelint-config-standard": "^15.0.1",
"transform-loader": "^0.2.3",
"url-loader": "0.5.7",
"webpack": "1.13.2",
"webpack-cleanup-plugin": "^0.3.0",
"webpack-dev-server": "1.15.1",
"webworkify-webpack": "^1.1.3"
"webpack": "1.14.0",
"webpack-cleanup-plugin": "^0.4.1",
"webpack-dev-server": "1.16.2"
}
}

View File

@@ -1,8 +1,6 @@
import React from 'react'
import { saveAs } from 'file-saver'
import Mousetrap from 'mousetrap'
import InspectionMap from './map/InspectionMap'
import MapboxGlMap from './map/MapboxGlMap'
import OpenLayers3Map from './map/OpenLayers3Map'
import LayerList from './layers/LayerList'
@@ -11,30 +9,54 @@ import Toolbar from './Toolbar'
import AppLayout from './AppLayout'
import MessagePanel from './MessagePanel'
import { downloadGlyphsMetadata, downloadSpriteMetadata } from '../libs/metadata'
import GlSpec from 'mapbox-gl-style-spec/reference/latest.js'
import validateStyleMin from 'mapbox-gl-style-spec/lib/validate_style.min'
import formatStyle from 'mapbox-gl-style-spec/lib/format'
import style from '../libs/style.js'
import { initialStyleUrl, loadStyleUrl } from '../libs/urlopen'
import { undoMessages, redoMessages } from '../libs/diffmessage'
import { loadDefaultStyle, StyleStore } from '../libs/stylestore'
import { ApiStyleStore } from '../libs/apistore'
import { RevisionStore } from '../libs/revisions'
import LayerWatcher from '../libs/layerwatcher'
import tokens from '../config/tokens.json'
function updateRootSpec(spec, fieldName, newValues) {
return {
...spec,
$root: {
...spec.$root,
[fieldName]: {
...spec.$root[fieldName],
values: newValues
}
}
}
}
export default class App extends React.Component {
constructor(props) {
super(props)
this.styleStore = new ApiStyleStore()
this.revisionStore = new RevisionStore()
this.styleStore.supported(isSupported => {
if(!isSupported) {
console.log('Falling back to local storage for storing styles')
this.styleStore = new StyleStore()
}
this.styleStore.latestStyle(mapStyle => this.onStyleChanged(mapStyle))
this.styleStore = new ApiStyleStore({
onLocalStyleChange: mapStyle => this.onStyleChanged(mapStyle, false)
})
const styleUrl = initialStyleUrl()
if(styleUrl) {
this.styleStore = new StyleStore()
loadStyleUrl(styleUrl, mapStyle => this.onStyleChanged(mapStyle))
} else {
this.styleStore.init(err => {
if(err) {
console.log('Falling back to local storage for storing styles')
this.styleStore = new StyleStore()
}
this.styleStore.latestStyle(mapStyle => this.onStyleChanged(mapStyle))
})
}
this.state = {
errors: [],
infos: [],
@@ -42,6 +64,8 @@ export default class App extends React.Component {
selectedLayerIndex: 0,
sources: {},
vectorLayers: {},
inspectModeEnabled: false,
spec: GlSpec,
}
this.layerWatcher = new LayerWatcher({
@@ -65,22 +89,36 @@ export default class App extends React.Component {
loadDefaultStyle(mapStyle => this.onStyleOpen(mapStyle))
}
onStyleDownload() {
const mapStyle = this.state.mapStyle
const blob = new Blob([JSON.stringify(mapStyle, null, 4)], {type: "application/json;charset=utf-8"});
saveAs(blob, mapStyle.id + ".json");
}
saveStyle(snapshotStyle) {
snapshotStyle.modified = new Date().toJSON()
this.styleStore.save(snapshotStyle)
}
onStyleChanged(newStyle) {
updateFonts(urlTemplate) {
const metadata = this.state.mapStyle.metadata || {}
const accessToken = metadata['maputnik:openmaptiles_access_token'] || tokens.openmaptiles
downloadGlyphsMetadata(urlTemplate.replace('{key}', accessToken), fonts => {
this.setState({ spec: updateRootSpec(this.state.spec, 'glyphs', fonts)})
})
}
updateIcons(baseUrl) {
downloadSpriteMetadata(baseUrl, icons => {
this.setState({ spec: updateRootSpec(this.state.spec, 'sprite', icons)})
})
}
onStyleChanged(newStyle, save=true) {
if(newStyle.glyphs !== this.state.mapStyle.glyphs) {
this.updateFonts(newStyle.glyphs)
}
if(newStyle.sprite !== this.state.mapStyle.sprite) {
this.updateIcons(newStyle.sprite)
}
const errors = validateStyleMin(newStyle, GlSpec)
if(errors.length === 0) {
this.revisionStore.addRevision(newStyle)
this.saveStyle(newStyle)
if(save) this.saveStyle(newStyle)
this.setState({
mapStyle: newStyle,
errors: [],
@@ -140,32 +178,30 @@ export default class App extends React.Component {
this.onLayersChange(changedLayers)
}
changeInspectMode() {
this.setState({
inspectModeEnabled: !this.state.inspectModeEnabled
})
}
mapRenderer() {
const metadata = this.state.mapStyle.metadata || {}
const mapProps = {
mapStyle: this.state.mapStyle,
accessToken: metadata['maputnik:access_token'],
mapStyle: style.replaceAccessToken(this.state.mapStyle),
onDataChange: (e) => {
this.layerWatcher.analyzeMap(e.map)
},
//TODO: This would actually belong to the layout component
style:{
top: 40,
//left: 500,
}
}
const metadata = this.state.mapStyle.metadata || {}
const renderer = metadata['maputnik:renderer'] || 'mbgljs'
// Check if OL3 code has been loaded?
if(renderer === 'ol3') {
return <OpenLayers3Map {...mapProps} />
} else if(renderer === 'inspection') {
return <InspectionMap {...mapProps}
sources={this.state.sources}
highlightedLayer={this.state.mapStyle.layers[this.state.selectedLayerIndex]} />
} else {
return <MapboxGlMap {...mapProps} />
return <MapboxGlMap {...mapProps}
inspectModeEnabled={this.state.inspectModeEnabled}
highlightedLayer={this.state.mapStyle.layers[this.state.selectedLayerIndex]} />
}
}
@@ -181,10 +217,11 @@ export default class App extends React.Component {
const toolbar = <Toolbar
mapStyle={this.state.mapStyle}
inspectModeEnabled={this.state.inspectModeEnabled}
sources={this.state.sources}
onStyleChanged={this.onStyleChanged.bind(this)}
onStyleOpen={this.onStyleChanged.bind(this)}
onStyleDownload={this.onStyleDownload.bind(this)}
onInspectModeToggle={this.changeInspectMode.bind(this)}
/>
const layerList = <LayerList
@@ -192,12 +229,14 @@ export default class App extends React.Component {
onLayerSelect={this.onLayerSelect.bind(this)}
selectedLayerIndex={this.state.selectedLayerIndex}
layers={layers}
sources={this.state.sources}
/>
const layerEditor = selectedLayer ? <LayerEditor
layer={selectedLayer}
sources={this.state.sources}
vectorLayers={this.state.vectorLayers}
spec={this.state.spec}
onLayerChanged={this.onLayerChanged.bind(this)}
onLayerIdChange={this.onLayerIdChange.bind(this)}
/> : null
@@ -216,4 +255,3 @@ export default class App extends React.Component {
/>
}
}

View File

@@ -1,10 +1,6 @@
import React from 'react'
import ScrollContainer from './ScrollContainer'
import theme from '../config/theme'
import colors from '../config/colors'
import { fontSizes } from '../config/scales'
class AppLayout extends React.Component {
static propTypes = {
toolbar: React.PropTypes.element.isRequired,
@@ -20,56 +16,25 @@ class AppLayout extends React.Component {
getChildContext() {
return {
reactIconBase: { size: fontSizes[3] }
reactIconBase: { size: 14 }
}
}
render() {
return <div style={{
fontFamily: theme.fontFamily,
color: theme.color,
fontWeight: 300
}}>
return <div className="maputnik-layout">
{this.props.toolbar}
<div style={{
position: 'fixed',
bottom: 0,
height: "100%",
top: 40,
left: 0,
zIndex: 1,
width: 200,
overflow: "hidden",
backgroundColor: colors.black
}}>
<div className="maputnik-layout-list">
<ScrollContainer>
{this.props.layerList}
</ScrollContainer>
</div>
<div style={{
position: 'fixed',
bottom: 0,
height: "100%",
top: 40,
left: 200,
zIndex: 1,
width: 350,
backgroundColor: colors.black
}}>
<div className="maputnik-layout-drawer">
<ScrollContainer>
{this.props.layerEditor}
</ScrollContainer>
</div>
{this.props.map}
{this.props.bottom && <div style={{
position: 'fixed',
height: 50,
bottom: 0,
left: 550,
zIndex: 1,
width: '100%',
backgroundColor: colors.black
}}>
{this.props.bottom && <div className="maputnik-layout-bottom">
{this.props.bottom}
</div>
}

View File

@@ -1,26 +1,18 @@
import React from 'react'
import colors from '../config/colors'
import { margins, fontSizes } from '../config/scales'
import classnames from 'classnames'
class Button extends React.Component {
static propTypes = {
onClick: React.PropTypes.func,
style: React.PropTypes.object,
className: React.PropTypes.string,
}
render() {
return <a
onClick={this.props.onClick}
style={{
cursor: 'pointer',
backgroundColor: colors.midgray,
color: colors.lowgray,
fontSize: fontSizes[5],
padding: margins[1],
userSelect: 'none',
borderRadius: 2,
...this.props.style,
}}>
className={classnames("maputnik-button", this.props.className)}
style={this.props.style}>
{this.props.children}
</a>
}

View File

@@ -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

View File

@@ -1,7 +1,4 @@
import React from 'react'
import Paragraph from './Paragraph'
import colors from '../config/colors'
import { fontSizes, margins } from '../config/scales'
class MessagePanel extends React.Component {
static propTypes = {
@@ -10,30 +7,15 @@ class MessagePanel extends React.Component {
}
render() {
const paragraphStyle = {
margin: 0,
lineHeight: 1.2,
}
const errors = this.props.errors.map((m, i) => {
return <Paragraph key={i}
style={{
...paragraphStyle,
color: colors.red,
}}>{m}</Paragraph>
return <p className="maputnik-message-panel-error">{m}</p>
})
const infos = this.props.infos.map((m, i) => {
return <Paragraph key={i}
style={{
...paragraphStyle,
color: colors.lowgray,
}}>{m}</Paragraph>
return <p key={i}>{m}</p>
})
return <div style={{
padding: margins[1],
}}>
return <div className="maputnik-message-panel">
{errors}
{infos}
</div>

View File

@@ -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

View File

@@ -1,15 +1,7 @@
import React from 'react'
const ScrollContainer = (props) => {
return <div style={{
overflowX: "visible",
overflowY: "scroll",
bottom:0,
left:0,
right:0,
top:1,
position: "absolute",
}}>
return <div className="maputnik-scroll-container">
{props.children}
</div>
}

View File

@@ -1,5 +1,6 @@
import React from 'react'
import FileReaderInput from 'react-file-reader-input'
import classnames from 'classnames'
import MdFileDownload from 'react-icons/lib/md/file-download'
import MdFileUpload from 'react-icons/lib/md/file-upload'
@@ -14,76 +15,48 @@ import MdInsertEmoticon from 'react-icons/lib/md/insert-emoticon'
import MdFontDownload from 'react-icons/lib/md/font-download'
import HelpIcon from 'react-icons/lib/md/help-outline'
import InspectionIcon from 'react-icons/lib/md/find-in-page'
import AddIcon from 'react-icons/lib/md/add-circle-outline'
import logoImage from '../img/maputnik.png'
import AddModal from './modals/AddModal'
import SettingsModal from './modals/SettingsModal'
import ExportModal from './modals/ExportModal'
import SourcesModal from './modals/SourcesModal'
import OpenModal from './modals/OpenModal'
import style from '../libs/style'
import colors from '../config/colors'
import { margins, fontSizes } from '../config/scales'
const IconText = props => <span style={{ paddingLeft: margins[0] }}>
{props.children}
</span>
const actionStyle = {
display: "inline-block",
padding: 10,
fontSize: fontSizes[4],
cursor: "pointer",
color: colors.white,
textDecoration: 'none',
function IconText(props) {
return <span className="maputnik-icon-text">{props.children}</span>
}
const ToolbarLink = props => <a
href={props.href}
target={"blank"}
style={{
...actionStyle,
...props.style,
}}>
{props.children}
</a>
function ToolbarLink(props) {
return <a
className={classnames('maputnik-toolbar-link', props.className)}
href={props.href}
target={"blank"}
>
{props.children}
</a>
}
class ToolbarAction extends React.Component {
static propTypes = {
onClick: React.PropTypes.func.isRequired,
}
constructor(props) {
super(props)
this.state = { hover: false }
}
render() {
return <a
onClick={this.props.onClick}
onMouseOver={e => this.setState({hover: true})}
onMouseOut={e => this.setState({hover: false})}
style={{
...actionStyle,
...this.props.style,
backgroundColor: this.state.hover ? colors.gray : null,
}}>
{this.props.children}
</a>
}
function ToolbarAction(props) {
return <a
className='maputnik-toolbar-action'
onClick={props.onClick}
>
{props.children}
</a>
}
export default class Toolbar extends React.Component {
static propTypes = {
mapStyle: React.PropTypes.object.isRequired,
inspectModeEnabled: React.PropTypes.bool.isRequired,
onStyleChanged: React.PropTypes.func.isRequired,
// A new style has been uploaded
onStyleOpen: React.PropTypes.func.isRequired,
// Current style is requested for download
onStyleDownload: React.PropTypes.func.isRequired,
// A dict of source id's and the available source layers
sources: React.PropTypes.object.isRequired,
onInspectModeToggle: React.PropTypes.func.isRequired
}
constructor(props) {
@@ -94,33 +67,11 @@ export default class Toolbar extends React.Component {
sources: false,
open: false,
add: false,
export: false,
}
}
}
downloadButton() {
return <ToolbarAction onClick={this.props.onStyleDownload}>
<MdFileDownload />
<IconText>Download</IconText>
</ToolbarAction>
}
toggleInspectionMode() {
const metadata = this.props.mapStyle.metadata || {}
const currentRenderer = metadata['maputnik:renderer'] || 'mbgljs'
const changedRenderer = currentRenderer === 'inspection' ? 'mbgljs' : 'inspection'
const changedStyle = {
...this.props.mapStyle,
metadata: {
'maputnik:renderer': changedRenderer
}
}
this.props.onStyleChanged(changedStyle)
}
toggleModal(modalName) {
this.setState({
isOpen: {
@@ -131,21 +82,19 @@ export default class Toolbar extends React.Component {
}
render() {
return <div style={{
position: "fixed",
height: 40,
width: '100%',
zIndex: 100,
left: 0,
top: 0,
backgroundColor: colors.black
}}>
return <div className='maputnik-toolbar'>
<SettingsModal
mapStyle={this.props.mapStyle}
onStyleChanged={this.props.onStyleChanged}
isOpen={this.state.isOpen.settings}
onOpenToggle={this.toggleModal.bind(this, 'settings')}
/>
<ExportModal
mapStyle={this.props.mapStyle}
onStyleChanged={this.props.onStyleChanged}
isOpen={this.state.isOpen.export}
onOpenToggle={this.toggleModal.bind(this, 'export')}
/>
<OpenModal
isOpen={this.state.isOpen.open}
onStyleOpen={this.props.onStyleOpen}
@@ -157,33 +106,20 @@ export default class Toolbar extends React.Component {
isOpen={this.state.isOpen.sources}
onOpenToggle={this.toggleModal.bind(this, 'sources')}
/>
<AddModal
mapStyle={this.props.mapStyle}
sources={this.props.sources}
isOpen={this.state.isOpen.add}
onOpenToggle={this.toggleModal.bind(this, 'add')}
onStyleChange={this.props.onStyleChanged}
/>
<ToolbarLink
href={"https://github.com/maputnik/editor"}
style={{
width: 180,
textAlign: 'left',
backgroundColor: colors.black,
padding: 5,
}}
className="maputnik-toolbar-logo"
>
<img src={logoImage} alt="Maputnik" style={{width: 30, height: 30, paddingRight: 5, verticalAlign: 'middle'}}/>
<span style={{fontSize: 20, verticalAlign: 'middle' }}>Maputnik</span>
<img src={logoImage} alt="Maputnik" />
<h1>Maputnik</h1>
</ToolbarLink>
<ToolbarAction onClick={this.toggleModal.bind(this, 'open')}>
<OpenIcon />
<IconText>Open</IconText>
</ToolbarAction>
{this.downloadButton()}
<ToolbarAction onClick={this.toggleModal.bind(this, 'add')}>
<AddIcon />
<IconText>Add Layer</IconText>
<ToolbarAction onClick={this.toggleModal.bind(this, 'export')}>
<MdFileDownload />
<IconText>Export</IconText>
</ToolbarAction>
<ToolbarAction onClick={this.toggleModal.bind(this, 'sources')}>
<SourcesIcon />
@@ -193,9 +129,12 @@ export default class Toolbar extends React.Component {
<SettingsIcon />
<IconText>Style Settings</IconText>
</ToolbarAction>
<ToolbarAction onClick={this.toggleInspectionMode.bind(this)}>
<ToolbarAction onClick={this.props.onInspectModeToggle}>
<InspectionIcon />
<IconText>Inspect</IconText>
<IconText>
{ this.props.inspectModeEnabled && <span>Map Mode</span> }
{ !this.props.inspectModeEnabled && <span>Inspect Mode</span> }
</IconText>
</ToolbarAction>
<ToolbarLink href={"https://github.com/maputnik/editor/wiki"}>
<HelpIcon />

View File

@@ -2,8 +2,6 @@ import React from 'react'
import Color from 'color'
import ChromePicker from 'react-color/lib/components/chrome/Chrome'
import input from '../../config/input.js'
function formatColor(color) {
const rgb = color.rgb
return `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${rgb.a})`
@@ -58,9 +56,10 @@ class ColorField extends React.Component {
render() {
const offset = this.calcPickerOffset()
const picker = <div
className="maputnik-color-picker-offset"
style={{
position: 'fixed',
zIndex: 1,
position: 'fixed',
zIndex: 1,
left: offset.left,
top: offset.top,
}}>
@@ -69,6 +68,7 @@ class ColorField extends React.Component {
onChange={c => this.props.onChange(formatColor(c))}
/>
<div
className="maputnik-color-picker-offset"
onClick={this.togglePicker.bind(this)}
style={{
zIndex: -1,
@@ -81,18 +81,13 @@ class ColorField extends React.Component {
/>
</div>
return <div style={{
position: 'relative',
display: 'inline',
}}>
return <div className="maputnik-color-wrapper">
{this.state.pickerOpened && picker}
<input
className="maputnik-color"
ref="colorInput"
onClick={this.togglePicker.bind(this)}
style={{
...input.input,
...this.props.style
}}
style={this.props.style}
name={this.props.name}
placeholder={this.props.default}
value={this.props.value ? this.props.value : ""}

View File

@@ -1,50 +1,22 @@
import React from 'react'
import input from '../../config/input.js'
import colors from '../../config/colors.js'
import { margins, fontSizes } from '../../config/scales.js'
export default class DocLabel extends React.Component {
static propTypes = {
label: React.PropTypes.string.isRequired,
label: React.PropTypes.oneOfType([
React.PropTypes.object,
React.PropTypes.string
]).isRequired,
doc: React.PropTypes.string.isRequired,
style: React.PropTypes.object,
}
constructor(props) {
super(props)
this.state = { showDoc: false }
}
render() {
return <label
style={{
...input.label,
...this.props.style,
position: 'relative',
}}
>
<span
onMouseOver={e => this.setState({showDoc: true})}
onMouseOut={e => this.setState({showDoc: false})}
style={{
cursor: 'help',
}}
>
{this.props.label}
</span>
<div style={{
backgroundColor: colors.gray,
padding: margins[1],
fontSize: 10,
position: 'absolute',
top: 20,
left: 0,
width: 120,
display: this.state.showDoc ? null : 'none',
zIndex: 3,
}}>
{this.props.doc}
</div>
return <label className="maputnik-doc-wrapper">
<div className="maputnik-doc-target">
<span>{this.props.label}</span>
<div className="maputnik-doc-popup">
{this.props.doc}
</div>
</div >
</label>
}
}

View File

@@ -1,20 +1,31 @@
import React from 'react'
import GlSpec from 'mapbox-gl-style-spec/reference/latest.js'
import ZoomSpecField from './ZoomSpecField'
import colors from '../../config/colors'
import { margins } from '../../config/scales'
const iconProperties = ['background-pattern', 'fill-pattern', 'line-pattern', 'fill-extrusion-pattern', 'icon-image']
/** Extract field spec by {@fieldName} from the {@layerType} in the
* style specification from either the paint or layout group */
function getFieldSpec(layerType, fieldName) {
const groupName = getGroupName(layerType, fieldName)
const group = GlSpec[groupName + '_' + layerType]
return group[fieldName]
function getFieldSpec(spec, layerType, fieldName) {
const groupName = getGroupName(spec, layerType, fieldName)
const group = spec[groupName + '_' + layerType]
const fieldSpec = group[fieldName]
if(iconProperties.indexOf(fieldName) >= 0) {
return {
...fieldSpec,
values: spec.$root.sprite.values
}
}
if(fieldName === 'text-font') {
return {
...fieldSpec,
values: spec.$root.glyphs.values
}
}
return fieldSpec
}
function getGroupName(layerType, fieldName) {
const paint = GlSpec['paint_' + layerType] || {}
function getGroupName(spec, layerType, fieldName) {
const paint = spec['paint_' + layerType] || {}
if (fieldName in paint) {
return 'paint'
} else {
@@ -27,16 +38,17 @@ export default class PropertyGroup extends React.Component {
layer: React.PropTypes.object.isRequired,
groupFields: React.PropTypes.array.isRequired,
onChange: React.PropTypes.func.isRequired,
spec: React.PropTypes.object.isRequired,
}
onPropertyChange(property, newValue) {
const group = getGroupName(this.props.layer.type, property)
const group = getGroupName(this.props.spec, this.props.layer.type, property)
this.props.onChange(group , property, newValue)
}
render() {
const fields = this.props.groupFields.map(fieldName => {
const fieldSpec = getFieldSpec(this.props.layer.type, fieldName)
const fieldSpec = getFieldSpec(this.props.spec, this.props.layer.type, fieldName)
const paint = this.props.layer.paint || {}
const layout = this.props.layer.layout || {}
@@ -46,12 +58,12 @@ export default class PropertyGroup extends React.Component {
onChange={this.onPropertyChange.bind(this)}
key={fieldName}
fieldName={fieldName}
value={fieldValue}
value={fieldValue === undefined ? fieldSpec.default : fieldValue}
fieldSpec={fieldSpec}
/>
})
return <div>
return <div className="maputnik-property-group">
{fields}
</div>
}

View File

@@ -1,7 +1,6 @@
import React from 'react'
import color from 'color'
import GlSpec from 'mapbox-gl-style-spec/reference/latest.min.js'
import ColorField from './ColorField'
import NumberInput from '../inputs/NumberInput'
import CheckboxInput from '../inputs/CheckboxInput'
@@ -10,10 +9,10 @@ import SelectInput from '../inputs/SelectInput'
import MultiButtonInput from '../inputs/MultiButtonInput'
import ArrayInput from '../inputs/ArrayInput'
import FontInput from '../inputs/FontInput'
import IconInput from '../inputs/IconInput'
import capitalize from 'lodash.capitalize'
import input from '../../config/input.js'
const iconProperties = ['background-pattern', 'fill-pattern', 'line-pattern', 'fill-extrusion-pattern', 'icon-image']
function labelFromFieldName(fieldName) {
let label = fieldName.split('-').slice(1).join(' ')
@@ -51,8 +50,8 @@ export default class SpecField extends React.Component {
render() {
const commonProps = {
style: this.props.style,
default: this.props.fieldSpec.default,
value: this.props.value,
default: this.props.fieldSpec.default,
name: this.props.fieldName,
onChange: newValue => this.props.onChange(this.props.fieldName, newValue)
}
@@ -78,11 +77,17 @@ export default class SpecField extends React.Component {
options={options}
/>
}
case 'string': return (
<StringInput
{...commonProps}
/>
)
case 'string':
if(iconProperties.indexOf(this.props.fieldName) >= 0) {
return <IconInput
{...commonProps}
icons={this.props.fieldSpec.values}
/>
} else {
return <StringInput
{...commonProps}
/>
}
case 'color': return (
<ColorField
{...commonProps}
@@ -97,6 +102,7 @@ export default class SpecField extends React.Component {
if(this.props.fieldName === 'text-font') {
return <FontInput
{...commonProps}
fonts={this.props.fieldSpec.values}
/>
} else {
return <ArrayInput

View File

@@ -5,14 +5,13 @@ import Button from '../Button'
import SpecField from './SpecField'
import NumberInput from '../inputs/NumberInput'
import DocLabel from './DocLabel'
import InputBlock from '../inputs/InputBlock'
import AddIcon from 'react-icons/lib/md/add-circle-outline'
import DeleteIcon from 'react-icons/lib/md/delete'
import FunctionIcon from 'react-icons/lib/md/functions'
import capitalize from 'lodash.capitalize'
import input from '../../config/input.js'
import colors from '../../config/colors.js'
import { margins, fontSizes } from '../../config/scales.js'
function isZoomField(value) {
return typeof value === 'object' && value.stops
@@ -21,7 +20,7 @@ function isZoomField(value) {
/** Supports displaying spec field for zoom function objects
* https://www.mapbox.com/mapbox-gl-style-spec/#types-function-zoom-property
*/
export default class ZoomSpecField extends React.Component {
export default class ZoomSpecProperty extends React.Component {
static propTypes = {
onChange: React.PropTypes.func.isRequired,
fieldName: React.PropTypes.string.isRequired,
@@ -64,6 +63,16 @@ export default class ZoomSpecField extends React.Component {
this.props.onChange(this.props.fieldName, changedValue)
}
makeZoomFunction() {
const zoomFunc = {
stops: [
[6, this.props.value],
[10, this.props.value]
]
}
this.props.onChange(this.props.fieldName, zoomFunc)
}
changeStop(changeIdx, zoomLevel, value) {
const stops = this.props.value.stops.slice(0)
stops[changeIdx] = [zoomLevel, value]
@@ -74,81 +83,95 @@ export default class ZoomSpecField extends React.Component {
this.props.onChange(this.props.fieldName, changedValue)
}
render() {
let label = <DocLabel
label={labelFromFieldName(this.props.fieldName)}
doc={this.props.fieldSpec.doc}
style={{
width: '50%',
}}
/>
renderZoomProperty() {
const zoomFields = this.props.value.stops.map((stop, idx) => {
const zoomLevel = stop[0]
const value = stop[1]
const deleteStopBtn= <DeleteStopButton onClick={this.deleteStop.bind(this, idx)} />
if(isZoomField(this.props.value)) {
const zoomFields = this.props.value.stops.map((stop, idx) => {
label = <DocLabel
doc={this.props.fieldSpec.doc}
style={{ width: '42.5%'}}
label={idx > 0 ? "" : labelFromFieldName(this.props.fieldName)}
/>
if(idx === 1) {
label = <label style={{...input.label, width: '42.5%'}}>
<Button
style={{fontSize: fontSizes[5]}}
onClick={this.addStop.bind(this)}
>
Add stop
</Button>
</label>
}
const zoomLevel = stop[0]
const value = stop[1]
return <div key={zoomLevel} style={{
...input.property,
marginLeft: 0,
marginRight: 0
}}>
{label}
<Button
style={{backgroundColor: null}}
onClick={this.deleteStop.bind(this, idx)}
>
<DeleteIcon />
</Button>
<NumberInput
style={{
width: '7%',
}}
value={zoomLevel}
onChange={changedStop => this.changeStop(idx, changedStop, value)}
min={0}
max={22}
/>
<SpecField
fieldName={this.props.fieldName}
fieldSpec={this.props.fieldSpec}
value={value}
onChange={(_, newValue) => this.changeStop(idx, zoomLevel, newValue)}
style={{
width: '42%',
marginLeft: margins[0],
}}
/>
return <InputBlock
key={zoomLevel}
doc={this.props.fieldSpec.doc}
label={labelFromFieldName(this.props.fieldName)}
action={deleteStopBtn}
>
<div>
<div className="maputnik-zoom-spec-property-stop-edit">
<NumberInput
value={zoomLevel}
onChange={changedStop => this.changeStop(idx, changedStop, value)}
min={0}
max={22}
/>
</div>
<div className="maputnik-zoom-spec-property-stop-value">
<SpecField
fieldName={this.props.fieldName}
fieldSpec={this.props.fieldSpec}
value={value}
onChange={(_, newValue) => this.changeStop(idx, zoomLevel, newValue)}
/>
</div>
</div>
})
</InputBlock>
})
return <div style={input.property}>
{zoomFields}
</div>
} else {
return <div style={input.property}>
{label}
<SpecField {...this.props} style={{ width: '50%' } }/>
</div>
}
return <div className="maputnik-zoom-spec-property">
{zoomFields}
<Button
className="maputnik-add-stop"
onClick={this.addStop.bind(this)}
>
Add stop
</Button>
</div>
}
renderProperty() {
let zoomBtn = null
if(this.props.fieldSpec['zoom-function']) {
zoomBtn = <MakeZoomFunctionButton onClick={this.makeZoomFunction.bind(this)} />
}
return <InputBlock
doc={this.props.fieldSpec.doc}
label={labelFromFieldName(this.props.fieldName)}
action={zoomBtn}
>
<SpecField {...this.props} />
</InputBlock>
}
render() {
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>
}
}
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) {

View File

@@ -1,19 +1,15 @@
import React from 'react'
import GlSpec from 'mapbox-gl-style-spec/reference/latest.min.js'
import input from '../../config/input.js'
import colors from '../../config/colors.js'
import { margins } from '../../config/scales.js'
import { combiningFilterOps } from '../../libs/filterops.js'
import GlSpec from 'mapbox-gl-style-spec/reference/latest.js'
import DocLabel from '../fields/DocLabel'
import SelectInput from '../inputs/SelectInput'
import StringInput from '../inputs/StringInput'
import AutocompleteInput from '../inputs/AutocompleteInput'
import SingleFilterEditor from './SingleFilterEditor'
import FilterEditorBlock from './FilterEditorBlock'
import Button from '../Button'
const combiningFilterOps = ['all', 'any', 'none']
const setFilterOps = ['in', '!in']
const otherFilterOps = Object
.keys(GlSpec.filter_operator.values)
.filter(op => combiningFilterOps.indexOf(op) < 0)
import DeleteIcon from 'react-icons/lib/md/delete'
import AddIcon from 'react-icons/lib/fa/plus'
function hasCombiningFilter(filter) {
return combiningFilterOps.indexOf(filter[0]) >= 0
@@ -27,131 +23,24 @@ function hasNestedCombiningFilter(filter) {
return false
}
class CombiningOperatorSelect extends React.Component {
static propTypes = {
value: React.PropTypes.string.isRequired,
onChange: React.PropTypes.func.isRequired,
}
render() {
const options = combiningFilterOps.map(op => {
return <option key={op} value={op}>{op}</option>
})
return <div
style={{
marginTop: margins[1],
marginBottom: margins[1],
}}
>
<select
style={{
...input.select,
width: '20.5%',
}}
value={this.props.value}
onChange={e => this.props.onChange(e.target.value)}
>
{options}
</select>
<label style={{
...input.label,
width: '60%',
marginLeft: margins[0],
}}>
of the filters matches
</label>
</div>
}
}
class OperatorSelect extends React.Component {
static propTypes = {
value: React.PropTypes.string.isRequired,
onChange: React.PropTypes.func.isRequired,
}
render() {
return <SelectInput
style={{
width: '15%',
margin: margins[0]
}}
value={this.props.value}
onChange={this.props.onChange}
options={otherFilterOps.map(op => [op, op])}
/>
}
}
class SingleFilterEditor extends React.Component {
static propTypes = {
filter: React.PropTypes.array.isRequired,
onChange: React.PropTypes.func.isRequired,
properties: React.PropTypes.object,
}
static defaultProps = {
properties: {},
}
onFilterPartChanged(filterOp, propertyName, filterArgs) {
const newFilter = [filterOp, propertyName, ...filterArgs]
this.props.onChange(newFilter)
}
render() {
const f = this.props.filter
const filterOp = f[0]
const propertyName = f[1]
const filterArgs = f.slice(2)
return <div style={{
marginTop: margins[1],
marginBottom: margins[1],
}}>
<AutocompleteInput
wrapperStyle={{
width: '31%',
marginRight: margins[0]
}}
value={propertyName}
options={Object.keys(this.props.properties).map(propName => [propName, propName])}
onChange={newPropertyName => this.onFilterPartChanged(filterOp, newPropertyName, filterArgs)}
/>
<OperatorSelect
value={filterOp}
onChange={newFilterOp => this.onFilterPartChanged(newFilterOp, propertyName, filterArgs)}
/>
<StringInput
style={{
width: '50%',
marginLeft: margins[0]
}}
value={filterArgs.join(',')}
onChange={ v=> this.onFilterPartChanged(filterOp, propertyName, v.split(','))}
/>
</div>
}
}
export default class CombiningFilterEditor extends React.Component {
static propTypes = {
/** Properties of the vector layer and the available fields */
properties: React.PropTypes.object.isRequired,
filter: React.PropTypes.array.isRequired,
properties: React.PropTypes.object,
filter: React.PropTypes.array,
onChange: React.PropTypes.func.isRequired,
}
// Convert filter to combining filter
combiningFilter() {
let combiningOp = this.props.filter[0]
let filters = this.props.filter.slice(1)
let filter = this.props.filter || ['all']
let combiningOp = filter[0]
let filters = filter.slice(1)
if(combiningFilterOps.indexOf(combiningOp) < 0) {
combiningOp = 'all'
filters = [this.props.filter.slice(0)]
filters = [filter.slice(0)]
}
return [combiningOp, ...filters]
@@ -163,31 +52,61 @@ export default class CombiningFilterEditor extends React.Component {
this.props.onChange(newFilter)
}
deleteFilterItem(filterIdx) {
const newFilter = this.combiningFilter().slice(0)
console.log('Delete', filterIdx, newFilter)
newFilter.splice(filterIdx + 1, 1)
this.props.onChange(newFilter)
}
addFilterItem() {
const newFilterItem = this.combiningFilter().slice(0)
newFilterItem.push(['==', 'name', ''])
this.props.onChange(newFilterItem)
}
render() {
const filter = this.combiningFilter()
let combiningOp = filter[0]
let filters = filter.slice(1)
const filterEditors = filters.map((f, idx) => {
return <SingleFilterEditor
key={idx}
properties={this.props.properties}
filter={f}
onChange={this.onFilterPartChanged.bind(this, idx + 1)}
/>
const editorBlocks = filters.map((f, idx) => {
return <FilterEditorBlock key={idx} onDelete={this.deleteFilterItem.bind(this, idx)}>
<SingleFilterEditor
properties={this.props.properties}
filter={f}
onChange={this.onFilterPartChanged.bind(this, idx + 1)}
/>
</FilterEditorBlock>
})
//TODO: Implement support for nested filter
if(hasNestedCombiningFilter(filter)) {
return null
return <div className="maputnik-filter-editor-unsupported">
Nested filters are not supported.
</div>
}
return <div>
<CombiningOperatorSelect
value={combiningOp}
onChange={this.onFilterPartChanged.bind(this, 0)}
/>
{filterEditors}
return <div className="maputnik-filter-editor">
<div className="maputnik-filter-editor-compound-select">
<DocLabel
label={"Compound Filter"}
doc={GlSpec.layer.filter.doc + " Combine multiple filters together by using a compound filter."}
/>
<SelectInput
value={combiningOp}
onChange={this.onFilterPartChanged.bind(this, 0)}
options={[["all", "every filter matches"], ["none", "no filter matches"], ["any", "any filter matches"]]}
/>
</div>
{editorBlocks}
<div className="maputnik-filter-editor-add-wrapper">
<Button
className="maputnik-add-filter"
onClick={this.addFilterItem.bind(this)}>
Add filter
</Button>
</div>
</div>
}
}

View 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

View 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

View File

@@ -16,6 +16,7 @@ class LayerIcon extends React.Component {
const iconProps = { style: this.props.style }
switch(this.props.type) {
case 'fill-extrusion': return <BackgroundIcon {...iconProps} />
case 'raster': return <FillIcon {...iconProps} />
case 'fill': return <FillIcon {...iconProps} />
case 'background': return <BackgroundIcon {...iconProps} />
case 'line': return <LineIcon {...iconProps} />

View File

@@ -1,17 +1,13 @@
import React from 'react'
import input from '../../config/input.js'
import StringInput from './StringInput'
import NumberInput from './NumberInput'
import { margins } from '../../config/scales.js'
class ArrayInput extends React.Component {
static propTypes = {
value: React.PropTypes.array,
type: React.PropTypes.string,
length: React.PropTypes.number,
default: React.PropTypes.array,
style: React.PropTypes.object,
onChange: React.PropTypes.func,
}
@@ -27,29 +23,23 @@ class ArrayInput extends React.Component {
}
render() {
const commonStyle = {
width: '49%',
marginRight: '1%',
}
const inputs = this.values.map((v, i) => {
if(this.props.type === 'number') {
return <NumberInput
key={i}
value={v}
style={commonStyle}
onChange={this.changeValue.bind(this, i)}
/>
} else {
return <StringInput
key={i}
value={v}
style={commonStyle}
onChange={this.changeValue.bind(this, i)}
/>
}
})
return <div style={{display: 'inline-block', width: '50%'}}>
return <div className="maputnik-array">
{inputs}
</div>
}

View File

@@ -1,15 +1,12 @@
import React from 'react'
import classnames from 'classnames'
import Autocomplete from 'react-autocomplete'
import input from '../../config/input'
import colors from '../../config/colors'
import { margins, fontSizes } from '../../config/scales'
class AutocompleteInput extends React.Component {
static propTypes = {
value: React.PropTypes.string,
options: React.PropTypes.array,
wrapperStyle: React.PropTypes.object,
inputStyle: React.PropTypes.object,
onChange: React.PropTypes.func,
}
@@ -19,26 +16,16 @@ class AutocompleteInput extends React.Component {
}
render() {
const AutocompleteMenu = (items, value, style) => <div className={"maputnik-autocomplete-menu"} children={items} />
return <Autocomplete
menuStyle={{
border: 'none',
padding: '2px 0',
position: 'fixed',
overflow: 'auto',
maxHeight: '50%',
background: colors.gray,
zIndex: 3,
}}
wrapperStyle={{
display: 'inline-block',
...this.props.wrapperStyle
wrapperProps={{
className: "maputnik-autocomplete",
style: null
}}
renderMenu={AutocompleteMenu}
inputProps={{
style: {
...input.input,
width: '100%',
...this.props.inputStyle,
}
className: "maputnik-string"
}}
value={this.props.value}
items={this.props.options}
@@ -48,15 +35,10 @@ class AutocompleteInput extends React.Component {
renderItem={(item, isHighlighted) => (
<div
key={item[0]}
style={{
userSelect: 'none',
color: colors.lowgray,
cursor: 'default',
background: isHighlighted ? colors.midgray : colors.gray,
padding: margins[0],
fontSize: fontSizes[5],
zIndex: 3,
}}
className={classnames({
"maputnik-autocomplete-menu-item": true,
"maputnik-autocomplete-menu-item-selected": isHighlighted,
})}
>
{item[1]}
</div>

View File

@@ -1,70 +1,29 @@
import React from 'react'
import input from '../../config/input.js'
import colors from '../../config/colors'
import { margins } from '../../config/scales'
class CheckboxInput extends React.Component {
static propTypes = {
value: React.PropTypes.string,
value: React.PropTypes.bool.isRequired,
style: React.PropTypes.object,
onChange: React.PropTypes.func,
}
render() {
const styles = {
root: {
...input.base,
padding: 0,
position: 'relative',
textAlign: 'center ',
cursor: 'pointer'
},
input: {
position: 'absolute',
zIndex: -1,
opacity: 0
},
box: {
display: 'inline-block',
textAlign: 'center ',
height: 15,
width: 15,
marginRight: margins[1],
marginBottom: null,
backgroundColor: colors.gray,
borderRadius: 2,
borderStyle: 'solid',
borderWidth: 2,
borderColor: colors.gray,
transition: 'background-color .1s ease-out'
},
icon: {
display: this.props.value ? null : 'none',
width: '75%',
height: '75%',
marginTop: 1,
fill: colors.lowgray
}
}
return <label style={styles.root}>
return <label className="maputnik-checkbox-wrapper">
<input
type="checkbox"
style={{
...styles.input,
...this.props.style,
}}
onChange={e => {this.props.onChange(!this.props.value)}}
checked={this.props.value}
/>
<div style={styles.box}>
<svg
viewBox='0 0 32 32'
style={styles.icon}>
className="maputnik-checkbox"
type="checkbox"
style={this.props.style}
onChange={e => this.props.onChange(!this.props.value)}
checked={this.props.value}
/>
<div className="maputnik-checkbox-box">
<svg 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>
</div>
</label>
}
}

View File

@@ -1,17 +1,18 @@
import React from 'react'
import AutocompleteInput from './AutocompleteInput'
import input from '../../config/input.js'
//TODO: Query available font stack dynamically
import fontStacks from '../../config/fontstacks.json'
class FontInput extends React.Component {
static propTypes = {
value: React.PropTypes.array.isRequired,
fonts: React.PropTypes.array,
style: React.PropTypes.object,
onChange: React.PropTypes.func.isRequired,
}
static defaultProps = {
fonts: []
}
get values() {
return this.props.value || this.props.default.slice(1) || []
}
@@ -27,12 +28,12 @@ class FontInput extends React.Component {
return <AutocompleteInput
key={i}
value={value}
options={fontStacks.map(f => [f, f])}
options={this.props.fonts.map(f => [f, f])}
onChange={this.changeFont.bind(this, i)}
/>
})
return <div style={{display: 'inline-block'}}>
return <div className="maputnik-font">
{inputs}
</div>
}

View 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

View File

@@ -1,11 +1,16 @@
import React from 'react'
import input from '../../config/input'
import { margins } from '../../config/scales'
import classnames from 'classnames'
import DocLabel from '../fields/DocLabel'
/** Wrap a component with a label */
class InputBlock extends React.Component {
static propTypes = {
label: React.PropTypes.string.isRequired,
label: React.PropTypes.oneOfType([
React.PropTypes.string,
React.PropTypes.element,
]).isRequired,
doc: React.PropTypes.string,
action: React.PropTypes.element,
children: React.PropTypes.element.isRequired,
style: React.PropTypes.object,
}
@@ -15,30 +20,34 @@ class InputBlock extends React.Component {
return this.props.onChange(value === "" ? null: value)
}
renderChildren() {
return React.Children.map(this.props.children, child => {
return React.cloneElement(child, {
style: {
...child.props.style,
width: '50%',
}
})
})
}
render() {
return <div style={{
...input.property,
...this.props.style,
}}>
<label
style={{
...input.label,
width: '50%',
}}>
return <div style={this.props.style}
className={classnames({
"maputnik-input-block": true,
"maputnik-action-block": this.props.action
})}
>
{this.props.doc &&
<div className="maputnik-input-block-label">
<DocLabel
label={this.props.label}
doc={this.props.doc}
/>
</div>
}
{!this.props.doc &&
<label className="maputnik-input-block-label">
{this.props.label}
</label>
{this.renderChildren()}
}
{this.props.action &&
<div className="maputnik-input-block-action">
{this.props.action}
</div>
}
<div className="maputnik-input-block-content">
{this.props.children}
</div>
</div>
}
}

View File

@@ -1,35 +1,32 @@
import React from 'react'
import classnames from 'classnames'
import Button from '../Button'
import colors from '../../config/colors'
import { margins } from '../../config/scales'
import input from '../../config/input.js'
class MultiButtonInput extends React.Component {
static propTypes = {
value: React.PropTypes.string.isRequired,
options: React.PropTypes.array.isRequired,
style: React.PropTypes.object,
onChange: React.PropTypes.func.isRequired,
}
render() {
const selectedValue = this.props.value || this.props.options[0][0]
const buttons = this.props.options.map(([val, label])=> {
let options = this.props.options
if(options.length > 0 && !Array.isArray(options[0])) {
options = options.map(v => [v, v])
}
const selectedValue = this.props.value || options[0][0]
const buttons = options.map(([val, label])=> {
return <Button
key={val}
style={{
marginRight: margins[0],
backgroundColor: val === selectedValue ? colors.midgray : colors.gray,
}}
onClick={e => this.props.onChange(val)}
className={classnames({"maputnik-button-selected": val === selectedValue})}
>
{label}
</Button>
})
return <div style={{display: 'inline-block'}}>
return <div className="maputnik-multibutton">
{buttons}
</div>
}

View File

@@ -1,10 +1,8 @@
import React from 'react'
import input from '../../config/input.js'
class NumberInput extends React.Component {
static propTypes = {
value: React.PropTypes.number,
style: React.PropTypes.object,
default: React.PropTypes.number,
min: React.PropTypes.number,
max: React.PropTypes.number,
@@ -68,10 +66,7 @@ class NumberInput extends React.Component {
render() {
return <input
style={{
...input.input,
...this.props.style
}}
className="maputnik-number"
placeholder={this.props.default}
value={this.state.value}
onChange={e => this.changeValue(e.target.value)}

View File

@@ -1,5 +1,4 @@
import React from 'react'
import input from '../../config/input.js'
class SelectInput extends React.Component {
static propTypes = {
@@ -11,19 +10,18 @@ class SelectInput extends React.Component {
render() {
const options = this.props.options.map(([val, label])=> {
return <option key={val} value={val}>{label}</option>
})
let options = this.props.options
if(options.length > 0 && !Array.isArray(options[0])) {
options = options.map(v => [v, v])
}
return <select
style={{
...input.select,
...this.props.style
}}
className="maputnik-select"
style={this.props.style}
value={this.props.value}
onChange={e => this.props.onChange(e.target.value)}
>
{options}
{ options.map(([val, label]) => <option key={val} value={val}>{label}</option>) }
</select>
}
}

View File

@@ -1,23 +1,34 @@
import React from 'react'
import input from '../../config/input.js'
class StringInput extends React.Component {
static propTypes = {
value: React.PropTypes.string,
style: React.PropTypes.object,
default: React.PropTypes.number,
default: React.PropTypes.string,
onChange: React.PropTypes.func,
}
constructor(props) {
super(props)
this.state = {
value: props.value || ''
}
}
componentWillReceiveProps(nextProps) {
this.setState({ value: nextProps.value || '' })
}
render() {
return <input
style={{
...input.input,
...this.props.style
}}
value={this.props.value}
className="maputnik-string"
style={this.props.style}
value={this.state.value}
placeholder={this.props.default}
onChange={e => this.props.onChange(e.target.value)}
onChange={e => this.setState({ value: e.target.value })}
onBlur={() => {
if(this.state.value!==this.props.value) this.props.onChange(this.state.value)
}}
/>
}
}

View 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} />
}
}

View File

@@ -5,9 +5,6 @@ import InputBlock from '../inputs/InputBlock'
import StringInput from '../inputs/StringInput'
import SelectInput from '../inputs/SelectInput'
import colors from '../../config/colors'
import { margins } from '../../config/scales'
import 'codemirror/mode/javascript/javascript'
import 'codemirror/lib/codemirror.css'
import '../../codemirror-maputnik.css'

View File

@@ -1,23 +1,21 @@
import React from 'react'
import GlSpec from 'mapbox-gl-style-spec/reference/latest.min.js'
import JSONEditor from './JSONEditor'
import FilterEditor from '../filter/FilterEditor'
import PropertyGroup from '../fields/PropertyGroup'
import LayerEditorGroup from './LayerEditorGroup'
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 input from '../../config/input.js'
import { changeType, changeProperty } from '../../libs/layer'
import layout from '../../config/layout.json'
import { margins, fontSizes } from '../../config/scales'
import colors from '../../config/colors'
class UnsupportedLayer extends React.Component {
render() {
@@ -25,12 +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. */
export default class LayerEditor extends React.Component {
static propTypes = {
layer: React.PropTypes.object.isRequired,
sources: React.PropTypes.object,
vectorLayers: React.PropTypes.object,
spec: React.PropTypes.object.isRequired,
onLayerChanged: React.PropTypes.func,
onLayerIdChange: React.PropTypes.func,
}
@@ -50,7 +65,7 @@ export default class LayerEditor extends React.Component {
//TODO: Clean this up and refactor into function
const editorGroups = {}
layout[this.props.layer.type].groups.forEach(group => {
layoutGroups(this.props.layer.type).forEach(group => {
editorGroups[group.title] = true
})
@@ -74,8 +89,8 @@ export default class LayerEditor extends React.Component {
getChildContext () {
return {
reactIconBase: {
size: fontSizes[4],
color: colors.lowgray,
size: 14,
color: '#8e8e8e',
}
}
}
@@ -96,40 +111,49 @@ export default class LayerEditor extends React.Component {
renderGroupType(type, fields) {
switch(type) {
case 'settings': return <div>
<LayerIdBlock
value={this.props.layer.id}
onChange={newId => this.props.onLayerIdChange(this.props.layer.id, newId)}
/>
<LayerTypeBlock
value={this.props.layer.type}
onChange={newType => this.props.onLayerChanged(changeType(this.props.layer, newType))}
/>
</div>
case 'source': return <div>
<LayerSourceBlock
case 'layer': return <div>
<LayerIdBlock
value={this.props.layer.id}
onChange={newId => this.props.onLayerIdChange(this.props.layer.id, newId)}
/>
<LayerTypeBlock
value={this.props.layer.type}
onChange={newType => this.props.onLayerChanged(changeType(this.props.layer, newType))}
/>
{this.props.layer.type !== 'background' && <LayerSourceBlock
sourceIds={Object.keys(this.props.sources)}
value={this.props.layer.source}
onChange={v => this.changeProperty(null, 'source', v)}
/>
<LayerSourceLayerBlock
}
{this.props.layer.type !== 'raster' && this.props.layer.type !== 'background' && <LayerSourceLayerBlock
sourceLayerIds={this.props.sources[this.props.layer.source]}
value={this.props.layer['source-layer']}
onChange={v => this.changeProperty(null, 'source-layer', v)}
/>
{this.props.layer.filter &&
<div style={input.property}>
}
<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
filter={this.props.layer.filter}
properties={this.props.vectorLayers[this.props.layer['source-layer']]}
onChange={f => this.changeProperty(null, 'filter', f)}
/>
</div>
}
</div>
case 'properties': return <PropertyGroup
layer={this.props.layer}
groupFields={fields}
spec={this.props.spec}
onChange={this.changeProperty.bind(this)}
/>
case 'jsoneditor': return <JSONEditor
@@ -142,8 +166,8 @@ export default class LayerEditor extends React.Component {
render() {
const layerType = this.props.layer.type
const layoutGroups = layout[layerType].groups.filter(group => {
return !(this.props.layer.type === 'background' && group.type === 'source')
const groups = layoutGroups(layerType).filter(group => {
return !(layerType === 'background' && group.type === 'source')
}).map(group => {
return <LayerEditorGroup
key={group.title}
@@ -155,8 +179,9 @@ export default class LayerEditor extends React.Component {
</LayerEditorGroup>
})
return <div>
{layoutGroups}
return <div className="maputnik-layer-editor"
>
{groups}
</div>
}
}

View File

@@ -1,25 +1,6 @@
import React from 'react'
import Color from 'color'
import colors from '../../config/colors'
import { margins, fontSizes } from '../../config/scales'
import Collapse from 'react-collapse'
import CollapseOpenIcon from 'react-icons/lib/md/arrow-drop-down'
import CollapseCloseIcon from 'react-icons/lib/md/arrow-drop-up'
class Collapser extends React.Component {
static propTypes = {
isCollapsed: React.PropTypes.bool.isRequired,
}
render() {
const iconStyle = {
width: 20,
height: 20,
}
return this.props.isCollapsed ? <CollapseCloseIcon style={iconStyle}/> : <CollapseOpenIcon style={iconStyle} />
}
}
import Collapser from './Collapser'
export default class LayerEditorGroup extends React.Component {
static propTypes = {
@@ -29,26 +10,9 @@ export default class LayerEditorGroup extends React.Component {
onActiveToggle: React.PropTypes.func.isRequired
}
constructor(props) {
super(props)
this.state = { hover: false }
}
render() {
return <div>
<div style={{
fontSize: fontSizes[4],
backgroundColor: this.state.hover ? Color(colors.black).lighten(0.30).string() : Color(colors.black).lighten(0.15).string(),
color: colors.lowgray,
cursor: 'pointer',
userSelect: 'none',
padding: margins[1],
display: 'flex',
flexDirection: 'row',
lineHeight: '20px',
}}
onMouseOver={e => this.setState({hover: true})}
onMouseOut={e => this.setState({hover: false})}
<div className="maputnik-layer-editor-group"
onClick={e => this.props.onActiveToggle(!this.props.isActive)}
>
<span>{this.props.title}</span>

View File

@@ -1,5 +1,6 @@
import React from 'react'
import GlSpec from 'mapbox-gl-style-spec/reference/latest.js'
import InputBlock from '../inputs/InputBlock'
import StringInput from '../inputs/StringInput'
@@ -10,7 +11,7 @@ class LayerIdBlock extends React.Component {
}
render() {
return <InputBlock label={"Layer ID"}>
return <InputBlock label={"ID"} doc={GlSpec.layer.id.doc}>
<StringInput
value={this.props.value}
onChange={this.props.onChange}

View File

@@ -1,11 +1,14 @@
import React from 'react'
import classnames from 'classnames'
import cloneDeep from 'lodash.clonedeep'
import Button from '../Button'
import LayerListGroup from './LayerListGroup'
import LayerListItem from './LayerListItem'
import AddIcon from 'react-icons/lib/md/add-circle-outline'
import AddModal from '../modals/AddModal'
import style from '../../libs/style.js'
import { margins } from '../../config/scales.js'
import {SortableContainer, SortableHandle, arrayMove} from 'react-sortable-hoc';
const layerListPropTypes = {
@@ -13,6 +16,25 @@ const layerListPropTypes = {
selectedLayerIndex: React.PropTypes.number.isRequired,
onLayersChange: React.PropTypes.func.isRequired,
onLayerSelect: React.PropTypes.func,
sources: React.PropTypes.object.isRequired,
}
function layerPrefix(name) {
return name.replace(' ', '-').replace('_', '-').split('-')[0]
}
function findClosestCommonPrefix(layers, idx) {
const currentLayerPrefix = layerPrefix(layers[idx].id)
let closestIdx = idx
for (let i = idx; i > 0; i--) {
const previousLayerPrefix = layerPrefix(layers[i-1].id)
if(previousLayerPrefix === currentLayerPrefix) {
closestIdx = i - 1
} else {
return closestIdx
}
}
return closestIdx
}
// List of collapsible layer editors
@@ -23,6 +45,16 @@ class LayerListContainer extends React.Component {
onLayerSelect: () => {},
}
constructor(props) {
super(props)
this.state = {
collapsedGroups: {},
isOpen: {
add: false,
}
}
}
onLayerDestroy(layerId) {
const remainingLayers = this.props.layers.slice(0)
const idx = style.indexOfLayer(remainingLayers, layerId)
@@ -53,28 +85,108 @@ class LayerListContainer extends React.Component {
this.props.onLayersChange(changedLayers)
}
render() {
const layerPanels = this.props.layers.map((layer, index) => {
const layerId = layer.id
return <LayerListItem
index={index}
key={layerId}
layerId={layerId}
layerType={layer.type}
visibility={(layer.layout || {}).visibility}
isSelected={index === this.props.selectedLayerIndex}
onLayerSelect={this.props.onLayerSelect}
onLayerDestroy={this.onLayerDestroy.bind(this)}
onLayerCopy={this.onLayerCopy.bind(this)}
onLayerVisibilityToggle={this.onLayerVisibilityToggle.bind(this)}
/>
toggleModal(modalName) {
this.setState({
isOpen: {
...this.state.isOpen,
[modalName]: !this.state.isOpen[modalName]
}
})
return <ul style={{
padding: 0,
margin: 0
}}>
{layerPanels}
</ul>
}
groupedLayers() {
const groups = []
for (let i = 0; i < this.props.layers.length; i++) {
const previousLayer = this.props.layers[i-1]
const layer = this.props.layers[i]
if(previousLayer && layerPrefix(previousLayer.id) == layerPrefix(layer.id)) {
const lastGroup = groups[groups.length - 1]
lastGroup.push(layer)
} else {
groups.push([layer])
}
}
return groups
}
toggleLayerGroup(groupPrefix, idx) {
const lookupKey = [groupPrefix, idx].join('-')
const newGroups = { ...this.state.collapsedGroups }
if(lookupKey in this.state.collapsedGroups) {
newGroups[lookupKey] = !this.state.collapsedGroups[lookupKey]
} else {
newGroups[lookupKey] = false
}
this.setState({
collapsedGroups: newGroups
})
}
isCollapsed(groupPrefix, idx) {
const collapsed = this.state.collapsedGroups[[groupPrefix, idx].join('-')]
return collapsed === undefined ? true : collapsed
}
render() {
const listItems = []
let idx = 0
this.groupedLayers().forEach(layers => {
const groupPrefix = layerPrefix(layers[0].id)
if(layers.length > 1) {
const grp = <LayerListGroup
key={[groupPrefix, idx].join('-')}
title={groupPrefix}
isActive={!this.isCollapsed(groupPrefix, idx)}
onActiveToggle={this.toggleLayerGroup.bind(this, groupPrefix, idx)}
/>
listItems.push(grp)
}
layers.forEach((layer, idxInGroup) => {
const groupIdx = findClosestCommonPrefix(this.props.layers, idx)
const listItem = <LayerListItem
className={classnames({
'maputnik-layer-list-item-collapsed': 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}
visibility={(layer.layout || {}).visibility}
isSelected={idx === this.props.selectedLayerIndex}
onLayerSelect={this.props.onLayerSelect}
onLayerDestroy={this.onLayerDestroy.bind(this)}
onLayerCopy={this.onLayerCopy.bind(this)}
onLayerVisibilityToggle={this.onLayerVisibilityToggle.bind(this)}
/>
listItems.push(listItem)
idx += 1
})
})
return <div className="maputnik-layer-list">
<AddModal
layers={this.props.layers}
sources={this.props.sources}
isOpen={this.state.isOpen.add}
onOpenToggle={this.toggleModal.bind(this, 'add')}
onLayersChange={this.props.onLayersChange}
/>
<header className="maputnik-layer-list-header">
<span className="maputnik-layer-list-header-title">Layers</span>
<span className="maputnik-space" />
<Button
onClick={this.toggleModal.bind(this, 'add')}
className="maputnik-add-layer">
Add Layer
</Button>
</header>
<ul className="maputnik-layer-list-container">
{listItems}
</ul>
</div>
}
}

View 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>
}
}

View File

@@ -1,5 +1,6 @@
import React from 'react'
import Color from 'color'
import classnames from 'classnames'
import CopyIcon from 'react-icons/lib/md/content-copy'
import VisibilityIcon from 'react-icons/lib/md/visibility'
@@ -10,10 +11,6 @@ import LayerIcon from '../icons/LayerIcon'
import LayerEditor from './LayerEditor'
import {SortableElement, SortableHandle} from 'react-sortable-hoc'
import colors from '../../config/colors.js'
import { fontSizes, margins } from '../../config/scales.js'
@SortableHandle
class LayerTypeDragHandle extends React.Component {
static propTypes = LayerIcon.propTypes
@@ -23,9 +20,9 @@ class LayerTypeDragHandle extends React.Component {
{...this.props}
style={{
cursor: 'move',
width: fontSizes[4],
height: fontSizes[4],
paddingRight: margins[0],
width: 14,
height: 14,
paddingRight: 3,
}}
/>
}
@@ -34,46 +31,23 @@ class LayerTypeDragHandle extends React.Component {
class IconAction extends React.Component {
static propTypes = {
action: React.PropTypes.string.isRequired,
active: React.PropTypes.bool,
onClick: React.PropTypes.func.isRequired,
}
constructor(props) {
super(props)
this.state = { hover: false }
}
renderIcon() {
const iconStyle = {
fill: colors.black
}
if(this.props.active) {
iconStyle.fill = colors.midgray
}
if(this.state.hover) {
iconStyle.fill = colors.lowgray
}
switch(this.props.action) {
case 'copy': return <CopyIcon style={iconStyle} />
case 'show': return <VisibilityIcon style={iconStyle} />
case 'hide': return <VisibilityOffIcon style={iconStyle} />
case 'delete': return <DeleteIcon style={iconStyle} />
case 'copy': return <CopyIcon />
case 'show': return <VisibilityIcon />
case 'hide': return <VisibilityOffIcon />
case 'delete': return <DeleteIcon />
default: return null
}
}
render() {
return <a
style={{
display: "inline",
marginLeft: margins[0],
...this.props.style
}}
className="maputnik-layer-list-icon-action"
onClick={this.props.onClick}
onMouseOver={e => this.setState({hover: true})}
onMouseOut={e => this.setState({hover: false})}
>
{this.renderIcon()}
</a>
@@ -87,6 +61,7 @@ class LayerListItem extends React.Component {
layerType: React.PropTypes.string.isRequired,
isSelected: React.PropTypes.bool,
visibility: React.PropTypes.string,
className: React.PropTypes.string,
onLayerSelect: React.PropTypes.func.isRequired,
onLayerCopy: React.PropTypes.func,
@@ -106,84 +81,36 @@ class LayerListItem extends React.Component {
reactIconBase: React.PropTypes.object
}
constructor(props) {
super(props)
this.state = {
hover: false
}
}
getChildContext() {
return {
reactIconBase: { size: fontSizes[4] }
reactIconBase: { size: 14 }
}
}
render() {
const itemStyle = {
fontWeight: 400,
color: colors.lowgray,
fontSize: fontSizes[5],
borderLeft: 0,
borderTop: 0,
borderBottom: 1,
borderRight: 0,
borderStyle: "solid",
userSelect: 'none',
listStyle: 'none',
zIndex: 2000,
cursor: 'pointer',
position: 'relative',
padding: margins[1],
borderColor: Color(colors.black).lighten(0.10).string(),
backgroundColor: colors.black,
lineHeight: 1.3,
}
if(this.state.hover) {
itemStyle.backgroundColor = Color(colors.black).lighten(0.10).string()
}
if(this.props.isSelected) {
itemStyle.backgroundColor = Color(colors.black).lighten(0.15).string()
}
const iconProps = {
active: this.state.hover || this.props.isSelected
}
return <li
key={this.props.layerId}
onClick={e => this.props.onLayerSelect(this.props.layerId)}
onMouseOver={e => this.setState({hover: true})}
onMouseOut={e => this.setState({hover: false})}
style={itemStyle}>
<div style={{
display: 'flex',
flexDirection: 'row'
}}>
className={classnames({
"maputnik-layer-list-item": true,
"maputnik-layer-list-item-selected": this.props.isSelected,
[this.props.className]: true,
})}>
<LayerTypeDragHandle type={this.props.layerType} />
<span style={{
width: 115,
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis'
}}>{this.props.layerId}</span>
<span className="maputnik-layer-list-item-id">{this.props.layerId}</span>
<span style={{flexGrow: 1}} />
<IconAction {...iconProps}
<IconAction
action={'delete'}
onClick={e => this.props.onLayerDestroy(this.props.layerId)}
/>
<IconAction {...iconProps}
<IconAction
action={'copy'}
onClick={e => this.props.onLayerCopy(this.props.layerId)}
/>
<IconAction {...iconProps}
active={this.state.hover || this.props.isSelected || this.props.visibility === 'none'}
<IconAction
action={this.props.visibility === 'visible' ? 'hide' : 'show'}
onClick={e => this.props.onLayerVisibilityToggle(this.props.layerId)}
/>
</div>
</li>
}
}

View File

@@ -1,5 +1,6 @@
import React from 'react'
import GlSpec from 'mapbox-gl-style-spec/reference/latest.js'
import InputBlock from '../inputs/InputBlock'
import StringInput from '../inputs/StringInput'
import SelectInput from '../inputs/SelectInput'
@@ -18,12 +19,11 @@ class LayerSourceBlock extends React.Component {
}
render() {
return <InputBlock label={"Source"}>
return <InputBlock label={"Source"} doc={GlSpec.layer.source.doc}>
<AutocompleteInput
value={this.props.value}
onChange={this.props.onChange}
options={this.props.sourceIds.map(src => [src, src])}
wrapperStyle={{ width: '50%' }}
/>
</InputBlock>
}

View File

@@ -1,5 +1,6 @@
import React from 'react'
import GlSpec from 'mapbox-gl-style-spec/reference/latest.js'
import InputBlock from '../inputs/InputBlock'
import StringInput from '../inputs/StringInput'
import SelectInput from '../inputs/SelectInput'
@@ -18,12 +19,11 @@ class LayerSourceLayer extends React.Component {
}
render() {
return <InputBlock label={"Source Layer"}>
return <InputBlock label={"Source Layer"} doc={GlSpec.layer['source-layer'].doc}>
<AutocompleteInput
value={this.props.value}
onChange={this.props.onChange}
options={this.props.sourceLayerIds.map(l => [l, l])}
wrapperStyle={{ width: '50%' }}
/>
</InputBlock>
}

View File

@@ -1,5 +1,6 @@
import React from 'react'
import GlSpec from 'mapbox-gl-style-spec/reference/latest.js'
import InputBlock from '../inputs/InputBlock'
import SelectInput from '../inputs/SelectInput'
@@ -10,7 +11,7 @@ class LayerTypeBlock extends React.Component {
}
render() {
return <InputBlock label={"Layer Type"}>
return <InputBlock label={"Type"} doc={GlSpec.layer.type.doc}>
<SelectInput
options={[
['background', 'Background'],

View 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

View 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

View 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

View File

@@ -1,66 +0,0 @@
import React from 'react'
import InputBlock from '../inputs/InputBlock'
import StringInput from '../inputs/StringInput'
import LayerIcon from '../icons/LayerIcon'
import input from '../../config/input'
import colors from '../../config/colors'
import { margins, fontSizes } from '../../config/scales'
const Panel = (props) => {
return <div style={{
backgroundColor: colors.gray,
padding: margins[0],
fontSize: fontSizes[5],
lineHeight: 1.2,
}}>{props.children}</div>
}
function renderFeature(feature) {
return <div>
<Panel>{feature.layer['source-layer']}</Panel>
</div>
}
function groupFeaturesBySourceLayer(features) {
const sources = {}
features.forEach(feature => {
sources[feature.layer['source-layer']] = sources[feature.layer['source-layer']] || []
sources[feature.layer['source-layer']].push(feature)
})
return sources
}
class FeatureLayerTable extends React.Component {
render() {
const sources = groupFeaturesBySourceLayer(this.props.features)
const items = Object.keys(sources).map(vectorLayerId => {
const layers = sources[vectorLayerId].map(feature => {
return <label style={{
...input.label,
display: 'block',
width: 'auto',
}}>
<LayerIcon type={feature.layer.type} style={{
width: fontSizes[4],
height: fontSizes[4],
paddingRight: margins[0],
}}/>
{feature.layer.id}
</label>
})
return <div>
<Panel>{vectorLayerId}</Panel>
{layers}
</div>
})
return <div>
{items}
</div>
}
}
export default FeatureLayerTable

View File

@@ -2,31 +2,30 @@ import React from 'react'
import InputBlock from '../inputs/InputBlock'
import StringInput from '../inputs/StringInput'
import colors from '../../config/colors'
import { margins, fontSizes } from '../../config/scales'
function displayValue(value) {
if (typeof value === 'undefined' || value === null) return value;
if (value instanceof Date) return value.toLocaleString();
if (typeof value === 'object' ||
typeof value === 'number' ||
typeof value === 'string') return value.toString();
return value;
}
function renderProperties(feature) {
return Object.keys(feature.properties).map(propertyName => {
const property = feature.properties[propertyName]
return <InputBlock label={propertyName} style={{marginTop: 0, marginBottom: 0}}>
<StringInput value={property} style={{backgroundColor: 'transparent'}}/>
return <InputBlock key={propertyName} label={propertyName}>
<StringInput value={displayValue(property)} style={{backgroundColor: 'transparent'}}/>
</InputBlock>
})
}
const Panel = (props) => {
return <div style={{
backgroundColor: colors.gray,
padding: margins[0],
fontSize: fontSizes[5],
lineHeight: 1.2,
}}>{props.children}</div>
}
function renderFeature(feature) {
console.log(feature)
return <div>
<Panel>{feature.layer['source-layer']}</Panel>
return <div key={feature.id}>
<div className="maputnik-popup-layer-id">{feature.layer['source-layer']}</div>
<InputBlock key={"property-type"} label={"$type"}>
<StringInput value={feature.geometry.type} style={{backgroundColor: 'transparent'}} />
</InputBlock>
{renderProperties(feature)}
</div>
}
@@ -35,7 +34,7 @@ class FeaturePropertyPopup extends React.Component {
render() {
const features = this.props.features
return <div>
return <div className="maputnik-feature-property-popup">
{features.map(renderFeature)}
</div>
}

View File

@@ -1,126 +0,0 @@
import React from 'react'
import ReactDOM from 'react-dom'
import MapboxGl from 'mapbox-gl/dist/mapbox-gl.js'
import validateColor from 'mapbox-gl-style-spec/lib/validate/validate_color'
import colors from '../../config/colors'
import style from '../../libs/style'
import FeaturePropertyPopup from './FeaturePropertyPopup'
import { colorHighlightedLayer, generateColoredLayers } from '../../libs/stylegen'
import 'mapbox-gl/dist/mapbox-gl.css'
import '../../mapboxgl.css'
function convertInspectStyle(mapStyle, sources, highlightedLayer) {
const coloredLayers = generateColoredLayers(sources)
const layer = colorHighlightedLayer(highlightedLayer)
if(layer) {
coloredLayers.push(layer)
}
const newStyle = {
...mapStyle,
layers: [
{
"id": "background",
"type": "background",
"paint": {
"background-color": colors.black,
}
},
...coloredLayers,
]
}
return newStyle
}
function renderPopup(features) {
var mountNode = document.createElement('div');
ReactDOM.render(<FeaturePropertyPopup features={features} />, mountNode)
return mountNode.innerHTML;
}
export default class InspectionMap extends React.Component {
static propTypes = {
onDataChange: React.PropTypes.func,
sources: React.PropTypes.object,
originalStyle: React.PropTypes.object,
highlightedLayer: React.PropTypes.object,
style: React.PropTypes.object,
}
static defaultProps = {
onMapLoaded: () => {},
onTileLoaded: () => {},
}
constructor(props) {
super(props)
this.state = { map: null }
}
componentWillReceiveProps(nextProps) {
if(!this.state.map) return
this.state.map.setStyle(convertInspectStyle(nextProps.mapStyle, this.props.sources, nextProps.highlightedLayer), { diff: true})
}
componentDidMount() {
MapboxGl.accessToken = this.props.accessToken
const map = new MapboxGl.Map({
container: this.container,
style: convertInspectStyle(this.props.mapStyle, this.props.sources, this.props.highlightedLayer),
hash: true,
})
const nav = new MapboxGl.NavigationControl();
map.addControl(nav, 'top-right');
map.on("style.load", () => {
this.setState({ map });
})
map.on("data", e => {
if(e.dataType !== 'tile') return
this.props.onDataChange({
map: this.state.map
})
})
map.on('click', this.displayPopup.bind(this))
map.on('mousemove', function(e) {
var features = map.queryRenderedFeatures(e.point, { layers: this.layers })
map.getCanvas().style.cursor = (features.length) ? 'pointer' : ''
})
}
displayPopup(e) {
const features = this.state.map.queryRenderedFeatures(e.point, {
layers: this.layers
});
if (!features.length) {
return
}
// Populate the popup and set its coordinates
// based on the feature found.
const popup = new MapboxGl.Popup()
.setLngLat(e.lngLat)
.setHTML(renderPopup(features))
.addTo(this.state.map)
}
render() {
return <div
ref={x => this.container = x}
style={{
position: "fixed",
top: 0,
bottom: 0,
height: "100%",
width: "100%",
...this.props.style,
}}></div>
}
}

View File

@@ -1,35 +1,80 @@
import React from 'react'
import ReactDOM from 'react-dom'
import MapboxGl from 'mapbox-gl/dist/mapbox-gl.js'
import FeatureLayerTable from './FeatureLayerTable'
import MapboxInspect from 'mapbox-gl-inspect'
import FeatureLayerPopup from './FeatureLayerPopup'
import FeaturePropertyPopup from './FeaturePropertyPopup'
import validateColor from 'mapbox-gl-style-spec/lib/validate/validate_color'
import style from '../../libs/style.js'
import tokens from '../../config/tokens.json'
import 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'
function renderPopup(features) {
function renderLayerPopup(features) {
var mountNode = document.createElement('div');
ReactDOM.render(<FeatureLayerTable features={features} />, mountNode)
ReactDOM.render(<FeatureLayerPopup features={features} />, mountNode)
return mountNode.innerHTML;
}
function renderPropertyPopup(features) {
var mountNode = document.createElement('div');
ReactDOM.render(<FeaturePropertyPopup features={features} />, mountNode)
return mountNode.innerHTML;
}
function buildInspectStyle(originalMapStyle, coloredLayers, highlightedLayer) {
const backgroundLayer = {
"id": "background",
"type": "background",
"paint": {
"background-color": '#1c1f24',
}
}
const layer = colorHighlightedLayer(highlightedLayer)
if(layer) {
coloredLayers.push(layer)
}
const sources = {}
Object.keys(originalMapStyle.sources).forEach(sourceId => {
const source = originalMapStyle.sources[sourceId]
if(source.type !== 'raster') {
sources[sourceId] = source
}
})
const inspectStyle = {
...originalMapStyle,
sources: sources,
layers: [backgroundLayer].concat(coloredLayers)
}
return inspectStyle
}
export default class MapboxGlMap extends React.Component {
static propTypes = {
onDataChange: React.PropTypes.func,
mapStyle: React.PropTypes.object.isRequired,
accessToken: React.PropTypes.string,
style: React.PropTypes.object,
inspectModeEnabled: React.PropTypes.bool.isRequired,
highlightedLayer: React.PropTypes.object,
}
static defaultProps = {
onMapLoaded: () => {},
onDataChange: () => {},
mapboxAccessToken: tokens.mapbox,
}
constructor(props) {
super(props)
MapboxGl.accessToken = tokens.mapbox
this.state = {
map: null,
inspect: null,
isPopupOpen: false,
popupX: 0,
popupY: 0,
@@ -38,26 +83,59 @@ export default class MapboxGlMap extends React.Component {
componentWillReceiveProps(nextProps) {
if(!this.state.map) return
const metadata = nextProps.mapStyle.metadata || {}
MapboxGl.accessToken = metadata['maputnik:mapbox_access_token'] || tokens.mapbox
//Mapbox GL now does diffing natively so we don't need to calculate
//the necessary operations ourselves!
this.state.map.setStyle(nextProps.mapStyle, { diff: true})
if(!nextProps.inspectModeEnabled) {
//Mapbox GL now does diffing natively so we don't need to calculate
//the necessary operations ourselves!
this.state.map.setStyle(nextProps.mapStyle, { diff: true})
}
}
componentDidUpdate(prevProps) {
if(this.props.inspectModeEnabled !== prevProps.inspectModeEnabled) {
this.state.inspect.toggleInspector()
}
if(this.props.inspectModeEnabled) {
this.state.inspect.render()
}
}
componentDidMount() {
MapboxGl.accessToken = this.props.accessToken
const map = new MapboxGl.Map({
container: this.container,
style: this.props.mapStyle,
hash: true,
})
const nav = new MapboxGl.NavigationControl();
map.addControl(nav, 'top-right');
const nav = new MapboxGl.NavigationControl();
map.addControl(nav, 'top-right');
const inspect = new MapboxInspect({
popup: new MapboxGl.Popup({
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 });
this.setState({ map, inspect });
})
map.on("data", e => {
@@ -66,37 +144,12 @@ export default class MapboxGlMap extends React.Component {
map: this.state.map
})
})
map.on('click', this.displayPopup.bind(this));
map.on('mousemove', function(e) {
var features = map.queryRenderedFeatures(e.point, { layers: this.layers })
map.getCanvas().style.cursor = (features.length) ? 'pointer' : ''
})
}
displayPopup(e) {
const features = this.state.map.queryRenderedFeatures(e.point, {
layers: this.layers
});
if(features.length < 1) return
const popup = new MapboxGl.Popup()
.setLngLat(e.lngLat)
.setHTML(renderPopup(features))
.addTo(this.state.map)
}
render() {
return <div
className="maputnik-map"
ref={x => this.container = x}
style={{
position: "fixed",
top: 0,
bottom: 0,
height: "100%",
width: "100%",
...this.props.style,
}}>
</div>
></div>
}
}

View File

@@ -1,5 +1,53 @@
import React from 'react'
import style from '../../libs/style.js'
import isEqual from 'lodash.isequal'
import { loadJSON } from '../../libs/urlopen'
function suitableVectorSource(mapStyle) {
const sources = Object.keys(mapStyle.sources)
.map(sourceId => {
return {
id: sourceId,
source: mapStyle.sources[sourceId]
}
})
.filter(({source}) => source.type === 'vector')
return sources[0]
}
function toVectorLayer(source, tilegrid, cb) {
function newMVTLayer(tileUrl) {
const ol = require('openlayers')
return new ol.layer.VectorTile({
source: new ol.source.VectorTile({
format: new ol.format.MVT(),
tileGrid: tilegrid,
tilePixelRatio: 8,
url: tileUrl
})
})
}
if(!source.tiles) {
sourceFromTileJSON(source.url, tileSource => {
cb(newMVTLayer(tileSource.tiles[0]))
})
} else {
cb(newMVTLayer(source.tiles[0]))
}
}
function sourceFromTileJSON(url, cb) {
loadJSON(url, null, tilejson => {
if(!tilejson) return
cb({
type: 'vector',
tiles: tilejson.tiles,
minzoom: tilejson.minzoom,
maxzoom: tilejson.maxzoom,
})
})
}
class OpenLayers3Map extends React.Component {
static propTypes = {
@@ -13,21 +61,51 @@ class OpenLayers3Map extends React.Component {
onDataChange: () => {},
}
constructor(props) {
super(props)
this.tilegrid = null
this.resolutions = null
this.layer = null
this.map = null
}
updateStyle(newMapStyle) {
const oldSource = suitableVectorSource(this.props.mapStyle)
const newSource = suitableVectorSource(newMapStyle)
const resolutions = this.resolutions
function setStyleFunc(map, layer) {
const olms = require('ol-mapbox-style')
const styleFunc = olms.getStyleFunction(newMapStyle, newSource.id, resolutions)
layer.setStyle(styleFunc)
//NOTE: We need to mark the source as changed in order
//to trigger a rerender
layer.getSource().changed()
map.render()
}
if(newSource) {
if(this.layer && !isEqual(oldSource, newSource)) {
this.map.removeLayer(this.layer)
this.layer = null
}
if(!this.layer) {
toVectorLayer(newSource.source, this.tilegrid, vectorLayer => {
this.layer = vectorLayer
this.map.addLayer(this.layer)
setStyleFunc(this.map, this.layer)
})
} else {
setStyleFunc(this.map, this.layer)
}
}
}
componentWillReceiveProps(nextProps) {
require.ensure(["openlayers", "ol-mapbox-style"], ()=> {
const ol = require('openlayers')
const olms = require('ol-mapbox-style')
const jsonStyle = nextProps.mapStyle
const styleFunc = olms.getStyleFunction(jsonStyle, 'openmaptiles', this.resolutions)
console.log('New style babee')
const layer = this.layer
layer.setStyle(styleFunc)
//NOTE: We need to mark the source as changed in order
//to trigger a rerender
layer.getSource().changed()
this.state.map.render()
require.ensure(["openlayers", "ol-mapbox-style"], () => {
if(!this.map || !this.resolutions) return
this.updateStyle(nextProps.mapStyle)
})
}
@@ -40,46 +118,37 @@ class OpenLayers3Map extends React.Component {
const ol = require('openlayers')
const olms = require('ol-mapbox-style')
const tilegrid = ol.tilegrid.createXYZ({tileSize: 512, maxZoom: 22})
this.resolutions = tilegrid.getResolutions()
this.layer = new ol.layer.VectorTile({
source: new ol.source.VectorTile({
attributions: '© <a href="http://www.openstreetmap.org/copyright">OpenStreetMap contributors</a>',
format: new ol.format.MVT(),
tileGrid: tilegrid,
tilePixelRatio: 8,
url: 'https://free-0.tilehosting.com/data/v3/{z}/{x}/{y}.pbf?key=tXiQqN3lIgskyDErJCeY'
})
})
const jsonStyle = this.props.mapStyle
const styleFunc = olms.getStyleFunction(jsonStyle, 'openmaptiles', this.resolutions)
this.layer.setStyle(styleFunc)
this.tilegrid = ol.tilegrid.createXYZ({tileSize: 512, maxZoom: 22})
this.resolutions = this.tilegrid.getResolutions()
const map = new ol.Map({
target: this.container,
layers: [this.layer],
layers: [],
view: new ol.View({
center: jsonStyle.center,
zoom: 2,
//zoom: jsonStyle.zoom,
center: [52.5, -78.4]
})
})
map.addControl(new ol.control.Zoom());
this.setState({ map });
map.addControl(new ol.control.Zoom())
this.map = map
this.updateStyle(this.props.mapStyle)
})
}
render() {
return <div
ref={x => this.container = x}
style={{
position: "fixed",
top: 0,
bottom: 0,
height: "100%",
width: "100%",
}}></div>
ref={x => this.container = x}
style={{
position: "fixed",
top: 0,
right: 0,
bottom: 0,
height: "100%",
width: "75%",
backgroundColor: '#fff',
...this.props.style,
}}>
</div>
}
}

View File

@@ -5,7 +5,6 @@ import InputBlock from '../inputs/InputBlock'
import StringInput from '../inputs/StringInput'
import SelectInput from '../inputs/SelectInput'
import Modal from './Modal'
import colors from '../../config/colors'
import LayerTypeBlock from '../layers/LayerTypeBlock'
import LayerIdBlock from '../layers/LayerIdBlock'
@@ -14,8 +13,8 @@ import LayerSourceLayerBlock from '../layers/LayerSourceLayerBlock'
class AddModal extends React.Component {
static propTypes = {
mapStyle: React.PropTypes.object.isRequired,
onStyleChange: React.PropTypes.func.isRequired,
layers: React.PropTypes.array.isRequired,
onLayersChange: React.PropTypes.func.isRequired,
isOpen: React.PropTypes.bool.isRequired,
onOpenToggle: React.PropTypes.func.isRequired,
@@ -24,7 +23,7 @@ class AddModal extends React.Component {
}
addLayer() {
const changedLayers = this.props.mapStyle.layers.slice(0)
const changedLayers = this.props.layers.slice(0)
const layer = {
id: this.state.id,
type: this.state.type,
@@ -32,17 +31,14 @@ class AddModal extends React.Component {
if(this.state.type !== 'background') {
layer.source = this.state.source
layer['source-layer'] = this.state['source-layer']
if(this.state.type !== 'raster' && this.state['source-layer']) {
layer['source-layer'] = this.state['source-layer']
}
}
changedLayers.push(layer)
const changedStyle = {
...this.props.mapStyle,
layers: changedLayers,
}
this.props.onStyleChange(changedStyle)
this.props.onLayersChange(changedLayers)
this.props.onOpenToggle(false)
}
@@ -64,7 +60,7 @@ class AddModal extends React.Component {
if(!this.state.source && sourceIds.length > 0) {
this.setState({
source: sourceIds[0],
'source-layer': this.state['source-layer'] || nextProps.sources[sourceIds[0]][0]
'source-layer': this.state['source-layer'] || (nextProps.sources[sourceIds[0]] || [])[0]
})
}
}
@@ -76,6 +72,7 @@ class AddModal extends React.Component {
onOpenToggle={this.props.onOpenToggle}
title={'Add Layer'}
>
<div className="maputnik-add-layer">
<LayerIdBlock
value={this.state.id}
onChange={v => this.setState({ id: v })}
@@ -91,16 +88,17 @@ class AddModal extends React.Component {
onChange={v => this.setState({ source: v })}
/>
}
{this.state.type !== 'background' &&
{this.state.type !== 'background' && this.state.type !== 'raster' &&
<LayerSourceLayerBlock
sourceLayerIds={this.props.sources[this.state.source] || []}
value={this.state['source-layer']}
onChange={v => this.setState({ 'source-layer': v })}
/>
}
<Button onClick={this.addLayer.bind(this)}>
<Button className="maputnik-add-layer-button" onClick={this.addLayer.bind(this)}>
Add Layer
</Button>
</div>
</Modal>
}
}

View 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

View File

@@ -1,10 +1,6 @@
import React from 'react'
import CloseIcon from 'react-icons/lib/md/close'
import Overlay from './Overlay'
import colors from '../../config/colors'
import { margins, fontSizes } from '../../config/scales'
class Modal extends React.Component {
static propTypes = {
@@ -15,32 +11,17 @@ class Modal extends React.Component {
render() {
return <Overlay isOpen={this.props.isOpen}>
<div style={{
minWidth: 350,
maxWidth: 600,
backgroundColor: colors.black,
boxShadow: '0px 0px 5px 0px rgba(0,0,0,0.3)',
}}>
<div style={{
backgroundColor: colors.gray,
display: 'flex',
flexDirection: 'row',
padding: margins[2],
fontSize: fontSizes[4],
}}>
{this.props.title}
<span style={{flexGrow: 1}} />
<a
<div className="maputnik-modal">
<header className="maputnik-modal-header">
<h1 className="maputnik-modal-header-title">{this.props.title}</h1>
<span className="maputnik-modal-header-space"></span>
<a className="maputnik-modal-header-toggle"
onClick={() => this.props.onOpenToggle(false)}
style={{ cursor: 'pointer' }} >
>
<CloseIcon />
</a>
</div>
<div style={{
padding: margins[2],
}}>
{this.props.children}
</div>
</header>
<div className="maputnik-modal-content">{this.props.children}</div>
</div>
</Overlay>
}

View File

@@ -1,8 +1,6 @@
import React from 'react'
import Modal from './Modal'
import Heading from '../Heading'
import Button from '../Button'
import Paragraph from '../Paragraph'
import FileReaderInput from 'react-file-reader-input'
import request from 'request'
@@ -10,8 +8,6 @@ import FileUploadIcon from 'react-icons/lib/md/file-upload'
import AddIcon from 'react-icons/lib/md/add-circle-outline'
import style from '../../libs/style.js'
import colors from '../../config/colors'
import { margins, fontSizes } from '../../config/scales'
import publicStyles from '../../config/styles.json'
class PublicStyle extends React.Component {
@@ -23,38 +19,18 @@ class PublicStyle extends React.Component {
}
render() {
return <div style={{
verticalAlign: 'top',
marginTop: margins[2],
marginRight: margins[2],
backgroundColor: colors.gray,
display: 'inline-block',
width: 180,
fontSize: fontSizes[4],
color: colors.lowgray,
}}>
return <div className="maputnik-public-style">
<Button
className="maputnik-public-style-button"
onClick={() => this.props.onSelect(this.props.url)}
style={{
backgroundColor: 'transparent',
padding: margins[2],
display: 'block',
}}
>
<div style={{
display: 'flex',
flexDirection: 'row',
}}>
<span style={{fontWeight: 700}}>{this.props.title}</span>
<span style={{flexGrow: 1}} />
<header className="maputnik-public-style-header">
<h4>{this.props.title}</h4>
<span className="maputnik-space" />
<AddIcon />
</div>
</header>
<img
style={{
display: 'block',
marginTop: margins[1],
maxWidth: '100%',
}}
className="maputnik-public-style-thumbnail"
src={this.props.thumbnailUrl}
alt={this.props.title}
/>
@@ -113,22 +89,22 @@ class OpenModal extends React.Component {
onOpenToggle={this.props.onOpenToggle}
title={'Open Style'}
>
<Heading level={4}>Upload Style</Heading>
<Paragraph>
Upload a JSON style from your computer.
</Paragraph>
<FileReaderInput onChange={this.onUpload.bind(this)}>
<Button>
<FileUploadIcon />
Upload
</Button>
</FileReaderInput>
<Heading level={4}>Gallery Styles</Heading>
<Paragraph>
Open one of the publicly available styles to start from.
</Paragraph>
{styleOptions}
<section className="maputnik-modal-section">
<h2>Upload Style</h2>
<p>Upload a JSON style from your computer.</p>
<FileReaderInput onChange={this.onUpload.bind(this)}>
<Button className="maputnik-upload-button"><FileUploadIcon /> Upload</Button>
</FileReaderInput>
</section>
<section className="maputnik-modal-section">
<h2>Gallery Styles</h2>
<p>
Open one of the publicly available styles to start from.
</p>
<div className="maputnik-style-gallery-container">
{styleOptions}
</div>
</section>
</Modal>
}
}

View File

@@ -1,26 +1,5 @@
import React from 'react'
class ViewportOverlay extends React.Component {
static propTypes = {
style: React.PropTypes.object
}
render() {
const overlayStyle = {
position: 'fixed',
top: 0,
right: 0,
bottom: 0,
left: 0,
zIndex: 2,
opacity: 0.875,
backgroundColor: 'rgb(28, 31, 36)',
...this.props.style
}
return <div style={overlayStyle} />
}
}
class Overlay extends React.Component {
static propTypes = {
@@ -29,23 +8,14 @@ class Overlay extends React.Component {
}
render() {
return <div style={{
top: 0,
right: 0,
bottom: 0,
left: 0,
position: 'fixed',
display: this.props.isOpen ? 'flex' : 'none',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center'
}}>
<ViewportOverlay />
<div style={{
zIndex: 3,
}}>
let overlayStyle = {}
if(!this.props.isOpen) {
overlayStyle['display'] = 'none';
}
return <div className={"maputnik-overlay"} style={overlayStyle}>
<div className={"maputnik-overlay-viewport"} />
{this.props.children}
</div>
</div>
}
}

View File

@@ -1,10 +1,10 @@
import React from 'react'
import GlSpec from 'mapbox-gl-style-spec/reference/latest.js'
import InputBlock from '../inputs/InputBlock'
import StringInput from '../inputs/StringInput'
import SelectInput from '../inputs/SelectInput'
import Modal from './Modal'
import colors from '../../config/colors'
class SettingsModal extends React.Component {
static propTypes = {
@@ -43,52 +43,60 @@ class SettingsModal extends React.Component {
return <Modal
isOpen={this.props.isOpen}
onOpenToggle={this.props.onOpenToggle}
title={'StyleSettings'}
title={'Style Settings'}
>
<InputBlock label={"Name"}>
<div style={{minWidth: 350}}>
<InputBlock label={"Name"} doc={GlSpec.$root.name.doc}>
<StringInput {...inputProps}
value={this.props.mapStyle.name}
onChange={this.changeStyleProperty.bind(this, "name")}
/>
</InputBlock>
<InputBlock label={"Owner"}>
<InputBlock label={"Owner"} doc={"Owner ID of the style. Used by Mapbox or future style APIs."}>
<StringInput {...inputProps}
value={this.props.mapStyle.owner}
onChange={this.changeStyleProperty.bind(this, "owner")}
/>
</InputBlock>
<InputBlock label={"Sprite URL"}>
<InputBlock label={"Sprite URL"} doc={GlSpec.$root.sprite.doc}>
<StringInput {...inputProps}
value={this.props.mapStyle.sprite}
onChange={this.changeStyleProperty.bind(this, "sprite")}
/>
</InputBlock>
<InputBlock label={"Glyphs URL"}>
<InputBlock label={"Glyphs URL"} doc={GlSpec.$root.glyphs.doc}>
<StringInput {...inputProps}
value={this.props.mapStyle.glyphs}
onChange={this.changeStyleProperty.bind(this, "glyphs")}
/>
</InputBlock>
<InputBlock label={"Access Token"}>
<InputBlock label={"Mapbox Access Token"} doc={"Public access token for Mapbox services."}>
<StringInput {...inputProps}
value={metadata['maputnik:access_token']}
onChange={this.changeMetadataProperty.bind(this, "maputnik:access_token")}
value={metadata['maputnik:mapbox_access_token']}
onChange={this.changeMetadataProperty.bind(this, "maputnik:mapbox_access_token")}
/>
</InputBlock>
<InputBlock label={"Style Renderer"}>
<InputBlock label={"OpenMapTiles Access Token"} doc={"Public access token for the OpenMapTiles CDN."}>
<StringInput {...inputProps}
value={metadata['maputnik:openmaptiles_access_token']}
onChange={this.changeMetadataProperty.bind(this, "maputnik:openmaptiles_access_token")}
/>
</InputBlock>
<InputBlock label={"Style Renderer"} doc={"Choose the default Maputnik renderer for this style."}>
<SelectInput {...inputProps}
options={[
['mbgljs', 'MapboxGL JS'],
['ol3', 'Open Layers 3'],
['inspection', 'Inspection Mode'],
]}
value={metadata['maputnik:renderer'] || 'mbgljs'}
onChange={this.changeMetadataProperty.bind(this, 'maputnik:renderer')}
/>
</InputBlock>
</div>
</Modal>
}
}

View File

@@ -1,17 +1,15 @@
import React from 'react'
import GlSpec from 'mapbox-gl-style-spec/reference/latest.js'
import Modal from './Modal'
import Heading from '../Heading'
import Button from '../Button'
import Paragraph from '../Paragraph'
import InputBlock from '../inputs/InputBlock'
import StringInput from '../inputs/StringInput'
import SelectInput from '../inputs/SelectInput'
import SourceTypeEditor from '../sources/SourceTypeEditor'
import style from '../../libs/style'
import { deleteSource, addSource, changeSource } from '../../libs/source'
import publicSources from '../../config/tilesets.json'
import colors from '../../config/colors'
import { margins, fontSizes } from '../../config/scales'
import AddIcon from 'react-icons/lib/md/add-circle-outline'
import DeleteIcon from 'react-icons/lib/md/delete'
@@ -25,82 +23,60 @@ class PublicSource extends React.Component {
}
render() {
return <div style={{
verticalAlign: 'top',
marginTop: margins[2],
marginRight: margins[2],
backgroundColor: colors.gray,
display: 'inline-block',
width: 240,
fontSize: fontSizes[4],
color: colors.lowgray,
}}>
<Button
onClick={() => this.props.onSelect(this.props.id)}
style={{
backgroundColor: 'transparent',
padding: margins[2],
display: 'flex',
flexDirection: 'row',
}}
>
<div>
<span style={{fontWeight: 700}}>{this.props.title}</span>
<br/>
<span style={{fontSize: fontSizes[5]}}>#{this.props.id}</span>
</div>
<span style={{flexGrow: 1}} />
<AddIcon />
</Button>
return <div className="maputnik-public-source">
<Button
className="maputnik-public-source-select"
onClick={() => this.props.onSelect(this.props.id)}
>
<div className="maputnik-public-source-info">
<p className="maputnik-public-source-name">{this.props.title}</p>
<p className="maputnik-public-source-id">#{this.props.id}</p>
</div>
<span className="maputnik-space" />
<AddIcon />
</Button>
</div>
}
}
function editorMode(source) {
if(source.type === 'geojson') return ' geojson'
if(source.type === 'vector' && source.tiles) {
return 'tilexyz'
if(source.type === 'raster') {
if(source.tiles) return 'tilexyz_raster'
return 'tilejson_raster'
}
return 'tilejson'
if(source.type === 'vector') {
if(source.tiles) return 'tilexyz_vector'
return 'tilejson_vector'
}
if(source.type === 'geojson') return 'geojson'
return null
}
class ActiveSourceTypeEditor extends React.Component {
static propTypes = {
sourceId: React.PropTypes.string.isRequired,
source: React.PropTypes.object.isRequired,
onSourceDelete: React.PropTypes.func.isRequired,
onSourceChange: React.PropTypes.func.isRequired,
onDelete: React.PropTypes.func.isRequired,
onChange: React.PropTypes.func.isRequired,
}
render() {
const inputProps = { }
return <div style={{
}}>
<div style={{
backgroundColor: colors.gray,
color: colors.lowgray,
padding: margins[1],
display: 'flex',
fontSize: fontSizes[4],
flexDirection: 'row',
}}>
<span style={{fontWeight: 700, fontSize: fontSizes[4], lineHeight: 2}}>#{this.props.sourceId}</span>
<span style={{flexGrow: 1}} />
return <div className="maputnik-active-source-type-editor">
<div className="maputnik-active-source-type-editor-header">
<span className="maputnik-active-source-type-editor-header-id">#{this.props.sourceId}</span>
<span className="maputnik-space" />
<Button
onClick={()=> this.props.onSourceDelete(this.props.sourceId)}
className="maputnik-active-source-type-editor-header-delete"
onClick={()=> this.props.onDelete(this.props.sourceId)}
style={{backgroundColor: 'transparent'}}
>
<DeleteIcon />
</Button>
</div>
<div style={{
borderColor: colors.gray,
borderWidth: 2,
borderStyle: 'solid',
padding: margins[1],
}}>
<div className="maputnik-active-source-type-editor-content">
<SourceTypeEditor
onChange={this.props.onSourceChange}
onChange={this.props.onChange}
mode={editorMode(this.props.source)}
source={this.props.source}
/>
@@ -111,15 +87,15 @@ class ActiveSourceTypeEditor extends React.Component {
class AddSource extends React.Component {
static propTypes = {
onSourceAdd: React.PropTypes.func.isRequired,
onAdd: React.PropTypes.func.isRequired,
}
constructor(props) {
super(props)
this.state = {
mode: 'tilejson',
mode: 'tilejson_vector',
sourceId: style.generateId(),
source: this.defaultSource('tilejson'),
source: this.defaultSource('tilejson_vector'),
}
}
@@ -130,51 +106,59 @@ class AddSource extends React.Component {
type: 'geojson',
data: source.data || 'http://localhost:3000/geojson.json'
}
case 'tilejson': return {
case 'tilejson_vector': return {
type: 'vector',
url: source.url || 'http://localhost:3000/tilejson.json'
}
case 'tilexyz': return {
case 'tilexyz_vector': return {
type: 'vector',
tiles: source.tiles || ['http://localhost:3000/{x}/{y}/{z}.pbf'],
minZoom: source.minZoom || 0,
maxZoom: source.maxZoom || 14
minZoom: source.minzoom || 0,
maxZoom: source.maxzoom || 14
}
case 'tilejson_raster': return {
type: 'raster',
url: source.url || 'http://localhost:3000/tilejson.json'
}
case 'tilexyz_raster': return {
type: 'raster',
tiles: source.tiles || ['http://localhost:3000/{x}/{y}/{z}.pbf'],
minzoom: source.minzoom || 0,
maxzoom: source.maxzoom || 14
}
default: return {}
}
}
onSourceChange(source) {
this.setState({
source: source
})
}
render() {
return <div>
<InputBlock label={"Source ID"}>
return <div className="maputnik-add-source">
<InputBlock label={"Source ID"} doc={"Unique ID that identifies the source and is used in the layer to reference the source."}>
<StringInput
value={this.state.sourceId}
onChange={v => this.setState({ sourceId: v})}
/>
</InputBlock>
<InputBlock label={"Source Type"}>
<InputBlock label={"Source Type"} doc={GlSpec.source_tile.type.doc}>
<SelectInput
options={[
['geojson', 'GeoJSON'],
['tilejson', 'Vector (TileJSON URL)'],
['tilexyz', 'Vector (XYZ URLs)'],
['tilejson_vector', 'Vector (TileJSON URL)'],
['tilexyz_vector', 'Vector (XYZ URLs)'],
['tilejson_raster', 'Raster (TileJSON URL)'],
['tilexyz_raster', 'Raster (XYZ URL)'],
]}
onChange={mode => this.setState({mode: mode, source: this.defaultSource(mode)})}
value={this.state.mode}
/>
</InputBlock>
<SourceTypeEditor
onChange={this.onSourceChange.bind(this)}
onChange={src => this.setState({ source: src })}
mode={this.state.mode}
source={this.state.source}
/>
<Button onClick={() => this.props.onSourceAdd(this.state.sourceId, this.state.source)}>
<Button
className="maputnik-add-source-button"
onClick={() => this.props.onAdd(this.state.sourceId, this.state.source)}>
Add Source
</Button>
</div>
@@ -189,31 +173,6 @@ class SourcesModal extends React.Component {
onStyleChanged: React.PropTypes.func.isRequired,
}
onSourceAdd(sourceId, source) {
const changedSources = {
...this.props.mapStyle.sources,
[sourceId]: source
}
const changedStyle = {
...this.props.mapStyle,
sources: changedSources
}
this.props.onStyleChanged(changedStyle)
}
deleteSource(sourceId) {
const remainingSources = { ...this.props.mapStyle.sources}
delete remainingSources[sourceId]
const changedStyle = {
...this.props.mapStyle,
sources: remainingSources
}
this.props.onStyleChanged(changedStyle)
}
stripTitle(source) {
const strippedSource = {...source}
delete strippedSource['title']
@@ -221,24 +180,26 @@ class SourcesModal extends React.Component {
}
render() {
const activeSources = Object.keys(this.props.mapStyle.sources).map(sourceId => {
const source = this.props.mapStyle.sources[sourceId]
const mapStyle = this.props.mapStyle
const activeSources = Object.keys(mapStyle.sources).map(sourceId => {
const source = mapStyle.sources[sourceId]
return <ActiveSourceTypeEditor
key={sourceId}
sourceId={sourceId}
source={source}
onSourceDelete={this.deleteSource.bind(this)}
onChange={src => this.props.onStyleChanged(changeSource(mapStyle, sourceId, src))}
onDelete={() => this.props.onStyleChanged(deleteSource(mapStyle, sourceId))}
/>
})
const tilesetOptions = Object.keys(publicSources).filter(sourceId => !(sourceId in this.props.mapStyle.sources)).map(sourceId => {
const tilesetOptions = Object.keys(publicSources).filter(sourceId => !(sourceId in mapStyle.sources)).map(sourceId => {
const source = publicSources[sourceId]
return <PublicSource
key={sourceId}
id={sourceId}
type={source.type}
title={source.title}
onSelect={() => this.onSourceAdd(sourceId, this.stripTitle(source))}
onSelect={() => this.props.onStyleChanged(addSource(mapStyle, sourceId, this.stripTitle(source)))}
/>
})
@@ -248,21 +209,27 @@ class SourcesModal extends React.Component {
onOpenToggle={this.props.onOpenToggle}
title={'Sources'}
>
<Heading level={4}>Active Sources</Heading>
{activeSources}
<Heading level={4}>Add New Source</Heading>
<div style={{maxWidth: 300}}>
<p style={{color: colors.lowgray, fontSize: fontSizes[5]}}>Add a new source to your style. You can only choose the source type and id at creation time!</p>
<AddSource onSourceAdd={this.onSourceAdd.bind(this)} />
<div className="maputnik-modal-section">
<h4>Active Sources</h4>
{activeSources}
</div>
<Heading level={4}>Choose Public Source</Heading>
<Paragraph>
Add one of the publicly availble sources to your style.
</Paragraph>
<div style={{maxwidth: 500}}>
{tilesetOptions}
<div className="maputnik-modal-section">
<h4>Choose Public Source</h4>
<p>
Add one of the publicly availble sources to your style.
</p>
<div style={{maxwidth: 500}}>
{tilesetOptions}
</div>
</div>
<div className="maputnik-modal-section">
<h4>Add New Source</h4>
<p>Add a new source to your style. You can only choose the source type and id at creation time!</p>
<AddSource
onAdd={(sourceId, source) => this.props.onStyleChanged(addSource(mapStyle, sourceId, source))}
/>
</div>
</Modal>
}

View File

@@ -1,4 +1,5 @@
import React from 'react'
import GlSpec from 'mapbox-gl-style-spec/reference/latest.js'
import InputBlock from '../inputs/InputBlock'
import StringInput from '../inputs/StringInput'
import NumberInput from '../inputs/NumberInput'
@@ -6,11 +7,11 @@ import NumberInput from '../inputs/NumberInput'
class TileJSONSourceEditor extends React.Component {
static propTypes = {
source: React.PropTypes.object.isRequired,
onChange: React.PropTypes.func,
onChange: React.PropTypes.func.isRequired,
}
render() {
return <InputBlock label={"TileJSON URL"}>
return <InputBlock label={"TileJSON URL"} doc={GlSpec.source_tile.url.doc}>
<StringInput
value={this.props.source.url}
onChange={url => this.props.onChange({
@@ -25,16 +26,26 @@ class TileJSONSourceEditor extends React.Component {
class TileURLSourceEditor extends React.Component {
static propTypes = {
source: React.PropTypes.object.isRequired,
onChange: React.PropTypes.func,
onChange: React.PropTypes.func.isRequired,
}
changeTileUrl(idx, value) {
const tiles = this.props.source.tiles.slice(0)
tiles[idx] = value
this.props.onChange({
...this.props.source,
tiles: tiles
})
}
renderTileUrls() {
const prefix = ['1st', '2nd', '3rd', '4th', '5th', '6th', '7th']
const tiles = this.props.source.tiles || []
return tiles.map((tileUrl, tileIndex) => {
return <InputBlock key={tileIndex} label={prefix[tileIndex] + " Tile URL"}>
return <InputBlock key={tileIndex} label={prefix[tileIndex] + " Tile URL"} doc={GlSpec.source_tile.tiles.doc}>
<StringInput
value={tileUrl}
onChange={this.changeTileUrl.bind(this, tileIndex)}
/>
</InputBlock>
})
@@ -43,21 +54,21 @@ class TileURLSourceEditor extends React.Component {
render() {
return <div>
{this.renderTileUrls()}
<InputBlock label={"Min Zoom"}>
<InputBlock label={"Min Zoom"} doc={GlSpec.source_tile.minzoom.doc}>
<NumberInput
value={this.props.source.minZoom}
onChange={minZoom => this.props.onChange({
value={this.props.source.minzoom || 0}
onChange={minzoom => this.props.onChange({
...this.props.source,
minZoom: minZoom
minzoom: minzoom
})}
/>
</InputBlock>
<InputBlock label={"Max Zoom"}>
<InputBlock label={"Max Zoom"} doc={GlSpec.source_tile.maxzoom.doc}>
<NumberInput
value={this.props.source.maxZoom}
onChange={maxZoom => this.props.onChange({
value={this.props.source.maxzoom || 22}
onChange={maxzoom => this.props.onChange({
...this.props.source,
maxZoom: maxZoom
maxzoom: maxzoom
})}
/>
</InputBlock>
@@ -69,11 +80,11 @@ class TileURLSourceEditor extends React.Component {
class GeoJSONSourceEditor extends React.Component {
static propTypes = {
source: React.PropTypes.object.isRequired,
onChange: React.PropTypes.func,
onChange: React.PropTypes.func.isRequired,
}
render() {
return <InputBlock label={"GeoJSON Data"}>
return <InputBlock label={"GeoJSON Data"} doc={GlSpec.source_geojson.data.doc}>
<StringInput
value={this.props.source.data}
onChange={data => this.props.onChange({
@@ -99,8 +110,10 @@ class SourceTypeEditor extends React.Component {
}
switch(this.props.mode) {
case 'geojson': return <GeoJSONSourceEditor {...commonProps} />
case 'tilejson': return <TileJSONSourceEditor {...commonProps} />
case 'tilexyz': return <TileURLSourceEditor {...commonProps} />
case 'tilejson_vector': return <TileJSONSourceEditor {...commonProps} />
case 'tilexyz_vector': return <TileURLSourceEditor {...commonProps} />
case 'tilejson_raster': return <TileJSONSourceEditor {...commonProps} />
case 'tilexyz_raster': return <TileURLSourceEditor {...commonProps} />
default: return null
}
}

View File

@@ -1,29 +0,0 @@
const baseColors = {
black: '#1c1f24',
gray: '#26282e',
midgray: '#36383e',
lowgray: '#8e8e8e',
white: '#fff',
blue: '#00d9f7',
green: '#B4C7AD',
orange: '#fb3',
red: '#cf4a4a',
}
const themeColors = {
primary: baseColors.gray,
secondary: baseColors.midgray,
default: baseColors.gray,
info: baseColors.blue,
success: baseColors.green,
warning: baseColors.orange,
error: baseColors.red
}
const colors = {
...baseColors,
...themeColors
}
export default colors

View 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": []
}

View File

@@ -1,35 +0,0 @@
[
"Metropolis Black Italic",
"Metropolis Black",
"Metropolis Bold Italic",
"Metropolis Bold",
"Metropolis Extra Bold Italic",
"Metropolis Extra Bold",
"Metropolis Extra Light Italic",
"Metropolis Extra Light",
"Metropolis Light Italic",
"Metropolis Light",
"Metropolis Medium Italic",
"Metropolis Medium",
"Metropolis Regular Italic",
"Metropolis Regular",
"Metropolis Semi Bold Italic",
"Metropolis Semi Bold",
"Metropolis Thin Italic",
"Metropolis Thin",
"Open Sans Bold Italic",
"Open Sans Bold",
"Open Sans Extra Bold Italic",
"Open Sans Extra Bold",
"Open Sans Italic",
"Open Sans Light Italic",
"Open Sans Light",
"Open Sans Regular",
"Open Sans Semibold Italic",
"Open Sans Semibold",
"Klokantech Noto Sans Bold",
"Klokantech Noto Sans CJK Bold",
"Klokantech Noto Sans CJK Regular",
"Klokantech Noto Sans Italic",
"Klokantech Noto Sans Regular"
]

View File

@@ -1,51 +0,0 @@
import colors from './colors'
import { margins, fontSizes } from './scales'
const base = {
display: 'inline-block',
boxSizing: 'border-box',
fontSize: fontSizes[5],
lineHeight: 2,
paddingLeft: 5,
paddingRight: 5,
}
const label = {
...base,
padding: null,
color: colors.lowgray,
userSelect: 'none',
}
const property = {
display: 'block',
margin: margins[2],
}
const input = {
...base,
border: 'none',
backgroundColor: colors.gray,
color: colors.lowgray,
}
const checkbox = {
...base,
border: '1px solid rgb(36, 36, 36)',
backgroundColor: colors.gray,
color: colors.lowgray,
}
const select = {
...input,
height: '2.15em',
}
export default {
base,
label,
select,
input,
property,
checkbox,
}

View File

@@ -2,15 +2,7 @@
"line": {
"groups": [
{
"title": "Settings",
"type": "settings"
},
{
"title": "Source",
"type": "source"
},
{
"title": "Paint",
"title": "Paint properties",
"type": "properties",
"fields": [
"line-opacity",
@@ -18,66 +10,42 @@
"line-width",
"line-offset",
"line-blur",
"line-pattern"
]
},
{
"title": "Secondary",
"type": "properties",
"fields": [
"line-dasharray",
"line-pattern",
"line-translate",
"line-translate-anchor",
"line-cap",
"line-join",
"line-miter-limit",
"line-round-limit",
"line-dasharray",
"line-gap-width"
]
},
{
"title": "JSON",
"type": "jsoneditor"
"title": "Layout properties",
"type": "properties",
"fields": [
"line-cap",
"line-join",
"line-miter-limit",
"line-round-limit"
]
}
]
},
"background": {
"groups": [
{
"title": "Settings",
"type": "settings"
},
{
"title": "Source",
"type": "source"
},
{
"title": "Basic",
"title": "Paint properties",
"type": "properties",
"fields": [
"background-color",
"background-pattern",
"background-opacity"
]
},
{
"title": "JSON",
"type": "jsoneditor"
}
]
},
"fill": {
"groups": [
{
"title": "Settings",
"type": "settings"
},
{
"title": "Source",
"type": "source"
},
{
"title": "Basic",
"title": "Paint properties",
"type": "properties",
"fields": [
"fill-opacity",
@@ -88,25 +56,13 @@
"fill-translate",
"fill-translate-anchor"
]
},
{
"title": "JSON",
"type": "jsoneditor"
}
]
},
"fill-extrusion": {
"groups": [
{
"title": "Settings",
"type": "settings"
},
{
"title": "Source",
"type": "source"
},
{
"title": "Basic",
"title": "Paint properties",
"type": "properties",
"fields": [
"fill-extrusion-opacity",
@@ -117,99 +73,51 @@
"fill-extrusion-height",
"fill-extrusion-base"
]
},
{
"title": "JSON",
"type": "jsoneditor"
}
]
},
"circle": {
"groups": [
{
"title": "Settings",
"type": "settings"
},
{
"title": "Source",
"type": "source"
},
{
"title": "Basic",
"title": "Paint properties",
"type": "properties",
"fields": [
"circle-color",
"circle-opacity",
"circle-stroke-color",
"circle-stroke-opacity",
"circle-blur"
]
},
{
"title": "Scale",
"type": "properties",
"fields": [
"circle-blur",
"circle-radius",
"circle-stroke-width",
"circle-pitch-scale"
]
},
{
"title": "Position",
"type": "properties",
"fields": [
"circle-pitch-scale",
"circle-translate",
"circle-translate-anchor"
]
},
{
"title": "JSON",
"type": "jsoneditor"
}
]
},
"symbol": {
"groups": [
{
"title": "Settings",
"type": "settings"
},
{
"title": "Source",
"type": "source"
},
{
"title": "Basic",
"type": "properties",
"fields": [
"text-field",
"text-font",
"text-color",
"text-size",
"text-line-height",
"text-halo-color",
"text-halo-width",
"text-halo-blur"
]
},
{
"title": "Placement",
"title": "General layout properties",
"type": "properties",
"fields": [
"symbol-placement",
"symbol-spacing",
"text-padding",
"symbol-avoid-edges",
"text-allow-overlap",
"text-ignore-placement",
"text-translate",
"text-translate-anchor"
"symbol-avoid-edges"
]
},
{
"title": "Text",
"title": "Text layout properties",
"type": "properties",
"fields": [
"text-field",
"text-font",
"text-size",
"text-line-height",
"text-padding",
"text-allow-overlap",
"text-ignore-placement",
"text-pitch-alignment",
"text-rotation-alignment",
"text-max-width",
@@ -225,7 +133,7 @@
]
},
{
"title": "Icon",
"title": "Icon layout properties",
"type": "properties",
"fields": [
"icon-allow-overlap",
@@ -239,12 +147,51 @@
"icon-rotate",
"icon-padding",
"icon-keep-upright",
"icon-offset"
"icon-offset"
]
},
{
"title": "JSON",
"type": "jsoneditor"
"title": "Text paint properties",
"type": "properties",
"fields": [
"text-color",
"text-opacity",
"text-halo-color",
"text-halo-width",
"text-halo-blur",
"text-translate",
"text-translate-anchor"
]
},
{
"title": "Icon paint properties",
"type": "properties",
"fields": [
"icon-color",
"icon-opacity",
"icon-halo-color",
"icon-halo-width",
"icon-halo-blur",
"icon-translate",
"icon-translate-anchor"
]
}
]
},
"raster": {
"groups": [
{
"title": "Paint properties",
"type": "properties",
"fields": [
"raster-opacity",
"raster-hue-rotate",
"raster-brightness-min",
"raster-brightness-max",
"raster-saturation",
"raster-contrast",
"raster-fade-duration"
]
}
]
}

View File

@@ -1,2 +0,0 @@
export const margins = [3, 5, 10, 30, 40]
export const fontSizes = [24, 20, 18, 16, 14, 12]

View File

@@ -2,31 +2,61 @@
{
"id": "klokantech-basic",
"title": "Klokantech Basic",
"url": "https://rawgit.com/openmaptiles/klokantech-basic-gl-style/gh-pages/style-cdn.json",
"thumbnail": "https://camo.githubusercontent.com/5cf548fdb9fc606f4a452d14fd2a7a959155fd40/68747470733a2f2f6170692e6d6170626f782e636f6d2f7374796c65732f76312f6d6f7267656e6b61666665652f63697578757465726630316135326971716f366b6f6c776b312f7374617469632f382e3534303538372c34372e3337303535352c31342e30382c302e30302c302e30302f363030783430303f6163636573735f746f6b656e3d706b2e65794a31496a6f69625739795a3256756132466d5a6d566c4969776959534936496a497a636d4e304e6c6b6966512e304c52544e6743632d656e76743964354d7a52373577"
"url": "https://rawgit.com/openmaptiles/klokantech-basic-gl-style/master/style.json",
"thumbnail": "http://maputnik.com/thumbnails/klokantech-basic.png"
},
{
"id": "dark-matter",
"title": "Dark Matter",
"url": "https://rawgit.com/openmaptiles/dark-matter-gl-style/gh-pages/style-cdn.json",
"thumbnail": "https://camo.githubusercontent.com/b73c515d633d2be7368e8e29e3c23e14117fd21b/68747470733a2f2f6170692e6d6170626f782e636f6d2f7374796c65732f76312f6d6f7267656e6b61666665652f6369757878356e37683031396c326870626e396c6970726d6e2f7374617469632f382e3631393138342c34372e3333363230332c392e30372c302e30302c302e30302f363030783430303f6163636573735f746f6b656e3d706b2e65794a31496a6f69625739795a3256756132466d5a6d566c4969776959534936496a497a636d4e304e6c6b6966512e304c52544e6743632d656e76743964354d7a52373577"
"url": "https://rawgit.com/openmaptiles/dark-matter-gl-style/master/style.json",
"thumbnail": "http://maputnik.com/thumbnails/dark-matter.png"
},
{
"id": "positron",
"title": "Positron",
"url": "https://rawgit.com/openmaptiles/positron-gl-style/gh-pages/style-cdn.json",
"thumbnail": "https://camo.githubusercontent.com/0dd866e3fa7b21ada87da69082eac6801e16ec99/68747470733a2f2f6170692e6d6170626f782e636f6d2f7374796c65732f76312f6d6f7267656e6b61666665652f63697578756e37736530313976326a6c387162326a743374662f7374617469632f382e3631393138342c34372e3333363230332c392e30372c302e30302c302e30302f363030783430303f6163636573735f746f6b656e3d706b2e65794a31496a6f69625739795a3256756132466d5a6d566c4969776959534936496a497a636d4e304e6c6b6966512e304c52544e6743632d656e76743964354d7a52373577"
"url": "https://rawgit.com/openmaptiles/positron-gl-style/master/style.json",
"thumbnail": "http://maputnik.com/thumbnails/positron.png"
},
{
"id": "osm-bright",
"title": "OSM Bright",
"url": "https://rawgit.com/openmaptiles/osm-bright-gl-style/gh-pages/style-cdn.json",
"thumbnail": "https://camo.githubusercontent.com/a15e23ab59202c56502e57cde963cb7772ed3bb1/68747470733a2f2f6170692e6d6170626f782e636f6d2f7374796c65732f76312f6f70656e6d617074696c65732f63697736637a7a326e30303234326b6d673668773230626f782f7374617469632f382e3534303538372c34372e3337303535352c31342e30382c302e30302c302e30302f363030783430303f6163636573735f746f6b656e3d706b2e65794a31496a6f696233426c626d3168634852706247567a4969776959534936496d4e70646e593365544a785a7a41774d474d796233427064574a6d616a63784e7a636966512e685031427863786c644968616b4d6350534a4c513151"
"url": "https://rawgit.com/openmaptiles/osm-bright-gl-style/master/style.json",
"thumbnail": "http://maputnik.com/thumbnails/osm-bright.png"
},
{
"id": "fiord-color",
"title": "Fiord Color",
"url": "https://rawgit.com/openmaptiles/fiord-color-gl-style/gh-pages/style-cdn.json",
"thumbnail": "https://camo.githubusercontent.com/605f2edc30e413b37d16a6ca1d500f265725d76d/68747470733a2f2f6170692e6d6170626f782e636f6d2f7374796c65732f76312f6f70656e6d617074696c65732f6369776775693378353030317732706e7668633063327767302f7374617469632f31302e3938373235382c34362e3435333135302c332e30322c302e30302c302e30302f363030783430303f6163636573735f746f6b656e3d706b2e65794a31496a6f696233426c626d3168634852706247567a4969776959534936496d4e70646e593365544a785a7a41774d474d796233427064574a6d616a63784e7a636966512e685031427863786c644968616b4d6350534a4c513151"
"id": "osm-liberty",
"title": "OSM Liberty",
"url": "https://rawgit.com/lukasmartinelli/osm-liberty/gh-pages/style.json",
"thumbnail": "https://cdn.rawgit.com/lukasmartinelli/osm-liberty/gh-pages/thumbnail.png"
},
{
"id": "empty-style",
"title": "Empty Style",
"url": "https://rawgit.com/maputnik/editor/master/src/config/empty-style.json",
"thumbnail": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAYAAAAECAQAAAAHDYbIAAAAEUlEQVR42mP8/58BDhiJ4wAA974H/U5Xe1oAAAAASUVORK5CYII="
},
{
"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"
}
]

View File

@@ -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

View File

@@ -4,6 +4,11 @@
"url": "mapbox://mapbox.mapbox-streets-v7",
"title": "Mapbox Streets"
},
"openmaptiles": {
"type": "vector",
"url": "https://free.tilehosting.com/data/v3.json?key={key}",
"title": "OpenMapTiles"
},
"tilezen": {
"type": "vector",
"tiles": [
@@ -12,15 +17,5 @@
"minZoom": 0,
"maxZoom": 15,
"title": "Mapzen Vector Tile Service"
},
"openmaptiles": {
"type": "vector",
"url": "https://free.tilehosting.com/data/v3.json?key=25ItXg7aI5wurYDtttD",
"title": "OpenMapTiles CDN"
},
"naturalearth-airports": {
"type": "geojson",
"data": "https://d2ad6b4ur7yvpq.cloudfront.net/naturalearth-3.3.0/ne_10m_airports.geojson",
"title": "NaturalEarth Airports GeoJSON"
}
}

4
src/config/tokens.json Normal file
View File

@@ -0,0 +1,4 @@
{
"mapbox": "pk.eyJ1IjoibW9yZ2Vua2FmZmVlIiwiYSI6ImNpeHJmNXNmZTAwNHIycXBid2NqdTJibjMifQ.Dv1-GDpTWi0NP6xW9Fct1w",
"openmaptiles": "Og58UhhtiiTaLVlPtPgs"
}

BIN
src/fonts/Roboto-Medium.ttf Normal file

Binary file not shown.

View File

@@ -1,34 +0,0 @@
@font-face {
font-family: 'Roboto';
src: url('./fonts/Roboto-Regular.ttf') format('truetype');
font-weight: normal;
font-style: normal;
}
html {
background-color: rgb(28, 31, 36);
}
.chrome-picker {
background-color: #1c1f24 !important;
font-family: inherit !important;
}
.chrome-picker input {
background-color: rgb(38, 40, 46) !important;
color: rgb(142, 142, 142) !important;
box-shadow: none !important;
}
::-webkit-scrollbar {
background-color: #26282e;
width: 5px;
}
::-webkit-scrollbar-thumb {
border-radius: 6px;
-webkit-box-shadow: inset 0 0 6px rgba(0,0,0,.3);
background-color: #40444e;
padding-left: 2px;
padding-right: 2px;
}

View File

@@ -2,7 +2,7 @@ import React from 'react';
import ReactDOM from 'react-dom';
import './favicon.ico'
import './index.css'
import './styles/index.scss'
import App from './components/App';
ReactDOM.render(<App/>, document.querySelector("#app"));

View File

@@ -1,40 +1,66 @@
import request from 'request'
import style from './style.js'
import ReconnectingWebSocket from 'reconnecting-websocket'
const host = 'localhost'
const port = '8000'
const localUrl = `http://${host}:${port}`
const websocketUrl = `ws://${host}:${port}/ws`
export class ApiStyleStore {
supported(cb) {
request('http://localhost:8000/styles', (error, response, body) => {
cb(error === undefined)
constructor(opts) {
this.onLocalStyleChange = opts.onLocalStyleChange || (() => {})
}
init(cb) {
request(localUrl + '/styles', (error, response, body) => {
if (!error && body && response.statusCode == 200) {
const styleIds = JSON.parse(body)
this.latestStyleId = styleIds[0]
this.notifyLocalChanges()
cb(null)
} else {
cb(new Error('Can not connect to style API'))
}
})
}
notifyLocalChanges() {
const connection = new ReconnectingWebSocket(websocketUrl)
connection.onmessage = e => {
if(!e.data) return
console.log('Received style update from API')
let parsedStyle = style.emptyStyle
try {
parsedStyle = JSON.parse(e.data)
} catch(err) {
console.error(err)
}
const updatedStyle = style.ensureStyleValidity(parsedStyle)
this.onLocalStyleChange(updatedStyle)
}
}
latestStyle(cb) {
if(this.latestStyleId) {
request('http://localhost:8000/styles/' + this.latestStyleId, (error, response, body) => {
cb(JSON.parse(body))
request(localUrl + '/styles/' + this.latestStyleId, (error, response, body) => {
cb(style.ensureStyleValidity(JSON.parse(body)))
})
} else {
request('http://localhost:8000/styles', (error, response, body) => {
if (!error && response.statusCode == 200) {
const styleIds = JSON.parse(body);
this.latestStyleId = styleIds[0];
request('http://localhost:8000/styles/' + this.latestStyleId, (error, response, body) => {
cb(style.fromJSON(JSON.parse(body)))
})
}
})
throw new Error('No latest style available. You need to init the api backend first.')
}
}
// Save current style replacing previous version
save(mapStyle) {
const id = mapStyle.get('id')
const id = mapStyle.id
request.put({
url: 'http://localhost:8000/styles/' + id,
url: localUrl + '/styles/' + id,
json: true,
body: style.toJSON(mapStyle)
body: mapStyle
}, (error, response, body) => {
console.log('Saved style');
if(error) console.error(error)
})
return mapStyle
}

6
src/libs/filterops.js Normal file
View 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
View 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
}

37
src/libs/metadata.js Normal file
View 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)))
}

27
src/libs/source.js Normal file
View 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
}

View File

@@ -1,6 +1,7 @@
import React from 'react';
import spec from 'mapbox-gl-style-spec/reference/latest.min.js'
import derefLayers from 'mapbox-gl-style-spec/lib/deref'
import tokens from '../config/tokens.json'
// Empty style is always used if no style could be restored or fetched
const emptyStyle = ensureStyleValidity({
@@ -19,10 +20,18 @@ function ensureHasId(style) {
return style
}
function ensureHasTimestamp(style) {
if('created' in style) return style
style.created = new Date().toJSON()
return style
function ensureHasNoInteractive(style) {
const changedLayers = style.layers.map(layer => {
const changedLayer = { ...layer }
delete changedLayer.interactive
return changedLayer
})
const nonInteractiveStyle = {
...style,
layers: changedLayers
}
return nonInteractiveStyle
}
function ensureHasNoRefs(style) {
@@ -34,7 +43,7 @@ function ensureHasNoRefs(style) {
}
function ensureStyleValidity(style) {
return ensureHasNoRefs(ensureHasId(ensureHasTimestamp(style)))
return ensureHasNoInteractive(ensureHasNoRefs(ensureHasId(style)))
}
function indexOfLayer(layers, layerId) {
@@ -46,9 +55,32 @@ function indexOfLayer(layers, layerId) {
return null
}
function replaceAccessToken(mapStyle) {
const omtSource = mapStyle.sources.openmaptiles
if(!omtSource) return mapStyle
const metadata = mapStyle.metadata || {}
const accessToken = metadata['maputnik:openmaptiles_access_token'] || tokens.openmaptiles
const changedSources = {
...mapStyle.sources,
openmaptiles: {
...omtSource,
url: omtSource.url.replace('{key}', accessToken)
}
}
const changedStyle = {
...mapStyle,
glyphs: mapStyle.glyphs.replace('{key}', accessToken),
sources: changedSources
}
return changedStyle
}
export default {
ensureStyleValidity,
emptyStyle,
indexOfLayer,
generateId,
replaceAccessToken,
}

View File

@@ -1,141 +0,0 @@
import randomColor from 'randomcolor'
import Color from 'color'
function assignVectorLayerColor(layerId) {
let hue = null
if(/water|ocean|lake|sea|river/.test(layerId)) {
hue = 'blue'
}
if(/road|highway|transport/.test(layerId)) {
hue = 'orange'
}
if(/building/.test(layerId)) {
hue = 'yellow'
}
if(/wood|forest|park|landcover|landuse/.test(layerId)) {
hue = 'green'
}
return randomColor({
luminosity: 'bright',
hue: hue,
seed: layerId,
})
}
function circleLayer(source, vectorLayer, color) {
const layer = {
id: `${source}_${vectorLayer}_circle`,
source: source,
type: 'circle',
paint: {
'circle-color': color,
'circle-radius': 2,
},
filter: ["==", "$type", "Point"]
}
if(vectorLayer) {
layer['source-layer'] = vectorLayer
}
return layer
}
function polygonLayer(source, vectorLayer, color, fillColor) {
const layer = {
id: `${source}_${vectorLayer}_polygon`,
source: source,
type: 'fill',
paint: {
'fill-color': fillColor,
'fill-antialias': true,
'fill-outline-color': color,
},
filter: ["==", "$type", "Polygon"]
}
if(vectorLayer) {
layer['source-layer'] = vectorLayer
}
return layer
}
function lineLayer(source, vectorLayer, color) {
const layer = {
id: `${source}_${vectorLayer}_line`,
source: source,
layout: {
'line-join': 'round',
'line-cap': 'round'
},
type: 'line',
paint: {
'line-color': color,
},
filter: ["==", "$type", "LineString"]
}
if(vectorLayer) {
layer['source-layer'] = vectorLayer
}
return layer
}
export function colorHighlightedLayer(layer) {
if(!layer || layer.type === 'background' || layer.type === 'raster') return null
function changeLayer(l) {
if(layer.filter) {
l.filter = layer.filter
} else {
delete l['filter']
}
l.id = l.id + '_highlight'
return l
}
const color = assignVectorLayerColor(layer.id)
const layers = []
if(layer.type === "fill" || layer.type === 'fill-extrusion') {
return changeLayer(polygonLayer(layer.source, layer['source-layer'], color, Color(color).alpha(0.2).string()))
}
if(layer.type === "symbol" || layer.type === 'circle') {
return changeLayer(circleLayer(layer.source, layer['source-layer'], color))
}
if(layer.type === 'line') {
return changeLayer(lineLayer(layer.source, layer['source-layer'], color))
}
return null
}
export function generateColoredLayers(sources) {
const polyLayers = []
const circleLayers = []
const lineLayers = []
Object.keys(sources).forEach(sourceId => {
const layers = sources[sourceId]
// Deal with GeoJSON sources that do not have any source layers
if(!layers) {
const color = Color(assignVectorLayerColor(sourceId))
circleLayers.push(circleLayer(sourceId, null, color.alpha(0.3).string()))
lineLayers.push(lineLayer(sourceId, null, color.alpha(0.3).string()))
polyLayers.push(polygonLayer(sourceId, null, color.alpha(0.2).string(), color.alpha(0.05).string()))
return
}
layers.forEach(layerId => {
const color = Color(assignVectorLayerColor(layerId))
circleLayers.push(circleLayer(sourceId, layerId, color.alpha(0.3).string()))
lineLayers.push(lineLayer(sourceId, layerId, color.alpha(0.3).string()))
polyLayers.push(polygonLayer(sourceId, layerId, color.alpha(0.2).string(), color.alpha(0.05).string()))
})
})
return polyLayers.concat(lineLayers).concat(circleLayers)
}

View File

@@ -1,5 +1,6 @@
import { colorizeLayers } from './style.js'
import style from './style.js'
import { loadStyleUrl } from './urlopen'
import publicSources from '../config/styles.json'
import request from 'request'
@@ -14,18 +15,7 @@ const defaultStyleUrl = publicSources[0].url
// Fetch a default style via URL and return it or a fallback style via callback
export function loadDefaultStyle(cb) {
console.log('Falling back to default style')
request({
url: defaultStyleUrl,
withCredentials: false,
}, (error, response, body) => {
if (!error && response.statusCode == 200) {
cb(style.ensureStyleValidity(JSON.parse(body)))
} else {
console.warn('Could not fetch default style', styleUrl)
cb(style.emptyStyle)
}
})
loadStyleUrl(defaultStyleUrl, cb)
}
// Return style ids and dates of all styles stored in local storage
@@ -69,8 +59,8 @@ export class StyleStore {
this.mapStyles = loadStoredStyles()
}
supported(cb) {
cb(window.localStorage !== undefined)
init(cb) {
cb(null)
}
// Delete entire style history

42
src/libs/urlopen.js Normal file
View 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)
}
})
}

View File

@@ -49,3 +49,12 @@
.mapboxgl-ctrl-icon.mapboxgl-ctrl-compass > span.arrow {
background-image: url("data:image/svg+xml;charset=utf8,%3Csvg%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%20viewBox%3D%270%200%2020%2020%27%3E%0A%09%3Cpolygon%20fill%3D%27%238e8e8e%27%20points%3D%276%2C9%2010%2C1%2014%2C9%27%2F%3E%0A%09%3Cpolygon%20fill%3D%27%23CCCCCC%27%20points%3D%276%2C11%2010%2C19%2014%2C11%20%27%2F%3E%0A%3C%2Fsvg%3E")
}
.mapboxgl-ctrl-inspect {
background-image: url('data:image/svg+xml;charset=utf8,<svg%20xmlns="http://www.w3.org/2000/svg"%20fill="#8e8e8e%22%20preserveAspectRatio=%22xMidYMid%20meet%22%20viewBox=%22-10%20-10%2060%2060%22%3E%3Cg%3E%3Cpath%20d=%22m15%2021.6q0-2%201.5-3.5t3.5-1.5%203.5%201.5%201.5%203.5-1.5%203.6-3.5%201.4-3.5-1.4-1.5-3.6z%20m18.4%2011.1l-6.4-6.5q1.4-2.1%201.4-4.6%200-3.4-2.5-5.8t-5.9-2.4-5.9%202.4-2.5%205.8%202.5%205.9%205.9%202.5q2.4%200%204.6-1.4l7.4%207.4q-0.9%200.6-2%200.6h-20q-1.3%200-2.3-0.9t-1.1-2.3l0.1-26.8q0-1.3%201-2.3t2.3-0.9h13.4l10%2010v19.3z%22%3E%3C/path%3E%3C/g%3E%3C/svg%3E');
}
.mapboxgl-ctrl-map {
background-image: url('data:image/svg+xml;charset=utf8,<svg%20xmlns="http://www.w3.org/2000/svg"%20fill="#8e8e8e%22%20viewBox=%22-10%20-10%2060%2060%22%20preserveAspectRatio=%22xMidYMid%20meet%22%3E%3Cg%3E%3Cpath%20d=%22m25%2031.640000000000004v-19.766666666666673l-10-3.511666666666663v19.766666666666666z%20m9.140000000000008-26.640000000000004q0.8599999999999923%200%200.8599999999999923%200.8600000000000003v25.156666666666666q0%200.625-0.625%200.783333333333335l-9.375%203.1999999999999993-10-3.5133333333333354-8.906666666666668%203.4383333333333326-0.2333333333333334%200.07833333333333314q-0.8616666666666664%200-0.8616666666666664-0.8599999999999994v-25.156666666666663q0-0.625%200.6233333333333331-0.7833333333333332l9.378333333333334-3.198333333333334%2010%203.5133333333333336%208.905000000000001-3.4383333333333344z%22%3E%3C/path%3E%3C/g%3E%3C/svg%3E');
}

78
src/styles/_base.scss Normal file
View 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
View 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;
}
}

5
src/styles/_export.scss Normal file
View File

@@ -0,0 +1,5 @@
.maputnik-export-gist {
label.maputnik-checkbox-wrapper {
display: inline-block;
}
}

View File

@@ -0,0 +1,96 @@
.maputnik-filter-editor-wrapper {
padding: $margin-3;
}
.maputnik-filter-editor {
color: $color-lowgray;
}
.maputnik-filter-editor-property {
display: inline-block;
width: '22%';
}
.maputnik-filter-editor-operator {
display: inline-block;
width: 19%;
margin-left: 2%;
}
.maputnik-filter-editor-args {
display: inline-block;
width: 54%;
margin-left: 2%;
}
.maputnik-filter-editor-compound-select {
margin-bottom: $margin-2;
.maputnik-doc-wrapper {
width: 50%;
}
.maputnik-select {
display: inline-block;
width: 50%;
}
}
.maputnik-filter-editor-unsupported {
color: $color-midgray;
}
.maputnik-filter-editor {
@extend .clearfix;
}
.maputnik-add-filter {
display: inline-block;
float: right;
margin-top: $margin-3;
}
.maputnik-delete-filter {
@extend .maputnik-icon-button;
}
.maputnik-filter-editor-block-action {
margin-top: $margin-2;
margin-bottom: $margin-2;
}
.maputnik-filter-editor-block-action {
display: inline-block;
width: 6%;
margin-right: 1.5%;
}
.maputnik-filter-editor-block-content {
display: inline-block;
width: 92.5%;
}
.maputnik-filter-editor-property {
display: inline-block;
width: 25%;
}
.maputnik-filter-editor-operator {
display: inline-block;
width: 17%;
.maputnik-select {
width: 100%;
}
}
.maputnik-filter-editor-args {
display: inline-block;
width: 54%;
.maputnik-string,
.maputnik-number {
width: 100%;
}
}

139
src/styles/_input.scss Normal file
View File

@@ -0,0 +1,139 @@
//INPUT
.maputnik-input {
height: 24px;
width: 100%;
display: block;
box-sizing: border-box;
font-size: $font-size-6;
line-height: 2;
padding-left: $margin-2;
padding-right: $margin-2;
border: none;
background-color: $color-gray;
color: lighten($color-lowgray, 12);
}
.maputnik-string {
@extend .maputnik-input;
}
.maputnik-number {
@extend .maputnik-input;
}
//COLOR PICKER
.maputnik-color {
@extend .maputnik-input;
height: 26px;
}
.maputnik-color-wrapper {
position: relative;
display: inline;
}
// ARRAY
.maputnik-array {
> * {
margin-bottom: $margin-3;
}
}
// SELECT
.maputnik-select {
@extend .maputnik-input;
height: 24px;
}
// MULTIBUTTON
.maputnik-multibutton {
padding: 0;
.maputnik-button {
margin-right: $margin-1;
}
}
.maputnik-button-selected {
background-color: lighten($color-midgray, 12);
outline: 1px $color-white;
color: white;
}
// CHECKBOX
.maputnik-checkbox {
position: absolute;
z-index: -1;
opacity: 0;
width: 50%;
&-wrapper {
@extend .maputnik-input;
padding-left: 0;
padding-right: 0;
position: relative;
text-align: center;
vertical-align: middle;
cursor: pointer;
max-width: 24px;
}
&-box {
display: inline-block;
text-align: center;
height: 24px;
width: 24px;
margin-right: $margin-2;
background-color: $color-gray;
border-radius: 2px;
border-style: solid;
border-width: 2px;
border-color: $color-gray;
transition: background-color 0.1s ease-out;
}
&-icon {
width: 50%;
height: 50%;
margin-top: 1px;
fill: $color-lowgray;
}
}
// AUTOCOMPLETE
.maputnik-autocomplete {
&-menu {
border: none;
padding: 2px 0;
position: fixed;
overflow: auto;
max-height: 50%;
background: $color-gray;
z-index: 3;
}
&-menu-item {
user-select: none;
color: $color-lowgray;
cursor: default;
padding: $margin-1;
font-size: $font-size-6;
z-index: 3;
background: $color-gray;
}
&-menu-item-selected {
background: $color-midgray;
}
}
// FONT
.maputnik-font {
.maputnik-autocomplete:not(:last-child) {
margin-bottom: $margin-3;
}
}

166
src/styles/_layer.scss Normal file
View File

@@ -0,0 +1,166 @@
// LAYER LIST
.maputnik-layer-list {
&-header {
padding: $margin-2;
@include flex-row;
> * {
vertical-align: middle;
margin-bottom: 0;
}
}
&-header-title {
font-size: $font-size-5;
color: $color-white;
font-weight: bold;
line-height: 1.3;
}
&-container {
padding: 0;
margin: 0;
padding-bottom: $margin-5;
}
&-item {
font-weight: 400;
color: $color-lowgray;
font-size: $font-size-6;
border-width: 0 0 1px;
border-style: solid;
border-color: lighten($color-black, 0.1);
user-select: none;
list-style: none;
z-index: 2000;
cursor: pointer;
position: relative;
padding: 5px 10px;
line-height: 1.3;
max-height: 50px;
opacity: 1;
-webkit-transition: opacity 600ms, visibility 600ms;
transition: opacity 600ms, visibility 600ms;
@include flex-row;
}
&-icon-action svg {
fill: $color-black;
}
.maputnik-layer-list-item:hover,
.maputnik-layer-list-item-selected {
background-color: lighten($color-black, 2);
.maputnik-layer-list-icon-action svg {
fill: darken($color-lowgray, 0.5);
&:hover {
fill: $color-white;
}
}
}
&-item-selected {
color: $color-white;
}
&-item-collapsed {
position: absolute;
max-height: 0;
overflow: hidden;
padding: 0;
opacity: 0;
visibility: hidden;
}
&-item-group-last {
border-bottom: 2px solid $color-gray;
}
&-item-id {
width: 115px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
&-group-header {
font-size: $font-size-6;
color: $color-lowgray;
background-color: lighten($color-black, 2);
cursor: pointer;
user-select: none;
padding: $margin-2;
@include flex-row;
svg {
width: 14px;
height: 14px;
}
}
&-group-title {
vertical-align: middle;
}
&-group-content {
margin-left: $margin-3;
}
}
// FILTER EDITOR
.maputnik-layer-editor-group {
font-weight: bold;
font-size: $font-size-5;
background-color: lighten($color-black, 2);
color: $color-white;
cursor: pointer;
user-select: none;
padding: $margin-2;
line-height: 20px;
@include flex-row;
&:hover {
background-color: $color-gray;
}
}
// PROPERTY
.maputnik-default-property {
.maputnik-input-block-label {
color: darken($color-lowgray, 25%);
}
.maputnik-string,
.maputnik-number,
.maputnik-color,
.maputnik-select,
.maputnik-checkbox-wrapper {
background-color: darken($color-gray, 2%);
color: darken($color-lowgray, 25%);
}
.maputnik-make-zoom-function svg {
opacity: 0.4;
}
.maputnik-multibutton .maputnik-button {
background-color: darken($color-midgray, 10%);
color: darken($color-lowgray, 25%);
&:hover {
background-color: lighten($color-midgray, 12);
color: $color-white;
}
}
.maputnik-multibutton .maputnik-button-selected {
background-color: darken($color-midgray, 2%);
color: $color-lowgray;
}
}

49
src/styles/_layout.scss Normal file
View File

@@ -0,0 +1,49 @@
//SCROLLING
.maputnik-scroll-container {
overflow-x: visible;
overflow-y: scroll;
bottom: 0;
left: 0;
right: 0;
top: 1px;
position: absolute;
}
//APP LAYOUT
.maputnik-layout {
font-family: $font-family;
color: $color-white;
&-list {
position: fixed;
bottom: 0;
height: calc(100% - $toolbar-height);
top: 40px;
left: 0;
z-index: 3;
width: 200px;
overflow: hidden;
background-color: $color-black;
}
&-drawer {
position: fixed;
bottom: 0;
height: calc(100% - $toolbar-height);
top: 40px;
left: 200px;
z-index: 1;
width: 350px;
background-color: $color-black;
}
&-bottom {
position: fixed;
height: 50px;
bottom: 0;
left: 550px;
z-index: 1;
width: 100%;
background-color: $color-black;
}
}

14
src/styles/_mixins.scss Normal file
View File

@@ -0,0 +1,14 @@
@mixin vendor-prefix($name, $argument) {
-webkit-#{$name}: #{$argument};
-ms-#{$name}: #{$argument};
-moz-#{$name}: #{$argument};
-o-#{$name}: #{$argument};
#{$name}: #{$argument};
}
@mixin flex-row {
display: flex;
display: -ms-flexbox;
@include vendor-prefix(flex-direction, row);
}

201
src/styles/_modal.scss Normal file
View File

@@ -0,0 +1,201 @@
//MODAL
.maputnik-modal {
min-width: 350px;
max-width: 600px;
background-color: $color-black;
box-shadow: 0 0 5px 0 rgba(0, 0, 0, 0.3);
z-index: 3;
}
.maputnik-modal-section {
padding-top: $margin-3;
padding-bottom: $margin-3;
}
.maputnik-modal-header {
background-color: $color-gray;
padding: $margin-3;
@include flex-row;
}
.maputnik-modal-header-title {
font-size: $font-size-5;
margin: 0;
}
.maputnik-modal-header-toggle {
cursor: pointer;
}
.maputnik-modal-content {
padding: $margin-3;
}
.maputnik-modal-header-space {
@extend .maputnik-space;
}
//OVERLAY
.maputnik-overlay-viewport {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 2;
opacity: 0.875;
background-color: rgb(28, 31, 36);
}
.maputnik-overlay {
top: 0;
right: 0;
bottom: 0;
left: 0;
position: fixed;
align-items: center;
justify-content: center;
@include flex-row;
}
//OPEN MODAL
.maputnik-upload-button {
@extend .maputnik-big-button;
}
.maputnik-style-gallery-container {
max-height: 400px;
overflow-y: scroll;
}
.maputnik-public-style {
vertical-align: top;
margin-top: 10px;
margin-right: 10px;
background-color: $color-gray;
display: inline-block;
width: 180px;
font-size: $font-size-2;
color: $color-lowgray;
}
.maputnik-public-style-button {
background-color: $color-gray;
padding: $margin-3;
display: block;
&:hover {
background-color: $color-midgray;
}
}
.maputnik-public-style-header {
@include flex-row;
}
.maputnik-public-style-thumbnail {
display: block;
margin-top: $margin-2;
width: 100%;
}
.maputnik-add-layer {
@extend .clearfix;
}
//ADD MODAL
.maputnik-add-layer-button {
@extend .maputnik-big-button;
margin-right: $margin-3;
float: right;
display: inline-block;
margin-top: 3;
margin-bottom: $margin-3;
text-align: right;
}
//SOURCE MODAL
.maputnik-public-source {
vertical-align: top;
margin-top: 1.5%;
margin-right: 1.5%;
background-color: $color-gray;
width: 48.5%;
display: inline-block;
}
.maputnik-public-source-select {
padding: $margin-3;
font-size: $font-size-5;
color: $color-lowgray;
background-color: transparent;
@include flex-row;
}
.maputnik-public-source-name {
font-weight: 700;
}
.maputnik-public-source-id {
font-weight: 400;
}
.maputnik-active-source-type-editor {
min-width: 500px;
}
.maputnik-active-source-type-editor-header {
background-color: $color-gray;
color: $color-lowgray;
padding: $margin-2;
@include flex-row;
}
.maputnik-active-source-type-editor-header-id {
font-weight: 700;
line-height: 2;
font-size: $font-size-5;
}
.maputnik-active-source-type-editor-content {
border-color: $color-gray;
border-width: 2px;
border-style: solid;
padding: $margin-2;
}
.maputnik-add-source {
@extend .clearfix;
}
.maputnik-add-source-button {
@extend .maputnik-big-button;
display: inline-block;
margin-top: 0;
margin-right: $margin-3;
float: right;
}
//EXPORT MODAL
.maputnik-export-gist {
font-size: $font-size-6;
.maputnik-input-block {
margin-left: 0;
margin-right: 0;
label {
vertical-align: middle;
}
}
span {
color: $color-lowgray;
}
}

12
src/styles/_picker.scss Normal file
View File

@@ -0,0 +1,12 @@
.chrome-picker {
background-color: #1c1f24 !important;
font-family: inherit !important;
}
.chrome-picker input {
background-color: rgb(38, 40, 46) !important;
color: rgb(142, 142, 142) !important;
box-shadow: none !important;
}

24
src/styles/_popup.scss Normal file
View File

@@ -0,0 +1,24 @@
.maputnik-popup-layer {
display: block;
color: $color-lowgray;
user-select: none;
line-height: 1.2;
padding: $margin-2;
padding-top: $margin-1;
padding-bottom: $margin-1;
}
.maputnik-popup-layer-id {
padding-left: $margin-2;
padding-right: $margin-2;
background-color: $color-midgray;
color: $color-white;
}
.maputnik-feature-property-popup {
.maputnik-input-block {
margin: 0;
margin-left: $margin-2;
margin-top: $margin-2;
}
}

49
src/styles/_reset.scss Normal file
View File

@@ -0,0 +1,49 @@
/* stylelint-disable */
html {
background-color: rgb(28, 31, 36);
}
/* CSS Reset */
html, body, div, span, applet, object, iframe,
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
a, abbr, acronym, address, big, cite, code,
del, dfn, em, img, ins, kbd, q, s, samp,
small, strike, strong, sub, sup, tt, var,
b, u, i, center,
dl, dt, dd, ol, ul, li,
fieldset, form, label, legend,
table, caption, tbody, tfoot, thead, tr, th, td,
article, aside, canvas, details, embed,
figure, figcaption, footer, header, hgroup,
menu, nav, output, ruby, section, summary,
time, mark, audio, video {
margin: 0;
padding: 0;
border: 0;
font-size: 100%;
font: inherit;
vertical-align: baseline;
}
/* HTML5 display-role reset for older browsers */
article, aside, details, figcaption, figure,
footer, header, hgroup, menu, nav, section {
display: block;
}
body {
line-height: 1;
}
ol, ul {
list-style: none;
}
blockquote, q {
quotes: none;
}
blockquote:before, blockquote:after,
q:before, q:after {
content: '';
content: none;
}
table {
border-collapse: collapse;
border-spacing: 0;
}

View File

@@ -0,0 +1,12 @@
::-webkit-scrollbar {
background-color: #26282e;
width: 5px;
}
::-webkit-scrollbar-thumb {
border-radius: 6px;
-webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
background-color: #666;
padding-left: 2px;
padding-right: 2px;
}

57
src/styles/_toolbar.scss Normal file
View File

@@ -0,0 +1,57 @@
// TOOLBAR
.maputnik-toolbar {
position: fixed;
height: $toolbar-height;
width: 100%;
z-index: 100;
left: 0;
top: 0;
background-color: $color-black;
}
.maputnik-toolbar-logo {
width: 180px;
text-align: left;
background-color: $color-black;
padding: $margin-2;
height: $toolbar-height;
h1 {
display: inline;
}
img {
width: 30px;
height: 30px;
padding-right: $margin-2;
vertical-align: middle;
}
}
.maputnik-toolbar-link {
vertical-align: top;
height: $toolbar-height;
display: inline-block;
padding: $margin-3;
font-size: $font-size-5;
cursor: pointer;
color: $color-white;
text-decoration: none;
&:hover {
background-color: $color-midgray;
}
}
.maputnik-toolbar-action {
@extend .maputnik-toolbar-link;
}
.maputnik-icon-text {
padding-left: $margin-1;
}
.maputnik-icon-action {
display: inline;
margin-left: $margin-1;
}

View File

@@ -0,0 +1,69 @@
// ZOOM FUNC
.maputnik-make-zoom-function {
background-color: transparent;
display: inline-block;
padding-bottom: 0;
padding-top: 0;
vertical-align: middle;
@extend .maputnik-icon-button;
}
// ZOOM PROPERTY
.maputnik-zoom-spec-property {
@extend .clearfix;
}
.maputnik-zoom-spec-property-label {
display: inline-block;
width: 41%;
}
.maputnik-zoom-spec-property-stop-item {
margin-bottom: $margin-2;
margin-top: $margin-2;
}
.maputnik-zoom-spec-property-stop-edit {
display: inline-block;
vertical-align: top;
width: 16%;
margin-right: 3%;
> * {
width: 100%;
}
}
.maputnik-zoom-spec-property-stop-value {
display: inline-block;
width: 81%;
> * {
width: 100%;
}
}
.maputnik-delete-stop {
@extend .maputnik-icon-button;
vertical-align: top;
.maputnik-doc-wrapper {
width: auto;
}
.maputnik-doc-target {
cursor: pointer;
}
}
.maputnik-add-stop {
display: inline-block;
float: right;
margin-right: $margin-3;
}
.maputnik-zoom-spec-property .maputnik-input-block:not(:first-child) .maputnik-input-block-label {
visibility: hidden;
}

36
src/styles/index.scss Normal file
View File

@@ -0,0 +1,36 @@
$color-black: #1c1f24;
$color-gray: #26282e;
$color-midgray: #36383e;
$color-lowgray: #8e8e8e;
$color-white: #f0f0f0;
$color-red: #cf4a4a;
$margin-1: 3px;
$margin-2: 5px;
$margin-3: 10px;
$margin-4: 30px;
$margin-5: 40px;
$font-size-1: 24px;
$font-size-2: 20px;
$font-size-3: 18px;
$font-size-4: 16px;
$font-size-5: 14px;
$font-size-6: 12px;
$font-family: Roboto, sans-serif;
$toolbar-height: 40px;
@import 'mixins';
@import 'reset';
@import 'base';
@import 'components';
@import 'scrollbar';
@import 'picker';
@import 'toolbar';
@import 'modal';
@import 'export';
@import 'layout';
@import 'layer';
@import 'input';
@import 'filtereditor';
@import 'zoomproperty';
@import 'popup';

Some files were not shown because too many files have changed in this diff Show More