Compare commits

..

308 Commits

Author SHA1 Message Date
orangemug
184bfeeaf8 1.7.0-beta4 2020-04-20 13:13:36 +01:00
orangemug
e45f8d960d Added space for beta tag in logo/version header 2020-04-20 13:12:48 +01:00
Orange Mug
1fede3af3a Merge pull request #661 from orangemug/fix/issue-660-v2
Added JSON linting back into <SourceTypeEditor/>
2020-04-20 13:09:29 +01:00
orangemug
5ad74048bd Added JSON linting back into <SourceTypeEditor/> 2020-04-20 11:07:08 +01:00
orangemug
a0a91474de 1.7.0-beta3 2020-04-19 08:45:18 +01:00
Orange Mug
c3670701e5 Merge pull request #606 from orangemug/fix/issue-591
Added style formatting into the api store
2020-04-19 08:38:37 +01:00
Orange Mug
86923330d9 Merge pull request #658 from pathmapper/feature_id_inspect
Add feature id to FeaturePropertyPopup
2020-04-19 08:00:00 +01:00
pathmapper
4517148e5a Add underscore to label 2020-04-18 11:43:52 +02:00
pathmapper
0433d66f45 Add feature id to FeaturePropertyPopup 2020-04-18 11:25:54 +02:00
Orange Mug
0c592bacab Merge pull request #650 from orangemug/fix/issue-647
Fixed crash raised in issue #647
2020-04-14 21:15:59 +01:00
Orange Mug
d98637cb12 Merge pull request #645 from orangemug/feature/add-support-for-identity-functions
Add support for identity functions
2020-04-14 09:12:35 +01:00
orangemug
1070209cb5 Another attempt and maputnik inspect crashing issue. 2020-04-14 09:11:09 +01:00
orangemug
b6189f77c4 Added icons to buttons. 2020-04-14 08:31:55 +01:00
Orange Mug
25322a3952 Merge pull request #655 from orangemug/fix/issue-653
Added missing inline error for 'source' field
2020-04-13 10:57:01 +01:00
Orange Mug
5943c6f282 Merge pull request #654 from orangemug/fix/issue-649
Fixed default values for <FontInput/>
2020-04-13 10:56:47 +01:00
orangemug
090a26bb40 Added missing inline error for 'source' field. 2020-04-13 09:10:30 +01:00
orangemug
af03b010a4 Fixed default values for <FontInput/> 2020-04-13 08:53:33 +01:00
orangemug
578a920b6d Fixed crash raised in issue #647 2020-04-13 08:39:23 +01:00
pathmapper
0858a16ffc Merge pull request #644 from orangemug/fix/remove-heavy-thunderforest-tiles
Remove notes from thunderforest sources
2020-04-12 21:04:35 +02:00
orangemug
7cfe0563bc Added support for identity functions. 2020-04-12 16:25:32 +01:00
orangemug
ee72389534 Remove mentions of 'heavy' from thunderforest tiles. 2020-04-12 12:14:56 +01:00
pathmapper
8f722c59de Merge pull request #642 from pathmapper/upgrade_thunderforest
Upgrade Thunderforest tilesets from v1 to v2
2020-04-11 15:48:03 +02:00
pathmapper
94d2e958eb Add version to titles 2020-04-10 18:23:31 +02:00
pathmapper
d931c7cb38 Upgrade Thunderforest tilesets 2020-04-10 18:13:23 +02:00
orangemug
6da83c4670 1.7.0-beta2 2020-04-06 16:57:02 +01:00
Orange Mug
d26af16003 Merge pull request #639 from orangemug/fix/only-scroll-layer-list-if-item-not-in-view
Only scroll to selected item in <LayerList/> if not already in view.
2020-04-06 16:55:26 +01:00
Orange Mug
d75b86c927 Merge pull request #638 from orangemug/maintenance/update-deps-20200406
Update all deps
2020-04-06 16:54:06 +01:00
orangemug
a0cd087ccc Revert webdriverio version updates. 2020-04-06 15:47:12 +01:00
orangemug
313b639a5f Only scroll to selected item in <LayerList/> if not already in view. 2020-04-06 15:30:16 +01:00
orangemug
93c45d5340 Update all deps. 2020-04-06 15:14:21 +01:00
Orange Mug
3be6cb5926 Merge pull request #637 from orangemug/fix/console-errors-2020-04-06
Fix a bunch of errors/warnings from the console
2020-04-06 15:06:39 +01:00
Orange Mug
9d151fdc1f Merge pull request #636 from pathmapper/promote_beta
Promote v1.7.0-beta in readme
2020-04-06 14:32:29 +01:00
orangemug
44d1a7a6b0 {arrayMove} will no longer be included in 'react-sortable-hoc', move to array-move. 2020-04-06 14:18:41 +01:00
pathmapper
0e5676eae0 Promote v1.7.0-beta in readme 2020-04-06 15:14:42 +02:00
orangemug
b8739915b2 Lots of smaller fixes found in the console logs during testing. 2020-04-06 13:59:08 +01:00
Orange Mug
a1dedd1aa6 Merge pull request #634 from orangemug/fix/issue-630
Scroll selected <LayerListItem/> into view
2020-04-06 13:11:53 +01:00
Orange Mug
33b4a40c35 Merge pull request #635 from orangemug/fix/issue-633
Fixes for <NumberInput/>
2020-04-06 13:10:48 +01:00
orangemug
a624909819 Reset dirtyValue on resetValue 2020-04-06 10:40:30 +01:00
orangemug
d5d387f349 Removed placeholder on range (doesn't work) in favour 'default' value merged into 'value'. 2020-04-06 10:24:31 +01:00
orangemug
c58ae0f895 Fix <NumberInput/> to allow for decimal numbers. 2020-04-06 09:55:07 +01:00
orangemug
c9e360d675 Fix layer selection via <FeatureLayerPopup/> 2020-04-06 08:47:13 +01:00
orangemug
75ece350bd Merge remote-tracking branch 'upstream/master' into fix/issue-630 2020-04-04 15:48:52 +01:00
Orange Mug
45680151ef Merge pull request #632 from orangemug/fix/color-popup-swatch
Fix color in <FeatureLayerPopup/>
2020-04-04 15:48:35 +01:00
orangemug
87bae82b17 Merge remote-tracking branch 'upstream/master' into fix/issue-630 2020-04-04 15:44:29 +01:00
orangemug
fcad636f85 Scroll selected <LayerListItem/> into view 2020-04-04 15:41:35 +01:00
orangemug
bac8495b3c Made <FeatureLayerPopup/> swatch simplier because colors from features are already evaluated 2020-04-04 10:45:11 +01:00
orangemug
df98cb9c7b Fix layer color swatch in <FeatureLayerPopup/> 2020-04-04 10:21:58 +01:00
Orange Mug
34c3015b42 Merge pull request #625 from orangemug/fix/invalid-style-with-duplicate-layer-ids
Fix UI when loading invalid style with duplicate layer ids
2020-03-30 20:38:25 +01:00
orangemug
7d51ea9b25 1.7.0-beta 2020-03-30 17:44:26 +01:00
orangemug
ca7bf9f4a7 Fixed lint errors. 2020-03-30 09:57:14 +01:00
orangemug
61ba399e1c Duplicate layer ids now show errors inline. 2020-03-30 09:47:46 +01:00
orangemug
b5c09a4f17 Merge remote-tracking branch 'upstream/master' into fix/invalid-style-with-duplicate-layer-ids 2020-03-30 08:52:37 +01:00
Orange Mug
fcfc7ab874 Merge pull request #626 from orangemug/fix/issue-321-accessible-layer-groups
Moved to using react-accessible-accordion for <LayerEditorGroup/>
2020-03-29 16:12:33 +01:00
pathmapper
a0bc4744a2 Merge pull request #627 from orangemug/fix/make-scrollbar-wider
Wider scrollbar
2020-03-29 13:55:55 +02:00
orangemug
e6e4c928f3 Make the scrollbar wider so it's easier to grab with the mouse pointer. 2020-03-29 07:40:43 +01:00
orangemug
00388e03b8 Moved to using react-accessible-accordion for <LayerEditorGroup/> 2020-03-28 12:53:46 +00:00
orangemug
6f83839a4c Added missing file. 2020-03-28 11:06:45 +00:00
orangemug
74b47e7e74 Fix UI when loading invalid style with duplicate layer ids.
Also fix for throwing error when 2 layers exist with empty strings as ids.
2020-03-28 10:58:47 +00:00
Orange Mug
f70d078ec6 Merge pull request #624 from orangemug/maintenance/update-deps-20200328
Update dependencies
2020-03-28 09:49:59 +00:00
orangemug
1d8131fb85 Update dependencies. 2020-03-28 08:33:57 +00:00
Orange Mug
8c82db9162 Merge pull request #620 from orangemug/feature/ui-errors-and-expressions
Added support for expressions and UI errors
2020-03-27 17:51:02 +00:00
orangemug
f23f60807a Tidy 2020-03-16 10:17:04 +00:00
orangemug
8f581956e8 Revert jsonlint 2020-03-08 19:00:36 +00:00
orangemug
87fb0f6a5c Updated lockfile. 2020-03-08 18:44:04 +00:00
orangemug
1c953bc296 Remove old hack 'src/vendor/codemirror/addon/lint/json-lint' 2020-03-08 18:43:46 +00:00
orangemug
ce976991d4 Added inline errors to the code-mirror editors based on field spec. 2020-03-08 18:38:32 +00:00
orangemug
be7642976b Can't use object as react-dom key. 2020-02-25 09:27:29 +00:00
orangemug
a5b226d9f3 Fixed lint error. 2020-02-25 09:25:52 +00:00
orangemug
1b3d8b5b79 Added JSON error to expression editor 2020-02-25 09:20:14 +00:00
orangemug
97a61afc24 Merge remote-tracking branch 'upstream/master' into feature/ui-errors-and-expressions 2020-02-23 13:34:05 +00:00
Orange Mug
d1f6bc95db Merge pull request #621 from orangemug/fix/CheckboxInput-props-error
Added default to <CheckboxInput/> value to fix warning in console.
2020-02-23 13:24:16 +00:00
orangemug
10b03c4e00 Also fixed invalid DOM nesting warning. 2020-02-22 18:47:46 +00:00
Orange Mug
449d8e7665 Merge pull request #617 from orangemug/fix/map-controls-and-improve-zoom-styling
Improved map control zoom styling
2020-02-22 18:41:05 +00:00
orangemug
4b8800e8ac Added default to <CheckboxInput/> value to fix warning in console. 2020-02-22 18:39:24 +00:00
orangemug
874c6460f6 Merge remote-tracking branch 'upstream/master' into fix/map-controls-and-improve-zoom-styling 2020-02-22 17:59:24 +00:00
orangemug
55cb86f721 Disabled survey modal on first visit to website. 2020-02-22 17:52:20 +00:00
orangemug
a30017fd2c Inline survey modal image 2020-02-22 17:50:14 +00:00
orangemug
3b5ba6c59e Fixed lint errors. 2020-02-22 13:15:55 +00:00
orangemug
a693f6db4e Added infobox for old style filters to allow you to switch to the filter editor. 2020-02-22 11:54:59 +00:00
Orange Mug
5be7e0c7ec Merge pull request #619 from orangemug/maintenance/update-deps-20200217-v2
Update all the deps
2020-02-22 08:57:07 +00:00
orangemug
7c6b3c0d80 Bumped deps. 2020-02-21 09:07:25 +00:00
orangemug
e5e03be382 Fixed missing command update to appveyor.yml 2020-02-21 08:46:46 +00:00
orangemug
0d35106cc8 Tidy test scripts. 2020-02-21 08:06:01 +00:00
orangemug
5710edcff7 Fixed invalid lockfile. 2020-02-18 21:42:25 +00:00
orangemug
2cc7c63bb1 Updated to new webdriverio config api, and fixed failing tests. 2020-02-18 21:39:11 +00:00
orangemug
ba9d21c045 Merge remote-tracking branch 'upstream/master' into maintenance/update-deps-20200217-v2 2020-02-18 19:10:20 +00:00
orangemug
4ef6ecb7eb Moved from magnify icon to 'zoom:' label. Also increased contrast on compass arrow. 2020-02-18 18:50:30 +00:00
orangemug
52e8b21b3d Tidy function. 2020-02-18 08:06:36 +00:00
Orange Mug
c6ba4f66e2 Merge pull request #618 from orangemug/feature/improve-loader
Added improved loader with maputnik logo
2020-02-18 07:50:56 +00:00
orangemug
5a47a96f09 Update all the deps 2020-02-17 19:38:59 +00:00
orangemug
ae878f6000 Added missing plugin from production. 2020-02-17 19:03:19 +00:00
orangemug
aebfe62a8e Fixed image path. 2020-02-17 18:55:28 +00:00
orangemug
6be3543616 Added improved loader. 2020-02-17 18:52:16 +00:00
orangemug
0f6708d9d4 Improved zoom styling and fixed zoom in/out button styling. 2020-02-17 15:39:54 +00:00
orangemug
0705522a24 Fixed width issue with expression error UI. 2020-02-17 13:44:05 +00:00
orangemug
35098111ac Include missing errors from expressions UI. 2020-02-17 13:41:11 +00:00
orangemug
39333953d7 Only show 'switch to layer' button if layer not already selected. 2020-02-17 13:24:14 +00:00
orangemug
adea3d0f13 Added ability to switch to layer from global error panel. 2020-02-17 13:19:26 +00:00
orangemug
d1cb2690fc Fixed lint errors. 2020-02-17 11:55:16 +00:00
orangemug
3ffdcc9639 Added the ability to convert from data/zoom function to expression 2020-02-17 11:52:40 +00:00
orangemug
793b5d15ad Fixed lint errors 2020-02-17 11:30:48 +00:00
orangemug
cff32696cc Better handling of undo history for expressions. 2020-02-17 11:29:05 +00:00
orangemug
029eff9317 Added margin-left to function errors to line up with values. 2020-02-16 22:02:05 +00:00
orangemug
b7d08dfaa6 Fixed data function display issues. 2020-02-16 21:56:03 +00:00
orangemug
94089836bf Fixed lint errors. 2020-02-16 21:30:27 +00:00
orangemug
ff8a8fb749 Added errors to filter functions 2020-02-16 21:25:41 +00:00
orangemug
1300951a29 Removed height from '.maputnik-layout-bottom' to allow it to expand. 2020-02-16 20:31:51 +00:00
orangemug
3cb1ed9403 Made function to check if expression/function more robust. 2020-02-16 20:25:43 +00:00
orangemug
a5ac1cc93d Guard against errors producing dirty map state. 2020-02-16 18:22:21 +00:00
orangemug
29a0ef0d1c Change to always attempt rendering of map state, even if broken, allowing the user to fix style issues step-by-step. 2020-02-16 18:11:47 +00:00
orangemug
26907f7014 Fix button style in disabled state as reported by @pathmapper 2020-02-16 17:34:23 +00:00
orangemug
3ac06c7cb1 Merge remote-tracking branch 'upstream/master' into feature/ui-errors-and-expressions 2020-02-10 15:57:25 +00:00
Orange Mug
f268f09ca2 Merge pull request #616 from orangemug/fix/error-bar-width
Fixed overflowing error bar
2020-02-10 15:50:53 +00:00
orangemug
f4c18fd91b Fixed error bar from overflowing. 2020-02-09 17:23:50 +00:00
orangemug
0567b098ec Fixed undo button disabled state for expressions. 2020-02-09 16:48:52 +00:00
orangemug
dc6006fd6d Fixed lint errors. 2020-02-09 16:36:17 +00:00
orangemug
109261ba00 Fixed filter defaults and removed auto-migrate. 2020-02-09 16:33:47 +00:00
orangemug
b539644b2b Added expression support for filters. 2020-02-09 15:08:24 +00:00
orangemug
be36eec93d Added 'undo' alongside 'trash' button 2020-02-08 10:38:31 +00:00
pathmapper
fe5066a2a4 Merge pull request #614 from orangemug/fix/map-container-background-color
Make background map container white to match html page default
2020-02-02 17:59:35 +01:00
Orange Mug
642e5c0b29 Merge pull request #613 from orangemug/fix/brackets-color-clash-with-cursor
Changed bracket matching color
2020-02-02 15:26:39 +00:00
orangemug
97bdc93a39 Fixed lint error. 2020-02-02 11:56:51 +00:00
orangemug
c770b440c2 Make background map container white to match html page default. 2020-02-02 11:47:37 +00:00
orangemug
7559985a2e Changed bracket matching color so it doesn't clash with cursor. 2020-02-02 11:32:58 +00:00
orangemug
532bbecb47 Only disable <LayerTypeBlock/> in <LayerEditor/> 2020-02-02 08:05:01 +00:00
orangemug
8ed67e98ce Fixed lint errors 2020-02-02 07:44:20 +00:00
orangemug
5792c632f9 hopefulMapStyle -> dirtyMapStyle 2020-02-01 17:55:26 +00:00
orangemug
3e2927e6a4 Fixed undo/redo 2020-02-01 17:53:01 +00:00
orangemug
f09cc25a3b Merge remote-tracking branch 'upstream/master' into feature/ui-errors-and-expressions 2020-02-01 17:36:59 +00:00
orangemug
c5c3e93aff Better support for expressions
- Expression editing state
 - CodeMirror JSON editor
 - Improved styling
2020-02-01 17:07:52 +00:00
Orange Mug
cc371d6a70 Merge pull request #612 from orangemug/fix/color-field-name-is-not-required
'name' is not a required property of <ColorField />
2020-01-30 09:47:06 +00:00
Orange Mug
1b17e8fa0a Merge pull request #610 from orangemug/fix/issue-609
Fix for stuck/unchanging pop-up in Inspect view
2020-01-30 09:22:43 +00:00
Orange Mug
bc4706de83 Merge pull request #607 from orangemug/fix/issue-567-better-solution-for-tooltips
Inline property documentation and SDK docs
2020-01-30 09:21:10 +00:00
orangemug
0f22eb83d3 'name' is not a required property of <ColorField /> 2020-01-30 09:18:23 +00:00
orangemug
a8cbe19f09 Added style_renderer to field-spec-additional 2020-01-30 08:54:08 +00:00
orangemug
c714e23d79 'id' is not guaranteed to be unique, see <https://github.com/maputnik/editor/pull/610/files#r372521304> 2020-01-30 08:46:26 +00:00
orangemug
5b3d579f87 Fix to fallback to index for unique feature key in <FeaturePropertyPopup /> 2020-01-29 08:44:27 +00:00
orangemug
725b752e35 Added initial expression work and UI errors. 2020-01-29 08:22:03 +00:00
orangemug
223721a65d Code review fixes
- Added docs to export modal
 - Fixed 'source type' doc in data sources modal
2020-01-27 08:49:44 +00:00
orangemug
9b4d924dff Fix to ignore modified styleSpec. 2020-01-24 17:46:43 +00:00
orangemug
b31537e063 Added property values to docs 2020-01-24 09:04:32 +00:00
Orange Mug
63ed8c1de3 Merge pull request #605 from orangemug/maintenance/update-deps-2020-01-19
Updated deps
2020-01-23 08:50:01 +00:00
Orange Mug
7aa0298f7c Merge pull request #603 from orangemug/feature/add-image-and-video-sources
Added ability to add image/video sources
2020-01-23 08:49:37 +00:00
orangemug
62f3cbe8fb Lint fixes. 2020-01-23 08:42:20 +00:00
orangemug
30facc885f Tidy inline docs and added sdk support table. 2020-01-23 08:33:12 +00:00
orangemug
17aa88e3b6 Merge remote-tracking branch 'upstream/master' into fix/issue-567-better-solution-for-tooltips 2020-01-22 08:57:45 +00:00
Orange Mug
5b9af07ebc Merge pull request #604 from orangemug/feature/issue-82
Link to OSM from debug panel
2020-01-19 20:11:30 +00:00
orangemug
6b45dc8b4d Added style formatting into apistore 2020-01-19 20:05:11 +00:00
orangemug
0009c74948 Fixes lat/lon precision and center change via zoom without pan. 2020-01-19 19:09:04 +00:00
Orange Mug
8911f83ef3 Merge pull request #522 from orangemug/feature/add-range-slider
Add range input for minZoom/maxZoom
2020-01-19 18:54:07 +00:00
orangemug
2fafafe0dc Updated deps 2020-01-19 16:39:41 +00:00
orangemug
27e6675d26 Fix replacing of access tokens in MapboxGl. 2020-01-19 16:30:27 +00:00
orangemug
4269e4573c Fixed lint errors. 2020-01-19 16:00:29 +00:00
orangemug
096e2b6aec Fixed image/video coord labels. 2020-01-19 15:48:11 +00:00
orangemug
33e04b3527 Added ability to add/edit image and video sources 2020-01-19 14:40:35 +00:00
orangemug
79fa2b3508 Feature to update current map view in OSM 2020-01-19 13:37:56 +00:00
orangemug
d5ef412300 Safari fixes. 2020-01-19 11:53:27 +00:00
orangemug
0726a494be Only set dirtyValue on valid change. 2020-01-19 11:41:37 +00:00
orangemug
926969b921 Always change via step from keyboard. 2020-01-19 11:28:03 +00:00
orangemug
59e070f463 Merge remote-tracking branch 'upstream/master' into feature/add-range-slider 2020-01-19 11:06:39 +00:00
orangemug
2ccd1d227e rangeValue -> dirtyValue 2020-01-19 11:06:24 +00:00
orangemug
655877f67e Fixed another bug with change events for <NumberInput/> and added range input keyboard support. Remove logging as react props/state is enough for debugging. 2020-01-19 10:30:08 +00:00
Orange Mug
6c240d53e4 Merge pull request #601 from orangemug/fix/issue-588
Added new fieldSpec.type 'resolvedImage'
2020-01-19 09:59:41 +00:00
Orange Mug
f89f8ed4ea Merge pull request #587 from pathmapper/matchingbrackets_addon
Add matchbrackets addon to JSON editor
2020-01-19 09:48:08 +00:00
Orange Mug
6123b464de Merge pull request #600 from orangemug/fix/issue-589
Fix feature selection in <FeatureLayerPopup />
2020-01-19 09:46:57 +00:00
Orange Mug
49dba02e8f Merge branch 'master' into matchingbrackets_addon 2020-01-19 08:29:25 +00:00
pathmapper
fb49a3abe5 Merge pull request #602 from orangemug/fix/issue-565
Update react-collapse
2020-01-19 08:54:28 +01:00
orangemug
c88f9ab5dc Believed fix for race condition in setting dirtyValue 2020-01-19 07:37:49 +00:00
orangemug
d886b14d09 Fix to set "editing: false" in blur/pointerup 2020-01-19 07:15:50 +00:00
orangemug
bd1204a7a5 Added react-collapse transition, required for react-collapse@^5 2020-01-18 22:11:02 +00:00
orangemug
9cadda0236 Update react-collapse which was needlessly updating and causing scroll position jump. 2020-01-18 22:03:29 +00:00
orangemug
90ea6323c1 Added new fieldSpec.type 'resolvedImage' 2020-01-18 20:57:59 +00:00
orangemug
51f2cfac16 Fix feature selection in <FeatureLayerPopup /> 2020-01-18 20:42:38 +00:00
Orange Mug
4dbb423ac2 Merge pull request #599 from pathmapper/update_omt_styles
Update OMT styles for v3.11
2020-01-15 10:06:58 +00:00
Orange Mug
a3ee1cc27e Merge pull request #595 from pathmapper/maxzoom
Increase maximum zoom level of the map to 24
2020-01-15 10:06:20 +00:00
pathmapper
fea0798349 Update OMT styles for v3.11 2020-01-14 10:28:32 +01:00
pathmapper
bd8abffa28 Merge pull request #597 from pathmapper/appveyor_vs_2019
Update appveyor to "Visual Studio 2019"
2020-01-13 09:39:48 +01:00
pathmapper
a5f3a43cde Merge pull request #596 from pathmapper/remove_node_v8
Remove node v8 from test runners
2020-01-13 09:39:28 +01:00
pathmapper
6c5dc7e06b Update appveyor to "Visual Studio 2019" 2020-01-11 10:26:07 +01:00
pathmapper
b1c8a12e88 Remove node v8 from appveyor 2020-01-11 09:39:09 +01:00
pathmapper
401c6971f4 Remove node v8 from circleci 2020-01-11 09:38:20 +01:00
pathmapper
7e5a5ce077 Set map maxZoom to 24 2020-01-10 14:06:17 +01:00
pathmapper
6b245c9894 Improve matchingbrackets and cursor visibility 2019-10-30 12:24:25 +01:00
pathmapper
b963fe9619 Add matchbrackets addon 2019-10-29 10:52:06 +01:00
Orange Mug
673887d93b Merge pull request #582 from orangemug/fix/add-source-errors
Add open modal and source http/https errors
2019-10-28 18:19:21 +00:00
Orange Mug
06898429fd Merge pull request #584 from orangemug/fix/issue-576
A hack fix for blocked popup
2019-10-28 18:18:33 +00:00
pathmapper
0196ba4eb4 Merge pull request #585 from orangemug/fix/issue-403
Update map style even if inspectModeEnabled=true
2019-10-28 06:38:49 +01:00
orangemug
ef81534a17 Update map style even if inspectModeEnabled=true 2019-10-27 20:04:57 +00:00
orangemug
a958ec943b Lint fix. 2019-10-27 19:16:13 +00:00
orangemug
4e3b395b3d A hack fix for blocked popup. 2019-10-27 19:11:34 +00:00
Orange Mug
5e7fd4f93c Update src/components/inputs/UrlInput.jsx
CORs -> CORS

Co-Authored-By: pathmapper <pathmapper@posteo.de>
2019-10-27 19:07:49 +00:00
orangemug
25cad5bb25 Added to GeoJSON url. 2019-10-27 18:58:22 +00:00
orangemug
f9c230414e Fix width of modal. 2019-10-27 18:54:40 +00:00
orangemug
866f8d034a Switched to <UrlInput/> for glyphs and sprites 2019-10-27 18:45:49 +00:00
pathmapper
be6aa559fb Merge pull request #583 from orangemug/feature/make-osm-liberty-the-default
Make osm-liberty the default style
2019-10-27 19:38:11 +01:00
orangemug
a560176d83 Add even more logging. 2019-10-27 18:15:54 +00:00
Orange Mug
4644e78fd2 Merge pull request #579 from orangemug/fix/function-field-fixes
Function field fixes
2019-10-27 17:58:06 +00:00
orangemug
237cc16b97 Make osm-liberty the default style. 2019-10-27 17:46:34 +00:00
orangemug
dffa54afb0 Removed more old styleUrlElement refs 2019-10-27 17:42:45 +00:00
orangemug
225e5c48e4 Fixed lint error. 2019-10-27 17:38:57 +00:00
orangemug
2e017d252a Added <UrlInput/> to open modal. 2019-10-27 17:36:20 +00:00
orangemug
e728e5f7e4 Exposed onInput in <UrlInput/> 2019-10-27 17:35:56 +00:00
orangemug
f0371b41b1 Also validate on constructor 2019-10-27 17:27:50 +00:00
orangemug
a51442921a Fix font-size inline with the rest of the UI. 2019-10-27 17:19:03 +00:00
orangemug
f39fb34f36 Protocol logic and lint fixes. 2019-10-27 17:15:17 +00:00
orangemug
566201fb45 Added UrlInput component to tidy things up a little. 2019-10-27 17:08:23 +00:00
orangemug
88841b56e7 Added another fix attempt and yet more logging. 2019-10-27 15:56:22 +00:00
orangemug
5aa0b4e7d9 More logging additional attempt at fixes. 2019-10-27 11:01:33 +00:00
Orange Mug
f19fc4a8a1 Merge pull request #581 from orangemug/fix/issue-535-data-field-ordering
Fix zoom ordering for data property fields
2019-10-27 10:18:34 +00:00
orangemug
cd162309a8 Added hopeful fix + logging. 2019-10-27 10:15:11 +00:00
orangemug
aead867e27 Incorrectly checking for falsey when should be checking for presence. 2019-10-27 10:05:05 +00:00
orangemug
663f295623 Added default protocol and stub in error. 2019-10-27 10:00:22 +00:00
orangemug
c588164190 console.log map errors. 2019-10-26 18:27:11 +01:00
orangemug
d61d0a5795 Fix test for range slider. 2019-10-26 18:09:07 +01:00
orangemug
dddd604f7b Removed left over conflict markers and set default. 2019-10-26 17:41:56 +01:00
orangemug
ea3b9a20c5 Merge remote-tracking branch 'upstream/master' into feature/add-range-slider 2019-10-26 17:22:52 +01:00
Orange Mug
7415b8af08 Merge pull request #580 from pathmapper/update_mb_deps
Update mbgl and style-spec
2019-10-26 17:19:04 +01:00
orangemug
d06e053d34 Fix zoom ordering for data property fields. 2019-10-26 17:15:25 +01:00
pathmapper
7075a8b05e Update mbgl and style-spec 2019-10-26 07:10:30 +02:00
orangemug
4cbcf14588 Make up defaults from function field if we can't find one from the spec. 2019-10-25 22:54:08 +01:00
orangemug
ca202d7701 Use <EnumInput/> for enums again 2019-10-25 22:48:37 +01:00
Orange Mug
8dfc16e7ee Merge pull request #577 from fredj/del_btn_style
Simplify and fix vertical alignment of the delete button
2019-10-25 22:15:23 +01:00
Frederic Junod
fbf5cec670 Simplify and fix vertical alignment
Use the same css as the `maputnik-make-data-function` and `maputnik-make-zoom-function` buttons.
2019-10-25 13:16:09 +02:00
Orange Mug
14d4383f8a Merge pull request #575 from snodnipper/feature/zoomstack-v2
set OS Zoomstack endpoints to v2
2019-10-24 17:58:54 +01:00
Oliver Snowden
58bdd39f9e set OS Zoomstack endpoints to v2 2019-10-25 00:24:55 +08:00
Orange Mug
ab9ab7acc7 Merge pull request #574 from fredj/GitHub_sponsor
Activate sponsor button in GitHub
2019-10-24 16:02:16 +01:00
Frederic Junod
be39fd2ec8 Activate sponsor button in GitHub
See: https://help.github.com/en/github/building-a-strong-community/displaying-a-sponsor-button-in-your-repository
2019-10-24 16:51:45 +02:00
pathmapper
3c0185da27 Merge pull request #573 from pathmapper/node_13
Add node v13 to test runners
2019-10-24 05:59:12 +02:00
pathmapper
b37b7276fb Add node v13 to test runners 2019-10-23 11:10:05 +02:00
Orange Mug
c45cf2f0c8 Merge pull request #563 from orangemug/feature/more-root-property-support
Added support for more root level properties
2019-10-21 10:18:37 +01:00
Orange Mug
1f03fdbb50 Merge pull request #571 from orangemug/fix/map-perf-improvement
Remove shouldComponentUpdate from <MapboxGlMap/>
2019-10-20 12:50:33 +01:00
orangemug
f3b8c5362a Remove shouldComponentUpdate because mapbox-gl will do the diff for us, so updates will no longer diff twice. 2019-10-20 12:25:43 +01:00
Orange Mug
c9a5dd01be Merge pull request #570 from orangemug/feature/issue-559-add-raw-geojson-as-source
Add support for raw GeoJSON as source
2019-10-20 12:23:37 +01:00
orangemug
0fa4d40e92 Remove duplicate import rule. 2019-10-20 11:21:38 +01:00
orangemug
8a6e64c8c2 Merge remote-tracking branch 'upstream/master' into feature/issue-559-add-raw-geojson-as-source 2019-10-20 11:19:34 +01:00
orangemug
72b6dd1ae9 Fix lint errors. 2019-10-20 11:12:54 +01:00
orangemug
ee525631fa Fixes for codemirror in sources modal
- Allows for max height
 - Override GeoJSON data when changing type
2019-10-20 11:09:20 +01:00
pathmapper
ee9e055af3 Merge pull request #562 from orangemug/feature/json-editor-perf-improvements
react-codemirror2 -> codemirror directly
2019-10-19 21:56:59 +02:00
pathmapper
b214c6ac7e Merge pull request #566 from orangemug/feature/add-profiling-build
Added profiling build to CI
2019-10-19 21:55:20 +02:00
orangemug
eb75020861 Merge branch 'feature/json-editor-perf-improvements' into fix/issue-567-better-solution-for-tooltips 2019-10-19 16:56:45 +01:00
orangemug
a44e757e31 Initial work for new help text implementation. 2019-10-19 14:43:27 +01:00
orangemug
9ac908948d Changed label/content styling for input blocks. 2019-10-19 13:12:09 +01:00
orangemug
19e82e5890 Added support for raw GeoJSON 2019-10-19 13:11:29 +01:00
orangemug
bf84fd24ee Moved profiling to wdio-steps 2019-10-19 12:33:54 +01:00
orangemug
affeb7c751 Fixed lint errors. 2019-10-19 12:21:38 +01:00
orangemug
9743361e0d Added back in 'light position' and 'center' with fixes for <ArrayInput/>
This also improves the usage of *-translate which uses the <ArrayInput/>
2019-10-19 12:16:56 +01:00
orangemug
ab16120af2 Added invalid style to inputs 2019-10-19 12:07:08 +01:00
orangemug
37e5ba0fff Remove 'center' and 'light position', as they weren't working properly. 2019-10-16 22:36:41 +01:00
orangemug
0aa0dad7fb Remove undefined root properties. 2019-10-16 22:31:29 +01:00
orangemug
2910efde6e Allow removal of light/transition properties. 2019-10-16 21:58:24 +01:00
orangemug
eac7656786 <NumberInput/> unsetting fix. 2019-10-16 21:57:29 +01:00
Orange Mug
be3175beae Merge pull request #569 from fredj/layer-type-test-issue
Fix the layer type test in LayerEditor component
2019-10-16 19:21:03 +01:00
Frederic Junod
26de95a263 Fix the layer type test in LayerEditor component
`LayerSourceLayerBlock` was always included
2019-10-16 15:36:25 +02:00
orangemug
d0a47bd122 Added support for transition root property 2019-10-16 10:15:46 +01:00
orangemug
8c760bb810 Added default support to <EnumInput/> 2019-10-16 10:12:59 +01:00
orangemug
c27deefdef Added profiling build to CI 2019-10-16 09:56:26 +01:00
orangemug
392a845460 Moved "Style Renderer" options to bottom of list. 2019-10-16 08:24:25 +01:00
orangemug
e7622c2080 Added root level light property support. 2019-10-16 08:17:25 +01:00
orangemug
3a558412ba Merge remote-tracking branch 'upstream/master' into feature/added-light-property-support 2019-10-16 08:16:28 +01:00
Orange Mug
95e205943a Merge pull request #564 from orangemug/fix/typo-in-export
Fixed typo in export
2019-10-16 08:14:21 +01:00
orangemug
eb8686325c Fixed typo in export. 2019-10-16 08:13:37 +01:00
orangemug
1f77e156e6 Added support for root level properties
- center
 - zoom
 - bearing
 - pitch
2019-10-16 07:38:50 +01:00
orangemug
92ee50a4a4 Added default support to <ArrayInput/> 2019-10-16 07:36:30 +01:00
orangemug
ef23f01e67 Removed react-codemirror2 as we no longer use it 2019-10-16 06:49:11 +01:00
orangemug
22b6a4a2bf Added max-width to allow overfow scroll on JSONEditor 2019-10-16 06:46:30 +01:00
Orange Mug
201ecac156 Merge pull request #561 from fredj/ol_attribution_color
Set a color for the OpenLayers attribution control
2019-10-15 17:37:47 +01:00
orangemug
563a78ed42 Moved away from react-codemirror2 to interacting with codemirror directly. 2019-10-15 17:34:36 +01:00
Frederic Junod
47acc2640b Set a color for the OpenLayers attribution control 2019-10-15 14:37:04 +02:00
Orange Mug
f088788246 Merge pull request #557 from orangemug/feature/perf-improvements
LayerList performance improvements
2019-10-15 09:35:56 +01:00
Orange Mug
e219dcd332 Merge pull request #556 from orangemug/feature/add-new-mgljs-features
Added new features from the mapbox-gl spec
2019-10-14 10:04:42 +01:00
orangemug
b8829d9a5c Remove *-sort-key until we support expressions. 2019-10-14 09:22:21 +01:00
pathmapper
2c83c976c6 Merge pull request #558 from pathmapper/liberty_thumbnail
Update thumbnail source for OSM Liberty
2019-10-14 06:59:58 +02:00
orangemug
d63782ddf2 Remove react warnings. 2019-10-13 18:03:58 +01:00
orangemug
3eabcbec72 Fix <NumberInput/> to allow for empty value (reset value) 2019-10-13 18:00:32 +01:00
orangemug
00ab303e44 Stop LayerEditor using same DOM elements for different layers. 2019-10-13 17:59:11 +01:00
pathmapper
38bf12701e Update thumbnail source for OSM Liberty 2019-10-13 17:30:43 +02:00
orangemug
e4ec1d155a Added mapbox-gl features 'text-variable-anchor' and 'text-radial-offset' 2019-10-13 13:32:42 +01:00
orangemug
361f083687 Merge remote-tracking branch 'upstream/master' into feature/add-new-mgljs-features 2019-10-13 12:40:56 +01:00
orangemug
c1a59200e2 1.6.1 2019-10-13 12:02:26 +01:00
orangemug
6e0432ff5e Added back in minifcation. 2019-10-13 11:43:23 +01:00
orangemug
1c83de08c1 Fixed lint errors. 2019-10-12 12:08:25 +01:00
orangemug
0af828543b Disable minify 2019-10-12 12:06:33 +01:00
Orange Mug
369cc23a30 Merge pull request #553 from orangemug/maintenance/update-deps-20191009
Update dependencies
2019-10-12 11:56:38 +01:00
orangemug
db56ad8b2e Remove duplicate formatColor call. 2019-10-12 10:16:46 +01:00
orangemug
7fa17d81ac Some perf improvements when updating the layers list, also throttled color picker. 2019-10-12 10:10:33 +01:00
orangemug
019c6a0086 Merge remote-tracking branch 'origin/maintenance/update-deps-20191009' into maintenance/update-deps-20191009 2019-10-12 09:10:42 +01:00
orangemug
c1bee74b57 Switched back from using yarn to npm for lockfiles. 2019-10-12 09:09:39 +01:00
Orange Mug
b794279304 Bumped mapbox-gl to 1.4.1
Co-Authored-By: pathmapper <pathmapper@posteo.de>
2019-10-12 07:08:30 +01:00
Orange Mug
935dfa1704 Merge pull request #555 from pathmapper/update_omt_styles
Update OMT styles
2019-10-12 07:03:17 +01:00
pathmapper
bda7a0e659 Update OMT styles 2019-10-11 16:13:08 +02:00
orangemug
8d1cc340b8 Added new features text-writing-mode and *-sort-key 2019-10-10 08:52:36 +01:00
orangemug
338c6b59a8 Lock deps in package.json 2019-10-09 22:06:59 +01:00
orangemug
021f8ab400 Update all the dependencies 2019-10-09 19:51:57 +01:00
Orange Mug
f305db9e3e Merge pull request #551 from pathmapper/update_readme
Remove 1.6-beta from readme
2019-10-08 21:25:02 +01:00
pathmapper
e916b25594 Remove 1.6-beta from readme 2019-10-08 21:59:34 +02:00
orangemug
1fec89b69e Moved back to data-wd-key approach and fixed tests 2019-05-21 19:07:28 +01:00
orangemug
911549aca3 Moved data-wd-key onto element 2019-05-21 18:54:09 +01:00
orangemug
41329ec2f8 Fixes for firefox, this makes the range input only update on pointerup 2019-05-21 18:42:19 +01:00
orangemug
15cdfbc980 Changed default of step 0.01 -> 1 2019-05-20 11:28:27 +01:00
orangemug
5053058c32 Fixed default values for range slider. 2019-05-20 11:21:23 +01:00
orangemug
8a8cfad303 Merge remote-tracking branch 'upstream/master' into feature/add-range-slider 2019-05-20 10:20:55 +01:00
orangemug
b1d097a40f Merge remote-tracking branch 'upstream/master' into feature/add-range-slider 2019-05-19 06:04:21 +01:00
orangemug
3a0fc6eeac Fixed typo 2018-11-02 08:54:20 +00:00
orangemug
b456b59c44 Fix to allow high precision on text input and integer on range. 2018-11-02 08:28:51 +00:00
orangemug
e18d304313 Changed min/max zoom range step from 0.1 -> 1 2018-11-01 18:28:58 +00:00
orangemug
fe0df2a4ef Make range step configurable 2018-11-01 08:28:49 +00:00
orangemug
a51fdb8435 Merge remote-tracking branch 'upstream/master' into feature/add-range-slider 2018-10-30 20:35:55 +00:00
orangemug
cdd5d27908 Added range slider to <NumberInput /> 2018-10-30 20:35:22 +00:00
102 changed files with 13227 additions and 9925 deletions

View File

@@ -4,6 +4,7 @@
"@babel/preset-react"
],
"plugins": [
"static-fs",
"react-hot-loader/babel",
"@babel/plugin-proposal-class-properties"
],

View File

@@ -18,8 +18,8 @@ templates:
- run: mkdir -p /tmp/artifacts/logs
- run: npm run build
- run: npm run lint
- run: npm run lint-styles
- run: npm run lint-js
- run: npm run lint-css
- store_artifacts:
path: /tmp/artifacts
destination: /artifacts
@@ -41,19 +41,15 @@ templates:
- run: mkdir -p /tmp/artifacts/logs
- run: npm run build
- run: npm run lint
- run: npm run lint-styles
- run: npm run profiling-build
- run: npm run lint-js
- run: npm run lint-css
- 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-v8:
docker:
- image: node:8
working_directory: ~/repo-linux-node-v8
steps: *build-steps
build-linux-node-v10:
docker:
- image: node:10
@@ -65,13 +61,10 @@ jobs:
- image: node:12
working_directory: ~/repo-linux-node-v12
steps: *build-steps
build-osx-node-v8:
macos:
xcode: "9.0"
dependencies:
override:
- brew install node@8
working_directory: ~/repo-osx-node-v8
build-linux-node-v13:
docker:
- image: node:13
working_directory: ~/repo-linux-node-v13
steps: *build-steps
build-osx-node-v10:
macos:
@@ -89,14 +82,22 @@ jobs:
- brew install node@12
working_directory: ~/repo-osx-node-v12
steps: *build-steps
build-osx-node-v13:
macos:
xcode: "9.0"
dependencies:
override:
- brew install node@13
working_directory: ~/repo-osx-node-v13
steps: *build-steps
workflows:
version: 2
build:
jobs:
- build-linux-node-v8
- build-linux-node-v10
- build-linux-node-v12
- build-osx-node-v8
- build-linux-node-v13
- build-osx-node-v10
- build-osx-node-v12
- build-osx-node-v13

1
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1 @@
custom: "https://maputnik.github.io/donate"

View File

@@ -18,7 +18,7 @@ A free and open visual editor for the [Mapbox GL styles](https://www.mapbox.com/
targeted at developers and map designers.
- :link: Design your maps online at **<https://maputnik.github.io/editor/>** (all in local storage)
- :link: Try out the v1.6.0-beta release at: https://maputnik.github.io/releases/v1.6.0-beta/
- :link: Try out the v1.7.0-beta release at: https://maputnik.github.io/releases/v1.7.0-beta/
- :link: Use the [Maputnik CLI](https://github.com/maputnik/editor/wiki/Maputnik-CLI) for local style development
Mapbox has built one of the best and most amazing OSS ecosystems. A key component to ensure its longevity and independence is an OSS map designer.

View File

@@ -1,9 +1,9 @@
image: Visual Studio 2015
image: Visual Studio 2019
environment:
matrix:
- nodejs_version: "8"
- nodejs_version: "10"
- nodejs_version: "12"
- nodejs_version: "13"
platform:
- x86
- x64
@@ -17,10 +17,10 @@ install:
Update-NodeJsInstallation (Get-NodeJsLatestBuild $env:nodejs_version) $env:platform
}
- md public
- npm --vs2015 install --global windows-build-tools
- npm install --global windows-build-tools
- npm install
build_script:
- npm run build
test_script:
- npm run lint
- npm run lint-styles
- npm run lint-js
- npm run lint-css

View File

@@ -97,7 +97,7 @@ exports.config = {
//
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",
hostname: process.env.DOCKER_HOST || "0.0.0.0",
// Set a base URL in order to shorten url command calls. If your `url` parameter starts
// with `/`, the base url gets prepended, not including the path portion of your baseUrl.
// If your `url` parameter starts without a scheme or `/` (like `some/path`), the base url
@@ -118,7 +118,6 @@ exports.config = {
// Services take over a specific job you don't want to take care of. They enhance
// your test setup with almost no effort. Unlike plugins, they don't add new
// commands. Instead, they hook themselves up into the test process.
services: ['selenium-standalone'],
//
// Framework you want to run your specs with.
// The following are supported: Mocha, Jasmine, and Cucumber
@@ -147,12 +146,16 @@ exports.config = {
onPrepare: function (config, capabilities) {
return new Promise(function(resolve, reject) {
var compiler = webpack(webpackConfig);
const serverHost = isDocker() ? "0.0.0.0" : "localhost";
server = new WebpackDevServer(compiler, {
host: serverHost,
stats: {
colors: true
}
});
server.listen(testConfig.port, (isDocker() ? "0.0.0.0" : "localhost"), function(err) {
server.listen(testConfig.port, serverHost, function(err) {
if(err) {
reject(err);
}

View File

@@ -3,6 +3,7 @@ var webpack = require('webpack');
var path = require('path');
var rules = require('./webpack.rules');
var HtmlWebpackPlugin = require('html-webpack-plugin');
var HtmlWebpackInlineSVGPlugin = require('html-webpack-inline-svg-plugin');
var CopyWebpackPlugin = require('copy-webpack-plugin');
const HOST = process.env.HOST || "127.0.0.1";
@@ -61,6 +62,9 @@ module.exports = {
title: 'Maputnik',
template: './src/template.html'
}),
new HtmlWebpackInlineSVGPlugin({
runPreEmit: true,
}),
new CopyWebpackPlugin([
{
from: './src/manifest.json',

View File

@@ -2,6 +2,7 @@ var webpack = require('webpack');
var path = require('path');
var rules = require('./webpack.rules');
var HtmlWebpackPlugin = require('html-webpack-plugin');
var HtmlWebpackInlineSVGPlugin = require('html-webpack-inline-svg-plugin');
var WebpackCleanupPlugin = require('webpack-cleanup-plugin');
var BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
var CopyWebpackPlugin = require('copy-webpack-plugin');
@@ -44,6 +45,9 @@ module.exports = {
template: './src/template.html',
title: 'Maputnik'
}),
new HtmlWebpackInlineSVGPlugin({
runPreEmit: true,
}),
new CopyWebpackPlugin([
{
from: './src/manifest.json',

View File

@@ -0,0 +1,20 @@
const webpackProdConfig = require('./webpack.production.config');
const artifacts = require("../test/artifacts");
const OUTPATH = artifacts.pathSync("/profiling");
module.exports = {
...webpackProdConfig,
output: {
...webpackProdConfig.output,
path: OUTPATH,
},
resolve: {
...webpackProdConfig.resolve,
alias: {
...webpackProdConfig.resolve.alias,
'react-dom$': 'react-dom/profiling',
'scheduler/tracing': 'scheduler/tracing-profiling',
}
}
};

View File

@@ -17,9 +17,15 @@ module.exports = [
use: 'file-loader?name=[name].[ext]'
},
{
test: /\.(svg|gif|jpg|png)$/,
test: /\.(gif|jpg|png)$/,
use: 'file-loader?name=img/[name].[ext]'
},
{
test: /\.svg$/,
use: [
'svg-inline-loader'
]
},
{
test: /[\/\\](node_modules|global|src)[\/\\].*\.scss$/,
use: [

18838
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,16 +1,18 @@
{
"name": "maputnik",
"version": "1.6.0",
"version": "1.7.0-beta4",
"description": "A MapboxGL visual style editor",
"main": "''",
"scripts": {
"stats": "webpack --config config/webpack.production.config.js --profile --json > stats.json",
"build": "webpack --config config/webpack.production.config.js --progress --profile --colors",
"profiling-build": "webpack --config config/webpack.profiling.config.js --progress --profile --colors",
"test": "cross-env NODE_ENV=test wdio config/wdio.conf.js",
"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",
"lint": "eslint --ext js --ext jsx src test",
"lint-styles": "stylelint \"src/styles/*.scss\""
"lint-js": "eslint --ext js --ext jsx src test",
"lint-css": "stylelint \"src/styles/*.scss\"",
"lint": "npm run lint-js && npm run lint-css"
},
"repository": {
"type": "git",
@@ -20,43 +22,47 @@
"license": "MIT",
"homepage": "https://github.com/maputnik/editor#readme",
"dependencies": {
"@babel/runtime": "^7.1.2",
"@mapbox/mapbox-gl-rtl-text": "^0.2.2",
"@mapbox/mapbox-gl-style-spec": "^13.7.2",
"@babel/runtime": "^7.8.4",
"@mapbox/mapbox-gl-rtl-text": "^0.2.3",
"@mapbox/mapbox-gl-style-spec": "^13.12.0",
"@mdi/react": "^1.4.0",
"array-move": "^2.2.1",
"classnames": "^2.2.6",
"codemirror": "^5.40.2",
"color": "^3.0.0",
"detect-browser": "^4.5.0",
"file-saver": "^1.3.8",
"codemirror": "^5.52.0",
"color": "^3.1.2",
"detect-browser": "^5.0.0",
"file-saver": "^2.0.2",
"json-to-ast": "^2.1.0",
"jsonlint": "github:josdejong/jsonlint#85a19d7",
"lodash": "^4.17.15",
"lodash.capitalize": "^4.2.1",
"lodash.clamp": "^4.0.3",
"lodash.clonedeep": "^4.5.0",
"lodash.get": "^4.4.2",
"lodash.isequal": "^4.5.0",
"lodash.throttle": "^4.1.1",
"mapbox-gl": "^1.2.0",
"mapbox-gl": "^1.9.1",
"mapbox-gl-inspect": "^1.3.1",
"maputnik-design": "github:maputnik/design",
"ol": "^6.0.0-beta.8",
"ol-mapbox-style": "^5.0.0-beta.2",
"prop-types": "^15.6.2",
"react": "^16.5.2",
"react-aria-menubutton": "^6.0.1",
"react-aria-modal": "^3.0.0",
"maputnik-design": "github:maputnik/design#f7a2b4d",
"ol": "^6.3.1",
"ol-mapbox-style": "^6.0.1",
"prop-types": "^15.7.2",
"react": "^16.12.0",
"react-accessible-accordion": "^3.0.1",
"react-aria-menubutton": "^6.3.0",
"react-aria-modal": "^4.0.0",
"react-autobind": "^1.0.6",
"react-autocomplete": "^1.8.1",
"react-codemirror2": "^5.1.0",
"react-collapse": "^4.0.3",
"react-color": "^2.14.1",
"react-dom": "^16.5.2",
"react-collapse": "^5.0.1",
"react-color": "^2.18.0",
"react-dom": "^16.12.0",
"react-file-reader-input": "^2.0.0",
"react-icon-base": "^2.1.2",
"react-icons": "^3.1.0",
"react-icons": "^3.9.0",
"react-motion": "^0.5.2",
"react-sortable-hoc": "^0.8.3",
"reconnecting-websocket": "^3.2.2",
"slugify": "^1.3.1",
"react-sortable-hoc": "^1.11.0",
"reconnecting-websocket": "^4.4.0",
"slugify": "^1.3.6",
"url": "^0.11.0"
},
"jshintConfig": {
@@ -97,54 +103,62 @@
"experimentalObjectRestSpread": true,
"jsx": true
}
},
"settings": {
"react": {
"version": "detect"
}
}
},
"devDependencies": {
"@babel/core": "^7.1.2",
"@babel/plugin-proposal-class-properties": "^7.1.0",
"@babel/plugin-transform-runtime": "^7.1.0",
"@babel/preset-env": "^7.1.0",
"@babel/core": "^7.8.4",
"@babel/plugin-proposal-class-properties": "^7.5.5",
"@babel/plugin-transform-runtime": "^7.6.2",
"@babel/preset-env": "^7.6.3",
"@babel/preset-flow": "^7.0.0",
"@babel/preset-react": "^7.0.0",
"@wdio/cli": "^5.10.4",
"@wdio/local-runner": "^5.10.4",
"@wdio/mocha-framework": "^5.10.1",
"@wdio/selenium-standalone-service": "^5.9.3",
"@wdio/spec-reporter": "^5.9.3",
"@wdio/sync": "^5.10.1",
"babel-eslint": "^10.0.1",
"babel-loader": "8.0.4",
"babel-plugin-istanbul": "^5.0.1",
"copy-webpack-plugin": "^4.5.2",
"cors": "^2.8.4",
"cross-env": "^5.2.0",
"css-loader": "^1.0.0",
"eslint": "^5.6.1",
"eslint-plugin-react": "^7.11.1",
"@babel/preset-react": "^7.6.3",
"@mdi/js": "^5.0.45",
"@wdio/cli": "^6.0.5",
"@wdio/local-runner": "^6.0.5",
"@wdio/mocha-framework": "^6.0.4",
"@wdio/selenium-standalone-service": "^6.0.4",
"@wdio/spec-reporter": "^6.0.4",
"@wdio/sync": "^6.0.1",
"babel-eslint": "^10.0.3",
"babel-loader": "^8.1.0",
"babel-plugin-istanbul": "^6.0.0",
"babel-plugin-static-fs": "^3.0.0",
"copy-webpack-plugin": "^5.1.1",
"cors": "^2.8.5",
"cross-env": "^7.0.0",
"css-loader": "^3.4.2",
"eslint": "^6.8.0",
"eslint-plugin-react": "^7.18.3",
"express": "^4.17.1",
"file-loader": "^2.0.0",
"file-loader": "^6.0.0",
"html-webpack-inline-svg-plugin": "^1.3.0",
"html-webpack-plugin": "^3.2.0",
"is-docker": "^2.0.0",
"istanbul": "^0.4.5",
"istanbul-lib-coverage": "^2.0.1",
"mkdirp": "^0.5.1",
"mocha": "^6.1.4",
"node-sass": "^4.12.0",
"raw-loader": "^0.5.1",
"react-hot-loader": "^4.3.11",
"sass-loader": "^7.1.0",
"selenium-standalone": "^6.16.0",
"style-loader": "^0.23.0",
"stylelint": "^10.0.0",
"stylelint-config-recommended-scss": "^3.2.0",
"stylelint-scss": "^3.5.4",
"istanbul-lib-coverage": "^3.0.0",
"mkdirp": "^1.0.4",
"mocha": "^7.0.1",
"node-sass": "^4.13.1",
"react-hot-loader": "^4.12.19",
"sass-loader": "^8.0.2",
"selenium-standalone": "^6.17.0",
"style-loader": "^1.1.3",
"stylelint": "^13.3.0",
"stylelint-config-recommended-scss": "^4.2.0",
"stylelint-scss": "^3.14.2",
"svg-inline-loader": "^0.8.2",
"transform-loader": "^0.2.4",
"uuid": "^3.3.2",
"webdriverio": "^5.10.4",
"webpack": "^4.20.2",
"webpack-bundle-analyzer": "^3.0.2",
"uuid": "^7.0.3",
"webdriverio": "^6.0.5",
"webpack": "^4.41.6",
"webpack-bundle-analyzer": "^3.6.0",
"webpack-cleanup-plugin": "^0.5.1",
"webpack-cli": "^3.1.2",
"webpack-dev-server": "^3.1.9"
"webpack-cli": "^3.3.11",
"webpack-dev-server": "^3.10.3"
}
}

View File

@@ -3,7 +3,8 @@ import React from 'react'
import cloneDeep from 'lodash.clonedeep'
import clamp from 'lodash.clamp'
import get from 'lodash.get'
import {arrayMove} from 'react-sortable-hoc'
import {unset} from 'lodash'
import arrayMove from 'array-move'
import url from 'url'
import MapboxGlMap from './map/MapboxGlMap'
@@ -35,6 +36,7 @@ import tokens from '../config/tokens.json'
import isEqual from 'lodash.isequal'
import Debug from '../libs/debug'
import queryUtil from '../libs/query-util'
import {formatLayerId} from './util/format';
import MapboxGl from 'mapbox-gl'
@@ -97,7 +99,7 @@ export default class App extends React.Component {
port = window.location.port
}
this.styleStore = new ApiStyleStore({
onLocalStyleChange: mapStyle => this.onStyleChanged(mapStyle, false),
onLocalStyleChange: mapStyle => this.onStyleChanged(mapStyle, {save: false}),
port: port,
host: params.get("localhost")
})
@@ -212,13 +214,21 @@ export default class App extends React.Component {
vectorLayers: {},
mapState: "map",
spec: latest,
mapView: {
zoom: 0,
center: {
lng: 0,
lat: 0,
},
},
isOpen: {
settings: false,
sources: false,
open: false,
shortcuts: false,
export: false,
survey: localStorage.hasOwnProperty('survey') ? false : true,
// TODO: Disabled for now, this should be opened on the Nth visit to the editor
survey: false,
debug: false,
},
mapboxGlDebugOptions: {
@@ -308,39 +318,139 @@ export default class App extends React.Component {
this.onStyleChanged(changedStyle)
}
onStyleChanged = (newStyle, save=true) => {
onStyleChanged = (newStyle, opts={}) => {
opts = {
save: true,
addRevision: true,
...opts,
};
const errors = validate(newStyle, latest)
if(errors.length === 0) {
const errors = validate(newStyle, latest) || [];
if(newStyle.glyphs !== this.state.mapStyle.glyphs) {
this.updateFonts(newStyle.glyphs)
}
if(newStyle.sprite !== this.state.mapStyle.sprite) {
this.updateIcons(newStyle.sprite)
}
this.revisionStore.addRevision(newStyle)
if(save) this.saveStyle(newStyle)
this.setState({
mapStyle: newStyle,
errors: [],
})
} else {
this.setState({
errors: errors.map(err => err.message)
})
// The validate function doesn't give us errors for duplicate error with
// empty string for layer.id, manually deal with that here.
const layerErrors = [];
if (newStyle && newStyle.layers) {
const foundLayers = new Map();
newStyle.layers.forEach((layer, index) => {
if (layer.id === "" && foundLayers.has(layer.id)) {
const message = `Duplicate layer: ${formatLayerId(layer.id)}`;
const error = new Error(
`layers[${index}]: duplicate layer id [empty_string], previously used`
);
layerErrors.push(error);
}
foundLayers.set(layer.id, true);
});
}
const mappedErrors = layerErrors.concat(errors).map(error => {
// Special case: Duplicate layer id
const dupMatch = error.message.match(/layers\[(\d+)\]: (duplicate layer id "?(.*)"?, previously used)/);
if (dupMatch) {
const [matchStr, index, message] = dupMatch;
return {
message: error.message,
parsed: {
type: "layer",
data: {
index: parseInt(index, 10),
key: "id",
message,
}
}
}
}
// Special case: Invalid source
const invalidSourceMatch = error.message.match(/layers\[(\d+)\]: (source "(?:.*)" not found)/);
if (invalidSourceMatch) {
const [matchStr, index, message] = invalidSourceMatch;
return {
message: error.message,
parsed: {
type: "layer",
data: {
index: parseInt(index, 10),
key: "source",
message,
}
}
}
}
const layerMatch = error.message.match(/layers\[(\d+)\]\.(?:(\S+)\.)?(\S+): (.*)/);
if (layerMatch) {
const [matchStr, index, group, property, message] = layerMatch;
const key = (group && property) ? [group, property].join(".") : property;
return {
message: error.message,
parsed: {
type: "layer",
data: {
index: parseInt(index, 10),
key,
message
}
}
}
}
else {
return {
message: error.message,
};
}
});
let dirtyMapStyle = undefined;
if (errors.length > 0) {
dirtyMapStyle = cloneDeep(newStyle);
errors.forEach(error => {
const {message} = error;
if (message) {
try {
const objPath = message.split(":")[0];
// Errors can be deply nested for example 'layers[0].filter[1][1][0]' we only care upto the property 'layers[0].filter'
const unsetPath = objPath.match(/^\S+?\[\d+\]\.[^\[]+/)[0];
unset(dirtyMapStyle, unsetPath);
}
catch (err) {
console.warn(err);
}
}
});
}
if(newStyle.glyphs !== this.state.mapStyle.glyphs) {
this.updateFonts(newStyle.glyphs)
}
if(newStyle.sprite !== this.state.mapStyle.sprite) {
this.updateIcons(newStyle.sprite)
}
if (opts.addRevision) {
this.revisionStore.addRevision(newStyle);
}
if (opts.save) {
this.saveStyle(newStyle);
}
this.setState({
mapStyle: newStyle,
dirtyMapStyle: dirtyMapStyle,
errors: mappedErrors,
})
this.fetchSources();
}
onUndo = () => {
const activeStyle = this.revisionStore.undo()
const messages = undoMessages(this.state.mapStyle, activeStyle)
this.saveStyle(activeStyle)
this.onStyleChanged(activeStyle, {addRevision: false});
this.setState({
mapStyle: activeStyle,
infos: messages,
})
}
@@ -348,9 +458,8 @@ export default class App extends React.Component {
onRedo = () => {
const activeStyle = this.revisionStore.redo()
const messages = redoMessages(this.state.mapStyle, activeStyle)
this.saveStyle(activeStyle)
this.onStyleChanged(activeStyle, {addRevision: false});
this.setState({
mapStyle: activeStyle,
infos: messages,
})
}
@@ -381,56 +490,50 @@ export default class App extends React.Component {
this.onStyleChanged(changedStyle)
}
onLayerDestroy = (layerId) => {
onLayerDestroy = (index) => {
let layers = this.state.mapStyle.layers;
const remainingLayers = layers.slice(0);
const idx = style.indexOfLayer(remainingLayers, layerId)
remainingLayers.splice(idx, 1);
remainingLayers.splice(index, 1);
this.onLayersChange(remainingLayers);
}
onLayerCopy = (layerId) => {
onLayerCopy = (index) => {
let layers = this.state.mapStyle.layers;
const changedLayers = layers.slice(0)
const idx = style.indexOfLayer(changedLayers, layerId)
const clonedLayer = cloneDeep(changedLayers[idx])
const clonedLayer = cloneDeep(changedLayers[index])
clonedLayer.id = clonedLayer.id + "-copy"
changedLayers.splice(idx, 0, clonedLayer)
changedLayers.splice(index, 0, clonedLayer)
this.onLayersChange(changedLayers)
}
onLayerVisibilityToggle = (layerId) => {
onLayerVisibilityToggle = (index) => {
let layers = this.state.mapStyle.layers;
const changedLayers = layers.slice(0)
const idx = style.indexOfLayer(changedLayers, layerId)
const layer = { ...changedLayers[idx] }
const layer = { ...changedLayers[index] }
const changedLayout = 'layout' in layer ? {...layer.layout} : {}
changedLayout.visibility = changedLayout.visibility === 'none' ? 'visible' : 'none'
layer.layout = changedLayout
changedLayers[idx] = layer
changedLayers[index] = layer
this.onLayersChange(changedLayers)
}
onLayerIdChange = (oldId, newId) => {
onLayerIdChange = (index, oldId, newId) => {
const changedLayers = this.state.mapStyle.layers.slice(0)
const idx = style.indexOfLayer(changedLayers, oldId)
changedLayers[idx] = {
...changedLayers[idx],
changedLayers[index] = {
...changedLayers[index],
id: newId
}
this.onLayersChange(changedLayers)
}
onLayerChanged = (layer) => {
onLayerChanged = (index, layer) => {
const changedLayers = this.state.mapStyle.layers.slice(0)
const idx = style.indexOfLayer(changedLayers, layer.id)
changedLayers[idx] = layer
changedLayers[index] = layer
this.onLayersChange(changedLayers)
}
@@ -531,11 +634,23 @@ export default class App extends React.Component {
return metadata['maputnik:renderer'] || 'mbgljs';
}
onMapChange = (mapView) => {
this.setState({
mapView,
});
}
mapRenderer() {
const {mapStyle, dirtyMapStyle} = this.state;
const metadata = this.state.mapStyle.metadata || {};
const mapProps = {
mapStyle: style.replaceAccessTokens(this.state.mapStyle, {allowFallback: true}),
mapStyle: (dirtyMapStyle || mapStyle),
replaceAccessTokens: (mapStyle) => {
return style.replaceAccessTokens(mapStyle, {
allowFallback: true
});
},
onDataChange: (e) => {
this.layerWatcher.analyzeMap(e.map)
this.fetchSources();
@@ -550,11 +665,13 @@ export default class App extends React.Component {
if(renderer === 'ol') {
mapElement = <OpenLayersMap
{...mapProps}
onChange={this.onMapChange}
debugToolbox={this.state.openlayersDebugOptions.debugToolbox}
onLayerSelect={this.onLayerSelect}
/>
} else {
mapElement = <MapboxGlMap {...mapProps}
onChange={this.onMapChange}
options={this.state.mapboxGlDebugOptions}
inspectModeEnabled={this.state.mapState === "inspect"}
highlightedLayer={this.state.mapStyle.layers[this.state.selectedLayerIndex]}
@@ -575,9 +692,8 @@ export default class App extends React.Component {
</div>
}
onLayerSelect = (layerId) => {
const idx = style.indexOfLayer(this.state.mapStyle.layers, layerId)
this.setState({ selectedLayerIndex: idx })
onLayerSelect = (index) => {
this.setState({ selectedLayerIndex: index })
}
setModal(modalName, value) {
@@ -642,9 +758,11 @@ export default class App extends React.Component {
selectedLayerIndex={this.state.selectedLayerIndex}
layers={layers}
sources={this.state.sources}
errors={this.state.errors}
/>
const layerEditor = selectedLayer ? <LayerEditor
key={selectedLayer.id}
layer={selectedLayer}
layerIndex={this.state.selectedLayerIndex}
isFirstLayer={this.state.selectedLayerIndex < 1}
@@ -658,9 +776,14 @@ export default class App extends React.Component {
onLayerCopy={this.onLayerCopy}
onLayerVisibilityToggle={this.onLayerVisibilityToggle}
onLayerIdChange={this.onLayerIdChange}
errors={this.state.errors}
/> : null
const bottomPanel = (this.state.errors.length + this.state.infos.length) > 0 ? <MessagePanel
currentLayer={selectedLayer}
selectedLayerIndex={this.state.selectedLayerIndex}
onLayerSelect={this.onLayerSelect}
mapStyle={this.state.mapStyle}
errors={this.state.errors}
infos={this.state.infos}
/> : null
@@ -675,6 +798,7 @@ export default class App extends React.Component {
onChangeOpenlayersDebug={this.onChangeOpenlayersDebug}
isOpen={this.state.isOpen.debug}
onOpenToggle={this.toggleModal.bind(this, 'debug')}
mapView={this.state.mapView}
/>
<ShortcutsModal
ref={(el) => this.shortcutEl = el}

View File

@@ -26,9 +26,7 @@ class AppLayout extends React.Component {
return <div className="maputnik-layout">
{this.props.toolbar}
<div className="maputnik-layout-list">
<ScrollContainer>
{this.props.layerList}
</ScrollContainer>
{this.props.layerList}
</div>
<div className="maputnik-layout-drawer">
<ScrollContainer>

View File

@@ -20,7 +20,8 @@ class Button extends React.Component {
aria-label={this.props["aria-label"]}
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}
</button>
}

View File

@@ -1,15 +1,52 @@
import React from 'react'
import PropTypes from 'prop-types'
import {formatLayerId} from './util/format';
class MessagePanel extends React.Component {
static propTypes = {
errors: PropTypes.array,
infos: PropTypes.array,
mapStyle: PropTypes.object,
onLayerSelect: PropTypes.func,
currentLayer: PropTypes.object,
selectedLayerIndex: PropTypes.number,
}
static defaultProps = {
onLayerSelect: () => {},
}
render() {
const errors = this.props.errors.map((m, i) => {
return <p key={"error-"+i} className="maputnik-message-panel-error">{m}</p>
const {selectedLayerIndex} = this.props;
const errors = this.props.errors.map((error, idx) => {
let content;
if (error.parsed && error.parsed.type === "layer") {
const {parsed} = error;
const {mapStyle, currentLayer} = this.props;
const layerId = mapStyle.layers[parsed.data.index].id;
content = (
<>
Layer <span>{formatLayerId(layerId)}</span>: {parsed.data.message}
{selectedLayerIndex !== parsed.data.index &&
<>
&nbsp;&mdash;&nbsp;
<button
className="maputnik-message-panel__switch-button"
onClick={() => this.props.onLayerSelect(parsed.data.index)}
>
switch to layer
</button>
</>
}
</>
);
}
else {
content = error.message;
}
return <p key={"error-"+idx} className="maputnik-message-panel-error">
{content}
</p>
})
const infos = this.props.infos.map((m, i) => {

View File

@@ -182,7 +182,7 @@ export default class Toolbar extends React.Component {
target="_blank"
className="maputnik-toolbar-logo"
>
<img src={logoImage} alt="Maputnik" />
<span dangerouslySetInnerHTML={{__html: logoImage}} />
<h1>
<span className="maputnik-toolbar-name">{pkgJson.name}</span>
<span className="maputnik-toolbar-version">v{pkgJson.version}</span>

View File

@@ -2,6 +2,7 @@ import React from 'react'
import Color from 'color'
import ChromePicker from 'react-color/lib/components/chrome/Chrome'
import PropTypes from 'prop-types'
import lodash from 'lodash';
function formatColor(color) {
const rgb = color.rgb
@@ -12,7 +13,7 @@ function formatColor(color) {
class ColorField extends React.Component {
static propTypes = {
onChange: PropTypes.func.isRequired,
name: PropTypes.string.isRequired,
name: PropTypes.string,
value: PropTypes.string,
doc: PropTypes.string,
style: PropTypes.object,
@@ -23,6 +24,15 @@ class ColorField extends React.Component {
pickerOpened: false
}
constructor () {
super();
this.onChangeNoCheck = lodash.throttle(this.onChangeNoCheck, 1000/30);
}
onChangeNoCheck (v) {
this.props.onChange(v);
}
//TODO: I much rather would do this with absolute positioning
//but I am too stupid to get it to work together with fixed position
//and scrollbars so I have to fallback to JavaScript
@@ -82,7 +92,7 @@ class ColorField extends React.Component {
}}>
<ChromePicker
color={currentColor}
onChange={c => this.props.onChange(formatColor(c))}
onChange={c => this.onChangeNoCheck(formatColor(c))}
/>
<div
className="maputnik-color-picker-offset"

View File

@@ -1,23 +1,63 @@
import React from 'react'
import PropTypes from 'prop-types'
import {MdInfoOutline, MdHighlightOff} from 'react-icons/md'
export default class DocLabel extends React.Component {
static propTypes = {
label: PropTypes.oneOfType([
PropTypes.object,
PropTypes.string
]).isRequired,
doc: PropTypes.string.isRequired,
fieldSpec: PropTypes.object,
onToggleDoc: PropTypes.func,
}
constructor (props) {
super(props);
this.state = {
open: false,
}
}
onToggleDoc = (open) => {
this.setState({
open,
}, () => {
if (this.props.onToggleDoc) {
this.props.onToggleDoc(this.state.open);
}
});
}
render() {
return <label className="maputnik-doc-wrapper">
<div className="maputnik-doc-target">
<span>{this.props.label}</span>
<div className="maputnik-doc-popup">
{this.props.doc}
const {label, fieldSpec} = this.props;
const {doc} = fieldSpec || {};
if (doc) {
return <label className="maputnik-doc-wrapper">
<div className="maputnik-doc-target">
{label}
{'\xa0'}
<button
aria-label={this.state.open ? "close property documentation" : "open property documentation"}
className={`maputnik-doc-button maputnik-doc-button--${this.state.open ? 'open' : 'closed'}`}
onClick={() => this.onToggleDoc(!this.state.open)}
>
{this.state.open ? <MdHighlightOff /> : <MdInfoOutline />}
</button>
</div>
</div>
</label>
</label>
}
else if (label) {
return <label className="maputnik-doc-wrapper">
<div className="maputnik-doc-target">
{label}
</div>
</label>
}
else {
<div />
}
}
}

View File

@@ -4,16 +4,105 @@ import PropTypes from 'prop-types'
import SpecProperty from './_SpecProperty'
import DataProperty from './_DataProperty'
import ZoomProperty from './_ZoomProperty'
import ExpressionProperty from './_ExpressionProperty'
import {function as styleFunction} from '@mapbox/mapbox-gl-style-spec';
import {findDefaultFromSpec} from '../util/spec-helper';
function isLiteralExpression (value) {
return (Array.isArray(value) && value.length === 2 && value[0] === "literal");
}
function isGetExpression (value) {
return (
Array.isArray(value) &&
value.length === 2 &&
value[0] === "get"
);
}
function isZoomField(value) {
return typeof value === 'object' && value.stops && typeof value.property === 'undefined'
return (
typeof(value) === 'object' &&
value.stops &&
typeof(value.property) === 'undefined' &&
Array.isArray(value.stops) &&
value.stops.length > 1 &&
value.stops.every(stop => {
return (
Array.isArray(stop) &&
stop.length === 2
);
})
);
}
function isIdentityProperty (value) {
return (
typeof(value) === 'object' &&
value.type === "identity" &&
value.hasOwnProperty("property")
);
}
function isDataStopProperty (value) {
return (
typeof(value) === 'object' &&
value.stops &&
typeof(value.property) !== 'undefined' &&
value.stops.length > 1 &&
Array.isArray(value.stops) &&
value.stops.every(stop => {
return (
Array.isArray(stop) &&
stop.length === 2 &&
typeof(stop[0]) === 'object'
);
})
);
}
function isDataField(value) {
return typeof value === 'object' && value.stops && typeof value.property !== 'undefined'
return (
isIdentityProperty(value) ||
isDataStopProperty(value)
);
}
function isPrimative (value) {
const valid = ["string", "boolean", "number"];
return valid.includes(typeof(value));
}
function isArrayOfPrimatives (values) {
if (Array.isArray(values)) {
return values.every(isPrimative);
}
return false;
}
function getDataType (value, fieldSpec={}) {
if (value === undefined) {
return "value";
}
else if (isPrimative(value)) {
return "value";
}
else if (fieldSpec.type === "array" && isArrayOfPrimatives(value)) {
return "value";
}
else if (isZoomField(value)) {
return "zoom_function";
}
else if (isDataField(value)) {
return "data_function";
}
else {
return "expression";
}
}
/** Supports displaying spec field for zoom function objects
* https://www.mapbox.com/mapbox-gl-style-spec/#types-function-zoom-property
*/
@@ -21,7 +110,9 @@ export default class FunctionSpecProperty extends React.Component {
static propTypes = {
onChange: PropTypes.func.isRequired,
fieldName: PropTypes.string.isRequired,
fieldType: PropTypes.string.isRequired,
fieldSpec: PropTypes.object.isRequired,
errors: PropTypes.object,
value: PropTypes.oneOfType([
PropTypes.object,
@@ -32,6 +123,27 @@ export default class FunctionSpecProperty extends React.Component {
]),
}
constructor (props) {
super();
this.state = {
dataType: getDataType(props.value, props.fieldSpec),
isEditing: false,
}
}
static getDerivedStateFromProps(props, state) {
// Because otherwise when editing values we end up accidentally changing field type.
if (state.isEditing) {
return {};
}
else {
return {
isEditing: false,
dataType: getDataType(props.value, props.fieldSpec)
};
}
}
getFieldFunctionType(fieldSpec) {
if (fieldSpec.expression.interpolated) {
return "exponential"
@@ -63,6 +175,14 @@ export default class FunctionSpecProperty extends React.Component {
this.props.onChange(this.props.fieldName, changedValue)
}
deleteExpression = () => {
const {fieldSpec, fieldName} = this.props;
this.props.onChange(fieldName, fieldSpec.default);
this.setState({
dataType: "value",
});
}
deleteStop = (stopIdx) => {
const stops = this.props.value.stops.slice(0)
stops.splice(stopIdx, 1)
@@ -82,13 +202,59 @@ export default class FunctionSpecProperty extends React.Component {
makeZoomFunction = () => {
const zoomFunc = {
stops: [
[6, this.props.value],
[10, this.props.value]
[6, this.props.value || findDefaultFromSpec(this.props.fieldSpec)],
[10, this.props.value || findDefaultFromSpec(this.props.fieldSpec)]
]
}
this.props.onChange(this.props.fieldName, zoomFunc)
}
undoExpression = () => {
const {value, fieldName} = this.props;
if (isGetExpression(value)) {
this.props.onChange(fieldName, {
"type": "identity",
"property": value[1]
});
this.setState({
dataType: "value",
});
}
else if (isLiteralExpression(value)) {
this.props.onChange(fieldName, value[1]);
this.setState({
dataType: "value",
});
}
}
canUndo = () => {
const {value, fieldSpec} = this.props;
return (
isGetExpression(value) ||
isLiteralExpression(value) ||
isPrimative(value) ||
(Array.isArray(value) && fieldSpec.type === "array")
);
}
makeExpression = () => {
const {value, fieldSpec} = this.props;
let expression;
if (typeof(value) === "object" && 'stops' in value) {
expression = styleFunction.convertFunction(value, fieldSpec);
}
else if (isIdentityProperty(value)) {
expression = ["get", value.property];
}
else {
expression = ["literal", value || this.props.fieldSpec.default];
}
this.props.onChange(this.props.fieldName, expression);
}
makeDataFunction = () => {
const functionType = this.getFieldFunctionType(this.props.fieldSpec);
const stopValue = functionType === 'categorical' ? '' : 0;
@@ -96,50 +262,85 @@ export default class FunctionSpecProperty extends React.Component {
property: "",
type: functionType,
stops: [
[{zoom: 6, value: stopValue}, this.props.value || stopValue],
[{zoom: 10, value: stopValue}, this.props.value || stopValue]
[{zoom: 6, value: stopValue}, this.props.value || findDefaultFromSpec(this.props.fieldSpec)],
[{zoom: 10, value: stopValue}, this.props.value || findDefaultFromSpec(this.props.fieldSpec)]
]
}
this.props.onChange(this.props.fieldName, dataFunc)
}
onMarkEditing = () => {
this.setState({isEditing: true});
}
onUnmarkEditing = () => {
this.setState({isEditing: false});
}
render() {
const {dataType} = this.state;
const propClass = this.props.fieldSpec.default === this.props.value ? "maputnik-default-property" : "maputnik-modified-property"
let specField;
if (isZoomField(this.props.value)) {
if (dataType === "expression") {
specField = (
<ExpressionProperty
errors={this.props.errors}
onChange={this.props.onChange.bind(this, this.props.fieldName)}
canUndo={this.canUndo}
onUndo={this.undoExpression}
onDelete={this.deleteExpression}
fieldType={this.props.fieldType}
fieldName={this.props.fieldName}
fieldSpec={this.props.fieldSpec}
value={this.props.value}
onFocus={this.onMarkEditing}
onBlur={this.onUnmarkEditing}
/>
);
}
else if (dataType === "zoom_function") {
specField = (
<ZoomProperty
errors={this.props.errors}
onChange={this.props.onChange.bind(this)}
fieldType={this.props.fieldType}
fieldName={this.props.fieldName}
fieldSpec={this.props.fieldSpec}
value={this.props.value}
onDeleteStop={this.deleteStop}
onAddStop={this.addStop}
onExpressionClick={this.makeExpression}
/>
)
}
else if (isDataField(this.props.value)) {
else if (dataType === "data_function") {
specField = (
<DataProperty
errors={this.props.errors}
onChange={this.props.onChange.bind(this)}
fieldType={this.props.fieldType}
fieldName={this.props.fieldName}
fieldSpec={this.props.fieldSpec}
value={this.props.value}
onDeleteStop={this.deleteStop}
onAddStop={this.addStop}
onExpressionClick={this.makeExpression}
/>
)
}
else {
specField = (
<SpecProperty
errors={this.props.errors}
onChange={this.props.onChange.bind(this)}
fieldType={this.props.fieldType}
fieldName={this.props.fieldName}
fieldSpec={this.props.fieldSpec}
value={this.props.value}
onZoomClick={this.makeZoomFunction}
onDataClick={this.makeDataFunction}
onExpressionClick={this.makeExpression}
/>
)
}

View File

@@ -40,6 +40,7 @@ export default class PropertyGroup extends React.Component {
groupFields: PropTypes.array.isRequired,
onChange: PropTypes.func.isRequired,
spec: PropTypes.object.isRequired,
errors: PropTypes.object,
}
onPropertyChange = (property, newValue) => {
@@ -48,18 +49,22 @@ export default class PropertyGroup extends React.Component {
}
render() {
const {errors} = this.props;
const fields = this.props.groupFields.map(fieldName => {
const fieldSpec = getFieldSpec(this.props.spec, this.props.layer.type, fieldName)
const paint = this.props.layer.paint || {}
const layout = this.props.layer.layout || {}
const fieldValue = fieldName in paint ? paint[fieldName] : layout[fieldName]
const fieldType = fieldName in paint ? 'paint' : 'layout';
return <FunctionSpecField
errors={errors}
onChange={this.onPropertyChange}
key={fieldName}
fieldName={fieldName}
value={fieldValue === undefined ? fieldSpec.default : fieldValue}
value={fieldValue}
fieldType={fieldType}
fieldSpec={fieldSpec}
/>
})

View File

@@ -11,6 +11,7 @@ import ArrayInput from '../inputs/ArrayInput'
import DynamicArrayInput from '../inputs/DynamicArrayInput'
import FontInput from '../inputs/FontInput'
import IconInput from '../inputs/IconInput'
import EnumInput from '../inputs/EnumInput'
import capitalize from 'lodash.capitalize'
const iconProperties = ['background-pattern', 'fill-pattern', 'line-pattern', 'fill-extrusion-pattern', 'icon-image']
@@ -70,17 +71,11 @@ export default class SpecField extends React.Component {
case 'enum':
const options = Object.keys(this.props.fieldSpec.values).map(v => [v, capitalize(v)])
if(options.length <= 3 && optionsLabelLength(options) <= 20) {
return <MultiButtonInput
{...commonProps}
options={options}
/>
} else {
return <SelectInput
{...commonProps}
options={options}
/>
}
return <EnumInput
{...commonProps}
options={options}
/>
case 'resolvedImage':
case 'formatted':
case 'string':
if(iconProperties.indexOf(this.props.fieldName) >= 0) {
@@ -119,6 +114,7 @@ export default class SpecField extends React.Component {
} else {
return <DynamicArrayInput
{...commonProps}
fieldSpec={this.props.fieldSpec}
type={this.props.fieldSpec.value}
/>
}

View File

@@ -1,5 +1,6 @@
import React from 'react'
import PropTypes from 'prop-types'
import {mdiFunctionVariant, mdiTableRowPlusAfter} from '@mdi/js';
import Button from '../Button'
import SpecField from './SpecField'
@@ -8,17 +9,41 @@ import StringInput from '../inputs/StringInput'
import SelectInput from '../inputs/SelectInput'
import DocLabel from './DocLabel'
import InputBlock from '../inputs/InputBlock'
import docUid from '../../libs/document-uid'
import sortNumerically from '../../libs/sort-numerically'
import {findDefaultFromSpec} from '../util/spec-helper';
import labelFromFieldName from './_labelFromFieldName'
import DeleteStopButton from './_DeleteStopButton'
function setStopRefs(props, state) {
// This is initialsed below only if required to improved performance.
let newRefs;
if(props.value && props.value.stops) {
props.value.stops.forEach((val, idx) => {
if(!state.refs.hasOwnProperty(idx)) {
if(!newRefs) {
newRefs = {...state};
}
newRefs[idx] = docUid("stop-");
}
})
}
return newRefs;
}
export default class DataProperty extends React.Component {
static propTypes = {
onChange: PropTypes.func,
onDeleteStop: PropTypes.func,
onAddStop: PropTypes.func,
onExpressionClick: PropTypes.func,
fieldName: PropTypes.string,
fieldType: PropTypes.string,
fieldSpec: PropTypes.object,
value: PropTypes.oneOfType([
PropTypes.object,
@@ -27,6 +52,31 @@ export default class DataProperty extends React.Component {
PropTypes.bool,
PropTypes.array
]),
errors: PropTypes.object,
}
state = {
refs: {}
}
componentDidMount() {
const newRefs = setStopRefs(this.props, this.state);
if(newRefs) {
this.setState({
refs: newRefs
})
}
}
static getDerivedStateFromProps(props, state) {
const newRefs = setStopRefs(props, state);
if(newRefs) {
return {
refs: newRefs
};
}
return null;
}
getFieldFunctionType(fieldSpec) {
@@ -41,23 +91,74 @@ export default class DataProperty extends React.Component {
getDataFunctionTypes(fieldSpec) {
if (fieldSpec.expression.interpolated) {
return ["categorical", "interval", "exponential"]
return ["categorical", "interval", "exponential", "identity"]
}
else {
return ["categorical", "interval"]
return ["categorical", "interval", "identity"]
}
}
// Order the stops altering the refs to reflect their new position.
orderStopsByZoom(stops) {
const mappedWithRef = stops
.map((stop, idx) => {
return {
ref: this.state.refs[idx],
data: stop
}
})
// Sort by zoom
.sort((a, b) => sortNumerically(a.data[0].zoom, b.data[0].zoom));
// Fetch the new position of the stops
const newRefs = {};
mappedWithRef
.forEach((stop, idx) =>{
newRefs[idx] = stop.ref;
})
this.setState({
refs: newRefs
});
return mappedWithRef.map((item) => item.data);
}
onChange = (fieldName, value) => {
if (value.type === "identity") {
value = {
type: value.type,
property: value.property,
};
}
else {
const stopValue = value.type === 'categorical' ? '' : 0;
value = {
property: "",
type: value.type,
// Default props if they don't already exist.
stops: [
[{zoom: 6, value: stopValue}, findDefaultFromSpec(this.props.fieldSpec)],
[{zoom: 10, value: stopValue}, findDefaultFromSpec(this.props.fieldSpec)]
],
...value,
}
}
this.props.onChange(fieldName, value);
}
changeStop(changeIdx, stopData, value) {
const stops = this.props.value.stops.slice(0)
const changedStop = stopData.zoom === undefined ? stopData.value : stopData
stops[changeIdx] = [changedStop, value]
const orderedStops = this.orderStopsByZoom(stops);
const changedValue = {
...this.props.value,
stops: stops,
stops: orderedStops,
}
this.props.onChange(this.props.fieldName, changedValue)
this.onChange(this.props.fieldName, changedValue)
}
changeDataProperty(propName, propVal) {
@@ -67,115 +168,151 @@ export default class DataProperty extends React.Component {
else {
delete this.props.value[propName]
}
this.props.onChange(this.props.fieldName, this.props.value)
this.onChange(this.props.fieldName, this.props.value)
}
render() {
const {fieldName, fieldType, errors} = this.props;
if (typeof this.props.value.type === "undefined") {
this.props.value.type = this.getFieldFunctionType(this.props.fieldSpec)
}
const dataFields = this.props.value.stops.map((stop, idx) => {
const zoomLevel = typeof stop[0] === 'object' ? stop[0].zoom : undefined;
const dataLevel = typeof stop[0] === 'object' ? stop[0].value : stop[0];
const value = stop[1]
const deleteStopBtn = <DeleteStopButton onClick={this.props.onDeleteStop.bind(this, idx)} />
let dataFields;
if (this.props.value.stops) {
dataFields = this.props.value.stops.map((stop, idx) => {
const zoomLevel = typeof stop[0] === 'object' ? stop[0].zoom : undefined;
const key = this.state.refs[idx];
const dataLevel = typeof stop[0] === 'object' ? stop[0].value : stop[0];
const value = stop[1]
const deleteStopBtn = <DeleteStopButton onClick={this.props.onDeleteStop.bind(this, idx)} />
const dataProps = {
label: "Data value",
value: dataLevel,
onChange: newData => this.changeStop(idx, { zoom: zoomLevel, value: newData }, value)
}
const dataProps = {
label: "Data value",
value: dataLevel,
onChange: newData => this.changeStop(idx, { zoom: zoomLevel, value: newData }, value)
}
let dataInput;
if(this.props.value.type === "categorical") {
dataInput = <StringInput {...dataProps} />
}
else {
dataInput = <NumberInput {...dataProps} />
}
let dataInput;
if(this.props.value.type === "categorical") {
dataInput = <StringInput {...dataProps} />
}
else {
dataInput = <NumberInput {...dataProps} />
}
let zoomInput = null;
if(zoomLevel !== undefined) {
zoomInput = <div className="maputnik-data-spec-property-stop-edit">
<NumberInput
value={zoomLevel}
onChange={newZoom => this.changeStop(idx, {zoom: newZoom, value: dataLevel}, value)}
min={0}
max={22}
/>
</div>
}
return <InputBlock key={idx} action={deleteStopBtn} label="">
{zoomInput}
<div className="maputnik-data-spec-property-stop-data">
{dataInput}
</div>
<div className="maputnik-data-spec-property-stop-value">
<SpecField
fieldName={this.props.fieldName}
fieldSpec={this.props.fieldSpec}
value={value}
onChange={(_, newValue) => this.changeStop(idx, {zoom: zoomLevel, value: dataLevel}, newValue)}
/>
</div>
</InputBlock>
})
return <div className="maputnik-data-spec-block">
<div className="maputnik-data-spec-property">
<InputBlock
doc={this.props.fieldSpec.doc}
label={labelFromFieldName(this.props.fieldName)}
>
<div className="maputnik-data-spec-property-group">
<DocLabel
label="Property"
doc={"Input a data property to base styles off of."}
/>
<div className="maputnik-data-spec-property-input">
<StringInput
value={this.props.value.property}
onChange={propVal => this.changeDataProperty("property", propVal)}
let zoomInput = null;
if(zoomLevel !== undefined) {
zoomInput = <div className="maputnik-data-spec-property-stop-edit">
<NumberInput
value={zoomLevel}
onChange={newZoom => this.changeStop(idx, {zoom: newZoom, value: dataLevel}, value)}
min={0}
max={22}
/>
</div>
</div>
<div className="maputnik-data-spec-property-group">
<DocLabel
label="Type"
doc={"Select a type of data scale (default is 'categorical')."}
/>
<div className="maputnik-data-spec-property-input">
<SelectInput
value={this.props.value.type}
onChange={propVal => this.changeDataProperty("type", propVal)}
options={this.getDataFunctionTypes(this.props.fieldSpec)}
/>
}
const errorKeyStart = `${fieldType}.${fieldName}.stops[${idx}]`;
const foundErrors = Object.entries(errors).filter(([key, error]) => {
return key.startsWith(errorKeyStart);
});
const message = foundErrors.map(([key, error]) => {
return error.message;
}).join("");
const error = message ? {message} : undefined;
return <InputBlock
error={error}
key={key}
action={deleteStopBtn}
label=""
>
{zoomInput}
<div className="maputnik-data-spec-property-stop-data">
{dataInput}
</div>
</div>
<div className="maputnik-data-spec-property-group">
<DocLabel
label="Default"
doc={"Input a default value for data if not covered by the scales."}
/>
<div className="maputnik-data-spec-property-input">
<div className="maputnik-data-spec-property-stop-value">
<SpecField
fieldName={this.props.fieldName}
fieldSpec={this.props.fieldSpec}
value={this.props.value.default}
onChange={(_, propVal) => this.changeDataProperty("default", propVal)}
value={value}
onChange={(_, newValue) => this.changeStop(idx, {zoom: zoomLevel, value: dataLevel}, newValue)}
/>
</div>
</div>
</InputBlock>
</div>
{dataFields}
</InputBlock>
})
}
return <div className="maputnik-data-spec-block">
<div className="maputnik-data-spec-property">
<InputBlock
fieldSpec={this.props.fieldSpec}
label={labelFromFieldName(this.props.fieldName)}
>
<div className="maputnik-data-spec-property-group">
<DocLabel
label="Type"
/>
<div className="maputnik-data-spec-property-input">
<SelectInput
value={this.props.value.type}
onChange={propVal => this.changeDataProperty("type", propVal)}
title={"Select a type of data scale (default is 'categorical')."}
options={this.getDataFunctionTypes(this.props.fieldSpec)}
/>
</div>
</div>
<div className="maputnik-data-spec-property-group">
<DocLabel
label="Property"
/>
<div className="maputnik-data-spec-property-input">
<StringInput
value={this.props.value.property}
title={"Input a data property to base styles off of."}
onChange={propVal => this.changeDataProperty("property", propVal)}
/>
</div>
</div>
{dataFields &&
<div className="maputnik-data-spec-property-group">
<DocLabel
label="Default"
/>
<div className="maputnik-data-spec-property-input">
<SpecField
fieldName={this.props.fieldName}
fieldSpec={this.props.fieldSpec}
value={this.props.value.default}
onChange={(_, propVal) => this.changeDataProperty("default", propVal)}
/>
</div>
</div>
}
</InputBlock>
</div>
{dataFields &&
<>
{dataFields}
<Button
className="maputnik-add-stop"
onClick={this.props.onAddStop.bind(this)}
>
<svg style={{width:"14px", height:"14px", verticalAlign: "text-bottom"}} viewBox="0 0 24 24">
<path fill="currentColor" d={mdiTableRowPlusAfter} />
</svg> Add stop
</Button>
</>
}
<Button
className="maputnik-add-stop"
onClick={this.props.onAddStop.bind(this)}
onClick={this.props.onExpressionClick.bind(this)}
>
Add stop
<svg style={{width:"14px", height:"14px", verticalAlign: "text-bottom"}} viewBox="0 0 24 24">
<path fill="currentColor" d={mdiFunctionVariant} />
</svg> Convert to expression
</Button>
</div>
}

View File

@@ -1,7 +1,6 @@
import React from 'react'
import PropTypes from 'prop-types'
import DocLabel from './DocLabel'
import Button from '../Button'
import {MdDelete} from 'react-icons/md'
@@ -15,11 +14,9 @@ export default class DeleteStopButton extends React.Component {
return <Button
className="maputnik-delete-stop"
onClick={this.props.onClick}
title={"Remove zoom level stop."}
>
<DocLabel
label={<MdDelete />}
doc={"Remove zoom level stop."}
/>
<MdDelete />
</Button>
}
}

View File

@@ -0,0 +1,135 @@
import React from 'react'
import PropTypes from 'prop-types'
import InputBlock from '../inputs/InputBlock'
import Button from '../Button'
import {MdDelete, MdUndo} from 'react-icons/md'
import StringInput from '../inputs/StringInput'
import labelFromFieldName from './_labelFromFieldName'
import stringifyPretty from 'json-stringify-pretty-compact'
import JSONEditor from '../layers/JSONEditor'
export default class ExpressionProperty extends React.Component {
static propTypes = {
onDelete: PropTypes.func,
fieldName: PropTypes.string,
fieldType: PropTypes.string,
fieldSpec: PropTypes.object,
value: PropTypes.any,
errors: PropTypes.object,
onChange: PropTypes.func,
onUndo: PropTypes.func,
canUndo: PropTypes.func,
onFocus: PropTypes.func,
onBlur: PropTypes.func,
}
static defaultProps = {
errors: {},
onFocus: () => {},
onBlur: () => {},
}
constructor (props) {
super();
this.state = {
jsonError: false,
};
}
onJSONInvalid = (err) => {
this.setState({
jsonError: true,
})
}
onJSONValid = () => {
this.setState({
jsonError: false,
})
}
render() {
const {errors, fieldName, fieldType, value, canUndo} = this.props;
const {jsonError} = this.state;
const undoDisabled = canUndo ? !canUndo() : true;
const deleteStopBtn = (
<>
{this.props.onUndo &&
<Button
key="undo_action"
onClick={this.props.onUndo}
disabled={undoDisabled}
className="maputnik-delete-stop"
>
<MdUndo />
</Button>
}
<Button
key="delete_action"
onClick={this.props.onDelete}
className="maputnik-delete-stop"
>
<MdDelete />
</Button>
</>
);
const fieldKey = fieldType === undefined ? fieldName : `${fieldType}.${fieldName}`;
const fieldError = errors[fieldKey];
const errorKeyStart = `${fieldKey}[`;
const foundErrors = [];
function getValue (data) {
return stringifyPretty(data, {indent: 2, maxLength: 38})
}
if (jsonError) {
foundErrors.push({message: "Invalid JSON"});
}
else {
Object.entries(errors)
.filter(([key, error]) => {
return key.startsWith(errorKeyStart);
})
.forEach(([key, error]) => {
return foundErrors.push(error);
})
if (fieldError) {
foundErrors.push(fieldError);
}
}
return <InputBlock
error={foundErrors}
fieldSpec={this.props.fieldSpec}
label={labelFromFieldName(this.props.fieldName)}
action={deleteStopBtn}
wideMode={true}
>
<JSONEditor
mode={{name: "mgl"}}
lint={{
context: "expression",
spec: this.props.fieldSpec,
}}
className="maputnik-expression-editor"
onFocus={this.props.onFocus}
onBlur={this.props.onBlur}
onJSONInvalid={this.onJSONInvalid}
onJSONValid={this.onJSONValid}
layer={value}
lineNumbers={false}
maxHeight={200}
lineWrapping={true}
getValue={getValue}
onChange={this.props.onChange}
/>
</InputBlock>
}
}

View File

@@ -1,48 +1,77 @@
import React from 'react'
import PropTypes from 'prop-types'
import DocLabel from './DocLabel'
import Button from '../Button'
import {MdFunctions, MdInsertChart} from 'react-icons/md'
import {mdiFunctionVariant} from '@mdi/js';
/**
* So here we can't just check is `Array.isArray(value)` because certain
* properties accept arrays as values, for example `text-font`. So we must try
* and create an expression.
*/
function isExpression(value, fieldSpec={}) {
if (!Array.isArray(value)) {
return false;
}
try {
expression.createExpression(value, fieldSpec);
return true;
}
catch (err) {
return false;
}
}
export default class FunctionButtons extends React.Component {
static propTypes = {
fieldSpec: PropTypes.object,
onZoomClick: PropTypes.func,
onDataClick: PropTypes.func,
onExpressionClick: PropTypes.func,
}
render() {
let makeZoomButton, makeDataButton
let makeZoomButton, makeDataButton, expressionButton;
if (this.props.fieldSpec.expression.parameters.includes('zoom')) {
expressionButton = (
<Button
className="maputnik-make-zoom-function"
onClick={this.props.onExpressionClick}
>
<svg style={{width:"14px", height:"14px", verticalAlign: "middle"}} viewBox="0 0 24 24">
<path fill="currentColor" d={mdiFunctionVariant} />
</svg>
</Button>
);
makeZoomButton = <Button
className="maputnik-make-zoom-function"
onClick={this.props.onZoomClick}
title={"Turn property into a zoom function to enable a map feature to change with map's zoom level."}
>
<DocLabel
label={<MdFunctions />}
cursorTargetStyle={{ cursor: 'pointer' }}
doc={"Turn property into a zoom function to enable a map feature to change with map's zoom level."}
/>
<MdFunctions />
</Button>
if (this.props.fieldSpec['property-type'] === 'data-driven') {
makeDataButton = <Button
className="maputnik-make-data-function"
onClick={this.props.onDataClick}
title={"Turn property into a data function to enable a map feature to change according to data properties and the map's zoom level."}
>
<DocLabel
label={<MdInsertChart />}
cursorTargetStyle={{ cursor: 'pointer' }}
doc={"Turn property into a data function to enable a map feature to change according to data properties and the map's zoom level."}
/>
<MdInsertChart />
</Button>
}
return <div>{makeDataButton}{makeZoomButton}</div>
return <div>
{expressionButton}
{makeDataButton}
{makeZoomButton}
</div>
}
else {
return null
return <div>{expressionButton}</div>
}
}
}

View File

@@ -13,18 +13,33 @@ export default class SpecProperty extends React.Component {
onZoomClick: PropTypes.func.isRequired,
onDataClick: PropTypes.func.isRequired,
fieldName: PropTypes.string,
fieldSpec: PropTypes.object
fieldType: PropTypes.string,
fieldSpec: PropTypes.object,
value: PropTypes.any,
errors: PropTypes.object,
onExpressionClick: PropTypes.func,
}
static defaultProps = {
errors: {},
}
render() {
const {errors, fieldName, fieldType} = this.props;
const functionBtn = <FunctionButtons
fieldSpec={this.props.fieldSpec}
onZoomClick={this.props.onZoomClick}
onDataClick={this.props.onDataClick}
value={this.props.value}
onExpressionClick={this.props.onExpressionClick}
/>
const error = errors[fieldType+"."+fieldName];
return <InputBlock
doc={this.props.fieldSpec.doc}
error={error}
fieldSpec={this.props.fieldSpec}
label={labelFromFieldName(this.props.fieldName)}
action={functionBtn}
>

View File

@@ -1,5 +1,6 @@
import React from 'react'
import PropTypes from 'prop-types'
import {mdiFunctionVariant, mdiTableRowPlusAfter} from '@mdi/js';
import Button from '../Button'
import SpecField from './SpecField'
@@ -42,8 +43,11 @@ export default class ZoomProperty extends React.Component {
onChange: PropTypes.func,
onDeleteStop: PropTypes.func,
onAddStop: PropTypes.func,
onExpressionClick: PropTypes.func,
fieldType: PropTypes.string,
fieldName: PropTypes.string,
fieldSpec: PropTypes.object,
errors: PropTypes.object,
value: PropTypes.oneOfType([
PropTypes.object,
PropTypes.string,
@@ -53,6 +57,10 @@ export default class ZoomProperty extends React.Component {
]),
}
static defaultProps = {
errors: {},
}
state = {
refs: {}
}
@@ -117,15 +125,28 @@ export default class ZoomProperty extends React.Component {
}
render() {
const {fieldName, fieldType, errors} = this.props;
const zoomFields = this.props.value.stops.map((stop, idx) => {
const zoomLevel = stop[0]
const key = this.state.refs[idx];
const value = stop[1]
const deleteStopBtn= <DeleteStopButton onClick={this.props.onDeleteStop.bind(this, idx)} />
const errorKeyStart = `${fieldType}.${fieldName}.stops[${idx}]`;
const foundErrors = Object.entries(errors).filter(([key, error]) => {
return key.startsWith(errorKeyStart);
});
const message = foundErrors.map(([key, error]) => {
return error.message;
}).join("");
const error = message ? {message} : undefined;
return <InputBlock
error={error}
key={key}
doc={this.props.fieldSpec.doc}
fieldSpec={this.props.fieldSpec}
label={labelFromFieldName(this.props.fieldName)}
action={deleteStopBtn}
>
@@ -156,7 +177,17 @@ export default class ZoomProperty extends React.Component {
className="maputnik-add-stop"
onClick={this.props.onAddStop.bind(this)}
>
Add stop
<svg style={{width:"14px", height:"14px", verticalAlign: "text-bottom"}} viewBox="0 0 24 24">
<path fill="currentColor" d={mdiTableRowPlusAfter} />
</svg> Add stop
</Button>
<Button
className="maputnik-add-stop"
onClick={this.props.onExpressionClick.bind(this)}
>
<svg style={{width:"14px", height:"14px", verticalAlign: "text-bottom"}} viewBox="0 0 24 24">
<path fill="currentColor" d={mdiFunctionVariant} />
</svg> Convert to expression
</Button>
</div>
}

View File

@@ -1,6 +1,13 @@
import capitalize from 'lodash.capitalize'
export default function labelFromFieldName(fieldName) {
let label = fieldName.split('-').slice(1).join(' ')
return capitalize(label)
let label;
const parts = fieldName.split('-');
if (parts.length > 1) {
label = fieldName.split('-').slice(1).join(' ');
}
else {
label = fieldName;
}
return capitalize(label);
}

View File

@@ -1,13 +1,86 @@
import React from 'react'
import PropTypes from 'prop-types'
import { combiningFilterOps } from '../../libs/filterops.js'
import {mdiTableRowPlusAfter} from '@mdi/js';
import {latest} from '@mapbox/mapbox-gl-style-spec'
import {latest, validate, migrate} from '@mapbox/mapbox-gl-style-spec'
import DocLabel from '../fields/DocLabel'
import SelectInput from '../inputs/SelectInput'
import InputBlock from '../inputs/InputBlock'
import SingleFilterEditor from './SingleFilterEditor'
import FilterEditorBlock from './FilterEditorBlock'
import Button from '../Button'
import SpecDoc from '../inputs/SpecDoc'
import ExpressionProperty from '../fields/_ExpressionProperty';
import {mdiFunctionVariant} from '@mdi/js';
function combiningFilter (props) {
let filter = props.filter || ['all'];
if (!Array.isArray(filter)) {
return filter;
}
let combiningOp = filter[0];
let filters = filter.slice(1);
if(combiningFilterOps.indexOf(combiningOp) < 0) {
combiningOp = 'all';
filters = [filter.slice(0)];
}
return [combiningOp, ...filters];
}
function migrateFilter (filter) {
return migrate(createStyleFromFilter(filter)).layers[0].filter;
}
function createStyleFromFilter (filter) {
return {
"id": "tmp",
"version": 8,
"name": "Empty Style",
"metadata": {"maputnik:renderer": "mbgljs"},
"sources": {
"tmp": {
"type": "geojson",
"data": {}
}
},
"sprite": "",
"glyphs": "https://orangemug.github.io/font-glyphs/glyphs/{fontstack}/{range}.pbf",
"layers": [
{
id: "tmp",
type: "fill",
source: "tmp",
filter: filter,
},
],
};
}
/**
* This is doing way more work than we need it to, however validating a whole
* style if the only thing that's exported from mapbox-gl-style-spec at the
* moment. Not really an issue though as it take ~0.1ms to calculate.
*/
function checkIfSimpleFilter (filter) {
if (!filter || !combiningFilterOps.includes(filter[0])) {
return false;
}
// Because "none" isn't supported by the next expression syntax we can test
// with ["none", ...] because it'll return false if it's a new style
// expression.
const moddedFilter = ["none", ...filter.slice(1)];
const tmpStyle = createStyleFromFilter(moddedFilter)
const errors = validate(tmpStyle);
return (errors.length < 1);
}
function hasCombiningFilter(filter) {
return combiningFilterOps.indexOf(filter[0]) >= 0
@@ -26,86 +99,215 @@ export default class CombiningFilterEditor extends React.Component {
/** Properties of the vector layer and the available fields */
properties: PropTypes.object,
filter: PropTypes.array,
errors: PropTypes.object,
onChange: PropTypes.func.isRequired,
}
// Convert filter to combining filter
combiningFilter() {
let filter = this.props.filter || ['all']
let combiningOp = filter[0]
let filters = filter.slice(1)
if(combiningFilterOps.indexOf(combiningOp) < 0) {
combiningOp = 'all'
filters = [filter.slice(0)]
}
return [combiningOp, ...filters]
static defaultProps = {
filter: ["all"],
}
constructor (props) {
super();
this.state = {
showDoc: false,
displaySimpleFilter: checkIfSimpleFilter(combiningFilter(props)),
};
}
// Convert filter to combining filter
onFilterPartChanged(filterIdx, newPart) {
const newFilter = this.combiningFilter().slice(0)
const newFilter = combiningFilter(this.props).slice(0)
newFilter[filterIdx] = newPart
this.props.onChange(newFilter)
}
deleteFilterItem(filterIdx) {
const newFilter = this.combiningFilter().slice(0)
console.log('Delete', filterIdx, newFilter)
const newFilter = combiningFilter(this.props).slice(0)
newFilter.splice(filterIdx + 1, 1)
this.props.onChange(newFilter)
}
addFilterItem = () => {
const newFilterItem = this.combiningFilter().slice(0)
const newFilterItem = combiningFilter(this.props).slice(0)
newFilterItem.push(['==', 'name', ''])
this.props.onChange(newFilterItem)
}
render() {
const filter = this.combiningFilter()
let combiningOp = filter[0]
let filters = filter.slice(1)
onToggleDoc = (val) => {
this.setState({
showDoc: val
});
}
const editorBlocks = filters.map((f, idx) => {
return <FilterEditorBlock key={idx} onDelete={this.deleteFilterItem.bind(this, idx)}>
<SingleFilterEditor
properties={this.props.properties}
filter={f}
onChange={this.onFilterPartChanged.bind(this, idx + 1)}
/>
</FilterEditorBlock>
makeFilter = () => {
this.setState({
displaySimpleFilter: true,
})
}
//TODO: Implement support for nested filter
if(hasNestedCombiningFilter(filter)) {
return <div className="maputnik-filter-editor-unsupported">
Nested filters are not supported.
</div>
makeExpression = () => {
let filter = combiningFilter(this.props);
this.props.onChange(migrateFilter(filter));
this.setState({
displaySimpleFilter: false,
})
}
static getDerivedStateFromProps (props, currentState) {
const {filter} = props;
const displaySimpleFilter = checkIfSimpleFilter(combiningFilter(props));
// Upgrade but never downgrade
if (!displaySimpleFilter && currentState.displaySimpleFilter === true) {
return {
displaySimpleFilter: false,
valueIsSimpleFilter: false,
};
}
else if (displaySimpleFilter && currentState.displaySimpleFilter === false) {
return {
valueIsSimpleFilter: true,
}
}
else {
return {
valueIsSimpleFilter: false,
};
}
}
return <div className="maputnik-filter-editor">
<div className="maputnik-filter-editor-compound-select" data-wd-key="layer-filter">
<DocLabel
label={"Compound Filter"}
doc={latest.layer.filter.doc + " Combine multiple filters together by using a compound filter."}
/>
<SelectInput
value={combiningOp}
onChange={this.onFilterPartChanged.bind(this, 0)}
options={[["all", "every filter matches"], ["none", "no filter matches"], ["any", "any filter matches"]]}
/>
</div>
{editorBlocks}
<div className="maputnik-filter-editor-add-wrapper">
render() {
const {errors} = this.props;
const {displaySimpleFilter} = this.state;
const fieldSpec={
doc: latest.layer.filter.doc + " Combine multiple filters together by using a compound filter."
};
const defaultFilter = ["all"];
const isNestedCombiningFilter = displaySimpleFilter && hasNestedCombiningFilter(combiningFilter(this.props));
if (isNestedCombiningFilter) {
return <div className="maputnik-filter-editor-unsupported">
<p>
Nested filters are not supported.
</p>
<Button
data-wd-key="layer-filter-button"
className="maputnik-add-filter"
onClick={this.addFilterItem}>
Add filter
onClick={this.makeExpression}
>
<svg style={{marginRight: "0.2em", width:"14px", height:"14px", verticalAlign: "middle"}} viewBox="0 0 24 24">
<path fill="currentColor" d={mdiFunctionVariant} />
</svg>
Upgrade to expression
</Button>
</div>
</div>
}
else if (displaySimpleFilter) {
const filter = combiningFilter(this.props);
let combiningOp = filter[0];
let filters = filter.slice(1)
const actions = (
<div>
<Button
onClick={this.makeExpression}
className="maputnik-make-zoom-function"
>
<svg style={{width:"14px", height:"14px", verticalAlign: "middle"}} viewBox="0 0 24 24">
<path fill="currentColor" d={mdiFunctionVariant} />
</svg>
</Button>
</div>
);
const editorBlocks = filters.map((f, idx) => {
const error = errors[`filter[${idx+1}]`];
return (
<div key={`block-${idx}`}>
<FilterEditorBlock key={idx} onDelete={this.deleteFilterItem.bind(this, idx)}>
<SingleFilterEditor
properties={this.props.properties}
filter={f}
onChange={this.onFilterPartChanged.bind(this, idx + 1)}
/>
</FilterEditorBlock>
{error &&
<div key="error" className="maputnik-inline-error">{error.message}</div>
}
</div>
);
})
return (
<>
<InputBlock
key="top"
fieldSpec={fieldSpec}
label={"Filter"}
action={actions}
>
<SelectInput
value={combiningOp}
onChange={this.onFilterPartChanged.bind(this, 0)}
options={[["all", "every filter matches"], ["none", "no filter matches"], ["any", "any filter matches"]]}
/>
</InputBlock>
{editorBlocks}
<div
key="buttons"
className="maputnik-filter-editor-add-wrapper"
>
<Button
data-wd-key="layer-filter-button"
className="maputnik-add-filter"
onClick={this.addFilterItem}
>
<svg style={{width:"14px", height:"14px", verticalAlign: "text-bottom"}} viewBox="0 0 24 24">
<path fill="currentColor" d={mdiTableRowPlusAfter} />
</svg> Add filter
</Button>
</div>
<div
key="doc"
className="maputnik-doc-inline"
style={{display: this.state.showDoc ? '' : 'none'}}
>
<SpecDoc fieldSpec={fieldSpec} />
</div>
</>
);
}
else {
let {filter} = this.props;
return (
<>
<ExpressionProperty
onDelete={() => {
this.setState({displaySimpleFilter: true});
this.props.onChange(defaultFilter);
}}
fieldName="filter"
fieldSpec={fieldSpec}
value={filter}
errors={errors}
onChange={this.props.onChange}
/>
{this.state.valueIsSimpleFilter &&
<div className="maputnik-expr-infobox">
You&apos;ve entered a old style filter,{' '}
<button
onClick={this.makeFilter}
className="maputnik-expr-infobox__button"
>
switch to filter editor
</button>
</div>
}
</>
);
}
}
}

View File

@@ -6,6 +6,7 @@ import FillIcon from './FillIcon.jsx'
import SymbolIcon from './SymbolIcon.jsx'
import BackgroundIcon from './BackgroundIcon.jsx'
import CircleIcon from './CircleIcon.jsx'
import MissingIcon from './MissingIcon.jsx'
class LayerIcon extends React.Component {
static propTypes = {
@@ -25,6 +26,7 @@ class LayerIcon extends React.Component {
case 'line': return <LineIcon {...iconProps} />
case 'symbol': return <SymbolIcon {...iconProps} />
case 'circle': return <CircleIcon {...iconProps} />
default: return <MissingIcon {...iconProps} />
}
}
}

View File

@@ -0,0 +1,11 @@
import React from 'react'
import {MdPriorityHigh} from 'react-icons/md'
export default class MissingIcon extends React.Component {
render() {
return (
<MdPriorityHigh {...this.props} />
)
}
}

View File

@@ -12,29 +12,89 @@ class ArrayInput extends React.Component {
onChange: PropTypes.func,
}
changeValue(idx, newValue) {
console.log(idx, newValue)
const values = this.values.slice(0)
values[idx] = newValue
this.props.onChange(values)
static defaultProps = {
value: [],
default: [],
}
get values() {
return this.props.value || this.props.default || []
constructor (props) {
super(props);
this.state = {
value: this.props.value.slice(0),
// This is so we can compare changes in getDerivedStateFromProps
initialPropsValue: this.props.value.slice(0),
};
}
static getDerivedStateFromProps(props, state) {
const value = [];
const initialPropsValue = state.initialPropsValue.slice(0);
Array(props.length).fill(null).map((_, i) => {
if (props.value[i] === state.initialPropsValue[i]) {
value[i] = state.value[i];
}
else {
value[i] = state.value[i];
initialPropsValue[i] = state.value[i];
}
})
return {
value,
initialPropsValue,
};
}
isComplete (value) {
return Array(this.props.length).fill(null).every((_, i) => {
const val = value[i]
return !(val === undefined || val === "");
});
}
changeValue(idx, newValue) {
const value = this.state.value.slice(0);
value[idx] = newValue;
this.setState({
value,
}, () => {
if (this.isComplete(value)) {
this.props.onChange(value);
}
else {
// Unset until complete
this.props.onChange(undefined);
}
});
}
render() {
const inputs = this.values.map((v, i) => {
const {value} = this.state;
const containsValues = (
value.length > 0 &&
!value.every(val => {
return (val === "" || val === undefined)
})
);
const inputs = Array(this.props.length).fill(null).map((_, i) => {
if(this.props.type === 'number') {
return <NumberInput
key={i}
value={v}
default={containsValues ? undefined : this.props.default[i]}
value={value[i]}
required={containsValues ? true : false}
onChange={this.changeValue.bind(this, i)}
/>
} else {
return <StringInput
key={i}
value={v}
default={containsValues ? undefined : this.props.default[i]}
value={value[i]}
required={containsValues ? true : false}
onChange={this.changeValue.bind(this, i)}
/>
}

View File

@@ -3,11 +3,15 @@ import PropTypes from 'prop-types'
class CheckboxInput extends React.Component {
static propTypes = {
value: PropTypes.bool.isRequired,
value: PropTypes.bool,
style: PropTypes.object,
onChange: PropTypes.func,
}
static defaultProps = {
value: false,
}
render() {
return <label className="maputnik-checkbox-wrapper">
<input

View File

@@ -5,6 +5,8 @@ import NumberInput from './NumberInput'
import Button from '../Button'
import {MdDelete} from 'react-icons/md'
import DocLabel from '../fields/DocLabel'
import EnumInput from '../inputs/SelectInput'
import capitalize from 'lodash.capitalize'
class DynamicArrayInput extends React.Component {
@@ -14,6 +16,7 @@ class DynamicArrayInput extends React.Component {
default: PropTypes.array,
onChange: PropTypes.func,
style: PropTypes.object,
fieldSpec: PropTypes.object,
}
changeValue(idx, newValue) {
@@ -31,6 +34,11 @@ class DynamicArrayInput extends React.Component {
const values = this.values.slice(0)
if (this.props.type === 'number') {
values.push(0)
}
else if (this.props.type === 'enum') {
const {fieldSpec} = this.props;
const defaultValue = Object.keys(fieldSpec.values)[0];
values.push(defaultValue);
} else {
values.push("")
}
@@ -48,15 +56,28 @@ class DynamicArrayInput extends React.Component {
render() {
const inputs = this.values.map((v, i) => {
const deleteValueBtn= <DeleteValueButton onClick={this.deleteValue.bind(this, i)} />
const input = this.props.type === 'number'
? <NumberInput
let input;
if (this.props.type === 'number') {
input = <NumberInput
value={v}
onChange={this.changeValue.bind(this, i)}
/>
: <StringInput
}
else if (this.props.type === 'enum') {
const options = Object.keys(this.props.fieldSpec.values).map(v => [v, capitalize(v)]);
input = <EnumInput
options={options}
value={v}
onChange={this.changeValue.bind(this, i)}
/>
}
else {
input = <StringInput
value={v}
onChange={this.changeValue.bind(this, i)}
/>
}
return <div
style={this.props.style}

View File

@@ -0,0 +1,45 @@
import React from 'react'
import PropTypes from 'prop-types'
import SelectInput from '../inputs/SelectInput'
import MultiButtonInput from '../inputs/MultiButtonInput'
function optionsLabelLength(options) {
let sum = 0;
options.forEach(([_, label]) => {
sum += label.length
})
return sum
}
class EnumInput extends React.Component {
static propTypes = {
"data-wd-key": PropTypes.string,
value: PropTypes.string,
style: PropTypes.object,
default: PropTypes.string,
onChange: PropTypes.func,
options: PropTypes.array,
}
render() {
const {options, value, onChange} = this.props;
if(options.length <= 3 && optionsLabelLength(options) <= 20) {
return <MultiButtonInput
options={options}
value={value || this.props.default}
onChange={onChange}
/>
} else {
return <SelectInput
options={options}
value={value || this.props.default}
onChange={onChange}
/>
}
}
}
export default EnumInput

View File

@@ -4,7 +4,7 @@ import AutocompleteInput from './AutocompleteInput'
class FontInput extends React.Component {
static propTypes = {
value: PropTypes.array.isRequired,
value: PropTypes.array,
default: PropTypes.array,
fonts: PropTypes.array,
style: PropTypes.object,
@@ -16,7 +16,7 @@ class FontInput extends React.Component {
}
get values() {
const out = this.props.value || this.props.default.slice(1) || [""];
const out = this.props.value || this.props.default || [];
// Always put a "" in the last field to you can keep adding entries
if (out[out.length-1] !== ""){

View File

@@ -2,6 +2,8 @@ import React from 'react'
import PropTypes from 'prop-types'
import classnames from 'classnames'
import DocLabel from '../fields/DocLabel'
import SpecDoc from './SpecDoc'
/** Wrap a component with a label */
class InputBlock extends React.Component {
@@ -11,11 +13,20 @@ class InputBlock extends React.Component {
PropTypes.string,
PropTypes.element,
]).isRequired,
doc: PropTypes.string,
action: PropTypes.element,
children: PropTypes.node.isRequired,
style: PropTypes.object,
onChange: PropTypes.func,
fieldSpec: PropTypes.object,
wideMode: PropTypes.bool,
error: PropTypes.array,
}
constructor (props) {
super(props);
this.state = {
showDoc: false,
}
}
onChange(e) {
@@ -23,23 +34,33 @@ class InputBlock extends React.Component {
return this.props.onChange(value === "" ? undefined : value)
}
onToggleDoc = (val) => {
this.setState({
showDoc: val
});
}
render() {
const errors = [].concat(this.props.error || []);
return <div style={this.props.style}
data-wd-key={this.props["data-wd-key"]}
className={classnames({
"maputnik-input-block": true,
"maputnik-input-block--wide": this.props.wideMode,
"maputnik-action-block": this.props.action
})}
>
{this.props.doc &&
{this.props.fieldSpec &&
<div className="maputnik-input-block-label">
<DocLabel
label={this.props.label}
doc={this.props.doc}
onToggleDoc={this.onToggleDoc}
fieldSpec={this.props.fieldSpec}
/>
</div>
}
{!this.props.doc &&
{!this.props.fieldSpec &&
<label className="maputnik-input-block-label">
{this.props.label}
</label>
@@ -52,6 +73,21 @@ class InputBlock extends React.Component {
<div className="maputnik-input-block-content">
{this.props.children}
</div>
{errors.length > 0 &&
<div className="maputnik-inline-error">
{[].concat(this.props.error).map((error, idx) => {
return <div key={idx}>{error.message}</div>
})}
</div>
}
{this.props.fieldSpec &&
<div
className="maputnik-doc-inline"
style={{display: this.state.showDoc ? '' : 'none'}}
>
<SpecDoc fieldSpec={this.props.fieldSpec} />
</div>
}
</div>
}
}

View File

@@ -1,6 +1,8 @@
import React from 'react'
import PropTypes from 'prop-types'
let IDX = 0;
class NumberInput extends React.Component {
static propTypes = {
value: PropTypes.number,
@@ -8,36 +10,64 @@ class NumberInput extends React.Component {
min: PropTypes.number,
max: PropTypes.number,
onChange: PropTypes.func,
allowRange: PropTypes.bool,
rangeStep: PropTypes.number,
wdKey: PropTypes.string,
required: PropTypes.bool,
}
static defaultProps = {
rangeStep: 1
}
constructor(props) {
super(props)
this.state = {
uuid: IDX++,
editing: false,
value: props.value,
dirtyValue: props.value,
}
}
static getDerivedStateFromProps(props, state) {
if (!state.editing) {
if (!state.editing && props.value !== state.value) {
return {
value: props.value
value: props.value,
dirtyValue: props.value,
};
}
return null;
}
changeValue(newValue) {
this.setState({editing: true});
const value = parseFloat(newValue)
const value = (newValue === "" || newValue === undefined) ?
undefined :
parseFloat(newValue);
const hasChanged = this.state.value !== value
const hasChanged = this.props.value !== value;
if(this.isValid(value) && hasChanged) {
this.props.onChange(value)
this.setState({
value: newValue,
});
}
this.setState({ value: newValue })
else if (!this.isValid(value) && hasChanged) {
this.setState({
value: undefined,
});
}
this.setState({
dirtyValue: newValue === "" ? undefined : newValue,
})
}
isValid(v) {
if (v === undefined) {
return true;
}
const value = parseFloat(v)
if(isNaN(value)) {
return false
@@ -58,28 +88,145 @@ class NumberInput extends React.Component {
this.setState({editing: false});
// Reset explicitly to default value if value has been cleared
if(this.state.value === "") {
return this.changeValue(this.props.default)
return;
}
// If set value is invalid fall back to the last valid value from props or at last resort the default value
if(!this.isValid(this.state.value)) {
if (!this.isValid(this.state.value)) {
if(this.isValid(this.props.value)) {
this.changeValue(this.props.value)
this.setState({dirtyValue: this.props.value});
} else {
this.changeValue(this.props.default)
this.changeValue(undefined);
this.setState({dirtyValue: undefined});
}
}
}
onChangeRange = (e) => {
let value = parseFloat(e.target.value, 10);
const step = this.props.rangeStep;
let dirtyValue = value;
if(step) {
// Can't do this with the <input/> range step attribute else we won't be able to set a high precision value via the text input.
const snap = value % step;
// Round up/down to step
if (this._keyboardEvent) {
// If it's keyboard event we might get a low positive/negative value,
// for example we might go from 13 to 13.23, however because we know
// that came from a keyboard event we always want to increase by a
// single step value.
if (value < this.state.dirtyValue) {
value = this.state.value - step;
}
else {
value = this.state.value + step
}
dirtyValue = value;
}
else {
if (snap < step/2) {
value = value - snap;
}
else {
value = value + (step - snap);
};
}
}
this._keyboardEvent = false;
// Clamp between min/max
value = Math.max(this.props.min, Math.min(this.props.max, value));
this.setState({value, dirtyValue});
this.props.onChange(value);
}
render() {
return <input
spellCheck="false"
className="maputnik-number"
placeholder={this.props.default}
value={this.state.value}
onChange={e => this.changeValue(e.target.value)}
onBlur={this.resetValue}
/>
if(
this.props.hasOwnProperty("min") && this.props.hasOwnProperty("max") &&
this.props.min !== undefined && this.props.max !== undefined &&
this.props.allowRange
) {
const value = this.state.editing ? this.state.dirtyValue : this.state.value;
const defaultValue = this.props.default === undefined ? "" : this.props.default;
let inputValue;
if (this.state.editingRange) {
inputValue = this.state.value;
}
else {
inputValue = value;
}
return <div className="maputnik-number-container">
<input
className="maputnik-number-range"
key="range"
type="range"
max={this.props.max}
min={this.props.min}
step="any"
spellCheck="false"
value={value === undefined ? defaultValue : value}
aria-hidden="true"
onChange={this.onChangeRange}
onKeyDown={() => {
this._keyboardEvent = true;
}}
onPointerDown={() => {
this.setState({editing: true, editingRange: true});
}}
onPointerUp={() => {
// Safari doesn't get onBlur event
this.setState({editing: false, editingRange: false});
}}
onBlur={() => {
this.setState({
editing: false,
editingRange: false,
dirtyValue: this.state.value,
});
}}
/>
<input
key="text"
type="text"
spellCheck="false"
className="maputnik-number"
placeholder={this.props.default}
value={inputValue === undefined ? "" : inputValue}
onFocus={e => {
this.setState({editing: true});
}}
onChange={e => {
this.changeValue(e.target.value);
}}
onBlur={e => {
this.setState({editing: false});
this.resetValue()
}}
/>
</div>
}
else {
const value = this.state.editing ? this.state.dirtyValue : this.state.value;
return <input
spellCheck="false"
className="maputnik-number"
placeholder={this.props.default}
value={value === undefined ? "" : value}
onChange={e => this.changeValue(e.target.value)}
onFocus={() => {
this.setState({editing: true});
}}
onBlur={this.resetValue}
required={this.props.required}
/>
}
}
}

View File

@@ -8,6 +8,7 @@ class SelectInput extends React.Component {
options: PropTypes.array.isRequired,
style: PropTypes.object,
onChange: PropTypes.func.isRequired,
title: PropTypes.string,
}
@@ -21,6 +22,7 @@ class SelectInput extends React.Component {
className="maputnik-select"
data-wd-key={this.props["data-wd-key"]}
style={this.props.style}
title={this.props.title}
value={this.props.value}
onChange={e => this.props.onChange(e.target.value)}
>

View File

@@ -0,0 +1,83 @@
import React from 'react'
import PropTypes from 'prop-types'
export default class SpecDoc extends React.Component {
static propTypes = {
fieldSpec: PropTypes.object.isRequired,
}
render () {
const {fieldSpec} = this.props;
const {doc, values} = fieldSpec;
const sdkSupport = fieldSpec['sdk-support'];
const headers = {
js: "JS",
android: "Android",
ios: "iOS",
macos: "macOS",
};
const renderValues = (
!!values &&
// HACK: Currently we merge additional values into the stylespec, so this is required
// See <https://github.com/maputnik/editor/blob/master/src/components/fields/PropertyGroup.jsx#L16>
!Array.isArray(values)
);
return (
<>
{doc &&
<div className="SpecDoc">
<div className="SpecDoc__doc">{doc}</div>
{renderValues &&
<ul className="SpecDoc__values">
{Object.entries(values).map(([key, value]) => {
return (
<li key={key}>
<code>{JSON.stringify(key)}</code>
<div>{value.doc}</div>
</li>
);
})}
</ul>
}
</div>
}
{sdkSupport &&
<div className="SpecDoc__sdk-support">
<table className="SpecDoc__sdk-support__table">
<thead>
<tr>
<th></th>
{Object.values(headers).map(header => {
return <th key={header}>{header}</th>;
})}
</tr>
</thead>
<tbody>
{Object.entries(sdkSupport).map(([key, supportObj]) => {
return (
<tr key={key}>
<td>{key}</td>
{Object.keys(headers).map(k => {
const value = supportObj[k];
if (supportObj.hasOwnProperty(k)) {
return <td key={k}>{supportObj[k]}</td>;
}
else {
return <td key={k}>no</td>;
}
})}
</tr>
);
})}
</tbody>
</table>
</div>
}
</>
);
}
}

View File

@@ -8,7 +8,15 @@ class StringInput extends React.Component {
style: PropTypes.object,
default: PropTypes.string,
onChange: PropTypes.func,
onInput: PropTypes.func,
multi: PropTypes.bool,
required: PropTypes.bool,
disabled: PropTypes.bool,
spellCheck: PropTypes.bool,
}
static defaultProps = {
onInput: () => {},
}
constructor(props) {
@@ -25,6 +33,7 @@ class StringInput extends React.Component {
value: props.value
};
}
return {};
}
render() {
@@ -45,25 +54,38 @@ class StringInput extends React.Component {
]
}
if(!!this.props.disabled) {
classes.push("maputnik-string--disabled");
}
return React.createElement(tag, {
"data-wd-key": this.props["data-wd-key"],
spellCheck: !(tag === "input"),
spellCheck: this.props.hasOwnProperty("spellCheck") ? this.props.spellCheck : !(tag === "input"),
disabled: this.props.disabled,
className: classes.join(" "),
style: this.props.style,
value: this.state.value,
value: this.state.value === undefined ? "" : this.state.value,
placeholder: this.props.default,
onChange: e => {
this.setState({
editing: true,
value: e.target.value
})
}, () => {
this.props.onInput(this.state.value);
});
},
onBlur: () => {
if(this.state.value!==this.props.value) {
this.setState({editing: false});
this.props.onChange(this.state.value);
}
}
},
onKeyDown: (e) => {
if (e.keyCode === 13) {
this.props.onChange(this.state.value);
}
},
required: this.props.required,
});
}
}

View File

@@ -0,0 +1,77 @@
import React from 'react'
import PropTypes from 'prop-types'
import StringInput from './StringInput'
import SmallError from '../util/SmallError'
function validate (url) {
let error;
const getProtocol = (url) => {
try {
const urlObj = new URL(url);
return urlObj.protocol;
}
catch (err) {
return undefined;
}
};
const protocol = getProtocol(url);
if (
protocol &&
protocol === "http:" &&
window.location.protocol === "https:"
) {
error = (
<SmallError>
CORS policy won&apos;t allow fetching resources served over http from https, use a <code>https://</code> domain
</SmallError>
);
}
return error;
}
class UrlInput extends React.Component {
static propTypes = {
"data-wd-key": PropTypes.string,
value: PropTypes.string,
style: PropTypes.object,
default: PropTypes.string,
onChange: PropTypes.func,
onInput: PropTypes.func,
multi: PropTypes.bool,
required: PropTypes.bool,
}
static defaultProps = {
onInput: () => {},
}
constructor (props) {
super(props);
this.state = {
error: validate(props.value)
};
}
onInput = (url) => {
this.setState({
error: validate(url)
});
this.props.onInput(url);
}
render () {
return (
<div>
<StringInput
{...this.props}
onInput={this.onInput}
/>
{this.state.error}
</div>
);
}
}
export default UrlInput

View File

@@ -11,9 +11,13 @@ class MetadataBlock extends React.Component {
}
render() {
const fieldSpec = {
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."}
fieldSpec={fieldSpec}
data-wd-key="layer-comment"
>
<StringInput

View File

@@ -1,79 +1,159 @@
import React from 'react'
import PropTypes from 'prop-types'
import classnames from 'classnames';
import {Controlled as CodeMirror} from 'react-codemirror2'
import InputBlock from '../inputs/InputBlock'
import StringInput from '../inputs/StringInput'
import CodeMirror from 'codemirror';
import 'codemirror/mode/javascript/javascript'
import 'codemirror/addon/lint/lint'
import 'codemirror/addon/edit/matchbrackets'
import 'codemirror/lib/codemirror.css'
import 'codemirror/addon/lint/lint.css'
import '../../codemirror-maputnik.css'
import jsonlint from 'jsonlint'
// This is mainly because of this issue <https://github.com/zaach/jsonlint/issues/57> also the API has changed, see comment in file
import '../../vendor/codemirror/addon/lint/json-lint'
import stringifyPretty from 'json-stringify-pretty-compact'
import '../util/codemirror-mgl';
class JSONEditor extends React.Component {
static propTypes = {
layer: PropTypes.object.isRequired,
layer: PropTypes.any.isRequired,
maxHeight: PropTypes.number,
onChange: PropTypes.func,
lineNumbers: PropTypes.bool,
lineWrapping: PropTypes.bool,
getValue: PropTypes.func,
gutters: PropTypes.array,
className: PropTypes.string,
onFocus: PropTypes.func,
onBlur: PropTypes.func,
onJSONValid: PropTypes.func,
onJSONInvalid: PropTypes.func,
mode: PropTypes.object,
lint: PropTypes.oneOfType([
PropTypes.bool,
PropTypes.object,
]),
}
static defaultProps = {
lineNumbers: true,
lineWrapping: false,
gutters: ["CodeMirror-lint-markers"],
getValue: (data) => {
return stringifyPretty(data, {indent: 2, maxLength: 40});
},
onFocus: () => {},
onBlur: () => {},
onJSONInvalid: () => {},
onJSONValid: () => {},
}
constructor(props) {
super(props)
this.state = {
code: JSON.stringify(props.layer, null, 2)
}
isEditing: false,
prevValue: this.props.getValue(this.props.layer),
};
}
componentDidUpdate(prevProps) {
if (prevProps.layer !== this.props.layer) {
this.setState({
code: JSON.stringify(this.props.layer, null, 2)
})
}
}
onCodeUpdate(newCode) {
try {
const parsedLayer = JSON.parse(newCode)
this.props.onChange(parsedLayer)
} catch(err) {
console.warn(err)
} finally {
this.setState({
code: newCode
})
}
}
resetValue() {
console.log('reset')
this.setState({
code: JSON.stringify(this.props.layer, null, 2)
})
}
render() {
const codeMirrorOptions = {
mode: {name: "javascript", json: true},
componentDidMount () {
this._doc = CodeMirror(this._el, {
value: this.props.getValue(this.props.layer),
mode: this.props.mode || {
name: "mgl",
},
lineWrapping: this.props.lineWrapping,
tabSize: 2,
theme: 'maputnik',
viewportMargin: Infinity,
lineNumbers: true,
lint: true,
gutters: ["CodeMirror-lint-markers"],
lineNumbers: this.props.lineNumbers,
lint: this.props.lint || {
context: "layer"
},
matchBrackets: true,
gutters: this.props.gutters,
scrollbarStyle: "null",
});
this._doc.on('change', this.onChange);
this._doc.on('focus', this.onFocus);
this._doc.on('blur', this.onBlur);
}
onFocus = () => {
this.props.onFocus();
this.setState({
isEditing: true
});
}
onBlur = () => {
this.props.onBlur();
this.setState({
isEditing: false
});
}
componentWillUnMount () {
this._doc.off('change', this.onChange);
this._doc.off('focus', this.onFocus);
this._doc.off('blur', this.onBlur);
}
componentDidUpdate(prevProps) {
if (!this.state.isEditing && prevProps.layer !== this.props.layer) {
this._cancelNextChange = true;
this._doc.setValue(
this.props.getValue(this.props.layer),
)
}
}
onChange = (e) => {
if (this._cancelNextChange) {
this._cancelNextChange = false;
this.setState({
prevValue: this._doc.getValue(),
})
return;
}
const newCode = this._doc.getValue();
if (this.state.prevValue !== newCode) {
let parsedLayer, err;
try {
parsedLayer = JSON.parse(newCode);
} catch(_err) {
err = _err;
console.warn(_err)
}
if (err) {
this.props.onJSONInvalid();
}
else {
this.props.onChange(parsedLayer)
this.props.onJSONValid();
}
}
return <CodeMirror
value={this.state.code}
onBeforeChange={(editor, data, value) => this.onCodeUpdate(value)}
onFocusChange={focused => focused ? true : this.resetValue()}
options={codeMirrorOptions}
this.setState({
prevValue: newCode,
});
}
render() {
const style = {};
if (this.props.maxHeight) {
style.maxHeight = this.props.maxHeight;
}
return <div
className={classnames("codemirror-container", this.props.className)}
ref={(el) => this._el = el}
style={style}
/>
}
}

View File

@@ -13,13 +13,19 @@ import MaxZoomBlock from './MaxZoomBlock'
import CommentBlock from './CommentBlock'
import LayerSourceBlock from './LayerSourceBlock'
import LayerSourceLayerBlock from './LayerSourceLayerBlock'
import {Accordion} from 'react-accessible-accordion';
import {MdMoreVert} from 'react-icons/md'
import { changeType, changeProperty } from '../../libs/layer'
import layout from '../../config/layout.json'
import {formatLayerId} from '../util/format';
function getLayoutForType (type) {
return layout[type] ? layout[type] : layout.invalid;
}
function layoutGroups(layerType) {
const layerGroup = {
title: 'Layer',
@@ -33,7 +39,9 @@ function layoutGroups(layerType) {
title: 'JSON Editor',
type: 'jsoneditor'
}
return [layerGroup, filterGroup].concat(layout[layerType].groups).concat([editorGroup])
return [layerGroup, filterGroup]
.concat(getLayoutForType(layerType).groups)
.concat([editorGroup])
}
/** Layer editor supporting multiple types of layers. */
@@ -52,6 +60,7 @@ export default class LayerEditor extends React.Component {
isFirstLayer: PropTypes.bool,
isLastLayer: PropTypes.bool,
layerIndex: PropTypes.number,
errors: PropTypes.array,
}
static defaultProps = {
@@ -79,7 +88,7 @@ export default class LayerEditor extends React.Component {
static getDerivedStateFromProps(props, state) {
const additionalGroups = { ...state.editorGroups }
layout[props.layer.type].groups.forEach(group => {
getLayoutForType(props.layer.type).groups.forEach(group => {
if(!(group.title in additionalGroups)) {
additionalGroups[group.title] = true
}
@@ -100,7 +109,10 @@ export default class LayerEditor extends React.Component {
}
changeProperty(group, property, newValue) {
this.props.onLayerChanged(changeProperty(this.props.layer, group, property, newValue))
this.props.onLayerChanged(
this.props.layerIndex,
changeProperty(this.props.layer, group, property, newValue)
)
}
onGroupToggle(groupTitle, active) {
@@ -118,6 +130,20 @@ export default class LayerEditor extends React.Component {
if(this.props.layer.metadata) {
comment = this.props.layer.metadata['maputnik:comment']
}
const {errors, layerIndex} = this.props;
const errorData = {};
errors.forEach(error => {
if (
error.parsed &&
error.parsed.type === "layer" &&
error.parsed.data.index == layerIndex
) {
errorData[error.parsed.data.key] = {
message: error.parsed.data.message
};
}
})
let sourceLayerIds;
if(this.props.sources.hasOwnProperty(this.props.layer.source)) {
@@ -129,34 +155,45 @@ export default class LayerEditor extends React.Component {
<LayerIdBlock
value={this.props.layer.id}
wdKey="layer-editor.layer-id"
onChange={newId => this.props.onLayerIdChange(this.props.layer.id, newId)}
error={errorData.id}
onChange={newId => this.props.onLayerIdChange(this.props.layerIndex, this.props.layer.id, newId)}
/>
<LayerTypeBlock
disabled={true}
error={errorData.type}
value={this.props.layer.type}
onChange={newType => this.props.onLayerChanged(changeType(this.props.layer, newType))}
onChange={newType => this.props.onLayerChanged(
this.props.layerIndex,
changeType(this.props.layer, newType)
)}
/>
{this.props.layer.type !== 'background' && <LayerSourceBlock
error={errorData.source}
sourceIds={Object.keys(this.props.sources)}
value={this.props.layer.source}
onChange={v => this.changeProperty(null, 'source', v)}
/>
}
{['background', 'raster', 'hillshade', 'heatmap'].indexOf(this.state.type) < 0 &&
{['background', 'raster', 'hillshade', 'heatmap'].indexOf(this.props.layer.type) < 0 &&
<LayerSourceLayerBlock
error={errorData['source-layer']}
sourceLayerIds={sourceLayerIds}
value={this.props.layer['source-layer']}
onChange={v => this.changeProperty(null, 'source-layer', v)}
/>
}
<MinZoomBlock
error={errorData.minzoom}
value={this.props.layer.minzoom}
onChange={v => this.changeProperty(null, 'minzoom', v)}
/>
<MaxZoomBlock
error={errorData.maxzoom}
value={this.props.layer.maxzoom}
onChange={v => this.changeProperty(null, 'maxzoom', v)}
/>
<CommentBlock
error={errorData.comment}
value={comment}
onChange={v => this.changeProperty('metadata', 'maputnik:comment', v == "" ? undefined : v)}
/>
@@ -164,6 +201,7 @@ export default class LayerEditor extends React.Component {
case 'filter': return <div>
<div className="maputnik-filter-editor-wrapper">
<FilterEditor
errors={errorData}
filter={this.props.layer.filter}
properties={this.props.vectorLayers[this.props.layer['source-layer']]}
onChange={f => this.changeProperty(null, 'filter', f)}
@@ -171,6 +209,7 @@ export default class LayerEditor extends React.Component {
</div>
</div>
case 'properties': return <PropertyGroup
errors={errorData}
layer={this.props.layer}
groupFields={fields}
spec={this.props.spec}
@@ -178,7 +217,12 @@ export default class LayerEditor extends React.Component {
/>
case 'jsoneditor': return <JSONEditor
layer={this.props.layer}
onChange={this.props.onLayerChanged}
onChange={(layer) => {
this.props.onLayerChanged(
this.props.layerIndex,
layer
);
}}
/>
}
}
@@ -191,12 +235,16 @@ export default class LayerEditor extends React.Component {
}
render() {
const groupIds = [];
const layerType = this.props.layer.type
const groups = layoutGroups(layerType).filter(group => {
return !(layerType === 'background' && group.type === 'source')
}).map(group => {
const groupId = group.title.replace(/ /g, "_");
groupIds.push(groupId);
return <LayerEditorGroup
data-wd-key={group.title}
id={groupId}
key={group.title}
title={group.title}
isActive={this.state.editorGroups[group.title]}
@@ -211,15 +259,15 @@ export default class LayerEditor extends React.Component {
const items = {
delete: {
text: "Delete",
handler: () => this.props.onLayerDestroy(this.props.layer.id)
handler: () => this.props.onLayerDestroy(this.props.layerIndex)
},
duplicate: {
text: "Duplicate",
handler: () => this.props.onLayerCopy(this.props.layer.id)
handler: () => this.props.onLayerCopy(this.props.layerIndex)
},
hide: {
text: (layout.visibility === "none") ? "Show" : "Hide",
handler: () => this.props.onLayerVisibilityToggle(this.props.layer.id)
handler: () => this.props.onLayerVisibilityToggle(this.props.layerIndex)
},
moveLayerUp: {
text: "Move layer up",
@@ -245,7 +293,7 @@ export default class LayerEditor extends React.Component {
<header>
<div className="layer-header">
<h2 className="layer-header__title">
Layer: {this.props.layer.id}
Layer: {formatLayerId(this.props.layer.id)}
</h2>
<div className="layer-header__info">
<Wrapper
@@ -273,7 +321,13 @@ export default class LayerEditor extends React.Component {
</div>
</header>
{groups}
<Accordion
allowMultipleExpanded={true}
allowZeroExpanded={true}
preExpanded={groupIds}
>
{groups}
</Accordion>
</div>
}
}

View File

@@ -1,11 +1,21 @@
import React from 'react'
import PropTypes from 'prop-types'
import Collapser from './Collapser'
import Collapse from './Collapse'
import Icon from '@mdi/react'
import {
mdiMenuDown,
mdiMenuUp
} from '@mdi/js';
import {
AccordionItem,
AccordionItemHeading,
AccordionItemButton,
AccordionItemPanel,
} from 'react-accessible-accordion';
export default class LayerEditorGroup extends React.Component {
static propTypes = {
"id": PropTypes.string,
"data-wd-key": PropTypes.string,
title: PropTypes.string.isRequired,
isActive: PropTypes.bool.isRequired,
@@ -14,20 +24,28 @@ export default class LayerEditorGroup extends React.Component {
}
render() {
return <div>
<div className="maputnik-layer-editor-group"
return <AccordionItem uuid={this.props.id}>
<AccordionItemHeading className="maputnik-layer-editor-group"
data-wd-key={"layer-editor-group:"+this.props["data-wd-key"]}
onClick={e => this.props.onActiveToggle(!this.props.isActive)}
>
<span>{this.props.title}</span>
<span style={{flexGrow: 1}} />
<Collapser isCollapsed={this.props.isActive} />
</div>
<Collapse isActive={this.props.isActive}>
<div className="react-collapse-container">
{this.props.children}
</div>
</Collapse>
</div>
<AccordionItemButton className="maputnik-layer-editor-group__button">
<span style={{flexGrow: 1}}>{this.props.title}</span>
<Icon
path={mdiMenuUp}
size={1}
className="maputnik-layer-editor-group__button__icon maputnik-layer-editor-group__button__icon--up"
/>
<Icon
path={mdiMenuDown}
size={1}
className="maputnik-layer-editor-group__button__icon maputnik-layer-editor-group__button__icon--down"
/>
</AccordionItemButton>
</AccordionItemHeading>
<AccordionItemPanel>
{this.props.children}
</AccordionItemPanel>
</AccordionItem>
}
}

View File

@@ -10,11 +10,13 @@ class LayerIdBlock extends React.Component {
value: PropTypes.string.isRequired,
wdKey: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired,
error: PropTypes.object,
}
render() {
return <InputBlock label={"ID"} doc={latest.layer.id.doc}
return <InputBlock label={"ID"} fieldSpec={latest.layer.id}
data-wd-key={this.props.wdKey}
error={this.props.error}
>
<StringInput
value={this.props.value}

View File

@@ -1,6 +1,7 @@
import React from 'react'
import PropTypes from 'prop-types'
import classnames from 'classnames'
import lodash from 'lodash';
import LayerListGroup from './LayerListGroup'
import LayerListItem from './LayerListItem'
@@ -41,11 +42,16 @@ class LayerListContainer extends React.Component {
onLayerSelect: () => {},
}
state = {
collapsedGroups: {},
areAllGroupsExpanded: false,
isOpen: {
add: false,
constructor(props) {
super(props);
this.selectedItemRef = React.createRef();
this.scrollContainerRef = React.createRef();
this.state = {
collapsedGroups: {},
areAllGroupsExpanded: false,
isOpen: {
add: false,
}
}
}
@@ -116,16 +122,84 @@ class LayerListContainer extends React.Component {
return collapsed === undefined ? true : collapsed
}
shouldComponentUpdate (nextProps, nextState) {
// Always update on state change
if (this.state !== nextState) {
return true;
}
// This component tree only requires id and visibility from the layers
// objects
function getRequiredProps (layer) {
const out = {
id: layer.id,
};
if (layer.layout) {
out.layout = {
visibility: layer.layout.visibility
};
}
return out;
}
const layersEqual = lodash.isEqual(
nextProps.layers.map(getRequiredProps),
this.props.layers.map(getRequiredProps),
);
function withoutLayers (props) {
const out = {
...props
};
delete out['layers'];
return out;
}
// Compare the props without layers because we've already compared them
// efficiently above.
const propsEqual = lodash.isEqual(
withoutLayers(this.props),
withoutLayers(nextProps)
);
const propsChanged = !(layersEqual && propsEqual);
return propsChanged;
}
componentDidUpdate (prevProps) {
if (prevProps.selectedLayerIndex !== this.props.selectedLayerIndex) {
const selectedItemNode = this.selectedItemRef.current;
if (selectedItemNode && selectedItemNode.node) {
const target = selectedItemNode.node;
const options = {
root: this.scrollContainerRef.current,
threshold: 1.0
}
const observer = new IntersectionObserver(entries => {
observer.unobserve(target);
if (entries.length > 0 && entries[0].intersectionRatio < 1) {
target.scrollIntoView();
}
}, options);
observer.observe(target);
}
}
}
render() {
const listItems = []
let idx = 0
this.groupedLayers().forEach(layers => {
const layerIdCount = new Map();
const layersByGroup = this.groupedLayers();
layersByGroup.forEach(layers => {
const groupPrefix = layerPrefix(layers[0].id)
if(layers.length > 1) {
const grp = <LayerListGroup
data-wd-key={[groupPrefix, idx].join('-')}
key={[groupPrefix, idx].join('-')}
key={`group-${groupPrefix}-${idx}`}
title={groupPrefix}
isActive={!this.isCollapsed(groupPrefix, idx) || idx === this.props.selectedLayerIndex}
onActiveToggle={this.toggleLayerGroup.bind(this, groupPrefix, idx)}
@@ -136,14 +210,33 @@ class LayerListContainer extends React.Component {
layers.forEach((layer, idxInGroup) => {
const groupIdx = findClosestCommonPrefix(this.props.layers, idx)
const layerError = this.props.errors.find(error => {
return (
error.parsed &&
error.parsed.type === "layer" &&
error.parsed.data.index == idx
);
});
const additionalProps = {};
if (idx === this.props.selectedLayerIndex) {
additionalProps.ref = this.selectedItemRef;
}
layerIdCount.set(layer.id,
layerIdCount.has(layer.id) ? layerIdCount.get(layer.id) + 1 : 0
);
const key = `${layer.id}-${layerIdCount.get(layer.id)}`;
const listItem = <LayerListItem
className={classnames({
'maputnik-layer-list-item-collapsed': layers.length > 1 && this.isCollapsed(groupPrefix, groupIdx) && idx !== this.props.selectedLayerIndex,
'maputnik-layer-list-item-group-last': idxInGroup == layers.length - 1 && layers.length > 1
'maputnik-layer-list-item-group-last': idxInGroup == layers.length - 1 && layers.length > 1,
'maputnik-layer-list-item--error': !!layerError
})}
index={idx}
key={layer.id}
key={key}
layerId={layer.id}
layerIndex={idx}
layerType={layer.type}
visibility={(layer.layout || {}).visibility}
isSelected={idx === this.props.selectedLayerIndex}
@@ -151,13 +244,14 @@ class LayerListContainer extends React.Component {
onLayerDestroy={this.props.onLayerDestroy.bind(this)}
onLayerCopy={this.props.onLayerCopy.bind(this)}
onLayerVisibilityToggle={this.props.onLayerVisibilityToggle.bind(this)}
{...additionalProps}
/>
listItems.push(listItem)
idx += 1
})
})
return <div className="maputnik-layer-list">
return <div className="maputnik-layer-list" ref={this.scrollContainerRef}>
<AddModal
layers={this.props.layers}
sources={this.props.sources}

View File

@@ -62,6 +62,7 @@ class IconAction extends React.Component {
class LayerListItem extends React.Component {
static propTypes = {
layerIndex: PropTypes.number.isRequired,
layerId: PropTypes.string.isRequired,
layerType: PropTypes.string.isRequired,
isSelected: PropTypes.bool,
@@ -97,7 +98,7 @@ class LayerListItem extends React.Component {
return <li
key={this.props.layerId}
onClick={e => this.props.onLayerSelect(this.props.layerId)}
onClick={e => this.props.onLayerSelect(this.props.layerIndex)}
data-wd-key={"layer-list-item:"+this.props.layerId}
className={classnames({
"maputnik-layer-list-item": true,
@@ -110,20 +111,20 @@ class LayerListItem extends React.Component {
wdKey={"layer-list-item:"+this.props.layerId+":delete"}
action={'delete'}
classBlockName="delete"
onClick={e => this.props.onLayerDestroy(this.props.layerId)}
onClick={e => this.props.onLayerDestroy(this.props.layerIndex)}
/>
<IconAction
wdKey={"layer-list-item:"+this.props.layerId+":copy"}
action={'duplicate'}
classBlockName="duplicate"
onClick={e => this.props.onLayerCopy(this.props.layerId)}
onClick={e => this.props.onLayerCopy(this.props.layerIndex)}
/>
<IconAction
wdKey={"layer-list-item:"+this.props.layerId+":toggle-visibility"}
action={visibilityAction}
classBlockName="visibility"
classBlockModifier={visibilityAction}
onClick={e => this.props.onLayerVisibilityToggle(this.props.layerId)}
onClick={e => this.props.onLayerVisibilityToggle(this.props.layerIndex)}
/>
</li>
}

View File

@@ -11,6 +11,7 @@ class LayerSourceBlock extends React.Component {
wdKey: PropTypes.string,
onChange: PropTypes.func,
sourceIds: PropTypes.array,
error: PropTypes.object,
}
static defaultProps = {
@@ -19,7 +20,10 @@ class LayerSourceBlock extends React.Component {
}
render() {
return <InputBlock label={"Source"} doc={latest.layer.source.doc}
return <InputBlock
label={"Source"}
fieldSpec={latest.layer.source}
error={this.props.error}
data-wd-key={this.props.wdKey}
>
<AutocompleteInput

View File

@@ -20,7 +20,7 @@ class LayerSourceLayer extends React.Component {
}
render() {
return <InputBlock label={"Source Layer"} doc={latest.layer['source-layer'].doc}
return <InputBlock label={"Source Layer"} fieldSpec={latest.layer['source-layer']}
data-wd-key="layer-source-layer"
>
<AutocompleteInput

View File

@@ -4,33 +4,49 @@ import PropTypes from 'prop-types'
import {latest} from '@mapbox/mapbox-gl-style-spec'
import InputBlock from '../inputs/InputBlock'
import SelectInput from '../inputs/SelectInput'
import StringInput from '../inputs/StringInput'
class LayerTypeBlock extends React.Component {
static propTypes = {
value: PropTypes.string.isRequired,
wdKey: PropTypes.string,
onChange: PropTypes.func.isRequired,
error: PropTypes.object,
disabled: PropTypes.bool,
}
static defaultProps = {
disabled: false,
}
render() {
return <InputBlock label={"Type"} doc={latest.layer.type.doc}
return <InputBlock label={"Type"} fieldSpec={latest.layer.type}
data-wd-key={this.props.wdKey}
error={this.props.error}
>
<SelectInput
options={[
['background', 'Background'],
['fill', 'Fill'],
['line', 'Line'],
['symbol', 'Symbol'],
['raster', 'Raster'],
['circle', 'Circle'],
['fill-extrusion', 'Fill Extrusion'],
['hillshade', 'Hillshade'],
['heatmap', 'Heatmap'],
]}
onChange={this.props.onChange}
value={this.props.value}
/>
{this.props.disabled &&
<StringInput
value={this.props.value}
disabled={true}
/>
}
{!this.props.disabled &&
<SelectInput
options={[
['background', 'Background'],
['fill', 'Fill'],
['line', 'Line'],
['symbol', 'Symbol'],
['raster', 'Raster'],
['circle', 'Circle'],
['fill-extrusion', 'Fill Extrusion'],
['hillshade', 'Hillshade'],
['heatmap', 'Heatmap'],
]}
onChange={this.props.onChange}
value={this.props.value}
/>
}
</InputBlock>
}
}

View File

@@ -9,13 +9,16 @@ class MaxZoomBlock extends React.Component {
static propTypes = {
value: PropTypes.number,
onChange: PropTypes.func.isRequired,
error: PropTypes.object,
}
render() {
return <InputBlock label={"Max Zoom"} doc={latest.layer.maxzoom.doc}
return <InputBlock label={"Max Zoom"} fieldSpec={latest.layer.maxzoom}
error={this.props.error}
data-wd-key="max-zoom"
>
<NumberInput
allowRange={true}
value={this.props.value}
onChange={this.props.onChange}
min={latest.layer.maxzoom.minimum}

View File

@@ -9,13 +9,16 @@ class MinZoomBlock extends React.Component {
static propTypes = {
value: PropTypes.number,
onChange: PropTypes.func.isRequired,
error: PropTypes.object,
}
render() {
return <InputBlock label={"Min Zoom"} doc={latest.layer.minzoom.doc}
return <InputBlock label={"Min Zoom"} fieldSpec={latest.layer.minzoom}
error={this.props.error}
data-wd-key="min-zoom"
>
<NumberInput
allowRange={true}
value={this.props.value}
onChange={this.props.onChange}
min={latest.layer.minzoom.minimum}

View File

@@ -58,23 +58,8 @@ class FeatureLayerPopup extends React.Component {
if(propName) {
const propertySpec = latest["paint_"+feature.layer.type][propName];
let color = feature.layer.paint[propName];
if(typeof(color) === "object") {
if(color.stops) {
color = styleFunction.convertFunction(color, propertySpec);
}
const exprResult = expression.createExpression(color, propertySpec);
const val = exprResult.value.evaluate({
zoom: zoom
}, feature);
return val.toString();
}
else {
return color;
}
return String(color);
}
else {
// Default color
@@ -84,7 +69,7 @@ class FeatureLayerPopup extends React.Component {
// This is quite complex, just incase there's an edgecase we're missing
// always return black if we get an unexpected error.
catch (err) {
console.error("Unable to get feature color, error:", err);
console.warn("Unable to get feature color, error:", err);
return "black";
}
}

View File

@@ -21,12 +21,19 @@ function renderProperties(feature) {
})
}
function renderFeature(feature) {
return <div key={`${feature.sourceLayer}-${feature.id}`}>
function renderFeatureId(feature) {
return <InputBlock key={"feature-id"} label={"feature_id"}>
<StringInput value={displayValue(feature.id)} style={{backgroundColor: 'transparent'}} />
</InputBlock>
}
function renderFeature(feature, idx) {
return <div key={`${feature.sourceLayer}-${idx}`}>
<div className="maputnik-popup-layer-id">{feature.layer['source-layer']}{feature.inspectModeCounter && <span> × {feature.inspectModeCounter}</span>}</div>
<InputBlock key={"property-type"} label={"$type"}>
<StringInput value={feature.geometry.type} style={{backgroundColor: 'transparent'}} />
</InputBlock>
{renderFeatureId(feature)}
{renderProperties(feature)}
</div>
}
@@ -36,7 +43,7 @@ function removeDuplicatedFeatures(features) {
features.forEach(feature => {
const featureIndex = uniqueFeatures.findIndex(feature2 => {
return feature.layer['source-layer'] === feature2.layer['source-layer']
return feature.layer['source-layer'] === feature2.layer['source-layer']
&& JSON.stringify(feature.properties) === JSON.stringify(feature2.properties)
})

View File

@@ -19,8 +19,7 @@ const IS_SUPPORTED = MapboxGl.supported();
function renderPopup(popup, mountNode) {
ReactDOM.render(popup, mountNode);
var content = mountNode.innerHTML;
return content;
return mountNode;
}
function buildInspectStyle(originalMapStyle, coloredLayers, highlightedLayer) {
@@ -61,12 +60,15 @@ export default class MapboxGlMap extends React.Component {
inspectModeEnabled: PropTypes.bool.isRequired,
highlightedLayer: PropTypes.object,
options: PropTypes.object,
replaceAccessTokens: PropTypes.func.isRequired,
onChange: PropTypes.func.isRequired,
}
static defaultProps = {
onMapLoaded: () => {},
onDataChange: () => {},
onLayerSelect: () => {},
onChange: () => {},
mapboxAccessToken: tokens.mapbox,
options: {},
}
@@ -87,21 +89,12 @@ export default class MapboxGlMap extends React.Component {
const metadata = props.mapStyle.metadata || {}
MapboxGl.accessToken = metadata['maputnik:mapbox_access_token'] || tokens.mapbox
if(!props.inspectModeEnabled) {
//Mapbox GL now does diffing natively so we don't need to calculate
//the necessary operations ourselves!
this.state.map.setStyle(props.mapStyle, { diff: true})
}
}
shouldComponentUpdate(nextProps, nextState) {
let should = false;
try {
should = JSON.stringify(this.props) !== JSON.stringify(nextProps) || JSON.stringify(this.state) !== JSON.stringify(nextState);
} catch(e) {
// no biggie, carry on
}
return should;
//Mapbox GL now does diffing natively so we don't need to calculate
//the necessary operations ourselves!
this.state.map.setStyle(
this.props.replaceAccessTokens(props.mapStyle),
{diff: true}
)
}
componentDidUpdate(prevProps) {
@@ -111,14 +104,24 @@ export default class MapboxGlMap extends React.Component {
this.updateMapFromProps(this.props);
if(this.props.inspectModeEnabled !== prevProps.inspectModeEnabled) {
if(this.state.inspect && this.props.inspectModeEnabled !== this.state.inspect._showInspectMap) {
// HACK: Fix for <https://github.com/maputnik/editor/issues/576>, while we wait for a proper fix.
// eslint-disable-next-line
this.state.inspect._popupBlocked = false;
this.state.inspect.toggleInspector()
}
if(this.props.inspectModeEnabled) {
this.state.inspect.render()
}
if (map) {
if (this.props.inspectModeEnabled) {
// HACK: We need to work out why we need to do this and what's causing
// this error. I'm assuming an issue with mapbox-gl update and
// mapbox-gl-inspect.
try {
this.state.inspect.render();
} catch(err) {
console.error("FIXME: Caught error", err);
}
}
map.showTileBoundaries = this.props.options.showTileBoundaries;
map.showCollisionBoxes = this.props.options.showCollisionBoxes;
map.showOverdrawInspector = this.props.options.showOverdrawInspector;
@@ -133,16 +136,24 @@ export default class MapboxGlMap extends React.Component {
container: this.container,
style: this.props.mapStyle,
hash: true,
maxZoom: 24
}
const map = new MapboxGl.Map(mapOpts);
const mapViewChange = () => {
const center = map.getCenter();
const zoom = map.getZoom();
this.props.onChange({center, zoom});
}
mapViewChange();
map.showTileBoundaries = mapOpts.showTileBoundaries;
map.showCollisionBoxes = mapOpts.showCollisionBoxes;
map.showOverdrawInspector = mapOpts.showOverdrawInspector;
const zoom = new ZoomControl;
map.addControl(zoom, 'top-right');
const zoomControl = new ZoomControl;
map.addControl(zoomControl, 'top-right');
const nav = new MapboxGl.NavigationControl({visualizePitch:true});
map.addControl(nav, 'top-right');
@@ -166,7 +177,7 @@ export default class MapboxGlMap extends React.Component {
if(this.props.inspectModeEnabled) {
return renderPopup(<FeaturePropertyPopup features={features} />, tmpNode);
} else {
return renderPopup(<FeatureLayerPopup features={features} onLayerSelect={this.props.onLayerSelect} zoom={this.state.zoom} />, tmpNode);
return renderPopup(<FeatureLayerPopup features={features} onLayerSelect={this.onLayerSelectById} zoom={this.state.zoom} />, tmpNode);
}
}
})
@@ -178,9 +189,6 @@ export default class MapboxGlMap extends React.Component {
inspect,
zoom: map.getZoom()
});
if(this.props.inspectModeEnabled) {
inspect.toggleInspector();
}
})
map.on("data", e => {
@@ -190,11 +198,23 @@ export default class MapboxGlMap extends React.Component {
})
})
map.on("error", e => {
console.log("ERROR", e);
})
map.on("zoom", e => {
this.setState({
zoom: map.getZoom()
});
})
});
map.on("dragend", mapViewChange);
map.on("zoomend", mapViewChange);
}
onLayerSelectById = (id) => {
const index = this.props.mapStyle.layers.findIndex(layer => layer.id === id);
this.props.onLayerSelect(index);
}
render() {

View File

@@ -32,6 +32,8 @@ export default class OpenLayersMap extends React.Component {
style: PropTypes.object,
onLayerSelect: PropTypes.func.isRequired,
debugToolbox: PropTypes.bool.isRequired,
replaceAccessTokens: PropTypes.func.isRequired,
onChange: PropTypes.func.isRequired,
}
static defaultProps = {
@@ -61,7 +63,9 @@ export default class OpenLayersMap extends React.Component {
componentDidUpdate(prevProps) {
if (this.props.mapStyle !== prevProps.mapStyle) {
this.updateStyle(this.props.mapStyle);
this.updateStyle(
this.props.replaceAccessTokens(this.props.mapStyle)
);
}
}
@@ -93,6 +97,22 @@ export default class OpenLayersMap extends React.Component {
})
})
const onMoveEnd = () => {
const zoom = map.getView().getZoom();
const center = toLonLat(map.getView().getCenter());
this.props.onChange({
zoom,
center: {
lng: center[0],
lat: center[1],
},
});
}
onMoveEnd();
map.on('moveend', onMoveEnd);
map.on('postrender', (evt) => {
const center = toLonLat(map.getView().getCenter());
this.setState({
@@ -108,7 +128,9 @@ export default class OpenLayersMap extends React.Component {
this.map = map;
this.updateStyle(this.props.mapStyle);
this.updateStyle(
this.props.replaceAccessTokens(this.props.mapStyle)
);
}
closeOverlay = (e) => {
@@ -136,7 +158,7 @@ export default class OpenLayersMap extends React.Component {
/>
</div>
<div className="maputnik-ol-zoom">
Zoom level: {this.state.zoom}
Zoom: {this.state.zoom}
</div>
{this.props.debugToolbox &&
<div className="maputnik-ol-debug">

View File

@@ -126,6 +126,7 @@ class AddModal extends React.Component {
onOpenToggle={this.props.onOpenToggle}
title={'Add Layer'}
data-wd-key="modal:add-layer"
className="maputnik-add-modal"
>
<div className="maputnik-add-layer">
<LayerIdBlock

View File

@@ -13,9 +13,16 @@ class DebugModal extends React.Component {
onOpenToggle: PropTypes.func.isRequired,
mapboxGlDebugOptions: PropTypes.object,
openlayersDebugOptions: PropTypes.object,
mapView: PropTypes.object,
}
render() {
const {mapView} = this.props;
const osmZoom = Math.round(mapView.zoom)+1;
const osmLon = Number.parseFloat(mapView.center.lng).toFixed(5);
const osmLat = Number.parseFloat(mapView.center.lat).toFixed(5);
return <Modal
data-wd-key="debug-modal"
isOpen={this.props.isOpen}
@@ -23,6 +30,7 @@ class DebugModal extends React.Component {
title={'Debug'}
>
<div className="maputnik-modal-section maputnik-modal-shortcuts">
<h4>Options</h4>
{this.props.renderer === 'mbgljs' &&
<ul>
{Object.entries(this.props.mapboxGlDebugOptions).map(([key, val]) => {
@@ -46,6 +54,18 @@ class DebugModal extends React.Component {
</ul>
}
</div>
<div className="maputnik-modal-section">
<h4>Links</h4>
<p>
<a
target="_blank"
rel="noopener noreferrer"
href={`https://www.openstreetmap.org/#map=${osmZoom}/${osmLat}/${osmLon}`}
>
Open in OSM
</a> &mdash; Opens the current view on openstreetmap.org
</p>
</div>
</Modal>
}
}

View File

@@ -11,19 +11,10 @@ import Button from '../Button'
import Modal from './Modal'
import {MdFileDownload} from 'react-icons/md'
import style from '../../libs/style'
import fieldSpecAdditional from '../../libs/field-spec-additional'
function stripAccessTokens(mapStyle) {
const changedMetadata = { ...mapStyle.metadata }
delete changedMetadata['maputnik:mapbox_access_token']
delete changedMetadata['maputnik:openmaptiles_access_token']
return {
...mapStyle,
metadata: changedMetadata
}
}
class ExportModal extends React.Component {
static propTypes = {
mapStyle: PropTypes.object.isRequired,
@@ -37,7 +28,11 @@ class ExportModal extends React.Component {
}
downloadStyle() {
const tokenStyle = format(stripAccessTokens(style.replaceAccessTokens(this.props.mapStyle)));
const tokenStyle = format(
style.stripAccessTokens(
style.replaceAccessTokens(this.props.mapStyle)
)
);
const blob = new Blob([tokenStyle], {type: "application/json;charset=utf-8"});
let exportName;
@@ -70,6 +65,7 @@ class ExportModal extends React.Component {
isOpen={this.props.isOpen}
onOpenToggle={this.props.onOpenToggle}
title={'Export Style'}
className="maputnik-export-modal"
>
<div className="maputnik-modal-section">
@@ -78,26 +74,35 @@ class ExportModal extends React.Component {
Download a JSON style to your computer.
</p>
<p>
<InputBlock label={"MapTiler Access Token: "}>
<div>
<InputBlock
label={fieldSpecAdditional.maputnik.mapbox_access_token.label}
fieldSpec={fieldSpecAdditional.maputnik.mapbox_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: "}>
<InputBlock
label={fieldSpecAdditional.maputnik.maptiler_access_token.label}
fieldSpec={fieldSpecAdditional.maputnik.maptiler_access_token}
>
<StringInput
value={(this.props.mapStyle.metadata || {})['maputnik:mapbox_access_token']}
onChange={this.changeMetadataProperty.bind(this, "maputnik:mapbox_access_token")}
/>
</InputBlock>
<InputBlock label={"Thunderforest Access Token: "}>
<InputBlock
label={fieldSpecAdditional.maputnik.thunderforest_access_token.label}
fieldSpec={fieldSpecAdditional.maputnik.thunderforest_access_token}
>
<StringInput
value={(this.props.mapStyle.metadata || {})['maputnik:thunderforest_access_token']}
onChange={this.changeMetadataProperty.bind(this, "maputnik:thunderforest_access_token")}
/>
</InputBlock>
</p>
</div>
<Button onClick={this.downloadStyle.bind(this)}>
<MdFileDownload />

View File

@@ -2,6 +2,7 @@ import React from 'react'
import PropTypes from 'prop-types'
import {MdClose} from 'react-icons/md'
import AriaModal from 'react-aria-modal'
import classnames from 'classnames';
class Modal extends React.Component {
@@ -13,6 +14,7 @@ class Modal extends React.Component {
children: PropTypes.node,
underlayClickExits: PropTypes.bool,
underlayProps: PropTypes.object,
className: PropTypes.string,
}
static defaultProps = {
@@ -45,7 +47,7 @@ class Modal extends React.Component {
verticallyCenter={true}
onExit={this.onClose}
>
<div className="maputnik-modal"
<div className={classnames("maputnik-modal", this.props.className)}
data-wd-key={this.props["data-wd-key"]}
>
<header className="maputnik-modal-header">

View File

@@ -4,6 +4,7 @@ import LoadingModal from './LoadingModal'
import Modal from './Modal'
import Button from '../Button'
import FileReaderInput from 'react-file-reader-input'
import UrlInput from '../inputs/UrlInput'
import {MdFileUpload} from 'react-icons/md'
import {MdAddCircleOutline} from 'react-icons/md'
@@ -122,9 +123,8 @@ class OpenModal extends React.Component {
})
}
onOpenUrl = () => {
const url = this.styleUrlElement.value;
this.onStyleSelect(url);
onOpenUrl = (url) => {
this.onStyleSelect(this.state.styleUrl);
}
onUpload = (_, files) => {
@@ -160,9 +160,9 @@ class OpenModal extends React.Component {
this.props.onOpenToggle();
}
onChangeUrl = () => {
onChangeUrl = (url) => {
this.setState({
styleUrl: this.styleUrlElement.value
styleUrl: url,
});
}
@@ -209,14 +209,13 @@ class OpenModal extends React.Component {
<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>.
</p>
<input
<UrlInput
data-wd-key="open-modal.url.input"
type="text"
ref={(input) => this.styleUrlElement = input}
className="maputnik-input"
placeholder="Enter URL..."
default="Enter URL..."
value={this.state.styleUrl}
onChange={this.onChangeUrl}
onInput={this.onChangeUrl}
/>
<div>
<Button

View File

@@ -3,9 +3,15 @@ import PropTypes from 'prop-types'
import {latest} from '@mapbox/mapbox-gl-style-spec'
import InputBlock from '../inputs/InputBlock'
import ArrayInput from '../inputs/ArrayInput'
import NumberInput from '../inputs/NumberInput'
import StringInput from '../inputs/StringInput'
import UrlInput from '../inputs/UrlInput'
import SelectInput from '../inputs/SelectInput'
import EnumInput from '../inputs/EnumInput'
import ColorField from '../fields/ColorField'
import Modal from './Modal'
import fieldSpecAdditional from '../../libs/field-spec-additional'
class SettingsModal extends React.Component {
static propTypes = {
@@ -16,56 +22,105 @@ class SettingsModal extends React.Component {
onOpenToggle: PropTypes.func.isRequired,
}
changeTransitionProperty(property, value) {
const transition = {
...this.props.mapStyle.transition,
}
if (value === undefined) {
delete transition[property];
}
else {
transition[property] = value;
}
this.props.onStyleChanged({
...this.props.mapStyle,
transition,
});
}
changeLightProperty(property, value) {
const light = {
...this.props.mapStyle.light,
}
if (value === undefined) {
delete light[property];
}
else {
light[property] = value;
}
this.props.onStyleChanged({
...this.props.mapStyle,
light,
});
}
changeStyleProperty(property, value) {
const changedStyle = {
...this.props.mapStyle,
[property]: value
};
if (value === undefined) {
delete changedStyle[property];
}
this.props.onStyleChanged(changedStyle)
else {
changedStyle[property] = value;
}
this.props.onStyleChanged(changedStyle);
}
render() {
const metadata = this.props.mapStyle.metadata || {}
const {onChangeMetadataProperty} = this.props;
const {onChangeMetadataProperty, mapStyle} = this.props;
const inputProps = { }
const light = this.props.mapStyle.light || {};
const transition = this.props.mapStyle.transition || {};
return <Modal
data-wd-key="modal-settings"
isOpen={this.props.isOpen}
onOpenToggle={this.props.onOpenToggle}
title={'Style Settings'}
>
<div style={{minWidth: 350}}>
<InputBlock label={"Name"} doc={latest.$root.name.doc}>
<div className="modal-settings">
<InputBlock label={"Name"} fieldSpec={latest.$root.name}>
<StringInput {...inputProps}
data-wd-key="modal-settings.name"
value={this.props.mapStyle.name}
onChange={this.changeStyleProperty.bind(this, "name")}
/>
</InputBlock>
<InputBlock label={"Owner"} doc={"Owner ID of the style. Used by Mapbox or future style APIs."}>
<InputBlock label={"Owner"} fieldSpec={{doc: "Owner ID of the style. Used by Mapbox or future style APIs."}}>
<StringInput {...inputProps}
data-wd-key="modal-settings.owner"
value={this.props.mapStyle.owner}
onChange={this.changeStyleProperty.bind(this, "owner")}
/>
</InputBlock>
<InputBlock label={"Sprite URL"} doc={latest.$root.sprite.doc}>
<StringInput {...inputProps}
<InputBlock label={"Sprite URL"} fieldSpec={latest.$root.sprite}>
<UrlInput {...inputProps}
data-wd-key="modal-settings.sprite"
value={this.props.mapStyle.sprite}
onChange={this.changeStyleProperty.bind(this, "sprite")}
/>
</InputBlock>
<InputBlock label={"Glyphs URL"} doc={latest.$root.glyphs.doc}>
<StringInput {...inputProps}
<InputBlock label={"Glyphs URL"} fieldSpec={latest.$root.glyphs}>
<UrlInput {...inputProps}
data-wd-key="modal-settings.glyphs"
value={this.props.mapStyle.glyphs}
onChange={this.changeStyleProperty.bind(this, "glyphs")}
/>
</InputBlock>
<InputBlock label={"Mapbox Access Token"} doc={"Public access token for Mapbox services."}>
<InputBlock
label={fieldSpecAdditional.maputnik.mapbox_access_token.label}
fieldSpec={fieldSpecAdditional.maputnik.mapbox_access_token}
>
<StringInput {...inputProps}
data-wd-key="modal-settings.maputnik:mapbox_access_token"
value={metadata['maputnik:mapbox_access_token']}
@@ -73,7 +128,10 @@ class SettingsModal extends React.Component {
/>
</InputBlock>
<InputBlock label={"MapTiler Access Token"} doc={"Public access token for MapTiler Cloud."}>
<InputBlock
label={fieldSpecAdditional.maputnik.maptiler_access_token.label}
fieldSpec={fieldSpecAdditional.maputnik.maptiler_access_token}
>
<StringInput {...inputProps}
data-wd-key="modal-settings.maputnik:openmaptiles_access_token"
value={metadata['maputnik:openmaptiles_access_token']}
@@ -81,7 +139,10 @@ class SettingsModal extends React.Component {
/>
</InputBlock>
<InputBlock label={"Thunderforest Access Token"} doc={"Public access token for Thunderforest services."}>
<InputBlock
label={fieldSpecAdditional.maputnik.thunderforest_access_token.label}
fieldSpec={fieldSpecAdditional.maputnik.thunderforest_access_token}
>
<StringInput {...inputProps}
data-wd-key="modal-settings.maputnik:thunderforest_access_token"
value={metadata['maputnik:thunderforest_access_token']}
@@ -89,7 +150,104 @@ class SettingsModal extends React.Component {
/>
</InputBlock>
<InputBlock label={"Style Renderer"} doc={"Choose the default Maputnik renderer for this style."}>
<InputBlock label={"Center"} fieldSpec={latest.$root.center}>
<ArrayInput
length={2}
type="number"
value={mapStyle.center}
default={latest.$root.center.default || [0, 0]}
onChange={this.changeStyleProperty.bind(this, "center")}
/>
</InputBlock>
<InputBlock label={"Zoom"} fieldSpec={latest.$root.zoom}>
<NumberInput
{...inputProps}
value={mapStyle.zoom}
default={latest.$root.zoom.default || 0}
onChange={this.changeStyleProperty.bind(this, "zoom")}
/>
</InputBlock>
<InputBlock label={"Bearing"} fieldSpec={latest.$root.bearing}>
<NumberInput
{...inputProps}
value={mapStyle.bearing}
default={latest.$root.bearing.default}
onChange={this.changeStyleProperty.bind(this, "bearing")}
/>
</InputBlock>
<InputBlock label={"Pitch"} fieldSpec={latest.$root.pitch}>
<NumberInput
{...inputProps}
value={mapStyle.pitch}
default={latest.$root.pitch.default}
onChange={this.changeStyleProperty.bind(this, "pitch")}
/>
</InputBlock>
<InputBlock label={"Light anchor"} fieldSpec={latest.light.anchor}>
<EnumInput
{...inputProps}
value={light.anchor}
options={Object.keys(latest.light.anchor.values)}
default={latest.light.anchor.default}
onChange={this.changeLightProperty.bind(this, "anchor")}
/>
</InputBlock>
<InputBlock label={"Light color"} fieldSpec={latest.light.color}>
<ColorField
{...inputProps}
value={light.color}
default={latest.light.color.default}
onChange={this.changeLightProperty.bind(this, "color")}
/>
</InputBlock>
<InputBlock label={"Light intensity"} fieldSpec={latest.light.intensity}>
<NumberInput
{...inputProps}
value={light.intensity}
default={latest.light.intensity.default}
onChange={this.changeLightProperty.bind(this, "intensity")}
/>
</InputBlock>
<InputBlock label={"Light position"} fieldSpec={latest.light.position}>
<ArrayInput
{...inputProps}
type="number"
length={latest.light.position.length}
value={light.position}
default={latest.light.position.default}
onChange={this.changeLightProperty.bind(this, "position")}
/>
</InputBlock>
<InputBlock label={"Transition delay"} fieldSpec={latest.transition.delay}>
<NumberInput
{...inputProps}
value={transition.delay}
default={latest.transition.delay.default}
onChange={this.changeTransitionProperty.bind(this, "delay")}
/>
</InputBlock>
<InputBlock label={"Transition duration"} fieldSpec={latest.transition.duration}>
<NumberInput
{...inputProps}
value={transition.duration}
default={latest.transition.duration.default}
onChange={this.changeTransitionProperty.bind(this, "duration")}
/>
</InputBlock>
<InputBlock
label={fieldSpecAdditional.maputnik.style_renderer.label}
fieldSpec={fieldSpecAdditional.maputnik.style_renderer}
>
<SelectInput {...inputProps}
data-wd-key="modal-settings.maputnik:renderer"
options={[
@@ -101,6 +259,8 @@ class SettingsModal extends React.Component {
/>
</InputBlock>
</div>
</Modal>
}

View File

@@ -52,7 +52,20 @@ function editorMode(source) {
if(source.tiles) return 'tilexyz_vector'
return 'tilejson_vector'
}
if(source.type === 'geojson') return 'geojson'
if(source.type === 'geojson') {
if (typeof(source.data) === "string") {
return 'geojson_url';
}
else {
return 'geojson_json';
}
}
if(source.type === 'image') {
return 'image';
}
if(source.type === 'video') {
return 'video';
}
return null
}
@@ -105,76 +118,128 @@ class AddSource extends React.Component {
defaultSource(mode) {
const source = (this.state || {}).source || {}
const {protocol} = window.location;
switch(mode) {
case 'geojson': return {
case 'geojson_url': return {
type: 'geojson',
data: source.data || 'http://localhost:3000/geojson.json'
data: `${protocol}//localhost:3000/geojson.json`
}
case 'geojson_json': return {
type: 'geojson',
data: {}
}
case 'tilejson_vector': return {
type: 'vector',
url: source.url || 'http://localhost:3000/tilejson.json'
url: source.url || `${protocol}//localhost:3000/tilejson.json`
}
case 'tilexyz_vector': return {
type: 'vector',
tiles: source.tiles || ['http://localhost:3000/{x}/{y}/{z}.pbf'],
tiles: source.tiles || [`${protocol}//localhost:3000/{x}/{y}/{z}.pbf`],
minZoom: source.minzoom || 0,
maxZoom: source.maxzoom || 14
}
case 'tilejson_raster': return {
type: 'raster',
url: source.url || 'http://localhost:3000/tilejson.json'
url: source.url || `${protocol}//localhost:3000/tilejson.json`
}
case 'tilexyz_raster': return {
type: 'raster',
tiles: source.tiles || ['http://localhost:3000/{x}/{y}/{z}.pbf'],
tiles: source.tiles || [`${protocol}//localhost:3000/{x}/{y}/{z}.pbf`],
minzoom: source.minzoom || 0,
maxzoom: source.maxzoom || 14
}
case 'tilejson_raster-dem': return {
type: 'raster-dem',
url: source.url || 'http://localhost:3000/tilejson.json'
url: source.url || `${protocol}//localhost:3000/tilejson.json`
}
case 'tilexyz_raster-dem': return {
type: 'raster-dem',
tiles: source.tiles || ['http://localhost:3000/{x}/{y}/{z}.pbf'],
tiles: source.tiles || [`${protocol}//localhost:3000/{x}/{y}/{z}.pbf`],
minzoom: source.minzoom || 0,
maxzoom: source.maxzoom || 14
}
case 'image': return {
type: 'image',
url: `${protocol}//localhost:3000/image.png`,
coordinates: [
[0,0],
[0,0],
[0,0],
[0,0],
],
}
case 'video': return {
type: 'video',
urls: [
`${protocol}//localhost:3000/movie.mp4`
],
coordinates: [
[0,0],
[0,0],
[0,0],
[0,0],
],
}
default: return {}
}
}
onAdd = () => {
const {source, sourceId} = this.state;
this.props.onAdd(sourceId, source);
}
onChangeSource = (source) => {
this.setState({source});
}
render() {
// Kind of a hack because the type changes, however maputnik has 1..n
// options per type, for example
//
// - 'geojson' - 'GeoJSON (URL)' and 'GeoJSON (JSON)'
// - 'raster' - 'Raster (TileJSON URL)' and 'Raster (XYZ URL)'
//
// So we just ignore the values entirely as they are self explanatory
const sourceTypeFieldSpec = {
doc: latest.source_vector.type.doc
};
return <div className="maputnik-add-source">
<InputBlock label={"Source ID"} doc={"Unique ID that identifies the source and is used in the layer to reference the source."}>
<InputBlock label={"Source ID"} fieldSpec={{doc: "Unique ID that identifies the source and is used in the layer to reference the source."}}>
<StringInput
value={this.state.sourceId}
onChange={v => this.setState({ sourceId: v})}
/>
</InputBlock>
<InputBlock label={"Source Type"} doc={latest.source_vector.type.doc}>
<InputBlock label={"Source Type"} fieldSpec={sourceTypeFieldSpec}>
<SelectInput
options={[
['geojson', 'GeoJSON'],
['geojson_json', 'GeoJSON (JSON)'],
['geojson_url', 'GeoJSON (URL)'],
['tilejson_vector', 'Vector (TileJSON URL)'],
['tilexyz_vector', 'Vector (XYZ URLs)'],
['tilejson_raster', 'Raster (TileJSON URL)'],
['tilexyz_raster', 'Raster (XYZ URL)'],
['tilejson_raster-dem', 'Raster DEM (TileJSON URL)'],
['tilexyz_raster-dem', 'Raster DEM (XYZ URLs)'],
['image', 'Image'],
['video', 'Video'],
]}
onChange={mode => this.setState({mode: mode, source: this.defaultSource(mode)})}
value={this.state.mode}
/>
</InputBlock>
<SourceTypeEditor
onChange={src => this.setState({ source: src })}
onChange={this.onChangeSource}
mode={this.state.mode}
source={this.state.source}
/>
<Button
className="maputnik-add-source-button"
onClick={() => this.props.onAdd(this.state.sourceId, this.state.source)}>
onClick={this.onAdd}
>
Add Source
</Button>
</div>
@@ -238,9 +303,6 @@ class SourcesModal extends React.Component {
<div className="maputnik-public-sources" style={{maxwidth: 500}}>
{tilesetOptions}
</div>
<p>
<strong>Note:</strong> Some of the tilesets are not optimised for online use, and as a result the file sizes of the tiles can be quite large (heavy) for online vector rendering. Please review any tilesets before use.
</p>
</div>
<div className="maputnik-modal-section">

View File

@@ -26,7 +26,7 @@ class SurveyModal extends React.Component {
title="Maputnik Survey"
>
<div className="maputnik-modal-survey">
<img className="maputnik-modal-survey__logo" src={logoImage} alt="" width="128" />
<div className="maputnik-modal-survey__logo" dangerouslySetInnerHTML={{__html: logoImage}} />
<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>

View File

@@ -3,8 +3,12 @@ import PropTypes from 'prop-types'
import {latest} from '@mapbox/mapbox-gl-style-spec'
import InputBlock from '../inputs/InputBlock'
import StringInput from '../inputs/StringInput'
import UrlInput from '../inputs/UrlInput'
import NumberInput from '../inputs/NumberInput'
import SelectInput from '../inputs/SelectInput'
import DynamicArrayInput from '../inputs/DynamicArrayInput'
import ArrayInput from '../inputs/ArrayInput'
import JSONEditor from '../layers/JSONEditor'
class TileJSONSourceEditor extends React.Component {
@@ -16,8 +20,8 @@ class TileJSONSourceEditor extends React.Component {
render() {
return <div>
<InputBlock label={"TileJSON URL"} doc={latest.source_vector.url.doc}>
<StringInput
<InputBlock label={"TileJSON URL"} fieldSpec={latest.source_vector.url}>
<UrlInput
value={this.props.source.url}
onChange={url => this.props.onChange({
...this.props.source,
@@ -50,8 +54,8 @@ class TileURLSourceEditor extends React.Component {
const prefix = ['1st', '2nd', '3rd', '4th', '5th', '6th', '7th']
const tiles = this.props.source.tiles || []
return tiles.map((tileUrl, tileIndex) => {
return <InputBlock key={tileIndex} label={prefix[tileIndex] + " Tile URL"} doc={latest.source_vector.tiles.doc}>
<StringInput
return <InputBlock key={tileIndex} label={prefix[tileIndex] + " Tile URL"} fieldSpec={latest.source_vector.tiles}>
<UrlInput
value={tileUrl}
onChange={this.changeTileUrl.bind(this, tileIndex)}
/>
@@ -62,7 +66,7 @@ class TileURLSourceEditor extends React.Component {
render() {
return <div>
{this.renderTileUrls()}
<InputBlock label={"Min Zoom"} doc={latest.source_vector.minzoom.doc}>
<InputBlock label={"Min Zoom"} fieldSpec={latest.source_vector.minzoom}>
<NumberInput
value={this.props.source.minzoom || 0}
onChange={minzoom => this.props.onChange({
@@ -71,7 +75,7 @@ class TileURLSourceEditor extends React.Component {
})}
/>
</InputBlock>
<InputBlock label={"Max Zoom"} doc={latest.source_vector.maxzoom.doc}>
<InputBlock label={"Max Zoom"} fieldSpec={latest.source_vector.maxzoom}>
<NumberInput
value={this.props.source.maxzoom || 22}
onChange={maxzoom => this.props.onChange({
@@ -86,15 +90,109 @@ class TileURLSourceEditor extends React.Component {
}
}
class GeoJSONSourceEditor extends React.Component {
class ImageSourceEditor extends React.Component {
static propTypes = {
source: PropTypes.object.isRequired,
onChange: PropTypes.func.isRequired,
}
render() {
return <InputBlock label={"GeoJSON Data"} doc={latest.source_geojson.data.doc}>
<StringInput
const changeCoord = (idx, val) => {
const coordinates = this.props.source.coordinates.slice(0);
coordinates[idx] = val;
this.props.onChange({
...this.props.source,
coordinates,
});
}
return <div>
<InputBlock label={"Image URL"} doc={latest.source_image.url.doc}>
<UrlInput
value={this.props.source.url}
onChange={url => this.props.onChange({
...this.props.source,
url,
})}
/>
</InputBlock>
{["top left", "top right", "bottom right", "bottom left"].map((label, idx) => {
return (
<InputBlock label={`Coord ${label}`} key={label}>
<ArrayInput
length={2}
type="number"
value={this.props.source.coordinates[idx]}
default={[0, 0]}
onChange={(val) => changeCoord(idx, val)}
/>
</InputBlock>
);
})}
</div>
}
}
class VideoSourceEditor extends React.Component {
static propTypes = {
source: PropTypes.object.isRequired,
onChange: PropTypes.func.isRequired,
}
render() {
const changeCoord = (idx, val) => {
const coordinates = this.props.source.coordinates.slice(0);
coordinates[idx] = val;
this.props.onChange({
...this.props.source,
coordinates,
});
}
const changeUrls = (urls) => {
this.props.onChange({
...this.props.source,
urls,
});
}
return <div>
<InputBlock label={"Video URL"} doc={latest.source_video.urls.doc}>
<DynamicArrayInput
type="string"
value={this.props.source.urls}
default={""}
onChange={changeUrls}
/>
</InputBlock>
{["top left", "top right", "bottom right", "bottom left"].map((label, idx) => {
return (
<InputBlock label={`Coord ${label}`} key={label}>
<ArrayInput
length={2}
type="number"
value={this.props.source.coordinates[idx]}
default={[0, 0]}
onChange={val => changeCoord(idx, val)}
/>
</InputBlock>
);
})}
</div>
}
}
class GeoJSONSourceUrlEditor extends React.Component {
static propTypes = {
source: PropTypes.object.isRequired,
onChange: PropTypes.func.isRequired,
}
render() {
return <InputBlock label={"GeoJSON URL"} fieldSpec={latest.source_geojson.data}>
<UrlInput
value={this.props.source.data}
onChange={data => this.props.onChange({
...this.props.source,
@@ -105,6 +203,33 @@ class GeoJSONSourceEditor extends React.Component {
}
}
class GeoJSONSourceJSONEditor extends React.Component {
static propTypes = {
source: PropTypes.object.isRequired,
onChange: PropTypes.func.isRequired,
}
render() {
return <InputBlock label={"GeoJSON"} fieldSpec={latest.source_geojson.data}>
<JSONEditor
layer={this.props.source.data}
maxHeight={200}
mode={{
name: "javascript",
json: true
}}
lint={true}
onChange={data => {
this.props.onChange({
...this.props.source,
data,
})
}}
/>
</InputBlock>
}
}
class SourceTypeEditor extends React.Component {
static propTypes = {
mode: PropTypes.string.isRequired,
@@ -118,14 +243,15 @@ class SourceTypeEditor extends React.Component {
onChange: this.props.onChange,
}
switch(this.props.mode) {
case 'geojson': return <GeoJSONSourceEditor {...commonProps} />
case 'geojson_url': return <GeoJSONSourceUrlEditor {...commonProps} />
case 'geojson_json': return <GeoJSONSourceJSONEditor {...commonProps} />
case 'tilejson_vector': return <TileJSONSourceEditor {...commonProps} />
case 'tilexyz_vector': return <TileURLSourceEditor {...commonProps} />
case 'tilejson_raster': return <TileJSONSourceEditor {...commonProps} />
case 'tilexyz_raster': return <TileURLSourceEditor {...commonProps} />
case 'tilejson_raster-dem': return <TileJSONSourceEditor {...commonProps} />
case 'tilexyz_raster-dem': return <TileURLSourceEditor {...commonProps}>
<InputBlock label={"Encoding"} doc={latest.source_raster_dem.encoding.doc}>
<InputBlock label={"Encoding"} fieldSpec={latest.source_raster_dem.encoding}>
<SelectInput
options={Object.keys(latest.source_raster_dem.encoding.values)}
onChange={encoding => this.props.onChange({
@@ -136,6 +262,8 @@ class SourceTypeEditor extends React.Component {
/>
</InputBlock>
</TileURLSourceEditor>
case 'image': return <ImageSourceEditor {...commonProps} />
case 'video': return <VideoSourceEditor {...commonProps} />
default: return null
}
}

View File

@@ -0,0 +1,20 @@
import React from 'react'
import PropTypes from 'prop-types'
import './SmallError.scss';
class SmallError extends React.Component {
static propTypes = {
children: PropTypes.node,
}
render () {
return (
<div className="SmallError">
Error: {this.props.children}
</div>
);
}
}
export default SmallError

View File

@@ -0,0 +1,7 @@
@import '../../styles/vars';
.SmallError {
color: #E57373;
font-size: $font-size-6;
margin-top: $margin-2
}

View File

@@ -0,0 +1,178 @@
import jsonlint from 'jsonlint';
import CodeMirror from 'codemirror';
import jsonToAst from 'json-to-ast';
import {expression, validate, latest} from '@mapbox/mapbox-gl-style-spec';
CodeMirror.defineMode("mgl", function(config, parserConfig) {
// Just using the javascript mode with json enabled. Our logic is in the linter below.
return CodeMirror.modes.javascript(
{...config, json: true},
parserConfig
);
});
CodeMirror.registerHelper("lint", "json", function(text) {
const found = [];
// NOTE: This was modified from the original to remove the global, also the
// old jsonlint API was 'jsonlint.parseError' its now
// 'jsonlint.parser.parseError'
jsonlint.parser.parseError = function(str, hash) {
const loc = hash.loc;
found.push({
from: CodeMirror.Pos(loc.first_line - 1, loc.first_column),
to: CodeMirror.Pos(loc.last_line - 1, loc.last_column),
message: str
});
};
try {
jsonlint.parse(text);
}
catch(e) {
// Do nothing we catch the error above
}
return found;
});
CodeMirror.registerHelper("lint", "mgl", function(text, opts, doc) {
const found = [];
const {parser} = jsonlint;
const {context} = opts;
parser.parseError = function(str, hash) {
const loc = hash.loc;
found.push({
from: CodeMirror.Pos(loc.first_line - 1, loc.first_column),
to: CodeMirror.Pos(loc.last_line - 1, loc.last_column),
message: str
});
};
try {
parser.parse(text);
}
catch (e) {}
if (found.length > 0) {
// JSON invalid so don't go any further
return found;
}
const ast = jsonToAst(text);
const input = JSON.parse(text);
function getArrayPositionalFromAst (node, path) {
if (!node) {
return undefined;
}
else if (path.length < 1) {
return node;
}
else if (!node.children) {
return undefined;
}
else {
const key = path[0];
let newNode;
if (key.match(/^[0-9]+$/)) {
newNode = node.children[path[0]];
}
else {
newNode = node.children.find(childNode => {
return (
childNode.key &&
childNode.key.type === "Identifier" &&
childNode.key.value === key
);
});
if (newNode) {
newNode = newNode.value;
}
}
return getArrayPositionalFromAst(newNode, path.slice(1))
}
}
let out;
if (context === "layer") {
// Just an empty style so we can validate a layer.
const errors = validate({
"version": 8,
"name": "Empty Style",
"metadata": {},
"sources": {},
"sprite": "",
"glyphs": "https://example.com/glyphs/{fontstack}/{range}.pbf",
"layers": [
input
]
});
if (errors) {
out = {
result: "error",
value: errors
.filter(err => {
// Remove missing 'layer source' errors, because we don't include them
if (err.message.match(/^layers\[0\]: source ".*" not found$/)) {
return false;
}
else {
return true;
}
})
.map(err => {
// Remove the 'layers[0].' as we're validating the layer only here
const errMessageParts = err.message.replace(/^layers\[0\]./, "").split(":");
return {
key: errMessageParts[0],
message: errMessageParts[1],
};
})
}
}
}
else if (context === "expression") {
out = expression.createExpression(input, opts.spec);
}
else {
throw new Error(`Invalid context ${context}`);
}
if (out.result === "error") {
const errors = out.value;
errors.forEach(error => {
const {key, message} = error;
if (!key) {
const lastLineHandle = doc.getLineHandle(doc.lastLine());
const err = {
from: CodeMirror.Pos(doc.firstLine(), 0),
to: CodeMirror.Pos(doc.lastLine(), lastLineHandle.text.length),
message: message,
}
found.push(err);
}
else if (key) {
const path = key.replace(/^\[|\]$/g, "").split(/\.|[\[\]]+/).filter(Boolean)
const parsedError = getArrayPositionalFromAst(ast, path);
if (!parsedError) {
console.warn("Something went wrong parsing error:", error);
return;
}
const {loc} = parsedError;
const {start, end} = loc;
found.push({
from: CodeMirror.Pos(start.line - 1, start.column),
to: CodeMirror.Pos(end.line - 1, end.column),
message: message,
});
}
})
}
return found;
});

View File

@@ -0,0 +1,3 @@
export function formatLayerId (id) {
return id === "" ? "[empty_string]" : `'${id}'`;
}

View File

@@ -0,0 +1,18 @@
/**
* If we don't have a default value just make one up
*/
export function findDefaultFromSpec (spec) {
if (spec.hasOwnProperty('default')) {
return spec.default;
}
const defaults = {
'color': '#000000',
'string': '',
'boolean': false,
'number': 0,
'array': [],
}
return defaults[spec.type] || '';
}

View File

@@ -128,11 +128,14 @@
"text-justify",
"text-anchor",
"text-max-angle",
"text-writing-mode",
"text-rotate",
"text-keep-upright",
"text-transform",
"text-offset",
"text-optional"
"text-optional",
"text-variable-anchor",
"text-radial-offset"
]
},
{
@@ -230,5 +233,8 @@
]
}
]
},
"invalid": {
"groups": []
}
}

View File

@@ -1,62 +1,62 @@
[
{
"id": "osm-liberty",
"title": "OSM Liberty",
"url": "https://maputnik.github.io/osm-liberty/style.json",
"thumbnail": "https://maputnik.github.io/thumbnails/osm-liberty.png"
},
{
"id": "klokantech-basic",
"title": "Klokantech Basic",
"url": "https://cdn.jsdelivr.net/gh/openmaptiles/klokantech-basic-gl-style@v1.8/style.json",
"url": "https://cdn.jsdelivr.net/gh/openmaptiles/klokantech-basic-gl-style@v1.9/style.json",
"thumbnail": "https://maputnik.github.io/thumbnails/klokantech-basic.png"
},
{
"id": "dark-matter",
"title": "Dark Matter",
"url": "https://cdn.jsdelivr.net/gh/openmaptiles/dark-matter-gl-style@v1.7/style.json",
"url": "https://cdn.jsdelivr.net/gh/openmaptiles/dark-matter-gl-style@v1.8/style.json",
"thumbnail": "https://maputnik.github.io/thumbnails/dark-matter.png"
},
{
"id": "positron",
"title": "Positron",
"url": "https://cdn.jsdelivr.net/gh/openmaptiles/positron-gl-style@v1.7/style.json",
"url": "https://cdn.jsdelivr.net/gh/openmaptiles/positron-gl-style@v1.8/style.json",
"thumbnail": "https://maputnik.github.io/thumbnails/positron.png"
},
{
"id": "osm-bright",
"title": "OSM Bright",
"url": "https://cdn.jsdelivr.net/gh/openmaptiles/osm-bright-gl-style@v1.8/style.json",
"url": "https://cdn.jsdelivr.net/gh/openmaptiles/osm-bright-gl-style@v1.9/style.json",
"thumbnail": "https://maputnik.github.io/thumbnails/osm-bright.png"
},
{
"id": "toner-gl-style",
"title": "Toner",
"url": "https://cdn.jsdelivr.net/gh/openmaptiles/toner-gl-style@c289223/style.json",
"url": "https://cdn.jsdelivr.net/gh/openmaptiles/toner-gl-style@dcb6e64/style.json",
"thumbnail": "https://maputnik.github.io/thumbnails/toner.png"
},
{
"id": "osm-liberty",
"title": "OSM Liberty",
"url": "https://maputnik.github.io/osm-liberty/style.json",
"thumbnail": "https://maputnik.github.io/osm-liberty/thumbnail.png"
},
{
"id": "os-zoomstack-outdoor",
"title": "Zoomstack Outdoor",
"url": "https://s3-eu-west-1.amazonaws.com/tiles.os.uk/styles/open-zoomstack-outdoor/style.json",
"url": "https://s3-eu-west-1.amazonaws.com/tiles.os.uk/v2/styles/open-zoomstack-outdoor/style.json",
"thumbnail": "https://maputnik.github.io/thumbnails/os-zoomstack-outdoor.png"
},
{
"id": "os-zoomstack-road",
"title": "Zoomstack Road",
"url": "https://s3-eu-west-1.amazonaws.com/tiles.os.uk/styles/open-zoomstack-road/style.json",
"url": "https://s3-eu-west-1.amazonaws.com/tiles.os.uk/v2/styles/open-zoomstack-road/style.json",
"thumbnail": "https://maputnik.github.io/thumbnails/os-zoomstack-road.png"
},
{
"id": "os-zoomstack-light",
"title": "Zoomstack Light",
"url": "https://s3-eu-west-1.amazonaws.com/tiles.os.uk/styles/open-zoomstack-light/style.json",
"url": "https://s3-eu-west-1.amazonaws.com/tiles.os.uk/v2/styles/open-zoomstack-light/style.json",
"thumbnail": "https://maputnik.github.io/thumbnails/os-zoomstack-light.png"
},
{
"id": "os-zoomstack-night",
"title": "Zoomstack Night",
"url": "https://s3-eu-west-1.amazonaws.com/tiles.os.uk/styles/open-zoomstack-night/style.json",
"url": "https://s3-eu-west-1.amazonaws.com/tiles.os.uk/v2/styles/open-zoomstack-night/style.json",
"thumbnail": "https://maputnik.github.io/thumbnails/os-zoomstack-night.png"
},
{

View File

@@ -2,21 +2,21 @@
"openmaptiles": {
"type": "vector",
"url": "https://api.maptiler.com/tiles/v3/tiles.json?key={key}",
"title": "OpenMapTiles"
"title": "OpenMapTiles v3"
},
"thunderforest_transport": {
"type": "vector",
"url": "https://tile.thunderforest.com/thunderforest.transport-v1.json?apikey={key}",
"title": "Thunderforest Transport (heavy)"
"url": "https://tile.thunderforest.com/thunderforest.transport-v2.json?apikey={key}",
"title": "Thunderforest Transport v2"
},
"thunderforest_outdoors": {
"type": "vector",
"url": "https://tile.thunderforest.com/thunderforest.outdoors-v1.json?apikey={key}",
"title": "Thunderforest Outdoors (heavy)"
"url": "https://tile.thunderforest.com/thunderforest.outdoors-v2.json?apikey={key}",
"title": "Thunderforest Outdoors v2"
},
"open_zoomstack": {
"type": "vector",
"url": "https://s3-eu-west-1.amazonaws.com/tiles.os.uk/data/vector/open-zoomstack/config.json",
"title": "OS Open Zoomstack"
"url": "https://s3-eu-west-1.amazonaws.com/tiles.os.uk/v2/data/vector/open-zoomstack/config.json",
"title": "OS Open Zoomstack v2"
}
}

View File

@@ -12,3 +12,6 @@ ReactDOM.render(
</IconContext.Provider>,
document.querySelector("#app")
);
// Hide the loader.
document.querySelector(".loading").style.display = "none";

View File

@@ -1,4 +1,5 @@
import style from './style.js'
import {format} from '@mapbox/mapbox-gl-style-spec'
import ReconnectingWebSocket from 'reconnecting-websocket'
export class ApiStyleStore {
@@ -64,6 +65,12 @@ export class ApiStyleStore {
// Save current style replacing previous version
save(mapStyle) {
const styleJSON = format(
style.stripAccessTokens(
style.replaceAccessTokens(newStyle)
)
);
const id = mapStyle.id
fetch(this.localUrl + '/styles/' + id, {
method: "PUT",
@@ -71,7 +78,7 @@ export class ApiStyleStore {
headers: {
"Content-Type": "application/json; charset=utf-8",
},
body: JSON.stringify(mapStyle)
body: styleJSON
})
.catch(function(error) {
if(error) console.error(error)

View File

@@ -0,0 +1,22 @@
const spec = {
maputnik: {
mapbox_access_token: {
label: "Mapbox Access Token",
doc: "Public access token for Mapbox services."
},
maptiler_access_token: {
label: "MapTiler Access Token",
doc: "Public access token for MapTiler Cloud."
},
thunderforest_access_token: {
label: "Thunderforest Access Token",
doc: "Public access token for Thunderforest services."
},
style_renderer: {
label: "Style Renderer",
doc: "Choose the default Maputnik renderer for this style.",
},
}
}
export default spec;

View File

@@ -1,7 +1,7 @@
import MapboxGl from 'mapbox-gl'
import {readFileSync} from 'fs'
// Load mapbox-gl-rtl-text using object urls without needing http://localhost for AJAX.
const data = require("raw-loader?mimetype=text/javascript!@mapbox/mapbox-gl-rtl-text/mapbox-gl-rtl-text.js");
const data = readFileSync(__dirname+"/../../node_modules/@mapbox/mapbox-gl-rtl-text/mapbox-gl-rtl-text.js", "utf8");
const blob = new window.Blob([data], {
type: "text/javascript"

View File

@@ -114,6 +114,18 @@ function replaceAccessTokens(mapStyle, opts={}) {
return changedStyle
}
function stripAccessTokens(mapStyle) {
const changedMetadata = {
...mapStyle.metadata
};
delete changedMetadata['maputnik:mapbox_access_token'];
delete changedMetadata['maputnik:openmaptiles_access_token'];
return {
...mapStyle,
metadata: changedMetadata
};
}
export default {
ensureStyleValidity,
emptyStyle,
@@ -121,4 +133,5 @@ export default {
generateId,
getAccessToken,
replaceAccessTokens,
stripAccessTokens,
}

View File

@@ -3,6 +3,10 @@ export default class ZoomControl {
this._map = map;
this._container = document.createElement('div');
this._container.className = 'mapboxgl-ctrl mapboxgl-ctrl-group mapboxgl-ctrl-zoom';
this._container.innerHTML = `
Zoom: <span></span>
`;
this._textEl = this._container.querySelector("span");
this.addEventListeners();
@@ -10,7 +14,7 @@ export default class ZoomControl {
}
updateZoomLevel() {
this._container.innerHTML = `Zoom level: ${this._map.getZoom().toFixed(2)}`;
this._textEl.innerHTML = this._map.getZoom().toFixed(2);
}
addEventListeners (){

View File

@@ -32,7 +32,6 @@
.mapboxgl-popup-content {
background-color: rgb(28, 31, 36);
border-radius: 0px;
box-shadow: rgba(0, 0, 0, 0.298039) 0px 0px 5px 0px;
padding: 0px;
}
@@ -57,20 +56,29 @@
border-color: rgb(28, 31, 36);
}
.mapboxgl-ctrl-group > button:hover {
.mapboxgl-ctrl button:not(:disabled):hover {
background-color: rgb(86, 83, 83);
}
.mapboxgl-ctrl-icon.mapboxgl-ctrl-zoom-in {
.mapboxgl-ctrl-zoom-in {
border-radius: 4px 4px 0 0;
}
.mapboxgl-ctrl-compass {
border-radius: 0 0 4px 4px;
}
.mapboxgl-ctrl button.mapboxgl-ctrl-zoom-in .mapboxgl-ctrl-icon {
background-image: url("data:image/svg+xml;charset=utf8,%3Csvg%20viewBox%3D%270%200%2020%2020%27%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%3E%0A%20%20%3Cpath%20style%3D%27fill%3A%23a4a4a4%3B%27%20d%3D%27M%2010%206%20C%209.446%206%209%206.4459904%209%207%20L%209%209%20L%207%209%20C%206.446%209%206%209.446%206%2010%20C%206%2010.554%206.446%2011%207%2011%20L%209%2011%20L%209%2013%20C%209%2013.55401%209.446%2014%2010%2014%20C%2010.554%2014%2011%2013.55401%2011%2013%20L%2011%2011%20L%2013%2011%20C%2013.554%2011%2014%2010.554%2014%2010%20C%2014%209.446%2013.554%209%2013%209%20L%2011%209%20L%2011%207%20C%2011%206.4459904%2010.554%206%2010%206%20z%27%20%2F%3E%0A%3C%2Fsvg%3E%0A")
}
.mapboxgl-ctrl-icon.mapboxgl-ctrl-zoom-out {
.mapboxgl-ctrl button.mapboxgl-ctrl-zoom-out .mapboxgl-ctrl-icon {
background-image: url("data:image/svg+xml;charset=utf8,%3Csvg%20viewBox%3D%270%200%2020%2020%27%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%3E%0A%20%20%3Cpath%20style%3D%27fill%3A%23a4a4a4%3B%27%20d%3D%27m%207%2C9%20c%20-0.554%2C0%20-1%2C0.446%20-1%2C1%200%2C0.554%200.446%2C1%201%2C1%20l%206%2C0%20c%200.554%2C0%201%2C-0.446%201%2C-1%200%2C-0.554%20-0.446%2C-1%20-1%2C-1%20z%27%20%2F%3E%0A%3C%2Fsvg%3E%0A")
}
.mapboxgl-ctrl-icon.mapboxgl-ctrl-compass > .mapboxgl-ctrl-compass-arrow {
background-image: url("data:image/svg+xml;charset=utf8,%3Csvg%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%20viewBox%3D%270%200%2020%2020%27%3E%0A%09%3Cpolygon%20fill%3D%27%23a4a4a4%27%20points%3D%276%2C9%2010%2C1%2014%2C9%27%2F%3E%0A%09%3Cpolygon%20fill%3D%27%23f0f0f0%27%20points%3D%276%2C11%2010%2C19%2014%2C11%20%27%2F%3E%0A%3C%2Fsvg%3E")
.mapboxgl-ctrl button.mapboxgl-ctrl-compass .mapboxgl-ctrl-icon {
background-image: url("data:image/svg+xml;charset=utf8,%3Csvg%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%20viewBox%3D%270%200%2020%2020%27%3E%0A%09%3Cpolygon%20fill%3D%27%23a4a4a4%27%20points%3D%276%2C9%2010%2C1%2014%2C9%27%2F%3E%0A%09%3Cpolygon%20fill%3D%27%23545454%27%20points%3D%276%2C11%2010%2C19%2014%2C11%20%27%2F%3E%0A%3C%2Fsvg%3E");
background-size: 18px;
}
.mapboxgl-ctrl-inspect {

View File

@@ -1,6 +1,17 @@
.CodeMirror-lint-tooltip {
z-index: 2000 !important;
}
.codemirror-container {
max-width: 100%;
position: relative;
overflow: auto;
}
.cm-s-maputnik.CodeMirror {
height: 100%;
font-size: 12px;
background: transparent;
}
.cm-s-maputnik.CodeMirror, .cm-s-maputnik .CodeMirror-gutters {
@@ -8,16 +19,12 @@
border: none;
}
.cm-s-maputnik.CodeMirror {
background: transparent;
}
.cm-s-maputnik .CodeMirror-gutters {
background: #212328;
}
.cm-s-maputnik .CodeMirror-cursor {
border-left: solid thin #8e8e8e !important;
border-left: solid thin #f0f0f0 !important;
}
.cm-s-maputnik.CodeMirror-focused div.CodeMirror-selected {
@@ -47,5 +54,11 @@
}
.cm-s-maputnik .CodeMirror-matchingbracket {
text-decoration: underline; color: white !important;
background: hsla(223, 12%, 35%, 1);
color: $color-white !important;
}
.cm-s-maputnik .CodeMirror-nonmatchingbracket {
background-color: #bb0000;
color: white !important;
}

View File

@@ -1,16 +1,13 @@
// MAP
.maputnik-map__container {
background: white;
display: flex;
position: fixed !important;
top: $toolbar-height + $toolbar-offset;
right: 0;
bottom: 0;
height: calc(100% - #{$toolbar-height + $toolbar-offset});
width: calc(
100%
- 200px /* layer list */
- 350px /* layer editor */
);
width: $layout-map-width;
&--error {
align-items: center;
@@ -28,6 +25,14 @@
height: 100%;
}
.maputnik-input-block:hover,
.maputnik-filter-editor-compound-select:hover {
.maputnik-doc-button {
opacity: 1;
pointer-events: all;
}
}
// DOC LABEL
.maputnik-doc {
&-target {
@@ -57,6 +62,33 @@
z-index: 10;
pointer-events: none;
}
&-button {
opacity: 0;
pointer-events: none;
background: $color-black;
color: white;
border: none;
padding: 0;
svg {
pointer-events: none;
}
&--open {
opacity: 1;
pointer-events: all;
}
}
}
.maputnik-doc-inline {
color: $color-lowgray;
background-color: $color-gray;
padding: $margin-2;
font-size: 12px;
margin-top: $margin-3;
line-height: 1.5;
}
.maputnik-doc-target:hover .maputnik-doc-popup {
@@ -114,7 +146,7 @@
.maputnik-icon-button {
background-color: transparent;
&:hover {
&:hover:not(:disabled) {
background-color: transparent;
label,
@@ -138,6 +170,7 @@
user-select: none;
width: 50%;
vertical-align: top;
font-size: 12px;
}
&-content {
@@ -149,18 +182,26 @@
.maputnik-action-block {
.maputnik-input-block-label {
display: inline-block;
width: 35%;
width: 32%;
}
.maputnik-input-block-action {
vertical-align: top;
display: inline-block;
width: 15%;
width: 18%;
}
.maputnik-input-block-action > div {
text-align: right;
}
}
.maputnik-data-spec-block,
.maputnik-zoom-spec-property {
.maputnik-inline-error {
margin-left: 32%;
}
}
// SPACE HELPER
@@ -175,6 +216,12 @@
&-error {
color: $color-red;
}
&__switch-button {
all: unset;
text-decoration: underline;
cursor: pointer;
}
}
.maputnik-dialog {
@@ -205,3 +252,51 @@
}
}
}
.maputnik-inline-error {
color: #a4a4a4;
padding: 0.4em 0.4em;
font-size: 0.9em;
border: solid 1px $color-red;
border-radius: 2px;
margin: $margin-2 0px;
}
.maputnik-expression-editor {
border: solid 1px $color-gray;
}
.maputnik-input-block--wide {
.maputnik-input-block-content {
display: block;
width: auto;
}
.maputnik-input-block-label {
width: 82%;
}
.maputnik-input-block-action {
text-align: right;
}
}
.maputnik-expr-infobox {
font-size: $font-size-6;
background: $color-midgray;
padding: $margin-2;
border-radius: 2px;
border-top-right-radius: 0px;
border-top-left-radius: 0px;
color: $color-white;
}
.maputnik-expr-infobox__button {
background: none;
border: none;
padding: 0;
text-decoration: underline;
color: currentColor;
cursor: pointer;
}

View File

@@ -1,5 +1,10 @@
.maputnik-filter-editor-wrapper {
padding: $margin-3;
overflow: hidden;
.maputnik-input-block {
margin: 0;
}
}
.maputnik-filter-editor {

View File

@@ -11,6 +11,11 @@
border: none;
background-color: $color-gray;
color: lighten($color-lowgray, 12);
&:invalid {
border: solid 1px #B71C1C;
border-radius: 2px;
}
}
.maputnik-string {
@@ -20,6 +25,21 @@
resize: vertical;
height: 78px;
}
&--disabled {
background: transparent;
border: none;
}
}
.maputnik-number-container {
display: flex;
}
.maputnik-number-range {
width: calc(100% - 4.5em);
margin-right: 0.5em;
flex-shrink: 0;
}
.maputnik-number {
@@ -173,3 +193,35 @@
margin-bottom: $margin-3;
}
}
.maputnik-input-block-content {
position: relative;
overflow: hidden;
}
.SpecDoc__sdk-support {
position: relative;
max-width: 100%;
overflow-x: auto;
}
.SpecDoc__sdk-support__table {
width: 100%;
margin-top: $margin-3;
td, th {
border: solid 1px $color-midgray;
padding: 4px 6px;
white-space: nowrap;
}
}
.SpecDoc__values li {
margin-top: $margin-3;
}
.SpecDoc__values code {
background: $color-midgray;
padding: 0.1em 0.3em;
border-radius: 2px;
}

View File

@@ -1,5 +1,8 @@
// LAYER LIST
.maputnik-layer-list {
height: 100%;
overflow: auto;
&-header {
padding: $margin-2 $margin-2 $margin-3;
@@ -99,6 +102,11 @@
}
}
.maputnik-layer-list-item--error {
color: $color-red;
}
&-item-selected {
color: $color-white;
}
@@ -158,10 +166,39 @@
color: $color-white;
cursor: pointer;
user-select: none;
padding: $margin-2;
line-height: 20px;
border-top: solid 1px #36383e;
&__button {
flex: 1;
display: flex;
padding: $margin-2;
&__icon {
fill: white;
}
&__icon--up {
display: block;
}
&__icon--down {
display: none;
}
}
&__button[aria-expanded="true"] {
.maputnik-layer-editor-group__button__icon--up {
display: none;
}
.maputnik-layer-editor-group__button__icon--down {
display: block;
}
}
@include flex-row;
&:hover {

View File

@@ -32,17 +32,16 @@
top: $toolbar-height + $toolbar-offset;
left: 200px;
z-index: 1;
width: 350px;
width: 370px;
background-color: $color-black;
}
&-bottom {
position: fixed;
height: 50px;
bottom: 0;
left: 550px;
right: 0;
z-index: 1;
width: 100%;
width: $layout-map-width;
background-color: $color-black;
}
}

View File

@@ -16,6 +16,11 @@
height: 20px;
}
.ol-attribution a {
color: rgba(0, 0, 0, 0.75);
text-decoration: none;
}
.ol-control {
button {
background-color: rgb(28, 31, 36);

View File

@@ -108,6 +108,16 @@
background-color: $color-midgray;
}
.maputnik-add-modal {
width: 400px;
max-width: 100%;
}
.maputnik-export-modal {
width: 400px;
max-width: 100%;
}
.maputnik-add-layer {
@extend .clearfix;
}
@@ -180,10 +190,26 @@
border-width: 2px;
border-style: solid;
padding: $margin-2;
.maputnik-input-block-label {
width: 30%;
}
.maputnik-input-block-content {
width: 70%;
}
}
.maputnik-add-source {
@extend .clearfix;
.maputnik-input-block-label {
width: 30%;
}
.maputnik-input-block-content {
width: 70%;
}
}
.maputnik-add-source-button {
@@ -254,6 +280,7 @@
display: block;
margin: 0 auto;
height: 128px;
width: 128px;
}
.maputnik-modal-survey__description {
@@ -264,3 +291,7 @@
color: $color-green;
margin-top: 16px;
}
.modal-settings {
width: 400px;
}

View File

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

View File

@@ -7,3 +7,7 @@
flex: 1;
}
}
.ReactCollapse--collapse {
transition: height 180ms;
}

View File

@@ -2,7 +2,8 @@
div:not(.maputnik-toolbar__actions) {
&::-webkit-scrollbar {
background-color: #26282e;
width: 5px;
width: 8px;
height: 8px;
}
&::-webkit-scrollbar-thumb {

View File

@@ -16,8 +16,8 @@
.maputnik-toolbar-logo {
text-decoration: none;
display: block;
flex: 0 0 180px;
width: 180px;
flex: 0 0 190px;
width: 190px;
text-align: left;
background-color: $color-black;
padding: $margin-2;
@@ -30,7 +30,7 @@
line-height: 26px;
}
img {
svg {
width: 30px;
padding-right: $margin-2;
vertical-align: top;

27
src/styles/_vars.scss Normal file
View File

@@ -0,0 +1,27 @@
$color-black: #191b20;
$color-gray: #222429;
$color-midgray: #303237;
$color-lowgray: #a4a4a4;
$color-white: #f0f0f0;
$color-red: #cf4a4a;
$color-green: #53b972;
$margin-1: 3px;
$margin-2: 5px;
$margin-3: 10px;
$margin-4: 30px;
$margin-5: 40px;
$font-size-1: 24px;
$font-size-2: 20px;
$font-size-3: 18px;
$font-size-4: 16px;
$font-size-5: 14px;
$font-size-6: 12px;
$font-family: Roboto, sans-serif;
$toolbar-height: 40px;
$toolbar-offset: 0;
$layout-list-width: 200px;
$layout-editor-width: 370px;
$layout-map-width: calc(100% - #{$layout-list-width + $layout-editor-width});

View File

@@ -2,9 +2,8 @@
.maputnik-make-zoom-function {
background-color: transparent;
display: inline-block;
padding-bottom: 0;
padding-top: 0;
vertical-align: middle;
padding: 0 $margin-2 0 0;
@extend .maputnik-icon-button;
}
@@ -45,17 +44,12 @@
}
.maputnik-delete-stop {
display: inline-block;
padding-bottom: 0;
padding-top: 0;
vertical-align: middle;
@extend .maputnik-icon-button;
vertical-align: top;
.maputnik-doc-wrapper {
width: auto;
}
.maputnik-doc-target {
cursor: pointer;
}
}
.maputnik-add-stop {
@@ -68,9 +62,8 @@
.maputnik-make-data-function {
background-color: transparent;
display: inline-block;
padding-bottom: 0;
padding-top: 0;
vertical-align: middle;
padding: 0 $margin-2 0 0;
@extend .maputnik-icon-button;
}
@@ -103,10 +96,6 @@
.maputnik-data-spec-property-input {
width: 75%;
display: inline-block;
.maputnik-string {
margin-bottom: 3%;
}
}
}
}

View File

@@ -1,26 +1,4 @@
$color-black: #191b20;
$color-gray: #222429;
$color-midgray: #303237;
$color-lowgray: #a4a4a4;
$color-white: #f0f0f0;
$color-red: #cf4a4a;
$color-green: #53b972;
$margin-1: 3px;
$margin-2: 5px;
$margin-3: 10px;
$margin-4: 30px;
$margin-5: 40px;
$font-size-1: 24px;
$font-size-2: 20px;
$font-size-3: 18px;
$font-size-4: 16px;
$font-size-5: 14px;
$font-size-6: 12px;
$font-family: Roboto, sans-serif;
$toolbar-height: 40px;
$toolbar-offset: 0;
@import 'vars';
@import 'mixins';
@import 'reset';
@import 'base';
@@ -37,8 +15,8 @@ $toolbar-offset: 0;
@import 'zoomproperty';
@import 'popup';
@import 'map';
@import 'codemirror';
@import 'react-collapse';
@import 'react-codemirror';
/**
* Hacks for webdriverio isVisibleWithinViewport
@@ -56,3 +34,4 @@ $toolbar-offset: 0;
width: 14px;
height: 14px;
}

View File

@@ -9,63 +9,32 @@
html {
background-color: rgb(28, 31, 36);
}
#loader,
#loader:before,
#loader:after {
border-radius: 50%;
width: 2.5em;
height: 2.5em;
-webkit-animation-fill-mode: both;
animation-fill-mode: both;
-webkit-animation: pulseload 1.8s infinite ease-in-out;
animation: pulseload 1.8s infinite ease-in-out;
}
#loader {
color: #ffffff;
font-size: 10px;
margin: 80px auto;
position: relative;
text-indent: -9999em;
-webkit-transform: translateZ(0);
-ms-transform: translateZ(0);
transform: translateZ(0);
-webkit-animation-delay: -0.16s;
animation-delay: -0.16s;
}
#loader:before,
#loader:after {
content: '';
.loading {
text-align: center;
position: absolute;
width: 100vw;
height: 100vh;
top: 0;
left: 0;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
#loader:before {
left: -3.5em;
-webkit-animation-delay: -0.32s;
animation-delay: -0.32s;
.loading__logo svg {
width: 200px;
height: 200px;
}
#loader:after {
left: 3.5em;
}
@-webkit-keyframes pulseload {
0%,
80%,
100% {
box-shadow: 0 2.5em 0 -1.3em;
}
40% {
box-shadow: 0 2.5em 0 0;
}
}
@keyframes pulseload {
0%,
80%,
100% {
box-shadow: 0 2.5em 0 -1.3em;
}
40% {
box-shadow: 0 2.5em 0 0;
}
.loading__text {
font-family: sans-serif;
color: white;
font-size: 1.2em;
padding-bottom: 2em;
}
</style>
</head>
<body>
@@ -150,8 +119,13 @@
</defs>
</svg>
<div id="app">
<div id="loader">Loading...</div>
<div id="app"></div>
<div class="loading">
<div class="loading__logo">
<!-- replaced by 'html-webpack-inline-svg-plugin' -->
<img inline src="node_modules/maputnik-design/logos/logo-loading.svg" />
</div>
<div class="loading__text">Loading&hellip;</div>
</div>
</body>
</html>

View File

@@ -1,31 +0,0 @@
// CodeMirror, copyright (c) by Marijn Haverbeke and others
// Distributed under an MIT license: http://codemirror.net/LICENSE
// Depends on fork of jsonlint from <https://github.com/josdejong/jsonlint>
// becuase of <https://github.com/zaach/jsonlint/issues/57>
var jsonlint = require("jsonlint");
var CodeMirror = require("codemirror");
CodeMirror.registerHelper("lint", "json", function(text) {
var found = [];
// NOTE: This was modified from the original to remove the global, also the
// old jsonlint API was 'jsonlint.parseError' its now
// 'jsonlint.parser.parseError'
jsonlint.parser.parseError = function(str, hash) {
var loc = hash.loc;
found.push({
from: CodeMirror.Pos(loc.first_line - 1, loc.first_column),
to: CodeMirror.Pos(loc.last_line - 1, loc.last_column),
message: str
});
};
try {
jsonlint.parse(text);
}
catch(e) {
// Do nothing we catch the error above
}
return found;
});

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