mirror of
https://github.com/maputnik/editor.git
synced 2025-12-06 14:20:02 +00:00
Compare commits
445 Commits
v1.0.2
...
v1.3.0-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7c00775515 | ||
|
|
4b5536b282 | ||
|
|
fb84cfee1c | ||
|
|
9132262106 | ||
|
|
5de9e708e9 | ||
|
|
4df63c7287 | ||
|
|
a88ca031d0 | ||
|
|
452706f35c | ||
|
|
8b0aa194cf | ||
|
|
b9aa7e9206 | ||
|
|
e35f106482 | ||
|
|
b7a97cf8ee | ||
|
|
9208115981 | ||
|
|
afbdaecd0a | ||
|
|
558f3d649d | ||
|
|
417511d577 | ||
|
|
df350534ce | ||
|
|
7167235146 | ||
|
|
7a7f2eb7de | ||
|
|
cd28a53f6a | ||
|
|
1fe31ac0ec | ||
|
|
ffce8e3ba5 | ||
|
|
a28a417ebc | ||
|
|
6cdb56d13f | ||
|
|
0516e587b4 | ||
|
|
5b4063105b | ||
|
|
d9a5548762 | ||
|
|
cae6cffb7b | ||
|
|
ede782abed | ||
|
|
00afbad7ac | ||
|
|
edd09ef585 | ||
|
|
1e09066779 | ||
|
|
32edb48e16 | ||
|
|
b116eef147 | ||
|
|
74d1cd2d01 | ||
|
|
fd48d82e42 | ||
|
|
480d54c2d8 | ||
|
|
ab9c39b862 | ||
|
|
dd122d1bac | ||
|
|
f9f5e8f925 | ||
|
|
aa2f4a091c | ||
|
|
13fc699d4a | ||
|
|
f5e8d473ad | ||
|
|
35353d75f5 | ||
|
|
0f103c3c00 | ||
|
|
019428a241 | ||
|
|
6200edea25 | ||
|
|
fc7395df96 | ||
|
|
272f662a34 | ||
|
|
d59d9cde95 | ||
|
|
c71fbcf436 | ||
|
|
54c79445db | ||
|
|
a82ba26f86 | ||
|
|
28af87391d | ||
|
|
0aabd33538 | ||
|
|
bd9076c4ff | ||
|
|
1aed761893 | ||
|
|
a2a6f6dcab | ||
|
|
db5dd0f6ee | ||
|
|
42c3dcf258 | ||
|
|
51a115d65a | ||
|
|
fc0fbd6a37 | ||
|
|
d80d76724c | ||
|
|
77da0a6d30 | ||
|
|
79b251d8b9 | ||
|
|
4f19f6a08c | ||
|
|
d2a6eab1e6 | ||
|
|
c7cf051502 | ||
|
|
6e21503e6b | ||
|
|
78d71a4e7e | ||
|
|
b8f32d46cf | ||
|
|
443782decf | ||
|
|
54e79e5eb8 | ||
|
|
221cd4ffd2 | ||
|
|
354b2fb3cb | ||
|
|
7cb2c36ac9 | ||
|
|
11d73595fc | ||
|
|
c241a6e280 | ||
|
|
198ff143f6 | ||
|
|
7b8b797f9c | ||
|
|
a41b25eea7 | ||
|
|
06eac68f9d | ||
|
|
8abf84ebc0 | ||
|
|
e9aa1f6dd6 | ||
|
|
8e7b838bf7 | ||
|
|
32db3c3c9b | ||
|
|
502586e5d5 | ||
|
|
d92d599d8a | ||
|
|
3487056c7d | ||
|
|
dbcfb08c15 | ||
|
|
e96141090e | ||
|
|
5bd25fc2ed | ||
|
|
334932b298 | ||
|
|
661006d7fb | ||
|
|
c917249517 | ||
|
|
d0ca732fe7 | ||
|
|
52821cd1df | ||
|
|
328e0b8ff7 | ||
|
|
f0147cc89a | ||
|
|
78a7f152e7 | ||
|
|
e936dd16bf | ||
|
|
3d4579288c | ||
|
|
b60df8b074 | ||
|
|
c4b92fa0a9 | ||
|
|
9808d44c71 | ||
|
|
1bdd135386 | ||
|
|
740a75f2e6 | ||
|
|
b62533fa3e | ||
|
|
044349e65f | ||
|
|
e8b0bd4d0a | ||
|
|
1805aee7ba | ||
|
|
8ba2123a26 | ||
|
|
687c08527d | ||
|
|
f0744f024d | ||
|
|
9e82599464 | ||
|
|
7a60df370e | ||
|
|
aee4a041fe | ||
|
|
6fa06e5483 | ||
|
|
15962481ee | ||
|
|
6bf695cd4b | ||
|
|
7ecbc14c39 | ||
|
|
fb0e531f4a | ||
|
|
bd44e6d071 | ||
|
|
3ae37f1c46 | ||
|
|
8c7a1f7075 | ||
|
|
3e97d8a5f1 | ||
|
|
6138257a89 | ||
|
|
0bd62985b9 | ||
|
|
a346d757fd | ||
|
|
84f3970730 | ||
|
|
050e22918a | ||
|
|
f205776695 | ||
|
|
4d427bcbc3 | ||
|
|
0b4910e3c3 | ||
|
|
11a59debdf | ||
|
|
dbe2c2637e | ||
|
|
d6ce13c356 | ||
|
|
6d094a8b3e | ||
|
|
4d0456fd68 | ||
|
|
ad83f940a7 | ||
|
|
edc7e02f58 | ||
|
|
7dfc5029a3 | ||
|
|
8e02722b52 | ||
|
|
984581e01a | ||
|
|
1de7ba7e86 | ||
|
|
a3fa86f7ee | ||
|
|
a589f89c4c | ||
|
|
3b599aed4c | ||
|
|
6953db74c6 | ||
|
|
1ad473a539 | ||
|
|
fafda9ec92 | ||
|
|
11b85bf565 | ||
|
|
6ecc6670dc | ||
|
|
553f0fe23e | ||
|
|
77ddf67201 | ||
|
|
a092bc2689 | ||
|
|
38e0786463 | ||
|
|
180b17d315 | ||
|
|
8acbd784a0 | ||
|
|
07efe1e1b8 | ||
|
|
7ea53cc3a1 | ||
|
|
de21eea21b | ||
|
|
8f8ed6dff3 | ||
|
|
8915bbfeb4 | ||
|
|
df3a42acce | ||
|
|
2a7ef82d23 | ||
|
|
95168f22e3 | ||
|
|
4360753263 | ||
|
|
ad491cb465 | ||
|
|
e5bed80c96 | ||
|
|
9bf3046d4c | ||
|
|
da8dc0f7a6 | ||
|
|
b66a4afd28 | ||
|
|
a94c53534c | ||
|
|
6b22c9130f | ||
|
|
7d5927bbc8 | ||
|
|
240d02a124 | ||
|
|
92ef1c4cbb | ||
|
|
5ce57d0803 | ||
|
|
1c134d757c | ||
|
|
32d808b230 | ||
|
|
ee3def492a | ||
|
|
41bd91fcd2 | ||
|
|
02c8542848 | ||
|
|
844abd38ce | ||
|
|
d9b6f28bb5 | ||
|
|
ed85b838ec | ||
|
|
f82b138a3d | ||
|
|
89c38991b9 | ||
|
|
0e4c06cc3e | ||
|
|
7e510a2582 | ||
|
|
f3cb9c4fdd | ||
|
|
f0f6130272 | ||
|
|
0ebb299fd0 | ||
|
|
9d96525f12 | ||
|
|
fc6f9251f7 | ||
|
|
53cb317155 | ||
|
|
4215b5808f | ||
|
|
2d2f9744e2 | ||
|
|
d0b835ee52 | ||
|
|
1798305f9c | ||
|
|
4b0768d0a6 | ||
|
|
2e79a8ff4c | ||
|
|
e64ca3eb93 | ||
|
|
094c4747d3 | ||
|
|
62f0843283 | ||
|
|
8062e304b7 | ||
|
|
18e7ead78a | ||
|
|
3cab1dc49f | ||
|
|
f8dcbb8fb7 | ||
|
|
c82f38c103 | ||
|
|
fe0e7af033 | ||
|
|
ac51902435 | ||
|
|
e0ff342702 | ||
|
|
664125d820 | ||
|
|
9ae2f2c5af | ||
|
|
721f9b36b3 | ||
|
|
a33d1b819c | ||
|
|
cb4f5ea963 | ||
|
|
3c0ebfabab | ||
|
|
a822430e1d | ||
|
|
0ba11b94c8 | ||
|
|
390e90e8c2 | ||
|
|
59ef8eb4e4 | ||
|
|
2b382a9946 | ||
|
|
d52d55dd6a | ||
|
|
dc40ce7d9e | ||
|
|
383a119127 | ||
|
|
3f492e6208 | ||
|
|
0cec0cf595 | ||
|
|
bc19aea438 | ||
|
|
211850c813 | ||
|
|
c1312fb288 | ||
|
|
0c2934c489 | ||
|
|
ad34147f28 | ||
|
|
1eb6c28617 | ||
|
|
2e8a188bce | ||
|
|
ed495c3216 | ||
|
|
a773958403 | ||
|
|
6a6595d971 | ||
|
|
942b2240a7 | ||
|
|
6e86c60f89 | ||
|
|
ace6812e89 | ||
|
|
604fa6317c | ||
|
|
4479473b37 | ||
|
|
4dc8fc9696 | ||
|
|
bac59d595d | ||
|
|
ed98db8ae3 | ||
|
|
b66eb66358 | ||
|
|
934a994ac5 | ||
|
|
199a989f7d | ||
|
|
a50b09e5a2 | ||
|
|
b20c69b15a | ||
|
|
25be173487 | ||
|
|
61808d5939 | ||
|
|
de24227b1f | ||
|
|
1f5608ec77 | ||
|
|
2d87e162f1 | ||
|
|
1941fdf8a0 | ||
|
|
33fdc52667 | ||
|
|
e11a5a823a | ||
|
|
b60d101d42 | ||
|
|
5e9263b787 | ||
|
|
949bd783f5 | ||
|
|
7fe3137fd0 | ||
|
|
3c97fbe587 | ||
|
|
030d469d7c | ||
|
|
135ef8ed89 | ||
|
|
002e9c4647 | ||
|
|
a4fbe55012 | ||
|
|
63ac707415 | ||
|
|
b5dc04bb4f | ||
|
|
f3ae20f3aa | ||
|
|
1838b8aefd | ||
|
|
e9c65e1ada | ||
|
|
9ea5d213f7 | ||
|
|
7dcd6d5552 | ||
|
|
0de8f2d633 | ||
|
|
cb2f854dd5 | ||
|
|
401c920e47 | ||
|
|
40235fe473 | ||
|
|
a76e08aee7 | ||
|
|
dfe7282510 | ||
|
|
3aae2e976f | ||
|
|
f79a945fa4 | ||
|
|
8234c51412 | ||
|
|
f464f997d1 | ||
|
|
e0b7cdf9dd | ||
|
|
a819154145 | ||
|
|
616f45c586 | ||
|
|
203aaf51b7 | ||
|
|
392d1fe26d | ||
|
|
f452ea0d26 | ||
|
|
97dbb74486 | ||
|
|
1f80cfcaa6 | ||
|
|
5d0fbabb6a | ||
|
|
b5ca0fa17b | ||
|
|
41e1704d08 | ||
|
|
d4569237f5 | ||
|
|
b6ae51b5e5 | ||
|
|
3015ba605d | ||
|
|
eb589d4039 | ||
|
|
271190f434 | ||
|
|
0836790daf | ||
|
|
b3b665fcb9 | ||
|
|
c050b02b8b | ||
|
|
a791403a6a | ||
|
|
a4c6a18353 | ||
|
|
9bc603a510 | ||
|
|
af25fb926b | ||
|
|
365a0518a5 | ||
|
|
9801f49f4e | ||
|
|
bb4f3482ad | ||
|
|
e148607c7a | ||
|
|
ae370f04c1 | ||
|
|
89f6343abd | ||
|
|
ea55687171 | ||
|
|
da0b4d7911 | ||
|
|
e303283098 | ||
|
|
1119ff06c9 | ||
|
|
adc8ed26c1 | ||
|
|
06554b83dc | ||
|
|
06ea1d1697 | ||
|
|
ddb3bcde43 | ||
|
|
db2f9efb93 | ||
|
|
d32b15d425 | ||
|
|
a67f9b2edb | ||
|
|
c38547d4e7 | ||
|
|
3f350c30da | ||
|
|
d502d9b1bb | ||
|
|
06e1be716e | ||
|
|
cda855f1b7 | ||
|
|
36def799c0 | ||
|
|
2e671250b9 | ||
|
|
c881534554 | ||
|
|
e1f7336aa9 | ||
|
|
aa92e9da02 | ||
|
|
232b48ff62 | ||
|
|
a95b2932db | ||
|
|
aa288a1e11 | ||
|
|
7e6efcb9b9 | ||
|
|
817d0a7e63 | ||
|
|
fa0067ce7b | ||
|
|
9beacf7ef3 | ||
|
|
b4292028c2 | ||
|
|
d7c099bcbb | ||
|
|
36cd15f4f1 | ||
|
|
92ff1a8499 | ||
|
|
4af7a71220 | ||
|
|
611e170b5e | ||
|
|
148f64c261 | ||
|
|
2c3f47d3cb | ||
|
|
8a6e24e5e7 | ||
|
|
1d29f67065 | ||
|
|
2ffb3e73e1 | ||
|
|
bba7aa3177 | ||
|
|
c950a33031 | ||
|
|
c9ab3bdbfc | ||
|
|
e32c2e865c | ||
|
|
9e52b0b7dc | ||
|
|
d731fb2cae | ||
|
|
e057fcaea1 | ||
|
|
fff1363134 | ||
|
|
4bbfe1040e | ||
|
|
bc6e2dc81b | ||
|
|
0005698c10 | ||
|
|
53711966d2 | ||
|
|
d3b991aad4 | ||
|
|
4ef19c321d | ||
|
|
a3e3b9dfe3 | ||
|
|
abbce3e9d1 | ||
|
|
0edbfd89ff | ||
|
|
040d585d57 | ||
|
|
c74ef7b0d3 | ||
|
|
23ef937100 | ||
|
|
5157742009 | ||
|
|
96d96edc9e | ||
|
|
2a10edcc25 | ||
|
|
e4477db413 | ||
|
|
b32d926b56 | ||
|
|
6b3b5a8b6f | ||
|
|
a7df8afd6e | ||
|
|
b8205f4c38 | ||
|
|
2adb1bf917 | ||
|
|
2825dd7e04 | ||
|
|
df04064e81 | ||
|
|
0555fc48ad | ||
|
|
cd425bd26d | ||
|
|
a98444b4e7 | ||
|
|
31d05cefbe | ||
|
|
c552838fdd | ||
|
|
45942e604b | ||
|
|
9b1dd44b9d | ||
|
|
df56faa55a | ||
|
|
14cdeae3eb | ||
|
|
f97d2b0e88 | ||
|
|
a7e2154422 | ||
|
|
d8e84d67da | ||
|
|
c3174a0c72 | ||
|
|
0b05284340 | ||
|
|
ac8ae0da66 | ||
|
|
4517a8a36a | ||
|
|
8ba7eadcb9 | ||
|
|
0700e5b05b | ||
|
|
3485b7bfb0 | ||
|
|
c71c50a729 | ||
|
|
2651ab891d | ||
|
|
1e429550c6 | ||
|
|
44e4ae3740 | ||
|
|
b1552248c3 | ||
|
|
5efd2caeb8 | ||
|
|
bed012cb9c | ||
|
|
319d9024db | ||
|
|
ff7e371404 | ||
|
|
d94ee2ba98 | ||
|
|
a112c29c21 | ||
|
|
c7d6734a26 | ||
|
|
32aa8b0e1f | ||
|
|
6b22ba2707 | ||
|
|
2400c8ed00 | ||
|
|
396022e8ea | ||
|
|
0d4449b9c2 | ||
|
|
32ac92f901 | ||
|
|
f70026b702 | ||
|
|
87acc3362d | ||
|
|
732d231c78 | ||
|
|
a76ce64e1d | ||
|
|
5433a4193b | ||
|
|
56f1e58df0 | ||
|
|
d0c9db41ce | ||
|
|
f162ffd9be | ||
|
|
decc390777 | ||
|
|
ad8fa7563a | ||
|
|
68859d279d | ||
|
|
5792a531ce | ||
|
|
03af10f850 | ||
|
|
2f059874aa | ||
|
|
a53d7763ba | ||
|
|
eb526a6186 | ||
|
|
6095f871ed | ||
|
|
e3b4fe582b | ||
|
|
bbf26a3f38 | ||
|
|
fd291490d0 | ||
|
|
767d68d905 | ||
|
|
32b18e9141 |
13
.babelrc
Normal file
13
.babelrc
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"presets": ["env", "react"],
|
||||
"plugins": ["transform-object-rest-spread", "transform-class-properties"],
|
||||
"env": {
|
||||
"test": {
|
||||
"plugins": [
|
||||
["istanbul", {
|
||||
exclude: ["node_modules/**", "test/**"]
|
||||
}]
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
103
.circleci/config.yml
Normal file
103
.circleci/config.yml
Normal file
@@ -0,0 +1,103 @@
|
||||
version: 2
|
||||
templates:
|
||||
# Test the build **only** no webdriver
|
||||
build-steps: &build-steps
|
||||
- checkout
|
||||
- run:
|
||||
name: "Create artifacts directory"
|
||||
command: mkdir /tmp/artifacts
|
||||
- restore_cache:
|
||||
key: v1-dependencies-{{ arch }}-{{ checksum "package.json" }}
|
||||
|
||||
- run: npm install
|
||||
|
||||
- save_cache:
|
||||
paths:
|
||||
- node_modules
|
||||
key: v1-dependencies-{{ arch }}-{{ checksum "package.json" }}
|
||||
|
||||
- run: mkdir -p /tmp/artifacts/logs
|
||||
- run: npm run build
|
||||
- run: npm run lint
|
||||
- run: npm run lint-styles
|
||||
- store_artifacts:
|
||||
path: /tmp/artifacts
|
||||
destination: /artifacts
|
||||
# Test in webdriver
|
||||
wdio-steps: &wdio-steps
|
||||
- checkout
|
||||
- run:
|
||||
name: "Create artifacts directory"
|
||||
command: mkdir /tmp/artifacts
|
||||
- restore_cache:
|
||||
key: v1-dependencies-{{ arch }}-{{ checksum "package.json" }}
|
||||
|
||||
- run: npm install
|
||||
|
||||
- save_cache:
|
||||
paths:
|
||||
- node_modules
|
||||
key: v1-dependencies-{{ arch }}-{{ checksum "package.json" }}
|
||||
|
||||
- run: mkdir -p /tmp/artifacts/logs
|
||||
- run: npm run build
|
||||
- run: npm run lint
|
||||
- run: npm run lint-styles
|
||||
- run: DOCKER_HOST=localhost npm test
|
||||
- run: ./node_modules/.bin/istanbul report --include /tmp/artifacts/coverage/coverage.json --dir /tmp/artifacts/coverage html lcov
|
||||
- store_artifacts:
|
||||
path: /tmp/artifacts
|
||||
destination: /artifacts
|
||||
jobs:
|
||||
build-linux-node-v6:
|
||||
docker:
|
||||
- image: node:6
|
||||
working_directory: ~/repo-linux-node-v6
|
||||
steps: *build-steps
|
||||
build-linux-node-v8:
|
||||
docker:
|
||||
- image: node:8
|
||||
- image: selenium/standalone-chrome:3.8.1
|
||||
working_directory: ~/repo-linux-node-v8
|
||||
steps: *wdio-steps
|
||||
build-linux-node-v10:
|
||||
docker:
|
||||
- image: node:10
|
||||
working_directory: ~/repo-linux-node-v10
|
||||
steps: *build-steps
|
||||
build-osx-node-v6:
|
||||
macos:
|
||||
xcode: "9.0"
|
||||
dependencies:
|
||||
override:
|
||||
- brew install node@6
|
||||
working_directory: ~/repo-osx-node-v6
|
||||
steps: *build-steps
|
||||
build-osx-node-v8:
|
||||
macos:
|
||||
xcode: "9.0"
|
||||
dependencies:
|
||||
override:
|
||||
- brew install node@8
|
||||
working_directory: ~/repo-osx-node-v8
|
||||
steps: *build-steps
|
||||
build-osx-node-v10:
|
||||
macos:
|
||||
xcode: "9.0"
|
||||
dependencies:
|
||||
override:
|
||||
- brew install node@10
|
||||
working_directory: ~/repo-osx-node-v10
|
||||
steps: *build-steps
|
||||
|
||||
workflows:
|
||||
version: 2
|
||||
build:
|
||||
jobs:
|
||||
- build-linux-node-v6
|
||||
- build-linux-node-v8
|
||||
- build-linux-node-v10
|
||||
- build-osx-node-v6
|
||||
- build-osx-node-v8
|
||||
- build-osx-node-v10
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -30,3 +30,6 @@ node_modules
|
||||
|
||||
# Ignore build files
|
||||
public
|
||||
/errorShots
|
||||
/old
|
||||
/build
|
||||
|
||||
28
.travis.yml
28
.travis.yml
@@ -1,17 +1,12 @@
|
||||
language: node_js
|
||||
addons:
|
||||
firefox: latest
|
||||
os:
|
||||
- linux
|
||||
- osx
|
||||
node_js:
|
||||
- "4.6"
|
||||
- "5.11"
|
||||
- "6.1"
|
||||
before_install:
|
||||
- export CHROME_BIN=chromium-browser
|
||||
- export DISPLAY=:99.0
|
||||
- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sh -e /etc/init.d/xvfb start; fi
|
||||
matrix:
|
||||
include:
|
||||
- os: osx
|
||||
node_js: "6"
|
||||
- os: osx
|
||||
node_js: "8"
|
||||
- os: osx
|
||||
node_js: "9"
|
||||
install:
|
||||
- npm install
|
||||
script:
|
||||
@@ -19,4 +14,9 @@ script:
|
||||
- node --stack_size=100000 $(which npm) run build
|
||||
- npm run lint
|
||||
- npm run lint-styles
|
||||
- npm run test
|
||||
addons:
|
||||
apt:
|
||||
sources:
|
||||
- ubuntu-toolchain-r-test
|
||||
packages:
|
||||
- g++-4.8
|
||||
|
||||
59
README.md
59
README.md
@@ -1,15 +1,32 @@
|
||||
# Maputnik [](https://travis-ci.org/maputnik/editor) [](https://ci.appveyor.com/project/lukasmartinelli/editor) [](https://tldrlegal.com/license/mit-license)
|
||||
# Maputnik
|
||||
|
||||
[][travis]
|
||||
[][appveyor]
|
||||
[][dm-prod]
|
||||
[][dm-dev]
|
||||
[][license]
|
||||
|
||||
[travis]: https://travis-ci.org/maputnik/editor
|
||||
[appveyor]: https://ci.appveyor.com/project/lukasmartinelli/editor
|
||||
[dm-prod]: https://david-dm.org/maputnik/editor
|
||||
[dm-dev]: https://david-dm.org/maputnik/editor#info=devDependencies
|
||||
[license]: https://tldrlegal.com/license/mit-license
|
||||
|
||||
<img width="200" align="right" alt="Maputnik" src="src/img/maputnik.png" />
|
||||
|
||||
A free and open visual editor for the [Mapbox GL styles](https://www.mapbox.com/mapbox-gl-style-spec/)
|
||||
targeted at developers and map designers.
|
||||
|
||||
- :link: Design your maps online at **http://maputnik.com/editor/** (all in local storage)
|
||||
- :link: Design your maps online at **<https://maputnik.github.io/editor/>** (all in local storage)
|
||||
- :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 independance is an OSS map designer.
|
||||
|
||||
|
||||
## Donations
|
||||
If you or your organisation has seen value from Maputnik, please consider donating at <https://maputnik.github.io/donate>
|
||||
|
||||
|
||||
## Documentation
|
||||
|
||||
The documentation can be found in the [Wiki](https://github.com/maputnik/editor/wiki). You are welcome to collaborate!
|
||||
@@ -37,7 +54,12 @@ npm install
|
||||
npm start
|
||||
```
|
||||
|
||||
Build a production package for distribution.
|
||||
The build process will watch for changes to the filesystem, rebuild and autoreload the editor. However note this from the webpack-dev-server docs
|
||||
|
||||
> webpack uses the file system to get notified of file changes. In some cases this does not work. For example, when using Network File System (NFS). Vagrant also has a lot of problems with this.
|
||||
Snippet from <https://webpack.js.org/configuration/dev-server/#devserver-watchoptions->
|
||||
|
||||
To enable polling add `export WEBPACK_DEV_SERVER_POLLING=1` to your enviroment.
|
||||
|
||||
```
|
||||
npm run build
|
||||
@@ -51,6 +73,37 @@ npm run lint
|
||||
npm run lint-styles
|
||||
```
|
||||
|
||||
|
||||
## Tests
|
||||
For testing we use [webdriverio](http://webdriver.io) and [selenium-standalone](https://github.com/vvo/selenium-standalone)
|
||||
|
||||
[selenium-standalone](https://github.com/vvo/selenium-standalone) starts a server that will launch browsers on your local machine. We use chrome so you **must** have chrome installed on your machine.
|
||||
|
||||
Now open and terminal and run the following. This will install the drivers on your local machine
|
||||
|
||||
```
|
||||
./node_modules/.bin/selenium-standalone install
|
||||
```
|
||||
|
||||
Now start the standalone server
|
||||
|
||||
```
|
||||
./node_modules/.bin/selenium-standalone start
|
||||
```
|
||||
|
||||
Then open another terminal and run
|
||||
|
||||
```
|
||||
npm test
|
||||
```
|
||||
|
||||
After some time you should see a browser launch which will be automated by the test runner.
|
||||
|
||||
|
||||
## Related Projects
|
||||
|
||||
- [maputnik-dev-server](https://github.com/nycplanning/labs-maputnik-dev-server) - An express.js server that allows for quickly loading the style from any mapboxGL map into mapuntnik.
|
||||
|
||||
## Sponsors
|
||||
|
||||
Thanks to the supporters of the **[Kickstarter campaign](https://www.kickstarter.com/projects/174808720/maputnik-visual-map-editor-for-mapbox-gl)**. This project would not be possible without these commercial and individual sponsors.
|
||||
|
||||
11
appveyor.yml
11
appveyor.yml
@@ -1,14 +1,17 @@
|
||||
environment:
|
||||
matrix:
|
||||
- nodejs_version: "4.6"
|
||||
- nodejs_version: "5.11"
|
||||
- nodejs_version: "6.1"
|
||||
- nodejs_version: "6"
|
||||
- nodejs_version: "8"
|
||||
- nodejs_version: "9"
|
||||
platform:
|
||||
- x86
|
||||
- x64
|
||||
install:
|
||||
- ps: Install-Product node $env:nodejs_version
|
||||
- md public
|
||||
- npm install --global --production windows-build-tools
|
||||
- npm install
|
||||
build_script:
|
||||
- npm run build
|
||||
test_script:
|
||||
- npm run lint
|
||||
- npm test
|
||||
|
||||
62
config/wdio.conf.js
Normal file
62
config/wdio.conf.js
Normal file
@@ -0,0 +1,62 @@
|
||||
var webpack = require("webpack");
|
||||
var WebpackDevServer = require("webpack-dev-server");
|
||||
var webpackConfig = require("./webpack.config");
|
||||
var testConfig = require("../test/config/specs");
|
||||
var artifacts = require("../test/artifacts");
|
||||
var isDocker = require("is-docker");
|
||||
|
||||
|
||||
var server;
|
||||
var SCREENSHOT_PATH = artifacts.pathSync("screenshots");
|
||||
|
||||
exports.config = {
|
||||
specs: [
|
||||
'./test/functional/index.js'
|
||||
],
|
||||
exclude: [
|
||||
],
|
||||
maxInstances: 10,
|
||||
capabilities: [{
|
||||
maxInstances: 5,
|
||||
browserName: 'chrome'
|
||||
}],
|
||||
sync: true,
|
||||
logLevel: 'verbose',
|
||||
coloredLogs: true,
|
||||
bail: 0,
|
||||
screenshotPath: SCREENSHOT_PATH,
|
||||
// Note: This is here because @orangemug currently runs Maputnik inside a docker container.
|
||||
host: process.env.DOCKER_HOST || "0.0.0.0",
|
||||
baseUrl: 'http://localhost',
|
||||
waitforTimeout: 10000,
|
||||
connectionRetryTimeout: 90000,
|
||||
connectionRetryCount: 3,
|
||||
framework: 'mocha',
|
||||
reporters: ['spec'],
|
||||
mochaOpts: {
|
||||
ui: 'bdd',
|
||||
// Because we don't know how long the initial build will take...
|
||||
timeout: 4*60*1000
|
||||
},
|
||||
onPrepare: function (config, capabilities) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
var compiler = webpack(webpackConfig);
|
||||
server = new WebpackDevServer(compiler, {
|
||||
stats: {
|
||||
colors: true
|
||||
}
|
||||
});
|
||||
server.listen(testConfig.port, (isDocker() ? "0.0.0.0" : "localhost"), function(err) {
|
||||
if(err) {
|
||||
reject(err);
|
||||
}
|
||||
else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
})
|
||||
},
|
||||
onComplete: function(exitCode) {
|
||||
server.close()
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ var webpack = require('webpack');
|
||||
var path = require('path');
|
||||
var loaders = require('./webpack.loaders');
|
||||
var HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
var CopyWebpackPlugin = require('copy-webpack-plugin');
|
||||
|
||||
const HOST = process.env.HOST || "127.0.0.1";
|
||||
const PORT = process.env.PORT || "8888";
|
||||
@@ -16,14 +17,17 @@ module.exports = {
|
||||
],
|
||||
devtool: process.env.WEBPACK_DEVTOOL || 'cheap-module-source-map',
|
||||
output: {
|
||||
path: path.join(__dirname, 'public'),
|
||||
path: path.join(__dirname, '..', 'public'),
|
||||
filename: 'bundle.js'
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['', '.js', '.jsx']
|
||||
extensions: ['.js', '.jsx']
|
||||
},
|
||||
module: {
|
||||
loaders
|
||||
noParse: [
|
||||
/mapbox-gl\/dist\/mapbox-gl.js/
|
||||
],
|
||||
loaders: loaders
|
||||
},
|
||||
node: {
|
||||
fs: "empty",
|
||||
@@ -41,14 +45,26 @@ module.exports = {
|
||||
// serve index.html in place of 404 responses to allow HTML5 history
|
||||
historyApiFallback: true,
|
||||
port: PORT,
|
||||
host: HOST
|
||||
host: HOST,
|
||||
watchOptions: {
|
||||
// Disabled polling by default as it causes lots of CPU usage and hence drains laptop batteries. To enable polling add WEBPACK_DEV_SERVER_POLLING to your environment
|
||||
// See <https://webpack.js.org/configuration/watch/#watchoptions-poll> for details
|
||||
poll: (!!process.env.WEBPACK_DEV_SERVER_POLLING ? true : false),
|
||||
watch: false
|
||||
}
|
||||
},
|
||||
plugins: [
|
||||
new webpack.NoErrorsPlugin(),
|
||||
new webpack.NoEmitOnErrorsPlugin(),
|
||||
new webpack.HotModuleReplacementPlugin(),
|
||||
new HtmlWebpackPlugin({
|
||||
title: 'Maputnik',
|
||||
template: './src/template.html'
|
||||
}),
|
||||
new CopyWebpackPlugin([
|
||||
{
|
||||
from: './src/manifest.json',
|
||||
to: 'manifest.json'
|
||||
}
|
||||
])
|
||||
]
|
||||
};
|
||||
55
config/webpack.loaders.js
Normal file
55
config/webpack.loaders.js
Normal file
@@ -0,0 +1,55 @@
|
||||
module.exports = [
|
||||
{
|
||||
test: /\.jsx?$/,
|
||||
exclude: /(node_modules|bower_components|public)/,
|
||||
loaders: ['react-hot-loader/webpack']
|
||||
},
|
||||
// HACK: This is a massive hack and reaches into the mapbox-gl private API.
|
||||
// We have to include this for access to `normalizeSourceURL`. We should
|
||||
// remove this ASAP, see <https://github.com/mapbox/mapbox-gl-js/issues/2416>
|
||||
{
|
||||
test: /.*node_modules[\/\\]mapbox-gl[\/\\]src[\/\\]util[\/\\].*\.js/,
|
||||
loader: 'babel-loader',
|
||||
query: {
|
||||
presets: ['env', 'react', 'flow'],
|
||||
plugins: ['transform-runtime', 'transform-decorators-legacy', 'transform-class-properties'],
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /\.jsx?$/,
|
||||
// Note: These modules aren't ES5 therefore we much compile them.
|
||||
exclude: /(.*node_modules(?)|bower_components|public)/,
|
||||
loader: 'babel-loader',
|
||||
query: {
|
||||
presets: ['env', 'react'],
|
||||
plugins: ['transform-runtime', 'transform-decorators-legacy', 'transform-class-properties'],
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /\.(eot|ttf|woff|woff2)$/,
|
||||
loader: 'file-loader?name=fonts/[name].[ext]'
|
||||
},
|
||||
{
|
||||
test: /\.ico$/,
|
||||
loader: 'file-loader?name=[name].[ext]'
|
||||
},
|
||||
{
|
||||
test: /\.(svg|gif|jpg|png)$/,
|
||||
loader: 'file-loader?name=img/[name].[ext]'
|
||||
},
|
||||
{
|
||||
test: /\.json$/,
|
||||
loader: 'json-loader'
|
||||
},
|
||||
{
|
||||
test: /[\/\\](node_modules|global|src)[\/\\].*\.scss$/,
|
||||
loaders: ["style-loader", "css-loader", "sass-loader"]
|
||||
},
|
||||
{
|
||||
test: /[\/\\](node_modules|global|src)[\/\\].*\.css$/,
|
||||
loaders: [
|
||||
'style-loader?sourceMap',
|
||||
'css-loader'
|
||||
]
|
||||
}
|
||||
];
|
||||
@@ -1,10 +1,15 @@
|
||||
|
||||
var webpack = require('webpack');
|
||||
var path = require('path');
|
||||
var loaders = require('./webpack.loaders');
|
||||
var ExtractTextPlugin = require('extract-text-webpack-plugin');
|
||||
var HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
var WebpackCleanupPlugin = require('webpack-cleanup-plugin');
|
||||
var BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
|
||||
var CopyWebpackPlugin = require('copy-webpack-plugin');
|
||||
var artifacts = require("../test/artifacts");
|
||||
var UglifyJsPlugin = require('uglifyjs-webpack-plugin');
|
||||
|
||||
var OUTPATH = artifacts.pathSync("/build");
|
||||
|
||||
module.exports = {
|
||||
entry: {
|
||||
@@ -12,8 +17,6 @@ module.exports = {
|
||||
vendor: [
|
||||
'file-saver',
|
||||
'mapbox-gl/dist/mapbox-gl.js',
|
||||
//TODO: Build failure because cannot resolve migrations file
|
||||
//"mapbox-gl-style-spec",
|
||||
"lodash.clonedeep",
|
||||
"lodash.throttle",
|
||||
'color',
|
||||
@@ -32,14 +35,17 @@ module.exports = {
|
||||
]
|
||||
},
|
||||
output: {
|
||||
path: path.join(__dirname, 'public'),
|
||||
path: OUTPATH,
|
||||
filename: '[name].[chunkhash].js',
|
||||
chunkFilename: '[chunkhash].js'
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['', '.js', '.jsx']
|
||||
extensions: ['.js', '.jsx']
|
||||
},
|
||||
module: {
|
||||
noParse: [
|
||||
/mapbox-gl\/dist\/mapbox-gl.js/
|
||||
],
|
||||
loaders
|
||||
},
|
||||
node: {
|
||||
@@ -48,21 +54,15 @@ module.exports = {
|
||||
tls: 'empty'
|
||||
},
|
||||
plugins: [
|
||||
new webpack.NoErrorsPlugin(),
|
||||
new webpack.optimize.CommonsChunkPlugin('vendor', '[chunkhash].vendor.js'),
|
||||
new webpack.NoEmitOnErrorsPlugin(),
|
||||
new webpack.optimize.CommonsChunkPlugin({ name: 'vendor', filename: '[chunkhash].vendor.js' }),
|
||||
new WebpackCleanupPlugin(),
|
||||
new webpack.DefinePlugin({
|
||||
'process.env': {
|
||||
NODE_ENV: '"production"'
|
||||
}
|
||||
}),
|
||||
new webpack.optimize.UglifyJsPlugin({
|
||||
compress: {
|
||||
warnings: false,
|
||||
screw_ie8: true,
|
||||
}
|
||||
}),
|
||||
new webpack.optimize.OccurenceOrderPlugin(),
|
||||
new UglifyJsPlugin(),
|
||||
new ExtractTextPlugin('[contenthash].css', {
|
||||
allChunks: true
|
||||
}),
|
||||
@@ -70,6 +70,19 @@ module.exports = {
|
||||
template: './src/template.html',
|
||||
title: 'Maputnik'
|
||||
}),
|
||||
new webpack.optimize.DedupePlugin()
|
||||
new CopyWebpackPlugin([
|
||||
{
|
||||
from: './src/manifest.json',
|
||||
to: 'manifest.json'
|
||||
}
|
||||
]),
|
||||
new BundleAnalyzerPlugin({
|
||||
analyzerMode: 'static',
|
||||
defaultSizes: 'gzip',
|
||||
openAnalyzer: false,
|
||||
generateStatsFile: true,
|
||||
reportFilename: 'bundle-stats.html',
|
||||
statsFilename: 'bundle-stats.json',
|
||||
})
|
||||
]
|
||||
};
|
||||
@@ -1,34 +0,0 @@
|
||||
var webpackConfig = require('./webpack.config.js');
|
||||
|
||||
// Karma configuration
|
||||
module.exports = function(config) {
|
||||
var browsers = ['Chrome'];
|
||||
if (process.env.TRAVIS) {
|
||||
browsers = ['Firefox'];
|
||||
}
|
||||
|
||||
config.set({
|
||||
browsers: browsers,
|
||||
frameworks: ['mocha'],
|
||||
// ... normal karma configuration
|
||||
files: [
|
||||
// all files ending in "_test"
|
||||
{pattern: 'test/*_test.js', watched: false},
|
||||
{pattern: 'test/**/*_test.js', watched: false}
|
||||
// each file acts as entry point for the webpack configuration
|
||||
],
|
||||
|
||||
preprocessors: {
|
||||
// add webpack as preprocessor
|
||||
'test/*_test.js': ['webpack'],
|
||||
'test/**/*_test.js': ['webpack']
|
||||
},
|
||||
|
||||
webpack: webpackConfig,
|
||||
webpackMiddleware: {
|
||||
// webpack-dev-middleware configuration
|
||||
// i. e.
|
||||
stats: 'errors-only'
|
||||
}
|
||||
});
|
||||
};
|
||||
16961
package-lock.json
generated
Normal file
16961
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
184
package.json
184
package.json
@@ -1,16 +1,17 @@
|
||||
{
|
||||
"name": "maputnik",
|
||||
"version": "1.0.1",
|
||||
"version": "1.3.0-beta",
|
||||
"description": "A MapboxGL visual style editor",
|
||||
"main": "''",
|
||||
"scripts": {
|
||||
"stats": "webpack --config webpack.production.config.js --profile --json > stats.json",
|
||||
"build": "webpack --config webpack.production.config.js --progress --profile --colors",
|
||||
"test": "karma start --single-run",
|
||||
"test-watch": "karma start",
|
||||
"start": "webpack-dev-server --progress --profile --colors --watch-poll",
|
||||
"stats": "webpack --config config/webpack.production.config.js --profile --json > stats.json",
|
||||
"build": "webpack --config config/webpack.production.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-styles": "stylelint 'src/styles/*.scss'",
|
||||
"nsp": "nsp check --reporter summary"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -20,58 +21,68 @@
|
||||
"license": "MIT",
|
||||
"homepage": "https://github.com/maputnik/editor#readme",
|
||||
"dependencies": {
|
||||
"@mapbox/mapbox-gl-rtl-text": "^0.1.2",
|
||||
"@mapbox/mapbox-gl-style-spec": "^12.0.0",
|
||||
"classnames": "^2.2.5",
|
||||
"codemirror": "^5.18.2",
|
||||
"color": "^1.0.3",
|
||||
"file-saver": "^1.3.2",
|
||||
"codemirror": "^5.37.0",
|
||||
"color": "^3.0.0",
|
||||
"file-saver": "^1.3.8",
|
||||
"github-api": "^3.0.0",
|
||||
"jsonlint": "github:josdejong/jsonlint#85a19d7",
|
||||
"lodash.capitalize": "^4.2.1",
|
||||
"lodash.clamp": "^4.0.3",
|
||||
"lodash.clonedeep": "^4.5.0",
|
||||
"lodash.isequal": "^4.4.0",
|
||||
"lodash.isequal": "^4.5.0",
|
||||
"lodash.throttle": "^4.1.1",
|
||||
"mapbox-gl": "^0.31.0",
|
||||
"mapbox-gl-inspect": "^1.2.1",
|
||||
"mapbox-gl-style-spec": "^8.11.0",
|
||||
"mousetrap": "^1.6.0",
|
||||
"ol-mapbox-style": "1.0.1",
|
||||
"openlayers": "^3.19.1",
|
||||
"react": "^15.4.0",
|
||||
"react-addons-pure-render-mixin": "^15.4.0",
|
||||
"react-autocomplete": "^1.4.0",
|
||||
"react-codemirror": "^0.3.0",
|
||||
"react-collapse": "^2.3.3",
|
||||
"react-color": "^2.10.0",
|
||||
"react-dom": "^15.4.0",
|
||||
"react-file-reader-input": "^1.1.0",
|
||||
"react-height": "^2.1.1",
|
||||
"react-icon-base": "^2.0.4",
|
||||
"react-icons": "^2.2.1",
|
||||
"react-motion": "^0.4.7",
|
||||
"react-sortable-hoc": "^0.4.5",
|
||||
"reconnecting-websocket": "^3.0.3",
|
||||
"request": "^2.79.0",
|
||||
"mapbox-gl": "^0.45.0",
|
||||
"mapbox-gl-inspect": "^1.3.1",
|
||||
"maputnik-design": "github:maputnik/design",
|
||||
"mousetrap": "^1.6.1",
|
||||
"ol-mapbox-style": "^2.10.1",
|
||||
"ol": "^4.6.5",
|
||||
"prop-types": "^15.6.0",
|
||||
"react": "^16.3.2",
|
||||
"react-addons-pure-render-mixin": "^15.6.2",
|
||||
"react-aria-menubutton": "^5.1.1",
|
||||
"react-aria-modal": "^2.12.1",
|
||||
"react-autocomplete": "^1.7.2",
|
||||
"react-codemirror2": "^4.2.1",
|
||||
"react-collapse": "^4.0.3",
|
||||
"react-color": "^2.14.1",
|
||||
"react-copy-to-clipboard": "^5.0.1",
|
||||
"react-dom": "^16.3.2",
|
||||
"react-file-reader-input": "^1.1.4",
|
||||
"react-height": "^3.0.0",
|
||||
"react-icon-base": "^2.1.1",
|
||||
"react-icons": "^2.2.7",
|
||||
"react-motion": "^0.5.2",
|
||||
"react-sortable-hoc": "^0.6.8",
|
||||
"reconnecting-websocket": "^3.2.2",
|
||||
"request": "^2.85.0",
|
||||
"url": "^0.11.0"
|
||||
},
|
||||
"babel": {
|
||||
"presets": [
|
||||
"es2015",
|
||||
"react"
|
||||
],
|
||||
"plugins": [
|
||||
"transform-object-rest-spread"
|
||||
]
|
||||
},
|
||||
"jshintConfig": {
|
||||
"esversion": 6
|
||||
},
|
||||
"stylelint": {
|
||||
"extends": "stylelint-config-standard"
|
||||
"extends": "stylelint-config-recommended-scss",
|
||||
"rules": {
|
||||
"no-descending-specificity": null,
|
||||
"media-feature-name-no-unknown": [
|
||||
true,
|
||||
{
|
||||
"ignoreMediaFeatureNames": [
|
||||
"prefers-reduced-motion"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"eslintConfig": {
|
||||
"plugins": [
|
||||
"react"
|
||||
],
|
||||
"extend": [
|
||||
"extends": [
|
||||
"plugin:react/recommended"
|
||||
],
|
||||
"env": {
|
||||
@@ -91,41 +102,58 @@
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-core": "6.21.0",
|
||||
"babel-eslint": "^7.1.1",
|
||||
"babel-loader": "6.2.10",
|
||||
"babel-plugin-transform-class-properties": "^6.11.5",
|
||||
"babel-core": "^6.26.3",
|
||||
"babel-eslint": "^8.2.3",
|
||||
"babel-loader": "7.1.4",
|
||||
"babel-plugin-istanbul": "^4.1.6",
|
||||
"babel-plugin-transform-class-properties": "^6.24.1",
|
||||
"babel-plugin-transform-decorators-legacy": "^1.3.4",
|
||||
"babel-plugin-transform-flow-strip-types": "^6.21.0",
|
||||
"babel-plugin-transform-object-rest-spread": "^6.8.0",
|
||||
"babel-plugin-transform-runtime": "^6.15.0",
|
||||
"babel-preset-es2015": "6.18.0",
|
||||
"babel-preset-react": "6.16.0",
|
||||
"babel-runtime": "^6.11.6",
|
||||
"css-loader": "0.26.1",
|
||||
"eslint": "^3.5.0",
|
||||
"eslint-plugin-react": "^6.2.0",
|
||||
"extract-text-webpack-plugin": "^1.0.1",
|
||||
"file-loader": "0.9.0",
|
||||
"html-webpack-plugin": "^2.22.0",
|
||||
"json-loader": "^0.5.4",
|
||||
"karma": "^1.3.0",
|
||||
"karma-chrome-launcher": "^2.0.0",
|
||||
"karma-firefox-launcher": "^1.0.0",
|
||||
"karma-mocha": "^1.3.0",
|
||||
"karma-webpack": "^2.0.1",
|
||||
"mocha": "^3.1.2",
|
||||
"mocha-loader": "^1.0.0",
|
||||
"node-sass": "^4.2.0",
|
||||
"react-hot-loader": "^3.0.0-beta.6",
|
||||
"sass-loader": "^4.0.1",
|
||||
"style-loader": "0.13.1",
|
||||
"stylelint": "^7.7.1",
|
||||
"stylelint-config-standard": "^15.0.1",
|
||||
"transform-loader": "^0.2.3",
|
||||
"url-loader": "0.5.7",
|
||||
"webpack": "1.14.0",
|
||||
"webpack-cleanup-plugin": "^0.4.1",
|
||||
"webpack-dev-server": "1.16.2"
|
||||
"babel-plugin-transform-flow-strip-types": "^6.22.0",
|
||||
"babel-plugin-transform-object-rest-spread": "^6.26.0",
|
||||
"babel-plugin-transform-runtime": "^6.23.0",
|
||||
"babel-preset-env": "^1.6.1",
|
||||
"babel-preset-es2015": "^6.24.1",
|
||||
"babel-preset-flow": "^6.23.0",
|
||||
"babel-preset-react": "^6.24.1",
|
||||
"babel-register": "^6.26.0",
|
||||
"babel-runtime": "^6.26.0",
|
||||
"base64-loader": "^1.0.0",
|
||||
"copy-webpack-plugin": "^4.5.1",
|
||||
"cors": "^2.8.4",
|
||||
"cross-env": "^5.1.4",
|
||||
"css-loader": "^0.28.11",
|
||||
"eslint": "^4.19.1",
|
||||
"eslint-plugin-react": "^7.4.0",
|
||||
"express": "^4.16.3",
|
||||
"extract-text-webpack-plugin": "^3.0.2",
|
||||
"file-loader": "^1.1.5",
|
||||
"html-webpack-plugin": "^3.2.0",
|
||||
"is-docker": "^1.1.0",
|
||||
"istanbul": "^0.4.5",
|
||||
"istanbul-lib-coverage": "^1.2.0",
|
||||
"json-loader": "^0.5.7",
|
||||
"mkdirp": "^0.5.1",
|
||||
"mocha": "^5.1.1",
|
||||
"node-sass": "^4.9.0",
|
||||
"nsp": "^3.1.0",
|
||||
"react-hot-loader": "^3.1.1",
|
||||
"sass-loader": "^7.0.1",
|
||||
"selenium-standalone": "^6.14.0",
|
||||
"style-loader": "^0.20.3",
|
||||
"stylelint": "^9.2.0",
|
||||
"stylelint-config-recommended-scss": "^3.2.0",
|
||||
"stylelint-scss": "^3.0.0",
|
||||
"transform-loader": "^0.2.4",
|
||||
"uglifyjs-webpack-plugin": "^1.2.4",
|
||||
"uuid": "^3.1.0",
|
||||
"wdio-mocha-framework": "^0.5.13",
|
||||
"wdio-phantomjs-service": "^0.2.2",
|
||||
"wdio-selenium-standalone-service": "0.0.10",
|
||||
"wdio-spec-reporter": "^0.1.2",
|
||||
"webdriverio": "^4.12.0",
|
||||
"webpack": "^3.8.1",
|
||||
"webpack-bundle-analyzer": "^2.9.0",
|
||||
"webpack-cleanup-plugin": "^0.5.1",
|
||||
"webpack-dev-server": "^2.9.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,10 +4,18 @@
|
||||
}
|
||||
|
||||
.cm-s-maputnik.CodeMirror, .cm-s-maputnik .CodeMirror-gutters {
|
||||
background: transparent;
|
||||
color: #8e8e8e;
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import React from 'react'
|
||||
import Mousetrap from 'mousetrap'
|
||||
import cloneDeep from 'lodash.clonedeep'
|
||||
import clamp from 'lodash.clamp'
|
||||
import {arrayMove} from 'react-sortable-hoc'
|
||||
import url from 'url'
|
||||
|
||||
import MapboxGlMap from './map/MapboxGlMap'
|
||||
import OpenLayers3Map from './map/OpenLayers3Map'
|
||||
@@ -9,10 +13,15 @@ import Toolbar from './Toolbar'
|
||||
import AppLayout from './AppLayout'
|
||||
import MessagePanel from './MessagePanel'
|
||||
|
||||
import SettingsModal from './modals/SettingsModal'
|
||||
import ExportModal from './modals/ExportModal'
|
||||
import SourcesModal from './modals/SourcesModal'
|
||||
import OpenModal from './modals/OpenModal'
|
||||
import ShortcutsModal from './modals/ShortcutsModal'
|
||||
import SurveyModal from './modals/SurveyModal'
|
||||
|
||||
import { downloadGlyphsMetadata, downloadSpriteMetadata } from '../libs/metadata'
|
||||
import GlSpec from 'mapbox-gl-style-spec/reference/latest.js'
|
||||
import validateStyleMin from 'mapbox-gl-style-spec/lib/validate_style.min'
|
||||
import formatStyle from 'mapbox-gl-style-spec/lib/format'
|
||||
import * as styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
|
||||
import style from '../libs/style.js'
|
||||
import { initialStyleUrl, loadStyleUrl } from '../libs/urlopen'
|
||||
import { undoMessages, redoMessages } from '../libs/diffmessage'
|
||||
@@ -21,6 +30,13 @@ import { ApiStyleStore } from '../libs/apistore'
|
||||
import { RevisionStore } from '../libs/revisions'
|
||||
import LayerWatcher from '../libs/layerwatcher'
|
||||
import tokens from '../config/tokens.json'
|
||||
import isEqual from 'lodash.isequal'
|
||||
import Debug from '../libs/debug'
|
||||
import queryUtil from '../libs/query-util'
|
||||
|
||||
import MapboxGl from 'mapbox-gl'
|
||||
import mapboxUtil from 'mapbox-gl/src/util/mapbox'
|
||||
|
||||
|
||||
function updateRootSpec(spec, fieldName, newValues) {
|
||||
return {
|
||||
@@ -43,6 +59,79 @@ export default class App extends React.Component {
|
||||
onLocalStyleChange: mapStyle => this.onStyleChanged(mapStyle, false)
|
||||
})
|
||||
|
||||
|
||||
const keyCodes = {
|
||||
"esc": 27,
|
||||
"?": 191,
|
||||
"o": 79,
|
||||
"e": 69,
|
||||
"s": 83,
|
||||
"d": 68,
|
||||
"i": 73,
|
||||
"m": 77,
|
||||
}
|
||||
|
||||
const shortcuts = [
|
||||
{
|
||||
keyCode: keyCodes["?"],
|
||||
handler: () => {
|
||||
this.toggleModal("shortcuts");
|
||||
}
|
||||
},
|
||||
{
|
||||
keyCode: keyCodes["o"],
|
||||
handler: () => {
|
||||
this.toggleModal("open");
|
||||
}
|
||||
},
|
||||
{
|
||||
keyCode: keyCodes["e"],
|
||||
handler: () => {
|
||||
this.toggleModal("export");
|
||||
}
|
||||
},
|
||||
{
|
||||
keyCode: keyCodes["d"],
|
||||
handler: () => {
|
||||
this.toggleModal("sources");
|
||||
}
|
||||
},
|
||||
{
|
||||
keyCode: keyCodes["s"],
|
||||
handler: () => {
|
||||
this.toggleModal("settings");
|
||||
}
|
||||
},
|
||||
{
|
||||
keyCode: keyCodes["i"],
|
||||
handler: () => {
|
||||
this.changeInspectMode();
|
||||
}
|
||||
},
|
||||
{
|
||||
keyCode: keyCodes["m"],
|
||||
handler: () => {
|
||||
document.querySelector(".mapboxgl-canvas").focus();
|
||||
}
|
||||
},
|
||||
]
|
||||
|
||||
document.body.addEventListener("keyup", (e) => {
|
||||
if(e.keyCode === keyCodes["esc"]) {
|
||||
e.target.blur();
|
||||
document.body.focus();
|
||||
}
|
||||
else if(document.activeElement === document.body) {
|
||||
const shortcut = shortcuts.find((shortcut) => {
|
||||
return (shortcut.keyCode === e.keyCode)
|
||||
})
|
||||
|
||||
if(shortcut) {
|
||||
shortcut.handler(e);
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const styleUrl = initialStyleUrl()
|
||||
if(styleUrl) {
|
||||
this.styleStore = new StyleStore()
|
||||
@@ -54,9 +143,21 @@ export default class App extends React.Component {
|
||||
this.styleStore = new StyleStore()
|
||||
}
|
||||
this.styleStore.latestStyle(mapStyle => this.onStyleChanged(mapStyle))
|
||||
|
||||
if(Debug.enabled()) {
|
||||
Debug.set("maputnik", "styleStore", this.styleStore);
|
||||
Debug.set("maputnik", "revisionStore", this.revisionStore);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if(Debug.enabled()) {
|
||||
Debug.set("maputnik", "revisionStore", this.revisionStore);
|
||||
Debug.set("maputnik", "styleStore", this.styleStore);
|
||||
}
|
||||
|
||||
const queryObj = url.parse(window.location.href, true).query;
|
||||
|
||||
this.state = {
|
||||
errors: [],
|
||||
infos: [],
|
||||
@@ -65,28 +166,34 @@ export default class App extends React.Component {
|
||||
sources: {},
|
||||
vectorLayers: {},
|
||||
inspectModeEnabled: false,
|
||||
spec: GlSpec,
|
||||
spec: styleSpec.latest,
|
||||
isOpen: {
|
||||
settings: false,
|
||||
sources: false,
|
||||
open: false,
|
||||
shortcuts: false,
|
||||
export: false,
|
||||
survey: localStorage.hasOwnProperty('survey') ? false : true
|
||||
},
|
||||
mapOptions: {
|
||||
showTileBoundaries: queryUtil.asBool(queryObj, "show-tile-boundaries")
|
||||
},
|
||||
mapFilter: queryObj["color-blindness-emulation"],
|
||||
}
|
||||
|
||||
this.layerWatcher = new LayerWatcher({
|
||||
onSourcesChange: v => this.setState({ sources: v }),
|
||||
onVectorLayersChange: v => this.setState({ vectorLayers: v })
|
||||
})
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
Mousetrap.bind(['ctrl+z'], this.onUndo.bind(this));
|
||||
Mousetrap.bind(['ctrl+y'], this.onRedo.bind(this));
|
||||
Mousetrap.bind(['mod+z'], this.onUndo.bind(this));
|
||||
Mousetrap.bind(['mod+y', 'mod+shift+z'], this.onRedo.bind(this));
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
Mousetrap.unbind(['ctrl+z'], this.onUndo.bind(this));
|
||||
Mousetrap.unbind(['ctrl+y'], this.onRedo.bind(this));
|
||||
}
|
||||
|
||||
onReset() {
|
||||
this.styleStore.purge()
|
||||
loadDefaultStyle(mapStyle => this.onStyleOpen(mapStyle))
|
||||
Mousetrap.unbind(['mod+z'], this.onUndo.bind(this));
|
||||
Mousetrap.unbind(['mod+y', 'mod+shift+z'], this.onRedo.bind(this));
|
||||
}
|
||||
|
||||
saveStyle(snapshotStyle) {
|
||||
@@ -96,7 +203,9 @@ export default class App extends React.Component {
|
||||
updateFonts(urlTemplate) {
|
||||
const metadata = this.state.mapStyle.metadata || {}
|
||||
const accessToken = metadata['maputnik:openmaptiles_access_token'] || tokens.openmaptiles
|
||||
downloadGlyphsMetadata(urlTemplate.replace('{key}', accessToken), fonts => {
|
||||
|
||||
let glyphUrl = (typeof urlTemplate === 'string')? urlTemplate.replace('{key}', accessToken): urlTemplate;
|
||||
downloadGlyphsMetadata(glyphUrl, fonts => {
|
||||
this.setState({ spec: updateRootSpec(this.state.spec, 'glyphs', fonts)})
|
||||
})
|
||||
}
|
||||
@@ -108,6 +217,10 @@ export default class App extends React.Component {
|
||||
}
|
||||
|
||||
onStyleChanged(newStyle, save=true) {
|
||||
|
||||
const errors = styleSpec.validate(newStyle, styleSpec.latest)
|
||||
if(errors.length === 0) {
|
||||
|
||||
if(newStyle.glyphs !== this.state.mapStyle.glyphs) {
|
||||
this.updateFonts(newStyle.glyphs)
|
||||
}
|
||||
@@ -115,8 +228,6 @@ export default class App extends React.Component {
|
||||
this.updateIcons(newStyle.sprite)
|
||||
}
|
||||
|
||||
const errors = validateStyleMin(newStyle, GlSpec)
|
||||
if(errors.length === 0) {
|
||||
this.revisionStore.addRevision(newStyle)
|
||||
if(save) this.saveStyle(newStyle)
|
||||
this.setState({
|
||||
@@ -128,6 +239,8 @@ export default class App extends React.Component {
|
||||
errors: errors.map(err => err.message)
|
||||
})
|
||||
}
|
||||
|
||||
this.fetchSources();
|
||||
}
|
||||
|
||||
onUndo() {
|
||||
@@ -150,6 +263,24 @@ export default class App extends React.Component {
|
||||
})
|
||||
}
|
||||
|
||||
onMoveLayer(move) {
|
||||
let { oldIndex, newIndex } = move;
|
||||
let layers = this.state.mapStyle.layers;
|
||||
oldIndex = clamp(oldIndex, 0, layers.length-1);
|
||||
newIndex = clamp(newIndex, 0, layers.length-1);
|
||||
if(oldIndex === newIndex) return;
|
||||
|
||||
if (oldIndex === this.state.selectedLayerIndex) {
|
||||
this.setState({
|
||||
selectedLayerIndex: newIndex
|
||||
});
|
||||
}
|
||||
|
||||
layers = layers.slice(0);
|
||||
layers = arrayMove(layers, oldIndex, newIndex);
|
||||
this.onLayersChange(layers);
|
||||
}
|
||||
|
||||
onLayersChange(changedLayers) {
|
||||
const changedStyle = {
|
||||
...this.state.mapStyle,
|
||||
@@ -158,6 +289,40 @@ export default class App extends React.Component {
|
||||
this.onStyleChanged(changedStyle)
|
||||
}
|
||||
|
||||
onLayerDestroy(layerId) {
|
||||
let layers = this.state.mapStyle.layers;
|
||||
const remainingLayers = layers.slice(0);
|
||||
const idx = style.indexOfLayer(remainingLayers, layerId)
|
||||
remainingLayers.splice(idx, 1);
|
||||
this.onLayersChange(remainingLayers);
|
||||
}
|
||||
|
||||
onLayerCopy(layerId) {
|
||||
let layers = this.state.mapStyle.layers;
|
||||
const changedLayers = layers.slice(0)
|
||||
const idx = style.indexOfLayer(changedLayers, layerId)
|
||||
|
||||
const clonedLayer = cloneDeep(changedLayers[idx])
|
||||
clonedLayer.id = clonedLayer.id + "-copy"
|
||||
changedLayers.splice(idx, 0, clonedLayer)
|
||||
this.onLayersChange(changedLayers)
|
||||
}
|
||||
|
||||
onLayerVisibilityToggle(layerId) {
|
||||
let layers = this.state.mapStyle.layers;
|
||||
const changedLayers = layers.slice(0)
|
||||
const idx = style.indexOfLayer(changedLayers, layerId)
|
||||
|
||||
const layer = { ...changedLayers[idx] }
|
||||
const changedLayout = 'layout' in layer ? {...layer.layout} : {}
|
||||
changedLayout.visibility = changedLayout.visibility === 'none' ? 'visible' : 'none'
|
||||
|
||||
layer.layout = changedLayout
|
||||
changedLayers[idx] = layer
|
||||
this.onLayersChange(changedLayers)
|
||||
}
|
||||
|
||||
|
||||
onLayerIdChange(oldId, newId) {
|
||||
const changedLayers = this.state.mapStyle.layers.slice(0)
|
||||
const idx = style.indexOfLayer(changedLayers, oldId)
|
||||
@@ -184,25 +349,94 @@ export default class App extends React.Component {
|
||||
})
|
||||
}
|
||||
|
||||
fetchSources() {
|
||||
const sourceList = {...this.state.sources};
|
||||
|
||||
for(let [key, val] of Object.entries(this.state.mapStyle.sources)) {
|
||||
if(sourceList.hasOwnProperty(key)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
sourceList[key] = {
|
||||
type: val.type,
|
||||
layers: []
|
||||
};
|
||||
|
||||
if(!this.state.sources.hasOwnProperty(key) && val.type === "vector" && val.hasOwnProperty("url")) {
|
||||
let url = val.url;
|
||||
try {
|
||||
url = mapboxUtil.normalizeSourceURL(url, MapboxGl.accessToken);
|
||||
} catch(err) {
|
||||
console.warn("Failed to normalizeSourceURL: ", err);
|
||||
}
|
||||
|
||||
fetch(url)
|
||||
.then((response) => {
|
||||
return response.json();
|
||||
})
|
||||
.then((json) => {
|
||||
if(!json.hasOwnProperty("vector_layers")) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create new objects before setState
|
||||
const sources = Object.assign({}, this.state.sources);
|
||||
|
||||
for(let layer of json.vector_layers) {
|
||||
sources[key].layers.push(layer.id)
|
||||
}
|
||||
|
||||
console.debug("Updating source: "+key);
|
||||
this.setState({
|
||||
sources: sources
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("Failed to process sources for '%s'", url, err);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if(!isEqual(this.state.sources, sourceList)) {
|
||||
console.debug("Setting sources");
|
||||
this.setState({
|
||||
sources: sourceList
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
mapRenderer() {
|
||||
const mapProps = {
|
||||
mapStyle: style.replaceAccessToken(this.state.mapStyle),
|
||||
mapStyle: style.replaceAccessToken(this.state.mapStyle, {allowFallback: true}),
|
||||
options: this.state.mapOptions,
|
||||
onDataChange: (e) => {
|
||||
this.layerWatcher.analyzeMap(e.map)
|
||||
this.fetchSources();
|
||||
},
|
||||
}
|
||||
|
||||
const metadata = this.state.mapStyle.metadata || {}
|
||||
const renderer = metadata['maputnik:renderer'] || 'mbgljs'
|
||||
|
||||
let mapElement;
|
||||
|
||||
// Check if OL3 code has been loaded?
|
||||
if(renderer === 'ol3') {
|
||||
return <OpenLayers3Map {...mapProps} />
|
||||
mapElement = <OpenLayers3Map {...mapProps} />
|
||||
} else {
|
||||
return <MapboxGlMap {...mapProps}
|
||||
mapElement = <MapboxGlMap {...mapProps}
|
||||
inspectModeEnabled={this.state.inspectModeEnabled}
|
||||
highlightedLayer={this.state.mapStyle.layers[this.state.selectedLayerIndex]} />
|
||||
highlightedLayer={this.state.mapStyle.layers[this.state.selectedLayerIndex]}
|
||||
onLayerSelect={this.onLayerSelect.bind(this)} />
|
||||
}
|
||||
|
||||
const elementStyle = {
|
||||
"filter": `url('#${this.state.mapFilter}')`
|
||||
};
|
||||
|
||||
return <div style={elementStyle}>
|
||||
{mapElement}
|
||||
</div>
|
||||
}
|
||||
|
||||
onLayerSelect(layerId) {
|
||||
@@ -210,6 +444,19 @@ export default class App extends React.Component {
|
||||
this.setState({ selectedLayerIndex: idx })
|
||||
}
|
||||
|
||||
toggleModal(modalName) {
|
||||
this.setState({
|
||||
isOpen: {
|
||||
...this.state.isOpen,
|
||||
[modalName]: !this.state.isOpen[modalName]
|
||||
}
|
||||
})
|
||||
|
||||
if(modalName === 'survey') {
|
||||
localStorage.setItem('survey', '');
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const layers = this.state.mapStyle.layers || []
|
||||
const selectedLayer = layers.length > 0 ? layers[this.state.selectedLayerIndex] : null
|
||||
@@ -222,9 +469,14 @@ export default class App extends React.Component {
|
||||
onStyleChanged={this.onStyleChanged.bind(this)}
|
||||
onStyleOpen={this.onStyleChanged.bind(this)}
|
||||
onInspectModeToggle={this.changeInspectMode.bind(this)}
|
||||
onToggleModal={this.toggleModal.bind(this)}
|
||||
/>
|
||||
|
||||
const layerList = <LayerList
|
||||
onMoveLayer={this.onMoveLayer.bind(this)}
|
||||
onLayerDestroy={this.onLayerDestroy.bind(this)}
|
||||
onLayerCopy={this.onLayerCopy.bind(this)}
|
||||
onLayerVisibilityToggle={this.onLayerVisibilityToggle.bind(this)}
|
||||
onLayersChange={this.onLayersChange.bind(this)}
|
||||
onLayerSelect={this.onLayerSelect.bind(this)}
|
||||
selectedLayerIndex={this.state.selectedLayerIndex}
|
||||
@@ -234,10 +486,17 @@ export default class App extends React.Component {
|
||||
|
||||
const layerEditor = selectedLayer ? <LayerEditor
|
||||
layer={selectedLayer}
|
||||
layerIndex={this.state.selectedLayerIndex}
|
||||
isFirstLayer={this.state.selectedLayerIndex < 1}
|
||||
isLastLayer={this.state.selectedLayerIndex === this.state.mapStyle.layers.length-1}
|
||||
sources={this.state.sources}
|
||||
vectorLayers={this.state.vectorLayers}
|
||||
spec={this.state.spec}
|
||||
onMoveLayer={this.onMoveLayer.bind(this)}
|
||||
onLayerChanged={this.onLayerChanged.bind(this)}
|
||||
onLayerDestroy={this.onLayerDestroy.bind(this)}
|
||||
onLayerCopy={this.onLayerCopy.bind(this)}
|
||||
onLayerVisibilityToggle={this.onLayerVisibilityToggle.bind(this)}
|
||||
onLayerIdChange={this.onLayerIdChange.bind(this)}
|
||||
/> : null
|
||||
|
||||
@@ -246,12 +505,48 @@ export default class App extends React.Component {
|
||||
infos={this.state.infos}
|
||||
/> : null
|
||||
|
||||
|
||||
const modals = <div>
|
||||
<ShortcutsModal
|
||||
isOpen={this.state.isOpen.shortcuts}
|
||||
onOpenToggle={this.toggleModal.bind(this, 'shortcuts')}
|
||||
/>
|
||||
<SettingsModal
|
||||
mapStyle={this.state.mapStyle}
|
||||
onStyleChanged={this.onStyleChanged.bind(this)}
|
||||
isOpen={this.state.isOpen.settings}
|
||||
onOpenToggle={this.toggleModal.bind(this, 'settings')}
|
||||
/>
|
||||
<ExportModal
|
||||
mapStyle={this.state.mapStyle}
|
||||
onStyleChanged={this.onStyleChanged.bind(this)}
|
||||
isOpen={this.state.isOpen.export}
|
||||
onOpenToggle={this.toggleModal.bind(this, 'export')}
|
||||
/>
|
||||
<OpenModal
|
||||
isOpen={this.state.isOpen.open}
|
||||
onStyleOpen={this.onStyleChanged.bind(this)}
|
||||
onOpenToggle={this.toggleModal.bind(this, 'open')}
|
||||
/>
|
||||
<SourcesModal
|
||||
mapStyle={this.state.mapStyle}
|
||||
onStyleChanged={this.onStyleChanged.bind(this)}
|
||||
isOpen={this.state.isOpen.sources}
|
||||
onOpenToggle={this.toggleModal.bind(this, 'sources')}
|
||||
/>
|
||||
<SurveyModal
|
||||
isOpen={this.state.isOpen.survey}
|
||||
onOpenToggle={this.toggleModal.bind(this, 'survey')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
return <AppLayout
|
||||
toolbar={toolbar}
|
||||
layerList={layerList}
|
||||
layerEditor={layerEditor}
|
||||
map={this.mapRenderer()}
|
||||
bottom={bottomPanel}
|
||||
modals={modals}
|
||||
/>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import ScrollContainer from './ScrollContainer'
|
||||
|
||||
class AppLayout extends React.Component {
|
||||
static propTypes = {
|
||||
toolbar: React.PropTypes.element.isRequired,
|
||||
layerList: React.PropTypes.element.isRequired,
|
||||
layerEditor: React.PropTypes.element,
|
||||
map: React.PropTypes.element.isRequired,
|
||||
bottom: React.PropTypes.element,
|
||||
toolbar: PropTypes.element.isRequired,
|
||||
layerList: PropTypes.element.isRequired,
|
||||
layerEditor: PropTypes.element,
|
||||
map: PropTypes.element.isRequired,
|
||||
bottom: PropTypes.element,
|
||||
modals: PropTypes.node,
|
||||
}
|
||||
|
||||
static childContextTypes = {
|
||||
reactIconBase: React.PropTypes.object
|
||||
reactIconBase: PropTypes.object
|
||||
}
|
||||
|
||||
getChildContext() {
|
||||
@@ -38,6 +40,7 @@ class AppLayout extends React.Component {
|
||||
{this.props.bottom}
|
||||
</div>
|
||||
}
|
||||
{this.props.modals}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,26 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import classnames from 'classnames'
|
||||
|
||||
class Button extends React.Component {
|
||||
static propTypes = {
|
||||
onClick: React.PropTypes.func,
|
||||
style: React.PropTypes.object,
|
||||
className: React.PropTypes.string,
|
||||
"data-wd-key": PropTypes.string,
|
||||
"aria-label": PropTypes.string,
|
||||
onClick: PropTypes.func,
|
||||
style: PropTypes.object,
|
||||
className: PropTypes.string,
|
||||
children: PropTypes.node
|
||||
}
|
||||
|
||||
render() {
|
||||
return <a
|
||||
return <button
|
||||
onClick={this.props.onClick}
|
||||
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}>
|
||||
{this.props.children}
|
||||
</a>
|
||||
</button>
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
class MessagePanel extends React.Component {
|
||||
static propTypes = {
|
||||
errors: React.PropTypes.array,
|
||||
infos: React.PropTypes.array,
|
||||
errors: PropTypes.array,
|
||||
infos: PropTypes.array,
|
||||
}
|
||||
|
||||
render() {
|
||||
const errors = this.props.errors.map((m, i) => {
|
||||
return <p className="maputnik-message-panel-error">{m}</p>
|
||||
return <p key={"error-"+i} className="maputnik-message-panel-error">{m}</p>
|
||||
})
|
||||
|
||||
const infos = this.props.infos.map((m, i) => {
|
||||
return <p key={i}>{m}</p>
|
||||
return <p key={"info-"+i}>{m}</p>
|
||||
})
|
||||
|
||||
return <div className="maputnik-message-panel">
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
const ScrollContainer = (props) => {
|
||||
class ScrollContainer extends React.Component {
|
||||
static propTypes = {
|
||||
children: PropTypes.node
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div className="maputnik-scroll-container">
|
||||
{props.children}
|
||||
{this.props.children}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
export default ScrollContainer
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import FileReaderInput from 'react-file-reader-input'
|
||||
import classnames from 'classnames'
|
||||
|
||||
@@ -15,48 +16,95 @@ import MdInsertEmoticon from 'react-icons/lib/md/insert-emoticon'
|
||||
import MdFontDownload from 'react-icons/lib/md/font-download'
|
||||
import HelpIcon from 'react-icons/lib/md/help-outline'
|
||||
import InspectionIcon from 'react-icons/lib/md/find-in-page'
|
||||
import SurveyIcon from 'react-icons/lib/md/assignment-turned-in'
|
||||
|
||||
import logoImage from '../img/maputnik.png'
|
||||
import SettingsModal from './modals/SettingsModal'
|
||||
import ExportModal from './modals/ExportModal'
|
||||
import SourcesModal from './modals/SourcesModal'
|
||||
import OpenModal from './modals/OpenModal'
|
||||
import logoImage from 'maputnik-design/logos/logo-color.svg'
|
||||
import pkgJson from '../../package.json'
|
||||
|
||||
import style from '../libs/style'
|
||||
|
||||
function IconText(props) {
|
||||
return <span className="maputnik-icon-text">{props.children}</span>
|
||||
class IconText extends React.Component {
|
||||
static propTypes = {
|
||||
children: PropTypes.node,
|
||||
}
|
||||
|
||||
function ToolbarLink(props) {
|
||||
render() {
|
||||
return <span className="maputnik-icon-text">{this.props.children}</span>
|
||||
}
|
||||
}
|
||||
|
||||
class ToolbarLink extends React.Component {
|
||||
static propTypes = {
|
||||
className: PropTypes.string,
|
||||
children: PropTypes.node,
|
||||
href: PropTypes.string,
|
||||
onToggleModal: PropTypes.func,
|
||||
}
|
||||
|
||||
render() {
|
||||
return <a
|
||||
className={classnames('maputnik-toolbar-link', props.className)}
|
||||
href={props.href}
|
||||
target={"blank"}
|
||||
className={classnames('maputnik-toolbar-link', this.props.className)}
|
||||
href={this.props.href}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
{props.children}
|
||||
{this.props.children}
|
||||
</a>
|
||||
}
|
||||
}
|
||||
|
||||
function ToolbarAction(props) {
|
||||
class ToolbarLinkHighlighted extends React.Component {
|
||||
static propTypes = {
|
||||
className: PropTypes.string,
|
||||
children: PropTypes.node,
|
||||
href: PropTypes.string,
|
||||
onToggleModal: PropTypes.func
|
||||
}
|
||||
|
||||
render() {
|
||||
return <a
|
||||
className={classnames('maputnik-toolbar-link', "maputnik-toolbar-link--highlighted", this.props.className)}
|
||||
href={this.props.href}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
<span className="maputnik-toolbar-link-wrapper">
|
||||
{this.props.children}
|
||||
</span>
|
||||
</a>
|
||||
}
|
||||
}
|
||||
|
||||
class ToolbarAction extends React.Component {
|
||||
static propTypes = {
|
||||
children: PropTypes.node,
|
||||
onClick: PropTypes.func,
|
||||
wdKey: PropTypes.string
|
||||
}
|
||||
|
||||
render() {
|
||||
return <button
|
||||
className='maputnik-toolbar-action'
|
||||
onClick={props.onClick}
|
||||
data-wd-key={this.props.wdKey}
|
||||
onClick={this.props.onClick}
|
||||
>
|
||||
{props.children}
|
||||
</a>
|
||||
{this.props.children}
|
||||
</button>
|
||||
}
|
||||
}
|
||||
|
||||
export default class Toolbar extends React.Component {
|
||||
static propTypes = {
|
||||
mapStyle: React.PropTypes.object.isRequired,
|
||||
inspectModeEnabled: React.PropTypes.bool.isRequired,
|
||||
onStyleChanged: React.PropTypes.func.isRequired,
|
||||
mapStyle: PropTypes.object.isRequired,
|
||||
inspectModeEnabled: PropTypes.bool.isRequired,
|
||||
onStyleChanged: PropTypes.func.isRequired,
|
||||
// A new style has been uploaded
|
||||
onStyleOpen: React.PropTypes.func.isRequired,
|
||||
onStyleOpen: PropTypes.func.isRequired,
|
||||
// A dict of source id's and the available source layers
|
||||
sources: React.PropTypes.object.isRequired,
|
||||
onInspectModeToggle: React.PropTypes.func.isRequired
|
||||
sources: PropTypes.object.isRequired,
|
||||
onInspectModeToggle: PropTypes.func.isRequired,
|
||||
children: PropTypes.node,
|
||||
onToggleModal: PropTypes.func,
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
@@ -72,64 +120,45 @@ export default class Toolbar extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
toggleModal(modalName) {
|
||||
this.setState({
|
||||
isOpen: {
|
||||
...this.state.isOpen,
|
||||
[modalName]: !this.state.isOpen[modalName]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div className='maputnik-toolbar'>
|
||||
<SettingsModal
|
||||
mapStyle={this.props.mapStyle}
|
||||
onStyleChanged={this.props.onStyleChanged}
|
||||
isOpen={this.state.isOpen.settings}
|
||||
onOpenToggle={this.toggleModal.bind(this, 'settings')}
|
||||
/>
|
||||
<ExportModal
|
||||
mapStyle={this.props.mapStyle}
|
||||
onStyleChanged={this.props.onStyleChanged}
|
||||
isOpen={this.state.isOpen.export}
|
||||
onOpenToggle={this.toggleModal.bind(this, 'export')}
|
||||
/>
|
||||
<OpenModal
|
||||
isOpen={this.state.isOpen.open}
|
||||
onStyleOpen={this.props.onStyleOpen}
|
||||
onOpenToggle={this.toggleModal.bind(this, 'open')}
|
||||
/>
|
||||
<SourcesModal
|
||||
mapStyle={this.props.mapStyle}
|
||||
onStyleChanged={this.props.onStyleChanged}
|
||||
isOpen={this.state.isOpen.sources}
|
||||
onOpenToggle={this.toggleModal.bind(this, 'sources')}
|
||||
/>
|
||||
<ToolbarLink
|
||||
href={"https://github.com/maputnik/editor"}
|
||||
<div className="maputnik-toolbar__inner">
|
||||
<div
|
||||
className="maputnik-toolbar-logo-container"
|
||||
>
|
||||
<a className="maputnik-toolbar-skip" href="#skip-menu">
|
||||
Skip navigation
|
||||
</a>
|
||||
<a
|
||||
href="https://github.com/maputnik/editor"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
className="maputnik-toolbar-logo"
|
||||
>
|
||||
<img src={logoImage} alt="Maputnik" />
|
||||
<h1>Maputnik</h1>
|
||||
</ToolbarLink>
|
||||
<ToolbarAction onClick={this.toggleModal.bind(this, 'open')}>
|
||||
<h1>Maputnik
|
||||
<span className="maputnik-toolbar-version">v{pkgJson.version}</span>
|
||||
</h1>
|
||||
</a>
|
||||
</div>
|
||||
<div className="maputnik-toolbar__actions">
|
||||
<ToolbarAction wdKey="nav:open" onClick={this.props.onToggleModal.bind(this, 'open')}>
|
||||
<OpenIcon />
|
||||
<IconText>Open</IconText>
|
||||
</ToolbarAction>
|
||||
<ToolbarAction onClick={this.toggleModal.bind(this, 'export')}>
|
||||
<ToolbarAction wdKey="nav:export" onClick={this.props.onToggleModal.bind(this, 'export')}>
|
||||
<MdFileDownload />
|
||||
<IconText>Export</IconText>
|
||||
</ToolbarAction>
|
||||
<ToolbarAction onClick={this.toggleModal.bind(this, 'sources')}>
|
||||
<ToolbarAction wdKey="nav:sources" onClick={this.props.onToggleModal.bind(this, 'sources')}>
|
||||
<SourcesIcon />
|
||||
<IconText>Sources</IconText>
|
||||
<IconText>Data Sources</IconText>
|
||||
</ToolbarAction>
|
||||
<ToolbarAction onClick={this.toggleModal.bind(this, 'settings')}>
|
||||
<ToolbarAction wdKey="nav:settings" onClick={this.props.onToggleModal.bind(this, 'settings')}>
|
||||
<SettingsIcon />
|
||||
<IconText>Style Settings</IconText>
|
||||
</ToolbarAction>
|
||||
<ToolbarAction onClick={this.props.onInspectModeToggle}>
|
||||
<ToolbarAction wdKey="nav:inspect" onClick={this.props.onInspectModeToggle}>
|
||||
<InspectionIcon />
|
||||
<IconText>
|
||||
{ this.props.inspectModeEnabled && <span>Map Mode</span> }
|
||||
@@ -140,6 +169,12 @@ export default class Toolbar extends React.Component {
|
||||
<HelpIcon />
|
||||
<IconText>Help</IconText>
|
||||
</ToolbarLink>
|
||||
<ToolbarLinkHighlighted href={"https://gregorywolanski.typeform.com/to/cPgaSY"}>
|
||||
<SurveyIcon />
|
||||
<IconText>Take the Maputnik Survey</IconText>
|
||||
</ToolbarLinkHighlighted>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react'
|
||||
import Color from 'color'
|
||||
import ChromePicker from 'react-color/lib/components/chrome/Chrome'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
function formatColor(color) {
|
||||
const rgb = color.rgb
|
||||
@@ -10,12 +11,12 @@ function formatColor(color) {
|
||||
/*** Number fields with support for min, max and units and documentation*/
|
||||
class ColorField extends React.Component {
|
||||
static propTypes = {
|
||||
onChange: React.PropTypes.func.isRequired,
|
||||
name: React.PropTypes.string.isRequired,
|
||||
value: React.PropTypes.string,
|
||||
doc: React.PropTypes.string,
|
||||
style: React.PropTypes.object,
|
||||
default: React.PropTypes.string,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
value: PropTypes.string,
|
||||
doc: PropTypes.string,
|
||||
style: PropTypes.object,
|
||||
default: PropTypes.string,
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
@@ -29,7 +30,7 @@ class ColorField extends React.Component {
|
||||
//but I am too stupid to get it to work together with fixed position
|
||||
//and scrollbars so I have to fallback to JavaScript
|
||||
calcPickerOffset() {
|
||||
const elem = this.refs.colorInput
|
||||
const elem = this.colorInput
|
||||
if(elem) {
|
||||
const pos = elem.getBoundingClientRect()
|
||||
return {
|
||||
@@ -37,7 +38,6 @@ class ColorField extends React.Component {
|
||||
left: pos.left + 196,
|
||||
}
|
||||
} else {
|
||||
console.warn('Color field has no element to adjust position')
|
||||
return {
|
||||
top: 160,
|
||||
left: 555,
|
||||
@@ -50,11 +50,27 @@ class ColorField extends React.Component {
|
||||
}
|
||||
|
||||
get color() {
|
||||
return Color(this.props.value || '#fff')
|
||||
// Catch invalid color.
|
||||
try {
|
||||
return Color(this.props.value).rgb()
|
||||
}
|
||||
catch(err) {
|
||||
console.warn("Error parsing color: ", err);
|
||||
return Color("rgb(255,255,255)");
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const offset = this.calcPickerOffset()
|
||||
var currentColor = this.color.object()
|
||||
currentColor = {
|
||||
r: currentColor.r,
|
||||
g: currentColor.g,
|
||||
b: currentColor.b,
|
||||
// Rename alpha -> a for ChromePicker
|
||||
a: currentColor.alpha
|
||||
}
|
||||
|
||||
const picker = <div
|
||||
className="maputnik-color-picker-offset"
|
||||
style={{
|
||||
@@ -64,7 +80,7 @@ class ColorField extends React.Component {
|
||||
top: offset.top,
|
||||
}}>
|
||||
<ChromePicker
|
||||
color={this.color.object()}
|
||||
color={currentColor}
|
||||
onChange={c => this.props.onChange(formatColor(c))}
|
||||
/>
|
||||
<div
|
||||
@@ -81,11 +97,17 @@ class ColorField extends React.Component {
|
||||
/>
|
||||
</div>
|
||||
|
||||
var swatchStyle = {
|
||||
backgroundColor: this.props.value
|
||||
};
|
||||
|
||||
return <div className="maputnik-color-wrapper">
|
||||
{this.state.pickerOpened && picker}
|
||||
<div className="maputnik-color-swatch" style={swatchStyle}></div>
|
||||
<input
|
||||
spellCheck="false"
|
||||
className="maputnik-color"
|
||||
ref="colorInput"
|
||||
ref={(input) => this.colorInput = input}
|
||||
onClick={this.togglePicker.bind(this)}
|
||||
style={this.props.style}
|
||||
name={this.props.name}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
export default class DocLabel extends React.Component {
|
||||
static propTypes = {
|
||||
label: React.PropTypes.oneOfType([
|
||||
React.PropTypes.object,
|
||||
React.PropTypes.string
|
||||
label: PropTypes.oneOfType([
|
||||
PropTypes.object,
|
||||
PropTypes.string
|
||||
]).isRequired,
|
||||
doc: React.PropTypes.string.isRequired,
|
||||
doc: PropTypes.string.isRequired,
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
139
src/components/fields/FunctionSpecField.jsx
Normal file
139
src/components/fields/FunctionSpecField.jsx
Normal file
@@ -0,0 +1,139 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import SpecProperty from './_SpecProperty'
|
||||
import DataProperty from './_DataProperty'
|
||||
import ZoomProperty from './_ZoomProperty'
|
||||
|
||||
|
||||
function isZoomField(value) {
|
||||
return typeof value === 'object' && value.stops && typeof value.property === 'undefined'
|
||||
}
|
||||
|
||||
function isDataField(value) {
|
||||
return typeof value === 'object' && value.stops && typeof value.property !== 'undefined'
|
||||
}
|
||||
|
||||
/** Supports displaying spec field for zoom function objects
|
||||
* https://www.mapbox.com/mapbox-gl-style-spec/#types-function-zoom-property
|
||||
*/
|
||||
export default class FunctionSpecProperty extends React.Component {
|
||||
static propTypes = {
|
||||
onChange: PropTypes.func.isRequired,
|
||||
fieldName: PropTypes.string.isRequired,
|
||||
fieldSpec: PropTypes.object.isRequired,
|
||||
|
||||
value: PropTypes.oneOfType([
|
||||
PropTypes.object,
|
||||
PropTypes.string,
|
||||
PropTypes.number,
|
||||
PropTypes.bool,
|
||||
PropTypes.array
|
||||
]),
|
||||
}
|
||||
|
||||
addStop() {
|
||||
const stops = this.props.value.stops.slice(0)
|
||||
const lastStop = stops[stops.length - 1]
|
||||
if (typeof lastStop[0] === "object") {
|
||||
stops.push([
|
||||
{zoom: lastStop[0].zoom + 1, value: lastStop[0].value},
|
||||
lastStop[1]
|
||||
])
|
||||
}
|
||||
else {
|
||||
stops.push([lastStop[0] + 1, lastStop[1]])
|
||||
}
|
||||
|
||||
const changedValue = {
|
||||
...this.props.value,
|
||||
stops: stops,
|
||||
}
|
||||
|
||||
this.props.onChange(this.props.fieldName, changedValue)
|
||||
}
|
||||
|
||||
deleteStop(stopIdx) {
|
||||
const stops = this.props.value.stops.slice(0)
|
||||
stops.splice(stopIdx, 1)
|
||||
|
||||
let changedValue = {
|
||||
...this.props.value,
|
||||
stops: stops,
|
||||
}
|
||||
|
||||
if(stops.length === 1) {
|
||||
changedValue = stops[0][1]
|
||||
}
|
||||
|
||||
this.props.onChange(this.props.fieldName, changedValue)
|
||||
}
|
||||
|
||||
makeZoomFunction() {
|
||||
const zoomFunc = {
|
||||
stops: [
|
||||
[6, this.props.value],
|
||||
[10, this.props.value]
|
||||
]
|
||||
}
|
||||
this.props.onChange(this.props.fieldName, zoomFunc)
|
||||
}
|
||||
|
||||
makeDataFunction() {
|
||||
const dataFunc = {
|
||||
property: "",
|
||||
type: "categorical",
|
||||
stops: [
|
||||
[{zoom: 6, value: 0}, this.props.value],
|
||||
[{zoom: 10, value: 0}, this.props.value]
|
||||
]
|
||||
}
|
||||
this.props.onChange(this.props.fieldName, dataFunc)
|
||||
}
|
||||
|
||||
render() {
|
||||
const propClass = this.props.fieldSpec.default === this.props.value ? "maputnik-default-property" : "maputnik-modified-property"
|
||||
let specField;
|
||||
|
||||
if (isZoomField(this.props.value)) {
|
||||
specField = (
|
||||
<ZoomProperty
|
||||
onChange={this.props.onChange.bind(this)}
|
||||
fieldName={this.props.fieldName}
|
||||
fieldSpec={this.props.fieldSpec}
|
||||
value={this.props.value}
|
||||
onDeleteStop={this.deleteStop.bind(this)}
|
||||
onAddStop={this.addStop.bind(this)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
else if (isDataField(this.props.value)) {
|
||||
specField = (
|
||||
<DataProperty
|
||||
onChange={this.props.onChange.bind(this)}
|
||||
fieldName={this.props.fieldName}
|
||||
fieldSpec={this.props.fieldSpec}
|
||||
value={this.props.value}
|
||||
onDeleteStop={this.deleteStop.bind(this)}
|
||||
onAddStop={this.addStop.bind(this)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
else {
|
||||
specField = (
|
||||
<SpecProperty
|
||||
onChange={this.props.onChange.bind(this)}
|
||||
fieldName={this.props.fieldName}
|
||||
fieldSpec={this.props.fieldSpec}
|
||||
value={this.props.value}
|
||||
onZoomClick={this.makeZoomFunction.bind(this)}
|
||||
onDataClick={this.makeDataFunction.bind(this)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
return <div className={propClass} data-wd-key={"spec-field:"+this.props.fieldName}>
|
||||
{specField}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import ZoomSpecField from './ZoomSpecField'
|
||||
import FunctionSpecField from './FunctionSpecField'
|
||||
const iconProperties = ['background-pattern', 'fill-pattern', 'line-pattern', 'fill-extrusion-pattern', 'icon-image']
|
||||
|
||||
/** Extract field spec by {@fieldName} from the {@layerType} in the
|
||||
@@ -35,10 +36,10 @@ function getGroupName(spec, layerType, fieldName) {
|
||||
|
||||
export default class PropertyGroup extends React.Component {
|
||||
static propTypes = {
|
||||
layer: React.PropTypes.object.isRequired,
|
||||
groupFields: React.PropTypes.array.isRequired,
|
||||
onChange: React.PropTypes.func.isRequired,
|
||||
spec: React.PropTypes.object.isRequired,
|
||||
layer: PropTypes.object.isRequired,
|
||||
groupFields: PropTypes.array.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
spec: PropTypes.object.isRequired,
|
||||
}
|
||||
|
||||
onPropertyChange(property, newValue) {
|
||||
@@ -54,7 +55,7 @@ export default class PropertyGroup extends React.Component {
|
||||
const layout = this.props.layer.layout || {}
|
||||
const fieldValue = fieldName in paint ? paint[fieldName] : layout[fieldName]
|
||||
|
||||
return <ZoomSpecField
|
||||
return <FunctionSpecField
|
||||
onChange={this.onPropertyChange.bind(this)}
|
||||
key={fieldName}
|
||||
fieldName={fieldName}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import color from 'color'
|
||||
|
||||
import ColorField from './ColorField'
|
||||
@@ -8,6 +9,7 @@ import StringInput from '../inputs/StringInput'
|
||||
import SelectInput from '../inputs/SelectInput'
|
||||
import MultiButtonInput from '../inputs/MultiButtonInput'
|
||||
import ArrayInput from '../inputs/ArrayInput'
|
||||
import DynamicArrayInput from '../inputs/DynamicArrayInput'
|
||||
import FontInput from '../inputs/FontInput'
|
||||
import IconInput from '../inputs/IconInput'
|
||||
import capitalize from 'lodash.capitalize'
|
||||
@@ -35,16 +37,17 @@ function optionsLabelLength(options) {
|
||||
* to display @{value}. */
|
||||
export default class SpecField extends React.Component {
|
||||
static propTypes = {
|
||||
onChange: React.PropTypes.func.isRequired,
|
||||
fieldName: React.PropTypes.string.isRequired,
|
||||
fieldSpec: React.PropTypes.object.isRequired,
|
||||
value: React.PropTypes.oneOfType([
|
||||
React.PropTypes.string,
|
||||
React.PropTypes.number,
|
||||
React.PropTypes.array,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
fieldName: PropTypes.string.isRequired,
|
||||
fieldSpec: PropTypes.object.isRequired,
|
||||
value: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.number,
|
||||
PropTypes.array,
|
||||
PropTypes.bool
|
||||
]),
|
||||
/** Override the style of the field */
|
||||
style: React.PropTypes.object,
|
||||
style: PropTypes.object,
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -55,6 +58,8 @@ export default class SpecField extends React.Component {
|
||||
name: this.props.fieldName,
|
||||
onChange: newValue => this.props.onChange(this.props.fieldName, newValue)
|
||||
}
|
||||
|
||||
function childNodes() {
|
||||
switch(this.props.fieldSpec.type) {
|
||||
case 'number': return (
|
||||
<NumberInput
|
||||
@@ -105,13 +110,27 @@ export default class SpecField extends React.Component {
|
||||
fonts={this.props.fieldSpec.values}
|
||||
/>
|
||||
} else {
|
||||
if (this.props.fieldSpec.length) {
|
||||
return <ArrayInput
|
||||
{...commonProps}
|
||||
type={this.props.fieldSpec.value}
|
||||
length={this.props.fieldSpec.length}
|
||||
/>
|
||||
} else {
|
||||
return <DynamicArrayInput
|
||||
{...commonProps}
|
||||
type={this.props.fieldSpec.value}
|
||||
/>
|
||||
}
|
||||
}
|
||||
default: return null
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div data-wd-key={"spec-field:"+this.props.fieldName}>
|
||||
{childNodes.call(this)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,180 +0,0 @@
|
||||
import React from 'react'
|
||||
import Color from 'color'
|
||||
|
||||
import Button from '../Button'
|
||||
import SpecField from './SpecField'
|
||||
import NumberInput from '../inputs/NumberInput'
|
||||
import DocLabel from './DocLabel'
|
||||
import InputBlock from '../inputs/InputBlock'
|
||||
|
||||
import AddIcon from 'react-icons/lib/md/add-circle-outline'
|
||||
import DeleteIcon from 'react-icons/lib/md/delete'
|
||||
import FunctionIcon from 'react-icons/lib/md/functions'
|
||||
|
||||
import capitalize from 'lodash.capitalize'
|
||||
|
||||
function isZoomField(value) {
|
||||
return typeof value === 'object' && value.stops
|
||||
}
|
||||
|
||||
/** Supports displaying spec field for zoom function objects
|
||||
* https://www.mapbox.com/mapbox-gl-style-spec/#types-function-zoom-property
|
||||
*/
|
||||
export default class ZoomSpecProperty extends React.Component {
|
||||
static propTypes = {
|
||||
onChange: React.PropTypes.func.isRequired,
|
||||
fieldName: React.PropTypes.string.isRequired,
|
||||
fieldSpec: React.PropTypes.object.isRequired,
|
||||
|
||||
value: React.PropTypes.oneOfType([
|
||||
React.PropTypes.object,
|
||||
React.PropTypes.string,
|
||||
React.PropTypes.number,
|
||||
React.PropTypes.bool,
|
||||
]),
|
||||
}
|
||||
|
||||
addStop() {
|
||||
const stops = this.props.value.stops.slice(0)
|
||||
const lastStop = stops[stops.length - 1]
|
||||
stops.push([lastStop[0] + 1, lastStop[1]])
|
||||
|
||||
const changedValue = {
|
||||
...this.props.value,
|
||||
stops: stops,
|
||||
}
|
||||
|
||||
this.props.onChange(this.props.fieldName, changedValue)
|
||||
}
|
||||
|
||||
deleteStop(stopIdx) {
|
||||
const stops = this.props.value.stops.slice(0)
|
||||
stops.splice(stopIdx, 1)
|
||||
|
||||
let changedValue = {
|
||||
...this.props.value,
|
||||
stops: stops,
|
||||
}
|
||||
|
||||
if(stops.length === 1) {
|
||||
changedValue = stops[0][1]
|
||||
}
|
||||
|
||||
this.props.onChange(this.props.fieldName, changedValue)
|
||||
}
|
||||
|
||||
makeZoomFunction() {
|
||||
const zoomFunc = {
|
||||
stops: [
|
||||
[6, this.props.value],
|
||||
[10, this.props.value]
|
||||
]
|
||||
}
|
||||
this.props.onChange(this.props.fieldName, zoomFunc)
|
||||
}
|
||||
|
||||
changeStop(changeIdx, zoomLevel, value) {
|
||||
const stops = this.props.value.stops.slice(0)
|
||||
stops[changeIdx] = [zoomLevel, value]
|
||||
const changedValue = {
|
||||
...this.props.value,
|
||||
stops: stops,
|
||||
}
|
||||
this.props.onChange(this.props.fieldName, changedValue)
|
||||
}
|
||||
|
||||
renderZoomProperty() {
|
||||
const zoomFields = this.props.value.stops.map((stop, idx) => {
|
||||
const zoomLevel = stop[0]
|
||||
const value = stop[1]
|
||||
const deleteStopBtn= <DeleteStopButton onClick={this.deleteStop.bind(this, idx)} />
|
||||
|
||||
return <InputBlock
|
||||
key={zoomLevel}
|
||||
doc={this.props.fieldSpec.doc}
|
||||
label={labelFromFieldName(this.props.fieldName)}
|
||||
action={deleteStopBtn}
|
||||
>
|
||||
<div>
|
||||
<div className="maputnik-zoom-spec-property-stop-edit">
|
||||
<NumberInput
|
||||
value={zoomLevel}
|
||||
onChange={changedStop => this.changeStop(idx, changedStop, value)}
|
||||
min={0}
|
||||
max={22}
|
||||
/>
|
||||
</div>
|
||||
<div className="maputnik-zoom-spec-property-stop-value">
|
||||
<SpecField
|
||||
fieldName={this.props.fieldName}
|
||||
fieldSpec={this.props.fieldSpec}
|
||||
value={value}
|
||||
onChange={(_, newValue) => this.changeStop(idx, zoomLevel, newValue)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</InputBlock>
|
||||
})
|
||||
|
||||
return <div className="maputnik-zoom-spec-property">
|
||||
{zoomFields}
|
||||
<Button
|
||||
className="maputnik-add-stop"
|
||||
onClick={this.addStop.bind(this)}
|
||||
>
|
||||
Add stop
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
|
||||
renderProperty() {
|
||||
let zoomBtn = null
|
||||
if(this.props.fieldSpec['zoom-function']) {
|
||||
zoomBtn = <MakeZoomFunctionButton onClick={this.makeZoomFunction.bind(this)} />
|
||||
}
|
||||
return <InputBlock
|
||||
doc={this.props.fieldSpec.doc}
|
||||
label={labelFromFieldName(this.props.fieldName)}
|
||||
action={zoomBtn}
|
||||
>
|
||||
<SpecField {...this.props} />
|
||||
</InputBlock>
|
||||
}
|
||||
|
||||
render() {
|
||||
const propClass = this.props.fieldSpec.default === this.props.value ? "maputnik-default-property" : "maputnik-modified-property"
|
||||
return <div className={propClass}>
|
||||
{isZoomField(this.props.value) ? this.renderZoomProperty() : this.renderProperty()}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
function MakeZoomFunctionButton(props) {
|
||||
return <Button
|
||||
className="maputnik-make-zoom-function"
|
||||
onClick={props.onClick}
|
||||
>
|
||||
<DocLabel
|
||||
label={<FunctionIcon />}
|
||||
cursorTargetStyle={{ cursor: 'pointer' }}
|
||||
doc={"Turn property into a zoom function to enable a map feature to change with map's zoom level."}
|
||||
/>
|
||||
</Button>
|
||||
}
|
||||
|
||||
function DeleteStopButton(props) {
|
||||
return <Button
|
||||
className="maputnik-delete-stop"
|
||||
onClick={props.onClick}
|
||||
>
|
||||
<DocLabel
|
||||
label={<DeleteIcon />}
|
||||
doc={"Remove zoom level stop."}
|
||||
/>
|
||||
</Button>
|
||||
}
|
||||
|
||||
function labelFromFieldName(fieldName) {
|
||||
let label = fieldName.split('-').slice(1).join(' ')
|
||||
return capitalize(label)
|
||||
}
|
||||
182
src/components/fields/_DataProperty.jsx
Normal file
182
src/components/fields/_DataProperty.jsx
Normal file
@@ -0,0 +1,182 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import Button from '../Button'
|
||||
import SpecField from './SpecField'
|
||||
import NumberInput from '../inputs/NumberInput'
|
||||
import StringInput from '../inputs/StringInput'
|
||||
import SelectInput from '../inputs/SelectInput'
|
||||
import DocLabel from './DocLabel'
|
||||
import InputBlock from '../inputs/InputBlock'
|
||||
|
||||
import labelFromFieldName from './_labelFromFieldName'
|
||||
import DeleteStopButton from './_DeleteStopButton'
|
||||
|
||||
|
||||
export default class DataProperty extends React.Component {
|
||||
static propTypes = {
|
||||
onChange: PropTypes.func,
|
||||
onDeleteStop: PropTypes.func,
|
||||
onAddStop: PropTypes.func,
|
||||
fieldName: PropTypes.string,
|
||||
fieldSpec: PropTypes.object,
|
||||
value: PropTypes.oneOfType([
|
||||
PropTypes.object,
|
||||
PropTypes.string,
|
||||
PropTypes.number,
|
||||
PropTypes.bool,
|
||||
PropTypes.array
|
||||
]),
|
||||
}
|
||||
|
||||
getFieldFunctionType(fieldSpec) {
|
||||
if (fieldSpec.function === "interpolated") {
|
||||
return "exponential"
|
||||
}
|
||||
if (fieldSpec.type === "number") {
|
||||
return "interval"
|
||||
}
|
||||
return "categorical"
|
||||
}
|
||||
|
||||
getDataFunctionTypes(functionType) {
|
||||
if (functionType === "interpolated") {
|
||||
return ["categorical", "interval", "exponential"]
|
||||
}
|
||||
else {
|
||||
return ["categorical", "interval"]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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 changedValue = {
|
||||
...this.props.value,
|
||||
stops: stops,
|
||||
}
|
||||
this.props.onChange(this.props.fieldName, changedValue)
|
||||
}
|
||||
|
||||
changeDataProperty(propName, propVal) {
|
||||
if (propVal) {
|
||||
this.props.value[propName] = propVal
|
||||
}
|
||||
else {
|
||||
delete this.props.value[propName]
|
||||
}
|
||||
this.props.onChange(this.props.fieldName, this.props.value)
|
||||
}
|
||||
|
||||
render() {
|
||||
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)} />
|
||||
|
||||
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 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)}
|
||||
/>
|
||||
</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.function)}
|
||||
/>
|
||||
</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">
|
||||
<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}
|
||||
<Button
|
||||
className="maputnik-add-stop"
|
||||
onClick={this.props.onAddStop.bind(this)}
|
||||
>
|
||||
Add stop
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
25
src/components/fields/_DeleteStopButton.jsx
Normal file
25
src/components/fields/_DeleteStopButton.jsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import DocLabel from './DocLabel'
|
||||
import Button from '../Button'
|
||||
import DeleteIcon from 'react-icons/lib/md/delete'
|
||||
|
||||
|
||||
export default class DeleteStopButton extends React.Component {
|
||||
static propTypes = {
|
||||
onClick: PropTypes.func,
|
||||
}
|
||||
|
||||
render() {
|
||||
return <Button
|
||||
className="maputnik-delete-stop"
|
||||
onClick={this.props.onClick}
|
||||
>
|
||||
<DocLabel
|
||||
label={<DeleteIcon />}
|
||||
doc={"Remove zoom level stop."}
|
||||
/>
|
||||
</Button>
|
||||
}
|
||||
}
|
||||
49
src/components/fields/_FunctionButtons.jsx
Normal file
49
src/components/fields/_FunctionButtons.jsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import DocLabel from './DocLabel'
|
||||
import Button from '../Button'
|
||||
import FunctionIcon from 'react-icons/lib/md/functions'
|
||||
import MdInsertChart from 'react-icons/lib/md/insert-chart'
|
||||
|
||||
|
||||
export default class FunctionButtons extends React.Component {
|
||||
static propTypes = {
|
||||
fieldSpec: PropTypes.object,
|
||||
onZoomClick: PropTypes.func,
|
||||
onDataClick: PropTypes.func,
|
||||
}
|
||||
|
||||
render() {
|
||||
let makeZoomButton, makeDataButton
|
||||
if (this.props.fieldSpec['zoom-function']) {
|
||||
makeZoomButton = <Button
|
||||
className="maputnik-make-zoom-function"
|
||||
onClick={this.props.onZoomClick}
|
||||
>
|
||||
<DocLabel
|
||||
label={<FunctionIcon />}
|
||||
cursorTargetStyle={{ cursor: 'pointer' }}
|
||||
doc={"Turn property into a zoom function to enable a map feature to change with map's zoom level."}
|
||||
/>
|
||||
</Button>
|
||||
|
||||
if (this.props.fieldSpec['property-function'] && ['piecewise-constant', 'interpolated'].indexOf(this.props.fieldSpec['function']) !== -1) {
|
||||
makeDataButton = <Button
|
||||
className="maputnik-make-data-function"
|
||||
onClick={this.props.onDataClick}
|
||||
>
|
||||
<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."}
|
||||
/>
|
||||
</Button>
|
||||
}
|
||||
return <div>{makeDataButton}{makeZoomButton}</div>
|
||||
}
|
||||
else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
34
src/components/fields/_SpecProperty.jsx
Normal file
34
src/components/fields/_SpecProperty.jsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import SpecField from './SpecField'
|
||||
import FunctionButtons from './_FunctionButtons'
|
||||
import InputBlock from '../inputs/InputBlock'
|
||||
|
||||
import labelFromFieldName from './_labelFromFieldName'
|
||||
|
||||
|
||||
export default class SpecProperty extends React.Component {
|
||||
static propTypes = {
|
||||
onZoomClick: PropTypes.func.isRequired,
|
||||
onDataClick: PropTypes.func.isRequired,
|
||||
fieldName: PropTypes.string,
|
||||
fieldSpec: PropTypes.object
|
||||
}
|
||||
|
||||
render() {
|
||||
const functionBtn = <FunctionButtons
|
||||
fieldSpec={this.props.fieldSpec}
|
||||
onZoomClick={this.props.onZoomClick}
|
||||
onDataClick={this.props.onDataClick}
|
||||
/>
|
||||
|
||||
return <InputBlock
|
||||
doc={this.props.fieldSpec.doc}
|
||||
label={labelFromFieldName(this.props.fieldName)}
|
||||
action={functionBtn}
|
||||
>
|
||||
<SpecField {...this.props} />
|
||||
</InputBlock>
|
||||
}
|
||||
}
|
||||
161
src/components/fields/_ZoomProperty.jsx
Normal file
161
src/components/fields/_ZoomProperty.jsx
Normal file
@@ -0,0 +1,161 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import Button from '../Button'
|
||||
import SpecField from './SpecField'
|
||||
import NumberInput from '../inputs/NumberInput'
|
||||
import InputBlock from '../inputs/InputBlock'
|
||||
|
||||
import DeleteStopButton from './_DeleteStopButton'
|
||||
import labelFromFieldName from './_labelFromFieldName'
|
||||
|
||||
import docUid from '../../libs/document-uid'
|
||||
import sortNumerically from '../../libs/sort-numerically'
|
||||
|
||||
|
||||
export default class ZoomProperty extends React.Component {
|
||||
static propTypes = {
|
||||
onChange: PropTypes.func,
|
||||
onDeleteStop: PropTypes.func,
|
||||
onAddStop: PropTypes.func,
|
||||
fieldName: PropTypes.string,
|
||||
fieldSpec: PropTypes.object,
|
||||
value: PropTypes.oneOfType([
|
||||
PropTypes.object,
|
||||
PropTypes.string,
|
||||
PropTypes.number,
|
||||
PropTypes.bool,
|
||||
PropTypes.array
|
||||
]),
|
||||
}
|
||||
|
||||
|
||||
constructor() {
|
||||
super()
|
||||
this.state = {
|
||||
refs: {}
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.setState({
|
||||
refs: this.setStopRefs(this.props)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* We cache a reference for each stop by its index.
|
||||
*
|
||||
* When the stops are reordered the references are also updated (see this.orderStops) this allows React to use the same key for the element and keep keyboard focus.
|
||||
*/
|
||||
setStopRefs(props) {
|
||||
// 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(!this.state.refs.hasOwnProperty(idx)) {
|
||||
if(!newRefs) {
|
||||
newRefs = {...this.state.refs};
|
||||
}
|
||||
newRefs[idx] = docUid("stop-");
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return newRefs;
|
||||
}
|
||||
|
||||
UNSAFE_componentWillReceiveProps(nextProps) {
|
||||
const newRefs = this.setStopRefs(nextProps);
|
||||
if(newRefs) {
|
||||
this.setState({
|
||||
refs: newRefs
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 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], b.data[0]));
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
changeZoomStop(changeIdx, stopData, value) {
|
||||
const stops = this.props.value.stops.slice(0);
|
||||
stops[changeIdx] = [stopData, value];
|
||||
|
||||
const orderedStops = this.orderStopsByZoom(stops);
|
||||
|
||||
const changedValue = {
|
||||
...this.props.value,
|
||||
stops: orderedStops
|
||||
}
|
||||
this.props.onChange(this.props.fieldName, changedValue)
|
||||
}
|
||||
|
||||
render() {
|
||||
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)} />
|
||||
|
||||
return <InputBlock
|
||||
key={key}
|
||||
doc={this.props.fieldSpec.doc}
|
||||
label={labelFromFieldName(this.props.fieldName)}
|
||||
action={deleteStopBtn}
|
||||
>
|
||||
<div>
|
||||
<div className="maputnik-zoom-spec-property-stop-edit">
|
||||
<NumberInput
|
||||
value={zoomLevel}
|
||||
onChange={changedStop => this.changeZoomStop(idx, changedStop, value)}
|
||||
min={0}
|
||||
max={22}
|
||||
/>
|
||||
</div>
|
||||
<div className="maputnik-zoom-spec-property-stop-value">
|
||||
<SpecField
|
||||
fieldName={this.props.fieldName}
|
||||
fieldSpec={this.props.fieldSpec}
|
||||
value={value}
|
||||
onChange={(_, newValue) => this.changeZoomStop(idx, zoomLevel, newValue)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</InputBlock>
|
||||
});
|
||||
|
||||
return <div className="maputnik-zoom-spec-property">
|
||||
{zoomFields}
|
||||
<Button
|
||||
className="maputnik-add-stop"
|
||||
onClick={this.props.onAddStop.bind(this)}
|
||||
>
|
||||
Add stop
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
6
src/components/fields/_labelFromFieldName.js
Normal file
6
src/components/fields/_labelFromFieldName.js
Normal file
@@ -0,0 +1,6 @@
|
||||
import capitalize from 'lodash.capitalize'
|
||||
|
||||
export default function labelFromFieldName(fieldName) {
|
||||
let label = fieldName.split('-').slice(1).join(' ')
|
||||
return capitalize(label)
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { combiningFilterOps } from '../../libs/filterops.js'
|
||||
|
||||
import GlSpec from 'mapbox-gl-style-spec/reference/latest.js'
|
||||
import * as styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
|
||||
import DocLabel from '../fields/DocLabel'
|
||||
import SelectInput from '../inputs/SelectInput'
|
||||
import SingleFilterEditor from './SingleFilterEditor'
|
||||
@@ -26,9 +27,9 @@ function hasNestedCombiningFilter(filter) {
|
||||
export default class CombiningFilterEditor extends React.Component {
|
||||
static propTypes = {
|
||||
/** Properties of the vector layer and the available fields */
|
||||
properties: React.PropTypes.object,
|
||||
filter: React.PropTypes.array,
|
||||
onChange: React.PropTypes.func.isRequired,
|
||||
properties: PropTypes.object,
|
||||
filter: PropTypes.array,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
// Convert filter to combining filter
|
||||
@@ -88,10 +89,10 @@ export default class CombiningFilterEditor extends React.Component {
|
||||
}
|
||||
|
||||
return <div className="maputnik-filter-editor">
|
||||
<div className="maputnik-filter-editor-compound-select">
|
||||
<div className="maputnik-filter-editor-compound-select" data-wd-key="layer-filter">
|
||||
<DocLabel
|
||||
label={"Compound Filter"}
|
||||
doc={GlSpec.layer.filter.doc + " Combine multiple filters together by using a compound filter."}
|
||||
doc={styleSpec.latest.layer.filter.doc + " Combine multiple filters together by using a compound filter."}
|
||||
/>
|
||||
<SelectInput
|
||||
value={combiningOp}
|
||||
@@ -102,6 +103,7 @@ export default class CombiningFilterEditor extends React.Component {
|
||||
{editorBlocks}
|
||||
<div className="maputnik-filter-editor-add-wrapper">
|
||||
<Button
|
||||
data-wd-key="layer-filter-button"
|
||||
className="maputnik-add-filter"
|
||||
onClick={this.addFilterItem.bind(this)}>
|
||||
Add filter
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import Button from '../Button'
|
||||
import DeleteIcon from 'react-icons/lib/md/delete'
|
||||
|
||||
class FilterEditorBlock extends React.Component {
|
||||
static propTypes = {
|
||||
onDelete: React.PropTypes.func.isRequired,
|
||||
children: React.PropTypes.element.isRequired,
|
||||
onDelete: PropTypes.func.isRequired,
|
||||
children: PropTypes.element.isRequired,
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import { otherFilterOps } from '../../libs/filterops.js'
|
||||
import StringInput from '../inputs/StringInput'
|
||||
@@ -11,11 +12,34 @@ function tryParseInt(v) {
|
||||
return parseFloat(v)
|
||||
}
|
||||
|
||||
function tryParseBool(v) {
|
||||
const isString = (typeof(v) === "string");
|
||||
if(!isString) {
|
||||
return v;
|
||||
}
|
||||
|
||||
if(v.match(/^\s*true\s*$/)) {
|
||||
return true;
|
||||
}
|
||||
else if(v.match(/^\s*false\s*$/)) {
|
||||
return false;
|
||||
}
|
||||
else {
|
||||
return v;
|
||||
}
|
||||
}
|
||||
|
||||
function parseFilter(v) {
|
||||
v = tryParseInt(v);
|
||||
v = tryParseBool(v);
|
||||
return v;
|
||||
}
|
||||
|
||||
class SingleFilterEditor extends React.Component {
|
||||
static propTypes = {
|
||||
filter: React.PropTypes.array.isRequired,
|
||||
onChange: React.PropTypes.func.isRequired,
|
||||
properties: React.PropTypes.object,
|
||||
filter: PropTypes.array.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
properties: PropTypes.object,
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
@@ -23,7 +47,7 @@ class SingleFilterEditor extends React.Component {
|
||||
}
|
||||
|
||||
onFilterPartChanged(filterOp, propertyName, filterArgs) {
|
||||
let newFilter = [filterOp, propertyName, ...filterArgs.map(tryParseInt)]
|
||||
let newFilter = [filterOp, propertyName, ...filterArgs.map(parseFilter)]
|
||||
if(filterOp === 'has' || filterOp === '!has') {
|
||||
newFilter = [filterOp, propertyName]
|
||||
} else if(filterArgs.length === 0) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import LineIcon from './LineIcon.jsx'
|
||||
import FillIcon from './FillIcon.jsx'
|
||||
@@ -8,8 +9,8 @@ import CircleIcon from './CircleIcon.jsx'
|
||||
|
||||
class LayerIcon extends React.Component {
|
||||
static propTypes = {
|
||||
type: React.PropTypes.string.isRequired,
|
||||
style: React.PropTypes.object,
|
||||
type: PropTypes.string.isRequired,
|
||||
style: PropTypes.object,
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -17,12 +18,13 @@ class LayerIcon extends React.Component {
|
||||
switch(this.props.type) {
|
||||
case 'fill-extrusion': return <BackgroundIcon {...iconProps} />
|
||||
case 'raster': return <FillIcon {...iconProps} />
|
||||
case 'hillshade': return <FillIcon {...iconProps} />
|
||||
case 'heatmap': return <FillIcon {...iconProps} />
|
||||
case 'fill': return <FillIcon {...iconProps} />
|
||||
case 'background': return <BackgroundIcon {...iconProps} />
|
||||
case 'line': return <LineIcon {...iconProps} />
|
||||
case 'symbol': return <SymbolIcon {...iconProps} />
|
||||
case 'circle': return <CircleIcon {...iconProps} />
|
||||
default: return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ export default class FillIcon extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<IconBase viewBox="0 0 20 20" {...this.props}>
|
||||
<path id="path8" d="M 12.34,1.29 C 12.5114,1.1076 12.7497,1.0029 13,1 13.5523,1 14,1.4477 14,2 14.0047,2.2478 13.907,2.4866 13.73,2.66 9.785626,6.5516986 6.6148407,9.7551593 2.65,13.72 2.4793,13.8963 2.2453,13.9971 2,14 1.4477,14 1,13.5523 1,13 0.9953,12.7522 1.093,12.5134 1.27,12.34 4.9761967,8.7018093 9.0356422,4.5930579 12.34,1.29 Z" transform="translate(2,2)" />
|
||||
<path d="M 12.34,1.29 C 12.5114,1.1076 12.7497,1.0029 13,1 13.5523,1 14,1.4477 14,2 14.0047,2.2478 13.907,2.4866 13.73,2.66 9.785626,6.5516986 6.6148407,9.7551593 2.65,13.72 2.4793,13.8963 2.2453,13.9971 2,14 1.4477,14 1,13.5523 1,13 0.9953,12.7522 1.093,12.5134 1.27,12.34 4.9761967,8.7018093 9.0356422,4.5930579 12.34,1.29 Z" transform="translate(2,2)" />
|
||||
</IconBase>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@ export default class SymbolIcon extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<IconBase viewBox="0 0 20 20" {...this.props}>
|
||||
<g id="svg_1" transform="matrix(1.2718518,0,0,1.2601269,16.559526,-7.4065264)">
|
||||
<path id="svg_2" d="m -9.7959773,11.060163 c -0.3734787,-0.724437 -0.3580577,-1.2147051 -0.00547,-1.8767873 l 8.6034029,-0.019416 c 0.39670292,0.6865644 0.38365934,1.4750693 -0.011097,1.8864953 l -3.1359613,-0.0033 -0.013695,7.1305 c -0.4055357,0.397083 -1.3146432,0.397083 -1.7769191,-0.02274 l 0.030226,-7.104422 z" />
|
||||
<g transform="matrix(1.2718518,0,0,1.2601269,16.559526,-7.4065264)">
|
||||
<path d="m -9.7959773,11.060163 c -0.3734787,-0.724437 -0.3580577,-1.2147051 -0.00547,-1.8767873 l 8.6034029,-0.019416 c 0.39670292,0.6865644 0.38365934,1.4750693 -0.011097,1.8864953 l -3.1359613,-0.0033 -0.013695,7.1305 c -0.4055357,0.397083 -1.3146432,0.397083 -1.7769191,-0.02274 l 0.030226,-7.104422 z" />
|
||||
</g>
|
||||
</IconBase>
|
||||
)
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import StringInput from './StringInput'
|
||||
import NumberInput from './NumberInput'
|
||||
|
||||
class ArrayInput extends React.Component {
|
||||
static propTypes = {
|
||||
value: React.PropTypes.array,
|
||||
type: React.PropTypes.string,
|
||||
length: React.PropTypes.number,
|
||||
default: React.PropTypes.array,
|
||||
onChange: React.PropTypes.func,
|
||||
value: PropTypes.array,
|
||||
type: PropTypes.string,
|
||||
length: PropTypes.number,
|
||||
default: PropTypes.array,
|
||||
onChange: PropTypes.func,
|
||||
}
|
||||
|
||||
changeValue(idx, newValue) {
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import classnames from 'classnames'
|
||||
import Autocomplete from 'react-autocomplete'
|
||||
|
||||
|
||||
const MAX_HEIGHT = 140;
|
||||
|
||||
class AutocompleteInput extends React.Component {
|
||||
static propTypes = {
|
||||
value: React.PropTypes.string,
|
||||
options: React.PropTypes.array,
|
||||
onChange: React.PropTypes.func,
|
||||
value: PropTypes.string,
|
||||
options: PropTypes.array,
|
||||
onChange: PropTypes.func,
|
||||
keepMenuWithinWindowBounds: PropTypes.bool
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
@@ -15,23 +19,61 @@ class AutocompleteInput extends React.Component {
|
||||
options: [],
|
||||
}
|
||||
|
||||
render() {
|
||||
const AutocompleteMenu = (items, value, style) => <div className={"maputnik-autocomplete-menu"} children={items} />
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
maxHeight: MAX_HEIGHT
|
||||
};
|
||||
}
|
||||
|
||||
return <Autocomplete
|
||||
calcMaxHeight() {
|
||||
if(this.props.keepMenuWithinWindowBounds) {
|
||||
const maxHeight = window.innerHeight - this.autocompleteMenuEl.getBoundingClientRect().top;
|
||||
const limitedMaxHeight = Math.min(maxHeight, MAX_HEIGHT);
|
||||
|
||||
if(limitedMaxHeight != this.state.maxHeight) {
|
||||
this.setState({
|
||||
maxHeight: limitedMaxHeight
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
componentDidMount() {
|
||||
this.calcMaxHeight();
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this.calcMaxHeight();
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div
|
||||
ref={(el) => {
|
||||
this.autocompleteMenuEl = el;
|
||||
}}
|
||||
>
|
||||
<Autocomplete
|
||||
menuStyle={{
|
||||
position: "fixed",
|
||||
overflow: "auto",
|
||||
maxHeight: this.state.maxHeight
|
||||
}}
|
||||
wrapperProps={{
|
||||
className: "maputnik-autocomplete",
|
||||
style: null
|
||||
}}
|
||||
renderMenu={AutocompleteMenu}
|
||||
inputProps={{
|
||||
className: "maputnik-string"
|
||||
className: "maputnik-string",
|
||||
spellCheck: false
|
||||
}}
|
||||
value={this.props.value}
|
||||
items={this.props.options}
|
||||
getItemValue={(item) => item[0]}
|
||||
onSelect={v => this.props.onChange(v)}
|
||||
onChange={(e, v) => this.props.onChange(v)}
|
||||
shouldItemRender={(item, value) => {
|
||||
return item[0].toLowerCase().indexOf(value.toLowerCase()) > -1
|
||||
}}
|
||||
renderItem={(item, isHighlighted) => (
|
||||
<div
|
||||
key={item[0]}
|
||||
@@ -44,6 +86,7 @@ class AutocompleteInput extends React.Component {
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
class CheckboxInput extends React.Component {
|
||||
static propTypes = {
|
||||
value: React.PropTypes.bool.isRequired,
|
||||
style: React.PropTypes.object,
|
||||
onChange: React.PropTypes.func,
|
||||
value: PropTypes.bool.isRequired,
|
||||
style: PropTypes.object,
|
||||
onChange: PropTypes.func,
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
106
src/components/inputs/DynamicArrayInput.jsx
Normal file
106
src/components/inputs/DynamicArrayInput.jsx
Normal file
@@ -0,0 +1,106 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import StringInput from './StringInput'
|
||||
import NumberInput from './NumberInput'
|
||||
import Button from '../Button'
|
||||
import DeleteIcon from 'react-icons/lib/md/delete'
|
||||
import DocLabel from '../fields/DocLabel'
|
||||
|
||||
|
||||
class DynamicArrayInput extends React.Component {
|
||||
static propTypes = {
|
||||
value: PropTypes.array,
|
||||
type: PropTypes.string,
|
||||
default: PropTypes.array,
|
||||
onChange: PropTypes.func,
|
||||
style: PropTypes.object,
|
||||
}
|
||||
|
||||
changeValue(idx, newValue) {
|
||||
console.log(idx, newValue)
|
||||
const values = this.values.slice(0)
|
||||
values[idx] = newValue
|
||||
this.props.onChange(values)
|
||||
}
|
||||
|
||||
get values() {
|
||||
return this.props.value || this.props.default || []
|
||||
}
|
||||
|
||||
addValue() {
|
||||
const values = this.values.slice(0)
|
||||
if (this.props.type === 'number') {
|
||||
values.push(0)
|
||||
} else {
|
||||
values.push("")
|
||||
}
|
||||
|
||||
|
||||
this.props.onChange(values)
|
||||
}
|
||||
|
||||
deleteValue(valueIdx) {
|
||||
const values = this.values.slice(0)
|
||||
values.splice(valueIdx, 1)
|
||||
|
||||
this.props.onChange(values)
|
||||
}
|
||||
|
||||
render() {
|
||||
const inputs = this.values.map((v, i) => {
|
||||
const deleteValueBtn= <DeleteValueButton onClick={this.deleteValue.bind(this, i)} />
|
||||
const input = this.props.type === 'number'
|
||||
? <NumberInput
|
||||
value={v}
|
||||
onChange={this.changeValue.bind(this, i)}
|
||||
/>
|
||||
: <StringInput
|
||||
value={v}
|
||||
onChange={this.changeValue.bind(this, i)}
|
||||
/>
|
||||
|
||||
return <div
|
||||
style={this.props.style}
|
||||
key={i}
|
||||
className="maputnik-array-block"
|
||||
>
|
||||
<div className="maputnik-array-block-action">
|
||||
{deleteValueBtn}
|
||||
</div>
|
||||
<div className="maputnik-array-block-content">
|
||||
{input}
|
||||
</div>
|
||||
</div>
|
||||
})
|
||||
|
||||
return <div className="maputnik-array">
|
||||
{inputs}
|
||||
<Button
|
||||
className="maputnik-array-add-value"
|
||||
onClick={this.addValue.bind(this)}
|
||||
>
|
||||
Add value
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
class DeleteValueButton extends React.Component {
|
||||
static propTypes = {
|
||||
onClick: PropTypes.func,
|
||||
}
|
||||
|
||||
render() {
|
||||
return <Button
|
||||
className="maputnik-delete-stop"
|
||||
onClick={this.props.onClick}
|
||||
>
|
||||
<DocLabel
|
||||
label={<DeleteIcon />}
|
||||
doc={"Remove array entry."}
|
||||
/>
|
||||
</Button>
|
||||
}
|
||||
}
|
||||
|
||||
export default DynamicArrayInput
|
||||
@@ -1,12 +1,14 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import AutocompleteInput from './AutocompleteInput'
|
||||
|
||||
class FontInput extends React.Component {
|
||||
static propTypes = {
|
||||
value: React.PropTypes.array.isRequired,
|
||||
fonts: React.PropTypes.array,
|
||||
style: React.PropTypes.object,
|
||||
onChange: React.PropTypes.func.isRequired,
|
||||
value: PropTypes.array.isRequired,
|
||||
default: PropTypes.array,
|
||||
fonts: PropTypes.array,
|
||||
style: PropTypes.object,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import AutocompleteInput from './AutocompleteInput'
|
||||
|
||||
|
||||
class IconInput extends React.Component {
|
||||
static propTypes = {
|
||||
value: React.PropTypes.array,
|
||||
icons: React.PropTypes.array,
|
||||
style: React.PropTypes.object,
|
||||
onChange: React.PropTypes.func.isRequired,
|
||||
value: PropTypes.array,
|
||||
icons: PropTypes.array,
|
||||
style: PropTypes.object,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
|
||||
@@ -1,18 +1,21 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import classnames from 'classnames'
|
||||
import DocLabel from '../fields/DocLabel'
|
||||
|
||||
/** Wrap a component with a label */
|
||||
class InputBlock extends React.Component {
|
||||
static propTypes = {
|
||||
label: React.PropTypes.oneOfType([
|
||||
React.PropTypes.string,
|
||||
React.PropTypes.element,
|
||||
"data-wd-key": PropTypes.string,
|
||||
label: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.element,
|
||||
]).isRequired,
|
||||
doc: React.PropTypes.string,
|
||||
action: React.PropTypes.element,
|
||||
children: React.PropTypes.element.isRequired,
|
||||
style: React.PropTypes.object,
|
||||
doc: PropTypes.string,
|
||||
action: PropTypes.element,
|
||||
children: PropTypes.node.isRequired,
|
||||
style: PropTypes.object,
|
||||
onChange: PropTypes.func,
|
||||
}
|
||||
|
||||
onChange(e) {
|
||||
@@ -22,6 +25,7 @@ class InputBlock extends React.Component {
|
||||
|
||||
render() {
|
||||
return <div style={this.props.style}
|
||||
data-wd-key={this.props["data-wd-key"]}
|
||||
className={classnames({
|
||||
"maputnik-input-block": true,
|
||||
"maputnik-action-block": this.props.action
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import classnames from 'classnames'
|
||||
import Button from '../Button'
|
||||
|
||||
class MultiButtonInput extends React.Component {
|
||||
static propTypes = {
|
||||
value: React.PropTypes.string.isRequired,
|
||||
options: React.PropTypes.array.isRequired,
|
||||
onChange: React.PropTypes.func.isRequired,
|
||||
value: PropTypes.string.isRequired,
|
||||
options: PropTypes.array.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
class NumberInput extends React.Component {
|
||||
static propTypes = {
|
||||
value: React.PropTypes.number,
|
||||
default: React.PropTypes.number,
|
||||
min: React.PropTypes.number,
|
||||
max: React.PropTypes.number,
|
||||
onChange: React.PropTypes.func,
|
||||
value: PropTypes.number,
|
||||
default: PropTypes.number,
|
||||
min: PropTypes.number,
|
||||
max: PropTypes.number,
|
||||
onChange: PropTypes.func,
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
@@ -16,7 +17,7 @@ class NumberInput extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
UNSAFE_componentWillReceiveProps(nextProps) {
|
||||
this.setState({ value: nextProps.value })
|
||||
}
|
||||
|
||||
@@ -66,6 +67,7 @@ class NumberInput extends React.Component {
|
||||
|
||||
render() {
|
||||
return <input
|
||||
spellCheck="false"
|
||||
className="maputnik-number"
|
||||
placeholder={this.props.default}
|
||||
value={this.state.value}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
class SelectInput extends React.Component {
|
||||
static propTypes = {
|
||||
value: React.PropTypes.string.isRequired,
|
||||
options: React.PropTypes.array.isRequired,
|
||||
style: React.PropTypes.object,
|
||||
onChange: React.PropTypes.func.isRequired,
|
||||
value: PropTypes.string.isRequired,
|
||||
"data-wd-key": PropTypes.string,
|
||||
options: PropTypes.array.isRequired,
|
||||
style: PropTypes.object,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +19,7 @@ class SelectInput extends React.Component {
|
||||
|
||||
return <select
|
||||
className="maputnik-select"
|
||||
data-wd-key={this.props["data-wd-key"]}
|
||||
style={this.props.style}
|
||||
value={this.props.value}
|
||||
onChange={e => this.props.onChange(e.target.value)}
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
class StringInput extends React.Component {
|
||||
static propTypes = {
|
||||
value: React.PropTypes.string,
|
||||
style: React.PropTypes.object,
|
||||
default: React.PropTypes.string,
|
||||
onChange: React.PropTypes.func,
|
||||
"data-wd-key": PropTypes.string,
|
||||
value: PropTypes.string,
|
||||
style: PropTypes.object,
|
||||
default: PropTypes.string,
|
||||
onChange: PropTypes.func,
|
||||
multi: PropTypes.bool,
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
@@ -15,21 +18,44 @@ class StringInput extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
UNSAFE_componentWillReceiveProps(nextProps) {
|
||||
this.setState({ value: nextProps.value || '' })
|
||||
}
|
||||
|
||||
render() {
|
||||
return <input
|
||||
className="maputnik-string"
|
||||
style={this.props.style}
|
||||
value={this.state.value}
|
||||
placeholder={this.props.default}
|
||||
onChange={e => this.setState({ value: e.target.value })}
|
||||
onBlur={() => {
|
||||
let tag;
|
||||
let classes;
|
||||
|
||||
if(!!this.props.multi) {
|
||||
tag = "textarea"
|
||||
classes = [
|
||||
"maputnik-string",
|
||||
"maputnik-string--multi"
|
||||
]
|
||||
}
|
||||
else {
|
||||
tag = "input"
|
||||
classes = [
|
||||
"maputnik-string"
|
||||
]
|
||||
}
|
||||
|
||||
return React.createElement(tag, {
|
||||
"data-wd-key": this.props["data-wd-key"],
|
||||
spellCheck: !(tag === "input"),
|
||||
className: classes.join(" "),
|
||||
style: this.props.style,
|
||||
value: this.state.value,
|
||||
placeholder: this.props.default,
|
||||
onChange: e => {
|
||||
this.setState({
|
||||
value: e.target.value
|
||||
})
|
||||
},
|
||||
onBlur: () => {
|
||||
if(this.state.value!==this.props.value) this.props.onChange(this.state.value)
|
||||
}}
|
||||
/>
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
30
src/components/layers/Collapse.jsx
Normal file
30
src/components/layers/Collapse.jsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import Collapse from 'react-collapse'
|
||||
import accessibility from '../../libs/accessibility'
|
||||
|
||||
|
||||
export default class CollapseAlt extends React.Component {
|
||||
static propTypes = {
|
||||
isActive: PropTypes.bool.isRequired,
|
||||
children: PropTypes.element.isRequired
|
||||
}
|
||||
|
||||
render() {
|
||||
if (accessibility.reducedMotionEnabled()) {
|
||||
return (
|
||||
<div style={{display: this.props.isActive ? "block" : "none"}}>
|
||||
{this.props.children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
else {
|
||||
return (
|
||||
<Collapse isOpened={this.props.isActive}>
|
||||
{this.props.children}
|
||||
</Collapse>
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import CollapseOpenIcon from 'react-icons/lib/md/arrow-drop-down'
|
||||
import CollapseCloseIcon from 'react-icons/lib/md/arrow-drop-up'
|
||||
|
||||
export default class Collapser extends React.Component {
|
||||
static propTypes = {
|
||||
isCollapsed: React.PropTypes.bool.isRequired,
|
||||
style: React.PropTypes.object,
|
||||
isCollapsed: PropTypes.bool.isRequired,
|
||||
style: PropTypes.object,
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
29
src/components/layers/CommentBlock.jsx
Normal file
29
src/components/layers/CommentBlock.jsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import InputBlock from '../inputs/InputBlock'
|
||||
import StringInput from '../inputs/StringInput'
|
||||
|
||||
class MetadataBlock extends React.Component {
|
||||
static propTypes = {
|
||||
value: PropTypes.string,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
render() {
|
||||
return <InputBlock
|
||||
label={"Comments"}
|
||||
doc={"Comments for the current layer. This is non-standard and not in the spec."}
|
||||
data-wd-key="layer-comment"
|
||||
>
|
||||
<StringInput
|
||||
multi={true}
|
||||
value={this.props.value}
|
||||
onChange={this.props.onChange}
|
||||
default="Comment..."
|
||||
/>
|
||||
</InputBlock>
|
||||
}
|
||||
}
|
||||
|
||||
export default MetadataBlock
|
||||
@@ -1,19 +1,25 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import CodeMirror from 'react-codemirror'
|
||||
import {Controlled as CodeMirror} from 'react-codemirror2'
|
||||
import InputBlock from '../inputs/InputBlock'
|
||||
import StringInput from '../inputs/StringInput'
|
||||
import SelectInput from '../inputs/SelectInput'
|
||||
|
||||
import 'codemirror/mode/javascript/javascript'
|
||||
import 'codemirror/addon/lint/lint'
|
||||
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'
|
||||
|
||||
|
||||
class JSONEditor extends React.Component {
|
||||
static propTypes = {
|
||||
layer: React.PropTypes.object.isRequired,
|
||||
onChange: React.PropTypes.func,
|
||||
layer: PropTypes.object.isRequired,
|
||||
onChange: PropTypes.func,
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
@@ -23,7 +29,7 @@ class JSONEditor extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
UNSAFE_componentWillReceiveProps(nextProps) {
|
||||
this.setState({
|
||||
code: JSON.stringify(nextProps.layer, null, 2)
|
||||
})
|
||||
@@ -66,13 +72,15 @@ class JSONEditor extends React.Component {
|
||||
tabSize: 2,
|
||||
theme: 'maputnik',
|
||||
viewportMargin: Infinity,
|
||||
lineNumbers: false,
|
||||
lineNumbers: true,
|
||||
lint: true,
|
||||
gutters: ["CodeMirror-lint-markers"],
|
||||
scrollbarStyle: "null",
|
||||
}
|
||||
|
||||
return <CodeMirror
|
||||
value={this.state.code}
|
||||
onChange={this.onCodeUpdate.bind(this)}
|
||||
onBeforeChange={(editor, data, value) => this.onCodeUpdate(value)}
|
||||
onFocusChange={focused => focused ? true : this.resetValue()}
|
||||
options={codeMirrorOptions}
|
||||
/>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { Wrapper, Button, Menu, MenuItem } from 'react-aria-menubutton'
|
||||
|
||||
import JSONEditor from './JSONEditor'
|
||||
import FilterEditor from '../filter/FilterEditor'
|
||||
@@ -8,20 +10,18 @@ import LayerTypeBlock from './LayerTypeBlock'
|
||||
import LayerIdBlock from './LayerIdBlock'
|
||||
import MinZoomBlock from './MinZoomBlock'
|
||||
import MaxZoomBlock from './MaxZoomBlock'
|
||||
import CommentBlock from './CommentBlock'
|
||||
import LayerSourceBlock from './LayerSourceBlock'
|
||||
import LayerSourceLayerBlock from './LayerSourceLayerBlock'
|
||||
|
||||
import MoreVertIcon from 'react-icons/lib/md/more-vert'
|
||||
|
||||
import InputBlock from '../inputs/InputBlock'
|
||||
import MultiButtonInput from '../inputs/MultiButtonInput'
|
||||
|
||||
import { changeType, changeProperty } from '../../libs/layer'
|
||||
import layout from '../../config/layout.json'
|
||||
|
||||
class UnsupportedLayer extends React.Component {
|
||||
render() {
|
||||
return <div></div>
|
||||
}
|
||||
}
|
||||
|
||||
function layoutGroups(layerType) {
|
||||
const layerGroup = {
|
||||
@@ -42,12 +42,19 @@ function layoutGroups(layerType) {
|
||||
/** Layer editor supporting multiple types of layers. */
|
||||
export default class LayerEditor extends React.Component {
|
||||
static propTypes = {
|
||||
layer: React.PropTypes.object.isRequired,
|
||||
sources: React.PropTypes.object,
|
||||
vectorLayers: React.PropTypes.object,
|
||||
spec: React.PropTypes.object.isRequired,
|
||||
onLayerChanged: React.PropTypes.func,
|
||||
onLayerIdChange: React.PropTypes.func,
|
||||
layer: PropTypes.object.isRequired,
|
||||
sources: PropTypes.object,
|
||||
vectorLayers: PropTypes.object,
|
||||
spec: PropTypes.object.isRequired,
|
||||
onLayerChanged: PropTypes.func,
|
||||
onLayerIdChange: PropTypes.func,
|
||||
onMoveLayer: PropTypes.func,
|
||||
onLayerDestroy: PropTypes.func,
|
||||
onLayerCopy: PropTypes.func,
|
||||
onLayerVisibilityToggle: PropTypes.func,
|
||||
isFirstLayer: PropTypes.bool,
|
||||
isLastLayer: PropTypes.bool,
|
||||
layerIndex: PropTypes.number,
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
@@ -57,7 +64,7 @@ export default class LayerEditor extends React.Component {
|
||||
}
|
||||
|
||||
static childContextTypes = {
|
||||
reactIconBase: React.PropTypes.object
|
||||
reactIconBase: PropTypes.object
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
@@ -72,7 +79,7 @@ export default class LayerEditor extends React.Component {
|
||||
this.state = { editorGroups }
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
UNSAFE_componentWillReceiveProps(nextProps) {
|
||||
const additionalGroups = { ...this.state.editorGroups }
|
||||
|
||||
layout[nextProps.layer.type].groups.forEach(group => {
|
||||
@@ -110,10 +117,21 @@ export default class LayerEditor extends React.Component {
|
||||
}
|
||||
|
||||
renderGroupType(type, fields) {
|
||||
let comment = ""
|
||||
if(this.props.layer.metadata) {
|
||||
comment = this.props.layer.metadata['maputnik:comment']
|
||||
}
|
||||
|
||||
let sourceLayerIds;
|
||||
if(this.props.sources.hasOwnProperty(this.props.layer.source)) {
|
||||
sourceLayerIds = this.props.sources[this.props.layer.source].layers;
|
||||
}
|
||||
|
||||
switch(type) {
|
||||
case 'layer': return <div>
|
||||
<LayerIdBlock
|
||||
value={this.props.layer.id}
|
||||
wdKey="layer-editor.layer-id"
|
||||
onChange={newId => this.props.onLayerIdChange(this.props.layer.id, newId)}
|
||||
/>
|
||||
<LayerTypeBlock
|
||||
@@ -126,8 +144,9 @@ export default class LayerEditor extends React.Component {
|
||||
onChange={v => this.changeProperty(null, 'source', v)}
|
||||
/>
|
||||
}
|
||||
{this.props.layer.type !== 'raster' && this.props.layer.type !== 'background' && <LayerSourceLayerBlock
|
||||
sourceLayerIds={this.props.sources[this.props.layer.source]}
|
||||
{['background', 'raster', 'hillshade', 'heatmap'].indexOf(this.state.type) < 0 &&
|
||||
<LayerSourceLayerBlock
|
||||
sourceLayerIds={sourceLayerIds}
|
||||
value={this.props.layer['source-layer']}
|
||||
onChange={v => this.changeProperty(null, 'source-layer', v)}
|
||||
/>
|
||||
@@ -140,6 +159,10 @@ export default class LayerEditor extends React.Component {
|
||||
value={this.props.layer.maxzoom}
|
||||
onChange={v => this.changeProperty(null, 'maxzoom', v)}
|
||||
/>
|
||||
<CommentBlock
|
||||
value={comment}
|
||||
onChange={v => this.changeProperty('metadata', 'maputnik:comment', v == "" ? undefined : v)}
|
||||
/>
|
||||
</div>
|
||||
case 'filter': return <div>
|
||||
<div className="maputnik-filter-editor-wrapper">
|
||||
@@ -160,16 +183,23 @@ export default class LayerEditor extends React.Component {
|
||||
layer={this.props.layer}
|
||||
onChange={this.props.onLayerChanged}
|
||||
/>
|
||||
default: return null
|
||||
}
|
||||
}
|
||||
|
||||
moveLayer(offset) {
|
||||
this.props.onMoveLayer({
|
||||
oldIndex: this.props.layerIndex,
|
||||
newIndex: this.props.layerIndex+offset
|
||||
})
|
||||
}
|
||||
|
||||
render() {
|
||||
const layerType = this.props.layer.type
|
||||
const groups = layoutGroups(layerType).filter(group => {
|
||||
return !(layerType === 'background' && group.type === 'source')
|
||||
}).map(group => {
|
||||
return <LayerEditorGroup
|
||||
data-wd-key={group.title}
|
||||
key={group.title}
|
||||
title={group.title}
|
||||
isActive={this.state.editorGroups[group.title]}
|
||||
@@ -179,8 +209,73 @@ export default class LayerEditor extends React.Component {
|
||||
</LayerEditorGroup>
|
||||
})
|
||||
|
||||
const layout = this.props.layer.layout || {}
|
||||
|
||||
const items = {
|
||||
delete: {
|
||||
text: "Delete",
|
||||
handler: () => this.props.onLayerDestroy(this.props.layer.id)
|
||||
},
|
||||
duplicate: {
|
||||
text: "Duplicate",
|
||||
handler: () => this.props.onLayerCopy(this.props.layer.id)
|
||||
},
|
||||
hide: {
|
||||
text: (layout.visibility === "none") ? "Show" : "Hide",
|
||||
handler: () => this.props.onLayerVisibilityToggle(this.props.layer.id)
|
||||
},
|
||||
moveLayerUp: {
|
||||
text: "Move layer up",
|
||||
// Not actually used...
|
||||
disabled: this.props.isFirstLayer,
|
||||
handler: () => this.moveLayer(-1)
|
||||
},
|
||||
moveLayerDown: {
|
||||
text: "Move layer down",
|
||||
// Not actually used...
|
||||
disabled: this.props.isLastLayer,
|
||||
handler: () => this.moveLayer(+1)
|
||||
}
|
||||
}
|
||||
|
||||
function handleSelection(id, event) {
|
||||
event.stopPropagation;
|
||||
items[id].handler();
|
||||
}
|
||||
|
||||
return <div className="maputnik-layer-editor"
|
||||
>
|
||||
<header>
|
||||
<div className="layer-header">
|
||||
<h2 className="layer-header__title">
|
||||
Layer: {this.props.layer.id}
|
||||
</h2>
|
||||
<div className="layer-header__info">
|
||||
<Wrapper
|
||||
className='more-menu'
|
||||
onSelection={handleSelection}
|
||||
closeOnSelection={false}
|
||||
>
|
||||
<Button className='more-menu__button'>
|
||||
<MoreVertIcon className="more-menu__button__svg" />
|
||||
</Button>
|
||||
<Menu>
|
||||
<ul className="more-menu__menu">
|
||||
{Object.keys(items).map((id, idx) => {
|
||||
const item = items[id];
|
||||
return <li key={id}>
|
||||
<MenuItem value={id} className='more-menu__menu__item'>
|
||||
{item.text}
|
||||
</MenuItem>
|
||||
</li>
|
||||
})}
|
||||
</ul>
|
||||
</Menu>
|
||||
</Wrapper>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</header>
|
||||
{groups}
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -1,26 +1,32 @@
|
||||
import React from 'react'
|
||||
import Collapse from 'react-collapse'
|
||||
import PropTypes from 'prop-types'
|
||||
import Collapser from './Collapser'
|
||||
import Collapse from './Collapse'
|
||||
|
||||
|
||||
export default class LayerEditorGroup extends React.Component {
|
||||
static propTypes = {
|
||||
title: React.PropTypes.string.isRequired,
|
||||
isActive: React.PropTypes.bool.isRequired,
|
||||
children: React.PropTypes.element.isRequired,
|
||||
onActiveToggle: React.PropTypes.func.isRequired
|
||||
"data-wd-key": PropTypes.string,
|
||||
title: PropTypes.string.isRequired,
|
||||
isActive: PropTypes.bool.isRequired,
|
||||
children: PropTypes.element.isRequired,
|
||||
onActiveToggle: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div>
|
||||
<div className="maputnik-layer-editor-group"
|
||||
data-wd-key={"layer-editor-group:"+this.props["data-wd-key"]}
|
||||
onClick={e => this.props.onActiveToggle(!this.props.isActive)}
|
||||
>
|
||||
<span>{this.props.title}</span>
|
||||
<span style={{flexGrow: 1}} />
|
||||
<Collapser isCollapsed={this.props.isActive} />
|
||||
</div>
|
||||
<Collapse isOpened={this.props.isActive}>
|
||||
<Collapse isActive={this.props.isActive}>
|
||||
<div className="react-collapse-container">
|
||||
{this.props.children}
|
||||
</div>
|
||||
</Collapse>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -1,17 +1,21 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import GlSpec from 'mapbox-gl-style-spec/reference/latest.js'
|
||||
import * as styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
|
||||
import InputBlock from '../inputs/InputBlock'
|
||||
import StringInput from '../inputs/StringInput'
|
||||
|
||||
class LayerIdBlock extends React.Component {
|
||||
static propTypes = {
|
||||
value: React.PropTypes.string.isRequired,
|
||||
onChange: React.PropTypes.func.isRequired,
|
||||
value: PropTypes.string.isRequired,
|
||||
wdKey: PropTypes.string.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
render() {
|
||||
return <InputBlock label={"ID"} doc={GlSpec.layer.id.doc}>
|
||||
return <InputBlock label={"ID"} doc={styleSpec.latest.layer.id.doc}
|
||||
data-wd-key={this.props.wdKey}
|
||||
>
|
||||
<StringInput
|
||||
value={this.props.value}
|
||||
onChange={this.props.onChange}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import classnames from 'classnames'
|
||||
import cloneDeep from 'lodash.clonedeep'
|
||||
|
||||
import Button from '../Button'
|
||||
import LayerListGroup from './LayerListGroup'
|
||||
@@ -9,14 +9,14 @@ import AddIcon from 'react-icons/lib/md/add-circle-outline'
|
||||
import AddModal from '../modals/AddModal'
|
||||
|
||||
import style from '../../libs/style.js'
|
||||
import {SortableContainer, SortableHandle, arrayMove} from 'react-sortable-hoc';
|
||||
import {SortableContainer, SortableHandle} from 'react-sortable-hoc';
|
||||
|
||||
const layerListPropTypes = {
|
||||
layers: React.PropTypes.array.isRequired,
|
||||
selectedLayerIndex: React.PropTypes.number.isRequired,
|
||||
onLayersChange: React.PropTypes.func.isRequired,
|
||||
onLayerSelect: React.PropTypes.func,
|
||||
sources: React.PropTypes.object.isRequired,
|
||||
layers: PropTypes.array.isRequired,
|
||||
selectedLayerIndex: PropTypes.number.isRequired,
|
||||
onLayersChange: PropTypes.func.isRequired,
|
||||
onLayerSelect: PropTypes.func,
|
||||
sources: PropTypes.object.isRequired,
|
||||
}
|
||||
|
||||
function layerPrefix(name) {
|
||||
@@ -49,42 +49,13 @@ class LayerListContainer extends React.Component {
|
||||
super(props)
|
||||
this.state = {
|
||||
collapsedGroups: {},
|
||||
areAllGroupsExpanded: false,
|
||||
isOpen: {
|
||||
add: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onLayerDestroy(layerId) {
|
||||
const remainingLayers = this.props.layers.slice(0)
|
||||
const idx = style.indexOfLayer(remainingLayers, layerId)
|
||||
remainingLayers.splice(idx, 1);
|
||||
this.props.onLayersChange(remainingLayers)
|
||||
}
|
||||
|
||||
onLayerCopy(layerId) {
|
||||
const changedLayers = this.props.layers.slice(0)
|
||||
const idx = style.indexOfLayer(changedLayers, layerId)
|
||||
|
||||
const clonedLayer = cloneDeep(changedLayers[idx])
|
||||
clonedLayer.id = clonedLayer.id + "-copy"
|
||||
changedLayers.splice(idx, 0, clonedLayer)
|
||||
this.props.onLayersChange(changedLayers)
|
||||
}
|
||||
|
||||
onLayerVisibilityToggle(layerId) {
|
||||
const changedLayers = this.props.layers.slice(0)
|
||||
const idx = style.indexOfLayer(changedLayers, layerId)
|
||||
|
||||
const layer = { ...changedLayers[idx] }
|
||||
const changedLayout = 'layout' in layer ? {...layer.layout} : {}
|
||||
changedLayout.visibility = changedLayout.visibility === 'none' ? 'visible' : 'none'
|
||||
|
||||
layer.layout = changedLayout
|
||||
changedLayers[idx] = layer
|
||||
this.props.onLayersChange(changedLayers)
|
||||
}
|
||||
|
||||
toggleModal(modalName) {
|
||||
this.setState({
|
||||
isOpen: {
|
||||
@@ -94,6 +65,31 @@ class LayerListContainer extends React.Component {
|
||||
})
|
||||
}
|
||||
|
||||
toggleLayers() {
|
||||
let idx=0
|
||||
|
||||
let newGroups=[]
|
||||
|
||||
this.groupedLayers().forEach(layers => {
|
||||
const groupPrefix = layerPrefix(layers[0].id)
|
||||
const lookupKey = [groupPrefix, idx].join('-')
|
||||
|
||||
|
||||
if (layers.length > 1) {
|
||||
newGroups[lookupKey] = this.state.areAllGroupsExpanded
|
||||
}
|
||||
|
||||
layers.forEach((layer) => {
|
||||
idx += 1
|
||||
})
|
||||
});
|
||||
|
||||
this.setState({
|
||||
collapsedGroups: newGroups,
|
||||
areAllGroupsExpanded: !this.state.areAllGroupsExpanded
|
||||
})
|
||||
}
|
||||
|
||||
groupedLayers() {
|
||||
const groups = []
|
||||
for (let i = 0; i < this.props.layers.length; i++) {
|
||||
@@ -135,9 +131,10 @@ class LayerListContainer extends React.Component {
|
||||
const groupPrefix = layerPrefix(layers[0].id)
|
||||
if(layers.length > 1) {
|
||||
const grp = <LayerListGroup
|
||||
data-wd-key={[groupPrefix, idx].join('-')}
|
||||
key={[groupPrefix, idx].join('-')}
|
||||
title={groupPrefix}
|
||||
isActive={!this.isCollapsed(groupPrefix, idx)}
|
||||
isActive={!this.isCollapsed(groupPrefix, idx) || idx === this.props.selectedLayerIndex}
|
||||
onActiveToggle={this.toggleLayerGroup.bind(this, groupPrefix, idx)}
|
||||
/>
|
||||
listItems.push(grp)
|
||||
@@ -145,9 +142,10 @@ class LayerListContainer extends React.Component {
|
||||
|
||||
layers.forEach((layer, idxInGroup) => {
|
||||
const groupIdx = findClosestCommonPrefix(this.props.layers, idx)
|
||||
|
||||
const listItem = <LayerListItem
|
||||
className={classnames({
|
||||
'maputnik-layer-list-item-collapsed': layers.length > 1 && this.isCollapsed(groupPrefix, groupIdx),
|
||||
'maputnik-layer-list-item-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
|
||||
})}
|
||||
index={idx}
|
||||
@@ -157,9 +155,9 @@ class LayerListContainer extends React.Component {
|
||||
visibility={(layer.layout || {}).visibility}
|
||||
isSelected={idx === this.props.selectedLayerIndex}
|
||||
onLayerSelect={this.props.onLayerSelect}
|
||||
onLayerDestroy={this.onLayerDestroy.bind(this)}
|
||||
onLayerCopy={this.onLayerCopy.bind(this)}
|
||||
onLayerVisibilityToggle={this.onLayerVisibilityToggle.bind(this)}
|
||||
onLayerDestroy={this.props.onLayerDestroy.bind(this)}
|
||||
onLayerCopy={this.props.onLayerCopy.bind(this)}
|
||||
onLayerVisibilityToggle={this.props.onLayerVisibilityToggle.bind(this)}
|
||||
/>
|
||||
listItems.push(listItem)
|
||||
idx += 1
|
||||
@@ -177,11 +175,26 @@ class LayerListContainer extends React.Component {
|
||||
<header className="maputnik-layer-list-header">
|
||||
<span className="maputnik-layer-list-header-title">Layers</span>
|
||||
<span className="maputnik-space" />
|
||||
<Button
|
||||
<div className="maputnik-default-property">
|
||||
<div className="maputnik-multibutton">
|
||||
<button
|
||||
id="skip-menu"
|
||||
onClick={this.toggleLayers.bind(this)}
|
||||
className="maputnik-button">
|
||||
{this.state.areAllGroupsExpanded === true ? "Collapse" : "Expand"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="maputnik-default-property">
|
||||
<div className="maputnik-multibutton">
|
||||
<button
|
||||
onClick={this.toggleModal.bind(this, 'add')}
|
||||
className="maputnik-add-layer">
|
||||
data-wd-key="layer-list:add-layer"
|
||||
className="maputnik-button maputnik-button-selected">
|
||||
Add Layer
|
||||
</Button>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<ul className="maputnik-layer-list-container">
|
||||
{listItems}
|
||||
@@ -193,18 +206,10 @@ class LayerListContainer extends React.Component {
|
||||
export default class LayerList extends React.Component {
|
||||
static propTypes = {...layerListPropTypes}
|
||||
|
||||
onSortEnd(move) {
|
||||
const { oldIndex, newIndex } = move
|
||||
if(oldIndex === newIndex) return
|
||||
let layers = this.props.layers.slice(0)
|
||||
layers = arrayMove(layers, oldIndex, newIndex)
|
||||
this.props.onLayersChange(layers)
|
||||
}
|
||||
|
||||
render() {
|
||||
return <LayerListContainer
|
||||
{...this.props}
|
||||
onSortEnd={this.onSortEnd.bind(this)}
|
||||
onSortEnd={this.props.onMoveLayer.bind(this)}
|
||||
useDragHandle={true}
|
||||
/>
|
||||
}
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import Collapser from './Collapser'
|
||||
|
||||
export default class LayerEditorGroup extends React.Component {
|
||||
export default class LayerListGroup extends React.Component {
|
||||
static propTypes = {
|
||||
title: React.PropTypes.string.isRequired,
|
||||
children: React.PropTypes.element.isRequired,
|
||||
isActive: React.PropTypes.bool.isRequired,
|
||||
onActiveToggle: React.PropTypes.func.isRequired
|
||||
title: PropTypes.string.isRequired,
|
||||
"data-wd-key": PropTypes.string,
|
||||
isActive: PropTypes.bool.isRequired,
|
||||
onActiveToggle: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div className="maputnik-layer-list-group">
|
||||
return <li className="maputnik-layer-list-group">
|
||||
<div className="maputnik-layer-list-group-header"
|
||||
data-wd-key={"layer-list-group:"+this.props["data-wd-key"]}
|
||||
onClick={e => this.props.onActiveToggle(!this.props.isActive)}
|
||||
>
|
||||
<span className="maputnik-layer-list-group-title">{this.props.title}</span>
|
||||
@@ -21,6 +23,6 @@ export default class LayerEditorGroup extends React.Component {
|
||||
isCollapsed={this.props.isActive}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import Color from 'color'
|
||||
import classnames from 'classnames'
|
||||
|
||||
@@ -30,43 +31,46 @@ class LayerTypeDragHandle extends React.Component {
|
||||
|
||||
class IconAction extends React.Component {
|
||||
static propTypes = {
|
||||
action: React.PropTypes.string.isRequired,
|
||||
onClick: React.PropTypes.func.isRequired,
|
||||
action: PropTypes.string.isRequired,
|
||||
onClick: PropTypes.func.isRequired,
|
||||
wdKey: PropTypes.string
|
||||
}
|
||||
|
||||
renderIcon() {
|
||||
switch(this.props.action) {
|
||||
case 'copy': return <CopyIcon />
|
||||
case 'duplicate': return <CopyIcon />
|
||||
case 'show': return <VisibilityIcon />
|
||||
case 'hide': return <VisibilityOffIcon />
|
||||
case 'delete': return <DeleteIcon />
|
||||
default: return null
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return <a
|
||||
return <button
|
||||
tabIndex="-1"
|
||||
title={this.props.action}
|
||||
className="maputnik-layer-list-icon-action"
|
||||
data-wd-key={this.props.wdKey}
|
||||
onClick={this.props.onClick}
|
||||
>
|
||||
{this.renderIcon()}
|
||||
</a>
|
||||
</button>
|
||||
}
|
||||
}
|
||||
|
||||
@SortableElement
|
||||
class LayerListItem extends React.Component {
|
||||
static propTypes = {
|
||||
layerId: React.PropTypes.string.isRequired,
|
||||
layerType: React.PropTypes.string.isRequired,
|
||||
isSelected: React.PropTypes.bool,
|
||||
visibility: React.PropTypes.string,
|
||||
className: React.PropTypes.string,
|
||||
layerId: PropTypes.string.isRequired,
|
||||
layerType: PropTypes.string.isRequired,
|
||||
isSelected: PropTypes.bool,
|
||||
visibility: PropTypes.string,
|
||||
className: PropTypes.string,
|
||||
|
||||
onLayerSelect: React.PropTypes.func.isRequired,
|
||||
onLayerCopy: React.PropTypes.func,
|
||||
onLayerDestroy: React.PropTypes.func,
|
||||
onLayerVisibilityToggle: React.PropTypes.func,
|
||||
onLayerSelect: PropTypes.func.isRequired,
|
||||
onLayerCopy: PropTypes.func,
|
||||
onLayerDestroy: PropTypes.func,
|
||||
onLayerVisibilityToggle: PropTypes.func,
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
@@ -78,7 +82,7 @@ class LayerListItem extends React.Component {
|
||||
}
|
||||
|
||||
static childContextTypes = {
|
||||
reactIconBase: React.PropTypes.object
|
||||
reactIconBase: PropTypes.object
|
||||
}
|
||||
|
||||
getChildContext() {
|
||||
@@ -91,6 +95,7 @@ class LayerListItem extends React.Component {
|
||||
return <li
|
||||
key={this.props.layerId}
|
||||
onClick={e => this.props.onLayerSelect(this.props.layerId)}
|
||||
data-wd-key={"layer-list-item:"+this.props.layerId}
|
||||
className={classnames({
|
||||
"maputnik-layer-list-item": true,
|
||||
"maputnik-layer-list-item-selected": this.props.isSelected,
|
||||
@@ -100,14 +105,17 @@ class LayerListItem extends React.Component {
|
||||
<span className="maputnik-layer-list-item-id">{this.props.layerId}</span>
|
||||
<span style={{flexGrow: 1}} />
|
||||
<IconAction
|
||||
wdKey={"layer-list-item:"+this.props.layerId+":delete"}
|
||||
action={'delete'}
|
||||
onClick={e => this.props.onLayerDestroy(this.props.layerId)}
|
||||
/>
|
||||
<IconAction
|
||||
action={'copy'}
|
||||
wdKey={"layer-list-item:"+this.props.layerId+":copy"}
|
||||
action={'duplicate'}
|
||||
onClick={e => this.props.onLayerCopy(this.props.layerId)}
|
||||
/>
|
||||
<IconAction
|
||||
wdKey={"layer-list-item:"+this.props.layerId+":toggle-visibility"}
|
||||
action={this.props.visibility === 'visible' ? 'hide' : 'show'}
|
||||
onClick={e => this.props.onLayerVisibilityToggle(this.props.layerId)}
|
||||
/>
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import GlSpec from 'mapbox-gl-style-spec/reference/latest.js'
|
||||
import * as styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
|
||||
import InputBlock from '../inputs/InputBlock'
|
||||
import StringInput from '../inputs/StringInput'
|
||||
import SelectInput from '../inputs/SelectInput'
|
||||
import AutocompleteInput from '../inputs/AutocompleteInput'
|
||||
|
||||
class LayerSourceBlock extends React.Component {
|
||||
static propTypes = {
|
||||
value: React.PropTypes.string,
|
||||
onChange: React.PropTypes.func,
|
||||
sourceIds: React.PropTypes.array,
|
||||
value: PropTypes.string,
|
||||
wdKey: PropTypes.string,
|
||||
onChange: PropTypes.func,
|
||||
sourceIds: PropTypes.array,
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
@@ -19,7 +20,9 @@ class LayerSourceBlock extends React.Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
return <InputBlock label={"Source"} doc={GlSpec.layer.source.doc}>
|
||||
return <InputBlock label={"Source"} doc={styleSpec.latest.layer.source.doc}
|
||||
data-wd-key={this.props.wdKey}
|
||||
>
|
||||
<AutocompleteInput
|
||||
value={this.props.value}
|
||||
onChange={this.props.onChange}
|
||||
|
||||
@@ -1,26 +1,31 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import GlSpec from 'mapbox-gl-style-spec/reference/latest.js'
|
||||
import * as styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
|
||||
import InputBlock from '../inputs/InputBlock'
|
||||
import StringInput from '../inputs/StringInput'
|
||||
import SelectInput from '../inputs/SelectInput'
|
||||
import AutocompleteInput from '../inputs/AutocompleteInput'
|
||||
|
||||
class LayerSourceLayer extends React.Component {
|
||||
static propTypes = {
|
||||
value: React.PropTypes.string,
|
||||
onChange: React.PropTypes.func,
|
||||
sourceLayerIds: React.PropTypes.array,
|
||||
value: PropTypes.string,
|
||||
onChange: PropTypes.func,
|
||||
sourceLayerIds: PropTypes.array,
|
||||
isFixed: PropTypes.bool,
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
onChange: () => {},
|
||||
sourceLayerIds: [],
|
||||
isFixed: false
|
||||
}
|
||||
|
||||
render() {
|
||||
return <InputBlock label={"Source Layer"} doc={GlSpec.layer['source-layer'].doc}>
|
||||
return <InputBlock label={"Source Layer"} doc={styleSpec.latest.layer['source-layer'].doc}
|
||||
data-wd-key="layer-source-layer"
|
||||
>
|
||||
<AutocompleteInput
|
||||
keepMenuWithinWindowBounds={!!this.props.isFixed}
|
||||
value={this.props.value}
|
||||
onChange={this.props.onChange}
|
||||
options={this.props.sourceLayerIds.map(l => [l, l])}
|
||||
|
||||
@@ -1,17 +1,21 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import GlSpec from 'mapbox-gl-style-spec/reference/latest.js'
|
||||
import * as styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
|
||||
import InputBlock from '../inputs/InputBlock'
|
||||
import SelectInput from '../inputs/SelectInput'
|
||||
|
||||
class LayerTypeBlock extends React.Component {
|
||||
static propTypes = {
|
||||
value: React.PropTypes.string.isRequired,
|
||||
onChange: React.PropTypes.func.isRequired,
|
||||
value: PropTypes.string.isRequired,
|
||||
wdKey: PropTypes.string,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
render() {
|
||||
return <InputBlock label={"Type"} doc={GlSpec.layer.type.doc}>
|
||||
return <InputBlock label={"Type"} doc={styleSpec.latest.layer.type.doc}
|
||||
data-wd-key={this.props.wdKey}
|
||||
>
|
||||
<SelectInput
|
||||
options={[
|
||||
['background', 'Background'],
|
||||
@@ -21,6 +25,8 @@ class LayerTypeBlock extends React.Component {
|
||||
['raster', 'Raster'],
|
||||
['circle', 'Circle'],
|
||||
['fill-extrusion', 'Fill Extrusion'],
|
||||
['hillshade', 'Hillshade'],
|
||||
['heatmap', 'Heatmap'],
|
||||
]}
|
||||
onChange={this.props.onChange}
|
||||
value={this.props.value}
|
||||
|
||||
@@ -1,23 +1,26 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import GlSpec from 'mapbox-gl-style-spec/reference/latest.js'
|
||||
import * as styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
|
||||
import InputBlock from '../inputs/InputBlock'
|
||||
import NumberInput from '../inputs/NumberInput'
|
||||
|
||||
class MaxZoomBlock extends React.Component {
|
||||
static propTypes = {
|
||||
value: React.PropTypes.number.isRequired,
|
||||
onChange: React.PropTypes.func.isRequired,
|
||||
value: PropTypes.number,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
render() {
|
||||
return <InputBlock label={"Max Zoom"} doc={GlSpec.layer.maxzoom.doc}>
|
||||
return <InputBlock label={"Max Zoom"} doc={styleSpec.latest.layer.maxzoom.doc}
|
||||
data-wd-key="max-zoom"
|
||||
>
|
||||
<NumberInput
|
||||
value={this.props.value}
|
||||
onChange={this.props.onChange}
|
||||
min={GlSpec.layer.maxzoom.minimum}
|
||||
max={GlSpec.layer.maxzoom.maximum}
|
||||
default={GlSpec.layer.maxzoom.maximum}
|
||||
min={styleSpec.latest.layer.maxzoom.minimum}
|
||||
max={styleSpec.latest.layer.maxzoom.maximum}
|
||||
default={styleSpec.latest.layer.maxzoom.maximum}
|
||||
/>
|
||||
</InputBlock>
|
||||
}
|
||||
|
||||
@@ -1,23 +1,26 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import GlSpec from 'mapbox-gl-style-spec/reference/latest.js'
|
||||
import * as styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
|
||||
import InputBlock from '../inputs/InputBlock'
|
||||
import NumberInput from '../inputs/NumberInput'
|
||||
|
||||
class MinZoomBlock extends React.Component {
|
||||
static propTypes = {
|
||||
value: React.PropTypes.number.isRequired,
|
||||
onChange: React.PropTypes.func.isRequired,
|
||||
value: PropTypes.number,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
render() {
|
||||
return <InputBlock label={"Min Zoom"} doc={GlSpec.layer.minzoom.doc}>
|
||||
return <InputBlock label={"Min Zoom"} doc={styleSpec.latest.layer.minzoom.doc}
|
||||
data-wd-key="min-zoom"
|
||||
>
|
||||
<NumberInput
|
||||
value={this.props.value}
|
||||
onChange={this.props.onChange}
|
||||
min={GlSpec.layer.minzoom.minimum}
|
||||
max={GlSpec.layer.minzoom.maximum}
|
||||
default={GlSpec.layer.minzoom.minimum}
|
||||
min={styleSpec.latest.layer.minzoom.minimum}
|
||||
max={styleSpec.latest.layer.minzoom.maximum}
|
||||
default={styleSpec.latest.layer.minzoom.minimum}
|
||||
/>
|
||||
</InputBlock>
|
||||
}
|
||||
|
||||
@@ -1,19 +1,38 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import InputBlock from '../inputs/InputBlock'
|
||||
import StringInput from '../inputs/StringInput'
|
||||
import LayerIcon from '../icons/LayerIcon'
|
||||
|
||||
|
||||
function groupFeaturesBySourceLayer(features) {
|
||||
const sources = {}
|
||||
|
||||
let returnedFeatures = {};
|
||||
|
||||
features.forEach(feature => {
|
||||
if(returnedFeatures.hasOwnProperty(feature.layer.id)) {
|
||||
returnedFeatures[feature.layer.id]++
|
||||
|
||||
const featureObject = sources[feature.layer['source-layer']].find(f => f.layer.id === feature.layer.id)
|
||||
|
||||
featureObject.counter = returnedFeatures[feature.layer.id]
|
||||
} else {
|
||||
sources[feature.layer['source-layer']] = sources[feature.layer['source-layer']] || []
|
||||
sources[feature.layer['source-layer']].push(feature)
|
||||
|
||||
returnedFeatures[feature.layer.id] = 1
|
||||
}
|
||||
})
|
||||
|
||||
return sources
|
||||
}
|
||||
|
||||
class FeatureLayerPopup extends React.Component {
|
||||
static propTypes = {
|
||||
onLayerSelect: PropTypes.func.isRequired,
|
||||
features: PropTypes.array
|
||||
}
|
||||
|
||||
render() {
|
||||
const sources = groupFeaturesBySourceLayer(this.props.features)
|
||||
|
||||
@@ -22,6 +41,9 @@ class FeatureLayerPopup extends React.Component {
|
||||
return <label
|
||||
key={idx}
|
||||
className="maputnik-popup-layer"
|
||||
onClick={() => {
|
||||
this.props.onLayerSelect(feature.layer.id)
|
||||
}}
|
||||
>
|
||||
<LayerIcon type={feature.layer.type} style={{
|
||||
width: 14,
|
||||
@@ -29,6 +51,7 @@ class FeatureLayerPopup extends React.Component {
|
||||
paddingRight: 3
|
||||
}}/>
|
||||
{feature.layer.id}
|
||||
{feature.counter && <span> × {feature.counter}</span>}
|
||||
</label>
|
||||
})
|
||||
return <div key={vectorLayerId}>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import InputBlock from '../inputs/InputBlock'
|
||||
import StringInput from '../inputs/StringInput'
|
||||
|
||||
@@ -22,7 +23,7 @@ function renderProperties(feature) {
|
||||
|
||||
function renderFeature(feature) {
|
||||
return <div key={feature.id}>
|
||||
<div className="maputnik-popup-layer-id">{feature.layer['source-layer']}</div>
|
||||
<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>
|
||||
@@ -30,10 +31,36 @@ function renderFeature(feature) {
|
||||
</div>
|
||||
}
|
||||
|
||||
function removeDuplicatedFeatures(features) {
|
||||
let uniqueFeatures = [];
|
||||
|
||||
features.forEach(feature => {
|
||||
const featureIndex = uniqueFeatures.findIndex(feature2 => {
|
||||
return feature.layer['source-layer'] === feature2.layer['source-layer']
|
||||
&& JSON.stringify(feature.properties) === JSON.stringify(feature2.properties)
|
||||
})
|
||||
|
||||
if(featureIndex === -1) {
|
||||
uniqueFeatures.push(feature)
|
||||
} else {
|
||||
if(uniqueFeatures[featureIndex].hasOwnProperty('counter')) {
|
||||
uniqueFeatures[featureIndex].inspectModeCounter++
|
||||
} else {
|
||||
uniqueFeatures[featureIndex].inspectModeCounter = 2
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return uniqueFeatures
|
||||
}
|
||||
|
||||
class FeaturePropertyPopup extends React.Component {
|
||||
static propTypes = {
|
||||
features: PropTypes.array
|
||||
}
|
||||
|
||||
render() {
|
||||
const features = this.props.features
|
||||
const features = removeDuplicatedFeatures(this.props.features)
|
||||
return <div className="maputnik-feature-property-popup">
|
||||
{features.map(renderFeature)}
|
||||
</div>
|
||||
|
||||
@@ -1,23 +1,19 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import ReactDOM from 'react-dom'
|
||||
import MapboxGl from 'mapbox-gl/dist/mapbox-gl.js'
|
||||
import MapboxGl from 'mapbox-gl'
|
||||
import MapboxInspect from 'mapbox-gl-inspect'
|
||||
import FeatureLayerPopup from './FeatureLayerPopup'
|
||||
import FeaturePropertyPopup from './FeaturePropertyPopup'
|
||||
import validateColor from 'mapbox-gl-style-spec/lib/validate/validate_color'
|
||||
import style from '../../libs/style.js'
|
||||
import tokens from '../../config/tokens.json'
|
||||
import colors from 'mapbox-gl-inspect/lib/colors'
|
||||
import Color from 'color'
|
||||
import ZoomControl from '../../libs/zoomcontrol'
|
||||
import { colorHighlightedLayer } from '../../libs/highlight'
|
||||
import 'mapbox-gl/dist/mapbox-gl.css'
|
||||
import '../../mapboxgl.css'
|
||||
|
||||
function renderLayerPopup(features) {
|
||||
var mountNode = document.createElement('div');
|
||||
ReactDOM.render(<FeatureLayerPopup features={features} />, mountNode)
|
||||
return mountNode.innerHTML;
|
||||
}
|
||||
import '../../libs/mapbox-rtl'
|
||||
|
||||
function renderPropertyPopup(features) {
|
||||
var mountNode = document.createElement('div');
|
||||
@@ -42,7 +38,7 @@ function buildInspectStyle(originalMapStyle, coloredLayers, highlightedLayer) {
|
||||
const sources = {}
|
||||
Object.keys(originalMapStyle.sources).forEach(sourceId => {
|
||||
const source = originalMapStyle.sources[sourceId]
|
||||
if(source.type !== 'raster') {
|
||||
if(source.type !== 'raster' && source.type !== 'raster-dem') {
|
||||
sources[sourceId] = source
|
||||
}
|
||||
})
|
||||
@@ -57,15 +53,18 @@ function buildInspectStyle(originalMapStyle, coloredLayers, highlightedLayer) {
|
||||
|
||||
export default class MapboxGlMap extends React.Component {
|
||||
static propTypes = {
|
||||
onDataChange: React.PropTypes.func,
|
||||
mapStyle: React.PropTypes.object.isRequired,
|
||||
inspectModeEnabled: React.PropTypes.bool.isRequired,
|
||||
highlightedLayer: React.PropTypes.object,
|
||||
onDataChange: PropTypes.func,
|
||||
onLayerSelect: PropTypes.func.isRequired,
|
||||
mapStyle: PropTypes.object.isRequired,
|
||||
inspectModeEnabled: PropTypes.bool.isRequired,
|
||||
highlightedLayer: PropTypes.object,
|
||||
options: PropTypes.object,
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
onMapLoaded: () => {},
|
||||
onDataChange: () => {},
|
||||
onLayerSelect: () => {},
|
||||
mapboxAccessToken: tokens.mapbox,
|
||||
}
|
||||
|
||||
@@ -81,7 +80,7 @@ export default class MapboxGlMap extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
UNSAFE_componentWillReceiveProps(nextProps) {
|
||||
if(!this.state.map) return
|
||||
const metadata = nextProps.mapStyle.metadata || {}
|
||||
MapboxGl.accessToken = metadata['maputnik:mapbox_access_token'] || tokens.mapbox
|
||||
@@ -94,20 +93,32 @@ export default class MapboxGlMap extends React.Component {
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const map = this.state.map;
|
||||
|
||||
if(this.props.inspectModeEnabled !== prevProps.inspectModeEnabled) {
|
||||
this.state.inspect.toggleInspector()
|
||||
}
|
||||
if(this.props.inspectModeEnabled) {
|
||||
this.state.inspect.render()
|
||||
}
|
||||
|
||||
map.showTileBoundaries = this.props.options.showTileBoundaries;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const map = new MapboxGl.Map({
|
||||
const mapOpts = {
|
||||
...this.props.options,
|
||||
container: this.container,
|
||||
style: this.props.mapStyle,
|
||||
hash: true,
|
||||
})
|
||||
}
|
||||
|
||||
const map = new MapboxGl.Map(mapOpts);
|
||||
|
||||
map.showTileBoundaries = mapOpts.showTileBoundaries;
|
||||
|
||||
const zoom = new ZoomControl;
|
||||
map.addControl(zoom, 'top-right');
|
||||
|
||||
const nav = new MapboxGl.NavigationControl();
|
||||
map.addControl(nav, 'top-right');
|
||||
@@ -120,6 +131,7 @@ export default class MapboxGlMap extends React.Component {
|
||||
showMapPopupOnHover: false,
|
||||
showInspectMapPopupOnHover: true,
|
||||
showInspectButton: false,
|
||||
blockHoverPopupOnClick: true,
|
||||
assignLayerColor: (layerId, alpha) => {
|
||||
return Color(colors.brightColor(layerId, alpha)).desaturate(0.5).string()
|
||||
},
|
||||
@@ -128,7 +140,9 @@ export default class MapboxGlMap extends React.Component {
|
||||
if(this.props.inspectModeEnabled) {
|
||||
return renderPropertyPopup(features)
|
||||
} else {
|
||||
return renderLayerPopup(features)
|
||||
var mountNode = document.createElement('div');
|
||||
ReactDOM.render(<FeatureLayerPopup features={features} onLayerSelect={this.props.onLayerSelect} />, mountNode)
|
||||
return mountNode
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,59 +1,17 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import style from '../../libs/style.js'
|
||||
import isEqual from 'lodash.isequal'
|
||||
import { loadJSON } from '../../libs/urlopen'
|
||||
import 'ol/ol.css'
|
||||
|
||||
function suitableVectorSource(mapStyle) {
|
||||
const sources = Object.keys(mapStyle.sources)
|
||||
.map(sourceId => {
|
||||
return {
|
||||
id: sourceId,
|
||||
source: mapStyle.sources[sourceId]
|
||||
}
|
||||
})
|
||||
.filter(({source}) => source.type === 'vector')
|
||||
return sources[0]
|
||||
}
|
||||
|
||||
function toVectorLayer(source, tilegrid, cb) {
|
||||
function newMVTLayer(tileUrl) {
|
||||
const ol = require('openlayers')
|
||||
return new ol.layer.VectorTile({
|
||||
source: new ol.source.VectorTile({
|
||||
format: new ol.format.MVT(),
|
||||
tileGrid: tilegrid,
|
||||
tilePixelRatio: 8,
|
||||
url: tileUrl
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
if(!source.tiles) {
|
||||
sourceFromTileJSON(source.url, tileSource => {
|
||||
cb(newMVTLayer(tileSource.tiles[0]))
|
||||
})
|
||||
} else {
|
||||
cb(newMVTLayer(source.tiles[0]))
|
||||
}
|
||||
}
|
||||
|
||||
function sourceFromTileJSON(url, cb) {
|
||||
loadJSON(url, null, tilejson => {
|
||||
if(!tilejson) return
|
||||
cb({
|
||||
type: 'vector',
|
||||
tiles: tilejson.tiles,
|
||||
minzoom: tilejson.minzoom,
|
||||
maxzoom: tilejson.maxzoom,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
class OpenLayers3Map extends React.Component {
|
||||
static propTypes = {
|
||||
onDataChange: React.PropTypes.func,
|
||||
mapStyle: React.PropTypes.object.isRequired,
|
||||
accessToken: React.PropTypes.string,
|
||||
onDataChange: PropTypes.func,
|
||||
mapStyle: PropTypes.object.isRequired,
|
||||
accessToken: PropTypes.string,
|
||||
style: PropTypes.object,
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
@@ -63,48 +21,17 @@ class OpenLayers3Map extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.tilegrid = null
|
||||
this.resolutions = null
|
||||
this.layer = null
|
||||
this.map = null
|
||||
}
|
||||
|
||||
updateStyle(newMapStyle) {
|
||||
const oldSource = suitableVectorSource(this.props.mapStyle)
|
||||
const newSource = suitableVectorSource(newMapStyle)
|
||||
const resolutions = this.resolutions
|
||||
|
||||
function setStyleFunc(map, layer) {
|
||||
const olms = require('ol-mapbox-style')
|
||||
const styleFunc = olms.getStyleFunction(newMapStyle, newSource.id, resolutions)
|
||||
layer.setStyle(styleFunc)
|
||||
//NOTE: We need to mark the source as changed in order
|
||||
//to trigger a rerender
|
||||
layer.getSource().changed()
|
||||
map.render()
|
||||
const olms = require('ol-mapbox-style');
|
||||
const styleFunc = olms.apply(this.map, newMapStyle)
|
||||
}
|
||||
|
||||
if(newSource) {
|
||||
if(this.layer && !isEqual(oldSource, newSource)) {
|
||||
this.map.removeLayer(this.layer)
|
||||
this.layer = null
|
||||
}
|
||||
|
||||
if(!this.layer) {
|
||||
toVectorLayer(newSource.source, this.tilegrid, vectorLayer => {
|
||||
this.layer = vectorLayer
|
||||
this.map.addLayer(this.layer)
|
||||
setStyleFunc(this.map, this.layer)
|
||||
})
|
||||
} else {
|
||||
setStyleFunc(this.map, this.layer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
require.ensure(["openlayers", "ol-mapbox-style"], () => {
|
||||
if(!this.map || !this.resolutions) return
|
||||
UNSAFE_componentWillReceiveProps(nextProps) {
|
||||
require.ensure(["ol", "ol-mapbox-style"], () => {
|
||||
if(!this.map) return
|
||||
this.updateStyle(nextProps.mapStyle)
|
||||
})
|
||||
}
|
||||
@@ -112,24 +39,22 @@ class OpenLayers3Map extends React.Component {
|
||||
componentDidMount() {
|
||||
//Load OpenLayers dynamically once we need it
|
||||
//TODO: Make this more convenient
|
||||
require.ensure(["openlayers", "ol-mapbox-style"], ()=> {
|
||||
require.ensure(["ol", "ol/map", "ol/view", "ol/control/zoom", "ol-mapbox-style"], ()=> {
|
||||
console.log('Loaded OpenLayers3 renderer')
|
||||
|
||||
const ol = require('openlayers')
|
||||
const olms = require('ol-mapbox-style')
|
||||
const olMap = require('ol/map').default
|
||||
const olView = require('ol/view').default
|
||||
const olZoom = require('ol/control/zoom').default
|
||||
|
||||
this.tilegrid = ol.tilegrid.createXYZ({tileSize: 512, maxZoom: 22})
|
||||
this.resolutions = this.tilegrid.getResolutions()
|
||||
|
||||
const map = new ol.Map({
|
||||
const map = new olMap({
|
||||
target: this.container,
|
||||
layers: [],
|
||||
view: new ol.View({
|
||||
view: new olView({
|
||||
zoom: 2,
|
||||
center: [52.5, -78.4]
|
||||
})
|
||||
})
|
||||
map.addControl(new ol.control.Zoom())
|
||||
map.addControl(new olZoom())
|
||||
this.map = map
|
||||
this.updateStyle(this.props.mapStyle)
|
||||
})
|
||||
@@ -140,10 +65,10 @@ class OpenLayers3Map extends React.Component {
|
||||
ref={x => this.container = x}
|
||||
style={{
|
||||
position: "fixed",
|
||||
top: 0,
|
||||
top: 40,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
height: "100%",
|
||||
height: 'calc(100% - 40px)',
|
||||
width: "75%",
|
||||
backgroundColor: '#fff',
|
||||
...this.props.style,
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import Button from '../Button'
|
||||
import InputBlock from '../inputs/InputBlock'
|
||||
import StringInput from '../inputs/StringInput'
|
||||
import SelectInput from '../inputs/SelectInput'
|
||||
import Modal from './Modal'
|
||||
|
||||
import LayerTypeBlock from '../layers/LayerTypeBlock'
|
||||
@@ -13,13 +13,13 @@ import LayerSourceLayerBlock from '../layers/LayerSourceLayerBlock'
|
||||
|
||||
class AddModal extends React.Component {
|
||||
static propTypes = {
|
||||
layers: React.PropTypes.array.isRequired,
|
||||
onLayersChange: React.PropTypes.func.isRequired,
|
||||
isOpen: React.PropTypes.bool.isRequired,
|
||||
onOpenToggle: React.PropTypes.func.isRequired,
|
||||
layers: PropTypes.array.isRequired,
|
||||
onLayersChange: PropTypes.func.isRequired,
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onOpenToggle: PropTypes.func.isRequired,
|
||||
|
||||
// A dict of source id's and the available source layers
|
||||
sources: React.PropTypes.object.isRequired,
|
||||
sources: PropTypes.object.isRequired,
|
||||
}
|
||||
|
||||
addLayer() {
|
||||
@@ -55,47 +55,105 @@ class AddModal extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
const sourceIds = Object.keys(nextProps.sources)
|
||||
if(!this.state.source && sourceIds.length > 0) {
|
||||
UNSAFE_componentWillUpdate(nextProps, nextState) {
|
||||
// Check if source is valid for new type
|
||||
const oldType = this.state.type;
|
||||
const newType = nextState.type;
|
||||
|
||||
const availableSourcesOld = this.getSources(oldType);
|
||||
const availableSourcesNew = this.getSources(newType);
|
||||
|
||||
if(
|
||||
// Type has changed
|
||||
oldType !== newType
|
||||
&& this.state.source !== ""
|
||||
// Was a valid source previously
|
||||
&& availableSourcesOld.indexOf(this.state.source) > -1
|
||||
// And is not a valid source now
|
||||
&& availableSourcesNew.indexOf(nextState.source) < 0
|
||||
) {
|
||||
// Clear the source
|
||||
this.setState({
|
||||
source: sourceIds[0],
|
||||
'source-layer': this.state['source-layer'] || (nextProps.sources[sourceIds[0]] || [])[0]
|
||||
})
|
||||
source: ""
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
getLayersForSource(source) {
|
||||
const sourceObj = this.props.sources[source] || {};
|
||||
return sourceObj.layers || [];
|
||||
}
|
||||
|
||||
getSources(type) {
|
||||
const sources = [];
|
||||
|
||||
const types = {
|
||||
vector: [
|
||||
"fill",
|
||||
"line",
|
||||
"symbol",
|
||||
"circle",
|
||||
"fill-extrusion"
|
||||
],
|
||||
raster: [
|
||||
"raster"
|
||||
]
|
||||
}
|
||||
|
||||
for(let [key, val] of Object.entries(this.props.sources)) {
|
||||
if(types[val.type] && types[val.type].indexOf(type) > -1) {
|
||||
sources.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
return sources;
|
||||
}
|
||||
|
||||
|
||||
render() {
|
||||
const sources = this.getSources(this.state.type);
|
||||
const layers = this.getLayersForSource(this.state.source);
|
||||
|
||||
return <Modal
|
||||
isOpen={this.props.isOpen}
|
||||
onOpenToggle={this.props.onOpenToggle}
|
||||
title={'Add Layer'}
|
||||
data-wd-key="modal:add-layer"
|
||||
>
|
||||
<div className="maputnik-add-layer">
|
||||
<LayerIdBlock
|
||||
value={this.state.id}
|
||||
onChange={v => this.setState({ id: v })}
|
||||
wdKey="add-layer.layer-id"
|
||||
onChange={v => {
|
||||
this.setState({ id: v })
|
||||
}}
|
||||
/>
|
||||
<LayerTypeBlock
|
||||
value={this.state.type}
|
||||
wdKey="add-layer.layer-type"
|
||||
onChange={v => this.setState({ type: v })}
|
||||
/>
|
||||
{this.state.type !== 'background' &&
|
||||
<LayerSourceBlock
|
||||
sourceIds={Object.keys(this.props.sources)}
|
||||
sourceIds={sources}
|
||||
wdKey="add-layer.layer-source-block"
|
||||
value={this.state.source}
|
||||
onChange={v => this.setState({ source: v })}
|
||||
/>
|
||||
}
|
||||
{this.state.type !== 'background' && this.state.type !== 'raster' &&
|
||||
{['background', 'raster', 'hillshade', 'heatmap'].indexOf(this.state.type) < 0 &&
|
||||
<LayerSourceLayerBlock
|
||||
sourceLayerIds={this.props.sources[this.state.source] || []}
|
||||
isFixed={true}
|
||||
sourceLayerIds={layers}
|
||||
value={this.state['source-layer']}
|
||||
onChange={v => this.setState({ 'source-layer': v })}
|
||||
/>
|
||||
}
|
||||
<Button className="maputnik-add-layer-button" onClick={this.addLayer.bind(this)}>
|
||||
<Button
|
||||
className="maputnik-add-layer-button"
|
||||
onClick={this.addLayer.bind(this)}
|
||||
data-wd-key="add-layer"
|
||||
>
|
||||
Add Layer
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -1,35 +1,37 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { saveAs } from 'file-saver'
|
||||
|
||||
import GlSpec from 'mapbox-gl-style-spec/reference/latest.js'
|
||||
import * as styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
|
||||
import InputBlock from '../inputs/InputBlock'
|
||||
import StringInput from '../inputs/StringInput'
|
||||
import SelectInput from '../inputs/SelectInput'
|
||||
import CheckboxInput from '../inputs/CheckboxInput'
|
||||
import Button from '../Button'
|
||||
import Modal from './Modal'
|
||||
import MdFileDownload from 'react-icons/lib/md/file-download'
|
||||
import TiClipboard from 'react-icons/lib/ti/clipboard'
|
||||
import style from '../../libs/style.js'
|
||||
import formatStyle from 'mapbox-gl-style-spec/lib/format'
|
||||
import GitHub from 'github-api'
|
||||
import { CopyToClipboard } from 'react-copy-to-clipboard'
|
||||
|
||||
|
||||
class Gist extends React.Component {
|
||||
static propTypes = {
|
||||
mapStyle: React.PropTypes.object.isRequired,
|
||||
onStyleChanged: React.PropTypes.func.isRequired,
|
||||
mapStyle: PropTypes.object.isRequired,
|
||||
onStyleChanged: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
preview: false,
|
||||
public: false,
|
||||
saving: false,
|
||||
latestGist: null,
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
UNSAFE_componentWillReceiveProps(nextProps) {
|
||||
this.setState({
|
||||
...this.state,
|
||||
preview: !!(nextProps.mapStyle.metadata || {})['maputnik:openmaptiles_access_token']
|
||||
@@ -41,11 +43,14 @@ class Gist extends React.Component {
|
||||
...this.state,
|
||||
saving: true
|
||||
});
|
||||
const preview = this.state.preview && (this.props.mapStyle.metadata || {})['maputnik:openmaptiles_access_token'];
|
||||
|
||||
const preview = this.state.preview;
|
||||
|
||||
const mapboxToken = (this.props.mapStyle.metadata || {})['maputnik:mapbox_access_token'];
|
||||
|
||||
const mapStyleStr = preview ?
|
||||
formatStyle(stripAccessTokens(style.replaceAccessToken(this.props.mapStyle))) :
|
||||
formatStyle(stripAccessTokens(this.props.mapStyle));
|
||||
styleSpec.format(stripAccessTokens(style.replaceAccessToken(this.props.mapStyle))) :
|
||||
styleSpec.format(stripAccessTokens(this.props.mapStyle));
|
||||
const styleTitle = this.props.mapStyle.name || 'Style';
|
||||
const htmlStr = `
|
||||
<!DOCTYPE html>
|
||||
@@ -54,8 +59,8 @@ class Gist extends React.Component {
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>`+styleTitle+` Preview</title>
|
||||
<link rel="stylesheet" type="text/css" href="https://api.mapbox.com/mapbox-gl-js/v0.28.0/mapbox-gl.css" />
|
||||
<script src="https://api.mapbox.com/mapbox-gl-js/v0.28.0/mapbox-gl.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="https://api.mapbox.com/mapbox-gl-js/v0.44.0/mapbox-gl.css" />
|
||||
<script src="https://api.mapbox.com/mapbox-gl-js/v0.44.0/mapbox-gl.js"></script>
|
||||
<style>
|
||||
body { margin:0; padding:0; }
|
||||
#map { position:absolute; top:0; bottom:0; width:100%; }
|
||||
@@ -64,6 +69,7 @@ class Gist extends React.Component {
|
||||
<body>
|
||||
<div id='map'></div>
|
||||
<script>
|
||||
mapboxgl.accessToken = '${mapboxToken}';
|
||||
var map = new mapboxgl.Map({
|
||||
container: 'map',
|
||||
style: 'style.json',
|
||||
@@ -88,7 +94,7 @@ class Gist extends React.Component {
|
||||
const gh = new GitHub();
|
||||
let gist = gh.getGist(); // not a gist yet
|
||||
gist.create({
|
||||
public: true,
|
||||
public: this.state.public,
|
||||
description: styleTitle,
|
||||
files: files
|
||||
}).then(function({data}) {
|
||||
@@ -109,6 +115,13 @@ class Gist extends React.Component {
|
||||
})
|
||||
}
|
||||
|
||||
onPublicChange(value) {
|
||||
this.setState({
|
||||
...this.state,
|
||||
public: value
|
||||
})
|
||||
}
|
||||
|
||||
changeMetadataProperty(property, value) {
|
||||
const changedStyle = {
|
||||
...this.props.mapStyle,
|
||||
@@ -125,7 +138,7 @@ class Gist extends React.Component {
|
||||
const user = gist.user || 'anonymous';
|
||||
const preview = !!gist.files['index.html'];
|
||||
if(preview) {
|
||||
return <span><a target="_blank" href={"https://bl.ocks.org/"+user+"/"+gist.id}>Preview</a>,{' '}</span>
|
||||
return <span><a target="_blank" rel="noopener noreferrer" href={"https://bl.ocks.org/"+user+"/"+gist.id}>Preview</a>,{' '}</span>
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -137,11 +150,21 @@ class Gist extends React.Component {
|
||||
return <p>Saving...</p>
|
||||
} else if(gist) {
|
||||
const user = gist.user || 'anonymous';
|
||||
return <p>
|
||||
const rawGistLink = "https://gist.githubusercontent.com/" + user + "/" + gist.id + "/raw/" + gist.history[0].version + "/style.json"
|
||||
const maputnikStyleLink = "https://maputnik.github.io/editor/?style=" + rawGistLink
|
||||
return <div className="maputnik-render-gist">
|
||||
<p>
|
||||
Latest saved gist:{' '}
|
||||
{this.renderPreviewLink(this)}
|
||||
<a target="_blank" href={"https://gist.github.com/"+user+"/"+gist.id}>Source</a>
|
||||
<a target="_blank" rel="noopener noreferrer" href={"https://gist.github.com/" + user + "/" + gist.id}>Source</a>
|
||||
</p>
|
||||
<p>
|
||||
<CopyToClipboard text={maputnikStyleLink}>
|
||||
<span>Share this style: <Button><TiClipboard size={18} /></Button></span>
|
||||
</CopyToClipboard>
|
||||
<StringInput value={maputnikStyleLink} />
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,13 +174,22 @@ class Gist extends React.Component {
|
||||
<MdFileDownload />
|
||||
Save to Gist (anonymous)
|
||||
</Button>
|
||||
{' '}
|
||||
<div className="maputnik-modal-sub-section">
|
||||
<CheckboxInput
|
||||
value={this.state.public}
|
||||
name='gist-style-public'
|
||||
onChange={this.onPublicChange.bind(this)}
|
||||
/>
|
||||
<span> Public gist</span>
|
||||
</div>
|
||||
<div className="maputnik-modal-sub-section">
|
||||
<CheckboxInput
|
||||
value={this.state.preview}
|
||||
name='gist-style-preview'
|
||||
onChange={this.onPreviewChange.bind(this)}
|
||||
/>
|
||||
<span> Include preview</span>
|
||||
</div>
|
||||
{this.state.preview ?
|
||||
<div>
|
||||
<InputBlock
|
||||
@@ -166,7 +198,13 @@ class Gist extends React.Component {
|
||||
value={(this.props.mapStyle.metadata || {})['maputnik:openmaptiles_access_token']}
|
||||
onChange={this.changeMetadataProperty.bind(this, "maputnik:openmaptiles_access_token")}/>
|
||||
</InputBlock>
|
||||
<a target="_blank" href="https://openmaptiles.com/hosting/">Get your free access token</a>
|
||||
<InputBlock
|
||||
label={"Mapbox Access Token: "}>
|
||||
<StringInput
|
||||
value={(this.props.mapStyle.metadata || {})['maputnik:mapbox_access_token']}
|
||||
onChange={this.changeMetadataProperty.bind(this, "maputnik:mapbox_access_token")}/>
|
||||
</InputBlock>
|
||||
<a target="_blank" rel="noopener noreferrer" href="https://openmaptiles.com/hosting/">Get your free access token</a>
|
||||
</div>
|
||||
: null}
|
||||
{this.renderLatestGist()}
|
||||
@@ -186,10 +224,10 @@ function stripAccessTokens(mapStyle) {
|
||||
|
||||
class ExportModal extends React.Component {
|
||||
static propTypes = {
|
||||
mapStyle: React.PropTypes.object.isRequired,
|
||||
onStyleChanged: React.PropTypes.func.isRequired,
|
||||
isOpen: React.PropTypes.bool.isRequired,
|
||||
onOpenToggle: React.PropTypes.func.isRequired,
|
||||
mapStyle: PropTypes.object.isRequired,
|
||||
onStyleChanged: PropTypes.func.isRequired,
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onOpenToggle: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
@@ -197,12 +235,13 @@ class ExportModal extends React.Component {
|
||||
}
|
||||
|
||||
downloadStyle() {
|
||||
const blob = new Blob([formatStyle(stripAccessTokens(this.props.mapStyle))], {type: "application/json;charset=utf-8"});
|
||||
const blob = new Blob([styleSpec.format(stripAccessTokens(this.props.mapStyle))], {type: "application/json;charset=utf-8"});
|
||||
saveAs(blob, this.props.mapStyle.id + ".json");
|
||||
}
|
||||
|
||||
render() {
|
||||
return <Modal
|
||||
data-wd-key="export-modal"
|
||||
isOpen={this.props.isOpen}
|
||||
onOpenToggle={this.props.onOpenToggle}
|
||||
title={'Export Style'}
|
||||
@@ -219,7 +258,7 @@ class ExportModal extends React.Component {
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="maputnik-modal-section">
|
||||
<div className="maputnik-modal-section hide">
|
||||
<h4>Save style</h4>
|
||||
<Gist mapStyle={this.props.mapStyle} onStyleChanged={this.props.onStyleChanged}/>
|
||||
</div>
|
||||
|
||||
49
src/components/modals/LoadingModal.jsx
Normal file
49
src/components/modals/LoadingModal.jsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import Button from '../Button'
|
||||
import Modal from './Modal'
|
||||
|
||||
|
||||
class LoadingModal extends React.Component {
|
||||
static propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onCancel: PropTypes.func.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
message: PropTypes.node.isRequired,
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
underlayOnClick(e) {
|
||||
// This stops click events falling through to underlying modals.
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
render() {
|
||||
return <Modal
|
||||
data-wd-key="loading-modal"
|
||||
isOpen={this.props.isOpen}
|
||||
underlayClickExits={false}
|
||||
underlayProps={{
|
||||
onClick: (e) => underlayProps(e)
|
||||
}}
|
||||
closeable={false}
|
||||
title={this.props.title}
|
||||
onOpenToggle={() => this.props.onCancel()}
|
||||
>
|
||||
<p>
|
||||
{this.props.message}
|
||||
</p>
|
||||
<p className="maputnik-dialog__buttons">
|
||||
<Button onClick={(e) => this.props.onCancel(e)}>
|
||||
Cancel
|
||||
</Button>
|
||||
</p>
|
||||
</Modal>
|
||||
}
|
||||
}
|
||||
|
||||
export default LoadingModal
|
||||
@@ -1,29 +1,61 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import CloseIcon from 'react-icons/lib/md/close'
|
||||
import Overlay from './Overlay'
|
||||
import AriaModal from 'react-aria-modal'
|
||||
|
||||
|
||||
class Modal extends React.Component {
|
||||
static propTypes = {
|
||||
isOpen: React.PropTypes.bool.isRequired,
|
||||
title: React.PropTypes.string.isRequired,
|
||||
onOpenToggle: React.PropTypes.func.isRequired,
|
||||
"data-wd-key": PropTypes.string,
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
onOpenToggle: PropTypes.func.isRequired,
|
||||
children: PropTypes.node,
|
||||
underlayClickExits: PropTypes.bool,
|
||||
underlayProps: PropTypes.object,
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
underlayClickExits: true
|
||||
}
|
||||
|
||||
getApplicationNode() {
|
||||
return document.getElementById('app');
|
||||
}
|
||||
|
||||
render() {
|
||||
return <Overlay isOpen={this.props.isOpen}>
|
||||
<div className="maputnik-modal">
|
||||
if(this.props.isOpen) {
|
||||
return <AriaModal
|
||||
titleText={this.props.title}
|
||||
underlayClickExits={this.props.underlayClickExits}
|
||||
underlayProps={this.props.underlayProps}
|
||||
getApplicationNode={this.getApplicationNode}
|
||||
data-wd-key={this.props["data-wd-key"]}
|
||||
verticallyCenter={true}
|
||||
onExit={() => this.props.onOpenToggle(false)}
|
||||
>
|
||||
<div className="maputnik-modal"
|
||||
data-wd-key={this.props["data-wd-key"]}
|
||||
>
|
||||
<header className="maputnik-modal-header">
|
||||
<h1 className="maputnik-modal-header-title">{this.props.title}</h1>
|
||||
<span className="maputnik-modal-header-space"></span>
|
||||
<a className="maputnik-modal-header-toggle"
|
||||
<button className="maputnik-modal-header-toggle"
|
||||
onClick={() => this.props.onOpenToggle(false)}
|
||||
data-wd-key={this.props["data-wd-key"]+".close-modal"}
|
||||
>
|
||||
<CloseIcon />
|
||||
</a>
|
||||
</button>
|
||||
</header>
|
||||
<div className="maputnik-modal-scroller">
|
||||
<div className="maputnik-modal-content">{this.props.children}</div>
|
||||
</div>
|
||||
</Overlay>
|
||||
</div>
|
||||
</AriaModal>
|
||||
}
|
||||
else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import LoadingModal from './LoadingModal'
|
||||
import Modal from './Modal'
|
||||
import Button from '../Button'
|
||||
import FileReaderInput from 'react-file-reader-input'
|
||||
@@ -12,16 +14,17 @@ import publicStyles from '../../config/styles.json'
|
||||
|
||||
class PublicStyle extends React.Component {
|
||||
static propTypes = {
|
||||
url: React.PropTypes.string.isRequired,
|
||||
thumbnailUrl: React.PropTypes.string.isRequired,
|
||||
title: React.PropTypes.string.isRequired,
|
||||
onSelect: React.PropTypes.func.isRequired,
|
||||
url: PropTypes.string.isRequired,
|
||||
thumbnailUrl: PropTypes.string.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
onSelect: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div className="maputnik-public-style">
|
||||
<Button
|
||||
className="maputnik-public-style-button"
|
||||
aria-label={this.props.title}
|
||||
onClick={() => this.props.onSelect(this.props.url)}
|
||||
>
|
||||
<header className="maputnik-public-style-header">
|
||||
@@ -29,11 +32,12 @@ class PublicStyle extends React.Component {
|
||||
<span className="maputnik-space" />
|
||||
<AddIcon />
|
||||
</header>
|
||||
<img
|
||||
<div
|
||||
className="maputnik-public-style-thumbnail"
|
||||
src={this.props.thumbnailUrl}
|
||||
alt={this.props.title}
|
||||
/>
|
||||
style={{
|
||||
backgroundImage: `url(${this.props.thumbnailUrl})`
|
||||
}}
|
||||
></div>
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
@@ -41,38 +45,100 @@ class PublicStyle extends React.Component {
|
||||
|
||||
class OpenModal extends React.Component {
|
||||
static propTypes = {
|
||||
isOpen: React.PropTypes.bool.isRequired,
|
||||
onOpenToggle: React.PropTypes.func.isRequired,
|
||||
onStyleOpen: React.PropTypes.func.isRequired,
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onOpenToggle: PropTypes.func.isRequired,
|
||||
onStyleOpen: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {};
|
||||
}
|
||||
|
||||
clearError() {
|
||||
this.setState({
|
||||
error: null
|
||||
})
|
||||
}
|
||||
|
||||
onCancelActiveRequest(e) {
|
||||
// Else the click propagates to the underlying modal
|
||||
if(e) e.stopPropagation();
|
||||
|
||||
if(this.state.activeRequest) {
|
||||
this.state.activeRequest.abort();
|
||||
this.setState({
|
||||
activeRequest: null,
|
||||
activeRequestUrl: null
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onStyleSelect(styleUrl) {
|
||||
request({
|
||||
this.clearError();
|
||||
|
||||
const reqOpts = {
|
||||
url: styleUrl,
|
||||
withCredentials: false,
|
||||
}, (error, response, body) => {
|
||||
}
|
||||
|
||||
const activeRequest = request(reqOpts, (error, response, body) => {
|
||||
this.setState({
|
||||
activeRequest: null,
|
||||
activeRequestUrl: null
|
||||
});
|
||||
|
||||
if (!error && response.statusCode == 200) {
|
||||
const mapStyle = style.ensureStyleValidity(JSON.parse(body))
|
||||
console.log('Loaded style ', mapStyle.id)
|
||||
this.props.onStyleOpen(mapStyle)
|
||||
this.onOpenToggle()
|
||||
} else {
|
||||
console.warn('Could not open the style URL', styleUrl)
|
||||
}
|
||||
})
|
||||
|
||||
this.setState({
|
||||
activeRequest: activeRequest,
|
||||
activeRequestUrl: reqOpts.url
|
||||
})
|
||||
}
|
||||
|
||||
onOpenUrl() {
|
||||
const url = this.styleUrlElement.value;
|
||||
this.onStyleSelect(url);
|
||||
}
|
||||
|
||||
onUpload(_, files) {
|
||||
const [e, file] = files[0];
|
||||
const reader = new FileReader();
|
||||
|
||||
this.clearError();
|
||||
|
||||
reader.readAsText(file, "UTF-8");
|
||||
reader.onload = e => {
|
||||
let mapStyle = JSON.parse(e.target.result)
|
||||
let mapStyle;
|
||||
try {
|
||||
mapStyle = JSON.parse(e.target.result)
|
||||
}
|
||||
catch(err) {
|
||||
this.setState({
|
||||
error: err.toString()
|
||||
});
|
||||
return;
|
||||
}
|
||||
mapStyle = style.ensureStyleValidity(mapStyle)
|
||||
this.props.onStyleOpen(mapStyle);
|
||||
this.onOpenToggle();
|
||||
}
|
||||
reader.onerror = e => console.log(e.target);
|
||||
}
|
||||
|
||||
onOpenToggle() {
|
||||
this.clearError();
|
||||
this.props.onOpenToggle();
|
||||
}
|
||||
|
||||
render() {
|
||||
const styleOptions = publicStyles.map(style => {
|
||||
return <PublicStyle
|
||||
@@ -84,19 +150,43 @@ class OpenModal extends React.Component {
|
||||
/>
|
||||
})
|
||||
|
||||
let errorElement;
|
||||
if(this.state.error) {
|
||||
errorElement = (
|
||||
<div className="maputnik-modal-error">
|
||||
{this.state.error}
|
||||
<a href="#" onClick={() => this.clearError()} className="maputnik-modal-error-close">×</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <Modal
|
||||
data-wd-key="open-modal"
|
||||
isOpen={this.props.isOpen}
|
||||
onOpenToggle={this.props.onOpenToggle}
|
||||
onOpenToggle={() => this.onOpenToggle()}
|
||||
title={'Open Style'}
|
||||
>
|
||||
{errorElement}
|
||||
<section className="maputnik-modal-section">
|
||||
<h2>Upload Style</h2>
|
||||
<p>Upload a JSON style from your computer.</p>
|
||||
<FileReaderInput onChange={this.onUpload.bind(this)}>
|
||||
<FileReaderInput onChange={this.onUpload.bind(this)} tabIndex="-1">
|
||||
<Button className="maputnik-upload-button"><FileUploadIcon /> Upload</Button>
|
||||
</FileReaderInput>
|
||||
</section>
|
||||
|
||||
<section className="maputnik-modal-section">
|
||||
<h2>Load from URL</h2>
|
||||
<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 data-wd-key="open-modal.url.input" type="text" ref={(input) => this.styleUrlElement = input} className="maputnik-input" placeholder="Enter URL..."/>
|
||||
<div>
|
||||
<Button data-wd-key="open-modal.url.button" className="maputnik-big-button" onClick={this.onOpenUrl.bind(this)}>Open URL</Button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="maputnik-modal-section maputnik-modal-section--shrink">
|
||||
<h2>Gallery Styles</h2>
|
||||
<p>
|
||||
Open one of the publicly available styles to start from.
|
||||
@@ -105,6 +195,13 @@ class OpenModal extends React.Component {
|
||||
{styleOptions}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<LoadingModal
|
||||
isOpen={!!this.state.activeRequest}
|
||||
title={'Loading style'}
|
||||
onCancel={(e) => this.onCancelActiveRequest(e)}
|
||||
message={"Loading: "+this.state.activeRequestUrl}
|
||||
/>
|
||||
</Modal>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
import React from 'react'
|
||||
|
||||
|
||||
class Overlay extends React.Component {
|
||||
static propTypes = {
|
||||
isOpen: React.PropTypes.bool.isRequired,
|
||||
children: React.PropTypes.element.isRequired
|
||||
}
|
||||
|
||||
render() {
|
||||
let overlayStyle = {}
|
||||
if(!this.props.isOpen) {
|
||||
overlayStyle['display'] = 'none';
|
||||
}
|
||||
|
||||
return <div className={"maputnik-overlay"} style={overlayStyle}>
|
||||
<div className={"maputnik-overlay-viewport"} />
|
||||
{this.props.children}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
export default Overlay
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import GlSpec from 'mapbox-gl-style-spec/reference/latest.js'
|
||||
import * as styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
|
||||
import InputBlock from '../inputs/InputBlock'
|
||||
import StringInput from '../inputs/StringInput'
|
||||
import SelectInput from '../inputs/SelectInput'
|
||||
@@ -8,10 +9,10 @@ import Modal from './Modal'
|
||||
|
||||
class SettingsModal extends React.Component {
|
||||
static propTypes = {
|
||||
mapStyle: React.PropTypes.object.isRequired,
|
||||
onStyleChanged: React.PropTypes.func.isRequired,
|
||||
isOpen: React.PropTypes.bool.isRequired,
|
||||
onOpenToggle: React.PropTypes.func.isRequired,
|
||||
mapStyle: PropTypes.object.isRequired,
|
||||
onStyleChanged: PropTypes.func.isRequired,
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onOpenToggle: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
@@ -41,32 +42,37 @@ class SettingsModal extends React.Component {
|
||||
const metadata = this.props.mapStyle.metadata || {}
|
||||
const inputProps = { }
|
||||
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={GlSpec.$root.name.doc}>
|
||||
<InputBlock label={"Name"} doc={styleSpec.latest.$root.name.doc}>
|
||||
<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."}>
|
||||
<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={GlSpec.$root.sprite.doc}>
|
||||
<InputBlock label={"Sprite URL"} doc={styleSpec.latest.$root.sprite.doc}>
|
||||
<StringInput {...inputProps}
|
||||
data-wd-key="modal-settings.sprite"
|
||||
value={this.props.mapStyle.sprite}
|
||||
onChange={this.changeStyleProperty.bind(this, "sprite")}
|
||||
/>
|
||||
</InputBlock>
|
||||
|
||||
<InputBlock label={"Glyphs URL"} doc={GlSpec.$root.glyphs.doc}>
|
||||
<InputBlock label={"Glyphs URL"} doc={styleSpec.latest.$root.glyphs.doc}>
|
||||
<StringInput {...inputProps}
|
||||
data-wd-key="modal-settings.glyphs"
|
||||
value={this.props.mapStyle.glyphs}
|
||||
onChange={this.changeStyleProperty.bind(this, "glyphs")}
|
||||
/>
|
||||
@@ -74,6 +80,7 @@ class SettingsModal extends React.Component {
|
||||
|
||||
<InputBlock label={"Mapbox Access Token"} doc={"Public access token for Mapbox services."}>
|
||||
<StringInput {...inputProps}
|
||||
data-wd-key="modal-settings.maputnik:mapbox_access_token"
|
||||
value={metadata['maputnik:mapbox_access_token']}
|
||||
onChange={this.changeMetadataProperty.bind(this, "maputnik:mapbox_access_token")}
|
||||
/>
|
||||
@@ -81,6 +88,7 @@ class SettingsModal extends React.Component {
|
||||
|
||||
<InputBlock label={"OpenMapTiles Access Token"} doc={"Public access token for the OpenMapTiles CDN."}>
|
||||
<StringInput {...inputProps}
|
||||
data-wd-key="modal-settings.maputnik:openmaptiles_access_token"
|
||||
value={metadata['maputnik:openmaptiles_access_token']}
|
||||
onChange={this.changeMetadataProperty.bind(this, "maputnik:openmaptiles_access_token")}
|
||||
/>
|
||||
@@ -88,6 +96,7 @@ class SettingsModal extends React.Component {
|
||||
|
||||
<InputBlock label={"Style Renderer"} doc={"Choose the default Maputnik renderer for this style."}>
|
||||
<SelectInput {...inputProps}
|
||||
data-wd-key="modal-settings.maputnik:renderer"
|
||||
options={[
|
||||
['mbgljs', 'MapboxGL JS'],
|
||||
['ol3', 'Open Layers 3'],
|
||||
|
||||
73
src/components/modals/ShortcutsModal.jsx
Normal file
73
src/components/modals/ShortcutsModal.jsx
Normal file
@@ -0,0 +1,73 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import Button from '../Button'
|
||||
import Modal from './Modal'
|
||||
|
||||
|
||||
class ShortcutsModal extends React.Component {
|
||||
static propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onOpenToggle: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
render() {
|
||||
const help = [
|
||||
{
|
||||
key: "?",
|
||||
text: "Shortcuts menu"
|
||||
},
|
||||
{
|
||||
key: "o",
|
||||
text: "Open modal"
|
||||
},
|
||||
{
|
||||
key: "e",
|
||||
text: "Export modal"
|
||||
},
|
||||
{
|
||||
key: "d",
|
||||
text: "Data Sources modal"
|
||||
},
|
||||
{
|
||||
key: "s",
|
||||
text: "Style Settings modal"
|
||||
},
|
||||
{
|
||||
key: "i",
|
||||
text: "Toggle inspect"
|
||||
},
|
||||
{
|
||||
key: "m",
|
||||
text: "Focus map"
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
return <Modal
|
||||
data-wd-key="shortcuts-modal"
|
||||
isOpen={this.props.isOpen}
|
||||
onOpenToggle={this.props.onOpenToggle}
|
||||
title={'Shortcuts'}
|
||||
>
|
||||
<div className="maputnik-modal-section maputnik-modal-shortcuts">
|
||||
<p>
|
||||
Press <code>ESC</code> to lose focus of any active elements, then press one of:
|
||||
</p>
|
||||
<ul>
|
||||
{help.map((item) => {
|
||||
return <li key={item.key}>
|
||||
<code>{item.key}</code> {item.text}
|
||||
</li>
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
</Modal>
|
||||
}
|
||||
}
|
||||
|
||||
export default ShortcutsModal
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react'
|
||||
import GlSpec from 'mapbox-gl-style-spec/reference/latest.js'
|
||||
import PropTypes from 'prop-types'
|
||||
import * as styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
|
||||
import Modal from './Modal'
|
||||
import Button from '../Button'
|
||||
import InputBlock from '../inputs/InputBlock'
|
||||
@@ -16,10 +17,10 @@ import DeleteIcon from 'react-icons/lib/md/delete'
|
||||
|
||||
class PublicSource extends React.Component {
|
||||
static propTypes = {
|
||||
id: React.PropTypes.string.isRequired,
|
||||
type: React.PropTypes.string.isRequired,
|
||||
title: React.PropTypes.string.isRequired,
|
||||
onSelect: React.PropTypes.func.isRequired,
|
||||
id: PropTypes.string.isRequired,
|
||||
type: PropTypes.string.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
onSelect: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -44,6 +45,10 @@ function editorMode(source) {
|
||||
if(source.tiles) return 'tilexyz_raster'
|
||||
return 'tilejson_raster'
|
||||
}
|
||||
if(source.type === 'raster-dem') {
|
||||
if(source.tiles) return 'tilexyz_raster-dem'
|
||||
return 'tilejson_raster-dem'
|
||||
}
|
||||
if(source.type === 'vector') {
|
||||
if(source.tiles) return 'tilexyz_vector'
|
||||
return 'tilejson_vector'
|
||||
@@ -54,10 +59,10 @@ function editorMode(source) {
|
||||
|
||||
class ActiveSourceTypeEditor extends React.Component {
|
||||
static propTypes = {
|
||||
sourceId: React.PropTypes.string.isRequired,
|
||||
source: React.PropTypes.object.isRequired,
|
||||
onDelete: React.PropTypes.func.isRequired,
|
||||
onChange: React.PropTypes.func.isRequired,
|
||||
sourceId: PropTypes.string.isRequired,
|
||||
source: PropTypes.object.isRequired,
|
||||
onDelete: PropTypes.func.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -87,7 +92,7 @@ class ActiveSourceTypeEditor extends React.Component {
|
||||
|
||||
class AddSource extends React.Component {
|
||||
static propTypes = {
|
||||
onAdd: React.PropTypes.func.isRequired,
|
||||
onAdd: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
@@ -126,6 +131,16 @@ class AddSource extends React.Component {
|
||||
minzoom: source.minzoom || 0,
|
||||
maxzoom: source.maxzoom || 14
|
||||
}
|
||||
case 'tilejson_raster-dem': return {
|
||||
type: 'raster-dem',
|
||||
url: source.url || 'http://localhost:3000/tilejson.json'
|
||||
}
|
||||
case 'tilexyz_raster-dem': return {
|
||||
type: 'raster-dem',
|
||||
tiles: source.tiles || ['http://localhost:3000/{x}/{y}/{z}.pbf'],
|
||||
minzoom: source.minzoom || 0,
|
||||
maxzoom: source.maxzoom || 14
|
||||
}
|
||||
default: return {}
|
||||
}
|
||||
}
|
||||
@@ -138,7 +153,7 @@ class AddSource extends React.Component {
|
||||
onChange={v => this.setState({ sourceId: v})}
|
||||
/>
|
||||
</InputBlock>
|
||||
<InputBlock label={"Source Type"} doc={GlSpec.source_tile.type.doc}>
|
||||
<InputBlock label={"Source Type"} doc={styleSpec.latest.source_vector.type.doc}>
|
||||
<SelectInput
|
||||
options={[
|
||||
['geojson', 'GeoJSON'],
|
||||
@@ -146,6 +161,8 @@ class AddSource extends React.Component {
|
||||
['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)'],
|
||||
]}
|
||||
onChange={mode => this.setState({mode: mode, source: this.defaultSource(mode)})}
|
||||
value={this.state.mode}
|
||||
@@ -167,10 +184,10 @@ class AddSource extends React.Component {
|
||||
|
||||
class SourcesModal extends React.Component {
|
||||
static propTypes = {
|
||||
mapStyle: React.PropTypes.object.isRequired,
|
||||
isOpen: React.PropTypes.bool.isRequired,
|
||||
onOpenToggle: React.PropTypes.func.isRequired,
|
||||
onStyleChanged: React.PropTypes.func.isRequired,
|
||||
mapStyle: PropTypes.object.isRequired,
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onOpenToggle: PropTypes.func.isRequired,
|
||||
onStyleChanged: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
stripTitle(source) {
|
||||
@@ -217,7 +234,7 @@ class SourcesModal extends React.Component {
|
||||
<div className="maputnik-modal-section">
|
||||
<h4>Choose Public Source</h4>
|
||||
<p>
|
||||
Add one of the publicly availble sources to your style.
|
||||
Add one of the publicly available sources to your style.
|
||||
</p>
|
||||
<div style={{maxwidth: 500}}>
|
||||
{tilesetOptions}
|
||||
|
||||
41
src/components/modals/SurveyModal.jsx
Normal file
41
src/components/modals/SurveyModal.jsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import Button from '../Button'
|
||||
import Modal from './Modal'
|
||||
|
||||
import logoImage from 'maputnik-design/logos/logo-color.svg'
|
||||
|
||||
class SurveyModal extends React.Component {
|
||||
static propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onOpenToggle: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
constructor(props) { super(props); }
|
||||
|
||||
onClick = () => {
|
||||
window.open('https://gregorywolanski.typeform.com/to/cPgaSY', '_blank');
|
||||
|
||||
this.props.onOpenToggle();
|
||||
}
|
||||
|
||||
render() {
|
||||
return <Modal
|
||||
data-wd-key="modal-survey"
|
||||
isOpen={this.props.isOpen}
|
||||
onOpenToggle={this.props.onOpenToggle}
|
||||
title="Maputnik Survey"
|
||||
>
|
||||
<div className="maputnik-modal-survey">
|
||||
<img className="maputnik-modal-survey__logo" src={logoImage} alt="" width="128" />
|
||||
<h1>You + Maputnik = Maputnik better for you</h1>
|
||||
<p className="maputnik-modal-survey__description">We don’t track you, so we don’t know how you use Maputnik. Help us make Maputnik better for you by completing a 7–minute survey carried out by our contributing designer.</p>
|
||||
<Button onClick={this.onClick} className="maputnik-big-button maputnik-white-button maputnik-wide-button">Take the Maputnik Survey</Button>
|
||||
<p className="maputnik-modal-survey__footnote">It takes 7 minutes, tops! Every question is optional.</p>
|
||||
</div>
|
||||
</Modal>
|
||||
}
|
||||
}
|
||||
|
||||
export default SurveyModal
|
||||
@@ -1,17 +1,22 @@
|
||||
import React from 'react'
|
||||
import GlSpec from 'mapbox-gl-style-spec/reference/latest.js'
|
||||
import PropTypes from 'prop-types'
|
||||
import * as styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
|
||||
import InputBlock from '../inputs/InputBlock'
|
||||
import StringInput from '../inputs/StringInput'
|
||||
import NumberInput from '../inputs/NumberInput'
|
||||
import SelectInput from '../inputs/SelectInput'
|
||||
|
||||
|
||||
class TileJSONSourceEditor extends React.Component {
|
||||
static propTypes = {
|
||||
source: React.PropTypes.object.isRequired,
|
||||
onChange: React.PropTypes.func.isRequired,
|
||||
source: PropTypes.object.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
children: PropTypes.node,
|
||||
}
|
||||
|
||||
render() {
|
||||
return <InputBlock label={"TileJSON URL"} doc={GlSpec.source_tile.url.doc}>
|
||||
return <div>
|
||||
<InputBlock label={"TileJSON URL"} doc={styleSpec.latest.source_vector.url.doc}>
|
||||
<StringInput
|
||||
value={this.props.source.url}
|
||||
onChange={url => this.props.onChange({
|
||||
@@ -20,13 +25,16 @@ class TileJSONSourceEditor extends React.Component {
|
||||
})}
|
||||
/>
|
||||
</InputBlock>
|
||||
{this.props.children}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
class TileURLSourceEditor extends React.Component {
|
||||
static propTypes = {
|
||||
source: React.PropTypes.object.isRequired,
|
||||
onChange: React.PropTypes.func.isRequired,
|
||||
source: PropTypes.object.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
children: PropTypes.node,
|
||||
}
|
||||
|
||||
changeTileUrl(idx, value) {
|
||||
@@ -42,7 +50,7 @@ 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={GlSpec.source_tile.tiles.doc}>
|
||||
return <InputBlock key={tileIndex} label={prefix[tileIndex] + " Tile URL"} doc={styleSpec.latest.source_vector.tiles.doc}>
|
||||
<StringInput
|
||||
value={tileUrl}
|
||||
onChange={this.changeTileUrl.bind(this, tileIndex)}
|
||||
@@ -54,7 +62,7 @@ class TileURLSourceEditor extends React.Component {
|
||||
render() {
|
||||
return <div>
|
||||
{this.renderTileUrls()}
|
||||
<InputBlock label={"Min Zoom"} doc={GlSpec.source_tile.minzoom.doc}>
|
||||
<InputBlock label={"Min Zoom"} doc={styleSpec.latest.source_vector.minzoom.doc}>
|
||||
<NumberInput
|
||||
value={this.props.source.minzoom || 0}
|
||||
onChange={minzoom => this.props.onChange({
|
||||
@@ -63,7 +71,7 @@ class TileURLSourceEditor extends React.Component {
|
||||
})}
|
||||
/>
|
||||
</InputBlock>
|
||||
<InputBlock label={"Max Zoom"} doc={GlSpec.source_tile.maxzoom.doc}>
|
||||
<InputBlock label={"Max Zoom"} doc={styleSpec.latest.source_vector.maxzoom.doc}>
|
||||
<NumberInput
|
||||
value={this.props.source.maxzoom || 22}
|
||||
onChange={maxzoom => this.props.onChange({
|
||||
@@ -72,6 +80,7 @@ class TileURLSourceEditor extends React.Component {
|
||||
})}
|
||||
/>
|
||||
</InputBlock>
|
||||
{this.props.children}
|
||||
</div>
|
||||
|
||||
}
|
||||
@@ -79,12 +88,12 @@ class TileURLSourceEditor extends React.Component {
|
||||
|
||||
class GeoJSONSourceEditor extends React.Component {
|
||||
static propTypes = {
|
||||
source: React.PropTypes.object.isRequired,
|
||||
onChange: React.PropTypes.func.isRequired,
|
||||
source: PropTypes.object.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
render() {
|
||||
return <InputBlock label={"GeoJSON Data"} doc={GlSpec.source_geojson.data.doc}>
|
||||
return <InputBlock label={"GeoJSON Data"} doc={styleSpec.latest.source_geojson.data.doc}>
|
||||
<StringInput
|
||||
value={this.props.source.data}
|
||||
onChange={data => this.props.onChange({
|
||||
@@ -98,9 +107,9 @@ class GeoJSONSourceEditor extends React.Component {
|
||||
|
||||
class SourceTypeEditor extends React.Component {
|
||||
static propTypes = {
|
||||
mode: React.PropTypes.string.isRequired,
|
||||
source: React.PropTypes.object.isRequired,
|
||||
onChange: React.PropTypes.func.isRequired,
|
||||
mode: PropTypes.string.isRequired,
|
||||
source: PropTypes.object.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -114,6 +123,19 @@ class SourceTypeEditor extends React.Component {
|
||||
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={styleSpec.latest.source_raster_dem.encoding.doc}>
|
||||
<SelectInput
|
||||
options={Object.keys(styleSpec.latest.source_raster_dem.encoding.values)}
|
||||
onChange={encoding => this.props.onChange({
|
||||
...this.props.source,
|
||||
encoding: encoding
|
||||
})}
|
||||
value={this.props.source.encoding || styleSpec.latest.source_raster_dem.encoding.default}
|
||||
/>
|
||||
</InputBlock>
|
||||
</TileURLSourceEditor>
|
||||
default: return null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,7 +91,8 @@
|
||||
"circle-stroke-width",
|
||||
"circle-pitch-scale",
|
||||
"circle-translate",
|
||||
"circle-translate-anchor"
|
||||
"circle-translate-anchor",
|
||||
"circle-pitch-alignment"
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -147,7 +148,9 @@
|
||||
"icon-rotate",
|
||||
"icon-padding",
|
||||
"icon-keep-upright",
|
||||
"icon-offset"
|
||||
"icon-offset",
|
||||
"icon-anchor",
|
||||
"icon-pitch-alignment"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -194,5 +197,35 @@
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"hillshade": {
|
||||
"groups": [
|
||||
{
|
||||
"title": "Paint properties",
|
||||
"type": "properties",
|
||||
"fields": [
|
||||
"hillshade-illumination-direction",
|
||||
"hillshade-illumination-anchor",
|
||||
"hillshade-exaggeration",
|
||||
"hillshade-shadow-color",
|
||||
"hillshade-highlight-color",
|
||||
"hillshade-accent-color"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"heatmap": {
|
||||
"groups": [
|
||||
{
|
||||
"title": "Paint properties",
|
||||
"type": "properties",
|
||||
"fields": [
|
||||
"heatmap-radius",
|
||||
"heatmap-weight",
|
||||
"heatmap-intensity",
|
||||
"heatmap-opacity"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,31 +3,31 @@
|
||||
"id": "klokantech-basic",
|
||||
"title": "Klokantech Basic",
|
||||
"url": "https://rawgit.com/openmaptiles/klokantech-basic-gl-style/master/style.json",
|
||||
"thumbnail": "http://maputnik.com/thumbnails/klokantech-basic.png"
|
||||
"thumbnail": "https://maputnik.github.io/thumbnails/klokantech-basic.png"
|
||||
},
|
||||
{
|
||||
"id": "dark-matter",
|
||||
"title": "Dark Matter",
|
||||
"url": "https://rawgit.com/openmaptiles/dark-matter-gl-style/master/style.json",
|
||||
"thumbnail": "http://maputnik.com/thumbnails/dark-matter.png"
|
||||
"thumbnail": "https://maputnik.github.io/thumbnails/dark-matter.png"
|
||||
},
|
||||
{
|
||||
"id": "positron",
|
||||
"title": "Positron",
|
||||
"url": "https://rawgit.com/openmaptiles/positron-gl-style/master/style.json",
|
||||
"thumbnail": "http://maputnik.com/thumbnails/positron.png"
|
||||
"thumbnail": "https://maputnik.github.io/thumbnails/positron.png"
|
||||
},
|
||||
{
|
||||
"id": "osm-bright",
|
||||
"title": "OSM Bright",
|
||||
"url": "https://rawgit.com/openmaptiles/osm-bright-gl-style/master/style.json",
|
||||
"thumbnail": "http://maputnik.com/thumbnails/osm-bright.png"
|
||||
"thumbnail": "https://maputnik.github.io/thumbnails/osm-bright.png"
|
||||
},
|
||||
{
|
||||
"id": "osm-liberty",
|
||||
"title": "OSM Liberty",
|
||||
"url": "https://rawgit.com/lukasmartinelli/osm-liberty/gh-pages/style.json",
|
||||
"thumbnail": "https://cdn.rawgit.com/lukasmartinelli/osm-liberty/gh-pages/thumbnail.png"
|
||||
"url": "https://rawgit.com/maputnik/osm-liberty/gh-pages/style.json",
|
||||
"thumbnail": "https://cdn.rawgit.com/maputnik/osm-liberty/gh-pages/thumbnail.png"
|
||||
},
|
||||
{
|
||||
"id": "empty-style",
|
||||
@@ -39,24 +39,18 @@
|
||||
"id": "mapbox-satellite",
|
||||
"title": "Mapbox Satellite",
|
||||
"url": "https://rawgit.com/mapbox/mapbox-gl-styles/master/styles/satellite-v9.json",
|
||||
"thumbnail": "http://maputnik.com/thumbnails/mapbox-satellite.png"
|
||||
"thumbnail": "https://maputnik.github.io/thumbnails/mapbox-satellite.png"
|
||||
},
|
||||
{
|
||||
"id": "mapbox-bright",
|
||||
"title": "Mapbox Bright",
|
||||
"url": "https://rawgit.com/mapbox/mapbox-gl-styles/master/styles/bright-v9.json",
|
||||
"thumbnail": "http://maputnik.com/thumbnails/mapbox-bright.png"
|
||||
"thumbnail": "https://maputnik.github.io/thumbnails/mapbox-bright.png"
|
||||
},
|
||||
{
|
||||
"id": "mapbox-basic",
|
||||
"title": "Mapbox Basic",
|
||||
"url": "https://rawgit.com/mapbox/mapbox-gl-styles/master/styles/basic-v9.json",
|
||||
"thumbnail": "http://maputnik.com/thumbnails/mapbox-basic.png"
|
||||
},
|
||||
{
|
||||
"id": "tilezen",
|
||||
"title": "Tilezen",
|
||||
"url": "https://rawgit.com/lukasmartinelli/tilezen-gl-style/master/style.json",
|
||||
"thumbnail": "http://maputnik.com/thumbnails/tilezen.png"
|
||||
"thumbnail": "https://maputnik.github.io/thumbnails/mapbox-basic.png"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -8,14 +8,5 @@
|
||||
"type": "vector",
|
||||
"url": "https://free.tilehosting.com/data/v3.json?key={key}",
|
||||
"title": "OpenMapTiles"
|
||||
},
|
||||
"tilezen": {
|
||||
"type": "vector",
|
||||
"tiles": [
|
||||
"http://tile.mapzen.com/mapzen/vector/v1/{layers}/{z}/{x}/{y}.pbf?api_key=mapzen-RVcyVL7"
|
||||
],
|
||||
"minZoom": 0,
|
||||
"maxZoom": 15,
|
||||
"title": "Mapzen Vector Tile Service"
|
||||
}
|
||||
}
|
||||
|
||||
12
src/libs/accessibility.js
Normal file
12
src/libs/accessibility.js
Normal file
@@ -0,0 +1,12 @@
|
||||
import lodash from 'lodash'
|
||||
|
||||
|
||||
// Throttle for 3 seconds so when a user enables it they don't have to refresh the page.
|
||||
const reducedMotionEnabled = lodash.throttle(() => {
|
||||
return window.matchMedia("(prefers-reduced-motion: reduce)").matches
|
||||
}, 3000);
|
||||
|
||||
|
||||
export default {
|
||||
reducedMotionEnabled
|
||||
}
|
||||
44
src/libs/debug.js
Normal file
44
src/libs/debug.js
Normal file
@@ -0,0 +1,44 @@
|
||||
import querystring from 'querystring'
|
||||
|
||||
|
||||
const debugStore = {};
|
||||
|
||||
function enabled() {
|
||||
const qs = querystring.parse(window.location.search.slice(1));
|
||||
if(qs.hasOwnProperty("debug")) {
|
||||
return !!qs.debug.match(/^(|1|true)$/);
|
||||
}
|
||||
else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function genErr() {
|
||||
return new Error("Debug not enabled, enable by appending '?debug' to your query string");
|
||||
}
|
||||
|
||||
function set(namespace, key, value) {
|
||||
if(!enabled()) {
|
||||
throw genErr();
|
||||
}
|
||||
debugStore[namespace] = debugStore[namespace] || {};
|
||||
debugStore[namespace][key] = value;
|
||||
}
|
||||
|
||||
function get(namespace, key) {
|
||||
if(!enabled()) {
|
||||
throw genErr();
|
||||
}
|
||||
if(debugStore.hasOwnProperty(namespace)) {
|
||||
return debugStore[namespace][key];
|
||||
}
|
||||
}
|
||||
|
||||
const mod = {
|
||||
enabled,
|
||||
get,
|
||||
set
|
||||
}
|
||||
|
||||
window.debug = mod;
|
||||
export default mod;
|
||||
@@ -1,7 +1,7 @@
|
||||
import diffStyles from 'mapbox-gl-style-spec/lib/diff'
|
||||
import * as styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
|
||||
|
||||
export function diffMessages(beforeStyle, afterStyle) {
|
||||
const changes = diffStyles(beforeStyle, afterStyle)
|
||||
const changes = styleSpec.diff(beforeStyle, afterStyle)
|
||||
return changes.map(cmd => cmd.command + ' ' + cmd.args.join(' '))
|
||||
}
|
||||
|
||||
|
||||
9
src/libs/document-uid.js
Normal file
9
src/libs/document-uid.js
Normal file
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* A unique id for the current document.
|
||||
*/
|
||||
let REF = 0;
|
||||
|
||||
export default function(prefix="") {
|
||||
REF++;
|
||||
return prefix+REF;
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import GlSpec from 'mapbox-gl-style-spec/reference/latest.js'
|
||||
import * as styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
|
||||
export const combiningFilterOps = ['all', 'any', 'none']
|
||||
export const setFilterOps = ['in', '!in']
|
||||
export const otherFilterOps = Object
|
||||
.keys(GlSpec.filter_operator.values)
|
||||
.keys(styleSpec.latest.filter_operator.values)
|
||||
.filter(op => combiningFilterOps.indexOf(op) < 0)
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import GlSpec from 'mapbox-gl-style-spec/reference/latest.js'
|
||||
import * as styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
|
||||
|
||||
export function changeType(layer, newType) {
|
||||
const changedPaintProps = { ...layer.paint }
|
||||
Object.keys(changedPaintProps).forEach(propertyName => {
|
||||
if(!(propertyName in GlSpec['paint_' + newType])) {
|
||||
if(!(propertyName in styleSpec.latest['paint_' + newType])) {
|
||||
delete changedPaintProps[propertyName]
|
||||
}
|
||||
})
|
||||
|
||||
const changedLayoutProps = { ...layer.layout }
|
||||
Object.keys(changedLayoutProps).forEach(propertyName => {
|
||||
if(!(propertyName in GlSpec['layout_' + newType])) {
|
||||
if(!(propertyName in styleSpec.latest['layout_' + newType])) {
|
||||
delete changedLayoutProps[propertyName]
|
||||
}
|
||||
})
|
||||
@@ -27,6 +27,28 @@ export function changeType(layer, newType) {
|
||||
* to a {@newValue}.
|
||||
*/
|
||||
export function changeProperty(layer, group, property, newValue) {
|
||||
// Remove the property if undefined
|
||||
if(newValue === undefined) {
|
||||
if(group) {
|
||||
const newLayer = {
|
||||
...layer
|
||||
};
|
||||
delete newLayer[group][property];
|
||||
|
||||
// Remove the group if it is now empty
|
||||
if(Object.keys(newLayer[group]).length < 1) {
|
||||
delete newLayer[group];
|
||||
}
|
||||
return newLayer;
|
||||
} else {
|
||||
const newLayer = {
|
||||
...layer
|
||||
};
|
||||
delete newLayer[property];
|
||||
return newLayer;
|
||||
}
|
||||
}
|
||||
else {
|
||||
if(group) {
|
||||
return {
|
||||
...layer,
|
||||
@@ -42,3 +64,4 @@ export function changeProperty(layer, group, property, newValue) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
11
src/libs/mapbox-rtl.js
Normal file
11
src/libs/mapbox-rtl.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import MapboxGl from 'mapbox-gl/dist/mapbox-gl.js'
|
||||
|
||||
// Load mapbox-gl-rtl-text using object urls without needing http://localhost for AJAX.
|
||||
const data = require("base64-loader?mimetype=text/javascript!@mapbox/mapbox-gl-rtl-text/mapbox-gl-rtl-text.js");
|
||||
|
||||
const blob = new window.Blob([window.atob(data)]);
|
||||
const objectUrl = window.URL.createObjectURL(blob, {
|
||||
type: "text/javascript"
|
||||
});
|
||||
|
||||
MapboxGl.setRTLTextPlugin(objectUrl);
|
||||
@@ -1,4 +1,5 @@
|
||||
import request from 'request'
|
||||
import npmurl from 'url'
|
||||
|
||||
function loadJSON(url, defaultValue, cb) {
|
||||
request({
|
||||
@@ -23,9 +24,15 @@ export function downloadGlyphsMetadata(urlTemplate, cb) {
|
||||
if(!urlTemplate) return cb([])
|
||||
|
||||
// Special handling because Tileserver GL serves the fontstacks metadata differently
|
||||
// https://github.com/klokantech/tileserver-gl/pull/104
|
||||
let url = urlTemplate.replace('/fonts/{fontstack}/{range}.pbf', '/fontstacks.json')
|
||||
url = url.replace('{fontstack}/{range}.pbf', 'fontstacks.json')
|
||||
// https://github.com/klokantech/tileserver-gl/pull/104#issuecomment-274444087
|
||||
let urlObj = npmurl.parse(urlTemplate);
|
||||
const normPathPart = '/%7Bfontstack%7D/%7Brange%7D.pbf';
|
||||
if(urlObj.pathname === normPathPart) {
|
||||
urlObj.pathname = '/fontstacks.json';
|
||||
} else {
|
||||
urlObj.pathname = urlObj.pathname.replace(normPathPart, '.json');
|
||||
}
|
||||
let url = npmurl.format(urlObj);
|
||||
|
||||
loadJSON(url, [], cb)
|
||||
}
|
||||
|
||||
17
src/libs/query-util.js
Normal file
17
src/libs/query-util.js
Normal file
@@ -0,0 +1,17 @@
|
||||
function asBool(queryObj, key) {
|
||||
if(queryObj.hasOwnProperty(key)) {
|
||||
if(queryObj[key].match(/^false|0$/)) {
|
||||
return false;
|
||||
}
|
||||
else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
asBool
|
||||
}
|
||||
14
src/libs/sort-numerically.js
Normal file
14
src/libs/sort-numerically.js
Normal file
@@ -0,0 +1,14 @@
|
||||
export default function(a, b) {
|
||||
a = parseFloat(a, 10);
|
||||
b = parseFloat(b, 10);
|
||||
|
||||
if(a < b) {
|
||||
return -1
|
||||
}
|
||||
else if(a > b) {
|
||||
return 1
|
||||
}
|
||||
else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import React from 'react';
|
||||
import spec from 'mapbox-gl-style-spec/reference/latest.min.js'
|
||||
import derefLayers from 'mapbox-gl-style-spec/lib/deref'
|
||||
import deref from '@mapbox/mapbox-gl-style-spec/deref'
|
||||
import tokens from '../config/tokens.json'
|
||||
|
||||
// Empty style is always used if no style could be restored or fetched
|
||||
@@ -37,7 +36,7 @@ function ensureHasNoInteractive(style) {
|
||||
function ensureHasNoRefs(style) {
|
||||
const derefedStyle = {
|
||||
...style,
|
||||
layers: derefLayers(style.layers)
|
||||
layers: deref(style.layers)
|
||||
}
|
||||
return derefedStyle
|
||||
}
|
||||
@@ -55,12 +54,23 @@ function indexOfLayer(layers, layerId) {
|
||||
return null
|
||||
}
|
||||
|
||||
function replaceAccessToken(mapStyle) {
|
||||
function replaceAccessToken(mapStyle, opts={}) {
|
||||
const omtSource = mapStyle.sources.openmaptiles
|
||||
if(!omtSource) return mapStyle
|
||||
if(!omtSource.hasOwnProperty("url")) return mapStyle
|
||||
|
||||
const metadata = mapStyle.metadata || {}
|
||||
const accessToken = metadata['maputnik:openmaptiles_access_token'] || tokens.openmaptiles
|
||||
let accessToken = metadata['maputnik:openmaptiles_access_token'];
|
||||
|
||||
if(opts.allowFallback && !accessToken) {
|
||||
accessToken = tokens.openmaptiles;
|
||||
}
|
||||
|
||||
if(!accessToken) {
|
||||
// Early exit.
|
||||
return mapStyle;
|
||||
}
|
||||
|
||||
const changedSources = {
|
||||
...mapStyle.sources,
|
||||
openmaptiles: {
|
||||
@@ -70,7 +80,7 @@ function replaceAccessToken(mapStyle) {
|
||||
}
|
||||
const changedStyle = {
|
||||
...mapStyle,
|
||||
glyphs: mapStyle.glyphs.replace('{key}', accessToken),
|
||||
glyphs: mapStyle.glyphs ? mapStyle.glyphs.replace('{key}', accessToken) : mapStyle.glyphs,
|
||||
sources: changedSources
|
||||
}
|
||||
|
||||
|
||||
26
src/libs/zoomcontrol.js
Normal file
26
src/libs/zoomcontrol.js
Normal file
@@ -0,0 +1,26 @@
|
||||
export default class ZoomControl {
|
||||
onAdd(map) {
|
||||
this._map = map;
|
||||
this._container = document.createElement('div');
|
||||
this._container.className = 'mapboxgl-ctrl mapboxgl-ctrl-group mapboxgl-ctrl-zoom';
|
||||
|
||||
this.addEventListeners();
|
||||
|
||||
return this._container;
|
||||
}
|
||||
|
||||
updateZoomLevel() {
|
||||
this._container.innerHTML = `Zoom level: ${this._map.getZoom().toFixed(2)}`;
|
||||
}
|
||||
|
||||
addEventListeners (){
|
||||
this._map.on('render', this.updateZoomLevel.bind(this) );
|
||||
this._map.on('zoomIn', this.updateZoomLevel.bind(this) );
|
||||
this._map.on('zoomOut', this.updateZoomLevel.bind(this) );
|
||||
}
|
||||
|
||||
onRemove() {
|
||||
this._container.parentNode.removeChild(this._container);
|
||||
this._map = undefined;
|
||||
}
|
||||
}
|
||||
9
src/manifest.json
Normal file
9
src/manifest.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "Maputnik",
|
||||
"short_name": "Maputnik",
|
||||
"description": "Visual Map Designer",
|
||||
"start_url": ".",
|
||||
"display": "browser",
|
||||
"background_color": "#1c1f24",
|
||||
"theme_color": "#1c1f24"
|
||||
}
|
||||
@@ -25,6 +25,13 @@
|
||||
color: white;
|
||||
}
|
||||
|
||||
.mapboxgl-ctrl-zoom {
|
||||
color: rgb(138, 138, 138);
|
||||
font-weight: bold;
|
||||
padding: 4px 8px;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.mapboxgl-ctrl-group {
|
||||
background: rgb(28, 31, 36);
|
||||
}
|
||||
|
||||
@@ -76,3 +76,7 @@ label:hover {
|
||||
a {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.hide {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
// MAP
|
||||
.maputnik-map {
|
||||
position: fixed !important;
|
||||
top: 40px;
|
||||
top: $toolbar-height + $toolbar-offset;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
height: calc(100% - $toolbar-height);
|
||||
height: calc(100% - #{$toolbar-height + $toolbar-offset});
|
||||
width: 75%;
|
||||
}
|
||||
|
||||
@@ -34,24 +34,29 @@
|
||||
top: 20px;
|
||||
left: 0;
|
||||
width: 120px;
|
||||
z-index: 3;
|
||||
z-index: 10;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.maputnik-doc-target:hover .maputnik-doc-popup {
|
||||
display: block;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
// BUTTON
|
||||
.maputnik-button {
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
background-color: $color-midgray;
|
||||
color: $color-lowgray;
|
||||
font-size: $font-size-6;
|
||||
padding: $margin-2;
|
||||
user-select: none;
|
||||
border-width: 0;
|
||||
border-radius: 2px;
|
||||
box-sizing: border-box;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
background-color: lighten($color-midgray, 12);
|
||||
@@ -66,6 +71,20 @@
|
||||
font-size: $font-size-5;
|
||||
}
|
||||
|
||||
.maputnik-wide-button {
|
||||
padding: $margin-2 $margin-3;
|
||||
}
|
||||
|
||||
.maputnik-green-button {
|
||||
background-color: $color-green;
|
||||
color: $color-black;
|
||||
}
|
||||
|
||||
.maputnik-white-button {
|
||||
background-color: $color-white;
|
||||
color: $color-black;
|
||||
}
|
||||
|
||||
.maputnik-icon-button {
|
||||
background-color: transparent;
|
||||
|
||||
@@ -104,13 +123,17 @@
|
||||
.maputnik-action-block {
|
||||
.maputnik-input-block-label {
|
||||
display: inline-block;
|
||||
width: 43%;
|
||||
width: 35%;
|
||||
}
|
||||
|
||||
.maputnik-input-block-action {
|
||||
vertical-align: top;
|
||||
display: inline-block;
|
||||
width: 7%;
|
||||
width: 15%;
|
||||
}
|
||||
|
||||
.maputnik-input-block-action > div {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,3 +3,15 @@
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
.maputnik-render-gist {
|
||||
p {
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
input.maputnik-string {
|
||||
margin-left: 5px;
|
||||
width: 60%;
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,24 +3,34 @@
|
||||
}
|
||||
|
||||
.maputnik-filter-editor {
|
||||
@extend .clearfix;
|
||||
color: $color-lowgray;
|
||||
}
|
||||
|
||||
.maputnik-filter-editor-property {
|
||||
display: inline-block;
|
||||
width: '22%';
|
||||
width: 25%;
|
||||
}
|
||||
|
||||
.maputnik-filter-editor-operator {
|
||||
display: inline-block;
|
||||
width: 19%;
|
||||
margin-left: 2%;
|
||||
display: inline-block;
|
||||
width: 17%;
|
||||
|
||||
.maputnik-select {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.maputnik-filter-editor-args {
|
||||
display: inline-block;
|
||||
width: 54%;
|
||||
margin-left: 2%;
|
||||
|
||||
.maputnik-string,
|
||||
.maputnik-number {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.maputnik-filter-editor-compound-select {
|
||||
@@ -40,10 +50,6 @@
|
||||
color: $color-midgray;
|
||||
}
|
||||
|
||||
.maputnik-filter-editor {
|
||||
@extend .clearfix;
|
||||
}
|
||||
|
||||
.maputnik-add-filter {
|
||||
display: inline-block;
|
||||
float: right;
|
||||
@@ -57,9 +63,6 @@
|
||||
.maputnik-filter-editor-block-action {
|
||||
margin-top: $margin-2;
|
||||
margin-bottom: $margin-2;
|
||||
}
|
||||
|
||||
.maputnik-filter-editor-block-action {
|
||||
display: inline-block;
|
||||
width: 6%;
|
||||
margin-right: 1.5%;
|
||||
@@ -70,27 +73,3 @@
|
||||
width: 92.5%;
|
||||
}
|
||||
|
||||
.maputnik-filter-editor-property {
|
||||
display: inline-block;
|
||||
width: 25%;
|
||||
}
|
||||
|
||||
.maputnik-filter-editor-operator {
|
||||
display: inline-block;
|
||||
width: 17%;
|
||||
|
||||
.maputnik-select {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.maputnik-filter-editor-args {
|
||||
display: inline-block;
|
||||
width: 54%;
|
||||
|
||||
.maputnik-string,
|
||||
.maputnik-number {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user