Compare commits
648 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
72b9e624a7 | ||
|
|
4c847faaae | ||
|
|
7089ca1b5c | ||
|
|
2cd6019cb2 | ||
|
|
775b5f4a39 | ||
|
|
93baf3d5e2 | ||
|
|
74f44a49af | ||
|
|
6310d6f11e | ||
|
|
8153050d38 | ||
|
|
917da6bc7d | ||
|
|
5f0ea7be24 | ||
|
|
8db5842e85 | ||
|
|
5a8effb363 | ||
|
|
236058e95b | ||
|
|
2da67c7963 | ||
|
|
f67704c545 | ||
|
|
83b96e8f80 | ||
|
|
59cf0fc47c | ||
|
|
bc8f9fd685 | ||
|
|
542e7777d8 | ||
|
|
12c5dd912b | ||
|
|
95dbed3f69 | ||
|
|
3acf7ccf5b | ||
|
|
6c4758c89c | ||
|
|
9bfea8f868 | ||
|
|
2d2a4739f1 | ||
|
|
fd218db356 | ||
|
|
bc2b08ed92 | ||
|
|
9af15e5359 | ||
|
|
669781ccca | ||
|
|
18da95d2a6 | ||
|
|
b47d105e1f | ||
|
|
5f9c21cf2b | ||
|
|
956d24c524 | ||
|
|
4e02c6e12d | ||
|
|
e9966e5a20 | ||
|
|
3deb491306 | ||
|
|
34572dc3f0 | ||
|
|
6bf79c2121 | ||
|
|
a925995f89 | ||
|
|
c674575fbc | ||
|
|
b030a2a707 | ||
|
|
74aa3b48db | ||
|
|
a399df0adc | ||
|
|
1282062b32 | ||
|
|
bb243db63c | ||
|
|
45c1281490 | ||
|
|
40e452d547 | ||
|
|
2e62b1802a | ||
|
|
ea4c3f4e3e | ||
|
|
19c538a29e | ||
|
|
d379d462f2 | ||
|
|
8eb9fe062f | ||
|
|
9ca274805c | ||
|
|
fc507c7e79 | ||
|
|
66453a46ca | ||
|
|
96b0c53fd2 | ||
|
|
663034b749 | ||
|
|
c82696d268 | ||
|
|
7d987cf68b | ||
|
|
075437555a | ||
|
|
654dc9c31b | ||
|
|
2c8bc5aa04 | ||
|
|
07bee66764 | ||
|
|
f675c7ff7b | ||
|
|
ad85fd8f12 | ||
|
|
634d664e46 | ||
|
|
0046122c87 | ||
|
|
c0f798a6f6 | ||
|
|
47941e3738 | ||
|
|
237457a159 | ||
|
|
d9dad5614e | ||
|
|
f18a594131 | ||
|
|
fde3d8fc18 | ||
|
|
a493d6df52 | ||
|
|
7e8eca6f97 | ||
|
|
c3764b65d9 | ||
|
|
56e151329d | ||
|
|
28d6589928 | ||
|
|
7adf516383 | ||
|
|
9a1385823e | ||
|
|
2fc5ab4509 | ||
|
|
3108b88e59 | ||
|
|
b910e4fdb6 | ||
|
|
907c09a927 | ||
|
|
16fb99d9b1 | ||
|
|
a364176a3e | ||
|
|
48164f5a9d | ||
|
|
046b1b3bb2 | ||
|
|
f33e09df62 | ||
|
|
7333eb6378 | ||
|
|
ffdc04b3aa | ||
|
|
223809dda5 | ||
|
|
744ad0f917 | ||
|
|
2f324c695b | ||
|
|
dd4cbb1b3d | ||
|
|
e61aa393c6 | ||
|
|
e9c24d5ac9 | ||
|
|
a7ed7cdb45 | ||
|
|
dea98ad7b6 | ||
|
|
987c3cd31e | ||
|
|
6d970fe73f | ||
|
|
9cfd0ced73 | ||
|
|
1464a337e3 | ||
|
|
1eaba084ed | ||
|
|
37ba7457d5 | ||
|
|
43a7c058fd | ||
|
|
87c7c7ff93 | ||
|
|
8c8241b13b | ||
|
|
c187f02c27 | ||
|
|
617adcdc48 | ||
|
|
e71c49e38c | ||
|
|
9572eefd48 | ||
|
|
3303a25737 | ||
|
|
49f91a69f1 | ||
|
|
c853c754b1 | ||
|
|
1b5596052c | ||
|
|
1c1b5cd208 | ||
|
|
c948814efc | ||
|
|
7b9d3512c6 | ||
|
|
edf3a58ea6 | ||
|
|
5a4b5fb9e9 | ||
|
|
6f9f53add6 | ||
|
|
e0199c9ce7 | ||
|
|
e1ed42f16f | ||
|
|
104c3c0c10 | ||
|
|
f4a1aa4729 | ||
|
|
fda7fac260 | ||
|
|
87cf81d1c9 | ||
|
|
8e35ed97e6 | ||
|
|
124ae98bf3 | ||
|
|
b784bf2b84 | ||
|
|
09a1f3f87b | ||
|
|
a22476cab2 | ||
|
|
a324ddb654 | ||
|
|
656264f2bc | ||
|
|
974dd7bfd9 | ||
|
|
fa182e66fa | ||
|
|
3bf0e510e6 | ||
|
|
e8d07fa694 | ||
|
|
4d1e2e6893 | ||
|
|
f219ff1e17 | ||
|
|
8eabfa5519 | ||
|
|
ad69cbdb20 | ||
|
|
17eaa3f204 | ||
|
|
1df2e36dbb | ||
|
|
0c46affda9 | ||
|
|
18f45e932b | ||
|
|
31d56c9fae | ||
|
|
b7838ad6e1 | ||
|
|
c92fd12854 | ||
|
|
eb55796461 | ||
|
|
4b97f82980 | ||
|
|
5d0b6e3201 | ||
|
|
5ab9be9fdb | ||
|
|
9659d41b83 | ||
|
|
52f949e152 | ||
|
|
e4d559f953 | ||
|
|
577663f706 | ||
|
|
393f4a38b9 | ||
|
|
3727c9ad5e | ||
|
|
a99cbc00ba | ||
|
|
fe5f7e8b8c | ||
|
|
3ed4b8f2d7 | ||
|
|
f17c2e8112 | ||
|
|
2be447f105 | ||
|
|
2fe6fa2be6 | ||
|
|
83dd21414b | ||
|
|
56d96a248d | ||
|
|
5b1ee7296b | ||
|
|
8e0546fba4 | ||
|
|
2ff3d08bb0 | ||
|
|
afe7a492a7 | ||
|
|
1f26ab707f | ||
|
|
233191e27c | ||
|
|
246f9a191d | ||
|
|
07f6efe45d | ||
|
|
ccd0402eea | ||
|
|
8ccee0ba75 | ||
|
|
d6b67be7b2 | ||
|
|
ac56ea4627 | ||
|
|
b00cf66ea6 | ||
|
|
8e329a0ff9 | ||
|
|
74cacd5bdf | ||
|
|
7d5fb23130 | ||
|
|
08bbd55f13 | ||
|
|
d6d4930513 | ||
|
|
6220e15723 | ||
|
|
72053a2dba | ||
|
|
bf27a35ef5 | ||
|
|
4705bf823a | ||
|
|
a8f6208561 | ||
|
|
af2629be75 | ||
|
|
8bfad6c9fd | ||
|
|
5c3713da90 | ||
|
|
174eae1cf4 | ||
|
|
d73add77e7 | ||
|
|
ab00c9f426 | ||
|
|
d6ab302815 | ||
|
|
f5646f57d1 | ||
|
|
c77d8f6625 | ||
|
|
e34c1ca4be | ||
|
|
87745f1fc9 | ||
|
|
9ba0fd5f39 | ||
|
|
70decbb5c1 | ||
|
|
51fa4a4377 | ||
|
|
fb6f4d73e2 | ||
|
|
63b14933ba | ||
|
|
a86c31cefa | ||
|
|
25e2554412 | ||
|
|
34bb3bc0a7 | ||
|
|
852243cd52 | ||
|
|
40faf86adf | ||
|
|
bb69f143b8 | ||
|
|
bb43200887 | ||
|
|
ae3f79f4ad | ||
|
|
731a315624 | ||
|
|
5e441454d5 | ||
|
|
a55716bbd9 | ||
|
|
44aea3745e | ||
|
|
a572bc02a6 | ||
|
|
4dee95fa2e | ||
|
|
381ff6292f | ||
|
|
c12db1703b | ||
|
|
2676583833 | ||
|
|
6ca2af7f8a | ||
|
|
553b17822d | ||
|
|
a6148e5f40 | ||
|
|
4f77629eb7 | ||
|
|
9103d9560a | ||
|
|
06c63509f7 | ||
|
|
bbe0af6c0e | ||
|
|
7455ccc3b7 | ||
|
|
8b766777ac | ||
|
|
8441abe907 | ||
|
|
ca56951256 | ||
|
|
5981151b27 | ||
|
|
21dbc6c4d9 | ||
|
|
6f060c2a0a | ||
|
|
24327541c5 | ||
|
|
0d6b9ee9d4 | ||
|
|
3ad487dce7 | ||
|
|
a46c834874 | ||
|
|
67bdea1827 | ||
|
|
cc4133aac1 | ||
|
|
4a6f58d61c | ||
|
|
e3dc98b76d | ||
|
|
09373dda44 | ||
|
|
c4b05b62b3 | ||
|
|
06bccfab10 | ||
|
|
b83c9a1ad9 | ||
|
|
0279daf7bd | ||
|
|
bfada7cace | ||
|
|
6c751fe1c4 | ||
|
|
34299c94ee | ||
|
|
5804b3c72a | ||
|
|
8ae6e9fc61 | ||
|
|
40579c3e0c | ||
|
|
f3906c8dd8 | ||
|
|
f911ed3522 | ||
|
|
2cc179acc1 | ||
|
|
2912db6e32 | ||
|
|
70eb3e785a | ||
|
|
8f944d9973 | ||
|
|
8faf841f3d | ||
|
|
d8ba8fcbfb | ||
|
|
d6f31ec82e | ||
|
|
b19eacf4f9 | ||
|
|
3d158a791a | ||
|
|
04b3b42524 | ||
|
|
af92aac7ec | ||
|
|
90dfbf37e0 | ||
|
|
e21f412933 | ||
|
|
da297fe82c | ||
|
|
624ccb5b00 | ||
|
|
9f0e5641ab | ||
|
|
d07b40ccef | ||
|
|
e0abd8251d | ||
|
|
324452e714 | ||
|
|
8d3ad6b1a1 | ||
|
|
3d4cc34a08 | ||
|
|
ff351716b6 | ||
|
|
c963a8cc59 | ||
|
|
52ad980aef | ||
|
|
fb04cce650 | ||
|
|
4b8acb10b0 | ||
|
|
86d67389fc | ||
|
|
9dad53e444 | ||
|
|
d5afeb14c1 | ||
|
|
85bb1d4d40 | ||
|
|
d95e25d185 | ||
|
|
a88f2bc0a3 | ||
|
|
5a4254d300 | ||
|
|
6bfe2aa364 | ||
|
|
0acd1fec0a | ||
|
|
3046fedb55 | ||
|
|
1574b49b01 | ||
|
|
4417a2d8f1 | ||
|
|
1f34e927e7 | ||
|
|
9af6a537ef | ||
|
|
6e07142f13 | ||
|
|
d2853f34a4 | ||
|
|
7faed0d27e | ||
|
|
22101f93ad | ||
|
|
0661899d54 | ||
|
|
862ac84464 | ||
|
|
1e4aadbb6d | ||
|
|
ce731e7d6b | ||
|
|
5448cdbe4e | ||
|
|
315a9b82c0 | ||
|
|
9e1c0e4c82 | ||
|
|
7db675e0d1 | ||
|
|
0aa629164a | ||
|
|
c2ec77e869 | ||
|
|
b28407a4a0 | ||
|
|
e3e6647e03 | ||
|
|
eb0f833d49 | ||
|
|
c5c1dd12b9 | ||
|
|
b7e414a042 | ||
|
|
81a6f31803 | ||
|
|
65cd050a18 | ||
|
|
c426dd7349 | ||
|
|
c5af645546 | ||
|
|
1bf0abfb5a | ||
|
|
18338de21a | ||
|
|
857117eb71 | ||
|
|
8d86bca8b3 | ||
|
|
dc4e6a0925 | ||
|
|
e9d6119ac6 | ||
|
|
cbdf45c852 | ||
|
|
a191c36f96 | ||
|
|
0a8d0974ca | ||
|
|
8e6c54564b | ||
|
|
4bbe2ce1ea | ||
|
|
1d48ab7ecf | ||
|
|
d85ed36e70 | ||
|
|
b554f4427b | ||
|
|
184bfeeaf8 | ||
|
|
e45f8d960d | ||
|
|
1fede3af3a | ||
|
|
5ad74048bd | ||
|
|
a0a91474de | ||
|
|
c3670701e5 | ||
|
|
86923330d9 | ||
|
|
4517148e5a | ||
|
|
0433d66f45 | ||
|
|
0c592bacab | ||
|
|
d98637cb12 | ||
|
|
1070209cb5 | ||
|
|
b6189f77c4 | ||
|
|
25322a3952 | ||
|
|
5943c6f282 | ||
|
|
090a26bb40 | ||
|
|
af03b010a4 | ||
|
|
578a920b6d | ||
|
|
0858a16ffc | ||
|
|
7cfe0563bc | ||
|
|
ee72389534 | ||
|
|
8f722c59de | ||
|
|
94d2e958eb | ||
|
|
d931c7cb38 | ||
|
|
6da83c4670 | ||
|
|
d26af16003 | ||
|
|
d75b86c927 | ||
|
|
a0cd087ccc | ||
|
|
313b639a5f | ||
|
|
93c45d5340 | ||
|
|
3be6cb5926 | ||
|
|
9d151fdc1f | ||
|
|
44d1a7a6b0 | ||
|
|
0e5676eae0 | ||
|
|
b8739915b2 | ||
|
|
a1dedd1aa6 | ||
|
|
33b4a40c35 | ||
|
|
a624909819 | ||
|
|
d5d387f349 | ||
|
|
c58ae0f895 | ||
|
|
c9e360d675 | ||
|
|
75ece350bd | ||
|
|
45680151ef | ||
|
|
87bae82b17 | ||
|
|
fcad636f85 | ||
|
|
bac8495b3c | ||
|
|
df98cb9c7b | ||
|
|
34c3015b42 | ||
|
|
7d51ea9b25 | ||
|
|
ca7bf9f4a7 | ||
|
|
61ba399e1c | ||
|
|
b5c09a4f17 | ||
|
|
fcfc7ab874 | ||
|
|
a0bc4744a2 | ||
|
|
e6e4c928f3 | ||
|
|
00388e03b8 | ||
|
|
6f83839a4c | ||
|
|
74b47e7e74 | ||
|
|
f70d078ec6 | ||
|
|
1d8131fb85 | ||
|
|
8c82db9162 | ||
|
|
f23f60807a | ||
|
|
8f581956e8 | ||
|
|
87fb0f6a5c | ||
|
|
1c953bc296 | ||
|
|
ce976991d4 | ||
|
|
be7642976b | ||
|
|
a5b226d9f3 | ||
|
|
1b3d8b5b79 | ||
|
|
97a61afc24 | ||
|
|
d1f6bc95db | ||
|
|
10b03c4e00 | ||
|
|
449d8e7665 | ||
|
|
4b8800e8ac | ||
|
|
874c6460f6 | ||
|
|
55cb86f721 | ||
|
|
a30017fd2c | ||
|
|
3b5ba6c59e | ||
|
|
a693f6db4e | ||
|
|
5be7e0c7ec | ||
|
|
7c6b3c0d80 | ||
|
|
e5e03be382 | ||
|
|
0d35106cc8 | ||
|
|
5710edcff7 | ||
|
|
2cc7c63bb1 | ||
|
|
ba9d21c045 | ||
|
|
4ef6ecb7eb | ||
|
|
52e8b21b3d | ||
|
|
c6ba4f66e2 | ||
|
|
5a47a96f09 | ||
|
|
ae878f6000 | ||
|
|
aebfe62a8e | ||
|
|
6be3543616 | ||
|
|
0f6708d9d4 | ||
|
|
0705522a24 | ||
|
|
35098111ac | ||
|
|
39333953d7 | ||
|
|
adea3d0f13 | ||
|
|
d1cb2690fc | ||
|
|
3ffdcc9639 | ||
|
|
793b5d15ad | ||
|
|
cff32696cc | ||
|
|
029eff9317 | ||
|
|
b7d08dfaa6 | ||
|
|
94089836bf | ||
|
|
ff8a8fb749 | ||
|
|
1300951a29 | ||
|
|
3cb1ed9403 | ||
|
|
a5ac1cc93d | ||
|
|
29a0ef0d1c | ||
|
|
26907f7014 | ||
|
|
3ac06c7cb1 | ||
|
|
f268f09ca2 | ||
|
|
f4c18fd91b | ||
|
|
0567b098ec | ||
|
|
dc6006fd6d | ||
|
|
109261ba00 | ||
|
|
b539644b2b | ||
|
|
be36eec93d | ||
|
|
fe5066a2a4 | ||
|
|
642e5c0b29 | ||
|
|
97bdc93a39 | ||
|
|
c770b440c2 | ||
|
|
7559985a2e | ||
|
|
532bbecb47 | ||
|
|
8ed67e98ce | ||
|
|
5792c632f9 | ||
|
|
3e2927e6a4 | ||
|
|
f09cc25a3b | ||
|
|
c5c3e93aff | ||
|
|
cc371d6a70 | ||
|
|
1b17e8fa0a | ||
|
|
bc4706de83 | ||
|
|
0f22eb83d3 | ||
|
|
a8cbe19f09 | ||
|
|
c714e23d79 | ||
|
|
5b3d579f87 | ||
|
|
725b752e35 | ||
|
|
223721a65d | ||
|
|
9b4d924dff | ||
|
|
b31537e063 | ||
|
|
63ed8c1de3 | ||
|
|
7aa0298f7c | ||
|
|
62f3cbe8fb | ||
|
|
30facc885f | ||
|
|
17aa88e3b6 | ||
|
|
5b9af07ebc | ||
|
|
6b45dc8b4d | ||
|
|
0009c74948 | ||
|
|
8911f83ef3 | ||
|
|
2fafafe0dc | ||
|
|
27e6675d26 | ||
|
|
4269e4573c | ||
|
|
096e2b6aec | ||
|
|
33e04b3527 | ||
|
|
79fa2b3508 | ||
|
|
d5ef412300 | ||
|
|
0726a494be | ||
|
|
926969b921 | ||
|
|
59e070f463 | ||
|
|
2ccd1d227e | ||
|
|
655877f67e | ||
|
|
6c240d53e4 | ||
|
|
f89f8ed4ea | ||
|
|
6123b464de | ||
|
|
49dba02e8f | ||
|
|
fb49a3abe5 | ||
|
|
c88f9ab5dc | ||
|
|
d886b14d09 | ||
|
|
bd1204a7a5 | ||
|
|
9cadda0236 | ||
|
|
90ea6323c1 | ||
|
|
51f2cfac16 | ||
|
|
4dbb423ac2 | ||
|
|
a3ee1cc27e | ||
|
|
fea0798349 | ||
|
|
bd8abffa28 | ||
|
|
a5f3a43cde | ||
|
|
6c5dc7e06b | ||
|
|
b1c8a12e88 | ||
|
|
401c6971f4 | ||
|
|
7e5a5ce077 | ||
|
|
6b245c9894 | ||
|
|
b963fe9619 | ||
|
|
673887d93b | ||
|
|
06898429fd | ||
|
|
0196ba4eb4 | ||
|
|
ef81534a17 | ||
|
|
a958ec943b | ||
|
|
4e3b395b3d | ||
|
|
5e7fd4f93c | ||
|
|
25cad5bb25 | ||
|
|
f9c230414e | ||
|
|
866f8d034a | ||
|
|
be6aa559fb | ||
|
|
a560176d83 | ||
|
|
4644e78fd2 | ||
|
|
237cc16b97 | ||
|
|
dffa54afb0 | ||
|
|
225e5c48e4 | ||
|
|
2e017d252a | ||
|
|
e728e5f7e4 | ||
|
|
f0371b41b1 | ||
|
|
a51442921a | ||
|
|
f39fb34f36 | ||
|
|
566201fb45 | ||
|
|
88841b56e7 | ||
|
|
5aa0b4e7d9 | ||
|
|
f19fc4a8a1 | ||
|
|
cd162309a8 | ||
|
|
aead867e27 | ||
|
|
663f295623 | ||
|
|
c588164190 | ||
|
|
d61d0a5795 | ||
|
|
dddd604f7b | ||
|
|
ea3b9a20c5 | ||
|
|
7415b8af08 | ||
|
|
d06e053d34 | ||
|
|
7075a8b05e | ||
|
|
4cbcf14588 | ||
|
|
ca202d7701 | ||
|
|
8dfc16e7ee | ||
|
|
fbf5cec670 | ||
|
|
14d4383f8a | ||
|
|
58bdd39f9e | ||
|
|
ab9ab7acc7 | ||
|
|
be39fd2ec8 | ||
|
|
3c0185da27 | ||
|
|
b37b7276fb | ||
|
|
c45cf2f0c8 | ||
|
|
1f03fdbb50 | ||
|
|
f3b8c5362a | ||
|
|
c9a5dd01be | ||
|
|
0fa4d40e92 | ||
|
|
8a6e64c8c2 | ||
|
|
72b6dd1ae9 | ||
|
|
ee525631fa | ||
|
|
ee9e055af3 | ||
|
|
b214c6ac7e | ||
|
|
eb75020861 | ||
|
|
a44e757e31 | ||
|
|
9ac908948d | ||
|
|
19e82e5890 | ||
|
|
bf84fd24ee | ||
|
|
affeb7c751 | ||
|
|
9743361e0d | ||
|
|
ab16120af2 | ||
|
|
37e5ba0fff | ||
|
|
0aa0dad7fb | ||
|
|
2910efde6e | ||
|
|
eac7656786 | ||
|
|
be3175beae | ||
|
|
26de95a263 | ||
|
|
d0a47bd122 | ||
|
|
8c760bb810 | ||
|
|
c27deefdef | ||
|
|
392a845460 | ||
|
|
e7622c2080 | ||
|
|
3a558412ba | ||
|
|
95e205943a | ||
|
|
eb8686325c | ||
|
|
1f77e156e6 | ||
|
|
92ee50a4a4 | ||
|
|
ef23f01e67 | ||
|
|
22b6a4a2bf | ||
|
|
201ecac156 | ||
|
|
563a78ed42 | ||
|
|
47acc2640b | ||
|
|
f088788246 | ||
|
|
e219dcd332 | ||
|
|
b8829d9a5c | ||
|
|
2c83c976c6 | ||
|
|
d63782ddf2 | ||
|
|
3eabcbec72 | ||
|
|
00ab303e44 | ||
|
|
38bf12701e | ||
|
|
e4ec1d155a | ||
|
|
361f083687 | ||
|
|
c1a59200e2 | ||
|
|
6e0432ff5e | ||
|
|
1c83de08c1 | ||
|
|
0af828543b | ||
|
|
369cc23a30 | ||
|
|
db56ad8b2e | ||
|
|
7fa17d81ac | ||
|
|
019c6a0086 | ||
|
|
c1bee74b57 | ||
|
|
b794279304 | ||
|
|
935dfa1704 | ||
|
|
bda7a0e659 | ||
|
|
8d1cc340b8 | ||
|
|
338c6b59a8 | ||
|
|
021f8ab400 | ||
|
|
f305db9e3e | ||
|
|
e916b25594 | ||
|
|
2a832955c4 | ||
|
|
608b836fe0 | ||
|
|
de9c4fcc4a | ||
|
|
1fec89b69e | ||
|
|
911549aca3 | ||
|
|
41329ec2f8 | ||
|
|
15cdfbc980 | ||
|
|
5053058c32 | ||
|
|
8a8cfad303 | ||
|
|
b1d097a40f | ||
|
|
3a0fc6eeac | ||
|
|
b456b59c44 | ||
|
|
e18d304313 | ||
|
|
fe0df2a4ef | ||
|
|
a51fdb8435 | ||
|
|
cdd5d27908 |
19
.babelrc
@@ -1,19 +0,0 @@
|
||||
{
|
||||
"presets": [
|
||||
"@babel/preset-env",
|
||||
"@babel/preset-react"
|
||||
],
|
||||
"plugins": [
|
||||
"react-hot-loader/babel",
|
||||
"@babel/plugin-proposal-class-properties"
|
||||
],
|
||||
"env": {
|
||||
"test": {
|
||||
"plugins": [
|
||||
["istanbul", {
|
||||
"exclude": ["node_modules/**", "test/**"]
|
||||
}]
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
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-v8:
|
||||
docker:
|
||||
- image: node:8
|
||||
working_directory: ~/repo-linux-node-v8
|
||||
steps: *build-steps
|
||||
build-linux-node-v10:
|
||||
docker:
|
||||
- image: node:10
|
||||
- image: selenium/standalone-chrome:3.141.59
|
||||
working_directory: ~/repo-linux-node-v10
|
||||
steps: *wdio-steps
|
||||
build-linux-node-v12:
|
||||
docker:
|
||||
- image: node:12
|
||||
working_directory: ~/repo-linux-node-v12
|
||||
steps: *build-steps
|
||||
build-osx-node-v8:
|
||||
macos:
|
||||
xcode: "9.0"
|
||||
dependencies:
|
||||
override:
|
||||
- brew install node@8
|
||||
working_directory: ~/repo-osx-node-v8
|
||||
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
|
||||
build-osx-node-v12:
|
||||
macos:
|
||||
xcode: "9.0"
|
||||
dependencies:
|
||||
override:
|
||||
- brew install node@12
|
||||
working_directory: ~/repo-osx-node-v12
|
||||
steps: *build-steps
|
||||
|
||||
workflows:
|
||||
version: 2
|
||||
build:
|
||||
jobs:
|
||||
- build-linux-node-v8
|
||||
- build-linux-node-v10
|
||||
- build-linux-node-v12
|
||||
- build-osx-node-v8
|
||||
- build-osx-node-v10
|
||||
- build-osx-node-v12
|
||||
@@ -1,14 +0,0 @@
|
||||
root = true
|
||||
|
||||
# Unix-style newlines with a newline ending every file
|
||||
[*]
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
|
||||
# Matches multiple files with brace expansion notation
|
||||
# Set default charset
|
||||
[*.{js,jsx,html,sass}]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
trim_trailing_whitespace = true
|
||||
35
.gitignore
vendored
@@ -1,35 +0,0 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
|
||||
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (http://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directory
|
||||
# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
|
||||
node_modules
|
||||
|
||||
# Ignore build files
|
||||
public
|
||||
/errorShots
|
||||
/old
|
||||
/build
|
||||
15
.topissuesrc
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"labels": {
|
||||
"bug": 5,
|
||||
"maintenance": 3,
|
||||
"mentioned in the 1st survey": 2
|
||||
},
|
||||
"reactions": {
|
||||
"+1": 2,
|
||||
"-1": -1,
|
||||
"laugh": 1,
|
||||
"hooray": 2,
|
||||
"confused": 1,
|
||||
"heart": 2
|
||||
}
|
||||
}
|
||||
21
Dockerfile
@@ -1,21 +0,0 @@
|
||||
FROM node:10-slim
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
git \
|
||||
python \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
EXPOSE 8888
|
||||
|
||||
ENV HOME /maputnik
|
||||
RUN mkdir ${HOME}
|
||||
|
||||
COPY . ${HOME}/
|
||||
|
||||
WORKDIR ${HOME}
|
||||
|
||||
RUN npm install -d
|
||||
RUN npm run build
|
||||
|
||||
WORKDIR ${HOME}/build/build
|
||||
CMD python -m SimpleHTTPServer 8888
|
||||
22
LICENSE
@@ -1,22 +0,0 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 Lukas Martinelli
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
171
README.md
@@ -1,171 +0,0 @@
|
||||
# Maputnik
|
||||
|
||||
[][circleci]
|
||||
[][appveyor]
|
||||
[][dm-prod]
|
||||
[][dm-dev]
|
||||
[][license]
|
||||
|
||||
[circleci]: https://circleci.com/gh/maputnik/editor/tree/master
|
||||
[appveyor]: https://ci.appveyor.com/project/lukasmartinelli/editor
|
||||
[dm-prod]: https://david-dm.org/maputnik/editor
|
||||
[dm-dev]: https://david-dm.org/maputnik/editor?type=dev
|
||||
[license]: https://tldrlegal.com/license/mit-license
|
||||
|
||||
<img width="200" align="right" alt="Maputnik" src="https://cdn.jsdelivr.net/gh/maputnik/editor@1.5.0/src/img/maputnik.png" />
|
||||
|
||||
A free and open visual editor for the [Mapbox GL styles](https://www.mapbox.com/mapbox-gl-style-spec/)
|
||||
targeted at developers and map designers.
|
||||
|
||||
- :link: Design your maps online at **<https://maputnik.github.io/editor/>** (all in local storage)
|
||||
- :link: Try out the v1.6.0-beta release at: https://maputnik.github.io/releases/v1.6.0-beta/
|
||||
- :link: Use the [Maputnik CLI](https://github.com/maputnik/editor/wiki/Maputnik-CLI) for local style development
|
||||
|
||||
Mapbox has built one of the best and most amazing OSS ecosystems. A key component to ensure its longevity and independence is an OSS map designer.
|
||||
|
||||
|
||||
## 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) and [Mapbox GL JS](https://www.mapbox.com/mapbox-gl-js/api/).
|
||||
|
||||
We ensure building and developing Maputnik works with the [current active LTS Node.js version and above](https://github.com/nodejs/Release#release-schedule).
|
||||
|
||||
Install the deps, start the dev server and open the web browser on `http://localhost:8888/`.
|
||||
|
||||
```bash
|
||||
# install dependencies
|
||||
npm install
|
||||
# start dev server
|
||||
npm start
|
||||
```
|
||||
|
||||
If you want Maputnik to be accessible externally use the [`--host` option](https://webpack.js.org/configuration/dev-server/#devserverhost):
|
||||
|
||||
```bash
|
||||
# start externally accessible dev server
|
||||
npm start -- --host 0.0.0.0
|
||||
```
|
||||
|
||||
The build process will watch for changes to the filesystem, rebuild and autoreload the editor. However note this from the [webpack-dev-server docs](https://webpack.js.org/configuration/dev-server/):
|
||||
|
||||
> 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 source](https://webpack.js.org/configuration/dev-server/#devserverwatchoptions-))
|
||||
|
||||
To enable polling add `export WEBPACK_DEV_SERVER_POLLING=1` to your environment.
|
||||
|
||||
```
|
||||
npm run build
|
||||
```
|
||||
|
||||
Lint the JavaScript code.
|
||||
|
||||
```
|
||||
# run linter
|
||||
npm run lint
|
||||
npm run lint-styles
|
||||
```
|
||||
|
||||
|
||||
## Tests
|
||||
For testing we use [webdriverio](http://webdriver.io) and [selenium-standalone](https://github.com/vvo/selenium-standalone)
|
||||
|
||||
[selenium-standalone](https://github.com/vvo/selenium-standalone) starts a server that will launch browsers on your local machine. We use chrome so you **must** have chrome installed on your machine.
|
||||
|
||||
Now open a terminal and run the following. This will install the drivers on your local machine
|
||||
|
||||
```
|
||||
./node_modules/.bin/selenium-standalone install
|
||||
```
|
||||
|
||||
Now start the standalone server
|
||||
|
||||
```
|
||||
./node_modules/.bin/selenium-standalone start
|
||||
```
|
||||
|
||||
Then open another terminal and run
|
||||
|
||||
```
|
||||
npm test
|
||||
```
|
||||
|
||||
After some time you should see a browser launch which will be automated by the test runner.
|
||||
|
||||
|
||||
## Related Projects
|
||||
|
||||
- [maputnik-dev-server](https://github.com/nycplanning/labs-maputnik-dev-server) - An express.js server that allows for quickly loading the style from any mapboxGL map into mapuntnik.
|
||||
|
||||
## Sponsors
|
||||
|
||||
Thanks to the supporters of the **[Kickstarter campaign](https://www.kickstarter.com/projects/174808720/maputnik-visual-map-editor-for-mapbox-gl)**. This project would not be possible without these commercial and individual sponsors.
|
||||
|
||||
### Gold
|
||||
|
||||
- [Wemap](https://getwemap.com/)
|
||||
- [Orbicon Informatik](https://www.orbiconinformatik.dk/)
|
||||
- [Terranodo](http://terranodo.io/)
|
||||
|
||||
<a href="https://getwemap.com/">
|
||||
<img width="33%" alt="Wemap" style="display:inline" src="https://cdn.jsdelivr.net/gh/maputnik/editor@1.5.0/media/sponsors/wemap.jpg" />
|
||||
</a>
|
||||
<a href="http://terranodo.io/">
|
||||
<img width="33%" alt="Terranodo" style="display:inline" src="https://cdn.jsdelivr.net/gh/maputnik/editor@1.5.0/media/sponsors/terranodo.png" />
|
||||
</a>
|
||||
<a href="https://www.orbiconinformatik.dk/">
|
||||
<img width="32%" alt="Terranodo" style="display:inline" src="https://cdn.jsdelivr.net/gh/maputnik/editor@1.5.0/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 width="18%" alt="Klokan Technologies" style="display:inline-block" src="https://cdn.jsdelivr.net/gh/maputnik/editor@1.5.0/media/sponsors/klokantech.png" />
|
||||
</a>
|
||||
<a href="http://www.geofabrik.de/">
|
||||
<img width="18%" alt="Geofabrik" style="display:inline-block" src="https://cdn.jsdelivr.net/gh/maputnik/editor@1.5.0/media/sponsors/geofabrik.png" />
|
||||
</a>
|
||||
<a href="https://www.dreipol.ch/">
|
||||
<img width="18%" alt="Dreipol" style="display:inline-block" src="https://cdn.jsdelivr.net/gh/maputnik/editor@1.5.0/media/sponsors/dreipol.png" />
|
||||
</a>
|
||||
|
||||
<br/>
|
||||
|
||||
### Individuals
|
||||
|
||||
**Influential Stakeholder**
|
||||
|
||||
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
|
||||
|
||||
**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
|
||||
|
||||
## License
|
||||
|
||||
Maputnik is [licensed under MIT](LICENSE) and is Copyright (c) Lukas Martinelli and contributors.
|
||||
|
||||
**Disclaimer** This project is not affiliated with Mapbox or Mapbox Studio. It is an independent style editor for the
|
||||
open source technology in the Mapbox GL ecosystem.
|
||||
As contributor please take extra care of not violating any Mapbox trademarks. Do not get inspired by Mapbox Studio and make your own decisions for a good style editor.
|
||||
26
appveyor.yml
@@ -1,26 +0,0 @@
|
||||
image: Visual Studio 2015
|
||||
environment:
|
||||
matrix:
|
||||
- nodejs_version: "8"
|
||||
- nodejs_version: "10"
|
||||
- nodejs_version: "12"
|
||||
platform:
|
||||
- x86
|
||||
- x64
|
||||
install:
|
||||
# https://github.com/appveyor/ci/issues/2921#issuecomment-501016533
|
||||
- ps: |
|
||||
try {
|
||||
Install-Product node $env:nodejs_version $env:platform
|
||||
} catch {
|
||||
echo "Unable to install node $env:nodejs_version, trying update..."
|
||||
Update-NodeJsInstallation (Get-NodeJsLatestBuild $env:nodejs_version) $env:platform
|
||||
}
|
||||
- md public
|
||||
- npm --vs2015 install --global windows-build-tools
|
||||
- npm install
|
||||
build_script:
|
||||
- npm run build
|
||||
test_script:
|
||||
- npm run lint
|
||||
- npm run lint-styles
|
||||
0
src/fonts/Roboto-Regular.ttf → assets/Roboto-Regular-B-HLW1rL.ttf
Executable file → Normal file
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
1
assets/index-CuVViU0P.css
Normal file
962
assets/index-DtxAbt8L.js
Normal file
1
assets/index-DtxAbt8L.js.map
Normal file
2
assets/translation-BhJ-ufwk.js
Normal file
1
assets/translation-BhJ-ufwk.js.map
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"translation-BhJ-ufwk.js","sources":[],"sourcesContent":[],"names":[],"mappings":""}
|
||||
2
assets/translation-BrzYPxJn.js
Normal file
1
assets/translation-BrzYPxJn.js.map
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"translation-BrzYPxJn.js","sources":[],"sourcesContent":[],"names":[],"mappings":""}
|
||||
2
assets/translation-CQD4fuPu.js
Normal file
1
assets/translation-CQD4fuPu.js.map
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"translation-CQD4fuPu.js","sources":[],"sourcesContent":[],"names":[],"mappings":""}
|
||||
2
assets/translation-CZ64AJ8H.js
Normal file
1
assets/translation-CZ64AJ8H.js.map
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"translation-CZ64AJ8H.js","sources":[],"sourcesContent":[],"names":[],"mappings":""}
|
||||
2
assets/translation-XoriI0W-.js
Normal file
1
assets/translation-XoriI0W-.js.map
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"translation-XoriI0W-.js","sources":[],"sourcesContent":[],"names":[],"mappings":""}
|
||||
2
assets/translation-aD1CAGoy.js
Normal file
1
assets/translation-aD1CAGoy.js.map
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"translation-aD1CAGoy.js","sources":[],"sourcesContent":[],"names":[],"mappings":""}
|
||||
@@ -1,288 +0,0 @@
|
||||
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 = {
|
||||
//
|
||||
// ====================
|
||||
// Runner Configuration
|
||||
// ====================
|
||||
//
|
||||
// WebdriverIO allows it to run your tests in arbitrary locations (e.g. locally or
|
||||
// on a remote machine).
|
||||
runner: 'local',
|
||||
//
|
||||
// ==================
|
||||
// Specify Test Files
|
||||
// ==================
|
||||
// Define which test specs should run. The pattern is relative to the directory
|
||||
// from which `wdio` was called. Notice that, if you are calling `wdio` from an
|
||||
// NPM script (see https://docs.npmjs.com/cli/run-script) then the current working
|
||||
// directory is where your package.json resides, so `wdio` will be called from there.
|
||||
//
|
||||
specs: [
|
||||
'./test/functional/index.js'
|
||||
],
|
||||
// Patterns to exclude.
|
||||
exclude: [
|
||||
// 'path/to/excluded/files'
|
||||
],
|
||||
//
|
||||
// ============
|
||||
// Capabilities
|
||||
// ============
|
||||
// Define your capabilities here. WebdriverIO can run multiple capabilities at the same
|
||||
// time. Depending on the number of capabilities, WebdriverIO launches several test
|
||||
// sessions. Within your capabilities you can overwrite the spec and exclude options in
|
||||
// order to group specific specs to a specific capability.
|
||||
//
|
||||
// First, you can define how many instances should be started at the same time. Let's
|
||||
// say you have 3 different capabilities (Chrome, Firefox, and Safari) and you have
|
||||
// set maxInstances to 1; wdio will spawn 3 processes. Therefore, if you have 10 spec
|
||||
// files and you set maxInstances to 10, all spec files will get tested at the same time
|
||||
// and 30 processes will get spawned. The property handles how many capabilities
|
||||
// from the same test should run tests.
|
||||
//
|
||||
maxInstances: 10,
|
||||
//
|
||||
// If you have trouble getting all important capabilities together, check out the
|
||||
// Sauce Labs platform configurator - a great tool to configure your capabilities:
|
||||
// https://docs.saucelabs.com/reference/platforms-configurator
|
||||
//
|
||||
capabilities: [{
|
||||
// maxInstances can get overwritten per capability. So if you have an in-house Selenium
|
||||
// grid with only 5 firefox instances available you can make sure that not more than
|
||||
// 5 instances get started at a time.
|
||||
maxInstances: 5,
|
||||
//
|
||||
browserName: 'chrome',
|
||||
// If outputDir is provided WebdriverIO can capture driver session logs
|
||||
// it is possible to configure which logTypes to include/exclude.
|
||||
// excludeDriverLogs: ['*'], // pass '*' to exclude all driver session logs
|
||||
// excludeDriverLogs: ['bugreport', 'server'],
|
||||
}],
|
||||
//
|
||||
// ===================
|
||||
// Test Configurations
|
||||
// ===================
|
||||
// Define all options that are relevant for the WebdriverIO instance here
|
||||
//
|
||||
// Level of logging verbosity: trace | debug | info | warn | error | silent
|
||||
logLevel: 'info',
|
||||
//
|
||||
// Set specific log levels per logger
|
||||
// loggers:
|
||||
// - webdriver, webdriverio
|
||||
// - @wdio/applitools-service, @wdio/browserstack-service, @wdio/devtools-service, @wdio/sauce-service
|
||||
// - @wdio/mocha-framework, @wdio/jasmine-framework
|
||||
// - @wdio/local-runner, @wdio/lambda-runner
|
||||
// - @wdio/sumologic-reporter
|
||||
// - @wdio/cli, @wdio/config, @wdio/sync, @wdio/utils
|
||||
// Level of logging verbosity: trace | debug | info | warn | error | silent
|
||||
// logLevels: {
|
||||
// webdriver: 'debug',
|
||||
// '@wdio/applitools-service': 'info'
|
||||
// },
|
||||
//
|
||||
// If you only want to run your tests until a specific amount of tests have failed use
|
||||
// bail (default is 0 - don't bail, run all tests).
|
||||
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",
|
||||
// Set a base URL in order to shorten url command calls. If your `url` parameter starts
|
||||
// with `/`, the base url gets prepended, not including the path portion of your baseUrl.
|
||||
// If your `url` parameter starts without a scheme or `/` (like `some/path`), the base url
|
||||
// gets prepended directly.
|
||||
baseUrl: 'http://localhost',
|
||||
//
|
||||
// Default timeout for all waitFor* commands.
|
||||
waitforTimeout: 10000,
|
||||
//
|
||||
// Default timeout in milliseconds for request
|
||||
// if Selenium Grid doesn't send response
|
||||
connectionRetryTimeout: 90000,
|
||||
//
|
||||
// Default request retries count
|
||||
connectionRetryCount: 3,
|
||||
//
|
||||
// Test runner services
|
||||
// Services take over a specific job you don't want to take care of. They enhance
|
||||
// your test setup with almost no effort. Unlike plugins, they don't add new
|
||||
// commands. Instead, they hook themselves up into the test process.
|
||||
services: ['selenium-standalone'],
|
||||
//
|
||||
// Framework you want to run your specs with.
|
||||
// The following are supported: Mocha, Jasmine, and Cucumber
|
||||
// see also: https://webdriver.io/docs/frameworks.html
|
||||
//
|
||||
// Make sure you have the wdio adapter package for the specific framework installed
|
||||
// before running any tests.
|
||||
framework: 'mocha',
|
||||
//
|
||||
// The number of times to retry the entire specfile when it fails as a whole
|
||||
// specFileRetries: 1,
|
||||
//
|
||||
// Test reporter for stdout.
|
||||
// The only one supported by default is 'dot'
|
||||
// see also: https://webdriver.io/docs/dot-reporter.html
|
||||
reporters: ['spec'],
|
||||
|
||||
//
|
||||
// Options to be passed to Mocha.
|
||||
// See the full list at http://mochajs.org/
|
||||
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()
|
||||
}
|
||||
//
|
||||
// =====
|
||||
// Hooks
|
||||
// =====
|
||||
// WebdriverIO provides several hooks you can use to interfere with the test process in order to enhance
|
||||
// it and to build services around it. You can either apply a single function or an array of
|
||||
// methods to it. If one of them returns with a promise, WebdriverIO will wait until that promise got
|
||||
// resolved to continue.
|
||||
/**
|
||||
* Gets executed once before all workers get launched.
|
||||
* @param {Object} config wdio configuration object
|
||||
* @param {Array.<Object>} capabilities list of capabilities details
|
||||
*/
|
||||
// onPrepare: function (config, capabilities) {
|
||||
// },
|
||||
/**
|
||||
* Gets executed just before initialising the webdriver session and test framework. It allows you
|
||||
* to manipulate configurations depending on the capability or spec.
|
||||
* @param {Object} config wdio configuration object
|
||||
* @param {Array.<Object>} capabilities list of capabilities details
|
||||
* @param {Array.<String>} specs List of spec file paths that are to be run
|
||||
*/
|
||||
// beforeSession: function (config, capabilities, specs) {
|
||||
// },
|
||||
/**
|
||||
* Gets executed before test execution begins. At this point you can access to all global
|
||||
* variables like `browser`. It is the perfect place to define custom commands.
|
||||
* @param {Array.<Object>} capabilities list of capabilities details
|
||||
* @param {Array.<String>} specs List of spec file paths that are to be run
|
||||
*/
|
||||
// before: function (capabilities, specs) {
|
||||
// },
|
||||
/**
|
||||
* Runs before a WebdriverIO command gets executed.
|
||||
* @param {String} commandName hook command name
|
||||
* @param {Array} args arguments that command would receive
|
||||
*/
|
||||
// beforeCommand: function (commandName, args) {
|
||||
// },
|
||||
|
||||
/**
|
||||
* Hook that gets executed before the suite starts
|
||||
* @param {Object} suite suite details
|
||||
*/
|
||||
// beforeSuite: function (suite) {
|
||||
// },
|
||||
/**
|
||||
* Function to be executed before a test (in Mocha/Jasmine) or a step (in Cucumber) starts.
|
||||
* @param {Object} test test details
|
||||
*/
|
||||
// beforeTest: function (test) {
|
||||
// },
|
||||
/**
|
||||
* Hook that gets executed _before_ a hook within the suite starts (e.g. runs before calling
|
||||
* beforeEach in Mocha)
|
||||
*/
|
||||
// beforeHook: function () {
|
||||
// },
|
||||
/**
|
||||
* Hook that gets executed _after_ a hook within the suite starts (e.g. runs after calling
|
||||
* afterEach in Mocha)
|
||||
*/
|
||||
// afterHook: function () {
|
||||
// },
|
||||
/**
|
||||
* Function to be executed after a test (in Mocha/Jasmine) or a step (in Cucumber) starts.
|
||||
* @param {Object} test test details
|
||||
*/
|
||||
// afterTest: function (test) {
|
||||
// },
|
||||
/**
|
||||
* Hook that gets executed after the suite has ended
|
||||
* @param {Object} suite suite details
|
||||
*/
|
||||
// afterSuite: function (suite) {
|
||||
// },
|
||||
|
||||
/**
|
||||
* Runs after a WebdriverIO command gets executed
|
||||
* @param {String} commandName hook command name
|
||||
* @param {Array} args arguments that command would receive
|
||||
* @param {Number} result 0 - command success, 1 - command error
|
||||
* @param {Object} error error object if any
|
||||
*/
|
||||
// afterCommand: function (commandName, args, result, error) {
|
||||
// },
|
||||
/**
|
||||
* Gets executed after all tests are done. You still have access to all global variables from
|
||||
* the test.
|
||||
* @param {Number} result 0 - test pass, 1 - test fail
|
||||
* @param {Array.<Object>} capabilities list of capabilities details
|
||||
* @param {Array.<String>} specs List of spec file paths that ran
|
||||
*/
|
||||
// after: function (result, capabilities, specs) {
|
||||
// },
|
||||
/**
|
||||
* Gets executed right after terminating the webdriver session.
|
||||
* @param {Object} config wdio configuration object
|
||||
* @param {Array.<Object>} capabilities list of capabilities details
|
||||
* @param {Array.<String>} specs List of spec file paths that ran
|
||||
*/
|
||||
// afterSession: function (config, capabilities, specs) {
|
||||
// },
|
||||
/**
|
||||
* Gets executed after all workers got shut down and the process is about to exit. An error
|
||||
* thrown in the onComplete hook will result in the test run failing.
|
||||
* @param {Object} exitCode 0 - success, 1 - fail
|
||||
* @param {Object} config wdio configuration object
|
||||
* @param {Array.<Object>} capabilities list of capabilities details
|
||||
* @param {<Object>} results object containing test results
|
||||
*/
|
||||
// onComplete: function(exitCode, config, capabilities, results) {
|
||||
// },
|
||||
/**
|
||||
* Gets executed when a refresh happens.
|
||||
* @param {String} oldSessionId session ID of the old session
|
||||
* @param {String} newSessionId session ID of the new session
|
||||
*/
|
||||
//onReload: function(oldSessionId, newSessionId) {
|
||||
//}
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
"use strict";
|
||||
var webpack = require('webpack');
|
||||
var path = require('path');
|
||||
var rules = require('./webpack.rules');
|
||||
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";
|
||||
|
||||
module.exports = {
|
||||
target: 'web',
|
||||
mode: 'development',
|
||||
entry: [
|
||||
`webpack-dev-server/client?http://${HOST}:${PORT}`,
|
||||
`webpack/hot/only-dev-server`,
|
||||
`./src/index.jsx` // Your appʼs entry point
|
||||
],
|
||||
devtool: process.env.WEBPACK_DEVTOOL || 'cheap-module-source-map',
|
||||
output: {
|
||||
path: path.join(__dirname, '..', 'public'),
|
||||
filename: 'bundle.js'
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.js', '.jsx']
|
||||
},
|
||||
module: {
|
||||
noParse: [
|
||||
/mapbox-gl\/dist\/mapbox-gl.js/
|
||||
],
|
||||
rules: rules
|
||||
},
|
||||
node: {
|
||||
fs: "empty",
|
||||
net: 'empty',
|
||||
tls: 'empty'
|
||||
},
|
||||
devServer: {
|
||||
contentBase: "./public",
|
||||
// do not print bundle build stats
|
||||
noInfo: true,
|
||||
// enable HMR
|
||||
hot: true,
|
||||
// embed the webpack-dev-server runtime into the bundle
|
||||
inline: true,
|
||||
// serve index.html in place of 404 responses to allow HTML5 history
|
||||
historyApiFallback: true,
|
||||
port: PORT,
|
||||
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.NoEmitOnErrorsPlugin(),
|
||||
new webpack.HotModuleReplacementPlugin(),
|
||||
new HtmlWebpackPlugin({
|
||||
title: 'Maputnik',
|
||||
template: './src/template.html'
|
||||
}),
|
||||
new CopyWebpackPlugin([
|
||||
{
|
||||
from: './src/manifest.json',
|
||||
to: 'manifest.json'
|
||||
}
|
||||
])
|
||||
]
|
||||
};
|
||||
@@ -1,62 +0,0 @@
|
||||
var webpack = require('webpack');
|
||||
var path = require('path');
|
||||
var rules = require('./webpack.rules');
|
||||
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 OUTPATH = artifacts.pathSync("/build");
|
||||
|
||||
module.exports = {
|
||||
entry: {
|
||||
app: './src/index.jsx',
|
||||
},
|
||||
output: {
|
||||
path: OUTPATH,
|
||||
filename: '[name].[contenthash].js',
|
||||
chunkFilename: '[contenthash].js'
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.js', '.jsx']
|
||||
},
|
||||
module: {
|
||||
noParse: [
|
||||
/mapbox-gl\/dist\/mapbox-gl.js/
|
||||
],
|
||||
rules: rules
|
||||
},
|
||||
node: {
|
||||
fs: "empty",
|
||||
net: 'empty',
|
||||
tls: 'empty'
|
||||
},
|
||||
plugins: [
|
||||
new webpack.NoEmitOnErrorsPlugin(),
|
||||
new WebpackCleanupPlugin(),
|
||||
new webpack.DefinePlugin({
|
||||
'process.env': {
|
||||
NODE_ENV: '"production"'
|
||||
}
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
template: './src/template.html',
|
||||
title: 'Maputnik'
|
||||
}),
|
||||
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,38 +0,0 @@
|
||||
const path = require("path");
|
||||
|
||||
module.exports = [
|
||||
{
|
||||
test: /\.jsx?$/,
|
||||
exclude: [
|
||||
path.resolve(__dirname, '../node_modules')
|
||||
],
|
||||
use: 'babel-loader'
|
||||
},
|
||||
{
|
||||
test: /\.(eot|ttf|woff|woff2)$/,
|
||||
use: 'file-loader?name=fonts/[name].[ext]'
|
||||
},
|
||||
{
|
||||
test: /\.ico$/,
|
||||
use: 'file-loader?name=[name].[ext]'
|
||||
},
|
||||
{
|
||||
test: /\.(svg|gif|jpg|png)$/,
|
||||
use: 'file-loader?name=img/[name].[ext]'
|
||||
},
|
||||
{
|
||||
test: /[\/\\](node_modules|global|src)[\/\\].*\.scss$/,
|
||||
use: [
|
||||
'style-loader',
|
||||
"css-loader",
|
||||
"sass-loader"
|
||||
]
|
||||
},
|
||||
{
|
||||
test: /[\/\\](node_modules|global|src)[\/\\].*\.css$/,
|
||||
use: [
|
||||
'style-loader',
|
||||
'css-loader'
|
||||
]
|
||||
}
|
||||
];
|
||||
133
index.html
Normal file
@@ -0,0 +1,133 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Maputnik</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="manifest" href="/maputnik/assets/manifest-BrZzkYP9.json">
|
||||
<link rel="icon" href="/maputnik/assets/favicon-DBn6BKLx.ico" type="image/x-icon" />
|
||||
<style>
|
||||
html {
|
||||
background-color: rgb(28, 31, 36);
|
||||
}
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
position: absolute;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
top: 0;
|
||||
left: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.loading__logo img {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
.loading__text {
|
||||
font-family: sans-serif;
|
||||
color: white;
|
||||
font-size: 1.2em;
|
||||
padding-bottom: 2em;
|
||||
}
|
||||
|
||||
</style>
|
||||
<script type="module" crossorigin src="/maputnik/assets/index-DtxAbt8L.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/maputnik/assets/index-CuVViU0P.css">
|
||||
</head>
|
||||
<body>
|
||||
<!-- From <https://github.com/hail2u/color-blindness-emulation> -->
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
version="1.1">
|
||||
<defs>
|
||||
<filter id="protanopia">
|
||||
<feColorMatrix
|
||||
in="SourceGraphic"
|
||||
type="matrix"
|
||||
values="0.567, 0.433, 0, 0, 0
|
||||
0.558, 0.442, 0, 0, 0
|
||||
0, 0.242, 0.758, 0, 0
|
||||
0, 0, 0, 1, 0"/>
|
||||
</filter>
|
||||
<filter id="protanomaly">
|
||||
<feColorMatrix
|
||||
in="SourceGraphic"
|
||||
type="matrix"
|
||||
values="0.817, 0.183, 0, 0, 0
|
||||
0.333, 0.667, 0, 0, 0
|
||||
0, 0.125, 0.875, 0, 0
|
||||
0, 0, 0, 1, 0"/>
|
||||
</filter>
|
||||
<filter id="deuteranopia">
|
||||
<feColorMatrix
|
||||
in="SourceGraphic"
|
||||
type="matrix"
|
||||
values="0.625, 0.375, 0, 0, 0
|
||||
0.7, 0.3, 0, 0, 0
|
||||
0, 0.3, 0.7, 0, 0
|
||||
0, 0, 0, 1, 0"/>
|
||||
</filter>
|
||||
<filter id="deuteranomaly">
|
||||
<feColorMatrix
|
||||
in="SourceGraphic"
|
||||
type="matrix"
|
||||
values="0.8, 0.2, 0, 0, 0
|
||||
0.258, 0.742, 0, 0, 0
|
||||
0, 0.142, 0.858, 0, 0
|
||||
0, 0, 0, 1, 0"/>
|
||||
</filter>
|
||||
<filter id="tritanopia">
|
||||
<feColorMatrix
|
||||
in="SourceGraphic"
|
||||
type="matrix"
|
||||
values="0.95, 0.05, 0, 0, 0
|
||||
0, 0.433, 0.567, 0, 0
|
||||
0, 0.475, 0.525, 0, 0
|
||||
0, 0, 0, 1, 0"/>
|
||||
</filter>
|
||||
<filter id="tritanomaly">
|
||||
<feColorMatrix
|
||||
in="SourceGraphic"
|
||||
type="matrix"
|
||||
values="0.967, 0.033, 0, 0, 0
|
||||
0, 0.733, 0.267, 0, 0
|
||||
0, 0.183, 0.817, 0, 0
|
||||
0, 0, 0, 1, 0"/>
|
||||
</filter>
|
||||
<filter id="achromatopsia">
|
||||
<feColorMatrix
|
||||
in="SourceGraphic"
|
||||
type="matrix"
|
||||
values="0.299, 0.587, 0.114, 0, 0
|
||||
0.299, 0.587, 0.114, 0, 0
|
||||
0.299, 0.587, 0.114, 0, 0
|
||||
0, 0, 0, 1, 0"/>
|
||||
</filter>
|
||||
<filter id="achromatomaly">
|
||||
<feColorMatrix
|
||||
in="SourceGraphic"
|
||||
type="matrix"
|
||||
values="0.618, 0.320, 0.062, 0, 0
|
||||
0.163, 0.775, 0.062, 0, 0
|
||||
0.163, 0.320, 0.516, 0, 0
|
||||
0, 0, 0, 1, 0"/>
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>
|
||||
|
||||
<div id="app"></div>
|
||||
<div class="loading">
|
||||
<div class="loading__logo">
|
||||
<img inline src="data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20xmlns:xlink='http://www.w3.org/1999/xlink'%20width='1200'%20height='1200'%20viewBox='0%200%20100%20100'%3e%3cstyle%3e@keyframes%20circle-anim{0%25,40%25{fill-opacity:0}60%25,to{fill-opacity:1}}.circle0,.circle1,.circle2,.circle3,.circle4,.circle5{stroke-opacity:0;animation-name:circle-anim;will-change:transform;animation-timing-function:east-in-out;animation-duration:800ms;animation-iteration-count:infinite;animation-direction:alternate}.circle0{animation-delay:100ms}.circle1{animation-delay:200ms}.circle2{animation-delay:300ms}.circle3{animation-delay:400ms}.circle4{animation-delay:500ms}.circle5{animation-delay:600ms}%3c/style%3e%3cg%20class='map'%20stroke='%23000'%3e%3cuse%20xlink:href='%23ref-1--map__main'%20fill='%234eba6f'%3e%3c/use%3e%3cuse%20xlink:href='%23ref-1--map__line1'%20fill='none'%3e%3c/use%3e%3cuse%20xlink:href='%23ref-1--map__line2'%20fill='none'%3e%3c/use%3e%3cuse%20xlink:href='%23ref-1--map__line3'%20fill='none'%3e%3c/use%3e%3c/g%3e%3cg%20class='palette'%3e%3cuse%20xlink:href='%23ref-1--palette__main'%20fill='%23fff'%20stroke='%23000'%3e%3c/use%3e%3cuse%20xlink:href='%23ref-1--palette__inner'%20fill='none'%20stroke='%23000'%3e%3c/use%3e%3cuse%20class='circle5'%20xlink:href='%23ref-1--palette__circle5'%20fill='%23f7c44c'%3e%3c/use%3e%3cuse%20class='circle4'%20xlink:href='%23ref-1--palette__circle4'%20fill='%234eba6f'%3e%3c/use%3e%3cuse%20class='circle3'%20xlink:href='%23ref-1--palette__circle3'%20fill='%23f7c44c'%3e%3c/use%3e%3cuse%20class='circle2'%20xlink:href='%23ref-1--palette__circle2'%20fill='%234eba6f'%3e%3c/use%3e%3cuse%20class='circle1'%20xlink:href='%23ref-1--palette__circle1'%20fill='%23f7c44c'%3e%3c/use%3e%3cuse%20class='circle0'%20xlink:href='%23ref-1--palette__circle0'%20fill='%234eba6f'%3e%3c/use%3e%3c/g%3e%3cg%20class='brush'%20stroke='%23000'%3e%3cuse%20xlink:href='%23ref-1--brush__bottom'%20fill='%23f7c44c'%3e%3c/use%3e%3cuse%20xlink:href='%23ref-1--brush__top'%20fill='%23fff'%3e%3c/use%3e%3c/g%3e%3cdefs%3e%3cpath%20id='ref-1--map__main'%20stroke-width='2.366'%20stroke-linejoin='round'%20d='M18.84%207.717l15.44%207.542%2015.75-7.762%2015.7%207.857L81.005%207.67%2096.31%2054.052%2073.598%2062.12%2050.93%2053.872l-25.1%208.066-22.668-8.066z'%3e%3c/path%3e%3cpath%20id='ref-1--map__line1'%20d='M65.556%2015.07l7.647%2046.838'%20stroke-width='1.104'%3e%3c/path%3e%3cpath%20id='ref-1--map__line2'%20d='M50.261%207.422l.717%2046.6'%20stroke-width='1.104'%3e%3c/path%3e%3cpath%20id='ref-1--map__line3'%20d='M34.011%2015.07l-8.603%2046.6'%20stroke-width='1.104'%3e%3c/path%3e%3cpath%20id='ref-1--palette__main'%20stroke-width='2.3'%20d='M47.352%2030.887c7.993.226%2016.934%209.725%2017.954%2015.25%201.02%205.527-.743%2011.125-4.298%2013.875-3.554%202.75-8.6%202.905-8.723%208.302-.097%204.237%208.457%208.5%208.088%2015.653-.406%207.857-15.508%2013.15-30.943%206.102-8.556-3.906-14.249-13.653-13.385-26.238C16.833%2052.334%2022.32%2043.658%2027.382%2039c5.977-5.503%2011.977-8.337%2019.97-8.112z'%3e%3c/path%3e%3ccircle%20id='ref-1--palette__inner'%20stroke-width='2.3'%20cx='41.873'%20cy='61.901'%20r='6.389'%3e%3c/circle%3e%3ccircle%20id='ref-1--palette__circle5'%20cy='44.56'%20cx='54.347'%20r='4.336'%3e%3c/circle%3e%3ccircle%20id='ref-1--palette__circle4'%20cx='40.443'%20cy='41.555'%20r='4.336'%3e%3c/circle%3e%3ccircle%20id='ref-1--palette__circle3'%20r='4.336'%20cy='51.102'%20cx='29.651'%3e%3c/circle%3e%3ccircle%20id='ref-1--palette__circle2'%20cx='25.293'%20cy='65.836'%20r='4.336'%3e%3c/circle%3e%3ccircle%20id='ref-1--palette__circle1'%20r='4.336'%20cy='79.326'%20cx='32.764'%3e%3c/circle%3e%3ccircle%20id='ref-1--palette__circle0'%20cx='46.669'%20cy='80.571'%20r='4.336'%3e%3c/circle%3e%3cpath%20id='ref-1--brush__bottom'%20d='M76.333%2089.333c-1.645-9.794-4.375-35.26-4.32-37.887.056-2.627%202.52-4.34%205.36-4.317%202.842.022%205.098%201.87%205.314%204.27.107%201.2-1.576%2028.06-2.318%2037.844-.332%204.374-3.31%204.413-4.036.09z'%20stroke-width='2.3'%20stroke-linejoin='round'%3e%3c/path%3e%3cpath%20id='ref-1--brush__top'%20stroke-linejoin='round'%20stroke-width='2.3'%20d='M77.184%2026.428s-5.621%207.02-5.621%2011.978c0%204.957%202.206%206.878%205.81%206.878%203.606%200%205.148-1.708%205.29-6.736.142-5.028-5.479-12.12-5.479-12.12z'%3e%3c/path%3e%3c/defs%3e%3c/svg%3e" />
|
||||
</div>
|
||||
<div class="loading__text">Loading…</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 8.2 KiB |
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 65 KiB |
|
Before Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 14 KiB |
16916
package-lock.json
generated
150
package.json
@@ -1,150 +0,0 @@
|
||||
{
|
||||
"name": "maputnik",
|
||||
"version": "1.6.0",
|
||||
"description": "A MapboxGL visual style editor",
|
||||
"main": "''",
|
||||
"scripts": {
|
||||
"stats": "webpack --config config/webpack.production.config.js --profile --json > stats.json",
|
||||
"build": "webpack --config config/webpack.production.config.js --progress --profile --colors",
|
||||
"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\""
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/maputnik/editor"
|
||||
},
|
||||
"author": "Lukas Martinelli",
|
||||
"license": "MIT",
|
||||
"homepage": "https://github.com/maputnik/editor#readme",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.1.2",
|
||||
"@mapbox/mapbox-gl-rtl-text": "^0.2.2",
|
||||
"@mapbox/mapbox-gl-style-spec": "^13.7.2",
|
||||
"classnames": "^2.2.6",
|
||||
"codemirror": "^5.40.2",
|
||||
"color": "^3.0.0",
|
||||
"detect-browser": "^4.5.0",
|
||||
"file-saver": "^1.3.8",
|
||||
"jsonlint": "github:josdejong/jsonlint#85a19d7",
|
||||
"lodash.capitalize": "^4.2.1",
|
||||
"lodash.clamp": "^4.0.3",
|
||||
"lodash.clonedeep": "^4.5.0",
|
||||
"lodash.get": "^4.4.2",
|
||||
"lodash.isequal": "^4.5.0",
|
||||
"lodash.throttle": "^4.1.1",
|
||||
"mapbox-gl": "^1.2.0",
|
||||
"mapbox-gl-inspect": "^1.3.1",
|
||||
"maputnik-design": "github:maputnik/design",
|
||||
"ol": "^6.0.0-beta.8",
|
||||
"ol-mapbox-style": "^5.0.0-beta.2",
|
||||
"prop-types": "^15.6.2",
|
||||
"react": "^16.5.2",
|
||||
"react-aria-menubutton": "^6.0.1",
|
||||
"react-aria-modal": "^3.0.0",
|
||||
"react-autobind": "^1.0.6",
|
||||
"react-autocomplete": "^1.8.1",
|
||||
"react-codemirror2": "^5.1.0",
|
||||
"react-collapse": "^4.0.3",
|
||||
"react-color": "^2.14.1",
|
||||
"react-dom": "^16.5.2",
|
||||
"react-file-reader-input": "^2.0.0",
|
||||
"react-icon-base": "^2.1.2",
|
||||
"react-icons": "^3.1.0",
|
||||
"react-motion": "^0.5.2",
|
||||
"react-sortable-hoc": "^0.8.3",
|
||||
"reconnecting-websocket": "^3.2.2",
|
||||
"slugify": "^1.3.1",
|
||||
"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"
|
||||
],
|
||||
"extends": [
|
||||
"plugin:react/recommended"
|
||||
],
|
||||
"env": {
|
||||
"browser": true,
|
||||
"node": true,
|
||||
"es6": true
|
||||
},
|
||||
"parser": "babel-eslint",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 6,
|
||||
"sourceType": "module",
|
||||
"ecmaFeatures": {
|
||||
"impliedStrict": true,
|
||||
"experimentalObjectRestSpread": true,
|
||||
"jsx": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.1.2",
|
||||
"@babel/plugin-proposal-class-properties": "^7.1.0",
|
||||
"@babel/plugin-transform-runtime": "^7.1.0",
|
||||
"@babel/preset-env": "^7.1.0",
|
||||
"@babel/preset-flow": "^7.0.0",
|
||||
"@babel/preset-react": "^7.0.0",
|
||||
"@wdio/cli": "^5.10.4",
|
||||
"@wdio/local-runner": "^5.10.4",
|
||||
"@wdio/mocha-framework": "^5.10.1",
|
||||
"@wdio/selenium-standalone-service": "^5.9.3",
|
||||
"@wdio/spec-reporter": "^5.9.3",
|
||||
"@wdio/sync": "^5.10.1",
|
||||
"babel-eslint": "^10.0.1",
|
||||
"babel-loader": "8.0.4",
|
||||
"babel-plugin-istanbul": "^5.0.1",
|
||||
"copy-webpack-plugin": "^4.5.2",
|
||||
"cors": "^2.8.4",
|
||||
"cross-env": "^5.2.0",
|
||||
"css-loader": "^1.0.0",
|
||||
"eslint": "^5.6.1",
|
||||
"eslint-plugin-react": "^7.11.1",
|
||||
"express": "^4.17.1",
|
||||
"file-loader": "^2.0.0",
|
||||
"html-webpack-plugin": "^3.2.0",
|
||||
"is-docker": "^2.0.0",
|
||||
"istanbul": "^0.4.5",
|
||||
"istanbul-lib-coverage": "^2.0.1",
|
||||
"mkdirp": "^0.5.1",
|
||||
"mocha": "^6.1.4",
|
||||
"node-sass": "^4.12.0",
|
||||
"raw-loader": "^0.5.1",
|
||||
"react-hot-loader": "^4.3.11",
|
||||
"sass-loader": "^7.1.0",
|
||||
"selenium-standalone": "^6.16.0",
|
||||
"style-loader": "^0.23.0",
|
||||
"stylelint": "^10.0.0",
|
||||
"stylelint-config-recommended-scss": "^3.2.0",
|
||||
"stylelint-scss": "^3.5.4",
|
||||
"transform-loader": "^0.2.4",
|
||||
"uuid": "^3.3.2",
|
||||
"webdriverio": "^5.10.4",
|
||||
"webpack": "^4.20.2",
|
||||
"webpack-bundle-analyzer": "^3.0.2",
|
||||
"webpack-cleanup-plugin": "^0.5.1",
|
||||
"webpack-cli": "^3.1.2",
|
||||
"webpack-dev-server": "^3.1.9"
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
.cm-s-maputnik.CodeMirror {
|
||||
height: 100%;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.cm-s-maputnik.CodeMirror, .cm-s-maputnik .CodeMirror-gutters {
|
||||
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;
|
||||
}
|
||||
|
||||
.cm-s-maputnik.CodeMirror-focused div.CodeMirror-selected {
|
||||
background: rgba(255, 255, 255, 0.10);
|
||||
}
|
||||
|
||||
.cm-s-maputnik .CodeMirror-line::selection,
|
||||
.cm-s-maputnik .CodeMirror-line > span::selection,
|
||||
.cm-s-maputnik .CodeMirror-line > span > span::selection {
|
||||
background: rgba(255, 255, 255, 0.10);
|
||||
}
|
||||
|
||||
.cm-s-maputnik .CodeMirror-line::-moz-selection,
|
||||
.cm-s-maputnik .CodeMirror-line > span::-moz-selection,
|
||||
.cm-s-maputnik .CodeMirror-line > span > span::-moz-selection {
|
||||
background: rgba(255, 255, 255, 0.10);
|
||||
}
|
||||
|
||||
.cm-s-maputnik span.cm-string, .cm-s-maputnik span.cm-string-2 {
|
||||
color: #8f9d6a;
|
||||
}
|
||||
.cm-s-maputnik span.cm-number { color: #91675f; }
|
||||
.cm-s-maputnik span.cm-property { color: #b8a077; }
|
||||
|
||||
.cm-s-maputnik .CodeMirror-activeline-background {
|
||||
background: rgba(255,255,255,0.1);
|
||||
}
|
||||
|
||||
.cm-s-maputnik .CodeMirror-matchingbracket {
|
||||
text-decoration: underline; color: white !important;
|
||||
}
|
||||
@@ -1,724 +0,0 @@
|
||||
import autoBind from 'react-autobind';
|
||||
import React from 'react'
|
||||
import cloneDeep from 'lodash.clonedeep'
|
||||
import clamp from 'lodash.clamp'
|
||||
import get from 'lodash.get'
|
||||
import {arrayMove} from 'react-sortable-hoc'
|
||||
import url from 'url'
|
||||
|
||||
import MapboxGlMap from './map/MapboxGlMap'
|
||||
import OpenLayersMap from './map/OpenLayersMap'
|
||||
import LayerList from './layers/LayerList'
|
||||
import LayerEditor from './layers/LayerEditor'
|
||||
import Toolbar from './Toolbar'
|
||||
import AppLayout from './AppLayout'
|
||||
import MessagePanel from './MessagePanel'
|
||||
|
||||
import SettingsModal from './modals/SettingsModal'
|
||||
import ExportModal from './modals/ExportModal'
|
||||
import SourcesModal from './modals/SourcesModal'
|
||||
import OpenModal from './modals/OpenModal'
|
||||
import ShortcutsModal from './modals/ShortcutsModal'
|
||||
import SurveyModal from './modals/SurveyModal'
|
||||
import DebugModal from './modals/DebugModal'
|
||||
|
||||
import { downloadGlyphsMetadata, downloadSpriteMetadata } from '../libs/metadata'
|
||||
import {latest, validate} from '@mapbox/mapbox-gl-style-spec'
|
||||
import style from '../libs/style'
|
||||
import { initialStyleUrl, loadStyleUrl, removeStyleQuerystring } from '../libs/urlopen'
|
||||
import { undoMessages, redoMessages } from '../libs/diffmessage'
|
||||
import { 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'
|
||||
|
||||
|
||||
// Similar functionality as <https://github.com/mapbox/mapbox-gl-js/blob/7e30aadf5177486c2cfa14fe1790c60e217b5e56/src/util/mapbox.js>
|
||||
function normalizeSourceURL (url, apiToken="") {
|
||||
const matches = url.match(/^mapbox:\/\/(.*)/);
|
||||
if (matches) {
|
||||
// mapbox://mapbox.mapbox-streets-v7
|
||||
return `https://api.mapbox.com/v4/${matches[1]}.json?secure&access_token=${apiToken}`
|
||||
}
|
||||
else {
|
||||
return url;
|
||||
}
|
||||
}
|
||||
|
||||
function setFetchAccessToken(url, mapStyle) {
|
||||
const matchesTilehosting = url.match(/\.tilehosting\.com/);
|
||||
const matchesMaptiler = url.match(/\.maptiler\.com/);
|
||||
const matchesThunderforest = url.match(/\.thunderforest\.com/);
|
||||
if (matchesTilehosting || matchesMaptiler) {
|
||||
const accessToken = style.getAccessToken("openmaptiles", mapStyle, {allowFallback: true})
|
||||
if (accessToken) {
|
||||
return url.replace('{key}', accessToken)
|
||||
}
|
||||
}
|
||||
else if (matchesThunderforest) {
|
||||
const accessToken = style.getAccessToken("thunderforest", mapStyle, {allowFallback: true})
|
||||
if (accessToken) {
|
||||
return url.replace('{key}', accessToken)
|
||||
}
|
||||
}
|
||||
else {
|
||||
return url;
|
||||
}
|
||||
}
|
||||
|
||||
function updateRootSpec(spec, fieldName, newValues) {
|
||||
return {
|
||||
...spec,
|
||||
$root: {
|
||||
...spec.$root,
|
||||
[fieldName]: {
|
||||
...spec.$root[fieldName],
|
||||
values: newValues
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default class App extends React.Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
autoBind(this);
|
||||
|
||||
this.revisionStore = new RevisionStore()
|
||||
const params = new URLSearchParams(window.location.search.substring(1))
|
||||
let port = params.get("localport")
|
||||
if (port == null && (window.location.port != 80 && window.location.port != 443)) {
|
||||
port = window.location.port
|
||||
}
|
||||
this.styleStore = new ApiStyleStore({
|
||||
onLocalStyleChange: mapStyle => this.onStyleChanged(mapStyle, false),
|
||||
port: port,
|
||||
host: params.get("localhost")
|
||||
})
|
||||
|
||||
|
||||
const shortcuts = [
|
||||
{
|
||||
key: "?",
|
||||
handler: () => {
|
||||
this.toggleModal("shortcuts");
|
||||
}
|
||||
},
|
||||
{
|
||||
key: "o",
|
||||
handler: () => {
|
||||
this.toggleModal("open");
|
||||
}
|
||||
},
|
||||
{
|
||||
key: "e",
|
||||
handler: () => {
|
||||
this.toggleModal("export");
|
||||
}
|
||||
},
|
||||
{
|
||||
key: "d",
|
||||
handler: () => {
|
||||
this.toggleModal("sources");
|
||||
}
|
||||
},
|
||||
{
|
||||
key: "s",
|
||||
handler: () => {
|
||||
this.toggleModal("settings");
|
||||
}
|
||||
},
|
||||
{
|
||||
key: "i",
|
||||
handler: () => {
|
||||
this.setMapState(
|
||||
this.state.mapState === "map" ? "inspect" : "map"
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
key: "m",
|
||||
handler: () => {
|
||||
document.querySelector(".mapboxgl-canvas").focus();
|
||||
}
|
||||
},
|
||||
{
|
||||
key: "!",
|
||||
handler: () => {
|
||||
this.toggleModal("debug");
|
||||
}
|
||||
},
|
||||
]
|
||||
|
||||
document.body.addEventListener("keyup", (e) => {
|
||||
if(e.key === "Escape") {
|
||||
e.target.blur();
|
||||
document.body.focus();
|
||||
}
|
||||
else if(this.state.isOpen.shortcuts || document.activeElement === document.body) {
|
||||
const shortcut = shortcuts.find((shortcut) => {
|
||||
return (shortcut.key === e.key)
|
||||
})
|
||||
|
||||
if(shortcut) {
|
||||
this.setModal("shortcuts", false);
|
||||
shortcut.handler(e);
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const styleUrl = initialStyleUrl()
|
||||
if(styleUrl && window.confirm("Load style from URL: " + styleUrl + " and discard current changes?")) {
|
||||
this.styleStore = new StyleStore()
|
||||
loadStyleUrl(styleUrl, mapStyle => this.onStyleChanged(mapStyle))
|
||||
removeStyleQuerystring()
|
||||
} else {
|
||||
if(styleUrl) {
|
||||
removeStyleQuerystring()
|
||||
}
|
||||
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: {},
|
||||
mapState: "map",
|
||||
spec: latest,
|
||||
isOpen: {
|
||||
settings: false,
|
||||
sources: false,
|
||||
open: false,
|
||||
shortcuts: false,
|
||||
export: false,
|
||||
survey: localStorage.hasOwnProperty('survey') ? false : true,
|
||||
debug: false,
|
||||
},
|
||||
mapboxGlDebugOptions: {
|
||||
showTileBoundaries: false,
|
||||
showCollisionBoxes: false,
|
||||
showOverdrawInspector: false,
|
||||
},
|
||||
openlayersDebugOptions: {
|
||||
debugToolbox: false,
|
||||
},
|
||||
}
|
||||
|
||||
this.layerWatcher = new LayerWatcher({
|
||||
onVectorLayersChange: v => this.setState({ vectorLayers: v })
|
||||
})
|
||||
}
|
||||
|
||||
handleKeyPress = (e) => {
|
||||
if(navigator.platform.toUpperCase().indexOf('MAC') >= 0) {
|
||||
if(e.metaKey && e.shiftKey && e.keyCode === 90) {
|
||||
e.preventDefault();
|
||||
this.onRedo(e);
|
||||
}
|
||||
else if(e.metaKey && e.keyCode === 90) {
|
||||
e.preventDefault();
|
||||
this.onUndo(e);
|
||||
}
|
||||
}
|
||||
else {
|
||||
if(e.ctrlKey && e.keyCode === 90) {
|
||||
e.preventDefault();
|
||||
this.onUndo(e);
|
||||
}
|
||||
else if(e.ctrlKey && e.keyCode === 89) {
|
||||
e.preventDefault();
|
||||
this.onRedo(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
window.addEventListener("keydown", this.handleKeyPress);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener("keydown", this.handleKeyPress);
|
||||
}
|
||||
|
||||
saveStyle(snapshotStyle) {
|
||||
this.styleStore.save(snapshotStyle)
|
||||
}
|
||||
|
||||
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)})
|
||||
})
|
||||
}
|
||||
|
||||
updateIcons(baseUrl) {
|
||||
downloadSpriteMetadata(baseUrl, icons => {
|
||||
this.setState({ spec: updateRootSpec(this.state.spec, 'sprite', icons)})
|
||||
})
|
||||
}
|
||||
|
||||
onChangeMetadataProperty = (property, value) => {
|
||||
// If we're changing renderer reset the map state.
|
||||
if (
|
||||
property === 'maputnik:renderer' &&
|
||||
value !== get(this.state.mapStyle, ['metadata', 'maputnik:renderer'], 'mbgljs')
|
||||
) {
|
||||
this.setState({
|
||||
mapState: 'map'
|
||||
});
|
||||
}
|
||||
|
||||
const changedStyle = {
|
||||
...this.state.mapStyle,
|
||||
metadata: {
|
||||
...this.state.mapStyle.metadata,
|
||||
[property]: value
|
||||
}
|
||||
}
|
||||
this.onStyleChanged(changedStyle)
|
||||
}
|
||||
|
||||
onStyleChanged = (newStyle, save=true) => {
|
||||
|
||||
const errors = validate(newStyle, 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) => {
|
||||
const changedStyle = {
|
||||
...this.state.mapStyle,
|
||||
layers: changedLayers
|
||||
}
|
||||
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)
|
||||
|
||||
changedLayers[idx] = {
|
||||
...changedLayers[idx],
|
||||
id: newId
|
||||
}
|
||||
|
||||
this.onLayersChange(changedLayers)
|
||||
}
|
||||
|
||||
onLayerChanged = (layer) => {
|
||||
const changedLayers = this.state.mapStyle.layers.slice(0)
|
||||
const idx = style.indexOfLayer(changedLayers, layer.id)
|
||||
changedLayers[idx] = layer
|
||||
|
||||
this.onLayersChange(changedLayers)
|
||||
}
|
||||
|
||||
setMapState = (newState) => {
|
||||
this.setState({
|
||||
mapState: newState
|
||||
})
|
||||
}
|
||||
|
||||
setDefaultValues = (styleObj) => {
|
||||
const metadata = styleObj.metadata || {}
|
||||
if(metadata['maputnik:renderer'] === undefined) {
|
||||
const changedStyle = {
|
||||
...styleObj,
|
||||
metadata: {
|
||||
...styleObj.metadata,
|
||||
'maputnik:renderer': 'mbgljs'
|
||||
}
|
||||
}
|
||||
return changedStyle
|
||||
} else {
|
||||
return styleObj
|
||||
}
|
||||
}
|
||||
|
||||
openStyle = (styleObj) => {
|
||||
styleObj = this.setDefaultValues(styleObj)
|
||||
this.onStyleChanged(styleObj)
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
try {
|
||||
url = setFetchAccessToken(url, this.state.mapStyle)
|
||||
} catch(err) {
|
||||
console.warn("Failed to setFetchAccessToken: ", err);
|
||||
}
|
||||
|
||||
fetch(url, {
|
||||
mode: 'cors',
|
||||
})
|
||||
.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
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
_getRenderer () {
|
||||
const metadata = this.state.mapStyle.metadata || {};
|
||||
return metadata['maputnik:renderer'] || 'mbgljs';
|
||||
}
|
||||
|
||||
mapRenderer() {
|
||||
const metadata = this.state.mapStyle.metadata || {};
|
||||
|
||||
const mapProps = {
|
||||
mapStyle: style.replaceAccessTokens(this.state.mapStyle, {allowFallback: true}),
|
||||
onDataChange: (e) => {
|
||||
this.layerWatcher.analyzeMap(e.map)
|
||||
this.fetchSources();
|
||||
},
|
||||
}
|
||||
|
||||
const renderer = this._getRenderer();
|
||||
|
||||
let mapElement;
|
||||
|
||||
// Check if OL code has been loaded?
|
||||
if(renderer === 'ol') {
|
||||
mapElement = <OpenLayersMap
|
||||
{...mapProps}
|
||||
debugToolbox={this.state.openlayersDebugOptions.debugToolbox}
|
||||
onLayerSelect={this.onLayerSelect}
|
||||
/>
|
||||
} else {
|
||||
mapElement = <MapboxGlMap {...mapProps}
|
||||
options={this.state.mapboxGlDebugOptions}
|
||||
inspectModeEnabled={this.state.mapState === "inspect"}
|
||||
highlightedLayer={this.state.mapStyle.layers[this.state.selectedLayerIndex]}
|
||||
onLayerSelect={this.onLayerSelect} />
|
||||
}
|
||||
|
||||
let filterName;
|
||||
if(this.state.mapState.match(/^filter-/)) {
|
||||
filterName = this.state.mapState.replace(/^filter-/, "");
|
||||
}
|
||||
const elementStyle = {};
|
||||
if (filterName) {
|
||||
elementStyle.filter = `url('#${filterName}')`;
|
||||
};
|
||||
|
||||
return <div style={elementStyle} className="maputnik-map__container">
|
||||
{mapElement}
|
||||
</div>
|
||||
}
|
||||
|
||||
onLayerSelect = (layerId) => {
|
||||
const idx = style.indexOfLayer(this.state.mapStyle.layers, layerId)
|
||||
this.setState({ selectedLayerIndex: idx })
|
||||
}
|
||||
|
||||
setModal(modalName, value) {
|
||||
if(modalName === 'survey' && value === false) {
|
||||
localStorage.setItem('survey', '');
|
||||
}
|
||||
|
||||
this.setState({
|
||||
isOpen: {
|
||||
...this.state.isOpen,
|
||||
[modalName]: value
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
toggleModal(modalName) {
|
||||
this.setModal(modalName, !this.state.isOpen[modalName]);
|
||||
}
|
||||
|
||||
onChangeOpenlayersDebug = (key, value) => {
|
||||
this.setState({
|
||||
openlayersDebugOptions: {
|
||||
...this.state.openlayersDebugOptions,
|
||||
[key]: value,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onChangeMaboxGlDebug = (key, value) => {
|
||||
this.setState({
|
||||
mapboxGlDebugOptions: {
|
||||
...this.state.mapboxGlDebugOptions,
|
||||
[key]: value,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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
|
||||
renderer={this._getRenderer()}
|
||||
mapState={this.state.mapState}
|
||||
mapStyle={this.state.mapStyle}
|
||||
inspectModeEnabled={this.state.mapState === "inspect"}
|
||||
sources={this.state.sources}
|
||||
onStyleChanged={this.onStyleChanged}
|
||||
onStyleOpen={this.onStyleChanged}
|
||||
onSetMapState={this.setMapState}
|
||||
onToggleModal={this.toggleModal.bind(this)}
|
||||
/>
|
||||
|
||||
const layerList = <LayerList
|
||||
onMoveLayer={this.onMoveLayer}
|
||||
onLayerDestroy={this.onLayerDestroy}
|
||||
onLayerCopy={this.onLayerCopy}
|
||||
onLayerVisibilityToggle={this.onLayerVisibilityToggle}
|
||||
onLayersChange={this.onLayersChange}
|
||||
onLayerSelect={this.onLayerSelect}
|
||||
selectedLayerIndex={this.state.selectedLayerIndex}
|
||||
layers={layers}
|
||||
sources={this.state.sources}
|
||||
/>
|
||||
|
||||
const layerEditor = selectedLayer ? <LayerEditor
|
||||
layer={selectedLayer}
|
||||
layerIndex={this.state.selectedLayerIndex}
|
||||
isFirstLayer={this.state.selectedLayerIndex < 1}
|
||||
isLastLayer={this.state.selectedLayerIndex === this.state.mapStyle.layers.length-1}
|
||||
sources={this.state.sources}
|
||||
vectorLayers={this.state.vectorLayers}
|
||||
spec={this.state.spec}
|
||||
onMoveLayer={this.onMoveLayer}
|
||||
onLayerChanged={this.onLayerChanged}
|
||||
onLayerDestroy={this.onLayerDestroy}
|
||||
onLayerCopy={this.onLayerCopy}
|
||||
onLayerVisibilityToggle={this.onLayerVisibilityToggle}
|
||||
onLayerIdChange={this.onLayerIdChange}
|
||||
/> : 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>
|
||||
<DebugModal
|
||||
renderer={this._getRenderer()}
|
||||
mapboxGlDebugOptions={this.state.mapboxGlDebugOptions}
|
||||
openlayersDebugOptions={this.state.openlayersDebugOptions}
|
||||
onChangeMaboxGlDebug={this.onChangeMaboxGlDebug}
|
||||
onChangeOpenlayersDebug={this.onChangeOpenlayersDebug}
|
||||
isOpen={this.state.isOpen.debug}
|
||||
onOpenToggle={this.toggleModal.bind(this, 'debug')}
|
||||
/>
|
||||
<ShortcutsModal
|
||||
ref={(el) => this.shortcutEl = el}
|
||||
isOpen={this.state.isOpen.shortcuts}
|
||||
onOpenToggle={this.toggleModal.bind(this, 'shortcuts')}
|
||||
/>
|
||||
<SettingsModal
|
||||
mapStyle={this.state.mapStyle}
|
||||
onStyleChanged={this.onStyleChanged}
|
||||
onChangeMetadataProperty={this.onChangeMetadataProperty}
|
||||
isOpen={this.state.isOpen.settings}
|
||||
onOpenToggle={this.toggleModal.bind(this, 'settings')}
|
||||
openlayersDebugOptions={this.state.openlayersDebugOptions}
|
||||
/>
|
||||
<ExportModal
|
||||
mapStyle={this.state.mapStyle}
|
||||
onStyleChanged={this.onStyleChanged}
|
||||
isOpen={this.state.isOpen.export}
|
||||
onOpenToggle={this.toggleModal.bind(this, 'export')}
|
||||
/>
|
||||
<OpenModal
|
||||
isOpen={this.state.isOpen.open}
|
||||
onStyleOpen={this.openStyle}
|
||||
onOpenToggle={this.toggleModal.bind(this, 'open')}
|
||||
/>
|
||||
<SourcesModal
|
||||
mapStyle={this.state.mapStyle}
|
||||
onStyleChanged={this.onStyleChanged}
|
||||
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,48 +0,0 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import ScrollContainer from './ScrollContainer'
|
||||
|
||||
class AppLayout extends React.Component {
|
||||
static propTypes = {
|
||||
toolbar: PropTypes.element.isRequired,
|
||||
layerList: PropTypes.element.isRequired,
|
||||
layerEditor: PropTypes.element,
|
||||
map: PropTypes.element.isRequired,
|
||||
bottom: PropTypes.element,
|
||||
modals: PropTypes.node,
|
||||
}
|
||||
|
||||
static childContextTypes = {
|
||||
reactIconBase: PropTypes.object
|
||||
}
|
||||
|
||||
getChildContext() {
|
||||
return {
|
||||
reactIconBase: { size: 14 }
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div className="maputnik-layout">
|
||||
{this.props.toolbar}
|
||||
<div className="maputnik-layout-list">
|
||||
<ScrollContainer>
|
||||
{this.props.layerList}
|
||||
</ScrollContainer>
|
||||
</div>
|
||||
<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>
|
||||
}
|
||||
}
|
||||
|
||||
export default AppLayout
|
||||
@@ -1,29 +0,0 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import classnames from 'classnames'
|
||||
|
||||
class Button extends React.Component {
|
||||
static propTypes = {
|
||||
"data-wd-key": PropTypes.string,
|
||||
"aria-label": PropTypes.string,
|
||||
onClick: PropTypes.func,
|
||||
style: PropTypes.object,
|
||||
className: PropTypes.string,
|
||||
children: PropTypes.node,
|
||||
disabled: PropTypes.bool,
|
||||
}
|
||||
|
||||
render() {
|
||||
return <button
|
||||
onClick={this.props.onClick}
|
||||
disabled={this.props.disabled}
|
||||
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}
|
||||
</button>
|
||||
}
|
||||
}
|
||||
|
||||
export default Button
|
||||
@@ -1,27 +0,0 @@
|
||||
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,16 +0,0 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
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,236 +0,0 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import classnames from 'classnames'
|
||||
import {detect} from 'detect-browser';
|
||||
|
||||
import {MdFileDownload, MdOpenInBrowser, MdSettings, MdLayers, MdHelpOutline, MdFindInPage, MdAssignmentTurnedIn} from 'react-icons/md'
|
||||
|
||||
|
||||
import logoImage from 'maputnik-design/logos/logo-color.svg'
|
||||
import pkgJson from '../../package.json'
|
||||
|
||||
|
||||
// This is required because of <https://stackoverflow.com/a/49846426>, there isn't another way to detect support that I'm aware of.
|
||||
const browser = detect();
|
||||
const colorAccessibilityFiltersEnabled = ['chrome', 'firefox'].indexOf(browser.name) > -1;
|
||||
|
||||
|
||||
class IconText extends React.Component {
|
||||
static propTypes = {
|
||||
children: PropTypes.node,
|
||||
}
|
||||
|
||||
render() {
|
||||
return <span className="maputnik-icon-text">{this.props.children}</span>
|
||||
}
|
||||
}
|
||||
|
||||
class ToolbarLink extends React.Component {
|
||||
static propTypes = {
|
||||
className: PropTypes.string,
|
||||
children: PropTypes.node,
|
||||
href: PropTypes.string,
|
||||
onToggleModal: PropTypes.func,
|
||||
}
|
||||
|
||||
render() {
|
||||
return <a
|
||||
className={classnames('maputnik-toolbar-link', 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 ToolbarSelect extends React.Component {
|
||||
static propTypes = {
|
||||
children: PropTypes.node,
|
||||
wdKey: PropTypes.string
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div
|
||||
className='maputnik-toolbar-select'
|
||||
data-wd-key={this.props.wdKey}
|
||||
>
|
||||
{this.props.children}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
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: PropTypes.object.isRequired,
|
||||
inspectModeEnabled: PropTypes.bool.isRequired,
|
||||
onStyleChanged: PropTypes.func.isRequired,
|
||||
// A new style has been uploaded
|
||||
onStyleOpen: PropTypes.func.isRequired,
|
||||
// A dict of source id's and the available source layers
|
||||
sources: PropTypes.object.isRequired,
|
||||
children: PropTypes.node,
|
||||
onToggleModal: PropTypes.func,
|
||||
onSetMapState: PropTypes.func,
|
||||
mapState: PropTypes.string,
|
||||
renderer: PropTypes.string,
|
||||
}
|
||||
|
||||
state = {
|
||||
isOpen: {
|
||||
settings: false,
|
||||
sources: false,
|
||||
open: false,
|
||||
add: false,
|
||||
export: false,
|
||||
}
|
||||
}
|
||||
|
||||
handleSelection(val) {
|
||||
this.props.onSetMapState(val);
|
||||
}
|
||||
|
||||
render() {
|
||||
const views = [
|
||||
{
|
||||
id: "map",
|
||||
title: "Map",
|
||||
},
|
||||
{
|
||||
id: "inspect",
|
||||
title: "Inspect",
|
||||
disabled: this.props.renderer !== 'mbgljs',
|
||||
},
|
||||
{
|
||||
id: "filter-deuteranopia",
|
||||
title: "Map (deuteranopia)",
|
||||
disabled: !colorAccessibilityFiltersEnabled,
|
||||
},
|
||||
{
|
||||
id: "filter-protanopia",
|
||||
title: "Map (protanopia)",
|
||||
disabled: !colorAccessibilityFiltersEnabled,
|
||||
},
|
||||
{
|
||||
id: "filter-tritanopia",
|
||||
title: "Map (tritanopia)",
|
||||
disabled: !colorAccessibilityFiltersEnabled,
|
||||
},
|
||||
{
|
||||
id: "filter-achromatopsia",
|
||||
title: "Map (achromatopsia)",
|
||||
disabled: !colorAccessibilityFiltersEnabled,
|
||||
},
|
||||
];
|
||||
|
||||
const currentView = views.find((view) => {
|
||||
return view.id === this.props.mapState;
|
||||
});
|
||||
|
||||
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>
|
||||
<span className="maputnik-toolbar-name">{pkgJson.name}</span>
|
||||
<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')}>
|
||||
<MdOpenInBrowser />
|
||||
<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')}>
|
||||
<MdLayers />
|
||||
<IconText>Data Sources</IconText>
|
||||
</ToolbarAction>
|
||||
<ToolbarAction wdKey="nav:settings" onClick={this.props.onToggleModal.bind(this, 'settings')}>
|
||||
<MdSettings />
|
||||
<IconText>Style Settings</IconText>
|
||||
</ToolbarAction>
|
||||
|
||||
<ToolbarSelect wdKey="nav:inspect">
|
||||
<MdFindInPage />
|
||||
<IconText>View </IconText>
|
||||
<select onChange={(e) => this.handleSelection(e.target.value)} value={currentView.id}>
|
||||
{views.map((item) => {
|
||||
return (
|
||||
<option key={item.id} value={item.id} disabled={item.disabled}>
|
||||
{item.title}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
</ToolbarSelect>
|
||||
|
||||
<ToolbarLink href={"https://github.com/maputnik/editor/wiki"}>
|
||||
<MdHelpOutline />
|
||||
<IconText>Help</IconText>
|
||||
</ToolbarLink>
|
||||
<ToolbarLinkHighlighted href={"https://gregorywolanski.typeform.com/to/cPgaSY"}>
|
||||
<MdAssignmentTurnedIn />
|
||||
<IconText>Take the Maputnik Survey</IconText>
|
||||
</ToolbarLinkHighlighted>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -1,123 +0,0 @@
|
||||
import React from 'react'
|
||||
import Color from 'color'
|
||||
import ChromePicker from 'react-color/lib/components/chrome/Chrome'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
function formatColor(color) {
|
||||
const rgb = color.rgb
|
||||
return `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${rgb.a})`
|
||||
}
|
||||
|
||||
/*** Number fields with support for min, max and units and documentation*/
|
||||
class ColorField extends React.Component {
|
||||
static propTypes = {
|
||||
onChange: PropTypes.func.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
value: PropTypes.string,
|
||||
doc: PropTypes.string,
|
||||
style: PropTypes.object,
|
||||
default: PropTypes.string,
|
||||
}
|
||||
|
||||
state = {
|
||||
pickerOpened: false
|
||||
}
|
||||
|
||||
//TODO: I much rather would do this with absolute positioning
|
||||
//but I am too stupid to get it to work together with fixed position
|
||||
//and scrollbars so I have to fallback to JavaScript
|
||||
calcPickerOffset = () => {
|
||||
const elem = this.colorInput
|
||||
if(elem) {
|
||||
const pos = elem.getBoundingClientRect()
|
||||
return {
|
||||
top: pos.top,
|
||||
left: pos.left + 196,
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
top: 160,
|
||||
left: 555,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
togglePicker = () => {
|
||||
this.setState({ pickerOpened: !this.state.pickerOpened })
|
||||
}
|
||||
|
||||
get color() {
|
||||
// Catch invalid color.
|
||||
try {
|
||||
return Color(this.props.value).rgb()
|
||||
}
|
||||
catch(err) {
|
||||
console.warn("Error parsing color: ", err);
|
||||
return Color("rgb(255,255,255)");
|
||||
}
|
||||
}
|
||||
|
||||
onChange (v) {
|
||||
this.props.onChange(v === "" ? undefined : v);
|
||||
}
|
||||
|
||||
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,
|
||||
left: offset.left,
|
||||
top: offset.top,
|
||||
}}>
|
||||
<ChromePicker
|
||||
color={currentColor}
|
||||
onChange={c => this.props.onChange(formatColor(c))}
|
||||
/>
|
||||
<div
|
||||
className="maputnik-color-picker-offset"
|
||||
onClick={this.togglePicker}
|
||||
style={{
|
||||
zIndex: -1,
|
||||
position: 'fixed',
|
||||
top: '0px',
|
||||
right: '0px',
|
||||
bottom: '0px',
|
||||
left: '0px',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
var swatchStyle = {
|
||||
backgroundColor: this.props.value
|
||||
};
|
||||
|
||||
return <div className="maputnik-color-wrapper">
|
||||
{this.state.pickerOpened && picker}
|
||||
<div className="maputnik-color-swatch" style={swatchStyle}></div>
|
||||
<input
|
||||
spellCheck="false"
|
||||
className="maputnik-color"
|
||||
ref={(input) => this.colorInput = input}
|
||||
onClick={this.togglePicker}
|
||||
style={this.props.style}
|
||||
name={this.props.name}
|
||||
placeholder={this.props.default}
|
||||
value={this.props.value ? this.props.value : ""}
|
||||
onChange={(e) => this.onChange(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
export default ColorField
|
||||
@@ -1,23 +0,0 @@
|
||||
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,151 +0,0 @@
|
||||
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
|
||||
]),
|
||||
}
|
||||
|
||||
getFieldFunctionType(fieldSpec) {
|
||||
if (fieldSpec.expression.interpolated) {
|
||||
return "exponential"
|
||||
}
|
||||
if (fieldSpec.type === "number") {
|
||||
return "interval"
|
||||
}
|
||||
return "categorical"
|
||||
}
|
||||
|
||||
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 functionType = this.getFieldFunctionType(this.props.fieldSpec);
|
||||
const stopValue = functionType === 'categorical' ? '' : 0;
|
||||
const dataFunc = {
|
||||
property: "",
|
||||
type: functionType,
|
||||
stops: [
|
||||
[{zoom: 6, value: stopValue}, this.props.value || stopValue],
|
||||
[{zoom: 10, value: stopValue}, this.props.value || stopValue]
|
||||
]
|
||||
}
|
||||
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}
|
||||
onAddStop={this.addStop}
|
||||
/>
|
||||
)
|
||||
}
|
||||
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}
|
||||
onAddStop={this.addStop}
|
||||
/>
|
||||
)
|
||||
}
|
||||
else {
|
||||
specField = (
|
||||
<SpecProperty
|
||||
onChange={this.props.onChange.bind(this)}
|
||||
fieldName={this.props.fieldName}
|
||||
fieldSpec={this.props.fieldSpec}
|
||||
value={this.props.value}
|
||||
onZoomClick={this.makeZoomFunction}
|
||||
onDataClick={this.makeDataFunction}
|
||||
/>
|
||||
)
|
||||
}
|
||||
return <div className={propClass} data-wd-key={"spec-field:"+this.props.fieldName}>
|
||||
{specField}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
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(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(spec, layerType, fieldName) {
|
||||
const paint = spec['paint_' + layerType] || {}
|
||||
if (fieldName in paint) {
|
||||
return 'paint'
|
||||
} else {
|
||||
return 'layout'
|
||||
}
|
||||
}
|
||||
|
||||
export default class PropertyGroup extends React.Component {
|
||||
static propTypes = {
|
||||
layer: PropTypes.object.isRequired,
|
||||
groupFields: PropTypes.array.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
spec: PropTypes.object.isRequired,
|
||||
}
|
||||
|
||||
onPropertyChange = (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.spec, this.props.layer.type, fieldName)
|
||||
|
||||
const paint = this.props.layer.paint || {}
|
||||
const layout = this.props.layer.layout || {}
|
||||
const fieldValue = fieldName in paint ? paint[fieldName] : layout[fieldName]
|
||||
|
||||
return <FunctionSpecField
|
||||
onChange={this.onPropertyChange}
|
||||
key={fieldName}
|
||||
fieldName={fieldName}
|
||||
value={fieldValue === undefined ? fieldSpec.default : fieldValue}
|
||||
fieldSpec={fieldSpec}
|
||||
/>
|
||||
})
|
||||
|
||||
return <div className="maputnik-property-group">
|
||||
{fields}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -1,136 +0,0 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import ColorField from './ColorField'
|
||||
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'
|
||||
|
||||
const iconProperties = ['background-pattern', 'fill-pattern', 'line-pattern', 'fill-extrusion-pattern', 'icon-image']
|
||||
|
||||
function labelFromFieldName(fieldName) {
|
||||
let label = fieldName.split('-').slice(1).join(' ')
|
||||
if(label.length > 0) {
|
||||
label = label.charAt(0).toUpperCase() + label.slice(1);
|
||||
}
|
||||
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: 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: PropTypes.object,
|
||||
}
|
||||
|
||||
render() {
|
||||
const commonProps = {
|
||||
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)
|
||||
}
|
||||
|
||||
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 'formatted':
|
||||
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,182 +0,0 @@
|
||||
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.expression.interpolated) {
|
||||
return "exponential"
|
||||
}
|
||||
if (fieldSpec.type === "number") {
|
||||
return "interval"
|
||||
}
|
||||
return "categorical"
|
||||
}
|
||||
|
||||
getDataFunctionTypes(fieldSpec) {
|
||||
if (fieldSpec.expression.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)}
|
||||
/>
|
||||
</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>
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import DocLabel from './DocLabel'
|
||||
import Button from '../Button'
|
||||
import {MdDelete} from 'react-icons/md'
|
||||
|
||||
|
||||
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={<MdDelete />}
|
||||
doc={"Remove zoom level stop."}
|
||||
/>
|
||||
</Button>
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import DocLabel from './DocLabel'
|
||||
import Button from '../Button'
|
||||
import {MdFunctions, MdInsertChart} from 'react-icons/md'
|
||||
|
||||
|
||||
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={<MdFunctions />}
|
||||
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-type'] === 'data-driven') {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
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>
|
||||
}
|
||||
}
|
||||
@@ -1,163 +0,0 @@
|
||||
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'
|
||||
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
function setStopRefs(props, state) {
|
||||
// This is initialsed below only if required to improved performance.
|
||||
let newRefs;
|
||||
|
||||
if(props.value && props.value.stops) {
|
||||
props.value.stops.forEach((val, idx) => {
|
||||
if(!state.refs.hasOwnProperty(idx)) {
|
||||
if(!newRefs) {
|
||||
newRefs = {...state};
|
||||
}
|
||||
newRefs[idx] = docUid("stop-");
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return newRefs;
|
||||
}
|
||||
|
||||
|
||||
export default class 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
|
||||
]),
|
||||
}
|
||||
|
||||
state = {
|
||||
refs: {}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const newRefs = setStopRefs(this.props, this.state);
|
||||
|
||||
if(newRefs) {
|
||||
this.setState({
|
||||
refs: newRefs
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps(props, state) {
|
||||
const newRefs = setStopRefs(props, state);
|
||||
if(newRefs) {
|
||||
return {
|
||||
refs: newRefs
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// 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>
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
import capitalize from 'lodash.capitalize'
|
||||
|
||||
export default function labelFromFieldName(fieldName) {
|
||||
let label = fieldName.split('-').slice(1).join(' ')
|
||||
return capitalize(label)
|
||||
}
|
||||
@@ -1,111 +0,0 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { combiningFilterOps } from '../../libs/filterops.js'
|
||||
|
||||
import {latest} from '@mapbox/mapbox-gl-style-spec'
|
||||
import DocLabel from '../fields/DocLabel'
|
||||
import SelectInput from '../inputs/SelectInput'
|
||||
import SingleFilterEditor from './SingleFilterEditor'
|
||||
import FilterEditorBlock from './FilterEditorBlock'
|
||||
import Button from '../Button'
|
||||
|
||||
function hasCombiningFilter(filter) {
|
||||
return combiningFilterOps.indexOf(filter[0]) >= 0
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
export default class CombiningFilterEditor extends React.Component {
|
||||
static propTypes = {
|
||||
/** Properties of the vector layer and the available fields */
|
||||
properties: PropTypes.object,
|
||||
filter: PropTypes.array,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
// Convert filter to combining filter
|
||||
combiningFilter() {
|
||||
let filter = this.props.filter || ['all']
|
||||
|
||||
let combiningOp = filter[0]
|
||||
let filters = filter.slice(1)
|
||||
|
||||
if(combiningFilterOps.indexOf(combiningOp) < 0) {
|
||||
combiningOp = 'all'
|
||||
filters = [filter.slice(0)]
|
||||
}
|
||||
|
||||
return [combiningOp, ...filters]
|
||||
}
|
||||
|
||||
onFilterPartChanged(filterIdx, newPart) {
|
||||
const newFilter = this.combiningFilter().slice(0)
|
||||
newFilter[filterIdx] = newPart
|
||||
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 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>
|
||||
})
|
||||
|
||||
//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={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}>
|
||||
Add filter
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import Button from '../Button'
|
||||
import {MdDelete} from 'react-icons/md'
|
||||
|
||||
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}
|
||||
>
|
||||
<MdDelete />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="maputnik-filter-editor-block-content">
|
||||
{this.props.children}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
export default FilterEditorBlock
|
||||
@@ -1,93 +0,0 @@
|
||||
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
|
||||
@@ -1,15 +0,0 @@
|
||||
import React from 'react'
|
||||
import IconBase from 'react-icon-base'
|
||||
|
||||
|
||||
export default class BackgroundIcon extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<IconBase viewBox="0 0 20 20" {...this.props}>
|
||||
<path d="m 1.821019,10.255581 7.414535,5.020197 c 0.372277,0.25206 0.958697,0.239771 1.30985,-0.02745 L 17.539255,9.926162 C 17.89041,9.658941 17.873288,9.238006 17.501015,8.985946 L 10.08648,3.9657402 C 9.714204,3.7136802 9.127782,3.7259703 8.776627,3.9931918 L 1.782775,9.315365 c -0.3511551,0.267221 -0.3340331,0.688156 0.03824,0.940216 z" />
|
||||
</IconBase>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
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,15 +0,0 @@
|
||||
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 d="M 2.84978,9.763512 9.462149,4.7316391 16.47225,9.478015 9.859886,14.509879 2.84978,9.763512 m -1.028761,0.492069 7.414535,5.020197 c 0.372277,0.25206 0.958697,0.239771 1.30985,-0.02745 L 17.539255,9.926162 C 17.89041,9.658941 17.873288,9.238006 17.501015,8.985946 L 10.08648,3.9657402 C 9.714204,3.7136802 9.127782,3.7259703 8.776627,3.9931918 L 1.782775,9.315365 c -0.3511551,0.267221 -0.3340331,0.688156 0.03824,0.940216 l 0,0 z" />
|
||||
</IconBase>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
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: 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} />
|
||||
case 'circle': return <CircleIcon {...iconProps} />
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default LayerIcon
|
||||
@@ -1,15 +0,0 @@
|
||||
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 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>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
import React from 'react'
|
||||
import IconBase from 'react-icon-base'
|
||||
|
||||
|
||||
export default class SymbolIcon extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<IconBase viewBox="0 0 20 20" {...this.props}>
|
||||
<g transform="matrix(1.2718518,0,0,1.2601269,16.559526,-7.4065264)">
|
||||
<path d="m -9.7959773,11.060163 c -0.3734787,-0.724437 -0.3580577,-1.2147051 -0.00547,-1.8767873 l 8.6034029,-0.019416 c 0.39670292,0.6865644 0.38365934,1.4750693 -0.011097,1.8864953 l -3.1359613,-0.0033 -0.013695,7.1305 c -0.4055357,0.397083 -1.3146432,0.397083 -1.7769191,-0.02274 l 0.030226,-7.104422 z" />
|
||||
</g>
|
||||
</IconBase>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
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
|
||||
@@ -1,98 +0,0 @@
|
||||
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
|
||||
}
|
||||
|
||||
state = {
|
||||
maxHeight: MAX_HEIGHT
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
onChange: () => {},
|
||||
options: [],
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
onChange (v) {
|
||||
this.props.onChange(v === "" ? undefined : v);
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div
|
||||
ref={(el) => {
|
||||
this.autocompleteMenuEl = el;
|
||||
}}
|
||||
>
|
||||
<Autocomplete
|
||||
menuStyle={{
|
||||
position: "fixed",
|
||||
overflow: "auto",
|
||||
maxHeight: this.state.maxHeight,
|
||||
zIndex: '998'
|
||||
}}
|
||||
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.onChange(v)}
|
||||
onChange={(e, v) => this.onChange(v)}
|
||||
shouldItemRender={(item, value="") => {
|
||||
if (typeof(value) === "string") {
|
||||
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
|
||||
@@ -1,31 +0,0 @@
|
||||
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
|
||||
@@ -1,105 +0,0 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import StringInput from './StringInput'
|
||||
import NumberInput from './NumberInput'
|
||||
import Button from '../Button'
|
||||
import {MdDelete} from 'react-icons/md'
|
||||
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}
|
||||
>
|
||||
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={<MdDelete />}
|
||||
doc={"Remove array entry."}
|
||||
/>
|
||||
</Button>
|
||||
}
|
||||
}
|
||||
|
||||
export default DynamicArrayInput
|
||||
@@ -1,56 +0,0 @@
|
||||
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() {
|
||||
const out = this.props.value || this.props.default.slice(1) || [""];
|
||||
|
||||
// Always put a "" in the last field to you can keep adding entries
|
||||
if (out[out.length-1] !== ""){
|
||||
return out.concat("");
|
||||
}
|
||||
else {
|
||||
return out;
|
||||
}
|
||||
}
|
||||
|
||||
changeFont(idx, newValue) {
|
||||
const changedValues = this.values.slice(0)
|
||||
changedValues[idx] = newValue
|
||||
const filteredValues = changedValues
|
||||
.filter(v => v !== undefined)
|
||||
.filter(v => v !== "")
|
||||
|
||||
this.props.onChange(filteredValues);
|
||||
}
|
||||
|
||||
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
|
||||
@@ -1,28 +0,0 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import AutocompleteInput from './AutocompleteInput'
|
||||
|
||||
|
||||
class IconInput extends React.Component {
|
||||
static propTypes = {
|
||||
value: PropTypes.string,
|
||||
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,59 +0,0 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import classnames from 'classnames'
|
||||
import DocLabel from '../fields/DocLabel'
|
||||
|
||||
/** Wrap a component with a label */
|
||||
class InputBlock extends React.Component {
|
||||
static propTypes = {
|
||||
"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) {
|
||||
const value = e.target.value
|
||||
return this.props.onChange(value === "" ? undefined : value)
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div style={this.props.style}
|
||||
data-wd-key={this.props["data-wd-key"]}
|
||||
className={classnames({
|
||||
"maputnik-input-block": true,
|
||||
"maputnik-action-block": this.props.action
|
||||
})}
|
||||
>
|
||||
{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>
|
||||
}
|
||||
}
|
||||
|
||||
export default InputBlock
|
||||
@@ -1,36 +0,0 @@
|
||||
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
|
||||
@@ -1,86 +0,0 @@
|
||||
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 = {
|
||||
editing: false,
|
||||
value: props.value,
|
||||
}
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps(props, state) {
|
||||
if (!state.editing) {
|
||||
return {
|
||||
value: props.value
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
changeValue(newValue) {
|
||||
this.setState({editing: true});
|
||||
const value = parseFloat(newValue)
|
||||
|
||||
const hasChanged = this.state.value !== value
|
||||
if(this.isValid(value) && hasChanged) {
|
||||
this.props.onChange(value)
|
||||
}
|
||||
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 = () => {
|
||||
this.setState({editing: false});
|
||||
// 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}
|
||||
/>
|
||||
}
|
||||
}
|
||||
|
||||
export default NumberInput
|
||||
@@ -1,32 +0,0 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
class SelectInput extends React.Component {
|
||||
static propTypes = {
|
||||
value: PropTypes.string.isRequired,
|
||||
"data-wd-key": PropTypes.string,
|
||||
options: PropTypes.array.isRequired,
|
||||
style: PropTypes.object,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
|
||||
render() {
|
||||
let options = this.props.options
|
||||
if(options.length > 0 && !Array.isArray(options[0])) {
|
||||
options = options.map(v => [v, v])
|
||||
}
|
||||
|
||||
return <select
|
||||
className="maputnik-select"
|
||||
data-wd-key={this.props["data-wd-key"]}
|
||||
style={this.props.style}
|
||||
value={this.props.value}
|
||||
onChange={e => this.props.onChange(e.target.value)}
|
||||
>
|
||||
{ options.map(([val, label]) => <option key={val} value={val}>{label}</option>) }
|
||||
</select>
|
||||
}
|
||||
}
|
||||
|
||||
export default SelectInput
|
||||
@@ -1,71 +0,0 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
class StringInput extends React.Component {
|
||||
static propTypes = {
|
||||
"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 = {
|
||||
editing: false,
|
||||
value: props.value || ''
|
||||
}
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps(props, state) {
|
||||
if (!state.editing) {
|
||||
return {
|
||||
value: props.value
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
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({
|
||||
editing: true,
|
||||
value: e.target.value
|
||||
})
|
||||
},
|
||||
onBlur: () => {
|
||||
if(this.state.value!==this.props.value) {
|
||||
this.setState({editing: false});
|
||||
this.props.onChange(this.state.value);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default StringInput
|
||||
@@ -1,34 +0,0 @@
|
||||
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
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
isActive: true
|
||||
}
|
||||
|
||||
render() {
|
||||
if (accessibility.reducedMotionEnabled()) {
|
||||
return (
|
||||
<div style={{display: this.props.isActive ? "block" : "none"}}>
|
||||
{this.props.children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
else {
|
||||
return (
|
||||
<Collapse isOpened={this.props.isActive}>
|
||||
{this.props.children}
|
||||
</Collapse>
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import {MdArrowDropDown, MdArrowDropUp} from 'react-icons/md'
|
||||
|
||||
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 ? <MdArrowDropUp style={iconStyle}/> : <MdArrowDropDown style={iconStyle} />
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
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,81 +0,0 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import {Controlled as CodeMirror} from 'react-codemirror2'
|
||||
import InputBlock from '../inputs/InputBlock'
|
||||
import StringInput from '../inputs/StringInput'
|
||||
|
||||
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: PropTypes.object.isRequired,
|
||||
onChange: PropTypes.func,
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
code: JSON.stringify(props.layer, null, 2)
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (prevProps.layer !== this.props.layer) {
|
||||
this.setState({
|
||||
code: JSON.stringify(this.props.layer, null, 2)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
onCodeUpdate(newCode) {
|
||||
try {
|
||||
const parsedLayer = JSON.parse(newCode)
|
||||
this.props.onChange(parsedLayer)
|
||||
} catch(err) {
|
||||
console.warn(err)
|
||||
} finally {
|
||||
this.setState({
|
||||
code: newCode
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
resetValue() {
|
||||
console.log('reset')
|
||||
this.setState({
|
||||
code: JSON.stringify(this.props.layer, null, 2)
|
||||
})
|
||||
}
|
||||
|
||||
render() {
|
||||
const codeMirrorOptions = {
|
||||
mode: {name: "javascript", json: true},
|
||||
tabSize: 2,
|
||||
theme: 'maputnik',
|
||||
viewportMargin: Infinity,
|
||||
lineNumbers: true,
|
||||
lint: true,
|
||||
gutters: ["CodeMirror-lint-markers"],
|
||||
scrollbarStyle: "null",
|
||||
}
|
||||
|
||||
return <CodeMirror
|
||||
value={this.state.code}
|
||||
onBeforeChange={(editor, data, value) => this.onCodeUpdate(value)}
|
||||
onFocusChange={focused => focused ? true : this.resetValue()}
|
||||
options={codeMirrorOptions}
|
||||
/>
|
||||
}
|
||||
}
|
||||
|
||||
export default JSONEditor
|
||||
@@ -1,279 +0,0 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { Wrapper, Button, Menu, MenuItem } from 'react-aria-menubutton'
|
||||
|
||||
import JSONEditor from './JSONEditor'
|
||||
import FilterEditor from '../filter/FilterEditor'
|
||||
import PropertyGroup from '../fields/PropertyGroup'
|
||||
import LayerEditorGroup from './LayerEditorGroup'
|
||||
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 {MdMoreVert} from 'react-icons/md'
|
||||
|
||||
import { changeType, changeProperty } from '../../libs/layer'
|
||||
import layout from '../../config/layout.json'
|
||||
|
||||
|
||||
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: 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 = {
|
||||
onLayerChanged: () => {},
|
||||
onLayerIdChange: () => {},
|
||||
onLayerDestroyed: () => {},
|
||||
}
|
||||
|
||||
static childContextTypes = {
|
||||
reactIconBase: PropTypes.object
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
//TODO: Clean this up and refactor into function
|
||||
const editorGroups = {}
|
||||
layoutGroups(this.props.layer.type).forEach(group => {
|
||||
editorGroups[group.title] = true
|
||||
})
|
||||
|
||||
this.state = { editorGroups }
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps(props, state) {
|
||||
const additionalGroups = { ...state.editorGroups }
|
||||
|
||||
layout[props.layer.type].groups.forEach(group => {
|
||||
if(!(group.title in additionalGroups)) {
|
||||
additionalGroups[group.title] = true
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
editorGroups: additionalGroups
|
||||
};
|
||||
}
|
||||
|
||||
getChildContext () {
|
||||
return {
|
||||
reactIconBase: {
|
||||
size: 14,
|
||||
color: '#8e8e8e',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
changeProperty(group, property, newValue) {
|
||||
this.props.onLayerChanged(changeProperty(this.props.layer, group, property, newValue))
|
||||
}
|
||||
|
||||
onGroupToggle(groupTitle, active) {
|
||||
const changedActiveGroups = {
|
||||
...this.state.editorGroups,
|
||||
[groupTitle]: active,
|
||||
}
|
||||
this.setState({
|
||||
editorGroups: changedActiveGroups
|
||||
})
|
||||
}
|
||||
|
||||
renderGroupType(type, fields) {
|
||||
let comment = ""
|
||||
if(this.props.layer.metadata) {
|
||||
comment = this.props.layer.metadata['maputnik:comment']
|
||||
}
|
||||
|
||||
let sourceLayerIds;
|
||||
if(this.props.sources.hasOwnProperty(this.props.layer.source)) {
|
||||
sourceLayerIds = this.props.sources[this.props.layer.source].layers;
|
||||
}
|
||||
|
||||
switch(type) {
|
||||
case 'layer': return <div>
|
||||
<LayerIdBlock
|
||||
value={this.props.layer.id}
|
||||
wdKey="layer-editor.layer-id"
|
||||
onChange={newId => this.props.onLayerIdChange(this.props.layer.id, newId)}
|
||||
/>
|
||||
<LayerTypeBlock
|
||||
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}
|
||||
spec={this.props.spec}
|
||||
onChange={this.changeProperty.bind(this)}
|
||||
/>
|
||||
case 'jsoneditor': return <JSONEditor
|
||||
layer={this.props.layer}
|
||||
onChange={this.props.onLayerChanged}
|
||||
/>
|
||||
}
|
||||
}
|
||||
|
||||
moveLayer(offset) {
|
||||
this.props.onMoveLayer({
|
||||
oldIndex: this.props.layerIndex,
|
||||
newIndex: this.props.layerIndex+offset
|
||||
})
|
||||
}
|
||||
|
||||
render() {
|
||||
const layerType = this.props.layer.type
|
||||
const groups = layoutGroups(layerType).filter(group => {
|
||||
return !(layerType === 'background' && group.type === 'source')
|
||||
}).map(group => {
|
||||
return <LayerEditorGroup
|
||||
data-wd-key={group.title}
|
||||
key={group.title}
|
||||
title={group.title}
|
||||
isActive={this.state.editorGroups[group.title]}
|
||||
onActiveToggle={this.onGroupToggle.bind(this, group.title)}
|
||||
>
|
||||
{this.renderGroupType(group.type, group.fields)}
|
||||
</LayerEditorGroup>
|
||||
})
|
||||
|
||||
const layout = this.props.layer.layout || {}
|
||||
|
||||
const items = {
|
||||
delete: {
|
||||
text: "Delete",
|
||||
handler: () => this.props.onLayerDestroy(this.props.layer.id)
|
||||
},
|
||||
duplicate: {
|
||||
text: "Duplicate",
|
||||
handler: () => this.props.onLayerCopy(this.props.layer.id)
|
||||
},
|
||||
hide: {
|
||||
text: (layout.visibility === "none") ? "Show" : "Hide",
|
||||
handler: () => this.props.onLayerVisibilityToggle(this.props.layer.id)
|
||||
},
|
||||
moveLayerUp: {
|
||||
text: "Move layer up",
|
||||
// Not actually used...
|
||||
disabled: this.props.isFirstLayer,
|
||||
handler: () => this.moveLayer(-1)
|
||||
},
|
||||
moveLayerDown: {
|
||||
text: "Move layer down",
|
||||
// Not actually used...
|
||||
disabled: this.props.isLastLayer,
|
||||
handler: () => this.moveLayer(+1)
|
||||
}
|
||||
}
|
||||
|
||||
function handleSelection(id, event) {
|
||||
event.stopPropagation;
|
||||
items[id].handler();
|
||||
}
|
||||
|
||||
return <div className="maputnik-layer-editor"
|
||||
>
|
||||
<header>
|
||||
<div className="layer-header">
|
||||
<h2 className="layer-header__title">
|
||||
Layer: {this.props.layer.id}
|
||||
</h2>
|
||||
<div className="layer-header__info">
|
||||
<Wrapper
|
||||
className='more-menu'
|
||||
onSelection={handleSelection}
|
||||
closeOnSelection={false}
|
||||
>
|
||||
<Button className='more-menu__button'>
|
||||
<MdMoreVert 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,33 +0,0 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import Collapser from './Collapser'
|
||||
import Collapse from './Collapse'
|
||||
|
||||
|
||||
export default class LayerEditorGroup extends React.Component {
|
||||
static propTypes = {
|
||||
"data-wd-key": PropTypes.string,
|
||||
title: PropTypes.string.isRequired,
|
||||
isActive: PropTypes.bool.isRequired,
|
||||
children: PropTypes.element.isRequired,
|
||||
onActiveToggle: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div>
|
||||
<div className="maputnik-layer-editor-group"
|
||||
data-wd-key={"layer-editor-group:"+this.props["data-wd-key"]}
|
||||
onClick={e => this.props.onActiveToggle(!this.props.isActive)}
|
||||
>
|
||||
<span>{this.props.title}</span>
|
||||
<span style={{flexGrow: 1}} />
|
||||
<Collapser isCollapsed={this.props.isActive} />
|
||||
</div>
|
||||
<Collapse isActive={this.props.isActive}>
|
||||
<div className="react-collapse-container">
|
||||
{this.props.children}
|
||||
</div>
|
||||
</Collapse>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import {latest} from '@mapbox/mapbox-gl-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={latest.layer.id.doc}
|
||||
data-wd-key={this.props.wdKey}
|
||||
>
|
||||
<StringInput
|
||||
value={this.props.value}
|
||||
onChange={this.props.onChange}
|
||||
/>
|
||||
</InputBlock>
|
||||
}
|
||||
}
|
||||
|
||||
export default LayerIdBlock
|
||||
@@ -1,212 +0,0 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import classnames from 'classnames'
|
||||
|
||||
import LayerListGroup from './LayerListGroup'
|
||||
import LayerListItem from './LayerListItem'
|
||||
import AddModal from '../modals/AddModal'
|
||||
|
||||
import {SortableContainer} from 'react-sortable-hoc';
|
||||
|
||||
const layerListPropTypes = {
|
||||
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
|
||||
class LayerListContainer extends React.Component {
|
||||
static propTypes = {...layerListPropTypes}
|
||||
static defaultProps = {
|
||||
onLayerSelect: () => {},
|
||||
}
|
||||
|
||||
state = {
|
||||
collapsedGroups: {},
|
||||
areAllGroupsExpanded: false,
|
||||
isOpen: {
|
||||
add: false,
|
||||
}
|
||||
}
|
||||
|
||||
toggleModal(modalName) {
|
||||
this.setState({
|
||||
isOpen: {
|
||||
...this.state.isOpen,
|
||||
[modalName]: !this.state.isOpen[modalName]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
toggleLayers = () => {
|
||||
let idx=0
|
||||
|
||||
let newGroups=[]
|
||||
|
||||
this.groupedLayers().forEach(layers => {
|
||||
const groupPrefix = layerPrefix(layers[0].id)
|
||||
const lookupKey = [groupPrefix, idx].join('-')
|
||||
|
||||
|
||||
if (layers.length > 1) {
|
||||
newGroups[lookupKey] = this.state.areAllGroupsExpanded
|
||||
}
|
||||
|
||||
layers.forEach((layer) => {
|
||||
idx += 1
|
||||
})
|
||||
});
|
||||
|
||||
this.setState({
|
||||
collapsedGroups: newGroups,
|
||||
areAllGroupsExpanded: !this.state.areAllGroupsExpanded
|
||||
})
|
||||
}
|
||||
|
||||
groupedLayers() {
|
||||
const groups = []
|
||||
for (let i = 0; i < this.props.layers.length; i++) {
|
||||
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 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 <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}
|
||||
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>
|
||||
}
|
||||
}
|
||||
|
||||
const LayerListContainerSortable = SortableContainer((props) => <LayerListContainer {...props} />)
|
||||
|
||||
export default class LayerList extends React.Component {
|
||||
static propTypes = {...layerListPropTypes}
|
||||
|
||||
render() {
|
||||
return <LayerListContainerSortable
|
||||
{...this.props}
|
||||
helperClass='sortableHelper'
|
||||
onSortEnd={this.props.onMoveLayer.bind(this)}
|
||||
useDragHandle={true}
|
||||
/>
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
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,134 +0,0 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import classnames from 'classnames'
|
||||
|
||||
import {MdContentCopy, MdVisibility, MdVisibilityOff, MdDelete} from 'react-icons/md'
|
||||
|
||||
import LayerIcon from '../icons/LayerIcon'
|
||||
import {SortableElement, SortableHandle} from 'react-sortable-hoc'
|
||||
|
||||
|
||||
const DraggableLabel = SortableHandle((props) => {
|
||||
return <div className="maputnik-layer-list-item-handle">
|
||||
<LayerIcon
|
||||
className="layer-handle__icon"
|
||||
type={props.layerType}
|
||||
/>
|
||||
<span className="maputnik-layer-list-item-id">{props.layerId}</span>
|
||||
</div>
|
||||
});
|
||||
|
||||
class IconAction extends React.Component {
|
||||
static propTypes = {
|
||||
action: PropTypes.string.isRequired,
|
||||
onClick: PropTypes.func.isRequired,
|
||||
wdKey: PropTypes.string,
|
||||
classBlockName: PropTypes.string,
|
||||
classBlockModifier: PropTypes.string,
|
||||
}
|
||||
|
||||
renderIcon() {
|
||||
switch(this.props.action) {
|
||||
case 'duplicate': return <MdContentCopy />
|
||||
case 'show': return <MdVisibility />
|
||||
case 'hide': return <MdVisibilityOff />
|
||||
case 'delete': return <MdDelete />
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {classBlockName, classBlockModifier} = this.props;
|
||||
|
||||
let classAdditions = '';
|
||||
if (classBlockName) {
|
||||
classAdditions = `maputnik-layer-list-icon-action__${classBlockName}`;
|
||||
|
||||
if (classBlockModifier) {
|
||||
classAdditions += ` maputnik-layer-list-icon-action__${classBlockName}--${classBlockModifier}`;
|
||||
}
|
||||
}
|
||||
|
||||
return <button
|
||||
tabIndex="-1"
|
||||
title={this.props.action}
|
||||
className={`maputnik-layer-list-icon-action ${classAdditions}`}
|
||||
data-wd-key={this.props.wdKey}
|
||||
onClick={this.props.onClick}
|
||||
>
|
||||
{this.renderIcon()}
|
||||
</button>
|
||||
}
|
||||
}
|
||||
|
||||
class LayerListItem extends React.Component {
|
||||
static propTypes = {
|
||||
layerId: PropTypes.string.isRequired,
|
||||
layerType: PropTypes.string.isRequired,
|
||||
isSelected: PropTypes.bool,
|
||||
visibility: PropTypes.string,
|
||||
className: PropTypes.string,
|
||||
|
||||
onLayerSelect: PropTypes.func.isRequired,
|
||||
onLayerCopy: PropTypes.func,
|
||||
onLayerDestroy: PropTypes.func,
|
||||
onLayerVisibilityToggle: PropTypes.func,
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
isSelected: false,
|
||||
visibility: 'visible',
|
||||
onLayerCopy: () => {},
|
||||
onLayerDestroy: () => {},
|
||||
onLayerVisibilityToggle: () => {},
|
||||
}
|
||||
|
||||
static childContextTypes = {
|
||||
reactIconBase: PropTypes.object
|
||||
}
|
||||
|
||||
getChildContext() {
|
||||
return {
|
||||
reactIconBase: { size: 14 }
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const visibilityAction = this.props.visibility === 'visible' ? 'show' : 'hide';
|
||||
|
||||
return <li
|
||||
key={this.props.layerId}
|
||||
onClick={e => this.props.onLayerSelect(this.props.layerId)}
|
||||
data-wd-key={"layer-list-item:"+this.props.layerId}
|
||||
className={classnames({
|
||||
"maputnik-layer-list-item": true,
|
||||
"maputnik-layer-list-item-selected": this.props.isSelected,
|
||||
[this.props.className]: true,
|
||||
})}>
|
||||
<DraggableLabel {...this.props} />
|
||||
<span style={{flexGrow: 1}} />
|
||||
<IconAction
|
||||
wdKey={"layer-list-item:"+this.props.layerId+":delete"}
|
||||
action={'delete'}
|
||||
classBlockName="delete"
|
||||
onClick={e => this.props.onLayerDestroy(this.props.layerId)}
|
||||
/>
|
||||
<IconAction
|
||||
wdKey={"layer-list-item:"+this.props.layerId+":copy"}
|
||||
action={'duplicate'}
|
||||
classBlockName="duplicate"
|
||||
onClick={e => this.props.onLayerCopy(this.props.layerId)}
|
||||
/>
|
||||
<IconAction
|
||||
wdKey={"layer-list-item:"+this.props.layerId+":toggle-visibility"}
|
||||
action={visibilityAction}
|
||||
classBlockName="visibility"
|
||||
classBlockModifier={visibilityAction}
|
||||
onClick={e => this.props.onLayerVisibilityToggle(this.props.layerId)}
|
||||
/>
|
||||
</li>
|
||||
}
|
||||
}
|
||||
|
||||
const LayerListItemSortable = SortableElement((props) => <LayerListItem {...props} />);
|
||||
|
||||
export default LayerListItemSortable;
|
||||
@@ -1,34 +0,0 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import {latest} from '@mapbox/mapbox-gl-style-spec'
|
||||
import InputBlock from '../inputs/InputBlock'
|
||||
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={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
|
||||
@@ -1,36 +0,0 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import {latest} from '@mapbox/mapbox-gl-style-spec'
|
||||
import InputBlock from '../inputs/InputBlock'
|
||||
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={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
|
||||
@@ -1,38 +0,0 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import {latest} from '@mapbox/mapbox-gl-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={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
|
||||
@@ -1,29 +0,0 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import {latest} from '@mapbox/mapbox-gl-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={latest.layer.maxzoom.doc}
|
||||
data-wd-key="max-zoom"
|
||||
>
|
||||
<NumberInput
|
||||
value={this.props.value}
|
||||
onChange={this.props.onChange}
|
||||
min={latest.layer.maxzoom.minimum}
|
||||
max={latest.layer.maxzoom.maximum}
|
||||
default={latest.layer.maxzoom.maximum}
|
||||
/>
|
||||
</InputBlock>
|
||||
}
|
||||
}
|
||||
|
||||
export default MaxZoomBlock
|
||||
@@ -1,29 +0,0 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import {latest} from '@mapbox/mapbox-gl-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={latest.layer.minzoom.doc}
|
||||
data-wd-key="min-zoom"
|
||||
>
|
||||
<NumberInput
|
||||
value={this.props.value}
|
||||
onChange={this.props.onChange}
|
||||
min={latest.layer.minzoom.minimum}
|
||||
max={latest.layer.minzoom.maximum}
|
||||
default={latest.layer.minzoom.minimum}
|
||||
/>
|
||||
</InputBlock>
|
||||
}
|
||||
}
|
||||
|
||||
export default MinZoomBlock
|
||||
@@ -1,138 +0,0 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import LayerIcon from '../icons/LayerIcon'
|
||||
import {latest, expression, function as styleFunction} from '@mapbox/mapbox-gl-style-spec'
|
||||
|
||||
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,
|
||||
zoom: PropTypes.number,
|
||||
}
|
||||
|
||||
_getFeatureColor(feature, zoom) {
|
||||
// Guard because openlayers won't have this
|
||||
if (!feature.layer.paint) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const paintProps = feature.layer.paint;
|
||||
let propName;
|
||||
|
||||
if(paintProps.hasOwnProperty("text-color") && paintProps["text-color"]) {
|
||||
propName = "text-color";
|
||||
}
|
||||
else if (paintProps.hasOwnProperty("fill-color") && paintProps["fill-color"]) {
|
||||
propName = "fill-color";
|
||||
}
|
||||
else if (paintProps.hasOwnProperty("line-color") && paintProps["line-color"]) {
|
||||
propName = "line-color";
|
||||
}
|
||||
else if (paintProps.hasOwnProperty("fill-extrusion-color") && paintProps["fill-extrusion-color"]) {
|
||||
propName = "fill-extrusion-color";
|
||||
}
|
||||
|
||||
if(propName) {
|
||||
const propertySpec = latest["paint_"+feature.layer.type][propName];
|
||||
|
||||
let color = feature.layer.paint[propName];
|
||||
|
||||
if(typeof(color) === "object") {
|
||||
if(color.stops) {
|
||||
color = styleFunction.convertFunction(color, propertySpec);
|
||||
}
|
||||
|
||||
const exprResult = expression.createExpression(color, propertySpec);
|
||||
const val = exprResult.value.evaluate({
|
||||
zoom: zoom
|
||||
}, feature);
|
||||
return val.toString();
|
||||
}
|
||||
else {
|
||||
return color;
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Default color
|
||||
return "black";
|
||||
}
|
||||
}
|
||||
// This is quite complex, just incase there's an edgecase we're missing
|
||||
// always return black if we get an unexpected error.
|
||||
catch (err) {
|
||||
console.error("Unable to get feature color, error:", err);
|
||||
return "black";
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const sources = groupFeaturesBySourceLayer(this.props.features)
|
||||
|
||||
const items = Object.keys(sources).map(vectorLayerId => {
|
||||
const layers = sources[vectorLayerId].map((feature, idx) => {
|
||||
const featureColor = this._getFeatureColor(feature, this.props.zoom);
|
||||
|
||||
return <div
|
||||
key={idx}
|
||||
className="maputnik-popup-layer"
|
||||
>
|
||||
<div
|
||||
className="maputnik-popup-layer__swatch"
|
||||
style={{background: featureColor}}
|
||||
></div>
|
||||
<label
|
||||
className="maputnik-popup-layer__label"
|
||||
onClick={() => {
|
||||
this.props.onLayerSelect(feature.layer.id)
|
||||
}}
|
||||
>
|
||||
{feature.layer.type &&
|
||||
<LayerIcon type={feature.layer.type} style={{
|
||||
width: 14,
|
||||
height: 14,
|
||||
paddingRight: 3
|
||||
}}/>
|
||||
}
|
||||
{feature.layer.id}
|
||||
{feature.counter && <span> × {feature.counter}</span>}
|
||||
</label>
|
||||
</div>
|
||||
})
|
||||
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
|
||||
@@ -1,71 +0,0 @@
|
||||
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.sourceLayer}-${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('inspectModeCounter')) {
|
||||
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,217 +0,0 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import ReactDOM from 'react-dom'
|
||||
import MapboxGl from 'mapbox-gl'
|
||||
import MapboxInspect from 'mapbox-gl-inspect'
|
||||
import FeatureLayerPopup from './FeatureLayerPopup'
|
||||
import FeaturePropertyPopup from './FeaturePropertyPopup'
|
||||
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'
|
||||
|
||||
|
||||
const IS_SUPPORTED = MapboxGl.supported();
|
||||
|
||||
function renderPopup(popup, mountNode) {
|
||||
ReactDOM.render(popup, mountNode);
|
||||
var content = mountNode.innerHTML;
|
||||
return content;
|
||||
}
|
||||
|
||||
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 = {
|
||||
onDataChange: PropTypes.func,
|
||||
onLayerSelect: PropTypes.func.isRequired,
|
||||
mapStyle: PropTypes.object.isRequired,
|
||||
inspectModeEnabled: PropTypes.bool.isRequired,
|
||||
highlightedLayer: PropTypes.object,
|
||||
options: PropTypes.object,
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
onMapLoaded: () => {},
|
||||
onDataChange: () => {},
|
||||
onLayerSelect: () => {},
|
||||
mapboxAccessToken: tokens.mapbox,
|
||||
options: {},
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
MapboxGl.accessToken = tokens.mapbox
|
||||
this.state = {
|
||||
map: null,
|
||||
inspect: null,
|
||||
}
|
||||
}
|
||||
|
||||
updateMapFromProps(props) {
|
||||
if(!IS_SUPPORTED) return;
|
||||
|
||||
if(!this.state.map) return
|
||||
const metadata = props.mapStyle.metadata || {}
|
||||
MapboxGl.accessToken = metadata['maputnik:mapbox_access_token'] || tokens.mapbox
|
||||
|
||||
if(!props.inspectModeEnabled) {
|
||||
//Mapbox GL now does diffing natively so we don't need to calculate
|
||||
//the necessary operations ourselves!
|
||||
this.state.map.setStyle(props.mapStyle, { diff: true})
|
||||
}
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
let should = false;
|
||||
try {
|
||||
should = JSON.stringify(this.props) !== JSON.stringify(nextProps) || JSON.stringify(this.state) !== JSON.stringify(nextState);
|
||||
} catch(e) {
|
||||
// no biggie, carry on
|
||||
}
|
||||
return should;
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if(!IS_SUPPORTED) return;
|
||||
|
||||
const map = this.state.map;
|
||||
|
||||
this.updateMapFromProps(this.props);
|
||||
|
||||
if(this.props.inspectModeEnabled !== prevProps.inspectModeEnabled) {
|
||||
this.state.inspect.toggleInspector()
|
||||
}
|
||||
if(this.props.inspectModeEnabled) {
|
||||
this.state.inspect.render()
|
||||
}
|
||||
|
||||
if (map) {
|
||||
map.showTileBoundaries = this.props.options.showTileBoundaries;
|
||||
map.showCollisionBoxes = this.props.options.showCollisionBoxes;
|
||||
map.showOverdrawInspector = this.props.options.showOverdrawInspector;
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if(!IS_SUPPORTED) return;
|
||||
|
||||
const mapOpts = {
|
||||
...this.props.options,
|
||||
container: this.container,
|
||||
style: this.props.mapStyle,
|
||||
hash: true,
|
||||
}
|
||||
|
||||
const map = new MapboxGl.Map(mapOpts);
|
||||
|
||||
map.showTileBoundaries = mapOpts.showTileBoundaries;
|
||||
map.showCollisionBoxes = mapOpts.showCollisionBoxes;
|
||||
map.showOverdrawInspector = mapOpts.showOverdrawInspector;
|
||||
|
||||
const zoom = new ZoomControl;
|
||||
map.addControl(zoom, 'top-right');
|
||||
|
||||
const nav = new MapboxGl.NavigationControl({visualizePitch:true});
|
||||
map.addControl(nav, 'top-right');
|
||||
|
||||
const tmpNode = document.createElement('div');
|
||||
|
||||
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 renderPopup(<FeaturePropertyPopup features={features} />, tmpNode);
|
||||
} else {
|
||||
return renderPopup(<FeatureLayerPopup features={features} onLayerSelect={this.props.onLayerSelect} zoom={this.state.zoom} />, tmpNode);
|
||||
}
|
||||
}
|
||||
})
|
||||
map.addControl(inspect)
|
||||
|
||||
map.on("style.load", () => {
|
||||
this.setState({
|
||||
map,
|
||||
inspect,
|
||||
zoom: map.getZoom()
|
||||
});
|
||||
if(this.props.inspectModeEnabled) {
|
||||
inspect.toggleInspector();
|
||||
}
|
||||
})
|
||||
|
||||
map.on("data", e => {
|
||||
if(e.dataType !== 'tile') return
|
||||
this.props.onDataChange({
|
||||
map: this.state.map
|
||||
})
|
||||
})
|
||||
|
||||
map.on("zoom", e => {
|
||||
this.setState({
|
||||
zoom: map.getZoom()
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
render() {
|
||||
if(IS_SUPPORTED) {
|
||||
return <div
|
||||
className="maputnik-map__map"
|
||||
ref={x => this.container = x}
|
||||
></div>
|
||||
}
|
||||
else {
|
||||
return <div
|
||||
className="maputnik-map maputnik-map--error"
|
||||
>
|
||||
<div className="maputnik-map__error-message">
|
||||
Error: Cannot load MapboxGL, WebGL is either unsupported or disabled
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,167 +0,0 @@
|
||||
import React from 'react'
|
||||
import {throttle} from 'lodash';
|
||||
import PropTypes from 'prop-types'
|
||||
import { loadJSON } from '../../libs/urlopen'
|
||||
|
||||
import FeatureLayerPopup from './FeatureLayerPopup';
|
||||
|
||||
import 'ol/ol.css'
|
||||
import {apply} from 'ol-mapbox-style';
|
||||
import {Map, View, Proj, Overlay} from 'ol';
|
||||
|
||||
import {toLonLat} from 'ol/proj';
|
||||
import {toStringHDMS} from 'ol/coordinate';
|
||||
|
||||
|
||||
function renderCoords (coords) {
|
||||
if (!coords || coords.length < 2) {
|
||||
return null;
|
||||
}
|
||||
else {
|
||||
return <span className="maputnik-coords">
|
||||
{coords.map((coord) => String(coord).padStart(7, "\u00A0")).join(', ')}
|
||||
</span>
|
||||
}
|
||||
}
|
||||
|
||||
export default class OpenLayersMap extends React.Component {
|
||||
static propTypes = {
|
||||
onDataChange: PropTypes.func,
|
||||
mapStyle: PropTypes.object.isRequired,
|
||||
accessToken: PropTypes.string,
|
||||
style: PropTypes.object,
|
||||
onLayerSelect: PropTypes.func.isRequired,
|
||||
debugToolbox: PropTypes.bool.isRequired,
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
onMapLoaded: () => {},
|
||||
onDataChange: () => {},
|
||||
onLayerSelect: () => {},
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
zoom: 0,
|
||||
rotation: 0,
|
||||
cursor: [],
|
||||
center: [],
|
||||
};
|
||||
this.updateStyle = throttle(this._updateStyle.bind(this), 200);
|
||||
}
|
||||
|
||||
_updateStyle(newMapStyle) {
|
||||
if(!this.map) return;
|
||||
|
||||
// See <https://github.com/openlayers/ol-mapbox-style/issues/215#issuecomment-493198815>
|
||||
this.map.getLayers().clear();
|
||||
apply(this.map, newMapStyle);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (this.props.mapStyle !== prevProps.mapStyle) {
|
||||
this.updateStyle(this.props.mapStyle);
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.overlay = new Overlay({
|
||||
element: this.popupContainer,
|
||||
autoPan: true,
|
||||
autoPanAnimation: {
|
||||
duration: 250
|
||||
}
|
||||
});
|
||||
|
||||
const map = new Map({
|
||||
target: this.container,
|
||||
overlays: [this.overlay],
|
||||
view: new View({
|
||||
zoom: 1,
|
||||
center: [180, -90],
|
||||
})
|
||||
});
|
||||
|
||||
map.on('pointermove', (evt) => {
|
||||
var coords = toLonLat(evt.coordinate);
|
||||
this.setState({
|
||||
cursor: [
|
||||
coords[0].toFixed(2),
|
||||
coords[1].toFixed(2)
|
||||
]
|
||||
})
|
||||
})
|
||||
|
||||
map.on('postrender', (evt) => {
|
||||
const center = toLonLat(map.getView().getCenter());
|
||||
this.setState({
|
||||
center: [
|
||||
center[0].toFixed(2),
|
||||
center[1].toFixed(2),
|
||||
],
|
||||
rotation: map.getView().getRotation().toFixed(2),
|
||||
zoom: map.getView().getZoom().toFixed(2)
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
this.map = map;
|
||||
this.updateStyle(this.props.mapStyle);
|
||||
}
|
||||
|
||||
closeOverlay = (e) => {
|
||||
e.target.blur();
|
||||
this.overlay.setPosition(undefined);
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div className="maputnik-ol-container">
|
||||
<div
|
||||
ref={x => this.popupContainer = x}
|
||||
style={{background: "black"}}
|
||||
className="maputnik-popup"
|
||||
>
|
||||
<button
|
||||
className="mapboxgl-popup-close-button"
|
||||
onClick={this.closeOverlay}
|
||||
aria-label="Close popup"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
<FeatureLayerPopup
|
||||
features={this.state.selectedFeatures || []}
|
||||
onLayerSelect={this.props.onLayerSelect}
|
||||
/>
|
||||
</div>
|
||||
<div className="maputnik-ol-zoom">
|
||||
Zoom level: {this.state.zoom}
|
||||
</div>
|
||||
{this.props.debugToolbox &&
|
||||
<div className="maputnik-ol-debug">
|
||||
<div>
|
||||
<label>cursor: </label>
|
||||
<span>{renderCoords(this.state.cursor)}</span>
|
||||
</div>
|
||||
<div>
|
||||
<label>center: </label>
|
||||
<span>{renderCoords(this.state.center)}</span>
|
||||
</div>
|
||||
<div>
|
||||
<label>rotation: </label>
|
||||
<span>{this.state.rotation}</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<div
|
||||
className="maputnik-ol"
|
||||
ref={x => this.container = x}
|
||||
style={{
|
||||
...this.props.style,
|
||||
}}>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,171 +0,0 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import Button from '../Button'
|
||||
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]
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
// Check if source is valid for new type
|
||||
const oldType = prevState.type;
|
||||
const newType = this.state.type;
|
||||
|
||||
const availableSourcesOld = this.getSources(oldType);
|
||||
const availableSourcesNew = this.getSources(newType);
|
||||
|
||||
if(
|
||||
// Type has changed
|
||||
oldType !== newType
|
||||
&& prevState.source !== ""
|
||||
// Was a valid source previously
|
||||
&& availableSourcesOld.indexOf(prevState.source) > -1
|
||||
// And is not a valid source now
|
||||
&& availableSourcesNew.indexOf(this.state.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",
|
||||
"heatmap"
|
||||
],
|
||||
raster: [
|
||||
"raster"
|
||||
],
|
||||
geojson: [
|
||||
"fill",
|
||||
"line",
|
||||
"symbol",
|
||||
"circle",
|
||||
"fill-extrusion",
|
||||
"heatmap"
|
||||
]
|
||||
}
|
||||
|
||||
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}
|
||||
data-wd-key="add-layer"
|
||||
>
|
||||
Add Layer
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
}
|
||||
}
|
||||
|
||||
export default AddModal
|
||||