Compare commits

..

54 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
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
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
orangemug
6b45dc8b4d Added style formatting into apistore 2020-01-19 20:05:11 +00:00
33 changed files with 574 additions and 293 deletions

View File

@@ -18,6 +18,7 @@ A free and open visual editor for the [Mapbox GL styles](https://www.mapbox.com/
targeted at developers and map designers. targeted at developers and map designers.
- :link: Design your maps online at **<https://maputnik.github.io/editor/>** (all in local storage) - :link: Design your maps online at **<https://maputnik.github.io/editor/>** (all in local storage)
- :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 - :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. 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.

95
package-lock.json generated
View File

@@ -1,6 +1,6 @@
{ {
"name": "maputnik", "name": "maputnik",
"version": "1.7.0-beta", "version": "1.7.0-beta4",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
@@ -1514,9 +1514,9 @@
"dev": true "dev": true
}, },
"@mdi/react": { "@mdi/react": {
"version": "1.3.0", "version": "1.4.0",
"resolved": "https://registry.npmjs.org/@mdi/react/-/react-1.3.0.tgz", "resolved": "https://registry.npmjs.org/@mdi/react/-/react-1.4.0.tgz",
"integrity": "sha512-RmdB3gsAW4iXOTTHaEaGQ//2w0sxGWiZEoIDteXcf1qTkDkaA+LBu6ub4nNi4VcmSKjcceGHnYHqHENh8fky7A==" "integrity": "sha512-OUH9RhfDJPhybQL3owwrSDIXz2yVKXg5lYeOZjyRCiT9wqywNK0FeYyDByOwNIZnnIQoQYmuSrMv+pOX0Uqkmw=="
}, },
"@nodelib/fs.scandir": { "@nodelib/fs.scandir": {
"version": "2.1.3", "version": "2.1.3",
@@ -2490,6 +2490,11 @@
"is-string": "^1.0.5" "is-string": "^1.0.5"
} }
}, },
"array-move": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/array-move/-/array-move-2.2.1.tgz",
"integrity": "sha512-qQpEHBnVT6HAFgEVUwRdHVd8TYJThrZIT5wSXpEUTPwBaYhPLclw12mEpyUvRWVdl1VwPOqnIy6LqTFN3cSeUQ=="
},
"array-union": { "array-union": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz",
@@ -5783,9 +5788,9 @@
"dev": true "dev": true
}, },
"fastq": { "fastq": {
"version": "1.6.1", "version": "1.7.0",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.6.1.tgz", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.7.0.tgz",
"integrity": "sha512-mpIH5sKYueh3YyeJwqtVo8sORi0CgtmkVbK6kZStpQlZBYQuTzG2CZ7idSiJuA7bY0SFCWUc5WIs+oYumGCQNw==", "integrity": "sha512-YOadQRnHd5q6PogvAR/x62BGituF2ufiEA6s8aavQANw5YKHERI4AREboX6KotzP8oX2klxYF2wcV/7bn1clfQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"reusify": "^1.0.4" "reusify": "^1.0.4"
@@ -6426,9 +6431,9 @@
} }
}, },
"gl-matrix": { "gl-matrix": {
"version": "3.2.1", "version": "3.3.0",
"resolved": "https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.2.1.tgz", "resolved": "https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.3.0.tgz",
"integrity": "sha512-YYVO8jUSf6+SakL4AJmx9Jc7zAZhkJQ+WhdtX3VQe5PJdCOX6/ybY4x1vk+h94ePnjRn6uml68+QxTAJneUpvA==" "integrity": "sha512-COb7LDz+SXaHtl/h4LeaFcNdJdAQSDeVqjiIihSXNrkWObZLhDI4hIkZC11Aeqp7bcE72clzB0BnDXr2SmslRA=="
}, },
"glob": { "glob": {
"version": "7.1.6", "version": "7.1.6",
@@ -6535,18 +6540,18 @@
} }
}, },
"gonzales-pe": { "gonzales-pe": {
"version": "4.2.4", "version": "4.3.0",
"resolved": "https://registry.npmjs.org/gonzales-pe/-/gonzales-pe-4.2.4.tgz", "resolved": "https://registry.npmjs.org/gonzales-pe/-/gonzales-pe-4.3.0.tgz",
"integrity": "sha512-v0Ts/8IsSbh9n1OJRnSfa7Nlxi4AkXIsWB6vPept8FDbL4bXn3FNuxjYtO/nmBGu7GDkL9MFeGebeSu6l55EPQ==", "integrity": "sha512-otgSPpUmdWJ43VXyiNgEYE4luzHCL2pz4wQ0OnDluC6Eg4Ko3Vexy/SrSynglw/eR+OhkzmqFCZa/OFa/RgAOQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"minimist": "1.1.x" "minimist": "^1.2.5"
}, },
"dependencies": { "dependencies": {
"minimist": { "minimist": {
"version": "1.1.3", "version": "1.2.5",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.1.3.tgz", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
"integrity": "sha1-O+39kaktOQFvz6ocaB6Pqhoe/ag=", "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
"dev": true "dev": true
} }
} }
@@ -8841,9 +8846,9 @@
} }
}, },
"mapbox-gl": { "mapbox-gl": {
"version": "1.9.0", "version": "1.9.1",
"resolved": "https://registry.npmjs.org/mapbox-gl/-/mapbox-gl-1.9.0.tgz", "resolved": "https://registry.npmjs.org/mapbox-gl/-/mapbox-gl-1.9.1.tgz",
"integrity": "sha512-PKpoiB2pPUMrqFfBJpt/oA8On3zcp0adEoDS2YIC2RA6o4EZ9Sq2NPZocb64y7ra3mLUvEb7ps1pLVlPMh6y7w==", "integrity": "sha512-jpBcqh+4qpOkj8RdxRdvwKPA8gzNYyMQ8HOcXgZYuEM5nKevRDjD3cEs+rUxi1JuYj4t8bIk68Lfh7aQQC1MjQ==",
"requires": { "requires": {
"@mapbox/geojson-rewind": "^0.4.0", "@mapbox/geojson-rewind": "^0.4.0",
"@mapbox/geojson-types": "^1.0.2", "@mapbox/geojson-types": "^1.0.2",
@@ -9180,9 +9185,9 @@
} }
}, },
"mkdirp": { "mkdirp": {
"version": "1.0.3", "version": "1.0.4",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.3.tgz", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
"integrity": "sha512-6uCP4Qc0sWsgMLy1EOqqS/3rjDHOEnsStVr/4vtAIK2Y5i2kA7lFFejYrpIyiN9w0pYf4ckeCYT9f1r1P9KX5g==", "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
"dev": true "dev": true
}, },
"mocha": { "mocha": {
@@ -9990,9 +9995,9 @@
"dev": true "dev": true
}, },
"ol": { "ol": {
"version": "6.2.1", "version": "6.3.1",
"resolved": "https://registry.npmjs.org/ol/-/ol-6.2.1.tgz", "resolved": "https://registry.npmjs.org/ol/-/ol-6.3.1.tgz",
"integrity": "sha512-CT2szew/COd7Zf9Bls+pdzewBYZNgyfxFivJ3L4Jv9Th7JdWjcQAT+pqMPH25L9SbVT+T17RCMq2H2m9uBCl1A==", "integrity": "sha512-cSSYizzUJQ7AhFSrLPAKopjDXPCHtJsXSmjf3xutPG+iyBeD9IOepQGSgpOSZavPI1gsYh9wowiH+NZwVJ/NYQ==",
"requires": { "requires": {
"elm-pep": "^1.0.4", "elm-pep": "^1.0.4",
"pbf": "3.2.1", "pbf": "3.2.1",
@@ -10723,12 +10728,12 @@
} }
}, },
"postcss-sass": { "postcss-sass": {
"version": "0.4.2", "version": "0.4.4",
"resolved": "https://registry.npmjs.org/postcss-sass/-/postcss-sass-0.4.2.tgz", "resolved": "https://registry.npmjs.org/postcss-sass/-/postcss-sass-0.4.4.tgz",
"integrity": "sha512-hcRgnd91OQ6Ot9R90PE/khUDCJHG8Uxxd3F7Y0+9VHjBiJgNv7sK5FxyHMCBtoLmmkzVbSj3M3OlqUfLJpq0CQ==", "integrity": "sha512-BYxnVYx4mQooOhr+zer0qWbSPYnarAy8ZT7hAQtbxtgVf8gy+LSLT/hHGe35h14/pZDTw1DsxdbrwxBN++H+fg==",
"dev": true, "dev": true,
"requires": { "requires": {
"gonzales-pe": "^4.2.4", "gonzales-pe": "^4.3.0",
"postcss": "^7.0.21" "postcss": "^7.0.21"
} }
}, },
@@ -13246,12 +13251,12 @@
"dev": true "dev": true
}, },
"stylelint": { "stylelint": {
"version": "13.2.1", "version": "13.3.0",
"resolved": "https://registry.npmjs.org/stylelint/-/stylelint-13.2.1.tgz", "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-13.3.0.tgz",
"integrity": "sha512-461ZV4KpUe7pEHHgMOsH4kkjF7qsjkCIMJYOf7QQC4cvgPUJ0z4Nj+ah5fvKl1rzqBqc5EZa6P0nna4CGoJX+A==", "integrity": "sha512-ehNzQu9JAbxuiNhUhmoyPgMjIdz7Fg1AxC5urPVhKotto/faF5GxwljSoLvQa6pB6yd+BVuofApWjWT/6/rBMQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"autoprefixer": "^9.7.4", "autoprefixer": "^9.7.5",
"balanced-match": "^1.0.0", "balanced-match": "^1.0.0",
"chalk": "^3.0.0", "chalk": "^3.0.0",
"cosmiconfig": "^6.0.0", "cosmiconfig": "^6.0.0",
@@ -13271,7 +13276,7 @@
"lodash": "^4.17.15", "lodash": "^4.17.15",
"log-symbols": "^3.0.0", "log-symbols": "^3.0.0",
"mathml-tag-names": "^2.1.3", "mathml-tag-names": "^2.1.3",
"meow": "^6.0.1", "meow": "^6.1.0",
"micromatch": "^4.0.2", "micromatch": "^4.0.2",
"normalize-selector": "^0.2.0", "normalize-selector": "^0.2.0",
"postcss": "^7.0.27", "postcss": "^7.0.27",
@@ -13282,7 +13287,7 @@
"postcss-media-query-parser": "^0.2.3", "postcss-media-query-parser": "^0.2.3",
"postcss-reporter": "^6.0.1", "postcss-reporter": "^6.0.1",
"postcss-resolve-nested-selector": "^0.1.1", "postcss-resolve-nested-selector": "^0.1.1",
"postcss-safe-parser": "^4.0.1", "postcss-safe-parser": "^4.0.2",
"postcss-sass": "^0.4.2", "postcss-sass": "^0.4.2",
"postcss-scss": "^2.0.0", "postcss-scss": "^2.0.0",
"postcss-selector-parser": "^6.0.2", "postcss-selector-parser": "^6.0.2",
@@ -13318,9 +13323,9 @@
"dev": true "dev": true
}, },
"camelcase-keys": { "camelcase-keys": {
"version": "6.2.1", "version": "6.2.2",
"resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-6.2.1.tgz", "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-6.2.2.tgz",
"integrity": "sha512-BPCNVH56RVIxQQIXskp5tLQXUNGQ6sXr7iCv1FHDt81xBOQ/1r6H8SPxf19InVP6DexWar4s87q9thfuk8X9HA==", "integrity": "sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==",
"dev": true, "dev": true,
"requires": { "requires": {
"camelcase": "^5.3.1", "camelcase": "^5.3.1",
@@ -14526,9 +14531,9 @@
"dev": true "dev": true
}, },
"uuid": { "uuid": {
"version": "7.0.2", "version": "7.0.3",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-7.0.2.tgz", "resolved": "https://registry.npmjs.org/uuid/-/uuid-7.0.3.tgz",
"integrity": "sha512-vy9V/+pKG+5ZTYKf+VcphF5Oc6EFiu3W8Nv3P3zIh0EqVI80ZxOzuPfe9EHjkFNvf8+xuTHVeei4Drydlx4zjw==", "integrity": "sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg==",
"dev": true "dev": true
}, },
"v8-compile-cache": { "v8-compile-cache": {
@@ -14606,9 +14611,9 @@
"dev": true "dev": true
}, },
"vfile-message": { "vfile-message": {
"version": "2.0.3", "version": "2.0.4",
"resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-2.0.3.tgz", "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-2.0.4.tgz",
"integrity": "sha512-qQg/2z8qnnBHL0psXyF72kCjb9YioIynvyltuNKFaUhRtqTIcIMP3xnBaPzirVZNuBrUe1qwFciSx2yApa4byw==", "integrity": "sha512-DjssxRGkMvifUOJre00juHoP9DPWuzjxKuMDrhNbk2TdaYYBNMStsNhEOt3idrtI12VQYM/1+iM0KOzXi4pxwQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"@types/unist": "^2.0.0", "@types/unist": "^2.0.0",

View File

@@ -1,6 +1,6 @@
{ {
"name": "maputnik", "name": "maputnik",
"version": "1.7.0-beta", "version": "1.7.0-beta4",
"description": "A MapboxGL visual style editor", "description": "A MapboxGL visual style editor",
"main": "''", "main": "''",
"scripts": { "scripts": {
@@ -25,7 +25,8 @@
"@babel/runtime": "^7.8.4", "@babel/runtime": "^7.8.4",
"@mapbox/mapbox-gl-rtl-text": "^0.2.3", "@mapbox/mapbox-gl-rtl-text": "^0.2.3",
"@mapbox/mapbox-gl-style-spec": "^13.12.0", "@mapbox/mapbox-gl-style-spec": "^13.12.0",
"@mdi/react": "^1.3.0", "@mdi/react": "^1.4.0",
"array-move": "^2.2.1",
"classnames": "^2.2.6", "classnames": "^2.2.6",
"codemirror": "^5.52.0", "codemirror": "^5.52.0",
"color": "^3.1.2", "color": "^3.1.2",
@@ -40,10 +41,10 @@
"lodash.get": "^4.4.2", "lodash.get": "^4.4.2",
"lodash.isequal": "^4.5.0", "lodash.isequal": "^4.5.0",
"lodash.throttle": "^4.1.1", "lodash.throttle": "^4.1.1",
"mapbox-gl": "^1.9.0", "mapbox-gl": "^1.9.1",
"mapbox-gl-inspect": "^1.3.1", "mapbox-gl-inspect": "^1.3.1",
"maputnik-design": "github:maputnik/design#f7a2b4d", "maputnik-design": "github:maputnik/design#f7a2b4d",
"ol": "^6.2.1", "ol": "^6.3.1",
"ol-mapbox-style": "^6.0.1", "ol-mapbox-style": "^6.0.1",
"prop-types": "^15.7.2", "prop-types": "^15.7.2",
"react": "^16.12.0", "react": "^16.12.0",
@@ -102,6 +103,11 @@
"experimentalObjectRestSpread": true, "experimentalObjectRestSpread": true,
"jsx": true "jsx": true
} }
},
"settings": {
"react": {
"version": "detect"
}
} }
}, },
"devDependencies": { "devDependencies": {
@@ -135,19 +141,19 @@
"is-docker": "^2.0.0", "is-docker": "^2.0.0",
"istanbul": "^0.4.5", "istanbul": "^0.4.5",
"istanbul-lib-coverage": "^3.0.0", "istanbul-lib-coverage": "^3.0.0",
"mkdirp": "^1.0.3", "mkdirp": "^1.0.4",
"mocha": "^7.0.1", "mocha": "^7.0.1",
"node-sass": "^4.13.1", "node-sass": "^4.13.1",
"react-hot-loader": "^4.12.19", "react-hot-loader": "^4.12.19",
"sass-loader": "^8.0.2", "sass-loader": "^8.0.2",
"selenium-standalone": "^6.17.0", "selenium-standalone": "^6.17.0",
"style-loader": "^1.1.3", "style-loader": "^1.1.3",
"stylelint": "^13.2.0", "stylelint": "^13.3.0",
"stylelint-config-recommended-scss": "^4.2.0", "stylelint-config-recommended-scss": "^4.2.0",
"stylelint-scss": "^3.14.2", "stylelint-scss": "^3.14.2",
"svg-inline-loader": "^0.8.2", "svg-inline-loader": "^0.8.2",
"transform-loader": "^0.2.4", "transform-loader": "^0.2.4",
"uuid": "^7.0.2", "uuid": "^7.0.3",
"webdriverio": "^6.0.5", "webdriverio": "^6.0.5",
"webpack": "^4.41.6", "webpack": "^4.41.6",
"webpack-bundle-analyzer": "^3.6.0", "webpack-bundle-analyzer": "^3.6.0",

View File

@@ -4,7 +4,7 @@ import cloneDeep from 'lodash.clonedeep'
import clamp from 'lodash.clamp' import clamp from 'lodash.clamp'
import get from 'lodash.get' import get from 'lodash.get'
import {unset} from 'lodash' import {unset} from 'lodash'
import {arrayMove} from 'react-sortable-hoc' import arrayMove from 'array-move'
import url from 'url' import url from 'url'
import MapboxGlMap from './map/MapboxGlMap' import MapboxGlMap from './map/MapboxGlMap'
@@ -36,6 +36,7 @@ import tokens from '../config/tokens.json'
import isEqual from 'lodash.isequal' import isEqual from 'lodash.isequal'
import Debug from '../libs/debug' import Debug from '../libs/debug'
import queryUtil from '../libs/query-util' import queryUtil from '../libs/query-util'
import {formatLayerId} from './util/format';
import MapboxGl from 'mapbox-gl' import MapboxGl from 'mapbox-gl'
@@ -325,7 +326,59 @@ export default class App extends React.Component {
}; };
const errors = validate(newStyle, latest) || []; const errors = validate(newStyle, latest) || [];
const mappedErrors = errors.map(error => {
// 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+): (.*)/); const layerMatch = error.message.match(/layers\[(\d+)\]\.(?:(\S+)\.)?(\S+): (.*)/);
if (layerMatch) { if (layerMatch) {
const [matchStr, index, group, property, message] = layerMatch; const [matchStr, index, group, property, message] = layerMatch;
@@ -335,7 +388,7 @@ export default class App extends React.Component {
parsed: { parsed: {
type: "layer", type: "layer",
data: { data: {
index, index: parseInt(index, 10),
key, key,
message message
} }
@@ -347,7 +400,7 @@ export default class App extends React.Component {
message: error.message, message: error.message,
}; };
} }
}) });
let dirtyMapStyle = undefined; let dirtyMapStyle = undefined;
if (errors.length > 0) { if (errors.length > 0) {
@@ -437,56 +490,50 @@ export default class App extends React.Component {
this.onStyleChanged(changedStyle) this.onStyleChanged(changedStyle)
} }
onLayerDestroy = (layerId) => { onLayerDestroy = (index) => {
let layers = this.state.mapStyle.layers; let layers = this.state.mapStyle.layers;
const remainingLayers = layers.slice(0); const remainingLayers = layers.slice(0);
const idx = style.indexOfLayer(remainingLayers, layerId) remainingLayers.splice(index, 1);
remainingLayers.splice(idx, 1);
this.onLayersChange(remainingLayers); this.onLayersChange(remainingLayers);
} }
onLayerCopy = (layerId) => { onLayerCopy = (index) => {
let layers = this.state.mapStyle.layers; let layers = this.state.mapStyle.layers;
const changedLayers = layers.slice(0) 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" clonedLayer.id = clonedLayer.id + "-copy"
changedLayers.splice(idx, 0, clonedLayer) changedLayers.splice(index, 0, clonedLayer)
this.onLayersChange(changedLayers) this.onLayersChange(changedLayers)
} }
onLayerVisibilityToggle = (layerId) => { onLayerVisibilityToggle = (index) => {
let layers = this.state.mapStyle.layers; let layers = this.state.mapStyle.layers;
const changedLayers = layers.slice(0) 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} : {} const changedLayout = 'layout' in layer ? {...layer.layout} : {}
changedLayout.visibility = changedLayout.visibility === 'none' ? 'visible' : 'none' changedLayout.visibility = changedLayout.visibility === 'none' ? 'visible' : 'none'
layer.layout = changedLayout layer.layout = changedLayout
changedLayers[idx] = layer changedLayers[index] = layer
this.onLayersChange(changedLayers) this.onLayersChange(changedLayers)
} }
onLayerIdChange = (oldId, newId) => { onLayerIdChange = (index, oldId, newId) => {
const changedLayers = this.state.mapStyle.layers.slice(0) const changedLayers = this.state.mapStyle.layers.slice(0)
const idx = style.indexOfLayer(changedLayers, oldId) changedLayers[index] = {
...changedLayers[index],
changedLayers[idx] = {
...changedLayers[idx],
id: newId id: newId
} }
this.onLayersChange(changedLayers) this.onLayersChange(changedLayers)
} }
onLayerChanged = (layer) => { onLayerChanged = (index, layer) => {
const changedLayers = this.state.mapStyle.layers.slice(0) const changedLayers = this.state.mapStyle.layers.slice(0)
const idx = style.indexOfLayer(changedLayers, layer.id) changedLayers[index] = layer
changedLayers[idx] = layer
this.onLayersChange(changedLayers) this.onLayersChange(changedLayers)
} }
@@ -645,9 +692,8 @@ export default class App extends React.Component {
</div> </div>
} }
onLayerSelect = (layerId) => { onLayerSelect = (index) => {
const idx = style.indexOfLayer(this.state.mapStyle.layers, layerId) this.setState({ selectedLayerIndex: index })
this.setState({ selectedLayerIndex: idx })
} }
setModal(modalName, value) { setModal(modalName, value) {
@@ -735,6 +781,7 @@ export default class App extends React.Component {
const bottomPanel = (this.state.errors.length + this.state.infos.length) > 0 ? <MessagePanel const bottomPanel = (this.state.errors.length + this.state.infos.length) > 0 ? <MessagePanel
currentLayer={selectedLayer} currentLayer={selectedLayer}
selectedLayerIndex={this.state.selectedLayerIndex}
onLayerSelect={this.onLayerSelect} onLayerSelect={this.onLayerSelect}
mapStyle={this.state.mapStyle} mapStyle={this.state.mapStyle}
errors={this.state.errors} errors={this.state.errors}

View File

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

View File

@@ -1,5 +1,6 @@
import React from 'react' import React from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import {formatLayerId} from './util/format';
class MessagePanel extends React.Component { class MessagePanel extends React.Component {
static propTypes = { static propTypes = {
@@ -8,6 +9,7 @@ class MessagePanel extends React.Component {
mapStyle: PropTypes.object, mapStyle: PropTypes.object,
onLayerSelect: PropTypes.func, onLayerSelect: PropTypes.func,
currentLayer: PropTypes.object, currentLayer: PropTypes.object,
selectedLayerIndex: PropTypes.number,
} }
static defaultProps = { static defaultProps = {
@@ -15,6 +17,7 @@ class MessagePanel extends React.Component {
} }
render() { render() {
const {selectedLayerIndex} = this.props;
const errors = this.props.errors.map((error, idx) => { const errors = this.props.errors.map((error, idx) => {
let content; let content;
if (error.parsed && error.parsed.type === "layer") { if (error.parsed && error.parsed.type === "layer") {
@@ -23,13 +26,13 @@ class MessagePanel extends React.Component {
const layerId = mapStyle.layers[parsed.data.index].id; const layerId = mapStyle.layers[parsed.data.index].id;
content = ( content = (
<> <>
Layer <span>&apos;{layerId}&apos;</span>: {parsed.data.message} Layer <span>{formatLayerId(layerId)}</span>: {parsed.data.message}
{currentLayer.id !== layerId && {selectedLayerIndex !== parsed.data.index &&
<> <>
&nbsp;&mdash;&nbsp; &nbsp;&mdash;&nbsp;
<button <button
className="maputnik-message-panel__switch-button" className="maputnik-message-panel__switch-button"
onClick={() => this.props.onLayerSelect(layerId)} onClick={() => this.props.onLayerSelect(parsed.data.index)}
> >
switch to layer switch to layer
</button> </button>

View File

@@ -9,8 +9,8 @@ export default class DocLabel extends React.Component {
PropTypes.object, PropTypes.object,
PropTypes.string PropTypes.string
]).isRequired, ]).isRequired,
fieldSpec: PropTypes.object.isRequired, fieldSpec: PropTypes.object,
onToggleDoc: PropTypes.func.isRequired, onToggleDoc: PropTypes.func,
} }
constructor (props) { constructor (props) {

View File

@@ -6,12 +6,21 @@ import DataProperty from './_DataProperty'
import ZoomProperty from './_ZoomProperty' import ZoomProperty from './_ZoomProperty'
import ExpressionProperty from './_ExpressionProperty' import ExpressionProperty from './_ExpressionProperty'
import {function as styleFunction} from '@mapbox/mapbox-gl-style-spec'; import {function as styleFunction} from '@mapbox/mapbox-gl-style-spec';
import {findDefaultFromSpec} from '../util/spec-helper';
function isLiteralExpression (value) { function isLiteralExpression (value) {
return (Array.isArray(value) && value.length === 2 && value[0] === "literal"); 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) { function isZoomField(value) {
return ( return (
typeof(value) === 'object' && typeof(value) === 'object' &&
@@ -28,7 +37,15 @@ function isZoomField(value) {
); );
} }
function isDataField(value) { function isIdentityProperty (value) {
return (
typeof(value) === 'object' &&
value.type === "identity" &&
value.hasOwnProperty("property")
);
}
function isDataStopProperty (value) {
return ( return (
typeof(value) === 'object' && typeof(value) === 'object' &&
value.stops && value.stops &&
@@ -45,6 +62,13 @@ function isDataField(value) {
); );
} }
function isDataField(value) {
return (
isIdentityProperty(value) ||
isDataStopProperty(value)
);
}
function isPrimative (value) { function isPrimative (value) {
const valid = ["string", "boolean", "number"]; const valid = ["string", "boolean", "number"];
return valid.includes(typeof(value)); return valid.includes(typeof(value));
@@ -78,24 +102,6 @@ function getDataType (value, fieldSpec={}) {
} }
} }
/**
* If we don't have a default value just make one up
*/
function findDefaultFromSpec (spec) {
if (spec.hasOwnProperty('default')) {
return spec.default;
}
const defaults = {
'color': '#000000',
'string': '',
'boolean': false,
'number': 0,
'array': [],
}
return defaults[spec.type] || '';
}
/** Supports displaying spec field for zoom function objects /** Supports displaying spec field for zoom function objects
* https://www.mapbox.com/mapbox-gl-style-spec/#types-function-zoom-property * https://www.mapbox.com/mapbox-gl-style-spec/#types-function-zoom-property
@@ -206,7 +212,16 @@ export default class FunctionSpecProperty extends React.Component {
undoExpression = () => { undoExpression = () => {
const {value, fieldName} = this.props; const {value, fieldName} = this.props;
if (isLiteralExpression(value)) { 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.props.onChange(fieldName, value[1]);
this.setState({ this.setState({
dataType: "value", dataType: "value",
@@ -217,6 +232,7 @@ export default class FunctionSpecProperty extends React.Component {
canUndo = () => { canUndo = () => {
const {value, fieldSpec} = this.props; const {value, fieldSpec} = this.props;
return ( return (
isGetExpression(value) ||
isLiteralExpression(value) || isLiteralExpression(value) ||
isPrimative(value) || isPrimative(value) ||
(Array.isArray(value) && fieldSpec.type === "array") (Array.isArray(value) && fieldSpec.type === "array")
@@ -230,6 +246,9 @@ export default class FunctionSpecProperty extends React.Component {
if (typeof(value) === "object" && 'stops' in value) { if (typeof(value) === "object" && 'stops' in value) {
expression = styleFunction.convertFunction(value, fieldSpec); expression = styleFunction.convertFunction(value, fieldSpec);
} }
else if (isIdentityProperty(value)) {
expression = ["get", value.property];
}
else { else {
expression = ["literal", value || this.props.fieldSpec.default]; expression = ["literal", value || this.props.fieldSpec.default];
} }

View File

@@ -1,5 +1,6 @@
import React from 'react' import React from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import {mdiFunctionVariant, mdiTableRowPlusAfter} from '@mdi/js';
import Button from '../Button' import Button from '../Button'
import SpecField from './SpecField' import SpecField from './SpecField'
@@ -10,6 +11,7 @@ import DocLabel from './DocLabel'
import InputBlock from '../inputs/InputBlock' import InputBlock from '../inputs/InputBlock'
import docUid from '../../libs/document-uid' import docUid from '../../libs/document-uid'
import sortNumerically from '../../libs/sort-numerically' import sortNumerically from '../../libs/sort-numerically'
import {findDefaultFromSpec} from '../util/spec-helper';
import labelFromFieldName from './_labelFromFieldName' import labelFromFieldName from './_labelFromFieldName'
import DeleteStopButton from './_DeleteStopButton' import DeleteStopButton from './_DeleteStopButton'
@@ -89,10 +91,10 @@ export default class DataProperty extends React.Component {
getDataFunctionTypes(fieldSpec) { getDataFunctionTypes(fieldSpec) {
if (fieldSpec.expression.interpolated) { if (fieldSpec.expression.interpolated) {
return ["categorical", "interval", "exponential"] return ["categorical", "interval", "exponential", "identity"]
} }
else { else {
return ["categorical", "interval"] return ["categorical", "interval", "identity"]
} }
} }
@@ -122,6 +124,29 @@ export default class DataProperty extends React.Component {
return mappedWithRef.map((item) => item.data); 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) { changeStop(changeIdx, stopData, value) {
const stops = this.props.value.stops.slice(0) const stops = this.props.value.stops.slice(0)
const changedStop = stopData.zoom === undefined ? stopData.value : stopData const changedStop = stopData.zoom === undefined ? stopData.value : stopData
@@ -133,7 +158,7 @@ export default class DataProperty extends React.Component {
...this.props.value, ...this.props.value,
stops: orderedStops, stops: orderedStops,
} }
this.props.onChange(this.props.fieldName, changedValue) this.onChange(this.props.fieldName, changedValue)
} }
changeDataProperty(propName, propVal) { changeDataProperty(propName, propVal) {
@@ -143,7 +168,7 @@ export default class DataProperty extends React.Component {
else { else {
delete this.props.value[propName] delete this.props.value[propName]
} }
this.props.onChange(this.props.fieldName, this.props.value) this.onChange(this.props.fieldName, this.props.value)
} }
render() { render() {
@@ -153,69 +178,72 @@ export default class DataProperty extends React.Component {
this.props.value.type = this.getFieldFunctionType(this.props.fieldSpec) this.props.value.type = this.getFieldFunctionType(this.props.fieldSpec)
} }
const dataFields = this.props.value.stops.map((stop, idx) => { let dataFields;
const zoomLevel = typeof stop[0] === 'object' ? stop[0].zoom : undefined; if (this.props.value.stops) {
const key = this.state.refs[idx]; dataFields = this.props.value.stops.map((stop, idx) => {
const dataLevel = typeof stop[0] === 'object' ? stop[0].value : stop[0]; const zoomLevel = typeof stop[0] === 'object' ? stop[0].zoom : undefined;
const value = stop[1] const key = this.state.refs[idx];
const deleteStopBtn = <DeleteStopButton onClick={this.props.onDeleteStop.bind(this, 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 = { const dataProps = {
label: "Data value", label: "Data value",
value: dataLevel, value: dataLevel,
onChange: newData => this.changeStop(idx, { zoom: zoomLevel, value: newData }, value) onChange: newData => this.changeStop(idx, { zoom: zoomLevel, value: newData }, value)
} }
let dataInput; let dataInput;
if(this.props.value.type === "categorical") { if(this.props.value.type === "categorical") {
dataInput = <StringInput {...dataProps} /> dataInput = <StringInput {...dataProps} />
} }
else { else {
dataInput = <NumberInput {...dataProps} /> dataInput = <NumberInput {...dataProps} />
} }
let zoomInput = null; let zoomInput = null;
if(zoomLevel !== undefined) { if(zoomLevel !== undefined) {
zoomInput = <div className="maputnik-data-spec-property-stop-edit"> zoomInput = <div className="maputnik-data-spec-property-stop-edit">
<NumberInput <NumberInput
value={zoomLevel} value={zoomLevel}
onChange={newZoom => this.changeStop(idx, {zoom: newZoom, value: dataLevel}, value)} onChange={newZoom => this.changeStop(idx, {zoom: newZoom, value: dataLevel}, value)}
min={0} min={0}
max={22} max={22}
/> />
</div> </div>
} }
const errorKeyStart = `${fieldType}.${fieldName}.stops[${idx}]`; const errorKeyStart = `${fieldType}.${fieldName}.stops[${idx}]`;
const foundErrors = Object.entries(errors).filter(([key, error]) => { const foundErrors = Object.entries(errors).filter(([key, error]) => {
return key.startsWith(errorKeyStart); return key.startsWith(errorKeyStart);
}); });
const message = foundErrors.map(([key, error]) => { const message = foundErrors.map(([key, error]) => {
return error.message; return error.message;
}).join(""); }).join("");
const error = message ? {message} : undefined; const error = message ? {message} : undefined;
return <InputBlock return <InputBlock
error={error} error={error}
key={key} key={key}
action={deleteStopBtn} action={deleteStopBtn}
label="" label=""
> >
{zoomInput} {zoomInput}
<div className="maputnik-data-spec-property-stop-data"> <div className="maputnik-data-spec-property-stop-data">
{dataInput} {dataInput}
</div> </div>
<div className="maputnik-data-spec-property-stop-value"> <div className="maputnik-data-spec-property-stop-value">
<SpecField <SpecField
fieldName={this.props.fieldName} fieldName={this.props.fieldName}
fieldSpec={this.props.fieldSpec} fieldSpec={this.props.fieldSpec}
value={value} value={value}
onChange={(_, newValue) => this.changeStop(idx, {zoom: zoomLevel, value: dataLevel}, newValue)} onChange={(_, newValue) => this.changeStop(idx, {zoom: zoomLevel, value: dataLevel}, newValue)}
/> />
</div> </div>
</InputBlock> </InputBlock>
}) })
}
return <div className="maputnik-data-spec-block"> return <div className="maputnik-data-spec-block">
<div className="maputnik-data-spec-property"> <div className="maputnik-data-spec-property">
@@ -223,18 +251,6 @@ export default class DataProperty extends React.Component {
fieldSpec={this.props.fieldSpec} fieldSpec={this.props.fieldSpec}
label={labelFromFieldName(this.props.fieldName)} label={labelFromFieldName(this.props.fieldName)}
> >
<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>
<div className="maputnik-data-spec-property-group"> <div className="maputnik-data-spec-property-group">
<DocLabel <DocLabel
label="Type" label="Type"
@@ -250,31 +266,53 @@ export default class DataProperty extends React.Component {
</div> </div>
<div className="maputnik-data-spec-property-group"> <div className="maputnik-data-spec-property-group">
<DocLabel <DocLabel
label="Default" label="Property"
/> />
<div className="maputnik-data-spec-property-input"> <div className="maputnik-data-spec-property-input">
<SpecField <StringInput
fieldName={this.props.fieldName} value={this.props.value.property}
fieldSpec={this.props.fieldSpec} title={"Input a data property to base styles off of."}
value={this.props.value.default} onChange={propVal => this.changeDataProperty("property", propVal)}
onChange={(_, propVal) => this.changeDataProperty("default", propVal)}
/> />
</div> </div>
</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> </InputBlock>
</div> </div>
{dataFields} {dataFields &&
<Button <>
className="maputnik-add-stop" {dataFields}
onClick={this.props.onAddStop.bind(this)} <Button
> className="maputnik-add-stop"
Add stop onClick={this.props.onAddStop.bind(this)}
</Button> >
<svg style={{width:"14px", height:"14px", verticalAlign: "text-bottom"}} viewBox="0 0 24 24">
<path fill="currentColor" d={mdiTableRowPlusAfter} />
</svg> Add stop
</Button>
</>
}
<Button <Button
className="maputnik-add-stop" className="maputnik-add-stop"
onClick={this.props.onExpressionClick.bind(this)} onClick={this.props.onExpressionClick.bind(this)}
> >
Convert to expression <svg style={{width:"14px", height:"14px", verticalAlign: "text-bottom"}} viewBox="0 0 24 24">
<path fill="currentColor" d={mdiFunctionVariant} />
</svg> Convert to expression
</Button> </Button>
</div> </div>
} }

View File

@@ -1,5 +1,6 @@
import React from 'react' import React from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import {mdiFunctionVariant, mdiTableRowPlusAfter} from '@mdi/js';
import Button from '../Button' import Button from '../Button'
import SpecField from './SpecField' import SpecField from './SpecField'
@@ -176,13 +177,17 @@ export default class ZoomProperty extends React.Component {
className="maputnik-add-stop" className="maputnik-add-stop"
onClick={this.props.onAddStop.bind(this)} 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>
<Button <Button
className="maputnik-add-stop" className="maputnik-add-stop"
onClick={this.props.onExpressionClick.bind(this)} onClick={this.props.onExpressionClick.bind(this)}
> >
Convert to expression <svg style={{width:"14px", height:"14px", verticalAlign: "text-bottom"}} viewBox="0 0 24 24">
<path fill="currentColor" d={mdiFunctionVariant} />
</svg> Convert to expression
</Button> </Button>
</div> </div>
} }

View File

@@ -1,6 +1,7 @@
import React from 'react' import React from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { combiningFilterOps } from '../../libs/filterops.js' import { combiningFilterOps } from '../../libs/filterops.js'
import {mdiTableRowPlusAfter} from '@mdi/js';
import {latest, validate, migrate} from '@mapbox/mapbox-gl-style-spec' import {latest, validate, migrate} from '@mapbox/mapbox-gl-style-spec'
import DocLabel from '../fields/DocLabel' import DocLabel from '../fields/DocLabel'
@@ -223,7 +224,7 @@ export default class CombiningFilterEditor extends React.Component {
const error = errors[`filter[${idx+1}]`]; const error = errors[`filter[${idx+1}]`];
return ( return (
<> <div key={`block-${idx}`}>
<FilterEditorBlock key={idx} onDelete={this.deleteFilterItem.bind(this, idx)}> <FilterEditorBlock key={idx} onDelete={this.deleteFilterItem.bind(this, idx)}>
<SingleFilterEditor <SingleFilterEditor
properties={this.props.properties} properties={this.props.properties}
@@ -232,9 +233,9 @@ export default class CombiningFilterEditor extends React.Component {
/> />
</FilterEditorBlock> </FilterEditorBlock>
{error && {error &&
<div className="maputnik-inline-error">{error.message}</div> <div key="error" className="maputnik-inline-error">{error.message}</div>
} }
</> </div>
); );
}) })
@@ -261,8 +262,11 @@ export default class CombiningFilterEditor extends React.Component {
<Button <Button
data-wd-key="layer-filter-button" data-wd-key="layer-filter-button"
className="maputnik-add-filter" className="maputnik-add-filter"
onClick={this.addFilterItem}> onClick={this.addFilterItem}
Add filter >
<svg style={{width:"14px", height:"14px", verticalAlign: "text-bottom"}} viewBox="0 0 24 24">
<path fill="currentColor" d={mdiTableRowPlusAfter} />
</svg> Add filter
</Button> </Button>
</div> </div>
<div <div

View File

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

View File

@@ -31,7 +31,7 @@ class NumberInput extends React.Component {
} }
static getDerivedStateFromProps(props, state) { static getDerivedStateFromProps(props, state) {
if (!state.editing) { if (!state.editing && props.value !== state.value) {
return { return {
value: props.value, value: props.value,
dirtyValue: props.value, dirtyValue: props.value,
@@ -49,12 +49,17 @@ class NumberInput extends React.Component {
if(this.isValid(value) && hasChanged) { if(this.isValid(value) && hasChanged) {
this.props.onChange(value) this.props.onChange(value)
this.setState({ this.setState({
dirtyValue: newValue, value: newValue,
});
}
else if (!this.isValid(value) && hasChanged) {
this.setState({
value: undefined,
}); });
} }
this.setState({ this.setState({
value: newValue, dirtyValue: newValue === "" ? undefined : newValue,
}) })
} }
@@ -87,11 +92,13 @@ class NumberInput extends React.Component {
} }
// If set value is invalid fall back to the last valid value from props or at last resort the default value // 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)) { if(this.isValid(this.props.value)) {
this.changeValue(this.props.value) this.changeValue(this.props.value)
this.setState({dirtyValue: this.props.value});
} else { } else {
this.changeValue(undefined); this.changeValue(undefined);
this.setState({dirtyValue: undefined});
} }
} }
} }
@@ -144,8 +151,15 @@ class NumberInput extends React.Component {
this.props.min !== undefined && this.props.max !== undefined && this.props.min !== undefined && this.props.max !== undefined &&
this.props.allowRange this.props.allowRange
) { ) {
const dirtyValue = this.state.dirtyValue === undefined ? this.props.default : this.state.dirtyValue const value = this.state.editing ? this.state.dirtyValue : this.state.value;
const value = this.state.value === undefined ? "" : 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"> return <div className="maputnik-number-container">
<input <input
@@ -156,21 +170,25 @@ class NumberInput extends React.Component {
min={this.props.min} min={this.props.min}
step="any" step="any"
spellCheck="false" spellCheck="false"
value={dirtyValue} value={value === undefined ? defaultValue : value}
aria-hidden="true" aria-hidden="true"
onChange={this.onChangeRange} onChange={this.onChangeRange}
onKeyDown={() => { onKeyDown={() => {
this._keyboardEvent = true; this._keyboardEvent = true;
}} }}
onPointerDown={() => { onPointerDown={() => {
this.setState({editing: true}); this.setState({editing: true, editingRange: true});
}} }}
onPointerUp={() => { onPointerUp={() => {
// Safari doesn't get onBlur event // Safari doesn't get onBlur event
this.setState({editing: false}); this.setState({editing: false, editingRange: false});
}} }}
onBlur={() => { onBlur={() => {
this.setState({editing: false}); this.setState({
editing: false,
editingRange: false,
dirtyValue: this.state.value,
});
}} }}
/> />
<input <input
@@ -179,25 +197,32 @@ class NumberInput extends React.Component {
spellCheck="false" spellCheck="false"
className="maputnik-number" className="maputnik-number"
placeholder={this.props.default} placeholder={this.props.default}
value={value} value={inputValue === undefined ? "" : inputValue}
onChange={e => { onFocus={e => {
if (!this.state.editing) { this.setState({editing: true});
this.changeValue(e.target.value); }}
} onChange={e => {
this.changeValue(e.target.value);
}}
onBlur={e => {
this.setState({editing: false});
this.resetValue()
}} }}
onBlur={this.resetValue}
/> />
</div> </div>
} }
else { else {
const value = this.state.value === undefined ? "" : this.state.value; const value = this.state.editing ? this.state.dirtyValue : this.state.value;
return <input return <input
spellCheck="false" spellCheck="false"
className="maputnik-number" className="maputnik-number"
placeholder={this.props.default} placeholder={this.props.default}
value={value} value={value === undefined ? "" : value}
onChange={e => this.changeValue(e.target.value)} onChange={e => this.changeValue(e.target.value)}
onFocus={() => {
this.setState({editing: true});
}}
onBlur={this.resetValue} onBlur={this.resetValue}
required={this.props.required} required={this.props.required}
/> />

View File

@@ -33,6 +33,7 @@ class StringInput extends React.Component {
value: props.value value: props.value
}; };
} }
return {};
} }
render() { render() {
@@ -79,6 +80,11 @@ class StringInput extends React.Component {
this.props.onChange(this.state.value); this.props.onChange(this.state.value);
} }
}, },
onKeyDown: (e) => {
if (e.keyCode === 13) {
this.props.onChange(this.state.value);
}
},
required: this.props.required, required: this.props.required,
}); });
} }

View File

@@ -19,6 +19,7 @@ import {MdMoreVert} from 'react-icons/md'
import { changeType, changeProperty } from '../../libs/layer' import { changeType, changeProperty } from '../../libs/layer'
import layout from '../../config/layout.json' import layout from '../../config/layout.json'
import {formatLayerId} from '../util/format';
function getLayoutForType (type) { function getLayoutForType (type) {
@@ -108,7 +109,10 @@ export default class LayerEditor extends React.Component {
} }
changeProperty(group, property, newValue) { 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) { onGroupToggle(groupTitle, active) {
@@ -152,16 +156,19 @@ export default class LayerEditor extends React.Component {
value={this.props.layer.id} value={this.props.layer.id}
wdKey="layer-editor.layer-id" wdKey="layer-editor.layer-id"
error={errorData.id} error={errorData.id}
onChange={newId => this.props.onLayerIdChange(this.props.layer.id, newId)} onChange={newId => this.props.onLayerIdChange(this.props.layerIndex, this.props.layer.id, newId)}
/> />
<LayerTypeBlock <LayerTypeBlock
disabled={true} disabled={true}
error={errorData.type} error={errorData.type}
value={this.props.layer.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 {this.props.layer.type !== 'background' && <LayerSourceBlock
error={errorData.sources} error={errorData.source}
sourceIds={Object.keys(this.props.sources)} sourceIds={Object.keys(this.props.sources)}
value={this.props.layer.source} value={this.props.layer.source}
onChange={v => this.changeProperty(null, 'source', v)} onChange={v => this.changeProperty(null, 'source', v)}
@@ -210,7 +217,12 @@ export default class LayerEditor extends React.Component {
/> />
case 'jsoneditor': return <JSONEditor case 'jsoneditor': return <JSONEditor
layer={this.props.layer} layer={this.props.layer}
onChange={this.props.onLayerChanged} onChange={(layer) => {
this.props.onLayerChanged(
this.props.layerIndex,
layer
);
}}
/> />
} }
} }
@@ -247,15 +259,15 @@ export default class LayerEditor extends React.Component {
const items = { const items = {
delete: { delete: {
text: "Delete", text: "Delete",
handler: () => this.props.onLayerDestroy(this.props.layer.id) handler: () => this.props.onLayerDestroy(this.props.layerIndex)
}, },
duplicate: { duplicate: {
text: "Duplicate", text: "Duplicate",
handler: () => this.props.onLayerCopy(this.props.layer.id) handler: () => this.props.onLayerCopy(this.props.layerIndex)
}, },
hide: { hide: {
text: (layout.visibility === "none") ? "Show" : "Hide", text: (layout.visibility === "none") ? "Show" : "Hide",
handler: () => this.props.onLayerVisibilityToggle(this.props.layer.id) handler: () => this.props.onLayerVisibilityToggle(this.props.layerIndex)
}, },
moveLayerUp: { moveLayerUp: {
text: "Move layer up", text: "Move layer up",
@@ -281,7 +293,7 @@ export default class LayerEditor extends React.Component {
<header> <header>
<div className="layer-header"> <div className="layer-header">
<h2 className="layer-header__title"> <h2 className="layer-header__title">
Layer: {this.props.layer.id} Layer: {formatLayerId(this.props.layer.id)}
</h2> </h2>
<div className="layer-header__info"> <div className="layer-header__info">
<Wrapper <Wrapper

View File

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

View File

@@ -42,11 +42,16 @@ class LayerListContainer extends React.Component {
onLayerSelect: () => {}, onLayerSelect: () => {},
} }
state = { constructor(props) {
collapsedGroups: {}, super(props);
areAllGroupsExpanded: false, this.selectedItemRef = React.createRef();
isOpen: { this.scrollContainerRef = React.createRef();
add: false, this.state = {
collapsedGroups: {},
areAllGroupsExpanded: false,
isOpen: {
add: false,
}
} }
} }
@@ -161,16 +166,40 @@ class LayerListContainer extends React.Component {
return propsChanged; 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() { render() {
const listItems = [] const listItems = []
let idx = 0 let idx = 0
this.groupedLayers().forEach(layers => { const layerIdCount = new Map();
const layersByGroup = this.groupedLayers();
layersByGroup.forEach(layers => {
const groupPrefix = layerPrefix(layers[0].id) const groupPrefix = layerPrefix(layers[0].id)
if(layers.length > 1) { if(layers.length > 1) {
const grp = <LayerListGroup const grp = <LayerListGroup
data-wd-key={[groupPrefix, idx].join('-')} data-wd-key={[groupPrefix, idx].join('-')}
key={[groupPrefix, idx].join('-')} key={`group-${groupPrefix}-${idx}`}
title={groupPrefix} title={groupPrefix}
isActive={!this.isCollapsed(groupPrefix, idx) || idx === this.props.selectedLayerIndex} isActive={!this.isCollapsed(groupPrefix, idx) || idx === this.props.selectedLayerIndex}
onActiveToggle={this.toggleLayerGroup.bind(this, groupPrefix, idx)} onActiveToggle={this.toggleLayerGroup.bind(this, groupPrefix, idx)}
@@ -189,6 +218,15 @@ class LayerListContainer extends React.Component {
); );
}); });
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 const listItem = <LayerListItem
className={classnames({ className={classnames({
'maputnik-layer-list-item-collapsed': layers.length > 1 && this.isCollapsed(groupPrefix, groupIdx) && idx !== this.props.selectedLayerIndex, 'maputnik-layer-list-item-collapsed': layers.length > 1 && this.isCollapsed(groupPrefix, groupIdx) && idx !== this.props.selectedLayerIndex,
@@ -196,8 +234,9 @@ class LayerListContainer extends React.Component {
'maputnik-layer-list-item--error': !!layerError 'maputnik-layer-list-item--error': !!layerError
})} })}
index={idx} index={idx}
key={layer.id} key={key}
layerId={layer.id} layerId={layer.id}
layerIndex={idx}
layerType={layer.type} layerType={layer.type}
visibility={(layer.layout || {}).visibility} visibility={(layer.layout || {}).visibility}
isSelected={idx === this.props.selectedLayerIndex} isSelected={idx === this.props.selectedLayerIndex}
@@ -205,13 +244,14 @@ class LayerListContainer extends React.Component {
onLayerDestroy={this.props.onLayerDestroy.bind(this)} onLayerDestroy={this.props.onLayerDestroy.bind(this)}
onLayerCopy={this.props.onLayerCopy.bind(this)} onLayerCopy={this.props.onLayerCopy.bind(this)}
onLayerVisibilityToggle={this.props.onLayerVisibilityToggle.bind(this)} onLayerVisibilityToggle={this.props.onLayerVisibilityToggle.bind(this)}
{...additionalProps}
/> />
listItems.push(listItem) listItems.push(listItem)
idx += 1 idx += 1
}) })
}) })
return <div className="maputnik-layer-list"> return <div className="maputnik-layer-list" ref={this.scrollContainerRef}>
<AddModal <AddModal
layers={this.props.layers} layers={this.props.layers}
sources={this.props.sources} sources={this.props.sources}

View File

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

View File

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

View File

@@ -58,23 +58,8 @@ class FeatureLayerPopup extends React.Component {
if(propName) { if(propName) {
const propertySpec = latest["paint_"+feature.layer.type][propName]; const propertySpec = latest["paint_"+feature.layer.type][propName];
let color = feature.layer.paint[propName]; let color = feature.layer.paint[propName];
return String(color);
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;
}
} }
else { else {
// Default color // Default color
@@ -84,7 +69,7 @@ class FeatureLayerPopup extends React.Component {
// This is quite complex, just incase there's an edgecase we're missing // This is quite complex, just incase there's an edgecase we're missing
// always return black if we get an unexpected error. // always return black if we get an unexpected error.
catch (err) { catch (err) {
console.error("Unable to get feature color, error:", err); console.warn("Unable to get feature color, error:", err);
return "black"; return "black";
} }
} }

View File

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

View File

@@ -104,17 +104,24 @@ export default class MapboxGlMap extends React.Component {
this.updateMapFromProps(this.props); 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. // HACK: Fix for <https://github.com/maputnik/editor/issues/576>, while we wait for a proper fix.
// eslint-disable-next-line // eslint-disable-next-line
this.state.inspect._popupBlocked = false; this.state.inspect._popupBlocked = false;
this.state.inspect.toggleInspector() this.state.inspect.toggleInspector()
} }
if(this.props.inspectModeEnabled) {
this.state.inspect.render()
}
if (map) { 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.showTileBoundaries = this.props.options.showTileBoundaries;
map.showCollisionBoxes = this.props.options.showCollisionBoxes; map.showCollisionBoxes = this.props.options.showCollisionBoxes;
map.showOverdrawInspector = this.props.options.showOverdrawInspector; map.showOverdrawInspector = this.props.options.showOverdrawInspector;
@@ -170,7 +177,7 @@ export default class MapboxGlMap extends React.Component {
if(this.props.inspectModeEnabled) { if(this.props.inspectModeEnabled) {
return renderPopup(<FeaturePropertyPopup features={features} />, tmpNode); return renderPopup(<FeaturePropertyPopup features={features} />, tmpNode);
} else { } 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);
} }
} }
}) })
@@ -182,9 +189,6 @@ export default class MapboxGlMap extends React.Component {
inspect, inspect,
zoom: map.getZoom() zoom: map.getZoom()
}); });
if(this.props.inspectModeEnabled) {
inspect.toggleInspector();
}
}) })
map.on("data", e => { map.on("data", e => {
@@ -208,6 +212,11 @@ export default class MapboxGlMap extends React.Component {
map.on("zoomend", mapViewChange); map.on("zoomend", mapViewChange);
} }
onLayerSelectById = (id) => {
const index = this.props.mapStyle.layers.findIndex(layer => layer.id === id);
this.props.onLayerSelect(index);
}
render() { render() {
if(IS_SUPPORTED) { if(IS_SUPPORTED) {
return <div return <div

View File

@@ -15,16 +15,6 @@ 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 { class ExportModal extends React.Component {
static propTypes = { static propTypes = {
mapStyle: PropTypes.object.isRequired, mapStyle: PropTypes.object.isRequired,
@@ -38,7 +28,11 @@ class ExportModal extends React.Component {
} }
downloadStyle() { 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"}); const blob = new Blob([tokenStyle], {type: "application/json;charset=utf-8"});
let exportName; let exportName;

View File

@@ -303,9 +303,6 @@ class SourcesModal extends React.Component {
<div className="maputnik-public-sources" style={{maxwidth: 500}}> <div className="maputnik-public-sources" style={{maxwidth: 500}}>
{tilesetOptions} {tilesetOptions}
</div> </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>
<div className="maputnik-modal-section"> <div className="maputnik-modal-section">

View File

@@ -214,6 +214,11 @@ class GeoJSONSourceJSONEditor extends React.Component {
<JSONEditor <JSONEditor
layer={this.props.source.data} layer={this.props.source.data}
maxHeight={200} maxHeight={200}
mode={{
name: "javascript",
json: true
}}
lint={true}
onChange={data => { onChange={data => {
this.props.onChange({ this.props.onChange({
...this.props.source, ...this.props.source,

View File

@@ -12,6 +12,30 @@ CodeMirror.defineMode("mgl", function(config, 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) { CodeMirror.registerHelper("lint", "mgl", function(text, opts, doc) {
const found = []; const found = [];
const {parser} = jsonlint; const {parser} = jsonlint;

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

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

View File

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

View File

@@ -114,6 +114,18 @@ function replaceAccessTokens(mapStyle, opts={}) {
return changedStyle 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 { export default {
ensureStyleValidity, ensureStyleValidity,
emptyStyle, emptyStyle,
@@ -121,4 +133,5 @@ export default {
generateId, generateId,
getAccessToken, getAccessToken,
replaceAccessTokens, replaceAccessTokens,
stripAccessTokens,
} }

View File

@@ -1,5 +1,8 @@
// LAYER LIST // LAYER LIST
.maputnik-layer-list { .maputnik-layer-list {
height: 100%;
overflow: auto;
&-header { &-header {
padding: $margin-2 $margin-2 $margin-3; padding: $margin-2 $margin-2 $margin-3;

View File

@@ -16,8 +16,8 @@
.maputnik-toolbar-logo { .maputnik-toolbar-logo {
text-decoration: none; text-decoration: none;
display: block; display: block;
flex: 0 0 180px; flex: 0 0 190px;
width: 180px; width: 190px;
text-align: left; text-align: left;
background-color: $color-black; background-color: $color-black;
padding: $margin-2; padding: $margin-2;