Compare commits

...

233 Commits

Author SHA1 Message Date
orangemug
cc51774259 1.4.0 2018-07-27 13:19:14 +01:00
Orange Mug
5a19245ee0 Merge pull request #349 from orangemug/fix/react-codemirror-overflow
Fix to prevent contents of react-codemirror being hidden
2018-07-18 19:16:14 +01:00
orangemug
45f45b7547 Fix to prevent contents of react-codemirror being hidden 2018-07-18 08:07:35 +01:00
Orange Mug
530bfaf3b3 Merge pull request #348 from orangemug/fix/color-filter-undefined
Undefined filter fix (color accessibility)
2018-07-17 21:51:54 +01:00
orangemug
6ea70ab9cf Fix what I believe to be a 'first boot' error. 2018-07-17 20:45:12 +01:00
orangemug
a0e2d68dae Only apply filter if defined. 2018-07-17 20:40:23 +01:00
Orange Mug
1447e8bfb5 Merge pull request #345 from orangemug/feature/option-to-download-with-own-tokens
Option to download styles with your own tokens
2018-07-16 08:10:43 +01:00
orangemug
c0480a50ea Option to download styles with own tokens. 2018-07-15 22:51:57 +01:00
Orange Mug
09ba2be416 Merge pull request #344 from orangemug/fix/map-overflow-zoom-issues
Fixed map width so it no longer overflows
2018-07-15 22:44:51 +01:00
Orange Mug
115ce3305d Merge pull request #343 from orangemug/fix/disable-bounce-scroll
Prevent bounce scroll on <body/>
2018-07-15 22:11:18 +01:00
orangemug
960b2022ed Fixed map width (fixes #260) 2018-07-15 22:08:06 +01:00
orangemug
252b442ca9 The UI is 100% height so prevent bounce scroll on OSX 2018-07-15 21:51:25 +01:00
Orange Mug
03b9ddda9c Merge pull request #342 from orangemug/fix/layer-editor-overflow
Fixed <LayerEditor/> overflow issues
2018-07-15 21:49:17 +01:00
orangemug
968d7d7fda Fixed <LayerEditor/> overflow issues. 2018-07-15 13:17:47 +01:00
orangemug
b211f1cd12 1.3.0 2018-07-12 15:54:01 +01:00
Orange Mug
870d4349f4 Merge pull request #341 from orangemug/fix/normalizeSourceURL-import-error
Fixed normalizeSourceURL import issue
2018-07-12 14:23:16 +01:00
orangemug
d88bc59720 Fixed normalizeSourceURL import issue. 2018-07-12 12:33:40 +01:00
orangemug
7c00775515 1.3.0-beta 2018-07-11 08:22:30 +01:00
Orange Mug
4b5536b282 Merge pull request #335 from gregorywolanski/survey
Survey
2018-07-08 15:50:29 +01:00
Gregory Wolanski
fb84cfee1c Survey (#328): Proper contrast ratio 2018-07-08 16:27:59 +02:00
Gregory Wolanski
9132262106 Merge branch 'survey' of https://github.com/gregorywolanski/editor into survey 2018-07-08 14:43:03 +02:00
Gregory Wolanski
5de9e708e9 Survey (#328): Cleaning 2018-07-08 14:42:49 +02:00
Gregory Wolanski
4df63c7287 Update _base.scss 2018-07-08 14:38:52 +02:00
Gregory Wolanski
a88ca031d0 Survey (#328)
Elements promoting the survey inside Maputnik after feedback
2018-07-08 14:34:46 +02:00
Gregory Wolanski
452706f35c Survey (#328) 2018-06-30 10:17:14 +02:00
Gregory Wolanski
8b0aa194cf Survey (#328)
Elements promoting the survey inside Maputnik after feedback
2018-06-30 10:09:23 +02:00
Orange Mug
b9aa7e9206 Merge pull request #333 from pathmapper/master
Update repository for OSM Liberty
2018-06-30 07:09:15 +02:00
pathmapper
e35f106482 Update repository for OSM Liberty 2018-06-29 11:20:32 +02:00
Gregory Wolanski
b7a97cf8ee Survey (#328)
Elements promoting the survey inside Maputnik
2018-06-25 19:52:48 +02:00
Orange Mug
9208115981 Merge pull request #330 from orangemug/feature/loading-modal
Loading dialog
2018-06-18 20:27:39 +01:00
orangemug
afbdaecd0a Abstracted out <LoadingModal/> 2018-06-18 19:06:16 +01:00
orangemug
558f3d649d Added dialog styling. 2018-06-18 18:17:33 +01:00
Orange Mug
417511d577 Merge pull request #329 from orangemug/feature/osm-donate-readme
Added link to <https://maputnik.github.io/donate>
2018-06-16 09:48:18 +01:00
orangemug
df350534ce Added link to <https://maputnik.github.io/donate> 2018-06-16 09:46:30 +01:00
orangemug
7167235146 Added loading modal when opening styles. 2018-06-15 20:57:39 +01:00
Orange Mug
7a7f2eb7de Merge pull request #315 from orangemug/feature/option-to-display-tile-boundaries
Added option to display tile boundaries
2018-06-03 20:26:17 +01:00
orangemug
cd28a53f6a Fixed failing tests, these weren't flaky tests... ooops! 2018-06-03 18:28:55 +01:00
orangemug
1fe31ac0ec Fix for bad lint error. 2018-06-03 17:55:46 +01:00
orangemug
ffce8e3ba5 Added missing file. 2018-06-03 17:37:54 +01:00
Orange Mug
a28a417ebc Merge pull request #314 from orangemug/fix/various-fixes
Small bug fixes
2018-06-03 17:35:44 +01:00
orangemug
6cdb56d13f Improved showTileBoundaries and query string support 2018-06-03 17:33:08 +01:00
orangemug
0516e587b4 Added option to display tile boundries (issue #202) 2018-06-03 17:17:45 +01:00
orangemug
5b4063105b Added missing 'noopener noreferrer' 2018-06-03 16:59:41 +01:00
orangemug
d9a5548762 Small bug fixes
- Logo DOM sctrucutre now valid, no longer <a/> within </a>
 - `data-wd-key` not longer required
 - `maputnik-doc-popup` not longer hidden by LayerEditor accordion
2018-06-03 16:37:46 +01:00
Orange Mug
cae6cffb7b Merge pull request #313 from orangemug/feature/shortcuts
Keyboard shortcuts
2018-06-03 11:18:16 +01:00
orangemug
ede782abed Fixed typo. 2018-06-03 10:18:55 +01:00
orangemug
00afbad7ac Fixed lint errors. 2018-06-03 10:00:50 +01:00
Orange Mug
edd09ef585 Merge pull request #306 from orangemug/feature/accessibility-list-reorder
Keyboard accessible layer options
2018-06-03 09:57:00 +01:00
orangemug
1e09066779 Merge branch 'feature/accessibility-list-reorder' into feature/shortcuts
Conflicts:
	src/components/App.jsx
2018-06-03 09:41:07 +01:00
orangemug
32edb48e16 Fix for when 'layout.visibility' is undefined 2018-06-03 09:31:02 +01:00
orangemug
b116eef147 Merge remote-tracking branch 'upstream/master' into feature/accessibility-list-reorder
Conflicts:
	src/components/App.jsx
2018-06-03 09:22:02 +01:00
orangemug
74d1cd2d01 Renamed 'Sources' -> 'Data Sources' to make it clearer and make shortcuts easier to remember. 2018-06-03 09:17:53 +01:00
Orange Mug
fd48d82e42 Merge pull request #312 from orangemug/feature/color-filters
Color blindness emulation
2018-06-02 10:21:39 +01:00
orangemug
480d54c2d8 Finished shortcuts modal styling 2018-06-02 10:17:39 +01:00
orangemug
ab9c39b862 Removed additional close button 2018-06-01 20:51:42 +01:00
orangemug
dd122d1bac Hide hidden FileReaderInput from keyboard focus 2018-06-01 20:45:05 +01:00
orangemug
f9f5e8f925 Changed close button from <a> to <button> 2018-06-01 20:40:51 +01:00
orangemug
aa2f4a091c Initial attempt at color blindness emulation 2018-06-01 09:22:18 +01:00
orangemug
13fc699d4a Styling fixes. 2018-05-31 21:09:31 +01:00
orangemug
f5e8d473ad Changed toggle visibility text from hide to show/hide 2018-05-31 20:40:21 +01:00
orangemug
35353d75f5 Added application shortcuts and shortcut modal.
Also moved modals into App.jsx to move the business logic to one place.
2018-05-29 17:06:00 +01:00
Orange Mug
0f103c3c00 Merge pull request #309 from orangemug/feature/skip-menu
Added skip-menu link for keyboard users
2018-05-28 13:17:02 +01:00
orangemug
019428a241 Added missing prop-types. 2018-05-28 12:06:22 +01:00
orangemug
6200edea25 Added initial shortcuts. 2018-05-28 12:03:47 +01:00
orangemug
fc7395df96 Fixed CircleCI cache to include {{arch}} 2018-05-28 11:34:12 +01:00
orangemug
272f662a34 Changed 'skip' wording
As outlined in <https://webaim.org/techniques/skipnav/>
2018-05-28 11:29:49 +01:00
orangemug
d59d9cde95 Fixed OSX working directory if CircleCI config. 2018-05-28 11:19:04 +01:00
orangemug
c71fbcf436 Tidy 2018-05-28 11:15:16 +01:00
Orange Mug
54c79445db Merge pull request #307 from orangemug/fix/public-source-button-size
Fixed public source button size
2018-05-28 10:52:23 +01:00
orangemug
a82ba26f86 Added skip-menu link for keyboard users. 2018-05-28 10:50:19 +01:00
orangemug
28af87391d Fixed public source button size. 2018-05-22 21:43:35 +01:00
orangemug
0aabd33538 Remove empty scss blocks 2018-05-22 21:26:11 +01:00
orangemug
bd9076c4ff Added additional menu in <LayerEditor/>
This is to make the following options accessible to keyboard users

 - reorder layers
 - duplicate layer
 - delete layer
 - hide/show layer
2018-05-22 21:16:46 +01:00
Orange Mug
1aed761893 Merge pull request #305 from orangemug/feature/public-style-aria-labels
Added aria-label to public styles
2018-05-19 09:39:13 +01:00
orangemug
a2a6f6dcab Added aria-label to public styles, also fixed button to reserve space in DOM (fixes #245) 2018-05-19 08:23:41 +01:00
Orange Mug
db5dd0f6ee Merge pull request #304 from orangemug/fix/disable-spellcheck-v2
Disable spellcheck on <input/>'s
2018-05-19 07:56:06 +01:00
orangemug
42c3dcf258 Updated package-lock.json 2018-05-17 13:49:24 +01:00
orangemug
51a115d65a Disable spell checking on <input>'s 2018-05-17 13:44:54 +01:00
Orange Mug
fc0fbd6a37 Merge pull request #302 from orangemug/feature/terrarium-encoding
Added support for encoding to raster-dem source
2018-05-17 13:42:01 +01:00
orangemug
d80d76724c Fixed more lint errors. 2018-05-17 11:46:33 +01:00
orangemug
77da0a6d30 React v16.3.0 fixes. 2018-05-17 11:24:39 +01:00
orangemug
79b251d8b9 DRY up the code. 2018-05-17 10:55:55 +01:00
orangemug
4f19f6a08c Added support for encoding to raster-dem source, enabling terrarium tiles. 2018-05-17 10:44:54 +01:00
Orange Mug
d2a6eab1e6 Merge pull request #291 from orangemug/feature/circle-ci-osx-builds
CircleCI OSX builds
2018-05-11 15:50:48 +01:00
Orange Mug
c7cf051502 Merge pull request #296 from orangemug/feature/prefers-reduced-motion
Added prefers-reduced-motion support
2018-05-11 15:50:08 +01:00
Orange Mug
6e21503e6b Merge pull request #297 from orangemug/accessibility/larger-color-swatch
Make color swatch larger so its easier to see
2018-05-11 15:49:32 +01:00
orangemug
78d71a4e7e Fixed duplicate definition. 2018-05-11 14:53:06 +01:00
orangemug
b8f32d46cf Rename <CollapseReducedMotion/> to <Collapse/> 2018-05-11 14:03:46 +01:00
Orange Mug
443782decf Merge pull request #300 from orangemug/accessibility/react-aria-modal
Added accessible modal via react-aria-modal
2018-05-11 13:54:39 +01:00
orangemug
54e79e5eb8 Added missing data-wd-key attribute. 2018-05-11 11:26:43 +01:00
orangemug
221cd4ffd2 Added accessible modal via react-aria-modal 2018-05-11 10:56:34 +01:00
Orange Mug
354b2fb3cb Merge pull request #298 from orangemug/fix/keyboard-accessible-buttons
Made buttons keyboard accessible
2018-05-11 10:47:11 +01:00
orangemug
7cb2c36ac9 Move accessibility checks into module. 2018-05-11 09:32:57 +01:00
orangemug
11d73595fc Made buttons keyboard accessible. 2018-05-10 16:50:37 +01:00
orangemug
c241a6e280 Ignore 'prefers-reduced-motion' in stylelint 2018-05-10 16:30:23 +01:00
orangemug
198ff143f6 Make color swatch larger so its easier to see. 2018-05-10 16:18:13 +01:00
orangemug
7b8b797f9c Fixed typo. 2018-05-10 16:07:34 +01:00
orangemug
a41b25eea7 Added 'prefers-reduced-motion' css support. 2018-05-10 16:05:55 +01:00
Orange Mug
06eac68f9d Merge pull request #293 from orangemug/maintenance/update-deps-20180509
Updated deps
2018-05-10 08:33:18 +01:00
orangemug
8abf84ebc0 Updated deps. 2018-05-09 09:39:03 +01:00
orangemug
e9aa1f6dd6 Fixed typo 2018-05-08 17:34:09 +01:00
orangemug
8e7b838bf7 Altered versions for node.js release schedule
See <https://github.com/nodejs/Release>
2018-05-08 17:30:42 +01:00
orangemug
32db3c3c9b Added build-osx-node-v9 to CircleCI 2018-05-08 17:23:03 +01:00
orangemug
502586e5d5 1.2.0 2018-05-08 16:11:13 +01:00
Orange Mug
d92d599d8a Merge pull request #290 from orangemug/fix/disable-gist
Disable gist export
2018-05-08 15:55:23 +01:00
orangemug
3487056c7d Disable gist, see <https://github.com/maputnik/editor/issues/269> 2018-05-08 15:21:14 +01:00
orangemug
dbcfb08c15 1.2.0-beta2 2018-04-20 15:31:00 +01:00
Orange Mug
e96141090e Merge pull request #287 from orangemug/fix/beta-version-wrapping
Fix to allow beta version strings to not wrap
2018-04-20 15:27:48 +01:00
orangemug
5bd25fc2ed Fix to allow beta version strings to not wrap. 2018-04-20 15:09:37 +01:00
orangemug
334932b298 1.2.0-beta 2018-04-20 14:53:51 +01:00
Orange Mug
661006d7fb Merge pull request #284 from pjsier/fix/276-null-zoom
Handle data functions without zoom
2018-04-20 14:14:02 +01:00
Orange Mug
c917249517 Merge pull request #286 from orangemug/maintenance/update-stylelint
Updated stylelint
2018-04-17 15:56:01 +01:00
orangemug
d0ca732fe7 Updated stylelint and fixed scss for 'stylelint-config-recommended-scss' 2018-04-17 14:55:33 +01:00
Orange Mug
52821cd1df Merge pull request #285 from orangemug/maintenance/update-deps-20180417
Updated deps
2018-04-17 12:11:22 +01:00
orangemug
328e0b8ff7 Updated deps. 2018-04-17 11:35:30 +01:00
Orange Mug
f0147cc89a Merge pull request #280 from orangemug/fix/web-driver-tests-v8
Improved tests
2018-04-16 20:48:56 +01:00
orangemug
78a7f152e7 Merge remote-tracking branch 'upstream/master' into fix/web-driver-tests-v8
Conflicts:
	src/styles/index.scss
2018-04-16 15:31:27 +01:00
pjsier
e936dd16bf Fix style linting error 2018-04-16 07:44:00 -05:00
pjsier
3d4579288c Handle data functions without zoom 2018-04-16 06:59:01 -05:00
Orange Mug
b60df8b074 Merge pull request #283 from orangemug/fix/issue-244
Fix to allow layer sections to expand smoothly
2018-04-15 14:56:21 +01:00
orangemug
c4b92fa0a9 Updated test instructions in README 2018-04-15 09:17:07 +01:00
orangemug
9808d44c71 Fix to allow layers sections to expand smoothly. Fixes #244 2018-04-13 17:00:51 +01:00
Orange Mug
1bdd135386 Merge pull request #282 from oterral/teo_fixed
Use a fixed position for autocomplete menu
2018-04-13 15:53:27 +01:00
Orange Mug
740a75f2e6 Merge pull request #281 from oterral/master
Block the popup on click in inspect mode
2018-04-13 15:06:28 +01:00
oterral
b62533fa3e Use a fixed position for autocomplete menu 2018-04-13 15:55:16 +02:00
oterral
044349e65f Block popup on click in inspect mode 2018-04-13 14:25:08 +02:00
oterral
e8b0bd4d0a Update mapbox-gl-inspect dependency 2018-04-13 14:24:39 +02:00
orangemug
1805aee7ba Removed lint-styles in appveyor
It doesn't work in windows and should be addressed in another PR
2018-04-10 16:16:51 +01:00
orangemug
8ba2123a26 Added missing propType. 2018-04-10 15:15:29 +01:00
orangemug
687c08527d Added test docs. 2018-04-10 15:13:55 +01:00
orangemug
f0744f024d Moved commit. 2018-04-10 15:07:36 +01:00
orangemug
9e82599464 Removed old comments. 2018-04-10 14:23:11 +01:00
orangemug
7a60df370e Changed url to be local (although not used) 2018-04-10 14:20:13 +01:00
orangemug
aee4a041fe Removed node:10 from appveyor 2018-04-10 14:03:09 +01:00
orangemug
6fa06e5483 Removed un-useful comments 2018-04-10 14:02:36 +01:00
orangemug
15962481ee Disable OSX until we get a open source plan for maputnik/editor 2018-04-10 13:35:02 +01:00
orangemug
6bf695cd4b Removed linux from travis. CircleCI now takes care of that 2018-04-10 13:34:20 +01:00
orangemug
7ecbc14c39 Added OSX build to tests. 2018-04-10 13:29:48 +01:00
orangemug
fb0e531f4a Removed node:10 as it doesn't exist yet. 2018-04-10 13:17:59 +01:00
orangemug
bd44e6d071 Fixed typo. 2018-04-10 13:11:42 +01:00
orangemug
3ae37f1c46 Updated appveyor to no longer test, only build/lint 2018-04-10 13:08:55 +01:00
orangemug
8c7a1f7075 Updated build config for circleci to only test webdriver in one job 2018-04-10 13:05:58 +01:00
orangemug
3e97d8a5f1 Merge remote-tracking branch 'upstream/master' into fix/web-driver-tests-v8 2018-04-10 12:56:43 +01:00
orangemug
6138257a89 Remove logging. 2018-04-10 12:52:59 +01:00
orangemug
0bd62985b9 Revert change to undo/redo 2018-04-10 12:45:44 +01:00
orangemug
a346d757fd Don't assume docker for mac. 2018-04-09 18:18:15 +01:00
orangemug
84f3970730 Updated selenium-standalone & webdriverio 2018-04-09 17:51:12 +01:00
orangemug
050e22918a Fix for running within docker. 2018-04-09 17:49:56 +01:00
Orange Mug
f205776695 Merge pull request #277 from maputnik/revert-275-maintenance/update-ol-mapbox-style
Revert "Update ol-mapbox-style ^2.10.1 -> ^2.11.2"
2018-04-09 13:54:12 +01:00
Orange Mug
4d427bcbc3 Revert "Update ol-mapbox-style ^2.10.1 -> ^2.11.2" 2018-04-09 13:53:25 +01:00
Orange Mug
0b4910e3c3 Merge pull request #275 from orangemug/maintenance/update-ol-mapbox-style
Update ol-mapbox-style ^2.10.1 -> ^2.11.2
2018-04-09 12:16:48 +01:00
orangemug
11a59debdf Update ol-mapbox-style ^2.10.1 -> ^2.11.2 2018-04-09 11:10:46 +01:00
orangemug
dbe2c2637e Better onPrepare for wdio 2018-04-09 10:20:37 +01:00
Orange Mug
d6ce13c356 Merge pull request #273 from cmarqu/patch-1
Fix small typo.
2018-04-09 09:42:46 +01:00
Orange Mug
6d094a8b3e Merge pull request #271 from ziveo/master
Adding mac keyboard bindings
2018-04-09 09:37:32 +01:00
Colin Marquardt
4d0456fd68 Fix small typo. 2018-03-27 00:45:42 +02:00
ziveo
ad83f940a7 Merge branch 'master' into master 2018-03-18 20:02:20 -04:00
ziveo
edc7e02f58 Merge pull request #2 from ziveo/develop__mac-keyboard-bindings
Improving keyboard bindings code
2018-03-16 23:01:34 -04:00
Bojan Zivkovic
7dfc5029a3 Improving keyboard bindings code 2018-03-16 23:00:33 -04:00
ziveo
8e02722b52 Merge pull request #1 from ziveo/develop__mac-keyboard-bindings
Adding mac keyboard bindings
2018-03-15 23:41:48 -04:00
Bojan Zivkovic
984581e01a Adding mac keyboard bindings 2018-03-15 23:39:32 -04:00
orangemug
1de7ba7e86 Use dev settings for test. 2018-03-06 21:11:58 +00:00
orangemug
a3fa86f7ee Merge remote-tracking branch 'upstream/master' into fix/web-driver-tests-v7
Conflicts:
	config/webpack.production.config.js
	package-lock.json
	package.json
2018-03-06 07:22:26 +00:00
Orange Mug
a589f89c4c Merge pull request #268 from orangemug/fix/update-mapbox-gl-inspect
Updated mapbox-gl-inspect to v1.3.0
2018-02-19 09:40:53 +00:00
orangemug
3b599aed4c Updated mapbox-gl-inspect to v1.3.0 2018-02-19 08:28:08 +00:00
Orange Mug
6953db74c6 Merge pull request #266 from orangemug/maintenance/added-circle-symbol-pitch-alignment-paint-props
Added [symbol|circle]-pitch-alignment props
2018-02-18 17:59:13 +00:00
Orange Mug
1ad473a539 Merge pull request #267 from orangemug/feature/heatmap
Added heatmap layer support
2018-02-18 16:34:42 +00:00
orangemug
fafda9ec92 Merge remote-tracking branch 'upstream/master' into maintenance/added-circle-symbol-pitch-alignment-paint-props 2018-02-18 15:00:22 +00:00
Orange Mug
11b85bf565 Merge pull request #263 from orangemug/feature/hillshading
Added hillshading support
2018-02-18 14:53:58 +00:00
orangemug
6ecc6670dc Added [symbol|circle]-pitch-alignment paint props 2018-02-18 13:23:04 +00:00
orangemug
553f0fe23e Drop support for 'heatmap-color'
See <https://github.com/maputnik/editor/issues/265#issuecomment-366511333>
2018-02-18 12:07:34 +00:00
orangemug
77ddf67201 Added heatmap layer type. 2018-02-18 11:50:04 +00:00
orangemug
a092bc2689 Moved to using orangemug/mapbox-gl-inspect#fix/only-vector-sources
While <https://github.com/lukasmartinelli/mapbox-gl-inspect/pull/11> is
waiting to be merged/released.
2018-02-18 11:22:01 +00:00
orangemug
38e0786463 Added missing hillshade / raster-dem guards. 2018-02-17 07:45:24 +00:00
orangemug
180b17d315 Fixed typo raster -> raster-dem 2018-02-16 20:34:50 +00:00
orangemug
8acbd784a0 Added hillshading support. 2018-02-16 19:52:19 +00:00
Orange Mug
07efe1e1b8 Merge pull request #253 from orangemug/maintenance/openlayers-update
Updated openlayers
2018-02-07 22:56:01 +00:00
orangemug
7ea53cc3a1 Increased build timeout. 2018-02-07 11:38:32 +00:00
orangemug
de21eea21b Some modules aren't ES5 so we much compile them 2018-02-07 11:00:24 +00:00
orangemug
8f8ed6dff3 Changed to uglifyjs-webpack-plugin for es2015 support. 2018-02-06 10:50:15 +00:00
orangemug
8915bbfeb4 Updated openlayers.
openlayers^4.4.2 -> ol^4.6.4
ol-mapbox-style^1.0.1 -> ol-mapbox-style^2.10.1

Fixes #246
2018-02-06 08:28:57 +00:00
Orange Mug
df3a42acce Merge pull request #241 from orangemug/feature/private-public-gist
Public/private gists
2018-02-03 15:43:12 +00:00
Orange Mug
2a7ef82d23 Merge pull request #248 from orangemug/feature/nsp
Added nsp (node security project)
2018-02-03 15:41:42 +00:00
orangemug
95168f22e3 Added nsp 2018-02-03 15:30:29 +00:00
Orange Mug
4360753263 Merge pull request #242 from orangemug/feature/update-mapbox-gl-v0.44.0
Updated mapbox-gl 0.43.0 -> 0.44.0
2018-02-03 14:02:45 +00:00
Orange Mug
ad491cb465 Merge pull request #240 from orangemug/fix/do-not-expose-fallback-tokens
Do not expose fallback tokens during export
2018-02-03 14:02:06 +00:00
orangemug
e5bed80c96 Updated mapbox-gl 0.43.0 -> 0.44.0. Fixes #237 2018-02-02 18:04:57 +00:00
orangemug
9bf3046d4c Public/private gists added. Fixes #238
Gists are now private by default with a option for public.
2018-02-02 17:23:21 +00:00
Orange Mug
da8dc0f7a6 Merge pull request #231 from justenPalmer/issues
Issue 229: Adding a style without Glyphs defined throws an exception with no feedback in interface
2018-02-02 16:43:18 +00:00
orangemug
b66a4afd28 Do not expose fallback tokens during export. Fixes #230 2018-02-02 15:33:15 +00:00
Orange Mug
a94c53534c Merge pull request #235 from orangemug/feature/export-token-fix
Fixes for export to add in mapbox access token
2018-02-02 11:47:41 +00:00
Orange Mug
6b22c9130f Merge pull request #236 from orangemug/fix/issue-234
Added guard in fetchSources
2018-02-02 11:24:23 +00:00
orangemug
7d5927bbc8 Added additional guard
As checking the key name is 'openmaptiles' isn't a guarantee
2018-02-01 22:00:26 +00:00
jPalmer
240d02a124 Merge branch 'master' of https://github.com/maputnik/editor into issues 2018-02-01 13:44:23 -08:00
jPalmer
92ef1c4cbb added more robust handling of glyphs in styles - addresses #229 2018-02-01 13:44:15 -08:00
orangemug
5ce57d0803 Added guard in fetchSources.
This will mean that autocomplete is broken for sources without vector_layers key present.
2018-02-01 21:37:17 +00:00
orangemug
1c134d757c Fixes for export to add in mapbox access token. 2018-02-01 19:54:44 +00:00
Orange Mug
32d808b230 Merge pull request #233 from orangemug/feature/version-in-ui
Added version number to the UI
2018-02-01 08:10:21 +00:00
orangemug
ee3def492a Fixed toolbar version position. 2018-01-31 21:36:47 +00:00
orangemug
41bd91fcd2 Center the toolbar button text. 2018-01-31 21:22:12 +00:00
orangemug
02c8542848 Added version number to the UI. Fixes #232 2018-01-31 21:04:49 +00:00
jPalmer
844abd38ce added missing glyphs property check on styleChanged - addresses #229 2018-01-31 11:28:09 -08:00
jPalmer
d9b6f28bb5 added missing glyphs property check on styleChanged - addresses #229 2018-01-31 11:26:10 -08:00
orangemug
ed85b838ec v1.1.0 2018-01-30 20:57:24 +00:00
Orange Mug
f82b138a3d Merge pull request #228 from orangemug/fix/source-layer-guard
Added guard to <LayerSourceLayerBlock/> sourceLayerIds
2018-01-30 17:49:41 +00:00
orangemug
89c38991b9 Added guard to <LayerSourceLayerBlock/> sourceLayerIds 2018-01-29 17:18:30 +00:00
orangemug
4215b5808f Merge remote-tracking branch 'upstream/master' into fix/web-driver-tests-v6-circleci-config
Conflicts:
	package-lock.json
	src/components/inputs/AutocompleteInput.jsx
2018-01-22 09:57:54 +00:00
orangemug
e64ca3eb93 Added back in other jobs. 2018-01-19 18:15:46 +00:00
orangemug
094c4747d3 Update selenium/standalone-chrome to 3.8.1 2018-01-19 18:02:08 +00:00
orangemug
62f0843283 Moved back to workflows. 2018-01-19 17:55:37 +00:00
orangemug
8062e304b7 Update selenium/standalone-chrome 2018-01-19 17:51:08 +00:00
orangemug
18e7ead78a Revert to old config. 2018-01-19 17:35:19 +00:00
orangemug
3cab1dc49f Remove special directory. 2018-01-19 17:29:55 +00:00
orangemug
f8dcbb8fb7 Reduce to single job. 2018-01-19 17:24:29 +00:00
orangemug
c82f38c103 Multiple working directories for test versions. 2018-01-19 17:14:56 +00:00
orangemug
fe0e7af033 Added multiple nodejs versions. 2018-01-19 15:13:46 +00:00
orangemug
ac51902435 Added missing workflow to .circleci/config.yml 2018-01-19 15:08:45 +00:00
orangemug
e0ff342702 Added yaml inheritance to .circleci/config.yml 2018-01-19 15:06:23 +00:00
orangemug
cb4f5ea963 Updated to react/react-dom v16.2.0 2018-01-18 23:15:59 +00:00
orangemug
a822430e1d Merge remote-tracking branch 'upstream/master' into fix/web-driver-tests-v6
Conflicts:
	package-lock.json
2018-01-18 22:59:59 +00:00
orangemug
dc40ce7d9e Fixed lint errors. 2018-01-17 17:58:01 +00:00
orangemug
383a119127 Added linting to circleci tests. 2018-01-17 17:55:21 +00:00
orangemug
3f492e6208 Change artifacts destination. 2018-01-17 17:43:35 +00:00
orangemug
0cec0cf595 Fix coverage in tests. 2018-01-17 17:36:46 +00:00
orangemug
bc19aea438 CircleCI test now just calls npm test 2018-01-17 17:01:55 +00:00
orangemug
211850c813 Added cross-env 2018-01-17 16:53:42 +00:00
orangemug
c1312fb288 Added '/build' to .gitignore 2018-01-17 15:46:48 +00:00
orangemug
0c2934c489 Code to store artifacts on circle ci 2018-01-17 15:44:00 +00:00
orangemug
ad34147f28 Fixed screenshots. 2018-01-17 15:39:17 +00:00
orangemug
1eb6c28617 Removed logging. 2018-01-17 14:59:48 +00:00
orangemug
2e8a188bce Increased timeouts. 2018-01-17 14:51:25 +00:00
orangemug
a773958403 Tidy tests. 2018-01-10 15:06:11 +00:00
orangemug
942b2240a7 Added more webdriver tests testing against a real browser. 2018-01-05 17:45:55 +00:00
94 changed files with 11876 additions and 6520 deletions

View File

@@ -1,4 +1,13 @@
{ {
"presets": ["env", "react"], "presets": ["env", "react"],
"plugins": ["transform-object-rest-spread", "transform-class-properties"] "plugins": ["transform-object-rest-spread", "transform-class-properties"],
"env": {
"test": {
"plugins": [
["istanbul", {
exclude: ["node_modules/**", "test/**"]
}]
]
}
}
} }

103
.circleci/config.yml Normal file
View File

@@ -0,0 +1,103 @@
version: 2
templates:
# Test the build **only** no webdriver
build-steps: &build-steps
- checkout
- run:
name: "Create artifacts directory"
command: mkdir /tmp/artifacts
- restore_cache:
key: v1-dependencies-{{ arch }}-{{ checksum "package.json" }}
- run: npm install
- save_cache:
paths:
- node_modules
key: v1-dependencies-{{ arch }}-{{ checksum "package.json" }}
- run: mkdir -p /tmp/artifacts/logs
- run: npm run build
- run: npm run lint
- run: npm run lint-styles
- store_artifacts:
path: /tmp/artifacts
destination: /artifacts
# Test in webdriver
wdio-steps: &wdio-steps
- checkout
- run:
name: "Create artifacts directory"
command: mkdir /tmp/artifacts
- restore_cache:
key: v1-dependencies-{{ arch }}-{{ checksum "package.json" }}
- run: npm install
- save_cache:
paths:
- node_modules
key: v1-dependencies-{{ arch }}-{{ checksum "package.json" }}
- run: mkdir -p /tmp/artifacts/logs
- run: npm run build
- run: npm run lint
- run: npm run lint-styles
- run: DOCKER_HOST=localhost npm test
- run: ./node_modules/.bin/istanbul report --include /tmp/artifacts/coverage/coverage.json --dir /tmp/artifacts/coverage html lcov
- store_artifacts:
path: /tmp/artifacts
destination: /artifacts
jobs:
build-linux-node-v6:
docker:
- image: node:6
working_directory: ~/repo-linux-node-v6
steps: *build-steps
build-linux-node-v8:
docker:
- image: node:8
- image: selenium/standalone-chrome:3.8.1
working_directory: ~/repo-linux-node-v8
steps: *wdio-steps
build-linux-node-v10:
docker:
- image: node:10
working_directory: ~/repo-linux-node-v10
steps: *build-steps
build-osx-node-v6:
macos:
xcode: "9.0"
dependencies:
override:
- brew install node@6
working_directory: ~/repo-osx-node-v6
steps: *build-steps
build-osx-node-v8:
macos:
xcode: "9.0"
dependencies:
override:
- brew install node@8
working_directory: ~/repo-osx-node-v8
steps: *build-steps
build-osx-node-v10:
macos:
xcode: "9.0"
dependencies:
override:
- brew install node@10
working_directory: ~/repo-osx-node-v10
steps: *build-steps
workflows:
version: 2
build:
jobs:
- build-linux-node-v6
- build-linux-node-v8
- build-linux-node-v10
- build-osx-node-v6
- build-osx-node-v8
- build-osx-node-v10

3
.gitignore vendored
View File

@@ -30,3 +30,6 @@ node_modules
# Ignore build files # Ignore build files
public public
/errorShots
/old
/build

View File

@@ -1,30 +1,12 @@
language: node_js language: node_js
addons:
firefox: latest
matrix: matrix:
include: include:
- os: linux
node_js: "6"
- os: linux
env: CXX=g++-4.8
node_js: "7"
- os: linux
node_js: "8"
- os: linux
env: CXX=g++-4.8
node_js: "9"
- os: osx - os: osx
node_js: "6" node_js: "6"
- os: osx
node_js: "7"
- os: osx - os: osx
node_js: "8" node_js: "8"
- os: osx - os: osx
node_js: "9" node_js: "9"
before_install:
- export CHROME_BIN=chromium-browser
- export DISPLAY=:99.0
- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sh -e /etc/init.d/xvfb start; fi
install: install:
- npm install - npm install
script: script:
@@ -32,7 +14,6 @@ script:
- node --stack_size=100000 $(which npm) run build - node --stack_size=100000 $(which npm) run build
- npm run lint - npm run lint
- npm run lint-styles - npm run lint-styles
- npm run test
addons: addons:
apt: apt:
sources: sources:

View File

@@ -22,6 +22,11 @@ targeted at developers and map designers.
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. 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.
## Donations
If you or your organisation has seen value from Maputnik, please consider donating at <https://maputnik.github.io/donate>
## Documentation ## Documentation
The documentation can be found in the [Wiki](https://github.com/maputnik/editor/wiki). You are welcome to collaborate! The documentation can be found in the [Wiki](https://github.com/maputnik/editor/wiki). You are welcome to collaborate!
@@ -68,6 +73,33 @@ npm run lint
npm run lint-styles npm run lint-styles
``` ```
## Tests
For testing we use [webdriverio](http://webdriver.io) and [selenium-standalone](https://github.com/vvo/selenium-standalone)
[selenium-standalone](https://github.com/vvo/selenium-standalone) starts a server that will launch browsers on your local machine. We use chrome so you **must** have chrome installed on your machine.
Now open and terminal and run the following. This will install the drivers on your local machine
```
./node_modules/.bin/selenium-standalone install
```
Now start the standalone server
```
./node_modules/.bin/selenium-standalone start
```
Then open another terminal and run
```
npm test
```
After some time you should see a browser launch which will be automated by the test runner.
## Related Projects ## Related Projects
- [maputnik-dev-server](https://github.com/nycplanning/labs-maputnik-dev-server) - An express.js server that allows for quickly loading the style from any mapboxGL map into mapuntnik. - [maputnik-dev-server](https://github.com/nycplanning/labs-maputnik-dev-server) - An express.js server that allows for quickly loading the style from any mapboxGL map into mapuntnik.

View File

@@ -1,7 +1,6 @@
environment: environment:
matrix: matrix:
- nodejs_version: "6" - nodejs_version: "6"
- nodejs_version: "7"
- nodejs_version: "8" - nodejs_version: "8"
- nodejs_version: "9" - nodejs_version: "9"
platform: platform:
@@ -16,4 +15,3 @@ build_script:
- npm run build - npm run build
test_script: test_script:
- npm run lint - npm run lint
- npm test

View File

@@ -1,6 +0,0 @@
machine:
node:
version: 6
test:
post:
- npm run build

View File

@@ -1,48 +1,62 @@
var webpack = require("webpack"); var webpack = require("webpack");
var WebpackDevServer = require("webpack-dev-server"); var WebpackDevServer = require("webpack-dev-server");
var webpackConfig = require("./webpack.production.config"); var webpackConfig = require("./webpack.config");
var testConfig = require("../test/config/specs"); var testConfig = require("../test/config/specs");
var artifacts = require("../test/artifacts");
var isDocker = require("is-docker");
var server; var server;
var SCREENSHOT_PATH = artifacts.pathSync("screenshots");
exports.config = { exports.config = {
specs: [ specs: [
'./test/specs/**/*.js' './test/functional/index.js'
], ],
exclude: [ exclude: [
], ],
maxInstances: 10, maxInstances: 10,
capabilities: [{ capabilities: [{
maxInstances: 5, maxInstances: 5,
browserName: 'firefox' browserName: 'chrome'
}], }],
sync: true, sync: true,
logLevel: 'verbose', logLevel: 'verbose',
coloredLogs: true, coloredLogs: true,
bail: 0, bail: 0,
screenshotPath: './errorShots/', screenshotPath: SCREENSHOT_PATH,
// Note: This is here because @orangemug currently runs Maputnik inside a docker container.
host: process.env.DOCKER_HOST || "0.0.0.0",
baseUrl: 'http://localhost', baseUrl: 'http://localhost',
waitforTimeout: 10000, waitforTimeout: 10000,
connectionRetryTimeout: 90000, connectionRetryTimeout: 90000,
connectionRetryCount: 3, connectionRetryCount: 3,
services: ['phantomjs'],
framework: 'mocha', framework: 'mocha',
reporters: ['spec'], reporters: ['spec'],
phantomjsOpts: {
webdriverLogfile: 'phantomjs.log'
},
mochaOpts: { mochaOpts: {
ui: 'bdd', ui: 'bdd',
// Because we don't know how long the initial build will take... // Because we don't know how long the initial build will take...
timeout: 2*60*1000 timeout: 4*60*1000
}, },
onPrepare: function (config, capabilities) { onPrepare: function (config, capabilities) {
return new Promise(function(resolve, reject) {
var compiler = webpack(webpackConfig); var compiler = webpack(webpackConfig);
server = new WebpackDevServer(compiler, {}); server = new WebpackDevServer(compiler, {
server.listen(testConfig.port); stats: {
colors: true
}
});
server.listen(testConfig.port, (isDocker() ? "0.0.0.0" : "localhost"), function(err) {
if(err) {
reject(err);
}
else {
resolve();
}
});
})
}, },
onComplete: function(exitCode) { onComplete: function(exitCode) {
server.close(); server.close()
} }
} }

View File

@@ -25,8 +25,7 @@ module.exports = {
}, },
module: { module: {
noParse: [ noParse: [
/mapbox-gl\/dist\/mapbox-gl.js/, /mapbox-gl\/dist\/mapbox-gl.js/
/openlayers\/dist\/ol.js/
], ],
loaders: loaders loaders: loaders
}, },

View File

@@ -17,7 +17,8 @@ module.exports = [
}, },
{ {
test: /\.jsx?$/, test: /\.jsx?$/,
exclude: /(.*node_modules(?![\/\\]@mapbox[\/\\]mapbox-gl-style-spec)|bower_components|public)/, // Note: These modules aren't ES5 therefore we much compile them.
exclude: /(.*node_modules(?![\/\\](@mapbox[\/\\]mapbox-gl-style-spec|ol|mapbox-to-ol-style))|bower_components|public)/,
loader: 'babel-loader', loader: 'babel-loader',
query: { query: {
presets: ['env', 'react'], presets: ['env', 'react'],

View File

@@ -1,4 +1,3 @@
var webpack = require('webpack'); var webpack = require('webpack');
var path = require('path'); var path = require('path');
var loaders = require('./webpack.loaders'); var loaders = require('./webpack.loaders');
@@ -7,14 +6,10 @@ var HtmlWebpackPlugin = require('html-webpack-plugin');
var WebpackCleanupPlugin = require('webpack-cleanup-plugin'); var WebpackCleanupPlugin = require('webpack-cleanup-plugin');
var BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; var BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
var CopyWebpackPlugin = require('copy-webpack-plugin'); var CopyWebpackPlugin = require('copy-webpack-plugin');
var artifacts = require("../test/artifacts");
var UglifyJsPlugin = require('uglifyjs-webpack-plugin');
var OUTPATH; var OUTPATH = artifacts.pathSync("/build");
if(process.env.CIRCLE_ARTIFACTS) {
OUTPATH = path.join(process.env.CIRCLE_ARTIFACTS, "build");
}
else {
OUTPATH = path.join(__dirname, '..', 'public');
}
module.exports = { module.exports = {
entry: { entry: {
@@ -49,8 +44,7 @@ module.exports = {
}, },
module: { module: {
noParse: [ noParse: [
/mapbox-gl\/dist\/mapbox-gl.js/, /mapbox-gl\/dist\/mapbox-gl.js/
/openlayers\/dist\/ol.js/
], ],
loaders loaders
}, },
@@ -68,12 +62,7 @@ module.exports = {
NODE_ENV: '"production"' NODE_ENV: '"production"'
} }
}), }),
new webpack.optimize.UglifyJsPlugin({ new UglifyJsPlugin(),
compress: {
warnings: false,
screw_ie8: true,
}
}),
new ExtractTextPlugin('[contenthash].css', { new ExtractTextPlugin('[contenthash].css', {
allChunks: true allChunks: true
}), }),

14770
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,16 +1,17 @@
{ {
"name": "maputnik", "name": "maputnik",
"version": "1.1.0-beta4", "version": "1.4.0",
"description": "A MapboxGL visual style editor", "description": "A MapboxGL visual style editor",
"main": "''", "main": "''",
"scripts": { "scripts": {
"stats": "webpack --config config/webpack.production.config.js --profile --json > stats.json", "stats": "webpack --config config/webpack.production.config.js --profile --json > stats.json",
"build": "webpack --config config/webpack.production.config.js --progress --profile --colors", "build": "webpack --config config/webpack.production.config.js --progress --profile --colors",
"test": "wdio config/wdio.conf.js", "test": "cross-env NODE_ENV=test wdio config/wdio.conf.js",
"test-watch": "wdio config/wdio.conf.js --watch", "test-watch": "cross-env NODE_ENV=test wdio config/wdio.conf.js --watch",
"start": "webpack-dev-server --progress --profile --colors --config config/webpack.config.js", "start": "webpack-dev-server --progress --profile --colors --config config/webpack.config.js",
"lint": "eslint --ext js --ext jsx {src,test}", "lint": "eslint --ext js --ext jsx {src,test}",
"lint-styles": "stylelint 'src/styles/*.scss'" "lint-styles": "stylelint 'src/styles/*.scss'",
"nsp": "nsp check --reporter summary"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
@@ -20,33 +21,36 @@
"license": "MIT", "license": "MIT",
"homepage": "https://github.com/maputnik/editor#readme", "homepage": "https://github.com/maputnik/editor#readme",
"dependencies": { "dependencies": {
"@mapbox/mapbox-gl-rtl-text": "^0.1.1", "@mapbox/mapbox-gl-rtl-text": "^0.1.2",
"@mapbox/mapbox-gl-style-spec": "^10.0.1", "@mapbox/mapbox-gl-style-spec": "^12.0.0",
"classnames": "^2.2.5", "classnames": "^2.2.5",
"codemirror": "^5.32.0", "codemirror": "^5.37.0",
"color": "^2.0.0", "color": "^3.0.0",
"file-saver": "^1.3.3", "file-saver": "^1.3.8",
"github-api": "^3.0.0", "github-api": "^3.0.0",
"jsonlint": "github:josdejong/jsonlint#85a19d7", "jsonlint": "github:josdejong/jsonlint#85a19d7",
"lodash.capitalize": "^4.2.1", "lodash.capitalize": "^4.2.1",
"lodash.clamp": "^4.0.3",
"lodash.clonedeep": "^4.5.0", "lodash.clonedeep": "^4.5.0",
"lodash.isequal": "^4.5.0", "lodash.isequal": "^4.5.0",
"lodash.throttle": "^4.1.1", "lodash.throttle": "^4.1.1",
"mapbox-gl": "^0.43.0", "mapbox-gl": "^0.45.0",
"mapbox-gl-inspect": "^1.2.6", "mapbox-gl-inspect": "^1.3.1",
"maputnik-design": "github:maputnik/design", "maputnik-design": "github:maputnik/design",
"mousetrap": "^1.6.1", "mousetrap": "^1.6.1",
"ol-mapbox-style": "^1.0.1", "ol-mapbox-style": "^2.10.1",
"openlayers": "^4.4.2", "ol": "^4.6.5",
"prop-types": "^15.6.0", "prop-types": "^15.6.0",
"react": "16.0.0", "react": "^16.3.2",
"react-addons-pure-render-mixin": "^15.6.2", "react-addons-pure-render-mixin": "^15.6.2",
"react-aria-menubutton": "^5.1.1",
"react-aria-modal": "^2.12.1",
"react-autocomplete": "^1.7.2", "react-autocomplete": "^1.7.2",
"react-codemirror2": "^3.0.7", "react-codemirror2": "^4.2.1",
"react-collapse": "^4.0.3", "react-collapse": "^4.0.3",
"react-color": "^2.13.8", "react-color": "^2.14.1",
"react-copy-to-clipboard": "^5.0.1", "react-copy-to-clipboard": "^5.0.1",
"react-dom": "16.0.0", "react-dom": "^16.3.2",
"react-file-reader-input": "^1.1.4", "react-file-reader-input": "^1.1.4",
"react-height": "^3.0.0", "react-height": "^3.0.0",
"react-icon-base": "^2.1.1", "react-icon-base": "^2.1.1",
@@ -54,14 +58,25 @@
"react-motion": "^0.5.2", "react-motion": "^0.5.2",
"react-sortable-hoc": "^0.6.8", "react-sortable-hoc": "^0.6.8",
"reconnecting-websocket": "^3.2.2", "reconnecting-websocket": "^3.2.2",
"request": "^2.83.0", "request": "^2.85.0",
"url": "^0.11.0" "url": "^0.11.0"
}, },
"jshintConfig": { "jshintConfig": {
"esversion": 6 "esversion": 6
}, },
"stylelint": { "stylelint": {
"extends": "stylelint-config-standard" "extends": "stylelint-config-recommended-scss",
"rules": {
"no-descending-specificity": null,
"media-feature-name-no-unknown": [
true,
{
"ignoreMediaFeatureNames": [
"prefers-reduced-motion"
]
}
]
}
}, },
"eslintConfig": { "eslintConfig": {
"plugins": [ "plugins": [
@@ -87,45 +102,55 @@
} }
}, },
"devDependencies": { "devDependencies": {
"babel-core": "^6.26.0", "babel-core": "^6.26.3",
"babel-eslint": "^8.0.2", "babel-eslint": "^8.2.3",
"babel-loader": "7.1.1", "babel-loader": "7.1.4",
"babel-plugin-istanbul": "^4.1.6",
"babel-plugin-transform-class-properties": "^6.24.1", "babel-plugin-transform-class-properties": "^6.24.1",
"babel-plugin-transform-decorators-legacy": "^1.3.4", "babel-plugin-transform-decorators-legacy": "^1.3.4",
"babel-plugin-transform-flow-strip-types": "^6.22.0", "babel-plugin-transform-flow-strip-types": "^6.22.0",
"babel-plugin-transform-object-rest-spread": "^6.26.0", "babel-plugin-transform-object-rest-spread": "^6.26.0",
"babel-plugin-transform-runtime": "^6.23.0", "babel-plugin-transform-runtime": "^6.23.0",
"babel-preset-env": "^1.6.1", "babel-preset-env": "^1.6.1",
"babel-preset-es2015": "^6.24.1",
"babel-preset-flow": "^6.23.0", "babel-preset-flow": "^6.23.0",
"babel-preset-react": "^6.24.1", "babel-preset-react": "^6.24.1",
"babel-register": "^6.26.0",
"babel-runtime": "^6.26.0", "babel-runtime": "^6.26.0",
"base64-loader": "^1.0.0", "base64-loader": "^1.0.0",
"copy-webpack-plugin": "^4.2.0", "copy-webpack-plugin": "^4.5.1",
"css-loader": "^0.28.7", "cors": "^2.8.4",
"eslint": "^4.10.0", "cross-env": "^5.1.4",
"css-loader": "^0.28.11",
"eslint": "^4.19.1",
"eslint-plugin-react": "^7.4.0", "eslint-plugin-react": "^7.4.0",
"express": "^4.16.3",
"extract-text-webpack-plugin": "^3.0.2", "extract-text-webpack-plugin": "^3.0.2",
"file-loader": "^1.1.5", "file-loader": "^1.1.5",
"html-webpack-plugin": "^2.30.1", "html-webpack-plugin": "^3.2.0",
"is-docker": "^1.1.0",
"istanbul": "^0.4.5",
"istanbul-lib-coverage": "^1.2.0",
"json-loader": "^0.5.7", "json-loader": "^0.5.7",
"karma": "^1.7.1", "mkdirp": "^0.5.1",
"karma-chrome-launcher": "^2.2.0", "mocha": "^5.1.1",
"karma-firefox-launcher": "^1.0.1", "node-sass": "^4.9.0",
"karma-mocha": "^1.3.0", "nsp": "^3.1.0",
"karma-webpack": "^2.0.5",
"mocha": "^4.0.1",
"mocha-loader": "^1.1.1",
"node-sass": "^4.6.0",
"react-hot-loader": "^3.1.1", "react-hot-loader": "^3.1.1",
"sass-loader": "^6.0.6", "sass-loader": "^7.0.1",
"style-loader": "^0.19.0", "selenium-standalone": "^6.14.0",
"stylelint": "^7.13.0", "style-loader": "^0.20.3",
"stylelint-config-standard": "^15.0.1", "stylelint": "^9.2.0",
"stylelint-config-recommended-scss": "^3.2.0",
"stylelint-scss": "^3.0.0",
"transform-loader": "^0.2.4", "transform-loader": "^0.2.4",
"wdio-mocha-framework": "^0.5.11", "uglifyjs-webpack-plugin": "^1.2.4",
"uuid": "^3.1.0",
"wdio-mocha-framework": "^0.5.13",
"wdio-phantomjs-service": "^0.2.2", "wdio-phantomjs-service": "^0.2.2",
"wdio-selenium-standalone-service": "0.0.10",
"wdio-spec-reporter": "^0.1.2", "wdio-spec-reporter": "^0.1.2",
"webdriverio": "^4.8.0", "webdriverio": "^4.12.0",
"webpack": "^3.8.1", "webpack": "^3.8.1",
"webpack-bundle-analyzer": "^2.9.0", "webpack-bundle-analyzer": "^2.9.0",
"webpack-cleanup-plugin": "^0.5.1", "webpack-cleanup-plugin": "^0.5.1",

View File

@@ -1,5 +1,9 @@
import React from 'react' import React from 'react'
import Mousetrap from 'mousetrap' import Mousetrap from 'mousetrap'
import cloneDeep from 'lodash.clonedeep'
import clamp from 'lodash.clamp'
import {arrayMove} from 'react-sortable-hoc'
import url from 'url'
import MapboxGlMap from './map/MapboxGlMap' import MapboxGlMap from './map/MapboxGlMap'
import OpenLayers3Map from './map/OpenLayers3Map' import OpenLayers3Map from './map/OpenLayers3Map'
@@ -9,8 +13,15 @@ import Toolbar from './Toolbar'
import AppLayout from './AppLayout' import AppLayout from './AppLayout'
import MessagePanel from './MessagePanel' import MessagePanel from './MessagePanel'
import SettingsModal from './modals/SettingsModal'
import ExportModal from './modals/ExportModal'
import SourcesModal from './modals/SourcesModal'
import OpenModal from './modals/OpenModal'
import ShortcutsModal from './modals/ShortcutsModal'
import SurveyModal from './modals/SurveyModal'
import { downloadGlyphsMetadata, downloadSpriteMetadata } from '../libs/metadata' import { downloadGlyphsMetadata, downloadSpriteMetadata } from '../libs/metadata'
import styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec' import * as styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
import style from '../libs/style.js' import style from '../libs/style.js'
import { initialStyleUrl, loadStyleUrl } from '../libs/urlopen' import { initialStyleUrl, loadStyleUrl } from '../libs/urlopen'
import { undoMessages, redoMessages } from '../libs/diffmessage' import { undoMessages, redoMessages } from '../libs/diffmessage'
@@ -20,9 +31,11 @@ import { RevisionStore } from '../libs/revisions'
import LayerWatcher from '../libs/layerwatcher' import LayerWatcher from '../libs/layerwatcher'
import tokens from '../config/tokens.json' import tokens from '../config/tokens.json'
import isEqual from 'lodash.isequal' import isEqual from 'lodash.isequal'
import Debug from '../libs/debug'
import queryUtil from '../libs/query-util'
import MapboxGl from 'mapbox-gl' import MapboxGl from 'mapbox-gl'
import mapboxUtil from 'mapbox-gl/src/util/mapbox' import { normalizeSourceURL } from 'mapbox-gl/src/util/mapbox'
function updateRootSpec(spec, fieldName, newValues) { function updateRootSpec(spec, fieldName, newValues) {
@@ -46,6 +59,79 @@ export default class App extends React.Component {
onLocalStyleChange: mapStyle => this.onStyleChanged(mapStyle, false) onLocalStyleChange: mapStyle => this.onStyleChanged(mapStyle, false)
}) })
const keyCodes = {
"esc": 27,
"?": 191,
"o": 79,
"e": 69,
"s": 83,
"d": 68,
"i": 73,
"m": 77,
}
const shortcuts = [
{
keyCode: keyCodes["?"],
handler: () => {
this.toggleModal("shortcuts");
}
},
{
keyCode: keyCodes["o"],
handler: () => {
this.toggleModal("open");
}
},
{
keyCode: keyCodes["e"],
handler: () => {
this.toggleModal("export");
}
},
{
keyCode: keyCodes["d"],
handler: () => {
this.toggleModal("sources");
}
},
{
keyCode: keyCodes["s"],
handler: () => {
this.toggleModal("settings");
}
},
{
keyCode: keyCodes["i"],
handler: () => {
this.changeInspectMode();
}
},
{
keyCode: keyCodes["m"],
handler: () => {
document.querySelector(".mapboxgl-canvas").focus();
}
},
]
document.body.addEventListener("keyup", (e) => {
if(e.keyCode === keyCodes["esc"]) {
e.target.blur();
document.body.focus();
}
else if(document.activeElement === document.body) {
const shortcut = shortcuts.find((shortcut) => {
return (shortcut.keyCode === e.keyCode)
})
if(shortcut) {
shortcut.handler(e);
}
}
})
const styleUrl = initialStyleUrl() const styleUrl = initialStyleUrl()
if(styleUrl) { if(styleUrl) {
this.styleStore = new StyleStore() this.styleStore = new StyleStore()
@@ -57,9 +143,21 @@ export default class App extends React.Component {
this.styleStore = new StyleStore() this.styleStore = new StyleStore()
} }
this.styleStore.latestStyle(mapStyle => this.onStyleChanged(mapStyle)) this.styleStore.latestStyle(mapStyle => this.onStyleChanged(mapStyle))
if(Debug.enabled()) {
Debug.set("maputnik", "styleStore", this.styleStore);
Debug.set("maputnik", "revisionStore", this.revisionStore);
}
}) })
} }
if(Debug.enabled()) {
Debug.set("maputnik", "revisionStore", this.revisionStore);
Debug.set("maputnik", "styleStore", this.styleStore);
}
const queryObj = url.parse(window.location.href, true).query;
this.state = { this.state = {
errors: [], errors: [],
infos: [], infos: [],
@@ -69,6 +167,18 @@ export default class App extends React.Component {
vectorLayers: {}, vectorLayers: {},
inspectModeEnabled: false, inspectModeEnabled: false,
spec: styleSpec.latest, spec: styleSpec.latest,
isOpen: {
settings: false,
sources: false,
open: false,
shortcuts: false,
export: false,
survey: localStorage.hasOwnProperty('survey') ? false : true
},
mapOptions: {
showTileBoundaries: queryUtil.asBool(queryObj, "show-tile-boundaries")
},
mapFilter: queryObj["color-blindness-emulation"],
} }
this.layerWatcher = new LayerWatcher({ this.layerWatcher = new LayerWatcher({
@@ -77,19 +187,13 @@ export default class App extends React.Component {
} }
componentDidMount() { componentDidMount() {
this.fetchSources(); Mousetrap.bind(['mod+z'], this.onUndo.bind(this));
Mousetrap.bind(['ctrl+z'], this.onUndo.bind(this)); Mousetrap.bind(['mod+y', 'mod+shift+z'], this.onRedo.bind(this));
Mousetrap.bind(['ctrl+y'], this.onRedo.bind(this));
} }
componentWillUnmount() { componentWillUnmount() {
Mousetrap.unbind(['ctrl+z'], this.onUndo.bind(this)); Mousetrap.unbind(['mod+z'], this.onUndo.bind(this));
Mousetrap.unbind(['ctrl+y'], this.onRedo.bind(this)); Mousetrap.unbind(['mod+y', 'mod+shift+z'], this.onRedo.bind(this));
}
onReset() {
this.styleStore.purge()
loadDefaultStyle(mapStyle => this.onStyleOpen(mapStyle))
} }
saveStyle(snapshotStyle) { saveStyle(snapshotStyle) {
@@ -99,7 +203,9 @@ export default class App extends React.Component {
updateFonts(urlTemplate) { updateFonts(urlTemplate) {
const metadata = this.state.mapStyle.metadata || {} const metadata = this.state.mapStyle.metadata || {}
const accessToken = metadata['maputnik:openmaptiles_access_token'] || tokens.openmaptiles const accessToken = metadata['maputnik:openmaptiles_access_token'] || tokens.openmaptiles
downloadGlyphsMetadata(urlTemplate.replace('{key}', accessToken), fonts => {
let glyphUrl = (typeof urlTemplate === 'string')? urlTemplate.replace('{key}', accessToken): urlTemplate;
downloadGlyphsMetadata(glyphUrl, fonts => {
this.setState({ spec: updateRootSpec(this.state.spec, 'glyphs', fonts)}) this.setState({ spec: updateRootSpec(this.state.spec, 'glyphs', fonts)})
}) })
} }
@@ -111,6 +217,10 @@ export default class App extends React.Component {
} }
onStyleChanged(newStyle, save=true) { onStyleChanged(newStyle, save=true) {
const errors = styleSpec.validate(newStyle, styleSpec.latest)
if(errors.length === 0) {
if(newStyle.glyphs !== this.state.mapStyle.glyphs) { if(newStyle.glyphs !== this.state.mapStyle.glyphs) {
this.updateFonts(newStyle.glyphs) this.updateFonts(newStyle.glyphs)
} }
@@ -118,8 +228,6 @@ export default class App extends React.Component {
this.updateIcons(newStyle.sprite) this.updateIcons(newStyle.sprite)
} }
const errors = styleSpec.validate(newStyle, styleSpec.latest)
if(errors.length === 0) {
this.revisionStore.addRevision(newStyle) this.revisionStore.addRevision(newStyle)
if(save) this.saveStyle(newStyle) if(save) this.saveStyle(newStyle)
this.setState({ this.setState({
@@ -155,6 +263,24 @@ export default class App extends React.Component {
}) })
} }
onMoveLayer(move) {
let { oldIndex, newIndex } = move;
let layers = this.state.mapStyle.layers;
oldIndex = clamp(oldIndex, 0, layers.length-1);
newIndex = clamp(newIndex, 0, layers.length-1);
if(oldIndex === newIndex) return;
if (oldIndex === this.state.selectedLayerIndex) {
this.setState({
selectedLayerIndex: newIndex
});
}
layers = layers.slice(0);
layers = arrayMove(layers, oldIndex, newIndex);
this.onLayersChange(layers);
}
onLayersChange(changedLayers) { onLayersChange(changedLayers) {
const changedStyle = { const changedStyle = {
...this.state.mapStyle, ...this.state.mapStyle,
@@ -163,6 +289,40 @@ export default class App extends React.Component {
this.onStyleChanged(changedStyle) this.onStyleChanged(changedStyle)
} }
onLayerDestroy(layerId) {
let layers = this.state.mapStyle.layers;
const remainingLayers = layers.slice(0);
const idx = style.indexOfLayer(remainingLayers, layerId)
remainingLayers.splice(idx, 1);
this.onLayersChange(remainingLayers);
}
onLayerCopy(layerId) {
let layers = this.state.mapStyle.layers;
const changedLayers = layers.slice(0)
const idx = style.indexOfLayer(changedLayers, layerId)
const clonedLayer = cloneDeep(changedLayers[idx])
clonedLayer.id = clonedLayer.id + "-copy"
changedLayers.splice(idx, 0, clonedLayer)
this.onLayersChange(changedLayers)
}
onLayerVisibilityToggle(layerId) {
let layers = this.state.mapStyle.layers;
const changedLayers = layers.slice(0)
const idx = style.indexOfLayer(changedLayers, layerId)
const layer = { ...changedLayers[idx] }
const changedLayout = 'layout' in layer ? {...layer.layout} : {}
changedLayout.visibility = changedLayout.visibility === 'none' ? 'visible' : 'none'
layer.layout = changedLayout
changedLayers[idx] = layer
this.onLayersChange(changedLayers)
}
onLayerIdChange(oldId, newId) { onLayerIdChange(oldId, newId) {
const changedLayers = this.state.mapStyle.layers.slice(0) const changedLayers = this.state.mapStyle.layers.slice(0)
const idx = style.indexOfLayer(changedLayers, oldId) const idx = style.indexOfLayer(changedLayers, oldId)
@@ -202,10 +362,10 @@ export default class App extends React.Component {
layers: [] layers: []
}; };
if(!this.state.sources.hasOwnProperty(key) && val.type === "vector") { if(!this.state.sources.hasOwnProperty(key) && val.type === "vector" && val.hasOwnProperty("url")) {
let url = val.url; let url = val.url;
try { try {
url = mapboxUtil.normalizeSourceURL(url, MapboxGl.accessToken); url = normalizeSourceURL(url, MapboxGl.accessToken);
} catch(err) { } catch(err) {
console.warn("Failed to normalizeSourceURL: ", err); console.warn("Failed to normalizeSourceURL: ", err);
} }
@@ -215,6 +375,10 @@ export default class App extends React.Component {
return response.json(); return response.json();
}) })
.then((json) => { .then((json) => {
if(!json.hasOwnProperty("vector_layers")) {
return;
}
// Create new objects before setState // Create new objects before setState
const sources = Object.assign({}, this.state.sources); const sources = Object.assign({}, this.state.sources);
@@ -243,7 +407,8 @@ export default class App extends React.Component {
mapRenderer() { mapRenderer() {
const mapProps = { const mapProps = {
mapStyle: style.replaceAccessToken(this.state.mapStyle), mapStyle: style.replaceAccessToken(this.state.mapStyle, {allowFallback: true}),
options: this.state.mapOptions,
onDataChange: (e) => { onDataChange: (e) => {
this.layerWatcher.analyzeMap(e.map) this.layerWatcher.analyzeMap(e.map)
this.fetchSources(); this.fetchSources();
@@ -253,15 +418,26 @@ export default class App extends React.Component {
const metadata = this.state.mapStyle.metadata || {} const metadata = this.state.mapStyle.metadata || {}
const renderer = metadata['maputnik:renderer'] || 'mbgljs' const renderer = metadata['maputnik:renderer'] || 'mbgljs'
let mapElement;
// Check if OL3 code has been loaded? // Check if OL3 code has been loaded?
if(renderer === 'ol3') { if(renderer === 'ol3') {
return <OpenLayers3Map {...mapProps} /> mapElement = <OpenLayers3Map {...mapProps} />
} else { } else {
return <MapboxGlMap {...mapProps} mapElement = <MapboxGlMap {...mapProps}
inspectModeEnabled={this.state.inspectModeEnabled} inspectModeEnabled={this.state.inspectModeEnabled}
highlightedLayer={this.state.mapStyle.layers[this.state.selectedLayerIndex]} highlightedLayer={this.state.mapStyle.layers[this.state.selectedLayerIndex]}
onLayerSelect={this.onLayerSelect.bind(this)} /> onLayerSelect={this.onLayerSelect.bind(this)} />
} }
const elementStyle = {};
if(this.state.mapFilter) {
elementStyle.filter = `url('#${this.state.mapFilter}')`;
}
return <div style={elementStyle}>
{mapElement}
</div>
} }
onLayerSelect(layerId) { onLayerSelect(layerId) {
@@ -269,6 +445,19 @@ export default class App extends React.Component {
this.setState({ selectedLayerIndex: idx }) this.setState({ selectedLayerIndex: idx })
} }
toggleModal(modalName) {
this.setState({
isOpen: {
...this.state.isOpen,
[modalName]: !this.state.isOpen[modalName]
}
})
if(modalName === 'survey') {
localStorage.setItem('survey', '');
}
}
render() { render() {
const layers = this.state.mapStyle.layers || [] const layers = this.state.mapStyle.layers || []
const selectedLayer = layers.length > 0 ? layers[this.state.selectedLayerIndex] : null const selectedLayer = layers.length > 0 ? layers[this.state.selectedLayerIndex] : null
@@ -281,9 +470,14 @@ export default class App extends React.Component {
onStyleChanged={this.onStyleChanged.bind(this)} onStyleChanged={this.onStyleChanged.bind(this)}
onStyleOpen={this.onStyleChanged.bind(this)} onStyleOpen={this.onStyleChanged.bind(this)}
onInspectModeToggle={this.changeInspectMode.bind(this)} onInspectModeToggle={this.changeInspectMode.bind(this)}
onToggleModal={this.toggleModal.bind(this)}
/> />
const layerList = <LayerList const layerList = <LayerList
onMoveLayer={this.onMoveLayer.bind(this)}
onLayerDestroy={this.onLayerDestroy.bind(this)}
onLayerCopy={this.onLayerCopy.bind(this)}
onLayerVisibilityToggle={this.onLayerVisibilityToggle.bind(this)}
onLayersChange={this.onLayersChange.bind(this)} onLayersChange={this.onLayersChange.bind(this)}
onLayerSelect={this.onLayerSelect.bind(this)} onLayerSelect={this.onLayerSelect.bind(this)}
selectedLayerIndex={this.state.selectedLayerIndex} selectedLayerIndex={this.state.selectedLayerIndex}
@@ -293,10 +487,17 @@ export default class App extends React.Component {
const layerEditor = selectedLayer ? <LayerEditor const layerEditor = selectedLayer ? <LayerEditor
layer={selectedLayer} layer={selectedLayer}
layerIndex={this.state.selectedLayerIndex}
isFirstLayer={this.state.selectedLayerIndex < 1}
isLastLayer={this.state.selectedLayerIndex === this.state.mapStyle.layers.length-1}
sources={this.state.sources} sources={this.state.sources}
vectorLayers={this.state.vectorLayers} vectorLayers={this.state.vectorLayers}
spec={this.state.spec} spec={this.state.spec}
onMoveLayer={this.onMoveLayer.bind(this)}
onLayerChanged={this.onLayerChanged.bind(this)} onLayerChanged={this.onLayerChanged.bind(this)}
onLayerDestroy={this.onLayerDestroy.bind(this)}
onLayerCopy={this.onLayerCopy.bind(this)}
onLayerVisibilityToggle={this.onLayerVisibilityToggle.bind(this)}
onLayerIdChange={this.onLayerIdChange.bind(this)} onLayerIdChange={this.onLayerIdChange.bind(this)}
/> : null /> : null
@@ -305,12 +506,48 @@ export default class App extends React.Component {
infos={this.state.infos} infos={this.state.infos}
/> : null /> : null
const modals = <div>
<ShortcutsModal
isOpen={this.state.isOpen.shortcuts}
onOpenToggle={this.toggleModal.bind(this, 'shortcuts')}
/>
<SettingsModal
mapStyle={this.state.mapStyle}
onStyleChanged={this.onStyleChanged.bind(this)}
isOpen={this.state.isOpen.settings}
onOpenToggle={this.toggleModal.bind(this, 'settings')}
/>
<ExportModal
mapStyle={this.state.mapStyle}
onStyleChanged={this.onStyleChanged.bind(this)}
isOpen={this.state.isOpen.export}
onOpenToggle={this.toggleModal.bind(this, 'export')}
/>
<OpenModal
isOpen={this.state.isOpen.open}
onStyleOpen={this.onStyleChanged.bind(this)}
onOpenToggle={this.toggleModal.bind(this, 'open')}
/>
<SourcesModal
mapStyle={this.state.mapStyle}
onStyleChanged={this.onStyleChanged.bind(this)}
isOpen={this.state.isOpen.sources}
onOpenToggle={this.toggleModal.bind(this, 'sources')}
/>
<SurveyModal
isOpen={this.state.isOpen.survey}
onOpenToggle={this.toggleModal.bind(this, 'survey')}
/>
</div>
return <AppLayout return <AppLayout
toolbar={toolbar} toolbar={toolbar}
layerList={layerList} layerList={layerList}
layerEditor={layerEditor} layerEditor={layerEditor}
map={this.mapRenderer()} map={this.mapRenderer()}
bottom={bottomPanel} bottom={bottomPanel}
modals={modals}
/> />
} }
} }

View File

@@ -9,6 +9,7 @@ class AppLayout extends React.Component {
layerEditor: PropTypes.element, layerEditor: PropTypes.element,
map: PropTypes.element.isRequired, map: PropTypes.element.isRequired,
bottom: PropTypes.element, bottom: PropTypes.element,
modals: PropTypes.node,
} }
static childContextTypes = { static childContextTypes = {
@@ -39,6 +40,7 @@ class AppLayout extends React.Component {
{this.props.bottom} {this.props.bottom}
</div> </div>
} }
{this.props.modals}
</div> </div>
} }
} }

View File

@@ -4,6 +4,8 @@ import classnames from 'classnames'
class Button extends React.Component { class Button extends React.Component {
static propTypes = { static propTypes = {
"data-wd-key": PropTypes.string,
"aria-label": PropTypes.string,
onClick: PropTypes.func, onClick: PropTypes.func,
style: PropTypes.object, style: PropTypes.object,
className: PropTypes.string, className: PropTypes.string,
@@ -11,12 +13,14 @@ class Button extends React.Component {
} }
render() { render() {
return <a return <button
onClick={this.props.onClick} onClick={this.props.onClick}
aria-label={this.props["aria-label"]}
className={classnames("maputnik-button", this.props.className)} className={classnames("maputnik-button", this.props.className)}
data-wd-key={this.props["data-wd-key"]}
style={this.props.style}> style={this.props.style}>
{this.props.children} {this.props.children}
</a> </button>
} }
} }

View File

@@ -16,12 +16,10 @@ import MdInsertEmoticon from 'react-icons/lib/md/insert-emoticon'
import MdFontDownload from 'react-icons/lib/md/font-download' import MdFontDownload from 'react-icons/lib/md/font-download'
import HelpIcon from 'react-icons/lib/md/help-outline' import HelpIcon from 'react-icons/lib/md/help-outline'
import InspectionIcon from 'react-icons/lib/md/find-in-page' import InspectionIcon from 'react-icons/lib/md/find-in-page'
import SurveyIcon from 'react-icons/lib/md/assignment-turned-in'
import logoImage from 'maputnik-design/logos/logo-color.svg' import logoImage from 'maputnik-design/logos/logo-color.svg'
import SettingsModal from './modals/SettingsModal' import pkgJson from '../../package.json'
import ExportModal from './modals/ExportModal'
import SourcesModal from './modals/SourcesModal'
import OpenModal from './modals/OpenModal'
import style from '../libs/style' import style from '../libs/style'
@@ -40,6 +38,7 @@ class ToolbarLink extends React.Component {
className: PropTypes.string, className: PropTypes.string,
children: PropTypes.node, children: PropTypes.node,
href: PropTypes.string, href: PropTypes.string,
onToggleModal: PropTypes.func,
} }
render() { render() {
@@ -54,19 +53,43 @@ class ToolbarLink extends React.Component {
} }
} }
class ToolbarAction extends React.Component { class ToolbarLinkHighlighted extends React.Component {
static propTypes = { static propTypes = {
className: PropTypes.string,
children: PropTypes.node, children: PropTypes.node,
onClick: PropTypes.func href: PropTypes.string,
onToggleModal: PropTypes.func
} }
render() { render() {
return <a return <a
className={classnames('maputnik-toolbar-link', "maputnik-toolbar-link--highlighted", this.props.className)}
href={this.props.href}
rel="noopener noreferrer"
target="_blank"
>
<span className="maputnik-toolbar-link-wrapper">
{this.props.children}
</span>
</a>
}
}
class ToolbarAction extends React.Component {
static propTypes = {
children: PropTypes.node,
onClick: PropTypes.func,
wdKey: PropTypes.string
}
render() {
return <button
className='maputnik-toolbar-action' className='maputnik-toolbar-action'
data-wd-key={this.props.wdKey}
onClick={this.props.onClick} onClick={this.props.onClick}
> >
{this.props.children} {this.props.children}
</a> </button>
} }
} }
@@ -80,7 +103,8 @@ export default class Toolbar extends React.Component {
// A dict of source id's and the available source layers // A dict of source id's and the available source layers
sources: PropTypes.object.isRequired, sources: PropTypes.object.isRequired,
onInspectModeToggle: PropTypes.func.isRequired, onInspectModeToggle: PropTypes.func.isRequired,
children: PropTypes.node children: PropTypes.node,
onToggleModal: PropTypes.func,
} }
constructor(props) { constructor(props) {
@@ -96,66 +120,45 @@ export default class Toolbar extends React.Component {
} }
} }
toggleModal(modalName) {
this.setState({
isOpen: {
...this.state.isOpen,
[modalName]: !this.state.isOpen[modalName]
}
})
}
render() { render() {
return <div className='maputnik-toolbar'> 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}
onOpenToggle={this.toggleModal.bind(this, 'open')}
/>
<SourcesModal
mapStyle={this.props.mapStyle}
onStyleChanged={this.props.onStyleChanged}
isOpen={this.state.isOpen.sources}
onOpenToggle={this.toggleModal.bind(this, 'sources')}
/>
<div className="maputnik-toolbar__inner"> <div className="maputnik-toolbar__inner">
<ToolbarLink <div
href={"https://github.com/maputnik/editor"} className="maputnik-toolbar-logo-container"
>
<a className="maputnik-toolbar-skip" href="#skip-menu">
Skip navigation
</a>
<a
href="https://github.com/maputnik/editor"
rel="noopener noreferrer"
target="_blank"
className="maputnik-toolbar-logo" className="maputnik-toolbar-logo"
> >
<img src={logoImage} alt="Maputnik" /> <img src={logoImage} alt="Maputnik" />
<h1>Maputnik</h1> <h1>Maputnik
</ToolbarLink> <span className="maputnik-toolbar-version">v{pkgJson.version}</span>
</h1>
</a>
</div>
<div className="maputnik-toolbar__actions"> <div className="maputnik-toolbar__actions">
<ToolbarAction onClick={this.toggleModal.bind(this, 'open')}> <ToolbarAction wdKey="nav:open" onClick={this.props.onToggleModal.bind(this, 'open')}>
<OpenIcon /> <OpenIcon />
<IconText>Open</IconText> <IconText>Open</IconText>
</ToolbarAction> </ToolbarAction>
<ToolbarAction onClick={this.toggleModal.bind(this, 'export')}> <ToolbarAction wdKey="nav:export" onClick={this.props.onToggleModal.bind(this, 'export')}>
<MdFileDownload /> <MdFileDownload />
<IconText>Export</IconText> <IconText>Export</IconText>
</ToolbarAction> </ToolbarAction>
<ToolbarAction onClick={this.toggleModal.bind(this, 'sources')}> <ToolbarAction wdKey="nav:sources" onClick={this.props.onToggleModal.bind(this, 'sources')}>
<SourcesIcon /> <SourcesIcon />
<IconText>Sources</IconText> <IconText>Data Sources</IconText>
</ToolbarAction> </ToolbarAction>
<ToolbarAction onClick={this.toggleModal.bind(this, 'settings')}> <ToolbarAction wdKey="nav:settings" onClick={this.props.onToggleModal.bind(this, 'settings')}>
<SettingsIcon /> <SettingsIcon />
<IconText>Style Settings</IconText> <IconText>Style Settings</IconText>
</ToolbarAction> </ToolbarAction>
<ToolbarAction onClick={this.props.onInspectModeToggle}> <ToolbarAction wdKey="nav:inspect" onClick={this.props.onInspectModeToggle}>
<InspectionIcon /> <InspectionIcon />
<IconText> <IconText>
{ this.props.inspectModeEnabled && <span>Map Mode</span> } { this.props.inspectModeEnabled && <span>Map Mode</span> }
@@ -166,6 +169,10 @@ export default class Toolbar extends React.Component {
<HelpIcon /> <HelpIcon />
<IconText>Help</IconText> <IconText>Help</IconText>
</ToolbarLink> </ToolbarLink>
<ToolbarLinkHighlighted href={"https://gregorywolanski.typeform.com/to/cPgaSY"}>
<SurveyIcon />
<IconText>Take the Maputnik Survey</IconText>
</ToolbarLinkHighlighted>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -105,6 +105,7 @@ class ColorField extends React.Component {
{this.state.pickerOpened && picker} {this.state.pickerOpened && picker}
<div className="maputnik-color-swatch" style={swatchStyle}></div> <div className="maputnik-color-swatch" style={swatchStyle}></div>
<input <input
spellCheck="false"
className="maputnik-color" className="maputnik-color"
ref={(input) => this.colorInput = input} ref={(input) => this.colorInput = input}
onClick={this.togglePicker.bind(this)} onClick={this.togglePicker.bind(this)}

View File

@@ -131,8 +131,7 @@ export default class FunctionSpecProperty extends React.Component {
/> />
) )
} }
return <div className={propClass} data-wd-key={"spec-field:"+this.props.fieldName}>
return <div className={propClass}>
{specField} {specField}
</div> </div>
} }

View File

@@ -58,6 +58,8 @@ export default class SpecField extends React.Component {
name: this.props.fieldName, name: this.props.fieldName,
onChange: newValue => this.props.onChange(this.props.fieldName, newValue) onChange: newValue => this.props.onChange(this.props.fieldName, newValue)
} }
function childNodes() {
switch(this.props.fieldSpec.type) { switch(this.props.fieldSpec.type) {
case 'number': return ( case 'number': return (
<NumberInput <NumberInput
@@ -124,4 +126,11 @@ export default class SpecField extends React.Component {
default: return null default: return null
} }
} }
return (
<div data-wd-key={"spec-field:"+this.props.fieldName}>
{childNodes.call(this)}
</div>
);
}
} }

View File

@@ -51,7 +51,8 @@ export default class DataProperty extends React.Component {
changeStop(changeIdx, stopData, value) { changeStop(changeIdx, stopData, value) {
const stops = this.props.value.stops.slice(0) const stops = this.props.value.stops.slice(0)
stops[changeIdx] = [stopData, value] const changedStop = stopData.zoom === undefined ? stopData.value : stopData
stops[changeIdx] = [changedStop, value]
const changedValue = { const changedValue = {
...this.props.value, ...this.props.value,
stops: stops, stops: stops,
@@ -75,8 +76,8 @@ export default class DataProperty extends React.Component {
} }
const dataFields = this.props.value.stops.map((stop, idx) => { const dataFields = this.props.value.stops.map((stop, idx) => {
const zoomLevel = stop[0].zoom const zoomLevel = typeof stop[0] === 'object' ? stop[0].zoom : undefined;
const dataLevel = stop[0].value const dataLevel = typeof stop[0] === 'object' ? stop[0].value : stop[0];
const value = stop[1] const value = stop[1]
const deleteStopBtn = <DeleteStopButton onClick={this.props.onDeleteStop.bind(this, idx)} /> const deleteStopBtn = <DeleteStopButton onClick={this.props.onDeleteStop.bind(this, idx)} />
@@ -94,8 +95,9 @@ export default class DataProperty extends React.Component {
dataInput = <NumberInput {...dataProps} /> dataInput = <NumberInput {...dataProps} />
} }
return <InputBlock key={idx} action={deleteStopBtn} label=""> let zoomInput = null;
<div className="maputnik-data-spec-property-stop-edit"> if(zoomLevel !== undefined) {
zoomInput = <div className="maputnik-data-spec-property-stop-edit">
<NumberInput <NumberInput
value={zoomLevel} value={zoomLevel}
onChange={newZoom => this.changeStop(idx, {zoom: newZoom, value: dataLevel}, value)} onChange={newZoom => this.changeStop(idx, {zoom: newZoom, value: dataLevel}, value)}
@@ -103,6 +105,10 @@ export default class DataProperty extends React.Component {
max={22} max={22}
/> />
</div> </div>
}
return <InputBlock key={idx} action={deleteStopBtn} label="">
{zoomInput}
<div className="maputnik-data-spec-property-stop-data"> <div className="maputnik-data-spec-property-stop-data">
{dataInput} {dataInput}
</div> </div>

View File

@@ -37,7 +37,7 @@ export default class ZoomProperty extends React.Component {
} }
} }
componentWillMount() { componentDidMount() {
this.setState({ this.setState({
refs: this.setStopRefs(this.props) refs: this.setStopRefs(this.props)
}) })
@@ -66,7 +66,7 @@ export default class ZoomProperty extends React.Component {
return newRefs; return newRefs;
} }
componentWillReceiveProps(nextProps) { UNSAFE_componentWillReceiveProps(nextProps) {
const newRefs = this.setStopRefs(nextProps); const newRefs = this.setStopRefs(nextProps);
if(newRefs) { if(newRefs) {
this.setState({ this.setState({

View File

@@ -2,7 +2,7 @@ import React from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { combiningFilterOps } from '../../libs/filterops.js' import { combiningFilterOps } from '../../libs/filterops.js'
import styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec' import * as styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
import DocLabel from '../fields/DocLabel' import DocLabel from '../fields/DocLabel'
import SelectInput from '../inputs/SelectInput' import SelectInput from '../inputs/SelectInput'
import SingleFilterEditor from './SingleFilterEditor' import SingleFilterEditor from './SingleFilterEditor'
@@ -89,7 +89,7 @@ export default class CombiningFilterEditor extends React.Component {
} }
return <div className="maputnik-filter-editor"> return <div className="maputnik-filter-editor">
<div className="maputnik-filter-editor-compound-select"> <div className="maputnik-filter-editor-compound-select" data-wd-key="layer-filter">
<DocLabel <DocLabel
label={"Compound Filter"} label={"Compound Filter"}
doc={styleSpec.latest.layer.filter.doc + " Combine multiple filters together by using a compound filter."} doc={styleSpec.latest.layer.filter.doc + " Combine multiple filters together by using a compound filter."}
@@ -103,6 +103,7 @@ export default class CombiningFilterEditor extends React.Component {
{editorBlocks} {editorBlocks}
<div className="maputnik-filter-editor-add-wrapper"> <div className="maputnik-filter-editor-add-wrapper">
<Button <Button
data-wd-key="layer-filter-button"
className="maputnik-add-filter" className="maputnik-add-filter"
onClick={this.addFilterItem.bind(this)}> onClick={this.addFilterItem.bind(this)}>
Add filter Add filter

View File

@@ -18,12 +18,13 @@ class LayerIcon extends React.Component {
switch(this.props.type) { switch(this.props.type) {
case 'fill-extrusion': return <BackgroundIcon {...iconProps} /> case 'fill-extrusion': return <BackgroundIcon {...iconProps} />
case 'raster': return <FillIcon {...iconProps} /> case 'raster': return <FillIcon {...iconProps} />
case 'hillshade': return <FillIcon {...iconProps} />
case 'heatmap': return <FillIcon {...iconProps} />
case 'fill': return <FillIcon {...iconProps} /> case 'fill': return <FillIcon {...iconProps} />
case 'background': return <BackgroundIcon {...iconProps} /> case 'background': return <BackgroundIcon {...iconProps} />
case 'line': return <LineIcon {...iconProps} /> case 'line': return <LineIcon {...iconProps} />
case 'symbol': return <SymbolIcon {...iconProps} /> case 'symbol': return <SymbolIcon {...iconProps} />
case 'circle': return <CircleIcon {...iconProps} /> case 'circle': return <CircleIcon {...iconProps} />
default: return null
} }
} }
} }

View File

@@ -54,7 +54,7 @@ class AutocompleteInput extends React.Component {
> >
<Autocomplete <Autocomplete
menuStyle={{ menuStyle={{
position: "absolute", position: "fixed",
overflow: "auto", overflow: "auto",
maxHeight: this.state.maxHeight maxHeight: this.state.maxHeight
}} }}
@@ -63,7 +63,8 @@ class AutocompleteInput extends React.Component {
style: null style: null
}} }}
inputProps={{ inputProps={{
className: "maputnik-string" className: "maputnik-string",
spellCheck: false
}} }}
value={this.props.value} value={this.props.value}
items={this.props.options} items={this.props.options}

View File

@@ -6,6 +6,7 @@ import DocLabel from '../fields/DocLabel'
/** Wrap a component with a label */ /** Wrap a component with a label */
class InputBlock extends React.Component { class InputBlock extends React.Component {
static propTypes = { static propTypes = {
"data-wd-key": PropTypes.string,
label: PropTypes.oneOfType([ label: PropTypes.oneOfType([
PropTypes.string, PropTypes.string,
PropTypes.element, PropTypes.element,
@@ -24,6 +25,7 @@ class InputBlock extends React.Component {
render() { render() {
return <div style={this.props.style} return <div style={this.props.style}
data-wd-key={this.props["data-wd-key"]}
className={classnames({ className={classnames({
"maputnik-input-block": true, "maputnik-input-block": true,
"maputnik-action-block": this.props.action "maputnik-action-block": this.props.action

View File

@@ -17,7 +17,7 @@ class NumberInput extends React.Component {
} }
} }
componentWillReceiveProps(nextProps) { UNSAFE_componentWillReceiveProps(nextProps) {
this.setState({ value: nextProps.value }) this.setState({ value: nextProps.value })
} }
@@ -67,6 +67,7 @@ class NumberInput extends React.Component {
render() { render() {
return <input return <input
spellCheck="false"
className="maputnik-number" className="maputnik-number"
placeholder={this.props.default} placeholder={this.props.default}
value={this.state.value} value={this.state.value}

View File

@@ -4,6 +4,7 @@ import PropTypes from 'prop-types'
class SelectInput extends React.Component { class SelectInput extends React.Component {
static propTypes = { static propTypes = {
value: PropTypes.string.isRequired, value: PropTypes.string.isRequired,
"data-wd-key": PropTypes.string,
options: PropTypes.array.isRequired, options: PropTypes.array.isRequired,
style: PropTypes.object, style: PropTypes.object,
onChange: PropTypes.func.isRequired, onChange: PropTypes.func.isRequired,
@@ -18,6 +19,7 @@ class SelectInput extends React.Component {
return <select return <select
className="maputnik-select" className="maputnik-select"
data-wd-key={this.props["data-wd-key"]}
style={this.props.style} style={this.props.style}
value={this.props.value} value={this.props.value}
onChange={e => this.props.onChange(e.target.value)} onChange={e => this.props.onChange(e.target.value)}

View File

@@ -3,6 +3,7 @@ import PropTypes from 'prop-types'
class StringInput extends React.Component { class StringInput extends React.Component {
static propTypes = { static propTypes = {
"data-wd-key": PropTypes.string,
value: PropTypes.string, value: PropTypes.string,
style: PropTypes.object, style: PropTypes.object,
default: PropTypes.string, default: PropTypes.string,
@@ -17,7 +18,7 @@ class StringInput extends React.Component {
} }
} }
componentWillReceiveProps(nextProps) { UNSAFE_componentWillReceiveProps(nextProps) {
this.setState({ value: nextProps.value || '' }) this.setState({ value: nextProps.value || '' })
} }
@@ -40,11 +41,17 @@ class StringInput extends React.Component {
} }
return React.createElement(tag, { return React.createElement(tag, {
"data-wd-key": this.props["data-wd-key"],
spellCheck: !(tag === "input"),
className: classes.join(" "), className: classes.join(" "),
style: this.props.style, style: this.props.style,
value: this.state.value, value: this.state.value,
placeholder: this.props.default, placeholder: this.props.default,
onChange: e => this.setState({ value: e.target.value }), onChange: e => {
this.setState({
value: e.target.value
})
},
onBlur: () => { onBlur: () => {
if(this.state.value!==this.props.value) this.props.onChange(this.state.value) if(this.state.value!==this.props.value) this.props.onChange(this.state.value)
} }

View File

@@ -0,0 +1,30 @@
import React from 'react'
import PropTypes from 'prop-types'
import Collapse from 'react-collapse'
import accessibility from '../../libs/accessibility'
export default class CollapseAlt extends React.Component {
static propTypes = {
isActive: PropTypes.bool.isRequired,
children: PropTypes.element.isRequired
}
render() {
if (accessibility.reducedMotionEnabled()) {
return (
<div style={{display: this.props.isActive ? "block" : "none"}}>
{this.props.children}
</div>
)
}
else {
return (
<Collapse isOpened={this.props.isActive}>
{this.props.children}
</Collapse>
)
}
}
}

View File

@@ -11,7 +11,11 @@ class MetadataBlock extends React.Component {
} }
render() { render() {
return <InputBlock label={"Comments"} doc={"Comments for the current layer. This is non-standard and not in the spec."}> return <InputBlock
label={"Comments"}
doc={"Comments for the current layer. This is non-standard and not in the spec."}
data-wd-key="layer-comment"
>
<StringInput <StringInput
multi={true} multi={true}
value={this.props.value} value={this.props.value}

View File

@@ -4,7 +4,6 @@ import PropTypes from 'prop-types'
import {Controlled as CodeMirror} from 'react-codemirror2' import {Controlled as CodeMirror} from 'react-codemirror2'
import InputBlock from '../inputs/InputBlock' import InputBlock from '../inputs/InputBlock'
import StringInput from '../inputs/StringInput' import StringInput from '../inputs/StringInput'
import SelectInput from '../inputs/SelectInput'
import 'codemirror/mode/javascript/javascript' import 'codemirror/mode/javascript/javascript'
import 'codemirror/addon/lint/lint' import 'codemirror/addon/lint/lint'
@@ -30,7 +29,7 @@ class JSONEditor extends React.Component {
} }
} }
componentWillReceiveProps(nextProps) { UNSAFE_componentWillReceiveProps(nextProps) {
this.setState({ this.setState({
code: JSON.stringify(nextProps.layer, null, 2) code: JSON.stringify(nextProps.layer, null, 2)
}) })

View File

@@ -1,5 +1,6 @@
import React from 'react' import React from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { Wrapper, Button, Menu, MenuItem } from 'react-aria-menubutton'
import JSONEditor from './JSONEditor' import JSONEditor from './JSONEditor'
import FilterEditor from '../filter/FilterEditor' import FilterEditor from '../filter/FilterEditor'
@@ -13,17 +14,14 @@ import CommentBlock from './CommentBlock'
import LayerSourceBlock from './LayerSourceBlock' import LayerSourceBlock from './LayerSourceBlock'
import LayerSourceLayerBlock from './LayerSourceLayerBlock' import LayerSourceLayerBlock from './LayerSourceLayerBlock'
import MoreVertIcon from 'react-icons/lib/md/more-vert'
import InputBlock from '../inputs/InputBlock' import InputBlock from '../inputs/InputBlock'
import MultiButtonInput from '../inputs/MultiButtonInput' import MultiButtonInput from '../inputs/MultiButtonInput'
import { changeType, changeProperty } from '../../libs/layer' import { changeType, changeProperty } from '../../libs/layer'
import layout from '../../config/layout.json' import layout from '../../config/layout.json'
class UnsupportedLayer extends React.Component {
render() {
return <div></div>
}
}
function layoutGroups(layerType) { function layoutGroups(layerType) {
const layerGroup = { const layerGroup = {
@@ -50,6 +48,13 @@ export default class LayerEditor extends React.Component {
spec: PropTypes.object.isRequired, spec: PropTypes.object.isRequired,
onLayerChanged: PropTypes.func, onLayerChanged: PropTypes.func,
onLayerIdChange: PropTypes.func, onLayerIdChange: PropTypes.func,
onMoveLayer: PropTypes.func,
onLayerDestroy: PropTypes.func,
onLayerCopy: PropTypes.func,
onLayerVisibilityToggle: PropTypes.func,
isFirstLayer: PropTypes.bool,
isLastLayer: PropTypes.bool,
layerIndex: PropTypes.number,
} }
static defaultProps = { static defaultProps = {
@@ -74,7 +79,7 @@ export default class LayerEditor extends React.Component {
this.state = { editorGroups } this.state = { editorGroups }
} }
componentWillReceiveProps(nextProps) { UNSAFE_componentWillReceiveProps(nextProps) {
const additionalGroups = { ...this.state.editorGroups } const additionalGroups = { ...this.state.editorGroups }
layout[nextProps.layer.type].groups.forEach(group => { layout[nextProps.layer.type].groups.forEach(group => {
@@ -117,10 +122,16 @@ export default class LayerEditor extends React.Component {
comment = this.props.layer.metadata['maputnik:comment'] comment = this.props.layer.metadata['maputnik:comment']
} }
let sourceLayerIds;
if(this.props.sources.hasOwnProperty(this.props.layer.source)) {
sourceLayerIds = this.props.sources[this.props.layer.source].layers;
}
switch(type) { switch(type) {
case 'layer': return <div> case 'layer': return <div>
<LayerIdBlock <LayerIdBlock
value={this.props.layer.id} value={this.props.layer.id}
wdKey="layer-editor.layer-id"
onChange={newId => this.props.onLayerIdChange(this.props.layer.id, newId)} onChange={newId => this.props.onLayerIdChange(this.props.layer.id, newId)}
/> />
<LayerTypeBlock <LayerTypeBlock
@@ -133,8 +144,9 @@ export default class LayerEditor extends React.Component {
onChange={v => this.changeProperty(null, 'source', v)} onChange={v => this.changeProperty(null, 'source', v)}
/> />
} }
{this.props.layer.type !== 'raster' && this.props.layer.type !== 'background' && <LayerSourceLayerBlock {['background', 'raster', 'hillshade', 'heatmap'].indexOf(this.state.type) < 0 &&
sourceLayerIds={this.props.sources[this.props.layer.source].layers} <LayerSourceLayerBlock
sourceLayerIds={sourceLayerIds}
value={this.props.layer['source-layer']} value={this.props.layer['source-layer']}
onChange={v => this.changeProperty(null, 'source-layer', v)} onChange={v => this.changeProperty(null, 'source-layer', v)}
/> />
@@ -171,16 +183,23 @@ export default class LayerEditor extends React.Component {
layer={this.props.layer} layer={this.props.layer}
onChange={this.props.onLayerChanged} onChange={this.props.onLayerChanged}
/> />
default: return null
} }
} }
moveLayer(offset) {
this.props.onMoveLayer({
oldIndex: this.props.layerIndex,
newIndex: this.props.layerIndex+offset
})
}
render() { render() {
const layerType = this.props.layer.type const layerType = this.props.layer.type
const groups = layoutGroups(layerType).filter(group => { const groups = layoutGroups(layerType).filter(group => {
return !(layerType === 'background' && group.type === 'source') return !(layerType === 'background' && group.type === 'source')
}).map(group => { }).map(group => {
return <LayerEditorGroup return <LayerEditorGroup
data-wd-key={group.title}
key={group.title} key={group.title}
title={group.title} title={group.title}
isActive={this.state.editorGroups[group.title]} isActive={this.state.editorGroups[group.title]}
@@ -190,8 +209,73 @@ export default class LayerEditor extends React.Component {
</LayerEditorGroup> </LayerEditorGroup>
}) })
const layout = this.props.layer.layout || {}
const items = {
delete: {
text: "Delete",
handler: () => this.props.onLayerDestroy(this.props.layer.id)
},
duplicate: {
text: "Duplicate",
handler: () => this.props.onLayerCopy(this.props.layer.id)
},
hide: {
text: (layout.visibility === "none") ? "Show" : "Hide",
handler: () => this.props.onLayerVisibilityToggle(this.props.layer.id)
},
moveLayerUp: {
text: "Move layer up",
// Not actually used...
disabled: this.props.isFirstLayer,
handler: () => this.moveLayer(-1)
},
moveLayerDown: {
text: "Move layer down",
// Not actually used...
disabled: this.props.isLastLayer,
handler: () => this.moveLayer(+1)
}
}
function handleSelection(id, event) {
event.stopPropagation;
items[id].handler();
}
return <div className="maputnik-layer-editor" return <div className="maputnik-layer-editor"
> >
<header>
<div className="layer-header">
<h2 className="layer-header__title">
Layer: {this.props.layer.id}
</h2>
<div className="layer-header__info">
<Wrapper
className='more-menu'
onSelection={handleSelection}
closeOnSelection={false}
>
<Button className='more-menu__button'>
<MoreVertIcon className="more-menu__button__svg" />
</Button>
<Menu>
<ul className="more-menu__menu">
{Object.keys(items).map((id, idx) => {
const item = items[id];
return <li key={id}>
<MenuItem value={id} className='more-menu__menu__item'>
{item.text}
</MenuItem>
</li>
})}
</ul>
</Menu>
</Wrapper>
</div>
</div>
</header>
{groups} {groups}
</div> </div>
} }

View File

@@ -1,10 +1,12 @@
import React from 'react' import React from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import Collapse from 'react-collapse'
import Collapser from './Collapser' import Collapser from './Collapser'
import Collapse from './Collapse'
export default class LayerEditorGroup extends React.Component { export default class LayerEditorGroup extends React.Component {
static propTypes = { static propTypes = {
"data-wd-key": PropTypes.string,
title: PropTypes.string.isRequired, title: PropTypes.string.isRequired,
isActive: PropTypes.bool.isRequired, isActive: PropTypes.bool.isRequired,
children: PropTypes.element.isRequired, children: PropTypes.element.isRequired,
@@ -14,14 +16,17 @@ export default class LayerEditorGroup extends React.Component {
render() { render() {
return <div> return <div>
<div className="maputnik-layer-editor-group" <div className="maputnik-layer-editor-group"
data-wd-key={"layer-editor-group:"+this.props["data-wd-key"]}
onClick={e => this.props.onActiveToggle(!this.props.isActive)} onClick={e => this.props.onActiveToggle(!this.props.isActive)}
> >
<span>{this.props.title}</span> <span>{this.props.title}</span>
<span style={{flexGrow: 1}} /> <span style={{flexGrow: 1}} />
<Collapser isCollapsed={this.props.isActive} /> <Collapser isCollapsed={this.props.isActive} />
</div> </div>
<Collapse isOpened={this.props.isActive}> <Collapse isActive={this.props.isActive}>
<div className="react-collapse-container">
{this.props.children} {this.props.children}
</div>
</Collapse> </Collapse>
</div> </div>
} }

View File

@@ -1,18 +1,21 @@
import React from 'react' import React from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec' import * as styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
import InputBlock from '../inputs/InputBlock' import InputBlock from '../inputs/InputBlock'
import StringInput from '../inputs/StringInput' import StringInput from '../inputs/StringInput'
class LayerIdBlock extends React.Component { class LayerIdBlock extends React.Component {
static propTypes = { static propTypes = {
value: PropTypes.string.isRequired, value: PropTypes.string.isRequired,
wdKey: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired, onChange: PropTypes.func.isRequired,
} }
render() { render() {
return <InputBlock label={"ID"} doc={styleSpec.latest.layer.id.doc}> return <InputBlock label={"ID"} doc={styleSpec.latest.layer.id.doc}
data-wd-key={this.props.wdKey}
>
<StringInput <StringInput
value={this.props.value} value={this.props.value}
onChange={this.props.onChange} onChange={this.props.onChange}

View File

@@ -1,7 +1,6 @@
import React from 'react' import React from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import classnames from 'classnames' import classnames from 'classnames'
import cloneDeep from 'lodash.clonedeep'
import Button from '../Button' import Button from '../Button'
import LayerListGroup from './LayerListGroup' import LayerListGroup from './LayerListGroup'
@@ -10,7 +9,7 @@ import AddIcon from 'react-icons/lib/md/add-circle-outline'
import AddModal from '../modals/AddModal' import AddModal from '../modals/AddModal'
import style from '../../libs/style.js' import style from '../../libs/style.js'
import {SortableContainer, SortableHandle, arrayMove} from 'react-sortable-hoc'; import {SortableContainer, SortableHandle} from 'react-sortable-hoc';
const layerListPropTypes = { const layerListPropTypes = {
layers: PropTypes.array.isRequired, layers: PropTypes.array.isRequired,
@@ -57,36 +56,6 @@ class LayerListContainer extends React.Component {
} }
} }
onLayerDestroy(layerId) {
const remainingLayers = this.props.layers.slice(0)
const idx = style.indexOfLayer(remainingLayers, layerId)
remainingLayers.splice(idx, 1);
this.props.onLayersChange(remainingLayers)
}
onLayerCopy(layerId) {
const changedLayers = this.props.layers.slice(0)
const idx = style.indexOfLayer(changedLayers, layerId)
const clonedLayer = cloneDeep(changedLayers[idx])
clonedLayer.id = clonedLayer.id + "-copy"
changedLayers.splice(idx, 0, clonedLayer)
this.props.onLayersChange(changedLayers)
}
onLayerVisibilityToggle(layerId) {
const changedLayers = this.props.layers.slice(0)
const idx = style.indexOfLayer(changedLayers, layerId)
const layer = { ...changedLayers[idx] }
const changedLayout = 'layout' in layer ? {...layer.layout} : {}
changedLayout.visibility = changedLayout.visibility === 'none' ? 'visible' : 'none'
layer.layout = changedLayout
changedLayers[idx] = layer
this.props.onLayersChange(changedLayers)
}
toggleModal(modalName) { toggleModal(modalName) {
this.setState({ this.setState({
isOpen: { isOpen: {
@@ -162,6 +131,7 @@ class LayerListContainer extends React.Component {
const groupPrefix = layerPrefix(layers[0].id) const groupPrefix = layerPrefix(layers[0].id)
if(layers.length > 1) { if(layers.length > 1) {
const grp = <LayerListGroup const grp = <LayerListGroup
data-wd-key={[groupPrefix, idx].join('-')}
key={[groupPrefix, idx].join('-')} key={[groupPrefix, idx].join('-')}
title={groupPrefix} title={groupPrefix}
isActive={!this.isCollapsed(groupPrefix, idx) || idx === this.props.selectedLayerIndex} isActive={!this.isCollapsed(groupPrefix, idx) || idx === this.props.selectedLayerIndex}
@@ -185,9 +155,9 @@ class LayerListContainer extends React.Component {
visibility={(layer.layout || {}).visibility} visibility={(layer.layout || {}).visibility}
isSelected={idx === this.props.selectedLayerIndex} isSelected={idx === this.props.selectedLayerIndex}
onLayerSelect={this.props.onLayerSelect} onLayerSelect={this.props.onLayerSelect}
onLayerDestroy={this.onLayerDestroy.bind(this)} onLayerDestroy={this.props.onLayerDestroy.bind(this)}
onLayerCopy={this.onLayerCopy.bind(this)} onLayerCopy={this.props.onLayerCopy.bind(this)}
onLayerVisibilityToggle={this.onLayerVisibilityToggle.bind(this)} onLayerVisibilityToggle={this.props.onLayerVisibilityToggle.bind(this)}
/> />
listItems.push(listItem) listItems.push(listItem)
idx += 1 idx += 1
@@ -207,20 +177,22 @@ class LayerListContainer extends React.Component {
<span className="maputnik-space" /> <span className="maputnik-space" />
<div className="maputnik-default-property"> <div className="maputnik-default-property">
<div className="maputnik-multibutton"> <div className="maputnik-multibutton">
<a <button
id="skip-menu"
onClick={this.toggleLayers.bind(this)} onClick={this.toggleLayers.bind(this)}
className="maputnik-button"> className="maputnik-button">
{this.state.areAllGroupsExpanded === true ? "Collapse" : "Expand"} {this.state.areAllGroupsExpanded === true ? "Collapse" : "Expand"}
</a> </button>
</div> </div>
</div> </div>
<div className="maputnik-default-property"> <div className="maputnik-default-property">
<div className="maputnik-multibutton"> <div className="maputnik-multibutton">
<a <button
onClick={this.toggleModal.bind(this, 'add')} onClick={this.toggleModal.bind(this, 'add')}
data-wd-key="layer-list:add-layer"
className="maputnik-button maputnik-button-selected"> className="maputnik-button maputnik-button-selected">
Add Layer Add Layer
</a> </button>
</div> </div>
</div> </div>
</header> </header>
@@ -234,18 +206,10 @@ class LayerListContainer extends React.Component {
export default class LayerList extends React.Component { export default class LayerList extends React.Component {
static propTypes = {...layerListPropTypes} static propTypes = {...layerListPropTypes}
onSortEnd(move) {
const { oldIndex, newIndex } = move
if(oldIndex === newIndex) return
let layers = this.props.layers.slice(0)
layers = arrayMove(layers, oldIndex, newIndex)
this.props.onLayersChange(layers)
}
render() { render() {
return <LayerListContainer return <LayerListContainer
{...this.props} {...this.props}
onSortEnd={this.onSortEnd.bind(this)} onSortEnd={this.props.onMoveLayer.bind(this)}
useDragHandle={true} useDragHandle={true}
/> />
} }

View File

@@ -5,6 +5,7 @@ import Collapser from './Collapser'
export default class LayerListGroup extends React.Component { export default class LayerListGroup extends React.Component {
static propTypes = { static propTypes = {
title: PropTypes.string.isRequired, title: PropTypes.string.isRequired,
"data-wd-key": PropTypes.string,
isActive: PropTypes.bool.isRequired, isActive: PropTypes.bool.isRequired,
onActiveToggle: PropTypes.func.isRequired onActiveToggle: PropTypes.func.isRequired
} }
@@ -12,6 +13,7 @@ export default class LayerListGroup extends React.Component {
render() { render() {
return <li className="maputnik-layer-list-group"> return <li className="maputnik-layer-list-group">
<div className="maputnik-layer-list-group-header" <div className="maputnik-layer-list-group-header"
data-wd-key={"layer-list-group:"+this.props["data-wd-key"]}
onClick={e => this.props.onActiveToggle(!this.props.isActive)} onClick={e => this.props.onActiveToggle(!this.props.isActive)}
> >
<span className="maputnik-layer-list-group-title">{this.props.title}</span> <span className="maputnik-layer-list-group-title">{this.props.title}</span>

View File

@@ -33,25 +33,28 @@ class IconAction extends React.Component {
static propTypes = { static propTypes = {
action: PropTypes.string.isRequired, action: PropTypes.string.isRequired,
onClick: PropTypes.func.isRequired, onClick: PropTypes.func.isRequired,
wdKey: PropTypes.string
} }
renderIcon() { renderIcon() {
switch(this.props.action) { switch(this.props.action) {
case 'copy': return <CopyIcon /> case 'duplicate': return <CopyIcon />
case 'show': return <VisibilityIcon /> case 'show': return <VisibilityIcon />
case 'hide': return <VisibilityOffIcon /> case 'hide': return <VisibilityOffIcon />
case 'delete': return <DeleteIcon /> case 'delete': return <DeleteIcon />
default: return null
} }
} }
render() { render() {
return <a return <button
tabIndex="-1"
title={this.props.action}
className="maputnik-layer-list-icon-action" className="maputnik-layer-list-icon-action"
data-wd-key={this.props.wdKey}
onClick={this.props.onClick} onClick={this.props.onClick}
> >
{this.renderIcon()} {this.renderIcon()}
</a> </button>
} }
} }
@@ -92,6 +95,7 @@ class LayerListItem extends React.Component {
return <li return <li
key={this.props.layerId} key={this.props.layerId}
onClick={e => this.props.onLayerSelect(this.props.layerId)} onClick={e => this.props.onLayerSelect(this.props.layerId)}
data-wd-key={"layer-list-item:"+this.props.layerId}
className={classnames({ className={classnames({
"maputnik-layer-list-item": true, "maputnik-layer-list-item": true,
"maputnik-layer-list-item-selected": this.props.isSelected, "maputnik-layer-list-item-selected": this.props.isSelected,
@@ -101,14 +105,17 @@ class LayerListItem extends React.Component {
<span className="maputnik-layer-list-item-id">{this.props.layerId}</span> <span className="maputnik-layer-list-item-id">{this.props.layerId}</span>
<span style={{flexGrow: 1}} /> <span style={{flexGrow: 1}} />
<IconAction <IconAction
wdKey={"layer-list-item:"+this.props.layerId+":delete"}
action={'delete'} action={'delete'}
onClick={e => this.props.onLayerDestroy(this.props.layerId)} onClick={e => this.props.onLayerDestroy(this.props.layerId)}
/> />
<IconAction <IconAction
action={'copy'} wdKey={"layer-list-item:"+this.props.layerId+":copy"}
action={'duplicate'}
onClick={e => this.props.onLayerCopy(this.props.layerId)} onClick={e => this.props.onLayerCopy(this.props.layerId)}
/> />
<IconAction <IconAction
wdKey={"layer-list-item:"+this.props.layerId+":toggle-visibility"}
action={this.props.visibility === 'visible' ? 'hide' : 'show'} action={this.props.visibility === 'visible' ? 'hide' : 'show'}
onClick={e => this.props.onLayerVisibilityToggle(this.props.layerId)} onClick={e => this.props.onLayerVisibilityToggle(this.props.layerId)}
/> />

View File

@@ -1,15 +1,15 @@
import React from 'react' import React from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec' import * as styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
import InputBlock from '../inputs/InputBlock' import InputBlock from '../inputs/InputBlock'
import StringInput from '../inputs/StringInput' import StringInput from '../inputs/StringInput'
import SelectInput from '../inputs/SelectInput'
import AutocompleteInput from '../inputs/AutocompleteInput' import AutocompleteInput from '../inputs/AutocompleteInput'
class LayerSourceBlock extends React.Component { class LayerSourceBlock extends React.Component {
static propTypes = { static propTypes = {
value: PropTypes.string, value: PropTypes.string,
wdKey: PropTypes.string,
onChange: PropTypes.func, onChange: PropTypes.func,
sourceIds: PropTypes.array, sourceIds: PropTypes.array,
} }
@@ -20,7 +20,9 @@ class LayerSourceBlock extends React.Component {
} }
render() { render() {
return <InputBlock label={"Source"} doc={styleSpec.latest.layer.source.doc}> return <InputBlock label={"Source"} doc={styleSpec.latest.layer.source.doc}
data-wd-key={this.props.wdKey}
>
<AutocompleteInput <AutocompleteInput
value={this.props.value} value={this.props.value}
onChange={this.props.onChange} onChange={this.props.onChange}

View File

@@ -1,10 +1,9 @@
import React from 'react' import React from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec' import * as styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
import InputBlock from '../inputs/InputBlock' import InputBlock from '../inputs/InputBlock'
import StringInput from '../inputs/StringInput' import StringInput from '../inputs/StringInput'
import SelectInput from '../inputs/SelectInput'
import AutocompleteInput from '../inputs/AutocompleteInput' import AutocompleteInput from '../inputs/AutocompleteInput'
class LayerSourceLayer extends React.Component { class LayerSourceLayer extends React.Component {
@@ -22,7 +21,9 @@ class LayerSourceLayer extends React.Component {
} }
render() { render() {
return <InputBlock label={"Source Layer"} doc={styleSpec.latest.layer['source-layer'].doc}> return <InputBlock label={"Source Layer"} doc={styleSpec.latest.layer['source-layer'].doc}
data-wd-key="layer-source-layer"
>
<AutocompleteInput <AutocompleteInput
keepMenuWithinWindowBounds={!!this.props.isFixed} keepMenuWithinWindowBounds={!!this.props.isFixed}
value={this.props.value} value={this.props.value}

View File

@@ -1,18 +1,21 @@
import React from 'react' import React from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec' import * as styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
import InputBlock from '../inputs/InputBlock' import InputBlock from '../inputs/InputBlock'
import SelectInput from '../inputs/SelectInput' import SelectInput from '../inputs/SelectInput'
class LayerTypeBlock extends React.Component { class LayerTypeBlock extends React.Component {
static propTypes = { static propTypes = {
value: PropTypes.string.isRequired, value: PropTypes.string.isRequired,
wdKey: PropTypes.string,
onChange: PropTypes.func.isRequired, onChange: PropTypes.func.isRequired,
} }
render() { render() {
return <InputBlock label={"Type"} doc={styleSpec.latest.layer.type.doc}> return <InputBlock label={"Type"} doc={styleSpec.latest.layer.type.doc}
data-wd-key={this.props.wdKey}
>
<SelectInput <SelectInput
options={[ options={[
['background', 'Background'], ['background', 'Background'],
@@ -22,6 +25,8 @@ class LayerTypeBlock extends React.Component {
['raster', 'Raster'], ['raster', 'Raster'],
['circle', 'Circle'], ['circle', 'Circle'],
['fill-extrusion', 'Fill Extrusion'], ['fill-extrusion', 'Fill Extrusion'],
['hillshade', 'Hillshade'],
['heatmap', 'Heatmap'],
]} ]}
onChange={this.props.onChange} onChange={this.props.onChange}
value={this.props.value} value={this.props.value}

View File

@@ -1,7 +1,7 @@
import React from 'react' import React from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec' import * as styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
import InputBlock from '../inputs/InputBlock' import InputBlock from '../inputs/InputBlock'
import NumberInput from '../inputs/NumberInput' import NumberInput from '../inputs/NumberInput'
@@ -12,7 +12,9 @@ class MaxZoomBlock extends React.Component {
} }
render() { render() {
return <InputBlock label={"Max Zoom"} doc={styleSpec.latest.layer.maxzoom.doc}> return <InputBlock label={"Max Zoom"} doc={styleSpec.latest.layer.maxzoom.doc}
data-wd-key="max-zoom"
>
<NumberInput <NumberInput
value={this.props.value} value={this.props.value}
onChange={this.props.onChange} onChange={this.props.onChange}

View File

@@ -1,7 +1,7 @@
import React from 'react' import React from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec' import * as styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
import InputBlock from '../inputs/InputBlock' import InputBlock from '../inputs/InputBlock'
import NumberInput from '../inputs/NumberInput' import NumberInput from '../inputs/NumberInput'
@@ -12,7 +12,9 @@ class MinZoomBlock extends React.Component {
} }
render() { render() {
return <InputBlock label={"Min Zoom"} doc={styleSpec.latest.layer.minzoom.doc}> return <InputBlock label={"Min Zoom"} doc={styleSpec.latest.layer.minzoom.doc}
data-wd-key="min-zoom"
>
<NumberInput <NumberInput
value={this.props.value} value={this.props.value}
onChange={this.props.onChange} onChange={this.props.onChange}

View File

@@ -38,7 +38,7 @@ function buildInspectStyle(originalMapStyle, coloredLayers, highlightedLayer) {
const sources = {} const sources = {}
Object.keys(originalMapStyle.sources).forEach(sourceId => { Object.keys(originalMapStyle.sources).forEach(sourceId => {
const source = originalMapStyle.sources[sourceId] const source = originalMapStyle.sources[sourceId]
if(source.type !== 'raster') { if(source.type !== 'raster' && source.type !== 'raster-dem') {
sources[sourceId] = source sources[sourceId] = source
} }
}) })
@@ -58,6 +58,7 @@ export default class MapboxGlMap extends React.Component {
mapStyle: PropTypes.object.isRequired, mapStyle: PropTypes.object.isRequired,
inspectModeEnabled: PropTypes.bool.isRequired, inspectModeEnabled: PropTypes.bool.isRequired,
highlightedLayer: PropTypes.object, highlightedLayer: PropTypes.object,
options: PropTypes.object,
} }
static defaultProps = { static defaultProps = {
@@ -65,6 +66,7 @@ export default class MapboxGlMap extends React.Component {
onDataChange: () => {}, onDataChange: () => {},
onLayerSelect: () => {}, onLayerSelect: () => {},
mapboxAccessToken: tokens.mapbox, mapboxAccessToken: tokens.mapbox,
options: {},
} }
constructor(props) { constructor(props) {
@@ -79,7 +81,7 @@ export default class MapboxGlMap extends React.Component {
} }
} }
componentWillReceiveProps(nextProps) { UNSAFE_componentWillReceiveProps(nextProps) {
if(!this.state.map) return if(!this.state.map) return
const metadata = nextProps.mapStyle.metadata || {} const metadata = nextProps.mapStyle.metadata || {}
MapboxGl.accessToken = metadata['maputnik:mapbox_access_token'] || tokens.mapbox MapboxGl.accessToken = metadata['maputnik:mapbox_access_token'] || tokens.mapbox
@@ -92,20 +94,29 @@ export default class MapboxGlMap extends React.Component {
} }
componentDidUpdate(prevProps) { componentDidUpdate(prevProps) {
const map = this.state.map;
if(this.props.inspectModeEnabled !== prevProps.inspectModeEnabled) { if(this.props.inspectModeEnabled !== prevProps.inspectModeEnabled) {
this.state.inspect.toggleInspector() this.state.inspect.toggleInspector()
} }
if(this.props.inspectModeEnabled) { if(this.props.inspectModeEnabled) {
this.state.inspect.render() this.state.inspect.render()
} }
map.showTileBoundaries = this.props.options.showTileBoundaries;
} }
componentDidMount() { componentDidMount() {
const map = new MapboxGl.Map({ const mapOpts = {
...this.props.options,
container: this.container, container: this.container,
style: this.props.mapStyle, style: this.props.mapStyle,
hash: true, hash: true,
}) }
const map = new MapboxGl.Map(mapOpts);
map.showTileBoundaries = mapOpts.showTileBoundaries;
const zoom = new ZoomControl; const zoom = new ZoomControl;
map.addControl(zoom, 'top-right'); map.addControl(zoom, 'top-right');
@@ -121,6 +132,7 @@ export default class MapboxGlMap extends React.Component {
showMapPopupOnHover: false, showMapPopupOnHover: false,
showInspectMapPopupOnHover: true, showInspectMapPopupOnHover: true,
showInspectButton: false, showInspectButton: false,
blockHoverPopupOnClick: true,
assignLayerColor: (layerId, alpha) => { assignLayerColor: (layerId, alpha) => {
return Color(colors.brightColor(layerId, alpha)).desaturate(0.5).string() return Color(colors.brightColor(layerId, alpha)).desaturate(0.5).string()
}, },

View File

@@ -3,66 +3,8 @@ import PropTypes from 'prop-types'
import style from '../../libs/style.js' import style from '../../libs/style.js'
import isEqual from 'lodash.isequal' import isEqual from 'lodash.isequal'
import { loadJSON } from '../../libs/urlopen' import { loadJSON } from '../../libs/urlopen'
import 'openlayers/dist/ol.css' import 'ol/ol.css'
function suitableVectorSource(mapStyle) {
const sources = Object.keys(mapStyle.sources)
.map(sourceId => {
return {
id: sourceId,
source: mapStyle.sources[sourceId]
}
})
.filter(({source}) => (source.type === 'vector' || source.type === 'geojson'))
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
})
})
}
function newGeoJSONLayer(sourceUrl) {
const ol = require('openlayers')
return new ol.layer.Vector({
source: new ol.source.Vector({
format: new ol.format.GeoJSON(),
url: sourceUrl
})
})
}
if (source.type === 'vector') {
if(!source.tiles) {
sourceFromTileJSON(source.url, tileSource => {
cb(newMVTLayer(tileSource.tiles[0]))
})
} else {
cb(newMVTLayer(source.tiles[0]))
}
} else if (source.type === 'geojson') {
cb(newGeoJSONLayer(source.data))
}
}
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 { class OpenLayers3Map extends React.Component {
static propTypes = { static propTypes = {
@@ -79,49 +21,17 @@ class OpenLayers3Map extends React.Component {
constructor(props) { constructor(props) {
super(props) super(props)
this.tilegrid = null
this.resolutions = null
this.layer = null
this.map = null this.map = null
} }
updateStyle(newMapStyle) { updateStyle(newMapStyle) {
const oldSource = suitableVectorSource(this.props.mapStyle) const olms = require('ol-mapbox-style');
const newSource = suitableVectorSource(newMapStyle) const styleFunc = olms.apply(this.map, 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) { UNSAFE_componentWillReceiveProps(nextProps) {
if(this.layer && !isEqual(oldSource, newSource)) { require.ensure(["ol", "ol-mapbox-style"], () => {
this.map.removeLayer(this.layer) if(!this.map) return
this.layer = null
}
if(!this.layer) {
var self = this
toVectorLayer(newSource.source, this.tilegrid, vectorLayer => {
self.layer = vectorLayer
self.map.addLayer(self.layer)
setStyleFunc(self.map, self.layer)
})
} else {
setStyleFunc(this.map, this.layer)
}
}
}
componentWillReceiveProps(nextProps) {
require.ensure(["openlayers", "ol-mapbox-style"], () => {
if(!this.map || !this.resolutions) return
this.updateStyle(nextProps.mapStyle) this.updateStyle(nextProps.mapStyle)
}) })
} }
@@ -129,24 +39,22 @@ class OpenLayers3Map extends React.Component {
componentDidMount() { componentDidMount() {
//Load OpenLayers dynamically once we need it //Load OpenLayers dynamically once we need it
//TODO: Make this more convenient //TODO: Make this more convenient
require.ensure(["openlayers", "ol-mapbox-style"], ()=> { require.ensure(["ol", "ol/map", "ol/view", "ol/control/zoom", "ol-mapbox-style"], ()=> {
console.log('Loaded OpenLayers3 renderer') console.log('Loaded OpenLayers3 renderer')
const ol = require('openlayers') const olMap = require('ol/map').default
const olms = require('ol-mapbox-style') const olView = require('ol/view').default
const olZoom = require('ol/control/zoom').default
this.tilegrid = ol.tilegrid.createXYZ({tileSize: 512, maxZoom: 22}) const map = new olMap({
this.resolutions = this.tilegrid.getResolutions()
const map = new ol.Map({
target: this.container, target: this.container,
layers: [], layers: [],
view: new ol.View({ view: new olView({
zoom: 2, zoom: 2,
center: [52.5, -78.4] center: [52.5, -78.4]
}) })
}) })
map.addControl(new ol.control.Zoom()) map.addControl(new olZoom())
this.map = map this.map = map
this.updateStyle(this.props.mapStyle) this.updateStyle(this.props.mapStyle)
}) })

View File

@@ -4,7 +4,6 @@ import PropTypes from 'prop-types'
import Button from '../Button' import Button from '../Button'
import InputBlock from '../inputs/InputBlock' import InputBlock from '../inputs/InputBlock'
import StringInput from '../inputs/StringInput' import StringInput from '../inputs/StringInput'
import SelectInput from '../inputs/SelectInput'
import Modal from './Modal' import Modal from './Modal'
import LayerTypeBlock from '../layers/LayerTypeBlock' import LayerTypeBlock from '../layers/LayerTypeBlock'
@@ -56,7 +55,7 @@ class AddModal extends React.Component {
} }
} }
componentWillUpdate(nextProps, nextState) { UNSAFE_componentWillUpdate(nextProps, nextState) {
// Check if source is valid for new type // Check if source is valid for new type
const oldType = this.state.type; const oldType = this.state.type;
const newType = nextState.type; const newType = nextState.type;
@@ -119,24 +118,30 @@ class AddModal extends React.Component {
isOpen={this.props.isOpen} isOpen={this.props.isOpen}
onOpenToggle={this.props.onOpenToggle} onOpenToggle={this.props.onOpenToggle}
title={'Add Layer'} title={'Add Layer'}
data-wd-key="modal:add-layer"
> >
<div className="maputnik-add-layer"> <div className="maputnik-add-layer">
<LayerIdBlock <LayerIdBlock
value={this.state.id} value={this.state.id}
onChange={v => this.setState({ id: v })} wdKey="add-layer.layer-id"
onChange={v => {
this.setState({ id: v })
}}
/> />
<LayerTypeBlock <LayerTypeBlock
value={this.state.type} value={this.state.type}
wdKey="add-layer.layer-type"
onChange={v => this.setState({ type: v })} onChange={v => this.setState({ type: v })}
/> />
{this.state.type !== 'background' && {this.state.type !== 'background' &&
<LayerSourceBlock <LayerSourceBlock
sourceIds={sources} sourceIds={sources}
wdKey="add-layer.layer-source-block"
value={this.state.source} value={this.state.source}
onChange={v => this.setState({ source: v })} onChange={v => this.setState({ source: v })}
/> />
} }
{this.state.type !== 'background' && this.state.type !== 'raster' && {['background', 'raster', 'hillshade', 'heatmap'].indexOf(this.state.type) < 0 &&
<LayerSourceLayerBlock <LayerSourceLayerBlock
isFixed={true} isFixed={true}
sourceLayerIds={layers} sourceLayerIds={layers}
@@ -144,7 +149,11 @@ class AddModal extends React.Component {
onChange={v => this.setState({ 'source-layer': v })} onChange={v => this.setState({ 'source-layer': v })}
/> />
} }
<Button className="maputnik-add-layer-button" onClick={this.addLayer.bind(this)}> <Button
className="maputnik-add-layer-button"
onClick={this.addLayer.bind(this)}
data-wd-key="add-layer"
>
Add Layer Add Layer
</Button> </Button>
</div> </div>

View File

@@ -2,10 +2,9 @@ import React from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { saveAs } from 'file-saver' import { saveAs } from 'file-saver'
import styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec' import * as styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
import InputBlock from '../inputs/InputBlock' import InputBlock from '../inputs/InputBlock'
import StringInput from '../inputs/StringInput' import StringInput from '../inputs/StringInput'
import SelectInput from '../inputs/SelectInput'
import CheckboxInput from '../inputs/CheckboxInput' import CheckboxInput from '../inputs/CheckboxInput'
import Button from '../Button' import Button from '../Button'
import Modal from './Modal' import Modal from './Modal'
@@ -26,12 +25,13 @@ class Gist extends React.Component {
super(props); super(props);
this.state = { this.state = {
preview: false, preview: false,
public: false,
saving: false, saving: false,
latestGist: null, latestGist: null,
} }
} }
componentWillReceiveProps(nextProps) { UNSAFE_componentWillReceiveProps(nextProps) {
this.setState({ this.setState({
...this.state, ...this.state,
preview: !!(nextProps.mapStyle.metadata || {})['maputnik:openmaptiles_access_token'] preview: !!(nextProps.mapStyle.metadata || {})['maputnik:openmaptiles_access_token']
@@ -43,7 +43,10 @@ class Gist extends React.Component {
...this.state, ...this.state,
saving: true saving: true
}); });
const preview = this.state.preview && (this.props.mapStyle.metadata || {})['maputnik:openmaptiles_access_token'];
const preview = this.state.preview;
const mapboxToken = (this.props.mapStyle.metadata || {})['maputnik:mapbox_access_token'];
const mapStyleStr = preview ? const mapStyleStr = preview ?
styleSpec.format(stripAccessTokens(style.replaceAccessToken(this.props.mapStyle))) : styleSpec.format(stripAccessTokens(style.replaceAccessToken(this.props.mapStyle))) :
@@ -56,8 +59,8 @@ class Gist extends React.Component {
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>`+styleTitle+` Preview</title> <title>`+styleTitle+` Preview</title>
<link rel="stylesheet" type="text/css" href="https://api.mapbox.com/mapbox-gl-js/v0.43.0/mapbox-gl.css" /> <link rel="stylesheet" type="text/css" href="https://api.mapbox.com/mapbox-gl-js/v0.44.0/mapbox-gl.css" />
<script src="https://api.mapbox.com/mapbox-gl-js/v0.43.0/mapbox-gl.js"></script> <script src="https://api.mapbox.com/mapbox-gl-js/v0.44.0/mapbox-gl.js"></script>
<style> <style>
body { margin:0; padding:0; } body { margin:0; padding:0; }
#map { position:absolute; top:0; bottom:0; width:100%; } #map { position:absolute; top:0; bottom:0; width:100%; }
@@ -66,6 +69,7 @@ class Gist extends React.Component {
<body> <body>
<div id='map'></div> <div id='map'></div>
<script> <script>
mapboxgl.accessToken = '${mapboxToken}';
var map = new mapboxgl.Map({ var map = new mapboxgl.Map({
container: 'map', container: 'map',
style: 'style.json', style: 'style.json',
@@ -90,7 +94,7 @@ class Gist extends React.Component {
const gh = new GitHub(); const gh = new GitHub();
let gist = gh.getGist(); // not a gist yet let gist = gh.getGist(); // not a gist yet
gist.create({ gist.create({
public: true, public: this.state.public,
description: styleTitle, description: styleTitle,
files: files files: files
}).then(function({data}) { }).then(function({data}) {
@@ -111,6 +115,13 @@ class Gist extends React.Component {
}) })
} }
onPublicChange(value) {
this.setState({
...this.state,
public: value
})
}
changeMetadataProperty(property, value) { changeMetadataProperty(property, value) {
const changedStyle = { const changedStyle = {
...this.props.mapStyle, ...this.props.mapStyle,
@@ -163,13 +174,22 @@ class Gist extends React.Component {
<MdFileDownload /> <MdFileDownload />
Save to Gist (anonymous) Save to Gist (anonymous)
</Button> </Button>
{' '} <div className="maputnik-modal-sub-section">
<CheckboxInput
value={this.state.public}
name='gist-style-public'
onChange={this.onPublicChange.bind(this)}
/>
<span> Public gist</span>
</div>
<div className="maputnik-modal-sub-section">
<CheckboxInput <CheckboxInput
value={this.state.preview} value={this.state.preview}
name='gist-style-preview' name='gist-style-preview'
onChange={this.onPreviewChange.bind(this)} onChange={this.onPreviewChange.bind(this)}
/> />
<span> Include preview</span> <span> Include preview</span>
</div>
{this.state.preview ? {this.state.preview ?
<div> <div>
<InputBlock <InputBlock
@@ -178,6 +198,12 @@ class Gist extends React.Component {
value={(this.props.mapStyle.metadata || {})['maputnik:openmaptiles_access_token']} value={(this.props.mapStyle.metadata || {})['maputnik:openmaptiles_access_token']}
onChange={this.changeMetadataProperty.bind(this, "maputnik:openmaptiles_access_token")}/> onChange={this.changeMetadataProperty.bind(this, "maputnik:openmaptiles_access_token")}/>
</InputBlock> </InputBlock>
<InputBlock
label={"Mapbox Access Token: "}>
<StringInput
value={(this.props.mapStyle.metadata || {})['maputnik:mapbox_access_token']}
onChange={this.changeMetadataProperty.bind(this, "maputnik:mapbox_access_token")}/>
</InputBlock>
<a target="_blank" rel="noopener noreferrer" href="https://openmaptiles.com/hosting/">Get your free access token</a> <a target="_blank" rel="noopener noreferrer" href="https://openmaptiles.com/hosting/">Get your free access token</a>
</div> </div>
: null} : null}
@@ -209,12 +235,27 @@ class ExportModal extends React.Component {
} }
downloadStyle() { downloadStyle() {
const blob = new Blob([styleSpec.format(stripAccessTokens(this.props.mapStyle))], {type: "application/json;charset=utf-8"}); const tokenStyle = styleSpec.format(stripAccessTokens(style.replaceAccessToken(this.props.mapStyle)));
const blob = new Blob([tokenStyle], {type: "application/json;charset=utf-8"});
saveAs(blob, this.props.mapStyle.id + ".json"); saveAs(blob, this.props.mapStyle.id + ".json");
} }
changeMetadataProperty(property, value) {
const changedStyle = {
...this.props.mapStyle,
metadata: {
...this.props.mapStyle.metadata,
[property]: value
}
}
this.props.onStyleChanged(changedStyle)
}
render() { render() {
return <Modal return <Modal
data-wd-key="export-modal"
isOpen={this.props.isOpen} isOpen={this.props.isOpen}
onOpenToggle={this.props.onOpenToggle} onOpenToggle={this.props.onOpenToggle}
title={'Export Style'} title={'Export Style'}
@@ -225,13 +266,29 @@ class ExportModal extends React.Component {
<p> <p>
Download a JSON style to your computer. Download a JSON style to your computer.
</p> </p>
<p>
<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>
<InputBlock label={"Mapbox Access Token: "}>
<StringInput
value={(this.props.mapStyle.metadata || {})['maputnik:mapbox_access_token']}
onChange={this.changeMetadataProperty.bind(this, "maputnik:mapbox_access_token")}
/>
</InputBlock>
</p>
<Button onClick={this.downloadStyle.bind(this)}> <Button onClick={this.downloadStyle.bind(this)}>
<MdFileDownload /> <MdFileDownload />
Download Download
</Button> </Button>
</div> </div>
<div className="maputnik-modal-section"> <div className="maputnik-modal-section hide">
<h4>Save style</h4> <h4>Save style</h4>
<Gist mapStyle={this.props.mapStyle} onStyleChanged={this.props.onStyleChanged}/> <Gist mapStyle={this.props.mapStyle} onStyleChanged={this.props.onStyleChanged}/>
</div> </div>

View File

@@ -0,0 +1,49 @@
import React from 'react'
import PropTypes from 'prop-types'
import Button from '../Button'
import Modal from './Modal'
class LoadingModal extends React.Component {
static propTypes = {
isOpen: PropTypes.bool.isRequired,
onCancel: PropTypes.func.isRequired,
title: PropTypes.string.isRequired,
message: PropTypes.node.isRequired,
}
constructor(props) {
super(props);
}
underlayOnClick(e) {
// This stops click events falling through to underlying modals.
e.stopPropagation();
}
render() {
return <Modal
data-wd-key="loading-modal"
isOpen={this.props.isOpen}
underlayClickExits={false}
underlayProps={{
onClick: (e) => underlayProps(e)
}}
closeable={false}
title={this.props.title}
onOpenToggle={() => this.props.onCancel()}
>
<p>
{this.props.message}
</p>
<p className="maputnik-dialog__buttons">
<Button onClick={(e) => this.props.onCancel(e)}>
Cancel
</Button>
</p>
</Modal>
}
}
export default LoadingModal

View File

@@ -1,33 +1,61 @@
import React from 'react' import React from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import CloseIcon from 'react-icons/lib/md/close' import CloseIcon from 'react-icons/lib/md/close'
import Overlay from './Overlay' import AriaModal from 'react-aria-modal'
class Modal extends React.Component { class Modal extends React.Component {
static propTypes = { static propTypes = {
"data-wd-key": PropTypes.string,
isOpen: PropTypes.bool.isRequired, isOpen: PropTypes.bool.isRequired,
title: PropTypes.string.isRequired, title: PropTypes.string.isRequired,
onOpenToggle: PropTypes.func.isRequired, onOpenToggle: PropTypes.func.isRequired,
children: PropTypes.node, children: PropTypes.node,
underlayClickExits: PropTypes.bool,
underlayProps: PropTypes.object,
}
static defaultProps = {
underlayClickExits: true
}
getApplicationNode() {
return document.getElementById('app');
} }
render() { render() {
return <Overlay isOpen={this.props.isOpen}> if(this.props.isOpen) {
<div className="maputnik-modal"> return <AriaModal
titleText={this.props.title}
underlayClickExits={this.props.underlayClickExits}
underlayProps={this.props.underlayProps}
getApplicationNode={this.getApplicationNode}
data-wd-key={this.props["data-wd-key"]}
verticallyCenter={true}
onExit={() => this.props.onOpenToggle(false)}
>
<div className="maputnik-modal"
data-wd-key={this.props["data-wd-key"]}
>
<header className="maputnik-modal-header"> <header className="maputnik-modal-header">
<h1 className="maputnik-modal-header-title">{this.props.title}</h1> <h1 className="maputnik-modal-header-title">{this.props.title}</h1>
<span className="maputnik-modal-header-space"></span> <span className="maputnik-modal-header-space"></span>
<a className="maputnik-modal-header-toggle" <button className="maputnik-modal-header-toggle"
onClick={() => this.props.onOpenToggle(false)} onClick={() => this.props.onOpenToggle(false)}
data-wd-key={this.props["data-wd-key"]+".close-modal"}
> >
<CloseIcon /> <CloseIcon />
</a> </button>
</header> </header>
<div className="maputnik-modal-scroller"> <div className="maputnik-modal-scroller">
<div className="maputnik-modal-content">{this.props.children}</div> <div className="maputnik-modal-content">{this.props.children}</div>
</div> </div>
</div> </div>
</Overlay> </AriaModal>
}
else {
return false;
}
} }
} }

View File

@@ -1,5 +1,6 @@
import React from 'react' import React from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import LoadingModal from './LoadingModal'
import Modal from './Modal' import Modal from './Modal'
import Button from '../Button' import Button from '../Button'
import FileReaderInput from 'react-file-reader-input' import FileReaderInput from 'react-file-reader-input'
@@ -23,6 +24,7 @@ class PublicStyle extends React.Component {
return <div className="maputnik-public-style"> return <div className="maputnik-public-style">
<Button <Button
className="maputnik-public-style-button" className="maputnik-public-style-button"
aria-label={this.props.title}
onClick={() => this.props.onSelect(this.props.url)} onClick={() => this.props.onSelect(this.props.url)}
> >
<header className="maputnik-public-style-header"> <header className="maputnik-public-style-header">
@@ -30,11 +32,12 @@ class PublicStyle extends React.Component {
<span className="maputnik-space" /> <span className="maputnik-space" />
<AddIcon /> <AddIcon />
</header> </header>
<img <div
className="maputnik-public-style-thumbnail" className="maputnik-public-style-thumbnail"
src={this.props.thumbnailUrl} style={{
alt={this.props.title} backgroundImage: `url(${this.props.thumbnailUrl})`
/> }}
></div>
</Button> </Button>
</div> </div>
} }
@@ -58,13 +61,33 @@ class OpenModal extends React.Component {
}) })
} }
onCancelActiveRequest(e) {
// Else the click propagates to the underlying modal
if(e) e.stopPropagation();
if(this.state.activeRequest) {
this.state.activeRequest.abort();
this.setState({
activeRequest: null,
activeRequestUrl: null
});
}
}
onStyleSelect(styleUrl) { onStyleSelect(styleUrl) {
this.clearError(); this.clearError();
request({ const reqOpts = {
url: styleUrl, url: styleUrl,
withCredentials: false, withCredentials: false,
}, (error, response, body) => { }
const activeRequest = request(reqOpts, (error, response, body) => {
this.setState({
activeRequest: null,
activeRequestUrl: null
});
if (!error && response.statusCode == 200) { if (!error && response.statusCode == 200) {
const mapStyle = style.ensureStyleValidity(JSON.parse(body)) const mapStyle = style.ensureStyleValidity(JSON.parse(body))
console.log('Loaded style ', mapStyle.id) console.log('Loaded style ', mapStyle.id)
@@ -74,6 +97,11 @@ class OpenModal extends React.Component {
console.warn('Could not open the style URL', styleUrl) console.warn('Could not open the style URL', styleUrl)
} }
}) })
this.setState({
activeRequest: activeRequest,
activeRequestUrl: reqOpts.url
})
} }
onOpenUrl() { onOpenUrl() {
@@ -133,6 +161,7 @@ class OpenModal extends React.Component {
} }
return <Modal return <Modal
data-wd-key="open-modal"
isOpen={this.props.isOpen} isOpen={this.props.isOpen}
onOpenToggle={() => this.onOpenToggle()} onOpenToggle={() => this.onOpenToggle()}
title={'Open Style'} title={'Open Style'}
@@ -141,7 +170,7 @@ class OpenModal extends React.Component {
<section className="maputnik-modal-section"> <section className="maputnik-modal-section">
<h2>Upload Style</h2> <h2>Upload Style</h2>
<p>Upload a JSON style from your computer.</p> <p>Upload a JSON style from your computer.</p>
<FileReaderInput onChange={this.onUpload.bind(this)}> <FileReaderInput onChange={this.onUpload.bind(this)} tabIndex="-1">
<Button className="maputnik-upload-button"><FileUploadIcon /> Upload</Button> <Button className="maputnik-upload-button"><FileUploadIcon /> Upload</Button>
</FileReaderInput> </FileReaderInput>
</section> </section>
@@ -151,9 +180,9 @@ class OpenModal extends React.Component {
<p> <p>
Load from a URL. Note that the URL must have <a href="https://enable-cors.org" target="_blank" rel="noopener noreferrer">CORS enabled</a>. Load from a URL. Note that the URL must have <a href="https://enable-cors.org" target="_blank" rel="noopener noreferrer">CORS enabled</a>.
</p> </p>
<input type="text" ref={(input) => this.styleUrlElement = input} className="maputnik-input" placeholder="Enter URL..."/> <input data-wd-key="open-modal.url.input" type="text" ref={(input) => this.styleUrlElement = input} className="maputnik-input" placeholder="Enter URL..."/>
<div> <div>
<Button className="maputnik-big-button" onClick={this.onOpenUrl.bind(this)}>Open URL</Button> <Button data-wd-key="open-modal.url.button" className="maputnik-big-button" onClick={this.onOpenUrl.bind(this)}>Open URL</Button>
</div> </div>
</section> </section>
@@ -166,6 +195,13 @@ class OpenModal extends React.Component {
{styleOptions} {styleOptions}
</div> </div>
</section> </section>
<LoadingModal
isOpen={!!this.state.activeRequest}
title={'Loading style'}
onCancel={(e) => this.onCancelActiveRequest(e)}
message={"Loading: "+this.state.activeRequestUrl}
/>
</Modal> </Modal>
} }
} }

View File

@@ -1,24 +0,0 @@
import React from 'react'
import PropTypes from 'prop-types'
class Overlay extends React.Component {
static propTypes = {
isOpen: PropTypes.bool.isRequired,
children: PropTypes.element.isRequired
}
render() {
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>
}
}
export default Overlay

View File

@@ -1,7 +1,7 @@
import React from 'react' import React from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec' import * as styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
import InputBlock from '../inputs/InputBlock' import InputBlock from '../inputs/InputBlock'
import StringInput from '../inputs/StringInput' import StringInput from '../inputs/StringInput'
import SelectInput from '../inputs/SelectInput' import SelectInput from '../inputs/SelectInput'
@@ -42,6 +42,7 @@ class SettingsModal extends React.Component {
const metadata = this.props.mapStyle.metadata || {} const metadata = this.props.mapStyle.metadata || {}
const inputProps = { } const inputProps = { }
return <Modal return <Modal
data-wd-key="modal-settings"
isOpen={this.props.isOpen} isOpen={this.props.isOpen}
onOpenToggle={this.props.onOpenToggle} onOpenToggle={this.props.onOpenToggle}
title={'Style Settings'} title={'Style Settings'}
@@ -49,18 +50,21 @@ class SettingsModal extends React.Component {
<div style={{minWidth: 350}}> <div style={{minWidth: 350}}>
<InputBlock label={"Name"} doc={styleSpec.latest.$root.name.doc}> <InputBlock label={"Name"} doc={styleSpec.latest.$root.name.doc}>
<StringInput {...inputProps} <StringInput {...inputProps}
data-wd-key="modal-settings.name"
value={this.props.mapStyle.name} value={this.props.mapStyle.name}
onChange={this.changeStyleProperty.bind(this, "name")} onChange={this.changeStyleProperty.bind(this, "name")}
/> />
</InputBlock> </InputBlock>
<InputBlock label={"Owner"} doc={"Owner ID of the style. Used by Mapbox or future style APIs."}> <InputBlock label={"Owner"} doc={"Owner ID of the style. Used by Mapbox or future style APIs."}>
<StringInput {...inputProps} <StringInput {...inputProps}
data-wd-key="modal-settings.owner"
value={this.props.mapStyle.owner} value={this.props.mapStyle.owner}
onChange={this.changeStyleProperty.bind(this, "owner")} onChange={this.changeStyleProperty.bind(this, "owner")}
/> />
</InputBlock> </InputBlock>
<InputBlock label={"Sprite URL"} doc={styleSpec.latest.$root.sprite.doc}> <InputBlock label={"Sprite URL"} doc={styleSpec.latest.$root.sprite.doc}>
<StringInput {...inputProps} <StringInput {...inputProps}
data-wd-key="modal-settings.sprite"
value={this.props.mapStyle.sprite} value={this.props.mapStyle.sprite}
onChange={this.changeStyleProperty.bind(this, "sprite")} onChange={this.changeStyleProperty.bind(this, "sprite")}
/> />
@@ -68,6 +72,7 @@ class SettingsModal extends React.Component {
<InputBlock label={"Glyphs URL"} doc={styleSpec.latest.$root.glyphs.doc}> <InputBlock label={"Glyphs URL"} doc={styleSpec.latest.$root.glyphs.doc}>
<StringInput {...inputProps} <StringInput {...inputProps}
data-wd-key="modal-settings.glyphs"
value={this.props.mapStyle.glyphs} value={this.props.mapStyle.glyphs}
onChange={this.changeStyleProperty.bind(this, "glyphs")} onChange={this.changeStyleProperty.bind(this, "glyphs")}
/> />
@@ -75,6 +80,7 @@ class SettingsModal extends React.Component {
<InputBlock label={"Mapbox Access Token"} doc={"Public access token for Mapbox services."}> <InputBlock label={"Mapbox Access Token"} doc={"Public access token for Mapbox services."}>
<StringInput {...inputProps} <StringInput {...inputProps}
data-wd-key="modal-settings.maputnik:mapbox_access_token"
value={metadata['maputnik:mapbox_access_token']} value={metadata['maputnik:mapbox_access_token']}
onChange={this.changeMetadataProperty.bind(this, "maputnik:mapbox_access_token")} onChange={this.changeMetadataProperty.bind(this, "maputnik:mapbox_access_token")}
/> />
@@ -82,6 +88,7 @@ class SettingsModal extends React.Component {
<InputBlock label={"OpenMapTiles Access Token"} doc={"Public access token for the OpenMapTiles CDN."}> <InputBlock label={"OpenMapTiles Access Token"} doc={"Public access token for the OpenMapTiles CDN."}>
<StringInput {...inputProps} <StringInput {...inputProps}
data-wd-key="modal-settings.maputnik:openmaptiles_access_token"
value={metadata['maputnik:openmaptiles_access_token']} value={metadata['maputnik:openmaptiles_access_token']}
onChange={this.changeMetadataProperty.bind(this, "maputnik:openmaptiles_access_token")} onChange={this.changeMetadataProperty.bind(this, "maputnik:openmaptiles_access_token")}
/> />
@@ -89,6 +96,7 @@ class SettingsModal extends React.Component {
<InputBlock label={"Style Renderer"} doc={"Choose the default Maputnik renderer for this style."}> <InputBlock label={"Style Renderer"} doc={"Choose the default Maputnik renderer for this style."}>
<SelectInput {...inputProps} <SelectInput {...inputProps}
data-wd-key="modal-settings.maputnik:renderer"
options={[ options={[
['mbgljs', 'MapboxGL JS'], ['mbgljs', 'MapboxGL JS'],
['ol3', 'Open Layers 3'], ['ol3', 'Open Layers 3'],

View File

@@ -0,0 +1,73 @@
import React from 'react'
import PropTypes from 'prop-types'
import Button from '../Button'
import Modal from './Modal'
class ShortcutsModal extends React.Component {
static propTypes = {
isOpen: PropTypes.bool.isRequired,
onOpenToggle: PropTypes.func.isRequired,
}
constructor(props) {
super(props);
}
render() {
const help = [
{
key: "?",
text: "Shortcuts menu"
},
{
key: "o",
text: "Open modal"
},
{
key: "e",
text: "Export modal"
},
{
key: "d",
text: "Data Sources modal"
},
{
key: "s",
text: "Style Settings modal"
},
{
key: "i",
text: "Toggle inspect"
},
{
key: "m",
text: "Focus map"
},
]
return <Modal
data-wd-key="shortcuts-modal"
isOpen={this.props.isOpen}
onOpenToggle={this.props.onOpenToggle}
title={'Shortcuts'}
>
<div className="maputnik-modal-section maputnik-modal-shortcuts">
<p>
Press <code>ESC</code> to lose focus of any active elements, then press one of:
</p>
<ul>
{help.map((item) => {
return <li key={item.key}>
<code>{item.key}</code> {item.text}
</li>
})}
</ul>
</div>
</Modal>
}
}
export default ShortcutsModal

View File

@@ -1,6 +1,6 @@
import React from 'react' import React from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec' import * as styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
import Modal from './Modal' import Modal from './Modal'
import Button from '../Button' import Button from '../Button'
import InputBlock from '../inputs/InputBlock' import InputBlock from '../inputs/InputBlock'
@@ -45,6 +45,10 @@ function editorMode(source) {
if(source.tiles) return 'tilexyz_raster' if(source.tiles) return 'tilexyz_raster'
return 'tilejson_raster' return 'tilejson_raster'
} }
if(source.type === 'raster-dem') {
if(source.tiles) return 'tilexyz_raster-dem'
return 'tilejson_raster-dem'
}
if(source.type === 'vector') { if(source.type === 'vector') {
if(source.tiles) return 'tilexyz_vector' if(source.tiles) return 'tilexyz_vector'
return 'tilejson_vector' return 'tilejson_vector'
@@ -127,6 +131,16 @@ class AddSource extends React.Component {
minzoom: source.minzoom || 0, minzoom: source.minzoom || 0,
maxzoom: source.maxzoom || 14 maxzoom: source.maxzoom || 14
} }
case 'tilejson_raster-dem': return {
type: 'raster-dem',
url: source.url || 'http://localhost:3000/tilejson.json'
}
case 'tilexyz_raster-dem': return {
type: 'raster-dem',
tiles: source.tiles || ['http://localhost:3000/{x}/{y}/{z}.pbf'],
minzoom: source.minzoom || 0,
maxzoom: source.maxzoom || 14
}
default: return {} default: return {}
} }
} }
@@ -147,6 +161,8 @@ class AddSource extends React.Component {
['tilexyz_vector', 'Vector (XYZ URLs)'], ['tilexyz_vector', 'Vector (XYZ URLs)'],
['tilejson_raster', 'Raster (TileJSON URL)'], ['tilejson_raster', 'Raster (TileJSON URL)'],
['tilexyz_raster', 'Raster (XYZ URL)'], ['tilexyz_raster', 'Raster (XYZ URL)'],
['tilejson_raster-dem', 'Raster DEM (TileJSON URL)'],
['tilexyz_raster-dem', 'Raster DEM (XYZ URLs)'],
]} ]}
onChange={mode => this.setState({mode: mode, source: this.defaultSource(mode)})} onChange={mode => this.setState({mode: mode, source: this.defaultSource(mode)})}
value={this.state.mode} value={this.state.mode}
@@ -218,7 +234,7 @@ class SourcesModal extends React.Component {
<div className="maputnik-modal-section"> <div className="maputnik-modal-section">
<h4>Choose Public Source</h4> <h4>Choose Public Source</h4>
<p> <p>
Add one of the publicly availble sources to your style. Add one of the publicly available sources to your style.
</p> </p>
<div style={{maxwidth: 500}}> <div style={{maxwidth: 500}}>
{tilesetOptions} {tilesetOptions}

View File

@@ -0,0 +1,41 @@
import React from 'react'
import PropTypes from 'prop-types'
import Button from '../Button'
import Modal from './Modal'
import logoImage from 'maputnik-design/logos/logo-color.svg'
class SurveyModal extends React.Component {
static propTypes = {
isOpen: PropTypes.bool.isRequired,
onOpenToggle: PropTypes.func.isRequired,
}
constructor(props) { super(props); }
onClick = () => {
window.open('https://gregorywolanski.typeform.com/to/cPgaSY', '_blank');
this.props.onOpenToggle();
}
render() {
return <Modal
data-wd-key="modal-survey"
isOpen={this.props.isOpen}
onOpenToggle={this.props.onOpenToggle}
title="Maputnik Survey"
>
<div className="maputnik-modal-survey">
<img className="maputnik-modal-survey__logo" src={logoImage} alt="" width="128" />
<h1>You + Maputnik = Maputnik better for you</h1>
<p className="maputnik-modal-survey__description">We dont track you, so we dont know how you use Maputnik. Help us make Maputnik better for you by completing a 7minute survey carried out by our contributing designer.</p>
<Button onClick={this.onClick} className="maputnik-big-button maputnik-white-button maputnik-wide-button">Take the Maputnik Survey</Button>
<p className="maputnik-modal-survey__footnote">It takes 7 minutes, tops! Every question is optional.</p>
</div>
</Modal>
}
}
export default SurveyModal

View File

@@ -1,18 +1,22 @@
import React from 'react' import React from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec' import * as styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
import InputBlock from '../inputs/InputBlock' import InputBlock from '../inputs/InputBlock'
import StringInput from '../inputs/StringInput' import StringInput from '../inputs/StringInput'
import NumberInput from '../inputs/NumberInput' import NumberInput from '../inputs/NumberInput'
import SelectInput from '../inputs/SelectInput'
class TileJSONSourceEditor extends React.Component { class TileJSONSourceEditor extends React.Component {
static propTypes = { static propTypes = {
source: PropTypes.object.isRequired, source: PropTypes.object.isRequired,
onChange: PropTypes.func.isRequired, onChange: PropTypes.func.isRequired,
children: PropTypes.node,
} }
render() { render() {
return <InputBlock label={"TileJSON URL"} doc={styleSpec.latest.source_vector.url.doc}> return <div>
<InputBlock label={"TileJSON URL"} doc={styleSpec.latest.source_vector.url.doc}>
<StringInput <StringInput
value={this.props.source.url} value={this.props.source.url}
onChange={url => this.props.onChange({ onChange={url => this.props.onChange({
@@ -21,6 +25,8 @@ class TileJSONSourceEditor extends React.Component {
})} })}
/> />
</InputBlock> </InputBlock>
{this.props.children}
</div>
} }
} }
@@ -28,6 +34,7 @@ class TileURLSourceEditor extends React.Component {
static propTypes = { static propTypes = {
source: PropTypes.object.isRequired, source: PropTypes.object.isRequired,
onChange: PropTypes.func.isRequired, onChange: PropTypes.func.isRequired,
children: PropTypes.node,
} }
changeTileUrl(idx, value) { changeTileUrl(idx, value) {
@@ -73,6 +80,7 @@ class TileURLSourceEditor extends React.Component {
})} })}
/> />
</InputBlock> </InputBlock>
{this.props.children}
</div> </div>
} }
@@ -115,6 +123,19 @@ class SourceTypeEditor extends React.Component {
case 'tilexyz_vector': return <TileURLSourceEditor {...commonProps} /> case 'tilexyz_vector': return <TileURLSourceEditor {...commonProps} />
case 'tilejson_raster': return <TileJSONSourceEditor {...commonProps} /> case 'tilejson_raster': return <TileJSONSourceEditor {...commonProps} />
case 'tilexyz_raster': return <TileURLSourceEditor {...commonProps} /> case 'tilexyz_raster': return <TileURLSourceEditor {...commonProps} />
case 'tilejson_raster-dem': return <TileJSONSourceEditor {...commonProps} />
case 'tilexyz_raster-dem': return <TileURLSourceEditor {...commonProps}>
<InputBlock label={"Encoding"} doc={styleSpec.latest.source_raster_dem.encoding.doc}>
<SelectInput
options={Object.keys(styleSpec.latest.source_raster_dem.encoding.values)}
onChange={encoding => this.props.onChange({
...this.props.source,
encoding: encoding
})}
value={this.props.source.encoding || styleSpec.latest.source_raster_dem.encoding.default}
/>
</InputBlock>
</TileURLSourceEditor>
default: return null default: return null
} }
} }

View File

@@ -91,7 +91,8 @@
"circle-stroke-width", "circle-stroke-width",
"circle-pitch-scale", "circle-pitch-scale",
"circle-translate", "circle-translate",
"circle-translate-anchor" "circle-translate-anchor",
"circle-pitch-alignment"
] ]
} }
] ]
@@ -147,7 +148,9 @@
"icon-rotate", "icon-rotate",
"icon-padding", "icon-padding",
"icon-keep-upright", "icon-keep-upright",
"icon-offset" "icon-offset",
"icon-anchor",
"icon-pitch-alignment"
] ]
}, },
{ {
@@ -194,5 +197,35 @@
] ]
} }
] ]
},
"hillshade": {
"groups": [
{
"title": "Paint properties",
"type": "properties",
"fields": [
"hillshade-illumination-direction",
"hillshade-illumination-anchor",
"hillshade-exaggeration",
"hillshade-shadow-color",
"hillshade-highlight-color",
"hillshade-accent-color"
]
}
]
},
"heatmap": {
"groups": [
{
"title": "Paint properties",
"type": "properties",
"fields": [
"heatmap-radius",
"heatmap-weight",
"heatmap-intensity",
"heatmap-opacity"
]
}
]
} }
} }

View File

@@ -26,8 +26,8 @@
{ {
"id": "osm-liberty", "id": "osm-liberty",
"title": "OSM Liberty", "title": "OSM Liberty",
"url": "https://rawgit.com/lukasmartinelli/osm-liberty/gh-pages/style.json", "url": "https://rawgit.com/maputnik/osm-liberty/gh-pages/style.json",
"thumbnail": "https://cdn.rawgit.com/lukasmartinelli/osm-liberty/gh-pages/thumbnail.png" "thumbnail": "https://cdn.rawgit.com/maputnik/osm-liberty/gh-pages/thumbnail.png"
}, },
{ {
"id": "empty-style", "id": "empty-style",

12
src/libs/accessibility.js Normal file
View File

@@ -0,0 +1,12 @@
import lodash from 'lodash'
// Throttle for 3 seconds so when a user enables it they don't have to refresh the page.
const reducedMotionEnabled = lodash.throttle(() => {
return window.matchMedia("(prefers-reduced-motion: reduce)").matches
}, 3000);
export default {
reducedMotionEnabled
}

44
src/libs/debug.js Normal file
View File

@@ -0,0 +1,44 @@
import querystring from 'querystring'
const debugStore = {};
function enabled() {
const qs = querystring.parse(window.location.search.slice(1));
if(qs.hasOwnProperty("debug")) {
return !!qs.debug.match(/^(|1|true)$/);
}
else {
return false;
}
}
function genErr() {
return new Error("Debug not enabled, enable by appending '?debug' to your query string");
}
function set(namespace, key, value) {
if(!enabled()) {
throw genErr();
}
debugStore[namespace] = debugStore[namespace] || {};
debugStore[namespace][key] = value;
}
function get(namespace, key) {
if(!enabled()) {
throw genErr();
}
if(debugStore.hasOwnProperty(namespace)) {
return debugStore[namespace][key];
}
}
const mod = {
enabled,
get,
set
}
window.debug = mod;
export default mod;

View File

@@ -1,4 +1,4 @@
import styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec' import * as styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
export function diffMessages(beforeStyle, afterStyle) { export function diffMessages(beforeStyle, afterStyle) {
const changes = styleSpec.diff(beforeStyle, afterStyle) const changes = styleSpec.diff(beforeStyle, afterStyle)

View File

@@ -1,4 +1,4 @@
import styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec' import * as styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
export const combiningFilterOps = ['all', 'any', 'none'] export const combiningFilterOps = ['all', 'any', 'none']
export const setFilterOps = ['in', '!in'] export const setFilterOps = ['in', '!in']
export const otherFilterOps = Object export const otherFilterOps = Object

View File

@@ -1,4 +1,4 @@
import styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec' import * as styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
export function changeType(layer, newType) { export function changeType(layer, newType) {
const changedPaintProps = { ...layer.paint } const changedPaintProps = { ...layer.paint }

17
src/libs/query-util.js Normal file
View File

@@ -0,0 +1,17 @@
function asBool(queryObj, key) {
if(queryObj.hasOwnProperty(key)) {
if(queryObj[key].match(/^false|0$/)) {
return false;
}
else {
return true;
}
}
else {
return false;
}
}
module.exports = {
asBool
}

View File

@@ -54,12 +54,23 @@ function indexOfLayer(layers, layerId) {
return null return null
} }
function replaceAccessToken(mapStyle) { function replaceAccessToken(mapStyle, opts={}) {
const omtSource = mapStyle.sources.openmaptiles const omtSource = mapStyle.sources.openmaptiles
if(!omtSource) return mapStyle if(!omtSource) return mapStyle
if(!omtSource.hasOwnProperty("url")) return mapStyle
const metadata = mapStyle.metadata || {} const metadata = mapStyle.metadata || {}
const accessToken = metadata['maputnik:openmaptiles_access_token'] || tokens.openmaptiles let accessToken = metadata['maputnik:openmaptiles_access_token'];
if(opts.allowFallback && !accessToken) {
accessToken = tokens.openmaptiles;
}
if(!accessToken) {
// Early exit.
return mapStyle;
}
const changedSources = { const changedSources = {
...mapStyle.sources, ...mapStyle.sources,
openmaptiles: { openmaptiles: {

View File

@@ -18,6 +18,11 @@ html {
box-sizing: border-box; box-sizing: border-box;
} }
body {
// The UI is 100% height so prevent bounce scroll on OSX
overflow: hidden;
}
*, *,
*::before, *::before,
*::after { *::after {
@@ -76,3 +81,7 @@ label:hover {
a { a {
color: white; color: white;
} }
.hide {
display: none !important;
}

View File

@@ -5,7 +5,11 @@
right: 0; right: 0;
bottom: 0; bottom: 0;
height: calc(100% - #{$toolbar-height + $toolbar-offset}); height: calc(100% - #{$toolbar-height + $toolbar-offset});
width: 75%; width: calc(
100%
- 200px /* layer list */
- 350px /* layer editor */
);
} }
// DOC LABEL // DOC LABEL
@@ -46,14 +50,17 @@
// BUTTON // BUTTON
.maputnik-button { .maputnik-button {
display: inline-block;
cursor: pointer; cursor: pointer;
background-color: $color-midgray; background-color: $color-midgray;
color: $color-lowgray; color: $color-lowgray;
font-size: $font-size-6; font-size: $font-size-6;
padding: $margin-2; padding: $margin-2;
user-select: none; user-select: none;
border-width: 0;
border-radius: 2px; border-radius: 2px;
box-sizing: border-box; box-sizing: border-box;
text-decoration: none;
&:hover { &:hover {
background-color: lighten($color-midgray, 12); background-color: lighten($color-midgray, 12);
@@ -68,6 +75,20 @@
font-size: $font-size-5; font-size: $font-size-5;
} }
.maputnik-wide-button {
padding: $margin-2 $margin-3;
}
.maputnik-green-button {
background-color: $color-green;
color: $color-black;
}
.maputnik-white-button {
background-color: $color-white;
color: $color-black;
}
.maputnik-icon-button { .maputnik-icon-button {
background-color: transparent; background-color: transparent;

View File

@@ -3,24 +3,34 @@
} }
.maputnik-filter-editor { .maputnik-filter-editor {
@extend .clearfix;
color: $color-lowgray; color: $color-lowgray;
} }
.maputnik-filter-editor-property { .maputnik-filter-editor-property {
display: inline-block; display: inline-block;
width: '22%'; width: 25%;
} }
.maputnik-filter-editor-operator { .maputnik-filter-editor-operator {
display: inline-block;
width: 19%;
margin-left: 2%; margin-left: 2%;
display: inline-block;
width: 17%;
.maputnik-select {
width: 100%;
}
} }
.maputnik-filter-editor-args { .maputnik-filter-editor-args {
display: inline-block; display: inline-block;
width: 54%; width: 54%;
margin-left: 2%; margin-left: 2%;
.maputnik-string,
.maputnik-number {
width: 100%;
}
} }
.maputnik-filter-editor-compound-select { .maputnik-filter-editor-compound-select {
@@ -40,10 +50,6 @@
color: $color-midgray; color: $color-midgray;
} }
.maputnik-filter-editor {
@extend .clearfix;
}
.maputnik-add-filter { .maputnik-add-filter {
display: inline-block; display: inline-block;
float: right; float: right;
@@ -57,9 +63,6 @@
.maputnik-filter-editor-block-action { .maputnik-filter-editor-block-action {
margin-top: $margin-2; margin-top: $margin-2;
margin-bottom: $margin-2; margin-bottom: $margin-2;
}
.maputnik-filter-editor-block-action {
display: inline-block; display: inline-block;
width: 6%; width: 6%;
margin-right: 1.5%; margin-right: 1.5%;
@@ -70,27 +73,3 @@
width: 92.5%; 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%;
}
}

View File

@@ -41,7 +41,7 @@
.maputnik-color-swatch { .maputnik-color-swatch {
height: 26px; height: 26px;
width: 3px; width: 14px;
flex-shrink: 0; flex-shrink: 0;
flex-grow: 0; flex-grow: 0;
} }
@@ -91,7 +91,6 @@
.maputnik-button-selected { .maputnik-button-selected {
background-color: lighten($color-midgray, 12); background-color: lighten($color-midgray, 12);
outline: 1px $color-white;
color: white; color: white;
} }
@@ -126,6 +125,10 @@
border-width: 2px; border-width: 2px;
border-color: $color-gray; border-color: $color-gray;
transition: background-color 0.1s ease-out; transition: background-color 0.1s ease-out;
@media screen and (prefers-reduced-motion: reduce) {
transition-duration: 0ms;
}
} }
&-icon { &-icon {

View File

@@ -43,17 +43,32 @@
-webkit-transition: opacity 600ms, visibility 600ms; -webkit-transition: opacity 600ms, visibility 600ms;
transition: opacity 600ms, visibility 600ms; transition: opacity 600ms, visibility 600ms;
@media screen and (prefers-reduced-motion: reduce) {
transition-duration: 0;
}
@include flex-row; @include flex-row;
} }
&-icon-action svg { &-icon-action {
display: none;
svg {
fill: $color-black; fill: $color-black;
} }
}
.maputnik-layer-list-item:hover, .maputnik-layer-list-item:hover,
.maputnik-layer-list-item-selected { .maputnik-layer-list-item-selected {
background-color: lighten($color-black, 2); background-color: lighten($color-black, 2);
.maputnik-layer-list-icon-action {
display: block;
background: initial;
border: none;
padding: 0 2px;
}
.maputnik-layer-list-icon-action svg { .maputnik-layer-list-icon-action svg {
fill: darken($color-lowgray, 0.5); fill: darken($color-lowgray, 0.5);
@@ -122,6 +137,7 @@
user-select: none; user-select: none;
padding: $margin-2; padding: $margin-2;
line-height: 20px; line-height: 20px;
border-top: solid 1px #36383e;
@include flex-row; @include flex-row;
@@ -164,3 +180,41 @@
color: $color-lowgray; color: $color-lowgray;
} }
} }
.more-menu {
position: relative;
&__menu {
position: absolute;
z-index: 9999;
background: $color-black;
border: solid 1px $color-midgray;
right: 0;
min-width: 120px;
}
&__button__svg {
width: 24px;
height: 24px;
}
&__menu__item {
padding: 4px;
}
}
.layer-header {
display: flex;
padding: 6px;
background: $color-black;
&__title {
flex: 1;
margin: 0;
line-height: 24px;
}
&__info {
min-width: 28px;
}
}

View File

@@ -1,6 +1,6 @@
//SCROLLING //SCROLLING
.maputnik-scroll-container { .maputnik-scroll-container {
overflow-x: visible; overflow-x: hidden;
overflow-y: scroll; overflow-y: scroll;
bottom: 0; bottom: 0;
left: 0; left: 0;

View File

@@ -7,6 +7,7 @@
box-shadow: 0 0 5px 0 rgba(0, 0, 0, 0.3); box-shadow: 0 0 5px 0 rgba(0, 0, 0, 0.3);
z-index: 3; z-index: 3;
position: relative; position: relative;
font-family: $font-family;
} }
.maputnik-modal-section { .maputnik-modal-section {
@@ -21,6 +22,10 @@
flex-shrink: 0; flex-shrink: 0;
} }
.maputnik-modal-sub-section {
margin-top: $margin-1;
}
.maputnik-modal-section--shrink { .maputnik-modal-section--shrink {
flex-shrink: 1; flex-shrink: 1;
} }
@@ -38,7 +43,10 @@
} }
.maputnik-modal-header-toggle { .maputnik-modal-header-toggle {
cursor: pointer; border: none;
background: initial;
color: white;
padding: 0;
} }
.maputnik-modal-scroller { .maputnik-modal-scroller {
@@ -56,30 +64,6 @@
@extend .maputnik-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 //OPEN MODAL
.maputnik-upload-button { .maputnik-upload-button {
@extend .maputnik-big-button; @extend .maputnik-big-button;
@@ -104,6 +88,7 @@
background-color: $color-gray; background-color: $color-gray;
padding: $margin-3; padding: $margin-3;
display: block; display: block;
width: 100%;
&:hover { &:hover {
background-color: $color-midgray; background-color: $color-midgray;
@@ -118,6 +103,9 @@
display: block; display: block;
margin-top: $margin-2; margin-top: $margin-2;
width: 100%; width: 100%;
padding-top: calc(400 / 600 * 100%);
background-size: cover;
background-color: $color-midgray;
} }
.maputnik-add-layer { .maputnik-add-layer {
@@ -151,6 +139,7 @@
font-size: $font-size-5; font-size: $font-size-5;
color: $color-lowgray; color: $color-lowgray;
background-color: transparent; background-color: transparent;
width: 100%;
@include flex-row; @include flex-row;
} }
@@ -234,3 +223,38 @@
text-decoration: none; text-decoration: none;
color: #ef5350; color: #ef5350;
} }
.maputnik-modal-shortcuts {
code {
color: white;
background: #3c3c3c;
padding: 2px 6px;
display: inline-block;
text-align: center;
border-radius: 2px;
margin-right: 4px;
font-family: monospace;
}
li {
margin-bottom: 4px;
}
}
.maputnik-modal-survey {
width: 372px;
}
.maputnik-modal-survey__logo {
display: block;
margin: 0 auto;
}
.maputnik-modal-survey__description {
line-height: 1.5;
}
.maputnik-modal-survey__footnote {
color: $color-green;
margin-top: 16px;
}

View File

@@ -0,0 +1,3 @@
.react-codemirror2 {
max-width: 100%;
}

View File

@@ -0,0 +1,9 @@
// See <https://github.com/nkbt/react-collapse/commit/4f4fbce7c6c07b082dc62062338c9294c656f9df>
.react-collapse-container {
display: flex;
max-width: 100%;
> * {
flex: 1;
}
}

View File

@@ -9,15 +9,25 @@
background-color: $color-black; background-color: $color-black;
} }
.maputnik-toolbar-logo-container {
position: relative;
}
.maputnik-toolbar-logo { .maputnik-toolbar-logo {
text-decoration: none;
display: block;
flex: 0 0 180px;
width: 180px; width: 180px;
text-align: left; text-align: left;
background-color: $color-black; background-color: $color-black;
padding: $margin-2; padding: $margin-2;
height: $toolbar-height; height: $toolbar-height;
position: relative;
overflow: hidden;
h1 { h1 {
display: inline; display: inline;
line-height: 26px;
} }
img { img {
@@ -36,13 +46,49 @@
cursor: pointer; cursor: pointer;
color: $color-white; color: $color-white;
text-decoration: none; text-decoration: none;
line-height: 20px;
h1 {
position: relative;
}
&:hover { &:hover {
background-color: $color-midgray; background-color: $color-midgray;
} }
} }
.maputnik-toolbar-link--highlighted {
line-height: 1;
padding: $margin-2 $margin-3;
.maputnik-toolbar-link-wrapper {
background-color: $color-white;
border-radius: 2px;
padding: $margin-2;
margin-top: $margin-1;
color: $color-black;
display: block;
}
&:hover {
background-color: $color-black;
}
&:hover .maputnik-toolbar-link-wrapper {
background-color: lighten($color-midgray, 12);
color: $color-white;
}
}
.maputnik-toolbar-version {
font-size: 10px;
margin-left: 4px;
white-space: nowrap;
}
.maputnik-toolbar-action { .maputnik-toolbar-action {
background: inherit;
border-width: 0;
@extend .maputnik-toolbar-link; @extend .maputnik-toolbar-link;
} }
@@ -55,10 +101,6 @@
margin-left: $margin-1; margin-left: $margin-1;
} }
.maputnik-toolbar-logo {
flex: 0 0 140px;
}
.maputnik-toolbar__inner { .maputnik-toolbar__inner {
display: flex; display: flex;
} }
@@ -68,3 +110,23 @@
flex: 1; flex: 1;
overflow-y: auto; overflow-y: auto;
} }
.maputnik-toolbar-skip {
position: absolute;
overflow: hidden;
width: 0px;
height: 100%;
text-align: center;
display: block;
background-color: $color-black;
z-index: 999;
line-height: 40px;
left: 0;
top: 0;
&:active,
&:focus {
width: 100%;
}
}

View File

@@ -64,10 +64,6 @@
margin-right: $margin-3; margin-right: $margin-3;
} }
.maputnik-zoom-spec-property .maputnik-input-block:not(:first-child) .maputnik-input-block-label {
visibility: hidden;
}
// DATA FUNC // DATA FUNC
.maputnik-make-data-function { .maputnik-make-data-function {
background-color: transparent; background-color: transparent;
@@ -79,16 +75,15 @@
@extend .maputnik-icon-button; @extend .maputnik-icon-button;
} }
// DATA PROPERTY
.maputnik-data-spec-block {
overflow: auto;
}
.maputnik-data-spec-property { .maputnik-data-spec-property {
.maputnik-input-block-label { .maputnik-input-block-label {
width: 30%; width: 30%;
} }
.maputnik-input-block:not(:first-child) .maputnik-input-block-label {
visibility: hidden;
}
.maputnik-input-block-content { .maputnik-input-block-content {
width: 70%; width: 70%;
} }
@@ -117,6 +112,8 @@
} }
.maputnik-data-spec-block { .maputnik-data-spec-block {
overflow: auto;
.maputnik-data-spec-property-stop-edit, .maputnik-data-spec-property-stop-edit,
.maputnik-data-spec-property-stop-data { .maputnik-data-spec-property-stop-data {
display: inline-block; display: inline-block;
@@ -129,6 +126,10 @@
} }
.maputnik-data-spec-property-stop-data { .maputnik-data-spec-property-stop-data {
width: 100%;
}
.maputnik-data-spec-property-stop-edit + .maputnik-data-spec-property-stop-data {
width: 78%; width: 78%;
} }
} }

View File

@@ -4,6 +4,7 @@ $color-midgray: #36383e;
$color-lowgray: #8e8e8e; $color-lowgray: #8e8e8e;
$color-white: #f0f0f0; $color-white: #f0f0f0;
$color-red: #cf4a4a; $color-red: #cf4a4a;
$color-green: #53b972;
$margin-1: 3px; $margin-1: 3px;
$margin-2: 5px; $margin-2: 5px;
$margin-3: 10px; $margin-3: 10px;
@@ -36,3 +37,16 @@ $toolbar-offset: 0;
@import 'zoomproperty'; @import 'zoomproperty';
@import 'popup'; @import 'popup';
@import 'map'; @import 'map';
@import 'react-collapse';
@import 'react-codemirror';
/**
* Hacks for webdriverio isVisibleWithinViewport
*/
#app {
height: 100vh;
}
.maputnik-layout {
height: 100vh;
}

View File

@@ -69,6 +69,87 @@
</style> </style>
</head> </head>
<body> <body>
<!-- TODO: Import dynamically -->
<!-- From <https://github.com/hail2u/color-blindness-emulation> -->
<svg
xmlns="http://www.w3.org/2000/svg"
version="1.1">
<defs>
<filter id="protanopia">
<feColorMatrix
in="SourceGraphic"
type="matrix"
values="0.567, 0.433, 0, 0, 0
0.558, 0.442, 0, 0, 0
0, 0.242, 0.758, 0, 0
0, 0, 0, 1, 0"/>
</filter>
<filter id="protanomaly">
<feColorMatrix
in="SourceGraphic"
type="matrix"
values="0.817, 0.183, 0, 0, 0
0.333, 0.667, 0, 0, 0
0, 0.125, 0.875, 0, 0
0, 0, 0, 1, 0"/>
</filter>
<filter id="deuteranopia">
<feColorMatrix
in="SourceGraphic"
type="matrix"
values="0.625, 0.375, 0, 0, 0
0.7, 0.3, 0, 0, 0
0, 0.3, 0.7, 0, 0
0, 0, 0, 1, 0"/>
</filter>
<filter id="deuteranomaly">
<feColorMatrix
in="SourceGraphic"
type="matrix"
values="0.8, 0.2, 0, 0, 0
0.258, 0.742, 0, 0, 0
0, 0.142, 0.858, 0, 0
0, 0, 0, 1, 0"/>
</filter>
<filter id="tritanopia">
<feColorMatrix
in="SourceGraphic"
type="matrix"
values="0.95, 0.05, 0, 0, 0
0, 0.433, 0.567, 0, 0
0, 0.475, 0.525, 0, 0
0, 0, 0, 1, 0"/>
</filter>
<filter id="tritanomaly">
<feColorMatrix
in="SourceGraphic"
type="matrix"
values="0.967, 0.033, 0, 0, 0
0, 0.733, 0.267, 0, 0
0, 0.183, 0.817, 0, 0
0, 0, 0, 1, 0"/>
</filter>
<filter id="achromatopsia">
<feColorMatrix
in="SourceGraphic"
type="matrix"
values="0.299, 0.587, 0.114, 0, 0
0.299, 0.587, 0.114, 0, 0
0.299, 0.587, 0.114, 0, 0
0, 0, 0, 1, 0"/>
</filter>
<filter id="achromatomaly">
<feColorMatrix
in="SourceGraphic"
type="matrix"
values="0.618, 0.320, 0.062, 0, 0
0.163, 0.775, 0.062, 0, 0
0.163, 0.320, 0.516, 0, 0
0, 0, 0, 1, 0"/>
</filter>
</defs>
</svg>
<div id="app"> <div id="app">
<div id="loader">Loading...</div> <div id="loader">Loading...</div>
</div> </div>

39
test/artifacts.js Normal file
View File

@@ -0,0 +1,39 @@
var path = require("path");
var mkdirp = require("mkdirp");
function genPath(subPath) {
subPath = subPath || ".";
var buildPath;
if(process.env.CIRCLECI) {
buildPath = path.join("/tmp/artifacts", subPath);
}
else {
buildPath = path.join(__dirname, '..', 'build', subPath);
}
return buildPath;
}
module.exports.path = function(subPath) {
var dirPath = genPath(subPath);
return new Promise(function(resolve, reject) {
mkdirp(dirPath, function(err) {
if(err) {
reject(err);
}
else {
resolve(dirPath);
}
});
});
}
module.exports.pathSync = function(subPath) {
var dirPath = genPath(subPath);
mkdirp.sync(dirPath);
return dirPath;
}

12
test/example-style.json Normal file
View File

@@ -0,0 +1,12 @@
{
"id": "test-style",
"version": 8,
"name": "Test Style",
"metadata": {
"maputnik:renderer": "mbgljs"
},
"sources": {},
"glyphs": "https://example.local/fonts/{fontstack}/{range}.pbf",
"sprites": "https://example.local/fonts/{fontstack}/{range}.pbf",
"layers": []
}

77
test/functional/helper.js Normal file
View File

@@ -0,0 +1,77 @@
var wd = require("../wd-helper");
var uuid = require('uuid/v1');
var geoServer = require("../geojson-server");
var geoserver = geoServer.listen(9002);
module.exports = {
getStyleUrl: function(styles) {
var port = geoserver.address().port;
return "http://localhost:"+port+"/styles/empty/"+styles.join(",");
},
getGeoServerUrl: function(urlPath) {
var port = geoserver.address().port;
return "http://localhost:"+port+"/"+urlPath;
},
getStyleStore: function(browser) {
var result = browser.executeAsync(function(done) {
window.debug.get("maputnik", "styleStore").latestStyle(done);
})
return result.value;
},
getRevisionStore: function(browser) {
var result = browser.execute(function(done) {
var rs = window.debug.get("maputnik", "revisionStore")
return {
currentIdx: rs.currentIdx,
revisions: rs.revisions
};
})
return result.value;
},
modal: {
addLayer: {
open: function() {
var selector = wd.$('layer-list:add-layer');
browser.click(selector);
// Wait for events
browser.flushReactUpdates();
browser.waitForExist(wd.$('modal:add-layer'));
browser.isVisible(wd.$('modal:add-layer'));
browser.isVisibleWithinViewport(wd.$('modal:add-layer'));
// Wait for events
browser.flushReactUpdates();
},
fill: function(opts) {
var type = opts.type;
var layer = opts.layer;
var id;
if(opts.id) {
id = opts.id
}
else {
id = type+":"+uuid();
}
browser.selectByValue(wd.$("add-layer.layer-type", "select"), type);
browser.flushReactUpdates();
browser.setValueSafe(wd.$("add-layer.layer-id", "input"), id);
if(layer) {
browser.setValueSafe(wd.$("add-layer.layer-source-block", "input"), layer);
}
browser.flushReactUpdates();
browser.click(wd.$("add-layer"));
return id;
}
}
}
}

View File

@@ -0,0 +1,100 @@
var assert = require("assert");
var config = require("../../config/specs");
var helper = require("../helper");
var wd = require("../../wd-helper");
describe.skip("history", function() {
/**
* See <https://github.com/webdriverio/webdriverio/issues/1126>
*/
it("undo/redo", function() {
var styleObj;
browser.url(config.baseUrl+"?debug&style="+helper.getStyleUrl([
"geojson:example"
]));
helper.modal.addLayer.open();
styleObj = helper.getStyleStore(browser);
assert.deepEqual(styleObj.layers, []);
helper.modal.addLayer.fill({
id: "step 1",
type: "background"
})
styleObj = helper.getStyleStore(browser);
assert.deepEqual(styleObj.layers, [
{
"id": "step 1",
"type": 'background'
}
]);
helper.modal.addLayer.open();
helper.modal.addLayer.fill({
id: "step 2",
type: "background"
})
styleObj = helper.getStyleStore(browser);
assert.deepEqual(styleObj.layers, [
{
"id": "step 1",
"type": 'background'
},
{
"id": "step 2",
"type": 'background'
}
]);
browser
.keys(['Control', 'z'])
.keys(['Control']);
styleObj = helper.getStyleStore(browser);
assert.deepEqual(styleObj.layers, [
{
"id": "step 1",
"type": 'background'
}
]);
browser
.keys(['Control', 'z'])
.keys(['Control']);
styleObj = helper.getStyleStore(browser);
assert.deepEqual(styleObj.layers, [
]);
browser
.keys(['Control', 'y'])
.keys(['Control']);
styleObj = helper.getStyleStore(browser);
assert.deepEqual(styleObj.layers, [
{
"id": "step 1",
"type": 'background'
}
]);
browser
.keys(['Control', 'y'])
.keys(['Control']);
styleObj = helper.getStyleStore(browser);
assert.deepEqual(styleObj.layers, [
{
"id": "step 1",
"type": 'background'
},
{
"id": "step 2",
"type": 'background'
}
]);
});
})

33
test/functional/index.js Normal file
View File

@@ -0,0 +1,33 @@
var assert = require('assert');
var config = require("../config/specs");
var geoServer = require("../geojson-server");
var helper = require("./helper");
require("./util/webdriverio-ext");
describe('maputnik', function() {
beforeEach(function() {
browser.url(config.baseUrl+"?debug&style="+helper.getStyleUrl([
"geojson:example",
"raster:raster"
]));
browser.waitForExist(".maputnik-toolbar-link");
browser.flushReactUpdates();
});
// -------- setup --------
require("./util/coverage");
// -----------------------
// ---- All the tests ----
require("./history");
require("./layers");
require("./map");
require("./modals");
require("./screenshots");
// ------------------------
});

View File

@@ -0,0 +1,485 @@
var assert = require("assert");
var config = require("../../config/specs");
var helper = require("../helper");
var uuid = require('uuid/v1');
var wd = require("../../wd-helper");
describe("layers", function() {
beforeEach(function() {
browser.url(config.baseUrl+"?debug&style="+helper.getStyleUrl([
"geojson:example",
"raster:raster"
]));
browser.waitForExist(".maputnik-toolbar-link");
browser.flushReactUpdates();
helper.modal.addLayer.open();
});
describe("ops", function() {
it("delete", function() {
var styleObj;
var id = helper.modal.addLayer.fill({
type: "background"
})
styleObj = helper.getStyleStore(browser);
assert.deepEqual(styleObj.layers, [
{
"id": id,
"type": 'background'
},
]);
browser.click(wd.$("layer-list-item:"+id+":delete", ""));
styleObj = helper.getStyleStore(browser);
assert.deepEqual(styleObj.layers, [
]);
});
it("duplicate", function() {
var styleObj;
var id = helper.modal.addLayer.fill({
type: "background"
})
styleObj = helper.getStyleStore(browser);
assert.deepEqual(styleObj.layers, [
{
"id": id,
"type": 'background'
},
]);
browser.click(wd.$("layer-list-item:"+id+":copy", ""));
styleObj = helper.getStyleStore(browser);
assert.deepEqual(styleObj.layers, [
{
"id": id+"-copy",
"type": "background"
},
{
"id": id,
"type": "background"
},
]);
});
it("hide", function() {
var styleObj;
var id = helper.modal.addLayer.fill({
type: "background"
})
styleObj = helper.getStyleStore(browser);
assert.deepEqual(styleObj.layers, [
{
"id": id,
"type": 'background'
},
]);
browser.click(wd.$("layer-list-item:"+id+":toggle-visibility", ""));
styleObj = helper.getStyleStore(browser);
assert.deepEqual(styleObj.layers, [
{
"id": id,
"type": "background",
"layout": {
"visibility": "none"
}
},
]);
browser.click(wd.$("layer-list-item:"+id+":toggle-visibility", ""));
styleObj = helper.getStyleStore(browser);
assert.deepEqual(styleObj.layers, [
{
"id": id,
"type": "background",
"layout": {
"visibility": "visible"
}
},
]);
})
})
describe("grouped", function() {
it("with underscore")
it("no without underscore")
it("double underscore only grouped once")
})
describe("tooltips", function() {
})
describe("help", function() {
})
describe('background', function () {
it.skip("add", function() {
var id = helper.modal.addLayer.fill({
type: "background"
})
browser.waitUntil(function() {
var styleObj = helper.getStyleStore(browser);
assert.deepEqual(styleObj.layers, [
{
"id": id,
"type": 'background'
}
]);
});
});
describe("modify", function() {
function createBackground() {
// Setup
var id = uuid();
browser.selectByValue(wd.$("add-layer.layer-type", "select"), "background");
browser.flushReactUpdates();
browser.setValueSafe(wd.$("add-layer.layer-id", "input"), "background:"+id);
browser.click(wd.$("add-layer"));
var styleObj = helper.getStyleStore(browser);
assert.deepEqual(styleObj.layers, [
{
"id": 'background:'+id,
"type": 'background'
}
]);
return id;
}
// ====> THESE SHOULD BE FROM THE SPEC
describe("layer", function() {
it("expand/collapse");
it("id", function() {
var bgId = createBackground();
browser.click(wd.$("layer-list-item:background:"+bgId))
var id = uuid();
browser.setValueSafe(wd.$("layer-editor.layer-id", "input"), "foobar:"+id)
browser.click(wd.$("min-zoom"))
var styleObj = helper.getStyleStore(browser);
assert.deepEqual(styleObj.layers, [
{
"id": 'foobar:'+id,
"type": 'background'
}
]);
});
// NOTE: This needs to be removed from the code
it("type");
it("min-zoom", function() {
var bgId = createBackground();
browser.click(wd.$("layer-list-item:background:"+bgId))
browser.setValueSafe(wd.$("min-zoom", "input"), 1)
browser.click(wd.$("layer-editor.layer-id", "input"));
var styleObj = helper.getStyleStore(browser);
assert.deepEqual(styleObj.layers, [
{
"id": 'background:'+bgId,
"type": 'background',
"minzoom": 1
}
]);
// AND RESET!
// browser.setValueSafe(wd.$("min-zoom", "input"), "")
// browser.click(wd.$("max-zoom", "input"));
// var styleObj = helper.getStyleStore(browser);
// assert.deepEqual(styleObj.layers, [
// {
// "id": 'background:'+bgId,
// "type": 'background'
// }
// ]);
});
it("max-zoom", function() {
var bgId = createBackground();
browser.click(wd.$("layer-list-item:background:"+bgId))
browser.setValueSafe(wd.$("max-zoom", "input"), 1)
browser.click(wd.$("layer-editor.layer-id", "input"));
var styleObj = helper.getStyleStore(browser);
assert.deepEqual(styleObj.layers, [
{
"id": 'background:'+bgId,
"type": 'background',
"maxzoom": 1
}
]);
});
it("comments", function() {
var bgId = createBackground();
var id = uuid();
browser.click(wd.$("layer-list-item:background:"+bgId));
browser.setValueSafe(wd.$("layer-comment", "textarea"), id);
browser.click(wd.$("layer-editor.layer-id", "input"));
var styleObj = helper.getStyleStore(browser);
assert.deepEqual(styleObj.layers, [
{
"id": 'background:'+bgId,
"type": 'background',
metadata: {
'maputnik:comment': id
}
}
]);
// Unset it again.
// TODO: This fails
// browser.setValueSafe(wd.$("layer-comment", "textarea"), "");
// browser.click(wd.$("min-zoom", "input"));
// browser.flushReactUpdates();
// var styleObj = helper.getStyleStore(browser);
// assert.deepEqual(styleObj.layers, [
// {
// "id": 'background:'+bgId,
// "type": 'background'
// }
// ]);
});
it("color", null, function() {
var bgId = createBackground();
var id = uuid();
browser.click(wd.$("layer-list-item:background:"+bgId));
browser.click(wd.$("spec-field:background-color", "input"))
// browser.debug();
var styleObj = helper.getStyleStore(browser);
assert.deepEqual(styleObj.layers, [
{
"id": 'background:'+bgId,
"type": 'background'
}
]);
})
})
describe("filter", function() {
it("expand/collapse");
it("compound filter");
})
describe("paint", function() {
it("expand/collapse");
it("color");
it("pattern");
it("opacity");
})
// <=====
describe("json-editor", function() {
it("expand/collapse");
it("modify");
// TODO
it.skip("parse error", function() {
var bgId = createBackground();
var id = uuid();
browser.click(wd.$("layer-list-item:background:"+bgId));
var errorSelector = ".CodeMirror-lint-marker-error";
assert.equal(browser.isExisting(errorSelector), false);
browser.click(".CodeMirror")
browser.keys("\uE013\uE013\uE013\uE013\uE013\uE013\uE013\uE013\uE013\uE013\uE013\uE013 {");
browser.waitForExist(errorSelector)
browser.click(wd.$("layer-editor.layer-id"));
});
});
})
});
describe('fill', function () {
it.skip("add", function() {
// browser.debug();
var id = helper.modal.addLayer.fill({
type: "fill",
layer: "example"
});
var styleObj = helper.getStyleStore(browser);
assert.deepEqual(styleObj.layers, [
{
"id": id,
"type": 'fill',
"source": "example"
}
]);
})
// TODO: Change source
it("change source")
});
describe('line', function () {
it.skip("add", function() {
var id = helper.modal.addLayer.fill({
type: "line",
layer: "example"
});
var styleObj = helper.getStyleStore(browser);
assert.deepEqual(styleObj.layers, [
{
"id": id,
"type": "line",
"source": "example",
}
]);
});
it("groups", null, function() {
// TODO
// Click each of the layer groups.
})
});
describe('symbol', function () {
it.skip("add", function() {
var id = helper.modal.addLayer.fill({
type: "symbol",
layer: "example"
});
var styleObj = helper.getStyleStore(browser);
assert.deepEqual(styleObj.layers, [
{
"id": id,
"type": "symbol",
"source": "example",
}
]);
});
});
describe('raster', function () {
it.skip("add", function() {
var id = helper.modal.addLayer.fill({
type: "raster",
layer: "raster"
});
var styleObj = helper.getStyleStore(browser);
assert.deepEqual(styleObj.layers, [
{
"id": id,
"type": "raster",
"source": "raster",
}
]);
});
});
describe('circle', function () {
it.skip("add", function() {
var id = helper.modal.addLayer.fill({
type: "circle",
layer: "example"
});
var styleObj = helper.getStyleStore(browser);
assert.deepEqual(styleObj.layers, [
{
"id": id,
"type": "circle",
"source": "example",
}
]);
});
});
describe('fill extrusion', function () {
it.skip("add", function() {
var id = helper.modal.addLayer.fill({
type: "fill-extrusion",
layer: "example"
});
var styleObj = helper.getStyleStore(browser);
assert.deepEqual(styleObj.layers, [
{
"id": id,
"type": 'fill-extrusion',
"source": "example"
}
]);
});
});
describe.skip("groups", function() {
it("simple", function() {
browser.url(config.baseUrl+"?debug&style="+getStyleUrl([
"geojson:example"
]));
helper.modal.addLayer.open();
var aId = helper.modal.addLayer.fill({
id: "foo",
type: "background"
})
helper.modal.addLayer.open();
var bId = helper.modal.addLayer.fill({
id: "foo_bar",
type: "background"
})
helper.modal.addLayer.open();
var bId = helper.modal.addLayer.fill({
id: "foo_baz",
type: "background"
})
browser.waitForExist(wd.$("layer-list-group:foo-0"));
assert.equal(browser.isVisibleWithinViewport(wd.$("layer-list-item:foo")), false);
assert.equal(browser.isVisibleWithinViewport(wd.$("layer-list-item:foo_bar")), false);
assert.equal(browser.isVisibleWithinViewport(wd.$("layer-list-item:foo_baz")), false);
browser.click(wd.$("layer-list-group:foo-0"));
assert.equal(browser.isVisibleWithinViewport(wd.$("layer-list-item:foo")), true);
assert.equal(browser.isVisibleWithinViewport(wd.$("layer-list-item:foo_bar")), true);
assert.equal(browser.isVisibleWithinViewport(wd.$("layer-list-item:foo_baz")), true);
})
})
});

View File

@@ -0,0 +1,35 @@
var assert = require('assert');
var wd = require("../../wd-helper");
var config = require("../../config/specs");
var helper = require("../helper");
describe("map", function() {
describe.skip("zoom level", function() {
it("via url", function() {
var zoomLevel = "12.37"
browser.url(config.baseUrl+"?debug&style="+helper.getStyleUrl([
"geojson:example"
])+"#"+zoomLevel+"/41.3805/2.1635");
browser.waitUntil(function () {
return (
browser.isVisible(".mapboxgl-ctrl-zoom")
&& browser.getText(".mapboxgl-ctrl-zoom") === "Zoom level: "+(zoomLevel)
);
}, 10*1000)
})
it("via map controls", function() {
var zoomLevel = 12.37;
browser.url(config.baseUrl+"?debug&style="+helper.getStyleUrl([
"geojson:example"
])+"#"+zoomLevel+"/41.3805/2.1635");
browser.click(".mapboxgl-ctrl-zoom-in")
browser.waitUntil(function () {
var text = browser.getText(".mapboxgl-ctrl-zoom")
return text === "Zoom level: "+(zoomLevel+1);
}, 10*1000)
})
})
})

View File

@@ -0,0 +1,188 @@
var assert = require('assert');
var fs = require("fs");
var wd = require("../../wd-helper");
var config = require("../../config/specs");
var helper = require("../helper");
function closeModal(wdKey) {
browser.waitUntil(function() {
return browser.isVisibleWithinViewport(wd.$(wdKey));
});
var closeBtnSelector = wd.$(wdKey+".close-modal");
browser.click(closeBtnSelector);
browser.waitUntil(function() {
return !browser.isVisibleWithinViewport(wd.$(wdKey));
});
}
describe("modals", function() {
describe("open", function() {
var styleFilePath = __dirname+"/../../example-style.json";
var styleFileData = JSON.parse(fs.readFileSync(styleFilePath));
beforeEach(function() {
browser.url(config.baseUrl+"?debug");
browser.waitForExist(".maputnik-toolbar-link");
browser.flushReactUpdates();
browser.click(wd.$("nav:open"))
browser.flushReactUpdates();
});
it("close", function() {
closeModal("open-modal");
});
it("upload", function() {
browser.waitForExist("*[type='file']")
browser.chooseFile("*[type='file']", styleFilePath);
var styleObj = helper.getStyleStore(browser);
assert.deepEqual(styleFileData, styleObj);
});
it("load from url", function() {
var styleFileUrl = helper.getGeoServerUrl("example-style.json");
browser.setValueSafe(wd.$("open-modal.url.input"), styleFileUrl);
var selector = wd.$("open-modal.url.button");
browser.click(selector);
// Allow the network request to happen
// NOTE: Its localhost so this should be fast.
browser.pause(300);
var styleObj = helper.getStyleStore(browser);
assert.deepEqual(styleFileData, styleObj);
});
// TODO: Need to work out how to mock out the end points
it("gallery")
})
describe("export", function() {
beforeEach(function() {
browser.url(config.baseUrl+"?debug");
browser.waitForExist(".maputnik-toolbar-link");
browser.flushReactUpdates();
browser.click(wd.$("nav:export"))
browser.flushReactUpdates();
});
it("close", function() {
closeModal("export-modal");
});
// TODO: Work out how to download a file and check the contents
it("download")
// TODO: Work out how to mock the end git points
it("save to gist")
})
describe("sources", function() {
it("active sources")
it("public source")
it("add new source")
})
describe("inspect", function() {
it("toggle", function() {
browser.url(config.baseUrl+"?debug&style="+helper.getStyleUrl([
"geojson:example"
]));
browser.click(wd.$("nav:inspect"));
})
})
describe("style settings", function() {
beforeEach(function() {
browser.url(config.baseUrl+"?debug");
browser.waitForExist(".maputnik-toolbar-link");
browser.flushReactUpdates();
browser.click(wd.$("nav:settings"))
browser.flushReactUpdates();
});
it("name", function() {
browser.setValueSafe(wd.$("modal-settings.name"), "foobar")
browser.click(wd.$("modal-settings.owner"))
browser.flushReactUpdates();
var styleObj = helper.getStyleStore(browser);
assert.equal(styleObj.name, "foobar");
})
it("owner", function() {
browser.setValueSafe(wd.$("modal-settings.owner"), "foobar")
browser.click(wd.$("modal-settings.name"))
browser.flushReactUpdates();
var styleObj = helper.getStyleStore(browser);
assert.equal(styleObj.owner, "foobar");
})
it("sprite url", function() {
browser.setValueSafe(wd.$("modal-settings.sprite"), "http://example.com")
browser.click(wd.$("modal-settings.name"))
browser.flushReactUpdates();
var styleObj = helper.getStyleStore(browser);
assert.equal(styleObj.sprite, "http://example.com");
})
it("glyphs url", function() {
var glyphsUrl = "http://example.com/{fontstack}/{range}.pbf"
browser.setValueSafe(wd.$("modal-settings.glyphs"), glyphsUrl)
browser.click(wd.$("modal-settings.name"))
browser.flushReactUpdates();
var styleObj = helper.getStyleStore(browser);
assert.equal(styleObj.glyphs, glyphsUrl);
})
it("mapbox access token", function() {
var apiKey = "testing123";
browser.setValueSafe(wd.$("modal-settings.maputnik:mapbox_access_token"), apiKey);
browser.click(wd.$("modal-settings.name"))
browser.flushReactUpdates();
var styleObj = helper.getStyleStore(browser);
browser.waitUntil(function() {
return styleObj.metadata["maputnik:mapbox_access_token"] == apiKey;
})
})
it("open map tiles access token", function() {
var apiKey = "testing123";
browser.setValueSafe(wd.$("modal-settings.maputnik:openmaptiles_access_token"), apiKey);
browser.click(wd.$("modal-settings.name"))
browser.flushReactUpdates();
var styleObj = helper.getStyleStore(browser);
assert.equal(styleObj.metadata["maputnik:openmaptiles_access_token"], apiKey);
})
it("style renderer", function() {
var selector = wd.$("modal-settings.maputnik:renderer");
browser.selectByValue(selector, "ol3");
browser.click(wd.$("modal-settings.name"))
browser.flushReactUpdates();
var styleObj = helper.getStyleStore(browser);
assert.equal(styleObj.metadata["maputnik:renderer"], "ol3");
})
})
describe("sources", function() {
it("toggle")
})
})

View File

@@ -0,0 +1,93 @@
var artifacts = require("../../artifacts");
var config = require("../../config/specs");
var helper = require("../helper");
var wd = require("../../wd-helper");
// These will get used in the marketing material. They are also useful to do a quick manual check of the styling across browsers
// NOTE: These duplicate some of the tests, however this is indended becuase it's likely these will change for aesthetic reasons over time
describe('screenshots', function() {
beforeEach(function() {
browser.windowHandleSize({
width: 1280,
height: 800
});
})
it("front_page", function() {
browser.url(config.baseUrl+"?debug&style="+helper.getStyleUrl([
"geojson:example"
]));
browser.waitForExist(".maputnik-toolbar-link");
browser.flushReactUpdates();
browser.takeScreenShot("/front_page.png")
})
it("open", function() {
browser.url(config.baseUrl+"?debug&style="+helper.getStyleUrl([
"geojson:example"
]));
browser.waitForExist(".maputnik-toolbar-link");
browser.flushReactUpdates();
browser.click(wd.$("nav:open"))
browser.flushReactUpdates();
browser.takeScreenShot("/open.png")
})
it("export", function() {
browser.url(config.baseUrl+"?debug&style="+helper.getStyleUrl([
"geojson:example"
]));
browser.waitForExist(".maputnik-toolbar-link");
browser.flushReactUpdates();
browser.click(wd.$("nav:export"))
browser.flushReactUpdates();
browser.takeScreenShot("/export.png")
})
it("sources", function() {
browser.url(config.baseUrl+"?debug&style="+helper.getStyleUrl([
"geojson:example"
]));
browser.waitForExist(".maputnik-toolbar-link");
browser.flushReactUpdates();
browser.click(wd.$("nav:sources"))
browser.flushReactUpdates();
browser.takeScreenShot("/sources.png")
})
it("style settings", function() {
browser.url(config.baseUrl+"?debug&style="+helper.getStyleUrl([
"geojson:example"
]));
browser.waitForExist(".maputnik-toolbar-link");
browser.flushReactUpdates();
browser.click(wd.$("nav:settings"))
browser.flushReactUpdates();
browser.takeScreenShot("/settings.png")
})
it("inspect", function() {
browser.url(config.baseUrl+"?debug&style="+helper.getStyleUrl([
"geojson:example"
]));
browser.waitForExist(".maputnik-toolbar-link");
browser.flushReactUpdates();
browser.click(wd.$("nav:inspect"))
browser.flushReactUpdates();
browser.takeScreenShot("/inspect.png")
})
})

View File

@@ -0,0 +1,24 @@
var artifacts = require("../../artifacts");
var fs = require("fs");
var istanbulCov = require('istanbul-lib-coverage');
var COVERAGE_PATH = artifacts.pathSync("/coverage");
var coverage = istanbulCov.createCoverageMap({});
// Capture the coverage after each test
afterEach(function() {
// Code coverage
var results = browser.execute(function() {
return window.__coverage__;
});
coverage.merge(results.value);
})
// Dump the coverage to a file
after(function() {
var jsonStr = JSON.stringify(coverage, null, 2);
fs.writeFileSync(COVERAGE_PATH+"/coverage.json", jsonStr);
})

View File

@@ -0,0 +1,58 @@
var artifacts = require("../../artifacts");
var fs = require("fs");
var path = require("path");
browser.timeoutsAsyncScript(20*1000);
browser.timeoutsImplicitWait(20*1000);
var SCREENSHOTS_PATH = artifacts.pathSync("/screenshots");
/**
* Sometimes chrome driver can result in the wrong text.
*
* See <https://github.com/webdriverio/webdriverio/issues/1886>
*/
try {
browser.addCommand('setValueSafe', function(selector, text) {
for(var i=0; i<10; i++) {
browser.waitForVisible(selector);
var elements = browser.elements(selector);
if(elements.length > 1) {
throw "Too many elements found";
}
browser.setValue(selector, text);
var browserText = browser.getValue(selector);
if(browserText == text) {
return;
}
else {
console.error("Warning: setValue failed, trying again");
}
}
// Wait for change events to fire and state updated
browser.flushReactUpdates();
})
browser.addCommand('takeScreenShot', function(filepath) {
var data = browser.screenshot();
fs.writeFileSync(path.join(SCREENSHOTS_PATH, filepath), data.value, 'base64');
});
browser.addCommand('flushReactUpdates', function() {
browser.executeAsync(function(done) {
// For any events to propogate
setImmediate(function() {
// For the DOM to be updated.
setImmediate(done);
})
})
})
} catch(err) {
console.error(">>> Ignored error: "+err);
}

90
test/geojson-server.js Normal file
View File

@@ -0,0 +1,90 @@
const cors = require("cors");
const express = require("express");
const fs = require("fs");
const sourceData = require("./sources");
var app = express();
app.use(cors());
function buildStyle(opts) {
opts = opts || {};
opts = Object.assign({
sources: {}
}, opts);
return {
"id": "test-style",
"version": 8,
"name": "Test Style",
"metadata": {
"maputnik:renderer": "mbgljs"
},
"sources": opts.sources,
"glyphs": "https://example.local/fonts/{fontstack}/{range}.pbf",
"sprites": "https://example.local/fonts/{fontstack}/{range}.pbf",
"layers": []
}
}
function buildGeoJSONSource(data) {
return {
type: "vector",
data: data
};
}
function buildResterSource(req, key) {
return {
"tileSize": 256,
"tiles": [
req.protocol + '://' + req.get('host') + "/" + key + "/{x}/{y}/{z}"
],
"type": "raster"
};
}
app.get("/sources/raster/{x}/{y}/{z}", function(req, res) {
res.status(404).end();
})
app.get("/styles/empty/:sources", function(req, res) {
var reqSources = req.params.sources.split(",");
var sources = {};
reqSources.forEach(function(key) {
var parts = key.split(":");
var type = parts[0];
var key = parts[1];
if(type === "geojson") {
sources[key] = buildGeoJSONSource(sourceData[key]);
}
else if(type === "raster") {
sources[key] = buildResterSource(req, key);
}
else {
console.error("ERR: Invalid type: %s", type);
throw "Invalid type"
}
});
var json = buildStyle({
sources: sources
});
res.send(json);
})
app.get("/example-style.json", function(req, res) {
res.json(
JSON.parse(
fs.readFileSync(__dirname+"/example-style.json").toString()
)
);
})
module.exports = app;

15
test/sources/example.json Normal file
View File

@@ -0,0 +1,15 @@
{
"type":"FeatureCollection",
"features":[
{
"type":"Feature",
"properties": {
"name": "Dinagat Islands"
},
"geometry":{
"type": "Point",
"coordinates": [125.6, 10.1]
}
}
]
}

3
test/sources/index.js Normal file
View File

@@ -0,0 +1,3 @@
module.exports = {
example: require("./example")
};

View File

@@ -1,15 +0,0 @@
var assert = require('assert');
var config = require("../config/specs");
describe('maputnik', function() {
it('check logo exists', function () {
browser.url(config.baseUrl);
browser.waitForExist(".maputnik-toolbar-link");
var src = browser.getAttribute(".maputnik-toolbar-link img", "src");
assert.equal(src, config.baseUrl+'/img/logo-color.svg');
});
});

6
test/wd-helper.js Normal file
View File

@@ -0,0 +1,6 @@
module.exports = {
"$": function(key, selector) {
selector = selector || "";
return "*[data-wd-key='"+key+"'] "+selector;
}
}