Compare commits
993 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7943252400 | ||
|
|
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 | ||
|
|
5f1e212759 | ||
|
|
2b7db498ef | ||
|
|
e6464790f6 | ||
|
|
13ddf9f754 | ||
|
|
30edb881ed | ||
|
|
b30bbdc248 | ||
|
|
824616f6bd | ||
|
|
2a832955c4 | ||
|
|
608b836fe0 | ||
|
|
de9c4fcc4a | ||
|
|
109198a524 | ||
|
|
920e4fe630 | ||
|
|
5e143e0a8e | ||
|
|
57f803d52c | ||
|
|
c55d342c7e | ||
|
|
e9065635cd | ||
|
|
6057721249 | ||
|
|
975487d271 | ||
|
|
46b2fd5978 | ||
|
|
f61313449f | ||
|
|
366ad4d7df | ||
|
|
b5cfb44cf0 | ||
|
|
050cc9cea9 | ||
|
|
b2f194eeee | ||
|
|
97b0b8541d | ||
|
|
b5eb74fe20 | ||
|
|
0500172d42 | ||
|
|
0e7bd98485 | ||
|
|
ff0ece5149 | ||
|
|
db9ad86ac2 | ||
|
|
a066710bfb | ||
|
|
52740483b6 | ||
|
|
518a624e20 | ||
|
|
4ba71c8bd5 | ||
|
|
ceeb628784 | ||
|
|
2ec6a90dc3 | ||
|
|
4e37d834ed | ||
|
|
a7922d894d | ||
|
|
eeda3296ab | ||
|
|
acd26e0162 | ||
|
|
fbf828e202 | ||
|
|
af9015f529 | ||
|
|
7a172b2022 | ||
|
|
a609dc4029 | ||
|
|
92bfee4bcc | ||
|
|
559d4618d1 | ||
|
|
5c391ee287 | ||
|
|
db74cfeb2a | ||
|
|
726b825e4c | ||
|
|
84d56b2606 | ||
|
|
e9a8b094a2 | ||
|
|
924b14621a | ||
|
|
b072e3a98c | ||
|
|
827bd5fa24 | ||
|
|
9e0410afe6 | ||
|
|
ef08a9347e | ||
|
|
9b732540a6 | ||
|
|
24c52074b8 | ||
|
|
cb6c6e0d9f | ||
|
|
884dc6fa49 | ||
|
|
efe42021f1 | ||
|
|
470277c253 | ||
|
|
c1cab38c7a | ||
|
|
1cf36ddb08 | ||
|
|
1fec89b69e | ||
|
|
911549aca3 | ||
|
|
41329ec2f8 | ||
|
|
15cdfbc980 | ||
|
|
5053058c32 | ||
|
|
8a8cfad303 | ||
|
|
cc3c17078d | ||
|
|
47965d5f57 | ||
|
|
c947d9e3ed | ||
|
|
8318180e96 | ||
|
|
87daf6fb76 | ||
|
|
5d254ac2ff | ||
|
|
482f322d9f | ||
|
|
b1d097a40f | ||
|
|
da456b08fe | ||
|
|
2776ac3ce0 | ||
|
|
eb2fc4c715 | ||
|
|
d2ffc3a0b1 | ||
|
|
40a9978f31 | ||
|
|
22688933b3 | ||
|
|
7cf01f0c12 | ||
|
|
1e87765f95 | ||
|
|
cc8fe4e02e | ||
|
|
979fc98e70 | ||
|
|
c3c0c35d8a | ||
|
|
f9913cad63 | ||
|
|
0ed64f94e8 | ||
|
|
1deecd4e2a | ||
|
|
e0e9201b46 | ||
|
|
362c7b437e | ||
|
|
efd0b547e9 | ||
|
|
afbaaa66bc | ||
|
|
de8c687dfb | ||
|
|
1bb079f078 | ||
|
|
b35322522f | ||
|
|
6a605571e0 | ||
|
|
c4ec77c911 | ||
|
|
f7643cee7e | ||
|
|
cd95202dcc | ||
|
|
facba3998b | ||
|
|
1f9cc2ce33 | ||
|
|
ad505378ab | ||
|
|
fb7b30c81d | ||
|
|
cb3f93c67d | ||
|
|
76cb5a6b7c | ||
|
|
990e47cb24 | ||
|
|
8be7465428 | ||
|
|
e2b7a6a517 | ||
|
|
284e00b665 | ||
|
|
1fbfa8428b | ||
|
|
b13b89c7ce | ||
|
|
3fa31e2a2e | ||
|
|
0e7d2d8ff5 | ||
|
|
2def3820dd | ||
|
|
e1a489f318 | ||
|
|
760839eb83 | ||
|
|
7136d2dea3 | ||
|
|
21f4d26b50 | ||
|
|
627ea268c9 | ||
|
|
65f77db4d6 | ||
|
|
03b4f1eb8d | ||
|
|
82d1fc0a8b | ||
|
|
3b16de5df4 | ||
|
|
94d4070653 | ||
|
|
d856a1cd8e | ||
|
|
7c1d0f7bee | ||
|
|
d02036321f | ||
|
|
1edadcc6bb | ||
|
|
aa92091d2d | ||
|
|
14e0385575 | ||
|
|
6cf861d44e | ||
|
|
1375240bfa | ||
|
|
8f391d7d52 | ||
|
|
84654e81af | ||
|
|
7ff0524bb7 | ||
|
|
06c3c92fd6 | ||
|
|
4c2941e9b6 | ||
|
|
ed7a25646e | ||
|
|
8c821176cf | ||
|
|
98ded98583 | ||
|
|
10ae69e41f | ||
|
|
04531b4305 | ||
|
|
7ab4b2481c | ||
|
|
6fa88e6869 | ||
|
|
5b90c31645 | ||
|
|
5eba11faee | ||
|
|
54b4fc473c | ||
|
|
fe8595cdc9 | ||
|
|
2565a89474 | ||
|
|
92b970377d | ||
|
|
dd3a550ec3 | ||
|
|
7d5d9e2d82 | ||
|
|
9daa71befc | ||
|
|
f10a2d28df | ||
|
|
d52c6c70bb | ||
|
|
6e2f46a0da | ||
|
|
2d6f91d0cd | ||
|
|
f409079d93 | ||
|
|
7004259867 | ||
|
|
f3128cb6d2 | ||
|
|
1dfd4d8d48 | ||
|
|
2f30eb6cbe | ||
|
|
cf45c04069 | ||
|
|
05d149bcfa | ||
|
|
3acbc3291c | ||
|
|
24aa2fd5fa | ||
|
|
cc7d7a56f5 | ||
|
|
da17646b8d | ||
|
|
f9233a1e31 | ||
|
|
c94f536e5a | ||
|
|
b712e7f184 | ||
|
|
b8ab802de5 | ||
|
|
a74eb2989c | ||
|
|
3a0fc6eeac | ||
|
|
0f0684e701 | ||
|
|
e9b5bfb572 | ||
|
|
b456b59c44 | ||
|
|
59ad91fdf8 | ||
|
|
e18d304313 | ||
|
|
5de5281b49 | ||
|
|
fe0df2a4ef | ||
|
|
deec7894dd | ||
|
|
c9a0c0400e | ||
|
|
419e62f69b | ||
|
|
9ffbe3a7a2 | ||
|
|
c8e548e3be | ||
|
|
b9160bd333 | ||
|
|
8ad8b4cdea | ||
|
|
c51c40a20e | ||
|
|
2ccb1a8e0a | ||
|
|
008bb75c04 | ||
|
|
a51fdb8435 | ||
|
|
cdd5d27908 | ||
|
|
82d3c934c8 | ||
|
|
f2f0270936 | ||
|
|
00f646d489 | ||
|
|
90a02df45c | ||
|
|
ad40a15a77 | ||
|
|
d6809cb504 | ||
|
|
ff8d3055b4 | ||
|
|
1f81449e3c | ||
|
|
c59e0cb046 | ||
|
|
2d1675c181 | ||
|
|
48ebca6236 | ||
|
|
bce8e8b807 | ||
|
|
1f4dfa7603 | ||
|
|
c23af0063d | ||
|
|
9f6250c489 | ||
|
|
cf6c6f1c17 | ||
|
|
c6163b6ba2 | ||
|
|
7121a680b4 | ||
|
|
cf391031f0 | ||
|
|
9cac5305cd | ||
|
|
b0adb8cd3d | ||
|
|
3d2a1d5d19 | ||
|
|
3c93c41de1 | ||
|
|
4baed5d8ab | ||
|
|
f17b02b1fe | ||
|
|
3c72d07a88 | ||
|
|
7495c0dfcf | ||
|
|
b0c877d4ae | ||
|
|
e1fd0f8014 | ||
|
|
cb2198b661 | ||
|
|
68beeeb599 | ||
|
|
218ce148d5 | ||
|
|
d0cafb06ee | ||
|
|
5671a58704 | ||
|
|
9cf74ca405 | ||
|
|
1c6e3648eb | ||
|
|
b3a4628a79 | ||
|
|
941cc37c87 | ||
|
|
906d7ac3d5 | ||
|
|
588b18d10e | ||
|
|
90024c5ec7 | ||
|
|
889005de6c | ||
|
|
843d3df8bc | ||
|
|
825b9044b9 | ||
|
|
92a1be83b6 | ||
|
|
36e35eb208 | ||
|
|
2fcdb47fe5 | ||
|
|
012e4b670e | ||
|
|
492cc244d8 | ||
|
|
d17d6b43c0 | ||
|
|
1bf10cd6d6 | ||
|
|
b0cd9140be | ||
|
|
cc6196969f | ||
|
|
802a7eb1be | ||
|
|
a666f86be0 | ||
|
|
44fad76d45 | ||
|
|
c8d23a534e | ||
|
|
cf3650c8cd | ||
|
|
1a8349f821 | ||
|
|
855771a6b6 | ||
|
|
b711168e44 | ||
|
|
4134919dde | ||
|
|
158153e366 | ||
|
|
6b890d162a | ||
|
|
1525807d06 | ||
|
|
a356bfd601 | ||
|
|
e6d2a6d5ff | ||
|
|
c8a004422f | ||
|
|
df457fc7bf | ||
|
|
6e03f1f077 | ||
|
|
4c13350c14 | ||
|
|
63a2495c68 | ||
|
|
f9de73e18a | ||
|
|
e6e2be61f0 | ||
|
|
060f7aa42c | ||
|
|
8b0ae178b8 | ||
|
|
6b94e9b78b | ||
|
|
b171bf3127 | ||
|
|
0c6a179cec | ||
|
|
aa50785c12 | ||
|
|
252403b1e3 | ||
|
|
bc1d0de057 | ||
|
|
4a0b9fd0de | ||
|
|
e8777e1857 | ||
|
|
94a2a16330 | ||
|
|
004d135d93 | ||
|
|
0973dcee8a | ||
|
|
c908f7dcd0 | ||
|
|
b7fd889fcd | ||
|
|
35600c253d | ||
|
|
673465af77 | ||
|
|
cc5d0dc4fe | ||
|
|
e6da977c48 | ||
|
|
e4aa016713 | ||
|
|
8b67499a64 | ||
|
|
bcdc7c6811 | ||
|
|
8f07a79a49 | ||
|
|
cdcf16196c | ||
|
|
a0ed6a379b | ||
|
|
7ffb44f604 | ||
|
|
225d0388ce | ||
|
|
0468db8cc2 | ||
|
|
695f612110 | ||
|
|
9c07852b87 | ||
|
|
9ef198fb86 | ||
|
|
fd34e31462 | ||
|
|
8eb49427fd | ||
|
|
ebafb3c3dd | ||
|
|
09c6154949 | ||
|
|
53c8661cd3 | ||
|
|
3d5eec897e | ||
|
|
3763ec3737 | ||
|
|
f1216795d2 | ||
|
|
0ac70df00f | ||
|
|
7d0a985f1d | ||
|
|
c5ff67b6e0 | ||
|
|
db6b9ac176 | ||
|
|
77475af3c6 | ||
|
|
805133d10c | ||
|
|
fff8fb72c5 | ||
|
|
e02b18cea3 | ||
|
|
b8e9307ce2 | ||
|
|
00b22eb902 | ||
|
|
be954143c3 | ||
|
|
b314642586 | ||
|
|
b5fc315b37 | ||
|
|
26ff9f63bb | ||
|
|
7e5fb4d42f | ||
|
|
762bb786be | ||
|
|
cec87765fc | ||
|
|
b966fae926 | ||
|
|
f1ddf4e57e | ||
|
|
64e65dc7d3 | ||
|
|
1e07a88aed | ||
|
|
6e49cc65a9 | ||
|
|
06d579118a | ||
|
|
f0e4b5b930 | ||
|
|
088127a9a5 | ||
|
|
a4b4d077fa | ||
|
|
bc2ec4d0b7 | ||
|
|
e4de101553 | ||
|
|
6207416b32 | ||
|
|
f0202241f4 | ||
|
|
0e8c94af1e | ||
|
|
922ee616ec | ||
|
|
98c235bc21 | ||
|
|
70f1f9ffac | ||
|
|
409f81f0d8 | ||
|
|
1aa90bef37 | ||
|
|
c5ea9494df | ||
|
|
9a34db7008 | ||
|
|
988b7fca0f | ||
|
|
bdc6489db4 | ||
|
|
49b096b601 | ||
|
|
31d83f6a26 | ||
|
|
03e52b7a72 | ||
|
|
551e950c39 | ||
|
|
a7620f83a6 | ||
|
|
0384181ee1 | ||
|
|
fd59f42819 | ||
|
|
85a28999fb | ||
|
|
45bdf53a41 | ||
|
|
00e94212bd |
13
.babelrc
@@ -1,13 +0,0 @@
|
|||||||
{
|
|
||||||
"presets": ["env", "react"],
|
|
||||||
"plugins": ["transform-object-rest-spread", "transform-class-properties"],
|
|
||||||
"env": {
|
|
||||||
"test": {
|
|
||||||
"plugins": [
|
|
||||||
["istanbul", {
|
|
||||||
exclude: ["node_modules/**", "test/**"]
|
|
||||||
}]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,103 +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-v6:
|
|
||||||
docker:
|
|
||||||
- image: node:6
|
|
||||||
working_directory: ~/repo-linux-node-v6
|
|
||||||
steps: *build-steps
|
|
||||||
build-linux-node-v8:
|
|
||||||
docker:
|
|
||||||
- image: node:8
|
|
||||||
- image: selenium/standalone-chrome:3.8.1
|
|
||||||
working_directory: ~/repo-linux-node-v8
|
|
||||||
steps: *wdio-steps
|
|
||||||
build-linux-node-v10:
|
|
||||||
docker:
|
|
||||||
- image: node:10
|
|
||||||
working_directory: ~/repo-linux-node-v10
|
|
||||||
steps: *build-steps
|
|
||||||
build-osx-node-v6:
|
|
||||||
macos:
|
|
||||||
xcode: "9.0"
|
|
||||||
dependencies:
|
|
||||||
override:
|
|
||||||
- brew install node@6
|
|
||||||
working_directory: ~/repo-osx-node-v6
|
|
||||||
steps: *build-steps
|
|
||||||
build-osx-node-v8:
|
|
||||||
macos:
|
|
||||||
xcode: "9.0"
|
|
||||||
dependencies:
|
|
||||||
override:
|
|
||||||
- brew install node@8
|
|
||||||
working_directory: ~/repo-osx-node-v8
|
|
||||||
steps: *build-steps
|
|
||||||
build-osx-node-v10:
|
|
||||||
macos:
|
|
||||||
xcode: "9.0"
|
|
||||||
dependencies:
|
|
||||||
override:
|
|
||||||
- brew install node@10
|
|
||||||
working_directory: ~/repo-osx-node-v10
|
|
||||||
steps: *build-steps
|
|
||||||
|
|
||||||
workflows:
|
|
||||||
version: 2
|
|
||||||
build:
|
|
||||||
jobs:
|
|
||||||
- build-linux-node-v6
|
|
||||||
- build-linux-node-v8
|
|
||||||
- build-linux-node-v10
|
|
||||||
- build-osx-node-v6
|
|
||||||
- build-osx-node-v8
|
|
||||||
- build-osx-node-v10
|
|
||||||
|
|
||||||
@@ -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
|
|
||||||
22
.travis.yml
@@ -1,22 +0,0 @@
|
|||||||
language: node_js
|
|
||||||
matrix:
|
|
||||||
include:
|
|
||||||
- os: osx
|
|
||||||
node_js: "6"
|
|
||||||
- os: osx
|
|
||||||
node_js: "8"
|
|
||||||
- os: osx
|
|
||||||
node_js: "9"
|
|
||||||
install:
|
|
||||||
- npm install
|
|
||||||
script:
|
|
||||||
- mkdir public
|
|
||||||
- node --stack_size=100000 $(which npm) run build
|
|
||||||
- npm run lint
|
|
||||||
- npm run lint-styles
|
|
||||||
addons:
|
|
||||||
apt:
|
|
||||||
sources:
|
|
||||||
- ubuntu-toolchain-r-test
|
|
||||||
packages:
|
|
||||||
- g++-4.8
|
|
||||||
15
Dockerfile
@@ -1,15 +0,0 @@
|
|||||||
FROM nodesource/xenial:6.1.0
|
|
||||||
|
|
||||||
EXPOSE 8888
|
|
||||||
|
|
||||||
ENV HOME /maputnik
|
|
||||||
RUN mkdir ${HOME}
|
|
||||||
|
|
||||||
COPY . ${HOME}/
|
|
||||||
|
|
||||||
WORKDIR ${HOME}
|
|
||||||
|
|
||||||
RUN npm install -d --dev
|
|
||||||
RUN npm run build
|
|
||||||
|
|
||||||
CMD npm run start -- --host 0.0.0.0
|
|
||||||
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.
|
|
||||||
|
|
||||||
167
README.md
@@ -1,167 +0,0 @@
|
|||||||
# Maputnik
|
|
||||||
|
|
||||||
[][travis]
|
|
||||||
[][appveyor]
|
|
||||||
[][dm-prod]
|
|
||||||
[][dm-dev]
|
|
||||||
[][license]
|
|
||||||
|
|
||||||
[travis]: https://travis-ci.org/maputnik/editor
|
|
||||||
[appveyor]: https://ci.appveyor.com/project/lukasmartinelli/editor
|
|
||||||
[dm-prod]: https://david-dm.org/maputnik/editor
|
|
||||||
[dm-dev]: https://david-dm.org/maputnik/editor#info=devDependencies
|
|
||||||
[license]: https://tldrlegal.com/license/mit-license
|
|
||||||
|
|
||||||
<img width="200" align="right" alt="Maputnik" src="src/img/maputnik.png" />
|
|
||||||
|
|
||||||
A free and open visual editor for the [Mapbox GL styles](https://www.mapbox.com/mapbox-gl-style-spec/)
|
|
||||||
targeted at developers and map designers.
|
|
||||||
|
|
||||||
- :link: Design your maps online at **<https://maputnik.github.io/editor/>** (all in local storage)
|
|
||||||
- :link: Use the [Maputnik CLI](https://github.com/maputnik/editor/wiki/Maputnik-CLI) for local style development
|
|
||||||
|
|
||||||
Mapbox has built one of the best and most amazing OSS ecosystems. A key component to ensure its longevity and independance is an OSS map designer.
|
|
||||||
|
|
||||||
|
|
||||||
## Donations
|
|
||||||
If you or your organisation has seen value from Maputnik, please consider donating at <https://maputnik.github.io/donate>
|
|
||||||
|
|
||||||
|
|
||||||
## Documentation
|
|
||||||
|
|
||||||
The documentation can be found in the [Wiki](https://github.com/maputnik/editor/wiki). You are welcome to collaborate!
|
|
||||||
|
|
||||||
- :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
|
|
||||||
|
|
||||||
- Linux, OSX and Windows
|
|
||||||
- Node >4
|
|
||||||
|
|
||||||
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
|
|
||||||
```
|
|
||||||
|
|
||||||
The build process will watch for changes to the filesystem, rebuild and autoreload the editor. However note this from the webpack-dev-server docs
|
|
||||||
|
|
||||||
> webpack uses the file system to get notified of file changes. In some cases this does not work. For example, when using Network File System (NFS). Vagrant also has a lot of problems with this.
|
|
||||||
Snippet from <https://webpack.js.org/configuration/dev-server/#devserver-watchoptions->
|
|
||||||
|
|
||||||
To enable polling add `export WEBPACK_DEV_SERVER_POLLING=1` to your enviroment.
|
|
||||||
|
|
||||||
```
|
|
||||||
npm run build
|
|
||||||
```
|
|
||||||
|
|
||||||
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 and terminal and run the following. This will install the drivers on your local machine
|
|
||||||
|
|
||||||
```
|
|
||||||
./node_modules/.bin/selenium-standalone install
|
|
||||||
```
|
|
||||||
|
|
||||||
Now start the standalone server
|
|
||||||
|
|
||||||
```
|
|
||||||
./node_modules/.bin/selenium-standalone start
|
|
||||||
```
|
|
||||||
|
|
||||||
Then open another terminal and run
|
|
||||||
|
|
||||||
```
|
|
||||||
npm test
|
|
||||||
```
|
|
||||||
|
|
||||||
After some time you should see a browser launch which will be automated by the test runner.
|
|
||||||
|
|
||||||
|
|
||||||
## Related Projects
|
|
||||||
|
|
||||||
- [maputnik-dev-server](https://github.com/nycplanning/labs-maputnik-dev-server) - An express.js server that allows for quickly loading the style from any mapboxGL map into mapuntnik.
|
|
||||||
|
|
||||||
## Sponsors
|
|
||||||
|
|
||||||
Thanks to the supporters of the **[Kickstarter campaign](https://www.kickstarter.com/projects/174808720/maputnik-visual-map-editor-for-mapbox-gl)**. This project would not be possible without these commercial and individual sponsors.
|
|
||||||
|
|
||||||
### 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="media/sponsors/wemap.jpg" />
|
|
||||||
</a>
|
|
||||||
<a href="http://terranodo.io/">
|
|
||||||
<img width="33%" alt="Terranodo" style="display:inline" src="media/sponsors/terranodo.png" />
|
|
||||||
</a>
|
|
||||||
<a href="https://www.orbiconinformatik.dk/">
|
|
||||||
<img width="32%" alt="Terranodo" style="display:inline" src="media/sponsors/orbicon_informatik.png" />
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<br/>
|
|
||||||
|
|
||||||
### Silver
|
|
||||||
|
|
||||||
- [Klokan Technologies](https://www.klokantech.com/)
|
|
||||||
- [Geofabrik](http://www.geofabrik.de/)
|
|
||||||
- [Dreipol](https://www.dreipol.ch/)
|
|
||||||
|
|
||||||
<a href="https://www.klokantech.com/">
|
|
||||||
<img width="18%" alt="Klokan Technologies" style="display:inline-block" src="media/sponsors/klokantech.png" />
|
|
||||||
</a>
|
|
||||||
<a href="http://www.geofabrik.de/">
|
|
||||||
<img width="18%" alt="Geofabrik" style="display:inline-block" src="media/sponsors/geofabrik.png" />
|
|
||||||
</a>
|
|
||||||
<a href="https://www.dreipol.ch/">
|
|
||||||
<img width="18%" alt="Dreipol" style="display:inline-block" src="media/sponsors/dreipol.png" />
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<br/>
|
|
||||||
|
|
||||||
### Individuals
|
|
||||||
|
|
||||||
**Influential Stakeholder**
|
|
||||||
|
|
||||||
Alan McConchie, Odi, Mats Norén, Uli [geOps](http://geops.ch/), Helge Fahrnberger ([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 a 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.
|
|
||||||
17
appveyor.yml
@@ -1,17 +0,0 @@
|
|||||||
environment:
|
|
||||||
matrix:
|
|
||||||
- nodejs_version: "6"
|
|
||||||
- nodejs_version: "8"
|
|
||||||
- nodejs_version: "9"
|
|
||||||
platform:
|
|
||||||
- x86
|
|
||||||
- x64
|
|
||||||
install:
|
|
||||||
- ps: Install-Product node $env:nodejs_version
|
|
||||||
- md public
|
|
||||||
- npm install --global --production windows-build-tools
|
|
||||||
- npm install
|
|
||||||
build_script:
|
|
||||||
- npm run build
|
|
||||||
test_script:
|
|
||||||
- npm run lint
|
|
||||||
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-S_bu68PO.js
Normal file
1
assets/index-S_bu68PO.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-DvW-3CJ8.js
Normal file
1
assets/translation-DvW-3CJ8.js.map
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"translation-DvW-3CJ8.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,62 +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 = {
|
|
||||||
specs: [
|
|
||||||
'./test/functional/index.js'
|
|
||||||
],
|
|
||||||
exclude: [
|
|
||||||
],
|
|
||||||
maxInstances: 10,
|
|
||||||
capabilities: [{
|
|
||||||
maxInstances: 5,
|
|
||||||
browserName: 'chrome'
|
|
||||||
}],
|
|
||||||
sync: true,
|
|
||||||
logLevel: 'verbose',
|
|
||||||
coloredLogs: true,
|
|
||||||
bail: 0,
|
|
||||||
screenshotPath: SCREENSHOT_PATH,
|
|
||||||
// Note: This is here because @orangemug currently runs Maputnik inside a docker container.
|
|
||||||
host: process.env.DOCKER_HOST || "0.0.0.0",
|
|
||||||
baseUrl: 'http://localhost',
|
|
||||||
waitforTimeout: 10000,
|
|
||||||
connectionRetryTimeout: 90000,
|
|
||||||
connectionRetryCount: 3,
|
|
||||||
framework: 'mocha',
|
|
||||||
reporters: ['spec'],
|
|
||||||
mochaOpts: {
|
|
||||||
ui: 'bdd',
|
|
||||||
// Because we don't know how long the initial build will take...
|
|
||||||
timeout: 4*60*1000
|
|
||||||
},
|
|
||||||
onPrepare: function (config, capabilities) {
|
|
||||||
return new Promise(function(resolve, reject) {
|
|
||||||
var compiler = webpack(webpackConfig);
|
|
||||||
server = new WebpackDevServer(compiler, {
|
|
||||||
stats: {
|
|
||||||
colors: true
|
|
||||||
}
|
|
||||||
});
|
|
||||||
server.listen(testConfig.port, (isDocker() ? "0.0.0.0" : "localhost"), function(err) {
|
|
||||||
if(err) {
|
|
||||||
reject(err);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
resolve();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
})
|
|
||||||
},
|
|
||||||
onComplete: function(exitCode) {
|
|
||||||
server.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
"use strict";
|
|
||||||
var webpack = require('webpack');
|
|
||||||
var path = require('path');
|
|
||||||
var loaders = require('./webpack.loaders');
|
|
||||||
var HtmlWebpackPlugin = require('html-webpack-plugin');
|
|
||||||
var CopyWebpackPlugin = require('copy-webpack-plugin');
|
|
||||||
|
|
||||||
const HOST = process.env.HOST || "127.0.0.1";
|
|
||||||
const PORT = process.env.PORT || "8888";
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
target: 'web',
|
|
||||||
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/
|
|
||||||
],
|
|
||||||
loaders: loaders
|
|
||||||
},
|
|
||||||
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,55 +0,0 @@
|
|||||||
module.exports = [
|
|
||||||
{
|
|
||||||
test: /\.jsx?$/,
|
|
||||||
exclude: /(node_modules|bower_components|public)/,
|
|
||||||
loaders: ['react-hot-loader/webpack']
|
|
||||||
},
|
|
||||||
// HACK: This is a massive hack and reaches into the mapbox-gl private API.
|
|
||||||
// We have to include this for access to `normalizeSourceURL`. We should
|
|
||||||
// remove this ASAP, see <https://github.com/mapbox/mapbox-gl-js/issues/2416>
|
|
||||||
{
|
|
||||||
test: /.*node_modules[\/\\]mapbox-gl[\/\\]src[\/\\]util[\/\\].*\.js/,
|
|
||||||
loader: 'babel-loader',
|
|
||||||
query: {
|
|
||||||
presets: ['env', 'react', 'flow'],
|
|
||||||
plugins: ['transform-runtime', 'transform-decorators-legacy', 'transform-class-properties'],
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.jsx?$/,
|
|
||||||
// Note: These modules aren't ES5 therefore we much compile them.
|
|
||||||
exclude: /(.*node_modules(?)|bower_components|public)/,
|
|
||||||
loader: 'babel-loader',
|
|
||||||
query: {
|
|
||||||
presets: ['env', 'react'],
|
|
||||||
plugins: ['transform-runtime', 'transform-decorators-legacy', 'transform-class-properties'],
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.(eot|ttf|woff|woff2)$/,
|
|
||||||
loader: 'file-loader?name=fonts/[name].[ext]'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.ico$/,
|
|
||||||
loader: 'file-loader?name=[name].[ext]'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.(svg|gif|jpg|png)$/,
|
|
||||||
loader: 'file-loader?name=img/[name].[ext]'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.json$/,
|
|
||||||
loader: 'json-loader'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /[\/\\](node_modules|global|src)[\/\\].*\.scss$/,
|
|
||||||
loaders: ["style-loader", "css-loader", "sass-loader"]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /[\/\\](node_modules|global|src)[\/\\].*\.css$/,
|
|
||||||
loaders: [
|
|
||||||
'style-loader?sourceMap',
|
|
||||||
'css-loader'
|
|
||||||
]
|
|
||||||
}
|
|
||||||
];
|
|
||||||
@@ -1,88 +0,0 @@
|
|||||||
var webpack = require('webpack');
|
|
||||||
var path = require('path');
|
|
||||||
var loaders = require('./webpack.loaders');
|
|
||||||
var ExtractTextPlugin = require('extract-text-webpack-plugin');
|
|
||||||
var HtmlWebpackPlugin = require('html-webpack-plugin');
|
|
||||||
var WebpackCleanupPlugin = require('webpack-cleanup-plugin');
|
|
||||||
var BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
|
|
||||||
var CopyWebpackPlugin = require('copy-webpack-plugin');
|
|
||||||
var artifacts = require("../test/artifacts");
|
|
||||||
var UglifyJsPlugin = require('uglifyjs-webpack-plugin');
|
|
||||||
|
|
||||||
var OUTPATH = artifacts.pathSync("/build");
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
entry: {
|
|
||||||
app: './src/index.jsx',
|
|
||||||
vendor: [
|
|
||||||
'file-saver',
|
|
||||||
'mapbox-gl/dist/mapbox-gl.js',
|
|
||||||
"lodash.clonedeep",
|
|
||||||
"lodash.throttle",
|
|
||||||
'color',
|
|
||||||
'react',
|
|
||||||
"react-dom",
|
|
||||||
"react-color",
|
|
||||||
"react-file-reader-input",
|
|
||||||
"react-collapse",
|
|
||||||
"react-height",
|
|
||||||
"react-icon-base",
|
|
||||||
"react-motion",
|
|
||||||
"react-sortable-hoc",
|
|
||||||
"request",
|
|
||||||
//TODO: Icons raise multi vendor errors?
|
|
||||||
//"react-icons",
|
|
||||||
]
|
|
||||||
},
|
|
||||||
output: {
|
|
||||||
path: OUTPATH,
|
|
||||||
filename: '[name].[chunkhash].js',
|
|
||||||
chunkFilename: '[chunkhash].js'
|
|
||||||
},
|
|
||||||
resolve: {
|
|
||||||
extensions: ['.js', '.jsx']
|
|
||||||
},
|
|
||||||
module: {
|
|
||||||
noParse: [
|
|
||||||
/mapbox-gl\/dist\/mapbox-gl.js/
|
|
||||||
],
|
|
||||||
loaders
|
|
||||||
},
|
|
||||||
node: {
|
|
||||||
fs: "empty",
|
|
||||||
net: 'empty',
|
|
||||||
tls: 'empty'
|
|
||||||
},
|
|
||||||
plugins: [
|
|
||||||
new webpack.NoEmitOnErrorsPlugin(),
|
|
||||||
new webpack.optimize.CommonsChunkPlugin({ name: 'vendor', filename: '[chunkhash].vendor.js' }),
|
|
||||||
new WebpackCleanupPlugin(),
|
|
||||||
new webpack.DefinePlugin({
|
|
||||||
'process.env': {
|
|
||||||
NODE_ENV: '"production"'
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
new UglifyJsPlugin(),
|
|
||||||
new ExtractTextPlugin('[contenthash].css', {
|
|
||||||
allChunks: true
|
|
||||||
}),
|
|
||||||
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',
|
|
||||||
})
|
|
||||||
]
|
|
||||||
};
|
|
||||||
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-S_bu68PO.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 |
16961
package-lock.json
generated
159
package.json
@@ -1,159 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "maputnik",
|
|
||||||
"version": "1.4.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'",
|
|
||||||
"nsp": "nsp check --reporter summary"
|
|
||||||
},
|
|
||||||
"repository": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "https://github.com/maputnik/editor"
|
|
||||||
},
|
|
||||||
"author": "Lukas Martinelli",
|
|
||||||
"license": "MIT",
|
|
||||||
"homepage": "https://github.com/maputnik/editor#readme",
|
|
||||||
"dependencies": {
|
|
||||||
"@mapbox/mapbox-gl-rtl-text": "^0.1.2",
|
|
||||||
"@mapbox/mapbox-gl-style-spec": "^12.0.0",
|
|
||||||
"classnames": "^2.2.5",
|
|
||||||
"codemirror": "^5.37.0",
|
|
||||||
"color": "^3.0.0",
|
|
||||||
"file-saver": "^1.3.8",
|
|
||||||
"github-api": "^3.0.0",
|
|
||||||
"jsonlint": "github:josdejong/jsonlint#85a19d7",
|
|
||||||
"lodash.capitalize": "^4.2.1",
|
|
||||||
"lodash.clamp": "^4.0.3",
|
|
||||||
"lodash.clonedeep": "^4.5.0",
|
|
||||||
"lodash.isequal": "^4.5.0",
|
|
||||||
"lodash.throttle": "^4.1.1",
|
|
||||||
"mapbox-gl": "^0.45.0",
|
|
||||||
"mapbox-gl-inspect": "^1.3.1",
|
|
||||||
"maputnik-design": "github:maputnik/design",
|
|
||||||
"mousetrap": "^1.6.1",
|
|
||||||
"ol-mapbox-style": "^2.10.1",
|
|
||||||
"ol": "^4.6.5",
|
|
||||||
"prop-types": "^15.6.0",
|
|
||||||
"react": "^16.3.2",
|
|
||||||
"react-addons-pure-render-mixin": "^15.6.2",
|
|
||||||
"react-aria-menubutton": "^5.1.1",
|
|
||||||
"react-aria-modal": "^2.12.1",
|
|
||||||
"react-autocomplete": "^1.7.2",
|
|
||||||
"react-codemirror2": "^4.2.1",
|
|
||||||
"react-collapse": "^4.0.3",
|
|
||||||
"react-color": "^2.14.1",
|
|
||||||
"react-copy-to-clipboard": "^5.0.1",
|
|
||||||
"react-dom": "^16.3.2",
|
|
||||||
"react-file-reader-input": "^1.1.4",
|
|
||||||
"react-height": "^3.0.0",
|
|
||||||
"react-icon-base": "^2.1.1",
|
|
||||||
"react-icons": "^2.2.7",
|
|
||||||
"react-motion": "^0.5.2",
|
|
||||||
"react-sortable-hoc": "^0.6.8",
|
|
||||||
"reconnecting-websocket": "^3.2.2",
|
|
||||||
"request": "^2.85.0",
|
|
||||||
"url": "^0.11.0"
|
|
||||||
},
|
|
||||||
"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": "^6.26.3",
|
|
||||||
"babel-eslint": "^8.2.3",
|
|
||||||
"babel-loader": "7.1.4",
|
|
||||||
"babel-plugin-istanbul": "^4.1.6",
|
|
||||||
"babel-plugin-transform-class-properties": "^6.24.1",
|
|
||||||
"babel-plugin-transform-decorators-legacy": "^1.3.4",
|
|
||||||
"babel-plugin-transform-flow-strip-types": "^6.22.0",
|
|
||||||
"babel-plugin-transform-object-rest-spread": "^6.26.0",
|
|
||||||
"babel-plugin-transform-runtime": "^6.23.0",
|
|
||||||
"babel-preset-env": "^1.6.1",
|
|
||||||
"babel-preset-es2015": "^6.24.1",
|
|
||||||
"babel-preset-flow": "^6.23.0",
|
|
||||||
"babel-preset-react": "^6.24.1",
|
|
||||||
"babel-register": "^6.26.0",
|
|
||||||
"babel-runtime": "^6.26.0",
|
|
||||||
"base64-loader": "^1.0.0",
|
|
||||||
"copy-webpack-plugin": "^4.5.1",
|
|
||||||
"cors": "^2.8.4",
|
|
||||||
"cross-env": "^5.1.4",
|
|
||||||
"css-loader": "^0.28.11",
|
|
||||||
"eslint": "^4.19.1",
|
|
||||||
"eslint-plugin-react": "^7.4.0",
|
|
||||||
"express": "^4.16.3",
|
|
||||||
"extract-text-webpack-plugin": "^3.0.2",
|
|
||||||
"file-loader": "^1.1.5",
|
|
||||||
"html-webpack-plugin": "^3.2.0",
|
|
||||||
"is-docker": "^1.1.0",
|
|
||||||
"istanbul": "^0.4.5",
|
|
||||||
"istanbul-lib-coverage": "^1.2.0",
|
|
||||||
"json-loader": "^0.5.7",
|
|
||||||
"mkdirp": "^0.5.1",
|
|
||||||
"mocha": "^5.1.1",
|
|
||||||
"node-sass": "^4.9.0",
|
|
||||||
"nsp": "^3.1.0",
|
|
||||||
"react-hot-loader": "^3.1.1",
|
|
||||||
"sass-loader": "^7.0.1",
|
|
||||||
"selenium-standalone": "^6.14.0",
|
|
||||||
"style-loader": "^0.20.3",
|
|
||||||
"stylelint": "^9.2.0",
|
|
||||||
"stylelint-config-recommended-scss": "^3.2.0",
|
|
||||||
"stylelint-scss": "^3.0.0",
|
|
||||||
"transform-loader": "^0.2.4",
|
|
||||||
"uglifyjs-webpack-plugin": "^1.2.4",
|
|
||||||
"uuid": "^3.1.0",
|
|
||||||
"wdio-mocha-framework": "^0.5.13",
|
|
||||||
"wdio-phantomjs-service": "^0.2.2",
|
|
||||||
"wdio-selenium-standalone-service": "0.0.10",
|
|
||||||
"wdio-spec-reporter": "^0.1.2",
|
|
||||||
"webdriverio": "^4.12.0",
|
|
||||||
"webpack": "^3.8.1",
|
|
||||||
"webpack-bundle-analyzer": "^2.9.0",
|
|
||||||
"webpack-cleanup-plugin": "^0.5.1",
|
|
||||||
"webpack-dev-server": "^2.9.4"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,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,553 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import Mousetrap from 'mousetrap'
|
|
||||||
import cloneDeep from 'lodash.clonedeep'
|
|
||||||
import clamp from 'lodash.clamp'
|
|
||||||
import {arrayMove} from 'react-sortable-hoc'
|
|
||||||
import url from 'url'
|
|
||||||
|
|
||||||
import MapboxGlMap from './map/MapboxGlMap'
|
|
||||||
import OpenLayers3Map from './map/OpenLayers3Map'
|
|
||||||
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 { downloadGlyphsMetadata, downloadSpriteMetadata } from '../libs/metadata'
|
|
||||||
import * as styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
|
|
||||||
import style from '../libs/style.js'
|
|
||||||
import { initialStyleUrl, loadStyleUrl } from '../libs/urlopen'
|
|
||||||
import { undoMessages, redoMessages } from '../libs/diffmessage'
|
|
||||||
import { loadDefaultStyle, StyleStore } from '../libs/stylestore'
|
|
||||||
import { ApiStyleStore } from '../libs/apistore'
|
|
||||||
import { RevisionStore } from '../libs/revisions'
|
|
||||||
import LayerWatcher from '../libs/layerwatcher'
|
|
||||||
import tokens from '../config/tokens.json'
|
|
||||||
import isEqual from 'lodash.isequal'
|
|
||||||
import Debug from '../libs/debug'
|
|
||||||
import queryUtil from '../libs/query-util'
|
|
||||||
|
|
||||||
import MapboxGl from 'mapbox-gl'
|
|
||||||
import { normalizeSourceURL } from 'mapbox-gl/src/util/mapbox'
|
|
||||||
|
|
||||||
|
|
||||||
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)
|
|
||||||
this.revisionStore = new RevisionStore()
|
|
||||||
this.styleStore = new ApiStyleStore({
|
|
||||||
onLocalStyleChange: mapStyle => this.onStyleChanged(mapStyle, false)
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
const keyCodes = {
|
|
||||||
"esc": 27,
|
|
||||||
"?": 191,
|
|
||||||
"o": 79,
|
|
||||||
"e": 69,
|
|
||||||
"s": 83,
|
|
||||||
"d": 68,
|
|
||||||
"i": 73,
|
|
||||||
"m": 77,
|
|
||||||
}
|
|
||||||
|
|
||||||
const shortcuts = [
|
|
||||||
{
|
|
||||||
keyCode: keyCodes["?"],
|
|
||||||
handler: () => {
|
|
||||||
this.toggleModal("shortcuts");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
keyCode: keyCodes["o"],
|
|
||||||
handler: () => {
|
|
||||||
this.toggleModal("open");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
keyCode: keyCodes["e"],
|
|
||||||
handler: () => {
|
|
||||||
this.toggleModal("export");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
keyCode: keyCodes["d"],
|
|
||||||
handler: () => {
|
|
||||||
this.toggleModal("sources");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
keyCode: keyCodes["s"],
|
|
||||||
handler: () => {
|
|
||||||
this.toggleModal("settings");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
keyCode: keyCodes["i"],
|
|
||||||
handler: () => {
|
|
||||||
this.changeInspectMode();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
keyCode: keyCodes["m"],
|
|
||||||
handler: () => {
|
|
||||||
document.querySelector(".mapboxgl-canvas").focus();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
document.body.addEventListener("keyup", (e) => {
|
|
||||||
if(e.keyCode === keyCodes["esc"]) {
|
|
||||||
e.target.blur();
|
|
||||||
document.body.focus();
|
|
||||||
}
|
|
||||||
else if(document.activeElement === document.body) {
|
|
||||||
const shortcut = shortcuts.find((shortcut) => {
|
|
||||||
return (shortcut.keyCode === e.keyCode)
|
|
||||||
})
|
|
||||||
|
|
||||||
if(shortcut) {
|
|
||||||
shortcut.handler(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const styleUrl = initialStyleUrl()
|
|
||||||
if(styleUrl) {
|
|
||||||
this.styleStore = new StyleStore()
|
|
||||||
loadStyleUrl(styleUrl, mapStyle => this.onStyleChanged(mapStyle))
|
|
||||||
} else {
|
|
||||||
this.styleStore.init(err => {
|
|
||||||
if(err) {
|
|
||||||
console.log('Falling back to local storage for storing styles')
|
|
||||||
this.styleStore = new StyleStore()
|
|
||||||
}
|
|
||||||
this.styleStore.latestStyle(mapStyle => this.onStyleChanged(mapStyle))
|
|
||||||
|
|
||||||
if(Debug.enabled()) {
|
|
||||||
Debug.set("maputnik", "styleStore", this.styleStore);
|
|
||||||
Debug.set("maputnik", "revisionStore", this.revisionStore);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if(Debug.enabled()) {
|
|
||||||
Debug.set("maputnik", "revisionStore", this.revisionStore);
|
|
||||||
Debug.set("maputnik", "styleStore", this.styleStore);
|
|
||||||
}
|
|
||||||
|
|
||||||
const queryObj = url.parse(window.location.href, true).query;
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
errors: [],
|
|
||||||
infos: [],
|
|
||||||
mapStyle: style.emptyStyle,
|
|
||||||
selectedLayerIndex: 0,
|
|
||||||
sources: {},
|
|
||||||
vectorLayers: {},
|
|
||||||
inspectModeEnabled: false,
|
|
||||||
spec: styleSpec.latest,
|
|
||||||
isOpen: {
|
|
||||||
settings: false,
|
|
||||||
sources: false,
|
|
||||||
open: false,
|
|
||||||
shortcuts: false,
|
|
||||||
export: false,
|
|
||||||
survey: localStorage.hasOwnProperty('survey') ? false : true
|
|
||||||
},
|
|
||||||
mapOptions: {
|
|
||||||
showTileBoundaries: queryUtil.asBool(queryObj, "show-tile-boundaries")
|
|
||||||
},
|
|
||||||
mapFilter: queryObj["color-blindness-emulation"],
|
|
||||||
}
|
|
||||||
|
|
||||||
this.layerWatcher = new LayerWatcher({
|
|
||||||
onVectorLayersChange: v => this.setState({ vectorLayers: v })
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
Mousetrap.bind(['mod+z'], this.onUndo.bind(this));
|
|
||||||
Mousetrap.bind(['mod+y', 'mod+shift+z'], this.onRedo.bind(this));
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
Mousetrap.unbind(['mod+z'], this.onUndo.bind(this));
|
|
||||||
Mousetrap.unbind(['mod+y', 'mod+shift+z'], this.onRedo.bind(this));
|
|
||||||
}
|
|
||||||
|
|
||||||
saveStyle(snapshotStyle) {
|
|
||||||
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)})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
onStyleChanged(newStyle, save=true) {
|
|
||||||
|
|
||||||
const errors = styleSpec.validate(newStyle, styleSpec.latest)
|
|
||||||
if(errors.length === 0) {
|
|
||||||
|
|
||||||
if(newStyle.glyphs !== this.state.mapStyle.glyphs) {
|
|
||||||
this.updateFonts(newStyle.glyphs)
|
|
||||||
}
|
|
||||||
if(newStyle.sprite !== this.state.mapStyle.sprite) {
|
|
||||||
this.updateIcons(newStyle.sprite)
|
|
||||||
}
|
|
||||||
|
|
||||||
this.revisionStore.addRevision(newStyle)
|
|
||||||
if(save) this.saveStyle(newStyle)
|
|
||||||
this.setState({
|
|
||||||
mapStyle: newStyle,
|
|
||||||
errors: [],
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
this.setState({
|
|
||||||
errors: errors.map(err => err.message)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
this.fetchSources();
|
|
||||||
}
|
|
||||||
|
|
||||||
onUndo() {
|
|
||||||
const activeStyle = this.revisionStore.undo()
|
|
||||||
const messages = undoMessages(this.state.mapStyle, activeStyle)
|
|
||||||
this.saveStyle(activeStyle)
|
|
||||||
this.setState({
|
|
||||||
mapStyle: activeStyle,
|
|
||||||
infos: messages,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
onRedo() {
|
|
||||||
const activeStyle = this.revisionStore.redo()
|
|
||||||
const messages = redoMessages(this.state.mapStyle, activeStyle)
|
|
||||||
this.saveStyle(activeStyle)
|
|
||||||
this.setState({
|
|
||||||
mapStyle: activeStyle,
|
|
||||||
infos: messages,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
onMoveLayer(move) {
|
|
||||||
let { oldIndex, newIndex } = move;
|
|
||||||
let layers = this.state.mapStyle.layers;
|
|
||||||
oldIndex = clamp(oldIndex, 0, layers.length-1);
|
|
||||||
newIndex = clamp(newIndex, 0, layers.length-1);
|
|
||||||
if(oldIndex === newIndex) return;
|
|
||||||
|
|
||||||
if (oldIndex === this.state.selectedLayerIndex) {
|
|
||||||
this.setState({
|
|
||||||
selectedLayerIndex: newIndex
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
layers = layers.slice(0);
|
|
||||||
layers = arrayMove(layers, oldIndex, newIndex);
|
|
||||||
this.onLayersChange(layers);
|
|
||||||
}
|
|
||||||
|
|
||||||
onLayersChange(changedLayers) {
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
changeInspectMode() {
|
|
||||||
this.setState({
|
|
||||||
inspectModeEnabled: !this.state.inspectModeEnabled
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fetchSources() {
|
|
||||||
const sourceList = {...this.state.sources};
|
|
||||||
|
|
||||||
for(let [key, val] of Object.entries(this.state.mapStyle.sources)) {
|
|
||||||
if(sourceList.hasOwnProperty(key)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
sourceList[key] = {
|
|
||||||
type: val.type,
|
|
||||||
layers: []
|
|
||||||
};
|
|
||||||
|
|
||||||
if(!this.state.sources.hasOwnProperty(key) && val.type === "vector" && val.hasOwnProperty("url")) {
|
|
||||||
let url = val.url;
|
|
||||||
try {
|
|
||||||
url = normalizeSourceURL(url, MapboxGl.accessToken);
|
|
||||||
} catch(err) {
|
|
||||||
console.warn("Failed to normalizeSourceURL: ", err);
|
|
||||||
}
|
|
||||||
|
|
||||||
fetch(url)
|
|
||||||
.then((response) => {
|
|
||||||
return response.json();
|
|
||||||
})
|
|
||||||
.then((json) => {
|
|
||||||
if(!json.hasOwnProperty("vector_layers")) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create new objects before setState
|
|
||||||
const sources = Object.assign({}, this.state.sources);
|
|
||||||
|
|
||||||
for(let layer of json.vector_layers) {
|
|
||||||
sources[key].layers.push(layer.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
console.debug("Updating source: "+key);
|
|
||||||
this.setState({
|
|
||||||
sources: sources
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
console.error("Failed to process sources for '%s'", url, err);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!isEqual(this.state.sources, sourceList)) {
|
|
||||||
console.debug("Setting sources");
|
|
||||||
this.setState({
|
|
||||||
sources: sourceList
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mapRenderer() {
|
|
||||||
const mapProps = {
|
|
||||||
mapStyle: style.replaceAccessToken(this.state.mapStyle, {allowFallback: true}),
|
|
||||||
options: this.state.mapOptions,
|
|
||||||
onDataChange: (e) => {
|
|
||||||
this.layerWatcher.analyzeMap(e.map)
|
|
||||||
this.fetchSources();
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
const metadata = this.state.mapStyle.metadata || {}
|
|
||||||
const renderer = metadata['maputnik:renderer'] || 'mbgljs'
|
|
||||||
|
|
||||||
let mapElement;
|
|
||||||
|
|
||||||
// Check if OL3 code has been loaded?
|
|
||||||
if(renderer === 'ol3') {
|
|
||||||
mapElement = <OpenLayers3Map {...mapProps} />
|
|
||||||
} else {
|
|
||||||
mapElement = <MapboxGlMap {...mapProps}
|
|
||||||
inspectModeEnabled={this.state.inspectModeEnabled}
|
|
||||||
highlightedLayer={this.state.mapStyle.layers[this.state.selectedLayerIndex]}
|
|
||||||
onLayerSelect={this.onLayerSelect.bind(this)} />
|
|
||||||
}
|
|
||||||
|
|
||||||
const elementStyle = {};
|
|
||||||
if(this.state.mapFilter) {
|
|
||||||
elementStyle.filter = `url('#${this.state.mapFilter}')`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return <div style={elementStyle}>
|
|
||||||
{mapElement}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
onLayerSelect(layerId) {
|
|
||||||
const idx = style.indexOfLayer(this.state.mapStyle.layers, layerId)
|
|
||||||
this.setState({ selectedLayerIndex: idx })
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleModal(modalName) {
|
|
||||||
this.setState({
|
|
||||||
isOpen: {
|
|
||||||
...this.state.isOpen,
|
|
||||||
[modalName]: !this.state.isOpen[modalName]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if(modalName === 'survey') {
|
|
||||||
localStorage.setItem('survey', '');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const layers = this.state.mapStyle.layers || []
|
|
||||||
const selectedLayer = layers.length > 0 ? layers[this.state.selectedLayerIndex] : null
|
|
||||||
const metadata = this.state.mapStyle.metadata || {}
|
|
||||||
|
|
||||||
const toolbar = <Toolbar
|
|
||||||
mapStyle={this.state.mapStyle}
|
|
||||||
inspectModeEnabled={this.state.inspectModeEnabled}
|
|
||||||
sources={this.state.sources}
|
|
||||||
onStyleChanged={this.onStyleChanged.bind(this)}
|
|
||||||
onStyleOpen={this.onStyleChanged.bind(this)}
|
|
||||||
onInspectModeToggle={this.changeInspectMode.bind(this)}
|
|
||||||
onToggleModal={this.toggleModal.bind(this)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
const layerList = <LayerList
|
|
||||||
onMoveLayer={this.onMoveLayer.bind(this)}
|
|
||||||
onLayerDestroy={this.onLayerDestroy.bind(this)}
|
|
||||||
onLayerCopy={this.onLayerCopy.bind(this)}
|
|
||||||
onLayerVisibilityToggle={this.onLayerVisibilityToggle.bind(this)}
|
|
||||||
onLayersChange={this.onLayersChange.bind(this)}
|
|
||||||
onLayerSelect={this.onLayerSelect.bind(this)}
|
|
||||||
selectedLayerIndex={this.state.selectedLayerIndex}
|
|
||||||
layers={layers}
|
|
||||||
sources={this.state.sources}
|
|
||||||
/>
|
|
||||||
|
|
||||||
const layerEditor = selectedLayer ? <LayerEditor
|
|
||||||
layer={selectedLayer}
|
|
||||||
layerIndex={this.state.selectedLayerIndex}
|
|
||||||
isFirstLayer={this.state.selectedLayerIndex < 1}
|
|
||||||
isLastLayer={this.state.selectedLayerIndex === this.state.mapStyle.layers.length-1}
|
|
||||||
sources={this.state.sources}
|
|
||||||
vectorLayers={this.state.vectorLayers}
|
|
||||||
spec={this.state.spec}
|
|
||||||
onMoveLayer={this.onMoveLayer.bind(this)}
|
|
||||||
onLayerChanged={this.onLayerChanged.bind(this)}
|
|
||||||
onLayerDestroy={this.onLayerDestroy.bind(this)}
|
|
||||||
onLayerCopy={this.onLayerCopy.bind(this)}
|
|
||||||
onLayerVisibilityToggle={this.onLayerVisibilityToggle.bind(this)}
|
|
||||||
onLayerIdChange={this.onLayerIdChange.bind(this)}
|
|
||||||
/> : null
|
|
||||||
|
|
||||||
const bottomPanel = (this.state.errors.length + this.state.infos.length) > 0 ? <MessagePanel
|
|
||||||
errors={this.state.errors}
|
|
||||||
infos={this.state.infos}
|
|
||||||
/> : null
|
|
||||||
|
|
||||||
|
|
||||||
const modals = <div>
|
|
||||||
<ShortcutsModal
|
|
||||||
isOpen={this.state.isOpen.shortcuts}
|
|
||||||
onOpenToggle={this.toggleModal.bind(this, 'shortcuts')}
|
|
||||||
/>
|
|
||||||
<SettingsModal
|
|
||||||
mapStyle={this.state.mapStyle}
|
|
||||||
onStyleChanged={this.onStyleChanged.bind(this)}
|
|
||||||
isOpen={this.state.isOpen.settings}
|
|
||||||
onOpenToggle={this.toggleModal.bind(this, 'settings')}
|
|
||||||
/>
|
|
||||||
<ExportModal
|
|
||||||
mapStyle={this.state.mapStyle}
|
|
||||||
onStyleChanged={this.onStyleChanged.bind(this)}
|
|
||||||
isOpen={this.state.isOpen.export}
|
|
||||||
onOpenToggle={this.toggleModal.bind(this, 'export')}
|
|
||||||
/>
|
|
||||||
<OpenModal
|
|
||||||
isOpen={this.state.isOpen.open}
|
|
||||||
onStyleOpen={this.onStyleChanged.bind(this)}
|
|
||||||
onOpenToggle={this.toggleModal.bind(this, 'open')}
|
|
||||||
/>
|
|
||||||
<SourcesModal
|
|
||||||
mapStyle={this.state.mapStyle}
|
|
||||||
onStyleChanged={this.onStyleChanged.bind(this)}
|
|
||||||
isOpen={this.state.isOpen.sources}
|
|
||||||
onOpenToggle={this.toggleModal.bind(this, 'sources')}
|
|
||||||
/>
|
|
||||||
<SurveyModal
|
|
||||||
isOpen={this.state.isOpen.survey}
|
|
||||||
onOpenToggle={this.toggleModal.bind(this, 'survey')}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
return <AppLayout
|
|
||||||
toolbar={toolbar}
|
|
||||||
layerList={layerList}
|
|
||||||
layerEditor={layerEditor}
|
|
||||||
map={this.mapRenderer()}
|
|
||||||
bottom={bottomPanel}
|
|
||||||
modals={modals}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,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,27 +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
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return <button
|
|
||||||
onClick={this.props.onClick}
|
|
||||||
aria-label={this.props["aria-label"]}
|
|
||||||
className={classnames("maputnik-button", this.props.className)}
|
|
||||||
data-wd-key={this.props["data-wd-key"]}
|
|
||||||
style={this.props.style}>
|
|
||||||
{this.props.children}
|
|
||||||
</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,180 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
import FileReaderInput from 'react-file-reader-input'
|
|
||||||
import classnames from 'classnames'
|
|
||||||
|
|
||||||
import MdFileDownload from 'react-icons/lib/md/file-download'
|
|
||||||
import MdFileUpload from 'react-icons/lib/md/file-upload'
|
|
||||||
import OpenIcon from 'react-icons/lib/md/open-in-browser'
|
|
||||||
import SettingsIcon from 'react-icons/lib/md/settings'
|
|
||||||
import MdInfo from 'react-icons/lib/md/info'
|
|
||||||
import SourcesIcon from 'react-icons/lib/md/layers'
|
|
||||||
import MdSave from 'react-icons/lib/md/save'
|
|
||||||
import MdStyle from 'react-icons/lib/md/style'
|
|
||||||
import MdMap from 'react-icons/lib/md/map'
|
|
||||||
import MdInsertEmoticon from 'react-icons/lib/md/insert-emoticon'
|
|
||||||
import MdFontDownload from 'react-icons/lib/md/font-download'
|
|
||||||
import HelpIcon from 'react-icons/lib/md/help-outline'
|
|
||||||
import InspectionIcon from 'react-icons/lib/md/find-in-page'
|
|
||||||
import SurveyIcon from 'react-icons/lib/md/assignment-turned-in'
|
|
||||||
|
|
||||||
import logoImage from 'maputnik-design/logos/logo-color.svg'
|
|
||||||
import pkgJson from '../../package.json'
|
|
||||||
|
|
||||||
import style from '../libs/style'
|
|
||||||
|
|
||||||
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 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,
|
|
||||||
onInspectModeToggle: PropTypes.func.isRequired,
|
|
||||||
children: PropTypes.node,
|
|
||||||
onToggleModal: PropTypes.func,
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
super(props)
|
|
||||||
this.state = {
|
|
||||||
isOpen: {
|
|
||||||
settings: false,
|
|
||||||
sources: false,
|
|
||||||
open: false,
|
|
||||||
add: false,
|
|
||||||
export: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return <div className='maputnik-toolbar'>
|
|
||||||
<div className="maputnik-toolbar__inner">
|
|
||||||
<div
|
|
||||||
className="maputnik-toolbar-logo-container"
|
|
||||||
>
|
|
||||||
<a className="maputnik-toolbar-skip" href="#skip-menu">
|
|
||||||
Skip navigation
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
href="https://github.com/maputnik/editor"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
target="_blank"
|
|
||||||
className="maputnik-toolbar-logo"
|
|
||||||
>
|
|
||||||
<img src={logoImage} alt="Maputnik" />
|
|
||||||
<h1>Maputnik
|
|
||||||
<span className="maputnik-toolbar-version">v{pkgJson.version}</span>
|
|
||||||
</h1>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div className="maputnik-toolbar__actions">
|
|
||||||
<ToolbarAction wdKey="nav:open" onClick={this.props.onToggleModal.bind(this, 'open')}>
|
|
||||||
<OpenIcon />
|
|
||||||
<IconText>Open</IconText>
|
|
||||||
</ToolbarAction>
|
|
||||||
<ToolbarAction wdKey="nav:export" onClick={this.props.onToggleModal.bind(this, 'export')}>
|
|
||||||
<MdFileDownload />
|
|
||||||
<IconText>Export</IconText>
|
|
||||||
</ToolbarAction>
|
|
||||||
<ToolbarAction wdKey="nav:sources" onClick={this.props.onToggleModal.bind(this, 'sources')}>
|
|
||||||
<SourcesIcon />
|
|
||||||
<IconText>Data Sources</IconText>
|
|
||||||
</ToolbarAction>
|
|
||||||
<ToolbarAction wdKey="nav:settings" onClick={this.props.onToggleModal.bind(this, 'settings')}>
|
|
||||||
<SettingsIcon />
|
|
||||||
<IconText>Style Settings</IconText>
|
|
||||||
</ToolbarAction>
|
|
||||||
<ToolbarAction wdKey="nav:inspect" onClick={this.props.onInspectModeToggle}>
|
|
||||||
<InspectionIcon />
|
|
||||||
<IconText>
|
|
||||||
{ this.props.inspectModeEnabled && <span>Map Mode</span> }
|
|
||||||
{ !this.props.inspectModeEnabled && <span>Inspect Mode</span> }
|
|
||||||
</IconText>
|
|
||||||
</ToolbarAction>
|
|
||||||
<ToolbarLink href={"https://github.com/maputnik/editor/wiki"}>
|
|
||||||
<HelpIcon />
|
|
||||||
<IconText>Help</IconText>
|
|
||||||
</ToolbarLink>
|
|
||||||
<ToolbarLinkHighlighted href={"https://gregorywolanski.typeform.com/to/cPgaSY"}>
|
|
||||||
<SurveyIcon />
|
|
||||||
<IconText>Take the Maputnik Survey</IconText>
|
|
||||||
</ToolbarLinkHighlighted>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,122 +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,
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
super(props)
|
|
||||||
this.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)");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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.bind(this)}
|
|
||||||
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.bind(this)}
|
|
||||||
style={this.props.style}
|
|
||||||
name={this.props.name}
|
|
||||||
placeholder={this.props.default}
|
|
||||||
value={this.props.value ? this.props.value : ""}
|
|
||||||
onChange={(e) => this.props.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,139 +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
|
|
||||||
]),
|
|
||||||
}
|
|
||||||
|
|
||||||
addStop() {
|
|
||||||
const stops = this.props.value.stops.slice(0)
|
|
||||||
const lastStop = stops[stops.length - 1]
|
|
||||||
if (typeof lastStop[0] === "object") {
|
|
||||||
stops.push([
|
|
||||||
{zoom: lastStop[0].zoom + 1, value: lastStop[0].value},
|
|
||||||
lastStop[1]
|
|
||||||
])
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
stops.push([lastStop[0] + 1, lastStop[1]])
|
|
||||||
}
|
|
||||||
|
|
||||||
const changedValue = {
|
|
||||||
...this.props.value,
|
|
||||||
stops: stops,
|
|
||||||
}
|
|
||||||
|
|
||||||
this.props.onChange(this.props.fieldName, changedValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
deleteStop(stopIdx) {
|
|
||||||
const stops = this.props.value.stops.slice(0)
|
|
||||||
stops.splice(stopIdx, 1)
|
|
||||||
|
|
||||||
let changedValue = {
|
|
||||||
...this.props.value,
|
|
||||||
stops: stops,
|
|
||||||
}
|
|
||||||
|
|
||||||
if(stops.length === 1) {
|
|
||||||
changedValue = stops[0][1]
|
|
||||||
}
|
|
||||||
|
|
||||||
this.props.onChange(this.props.fieldName, changedValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
makeZoomFunction() {
|
|
||||||
const zoomFunc = {
|
|
||||||
stops: [
|
|
||||||
[6, this.props.value],
|
|
||||||
[10, this.props.value]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
this.props.onChange(this.props.fieldName, zoomFunc)
|
|
||||||
}
|
|
||||||
|
|
||||||
makeDataFunction() {
|
|
||||||
const dataFunc = {
|
|
||||||
property: "",
|
|
||||||
type: "categorical",
|
|
||||||
stops: [
|
|
||||||
[{zoom: 6, value: 0}, this.props.value],
|
|
||||||
[{zoom: 10, value: 0}, this.props.value]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
this.props.onChange(this.props.fieldName, dataFunc)
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const propClass = this.props.fieldSpec.default === this.props.value ? "maputnik-default-property" : "maputnik-modified-property"
|
|
||||||
let specField;
|
|
||||||
|
|
||||||
if (isZoomField(this.props.value)) {
|
|
||||||
specField = (
|
|
||||||
<ZoomProperty
|
|
||||||
onChange={this.props.onChange.bind(this)}
|
|
||||||
fieldName={this.props.fieldName}
|
|
||||||
fieldSpec={this.props.fieldSpec}
|
|
||||||
value={this.props.value}
|
|
||||||
onDeleteStop={this.deleteStop.bind(this)}
|
|
||||||
onAddStop={this.addStop.bind(this)}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
else if (isDataField(this.props.value)) {
|
|
||||||
specField = (
|
|
||||||
<DataProperty
|
|
||||||
onChange={this.props.onChange.bind(this)}
|
|
||||||
fieldName={this.props.fieldName}
|
|
||||||
fieldSpec={this.props.fieldSpec}
|
|
||||||
value={this.props.value}
|
|
||||||
onDeleteStop={this.deleteStop.bind(this)}
|
|
||||||
onAddStop={this.addStop.bind(this)}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
specField = (
|
|
||||||
<SpecProperty
|
|
||||||
onChange={this.props.onChange.bind(this)}
|
|
||||||
fieldName={this.props.fieldName}
|
|
||||||
fieldSpec={this.props.fieldSpec}
|
|
||||||
value={this.props.value}
|
|
||||||
onZoomClick={this.makeZoomFunction.bind(this)}
|
|
||||||
onDataClick={this.makeDataFunction.bind(this)}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return <div className={propClass} data-wd-key={"spec-field:"+this.props.fieldName}>
|
|
||||||
{specField}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,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.bind(this)}
|
|
||||||
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 color from 'color'
|
|
||||||
|
|
||||||
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 '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.function === "interpolated") {
|
|
||||||
return "exponential"
|
|
||||||
}
|
|
||||||
if (fieldSpec.type === "number") {
|
|
||||||
return "interval"
|
|
||||||
}
|
|
||||||
return "categorical"
|
|
||||||
}
|
|
||||||
|
|
||||||
getDataFunctionTypes(functionType) {
|
|
||||||
if (functionType === "interpolated") {
|
|
||||||
return ["categorical", "interval", "exponential"]
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return ["categorical", "interval"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
changeStop(changeIdx, stopData, value) {
|
|
||||||
const stops = this.props.value.stops.slice(0)
|
|
||||||
const changedStop = stopData.zoom === undefined ? stopData.value : stopData
|
|
||||||
stops[changeIdx] = [changedStop, value]
|
|
||||||
const changedValue = {
|
|
||||||
...this.props.value,
|
|
||||||
stops: stops,
|
|
||||||
}
|
|
||||||
this.props.onChange(this.props.fieldName, changedValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
changeDataProperty(propName, propVal) {
|
|
||||||
if (propVal) {
|
|
||||||
this.props.value[propName] = propVal
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
delete this.props.value[propName]
|
|
||||||
}
|
|
||||||
this.props.onChange(this.props.fieldName, this.props.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
if (typeof this.props.value.type === "undefined") {
|
|
||||||
this.props.value.type = this.getFieldFunctionType(this.props.fieldSpec)
|
|
||||||
}
|
|
||||||
|
|
||||||
const dataFields = this.props.value.stops.map((stop, idx) => {
|
|
||||||
const zoomLevel = typeof stop[0] === 'object' ? stop[0].zoom : undefined;
|
|
||||||
const dataLevel = typeof stop[0] === 'object' ? stop[0].value : stop[0];
|
|
||||||
const value = stop[1]
|
|
||||||
const deleteStopBtn = <DeleteStopButton onClick={this.props.onDeleteStop.bind(this, idx)} />
|
|
||||||
|
|
||||||
const dataProps = {
|
|
||||||
label: "Data value",
|
|
||||||
value: dataLevel,
|
|
||||||
onChange: newData => this.changeStop(idx, { zoom: zoomLevel, value: newData }, value)
|
|
||||||
}
|
|
||||||
|
|
||||||
let dataInput;
|
|
||||||
if(this.props.value.type === "categorical") {
|
|
||||||
dataInput = <StringInput {...dataProps} />
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
dataInput = <NumberInput {...dataProps} />
|
|
||||||
}
|
|
||||||
|
|
||||||
let zoomInput = null;
|
|
||||||
if(zoomLevel !== undefined) {
|
|
||||||
zoomInput = <div className="maputnik-data-spec-property-stop-edit">
|
|
||||||
<NumberInput
|
|
||||||
value={zoomLevel}
|
|
||||||
onChange={newZoom => this.changeStop(idx, {zoom: newZoom, value: dataLevel}, value)}
|
|
||||||
min={0}
|
|
||||||
max={22}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
return <InputBlock key={idx} action={deleteStopBtn} label="">
|
|
||||||
{zoomInput}
|
|
||||||
<div className="maputnik-data-spec-property-stop-data">
|
|
||||||
{dataInput}
|
|
||||||
</div>
|
|
||||||
<div className="maputnik-data-spec-property-stop-value">
|
|
||||||
<SpecField
|
|
||||||
fieldName={this.props.fieldName}
|
|
||||||
fieldSpec={this.props.fieldSpec}
|
|
||||||
value={value}
|
|
||||||
onChange={(_, newValue) => this.changeStop(idx, {zoom: zoomLevel, value: dataLevel}, newValue)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</InputBlock>
|
|
||||||
})
|
|
||||||
|
|
||||||
return <div className="maputnik-data-spec-block">
|
|
||||||
<div className="maputnik-data-spec-property">
|
|
||||||
<InputBlock
|
|
||||||
doc={this.props.fieldSpec.doc}
|
|
||||||
label={labelFromFieldName(this.props.fieldName)}
|
|
||||||
>
|
|
||||||
<div className="maputnik-data-spec-property-group">
|
|
||||||
<DocLabel
|
|
||||||
label="Property"
|
|
||||||
doc={"Input a data property to base styles off of."}
|
|
||||||
/>
|
|
||||||
<div className="maputnik-data-spec-property-input">
|
|
||||||
<StringInput
|
|
||||||
value={this.props.value.property}
|
|
||||||
onChange={propVal => this.changeDataProperty("property", propVal)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="maputnik-data-spec-property-group">
|
|
||||||
<DocLabel
|
|
||||||
label="Type"
|
|
||||||
doc={"Select a type of data scale (default is 'categorical')."}
|
|
||||||
/>
|
|
||||||
<div className="maputnik-data-spec-property-input">
|
|
||||||
<SelectInput
|
|
||||||
value={this.props.value.type}
|
|
||||||
onChange={propVal => this.changeDataProperty("type", propVal)}
|
|
||||||
options={this.getDataFunctionTypes(this.props.fieldSpec.function)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="maputnik-data-spec-property-group">
|
|
||||||
<DocLabel
|
|
||||||
label="Default"
|
|
||||||
doc={"Input a default value for data if not covered by the scales."}
|
|
||||||
/>
|
|
||||||
<div className="maputnik-data-spec-property-input">
|
|
||||||
<SpecField
|
|
||||||
fieldName={this.props.fieldName}
|
|
||||||
fieldSpec={this.props.fieldSpec}
|
|
||||||
value={this.props.value.default}
|
|
||||||
onChange={(_, propVal) => this.changeDataProperty("default", propVal)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</InputBlock>
|
|
||||||
</div>
|
|
||||||
{dataFields}
|
|
||||||
<Button
|
|
||||||
className="maputnik-add-stop"
|
|
||||||
onClick={this.props.onAddStop.bind(this)}
|
|
||||||
>
|
|
||||||
Add stop
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
|
|
||||||
import DocLabel from './DocLabel'
|
|
||||||
import Button from '../Button'
|
|
||||||
import DeleteIcon from 'react-icons/lib/md/delete'
|
|
||||||
|
|
||||||
|
|
||||||
export default class DeleteStopButton extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
onClick: PropTypes.func,
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return <Button
|
|
||||||
className="maputnik-delete-stop"
|
|
||||||
onClick={this.props.onClick}
|
|
||||||
>
|
|
||||||
<DocLabel
|
|
||||||
label={<DeleteIcon />}
|
|
||||||
doc={"Remove zoom level stop."}
|
|
||||||
/>
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
|
|
||||||
import DocLabel from './DocLabel'
|
|
||||||
import Button from '../Button'
|
|
||||||
import FunctionIcon from 'react-icons/lib/md/functions'
|
|
||||||
import MdInsertChart from 'react-icons/lib/md/insert-chart'
|
|
||||||
|
|
||||||
|
|
||||||
export default class FunctionButtons extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
fieldSpec: PropTypes.object,
|
|
||||||
onZoomClick: PropTypes.func,
|
|
||||||
onDataClick: PropTypes.func,
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
let makeZoomButton, makeDataButton
|
|
||||||
if (this.props.fieldSpec['zoom-function']) {
|
|
||||||
makeZoomButton = <Button
|
|
||||||
className="maputnik-make-zoom-function"
|
|
||||||
onClick={this.props.onZoomClick}
|
|
||||||
>
|
|
||||||
<DocLabel
|
|
||||||
label={<FunctionIcon />}
|
|
||||||
cursorTargetStyle={{ cursor: 'pointer' }}
|
|
||||||
doc={"Turn property into a zoom function to enable a map feature to change with map's zoom level."}
|
|
||||||
/>
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
if (this.props.fieldSpec['property-function'] && ['piecewise-constant', 'interpolated'].indexOf(this.props.fieldSpec['function']) !== -1) {
|
|
||||||
makeDataButton = <Button
|
|
||||||
className="maputnik-make-data-function"
|
|
||||||
onClick={this.props.onDataClick}
|
|
||||||
>
|
|
||||||
<DocLabel
|
|
||||||
label={<MdInsertChart />}
|
|
||||||
cursorTargetStyle={{ cursor: 'pointer' }}
|
|
||||||
doc={"Turn property into a data function to enable a map feature to change according to data properties and the map's zoom level."}
|
|
||||||
/>
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
return <div>{makeDataButton}{makeZoomButton}</div>
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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,161 +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'
|
|
||||||
|
|
||||||
|
|
||||||
export default class ZoomProperty extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
onChange: PropTypes.func,
|
|
||||||
onDeleteStop: PropTypes.func,
|
|
||||||
onAddStop: PropTypes.func,
|
|
||||||
fieldName: PropTypes.string,
|
|
||||||
fieldSpec: PropTypes.object,
|
|
||||||
value: PropTypes.oneOfType([
|
|
||||||
PropTypes.object,
|
|
||||||
PropTypes.string,
|
|
||||||
PropTypes.number,
|
|
||||||
PropTypes.bool,
|
|
||||||
PropTypes.array
|
|
||||||
]),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super()
|
|
||||||
this.state = {
|
|
||||||
refs: {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
this.setState({
|
|
||||||
refs: this.setStopRefs(this.props)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* We cache a reference for each stop by its index.
|
|
||||||
*
|
|
||||||
* When the stops are reordered the references are also updated (see this.orderStops) this allows React to use the same key for the element and keep keyboard focus.
|
|
||||||
*/
|
|
||||||
setStopRefs(props) {
|
|
||||||
// This is initialsed below only if required to improved performance.
|
|
||||||
let newRefs;
|
|
||||||
|
|
||||||
if(props.value && props.value.stops) {
|
|
||||||
props.value.stops.forEach((val, idx) => {
|
|
||||||
if(!this.state.refs.hasOwnProperty(idx)) {
|
|
||||||
if(!newRefs) {
|
|
||||||
newRefs = {...this.state.refs};
|
|
||||||
}
|
|
||||||
newRefs[idx] = docUid("stop-");
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return newRefs;
|
|
||||||
}
|
|
||||||
|
|
||||||
UNSAFE_componentWillReceiveProps(nextProps) {
|
|
||||||
const newRefs = this.setStopRefs(nextProps);
|
|
||||||
if(newRefs) {
|
|
||||||
this.setState({
|
|
||||||
refs: newRefs
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Order the stops altering the refs to reflect their new position.
|
|
||||||
orderStopsByZoom(stops) {
|
|
||||||
const mappedWithRef = stops
|
|
||||||
.map((stop, idx) => {
|
|
||||||
return {
|
|
||||||
ref: this.state.refs[idx],
|
|
||||||
data: stop
|
|
||||||
}
|
|
||||||
})
|
|
||||||
// Sort by zoom
|
|
||||||
.sort((a, b) => sortNumerically(a.data[0], b.data[0]));
|
|
||||||
|
|
||||||
// Fetch the new position of the stops
|
|
||||||
const newRefs = {};
|
|
||||||
mappedWithRef
|
|
||||||
.forEach((stop, idx) =>{
|
|
||||||
newRefs[idx] = stop.ref;
|
|
||||||
})
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
refs: newRefs
|
|
||||||
});
|
|
||||||
|
|
||||||
return mappedWithRef.map((item) => item.data);
|
|
||||||
}
|
|
||||||
|
|
||||||
changeZoomStop(changeIdx, stopData, value) {
|
|
||||||
const stops = this.props.value.stops.slice(0);
|
|
||||||
stops[changeIdx] = [stopData, value];
|
|
||||||
|
|
||||||
const orderedStops = this.orderStopsByZoom(stops);
|
|
||||||
|
|
||||||
const changedValue = {
|
|
||||||
...this.props.value,
|
|
||||||
stops: orderedStops
|
|
||||||
}
|
|
||||||
this.props.onChange(this.props.fieldName, changedValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const zoomFields = this.props.value.stops.map((stop, idx) => {
|
|
||||||
const zoomLevel = stop[0]
|
|
||||||
const key = this.state.refs[idx];
|
|
||||||
const value = stop[1]
|
|
||||||
const deleteStopBtn= <DeleteStopButton onClick={this.props.onDeleteStop.bind(this, idx)} />
|
|
||||||
|
|
||||||
return <InputBlock
|
|
||||||
key={key}
|
|
||||||
doc={this.props.fieldSpec.doc}
|
|
||||||
label={labelFromFieldName(this.props.fieldName)}
|
|
||||||
action={deleteStopBtn}
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<div className="maputnik-zoom-spec-property-stop-edit">
|
|
||||||
<NumberInput
|
|
||||||
value={zoomLevel}
|
|
||||||
onChange={changedStop => this.changeZoomStop(idx, changedStop, value)}
|
|
||||||
min={0}
|
|
||||||
max={22}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="maputnik-zoom-spec-property-stop-value">
|
|
||||||
<SpecField
|
|
||||||
fieldName={this.props.fieldName}
|
|
||||||
fieldSpec={this.props.fieldSpec}
|
|
||||||
value={value}
|
|
||||||
onChange={(_, newValue) => this.changeZoomStop(idx, zoomLevel, newValue)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</InputBlock>
|
|
||||||
});
|
|
||||||
|
|
||||||
return <div className="maputnik-zoom-spec-property">
|
|
||||||
{zoomFields}
|
|
||||||
<Button
|
|
||||||
className="maputnik-add-stop"
|
|
||||||
onClick={this.props.onAddStop.bind(this)}
|
|
||||||
>
|
|
||||||
Add stop
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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,114 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
import { combiningFilterOps } from '../../libs/filterops.js'
|
|
||||||
|
|
||||||
import * as styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
|
|
||||||
import DocLabel from '../fields/DocLabel'
|
|
||||||
import SelectInput from '../inputs/SelectInput'
|
|
||||||
import SingleFilterEditor from './SingleFilterEditor'
|
|
||||||
import FilterEditorBlock from './FilterEditorBlock'
|
|
||||||
import Button from '../Button'
|
|
||||||
|
|
||||||
import DeleteIcon from 'react-icons/lib/md/delete'
|
|
||||||
import AddIcon from 'react-icons/lib/fa/plus'
|
|
||||||
|
|
||||||
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={styleSpec.latest.layer.filter.doc + " Combine multiple filters together by using a compound filter."}
|
|
||||||
/>
|
|
||||||
<SelectInput
|
|
||||||
value={combiningOp}
|
|
||||||
onChange={this.onFilterPartChanged.bind(this, 0)}
|
|
||||||
options={[["all", "every filter matches"], ["none", "no filter matches"], ["any", "any filter matches"]]}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{editorBlocks}
|
|
||||||
<div className="maputnik-filter-editor-add-wrapper">
|
|
||||||
<Button
|
|
||||||
data-wd-key="layer-filter-button"
|
|
||||||
className="maputnik-add-filter"
|
|
||||||
onClick={this.addFilterItem.bind(this)}>
|
|
||||||
Add filter
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
import Button from '../Button'
|
|
||||||
import DeleteIcon from 'react-icons/lib/md/delete'
|
|
||||||
|
|
||||||
class FilterEditorBlock extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
onDelete: PropTypes.func.isRequired,
|
|
||||||
children: PropTypes.element.isRequired,
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return <div className="maputnik-filter-editor-block">
|
|
||||||
<div className="maputnik-filter-editor-block-action">
|
|
||||||
<Button
|
|
||||||
className="maputnik-delete-filter"
|
|
||||||
onClick={this.props.onDelete}
|
|
||||||
>
|
|
||||||
<DeleteIcon />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div className="maputnik-filter-editor-block-content">
|
|
||||||
{this.props.children}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default FilterEditorBlock
|
|
||||||
@@ -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,93 +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
|
|
||||||
}
|
|
||||||
|
|
||||||
static defaultProps = {
|
|
||||||
onChange: () => {},
|
|
||||||
options: [],
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
this.state = {
|
|
||||||
maxHeight: MAX_HEIGHT
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
calcMaxHeight() {
|
|
||||||
if(this.props.keepMenuWithinWindowBounds) {
|
|
||||||
const maxHeight = window.innerHeight - this.autocompleteMenuEl.getBoundingClientRect().top;
|
|
||||||
const limitedMaxHeight = Math.min(maxHeight, MAX_HEIGHT);
|
|
||||||
|
|
||||||
if(limitedMaxHeight != this.state.maxHeight) {
|
|
||||||
this.setState({
|
|
||||||
maxHeight: limitedMaxHeight
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
componentDidMount() {
|
|
||||||
this.calcMaxHeight();
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate() {
|
|
||||||
this.calcMaxHeight();
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return <div
|
|
||||||
ref={(el) => {
|
|
||||||
this.autocompleteMenuEl = el;
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Autocomplete
|
|
||||||
menuStyle={{
|
|
||||||
position: "fixed",
|
|
||||||
overflow: "auto",
|
|
||||||
maxHeight: this.state.maxHeight
|
|
||||||
}}
|
|
||||||
wrapperProps={{
|
|
||||||
className: "maputnik-autocomplete",
|
|
||||||
style: null
|
|
||||||
}}
|
|
||||||
inputProps={{
|
|
||||||
className: "maputnik-string",
|
|
||||||
spellCheck: false
|
|
||||||
}}
|
|
||||||
value={this.props.value}
|
|
||||||
items={this.props.options}
|
|
||||||
getItemValue={(item) => item[0]}
|
|
||||||
onSelect={v => this.props.onChange(v)}
|
|
||||||
onChange={(e, v) => this.props.onChange(v)}
|
|
||||||
shouldItemRender={(item, value) => {
|
|
||||||
return item[0].toLowerCase().indexOf(value.toLowerCase()) > -1
|
|
||||||
}}
|
|
||||||
renderItem={(item, isHighlighted) => (
|
|
||||||
<div
|
|
||||||
key={item[0]}
|
|
||||||
className={classnames({
|
|
||||||
"maputnik-autocomplete-menu-item": true,
|
|
||||||
"maputnik-autocomplete-menu-item-selected": isHighlighted,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{item[1]}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default AutocompleteInput
|
|
||||||
@@ -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,106 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
import StringInput from './StringInput'
|
|
||||||
import NumberInput from './NumberInput'
|
|
||||||
import Button from '../Button'
|
|
||||||
import DeleteIcon from 'react-icons/lib/md/delete'
|
|
||||||
import DocLabel from '../fields/DocLabel'
|
|
||||||
|
|
||||||
|
|
||||||
class DynamicArrayInput extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
value: PropTypes.array,
|
|
||||||
type: PropTypes.string,
|
|
||||||
default: PropTypes.array,
|
|
||||||
onChange: PropTypes.func,
|
|
||||||
style: PropTypes.object,
|
|
||||||
}
|
|
||||||
|
|
||||||
changeValue(idx, newValue) {
|
|
||||||
console.log(idx, newValue)
|
|
||||||
const values = this.values.slice(0)
|
|
||||||
values[idx] = newValue
|
|
||||||
this.props.onChange(values)
|
|
||||||
}
|
|
||||||
|
|
||||||
get values() {
|
|
||||||
return this.props.value || this.props.default || []
|
|
||||||
}
|
|
||||||
|
|
||||||
addValue() {
|
|
||||||
const values = this.values.slice(0)
|
|
||||||
if (this.props.type === 'number') {
|
|
||||||
values.push(0)
|
|
||||||
} else {
|
|
||||||
values.push("")
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
this.props.onChange(values)
|
|
||||||
}
|
|
||||||
|
|
||||||
deleteValue(valueIdx) {
|
|
||||||
const values = this.values.slice(0)
|
|
||||||
values.splice(valueIdx, 1)
|
|
||||||
|
|
||||||
this.props.onChange(values)
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const inputs = this.values.map((v, i) => {
|
|
||||||
const deleteValueBtn= <DeleteValueButton onClick={this.deleteValue.bind(this, i)} />
|
|
||||||
const input = this.props.type === 'number'
|
|
||||||
? <NumberInput
|
|
||||||
value={v}
|
|
||||||
onChange={this.changeValue.bind(this, i)}
|
|
||||||
/>
|
|
||||||
: <StringInput
|
|
||||||
value={v}
|
|
||||||
onChange={this.changeValue.bind(this, i)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
return <div
|
|
||||||
style={this.props.style}
|
|
||||||
key={i}
|
|
||||||
className="maputnik-array-block"
|
|
||||||
>
|
|
||||||
<div className="maputnik-array-block-action">
|
|
||||||
{deleteValueBtn}
|
|
||||||
</div>
|
|
||||||
<div className="maputnik-array-block-content">
|
|
||||||
{input}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
})
|
|
||||||
|
|
||||||
return <div className="maputnik-array">
|
|
||||||
{inputs}
|
|
||||||
<Button
|
|
||||||
className="maputnik-array-add-value"
|
|
||||||
onClick={this.addValue.bind(this)}
|
|
||||||
>
|
|
||||||
Add value
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class DeleteValueButton extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
onClick: PropTypes.func,
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return <Button
|
|
||||||
className="maputnik-delete-stop"
|
|
||||||
onClick={this.props.onClick}
|
|
||||||
>
|
|
||||||
<DocLabel
|
|
||||||
label={<DeleteIcon />}
|
|
||||||
doc={"Remove array entry."}
|
|
||||||
/>
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default DynamicArrayInput
|
|
||||||
@@ -1,44 +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() {
|
|
||||||
return this.props.value || this.props.default.slice(1) || []
|
|
||||||
}
|
|
||||||
|
|
||||||
changeFont(idx, newValue) {
|
|
||||||
const changedValues = this.values.slice(0)
|
|
||||||
changedValues[idx] = newValue
|
|
||||||
this.props.onChange(changedValues)
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const inputs = this.values.map((value, i) => {
|
|
||||||
return <AutocompleteInput
|
|
||||||
key={i}
|
|
||||||
value={value}
|
|
||||||
options={this.props.fonts.map(f => [f, f])}
|
|
||||||
onChange={this.changeFont.bind(this, i)}
|
|
||||||
/>
|
|
||||||
})
|
|
||||||
|
|
||||||
return <div className="maputnik-font">
|
|
||||||
{inputs}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default FontInput
|
|
||||||
@@ -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.array,
|
|
||||||
icons: PropTypes.array,
|
|
||||||
style: PropTypes.object,
|
|
||||||
onChange: PropTypes.func.isRequired,
|
|
||||||
}
|
|
||||||
|
|
||||||
static defaultProps = {
|
|
||||||
icons: []
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return <AutocompleteInput
|
|
||||||
value={this.props.value}
|
|
||||||
options={this.props.icons.map(f => [f, f])}
|
|
||||||
onChange={this.props.onChange}
|
|
||||||
wrapperStyle={this.props.style}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default IconInput
|
|
||||||
@@ -1,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 === "" ? null: 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,80 +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 = {
|
|
||||||
value: props.value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
UNSAFE_componentWillReceiveProps(nextProps) {
|
|
||||||
this.setState({ value: nextProps.value })
|
|
||||||
}
|
|
||||||
|
|
||||||
changeValue(newValue) {
|
|
||||||
const value = parseFloat(newValue)
|
|
||||||
|
|
||||||
const hasChanged = this.state.value !== value
|
|
||||||
if(this.isValid(value) && hasChanged) {
|
|
||||||
this.props.onChange(value)
|
|
||||||
} else {
|
|
||||||
this.setState({ value: newValue })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
isValid(v) {
|
|
||||||
const value = parseFloat(v)
|
|
||||||
if(isNaN(value)) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!isNaN(this.props.min) && value < this.props.min) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!isNaN(this.props.max) && value > this.props.max) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
resetValue() {
|
|
||||||
// Reset explicitly to default value if value has been cleared
|
|
||||||
if(this.state.value === "") {
|
|
||||||
return this.changeValue(this.props.default)
|
|
||||||
}
|
|
||||||
|
|
||||||
// If set value is invalid fall back to the last valid value from props or at last resort the default value
|
|
||||||
if(!this.isValid(this.state.value)) {
|
|
||||||
if(this.isValid(this.props.value)) {
|
|
||||||
this.changeValue(this.props.value)
|
|
||||||
} else {
|
|
||||||
this.changeValue(this.props.default)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return <input
|
|
||||||
spellCheck="false"
|
|
||||||
className="maputnik-number"
|
|
||||||
placeholder={this.props.default}
|
|
||||||
value={this.state.value}
|
|
||||||
onChange={e => this.changeValue(e.target.value)}
|
|
||||||
onBlur={this.resetValue.bind(this)}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default NumberInput
|
|
||||||
@@ -1,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,62 +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 = {
|
|
||||||
value: props.value || ''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
UNSAFE_componentWillReceiveProps(nextProps) {
|
|
||||||
this.setState({ value: nextProps.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({
|
|
||||||
value: e.target.value
|
|
||||||
})
|
|
||||||
},
|
|
||||||
onBlur: () => {
|
|
||||||
if(this.state.value!==this.props.value) this.props.onChange(this.state.value)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default StringInput
|
|
||||||
@@ -1,30 +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
|
|
||||||
}
|
|
||||||
|
|
||||||
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,21 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
import CollapseOpenIcon from 'react-icons/lib/md/arrow-drop-down'
|
|
||||||
import CollapseCloseIcon from 'react-icons/lib/md/arrow-drop-up'
|
|
||||||
|
|
||||||
export default class Collapser extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
isCollapsed: PropTypes.bool.isRequired,
|
|
||||||
style: PropTypes.object,
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const iconStyle = {
|
|
||||||
width: 20,
|
|
||||||
height: 20,
|
|
||||||
...this.props.style,
|
|
||||||
}
|
|
||||||
return this.props.isCollapsed ? <CollapseCloseIcon style={iconStyle}/> : <CollapseOpenIcon style={iconStyle} />
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -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,90 +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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
UNSAFE_componentWillReceiveProps(nextProps) {
|
|
||||||
this.setState({
|
|
||||||
code: JSON.stringify(nextProps.layer, null, 2)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
shouldComponentUpdate(nextProps, nextState) {
|
|
||||||
try {
|
|
||||||
const parsedLayer = JSON.parse(this.state.code)
|
|
||||||
// If the structure is still the same do not update
|
|
||||||
// because it affects editing experience by reformatting all the time
|
|
||||||
return nextState.code !== JSON.stringify(parsedLayer, null, 2)
|
|
||||||
} catch(err) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onCodeUpdate(newCode) {
|
|
||||||
try {
|
|
||||||
const parsedLayer = JSON.parse(newCode)
|
|
||||||
this.props.onChange(parsedLayer)
|
|
||||||
} catch(err) {
|
|
||||||
console.warn(err)
|
|
||||||
} finally {
|
|
||||||
this.setState({
|
|
||||||
code: newCode
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
resetValue() {
|
|
||||||
console.log('reset')
|
|
||||||
this.setState({
|
|
||||||
code: JSON.stringify(this.props.layer, null, 2)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const codeMirrorOptions = {
|
|
||||||
mode: {name: "javascript", json: true},
|
|
||||||
tabSize: 2,
|
|
||||||
theme: 'maputnik',
|
|
||||||
viewportMargin: Infinity,
|
|
||||||
lineNumbers: 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,282 +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 MoreVertIcon from 'react-icons/lib/md/more-vert'
|
|
||||||
|
|
||||||
import InputBlock from '../inputs/InputBlock'
|
|
||||||
import MultiButtonInput from '../inputs/MultiButtonInput'
|
|
||||||
|
|
||||||
import { changeType, changeProperty } from '../../libs/layer'
|
|
||||||
import layout from '../../config/layout.json'
|
|
||||||
|
|
||||||
|
|
||||||
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 }
|
|
||||||
}
|
|
||||||
|
|
||||||
UNSAFE_componentWillReceiveProps(nextProps) {
|
|
||||||
const additionalGroups = { ...this.state.editorGroups }
|
|
||||||
|
|
||||||
layout[nextProps.layer.type].groups.forEach(group => {
|
|
||||||
if(!(group.title in additionalGroups)) {
|
|
||||||
additionalGroups[group.title] = true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
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'>
|
|
||||||
<MoreVertIcon className="more-menu__button__svg" />
|
|
||||||
</Button>
|
|
||||||
<Menu>
|
|
||||||
<ul className="more-menu__menu">
|
|
||||||
{Object.keys(items).map((id, idx) => {
|
|
||||||
const item = items[id];
|
|
||||||
return <li key={id}>
|
|
||||||
<MenuItem value={id} className='more-menu__menu__item'>
|
|
||||||
{item.text}
|
|
||||||
</MenuItem>
|
|
||||||
</li>
|
|
||||||
})}
|
|
||||||
</ul>
|
|
||||||
</Menu>
|
|
||||||
</Wrapper>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</header>
|
|
||||||
{groups}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,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 * as styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
|
|
||||||
import InputBlock from '../inputs/InputBlock'
|
|
||||||
import StringInput from '../inputs/StringInput'
|
|
||||||
|
|
||||||
class LayerIdBlock extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
value: PropTypes.string.isRequired,
|
|
||||||
wdKey: PropTypes.string.isRequired,
|
|
||||||
onChange: PropTypes.func.isRequired,
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return <InputBlock label={"ID"} doc={styleSpec.latest.layer.id.doc}
|
|
||||||
data-wd-key={this.props.wdKey}
|
|
||||||
>
|
|
||||||
<StringInput
|
|
||||||
value={this.props.value}
|
|
||||||
onChange={this.props.onChange}
|
|
||||||
/>
|
|
||||||
</InputBlock>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default LayerIdBlock
|
|
||||||
@@ -1,216 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
import classnames from 'classnames'
|
|
||||||
|
|
||||||
import Button from '../Button'
|
|
||||||
import LayerListGroup from './LayerListGroup'
|
|
||||||
import LayerListItem from './LayerListItem'
|
|
||||||
import AddIcon from 'react-icons/lib/md/add-circle-outline'
|
|
||||||
import AddModal from '../modals/AddModal'
|
|
||||||
|
|
||||||
import style from '../../libs/style.js'
|
|
||||||
import {SortableContainer, SortableHandle} 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
|
|
||||||
@SortableContainer
|
|
||||||
class LayerListContainer extends React.Component {
|
|
||||||
static propTypes = {...layerListPropTypes}
|
|
||||||
static defaultProps = {
|
|
||||||
onLayerSelect: () => {},
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
super(props)
|
|
||||||
this.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.bind(this)}
|
|
||||||
className="maputnik-button">
|
|
||||||
{this.state.areAllGroupsExpanded === true ? "Collapse" : "Expand"}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="maputnik-default-property">
|
|
||||||
<div className="maputnik-multibutton">
|
|
||||||
<button
|
|
||||||
onClick={this.toggleModal.bind(this, 'add')}
|
|
||||||
data-wd-key="layer-list:add-layer"
|
|
||||||
className="maputnik-button maputnik-button-selected">
|
|
||||||
Add Layer
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
<ul className="maputnik-layer-list-container">
|
|
||||||
{listItems}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class LayerList extends React.Component {
|
|
||||||
static propTypes = {...layerListPropTypes}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return <LayerListContainer
|
|
||||||
{...this.props}
|
|
||||||
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,126 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
import Color from 'color'
|
|
||||||
import classnames from 'classnames'
|
|
||||||
|
|
||||||
import CopyIcon from 'react-icons/lib/md/content-copy'
|
|
||||||
import VisibilityIcon from 'react-icons/lib/md/visibility'
|
|
||||||
import VisibilityOffIcon from 'react-icons/lib/md/visibility-off'
|
|
||||||
import DeleteIcon from 'react-icons/lib/md/delete'
|
|
||||||
|
|
||||||
import LayerIcon from '../icons/LayerIcon'
|
|
||||||
import LayerEditor from './LayerEditor'
|
|
||||||
import {SortableElement, SortableHandle} from 'react-sortable-hoc'
|
|
||||||
|
|
||||||
@SortableHandle
|
|
||||||
class LayerTypeDragHandle extends React.Component {
|
|
||||||
static propTypes = LayerIcon.propTypes
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return <LayerIcon
|
|
||||||
{...this.props}
|
|
||||||
style={{
|
|
||||||
cursor: 'move',
|
|
||||||
width: 14,
|
|
||||||
height: 14,
|
|
||||||
paddingRight: 3,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class IconAction extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
action: PropTypes.string.isRequired,
|
|
||||||
onClick: PropTypes.func.isRequired,
|
|
||||||
wdKey: PropTypes.string
|
|
||||||
}
|
|
||||||
|
|
||||||
renderIcon() {
|
|
||||||
switch(this.props.action) {
|
|
||||||
case 'duplicate': return <CopyIcon />
|
|
||||||
case 'show': return <VisibilityIcon />
|
|
||||||
case 'hide': return <VisibilityOffIcon />
|
|
||||||
case 'delete': return <DeleteIcon />
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return <button
|
|
||||||
tabIndex="-1"
|
|
||||||
title={this.props.action}
|
|
||||||
className="maputnik-layer-list-icon-action"
|
|
||||||
data-wd-key={this.props.wdKey}
|
|
||||||
onClick={this.props.onClick}
|
|
||||||
>
|
|
||||||
{this.renderIcon()}
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@SortableElement
|
|
||||||
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() {
|
|
||||||
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,
|
|
||||||
})}>
|
|
||||||
<LayerTypeDragHandle type={this.props.layerType} />
|
|
||||||
<span className="maputnik-layer-list-item-id">{this.props.layerId}</span>
|
|
||||||
<span style={{flexGrow: 1}} />
|
|
||||||
<IconAction
|
|
||||||
wdKey={"layer-list-item:"+this.props.layerId+":delete"}
|
|
||||||
action={'delete'}
|
|
||||||
onClick={e => this.props.onLayerDestroy(this.props.layerId)}
|
|
||||||
/>
|
|
||||||
<IconAction
|
|
||||||
wdKey={"layer-list-item:"+this.props.layerId+":copy"}
|
|
||||||
action={'duplicate'}
|
|
||||||
onClick={e => this.props.onLayerCopy(this.props.layerId)}
|
|
||||||
/>
|
|
||||||
<IconAction
|
|
||||||
wdKey={"layer-list-item:"+this.props.layerId+":toggle-visibility"}
|
|
||||||
action={this.props.visibility === 'visible' ? 'hide' : 'show'}
|
|
||||||
onClick={e => this.props.onLayerVisibilityToggle(this.props.layerId)}
|
|
||||||
/>
|
|
||||||
</li>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default LayerListItem;
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
|
|
||||||
import * as styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
|
|
||||||
import InputBlock from '../inputs/InputBlock'
|
|
||||||
import StringInput from '../inputs/StringInput'
|
|
||||||
import AutocompleteInput from '../inputs/AutocompleteInput'
|
|
||||||
|
|
||||||
class LayerSourceBlock extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
value: PropTypes.string,
|
|
||||||
wdKey: PropTypes.string,
|
|
||||||
onChange: PropTypes.func,
|
|
||||||
sourceIds: PropTypes.array,
|
|
||||||
}
|
|
||||||
|
|
||||||
static defaultProps = {
|
|
||||||
onChange: () => {},
|
|
||||||
sourceIds: [],
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return <InputBlock label={"Source"} doc={styleSpec.latest.layer.source.doc}
|
|
||||||
data-wd-key={this.props.wdKey}
|
|
||||||
>
|
|
||||||
<AutocompleteInput
|
|
||||||
value={this.props.value}
|
|
||||||
onChange={this.props.onChange}
|
|
||||||
options={this.props.sourceIds.map(src => [src, src])}
|
|
||||||
/>
|
|
||||||
</InputBlock>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default LayerSourceBlock
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
|
|
||||||
import * as styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
|
|
||||||
import InputBlock from '../inputs/InputBlock'
|
|
||||||
import StringInput from '../inputs/StringInput'
|
|
||||||
import AutocompleteInput from '../inputs/AutocompleteInput'
|
|
||||||
|
|
||||||
class LayerSourceLayer extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
value: PropTypes.string,
|
|
||||||
onChange: PropTypes.func,
|
|
||||||
sourceLayerIds: PropTypes.array,
|
|
||||||
isFixed: PropTypes.bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
static defaultProps = {
|
|
||||||
onChange: () => {},
|
|
||||||
sourceLayerIds: [],
|
|
||||||
isFixed: false
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return <InputBlock label={"Source Layer"} doc={styleSpec.latest.layer['source-layer'].doc}
|
|
||||||
data-wd-key="layer-source-layer"
|
|
||||||
>
|
|
||||||
<AutocompleteInput
|
|
||||||
keepMenuWithinWindowBounds={!!this.props.isFixed}
|
|
||||||
value={this.props.value}
|
|
||||||
onChange={this.props.onChange}
|
|
||||||
options={this.props.sourceLayerIds.map(l => [l, l])}
|
|
||||||
/>
|
|
||||||
</InputBlock>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default LayerSourceLayer
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
|
|
||||||
import * as styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
|
|
||||||
import InputBlock from '../inputs/InputBlock'
|
|
||||||
import SelectInput from '../inputs/SelectInput'
|
|
||||||
|
|
||||||
class LayerTypeBlock extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
value: PropTypes.string.isRequired,
|
|
||||||
wdKey: PropTypes.string,
|
|
||||||
onChange: PropTypes.func.isRequired,
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return <InputBlock label={"Type"} doc={styleSpec.latest.layer.type.doc}
|
|
||||||
data-wd-key={this.props.wdKey}
|
|
||||||
>
|
|
||||||
<SelectInput
|
|
||||||
options={[
|
|
||||||
['background', 'Background'],
|
|
||||||
['fill', 'Fill'],
|
|
||||||
['line', 'Line'],
|
|
||||||
['symbol', 'Symbol'],
|
|
||||||
['raster', 'Raster'],
|
|
||||||
['circle', 'Circle'],
|
|
||||||
['fill-extrusion', 'Fill Extrusion'],
|
|
||||||
['hillshade', 'Hillshade'],
|
|
||||||
['heatmap', 'Heatmap'],
|
|
||||||
]}
|
|
||||||
onChange={this.props.onChange}
|
|
||||||
value={this.props.value}
|
|
||||||
/>
|
|
||||||
</InputBlock>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default LayerTypeBlock
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
|
|
||||||
import * as styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
|
|
||||||
import InputBlock from '../inputs/InputBlock'
|
|
||||||
import NumberInput from '../inputs/NumberInput'
|
|
||||||
|
|
||||||
class MaxZoomBlock extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
value: PropTypes.number,
|
|
||||||
onChange: PropTypes.func.isRequired,
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return <InputBlock label={"Max Zoom"} doc={styleSpec.latest.layer.maxzoom.doc}
|
|
||||||
data-wd-key="max-zoom"
|
|
||||||
>
|
|
||||||
<NumberInput
|
|
||||||
value={this.props.value}
|
|
||||||
onChange={this.props.onChange}
|
|
||||||
min={styleSpec.latest.layer.maxzoom.minimum}
|
|
||||||
max={styleSpec.latest.layer.maxzoom.maximum}
|
|
||||||
default={styleSpec.latest.layer.maxzoom.maximum}
|
|
||||||
/>
|
|
||||||
</InputBlock>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default MaxZoomBlock
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
|
|
||||||
import * as styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
|
|
||||||
import InputBlock from '../inputs/InputBlock'
|
|
||||||
import NumberInput from '../inputs/NumberInput'
|
|
||||||
|
|
||||||
class MinZoomBlock extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
value: PropTypes.number,
|
|
||||||
onChange: PropTypes.func.isRequired,
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return <InputBlock label={"Min Zoom"} doc={styleSpec.latest.layer.minzoom.doc}
|
|
||||||
data-wd-key="min-zoom"
|
|
||||||
>
|
|
||||||
<NumberInput
|
|
||||||
value={this.props.value}
|
|
||||||
onChange={this.props.onChange}
|
|
||||||
min={styleSpec.latest.layer.minzoom.minimum}
|
|
||||||
max={styleSpec.latest.layer.minzoom.maximum}
|
|
||||||
default={styleSpec.latest.layer.minzoom.minimum}
|
|
||||||
/>
|
|
||||||
</InputBlock>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default MinZoomBlock
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
import InputBlock from '../inputs/InputBlock'
|
|
||||||
import StringInput from '../inputs/StringInput'
|
|
||||||
import LayerIcon from '../icons/LayerIcon'
|
|
||||||
|
|
||||||
function groupFeaturesBySourceLayer(features) {
|
|
||||||
const sources = {}
|
|
||||||
|
|
||||||
let returnedFeatures = {};
|
|
||||||
|
|
||||||
features.forEach(feature => {
|
|
||||||
if(returnedFeatures.hasOwnProperty(feature.layer.id)) {
|
|
||||||
returnedFeatures[feature.layer.id]++
|
|
||||||
|
|
||||||
const featureObject = sources[feature.layer['source-layer']].find(f => f.layer.id === feature.layer.id)
|
|
||||||
|
|
||||||
featureObject.counter = returnedFeatures[feature.layer.id]
|
|
||||||
} else {
|
|
||||||
sources[feature.layer['source-layer']] = sources[feature.layer['source-layer']] || []
|
|
||||||
sources[feature.layer['source-layer']].push(feature)
|
|
||||||
|
|
||||||
returnedFeatures[feature.layer.id] = 1
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return sources
|
|
||||||
}
|
|
||||||
|
|
||||||
class FeatureLayerPopup extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
onLayerSelect: PropTypes.func.isRequired,
|
|
||||||
features: PropTypes.array
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const sources = groupFeaturesBySourceLayer(this.props.features)
|
|
||||||
|
|
||||||
const items = Object.keys(sources).map(vectorLayerId => {
|
|
||||||
const layers = sources[vectorLayerId].map((feature, idx) => {
|
|
||||||
return <label
|
|
||||||
key={idx}
|
|
||||||
className="maputnik-popup-layer"
|
|
||||||
onClick={() => {
|
|
||||||
this.props.onLayerSelect(feature.layer.id)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<LayerIcon type={feature.layer.type} style={{
|
|
||||||
width: 14,
|
|
||||||
height: 14,
|
|
||||||
paddingRight: 3
|
|
||||||
}}/>
|
|
||||||
{feature.layer.id}
|
|
||||||
{feature.counter && <span> × {feature.counter}</span>}
|
|
||||||
</label>
|
|
||||||
})
|
|
||||||
return <div key={vectorLayerId}>
|
|
||||||
<div className="maputnik-popup-layer-id">{vectorLayerId}</div>
|
|
||||||
{layers}
|
|
||||||
</div>
|
|
||||||
})
|
|
||||||
|
|
||||||
return <div className="maputnik-feature-layer-popup">
|
|
||||||
{items}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export default FeatureLayerPopup
|
|
||||||
@@ -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.id}>
|
|
||||||
<div className="maputnik-popup-layer-id">{feature.layer['source-layer']}{feature.inspectModeCounter && <span> × {feature.inspectModeCounter}</span>}</div>
|
|
||||||
<InputBlock key={"property-type"} label={"$type"}>
|
|
||||||
<StringInput value={feature.geometry.type} style={{backgroundColor: 'transparent'}} />
|
|
||||||
</InputBlock>
|
|
||||||
{renderProperties(feature)}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeDuplicatedFeatures(features) {
|
|
||||||
let uniqueFeatures = [];
|
|
||||||
|
|
||||||
features.forEach(feature => {
|
|
||||||
const featureIndex = uniqueFeatures.findIndex(feature2 => {
|
|
||||||
return feature.layer['source-layer'] === feature2.layer['source-layer']
|
|
||||||
&& JSON.stringify(feature.properties) === JSON.stringify(feature2.properties)
|
|
||||||
})
|
|
||||||
|
|
||||||
if(featureIndex === -1) {
|
|
||||||
uniqueFeatures.push(feature)
|
|
||||||
} else {
|
|
||||||
if(uniqueFeatures[featureIndex].hasOwnProperty('counter')) {
|
|
||||||
uniqueFeatures[featureIndex].inspectModeCounter++
|
|
||||||
} else {
|
|
||||||
uniqueFeatures[featureIndex].inspectModeCounter = 2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return uniqueFeatures
|
|
||||||
}
|
|
||||||
|
|
||||||
class FeaturePropertyPopup extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
features: PropTypes.array
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const features = removeDuplicatedFeatures(this.props.features)
|
|
||||||
return <div className="maputnik-feature-property-popup">
|
|
||||||
{features.map(renderFeature)}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export default FeaturePropertyPopup
|
|
||||||
@@ -1,170 +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 style from '../../libs/style.js'
|
|
||||||
import tokens from '../../config/tokens.json'
|
|
||||||
import colors from 'mapbox-gl-inspect/lib/colors'
|
|
||||||
import Color from 'color'
|
|
||||||
import ZoomControl from '../../libs/zoomcontrol'
|
|
||||||
import { colorHighlightedLayer } from '../../libs/highlight'
|
|
||||||
import 'mapbox-gl/dist/mapbox-gl.css'
|
|
||||||
import '../../mapboxgl.css'
|
|
||||||
import '../../libs/mapbox-rtl'
|
|
||||||
|
|
||||||
function renderPropertyPopup(features) {
|
|
||||||
var mountNode = document.createElement('div');
|
|
||||||
ReactDOM.render(<FeaturePropertyPopup features={features} />, mountNode)
|
|
||||||
return mountNode.innerHTML;
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildInspectStyle(originalMapStyle, coloredLayers, highlightedLayer) {
|
|
||||||
const backgroundLayer = {
|
|
||||||
"id": "background",
|
|
||||||
"type": "background",
|
|
||||||
"paint": {
|
|
||||||
"background-color": '#1c1f24',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const layer = colorHighlightedLayer(highlightedLayer)
|
|
||||||
if(layer) {
|
|
||||||
coloredLayers.push(layer)
|
|
||||||
}
|
|
||||||
|
|
||||||
const sources = {}
|
|
||||||
Object.keys(originalMapStyle.sources).forEach(sourceId => {
|
|
||||||
const source = originalMapStyle.sources[sourceId]
|
|
||||||
if(source.type !== 'raster' && source.type !== 'raster-dem') {
|
|
||||||
sources[sourceId] = source
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const inspectStyle = {
|
|
||||||
...originalMapStyle,
|
|
||||||
sources: sources,
|
|
||||||
layers: [backgroundLayer].concat(coloredLayers)
|
|
||||||
}
|
|
||||||
return inspectStyle
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class MapboxGlMap extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
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,
|
|
||||||
isPopupOpen: false,
|
|
||||||
popupX: 0,
|
|
||||||
popupY: 0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
UNSAFE_componentWillReceiveProps(nextProps) {
|
|
||||||
if(!this.state.map) return
|
|
||||||
const metadata = nextProps.mapStyle.metadata || {}
|
|
||||||
MapboxGl.accessToken = metadata['maputnik:mapbox_access_token'] || tokens.mapbox
|
|
||||||
|
|
||||||
if(!nextProps.inspectModeEnabled) {
|
|
||||||
//Mapbox GL now does diffing natively so we don't need to calculate
|
|
||||||
//the necessary operations ourselves!
|
|
||||||
this.state.map.setStyle(nextProps.mapStyle, { diff: true})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
|
||||||
const map = this.state.map;
|
|
||||||
|
|
||||||
if(this.props.inspectModeEnabled !== prevProps.inspectModeEnabled) {
|
|
||||||
this.state.inspect.toggleInspector()
|
|
||||||
}
|
|
||||||
if(this.props.inspectModeEnabled) {
|
|
||||||
this.state.inspect.render()
|
|
||||||
}
|
|
||||||
|
|
||||||
map.showTileBoundaries = this.props.options.showTileBoundaries;
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
const mapOpts = {
|
|
||||||
...this.props.options,
|
|
||||||
container: this.container,
|
|
||||||
style: this.props.mapStyle,
|
|
||||||
hash: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
const map = new MapboxGl.Map(mapOpts);
|
|
||||||
|
|
||||||
map.showTileBoundaries = mapOpts.showTileBoundaries;
|
|
||||||
|
|
||||||
const zoom = new ZoomControl;
|
|
||||||
map.addControl(zoom, 'top-right');
|
|
||||||
|
|
||||||
const nav = new MapboxGl.NavigationControl();
|
|
||||||
map.addControl(nav, 'top-right');
|
|
||||||
|
|
||||||
const inspect = new MapboxInspect({
|
|
||||||
popup: new MapboxGl.Popup({
|
|
||||||
closeOnClick: false
|
|
||||||
}),
|
|
||||||
showMapPopup: true,
|
|
||||||
showMapPopupOnHover: false,
|
|
||||||
showInspectMapPopupOnHover: true,
|
|
||||||
showInspectButton: false,
|
|
||||||
blockHoverPopupOnClick: true,
|
|
||||||
assignLayerColor: (layerId, alpha) => {
|
|
||||||
return Color(colors.brightColor(layerId, alpha)).desaturate(0.5).string()
|
|
||||||
},
|
|
||||||
buildInspectStyle: (originalMapStyle, coloredLayers) => buildInspectStyle(originalMapStyle, coloredLayers, this.props.highlightedLayer),
|
|
||||||
renderPopup: features => {
|
|
||||||
if(this.props.inspectModeEnabled) {
|
|
||||||
return renderPropertyPopup(features)
|
|
||||||
} else {
|
|
||||||
var mountNode = document.createElement('div');
|
|
||||||
ReactDOM.render(<FeatureLayerPopup features={features} onLayerSelect={this.props.onLayerSelect} />, mountNode)
|
|
||||||
return mountNode
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
map.addControl(inspect)
|
|
||||||
|
|
||||||
map.on("style.load", () => {
|
|
||||||
this.setState({ map, inspect });
|
|
||||||
})
|
|
||||||
|
|
||||||
map.on("data", e => {
|
|
||||||
if(e.dataType !== 'tile') return
|
|
||||||
this.props.onDataChange({
|
|
||||||
map: this.state.map
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return <div
|
|
||||||
className="maputnik-map"
|
|
||||||
ref={x => this.container = x}
|
|
||||||
></div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||