mirror of
https://github.com/maputnik/editor.git
synced 2025-12-06 14:20:02 +00:00
Compare commits
702 Commits
v0.2.2
...
v1.5.0-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f1ddf4e57e | ||
|
|
64e65dc7d3 | ||
|
|
1e07a88aed | ||
|
|
6e49cc65a9 | ||
|
|
06d579118a | ||
|
|
f0e4b5b930 | ||
|
|
088127a9a5 | ||
|
|
98c235bc21 | ||
|
|
70f1f9ffac | ||
|
|
c5ea9494df | ||
|
|
9a34db7008 | ||
|
|
988b7fca0f | ||
|
|
bdc6489db4 | ||
|
|
49b096b601 | ||
|
|
31d83f6a26 | ||
|
|
03e52b7a72 | ||
|
|
551e950c39 | ||
|
|
a7620f83a6 | ||
|
|
0384181ee1 | ||
|
|
fd59f42819 | ||
|
|
cc51774259 | ||
|
|
5a19245ee0 | ||
|
|
45f45b7547 | ||
|
|
530bfaf3b3 | ||
|
|
6ea70ab9cf | ||
|
|
a0e2d68dae | ||
|
|
1447e8bfb5 | ||
|
|
c0480a50ea | ||
|
|
09ba2be416 | ||
|
|
115ce3305d | ||
|
|
960b2022ed | ||
|
|
252b442ca9 | ||
|
|
03b9ddda9c | ||
|
|
968d7d7fda | ||
|
|
b211f1cd12 | ||
|
|
870d4349f4 | ||
|
|
d88bc59720 | ||
|
|
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 | ||
|
|
45bdf53a41 | ||
|
|
00e94212bd | ||
|
|
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 | ||
|
|
5c286f8d96 | ||
|
|
404b53587f | ||
|
|
e5fbe3b74a | ||
|
|
3f262885ca | ||
|
|
c837179f71 | ||
|
|
9a947658e2 | ||
|
|
2458d4b637 | ||
|
|
e4850805fb | ||
|
|
3a15a3bb06 | ||
|
|
75ca1fa930 | ||
|
|
377840ca24 | ||
|
|
48e9589b58 | ||
|
|
11e9cef834 | ||
|
|
7e3aa09d3e | ||
|
|
e3b7e002b4 | ||
|
|
3b7fb7ae75 | ||
|
|
fab004cdfe | ||
|
|
07523c00f0 | ||
|
|
c15ac14f88 | ||
|
|
8f6006c19f | ||
|
|
16bedcf5b1 | ||
|
|
05349d8ffe | ||
|
|
a1e1895651 | ||
|
|
a111599850 | ||
|
|
121a95cee8 | ||
|
|
decd1f3ea2 | ||
|
|
c632718324 | ||
|
|
9509b59696 | ||
|
|
24dc71344e | ||
|
|
82a11e4b98 | ||
|
|
fc8665ed93 | ||
|
|
ca9424e23d | ||
|
|
99856b1bb3 | ||
|
|
fb518c2be5 | ||
|
|
1248a53029 | ||
|
|
6ce43840e5 | ||
|
|
41d9fb1c44 | ||
|
|
fd9be8f08f | ||
|
|
69a665373f | ||
|
|
8c2b110115 | ||
|
|
5e3b2dd0df | ||
|
|
d045213fa3 | ||
|
|
63bba67750 | ||
|
|
52e8fd2c29 | ||
|
|
5479b240e1 | ||
|
|
f209d8e9a5 | ||
|
|
ac40d7727e | ||
|
|
7bd9d3f5da | ||
|
|
68685dcf42 | ||
|
|
6be6db8f5e | ||
|
|
236dd79b85 | ||
|
|
7d905c5e06 | ||
|
|
6fa2542b56 | ||
|
|
7627b8fb45 | ||
|
|
5901427534 | ||
|
|
a30e57c4d8 | ||
|
|
69f2e12ea0 | ||
|
|
93c7f323fc | ||
|
|
cbe2a4c180 | ||
|
|
2e0cc4511c | ||
|
|
bcab165f97 | ||
|
|
2516fba105 | ||
|
|
9ca8760564 | ||
|
|
df94d9c842 | ||
|
|
abceb457c9 | ||
|
|
26a865bb50 | ||
|
|
d0f047d88a | ||
|
|
76d2d06e77 | ||
|
|
6c56006fbf | ||
|
|
bbe45cf8ee | ||
|
|
82da251218 | ||
|
|
196d9f0a10 | ||
|
|
cb752c0343 | ||
|
|
3917a3e323 | ||
|
|
fed1f09434 | ||
|
|
840778b64f | ||
|
|
0908856b4f | ||
|
|
b51354ae1d | ||
|
|
9ef24428fe | ||
|
|
4a75b0381b | ||
|
|
2426117233 | ||
|
|
d40c704c69 | ||
|
|
cb4fdb0f9f | ||
|
|
f0d04bdb07 | ||
|
|
df61ae8c7a | ||
|
|
2ff8ec07bb | ||
|
|
6021b51385 | ||
|
|
40111e0d8e | ||
|
|
43d9440e05 | ||
|
|
3a3e90c3dc | ||
|
|
104d6311ec | ||
|
|
f5256cf80a | ||
|
|
b470885263 | ||
|
|
7ff0ac9bb5 | ||
|
|
0fb59ca544 | ||
|
|
09b6b2dffe | ||
|
|
a8a3b7a5ad | ||
|
|
766a3e387e | ||
|
|
ec9fc8f6ad | ||
|
|
0f272e233b | ||
|
|
f806e797fa | ||
|
|
cff0a15f7e | ||
|
|
d3276829b2 | ||
|
|
a3caf8499c | ||
|
|
d739ca812c | ||
|
|
cb89ca6ef7 | ||
|
|
c3417241f1 | ||
|
|
5d70de6202 | ||
|
|
c09ffc9d41 | ||
|
|
e19a41d015 | ||
|
|
0a0400a297 | ||
|
|
153232c143 | ||
|
|
7e8813f417 | ||
|
|
b72f86a78d | ||
|
|
fed530f5f2 | ||
|
|
ba0a94f3ad | ||
|
|
d9b458d7fd | ||
|
|
ed9b806143 | ||
|
|
5bb68a38c2 | ||
|
|
cfeaf2cdce | ||
|
|
887b23ce1f | ||
|
|
f227392f9b | ||
|
|
2f7658e245 | ||
|
|
4f0c641eb0 | ||
|
|
1538f2e174 | ||
|
|
580068bf63 | ||
|
|
91604afccb | ||
|
|
c363c88f23 | ||
|
|
e9daee4470 | ||
|
|
118f0360d0 | ||
|
|
7c9dcb3083 | ||
|
|
7c3906fa40 | ||
|
|
7b24cbf39b | ||
|
|
e7b11d8bc9 | ||
|
|
08854cd88f | ||
|
|
cb46ac5421 | ||
|
|
c9fd00e2ed | ||
|
|
7c23fe3646 | ||
|
|
56aacb0149 | ||
|
|
12411ee886 | ||
|
|
85cef2945d | ||
|
|
a1dfeca6e0 | ||
|
|
3be6d14637 | ||
|
|
74b3ef9e88 | ||
|
|
019dfe9f8a | ||
|
|
e92dfd8284 | ||
|
|
fa38667125 | ||
|
|
ce39ae723c | ||
|
|
99acbd4d92 | ||
|
|
b0e9790382 | ||
|
|
e00cdde3af | ||
|
|
c3a634b216 | ||
|
|
4f26a521a0 | ||
|
|
ca6b48843c | ||
|
|
0eb00312f4 | ||
|
|
e7709dae15 | ||
|
|
03796c963b | ||
|
|
b50855a4a9 | ||
|
|
24a90b3c57 | ||
|
|
cf80e80025 | ||
|
|
48f10bcb73 | ||
|
|
7bc2323401 | ||
|
|
a71ac502d6 | ||
|
|
f2dd785e7b | ||
|
|
0b99e571c4 | ||
|
|
cfc6085718 | ||
|
|
384b2d4bea | ||
|
|
1058dbfb5a | ||
|
|
bda7ce7390 | ||
|
|
7b631b0510 | ||
|
|
1d7768e37c | ||
|
|
89d497c73f | ||
|
|
886c87f231 | ||
|
|
d567a4f98b | ||
|
|
5eb0e36faf | ||
|
|
51a2eabc91 | ||
|
|
007bdad70a | ||
|
|
1f1a919c77 | ||
|
|
3be3a716d4 | ||
|
|
ae9afdd8d9 | ||
|
|
a5307054b3 | ||
|
|
d16c3f4356 | ||
|
|
853361ace7 | ||
|
|
e41e1eb2f1 | ||
|
|
e36c233b49 | ||
|
|
d1b8f8d63e | ||
|
|
29cfb58a56 | ||
|
|
bf5131cadd | ||
|
|
ccc39b87db | ||
|
|
604be38b7c | ||
|
|
160bd9563b | ||
|
|
488fdf2bd5 | ||
|
|
a0e1e6152b | ||
|
|
58897f1856 | ||
|
|
80678af691 | ||
|
|
ba271e1fc6 | ||
|
|
c7ac90ba15 | ||
|
|
0dc335ea5f | ||
|
|
acac314d27 | ||
|
|
916c1dc9fc | ||
|
|
c159f7041f | ||
|
|
a3d586a75d | ||
|
|
6b0b29d1da | ||
|
|
8afda2fe28 | ||
|
|
beb1a2a8d1 | ||
|
|
436e0c2095 | ||
|
|
e1bc2a321a | ||
|
|
720c8f108b | ||
|
|
4db5c7cf68 | ||
|
|
8f561d8a27 | ||
|
|
0c483cffe3 | ||
|
|
def5ebb587 | ||
|
|
6e9e66b147 | ||
|
|
f332d517f3 | ||
|
|
04eab70e27 | ||
|
|
cfbcdc7fa1 | ||
|
|
c95dd75e2a | ||
|
|
4408f3ab3b |
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
|
||||
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -1,6 +1,8 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
@@ -28,3 +30,6 @@ node_modules
|
||||
|
||||
# Ignore build files
|
||||
public
|
||||
/errorShots
|
||||
/old
|
||||
/build
|
||||
|
||||
31
.travis.yml
31
.travis.yml
@@ -1,21 +1,22 @@
|
||||
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:
|
||||
- mkdir public
|
||||
- npm run build
|
||||
- node --stack_size=100000 $(which npm) run build
|
||||
- npm run lint
|
||||
- npm run test
|
||||
- npm run lint-styles
|
||||
addons:
|
||||
apt:
|
||||
sources:
|
||||
- ubuntu-toolchain-r-test
|
||||
packages:
|
||||
- g++-4.8
|
||||
|
||||
167
README.md
167
README.md
@@ -1,28 +1,44 @@
|
||||
# Maputnik [](https://travis-ci.org/maputnik/editor) [](https://ci.appveyor.com/project/lukasmartinelli/editor) [](https://tldrlegal.com/license/mit-license)
|
||||
# Maputnik
|
||||
|
||||
<img width="200" align="right" alt="Maputnik" src="media/maputnik.png" />
|
||||
[][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. Creating your own custom map is easy with **Maputnik**.
|
||||
targeted at developers and map designers.
|
||||
|
||||
*Maputnik is an early prototype and is under development.
|
||||
[Thanks to the supporters of the Kickstarter campaign who made this project possible](https://www.kickstarter.com/projects/174808720/maputnik-visual-map-editor-for-mapbox-gl)*.
|
||||
- :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
|
||||
|
||||
## Features
|
||||
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.
|
||||
|
||||
- [x] Completely free and open source
|
||||
- [x] Visual interface for designing maps
|
||||
- [x] Immediate feedback (thanks to [style diffs](https://github.com/mapbox/mapbox-gl-style-spec/blob/mb-pages/lib/diff.js))
|
||||
- [x] Edit layers
|
||||
- [x] Easy to deploy as single HTML file
|
||||
- [ ] Support for Open Layers 3
|
||||
|
||||

|
||||
## 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!
|
||||
|
||||
- :link: **Study the [Maputnik Wiki](https://github.com/maputnik/editor/wiki)**
|
||||
- :video_camera: Design a map from Scratch https://youtu.be/XoDh0gEnBQo
|
||||
|
||||
[](https://youtu.be/XoDh0gEnBQo)
|
||||
|
||||
## Develop
|
||||
|
||||
Maputnik is written in ES6 and is using [React](https://github.com/facebook/react), [Immutable.js](https://facebook.github.io/immutable-js/) and [Mapbox GL JS](https://www.mapbox.com/mapbox-gl-js/api/).
|
||||
Maputnik is written in ES6 and is using [React](https://github.com/facebook/react) and [Mapbox GL JS](https://www.mapbox.com/mapbox-gl-js/api/).
|
||||
|
||||
We ensure building and developing Maputnik works with
|
||||
|
||||
@@ -38,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
|
||||
@@ -47,103 +68,95 @@ npm run build
|
||||
Lint the JavaScript code.
|
||||
|
||||
```
|
||||
# install lint dependencies
|
||||
npm install --save-dev eslint eslint-plugin-react
|
||||
# run linter
|
||||
npm run lint
|
||||
npm run lint-styles
|
||||
```
|
||||
|
||||
## Docker
|
||||
|
||||
Start a container using the official Docker image.
|
||||
```
|
||||
docker run --name maputnik -p 8888:8888 -d maputnik/editor
|
||||
```
|
||||
## Tests
|
||||
For testing we use [webdriverio](http://webdriver.io) and [selenium-standalone](https://github.com/vvo/selenium-standalone)
|
||||
|
||||
Stop the container
|
||||
[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
|
||||
|
||||
```
|
||||
docker stop maputnik
|
||||
./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
|
||||
|
||||
This project would not be possible without commercial and individual sponsors.
|
||||
Thanks to the supporters of the **[Kickstarter campaign](https://www.kickstarter.com/projects/174808720/maputnik-visual-map-editor-for-mapbox-gl)**. This project would not be possible without these commercial and individual sponsors.
|
||||
|
||||
### Gold
|
||||
|
||||
[](https://getwemap.com/)
|
||||
- [Wemap](https://getwemap.com/)
|
||||
- [Orbicon Informatik](https://www.orbiconinformatik.dk/)
|
||||
- [Terranodo](http://terranodo.io/)
|
||||
|
||||
[](http://terranodo.io/)
|
||||
<a href="https://getwemap.com/">
|
||||
<img width="33%" alt="Wemap" style="display:inline" src="media/sponsors/wemap.jpg" />
|
||||
</a>
|
||||
<a href="http://terranodo.io/">
|
||||
<img width="33%" alt="Terranodo" style="display:inline" src="media/sponsors/terranodo.png" />
|
||||
</a>
|
||||
<a href="https://www.orbiconinformatik.dk/">
|
||||
<img width="32%" alt="Terranodo" style="display:inline" src="media/sponsors/orbicon_informatik.png" />
|
||||
</a>
|
||||
|
||||
<br/>
|
||||
|
||||
### Silver
|
||||
|
||||
- [Klokan Technologies](https://www.klokantech.com/)
|
||||
- [Geofabrik](http://www.geofabrik.de/)
|
||||
- [Dreipol](https://www.dreipol.ch/)
|
||||
|
||||
<a href="https://www.klokantech.com/">
|
||||
<img alt="Klokan Technologies" style="display:inline" src="media/sponsors/klokantech.png" />
|
||||
<img width="18%" alt="Klokan Technologies" style="display:inline-block" src="media/sponsors/klokantech.png" />
|
||||
</a>
|
||||
<a href="http://www.geofabrik.de/">
|
||||
<img width="18%" alt="Geofabrik" style="display:inline-block" src="media/sponsors/geofabrik.png" />
|
||||
</a>
|
||||
<a href="https://www.dreipol.ch/">
|
||||
<img alt="Dreipol" style="display:inline" src="media/sponsors/dreipol.png" />
|
||||
<img width="18%" alt="Dreipol" style="display:inline-block" src="media/sponsors/dreipol.png" />
|
||||
</a>
|
||||
|
||||
<br/>
|
||||
|
||||
### Individuals
|
||||
|
||||
**Influential Stakeholder**
|
||||
|
||||
- Alan McConchie
|
||||
- Odi
|
||||
- Mats Norén
|
||||
- Uli [geOps](http://geops.ch/)
|
||||
- Helge Fahrnberger
|
||||
Kirusanth Poopalasingam
|
||||
Alan McConchie, Odi, Mats Norén, Uli [geOps](http://geops.ch/), Helge Fahrnberger ([Toursprung](http://www.toursprung.com/)), Kirusanth Poopalasingam
|
||||
|
||||
**Stakeholder**
|
||||
|
||||
- Brian Flood
|
||||
- Vasile Coțovanu
|
||||
- Andreas Kalkbrenner
|
||||
- Christian Mäder
|
||||
- Gregor Wassmann
|
||||
- Lee Armstrong
|
||||
- Rafel
|
||||
- Jon Burgess
|
||||
- Lukas Lehmann
|
||||
- Joachim Ungar
|
||||
- Alois Ackermann
|
||||
- Zsolt Ero
|
||||
- Jordan Meek
|
||||
Brian Flood, Vasile Coțovanu, Andreas Kalkbrenner, Christian Mäder, Gregor Wassmann, Lee Armstrong, Rafel, Jon Burgess, Lukas Lehmann, Joachim Ungar, Alois Ackermann, Zsolt Ero, Jordan Meek
|
||||
|
||||
**Supporter**
|
||||
|
||||
- Sina Martinelli
|
||||
- Nicholas Doiron
|
||||
- Neil Cawse
|
||||
- Urs42
|
||||
- Benedikt Groß
|
||||
- Manuel Roth
|
||||
- Janko Mihelić
|
||||
- Moritz Stefaner
|
||||
- Sebastian Ahoi
|
||||
- Juerg Uhlmann
|
||||
- Tom Wider
|
||||
- Nadia Panchaud
|
||||
- Oliver Snowden
|
||||
- Stephan Heuel
|
||||
- Tobin Bradley
|
||||
- Adrian Herzog
|
||||
- Antti Lehto
|
||||
- Pascal Mages
|
||||
- Marc Gehling
|
||||
- Imre Samu
|
||||
- Lauri K.
|
||||
- Visahavel Parthasarathy
|
||||
- Christophe Waterlot-Buisine
|
||||
- Max Galka
|
||||
- ubahnverleih
|
||||
- Wouter van Dam
|
||||
- Jakob Lobensteiner
|
||||
- Samuel Kurath
|
||||
- Brian Bancroft
|
||||
Sina Martinelli, Nicholas Doiron, Neil Cawse, Urs42, Benedikt Groß, Manuel Roth, Janko Mihelić, Moritz Stefaner, Sebastian Ahoi, Juerg Uhlmann, Tom Wider, Nadia Panchaud, Oliver Snowden, Stephan Heuel, Tobin Bradley, Adrian Herzog, Antti Lehto, Pascal Mages, Marc Gehling, Imre Samu, Lauri K., Visahavel Parthasarathy, Christophe Waterlot-Buisine, Max Galka, ubahnverleih, Wouter van Dam, Jakob Lobensteiner, Samuel Kurath, Brian Bancroft
|
||||
|
||||
## License
|
||||
|
||||
|
||||
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,20 +3,11 @@ 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";
|
||||
|
||||
// local scss modules
|
||||
loaders.push({
|
||||
test: /[\/\\]src[\/\\].*\.scss/,
|
||||
loaders: [
|
||||
'style?sourceMap',
|
||||
'css?modules&importLoaders=1&localIdentName=[path]___[name]__[local]___[hash:base64:5]',
|
||||
'sass'
|
||||
]
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
target: 'web',
|
||||
entry: [
|
||||
@@ -26,24 +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: {
|
||||
alias: {
|
||||
'webworkify': 'webworkify-webpack',
|
||||
// TODO: otherwise I get a max call stack error in browser?
|
||||
// 'mapbox-gl': path.resolve('./node_modules/mapbox-gl/dist/mapbox-gl.js')
|
||||
},
|
||||
extensions: ['', '.js', '.jsx']
|
||||
extensions: ['.js', '.jsx']
|
||||
},
|
||||
module: {
|
||||
loaders,
|
||||
postLoaders: [{
|
||||
include: /node_modules\/mapbox-gl\/js\/render\/shaders.js/,
|
||||
loader: 'transform',
|
||||
query: 'brfs'
|
||||
}]
|
||||
noParse: [
|
||||
/mapbox-gl\/dist\/mapbox-gl.js/
|
||||
],
|
||||
loaders: loaders
|
||||
},
|
||||
node: {
|
||||
fs: "empty",
|
||||
@@ -61,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,29 +1,24 @@
|
||||
|
||||
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');
|
||||
|
||||
// local scss modules
|
||||
loaders.push({
|
||||
test: /[\/\\]src[\/\\].*\.scss/,
|
||||
loader: ExtractTextPlugin.extract('style', 'css?modules&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]', 'sass')
|
||||
});
|
||||
var OUTPATH = artifacts.pathSync("/build");
|
||||
|
||||
module.exports = {
|
||||
entry: {
|
||||
app: './src/index.jsx',
|
||||
vendor: [
|
||||
'file-saver',
|
||||
'mapbox-gl',
|
||||
//TODO: Build failure because cannot resolve migrations file
|
||||
//"mapbox-gl-style-spec",
|
||||
"randomcolor",
|
||||
'mapbox-gl/dist/mapbox-gl.js',
|
||||
"lodash.clonedeep",
|
||||
"lodash.throttle",
|
||||
"lodash.topairs",
|
||||
'color',
|
||||
'react',
|
||||
"react-dom",
|
||||
@@ -40,23 +35,18 @@ module.exports = {
|
||||
]
|
||||
},
|
||||
output: {
|
||||
path: path.join(__dirname, 'public'),
|
||||
path: OUTPATH,
|
||||
filename: '[name].[chunkhash].js',
|
||||
chunkFilename: '[chunkhash].js'
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'webworkify': 'webworkify-webpack',
|
||||
},
|
||||
extensions: ['', '.js', '.jsx']
|
||||
extensions: ['.js', '.jsx']
|
||||
},
|
||||
module: {
|
||||
loaders,
|
||||
postLoaders: [{
|
||||
include: /node_modules\/mapbox-gl-shaders/,
|
||||
loader: 'transform',
|
||||
query: 'brfs'
|
||||
}]
|
||||
noParse: [
|
||||
/mapbox-gl\/dist\/mapbox-gl.js/
|
||||
],
|
||||
loaders
|
||||
},
|
||||
node: {
|
||||
fs: "empty",
|
||||
@@ -64,23 +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,
|
||||
drop_console: true,
|
||||
drop_debugger: true
|
||||
}
|
||||
}),
|
||||
new webpack.optimize.OccurenceOrderPlugin(),
|
||||
new UglifyJsPlugin(),
|
||||
new ExtractTextPlugin('[contenthash].css', {
|
||||
allChunks: true
|
||||
}),
|
||||
@@ -88,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'
|
||||
}
|
||||
});
|
||||
};
|
||||
BIN
media/demo.gif
BIN
media/demo.gif
Binary file not shown.
|
Before Width: | Height: | Size: 3.2 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 410 KiB |
BIN
media/sponsors/geofabrik.png
Normal file
BIN
media/sponsors/geofabrik.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.2 KiB |
BIN
media/sponsors/orbicon_informatik.png
Normal file
BIN
media/sponsors/orbicon_informatik.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 65 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 37 KiB |
16961
package-lock.json
generated
Normal file
16961
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
185
package.json
185
package.json
@@ -1,15 +1,17 @@
|
||||
{
|
||||
"name": "maputnik",
|
||||
"version": "0.0.1",
|
||||
"version": "1.5.0-beta3",
|
||||
"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",
|
||||
"lint": "eslint --ext js --ext jsx {src,test}"
|
||||
"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'",
|
||||
"nsp": "nsp check --reporter summary"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -19,47 +21,68 @@
|
||||
"license": "MIT",
|
||||
"homepage": "https://github.com/maputnik/editor#readme",
|
||||
"dependencies": {
|
||||
"color": "^1.0.3",
|
||||
"file-saver": "^1.3.2",
|
||||
"@mapbox/mapbox-gl-rtl-text": "^0.2.0",
|
||||
"@mapbox/mapbox-gl-style-spec": "^13.1.0",
|
||||
"classnames": "^2.2.5",
|
||||
"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.5.0",
|
||||
"lodash.throttle": "^4.1.1",
|
||||
"lodash.topairs": "^4.3.0",
|
||||
"mapbox-gl": "mapbox/mapbox-gl-js#6c24b9621d2aa770eda67fb5638b4d78087b5624",
|
||||
"mapbox-gl-style-spec": "mapbox/mapbox-gl-style-spec#e85407a377510acb647161de6be6357ab4f606dd",
|
||||
"ol-mapbox-style": "0.0.11",
|
||||
"openlayers": "^3.19.1",
|
||||
"randomcolor": "^0.4.4",
|
||||
"react": "^15.4.0",
|
||||
"react-addons-pure-render-mixin": "^15.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",
|
||||
"request": "^2.79.0"
|
||||
},
|
||||
"babel": {
|
||||
"presets": [
|
||||
"es2015",
|
||||
"react"
|
||||
],
|
||||
"plugins": [
|
||||
"transform-object-rest-spread"
|
||||
]
|
||||
"mapbox-gl": "^0.47.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"
|
||||
},
|
||||
"jshintConfig": {
|
||||
"esversion": 6
|
||||
},
|
||||
"stylelint": {
|
||||
"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": {
|
||||
@@ -79,40 +102,58 @@
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-core": "6.14.0",
|
||||
"babel-eslint": "^6.1.2",
|
||||
"babel-loader": "6.2.4",
|
||||
"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.14.0",
|
||||
"babel-preset-react": "6.11.1",
|
||||
"babel-runtime": "^6.11.6",
|
||||
"css-loader": "0.25.0",
|
||||
"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": "^1.8.0",
|
||||
"mocha": "^3.1.2",
|
||||
"mocha-loader": "^1.0.0",
|
||||
"node-sass": "^3.9.2",
|
||||
"react-hot-loader": "^3.0.0-beta.6",
|
||||
"sass-loader": "^4.0.1",
|
||||
"style-loader": "0.13.1",
|
||||
"transform-loader": "^0.2.3",
|
||||
"url-loader": "0.5.7",
|
||||
"webpack": "1.13.2",
|
||||
"webpack-cleanup-plugin": "^0.3.0",
|
||||
"webpack-dev-server": "1.15.1",
|
||||
"webworkify-webpack": "^1.1.3"
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,21 @@
|
||||
.cm-s-maputnik.CodeMirror {
|
||||
height: 100%;
|
||||
font-size: 10px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.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 { saveAs } from 'file-saver'
|
||||
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'
|
||||
@@ -7,59 +11,275 @@ import LayerList from './layers/LayerList'
|
||||
import LayerEditor from './layers/LayerEditor'
|
||||
import Toolbar from './Toolbar'
|
||||
import AppLayout from './AppLayout'
|
||||
import MessagePanel from './MessagePanel'
|
||||
|
||||
import style from '../libs/style.js'
|
||||
import { loadDefaultStyle, SettingsStore, StyleStore } from '../libs/stylestore'
|
||||
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 * as styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
|
||||
import style from '../libs/style'
|
||||
import { initialStyleUrl, loadStyleUrl } from '../libs/urlopen'
|
||||
import { undoMessages, redoMessages } from '../libs/diffmessage'
|
||||
import { loadDefaultStyle, StyleStore } from '../libs/stylestore'
|
||||
import { ApiStyleStore } from '../libs/apistore'
|
||||
import { RevisionStore } from '../libs/revisions'
|
||||
import LayerWatcher from '../libs/layerwatcher'
|
||||
import tokens from '../config/tokens.json'
|
||||
import isEqual from 'lodash.isequal'
|
||||
import Debug from '../libs/debug'
|
||||
import queryUtil from '../libs/query-util'
|
||||
|
||||
import MapboxGl from 'mapbox-gl'
|
||||
import { normalizeSourceURL } from 'mapbox-gl/src/util/mapbox'
|
||||
|
||||
|
||||
export default class App extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.layerWatcher = new LayerWatcher()
|
||||
this.styleStore = new ApiStyleStore()
|
||||
this.styleStore.supported(isSupported => {
|
||||
if(!isSupported) {
|
||||
console.log('Falling back to local storage for storing styles')
|
||||
this.styleStore = new StyleStore()
|
||||
function updateRootSpec(spec, fieldName, newValues) {
|
||||
return {
|
||||
...spec,
|
||||
$root: {
|
||||
...spec.$root,
|
||||
[fieldName]: {
|
||||
...spec.$root[fieldName],
|
||||
values: newValues
|
||||
}
|
||||
this.styleStore.latestStyle(mapStyle => this.onStyleChanged(mapStyle))
|
||||
})
|
||||
|
||||
this.settingsStore = new SettingsStore()
|
||||
this.state = {
|
||||
accessToken: this.settingsStore.accessToken,
|
||||
mapStyle: style.emptyStyle,
|
||||
selectedLayerIndex: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onReset() {
|
||||
this.styleStore.purge()
|
||||
loadDefaultStyle(mapStyle => this.onStyleOpen(mapStyle))
|
||||
export default class App extends React.Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.revisionStore = new RevisionStore()
|
||||
this.styleStore = new ApiStyleStore({
|
||||
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()
|
||||
loadStyleUrl(styleUrl, mapStyle => this.onStyleChanged(mapStyle))
|
||||
} else {
|
||||
this.styleStore.init(err => {
|
||||
if(err) {
|
||||
console.log('Falling back to local storage for storing styles')
|
||||
this.styleStore = new StyleStore()
|
||||
}
|
||||
this.styleStore.latestStyle(mapStyle => this.onStyleChanged(mapStyle))
|
||||
|
||||
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: [],
|
||||
mapStyle: style.emptyStyle,
|
||||
selectedLayerIndex: 0,
|
||||
sources: {},
|
||||
vectorLayers: {},
|
||||
inspectModeEnabled: false,
|
||||
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"),
|
||||
showCollisionBoxes: queryUtil.asBool(queryObj, "show-collision-boxes")
|
||||
},
|
||||
mapFilter: queryObj["color-blindness-emulation"],
|
||||
}
|
||||
|
||||
this.layerWatcher = new LayerWatcher({
|
||||
onVectorLayersChange: v => this.setState({ vectorLayers: v })
|
||||
})
|
||||
}
|
||||
|
||||
onStyleDownload() {
|
||||
const mapStyle = this.state.mapStyle
|
||||
const blob = new Blob([JSON.stringify(mapStyle, null, 4)], {type: "application/json;charset=utf-8"});
|
||||
saveAs(blob, mapStyle.id + ".json");
|
||||
componentDidMount() {
|
||||
Mousetrap.bind(['mod+z'], this.onUndo.bind(this));
|
||||
Mousetrap.bind(['mod+y', 'mod+shift+z'], this.onRedo.bind(this));
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
Mousetrap.unbind(['mod+z'], this.onUndo.bind(this));
|
||||
Mousetrap.unbind(['mod+y', 'mod+shift+z'], this.onRedo.bind(this));
|
||||
}
|
||||
|
||||
saveStyle(snapshotStyle) {
|
||||
snapshotStyle.modified = new Date().toJSON()
|
||||
this.styleStore.save(snapshotStyle)
|
||||
}
|
||||
|
||||
onStyleChanged(newStyle) {
|
||||
this.saveStyle(newStyle)
|
||||
this.setState({ mapStyle: newStyle })
|
||||
updateFonts(urlTemplate) {
|
||||
const metadata = this.state.mapStyle.metadata || {}
|
||||
const accessToken = metadata['maputnik:openmaptiles_access_token'] || tokens.openmaptiles
|
||||
|
||||
let glyphUrl = (typeof urlTemplate === 'string')? urlTemplate.replace('{key}', accessToken): urlTemplate;
|
||||
downloadGlyphsMetadata(glyphUrl, fonts => {
|
||||
this.setState({ spec: updateRootSpec(this.state.spec, 'glyphs', fonts)})
|
||||
})
|
||||
}
|
||||
|
||||
onAccessTokenChanged(newToken) {
|
||||
this.settingsStore.accessToken = newToken
|
||||
this.setState({ accessToken: newToken })
|
||||
updateIcons(baseUrl) {
|
||||
downloadSpriteMetadata(baseUrl, icons => {
|
||||
this.setState({ spec: updateRootSpec(this.state.spec, 'sprite', icons)})
|
||||
})
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
if(newStyle.sprite !== this.state.mapStyle.sprite) {
|
||||
this.updateIcons(newStyle.sprite)
|
||||
}
|
||||
|
||||
this.revisionStore.addRevision(newStyle)
|
||||
if(save) this.saveStyle(newStyle)
|
||||
this.setState({
|
||||
mapStyle: newStyle,
|
||||
errors: [],
|
||||
})
|
||||
} else {
|
||||
this.setState({
|
||||
errors: errors.map(err => err.message)
|
||||
})
|
||||
}
|
||||
|
||||
this.fetchSources();
|
||||
}
|
||||
|
||||
onUndo() {
|
||||
const activeStyle = this.revisionStore.undo()
|
||||
const messages = undoMessages(this.state.mapStyle, activeStyle)
|
||||
this.saveStyle(activeStyle)
|
||||
this.setState({
|
||||
mapStyle: activeStyle,
|
||||
infos: messages,
|
||||
})
|
||||
}
|
||||
|
||||
onRedo() {
|
||||
const activeStyle = this.revisionStore.redo()
|
||||
const messages = redoMessages(this.state.mapStyle, activeStyle)
|
||||
this.saveStyle(activeStyle)
|
||||
this.setState({
|
||||
mapStyle: activeStyle,
|
||||
infos: messages,
|
||||
})
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -70,6 +290,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)
|
||||
@@ -90,24 +344,101 @@ export default class App extends React.Component {
|
||||
this.onLayersChange(changedLayers)
|
||||
}
|
||||
|
||||
changeInspectMode() {
|
||||
this.setState({
|
||||
inspectModeEnabled: !this.state.inspectModeEnabled
|
||||
})
|
||||
}
|
||||
|
||||
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 = 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: this.state.mapStyle,
|
||||
accessToken: this.state.accessToken,
|
||||
onMapLoaded: (map) => {
|
||||
this.layerWatcher.map = map
|
||||
}
|
||||
mapStyle: style.replaceAccessTokens(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]}
|
||||
onLayerSelect={this.onLayerSelect.bind(this)} />
|
||||
}
|
||||
|
||||
const elementStyle = {};
|
||||
if(this.state.mapFilter) {
|
||||
elementStyle.filter = `url('#${this.state.mapFilter}')`;
|
||||
}
|
||||
|
||||
return <div style={elementStyle}>
|
||||
{mapElement}
|
||||
</div>
|
||||
}
|
||||
|
||||
onLayerSelect(layerId) {
|
||||
@@ -115,38 +446,109 @@ 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
|
||||
const metadata = this.state.mapStyle.metadata || {}
|
||||
|
||||
const toolbar = <Toolbar
|
||||
mapStyle={this.state.mapStyle}
|
||||
inspectModeEnabled={this.state.inspectModeEnabled}
|
||||
sources={this.state.sources}
|
||||
onStyleChanged={this.onStyleChanged.bind(this)}
|
||||
onStyleOpen={this.onStyleChanged.bind(this)}
|
||||
onStyleDownload={this.onStyleDownload.bind(this)}
|
||||
onInspectModeToggle={this.changeInspectMode.bind(this)}
|
||||
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}
|
||||
layers={layers}
|
||||
sources={this.state.sources}
|
||||
/>
|
||||
|
||||
const layerEditor = selectedLayer ? <LayerEditor
|
||||
layer={selectedLayer}
|
||||
sources={this.layerWatcher.sources}
|
||||
vectorLayers={this.layerWatcher.vectorLayers}
|
||||
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
|
||||
|
||||
const bottomPanel = (this.state.errors.length + this.state.infos.length) > 0 ? <MessagePanel
|
||||
errors={this.state.errors}
|
||||
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,19 +1,19 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import ScrollContainer from './ScrollContainer'
|
||||
|
||||
import theme from '../config/theme'
|
||||
import colors from '../config/colors'
|
||||
|
||||
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,
|
||||
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() {
|
||||
@@ -23,42 +23,24 @@ class AppLayout extends React.Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div style={{
|
||||
fontFamily: theme.fontFamily,
|
||||
color: theme.color,
|
||||
fontWeight: 300
|
||||
}}>
|
||||
return <div className="maputnik-layout">
|
||||
{this.props.toolbar}
|
||||
<div style={{
|
||||
position: 'fixed',
|
||||
bottom: 0,
|
||||
height: "100%",
|
||||
top: 40,
|
||||
left: 0,
|
||||
zIndex: 1,
|
||||
width: 200,
|
||||
overflow: "hidden",
|
||||
backgroundColor: colors.black
|
||||
}}>
|
||||
<div className="maputnik-layout-list">
|
||||
<ScrollContainer>
|
||||
{this.props.layerList}
|
||||
</ScrollContainer>
|
||||
</div>
|
||||
<div style={{
|
||||
position: 'fixed',
|
||||
bottom: 0,
|
||||
height: "100%",
|
||||
top: 40,
|
||||
left: 200,
|
||||
zIndex: 1,
|
||||
width: 300,
|
||||
backgroundColor: colors.black
|
||||
}}>
|
||||
<div className="maputnik-layout-drawer">
|
||||
<ScrollContainer>
|
||||
{this.props.layerEditor}
|
||||
</ScrollContainer>
|
||||
</div>
|
||||
{this.props.map}
|
||||
{this.props.bottom && <div className="maputnik-layout-bottom">
|
||||
{this.props.bottom}
|
||||
</div>
|
||||
}
|
||||
{this.props.modals}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,28 +1,26 @@
|
||||
import React from 'react'
|
||||
import colors from '../config/colors'
|
||||
import { margins, fontSizes } from '../config/scales'
|
||||
import PropTypes from 'prop-types'
|
||||
import classnames from 'classnames'
|
||||
|
||||
class Button extends React.Component {
|
||||
static propTypes = {
|
||||
onClick: React.PropTypes.func,
|
||||
style: React.PropTypes.object,
|
||||
"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}
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
backgroundColor: colors.midgray,
|
||||
color: colors.lowgray,
|
||||
fontSize: fontSizes[4],
|
||||
padding: margins[1],
|
||||
userSelect: 'none',
|
||||
borderRadius: 2,
|
||||
...this.props.style,
|
||||
}}>
|
||||
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,32 +0,0 @@
|
||||
import React from 'react'
|
||||
import { fontSizes, margins } from '../config/scales'
|
||||
|
||||
class Heading extends React.Component {
|
||||
static propTypes = {
|
||||
level: React.PropTypes.number.isRequired,
|
||||
style: React.PropTypes.object,
|
||||
}
|
||||
|
||||
render() {
|
||||
const headingProps = {
|
||||
style: {
|
||||
fontWeight: 400,
|
||||
fontSize: fontSizes[this.props.level - 1],
|
||||
marginBottom: margins[1],
|
||||
...this.props.style
|
||||
}
|
||||
}
|
||||
|
||||
switch(this.props.level) {
|
||||
case 1: return <h1 {...headingProps}>{this.props.children}</h1>
|
||||
case 2: return <h2 {...headingProps}>{this.props.children}</h2>
|
||||
case 3: return <h3 {...headingProps}>{this.props.children}</h3>
|
||||
case 4: return <h4 {...headingProps}>{this.props.children}</h4>
|
||||
case 5: return <h5 {...headingProps}>{this.props.children}</h5>
|
||||
default: return <h6 {...headingProps}>{this.props.children}</h6>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default Heading
|
||||
27
src/components/MessagePanel.jsx
Normal file
27
src/components/MessagePanel.jsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
class MessagePanel extends React.Component {
|
||||
static propTypes = {
|
||||
errors: PropTypes.array,
|
||||
infos: PropTypes.array,
|
||||
}
|
||||
|
||||
render() {
|
||||
const errors = this.props.errors.map((m, i) => {
|
||||
return <p key={"error-"+i} className="maputnik-message-panel-error">{m}</p>
|
||||
})
|
||||
|
||||
const infos = this.props.infos.map((m, i) => {
|
||||
return <p key={"info-"+i}>{m}</p>
|
||||
})
|
||||
|
||||
return <div className="maputnik-message-panel">
|
||||
{errors}
|
||||
{infos}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default MessagePanel
|
||||
@@ -1,13 +0,0 @@
|
||||
import React from 'react'
|
||||
import colors from '../config/colors'
|
||||
import { margins, fontSizes } from '../config/scales'
|
||||
|
||||
const Paragraph = (props) => <p style={{
|
||||
color: colors.lowgray,
|
||||
fontSize: fontSizes[5],
|
||||
...props.style
|
||||
}}>
|
||||
{props.children}
|
||||
</p>
|
||||
|
||||
export default Paragraph
|
||||
@@ -1,18 +1,16 @@
|
||||
import React from 'react'
|
||||
import scrollbars from './scrollbars.scss'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
const ScrollContainer = (props) => {
|
||||
return <div className={scrollbars.darkScrollbar} style={{
|
||||
overflowX: "visible",
|
||||
overflowY: "scroll",
|
||||
bottom:0,
|
||||
left:0,
|
||||
right:0,
|
||||
top:1,
|
||||
position: "absolute",
|
||||
}}>
|
||||
{props.children}
|
||||
</div>
|
||||
class ScrollContainer extends React.Component {
|
||||
static propTypes = {
|
||||
children: PropTypes.node
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div className="maputnik-scroll-container">
|
||||
{this.props.children}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
export default ScrollContainer
|
||||
|
||||
@@ -1,84 +1,110 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import FileReaderInput from 'react-file-reader-input'
|
||||
import classnames from 'classnames'
|
||||
|
||||
import MdFileDownload from 'react-icons/lib/md/file-download'
|
||||
import MdFileUpload from 'react-icons/lib/md/file-upload'
|
||||
import MdOpenInBrowser from 'react-icons/lib/md/open-in-browser'
|
||||
import MdSettings from 'react-icons/lib/md/settings'
|
||||
import OpenIcon from 'react-icons/lib/md/open-in-browser'
|
||||
import SettingsIcon from 'react-icons/lib/md/settings'
|
||||
import MdInfo from 'react-icons/lib/md/info'
|
||||
import MdLayers from 'react-icons/lib/md/layers'
|
||||
import SourcesIcon from 'react-icons/lib/md/layers'
|
||||
import MdSave from 'react-icons/lib/md/save'
|
||||
import MdStyle from 'react-icons/lib/md/style'
|
||||
import MdMap from 'react-icons/lib/md/map'
|
||||
import MdInsertEmoticon from 'react-icons/lib/md/insert-emoticon'
|
||||
import MdFontDownload from 'react-icons/lib/md/font-download'
|
||||
import MdHelpOutline from 'react-icons/lib/md/help-outline'
|
||||
import MdFindInPage from 'react-icons/lib/md/find-in-page'
|
||||
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 SettingsModal from './modals/SettingsModal'
|
||||
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'
|
||||
import colors from '../config/colors'
|
||||
import { margins, fontSizes } from '../config/scales'
|
||||
|
||||
const IconText = props => <span style={{ paddingLeft: margins[0] }}>
|
||||
{props.children}
|
||||
</span>
|
||||
|
||||
const actionStyle = {
|
||||
display: "inline-block",
|
||||
padding: 12.5,
|
||||
fontSize: fontSizes[4],
|
||||
cursor: "pointer",
|
||||
color: colors.white,
|
||||
textDecoration: 'none',
|
||||
}
|
||||
|
||||
const ToolbarLink = props => <a
|
||||
href={props.href}
|
||||
target={"blank"}
|
||||
style={{
|
||||
...actionStyle,
|
||||
...props.style,
|
||||
}}>
|
||||
{props.children}
|
||||
</a>
|
||||
|
||||
class ToolbarAction extends React.Component {
|
||||
class IconText extends React.Component {
|
||||
static propTypes = {
|
||||
onClick: React.PropTypes.func.isRequired,
|
||||
children: PropTypes.node,
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = { hover: false }
|
||||
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
|
||||
onClick={this.props.onClick}
|
||||
onMouseOver={e => this.setState({hover: true})}
|
||||
onMouseOut={e => this.setState({hover: false})}
|
||||
style={{
|
||||
...actionStyle,
|
||||
...this.props.style,
|
||||
backgroundColor: this.state.hover ? colors.gray : null,
|
||||
}}>
|
||||
className={classnames('maputnik-toolbar-link', this.props.className)}
|
||||
href={this.props.href}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
{this.props.children}
|
||||
</a>
|
||||
}
|
||||
}
|
||||
|
||||
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'
|
||||
data-wd-key={this.props.wdKey}
|
||||
onClick={this.props.onClick}
|
||||
>
|
||||
{this.props.children}
|
||||
</button>
|
||||
}
|
||||
}
|
||||
|
||||
export default class Toolbar extends React.Component {
|
||||
static propTypes = {
|
||||
mapStyle: React.PropTypes.object.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,
|
||||
// Current style is requested for download
|
||||
onStyleDownload: React.PropTypes.func.isRequired,
|
||||
onStyleOpen: PropTypes.func.isRequired,
|
||||
// A dict of source id's and the available source layers
|
||||
sources: PropTypes.object.isRequired,
|
||||
onInspectModeToggle: PropTypes.func.isRequired,
|
||||
children: PropTypes.node,
|
||||
onToggleModal: PropTypes.func,
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
@@ -88,86 +114,67 @@ export default class Toolbar extends React.Component {
|
||||
settings: false,
|
||||
sources: false,
|
||||
open: false,
|
||||
add: false,
|
||||
export: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
downloadButton() {
|
||||
return <ToolbarAction onClick={this.props.onStyleDownload}>
|
||||
<MdFileDownload />
|
||||
<IconText>Download</IconText>
|
||||
</ToolbarAction>
|
||||
}
|
||||
|
||||
toggleModal(modalName) {
|
||||
this.setState({
|
||||
isOpen: {
|
||||
...this.state.isOpen,
|
||||
[modalName]: !this.state.isOpen[modalName]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div style={{
|
||||
position: "fixed",
|
||||
height: 40,
|
||||
width: '100%',
|
||||
zIndex: 100,
|
||||
left: 0,
|
||||
top: 0,
|
||||
backgroundColor: colors.black
|
||||
}}>
|
||||
<SettingsModal
|
||||
mapStyle={this.props.mapStyle}
|
||||
onStyleChanged={this.props.onStyleChanged}
|
||||
isOpen={this.state.isOpen.settings}
|
||||
onOpenToggle={this.toggleModal.bind(this, 'settings')}
|
||||
/>
|
||||
<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"}
|
||||
style={{
|
||||
width: 180,
|
||||
textAlign: 'left',
|
||||
backgroundColor: colors.black,
|
||||
padding: 5,
|
||||
}}
|
||||
>
|
||||
<img src="https://github.com/maputnik/editor/raw/master/media/maputnik.png" alt="Maputnik" style={{width: 30, height: 30, paddingRight: 5, verticalAlign: 'middle'}}/>
|
||||
<span style={{fontSize: 20, verticalAlign: 'middle' }}>Maputnik</span>
|
||||
</ToolbarLink>
|
||||
<ToolbarAction onClick={this.toggleModal.bind(this, 'open')}>
|
||||
<MdOpenInBrowser />
|
||||
<IconText>Open</IconText>
|
||||
</ToolbarAction>
|
||||
{this.downloadButton()}
|
||||
<ToolbarAction onClick={this.toggleModal.bind(this, 'sources')}>
|
||||
<MdLayers />
|
||||
<IconText>Sources</IconText>
|
||||
</ToolbarAction>
|
||||
<ToolbarAction onClick={this.toggleModal.bind(this, 'settings')}>
|
||||
<MdSettings />
|
||||
<IconText>Style Settings</IconText>
|
||||
</ToolbarAction>
|
||||
<ToolbarAction onClick={this.toggleModal.bind(this, 'settings')}>
|
||||
<MdFindInPage />
|
||||
<IconText>Inspect</IconText>
|
||||
</ToolbarAction>
|
||||
<ToolbarLink href={"https://github.com/maputnik/editor/wiki"}>
|
||||
<MdHelpOutline />
|
||||
<IconText>Help</IconText>
|
||||
</ToolbarLink>
|
||||
return <div className='maputnik-toolbar'>
|
||||
<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
|
||||
<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 wdKey="nav:export" onClick={this.props.onToggleModal.bind(this, 'export')}>
|
||||
<MdFileDownload />
|
||||
<IconText>Export</IconText>
|
||||
</ToolbarAction>
|
||||
<ToolbarAction wdKey="nav:sources" onClick={this.props.onToggleModal.bind(this, 'sources')}>
|
||||
<SourcesIcon />
|
||||
<IconText>Data Sources</IconText>
|
||||
</ToolbarAction>
|
||||
<ToolbarAction wdKey="nav:settings" onClick={this.props.onToggleModal.bind(this, 'settings')}>
|
||||
<SettingsIcon />
|
||||
<IconText>Style Settings</IconText>
|
||||
</ToolbarAction>
|
||||
<ToolbarAction wdKey="nav:inspect" onClick={this.props.onInspectModeToggle}>
|
||||
<InspectionIcon />
|
||||
<IconText>
|
||||
{ this.props.inspectModeEnabled && <span>Map Mode</span> }
|
||||
{ !this.props.inspectModeEnabled && <span>Inspect Mode</span> }
|
||||
</IconText>
|
||||
</ToolbarAction>
|
||||
<ToolbarLink href={"https://github.com/maputnik/editor/wiki"}>
|
||||
<HelpIcon />
|
||||
<IconText>Help</IconText>
|
||||
</ToolbarLink>
|
||||
<ToolbarLinkHighlighted href={"https://gregorywolanski.typeform.com/to/cPgaSY"}>
|
||||
<SurveyIcon />
|
||||
<IconText>Take the Maputnik Survey</IconText>
|
||||
</ToolbarLinkHighlighted>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
import React from 'react'
|
||||
|
||||
import input from '../../config/input'
|
||||
import colors from '../../config/colors'
|
||||
import { margins } from '../../config/scales'
|
||||
|
||||
class BooleanField extends React.Component {
|
||||
static propTypes = {
|
||||
onChange: React.PropTypes.func.isRequired,
|
||||
name: React.PropTypes.string.isRequired,
|
||||
value: React.PropTypes.bool,
|
||||
doc: React.PropTypes.string,
|
||||
style: React.PropTypes.object,
|
||||
}
|
||||
|
||||
render() {
|
||||
const styles = {
|
||||
root: {
|
||||
...input.base,
|
||||
padding: 0,
|
||||
position: 'relative',
|
||||
textAlign: 'center ',
|
||||
cursor: 'pointer'
|
||||
},
|
||||
input: {
|
||||
position: 'absolute',
|
||||
zIndex: -1,
|
||||
opacity: 0
|
||||
},
|
||||
box: {
|
||||
display: 'inline-block',
|
||||
textAlign: 'center ',
|
||||
height: 15,
|
||||
width: 15,
|
||||
marginRight: margins[1],
|
||||
marginBottom: null,
|
||||
backgroundColor: colors.gray,
|
||||
borderRadius: 2,
|
||||
borderStyle: 'solid',
|
||||
borderWidth: 2,
|
||||
borderColor: colors.gray,
|
||||
transition: 'background-color .1s ease-out'
|
||||
},
|
||||
icon: {
|
||||
display: this.props.value ? null : 'none',
|
||||
width: '75%',
|
||||
height: '75%',
|
||||
marginTop: 1,
|
||||
fill: colors.lowgray
|
||||
}
|
||||
}
|
||||
|
||||
return <label style={styles.root}>
|
||||
<input
|
||||
type="checkbox"
|
||||
style={{
|
||||
...styles.input,
|
||||
...this.props.style,
|
||||
}}
|
||||
value={this.props.value}
|
||||
onChange={e => {this.props.onChange(!this.props.value)}}
|
||||
checked={this.props.value}
|
||||
/>
|
||||
<div style={styles.box}>
|
||||
<svg
|
||||
viewBox='0 0 32 32'
|
||||
style={styles.icon}>
|
||||
<path d='M1 14 L5 10 L13 18 L27 4 L31 8 L13 26 z' />
|
||||
</svg>
|
||||
</div>
|
||||
</label>
|
||||
}
|
||||
}
|
||||
|
||||
export default BooleanField
|
||||
@@ -1,8 +1,7 @@
|
||||
import React from 'react'
|
||||
import Color from 'color'
|
||||
import ChromePicker from 'react-color/lib/components/chrome/Chrome'
|
||||
|
||||
import input from '../../config/input.js'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
function formatColor(color) {
|
||||
const rgb = color.rgb
|
||||
@@ -12,11 +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,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
value: PropTypes.string,
|
||||
doc: PropTypes.string,
|
||||
style: PropTypes.object,
|
||||
default: PropTypes.string,
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
@@ -30,18 +30,17 @@ 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 {
|
||||
top: pos.top,
|
||||
left: pos.left + 165,
|
||||
left: pos.left + 196,
|
||||
}
|
||||
} else {
|
||||
console.warn('Color field has no element to adjust position')
|
||||
return {
|
||||
top: 160,
|
||||
left: 500,
|
||||
left: 555,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -51,23 +50,41 @@ 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={{
|
||||
position: 'fixed',
|
||||
zIndex: 1,
|
||||
position: 'fixed',
|
||||
zIndex: 1,
|
||||
left: offset.left,
|
||||
top: offset.top,
|
||||
}}>
|
||||
<ChromePicker
|
||||
color={this.color.object()}
|
||||
color={currentColor}
|
||||
onChange={c => this.props.onChange(formatColor(c))}
|
||||
/>
|
||||
<div
|
||||
className="maputnik-color-picker-offset"
|
||||
onClick={this.togglePicker.bind(this)}
|
||||
style={{
|
||||
zIndex: -1,
|
||||
@@ -80,19 +97,19 @@ class ColorField extends React.Component {
|
||||
/>
|
||||
</div>
|
||||
|
||||
return <div style={{
|
||||
...input.property,
|
||||
position: 'relative',
|
||||
display: 'inline',
|
||||
}}>
|
||||
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
|
||||
ref="colorInput"
|
||||
spellCheck="false"
|
||||
className="maputnik-color"
|
||||
ref={(input) => this.colorInput = input}
|
||||
onClick={this.togglePicker.bind(this)}
|
||||
style={{
|
||||
...input.select,
|
||||
...this.props.style
|
||||
}}
|
||||
style={this.props.style}
|
||||
name={this.props.name}
|
||||
placeholder={this.props.default}
|
||||
value={this.props.value ? this.props.value : ""}
|
||||
|
||||
23
src/components/fields/DocLabel.jsx
Normal file
23
src/components/fields/DocLabel.jsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
export default class DocLabel extends React.Component {
|
||||
static propTypes = {
|
||||
label: PropTypes.oneOfType([
|
||||
PropTypes.object,
|
||||
PropTypes.string
|
||||
]).isRequired,
|
||||
doc: PropTypes.string.isRequired,
|
||||
}
|
||||
|
||||
render() {
|
||||
return <label className="maputnik-doc-wrapper">
|
||||
<div className="maputnik-doc-target">
|
||||
<span>{this.props.label}</span>
|
||||
<div className="maputnik-doc-popup">
|
||||
{this.props.doc}
|
||||
</div>
|
||||
</div >
|
||||
</label>
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
import React from 'react'
|
||||
import input from '../../config/input.js'
|
||||
|
||||
class EnumField extends React.Component {
|
||||
static propTypes = {
|
||||
onChange: React.PropTypes.func.isRequired,
|
||||
name: React.PropTypes.string.isRequired,
|
||||
value: React.PropTypes.string,
|
||||
allowedValues: React.PropTypes.array.isRequired,
|
||||
doc: React.PropTypes.string,
|
||||
style: React.PropTypes.object,
|
||||
}
|
||||
|
||||
onChange(e) {
|
||||
return this.props.onChange(e.target.value)
|
||||
}
|
||||
|
||||
render() {
|
||||
const options = this.props.allowedValues.map(val => {
|
||||
return <option key={val} value={val}>{val}</option>
|
||||
})
|
||||
|
||||
return <select
|
||||
style={{
|
||||
...input.select,
|
||||
...this.props.style
|
||||
}}
|
||||
value={this.props.value}
|
||||
onChange={this.onChange.bind(this)}
|
||||
>
|
||||
{options}
|
||||
</select>
|
||||
}
|
||||
}
|
||||
|
||||
export default EnumField
|
||||
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,50 +0,0 @@
|
||||
import React from 'react'
|
||||
import input from '../../config/input.js'
|
||||
|
||||
/*** Number fields with support for min, max and units and documentation*/
|
||||
class NumberField extends React.Component {
|
||||
static propTypes = {
|
||||
onChange: React.PropTypes.func.isRequired,
|
||||
name: React.PropTypes.string.isRequired,
|
||||
value: React.PropTypes.number,
|
||||
default: React.PropTypes.number,
|
||||
unit: React.PropTypes.string,
|
||||
min: React.PropTypes.number,
|
||||
max: React.PropTypes.number,
|
||||
doc: React.PropTypes.string,
|
||||
style: React.PropTypes.object,
|
||||
}
|
||||
|
||||
onChange(e) {
|
||||
const value = parseFloat(e.target.value)
|
||||
/*TODO: we can do range validation already here?
|
||||
if(this.props.min && value < this.props.min) return
|
||||
if(this.props.max && value > this.props.max) return
|
||||
*/
|
||||
this.props.onChange(value)
|
||||
}
|
||||
|
||||
render() {
|
||||
let stepSize = null
|
||||
if(this.props.max && this.props.min) {
|
||||
stepSize = (this.props.max - this.props.min) / 10
|
||||
}
|
||||
|
||||
return <input
|
||||
style={{
|
||||
...input.input,
|
||||
...this.props.style
|
||||
}}
|
||||
type="number"
|
||||
min={this.props.min}
|
||||
max={this.props.max}
|
||||
step={stepSize}
|
||||
name={this.props.name}
|
||||
placeholder={this.props.default}
|
||||
value={this.props.value}
|
||||
onChange={this.onChange.bind(this)}
|
||||
/>
|
||||
}
|
||||
}
|
||||
|
||||
export default NumberField
|
||||
@@ -1,20 +1,32 @@
|
||||
import React from 'react'
|
||||
import GlSpec from 'mapbox-gl-style-spec/reference/latest.min.js'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import ZoomSpecField from './ZoomSpecField'
|
||||
import colors from '../../config/colors'
|
||||
import { margins } from '../../config/scales'
|
||||
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
|
||||
* style specification from either the paint or layout group */
|
||||
function getFieldSpec(layerType, fieldName) {
|
||||
const groupName = getGroupName(layerType, fieldName)
|
||||
const group = GlSpec[groupName + '_' + layerType]
|
||||
return group[fieldName]
|
||||
function getFieldSpec(spec, layerType, fieldName) {
|
||||
const groupName = getGroupName(spec, layerType, fieldName)
|
||||
const group = spec[groupName + '_' + layerType]
|
||||
const fieldSpec = group[fieldName]
|
||||
if(iconProperties.indexOf(fieldName) >= 0) {
|
||||
return {
|
||||
...fieldSpec,
|
||||
values: spec.$root.sprite.values
|
||||
}
|
||||
}
|
||||
if(fieldName === 'text-font') {
|
||||
return {
|
||||
...fieldSpec,
|
||||
values: spec.$root.glyphs.values
|
||||
}
|
||||
}
|
||||
return fieldSpec
|
||||
}
|
||||
|
||||
function getGroupName(layerType, fieldName) {
|
||||
const paint = GlSpec['paint_' + layerType] || {}
|
||||
function getGroupName(spec, layerType, fieldName) {
|
||||
const paint = spec['paint_' + layerType] || {}
|
||||
if (fieldName in paint) {
|
||||
return 'paint'
|
||||
} else {
|
||||
@@ -24,38 +36,35 @@ function getGroupName(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,
|
||||
layer: PropTypes.object.isRequired,
|
||||
groupFields: PropTypes.array.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
spec: PropTypes.object.isRequired,
|
||||
}
|
||||
|
||||
onPropertyChange(property, newValue) {
|
||||
const group = getGroupName(this.props.layer.type, property)
|
||||
this.props.onChange(group , property ,newValue)
|
||||
const group = getGroupName(this.props.spec, this.props.layer.type, property)
|
||||
this.props.onChange(group , property, newValue)
|
||||
}
|
||||
|
||||
render() {
|
||||
const fields = this.props.groupFields.map(fieldName => {
|
||||
const fieldSpec = getFieldSpec(this.props.layer.type, fieldName)
|
||||
const fieldSpec = getFieldSpec(this.props.spec, this.props.layer.type, fieldName)
|
||||
|
||||
const paint = this.props.layer.paint || {}
|
||||
const layout = this.props.layer.layout || {}
|
||||
const fieldValue = paint[fieldName] || layout[fieldName]
|
||||
const fieldValue = fieldName in paint ? paint[fieldName] : layout[fieldName]
|
||||
|
||||
return <ZoomSpecField
|
||||
return <FunctionSpecField
|
||||
onChange={this.onPropertyChange.bind(this)}
|
||||
key={fieldName}
|
||||
fieldName={fieldName}
|
||||
value={fieldValue}
|
||||
value={fieldValue === undefined ? fieldSpec.default : fieldValue}
|
||||
fieldSpec={fieldSpec}
|
||||
/>
|
||||
})
|
||||
|
||||
return <div style={{
|
||||
padding: margins[2],
|
||||
paddingRight: 0,
|
||||
backgroundColor: colors.black,
|
||||
}}>
|
||||
return <div className="maputnik-property-group">
|
||||
{fields}
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -1,14 +1,20 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import color from 'color'
|
||||
|
||||
import GlSpec from 'mapbox-gl-style-spec/reference/latest.min.js'
|
||||
import NumberField from './NumberField'
|
||||
import EnumField from './EnumField'
|
||||
import BooleanField from './BooleanField'
|
||||
import ColorField from './ColorField'
|
||||
import StringField from './StringField'
|
||||
import NumberInput from '../inputs/NumberInput'
|
||||
import CheckboxInput from '../inputs/CheckboxInput'
|
||||
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'
|
||||
|
||||
import input from '../../config/input.js'
|
||||
const iconProperties = ['background-pattern', 'fill-pattern', 'line-pattern', 'fill-extrusion-pattern', 'icon-image']
|
||||
|
||||
function labelFromFieldName(fieldName) {
|
||||
let label = fieldName.split('-').slice(1).join(' ')
|
||||
@@ -18,63 +24,113 @@ function labelFromFieldName(fieldName) {
|
||||
return label
|
||||
}
|
||||
|
||||
function optionsLabelLength(options) {
|
||||
let sum = 0;
|
||||
options.forEach(([_, label]) => {
|
||||
sum += label.length
|
||||
})
|
||||
return sum
|
||||
}
|
||||
|
||||
/** Display any field from the Mapbox GL style spec and
|
||||
* choose the correct field component based on the @{fieldSpec}
|
||||
* 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,
|
||||
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() {
|
||||
const commonProps = {
|
||||
doc: this.props.fieldSpec.doc,
|
||||
style: this.props.style,
|
||||
value: this.props.value,
|
||||
default: this.props.fieldSpec.default,
|
||||
name: this.props.fieldName,
|
||||
onChange: newValue => this.props.onChange(this.props.fieldName, newValue)
|
||||
}
|
||||
switch(this.props.fieldSpec.type) {
|
||||
case 'number': return (
|
||||
<NumberField
|
||||
{...commonProps}
|
||||
default={this.props.fieldSpec.default}
|
||||
min={this.props.fieldSpec.minimum}
|
||||
max={this.props.fieldSpec.maximum}
|
||||
unit={this.props.fieldSpec.unit}
|
||||
/>
|
||||
)
|
||||
case 'enum': return (
|
||||
<EnumField
|
||||
{...commonProps}
|
||||
allowedValues={Object.keys(this.props.fieldSpec.values)}
|
||||
/>
|
||||
)
|
||||
case 'string': return (
|
||||
<StringField
|
||||
{...commonProps}
|
||||
/>
|
||||
)
|
||||
case 'color': return (
|
||||
<ColorField
|
||||
{...commonProps}
|
||||
/>
|
||||
)
|
||||
case 'boolean': return (
|
||||
<BooleanField
|
||||
{...commonProps}
|
||||
/>
|
||||
)
|
||||
default: return null
|
||||
|
||||
function childNodes() {
|
||||
switch(this.props.fieldSpec.type) {
|
||||
case 'number': return (
|
||||
<NumberInput
|
||||
{...commonProps}
|
||||
min={this.props.fieldSpec.minimum}
|
||||
max={this.props.fieldSpec.maximum}
|
||||
/>
|
||||
)
|
||||
case 'enum':
|
||||
const options = Object.keys(this.props.fieldSpec.values).map(v => [v, capitalize(v)])
|
||||
|
||||
if(options.length <= 3 && optionsLabelLength(options) <= 20) {
|
||||
return <MultiButtonInput
|
||||
{...commonProps}
|
||||
options={options}
|
||||
/>
|
||||
} else {
|
||||
return <SelectInput
|
||||
{...commonProps}
|
||||
options={options}
|
||||
/>
|
||||
}
|
||||
case 'string':
|
||||
if(iconProperties.indexOf(this.props.fieldName) >= 0) {
|
||||
return <IconInput
|
||||
{...commonProps}
|
||||
icons={this.props.fieldSpec.values}
|
||||
/>
|
||||
} else {
|
||||
return <StringInput
|
||||
{...commonProps}
|
||||
/>
|
||||
}
|
||||
case 'color': return (
|
||||
<ColorField
|
||||
{...commonProps}
|
||||
/>
|
||||
)
|
||||
case 'boolean': return (
|
||||
<CheckboxInput
|
||||
{...commonProps}
|
||||
/>
|
||||
)
|
||||
case 'array':
|
||||
if(this.props.fieldName === 'text-font') {
|
||||
return <FontInput
|
||||
{...commonProps}
|
||||
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,34 +0,0 @@
|
||||
import React from 'react'
|
||||
import input from '../../config/input.js'
|
||||
|
||||
/*** Number fields with support for min, max and units and documentation*/
|
||||
class StringField extends React.Component {
|
||||
static propTypes = {
|
||||
onChange: React.PropTypes.func.isRequired,
|
||||
name: React.PropTypes.string.isRequired,
|
||||
value: React.PropTypes.string,
|
||||
default: React.PropTypes.number,
|
||||
doc: React.PropTypes.string,
|
||||
style: React.PropTypes.object,
|
||||
}
|
||||
|
||||
onChange(e) {
|
||||
const value = e.target.value
|
||||
return this.props.onChange(value === "" ? null: value)
|
||||
}
|
||||
|
||||
render() {
|
||||
return <input
|
||||
style={{
|
||||
...input.input,
|
||||
...this.props.style
|
||||
}}
|
||||
name={this.props.name}
|
||||
placeholder={this.props.default}
|
||||
value={this.props.value ? this.props.value : ""}
|
||||
onChange={this.onChange.bind(this)}
|
||||
/>
|
||||
}
|
||||
}
|
||||
|
||||
export default StringField
|
||||
@@ -1,97 +0,0 @@
|
||||
import React from 'react'
|
||||
import Color from 'color'
|
||||
|
||||
import NumberField from './NumberField'
|
||||
import EnumField from './EnumField'
|
||||
import BooleanField from './BooleanField'
|
||||
import ColorField from './ColorField'
|
||||
import StringField from './StringField'
|
||||
import SpecField from './SpecField'
|
||||
|
||||
import input from '../../config/input.js'
|
||||
import colors from '../../config/colors.js'
|
||||
import { margins } from '../../config/scales.js'
|
||||
|
||||
function isZoomField(value) {
|
||||
return typeof value === 'object' && value.stops
|
||||
}
|
||||
|
||||
const specFieldProps = {
|
||||
onChange: React.PropTypes.func.isRequired,
|
||||
fieldName: React.PropTypes.string.isRequired,
|
||||
fieldSpec: React.PropTypes.object.isRequired,
|
||||
}
|
||||
|
||||
/** Supports displaying spec field for zoom function objects
|
||||
* https://www.mapbox.com/mapbox-gl-style-spec/#types-function-zoom-property
|
||||
*/
|
||||
export default class ZoomSpecField extends React.Component {
|
||||
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,
|
||||
]),
|
||||
}
|
||||
|
||||
render() {
|
||||
const label = <label style={input.label}>
|
||||
{labelFromFieldName(this.props.fieldName)}
|
||||
</label>
|
||||
|
||||
if(isZoomField(this.props.value)) {
|
||||
const zoomFields = this.props.value.stops.map(stop => {
|
||||
const zoomLevel = stop[0]
|
||||
const value = stop[1]
|
||||
|
||||
return <div style={input.property} key={zoomLevel}>
|
||||
{label}
|
||||
<SpecField {...this.props}
|
||||
value={value}
|
||||
style={{
|
||||
width: '33%'
|
||||
}}
|
||||
/>
|
||||
|
||||
<input
|
||||
style={{
|
||||
...input.input,
|
||||
width: '10%',
|
||||
marginLeft: margins[0],
|
||||
}}
|
||||
type="number"
|
||||
value={zoomLevel}
|
||||
min={0}
|
||||
max={22}
|
||||
/>
|
||||
</div>
|
||||
})
|
||||
return <div style={{
|
||||
border: 1,
|
||||
borderStyle: 'solid',
|
||||
borderColor: Color(colors.gray).lighten(0.1).string(),
|
||||
padding: margins[1],
|
||||
}}>
|
||||
{zoomFields}
|
||||
</div>
|
||||
} else {
|
||||
return <div style={input.property}>
|
||||
{label}
|
||||
<SpecField {...this.props} />
|
||||
</div>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function labelFromFieldName(fieldName) {
|
||||
let label = fieldName.split('-').slice(1).join(' ')
|
||||
if(label.length > 0) {
|
||||
label = label.charAt(0).toUpperCase() + label.slice(1);
|
||||
}
|
||||
return 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.expression.parameters.includes('zoom')) {
|
||||
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,145 +1,47 @@
|
||||
import React from 'react'
|
||||
import GlSpec from 'mapbox-gl-style-spec/reference/latest.min.js'
|
||||
import PropTypes from 'prop-types'
|
||||
import { combiningFilterOps } from '../../libs/filterops.js'
|
||||
|
||||
import input from '../../config/input.js'
|
||||
import colors from '../../config/colors.js'
|
||||
import { margins } from '../../config/scales.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'
|
||||
import FilterEditorBlock from './FilterEditorBlock'
|
||||
import Button from '../Button'
|
||||
|
||||
const combiningFilterOps = ['all', 'any', 'none']
|
||||
const setFilterOps = ['in', '!in']
|
||||
const otherFilterOps = Object
|
||||
.keys(GlSpec.filter_operator.values)
|
||||
.filter(op => combiningFilterOps.indexOf(op) < 0)
|
||||
import DeleteIcon from 'react-icons/lib/md/delete'
|
||||
import AddIcon from 'react-icons/lib/fa/plus'
|
||||
|
||||
class CombiningOperatorSelect extends React.Component {
|
||||
static propTypes = {
|
||||
value: React.PropTypes.string.isRequired,
|
||||
onChange: React.PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
render() {
|
||||
const options = combiningFilterOps.map(op => {
|
||||
return <option key={op} value={op}>{op}</option>
|
||||
})
|
||||
|
||||
return <div>
|
||||
<select
|
||||
style={{
|
||||
...input.select,
|
||||
width: '20.5%',
|
||||
margin: margins[0],
|
||||
}}
|
||||
value={this.props.value}
|
||||
onChange={e => this.props.onChange(e.target.value)}
|
||||
>
|
||||
{options}
|
||||
</select>
|
||||
<label style={{
|
||||
...input.label,
|
||||
width: '60%',
|
||||
marginLeft: margins[0],
|
||||
}}>
|
||||
of the filters matches
|
||||
</label>
|
||||
</div>
|
||||
}
|
||||
function hasCombiningFilter(filter) {
|
||||
return combiningFilterOps.indexOf(filter[0]) >= 0
|
||||
}
|
||||
|
||||
class OperatorSelect extends React.Component {
|
||||
static propTypes = {
|
||||
value: React.PropTypes.string.isRequired,
|
||||
onChange: React.PropTypes.func.isRequired,
|
||||
function hasNestedCombiningFilter(filter) {
|
||||
if(hasCombiningFilter(filter)) {
|
||||
const combinedFilters = filter.slice(1)
|
||||
return filter.slice(1).map(f => hasCombiningFilter(f)).filter(f => f == true).length > 0
|
||||
}
|
||||
|
||||
render() {
|
||||
const options = otherFilterOps.map(op => {
|
||||
return <option key={op} value={op}>{op}</option>
|
||||
})
|
||||
return <select
|
||||
style={{
|
||||
...input.select,
|
||||
width: '15%',
|
||||
margin: margins[0]
|
||||
}}
|
||||
value={this.props.value}
|
||||
onChange={e => this.props.onChange(e.target.value)}
|
||||
>
|
||||
{options}
|
||||
</select>
|
||||
}
|
||||
}
|
||||
|
||||
class SingleFilterEditor extends React.Component {
|
||||
static propTypes = {
|
||||
filter: React.PropTypes.array.isRequired,
|
||||
onChange: React.PropTypes.func.isRequired,
|
||||
properties: React.PropTypes.object,
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
properties: {},
|
||||
}
|
||||
|
||||
onFilterPartChanged(filterOp, propertyName, filterArgs) {
|
||||
const newFilter = [filterOp, propertyName, ...filterArgs]
|
||||
this.props.onChange(newFilter)
|
||||
}
|
||||
|
||||
render() {
|
||||
const f = this.props.filter
|
||||
const filterOp = f[0]
|
||||
const propertyName = f[1]
|
||||
const filterArgs = f.slice(2)
|
||||
|
||||
return <div>
|
||||
<select
|
||||
style={{
|
||||
...input.select,
|
||||
width: '17%',
|
||||
margin: margins[0]
|
||||
}}
|
||||
value={propertyName}
|
||||
onChange={newPropertyName => this.onFilterPartChanged(filterOp, newPropertyName, filterArgs)}
|
||||
>
|
||||
{Object.keys(this.props.properties).map(propName => {
|
||||
return <option key={propName} value={propName}>{propName}</option>
|
||||
})}
|
||||
</select>
|
||||
<OperatorSelect
|
||||
value={filterOp}
|
||||
onChange={newFilterOp => this.onFilterPartChanged(newFilterOp, propertyName, filterArgs)}
|
||||
/>
|
||||
<input
|
||||
style={{
|
||||
...input.input,
|
||||
width: '53%',
|
||||
margin: margins[0]
|
||||
}}
|
||||
value={filterArgs.join(',')}
|
||||
onChange={e => {
|
||||
this.onFilterPartChanged(filterOp, propertyName, e.target.value.split(','))}}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
export default class CombiningFilterEditor extends React.Component {
|
||||
static propTypes = {
|
||||
/** Properties of the vector layer and the available fields */
|
||||
properties: React.PropTypes.object.isRequired,
|
||||
filter: React.PropTypes.array.isRequired,
|
||||
onChange: React.PropTypes.func.isRequired,
|
||||
properties: PropTypes.object,
|
||||
filter: PropTypes.array,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
// Convert filter to combining filter
|
||||
combiningFilter() {
|
||||
let combiningOp = this.props.filter[0]
|
||||
let filters = this.props.filter.slice(1)
|
||||
let filter = this.props.filter || ['all']
|
||||
|
||||
let combiningOp = filter[0]
|
||||
let filters = filter.slice(1)
|
||||
|
||||
if(combiningFilterOps.indexOf(combiningOp) < 0) {
|
||||
combiningOp = 'all'
|
||||
filters = [this.props.filter.slice(0)]
|
||||
filters = [filter.slice(0)]
|
||||
}
|
||||
|
||||
return [combiningOp, ...filters]
|
||||
@@ -151,30 +53,62 @@ export default class CombiningFilterEditor extends React.Component {
|
||||
this.props.onChange(newFilter)
|
||||
}
|
||||
|
||||
deleteFilterItem(filterIdx) {
|
||||
const newFilter = this.combiningFilter().slice(0)
|
||||
console.log('Delete', filterIdx, newFilter)
|
||||
newFilter.splice(filterIdx + 1, 1)
|
||||
this.props.onChange(newFilter)
|
||||
}
|
||||
|
||||
addFilterItem() {
|
||||
const newFilterItem = this.combiningFilter().slice(0)
|
||||
newFilterItem.push(['==', 'name', ''])
|
||||
this.props.onChange(newFilterItem)
|
||||
}
|
||||
|
||||
render() {
|
||||
const filter = this.combiningFilter()
|
||||
let combiningOp = filter[0]
|
||||
let filters = filter.slice(1)
|
||||
|
||||
const filterEditors = filters.map((f, idx) => {
|
||||
return <SingleFilterEditor
|
||||
key={idx}
|
||||
properties={this.props.properties}
|
||||
filter={f}
|
||||
onChange={this.onFilterPartChanged.bind(this, idx + 1)}
|
||||
/>
|
||||
const editorBlocks = filters.map((f, idx) => {
|
||||
return <FilterEditorBlock key={idx} onDelete={this.deleteFilterItem.bind(this, idx)}>
|
||||
<SingleFilterEditor
|
||||
properties={this.props.properties}
|
||||
filter={f}
|
||||
onChange={this.onFilterPartChanged.bind(this, idx + 1)}
|
||||
/>
|
||||
</FilterEditorBlock>
|
||||
})
|
||||
|
||||
return <div style={{
|
||||
padding: margins[2],
|
||||
paddingRight: 0,
|
||||
backgroundColor: colors.black
|
||||
}}>
|
||||
<CombiningOperatorSelect
|
||||
value={combiningOp}
|
||||
onChange={this.onFilterPartChanged.bind(this, 0)}
|
||||
/>
|
||||
{filterEditors}
|
||||
//TODO: Implement support for nested filter
|
||||
if(hasNestedCombiningFilter(filter)) {
|
||||
return <div className="maputnik-filter-editor-unsupported">
|
||||
Nested filters are not supported.
|
||||
</div>
|
||||
}
|
||||
|
||||
return <div className="maputnik-filter-editor">
|
||||
<div className="maputnik-filter-editor-compound-select" data-wd-key="layer-filter">
|
||||
<DocLabel
|
||||
label={"Compound Filter"}
|
||||
doc={styleSpec.latest.layer.filter.doc + " Combine multiple filters together by using a compound filter."}
|
||||
/>
|
||||
<SelectInput
|
||||
value={combiningOp}
|
||||
onChange={this.onFilterPartChanged.bind(this, 0)}
|
||||
options={[["all", "every filter matches"], ["none", "no filter matches"], ["any", "any filter matches"]]}
|
||||
/>
|
||||
</div>
|
||||
{editorBlocks}
|
||||
<div className="maputnik-filter-editor-add-wrapper">
|
||||
<Button
|
||||
data-wd-key="layer-filter-button"
|
||||
className="maputnik-add-filter"
|
||||
onClick={this.addFilterItem.bind(this)}>
|
||||
Add filter
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
29
src/components/filter/FilterEditorBlock.jsx
Normal file
29
src/components/filter/FilterEditorBlock.jsx
Normal file
@@ -0,0 +1,29 @@
|
||||
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: PropTypes.func.isRequired,
|
||||
children: PropTypes.element.isRequired,
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div className="maputnik-filter-editor-block">
|
||||
<div className="maputnik-filter-editor-block-action">
|
||||
<Button
|
||||
className="maputnik-delete-filter"
|
||||
onClick={this.props.onDelete}
|
||||
>
|
||||
<DeleteIcon />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="maputnik-filter-editor-block-content">
|
||||
{this.props.children}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
export default FilterEditorBlock
|
||||
93
src/components/filter/SingleFilterEditor.jsx
Normal file
93
src/components/filter/SingleFilterEditor.jsx
Normal file
@@ -0,0 +1,93 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import { otherFilterOps } from '../../libs/filterops.js'
|
||||
import StringInput from '../inputs/StringInput'
|
||||
import AutocompleteInput from '../inputs/AutocompleteInput'
|
||||
import SelectInput from '../inputs/SelectInput'
|
||||
|
||||
function tryParseInt(v) {
|
||||
if (v === '') return v
|
||||
if (isNaN(v)) return v
|
||||
return parseFloat(v)
|
||||
}
|
||||
|
||||
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: PropTypes.array.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
properties: PropTypes.object,
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
properties: {},
|
||||
}
|
||||
|
||||
onFilterPartChanged(filterOp, propertyName, filterArgs) {
|
||||
let newFilter = [filterOp, propertyName, ...filterArgs.map(parseFilter)]
|
||||
if(filterOp === 'has' || filterOp === '!has') {
|
||||
newFilter = [filterOp, propertyName]
|
||||
} else if(filterArgs.length === 0) {
|
||||
newFilter = [filterOp, propertyName, '']
|
||||
}
|
||||
this.props.onChange(newFilter)
|
||||
}
|
||||
|
||||
render() {
|
||||
const f = this.props.filter
|
||||
const filterOp = f[0]
|
||||
const propertyName = f[1]
|
||||
const filterArgs = f.slice(2)
|
||||
|
||||
return <div className="maputnik-filter-editor-single">
|
||||
<div className="maputnik-filter-editor-property">
|
||||
<AutocompleteInput
|
||||
value={propertyName}
|
||||
options={Object.keys(this.props.properties).map(propName => [propName, propName])}
|
||||
onChange={newPropertyName => this.onFilterPartChanged(filterOp, newPropertyName, filterArgs)}
|
||||
/>
|
||||
</div>
|
||||
<div className="maputnik-filter-editor-operator">
|
||||
<SelectInput
|
||||
value={filterOp}
|
||||
onChange={newFilterOp => this.onFilterPartChanged(newFilterOp, propertyName, filterArgs)}
|
||||
options={otherFilterOps}
|
||||
/>
|
||||
</div>
|
||||
{filterArgs.length > 0 &&
|
||||
<div className="maputnik-filter-editor-args">
|
||||
<StringInput
|
||||
value={filterArgs.join(',')}
|
||||
onChange={ v=> this.onFilterPartChanged(filterOp, propertyName, v.split(','))}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default SingleFilterEditor
|
||||
15
src/components/icons/CircleIcon.jsx
Normal file
15
src/components/icons/CircleIcon.jsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import React from 'react'
|
||||
import IconBase from 'react-icon-base'
|
||||
|
||||
|
||||
export default class FillIcon extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<IconBase viewBox="0 0 20 20" {...this.props}>
|
||||
<path transform="translate(2 2)" d="M7.5,0C11.6422,0,15,3.3578,15,7.5S11.6422,15,7.5,15 S0,11.6422,0,7.5S3.3578,0,7.5,0z M7.5,1.6666c-3.2217,0-5.8333,2.6117-5.8333,5.8334S4.2783,13.3334,7.5,13.3334 s5.8333-2.6117,5.8333-5.8334S10.7217,1.6666,7.5,1.6666z"></path>
|
||||
</IconBase>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,24 +1,30 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import LineIcon from './LineIcon.jsx'
|
||||
import FillIcon from './FillIcon.jsx'
|
||||
import SymbolIcon from './SymbolIcon.jsx'
|
||||
import BackgroundIcon from './BackgroundIcon.jsx'
|
||||
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() {
|
||||
const iconProps = { style: this.props.style }
|
||||
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} />
|
||||
default: return null
|
||||
case 'circle': return <CircleIcon {...iconProps} />
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
49
src/components/inputs/ArrayInput.jsx
Normal file
49
src/components/inputs/ArrayInput.jsx
Normal file
@@ -0,0 +1,49 @@
|
||||
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: PropTypes.array,
|
||||
type: PropTypes.string,
|
||||
length: PropTypes.number,
|
||||
default: PropTypes.array,
|
||||
onChange: PropTypes.func,
|
||||
}
|
||||
|
||||
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 || []
|
||||
}
|
||||
|
||||
render() {
|
||||
const inputs = this.values.map((v, i) => {
|
||||
if(this.props.type === 'number') {
|
||||
return <NumberInput
|
||||
key={i}
|
||||
value={v}
|
||||
onChange={this.changeValue.bind(this, i)}
|
||||
/>
|
||||
} else {
|
||||
return <StringInput
|
||||
key={i}
|
||||
value={v}
|
||||
onChange={this.changeValue.bind(this, i)}
|
||||
/>
|
||||
}
|
||||
})
|
||||
|
||||
return <div className="maputnik-array">
|
||||
{inputs}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
export default ArrayInput
|
||||
93
src/components/inputs/AutocompleteInput.jsx
Normal file
93
src/components/inputs/AutocompleteInput.jsx
Normal file
@@ -0,0 +1,93 @@
|
||||
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: PropTypes.string,
|
||||
options: PropTypes.array,
|
||||
onChange: PropTypes.func,
|
||||
keepMenuWithinWindowBounds: PropTypes.bool
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
onChange: () => {},
|
||||
options: [],
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
maxHeight: MAX_HEIGHT
|
||||
};
|
||||
}
|
||||
|
||||
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
|
||||
}}
|
||||
inputProps={{
|
||||
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]}
|
||||
className={classnames({
|
||||
"maputnik-autocomplete-menu-item": true,
|
||||
"maputnik-autocomplete-menu-item-selected": isHighlighted,
|
||||
})}
|
||||
>
|
||||
{item[1]}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
export default AutocompleteInput
|
||||
31
src/components/inputs/CheckboxInput.jsx
Normal file
31
src/components/inputs/CheckboxInput.jsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
class CheckboxInput extends React.Component {
|
||||
static propTypes = {
|
||||
value: PropTypes.bool.isRequired,
|
||||
style: PropTypes.object,
|
||||
onChange: PropTypes.func,
|
||||
}
|
||||
|
||||
render() {
|
||||
return <label className="maputnik-checkbox-wrapper">
|
||||
<input
|
||||
className="maputnik-checkbox"
|
||||
type="checkbox"
|
||||
style={this.props.style}
|
||||
onChange={e => this.props.onChange(!this.props.value)}
|
||||
checked={this.props.value}
|
||||
/>
|
||||
<div className="maputnik-checkbox-box">
|
||||
<svg style={{
|
||||
display: this.props.value ? 'inline' : 'none'
|
||||
}} className="maputnik-checkbox-icon" viewBox='0 0 32 32'>
|
||||
<path d='M1 14 L5 10 L13 18 L27 4 L31 8 L13 26 z' />
|
||||
</svg>
|
||||
</div>
|
||||
</label>
|
||||
}
|
||||
}
|
||||
|
||||
export default CheckboxInput
|
||||
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
|
||||
44
src/components/inputs/FontInput.jsx
Normal file
44
src/components/inputs/FontInput.jsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import AutocompleteInput from './AutocompleteInput'
|
||||
|
||||
class FontInput extends React.Component {
|
||||
static propTypes = {
|
||||
value: PropTypes.array.isRequired,
|
||||
default: PropTypes.array,
|
||||
fonts: PropTypes.array,
|
||||
style: PropTypes.object,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
fonts: []
|
||||
}
|
||||
|
||||
get values() {
|
||||
return this.props.value || this.props.default.slice(1) || []
|
||||
}
|
||||
|
||||
changeFont(idx, newValue) {
|
||||
const changedValues = this.values.slice(0)
|
||||
changedValues[idx] = newValue
|
||||
this.props.onChange(changedValues)
|
||||
}
|
||||
|
||||
render() {
|
||||
const inputs = this.values.map((value, i) => {
|
||||
return <AutocompleteInput
|
||||
key={i}
|
||||
value={value}
|
||||
options={this.props.fonts.map(f => [f, f])}
|
||||
onChange={this.changeFont.bind(this, i)}
|
||||
/>
|
||||
})
|
||||
|
||||
return <div className="maputnik-font">
|
||||
{inputs}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
export default FontInput
|
||||
28
src/components/inputs/IconInput.jsx
Normal file
28
src/components/inputs/IconInput.jsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import AutocompleteInput from './AutocompleteInput'
|
||||
|
||||
|
||||
class IconInput extends React.Component {
|
||||
static propTypes = {
|
||||
value: PropTypes.array,
|
||||
icons: PropTypes.array,
|
||||
style: PropTypes.object,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
icons: []
|
||||
}
|
||||
|
||||
render() {
|
||||
return <AutocompleteInput
|
||||
value={this.props.value}
|
||||
options={this.props.icons.map(f => [f, f])}
|
||||
onChange={this.props.onChange}
|
||||
wrapperStyle={this.props.style}
|
||||
/>
|
||||
}
|
||||
}
|
||||
|
||||
export default IconInput
|
||||
@@ -1,12 +1,21 @@
|
||||
import React from 'react'
|
||||
import input from '../../config/input'
|
||||
import { margins } from '../../config/scales'
|
||||
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.string.isRequired,
|
||||
children: React.PropTypes.element.isRequired,
|
||||
"data-wd-key": PropTypes.string,
|
||||
label: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.element,
|
||||
]).isRequired,
|
||||
doc: PropTypes.string,
|
||||
action: PropTypes.element,
|
||||
children: PropTypes.node.isRequired,
|
||||
style: PropTypes.object,
|
||||
onChange: PropTypes.func,
|
||||
}
|
||||
|
||||
onChange(e) {
|
||||
@@ -15,13 +24,34 @@ class InputBlock extends React.Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div style={{
|
||||
display: 'block',
|
||||
marginTop: margins[2],
|
||||
marginBottom: margins[2],
|
||||
}}>
|
||||
<label style={input.label}>{this.props.label}</label>
|
||||
{this.props.children}
|
||||
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
|
||||
})}
|
||||
>
|
||||
{this.props.doc &&
|
||||
<div className="maputnik-input-block-label">
|
||||
<DocLabel
|
||||
label={this.props.label}
|
||||
doc={this.props.doc}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
{!this.props.doc &&
|
||||
<label className="maputnik-input-block-label">
|
||||
{this.props.label}
|
||||
</label>
|
||||
}
|
||||
{this.props.action &&
|
||||
<div className="maputnik-input-block-action">
|
||||
{this.props.action}
|
||||
</div>
|
||||
}
|
||||
<div className="maputnik-input-block-content">
|
||||
{this.props.children}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
36
src/components/inputs/MultiButtonInput.jsx
Normal file
36
src/components/inputs/MultiButtonInput.jsx
Normal file
@@ -0,0 +1,36 @@
|
||||
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: PropTypes.string.isRequired,
|
||||
options: PropTypes.array.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
render() {
|
||||
let options = this.props.options
|
||||
if(options.length > 0 && !Array.isArray(options[0])) {
|
||||
options = options.map(v => [v, v])
|
||||
}
|
||||
|
||||
const selectedValue = this.props.value || options[0][0]
|
||||
const buttons = options.map(([val, label])=> {
|
||||
return <Button
|
||||
key={val}
|
||||
onClick={e => this.props.onChange(val)}
|
||||
className={classnames({"maputnik-button-selected": val === selectedValue})}
|
||||
>
|
||||
{label}
|
||||
</Button>
|
||||
})
|
||||
|
||||
return <div className="maputnik-multibutton">
|
||||
{buttons}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
export default MultiButtonInput
|
||||
80
src/components/inputs/NumberInput.jsx
Normal file
80
src/components/inputs/NumberInput.jsx
Normal file
@@ -0,0 +1,80 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
class NumberInput extends React.Component {
|
||||
static propTypes = {
|
||||
value: PropTypes.number,
|
||||
default: PropTypes.number,
|
||||
min: PropTypes.number,
|
||||
max: PropTypes.number,
|
||||
onChange: PropTypes.func,
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
value: props.value
|
||||
}
|
||||
}
|
||||
|
||||
UNSAFE_componentWillReceiveProps(nextProps) {
|
||||
this.setState({ value: nextProps.value })
|
||||
}
|
||||
|
||||
changeValue(newValue) {
|
||||
const value = parseFloat(newValue)
|
||||
|
||||
const hasChanged = this.state.value !== value
|
||||
if(this.isValid(value) && hasChanged) {
|
||||
this.props.onChange(value)
|
||||
} else {
|
||||
this.setState({ value: newValue })
|
||||
}
|
||||
}
|
||||
|
||||
isValid(v) {
|
||||
const value = parseFloat(v)
|
||||
if(isNaN(value)) {
|
||||
return false
|
||||
}
|
||||
|
||||
if(!isNaN(this.props.min) && value < this.props.min) {
|
||||
return false
|
||||
}
|
||||
|
||||
if(!isNaN(this.props.max) && value > this.props.max) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
resetValue() {
|
||||
// Reset explicitly to default value if value has been cleared
|
||||
if(this.state.value === "") {
|
||||
return this.changeValue(this.props.default)
|
||||
}
|
||||
|
||||
// If set value is invalid fall back to the last valid value from props or at last resort the default value
|
||||
if(!this.isValid(this.state.value)) {
|
||||
if(this.isValid(this.props.value)) {
|
||||
this.changeValue(this.props.value)
|
||||
} else {
|
||||
this.changeValue(this.props.default)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return <input
|
||||
spellCheck="false"
|
||||
className="maputnik-number"
|
||||
placeholder={this.props.default}
|
||||
value={this.state.value}
|
||||
onChange={e => this.changeValue(e.target.value)}
|
||||
onBlur={this.resetValue.bind(this)}
|
||||
/>
|
||||
}
|
||||
}
|
||||
|
||||
export default NumberInput
|
||||
@@ -1,29 +1,30 @@
|
||||
import React from 'react'
|
||||
import input from '../../config/input.js'
|
||||
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,
|
||||
}
|
||||
|
||||
|
||||
render() {
|
||||
const options = this.props.options.map(([val, label])=> {
|
||||
return <option key={val} value={val}>{label}</option>
|
||||
})
|
||||
let options = this.props.options
|
||||
if(options.length > 0 && !Array.isArray(options[0])) {
|
||||
options = options.map(v => [v, v])
|
||||
}
|
||||
|
||||
return <select
|
||||
style={{
|
||||
...input.select,
|
||||
...this.props.style
|
||||
}}
|
||||
className="maputnik-select"
|
||||
data-wd-key={this.props["data-wd-key"]}
|
||||
style={this.props.style}
|
||||
value={this.props.value}
|
||||
onChange={e => this.props.onChange(e.target.value)}
|
||||
>
|
||||
{options}
|
||||
{ options.map(([val, label]) => <option key={val} value={val}>{label}</option>) }
|
||||
</select>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,61 @@
|
||||
import React from 'react'
|
||||
import input from '../../config/input.js'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
class StringInput extends React.Component {
|
||||
static propTypes = {
|
||||
value: React.PropTypes.string,
|
||||
style: React.PropTypes.object,
|
||||
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) {
|
||||
super(props)
|
||||
this.state = {
|
||||
value: props.value || ''
|
||||
}
|
||||
}
|
||||
|
||||
UNSAFE_componentWillReceiveProps(nextProps) {
|
||||
this.setState({ value: nextProps.value || '' })
|
||||
}
|
||||
|
||||
render() {
|
||||
return <input
|
||||
style={{
|
||||
...input.input,
|
||||
...this.props.style
|
||||
}}
|
||||
value={this.props.value}
|
||||
onChange={e => this.props.onChange(e.target.value)}
|
||||
/>
|
||||
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>
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
21
src/components/layers/Collapser.jsx
Normal file
21
src/components/layers/Collapser.jsx
Normal file
@@ -0,0 +1,21 @@
|
||||
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: PropTypes.bool.isRequired,
|
||||
style: PropTypes.object,
|
||||
}
|
||||
|
||||
render() {
|
||||
const iconStyle = {
|
||||
width: 20,
|
||||
height: 20,
|
||||
...this.props.style,
|
||||
}
|
||||
return this.props.isCollapsed ? <CollapseCloseIcon style={iconStyle}/> : <CollapseOpenIcon style={iconStyle} />
|
||||
}
|
||||
}
|
||||
|
||||
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,22 +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 colors from '../../config/colors'
|
||||
import { margins } from '../../config/scales'
|
||||
|
||||
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) {
|
||||
@@ -26,36 +29,59 @@ class JSONEditor extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
UNSAFE_componentWillReceiveProps(nextProps) {
|
||||
this.setState({
|
||||
code: JSON.stringify(nextProps.layer, null, 2)
|
||||
})
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
try {
|
||||
const parsedLayer = JSON.parse(this.state.code)
|
||||
// If the structure is still the same do not update
|
||||
// because it affects editing experience by reformatting all the time
|
||||
return nextState.code !== JSON.stringify(parsedLayer, null, 2)
|
||||
} catch(err) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
onCodeUpdate(newCode) {
|
||||
try {
|
||||
const parsedLayer = JSON.parse(newCode)
|
||||
this.props.onChange(parsedLayer)
|
||||
} catch(err) {
|
||||
console.warn(err)
|
||||
} finally {
|
||||
this.setState({
|
||||
code: newCode
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
resetValue() {
|
||||
console.log('reset')
|
||||
this.setState({
|
||||
code: JSON.stringify(this.props.layer, null, 2)
|
||||
})
|
||||
}
|
||||
|
||||
render() {
|
||||
const codeMirrorOptions = {
|
||||
mode: {name: "javascript", json: true},
|
||||
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,30 +1,60 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { Wrapper, Button, Menu, MenuItem } from 'react-aria-menubutton'
|
||||
|
||||
import JSONEditor from './JSONEditor'
|
||||
import SourceEditor from './SourceEditor'
|
||||
import FilterEditor from '../filter/FilterEditor'
|
||||
import PropertyGroup from '../fields/PropertyGroup'
|
||||
import LayerEditorGroup from './LayerEditorGroup'
|
||||
import LayerSettings from './LayerSettings'
|
||||
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'
|
||||
import { margins, fontSizes } from '../../config/scales'
|
||||
import colors from '../../config/colors'
|
||||
|
||||
class UnsupportedLayer extends React.Component {
|
||||
render() {
|
||||
return <div></div>
|
||||
|
||||
function layoutGroups(layerType) {
|
||||
const layerGroup = {
|
||||
title: 'Layer',
|
||||
type: 'layer'
|
||||
}
|
||||
const filterGroup = {
|
||||
title: 'Filter',
|
||||
type: 'filter'
|
||||
}
|
||||
const editorGroup = {
|
||||
title: 'JSON Editor',
|
||||
type: 'jsoneditor'
|
||||
}
|
||||
return [layerGroup, filterGroup].concat(layout[layerType].groups).concat([editorGroup])
|
||||
}
|
||||
|
||||
/** Layer editor supporting multiple types of layers. */
|
||||
export default class LayerEditor extends React.Component {
|
||||
static propTypes = {
|
||||
layer: React.PropTypes.object.isRequired,
|
||||
sources: React.PropTypes.object,
|
||||
vectorLayers: React.PropTypes.object,
|
||||
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 = {
|
||||
@@ -34,7 +64,7 @@ export default class LayerEditor extends React.Component {
|
||||
}
|
||||
|
||||
static childContextTypes = {
|
||||
reactIconBase: React.PropTypes.object
|
||||
reactIconBase: PropTypes.object
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
@@ -42,14 +72,14 @@ export default class LayerEditor extends React.Component {
|
||||
|
||||
//TODO: Clean this up and refactor into function
|
||||
const editorGroups = {}
|
||||
layout[this.props.layer.type].groups.forEach(group => {
|
||||
layoutGroups(this.props.layer.type).forEach(group => {
|
||||
editorGroups[group.title] = true
|
||||
})
|
||||
|
||||
this.state = { editorGroups }
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
UNSAFE_componentWillReceiveProps(nextProps) {
|
||||
const additionalGroups = { ...this.state.editorGroups }
|
||||
|
||||
layout[nextProps.layer.type].groups.forEach(group => {
|
||||
@@ -66,38 +96,14 @@ export default class LayerEditor extends React.Component {
|
||||
getChildContext () {
|
||||
return {
|
||||
reactIconBase: {
|
||||
size: fontSizes[4],
|
||||
color: colors.lowgray,
|
||||
size: 14,
|
||||
color: '#8e8e8e',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** A {@property} in either the paint our layout {@group} has changed
|
||||
* to a {@newValue}.
|
||||
*/
|
||||
onPropertyChange(group, property, newValue) {
|
||||
if(group) {
|
||||
this.props.onLayerChanged({
|
||||
...this.props.layer,
|
||||
[group]: {
|
||||
...this.props.layer[group],
|
||||
[property]: newValue
|
||||
}
|
||||
})
|
||||
} else {
|
||||
this.props.onLayerChanged({
|
||||
...this.props.layer,
|
||||
[property]: newValue
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
onFilterChange(newValue) {
|
||||
const changedLayer = {
|
||||
...this.props.layer,
|
||||
filter: newValue
|
||||
}
|
||||
this.props.onLayerChanged(changedLayer)
|
||||
changeProperty(group, property, newValue) {
|
||||
this.props.onLayerChanged(changeProperty(this.props.layer, group, property, newValue))
|
||||
}
|
||||
|
||||
onGroupToggle(groupTitle, active) {
|
||||
@@ -111,46 +117,89 @@ 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 'settings': return <LayerSettings
|
||||
id={this.props.layer.id}
|
||||
type={this.props.layer.type}
|
||||
onTypeChange={v => this.onPropertyChange(null, 'type', v)}
|
||||
onIdChange={newId => this.props.onLayerIdChange(this.props.layer.id, newId)}
|
||||
/>
|
||||
case 'source': return <div>
|
||||
<FilterEditor
|
||||
filter={this.props.layer.filter}
|
||||
properties={this.props.vectorLayers[this.props.layer['source-layer']]}
|
||||
onChange={f => this.onFilterChange(f)}
|
||||
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)}
|
||||
/>
|
||||
<SourceEditor
|
||||
source={this.props.layer.source}
|
||||
sourceLayer={this.props.layer['source-layer']}
|
||||
sources={this.props.sources}
|
||||
onSourceChange={console.log}
|
||||
onSourceLayerChange={console.log}
|
||||
<LayerTypeBlock
|
||||
value={this.props.layer.type}
|
||||
onChange={newType => this.props.onLayerChanged(changeType(this.props.layer, newType))}
|
||||
/>
|
||||
{this.props.layer.type !== 'background' && <LayerSourceBlock
|
||||
sourceIds={Object.keys(this.props.sources)}
|
||||
value={this.props.layer.source}
|
||||
onChange={v => this.changeProperty(null, 'source', v)}
|
||||
/>
|
||||
}
|
||||
{['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)}
|
||||
/>
|
||||
}
|
||||
<MinZoomBlock
|
||||
value={this.props.layer.minzoom}
|
||||
onChange={v => this.changeProperty(null, 'minzoom', v)}
|
||||
/>
|
||||
<MaxZoomBlock
|
||||
value={this.props.layer.maxzoom}
|
||||
onChange={v => this.changeProperty(null, 'maxzoom', v)}
|
||||
/>
|
||||
<CommentBlock
|
||||
value={comment}
|
||||
onChange={v => this.changeProperty('metadata', 'maputnik:comment', v == "" ? undefined : v)}
|
||||
/>
|
||||
</div>
|
||||
case 'filter': return <div>
|
||||
<div className="maputnik-filter-editor-wrapper">
|
||||
<FilterEditor
|
||||
filter={this.props.layer.filter}
|
||||
properties={this.props.vectorLayers[this.props.layer['source-layer']]}
|
||||
onChange={f => this.changeProperty(null, 'filter', f)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
case 'properties': return <PropertyGroup
|
||||
layer={this.props.layer}
|
||||
groupFields={fields}
|
||||
onChange={this.onPropertyChange.bind(this)}
|
||||
spec={this.props.spec}
|
||||
onChange={this.changeProperty.bind(this)}
|
||||
/>
|
||||
case 'jsoneditor': return <JSONEditor
|
||||
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 layoutGroups = layout[layerType].groups.filter(group => {
|
||||
return !(this.props.layer.type === 'background' && group.type === 'source')
|
||||
const groups = layoutGroups(layerType).filter(group => {
|
||||
return !(layerType === 'background' && group.type === 'source')
|
||||
}).map(group => {
|
||||
return <LayerEditorGroup
|
||||
data-wd-key={group.title}
|
||||
key={group.title}
|
||||
title={group.title}
|
||||
isActive={this.state.editorGroups[group.title]}
|
||||
@@ -160,8 +209,74 @@ export default class LayerEditor extends React.Component {
|
||||
</LayerEditorGroup>
|
||||
})
|
||||
|
||||
return <div>
|
||||
{layoutGroups}
|
||||
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,62 +1,32 @@
|
||||
import React from 'react'
|
||||
import Color from 'color'
|
||||
import colors from '../../config/colors'
|
||||
import { margins, fontSizes } from '../../config/scales'
|
||||
import PropTypes from 'prop-types'
|
||||
import Collapser from './Collapser'
|
||||
import Collapse from './Collapse'
|
||||
|
||||
import Collapse from 'react-collapse'
|
||||
import CollapseOpenIcon from 'react-icons/lib/md/arrow-drop-down'
|
||||
import CollapseCloseIcon from 'react-icons/lib/md/arrow-drop-up'
|
||||
|
||||
class Collapser extends React.Component {
|
||||
static propTypes = {
|
||||
isCollapsed: React.PropTypes.bool.isRequired,
|
||||
}
|
||||
|
||||
render() {
|
||||
const iconStyle = {
|
||||
width: 20,
|
||||
height: 20,
|
||||
}
|
||||
return this.props.isCollapsed ? <CollapseCloseIcon style={iconStyle}/> : <CollapseOpenIcon style={iconStyle} />
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = { hover: false }
|
||||
"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 style={{
|
||||
fontSize: fontSizes[4],
|
||||
backgroundColor: this.state.hover ? Color(colors.black).lighten(0.30).string() : Color(colors.black).lighten(0.15).string(),
|
||||
color: colors.lowgray,
|
||||
cursor: 'pointer',
|
||||
userSelect: 'none',
|
||||
padding: margins[1],
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
lineHeight: '20px',
|
||||
}}
|
||||
onMouseOver={e => this.setState({hover: true})}
|
||||
onMouseOut={e => this.setState({hover: false})}
|
||||
<div className="maputnik-layer-editor-group"
|
||||
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}>
|
||||
{this.props.children}
|
||||
<Collapse isActive={this.props.isActive}>
|
||||
<div className="react-collapse-container">
|
||||
{this.props.children}
|
||||
</div>
|
||||
</Collapse>
|
||||
</div>
|
||||
}
|
||||
|
||||
27
src/components/layers/LayerIdBlock.jsx
Normal file
27
src/components/layers/LayerIdBlock.jsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import React from 'react'
|
||||
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'
|
||||
|
||||
class LayerIdBlock extends React.Component {
|
||||
static propTypes = {
|
||||
value: PropTypes.string.isRequired,
|
||||
wdKey: PropTypes.string.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
render() {
|
||||
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}
|
||||
/>
|
||||
</InputBlock>
|
||||
}
|
||||
}
|
||||
|
||||
export default LayerIdBlock
|
||||
@@ -1,18 +1,40 @@
|
||||
import React from 'react'
|
||||
import cloneDeep from 'lodash.clonedeep'
|
||||
import PropTypes from 'prop-types'
|
||||
import classnames from 'classnames'
|
||||
|
||||
import Button from '../Button'
|
||||
import LayerListGroup from './LayerListGroup'
|
||||
import LayerListItem from './LayerListItem'
|
||||
import AddIcon from 'react-icons/lib/md/add-circle-outline'
|
||||
import AddModal from '../modals/AddModal'
|
||||
|
||||
import style from '../../libs/style.js'
|
||||
import { margins } from '../../config/scales.js'
|
||||
|
||||
import {SortableContainer, SortableHandle, arrayMove} from 'react-sortable-hoc';
|
||||
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,
|
||||
layers: PropTypes.array.isRequired,
|
||||
selectedLayerIndex: PropTypes.number.isRequired,
|
||||
onLayersChange: PropTypes.func.isRequired,
|
||||
onLayerSelect: PropTypes.func,
|
||||
sources: PropTypes.object.isRequired,
|
||||
}
|
||||
|
||||
function layerPrefix(name) {
|
||||
return name.replace(' ', '-').replace('_', '-').split('-')[0]
|
||||
}
|
||||
|
||||
function findClosestCommonPrefix(layers, idx) {
|
||||
const currentLayerPrefix = layerPrefix(layers[idx].id)
|
||||
let closestIdx = idx
|
||||
for (let i = idx; i > 0; i--) {
|
||||
const previousLayerPrefix = layerPrefix(layers[i-1].id)
|
||||
if(previousLayerPrefix === currentLayerPrefix) {
|
||||
closestIdx = i - 1
|
||||
} else {
|
||||
return closestIdx
|
||||
}
|
||||
}
|
||||
return closestIdx
|
||||
}
|
||||
|
||||
// List of collapsible layer editors
|
||||
@@ -23,76 +45,171 @@ class LayerListContainer extends React.Component {
|
||||
onLayerSelect: () => {},
|
||||
}
|
||||
|
||||
onLayerDestroy(layerId) {
|
||||
const remainingLayers = this.props.layers.slice(0)
|
||||
const idx = style.indexOfLayer(remainingLayers, layerId)
|
||||
remainingLayers.splice(idx, 1);
|
||||
this.props.onLayersChange(remainingLayers)
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
collapsedGroups: {},
|
||||
areAllGroupsExpanded: false,
|
||||
isOpen: {
|
||||
add: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
toggleModal(modalName) {
|
||||
this.setState({
|
||||
isOpen: {
|
||||
...this.state.isOpen,
|
||||
[modalName]: !this.state.isOpen[modalName]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
onLayerVisibilityToggle(layerId) {
|
||||
const changedLayers = this.props.layers.slice(0)
|
||||
const idx = style.indexOfLayer(changedLayers, layerId)
|
||||
toggleLayers() {
|
||||
let idx=0
|
||||
|
||||
const layer = { ...changedLayers[idx] }
|
||||
const changedLayout = 'layout' in layer ? {...layer.layout} : {}
|
||||
changedLayout.visibility = changedLayout.visibility === 'none' ? 'visible' : 'none'
|
||||
let newGroups=[]
|
||||
|
||||
layer.layout = changedLayout
|
||||
changedLayers[idx] = layer
|
||||
this.props.onLayersChange(changedLayers)
|
||||
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++) {
|
||||
const previousLayer = this.props.layers[i-1]
|
||||
const layer = this.props.layers[i]
|
||||
if(previousLayer && layerPrefix(previousLayer.id) == layerPrefix(layer.id)) {
|
||||
const lastGroup = groups[groups.length - 1]
|
||||
lastGroup.push(layer)
|
||||
} else {
|
||||
groups.push([layer])
|
||||
}
|
||||
}
|
||||
return groups
|
||||
}
|
||||
|
||||
toggleLayerGroup(groupPrefix, idx) {
|
||||
const lookupKey = [groupPrefix, idx].join('-')
|
||||
const newGroups = { ...this.state.collapsedGroups }
|
||||
if(lookupKey in this.state.collapsedGroups) {
|
||||
newGroups[lookupKey] = !this.state.collapsedGroups[lookupKey]
|
||||
} else {
|
||||
newGroups[lookupKey] = false
|
||||
}
|
||||
this.setState({
|
||||
collapsedGroups: newGroups
|
||||
})
|
||||
}
|
||||
|
||||
isCollapsed(groupPrefix, idx) {
|
||||
const collapsed = this.state.collapsedGroups[[groupPrefix, idx].join('-')]
|
||||
return collapsed === undefined ? true : collapsed
|
||||
}
|
||||
|
||||
render() {
|
||||
const layerPanels = this.props.layers.map((layer, index) => {
|
||||
const layerId = layer.id
|
||||
return <LayerListItem
|
||||
index={index}
|
||||
key={layerId}
|
||||
layerId={layerId}
|
||||
layerType={layer.type}
|
||||
visibility={(layer.layout || {}).visibility}
|
||||
isSelected={index === this.props.selectedLayerIndex}
|
||||
onLayerSelect={this.props.onLayerSelect}
|
||||
onLayerDestroy={this.onLayerDestroy.bind(this)}
|
||||
onLayerCopy={this.onLayerCopy.bind(this)}
|
||||
onLayerVisibilityToggle={this.onLayerVisibilityToggle.bind(this)}
|
||||
/>
|
||||
|
||||
const listItems = []
|
||||
let idx = 0
|
||||
this.groupedLayers().forEach(layers => {
|
||||
const groupPrefix = layerPrefix(layers[0].id)
|
||||
if(layers.length > 1) {
|
||||
const grp = <LayerListGroup
|
||||
data-wd-key={[groupPrefix, idx].join('-')}
|
||||
key={[groupPrefix, idx].join('-')}
|
||||
title={groupPrefix}
|
||||
isActive={!this.isCollapsed(groupPrefix, idx) || idx === this.props.selectedLayerIndex}
|
||||
onActiveToggle={this.toggleLayerGroup.bind(this, groupPrefix, idx)}
|
||||
/>
|
||||
listItems.push(grp)
|
||||
}
|
||||
|
||||
layers.forEach((layer, idxInGroup) => {
|
||||
const groupIdx = findClosestCommonPrefix(this.props.layers, idx)
|
||||
|
||||
const listItem = <LayerListItem
|
||||
className={classnames({
|
||||
'maputnik-layer-list-item-collapsed': layers.length > 1 && this.isCollapsed(groupPrefix, groupIdx) && idx !== this.props.selectedLayerIndex,
|
||||
'maputnik-layer-list-item-group-last': idxInGroup == layers.length - 1 && layers.length > 1
|
||||
})}
|
||||
index={idx}
|
||||
key={layer.id}
|
||||
layerId={layer.id}
|
||||
layerType={layer.type}
|
||||
visibility={(layer.layout || {}).visibility}
|
||||
isSelected={idx === this.props.selectedLayerIndex}
|
||||
onLayerSelect={this.props.onLayerSelect}
|
||||
onLayerDestroy={this.props.onLayerDestroy.bind(this)}
|
||||
onLayerCopy={this.props.onLayerCopy.bind(this)}
|
||||
onLayerVisibilityToggle={this.props.onLayerVisibilityToggle.bind(this)}
|
||||
/>
|
||||
listItems.push(listItem)
|
||||
idx += 1
|
||||
})
|
||||
})
|
||||
return <ul style={{
|
||||
padding: 0,
|
||||
margin: 0
|
||||
}}>
|
||||
{layerPanels}
|
||||
</ul>
|
||||
|
||||
return <div className="maputnik-layer-list">
|
||||
<AddModal
|
||||
layers={this.props.layers}
|
||||
sources={this.props.sources}
|
||||
isOpen={this.state.isOpen.add}
|
||||
onOpenToggle={this.toggleModal.bind(this, 'add')}
|
||||
onLayersChange={this.props.onLayersChange}
|
||||
/>
|
||||
<header className="maputnik-layer-list-header">
|
||||
<span className="maputnik-layer-list-header-title">Layers</span>
|
||||
<span className="maputnik-space" />
|
||||
<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')}
|
||||
data-wd-key="layer-list:add-layer"
|
||||
className="maputnik-button maputnik-button-selected">
|
||||
Add Layer
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<ul className="maputnik-layer-list-container">
|
||||
{listItems}
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
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}
|
||||
/>
|
||||
}
|
||||
|
||||
28
src/components/layers/LayerListGroup.jsx
Normal file
28
src/components/layers/LayerListGroup.jsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import Collapser from './Collapser'
|
||||
|
||||
export default class LayerListGroup extends React.Component {
|
||||
static propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
"data-wd-key": PropTypes.string,
|
||||
isActive: PropTypes.bool.isRequired,
|
||||
onActiveToggle: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
render() {
|
||||
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>
|
||||
<span className="maputnik-space" />
|
||||
<Collapser
|
||||
style={{ height: 14, width: 14 }}
|
||||
isCollapsed={this.props.isActive}
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import Color from 'color'
|
||||
import classnames from 'classnames'
|
||||
|
||||
import CopyIcon from 'react-icons/lib/md/content-copy'
|
||||
import VisibilityIcon from 'react-icons/lib/md/visibility'
|
||||
@@ -10,10 +12,6 @@ import LayerIcon from '../icons/LayerIcon'
|
||||
import LayerEditor from './LayerEditor'
|
||||
import {SortableElement, SortableHandle} from 'react-sortable-hoc'
|
||||
|
||||
import colors from '../../config/colors.js'
|
||||
import { fontSizes, margins } from '../../config/scales.js'
|
||||
|
||||
|
||||
@SortableHandle
|
||||
class LayerTypeDragHandle extends React.Component {
|
||||
static propTypes = LayerIcon.propTypes
|
||||
@@ -23,9 +21,9 @@ class LayerTypeDragHandle extends React.Component {
|
||||
{...this.props}
|
||||
style={{
|
||||
cursor: 'move',
|
||||
width: 15,
|
||||
height: 15,
|
||||
paddingRight: margins[0],
|
||||
width: 14,
|
||||
height: 14,
|
||||
paddingRight: 3,
|
||||
}}
|
||||
/>
|
||||
}
|
||||
@@ -33,65 +31,46 @@ class LayerTypeDragHandle extends React.Component {
|
||||
|
||||
class IconAction extends React.Component {
|
||||
static propTypes = {
|
||||
action: React.PropTypes.string.isRequired,
|
||||
active: React.PropTypes.bool,
|
||||
onClick: React.PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = { hover: false }
|
||||
action: PropTypes.string.isRequired,
|
||||
onClick: PropTypes.func.isRequired,
|
||||
wdKey: PropTypes.string
|
||||
}
|
||||
|
||||
renderIcon() {
|
||||
const iconStyle = {
|
||||
fill: colors.black
|
||||
}
|
||||
|
||||
if(this.props.active) {
|
||||
iconStyle.fill = colors.midgray
|
||||
}
|
||||
if(this.state.hover) {
|
||||
iconStyle.fill = colors.lowgray
|
||||
}
|
||||
|
||||
switch(this.props.action) {
|
||||
case 'copy': return <CopyIcon style={iconStyle} />
|
||||
case 'show': return <VisibilityIcon style={iconStyle} />
|
||||
case 'hide': return <VisibilityOffIcon style={iconStyle} />
|
||||
case 'delete': return <DeleteIcon style={iconStyle} />
|
||||
default: return null
|
||||
case 'duplicate': return <CopyIcon />
|
||||
case 'show': return <VisibilityIcon />
|
||||
case 'hide': return <VisibilityOffIcon />
|
||||
case 'delete': return <DeleteIcon />
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return <a
|
||||
style={{
|
||||
display: "inline",
|
||||
marginLeft: margins[0],
|
||||
...this.props.style
|
||||
}}
|
||||
return <button
|
||||
tabIndex="-1"
|
||||
title={this.props.action}
|
||||
className="maputnik-layer-list-icon-action"
|
||||
data-wd-key={this.props.wdKey}
|
||||
onClick={this.props.onClick}
|
||||
onMouseOver={e => this.setState({hover: true})}
|
||||
onMouseOut={e => this.setState({hover: false})}
|
||||
>
|
||||
{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,
|
||||
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 = {
|
||||
@@ -103,86 +82,43 @@ class LayerListItem extends React.Component {
|
||||
}
|
||||
|
||||
static childContextTypes = {
|
||||
reactIconBase: React.PropTypes.object
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
hover: false
|
||||
}
|
||||
reactIconBase: PropTypes.object
|
||||
}
|
||||
|
||||
getChildContext() {
|
||||
return {
|
||||
reactIconBase: { size: 12 }
|
||||
reactIconBase: { size: 14 }
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const itemStyle = {
|
||||
fontWeight: 400,
|
||||
color: colors.lowgray,
|
||||
fontSize: fontSizes[5],
|
||||
borderLeft: 0,
|
||||
borderTop: 0,
|
||||
borderBottom: 1,
|
||||
borderRight: 0,
|
||||
borderStyle: "solid",
|
||||
userSelect: 'none',
|
||||
listStyle: 'none',
|
||||
zIndex: 2000,
|
||||
cursor: 'pointer',
|
||||
position: 'relative',
|
||||
padding: margins[1],
|
||||
borderColor: Color(colors.black).lighten(0.10).string(),
|
||||
backgroundColor: colors.black,
|
||||
}
|
||||
|
||||
if(this.state.hover) {
|
||||
itemStyle.backgroundColor = Color(colors.black).lighten(0.10).string()
|
||||
}
|
||||
|
||||
if(this.props.isSelected) {
|
||||
itemStyle.backgroundColor = Color(colors.black).lighten(0.15).string()
|
||||
}
|
||||
|
||||
const iconProps = {
|
||||
active: this.state.hover || this.props.isSelected
|
||||
}
|
||||
|
||||
return <li
|
||||
key={this.props.layerId}
|
||||
onClick={e => this.props.onLayerSelect(this.props.layerId)}
|
||||
onMouseOver={e => this.setState({hover: true})}
|
||||
onMouseOut={e => this.setState({hover: false})}
|
||||
style={itemStyle}>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row'
|
||||
}}>
|
||||
data-wd-key={"layer-list-item:"+this.props.layerId}
|
||||
className={classnames({
|
||||
"maputnik-layer-list-item": true,
|
||||
"maputnik-layer-list-item-selected": this.props.isSelected,
|
||||
[this.props.className]: true,
|
||||
})}>
|
||||
<LayerTypeDragHandle type={this.props.layerType} />
|
||||
<span style={{
|
||||
width: 115,
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis'
|
||||
}}>{this.props.layerId}</span>
|
||||
<span className="maputnik-layer-list-item-id">{this.props.layerId}</span>
|
||||
<span style={{flexGrow: 1}} />
|
||||
<IconAction {...iconProps}
|
||||
<IconAction
|
||||
wdKey={"layer-list-item:"+this.props.layerId+":delete"}
|
||||
action={'delete'}
|
||||
onClick={e => this.props.onLayerDestroy(this.props.layerId)}
|
||||
/>
|
||||
<IconAction {...iconProps}
|
||||
action={'copy'}
|
||||
<IconAction
|
||||
wdKey={"layer-list-item:"+this.props.layerId+":copy"}
|
||||
action={'duplicate'}
|
||||
onClick={e => this.props.onLayerCopy(this.props.layerId)}
|
||||
/>
|
||||
<IconAction {...iconProps}
|
||||
active={this.state.hover || this.props.isSelected || this.props.visibility === 'none'}
|
||||
<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)}
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
import React from 'react'
|
||||
import GlSpec from 'mapbox-gl-style-spec/reference/latest.min.js'
|
||||
import InputBlock from '../inputs/InputBlock'
|
||||
import StringInput from '../inputs/StringInput'
|
||||
import SelectInput from '../inputs/SelectInput'
|
||||
|
||||
import colors from '../../config/colors'
|
||||
import { margins } from '../../config/scales'
|
||||
|
||||
|
||||
class LayerSettings extends React.Component {
|
||||
static propTypes = {
|
||||
id: React.PropTypes.string.isRequired,
|
||||
type: React.PropTypes.oneOf(Object.keys(GlSpec.layer.type.values)).isRequired,
|
||||
onIdChange: React.PropTypes.func.isRequired,
|
||||
onTypeChange: React.PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div style={{
|
||||
padding: margins[2],
|
||||
paddingRight: 0,
|
||||
backgroundColor: colors.black,
|
||||
}}>
|
||||
<InputBlock label={"Layer ID"}>
|
||||
<StringInput
|
||||
value={this.props.id}
|
||||
onChange={this.props.onIdChange}
|
||||
/>
|
||||
</InputBlock>
|
||||
<InputBlock label={"Layer Type"}>
|
||||
<SelectInput
|
||||
options={[
|
||||
['background', 'Background'],
|
||||
['fill', 'Fill'],
|
||||
['line', 'Line'],
|
||||
['symbol', 'Symbol'],
|
||||
['raster', 'Raster'],
|
||||
['circle', 'Circle'],
|
||||
['fill-extrusion', 'Fill Extrusion'],
|
||||
]}
|
||||
onChange={this.props.onTypeChange}
|
||||
value={this.props.type}
|
||||
/>
|
||||
</InputBlock>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
export default LayerSettings
|
||||
35
src/components/layers/LayerSourceBlock.jsx
Normal file
35
src/components/layers/LayerSourceBlock.jsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import React from 'react'
|
||||
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 AutocompleteInput from '../inputs/AutocompleteInput'
|
||||
|
||||
class LayerSourceBlock extends React.Component {
|
||||
static propTypes = {
|
||||
value: PropTypes.string,
|
||||
wdKey: PropTypes.string,
|
||||
onChange: PropTypes.func,
|
||||
sourceIds: PropTypes.array,
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
onChange: () => {},
|
||||
sourceIds: [],
|
||||
}
|
||||
|
||||
render() {
|
||||
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}
|
||||
options={this.props.sourceIds.map(src => [src, src])}
|
||||
/>
|
||||
</InputBlock>
|
||||
}
|
||||
}
|
||||
|
||||
export default LayerSourceBlock
|
||||
37
src/components/layers/LayerSourceLayerBlock.jsx
Normal file
37
src/components/layers/LayerSourceLayerBlock.jsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import React from 'react'
|
||||
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 AutocompleteInput from '../inputs/AutocompleteInput'
|
||||
|
||||
class LayerSourceLayer extends React.Component {
|
||||
static propTypes = {
|
||||
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={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])}
|
||||
/>
|
||||
</InputBlock>
|
||||
}
|
||||
}
|
||||
|
||||
export default LayerSourceLayer
|
||||
38
src/components/layers/LayerTypeBlock.jsx
Normal file
38
src/components/layers/LayerTypeBlock.jsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
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: PropTypes.string.isRequired,
|
||||
wdKey: PropTypes.string,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
render() {
|
||||
return <InputBlock label={"Type"} doc={styleSpec.latest.layer.type.doc}
|
||||
data-wd-key={this.props.wdKey}
|
||||
>
|
||||
<SelectInput
|
||||
options={[
|
||||
['background', 'Background'],
|
||||
['fill', 'Fill'],
|
||||
['line', 'Line'],
|
||||
['symbol', 'Symbol'],
|
||||
['raster', 'Raster'],
|
||||
['circle', 'Circle'],
|
||||
['fill-extrusion', 'Fill Extrusion'],
|
||||
['hillshade', 'Hillshade'],
|
||||
['heatmap', 'Heatmap'],
|
||||
]}
|
||||
onChange={this.props.onChange}
|
||||
value={this.props.value}
|
||||
/>
|
||||
</InputBlock>
|
||||
}
|
||||
}
|
||||
|
||||
export default LayerTypeBlock
|
||||
29
src/components/layers/MaxZoomBlock.jsx
Normal file
29
src/components/layers/MaxZoomBlock.jsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
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: PropTypes.number,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
render() {
|
||||
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={styleSpec.latest.layer.maxzoom.minimum}
|
||||
max={styleSpec.latest.layer.maxzoom.maximum}
|
||||
default={styleSpec.latest.layer.maxzoom.maximum}
|
||||
/>
|
||||
</InputBlock>
|
||||
}
|
||||
}
|
||||
|
||||
export default MaxZoomBlock
|
||||
29
src/components/layers/MinZoomBlock.jsx
Normal file
29
src/components/layers/MinZoomBlock.jsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
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: PropTypes.number,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
render() {
|
||||
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={styleSpec.latest.layer.minzoom.minimum}
|
||||
max={styleSpec.latest.layer.minzoom.maximum}
|
||||
default={styleSpec.latest.layer.minzoom.minimum}
|
||||
/>
|
||||
</InputBlock>
|
||||
}
|
||||
}
|
||||
|
||||
export default MinZoomBlock
|
||||
@@ -1,54 +0,0 @@
|
||||
import React from 'react'
|
||||
|
||||
import PropertyGroup from '../fields/PropertyGroup'
|
||||
import input from '../../config/input.js'
|
||||
|
||||
/** Choose tileset (source) and the source layer */
|
||||
export default class SourceEditor extends React.Component {
|
||||
static propTypes = {
|
||||
source: React.PropTypes.string.isRequired,
|
||||
sourceLayer: React.PropTypes.string.isRequired,
|
||||
|
||||
onSourceChange: React.PropTypes.func.isRequired,
|
||||
onSourceLayerChange: React.PropTypes.func.isRequired,
|
||||
|
||||
/** List of available sources in the style
|
||||
* https://www.mapbox.com/mapbox-gl-style-spec/#root-sources */
|
||||
sources: React.PropTypes.object.isRequired,
|
||||
}
|
||||
|
||||
render() {
|
||||
const options = Object.keys(this.props.sources).map(sourceId => {
|
||||
return <option key={sourceId} value={sourceId}>{sourceId}</option>
|
||||
})
|
||||
|
||||
const layerOptions = this.props.sources[this.props.source].map(vectorLayerId => {
|
||||
const id = vectorLayerId
|
||||
return <option key={id} value={id}>{id}</option>
|
||||
})
|
||||
|
||||
return <div>
|
||||
<div style={input.property}>
|
||||
<label style={input.label}>Source</label>
|
||||
<select
|
||||
style={input.select}
|
||||
value={this.props.source}
|
||||
onChange={(e) => this.onSourceChange(e.target.value)}
|
||||
>
|
||||
{options}
|
||||
</select>
|
||||
</div>
|
||||
<div style={input.property}>
|
||||
<label style={input.label}>Source Layer</label>
|
||||
<select
|
||||
style={input.select}
|
||||
value={this.props.sourceLayer}
|
||||
onChange={(e) => this.onSourceLayerChange(e.target.value)}
|
||||
>
|
||||
{layerOptions}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
}
|
||||
}
|
||||
70
src/components/map/FeatureLayerPopup.jsx
Normal file
70
src/components/map/FeatureLayerPopup.jsx
Normal file
@@ -0,0 +1,70 @@
|
||||
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)
|
||||
|
||||
const items = Object.keys(sources).map(vectorLayerId => {
|
||||
const layers = sources[vectorLayerId].map((feature, idx) => {
|
||||
return <label
|
||||
key={idx}
|
||||
className="maputnik-popup-layer"
|
||||
onClick={() => {
|
||||
this.props.onLayerSelect(feature.layer.id)
|
||||
}}
|
||||
>
|
||||
<LayerIcon type={feature.layer.type} style={{
|
||||
width: 14,
|
||||
height: 14,
|
||||
paddingRight: 3
|
||||
}}/>
|
||||
{feature.layer.id}
|
||||
{feature.counter && <span> × {feature.counter}</span>}
|
||||
</label>
|
||||
})
|
||||
return <div key={vectorLayerId}>
|
||||
<div className="maputnik-popup-layer-id">{vectorLayerId}</div>
|
||||
{layers}
|
||||
</div>
|
||||
})
|
||||
|
||||
return <div className="maputnik-feature-layer-popup">
|
||||
{items}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default FeatureLayerPopup
|
||||
71
src/components/map/FeaturePropertyPopup.jsx
Normal file
71
src/components/map/FeaturePropertyPopup.jsx
Normal file
@@ -0,0 +1,71 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import InputBlock from '../inputs/InputBlock'
|
||||
import StringInput from '../inputs/StringInput'
|
||||
|
||||
function displayValue(value) {
|
||||
if (typeof value === 'undefined' || value === null) return value;
|
||||
if (value instanceof Date) return value.toLocaleString();
|
||||
if (typeof value === 'object' ||
|
||||
typeof value === 'number' ||
|
||||
typeof value === 'string') return value.toString();
|
||||
return value;
|
||||
}
|
||||
|
||||
function renderProperties(feature) {
|
||||
return Object.keys(feature.properties).map(propertyName => {
|
||||
const property = feature.properties[propertyName]
|
||||
return <InputBlock key={propertyName} label={propertyName}>
|
||||
<StringInput value={displayValue(property)} style={{backgroundColor: 'transparent'}}/>
|
||||
</InputBlock>
|
||||
})
|
||||
}
|
||||
|
||||
function renderFeature(feature) {
|
||||
return <div key={feature.id}>
|
||||
<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>
|
||||
{renderProperties(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 = removeDuplicatedFeatures(this.props.features)
|
||||
return <div className="maputnik-feature-property-popup">
|
||||
{features.map(renderFeature)}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default FeaturePropertyPopup
|
||||
@@ -1,20 +0,0 @@
|
||||
import React from 'react'
|
||||
|
||||
export default class Map extends React.Component {
|
||||
static propTypes = {
|
||||
mapStyle: React.PropTypes.object.isRequired,
|
||||
accessToken: React.PropTypes.string,
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div
|
||||
ref={x => this.container = x}
|
||||
style={{
|
||||
position: "fixed",
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
}}></div>
|
||||
}
|
||||
}
|
||||
@@ -1,42 +1,172 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import ReactDOM from 'react-dom'
|
||||
import MapboxGl from 'mapbox-gl'
|
||||
import validateColor from 'mapbox-gl-style-spec/lib/validate/validate_color'
|
||||
|
||||
import Map from './Map.jsx'
|
||||
import MapboxInspect from 'mapbox-gl-inspect'
|
||||
import FeatureLayerPopup from './FeatureLayerPopup'
|
||||
import FeaturePropertyPopup from './FeaturePropertyPopup'
|
||||
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'
|
||||
import '../../libs/mapbox-rtl'
|
||||
|
||||
export default class MapboxGlMap extends Map {
|
||||
function renderPropertyPopup(features) {
|
||||
var mountNode = document.createElement('div');
|
||||
ReactDOM.render(<FeaturePropertyPopup features={features} />, mountNode)
|
||||
return mountNode.innerHTML;
|
||||
}
|
||||
|
||||
function buildInspectStyle(originalMapStyle, coloredLayers, highlightedLayer) {
|
||||
const backgroundLayer = {
|
||||
"id": "background",
|
||||
"type": "background",
|
||||
"paint": {
|
||||
"background-color": '#1c1f24',
|
||||
}
|
||||
}
|
||||
|
||||
const layer = colorHighlightedLayer(highlightedLayer)
|
||||
if(layer) {
|
||||
coloredLayers.push(layer)
|
||||
}
|
||||
|
||||
const sources = {}
|
||||
Object.keys(originalMapStyle.sources).forEach(sourceId => {
|
||||
const source = originalMapStyle.sources[sourceId]
|
||||
if(source.type !== 'raster' && source.type !== 'raster-dem') {
|
||||
sources[sourceId] = source
|
||||
}
|
||||
})
|
||||
|
||||
const inspectStyle = {
|
||||
...originalMapStyle,
|
||||
sources: sources,
|
||||
layers: [backgroundLayer].concat(coloredLayers)
|
||||
}
|
||||
return inspectStyle
|
||||
}
|
||||
|
||||
export default class MapboxGlMap extends React.Component {
|
||||
static propTypes = {
|
||||
onMapLoaded: React.PropTypes.func,
|
||||
onDataChange: PropTypes.func,
|
||||
onLayerSelect: PropTypes.func.isRequired,
|
||||
mapStyle: PropTypes.object.isRequired,
|
||||
inspectModeEnabled: PropTypes.bool.isRequired,
|
||||
highlightedLayer: PropTypes.object,
|
||||
options: PropTypes.object,
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
onMapLoaded: () => {}
|
||||
onMapLoaded: () => {},
|
||||
onDataChange: () => {},
|
||||
onLayerSelect: () => {},
|
||||
mapboxAccessToken: tokens.mapbox,
|
||||
options: {},
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = { map: null }
|
||||
MapboxGl.accessToken = tokens.mapbox
|
||||
this.state = {
|
||||
map: null,
|
||||
inspect: null,
|
||||
isPopupOpen: false,
|
||||
popupX: 0,
|
||||
popupY: 0,
|
||||
}
|
||||
}
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if(!this.state.map) return
|
||||
|
||||
//Mapbox GL now does diffing natively so we don't need to calculate
|
||||
//the necessary operations ourselves!
|
||||
this.state.map.setStyle(nextProps.mapStyle, { diff: true})
|
||||
UNSAFE_componentWillReceiveProps(nextProps) {
|
||||
if(!this.state.map) return
|
||||
const metadata = nextProps.mapStyle.metadata || {}
|
||||
MapboxGl.accessToken = metadata['maputnik:mapbox_access_token'] || tokens.mapbox
|
||||
|
||||
if(!nextProps.inspectModeEnabled) {
|
||||
//Mapbox GL now does diffing natively so we don't need to calculate
|
||||
//the necessary operations ourselves!
|
||||
this.state.map.setStyle(nextProps.mapStyle, { diff: true})
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
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;
|
||||
map.showCollisionBoxes = this.props.options.showCollisionBoxes;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
MapboxGl.accessToken = this.props.accessToken
|
||||
|
||||
const map = new MapboxGl.Map({
|
||||
const mapOpts = {
|
||||
...this.props.options,
|
||||
container: this.container,
|
||||
style: this.props.mapStyle,
|
||||
});
|
||||
hash: true,
|
||||
}
|
||||
|
||||
map.on("style.load", (...args) => {
|
||||
this.props.onMapLoaded(map)
|
||||
this.setState({ map });
|
||||
});
|
||||
const map = new MapboxGl.Map(mapOpts);
|
||||
|
||||
map.showTileBoundaries = mapOpts.showTileBoundaries;
|
||||
map.showCollisionBoxes = mapOpts.showCollisionBoxes;
|
||||
|
||||
const zoom = new ZoomControl;
|
||||
map.addControl(zoom, 'top-right');
|
||||
|
||||
const nav = new MapboxGl.NavigationControl();
|
||||
map.addControl(nav, 'top-right');
|
||||
|
||||
const inspect = new MapboxInspect({
|
||||
popup: new MapboxGl.Popup({
|
||||
closeOnClick: false
|
||||
}),
|
||||
showMapPopup: true,
|
||||
showMapPopupOnHover: false,
|
||||
showInspectMapPopupOnHover: true,
|
||||
showInspectButton: false,
|
||||
blockHoverPopupOnClick: true,
|
||||
assignLayerColor: (layerId, alpha) => {
|
||||
return Color(colors.brightColor(layerId, alpha)).desaturate(0.5).string()
|
||||
},
|
||||
buildInspectStyle: (originalMapStyle, coloredLayers) => buildInspectStyle(originalMapStyle, coloredLayers, this.props.highlightedLayer),
|
||||
renderPopup: features => {
|
||||
if(this.props.inspectModeEnabled) {
|
||||
return renderPropertyPopup(features)
|
||||
} else {
|
||||
var mountNode = document.createElement('div');
|
||||
ReactDOM.render(<FeatureLayerPopup features={features} onLayerSelect={this.props.onLayerSelect} />, mountNode)
|
||||
return mountNode
|
||||
}
|
||||
}
|
||||
})
|
||||
map.addControl(inspect)
|
||||
|
||||
map.on("style.load", () => {
|
||||
this.setState({ map, inspect });
|
||||
})
|
||||
|
||||
map.on("data", e => {
|
||||
if(e.dataType !== 'tile') return
|
||||
this.props.onDataChange({
|
||||
map: this.state.map
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div
|
||||
className="maputnik-map"
|
||||
ref={x => this.container = x}
|
||||
></div>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,68 +1,80 @@
|
||||
import React from 'react'
|
||||
import Map from './Map'
|
||||
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'
|
||||
|
||||
class OpenLayers3Map extends Map {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
class OpenLayers3Map extends React.Component {
|
||||
static propTypes = {
|
||||
onDataChange: PropTypes.func,
|
||||
mapStyle: PropTypes.object.isRequired,
|
||||
accessToken: PropTypes.string,
|
||||
style: PropTypes.object,
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
require.ensure(["openlayers", "ol-mapbox-style"], ()=> {
|
||||
const ol = require('openlayers')
|
||||
const olms = require('ol-mapbox-style')
|
||||
const jsonStyle = nextProps.mapStyle
|
||||
const styleFunc = olms.getStyleFunction(jsonStyle, 'openmaptiles', this.resolutions)
|
||||
console.log('New style babee')
|
||||
static defaultProps = {
|
||||
onMapLoaded: () => {},
|
||||
onDataChange: () => {},
|
||||
}
|
||||
|
||||
const layer = this.layer
|
||||
layer.setStyle(styleFunc)
|
||||
//NOTE: We need to mark the source as changed in order
|
||||
//to trigger a rerender
|
||||
layer.getSource().changed()
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.map = null
|
||||
}
|
||||
|
||||
this.state.map.render()
|
||||
updateStyle(newMapStyle) {
|
||||
const olms = require('ol-mapbox-style');
|
||||
const styleFunc = olms.apply(this.map, newMapStyle)
|
||||
}
|
||||
|
||||
UNSAFE_componentWillReceiveProps(nextProps) {
|
||||
require.ensure(["ol", "ol-mapbox-style"], () => {
|
||||
if(!this.map) return
|
||||
this.updateStyle(nextProps.mapStyle)
|
||||
})
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
const tilegrid = ol.tilegrid.createXYZ({tileSize: 512, maxZoom: 22})
|
||||
this.resolutions = tilegrid.getResolutions()
|
||||
this.layer = new ol.layer.VectorTile({
|
||||
source: new ol.source.VectorTile({
|
||||
attributions: '© <a href="http://www.openstreetmap.org/copyright">OpenStreetMap contributors</a>',
|
||||
format: new ol.format.MVT(),
|
||||
tileGrid: tilegrid,
|
||||
tilePixelRatio: 8,
|
||||
url: 'https://free-0.tilehosting.com/data/v3/{z}/{x}/{y}.pbf?key=tXiQqN3lIgskyDErJCeY'
|
||||
})
|
||||
})
|
||||
|
||||
const jsonStyle = this.props.mapStyle
|
||||
const styleFunc = olms.getStyleFunction(jsonStyle, 'openmaptiles', this.resolutions)
|
||||
this.layer.setStyle(styleFunc)
|
||||
|
||||
const map = new ol.Map({
|
||||
const map = new olMap({
|
||||
target: this.container,
|
||||
layers: [this.layer],
|
||||
view: new ol.View({
|
||||
center: jsonStyle.center,
|
||||
layers: [],
|
||||
view: new olView({
|
||||
zoom: 2,
|
||||
//zoom: jsonStyle.zoom,
|
||||
center: [52.5, -78.4]
|
||||
})
|
||||
})
|
||||
map.addControl(new ol.control.Zoom());
|
||||
this.setState({ map });
|
||||
map.addControl(new olZoom())
|
||||
this.map = map
|
||||
this.updateStyle(this.props.mapStyle)
|
||||
})
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div
|
||||
ref={x => this.container = x}
|
||||
style={{
|
||||
position: "fixed",
|
||||
top: 40,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
height: 'calc(100% - 40px)',
|
||||
width: "75%",
|
||||
backgroundColor: '#fff',
|
||||
...this.props.style,
|
||||
}}>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
export default OpenLayers3Map
|
||||
|
||||
164
src/components/modals/AddModal.jsx
Normal file
164
src/components/modals/AddModal.jsx
Normal file
@@ -0,0 +1,164 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import Button from '../Button'
|
||||
import InputBlock from '../inputs/InputBlock'
|
||||
import StringInput from '../inputs/StringInput'
|
||||
import Modal from './Modal'
|
||||
|
||||
import LayerTypeBlock from '../layers/LayerTypeBlock'
|
||||
import LayerIdBlock from '../layers/LayerIdBlock'
|
||||
import LayerSourceBlock from '../layers/LayerSourceBlock'
|
||||
import LayerSourceLayerBlock from '../layers/LayerSourceLayerBlock'
|
||||
|
||||
class AddModal extends React.Component {
|
||||
static propTypes = {
|
||||
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: PropTypes.object.isRequired,
|
||||
}
|
||||
|
||||
addLayer() {
|
||||
const changedLayers = this.props.layers.slice(0)
|
||||
const layer = {
|
||||
id: this.state.id,
|
||||
type: this.state.type,
|
||||
}
|
||||
|
||||
if(this.state.type !== 'background') {
|
||||
layer.source = this.state.source
|
||||
if(this.state.type !== 'raster' && this.state['source-layer']) {
|
||||
layer['source-layer'] = this.state['source-layer']
|
||||
}
|
||||
}
|
||||
|
||||
changedLayers.push(layer)
|
||||
|
||||
this.props.onLayersChange(changedLayers)
|
||||
this.props.onOpenToggle(false)
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
type: 'fill',
|
||||
id: '',
|
||||
}
|
||||
|
||||
if(props.sources.length > 0) {
|
||||
this.state.source = Object.keys(this.props.sources)[0]
|
||||
this.state['source-layer'] = this.props.sources[this.state.source][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: ""
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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}
|
||||
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={sources}
|
||||
wdKey="add-layer.layer-source-block"
|
||||
value={this.state.source}
|
||||
onChange={v => this.setState({ source: v })}
|
||||
/>
|
||||
}
|
||||
{['background', 'raster', 'hillshade', 'heatmap'].indexOf(this.state.type) < 0 &&
|
||||
<LayerSourceLayerBlock
|
||||
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)}
|
||||
data-wd-key="add-layer"
|
||||
>
|
||||
Add Layer
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
}
|
||||
}
|
||||
|
||||
export default AddModal
|
||||
305
src/components/modals/ExportModal.jsx
Normal file
305
src/components/modals/ExportModal.jsx
Normal file
@@ -0,0 +1,305 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { saveAs } from 'file-saver'
|
||||
|
||||
import * as styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
|
||||
import InputBlock from '../inputs/InputBlock'
|
||||
import StringInput from '../inputs/StringInput'
|
||||
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'
|
||||
import GitHub from 'github-api'
|
||||
import { CopyToClipboard } from 'react-copy-to-clipboard'
|
||||
|
||||
|
||||
class Gist extends React.Component {
|
||||
static propTypes = {
|
||||
mapStyle: PropTypes.object.isRequired,
|
||||
onStyleChanged: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
preview: false,
|
||||
public: false,
|
||||
saving: false,
|
||||
latestGist: null,
|
||||
}
|
||||
}
|
||||
|
||||
UNSAFE_componentWillReceiveProps(nextProps) {
|
||||
this.setState({
|
||||
...this.state,
|
||||
preview: !!(nextProps.mapStyle.metadata || {})['maputnik:openmaptiles_access_token']
|
||||
})
|
||||
}
|
||||
|
||||
onSave() {
|
||||
this.setState({
|
||||
...this.state,
|
||||
saving: true
|
||||
});
|
||||
|
||||
const preview = this.state.preview;
|
||||
|
||||
const mapboxToken = (this.props.mapStyle.metadata || {})['maputnik:mapbox_access_token'];
|
||||
|
||||
const mapStyleStr = preview ?
|
||||
styleSpec.format(stripAccessTokens(style.replaceAccessTokens(this.props.mapStyle))) :
|
||||
styleSpec.format(stripAccessTokens(this.props.mapStyle));
|
||||
const styleTitle = this.props.mapStyle.name || 'Style';
|
||||
const htmlStr = `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>`+styleTitle+` Preview</title>
|
||||
<link rel="stylesheet" type="text/css" href="https://api.mapbox.com/mapbox-gl-js/v0.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%; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id='map'></div>
|
||||
<script>
|
||||
mapboxgl.accessToken = '${mapboxToken}';
|
||||
var map = new mapboxgl.Map({
|
||||
container: 'map',
|
||||
style: 'style.json',
|
||||
attributionControl: true,
|
||||
hash: true
|
||||
});
|
||||
map.addControl(new mapboxgl.NavigationControl());
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
`
|
||||
const files = {
|
||||
"style.json": {
|
||||
content: mapStyleStr
|
||||
}
|
||||
}
|
||||
if(preview) {
|
||||
files["index.html"] = {
|
||||
content: htmlStr
|
||||
}
|
||||
}
|
||||
const gh = new GitHub();
|
||||
let gist = gh.getGist(); // not a gist yet
|
||||
gist.create({
|
||||
public: this.state.public,
|
||||
description: styleTitle,
|
||||
files: files
|
||||
}).then(function({data}) {
|
||||
return gist.read();
|
||||
}).then(function({data}) {
|
||||
this.setState({
|
||||
...this.state,
|
||||
latestGist: data,
|
||||
saving: false,
|
||||
});
|
||||
}.bind(this));
|
||||
}
|
||||
|
||||
onPreviewChange(value) {
|
||||
this.setState({
|
||||
...this.state,
|
||||
preview: value
|
||||
})
|
||||
}
|
||||
|
||||
onPublicChange(value) {
|
||||
this.setState({
|
||||
...this.state,
|
||||
public: value
|
||||
})
|
||||
}
|
||||
|
||||
changeMetadataProperty(property, value) {
|
||||
const changedStyle = {
|
||||
...this.props.mapStyle,
|
||||
metadata: {
|
||||
...this.props.mapStyle.metadata,
|
||||
[property]: value
|
||||
}
|
||||
}
|
||||
this.props.onStyleChanged(changedStyle)
|
||||
}
|
||||
|
||||
renderPreviewLink() {
|
||||
const gist = this.state.latestGist;
|
||||
const user = gist.user || 'anonymous';
|
||||
const preview = !!gist.files['index.html'];
|
||||
if(preview) {
|
||||
return <span><a target="_blank" rel="noopener noreferrer" href={"https://bl.ocks.org/"+user+"/"+gist.id}>Preview</a>,{' '}</span>
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
renderLatestGist() {
|
||||
const gist = this.state.latestGist;
|
||||
const saving = this.state.saving;
|
||||
if(saving) {
|
||||
return <p>Saving...</p>
|
||||
} else if(gist) {
|
||||
const user = gist.user || 'anonymous';
|
||||
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" 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>
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div className="maputnik-export-gist">
|
||||
<Button onClick={this.onSave.bind(this)}>
|
||||
<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
|
||||
label={"OpenMapTiles Access Token: "}>
|
||||
<StringInput
|
||||
value={(this.props.mapStyle.metadata || {})['maputnik:openmaptiles_access_token']}
|
||||
onChange={this.changeMetadataProperty.bind(this, "maputnik:openmaptiles_access_token")}/>
|
||||
</InputBlock>
|
||||
<InputBlock
|
||||
label={"Mapbox Access Token: "}>
|
||||
<StringInput
|
||||
value={(this.props.mapStyle.metadata || {})['maputnik:mapbox_access_token']}
|
||||
onChange={this.changeMetadataProperty.bind(this, "maputnik:mapbox_access_token")}/>
|
||||
</InputBlock>
|
||||
<a target="_blank" rel="noopener noreferrer" href="https://openmaptiles.com/hosting/">Get your free access token</a>
|
||||
</div>
|
||||
: null}
|
||||
{this.renderLatestGist()}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
function stripAccessTokens(mapStyle) {
|
||||
const changedMetadata = { ...mapStyle.metadata }
|
||||
delete changedMetadata['maputnik:mapbox_access_token']
|
||||
delete changedMetadata['maputnik:openmaptiles_access_token']
|
||||
return {
|
||||
...mapStyle,
|
||||
metadata: changedMetadata
|
||||
}
|
||||
}
|
||||
|
||||
class ExportModal extends React.Component {
|
||||
static propTypes = {
|
||||
mapStyle: PropTypes.object.isRequired,
|
||||
onStyleChanged: PropTypes.func.isRequired,
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onOpenToggle: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
downloadStyle() {
|
||||
const tokenStyle = styleSpec.format(stripAccessTokens(style.replaceAccessTokens(this.props.mapStyle)));
|
||||
|
||||
const blob = new Blob([tokenStyle], {type: "application/json;charset=utf-8"});
|
||||
saveAs(blob, this.props.mapStyle.id + ".json");
|
||||
}
|
||||
|
||||
changeMetadataProperty(property, value) {
|
||||
const changedStyle = {
|
||||
...this.props.mapStyle,
|
||||
metadata: {
|
||||
...this.props.mapStyle.metadata,
|
||||
[property]: value
|
||||
}
|
||||
}
|
||||
this.props.onStyleChanged(changedStyle)
|
||||
}
|
||||
|
||||
|
||||
render() {
|
||||
return <Modal
|
||||
data-wd-key="export-modal"
|
||||
isOpen={this.props.isOpen}
|
||||
onOpenToggle={this.props.onOpenToggle}
|
||||
title={'Export Style'}
|
||||
>
|
||||
|
||||
<div className="maputnik-modal-section">
|
||||
<h4>Download Style</h4>
|
||||
<p>
|
||||
Download a JSON style to your computer.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<InputBlock label={"OpenMapTiles Access Token: "}>
|
||||
<StringInput
|
||||
value={(this.props.mapStyle.metadata || {})['maputnik:openmaptiles_access_token']}
|
||||
onChange={this.changeMetadataProperty.bind(this, "maputnik:openmaptiles_access_token")}
|
||||
/>
|
||||
</InputBlock>
|
||||
<InputBlock label={"Mapbox Access Token: "}>
|
||||
<StringInput
|
||||
value={(this.props.mapStyle.metadata || {})['maputnik:mapbox_access_token']}
|
||||
onChange={this.changeMetadataProperty.bind(this, "maputnik:mapbox_access_token")}
|
||||
/>
|
||||
</InputBlock>
|
||||
<InputBlock label={"Thunderforest Access Token: "}>
|
||||
<StringInput
|
||||
value={(this.props.mapStyle.metadata || {})['maputnik:thunderforest_access_token']}
|
||||
onChange={this.changeMetadataProperty.bind(this, "maputnik:thunderforest_access_token")}
|
||||
/>
|
||||
</InputBlock>
|
||||
</p>
|
||||
|
||||
<Button onClick={this.downloadStyle.bind(this)}>
|
||||
<MdFileDownload />
|
||||
Download
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="maputnik-modal-section hide">
|
||||
<h4>Save style</h4>
|
||||
<Gist mapStyle={this.props.mapStyle} onStyleChanged={this.props.onStyleChanged}/>
|
||||
</div>
|
||||
</Modal>
|
||||
}
|
||||
}
|
||||
|
||||
export default ExportModal
|
||||
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,48 +1,61 @@
|
||||
import React from 'react'
|
||||
|
||||
import PropTypes from 'prop-types'
|
||||
import CloseIcon from 'react-icons/lib/md/close'
|
||||
import AriaModal from 'react-aria-modal'
|
||||
|
||||
import Overlay from './Overlay'
|
||||
import colors from '../../config/colors'
|
||||
import { margins, fontSizes } from '../../config/scales'
|
||||
|
||||
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 style={{
|
||||
minWidth: 350,
|
||||
maxWidth: 600,
|
||||
backgroundColor: colors.black,
|
||||
boxShadow: '0px 0px 5px 0px rgba(0,0,0,0.3)',
|
||||
}}>
|
||||
<div style={{
|
||||
backgroundColor: colors.gray,
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
padding: margins[2],
|
||||
fontSize: fontSizes[4],
|
||||
}}>
|
||||
{this.props.title}
|
||||
<span style={{flexGrow: 1}} />
|
||||
<a
|
||||
onClick={() => this.props.onOpenToggle(false)}
|
||||
style={{ cursor: 'pointer' }} >
|
||||
<CloseIcon />
|
||||
</a>
|
||||
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>
|
||||
<button className="maputnik-modal-header-toggle"
|
||||
onClick={() => this.props.onOpenToggle(false)}
|
||||
data-wd-key={this.props["data-wd-key"]+".close-modal"}
|
||||
>
|
||||
<CloseIcon />
|
||||
</button>
|
||||
</header>
|
||||
<div className="maputnik-modal-scroller">
|
||||
<div className="maputnik-modal-content">{this.props.children}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{
|
||||
padding: margins[2],
|
||||
}}>
|
||||
{this.props.children}
|
||||
</div>
|
||||
</div>
|
||||
</Overlay>
|
||||
</AriaModal>
|
||||
}
|
||||
else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import LoadingModal from './LoadingModal'
|
||||
import Modal from './Modal'
|
||||
import Heading from '../Heading'
|
||||
import Button from '../Button'
|
||||
import Paragraph from '../Paragraph'
|
||||
import FileReaderInput from 'react-file-reader-input'
|
||||
import request from 'request'
|
||||
|
||||
@@ -10,54 +10,34 @@ import FileUploadIcon from 'react-icons/lib/md/file-upload'
|
||||
import AddIcon from 'react-icons/lib/md/add-circle-outline'
|
||||
|
||||
import style from '../../libs/style.js'
|
||||
import colors from '../../config/colors'
|
||||
import { margins, fontSizes } from '../../config/scales'
|
||||
import publicStyles from '../../config/styles.json'
|
||||
|
||||
class PublicStyle extends React.Component {
|
||||
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 style={{
|
||||
verticalAlign: 'top',
|
||||
marginTop: margins[2],
|
||||
marginRight: margins[2],
|
||||
backgroundColor: colors.gray,
|
||||
display: 'inline-block',
|
||||
width: 180,
|
||||
fontSize: fontSizes[4],
|
||||
color: colors.lowgray,
|
||||
}}>
|
||||
return <div className="maputnik-public-style">
|
||||
<Button
|
||||
className="maputnik-public-style-button"
|
||||
aria-label={this.props.title}
|
||||
onClick={() => this.props.onSelect(this.props.url)}
|
||||
style={{
|
||||
backgroundColor: 'transparent',
|
||||
padding: margins[2],
|
||||
display: 'block',
|
||||
}}
|
||||
>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
}}>
|
||||
<span style={{fontWeight: 700}}>{this.props.title}</span>
|
||||
<span style={{flexGrow: 1}} />
|
||||
<header className="maputnik-public-style-header">
|
||||
<h4>{this.props.title}</h4>
|
||||
<span className="maputnik-space" />
|
||||
<AddIcon />
|
||||
</div>
|
||||
<img
|
||||
</header>
|
||||
<div
|
||||
className="maputnik-public-style-thumbnail"
|
||||
style={{
|
||||
display: 'block',
|
||||
marginTop: margins[1],
|
||||
maxWidth: '100%',
|
||||
backgroundImage: `url(${this.props.thumbnailUrl})`
|
||||
}}
|
||||
src={this.props.thumbnailUrl}
|
||||
alt={this.props.title}
|
||||
/>
|
||||
></div>
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
@@ -65,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.ensureMetadataExists(JSON.parse(body))
|
||||
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)
|
||||
mapStyle = style.ensureMetadataExists(mapStyle)
|
||||
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
|
||||
@@ -108,27 +150,58 @@ 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'}
|
||||
>
|
||||
<Heading level={4}>Upload Style</Heading>
|
||||
<Paragraph>
|
||||
Upload a JSON style from your computer.
|
||||
</Paragraph>
|
||||
<FileReaderInput onChange={this.onUpload.bind(this)}>
|
||||
<Button>
|
||||
<FileUploadIcon />
|
||||
Upload
|
||||
</Button>
|
||||
</FileReaderInput>
|
||||
{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)} tabIndex="-1">
|
||||
<Button className="maputnik-upload-button"><FileUploadIcon /> Upload</Button>
|
||||
</FileReaderInput>
|
||||
</section>
|
||||
|
||||
<Heading level={4}>Gallery Styles</Heading>
|
||||
<Paragraph>
|
||||
Open one of the publicly available styles to start from.
|
||||
</Paragraph>
|
||||
{styleOptions}
|
||||
<section className="maputnik-modal-section">
|
||||
<h2>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.
|
||||
</p>
|
||||
<div className="maputnik-style-gallery-container">
|
||||
{styleOptions}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<LoadingModal
|
||||
isOpen={!!this.state.activeRequest}
|
||||
title={'Loading style'}
|
||||
onCancel={(e) => this.onCancelActiveRequest(e)}
|
||||
message={"Loading: "+this.state.activeRequestUrl}
|
||||
/>
|
||||
</Modal>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
import React from 'react'
|
||||
|
||||
class ViewportOverlay extends React.Component {
|
||||
static propTypes = {
|
||||
style: React.PropTypes.object
|
||||
}
|
||||
|
||||
render() {
|
||||
const overlayStyle = {
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
zIndex: 2,
|
||||
opacity: 0.875,
|
||||
backgroundColor: 'rgb(28, 31, 36)',
|
||||
...this.props.style
|
||||
}
|
||||
|
||||
return <div style={overlayStyle} />
|
||||
}
|
||||
}
|
||||
|
||||
class Overlay extends React.Component {
|
||||
static propTypes = {
|
||||
isOpen: React.PropTypes.bool.isRequired,
|
||||
children: React.PropTypes.element.isRequired
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div style={{
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
position: 'fixed',
|
||||
display: this.props.isOpen ? 'flex' : 'none',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}>
|
||||
<ViewportOverlay />
|
||||
<div style={{
|
||||
zIndex: 3,
|
||||
}}>
|
||||
{this.props.children}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
export default Overlay
|
||||
@@ -1,83 +1,119 @@
|
||||
import React from 'react'
|
||||
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 SelectInput from '../inputs/SelectInput'
|
||||
import Modal from './Modal'
|
||||
import colors from '../../config/colors'
|
||||
|
||||
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) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
onChange(property, e) {
|
||||
const changedStyle = this.props.mapStyle.set(property, e.target.value)
|
||||
changeStyleProperty(property, value) {
|
||||
const changedStyle = {
|
||||
...this.props.mapStyle,
|
||||
[property]: value
|
||||
}
|
||||
this.props.onStyleChanged(changedStyle)
|
||||
}
|
||||
|
||||
onRendererChange(renderer) {
|
||||
changeMetadataProperty(property, value) {
|
||||
const changedStyle = {
|
||||
...this.props.mapStyle,
|
||||
metadata: {
|
||||
...this.props.mapStyle.metadata,
|
||||
'maputnik:renderer': renderer,
|
||||
[property]: value
|
||||
}
|
||||
}
|
||||
this.props.onStyleChanged(changedStyle)
|
||||
}
|
||||
|
||||
render() {
|
||||
const metadata = this.props.mapStyle.metadata || {}
|
||||
const inputProps = { }
|
||||
return <Modal
|
||||
data-wd-key="modal-settings"
|
||||
isOpen={this.props.isOpen}
|
||||
onOpenToggle={this.props.onOpenToggle}
|
||||
title={'StyleSettings'}
|
||||
title={'Style Settings'}
|
||||
>
|
||||
<InputBlock label={"Name"}>
|
||||
<div style={{minWidth: 350}}>
|
||||
<InputBlock label={"Name"} doc={styleSpec.latest.$root.name.doc}>
|
||||
<StringInput {...inputProps}
|
||||
data-wd-key="modal-settings.name"
|
||||
value={this.props.mapStyle.name}
|
||||
onChange={this.onChange.bind(this, "name")}
|
||||
onChange={this.changeStyleProperty.bind(this, "name")}
|
||||
/>
|
||||
</InputBlock>
|
||||
<InputBlock label={"Owner"}>
|
||||
<InputBlock label={"Owner"} doc={"Owner ID of the style. Used by Mapbox or future style APIs."}>
|
||||
<StringInput {...inputProps}
|
||||
data-wd-key="modal-settings.owner"
|
||||
value={this.props.mapStyle.owner}
|
||||
onChange={this.onChange.bind(this, "owner")}
|
||||
onChange={this.changeStyleProperty.bind(this, "owner")}
|
||||
/>
|
||||
</InputBlock>
|
||||
<InputBlock label={"Sprite URL"}>
|
||||
<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.onChange.bind(this, "sprite")}
|
||||
onChange={this.changeStyleProperty.bind(this, "sprite")}
|
||||
/>
|
||||
</InputBlock>
|
||||
|
||||
<InputBlock label={"Glyphs URL"}>
|
||||
<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.onChange.bind(this, "glyphs")}
|
||||
onChange={this.changeStyleProperty.bind(this, "glyphs")}
|
||||
/>
|
||||
</InputBlock>
|
||||
|
||||
<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")}
|
||||
/>
|
||||
</InputBlock>
|
||||
|
||||
<InputBlock label={"Style Renderer"}>
|
||||
<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")}
|
||||
/>
|
||||
</InputBlock>
|
||||
|
||||
<InputBlock label={"Thunderforest Access Token"} doc={"Public access token for Thunderforest services."}>
|
||||
<StringInput {...inputProps}
|
||||
data-wd-key="modal-settings.maputnik:thunderforest_access_token"
|
||||
value={metadata['maputnik:thunderforest_access_token']}
|
||||
onChange={this.changeMetadataProperty.bind(this, "maputnik:thunderforest_access_token")}
|
||||
/>
|
||||
</InputBlock>
|
||||
|
||||
<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']
|
||||
['ol3', 'Open Layers 3'],
|
||||
]}
|
||||
value={(this.props.mapStyle.metadata || {})['maputnik:renderer'] || 'mbgljs'}
|
||||
onChange={this.onRendererChange.bind(this)}
|
||||
value={metadata['maputnik:renderer'] || 'mbgljs'}
|
||||
onChange={this.changeMetadataProperty.bind(this, 'maputnik:renderer')}
|
||||
/>
|
||||
</InputBlock>
|
||||
</div>
|
||||
</Modal>
|
||||
}
|
||||
}
|
||||
|
||||
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,106 +1,87 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import * as styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
|
||||
import Modal from './Modal'
|
||||
import Heading from '../Heading'
|
||||
import Button from '../Button'
|
||||
import Paragraph from '../Paragraph'
|
||||
import InputBlock from '../inputs/InputBlock'
|
||||
import StringInput from '../inputs/StringInput'
|
||||
import SelectInput from '../inputs/SelectInput'
|
||||
import SourceTypeEditor from '../sources/SourceTypeEditor'
|
||||
|
||||
import style from '../../libs/style'
|
||||
import { deleteSource, addSource, changeSource } from '../../libs/source'
|
||||
import publicSources from '../../config/tilesets.json'
|
||||
import colors from '../../config/colors'
|
||||
import { margins, fontSizes } from '../../config/scales'
|
||||
|
||||
import AddIcon from 'react-icons/lib/md/add-circle-outline'
|
||||
import DeleteIcon from 'react-icons/lib/md/delete'
|
||||
|
||||
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() {
|
||||
return <div style={{
|
||||
verticalAlign: 'top',
|
||||
marginTop: margins[2],
|
||||
marginRight: margins[2],
|
||||
backgroundColor: colors.gray,
|
||||
display: 'inline-block',
|
||||
width: 240,
|
||||
fontSize: fontSizes[4],
|
||||
color: colors.lowgray,
|
||||
}}>
|
||||
<Button
|
||||
onClick={() => this.props.onSelect(this.props.id)}
|
||||
style={{
|
||||
backgroundColor: 'transparent',
|
||||
padding: margins[2],
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<span style={{fontWeight: 700}}>{this.props.title}</span>
|
||||
<br/>
|
||||
<span style={{fontSize: fontSizes[5]}}>#{this.props.id}</span>
|
||||
</div>
|
||||
<span style={{flexGrow: 1}} />
|
||||
<AddIcon />
|
||||
</Button>
|
||||
return <div className="maputnik-public-source">
|
||||
<Button
|
||||
className="maputnik-public-source-select"
|
||||
onClick={() => this.props.onSelect(this.props.id)}
|
||||
>
|
||||
<div className="maputnik-public-source-info">
|
||||
<p className="maputnik-public-source-name">{this.props.title}</p>
|
||||
<p className="maputnik-public-source-id">#{this.props.id}</p>
|
||||
</div>
|
||||
<span className="maputnik-space" />
|
||||
<AddIcon />
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
function editorMode(source) {
|
||||
if(source.type === 'geojson') return ' geojson'
|
||||
if(source.type === 'vector' && source.tiles) {
|
||||
return 'tilexyz'
|
||||
if(source.type === 'raster') {
|
||||
if(source.tiles) return 'tilexyz_raster'
|
||||
return 'tilejson_raster'
|
||||
}
|
||||
return 'tilejson'
|
||||
if(source.type === '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'
|
||||
}
|
||||
if(source.type === 'geojson') return 'geojson'
|
||||
return null
|
||||
}
|
||||
|
||||
class SourceEditorLayout extends React.Component {
|
||||
class ActiveSourceTypeEditor extends React.Component {
|
||||
static propTypes = {
|
||||
sourceId: React.PropTypes.string.isRequired,
|
||||
source: React.PropTypes.object.isRequired,
|
||||
onSourceDelete: React.PropTypes.func.isRequired,
|
||||
onSourceChange: React.PropTypes.func.isRequired,
|
||||
sourceId: PropTypes.string.isRequired,
|
||||
source: PropTypes.object.isRequired,
|
||||
onDelete: PropTypes.func.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
render() {
|
||||
const inputProps = { }
|
||||
return <div style={{
|
||||
}}>
|
||||
<div style={{
|
||||
backgroundColor: colors.gray,
|
||||
color: colors.lowgray,
|
||||
padding: margins[1],
|
||||
display: 'flex',
|
||||
fontSize: fontSizes[4],
|
||||
flexDirection: 'row',
|
||||
}}>
|
||||
<span style={{fontWeight: 700, fontSize: fontSizes[4], lineHeight: 2}}>#{this.props.sourceId}</span>
|
||||
<span style={{flexGrow: 1}} />
|
||||
return <div className="maputnik-active-source-type-editor">
|
||||
<div className="maputnik-active-source-type-editor-header">
|
||||
<span className="maputnik-active-source-type-editor-header-id">#{this.props.sourceId}</span>
|
||||
<span className="maputnik-space" />
|
||||
<Button
|
||||
onClick={this.props.onSourceDelete}
|
||||
className="maputnik-active-source-type-editor-header-delete"
|
||||
onClick={()=> this.props.onDelete(this.props.sourceId)}
|
||||
style={{backgroundColor: 'transparent'}}
|
||||
>
|
||||
<DeleteIcon />
|
||||
</Button>
|
||||
</div>
|
||||
<div style={{
|
||||
borderColor: colors.gray,
|
||||
borderWidth: 2,
|
||||
borderStyle: 'solid',
|
||||
padding: margins[1],
|
||||
}}>
|
||||
<div className="maputnik-active-source-type-editor-content">
|
||||
<SourceTypeEditor
|
||||
onChange={this.props.onSourceChange}
|
||||
onChange={this.props.onChange}
|
||||
mode={editorMode(this.props.source)}
|
||||
source={this.props.source}
|
||||
/>
|
||||
@@ -111,59 +92,90 @@ class SourceEditorLayout extends React.Component {
|
||||
|
||||
class AddSource extends React.Component {
|
||||
static propTypes = {
|
||||
onSourceAdd: React.PropTypes.func.isRequired,
|
||||
onAdd: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
mode: 'tilejson',
|
||||
source: {
|
||||
id: style.generateId(),
|
||||
}
|
||||
mode: 'tilejson_vector',
|
||||
sourceId: style.generateId(),
|
||||
source: this.defaultSource('tilejson_vector'),
|
||||
}
|
||||
}
|
||||
|
||||
onSourceIdChange(newId) {
|
||||
this.setState({
|
||||
source: {
|
||||
...this.state.source,
|
||||
id: newId,
|
||||
defaultSource(mode) {
|
||||
const source = (this.state || {}).source || {}
|
||||
switch(mode) {
|
||||
case 'geojson': return {
|
||||
type: 'geojson',
|
||||
data: source.data || 'http://localhost:3000/geojson.json'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
onSourceChange(source) {
|
||||
this.setState({
|
||||
source: source
|
||||
})
|
||||
case 'tilejson_vector': return {
|
||||
type: 'vector',
|
||||
url: source.url || 'http://localhost:3000/tilejson.json'
|
||||
}
|
||||
case 'tilexyz_vector': return {
|
||||
type: 'vector',
|
||||
tiles: source.tiles || ['http://localhost:3000/{x}/{y}/{z}.pbf'],
|
||||
minZoom: source.minzoom || 0,
|
||||
maxZoom: source.maxzoom || 14
|
||||
}
|
||||
case 'tilejson_raster': return {
|
||||
type: 'raster',
|
||||
url: source.url || 'http://localhost:3000/tilejson.json'
|
||||
}
|
||||
case 'tilexyz_raster': return {
|
||||
type: 'raster',
|
||||
tiles: source.tiles || ['http://localhost:3000/{x}/{y}/{z}.pbf'],
|
||||
minzoom: source.minzoom || 0,
|
||||
maxzoom: source.maxzoom || 14
|
||||
}
|
||||
case 'tilejson_raster-dem': return {
|
||||
type: 'raster-dem',
|
||||
url: source.url || 'http://localhost:3000/tilejson.json'
|
||||
}
|
||||
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 {}
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div>
|
||||
<InputBlock label={"Source ID"}>
|
||||
return <div className="maputnik-add-source">
|
||||
<InputBlock label={"Source ID"} doc={"Unique ID that identifies the source and is used in the layer to reference the source."}>
|
||||
<StringInput
|
||||
value={this.state.source.id}
|
||||
onChange={this.onSourceIdChange.bind(this)}
|
||||
value={this.state.sourceId}
|
||||
onChange={v => this.setState({ sourceId: v})}
|
||||
/>
|
||||
</InputBlock>
|
||||
<InputBlock label={"Source Type"}>
|
||||
<InputBlock label={"Source Type"} doc={styleSpec.latest.source_vector.type.doc}>
|
||||
<SelectInput
|
||||
options={[
|
||||
['geojson', 'GeoJSON'],
|
||||
['tilejson', 'Vector (TileJSON URL)'],
|
||||
['tilexyz', 'Vector (XYZ URLs)'],
|
||||
['tilejson_vector', 'Vector (TileJSON URL)'],
|
||||
['tilexyz_vector', 'Vector (XYZ URLs)'],
|
||||
['tilejson_raster', 'Raster (TileJSON URL)'],
|
||||
['tilexyz_raster', 'Raster (XYZ URL)'],
|
||||
['tilejson_raster-dem', 'Raster DEM (TileJSON URL)'],
|
||||
['tilexyz_raster-dem', 'Raster DEM (XYZ URLs)'],
|
||||
]}
|
||||
onChange={v => this.setState({mode: v})}
|
||||
onChange={mode => this.setState({mode: mode, source: this.defaultSource(mode)})}
|
||||
value={this.state.mode}
|
||||
/>
|
||||
</InputBlock>
|
||||
<SourceTypeEditor
|
||||
onChange={this.onSourceChange.bind(this)}
|
||||
onChange={src => this.setState({ source: src })}
|
||||
mode={this.state.mode}
|
||||
source={this.state.source}
|
||||
/>
|
||||
<Button onClick={() => this.onSourceAdd(this.state.source)}>
|
||||
<Button
|
||||
className="maputnik-add-source-button"
|
||||
onClick={() => this.props.onAdd(this.state.sourceId, this.state.source)}>
|
||||
Add Source
|
||||
</Button>
|
||||
</div>
|
||||
@@ -172,44 +184,39 @@ 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,
|
||||
}
|
||||
|
||||
onSourceAdd(source) {
|
||||
const changedSources = {
|
||||
...this.props.mapStyle.sources,
|
||||
[source.id]: source
|
||||
}
|
||||
|
||||
const changedStyle = {
|
||||
...this.props.mapStyle,
|
||||
sources: changedSources
|
||||
}
|
||||
|
||||
this.props.onStyleChanged(changedStyle)
|
||||
stripTitle(source) {
|
||||
const strippedSource = {...source}
|
||||
delete strippedSource['title']
|
||||
return strippedSource
|
||||
}
|
||||
|
||||
render() {
|
||||
const activeSources = Object.keys(this.props.mapStyle.sources).map(sourceId => {
|
||||
const source = this.props.mapStyle.sources[sourceId]
|
||||
return <SourceEditorLayout
|
||||
const mapStyle = this.props.mapStyle
|
||||
const activeSources = Object.keys(mapStyle.sources).map(sourceId => {
|
||||
const source = mapStyle.sources[sourceId]
|
||||
return <ActiveSourceTypeEditor
|
||||
key={sourceId}
|
||||
sourceId={sourceId}
|
||||
source={source}
|
||||
onChange={src => this.props.onStyleChanged(changeSource(mapStyle, sourceId, src))}
|
||||
onDelete={() => this.props.onStyleChanged(deleteSource(mapStyle, sourceId))}
|
||||
/>
|
||||
})
|
||||
|
||||
const tilesetOptions = publicSources.filter(source => !(source.id in this.props.mapStyle.sources)).map(source => {
|
||||
const tilesetOptions = Object.keys(publicSources).filter(sourceId => !(sourceId in mapStyle.sources)).map(sourceId => {
|
||||
const source = publicSources[sourceId]
|
||||
return <PublicSource
|
||||
key={source.id}
|
||||
id={source.id}
|
||||
key={sourceId}
|
||||
id={sourceId}
|
||||
type={source.type}
|
||||
title={source.title}
|
||||
description={source.description}
|
||||
onSelect={() => this.onSourceAdd(source)}
|
||||
onSelect={() => this.props.onStyleChanged(addSource(mapStyle, sourceId, this.stripTitle(source)))}
|
||||
/>
|
||||
})
|
||||
|
||||
@@ -219,21 +226,30 @@ class SourcesModal extends React.Component {
|
||||
onOpenToggle={this.props.onOpenToggle}
|
||||
title={'Sources'}
|
||||
>
|
||||
<Heading level={4}>Active Sources</Heading>
|
||||
{activeSources}
|
||||
|
||||
<Heading level={4}>Add New Source</Heading>
|
||||
<div style={{maxWidth: 300}}>
|
||||
<p style={{color: colors.lowgray, fontSize: fontSizes[5]}}>Add a new source to your style. You can only choose the source type and id at creation time!</p>
|
||||
<AddSource onSourceAdd={this.onSourceAdd.bind(this)} />
|
||||
<div className="maputnik-modal-section">
|
||||
<h4>Active Sources</h4>
|
||||
{activeSources}
|
||||
</div>
|
||||
|
||||
<Heading level={4}>Choose Public Source</Heading>
|
||||
<Paragraph>
|
||||
Add one of the publicly availble sources to your style.
|
||||
</Paragraph>
|
||||
<div style={{maxwidth: 500}}>
|
||||
{tilesetOptions}
|
||||
<div className="maputnik-modal-section">
|
||||
<h4>Choose Public Source</h4>
|
||||
<p>
|
||||
Add one of the publicly available sources to your style.
|
||||
</p>
|
||||
<div className="maputnik-public-sources" style={{maxwidth: 500}}>
|
||||
{tilesetOptions}
|
||||
</div>
|
||||
<p>
|
||||
<strong>Note:</strong> Some of the tilesets are not optimised for online use, and as a result the file sizes of the tiles can be quite large (heavy) for online vector rendering. Please review any tilesets before use.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="maputnik-modal-section">
|
||||
<h4>Add New Source</h4>
|
||||
<p>Add a new source to your style. You can only choose the source type and id at creation time!</p>
|
||||
<AddSource
|
||||
onAdd={(sourceId, source) => this.props.onStyleChanged(addSource(mapStyle, sourceId, source))}
|
||||
/>
|
||||
</div>
|
||||
</Modal>
|
||||
}
|
||||
|
||||
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,12 +0,0 @@
|
||||
.darkScrollbar::-webkit-scrollbar {
|
||||
background-color: #26282e;
|
||||
width: 5px;
|
||||
}
|
||||
|
||||
.darkScrollbar::-webkit-scrollbar-thumb {
|
||||
border-radius: 6px;
|
||||
-webkit-box-shadow: inset 0 0 6px rgba(0,0,0,.3);
|
||||
background-color: #40444e;
|
||||
padding-left: 2px;
|
||||
padding-right: 2px;
|
||||
}
|
||||
@@ -1,56 +1,86 @@
|
||||
import React from 'react'
|
||||
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 = {
|
||||
url: React.PropTypes.string.isRequired,
|
||||
onChange: React.PropTypes.func,
|
||||
source: PropTypes.object.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
children: PropTypes.node,
|
||||
}
|
||||
|
||||
render() {
|
||||
return <InputBlock label={"TileJSON URL"}>
|
||||
<StringInput
|
||||
value={this.props.url}
|
||||
onChange={this.props.onChange}
|
||||
/>
|
||||
</InputBlock>
|
||||
return <div>
|
||||
<InputBlock label={"TileJSON URL"} doc={styleSpec.latest.source_vector.url.doc}>
|
||||
<StringInput
|
||||
value={this.props.source.url}
|
||||
onChange={url => this.props.onChange({
|
||||
...this.props.source,
|
||||
url: url
|
||||
})}
|
||||
/>
|
||||
</InputBlock>
|
||||
{this.props.children}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
class TileURLSourceEditor extends React.Component {
|
||||
static propTypes = {
|
||||
tiles: React.PropTypes.array.isRequired,
|
||||
minZoom: React.PropTypes.number.isRequired,
|
||||
maxZoom: React.PropTypes.number.isRequired,
|
||||
onChange: React.PropTypes.func,
|
||||
source: PropTypes.object.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
children: PropTypes.node,
|
||||
}
|
||||
|
||||
changeTileUrl(idx, value) {
|
||||
const tiles = this.props.source.tiles.slice(0)
|
||||
tiles[idx] = value
|
||||
this.props.onChange({
|
||||
...this.props.source,
|
||||
tiles: tiles
|
||||
})
|
||||
}
|
||||
|
||||
renderTileUrls() {
|
||||
const prefix = ['1st', '2nd', '3rd', '4th', '5th', '6th', '7th']
|
||||
return this.props.tiles.map((tileUrl, tileIndex) => {
|
||||
return <InputBlock key={tileIndex} label={prefix[tileIndex] + " Tile URL"}>
|
||||
const tiles = this.props.source.tiles || []
|
||||
return tiles.map((tileUrl, tileIndex) => {
|
||||
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)}
|
||||
/>
|
||||
</InputBlock>
|
||||
})
|
||||
}
|
||||
|
||||
render() {
|
||||
console.log(this.props.tiles)
|
||||
return <div>
|
||||
{this.renderTileUrls()}
|
||||
<InputBlock label={"Min Zoom"}>
|
||||
<StringInput
|
||||
value={this.props.minZoom}
|
||||
<InputBlock label={"Min Zoom"} doc={styleSpec.latest.source_vector.minzoom.doc}>
|
||||
<NumberInput
|
||||
value={this.props.source.minzoom || 0}
|
||||
onChange={minzoom => this.props.onChange({
|
||||
...this.props.source,
|
||||
minzoom: minzoom
|
||||
})}
|
||||
/>
|
||||
</InputBlock>
|
||||
<InputBlock label={"Max Zoom"}>
|
||||
<StringInput
|
||||
value={this.props.maxZoom}
|
||||
<InputBlock label={"Max Zoom"} doc={styleSpec.latest.source_vector.maxzoom.doc}>
|
||||
<NumberInput
|
||||
value={this.props.source.maxzoom || 22}
|
||||
onChange={maxzoom => this.props.onChange({
|
||||
...this.props.source,
|
||||
maxzoom: maxzoom
|
||||
})}
|
||||
/>
|
||||
</InputBlock>
|
||||
{this.props.children}
|
||||
</div>
|
||||
|
||||
}
|
||||
@@ -58,15 +88,18 @@ class TileURLSourceEditor extends React.Component {
|
||||
|
||||
class GeoJSONSourceEditor extends React.Component {
|
||||
static propTypes = {
|
||||
data: React.PropTypes.string.isRequired,
|
||||
onChange: React.PropTypes.func,
|
||||
source: PropTypes.object.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
render() {
|
||||
return <InputBlock label={"GeoJSON Data"}>
|
||||
return <InputBlock label={"GeoJSON Data"} doc={styleSpec.latest.source_geojson.data.doc}>
|
||||
<StringInput
|
||||
value={this.props.data}
|
||||
onChange={this.props.onChange}
|
||||
value={this.props.source.data}
|
||||
onChange={data => this.props.onChange({
|
||||
...this.props.source,
|
||||
data: data
|
||||
})}
|
||||
/>
|
||||
</InputBlock>
|
||||
}
|
||||
@@ -74,17 +107,35 @@ 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() {
|
||||
const source = this.props.source
|
||||
const commonProps = {
|
||||
source: this.props.source,
|
||||
onChange: this.props.onChange,
|
||||
}
|
||||
switch(this.props.mode) {
|
||||
case 'geojson': return <GeoJSONSourceEditor data={source.data || 'http://localhost:3000/mygeojson.json'} />
|
||||
case 'tilejson': return <TileJSONSourceEditor url={source.url || 'http://localhost:3000/tiles.json'}/>
|
||||
case 'tilexyz': return <TileURLSourceEditor tiles={source.tiles || ['http://localhost:3000/{x}/{y}/{z}.pbf']} minZoom={source.minZoom || 0} maxZoom={source.maxZoom || 14}/>
|
||||
case 'geojson': return <GeoJSONSourceEditor {...commonProps} />
|
||||
case 'tilejson_vector': return <TileJSONSourceEditor {...commonProps} />
|
||||
case 'tilexyz_vector': return <TileURLSourceEditor {...commonProps} />
|
||||
case 'tilejson_raster': return <TileJSONSourceEditor {...commonProps} />
|
||||
case 'tilexyz_raster': return <TileURLSourceEditor {...commonProps} />
|
||||
case 'tilejson_raster-dem': return <TileJSONSourceEditor {...commonProps} />
|
||||
case 'tilexyz_raster-dem': return <TileURLSourceEditor {...commonProps}>
|
||||
<InputBlock label={"Encoding"} doc={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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
const baseColors = {
|
||||
black: '#1c1f24',
|
||||
gray: '#26282e',
|
||||
midgray: '#36383e',
|
||||
lowgray: '#8e8e8e',
|
||||
|
||||
white: '#fff',
|
||||
blue: '#00d9f7',
|
||||
green: '#B4C7AD',
|
||||
orange: '#fb3',
|
||||
red: '#f04',
|
||||
}
|
||||
|
||||
const themeColors = {
|
||||
primary: baseColors.gray,
|
||||
secondary: baseColors.midgray,
|
||||
default: baseColors.gray,
|
||||
info: baseColors.blue,
|
||||
success: baseColors.green,
|
||||
warning: baseColors.orange,
|
||||
error: baseColors.red
|
||||
}
|
||||
|
||||
const colors = {
|
||||
...baseColors,
|
||||
...themeColors
|
||||
}
|
||||
|
||||
export default colors
|
||||
14
src/config/empty-style.json
Normal file
14
src/config/empty-style.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"version": 8,
|
||||
"name": "Empty Style",
|
||||
"metadata": {
|
||||
"mapbox:autocomposite": false,
|
||||
"mapbox:type": "template",
|
||||
"maputnik:renderer": "mbgljs",
|
||||
"openmaptiles:version": "3.x"
|
||||
},
|
||||
"sources": { },
|
||||
"glyphs": "https://demo.tileserver.org/fonts/{fontstack}/{range}.pbf",
|
||||
"sprites": "https://demo.tileserver.org/fonts/{fontstack}/{range}.pbf",
|
||||
"layers": []
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
import colors from './colors'
|
||||
import { margins, fontSizes } from './scales'
|
||||
|
||||
const base = {
|
||||
display: 'inline-block',
|
||||
fontSize: fontSizes[5],
|
||||
lineHeight: 2,
|
||||
paddingLeft: 5,
|
||||
paddingRight: 5,
|
||||
}
|
||||
|
||||
const label = {
|
||||
...base,
|
||||
width: '40%',
|
||||
color: colors.lowgray,
|
||||
userSelect: 'none',
|
||||
}
|
||||
|
||||
const property = {
|
||||
marginTop: margins[2],
|
||||
marginBottom: margins[2],
|
||||
}
|
||||
|
||||
const input = {
|
||||
...base,
|
||||
border: 'none',
|
||||
width: '47%',
|
||||
backgroundColor: colors.gray,
|
||||
color: colors.lowgray,
|
||||
}
|
||||
|
||||
const checkbox = {
|
||||
...base,
|
||||
border: '1px solid rgb(36, 36, 36)',
|
||||
backgroundColor: colors.gray,
|
||||
color: colors.lowgray,
|
||||
}
|
||||
|
||||
const select = {
|
||||
...input,
|
||||
width: '51%',
|
||||
height: '2.3em',
|
||||
}
|
||||
|
||||
export default {
|
||||
base,
|
||||
label,
|
||||
select,
|
||||
input,
|
||||
property,
|
||||
checkbox,
|
||||
}
|
||||
@@ -2,15 +2,7 @@
|
||||
"line": {
|
||||
"groups": [
|
||||
{
|
||||
"title": "Settings",
|
||||
"type": "settings"
|
||||
},
|
||||
{
|
||||
"title": "Source",
|
||||
"type": "source"
|
||||
},
|
||||
{
|
||||
"title": "Paint",
|
||||
"title": "Paint properties",
|
||||
"type": "properties",
|
||||
"fields": [
|
||||
"line-opacity",
|
||||
@@ -18,66 +10,42 @@
|
||||
"line-width",
|
||||
"line-offset",
|
||||
"line-blur",
|
||||
"line-pattern"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Secondary",
|
||||
"type": "properties",
|
||||
"fields": [
|
||||
"line-dasharray",
|
||||
"line-pattern",
|
||||
"line-translate",
|
||||
"line-translate-anchor",
|
||||
"line-cap",
|
||||
"line-join",
|
||||
"line-miter-limit",
|
||||
"line-round-limit",
|
||||
"line-dasharray",
|
||||
"line-gap-width"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "JSON",
|
||||
"type": "jsoneditor"
|
||||
"title": "Layout properties",
|
||||
"type": "properties",
|
||||
"fields": [
|
||||
"line-cap",
|
||||
"line-join",
|
||||
"line-miter-limit",
|
||||
"line-round-limit"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"background": {
|
||||
"groups": [
|
||||
{
|
||||
"title": "Settings",
|
||||
"type": "settings"
|
||||
},
|
||||
{
|
||||
"title": "Source",
|
||||
"type": "source"
|
||||
},
|
||||
{
|
||||
"title": "Basic",
|
||||
"title": "Paint properties",
|
||||
"type": "properties",
|
||||
"fields": [
|
||||
"background-color",
|
||||
"background-pattern",
|
||||
"background-opacity"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "JSON",
|
||||
"type": "jsoneditor"
|
||||
}
|
||||
]
|
||||
},
|
||||
"fill": {
|
||||
"groups": [
|
||||
{
|
||||
"title": "Settings",
|
||||
"type": "settings"
|
||||
},
|
||||
{
|
||||
"title": "Source",
|
||||
"type": "source"
|
||||
},
|
||||
{
|
||||
"title": "Basic",
|
||||
"title": "Paint properties",
|
||||
"type": "properties",
|
||||
"fields": [
|
||||
"fill-opacity",
|
||||
@@ -88,49 +56,69 @@
|
||||
"fill-translate",
|
||||
"fill-translate-anchor"
|
||||
]
|
||||
},
|
||||
}
|
||||
]
|
||||
},
|
||||
"fill-extrusion": {
|
||||
"groups": [
|
||||
{
|
||||
"title": "JSON",
|
||||
"type": "jsoneditor"
|
||||
"title": "Paint properties",
|
||||
"type": "properties",
|
||||
"fields": [
|
||||
"fill-extrusion-opacity",
|
||||
"fill-extrusion-color",
|
||||
"fill-extrusion-translate",
|
||||
"fill-extrusion-translate-anchor",
|
||||
"fill-extrusion-pattern",
|
||||
"fill-extrusion-height",
|
||||
"fill-extrusion-base"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"circle": {
|
||||
"groups": [
|
||||
{
|
||||
"title": "Paint properties",
|
||||
"type": "properties",
|
||||
"fields": [
|
||||
"circle-color",
|
||||
"circle-opacity",
|
||||
"circle-stroke-color",
|
||||
"circle-stroke-opacity",
|
||||
"circle-blur",
|
||||
"circle-radius",
|
||||
"circle-stroke-width",
|
||||
"circle-pitch-scale",
|
||||
"circle-translate",
|
||||
"circle-translate-anchor",
|
||||
"circle-pitch-alignment"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"symbol": {
|
||||
"groups": [
|
||||
{
|
||||
"title": "Settings",
|
||||
"type": "settings"
|
||||
"title": "General layout properties",
|
||||
"type": "properties",
|
||||
"fields": [
|
||||
"symbol-placement",
|
||||
"symbol-spacing",
|
||||
"symbol-avoid-edges"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Source",
|
||||
"type": "source"
|
||||
},
|
||||
{
|
||||
"title": "Basic",
|
||||
"title": "Text layout properties",
|
||||
"type": "properties",
|
||||
"fields": [
|
||||
"text-field",
|
||||
"text-font",
|
||||
"text-size",
|
||||
"text-line-height"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Placement",
|
||||
"type": "properties",
|
||||
"fields": [
|
||||
"symbol-placement",
|
||||
"symbol-spacing",
|
||||
"symbol-avoid-edges",
|
||||
"text-line-height",
|
||||
"text-padding",
|
||||
"text-allow-overlap",
|
||||
"text-ignore-placement"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Layout",
|
||||
"type": "properties",
|
||||
"fields": [
|
||||
"text-ignore-placement",
|
||||
"text-pitch-alignment",
|
||||
"text-rotation-alignment",
|
||||
"text-max-width",
|
||||
@@ -146,7 +134,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Icon",
|
||||
"title": "Icon layout properties",
|
||||
"type": "properties",
|
||||
"fields": [
|
||||
"icon-allow-overlap",
|
||||
@@ -160,12 +148,84 @@
|
||||
"icon-rotate",
|
||||
"icon-padding",
|
||||
"icon-keep-upright",
|
||||
"icon-offset"
|
||||
"icon-offset",
|
||||
"icon-anchor",
|
||||
"icon-pitch-alignment"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "JSON",
|
||||
"type": "jsoneditor"
|
||||
"title": "Text paint properties",
|
||||
"type": "properties",
|
||||
"fields": [
|
||||
"text-color",
|
||||
"text-opacity",
|
||||
"text-halo-color",
|
||||
"text-halo-width",
|
||||
"text-halo-blur",
|
||||
"text-translate",
|
||||
"text-translate-anchor"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Icon paint properties",
|
||||
"type": "properties",
|
||||
"fields": [
|
||||
"icon-color",
|
||||
"icon-opacity",
|
||||
"icon-halo-color",
|
||||
"icon-halo-width",
|
||||
"icon-halo-blur",
|
||||
"icon-translate",
|
||||
"icon-translate-anchor"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"raster": {
|
||||
"groups": [
|
||||
{
|
||||
"title": "Paint properties",
|
||||
"type": "properties",
|
||||
"fields": [
|
||||
"raster-opacity",
|
||||
"raster-hue-rotate",
|
||||
"raster-brightness-min",
|
||||
"raster-brightness-max",
|
||||
"raster-saturation",
|
||||
"raster-contrast",
|
||||
"raster-fade-duration",
|
||||
"raster-resampling"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"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"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
export const margins = [3, 5, 10, 30, 40]
|
||||
export const fontSizes = [26, 20, 16, 14, 12, 10]
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user