mirror of
https://github.com/maputnik/editor.git
synced 2025-12-07 14:50:02 +00:00
Compare commits
1511 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
cc51774259 | ||
|
|
5a19245ee0 | ||
|
|
45f45b7547 | ||
|
|
530bfaf3b3 | ||
|
|
6ea70ab9cf | ||
|
|
a0e2d68dae | ||
|
|
1447e8bfb5 | ||
|
|
c0480a50ea | ||
|
|
09ba2be416 | ||
|
|
115ce3305d | ||
|
|
960b2022ed | ||
|
|
252b442ca9 | ||
|
|
03b9ddda9c | ||
|
|
968d7d7fda | ||
|
|
b211f1cd12 | ||
|
|
870d4349f4 | ||
|
|
d88bc59720 | ||
|
|
7c00775515 | ||
|
|
4b5536b282 | ||
|
|
fb84cfee1c | ||
|
|
9132262106 | ||
|
|
5de9e708e9 | ||
|
|
4df63c7287 | ||
|
|
a88ca031d0 | ||
|
|
452706f35c | ||
|
|
8b0aa194cf | ||
|
|
b9aa7e9206 | ||
|
|
e35f106482 | ||
|
|
b7a97cf8ee | ||
|
|
85a28999fb | ||
|
|
9208115981 | ||
|
|
afbdaecd0a | ||
|
|
558f3d649d | ||
|
|
417511d577 | ||
|
|
df350534ce | ||
|
|
7167235146 | ||
|
|
7a7f2eb7de | ||
|
|
cd28a53f6a | ||
|
|
1fe31ac0ec | ||
|
|
ffce8e3ba5 | ||
|
|
a28a417ebc | ||
|
|
6cdb56d13f | ||
|
|
0516e587b4 | ||
|
|
5b4063105b | ||
|
|
d9a5548762 | ||
|
|
cae6cffb7b | ||
|
|
ede782abed | ||
|
|
00afbad7ac | ||
|
|
edd09ef585 | ||
|
|
1e09066779 | ||
|
|
32edb48e16 | ||
|
|
b116eef147 | ||
|
|
74d1cd2d01 | ||
|
|
fd48d82e42 | ||
|
|
480d54c2d8 | ||
|
|
ab9c39b862 | ||
|
|
dd122d1bac | ||
|
|
f9f5e8f925 | ||
|
|
aa2f4a091c | ||
|
|
13fc699d4a | ||
|
|
f5e8d473ad | ||
|
|
35353d75f5 | ||
|
|
0f103c3c00 | ||
|
|
019428a241 | ||
|
|
6200edea25 | ||
|
|
fc7395df96 | ||
|
|
272f662a34 | ||
|
|
d59d9cde95 | ||
|
|
c71fbcf436 | ||
|
|
54c79445db | ||
|
|
a82ba26f86 | ||
|
|
28af87391d | ||
|
|
0aabd33538 | ||
|
|
bd9076c4ff | ||
|
|
1aed761893 | ||
|
|
a2a6f6dcab | ||
|
|
db5dd0f6ee | ||
|
|
42c3dcf258 | ||
|
|
51a115d65a | ||
|
|
fc0fbd6a37 | ||
|
|
d80d76724c | ||
|
|
77da0a6d30 | ||
|
|
79b251d8b9 | ||
|
|
4f19f6a08c | ||
|
|
d2a6eab1e6 | ||
|
|
c7cf051502 | ||
|
|
6e21503e6b | ||
|
|
78d71a4e7e | ||
|
|
b8f32d46cf | ||
|
|
443782decf | ||
|
|
54e79e5eb8 | ||
|
|
221cd4ffd2 | ||
|
|
354b2fb3cb | ||
|
|
7cb2c36ac9 | ||
|
|
11d73595fc | ||
|
|
c241a6e280 | ||
|
|
198ff143f6 | ||
|
|
7b8b797f9c | ||
|
|
a41b25eea7 | ||
|
|
06eac68f9d | ||
|
|
8abf84ebc0 | ||
|
|
e9aa1f6dd6 | ||
|
|
8e7b838bf7 | ||
|
|
32db3c3c9b | ||
|
|
502586e5d5 | ||
|
|
d92d599d8a | ||
|
|
3487056c7d | ||
|
|
dbcfb08c15 | ||
|
|
e96141090e | ||
|
|
5bd25fc2ed | ||
|
|
334932b298 | ||
|
|
661006d7fb | ||
|
|
c917249517 | ||
|
|
d0ca732fe7 | ||
|
|
52821cd1df | ||
|
|
328e0b8ff7 | ||
|
|
f0147cc89a | ||
|
|
78a7f152e7 | ||
|
|
e936dd16bf | ||
|
|
3d4579288c | ||
|
|
b60df8b074 | ||
|
|
c4b92fa0a9 | ||
|
|
9808d44c71 | ||
|
|
1bdd135386 | ||
|
|
740a75f2e6 | ||
|
|
b62533fa3e | ||
|
|
044349e65f | ||
|
|
e8b0bd4d0a | ||
|
|
45bdf53a41 | ||
|
|
00e94212bd | ||
|
|
1805aee7ba | ||
|
|
8ba2123a26 | ||
|
|
687c08527d | ||
|
|
f0744f024d | ||
|
|
9e82599464 | ||
|
|
7a60df370e | ||
|
|
aee4a041fe | ||
|
|
6fa06e5483 | ||
|
|
15962481ee | ||
|
|
6bf695cd4b | ||
|
|
7ecbc14c39 | ||
|
|
fb0e531f4a | ||
|
|
bd44e6d071 | ||
|
|
3ae37f1c46 | ||
|
|
8c7a1f7075 | ||
|
|
3e97d8a5f1 | ||
|
|
6138257a89 | ||
|
|
0bd62985b9 | ||
|
|
a346d757fd | ||
|
|
84f3970730 | ||
|
|
050e22918a | ||
|
|
f205776695 | ||
|
|
4d427bcbc3 | ||
|
|
0b4910e3c3 | ||
|
|
11a59debdf | ||
|
|
dbe2c2637e | ||
|
|
d6ce13c356 | ||
|
|
6d094a8b3e | ||
|
|
4d0456fd68 | ||
|
|
ad83f940a7 | ||
|
|
edc7e02f58 | ||
|
|
7dfc5029a3 | ||
|
|
8e02722b52 | ||
|
|
984581e01a | ||
|
|
1de7ba7e86 | ||
|
|
a3fa86f7ee | ||
|
|
a589f89c4c | ||
|
|
3b599aed4c | ||
|
|
6953db74c6 | ||
|
|
1ad473a539 | ||
|
|
fafda9ec92 | ||
|
|
11b85bf565 | ||
|
|
6ecc6670dc | ||
|
|
553f0fe23e | ||
|
|
77ddf67201 | ||
|
|
a092bc2689 | ||
|
|
38e0786463 | ||
|
|
180b17d315 | ||
|
|
8acbd784a0 | ||
|
|
07efe1e1b8 | ||
|
|
7ea53cc3a1 | ||
|
|
de21eea21b | ||
|
|
8f8ed6dff3 | ||
|
|
8915bbfeb4 | ||
|
|
df3a42acce | ||
|
|
2a7ef82d23 | ||
|
|
95168f22e3 | ||
|
|
4360753263 | ||
|
|
ad491cb465 | ||
|
|
e5bed80c96 | ||
|
|
9bf3046d4c | ||
|
|
da8dc0f7a6 | ||
|
|
b66a4afd28 | ||
|
|
a94c53534c | ||
|
|
6b22c9130f | ||
|
|
7d5927bbc8 | ||
|
|
240d02a124 | ||
|
|
92ef1c4cbb | ||
|
|
5ce57d0803 | ||
|
|
1c134d757c | ||
|
|
32d808b230 | ||
|
|
ee3def492a | ||
|
|
41bd91fcd2 | ||
|
|
02c8542848 | ||
|
|
844abd38ce | ||
|
|
d9b6f28bb5 | ||
|
|
ed85b838ec | ||
|
|
f82b138a3d | ||
|
|
89c38991b9 | ||
|
|
0e4c06cc3e | ||
|
|
7e510a2582 | ||
|
|
f3cb9c4fdd | ||
|
|
f0f6130272 | ||
|
|
0ebb299fd0 | ||
|
|
9d96525f12 | ||
|
|
fc6f9251f7 | ||
|
|
53cb317155 | ||
|
|
4215b5808f | ||
|
|
2d2f9744e2 | ||
|
|
d0b835ee52 | ||
|
|
1798305f9c | ||
|
|
4b0768d0a6 | ||
|
|
2e79a8ff4c | ||
|
|
e64ca3eb93 | ||
|
|
094c4747d3 | ||
|
|
62f0843283 | ||
|
|
8062e304b7 | ||
|
|
18e7ead78a | ||
|
|
3cab1dc49f | ||
|
|
f8dcbb8fb7 | ||
|
|
c82f38c103 | ||
|
|
fe0e7af033 | ||
|
|
ac51902435 | ||
|
|
e0ff342702 | ||
|
|
664125d820 | ||
|
|
9ae2f2c5af | ||
|
|
721f9b36b3 | ||
|
|
a33d1b819c | ||
|
|
cb4f5ea963 | ||
|
|
3c0ebfabab | ||
|
|
a822430e1d | ||
|
|
0ba11b94c8 | ||
|
|
390e90e8c2 | ||
|
|
59ef8eb4e4 | ||
|
|
2b382a9946 | ||
|
|
d52d55dd6a | ||
|
|
dc40ce7d9e | ||
|
|
383a119127 | ||
|
|
3f492e6208 | ||
|
|
0cec0cf595 | ||
|
|
bc19aea438 | ||
|
|
211850c813 | ||
|
|
c1312fb288 | ||
|
|
0c2934c489 | ||
|
|
ad34147f28 | ||
|
|
1eb6c28617 | ||
|
|
2e8a188bce | ||
|
|
ed495c3216 | ||
|
|
a773958403 | ||
|
|
6a6595d971 | ||
|
|
942b2240a7 | ||
|
|
6e86c60f89 | ||
|
|
ace6812e89 | ||
|
|
604fa6317c | ||
|
|
4479473b37 | ||
|
|
4dc8fc9696 | ||
|
|
bac59d595d | ||
|
|
ed98db8ae3 | ||
|
|
b66eb66358 | ||
|
|
934a994ac5 | ||
|
|
199a989f7d | ||
|
|
a50b09e5a2 | ||
|
|
b20c69b15a | ||
|
|
25be173487 | ||
|
|
61808d5939 | ||
|
|
de24227b1f | ||
|
|
1f5608ec77 | ||
|
|
2d87e162f1 | ||
|
|
1941fdf8a0 | ||
|
|
33fdc52667 | ||
|
|
e11a5a823a | ||
|
|
b60d101d42 | ||
|
|
5e9263b787 | ||
|
|
949bd783f5 | ||
|
|
7fe3137fd0 | ||
|
|
3c97fbe587 | ||
|
|
030d469d7c | ||
|
|
135ef8ed89 | ||
|
|
002e9c4647 | ||
|
|
a4fbe55012 | ||
|
|
63ac707415 | ||
|
|
b5dc04bb4f | ||
|
|
f3ae20f3aa | ||
|
|
1838b8aefd | ||
|
|
e9c65e1ada | ||
|
|
9ea5d213f7 | ||
|
|
7dcd6d5552 | ||
|
|
0de8f2d633 | ||
|
|
cb2f854dd5 | ||
|
|
401c920e47 | ||
|
|
40235fe473 | ||
|
|
a76e08aee7 | ||
|
|
dfe7282510 | ||
|
|
3aae2e976f | ||
|
|
f79a945fa4 | ||
|
|
8234c51412 | ||
|
|
f464f997d1 | ||
|
|
e0b7cdf9dd | ||
|
|
a819154145 | ||
|
|
616f45c586 | ||
|
|
203aaf51b7 | ||
|
|
392d1fe26d | ||
|
|
f452ea0d26 | ||
|
|
97dbb74486 | ||
|
|
1f80cfcaa6 | ||
|
|
5d0fbabb6a | ||
|
|
b5ca0fa17b | ||
|
|
41e1704d08 | ||
|
|
d4569237f5 | ||
|
|
b6ae51b5e5 | ||
|
|
3015ba605d | ||
|
|
eb589d4039 | ||
|
|
271190f434 | ||
|
|
0836790daf | ||
|
|
b3b665fcb9 | ||
|
|
c050b02b8b | ||
|
|
a791403a6a | ||
|
|
a4c6a18353 | ||
|
|
9bc603a510 | ||
|
|
af25fb926b | ||
|
|
365a0518a5 | ||
|
|
9801f49f4e | ||
|
|
bb4f3482ad | ||
|
|
e148607c7a | ||
|
|
ae370f04c1 | ||
|
|
89f6343abd | ||
|
|
ea55687171 | ||
|
|
da0b4d7911 | ||
|
|
e303283098 | ||
|
|
1119ff06c9 | ||
|
|
adc8ed26c1 | ||
|
|
06554b83dc | ||
|
|
06ea1d1697 | ||
|
|
ddb3bcde43 | ||
|
|
db2f9efb93 | ||
|
|
d32b15d425 | ||
|
|
a67f9b2edb | ||
|
|
c38547d4e7 | ||
|
|
3f350c30da | ||
|
|
d502d9b1bb | ||
|
|
06e1be716e | ||
|
|
cda855f1b7 | ||
|
|
36def799c0 | ||
|
|
2e671250b9 | ||
|
|
c881534554 | ||
|
|
e1f7336aa9 | ||
|
|
aa92e9da02 | ||
|
|
232b48ff62 | ||
|
|
a95b2932db | ||
|
|
aa288a1e11 | ||
|
|
7e6efcb9b9 | ||
|
|
817d0a7e63 | ||
|
|
fa0067ce7b | ||
|
|
9beacf7ef3 | ||
|
|
b4292028c2 | ||
|
|
d7c099bcbb | ||
|
|
36cd15f4f1 | ||
|
|
92ff1a8499 | ||
|
|
4af7a71220 | ||
|
|
611e170b5e | ||
|
|
148f64c261 | ||
|
|
2c3f47d3cb | ||
|
|
8a6e24e5e7 | ||
|
|
1d29f67065 | ||
|
|
2ffb3e73e1 | ||
|
|
bba7aa3177 | ||
|
|
c950a33031 | ||
|
|
c9ab3bdbfc | ||
|
|
e32c2e865c | ||
|
|
9e52b0b7dc | ||
|
|
d731fb2cae | ||
|
|
e057fcaea1 | ||
|
|
fff1363134 | ||
|
|
4bbfe1040e | ||
|
|
bc6e2dc81b | ||
|
|
0005698c10 | ||
|
|
53711966d2 | ||
|
|
d3b991aad4 | ||
|
|
4ef19c321d | ||
|
|
a3e3b9dfe3 | ||
|
|
abbce3e9d1 | ||
|
|
0edbfd89ff | ||
|
|
040d585d57 | ||
|
|
c74ef7b0d3 | ||
|
|
23ef937100 | ||
|
|
5157742009 | ||
|
|
96d96edc9e | ||
|
|
2a10edcc25 | ||
|
|
e4477db413 | ||
|
|
b32d926b56 | ||
|
|
6b3b5a8b6f | ||
|
|
a7df8afd6e | ||
|
|
b8205f4c38 | ||
|
|
2adb1bf917 | ||
|
|
2825dd7e04 | ||
|
|
df04064e81 | ||
|
|
0555fc48ad | ||
|
|
cd425bd26d | ||
|
|
a98444b4e7 | ||
|
|
31d05cefbe | ||
|
|
c552838fdd | ||
|
|
45942e604b | ||
|
|
9b1dd44b9d | ||
|
|
df56faa55a | ||
|
|
14cdeae3eb | ||
|
|
f97d2b0e88 | ||
|
|
a7e2154422 | ||
|
|
d8e84d67da | ||
|
|
c3174a0c72 | ||
|
|
0b05284340 | ||
|
|
ac8ae0da66 | ||
|
|
4517a8a36a | ||
|
|
8ba7eadcb9 | ||
|
|
0700e5b05b | ||
|
|
3485b7bfb0 | ||
|
|
c71c50a729 | ||
|
|
2651ab891d | ||
|
|
1e429550c6 | ||
|
|
44e4ae3740 | ||
|
|
b1552248c3 | ||
|
|
5efd2caeb8 | ||
|
|
bed012cb9c | ||
|
|
319d9024db | ||
|
|
ff7e371404 | ||
|
|
d94ee2ba98 | ||
|
|
a112c29c21 | ||
|
|
c7d6734a26 | ||
|
|
32aa8b0e1f | ||
|
|
6b22ba2707 | ||
|
|
2400c8ed00 | ||
|
|
396022e8ea | ||
|
|
0d4449b9c2 | ||
|
|
32ac92f901 | ||
|
|
f70026b702 | ||
|
|
87acc3362d | ||
|
|
732d231c78 | ||
|
|
a76ce64e1d | ||
|
|
5433a4193b | ||
|
|
56f1e58df0 | ||
|
|
d0c9db41ce | ||
|
|
f162ffd9be | ||
|
|
decc390777 | ||
|
|
ad8fa7563a | ||
|
|
68859d279d | ||
|
|
5792a531ce | ||
|
|
03af10f850 | ||
|
|
2f059874aa | ||
|
|
a53d7763ba | ||
|
|
eb526a6186 | ||
|
|
6095f871ed | ||
|
|
e3b4fe582b | ||
|
|
bbf26a3f38 | ||
|
|
fd291490d0 | ||
|
|
767d68d905 | ||
|
|
32b18e9141 | ||
|
|
5c286f8d96 | ||
|
|
404b53587f | ||
|
|
e5fbe3b74a | ||
|
|
3f262885ca | ||
|
|
c837179f71 | ||
|
|
9a947658e2 | ||
|
|
2458d4b637 | ||
|
|
e4850805fb | ||
|
|
3a15a3bb06 | ||
|
|
75ca1fa930 | ||
|
|
377840ca24 | ||
|
|
48e9589b58 | ||
|
|
11e9cef834 | ||
|
|
7e3aa09d3e | ||
|
|
e3b7e002b4 | ||
|
|
3b7fb7ae75 | ||
|
|
fab004cdfe | ||
|
|
07523c00f0 | ||
|
|
c15ac14f88 | ||
|
|
8f6006c19f | ||
|
|
16bedcf5b1 | ||
|
|
05349d8ffe | ||
|
|
a1e1895651 | ||
|
|
a111599850 | ||
|
|
121a95cee8 | ||
|
|
decd1f3ea2 | ||
|
|
c632718324 | ||
|
|
9509b59696 | ||
|
|
24dc71344e | ||
|
|
82a11e4b98 | ||
|
|
fc8665ed93 | ||
|
|
ca9424e23d | ||
|
|
99856b1bb3 | ||
|
|
fb518c2be5 | ||
|
|
1248a53029 | ||
|
|
6ce43840e5 | ||
|
|
41d9fb1c44 | ||
|
|
fd9be8f08f | ||
|
|
69a665373f | ||
|
|
8c2b110115 | ||
|
|
5e3b2dd0df | ||
|
|
d045213fa3 | ||
|
|
63bba67750 | ||
|
|
52e8fd2c29 | ||
|
|
5479b240e1 | ||
|
|
f209d8e9a5 | ||
|
|
ac40d7727e | ||
|
|
7bd9d3f5da | ||
|
|
68685dcf42 | ||
|
|
6be6db8f5e | ||
|
|
236dd79b85 | ||
|
|
7d905c5e06 | ||
|
|
6fa2542b56 | ||
|
|
7627b8fb45 | ||
|
|
5901427534 | ||
|
|
a30e57c4d8 | ||
|
|
69f2e12ea0 | ||
|
|
93c7f323fc | ||
|
|
cbe2a4c180 | ||
|
|
2e0cc4511c | ||
|
|
bcab165f97 | ||
|
|
2516fba105 | ||
|
|
9ca8760564 | ||
|
|
df94d9c842 | ||
|
|
abceb457c9 | ||
|
|
26a865bb50 | ||
|
|
d0f047d88a | ||
|
|
76d2d06e77 | ||
|
|
6c56006fbf | ||
|
|
bbe45cf8ee | ||
|
|
82da251218 | ||
|
|
196d9f0a10 | ||
|
|
cb752c0343 | ||
|
|
3917a3e323 | ||
|
|
fed1f09434 | ||
|
|
840778b64f | ||
|
|
0908856b4f | ||
|
|
b51354ae1d | ||
|
|
9ef24428fe | ||
|
|
4a75b0381b | ||
|
|
2426117233 | ||
|
|
d40c704c69 | ||
|
|
cb4fdb0f9f | ||
|
|
f0d04bdb07 | ||
|
|
df61ae8c7a | ||
|
|
2ff8ec07bb | ||
|
|
6021b51385 | ||
|
|
40111e0d8e | ||
|
|
43d9440e05 | ||
|
|
3a3e90c3dc | ||
|
|
104d6311ec | ||
|
|
f5256cf80a | ||
|
|
b470885263 | ||
|
|
7ff0ac9bb5 | ||
|
|
0fb59ca544 | ||
|
|
09b6b2dffe | ||
|
|
a8a3b7a5ad | ||
|
|
766a3e387e | ||
|
|
ec9fc8f6ad | ||
|
|
0f272e233b | ||
|
|
f806e797fa | ||
|
|
cff0a15f7e | ||
|
|
d3276829b2 | ||
|
|
a3caf8499c | ||
|
|
d739ca812c | ||
|
|
cb89ca6ef7 | ||
|
|
c3417241f1 | ||
|
|
5d70de6202 | ||
|
|
c09ffc9d41 | ||
|
|
e19a41d015 | ||
|
|
0a0400a297 | ||
|
|
153232c143 | ||
|
|
7e8813f417 | ||
|
|
b72f86a78d | ||
|
|
fed530f5f2 | ||
|
|
ba0a94f3ad | ||
|
|
d9b458d7fd | ||
|
|
ed9b806143 | ||
|
|
5bb68a38c2 | ||
|
|
cfeaf2cdce | ||
|
|
887b23ce1f | ||
|
|
f227392f9b | ||
|
|
2f7658e245 | ||
|
|
4f0c641eb0 | ||
|
|
1538f2e174 | ||
|
|
580068bf63 | ||
|
|
91604afccb | ||
|
|
c363c88f23 | ||
|
|
e9daee4470 | ||
|
|
118f0360d0 | ||
|
|
7c9dcb3083 | ||
|
|
7c3906fa40 | ||
|
|
7b24cbf39b | ||
|
|
e7b11d8bc9 | ||
|
|
08854cd88f | ||
|
|
cb46ac5421 | ||
|
|
c9fd00e2ed | ||
|
|
7c23fe3646 | ||
|
|
56aacb0149 | ||
|
|
12411ee886 | ||
|
|
85cef2945d | ||
|
|
a1dfeca6e0 | ||
|
|
3be6d14637 | ||
|
|
74b3ef9e88 | ||
|
|
019dfe9f8a | ||
|
|
e92dfd8284 | ||
|
|
fa38667125 | ||
|
|
ce39ae723c | ||
|
|
99acbd4d92 | ||
|
|
b0e9790382 | ||
|
|
e00cdde3af | ||
|
|
c3a634b216 | ||
|
|
4f26a521a0 | ||
|
|
ca6b48843c | ||
|
|
0eb00312f4 | ||
|
|
e7709dae15 | ||
|
|
03796c963b | ||
|
|
b50855a4a9 | ||
|
|
24a90b3c57 | ||
|
|
cf80e80025 | ||
|
|
48f10bcb73 | ||
|
|
7bc2323401 | ||
|
|
a71ac502d6 | ||
|
|
f2dd785e7b | ||
|
|
0b99e571c4 | ||
|
|
cfc6085718 | ||
|
|
384b2d4bea | ||
|
|
1058dbfb5a | ||
|
|
bda7ce7390 | ||
|
|
7b631b0510 | ||
|
|
1d7768e37c | ||
|
|
89d497c73f | ||
|
|
886c87f231 | ||
|
|
d567a4f98b | ||
|
|
5eb0e36faf | ||
|
|
51a2eabc91 | ||
|
|
007bdad70a | ||
|
|
1f1a919c77 | ||
|
|
3be3a716d4 | ||
|
|
ae9afdd8d9 | ||
|
|
a5307054b3 | ||
|
|
d16c3f4356 | ||
|
|
853361ace7 | ||
|
|
e41e1eb2f1 | ||
|
|
e36c233b49 | ||
|
|
d1b8f8d63e | ||
|
|
29cfb58a56 | ||
|
|
bf5131cadd | ||
|
|
ccc39b87db | ||
|
|
604be38b7c | ||
|
|
160bd9563b | ||
|
|
488fdf2bd5 | ||
|
|
a0e1e6152b | ||
|
|
58897f1856 | ||
|
|
80678af691 | ||
|
|
ba271e1fc6 | ||
|
|
c7ac90ba15 | ||
|
|
0dc335ea5f | ||
|
|
acac314d27 | ||
|
|
916c1dc9fc | ||
|
|
c159f7041f | ||
|
|
a3d586a75d | ||
|
|
6b0b29d1da | ||
|
|
8afda2fe28 | ||
|
|
beb1a2a8d1 | ||
|
|
436e0c2095 | ||
|
|
e1bc2a321a | ||
|
|
720c8f108b | ||
|
|
4db5c7cf68 | ||
|
|
8f561d8a27 | ||
|
|
0c483cffe3 | ||
|
|
def5ebb587 | ||
|
|
6e9e66b147 | ||
|
|
f332d517f3 | ||
|
|
04eab70e27 | ||
|
|
cfbcdc7fa1 | ||
|
|
c95dd75e2a | ||
|
|
4408f3ab3b |
21
.babelrc
Normal file
21
.babelrc
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"presets": [
|
||||
"@babel/preset-env",
|
||||
"@babel/preset-react"
|
||||
],
|
||||
"plugins": [
|
||||
"static-fs",
|
||||
"react-hot-loader/babel",
|
||||
"@babel/plugin-proposal-class-properties",
|
||||
"@babel/transform-runtime"
|
||||
],
|
||||
"env": {
|
||||
"test": {
|
||||
"plugins": [
|
||||
["istanbul", {
|
||||
"exclude": ["node_modules/**", "test/**"]
|
||||
}]
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
4
.codesandbox/ci.json
Normal file
4
.codesandbox/ci.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"packages": [],
|
||||
"sandboxes": ["/"]
|
||||
}
|
||||
45
.dockerignore
Normal file
45
.dockerignore
Normal file
@@ -0,0 +1,45 @@
|
||||
.git
|
||||
.gitignore
|
||||
Dockerfile
|
||||
|
||||
#
|
||||
#
|
||||
# COPIED FROM .gitignore , please keep it in sync
|
||||
#
|
||||
#
|
||||
|
||||
# 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
|
||||
1
.github/FUNDING.yml
vendored
Normal file
1
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1 @@
|
||||
custom: "https://maputnik.github.io/donate"
|
||||
27
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
27
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve Maputnik
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
<!-- Thanks for your feedback! Please complete the following information: -->
|
||||
|
||||
**Maputnik version**:<!-- e.g v1.7.0, master -->
|
||||
**Browser**:
|
||||
**OS**:<!-- (Windows, macOS, Linux) -->
|
||||
|
||||
**Description of the bug**:
|
||||
|
||||
**Steps to reproduce the behavior**:
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
|
||||
**Style file or style URL**:
|
||||
<!-- If applicable, attach a style file (zip) or provide a style URL. -->
|
||||
|
||||
**Screenshots**:
|
||||
<!-- If applicable, add screenshots to help explain your problem. -->
|
||||
11
.github/ISSUE_TEMPLATE/other-issue.md
vendored
Normal file
11
.github/ISSUE_TEMPLATE/other-issue.md
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
---
|
||||
name: Other issue
|
||||
about: Feature request or other issue which is no bug report
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
<!-- Thanks for reaching out! If you are having general Maputnik mapping questions, please asking them at https://gis.stackexchange.com/ using the 'maputnik' tag https://gis.stackexchange.com/questions/tagged/maputnik and read https://gis.stackexchange.com/help/how-to-ask before you do so (please keep in mind that you're asking there in a general GIS forum, not a dedicated support channel) -->
|
||||
|
||||
193
.github/workflows/ci.yml
vendored
Normal file
193
.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,193 @@
|
||||
name: ci
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
push:
|
||||
branches: [ master ]
|
||||
|
||||
jobs:
|
||||
|
||||
# post a comment linking to codesandbox with the current branch
|
||||
# meta-demo-comment:
|
||||
# name: meta/demo-comment
|
||||
# runs-on: ubuntu-latest
|
||||
|
||||
# if: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
# steps:
|
||||
# - uses: unsplash/comment-on-pr@v1.2.0
|
||||
# env:
|
||||
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
# with:
|
||||
# msg: "Demo: <https://codesandbox.io/embed/github/${{ github.repository }}/tree/${{ github.head_ref }}?view=preview>"
|
||||
|
||||
build-docker:
|
||||
name: build/docker
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request' }}
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- run: docker build -t docker.pkg.github.com/maputnik/editor/editor:master .
|
||||
|
||||
# build the editor
|
||||
build-node:
|
||||
name: "build/node@${{ matrix.node-version }} (${{ matrix.os }})"
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request' }}
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, windows-latest, macos-latest]
|
||||
node-version: [16.x]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- uses: actions/cache@v1
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-node-
|
||||
- run: npm ci
|
||||
- run: npm run build
|
||||
|
||||
|
||||
build-artifacts:
|
||||
name: "build/artifacts (${{ matrix.os }})"
|
||||
runs-on: ${{ matrix.os }}
|
||||
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request' }}
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest]
|
||||
node-version: [16.x]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- uses: actions/cache@v1
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-node-
|
||||
- run: npm ci
|
||||
- run: npm run build
|
||||
- run: npm run build-storybook
|
||||
- name: artifacts/editor
|
||||
uses: actions/upload-artifact@v1
|
||||
with:
|
||||
name: editor
|
||||
path: build/build
|
||||
- run: npm run profiling-build
|
||||
- name: artifacts/editor-profiling
|
||||
uses: actions/upload-artifact@v1
|
||||
with:
|
||||
name: editor-profiling
|
||||
path: build/profiling
|
||||
- name: artifacts/storybook
|
||||
uses: actions/upload-artifact@v1
|
||||
with:
|
||||
name: storybook
|
||||
path: build/storybook
|
||||
|
||||
# Build and upload desktop CLI artifacts
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: ^1.19.x
|
||||
id: go
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
repository: maputnik/desktop
|
||||
ref: master
|
||||
path: ./src/github.com/maputnik/desktop/
|
||||
|
||||
- name: Make
|
||||
run: cd src/github.com/maputnik/desktop/ && make
|
||||
|
||||
- name: Artifacts/linux
|
||||
uses: actions/upload-artifact@v1
|
||||
with:
|
||||
name: maputnik-linux
|
||||
path: ./src/github.com/maputnik/desktop/bin/linux/
|
||||
|
||||
- name: Artifacts/darwin
|
||||
uses: actions/upload-artifact@v1
|
||||
with:
|
||||
name: maputnik-darwin
|
||||
path: ./src/github.com/maputnik/desktop/bin/darwin/
|
||||
|
||||
- name: Artifacts/windows
|
||||
uses: actions/upload-artifact@v1
|
||||
with:
|
||||
name: maputnik-windows
|
||||
path: ./src/github.com/maputnik/desktop/bin/windows/
|
||||
|
||||
# build and test the editor
|
||||
test_selenium_standalone:
|
||||
name: "test/standalone-${{ matrix.browser }} (${{ matrix.os }})"
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request' }}
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest]
|
||||
node-version: [16]
|
||||
browser: [chrome, firefox]
|
||||
|
||||
container:
|
||||
image: node:${{ matrix.node-version }}
|
||||
options: --network-alias testhost
|
||||
|
||||
services:
|
||||
selenium:
|
||||
# geckodriver-0.31 seems to have problems as of 2022 May 1
|
||||
image: selenium/standalone-${{ matrix.browser == 'firefox' && 'firefox:99.0-geckodriver-0.30-20220427' || matrix.browser }}
|
||||
ports:
|
||||
- 4444:4444
|
||||
options: --shm-size=2gb
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/cache@v1
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-node-
|
||||
- run: npm ci
|
||||
- run: BROWSER=${{ matrix.browser }} TEST_NETWORK=testhost DOCKER_HOST=selenium npm run test
|
||||
- if: ${{ matrix.browser == 'chrome' }}
|
||||
run: ./node_modules/.bin/istanbul report --include build/coverage/coverage.json --dir build/coverage html lcov
|
||||
- if: ${{ matrix.browser == 'chrome' }}
|
||||
name: artifacts/coverage
|
||||
uses: actions/upload-artifact@v1
|
||||
with:
|
||||
name: coverage
|
||||
path: build/coverage
|
||||
- name: artifacts/screenshots
|
||||
uses: actions/upload-artifact@v1
|
||||
with:
|
||||
name: screenshots-${{ matrix.browser }}
|
||||
path: build/screenshots
|
||||
28
.github/workflows/deploy.yml
vendored
Normal file
28
.github/workflows/deploy.yml
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
name: deploy
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
# publish docker to github registry
|
||||
deploy-docker:
|
||||
name: deploy/docker
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
if: ${{ github.event_name == 'push' }}
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login docker.pkg.github.com -u orangemug --password-stdin
|
||||
- run: docker build -t docker.pkg.github.com/maputnik/editor/editor:master .
|
||||
- run: docker push docker.pkg.github.com/maputnik/editor/editor:master
|
||||
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -1,6 +1,8 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
@@ -28,3 +30,6 @@ node_modules
|
||||
|
||||
# Ignore build files
|
||||
public
|
||||
/errorShots
|
||||
/old
|
||||
/build
|
||||
|
||||
24
.storybook/main.js
Normal file
24
.storybook/main.js
Normal file
@@ -0,0 +1,24 @@
|
||||
const rules = require('../config/webpack.rules');
|
||||
|
||||
module.exports = {
|
||||
stories: ['../stories/**/*.stories.js'],
|
||||
addons: [
|
||||
'@storybook/addon-actions',
|
||||
'@storybook/addon-links',
|
||||
'@storybook/addon-a11y/register',
|
||||
'@storybook/addon-storysource',
|
||||
],
|
||||
webpackFinal: async config => {
|
||||
// do mutation to the config
|
||||
console.log("config.module", config.module);
|
||||
|
||||
return {
|
||||
...config,
|
||||
module: {
|
||||
rules: [
|
||||
...rules,
|
||||
]
|
||||
}
|
||||
};
|
||||
},
|
||||
};
|
||||
7
.storybook/manager.js
Normal file
7
.storybook/manager.js
Normal file
@@ -0,0 +1,7 @@
|
||||
import { addons } from '@storybook/addons';
|
||||
import { themes } from '@storybook/theming';
|
||||
import theme from './maputnik.theme';
|
||||
|
||||
addons.setConfig({
|
||||
theme: theme,
|
||||
});
|
||||
8
.storybook/maputnik.theme.js
Normal file
8
.storybook/maputnik.theme.js
Normal file
@@ -0,0 +1,8 @@
|
||||
import { create } from '@storybook/theming/create';
|
||||
|
||||
export default create({
|
||||
base: 'light',
|
||||
|
||||
brandTitle: 'Maputnik',
|
||||
brandUrl: 'https://github.com/maputnik/editor',
|
||||
});
|
||||
15
.topissuesrc
Normal file
15
.topissuesrc
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"labels": {
|
||||
"bug": 5,
|
||||
"maintenance": 3,
|
||||
"mentioned in the 1st survey": 2
|
||||
},
|
||||
"reactions": {
|
||||
"+1": 2,
|
||||
"-1": -1,
|
||||
"laugh": 1,
|
||||
"hooray": 2,
|
||||
"confused": 1,
|
||||
"heart": 2
|
||||
}
|
||||
}
|
||||
21
.travis.yml
21
.travis.yml
@@ -1,21 +0,0 @@
|
||||
language: node_js
|
||||
addons:
|
||||
firefox: latest
|
||||
os:
|
||||
- linux
|
||||
- osx
|
||||
node_js:
|
||||
- "4.6"
|
||||
- "5.11"
|
||||
- "6.1"
|
||||
before_install:
|
||||
- export CHROME_BIN=chromium-browser
|
||||
- export DISPLAY=:99.0
|
||||
- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sh -e /etc/init.d/xvfb start; fi
|
||||
install:
|
||||
- npm install
|
||||
script:
|
||||
- mkdir public
|
||||
- npm run build
|
||||
- npm run lint
|
||||
- npm run test
|
||||
29
Dockerfile
29
Dockerfile
@@ -1,15 +1,22 @@
|
||||
FROM nodesource/xenial:6.1.0
|
||||
FROM node:10 as builder
|
||||
WORKDIR /maputnik
|
||||
|
||||
EXPOSE 8888
|
||||
# Only copy package.json to prevent npm install from running on every build
|
||||
COPY package.json package-lock.json ./
|
||||
RUN npm install
|
||||
|
||||
ENV HOME /maputnik
|
||||
RUN mkdir ${HOME}
|
||||
|
||||
COPY . ${HOME}/
|
||||
|
||||
WORKDIR ${HOME}
|
||||
|
||||
RUN npm install -d --dev
|
||||
# Build maputnik
|
||||
# TODO: we should also do a npm run test here (needs more dependencies)
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
|
||||
CMD npm run start -- --host 0.0.0.0
|
||||
#---------------------------------------------------------------------------
|
||||
|
||||
# Create a clean python-based image with just the build results
|
||||
FROM python:3-slim
|
||||
WORKDIR /maputnik
|
||||
|
||||
COPY --from=builder /maputnik/build/build .
|
||||
|
||||
EXPOSE 8888
|
||||
CMD python -m http.server 8888
|
||||
|
||||
172
README.md
172
README.md
@@ -1,33 +1,44 @@
|
||||
# Maputnik [](https://travis-ci.org/maputnik/editor) [](https://ci.appveyor.com/project/lukasmartinelli/editor) [](https://tldrlegal.com/license/mit-license)
|
||||
<img width="200" alt="Maputnik logo" src="https://cdn.jsdelivr.net/gh/maputnik/design/logos/logo-color.png" />
|
||||
|
||||
<img width="200" align="right" alt="Maputnik" src="media/maputnik.png" />
|
||||
# Maputnik
|
||||
[][github-action-ci]
|
||||
[][license]
|
||||
|
||||
[github-action-ci]: https://github.com/maputnik/editor/actions?query=workflow%3Aci
|
||||
[license]: https://tldrlegal.com/license/mit-license
|
||||
|
||||
A free and open visual editor for the [Mapbox GL styles](https://www.mapbox.com/mapbox-gl-style-spec/)
|
||||
targeted at developers and map designers. Creating your own custom map is easy with **Maputnik**.
|
||||
targeted at developers and map designers.
|
||||
|
||||
*Maputnik is an early prototype and is under development.
|
||||
[Thanks to the supporters of the Kickstarter campaign who made this project possible](https://www.kickstarter.com/projects/174808720/maputnik-visual-map-editor-for-mapbox-gl)*.
|
||||
|
||||
## Features
|
||||
## Usage
|
||||
|
||||
- [x] Completely free and open source
|
||||
- [x] Visual interface for designing maps
|
||||
- [x] Immediate feedback (thanks to [style diffs](https://github.com/mapbox/mapbox-gl-style-spec/blob/mb-pages/lib/diff.js))
|
||||
- [x] Edit layers
|
||||
- [x] Easy to deploy as single HTML file
|
||||
- [ ] Support for Open Layers 3
|
||||
- :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
|
||||
- In a Docker, run this command and browse to http://localhost:8888, Ctrl+C to stop the server.
|
||||
|
||||

|
||||
```bash
|
||||
docker run -it --rm -p 8888:8888 maputnik/editor
|
||||
```
|
||||
|
||||
## Donations
|
||||
Mapbox has built one of the best and most amazing OSS ecosystems. A key component to ensure its longevity and independence is an OSS map designer.
|
||||
If you or your organisation has seen value from Maputnik, please consider donating at <https://maputnik.github.io/donate>
|
||||
|
||||
## Documentation
|
||||
|
||||
The documentation can be found in the [Wiki](https://github.com/maputnik/editor/wiki). You are welcome to collaborate!
|
||||
|
||||
- :link: **Study the [Maputnik Wiki](https://github.com/maputnik/editor/wiki)**
|
||||
- :video_camera: Design a map from Scratch https://youtu.be/XoDh0gEnBQo
|
||||
|
||||
[](https://youtu.be/XoDh0gEnBQo)
|
||||
|
||||
## Develop
|
||||
|
||||
Maputnik is written in ES6 and is using [React](https://github.com/facebook/react), [Immutable.js](https://facebook.github.io/immutable-js/) and [Mapbox GL JS](https://www.mapbox.com/mapbox-gl-js/api/).
|
||||
Maputnik is written in ES6 and is using [React](https://github.com/facebook/react) and [Mapbox GL JS](https://www.mapbox.com/mapbox-gl-js/api/).
|
||||
|
||||
We ensure building and developing Maputnik works with
|
||||
|
||||
- Linux, OSX and Windows
|
||||
- Node >4
|
||||
We ensure building and developing Maputnik works with the [current active LTS Node.js version and above](https://github.com/nodejs/Release#release-schedule).
|
||||
|
||||
Install the deps, start the dev server and open the web browser on `http://localhost:8888/`.
|
||||
|
||||
@@ -35,10 +46,21 @@ Install the deps, start the dev server and open the web browser on `http://local
|
||||
# install dependencies
|
||||
npm install
|
||||
# start dev server
|
||||
npm start
|
||||
npm run start
|
||||
```
|
||||
|
||||
Build a production package for distribution.
|
||||
If you want Maputnik to be accessible externally use the [`--host` option](https://webpack.js.org/configuration/dev-server/#devserverhost):
|
||||
|
||||
```bash
|
||||
# start externally accessible dev server
|
||||
npm run start -- --host 0.0.0.0
|
||||
```
|
||||
|
||||
The build process will watch for changes to the filesystem, rebuild and autoreload the editor. However note this from the [webpack-dev-server docs](https://webpack.js.org/configuration/dev-server/):
|
||||
|
||||
> webpack uses the file system to get notified of file changes. In some cases this does not work. For example, when using Network File System (NFS). Vagrant also has a lot of problems with this. ([snippet source](https://webpack.js.org/configuration/dev-server/#devserverwatchoptions-))
|
||||
|
||||
To enable polling add `export WEBPACK_DEV_SERVER_POLLING=1` to your environment.
|
||||
|
||||
```
|
||||
npm run build
|
||||
@@ -47,108 +69,92 @@ npm run build
|
||||
Lint the JavaScript code.
|
||||
|
||||
```
|
||||
# install lint dependencies
|
||||
npm install --save-dev eslint eslint-plugin-react
|
||||
# run linter
|
||||
npm run lint
|
||||
npm run lint-styles
|
||||
```
|
||||
|
||||
## Docker
|
||||
|
||||
Start a container using the official Docker image.
|
||||
```
|
||||
docker run --name maputnik -p 8888:8888 -d maputnik/editor
|
||||
```
|
||||
## Tests
|
||||
For testing we use [webdriverio](https://webdriver.io) and [selenium-standalone](https://github.com/webdriverio/selenium-standalone).
|
||||
|
||||
Stop the container
|
||||
[selenium-standalone](https://github.com/webdriverio/selenium-standalone) starts a server that will launch browsers on your local machine. You need to have Java installed on your machine as well as *chrome* or *firefox*.
|
||||
|
||||
Now open a terminal and run the following using *chrome*:
|
||||
|
||||
```
|
||||
docker stop maputnik
|
||||
npm run test
|
||||
```
|
||||
or *firefox*:
|
||||
```
|
||||
BROWSER=firefox npm run test
|
||||
```
|
||||
|
||||
After some time you should see a browser launch which will be automated by the test runner.
|
||||
|
||||
|
||||
## Related Projects
|
||||
|
||||
- [maputnik-dev-server](https://github.com/nycplanning/labs-maputnik-dev-server) - An express.js server that allows for quickly loading the style from any mapboxGL map into mapuntnik.
|
||||
|
||||
## Sponsors
|
||||
|
||||
This project would not be possible without commercial and individual sponsors.
|
||||
Thanks to the supporters of the **[Kickstarter campaign](https://www.kickstarter.com/projects/174808720/maputnik-visual-map-editor-for-mapbox-gl)**. This project would not be possible without these commercial and individual sponsors.
|
||||
|
||||
### Gold
|
||||
|
||||
[](https://getwemap.com/)
|
||||
- [Wemap](https://getwemap.com/)
|
||||
- [Orbicon Informatik](https://www.orbiconinformatik.dk/)
|
||||
- [Terranodo](http://terranodo.io/)
|
||||
|
||||
[](http://terranodo.io/)
|
||||
<a href="https://getwemap.com/">
|
||||
<img width="33%" alt="Wemap" style="display:inline" src="https://cdn.jsdelivr.net/gh/maputnik/editor@1.5.0/media/sponsors/wemap.jpg" />
|
||||
</a>
|
||||
<a href="http://terranodo.io/">
|
||||
<img width="33%" alt="Terranodo" style="display:inline" src="https://cdn.jsdelivr.net/gh/maputnik/editor@1.5.0/media/sponsors/terranodo.png" />
|
||||
</a>
|
||||
<a href="https://www.orbiconinformatik.dk/">
|
||||
<img width="32%" alt="Terranodo" style="display:inline" src="https://cdn.jsdelivr.net/gh/maputnik/editor@1.5.0/media/sponsors/orbicon_informatik.png" />
|
||||
</a>
|
||||
|
||||
<br/>
|
||||
|
||||
### Silver
|
||||
|
||||
- [Klokan Technologies](https://www.klokantech.com/)
|
||||
- [Geofabrik](http://www.geofabrik.de/)
|
||||
- [Dreipol](https://www.dreipol.ch/)
|
||||
|
||||
<a href="https://www.klokantech.com/">
|
||||
<img alt="Klokan Technologies" style="display:inline" src="media/sponsors/klokantech.png" />
|
||||
<img width="18%" alt="Klokan Technologies" style="display:inline-block" src="https://cdn.jsdelivr.net/gh/maputnik/editor@1.5.0/media/sponsors/klokantech.png" />
|
||||
</a>
|
||||
<a href="http://www.geofabrik.de/">
|
||||
<img width="18%" alt="Geofabrik" style="display:inline-block" src="https://cdn.jsdelivr.net/gh/maputnik/editor@1.5.0/media/sponsors/geofabrik.png" />
|
||||
</a>
|
||||
<a href="https://www.dreipol.ch/">
|
||||
<img alt="Dreipol" style="display:inline" src="media/sponsors/dreipol.png" />
|
||||
<img width="18%" alt="Dreipol" style="display:inline-block" src="https://cdn.jsdelivr.net/gh/maputnik/editor@1.5.0/media/sponsors/dreipol.png" />
|
||||
</a>
|
||||
|
||||
<br/>
|
||||
|
||||
### Individuals
|
||||
|
||||
**Influential Stakeholder**
|
||||
|
||||
- Alan McConchie
|
||||
- Odi
|
||||
- Mats Norén
|
||||
- Uli [geOps](http://geops.ch/)
|
||||
- Helge Fahrnberger
|
||||
Kirusanth Poopalasingam
|
||||
Alan McConchie, Odi, Mats Norén, Uli [geOps](http://geops.ch/), Helge Fahrnberger ([Toursprung](http://www.toursprung.com/)), Kirusanth Poopalasingam
|
||||
|
||||
**Stakeholder**
|
||||
|
||||
- Brian Flood
|
||||
- Vasile Coțovanu
|
||||
- Andreas Kalkbrenner
|
||||
- Christian Mäder
|
||||
- Gregor Wassmann
|
||||
- Lee Armstrong
|
||||
- Rafel
|
||||
- Jon Burgess
|
||||
- Lukas Lehmann
|
||||
- Joachim Ungar
|
||||
- Alois Ackermann
|
||||
- Zsolt Ero
|
||||
- Jordan Meek
|
||||
Brian Flood, Vasile Coțovanu, Andreas Kalkbrenner, Christian Mäder, Gregor Wassmann, Lee Armstrong, Rafel, Jon Burgess, Lukas Lehmann, Joachim Ungar, Alois Ackermann, Zsolt Ero, Jordan Meek
|
||||
|
||||
**Supporter**
|
||||
|
||||
- Sina Martinelli
|
||||
- Nicholas Doiron
|
||||
- Neil Cawse
|
||||
- Urs42
|
||||
- Benedikt Groß
|
||||
- Manuel Roth
|
||||
- Janko Mihelić
|
||||
- Moritz Stefaner
|
||||
- Sebastian Ahoi
|
||||
- Juerg Uhlmann
|
||||
- Tom Wider
|
||||
- Nadia Panchaud
|
||||
- Oliver Snowden
|
||||
- Stephan Heuel
|
||||
- Tobin Bradley
|
||||
- Adrian Herzog
|
||||
- Antti Lehto
|
||||
- Pascal Mages
|
||||
- Marc Gehling
|
||||
- Imre Samu
|
||||
- Lauri K.
|
||||
- Visahavel Parthasarathy
|
||||
- Christophe Waterlot-Buisine
|
||||
- Max Galka
|
||||
- ubahnverleih
|
||||
- Wouter van Dam
|
||||
- Jakob Lobensteiner
|
||||
- Samuel Kurath
|
||||
- Brian Bancroft
|
||||
Sina Martinelli, Nicholas Doiron, Neil Cawse, Urs42, Benedikt Groß, Manuel Roth, Janko Mihelić, Moritz Stefaner, Sebastian Ahoi, Juerg Uhlmann, Tom Wider, Nadia Panchaud, Oliver Snowden, Stephan Heuel, Tobin Bradley, Adrian Herzog, Antti Lehto, Pascal Mages, Marc Gehling, Imre Samu, Lauri K., Visahavel Parthasarathy, Christophe Waterlot-Buisine, Max Galka, ubahnverleih, Wouter van Dam, Jakob Lobensteiner, Samuel Kurath, Brian Bancroft
|
||||
|
||||
## License
|
||||
|
||||
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
|
||||
**Disclaimer** This project is not affiliated with Mapbox or Mapbox Studio. It is an independent style editor for the
|
||||
open source technology in the Mapbox GL ecosystem.
|
||||
As contributor please take extra care of not violating any Mapbox trademarks. Do not get inspired by Mapbox Studio and make your own decisions for a good style editor.
|
||||
|
||||
14
appveyor.yml
14
appveyor.yml
@@ -1,14 +0,0 @@
|
||||
environment:
|
||||
matrix:
|
||||
- nodejs_version: "4.6"
|
||||
- nodejs_version: "5.11"
|
||||
- nodejs_version: "6.1"
|
||||
install:
|
||||
- ps: Install-Product node $env:nodejs_version
|
||||
- md public
|
||||
- npm install
|
||||
build_script:
|
||||
- npm run build
|
||||
test_script:
|
||||
- npm run lint
|
||||
- npm test
|
||||
47
config/wdio.conf.js
Normal file
47
config/wdio.conf.js
Normal file
@@ -0,0 +1,47 @@
|
||||
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 server;
|
||||
var SCREENSHOT_PATH = artifacts.pathSync("screenshots");
|
||||
|
||||
exports.config = {
|
||||
runner: 'local',
|
||||
path: '/wd/hub',
|
||||
specs: [
|
||||
'./test/functional/index.js'
|
||||
],
|
||||
maxInstances: 10,
|
||||
capabilities: [
|
||||
{
|
||||
maxInstances: 5,
|
||||
browserName: (process.env.BROWSER || 'chrome'),
|
||||
}
|
||||
],
|
||||
// geckodriver-0.31 seems to have problems as of 2022 May 1
|
||||
services: process.env.DOCKER_HOST ? [] : [ ['selenium-standalone', { drivers: { firefox: '0.30.0', chrome: 'latest' } } ] ],
|
||||
logLevel: 'info',
|
||||
bail: 0,
|
||||
screenshotPath: SCREENSHOT_PATH,
|
||||
hostname: process.env.DOCKER_HOST || "0.0.0.0",
|
||||
framework: 'mocha',
|
||||
reporters: ['spec'],
|
||||
mochaOpts: {
|
||||
ui: 'bdd',
|
||||
// Because we don't know how long the initial build will take...
|
||||
timeout: 4*60*1000,
|
||||
},
|
||||
onPrepare: async function (config, capabilities) {
|
||||
webpackConfig.devServer.host = testConfig.testNetwork;
|
||||
webpackConfig.devServer.port = testConfig.port;
|
||||
const compiler = webpack(webpackConfig);
|
||||
server = new WebpackDevServer(webpackConfig.devServer, compiler);
|
||||
await server.start();
|
||||
},
|
||||
onComplete: async function (exitCode, config, capabilities) {
|
||||
await server.stop();
|
||||
}
|
||||
}
|
||||
74
config/webpack.config.js
Normal file
74
config/webpack.config.js
Normal file
@@ -0,0 +1,74 @@
|
||||
"use strict";
|
||||
var path = require('path');
|
||||
var rules = require('./webpack.rules');
|
||||
var HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
var HtmlWebpackInlineSVGPlugin = require('html-webpack-inline-svg-plugin');
|
||||
var CopyWebpackPlugin = require('copy-webpack-plugin');
|
||||
|
||||
const HOST = process.env.HOST || "127.0.0.1";
|
||||
const PORT = process.env.PORT || "8888";
|
||||
|
||||
module.exports = {
|
||||
target: 'web',
|
||||
mode: 'development',
|
||||
entry: [
|
||||
`webpack-dev-server/client?http://${HOST}:${PORT}`,
|
||||
`webpack/hot/only-dev-server`,
|
||||
`./src/index.jsx` // Your appʼs entry point
|
||||
],
|
||||
devtool: process.env.WEBPACK_DEVTOOL || 'cheap-module-source-map',
|
||||
output: {
|
||||
path: path.join(__dirname, '..', 'public'),
|
||||
filename: 'bundle.js'
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.js', '.jsx']
|
||||
},
|
||||
module: {
|
||||
noParse: [
|
||||
/mapbox-gl\/dist\/mapbox-gl.js/
|
||||
],
|
||||
rules: rules
|
||||
},
|
||||
node: {
|
||||
fs: "empty",
|
||||
net: 'empty',
|
||||
tls: 'empty'
|
||||
},
|
||||
devServer: {
|
||||
// enable HMR
|
||||
hot: true,
|
||||
// serve index.html in place of 404 responses to allow HTML5 history
|
||||
historyApiFallback: true,
|
||||
port: PORT,
|
||||
host: HOST,
|
||||
watchFiles: {
|
||||
options: {
|
||||
// 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
|
||||
usePolling: (!!process.env.WEBPACK_DEV_SERVER_POLLING ? true : false),
|
||||
watch: false
|
||||
}
|
||||
}
|
||||
},
|
||||
optimization: {
|
||||
noEmitOnErrors: true,
|
||||
},
|
||||
plugins: [
|
||||
new HtmlWebpackPlugin({
|
||||
title: 'Maputnik',
|
||||
template: './src/template.html'
|
||||
}),
|
||||
new HtmlWebpackInlineSVGPlugin({
|
||||
runPreEmit: true,
|
||||
}),
|
||||
new CopyWebpackPlugin({
|
||||
patterns: [
|
||||
{
|
||||
from: './src/manifest.json',
|
||||
to: 'manifest.json'
|
||||
}
|
||||
]
|
||||
})
|
||||
]
|
||||
};
|
||||
68
config/webpack.production.config.js
Normal file
68
config/webpack.production.config.js
Normal file
@@ -0,0 +1,68 @@
|
||||
var webpack = require('webpack');
|
||||
var path = require('path');
|
||||
var rules = require('./webpack.rules');
|
||||
var HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
var HtmlWebpackInlineSVGPlugin = require('html-webpack-inline-svg-plugin');
|
||||
var WebpackCleanupPlugin = require('webpack-cleanup-plugin');
|
||||
var BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
|
||||
var CopyWebpackPlugin = require('copy-webpack-plugin');
|
||||
var artifacts = require("../test/artifacts");
|
||||
|
||||
var OUTPATH = artifacts.pathSync("/build");
|
||||
|
||||
module.exports = {
|
||||
entry: {
|
||||
app: './src/index.jsx',
|
||||
},
|
||||
output: {
|
||||
path: OUTPATH,
|
||||
filename: '[name].[contenthash].js',
|
||||
chunkFilename: '[contenthash].js'
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.js', '.jsx']
|
||||
},
|
||||
module: {
|
||||
noParse: [
|
||||
/mapbox-gl\/dist\/mapbox-gl.js/
|
||||
],
|
||||
rules: rules
|
||||
},
|
||||
node: {
|
||||
fs: "empty",
|
||||
net: 'empty',
|
||||
tls: 'empty'
|
||||
},
|
||||
plugins: [
|
||||
new webpack.NoEmitOnErrorsPlugin(),
|
||||
new WebpackCleanupPlugin(),
|
||||
new webpack.DefinePlugin({
|
||||
'process.env': {
|
||||
NODE_ENV: '"production"'
|
||||
}
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
template: './src/template.html',
|
||||
title: 'Maputnik'
|
||||
}),
|
||||
new HtmlWebpackInlineSVGPlugin({
|
||||
runPreEmit: true,
|
||||
}),
|
||||
new CopyWebpackPlugin({
|
||||
patterns: [
|
||||
{
|
||||
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',
|
||||
})
|
||||
]
|
||||
};
|
||||
20
config/webpack.profiling.config.js
Normal file
20
config/webpack.profiling.config.js
Normal file
@@ -0,0 +1,20 @@
|
||||
const webpackProdConfig = require('./webpack.production.config');
|
||||
const artifacts = require("../test/artifacts");
|
||||
|
||||
const OUTPATH = artifacts.pathSync("/profiling");
|
||||
|
||||
module.exports = {
|
||||
...webpackProdConfig,
|
||||
output: {
|
||||
...webpackProdConfig.output,
|
||||
path: OUTPATH,
|
||||
},
|
||||
resolve: {
|
||||
...webpackProdConfig.resolve,
|
||||
alias: {
|
||||
...webpackProdConfig.resolve.alias,
|
||||
'react-dom$': 'react-dom/profiling',
|
||||
'scheduler/tracing': 'scheduler/tracing-profiling',
|
||||
}
|
||||
}
|
||||
};
|
||||
44
config/webpack.rules.js
Normal file
44
config/webpack.rules.js
Normal file
@@ -0,0 +1,44 @@
|
||||
const path = require("path");
|
||||
|
||||
module.exports = [
|
||||
{
|
||||
test: /\.jsx?$/,
|
||||
exclude: [
|
||||
path.resolve(__dirname, '../node_modules')
|
||||
],
|
||||
use: 'babel-loader'
|
||||
},
|
||||
{
|
||||
test: /\.(eot|ttf|woff|woff2)$/,
|
||||
use: 'file-loader?name=fonts/[name].[ext]'
|
||||
},
|
||||
{
|
||||
test: /\.ico$/,
|
||||
use: 'file-loader?name=[name].[ext]'
|
||||
},
|
||||
{
|
||||
test: /\.(gif|jpg|png)$/,
|
||||
use: 'file-loader?name=img/[name].[ext]'
|
||||
},
|
||||
{
|
||||
test: /\.svg$/,
|
||||
use: [
|
||||
'svg-inline-loader'
|
||||
]
|
||||
},
|
||||
{
|
||||
test: /[\/\\](node_modules|global|src)[\/\\].*\.scss$/,
|
||||
use: [
|
||||
'style-loader',
|
||||
"css-loader",
|
||||
"sass-loader"
|
||||
]
|
||||
},
|
||||
{
|
||||
test: /[\/\\](node_modules|global|src)[\/\\].*\.css$/,
|
||||
use: [
|
||||
'style-loader',
|
||||
'css-loader'
|
||||
]
|
||||
}
|
||||
];
|
||||
@@ -1,34 +0,0 @@
|
||||
var webpackConfig = require('./webpack.config.js');
|
||||
|
||||
// Karma configuration
|
||||
module.exports = function(config) {
|
||||
var browsers = ['Chrome'];
|
||||
if (process.env.TRAVIS) {
|
||||
browsers = ['Firefox'];
|
||||
}
|
||||
|
||||
config.set({
|
||||
browsers: browsers,
|
||||
frameworks: ['mocha'],
|
||||
// ... normal karma configuration
|
||||
files: [
|
||||
// all files ending in "_test"
|
||||
{pattern: 'test/*_test.js', watched: false},
|
||||
{pattern: 'test/**/*_test.js', watched: false}
|
||||
// each file acts as entry point for the webpack configuration
|
||||
],
|
||||
|
||||
preprocessors: {
|
||||
// add webpack as preprocessor
|
||||
'test/*_test.js': ['webpack'],
|
||||
'test/**/*_test.js': ['webpack']
|
||||
},
|
||||
|
||||
webpack: webpackConfig,
|
||||
webpackMiddleware: {
|
||||
// webpack-dev-middleware configuration
|
||||
// i. e.
|
||||
stats: 'errors-only'
|
||||
}
|
||||
});
|
||||
};
|
||||
BIN
media/demo.gif
BIN
media/demo.gif
Binary file not shown.
|
Before Width: | Height: | Size: 3.2 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 410 KiB |
BIN
media/sponsors/geofabrik.png
Normal file
BIN
media/sponsors/geofabrik.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.2 KiB |
BIN
media/sponsors/orbicon_informatik.png
Normal file
BIN
media/sponsors/orbicon_informatik.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 65 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 37 KiB |
47275
package-lock.json
generated
Normal file
47275
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
207
package.json
207
package.json
@@ -1,15 +1,22 @@
|
||||
{
|
||||
"name": "maputnik",
|
||||
"version": "0.0.1",
|
||||
"description": "A MapboxGL visual style editor",
|
||||
"version": "2.0.0-pre.1",
|
||||
"description": "A MapLibre GL visual style editor",
|
||||
"main": "''",
|
||||
"scripts": {
|
||||
"stats": "webpack --config webpack.production.config.js --profile --json > stats.json",
|
||||
"build": "webpack --config webpack.production.config.js --progress --profile --colors",
|
||||
"test": "karma start --single-run",
|
||||
"test-watch": "karma start",
|
||||
"start": "webpack-dev-server --progress --profile --colors --watch-poll",
|
||||
"lint": "eslint --ext js --ext jsx {src,test}"
|
||||
"stats": "webpack --config config/webpack.production.config.js --progress=profile --json > stats.json",
|
||||
"build": "webpack --config config/webpack.production.config.js --progress=profile --color",
|
||||
"profiling-build": "webpack --config config/webpack.profiling.config.js --progress=profile --color",
|
||||
"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 --color --config config/webpack.config.js",
|
||||
"start-prod": "webpack-dev-server --progress=profile --color --config config/webpack.production.config.js",
|
||||
"start-sandbox": "webpack-dev-server --disable-host-check --host 0.0.0.0 --progress=profile --color --config config/webpack.production.config.js",
|
||||
"lint-js": "eslint --ext js --ext jsx src test",
|
||||
"lint-css": "stylelint \"src/styles/*.scss\"",
|
||||
"lint": "npm run lint-js && npm run lint-css",
|
||||
"storybook": "start-storybook -h 0.0.0.0 -p 6006",
|
||||
"build-storybook": "build-storybook -o build/storybook"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -19,47 +26,74 @@
|
||||
"license": "MIT",
|
||||
"homepage": "https://github.com/maputnik/editor#readme",
|
||||
"dependencies": {
|
||||
"color": "^1.0.3",
|
||||
"file-saver": "^1.3.2",
|
||||
"@babel/runtime": "^7.17.9",
|
||||
"@mapbox/mapbox-gl-rtl-text": "^0.2.3",
|
||||
"@maplibre/maplibre-gl-style-spec": "^17.0.1",
|
||||
"@mdi/react": "^1.5.0",
|
||||
"array-move": "^4.0.0",
|
||||
"buffer": "^6.0.3",
|
||||
"classnames": "^2.3.1",
|
||||
"codemirror": "^5.65.2",
|
||||
"color": "^4.2.3",
|
||||
"detect-browser": "^5.3.0",
|
||||
"file-saver": "^2.0.5",
|
||||
"json-stringify-pretty-compact": "^3.0.0",
|
||||
"json-to-ast": "^2.1.0",
|
||||
"jsonlint": "github:josdejong/jsonlint#85a19d7",
|
||||
"lodash": "^4.17.21",
|
||||
"lodash.capitalize": "^4.2.1",
|
||||
"lodash.clamp": "^4.0.3",
|
||||
"lodash.clonedeep": "^4.5.0",
|
||||
"lodash.get": "^4.4.2",
|
||||
"lodash.isequal": "^4.5.0",
|
||||
"lodash.throttle": "^4.1.1",
|
||||
"lodash.topairs": "^4.3.0",
|
||||
"mapbox-gl": "mapbox/mapbox-gl-js#6c24b9621d2aa770eda67fb5638b4d78087b5624",
|
||||
"mapbox-gl-style-spec": "mapbox/mapbox-gl-style-spec#e85407a377510acb647161de6be6357ab4f606dd",
|
||||
"ol-mapbox-style": "0.0.11",
|
||||
"openlayers": "^3.19.1",
|
||||
"randomcolor": "^0.4.4",
|
||||
"react": "^15.4.0",
|
||||
"react-addons-pure-render-mixin": "^15.4.0",
|
||||
"react-codemirror": "^0.3.0",
|
||||
"react-collapse": "^2.3.3",
|
||||
"react-color": "^2.10.0",
|
||||
"react-dom": "^15.4.0",
|
||||
"react-file-reader-input": "^1.1.0",
|
||||
"react-height": "^2.1.1",
|
||||
"react-icon-base": "^2.0.4",
|
||||
"react-icons": "^2.2.1",
|
||||
"react-motion": "^0.4.7",
|
||||
"react-sortable-hoc": "^0.4.5",
|
||||
"request": "^2.79.0"
|
||||
},
|
||||
"babel": {
|
||||
"presets": [
|
||||
"es2015",
|
||||
"react"
|
||||
],
|
||||
"plugins": [
|
||||
"transform-object-rest-spread"
|
||||
]
|
||||
"mapbox-gl-inspect": "^1.3.1",
|
||||
"maplibre-gl": "^2.4.0",
|
||||
"maputnik-design": "github:maputnik/design#172b06c",
|
||||
"ol": "^6.14.1",
|
||||
"ol-mapbox-style": "^7.1.1",
|
||||
"prop-types": "^15.8.1",
|
||||
"react": "^16.0.0",
|
||||
"react-accessible-accordion": "^4.0.0",
|
||||
"react-aria-menubutton": "^7.0.3",
|
||||
"react-aria-modal": "^4.0.1",
|
||||
"react-autobind": "^1.0.6",
|
||||
"react-autocomplete": "^1.8.1",
|
||||
"react-collapse": "^5.1.1",
|
||||
"react-color": "^2.19.3",
|
||||
"react-dom": "^16.0.0",
|
||||
"react-file-reader-input": "^2.0.0",
|
||||
"react-icon-base": "^2.1.2",
|
||||
"react-icons": "^4.3.1",
|
||||
"react-sortable-hoc": "^2.0.0",
|
||||
"reconnecting-websocket": "^4.4.0",
|
||||
"sass": "^1.50.0",
|
||||
"slugify": "^1.6.5",
|
||||
"string-hash": "^1.1.3",
|
||||
"url": "^0.11.0"
|
||||
},
|
||||
"jshintConfig": {
|
||||
"esversion": 6
|
||||
},
|
||||
"stylelint": {
|
||||
"extends": "stylelint-config-recommended-scss",
|
||||
"rules": {
|
||||
"no-descending-specificity": null,
|
||||
"media-feature-name-no-unknown": [
|
||||
true,
|
||||
{
|
||||
"ignoreMediaFeatureNames": [
|
||||
"prefers-reduced-motion"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"eslintConfig": {
|
||||
"plugins": [
|
||||
"react"
|
||||
],
|
||||
"extend": [
|
||||
"extends": [
|
||||
"plugin:react/recommended"
|
||||
],
|
||||
"env": {
|
||||
@@ -67,7 +101,7 @@
|
||||
"node": true,
|
||||
"es6": true
|
||||
},
|
||||
"parser": "babel-eslint",
|
||||
"parser": "@babel/eslint-parser",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 6,
|
||||
"sourceType": "module",
|
||||
@@ -76,43 +110,66 @@
|
||||
"experimentalObjectRestSpread": true,
|
||||
"jsx": true
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"react": {
|
||||
"version": "detect"
|
||||
}
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-core": "6.14.0",
|
||||
"babel-eslint": "^6.1.2",
|
||||
"babel-loader": "6.2.4",
|
||||
"babel-plugin-transform-class-properties": "^6.11.5",
|
||||
"babel-plugin-transform-decorators-legacy": "^1.3.4",
|
||||
"babel-plugin-transform-flow-strip-types": "^6.21.0",
|
||||
"babel-plugin-transform-object-rest-spread": "^6.8.0",
|
||||
"babel-plugin-transform-runtime": "^6.15.0",
|
||||
"babel-preset-es2015": "6.14.0",
|
||||
"babel-preset-react": "6.11.1",
|
||||
"babel-runtime": "^6.11.6",
|
||||
"css-loader": "0.25.0",
|
||||
"eslint": "^3.5.0",
|
||||
"eslint-plugin-react": "^6.2.0",
|
||||
"extract-text-webpack-plugin": "^1.0.1",
|
||||
"file-loader": "0.9.0",
|
||||
"html-webpack-plugin": "^2.22.0",
|
||||
"json-loader": "^0.5.4",
|
||||
"karma": "^1.3.0",
|
||||
"karma-chrome-launcher": "^2.0.0",
|
||||
"karma-firefox-launcher": "^1.0.0",
|
||||
"karma-mocha": "^1.3.0",
|
||||
"karma-webpack": "^1.8.0",
|
||||
"mocha": "^3.1.2",
|
||||
"mocha-loader": "^1.0.0",
|
||||
"node-sass": "^3.9.2",
|
||||
"react-hot-loader": "^3.0.0-beta.6",
|
||||
"sass-loader": "^4.0.1",
|
||||
"style-loader": "0.13.1",
|
||||
"transform-loader": "^0.2.3",
|
||||
"url-loader": "0.5.7",
|
||||
"webpack": "1.13.2",
|
||||
"webpack-cleanup-plugin": "^0.3.0",
|
||||
"webpack-dev-server": "1.15.1",
|
||||
"webworkify-webpack": "^1.1.3"
|
||||
"@babel/core": "^7.17.9",
|
||||
"@babel/eslint-parser": "^7.19.1",
|
||||
"@babel/plugin-proposal-class-properties": "^7.16.7",
|
||||
"@babel/plugin-transform-runtime": "^7.17.0",
|
||||
"@babel/preset-env": "^7.16.11",
|
||||
"@babel/preset-flow": "^7.16.7",
|
||||
"@babel/preset-react": "^7.16.7",
|
||||
"@mdi/js": "^6.6.96",
|
||||
"@storybook/addon-a11y": "^6.4.20",
|
||||
"@storybook/addon-actions": "^6.4.20",
|
||||
"@storybook/addon-links": "^6.4.20",
|
||||
"@storybook/addon-storysource": "^6.4.20",
|
||||
"@storybook/addons": "^6.4.20",
|
||||
"@storybook/react": "^6.4.20",
|
||||
"@storybook/theming": "^6.4.20",
|
||||
"@wdio/cli": "^7.19.3",
|
||||
"@wdio/local-runner": "^7.19.3",
|
||||
"@wdio/mocha-framework": "^7.19.3",
|
||||
"@wdio/selenium-standalone-service": "^7.19.1",
|
||||
"@wdio/spec-reporter": "^7.19.1",
|
||||
"babel-loader": "^8.2.4",
|
||||
"babel-plugin-istanbul": "^6.1.1",
|
||||
"babel-plugin-static-fs": "^3.0.0",
|
||||
"copy-webpack-plugin": "^6.4.1",
|
||||
"cors": "^2.8.5",
|
||||
"cross-env": "^7.0.3",
|
||||
"css-loader": "^5.2.7",
|
||||
"eslint": "^8.12.0",
|
||||
"eslint-plugin-react": "^7.29.4",
|
||||
"express": "^4.17.3",
|
||||
"html-webpack-inline-svg-plugin": "^2.3.0",
|
||||
"html-webpack-plugin": "^4.5.2",
|
||||
"istanbul": "^0.4.5",
|
||||
"istanbul-lib-coverage": "^3.2.0",
|
||||
"mkdirp": "^1.0.4",
|
||||
"mocha": "^9.2.2",
|
||||
"postcss": "^8.4.12",
|
||||
"react-hot-loader": "^4.13.0",
|
||||
"sass-loader": "^10.2.1",
|
||||
"style-loader": "^2.0.0",
|
||||
"stylelint": "^14.6.1",
|
||||
"stylelint-config-recommended-scss": "^6.0.0",
|
||||
"stylelint-scss": "^4.2.0",
|
||||
"svg-inline-loader": "^0.8.2",
|
||||
"transform-loader": "^0.2.4",
|
||||
"typescript": "^4.6.3",
|
||||
"uuid": "^8.3.2",
|
||||
"webdriverio": "^7.19.3",
|
||||
"webpack": "^4.46.0",
|
||||
"webpack-bundle-analyzer": "^4.5.0",
|
||||
"webpack-cleanup-plugin": "^0.5.1",
|
||||
"webpack-cli": "^4.9.2",
|
||||
"webpack-dev-server": "^4.8.1"
|
||||
}
|
||||
}
|
||||
|
||||
5
sandbox.config.json
Normal file
5
sandbox.config.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"container": {
|
||||
"startScript": "start-sandbox"
|
||||
}
|
||||
}
|
||||
@@ -1,68 +1,484 @@
|
||||
import autoBind from 'react-autobind';
|
||||
import React from 'react'
|
||||
import { saveAs } from 'file-saver'
|
||||
import cloneDeep from 'lodash.clonedeep'
|
||||
import clamp from 'lodash.clamp'
|
||||
import buffer from 'buffer'
|
||||
import get from 'lodash.get'
|
||||
import {unset} from 'lodash'
|
||||
import {arrayMoveMutable} from 'array-move'
|
||||
import url from 'url'
|
||||
import hash from "string-hash";
|
||||
|
||||
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 MapMapboxGl from './MapMapboxGl'
|
||||
import MapOpenLayers from './MapOpenLayers'
|
||||
import LayerList from './LayerList'
|
||||
import LayerEditor from './LayerEditor'
|
||||
import AppToolbar from './AppToolbar'
|
||||
import AppLayout from './AppLayout'
|
||||
import MessagePanel from './AppMessagePanel'
|
||||
|
||||
import style from '../libs/style.js'
|
||||
import { loadDefaultStyle, SettingsStore, StyleStore } from '../libs/stylestore'
|
||||
import ModalSettings from './ModalSettings'
|
||||
import ModalExport from './ModalExport'
|
||||
import ModalSources from './ModalSources'
|
||||
import ModalOpen from './ModalOpen'
|
||||
import ModalShortcuts from './ModalShortcuts'
|
||||
import ModalSurvey from './ModalSurvey'
|
||||
import ModalDebug from './ModalDebug'
|
||||
|
||||
import { downloadGlyphsMetadata, downloadSpriteMetadata } from '../libs/metadata'
|
||||
import {latest, validate} from '@maplibre/maplibre-gl-style-spec'
|
||||
import style from '../libs/style'
|
||||
import { initialStyleUrl, loadStyleUrl, removeStyleQuerystring } from '../libs/urlopen'
|
||||
import { undoMessages, redoMessages } from '../libs/diffmessage'
|
||||
import { StyleStore } from '../libs/stylestore'
|
||||
import { ApiStyleStore } from '../libs/apistore'
|
||||
import { RevisionStore } from '../libs/revisions'
|
||||
import LayerWatcher from '../libs/layerwatcher'
|
||||
import tokens from '../config/tokens.json'
|
||||
import isEqual from 'lodash.isequal'
|
||||
import Debug from '../libs/debug'
|
||||
import {formatLayerId} from '../util/format';
|
||||
|
||||
// Buffer must be defined globally for @maplibre/maplibre-gl-style-spec validate() function to succeed.
|
||||
window.Buffer = buffer.Buffer;
|
||||
|
||||
function setFetchAccessToken(url, mapStyle) {
|
||||
const matchesTilehosting = url.match(/\.tilehosting\.com/);
|
||||
const matchesMaptiler = url.match(/\.maptiler\.com/);
|
||||
const matchesThunderforest = url.match(/\.thunderforest\.com/);
|
||||
if (matchesTilehosting || matchesMaptiler) {
|
||||
const accessToken = style.getAccessToken("openmaptiles", mapStyle, {allowFallback: true})
|
||||
if (accessToken) {
|
||||
return url.replace('{key}', accessToken)
|
||||
}
|
||||
}
|
||||
else if (matchesThunderforest) {
|
||||
const accessToken = style.getAccessToken("thunderforest", mapStyle, {allowFallback: true})
|
||||
if (accessToken) {
|
||||
return url.replace('{key}', accessToken)
|
||||
}
|
||||
}
|
||||
else {
|
||||
return url;
|
||||
}
|
||||
}
|
||||
|
||||
function updateRootSpec(spec, fieldName, newValues) {
|
||||
return {
|
||||
...spec,
|
||||
$root: {
|
||||
...spec.$root,
|
||||
[fieldName]: {
|
||||
...spec.$root[fieldName],
|
||||
values: newValues
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default class App extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.layerWatcher = new LayerWatcher()
|
||||
this.styleStore = new ApiStyleStore()
|
||||
this.styleStore.supported(isSupported => {
|
||||
if(!isSupported) {
|
||||
console.log('Falling back to local storage for storing styles')
|
||||
this.styleStore = new StyleStore()
|
||||
}
|
||||
this.styleStore.latestStyle(mapStyle => this.onStyleChanged(mapStyle))
|
||||
autoBind(this);
|
||||
|
||||
this.revisionStore = new RevisionStore()
|
||||
const params = new URLSearchParams(window.location.search.substring(1))
|
||||
let port = params.get("localport")
|
||||
if (port == null && (window.location.port != 80 && window.location.port != 443)) {
|
||||
port = window.location.port
|
||||
}
|
||||
this.styleStore = new ApiStyleStore({
|
||||
onLocalStyleChange: mapStyle => this.onStyleChanged(mapStyle, {save: false}),
|
||||
port: port,
|
||||
host: params.get("localhost")
|
||||
})
|
||||
|
||||
this.settingsStore = new SettingsStore()
|
||||
|
||||
const shortcuts = [
|
||||
{
|
||||
key: "?",
|
||||
handler: () => {
|
||||
this.toggleModal("shortcuts");
|
||||
}
|
||||
},
|
||||
{
|
||||
key: "o",
|
||||
handler: () => {
|
||||
this.toggleModal("open");
|
||||
}
|
||||
},
|
||||
{
|
||||
key: "e",
|
||||
handler: () => {
|
||||
this.toggleModal("export");
|
||||
}
|
||||
},
|
||||
{
|
||||
key: "d",
|
||||
handler: () => {
|
||||
this.toggleModal("sources");
|
||||
}
|
||||
},
|
||||
{
|
||||
key: "s",
|
||||
handler: () => {
|
||||
this.toggleModal("settings");
|
||||
}
|
||||
},
|
||||
{
|
||||
key: "i",
|
||||
handler: () => {
|
||||
this.setMapState(
|
||||
this.state.mapState === "map" ? "inspect" : "map"
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
key: "m",
|
||||
handler: () => {
|
||||
document.querySelector(".mapboxgl-canvas").focus();
|
||||
}
|
||||
},
|
||||
{
|
||||
key: "!",
|
||||
handler: () => {
|
||||
this.toggleModal("debug");
|
||||
}
|
||||
},
|
||||
]
|
||||
|
||||
document.body.addEventListener("keyup", (e) => {
|
||||
if(e.key === "Escape") {
|
||||
e.target.blur();
|
||||
document.body.focus();
|
||||
}
|
||||
else if(this.state.isOpen.shortcuts || document.activeElement === document.body) {
|
||||
const shortcut = shortcuts.find((shortcut) => {
|
||||
return (shortcut.key === e.key)
|
||||
})
|
||||
|
||||
if(shortcut) {
|
||||
this.setModal("shortcuts", false);
|
||||
shortcut.handler(e);
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const styleUrl = initialStyleUrl()
|
||||
if(styleUrl && window.confirm("Load style from URL: " + styleUrl + " and discard current changes?")) {
|
||||
this.styleStore = new StyleStore()
|
||||
loadStyleUrl(styleUrl, mapStyle => this.onStyleChanged(mapStyle))
|
||||
removeStyleQuerystring()
|
||||
} else {
|
||||
if(styleUrl) {
|
||||
removeStyleQuerystring()
|
||||
}
|
||||
this.styleStore.init(err => {
|
||||
if(err) {
|
||||
console.log('Falling back to local storage for storing styles')
|
||||
this.styleStore = new StyleStore()
|
||||
}
|
||||
this.styleStore.latestStyle(mapStyle => this.onStyleChanged(mapStyle, {initialLoad: true}))
|
||||
|
||||
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 = {
|
||||
accessToken: this.settingsStore.accessToken,
|
||||
errors: [],
|
||||
infos: [],
|
||||
mapStyle: style.emptyStyle,
|
||||
selectedLayerIndex: 0,
|
||||
sources: {},
|
||||
vectorLayers: {},
|
||||
mapState: "map",
|
||||
spec: latest,
|
||||
mapView: {
|
||||
zoom: 0,
|
||||
center: {
|
||||
lng: 0,
|
||||
lat: 0,
|
||||
},
|
||||
},
|
||||
isOpen: {
|
||||
settings: false,
|
||||
sources: false,
|
||||
open: false,
|
||||
shortcuts: false,
|
||||
export: false,
|
||||
// TODO: Disabled for now, this should be opened on the Nth visit to the editor
|
||||
survey: false,
|
||||
debug: false,
|
||||
},
|
||||
mapboxGlDebugOptions: {
|
||||
showTileBoundaries: false,
|
||||
showCollisionBoxes: false,
|
||||
showOverdrawInspector: false,
|
||||
},
|
||||
openlayersDebugOptions: {
|
||||
debugToolbox: false,
|
||||
},
|
||||
}
|
||||
|
||||
this.layerWatcher = new LayerWatcher({
|
||||
onVectorLayersChange: v => this.setState({ vectorLayers: v })
|
||||
})
|
||||
}
|
||||
|
||||
handleKeyPress = (e) => {
|
||||
if(navigator.platform.toUpperCase().indexOf('MAC') >= 0) {
|
||||
if(e.metaKey && e.shiftKey && e.keyCode === 90) {
|
||||
e.preventDefault();
|
||||
this.onRedo(e);
|
||||
}
|
||||
else if(e.metaKey && e.keyCode === 90) {
|
||||
e.preventDefault();
|
||||
this.onUndo(e);
|
||||
}
|
||||
}
|
||||
else {
|
||||
if(e.ctrlKey && e.keyCode === 90) {
|
||||
e.preventDefault();
|
||||
this.onUndo(e);
|
||||
}
|
||||
else if(e.ctrlKey && e.keyCode === 89) {
|
||||
e.preventDefault();
|
||||
this.onRedo(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onReset() {
|
||||
this.styleStore.purge()
|
||||
loadDefaultStyle(mapStyle => this.onStyleOpen(mapStyle))
|
||||
componentDidMount() {
|
||||
window.addEventListener("keydown", this.handleKeyPress);
|
||||
}
|
||||
|
||||
onStyleDownload() {
|
||||
const mapStyle = this.state.mapStyle
|
||||
const blob = new Blob([JSON.stringify(mapStyle, null, 4)], {type: "application/json;charset=utf-8"});
|
||||
saveAs(blob, mapStyle.id + ".json");
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener("keydown", this.handleKeyPress);
|
||||
}
|
||||
|
||||
saveStyle(snapshotStyle) {
|
||||
snapshotStyle.modified = new Date().toJSON()
|
||||
this.styleStore.save(snapshotStyle)
|
||||
}
|
||||
|
||||
onStyleChanged(newStyle) {
|
||||
this.saveStyle(newStyle)
|
||||
this.setState({ mapStyle: newStyle })
|
||||
updateFonts(urlTemplate) {
|
||||
const metadata = this.state.mapStyle.metadata || {}
|
||||
const accessToken = metadata['maputnik:openmaptiles_access_token'] || tokens.openmaptiles
|
||||
|
||||
let glyphUrl = (typeof urlTemplate === 'string')? urlTemplate.replace('{key}', accessToken): urlTemplate;
|
||||
downloadGlyphsMetadata(glyphUrl, fonts => {
|
||||
this.setState({ spec: updateRootSpec(this.state.spec, 'glyphs', fonts)})
|
||||
})
|
||||
}
|
||||
|
||||
onAccessTokenChanged(newToken) {
|
||||
this.settingsStore.accessToken = newToken
|
||||
this.setState({ accessToken: newToken })
|
||||
updateIcons(baseUrl) {
|
||||
downloadSpriteMetadata(baseUrl, icons => {
|
||||
this.setState({ spec: updateRootSpec(this.state.spec, 'sprite', icons)})
|
||||
})
|
||||
}
|
||||
|
||||
onLayersChange(changedLayers) {
|
||||
onChangeMetadataProperty = (property, value) => {
|
||||
// If we're changing renderer reset the map state.
|
||||
if (
|
||||
property === 'maputnik:renderer' &&
|
||||
value !== get(this.state.mapStyle, ['metadata', 'maputnik:renderer'], 'mbgljs')
|
||||
) {
|
||||
this.setState({
|
||||
mapState: 'map'
|
||||
});
|
||||
}
|
||||
|
||||
const changedStyle = {
|
||||
...this.state.mapStyle,
|
||||
metadata: {
|
||||
...this.state.mapStyle.metadata,
|
||||
[property]: value
|
||||
}
|
||||
}
|
||||
this.onStyleChanged(changedStyle)
|
||||
}
|
||||
|
||||
onStyleChanged = (newStyle, opts={}) => {
|
||||
opts = {
|
||||
save: true,
|
||||
addRevision: true,
|
||||
initialLoad: false,
|
||||
...opts,
|
||||
};
|
||||
|
||||
if (opts.initialLoad) {
|
||||
this.getInitialStateFromUrl(newStyle);
|
||||
}
|
||||
|
||||
const errors = validate(newStyle, latest) || [];
|
||||
|
||||
// The validate function doesn't give us errors for duplicate error with
|
||||
// empty string for layer.id, manually deal with that here.
|
||||
const layerErrors = [];
|
||||
if (newStyle && newStyle.layers) {
|
||||
const foundLayers = new Map();
|
||||
newStyle.layers.forEach((layer, index) => {
|
||||
if (layer.id === "" && foundLayers.has(layer.id)) {
|
||||
const message = `Duplicate layer: ${formatLayerId(layer.id)}`;
|
||||
const error = new Error(
|
||||
`layers[${index}]: duplicate layer id [empty_string], previously used`
|
||||
);
|
||||
layerErrors.push(error);
|
||||
}
|
||||
foundLayers.set(layer.id, true);
|
||||
});
|
||||
}
|
||||
|
||||
const mappedErrors = layerErrors.concat(errors).map(error => {
|
||||
// Special case: Duplicate layer id
|
||||
const dupMatch = error.message.match(/layers\[(\d+)\]: (duplicate layer id "?(.*)"?, previously used)/);
|
||||
if (dupMatch) {
|
||||
const [matchStr, index, message] = dupMatch;
|
||||
return {
|
||||
message: error.message,
|
||||
parsed: {
|
||||
type: "layer",
|
||||
data: {
|
||||
index: parseInt(index, 10),
|
||||
key: "id",
|
||||
message,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Special case: Invalid source
|
||||
const invalidSourceMatch = error.message.match(/layers\[(\d+)\]: (source "(?:.*)" not found)/);
|
||||
if (invalidSourceMatch) {
|
||||
const [matchStr, index, message] = invalidSourceMatch;
|
||||
return {
|
||||
message: error.message,
|
||||
parsed: {
|
||||
type: "layer",
|
||||
data: {
|
||||
index: parseInt(index, 10),
|
||||
key: "source",
|
||||
message,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const layerMatch = error.message.match(/layers\[(\d+)\]\.(?:(\S+)\.)?(\S+): (.*)/);
|
||||
if (layerMatch) {
|
||||
const [matchStr, index, group, property, message] = layerMatch;
|
||||
const key = (group && property) ? [group, property].join(".") : property;
|
||||
return {
|
||||
message: error.message,
|
||||
parsed: {
|
||||
type: "layer",
|
||||
data: {
|
||||
index: parseInt(index, 10),
|
||||
key,
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
return {
|
||||
message: error.message,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
let dirtyMapStyle = undefined;
|
||||
if (errors.length > 0) {
|
||||
dirtyMapStyle = cloneDeep(newStyle);
|
||||
|
||||
errors.forEach(error => {
|
||||
const {message} = error;
|
||||
if (message) {
|
||||
try {
|
||||
const objPath = message.split(":")[0];
|
||||
// Errors can be deply nested for example 'layers[0].filter[1][1][0]' we only care upto the property 'layers[0].filter'
|
||||
const unsetPath = objPath.match(/^\S+?\[\d+\]\.[^\[]+/)[0];
|
||||
unset(dirtyMapStyle, unsetPath);
|
||||
}
|
||||
catch (err) {
|
||||
console.warn(err);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if(newStyle.glyphs !== this.state.mapStyle.glyphs) {
|
||||
this.updateFonts(newStyle.glyphs)
|
||||
}
|
||||
if(newStyle.sprite !== this.state.mapStyle.sprite) {
|
||||
this.updateIcons(newStyle.sprite)
|
||||
}
|
||||
|
||||
if (opts.addRevision) {
|
||||
this.revisionStore.addRevision(newStyle);
|
||||
}
|
||||
if (opts.save) {
|
||||
this.saveStyle(newStyle);
|
||||
}
|
||||
|
||||
this.setState({
|
||||
mapStyle: newStyle,
|
||||
dirtyMapStyle: dirtyMapStyle,
|
||||
errors: mappedErrors,
|
||||
}, () => {
|
||||
this.fetchSources();
|
||||
this.setStateInUrl();
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
onUndo = () => {
|
||||
const activeStyle = this.revisionStore.undo()
|
||||
|
||||
const messages = undoMessages(this.state.mapStyle, activeStyle)
|
||||
this.onStyleChanged(activeStyle, {addRevision: false});
|
||||
this.setState({
|
||||
infos: messages,
|
||||
})
|
||||
}
|
||||
|
||||
onRedo = () => {
|
||||
const activeStyle = this.revisionStore.redo()
|
||||
const messages = redoMessages(this.state.mapStyle, activeStyle)
|
||||
this.onStyleChanged(activeStyle, {addRevision: false});
|
||||
this.setState({
|
||||
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);
|
||||
arrayMoveMutable(layers, oldIndex, newIndex);
|
||||
this.onLayersChange(layers);
|
||||
}
|
||||
|
||||
onLayersChange = (changedLayers) => {
|
||||
const changedStyle = {
|
||||
...this.state.mapStyle,
|
||||
layers: changedLayers
|
||||
@@ -70,83 +486,441 @@ export default class App extends React.Component {
|
||||
this.onStyleChanged(changedStyle)
|
||||
}
|
||||
|
||||
onLayerIdChange(oldId, newId) {
|
||||
const changedLayers = this.state.mapStyle.layers.slice(0)
|
||||
const idx = style.indexOfLayer(changedLayers, oldId)
|
||||
onLayerDestroy = (index) => {
|
||||
let layers = this.state.mapStyle.layers;
|
||||
const remainingLayers = layers.slice(0);
|
||||
remainingLayers.splice(index, 1);
|
||||
this.onLayersChange(remainingLayers);
|
||||
}
|
||||
|
||||
changedLayers[idx] = {
|
||||
...changedLayers[idx],
|
||||
onLayerCopy = (index) => {
|
||||
let layers = this.state.mapStyle.layers;
|
||||
const changedLayers = layers.slice(0)
|
||||
|
||||
const clonedLayer = cloneDeep(changedLayers[index])
|
||||
clonedLayer.id = clonedLayer.id + "-copy"
|
||||
changedLayers.splice(index, 0, clonedLayer)
|
||||
this.onLayersChange(changedLayers)
|
||||
}
|
||||
|
||||
onLayerVisibilityToggle = (index) => {
|
||||
let layers = this.state.mapStyle.layers;
|
||||
const changedLayers = layers.slice(0)
|
||||
|
||||
const layer = { ...changedLayers[index] }
|
||||
const changedLayout = 'layout' in layer ? {...layer.layout} : {}
|
||||
changedLayout.visibility = changedLayout.visibility === 'none' ? 'visible' : 'none'
|
||||
|
||||
layer.layout = changedLayout
|
||||
changedLayers[index] = layer
|
||||
this.onLayersChange(changedLayers)
|
||||
}
|
||||
|
||||
|
||||
onLayerIdChange = (index, oldId, newId) => {
|
||||
const changedLayers = this.state.mapStyle.layers.slice(0)
|
||||
changedLayers[index] = {
|
||||
...changedLayers[index],
|
||||
id: newId
|
||||
}
|
||||
|
||||
this.onLayersChange(changedLayers)
|
||||
}
|
||||
|
||||
onLayerChanged(layer) {
|
||||
onLayerChanged = (index, layer) => {
|
||||
const changedLayers = this.state.mapStyle.layers.slice(0)
|
||||
const idx = style.indexOfLayer(changedLayers, layer.id)
|
||||
changedLayers[idx] = layer
|
||||
changedLayers[index] = layer
|
||||
|
||||
this.onLayersChange(changedLayers)
|
||||
}
|
||||
|
||||
mapRenderer() {
|
||||
const mapProps = {
|
||||
mapStyle: this.state.mapStyle,
|
||||
accessToken: this.state.accessToken,
|
||||
onMapLoaded: (map) => {
|
||||
this.layerWatcher.map = map
|
||||
setMapState = (newState) => {
|
||||
this.setState({
|
||||
mapState: newState
|
||||
}, this.setStateInUrl);
|
||||
}
|
||||
|
||||
setDefaultValues = (styleObj) => {
|
||||
const metadata = styleObj.metadata || {}
|
||||
if(metadata['maputnik:renderer'] === undefined) {
|
||||
const changedStyle = {
|
||||
...styleObj,
|
||||
metadata: {
|
||||
...styleObj.metadata,
|
||||
'maputnik:renderer': 'mbgljs'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const metadata = this.state.mapStyle.metadata || {}
|
||||
const renderer = metadata['maputnik:renderer'] || 'mbgljs'
|
||||
|
||||
// Check if OL3 code has been loaded?
|
||||
if(renderer === 'ol3') {
|
||||
return <OpenLayers3Map {...mapProps} />
|
||||
return changedStyle
|
||||
} else {
|
||||
return <MapboxGlMap {...mapProps} />
|
||||
return styleObj
|
||||
}
|
||||
}
|
||||
|
||||
onLayerSelect(layerId) {
|
||||
const idx = style.indexOfLayer(this.state.mapStyle.layers, layerId)
|
||||
this.setState({ selectedLayerIndex: idx })
|
||||
openStyle = (styleObj) => {
|
||||
styleObj = this.setDefaultValues(styleObj)
|
||||
this.onStyleChanged(styleObj)
|
||||
}
|
||||
|
||||
fetchSources() {
|
||||
const sourceList = {};
|
||||
|
||||
for(let [key, val] of Object.entries(this.state.mapStyle.sources)) {
|
||||
if(
|
||||
!this.state.sources.hasOwnProperty(key) &&
|
||||
val.type === "vector" &&
|
||||
val.hasOwnProperty("url")
|
||||
) {
|
||||
sourceList[key] = {
|
||||
type: val.type,
|
||||
layers: []
|
||||
};
|
||||
|
||||
let url = val.url;
|
||||
|
||||
try {
|
||||
url = setFetchAccessToken(url, this.state.mapStyle)
|
||||
} catch(err) {
|
||||
console.warn("Failed to setFetchAccessToken: ", err);
|
||||
}
|
||||
|
||||
fetch(url, {
|
||||
mode: 'cors',
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(json => {
|
||||
|
||||
if(!json.hasOwnProperty("vector_layers")) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create new objects before setState
|
||||
const sources = Object.assign({}, {
|
||||
[key]: this.state.sources[key],
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
}
|
||||
else {
|
||||
sourceList[key] = this.state.sources[key] || this.state.mapStyle.sources[key];
|
||||
}
|
||||
}
|
||||
|
||||
if(!isEqual(this.state.sources, sourceList)) {
|
||||
console.debug("Setting sources");
|
||||
this.setState({
|
||||
sources: sourceList
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
_getRenderer () {
|
||||
const metadata = this.state.mapStyle.metadata || {};
|
||||
return metadata['maputnik:renderer'] || 'mbgljs';
|
||||
}
|
||||
|
||||
onMapChange = (mapView) => {
|
||||
this.setState({
|
||||
mapView,
|
||||
});
|
||||
}
|
||||
|
||||
mapRenderer() {
|
||||
const {mapStyle, dirtyMapStyle} = this.state;
|
||||
const metadata = this.state.mapStyle.metadata || {};
|
||||
|
||||
const mapProps = {
|
||||
mapStyle: (dirtyMapStyle || mapStyle),
|
||||
replaceAccessTokens: (mapStyle) => {
|
||||
return style.replaceAccessTokens(mapStyle, {
|
||||
allowFallback: true
|
||||
});
|
||||
},
|
||||
onDataChange: (e) => {
|
||||
this.layerWatcher.analyzeMap(e.map)
|
||||
this.fetchSources();
|
||||
},
|
||||
}
|
||||
|
||||
const renderer = this._getRenderer();
|
||||
|
||||
let mapElement;
|
||||
|
||||
// Check if OL code has been loaded?
|
||||
if(renderer === 'ol') {
|
||||
mapElement = <MapOpenLayers
|
||||
{...mapProps}
|
||||
onChange={this.onMapChange}
|
||||
debugToolbox={this.state.openlayersDebugOptions.debugToolbox}
|
||||
onLayerSelect={this.onLayerSelect}
|
||||
/>
|
||||
} else {
|
||||
mapElement = <MapMapboxGl {...mapProps}
|
||||
onChange={this.onMapChange}
|
||||
options={this.state.mapboxGlDebugOptions}
|
||||
inspectModeEnabled={this.state.mapState === "inspect"}
|
||||
highlightedLayer={this.state.mapStyle.layers[this.state.selectedLayerIndex]}
|
||||
onLayerSelect={this.onLayerSelect} />
|
||||
}
|
||||
|
||||
let filterName;
|
||||
if(this.state.mapState.match(/^filter-/)) {
|
||||
filterName = this.state.mapState.replace(/^filter-/, "");
|
||||
}
|
||||
const elementStyle = {};
|
||||
if (filterName) {
|
||||
elementStyle.filter = `url('#${filterName}')`;
|
||||
}
|
||||
|
||||
return <div style={elementStyle} className="maputnik-map__container">
|
||||
{mapElement}
|
||||
</div>
|
||||
}
|
||||
|
||||
setStateInUrl = () => {
|
||||
const {mapState, mapStyle, isOpen} = this.state;
|
||||
const {selectedLayerIndex} = this.state;
|
||||
const url = new URL(location.href);
|
||||
const hashVal = hash(JSON.stringify(mapStyle));
|
||||
url.searchParams.set("layer", `${hashVal}~${selectedLayerIndex}`);
|
||||
|
||||
const openModals = Object.entries(isOpen)
|
||||
.map(([key, val]) => (val === true ? key : null))
|
||||
.filter(val => val !== null);
|
||||
|
||||
if (openModals.length > 0) {
|
||||
url.searchParams.set("modal", openModals.join(","));
|
||||
}
|
||||
else {
|
||||
url.searchParams.delete("modal");
|
||||
}
|
||||
|
||||
if (mapState === "map") {
|
||||
url.searchParams.delete("view");
|
||||
}
|
||||
else if (mapState === "inspect") {
|
||||
url.searchParams.set("view", "inspect");
|
||||
}
|
||||
|
||||
history.replaceState({selectedLayerIndex}, "Maputnik", url.href);
|
||||
}
|
||||
|
||||
getInitialStateFromUrl = (mapStyle) => {
|
||||
const url = new URL(location.href);
|
||||
const modalParam = url.searchParams.get("modal");
|
||||
if (modalParam && modalParam !== "") {
|
||||
const modals = modalParam.split(",");
|
||||
const modalObj = {};
|
||||
modals.forEach(modalName => {
|
||||
modalObj[modalName] = true;
|
||||
});
|
||||
|
||||
this.setState({
|
||||
isOpen: {
|
||||
...this.state.isOpen,
|
||||
...modalObj,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const view = url.searchParams.get("view");
|
||||
if (view && view !== "") {
|
||||
this.setMapState(view);
|
||||
}
|
||||
|
||||
const path = url.searchParams.get("layer");
|
||||
if (path) {
|
||||
try {
|
||||
const parts = path.split("~");
|
||||
const [hashVal, selectedLayerIndex] = [
|
||||
parts[0],
|
||||
parseInt(parts[1], 10),
|
||||
];
|
||||
|
||||
let valid = true;
|
||||
if (hashVal !== "-") {
|
||||
const currentHashVal = hash(JSON.stringify(mapStyle));
|
||||
if (currentHashVal !== parseInt(hashVal, 10)) {
|
||||
valid = false;
|
||||
}
|
||||
}
|
||||
if (valid) {
|
||||
this.setState({
|
||||
selectedLayerIndex,
|
||||
selectedLayerOriginalId: mapStyle.layers[selectedLayerIndex].id,
|
||||
});
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
console.warn(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onLayerSelect = (index) => {
|
||||
this.setState({
|
||||
selectedLayerIndex: index,
|
||||
selectedLayerOriginalId: this.state.mapStyle.layers[index].id,
|
||||
}, this.setStateInUrl);
|
||||
}
|
||||
|
||||
setModal(modalName, value) {
|
||||
if(modalName === 'survey' && value === false) {
|
||||
localStorage.setItem('survey', '');
|
||||
}
|
||||
|
||||
this.setState({
|
||||
isOpen: {
|
||||
...this.state.isOpen,
|
||||
[modalName]: value
|
||||
}
|
||||
}, this.setStateInUrl)
|
||||
}
|
||||
|
||||
toggleModal(modalName) {
|
||||
this.setModal(modalName, !this.state.isOpen[modalName]);
|
||||
}
|
||||
|
||||
onChangeOpenlayersDebug = (key, value) => {
|
||||
this.setState({
|
||||
openlayersDebugOptions: {
|
||||
...this.state.openlayersDebugOptions,
|
||||
[key]: value,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onChangeMaboxGlDebug = (key, value) => {
|
||||
this.setState({
|
||||
mapboxGlDebugOptions: {
|
||||
...this.state.mapboxGlDebugOptions,
|
||||
[key]: value,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const layers = this.state.mapStyle.layers || []
|
||||
const selectedLayer = layers.length > 0 ? layers[this.state.selectedLayerIndex] : null
|
||||
const metadata = this.state.mapStyle.metadata || {}
|
||||
|
||||
const toolbar = <Toolbar
|
||||
const toolbar = <AppToolbar
|
||||
renderer={this._getRenderer()}
|
||||
mapState={this.state.mapState}
|
||||
mapStyle={this.state.mapStyle}
|
||||
onStyleChanged={this.onStyleChanged.bind(this)}
|
||||
onStyleOpen={this.onStyleChanged.bind(this)}
|
||||
onStyleDownload={this.onStyleDownload.bind(this)}
|
||||
inspectModeEnabled={this.state.mapState === "inspect"}
|
||||
sources={this.state.sources}
|
||||
onStyleChanged={this.onStyleChanged}
|
||||
onStyleOpen={this.onStyleChanged}
|
||||
onSetMapState={this.setMapState}
|
||||
onToggleModal={this.toggleModal.bind(this)}
|
||||
/>
|
||||
|
||||
const layerList = <LayerList
|
||||
onLayersChange={this.onLayersChange.bind(this)}
|
||||
onLayerSelect={this.onLayerSelect.bind(this)}
|
||||
onMoveLayer={this.onMoveLayer}
|
||||
onLayerDestroy={this.onLayerDestroy}
|
||||
onLayerCopy={this.onLayerCopy}
|
||||
onLayerVisibilityToggle={this.onLayerVisibilityToggle}
|
||||
onLayersChange={this.onLayersChange}
|
||||
onLayerSelect={this.onLayerSelect}
|
||||
selectedLayerIndex={this.state.selectedLayerIndex}
|
||||
layers={layers}
|
||||
sources={this.state.sources}
|
||||
errors={this.state.errors}
|
||||
/>
|
||||
|
||||
const layerEditor = selectedLayer ? <LayerEditor
|
||||
key={this.state.selectedLayerOriginalId}
|
||||
layer={selectedLayer}
|
||||
sources={this.layerWatcher.sources}
|
||||
vectorLayers={this.layerWatcher.vectorLayers}
|
||||
onLayerChanged={this.onLayerChanged.bind(this)}
|
||||
onLayerIdChange={this.onLayerIdChange.bind(this)}
|
||||
layerIndex={this.state.selectedLayerIndex}
|
||||
isFirstLayer={this.state.selectedLayerIndex < 1}
|
||||
isLastLayer={this.state.selectedLayerIndex === this.state.mapStyle.layers.length-1}
|
||||
sources={this.state.sources}
|
||||
vectorLayers={this.state.vectorLayers}
|
||||
spec={this.state.spec}
|
||||
onMoveLayer={this.onMoveLayer}
|
||||
onLayerChanged={this.onLayerChanged}
|
||||
onLayerDestroy={this.onLayerDestroy}
|
||||
onLayerCopy={this.onLayerCopy}
|
||||
onLayerVisibilityToggle={this.onLayerVisibilityToggle}
|
||||
onLayerIdChange={this.onLayerIdChange}
|
||||
errors={this.state.errors}
|
||||
/> : null
|
||||
|
||||
const bottomPanel = (this.state.errors.length + this.state.infos.length) > 0 ? <MessagePanel
|
||||
currentLayer={selectedLayer}
|
||||
selectedLayerIndex={this.state.selectedLayerIndex}
|
||||
onLayerSelect={this.onLayerSelect}
|
||||
mapStyle={this.state.mapStyle}
|
||||
errors={this.state.errors}
|
||||
infos={this.state.infos}
|
||||
/> : null
|
||||
|
||||
|
||||
const modals = <div>
|
||||
<ModalDebug
|
||||
renderer={this._getRenderer()}
|
||||
mapboxGlDebugOptions={this.state.mapboxGlDebugOptions}
|
||||
openlayersDebugOptions={this.state.openlayersDebugOptions}
|
||||
onChangeMaboxGlDebug={this.onChangeMaboxGlDebug}
|
||||
onChangeOpenlayersDebug={this.onChangeOpenlayersDebug}
|
||||
isOpen={this.state.isOpen.debug}
|
||||
onOpenToggle={this.toggleModal.bind(this, 'debug')}
|
||||
mapView={this.state.mapView}
|
||||
/>
|
||||
<ModalShortcuts
|
||||
ref={(el) => this.shortcutEl = el}
|
||||
isOpen={this.state.isOpen.shortcuts}
|
||||
onOpenToggle={this.toggleModal.bind(this, 'shortcuts')}
|
||||
/>
|
||||
<ModalSettings
|
||||
mapStyle={this.state.mapStyle}
|
||||
onStyleChanged={this.onStyleChanged}
|
||||
onChangeMetadataProperty={this.onChangeMetadataProperty}
|
||||
isOpen={this.state.isOpen.settings}
|
||||
onOpenToggle={this.toggleModal.bind(this, 'settings')}
|
||||
openlayersDebugOptions={this.state.openlayersDebugOptions}
|
||||
/>
|
||||
<ModalExport
|
||||
mapStyle={this.state.mapStyle}
|
||||
onStyleChanged={this.onStyleChanged}
|
||||
isOpen={this.state.isOpen.export}
|
||||
onOpenToggle={this.toggleModal.bind(this, 'export')}
|
||||
/>
|
||||
<ModalOpen
|
||||
isOpen={this.state.isOpen.open}
|
||||
onStyleOpen={this.openStyle}
|
||||
onOpenToggle={this.toggleModal.bind(this, 'open')}
|
||||
/>
|
||||
<ModalSources
|
||||
mapStyle={this.state.mapStyle}
|
||||
onStyleChanged={this.onStyleChanged}
|
||||
isOpen={this.state.isOpen.sources}
|
||||
onOpenToggle={this.toggleModal.bind(this, 'sources')}
|
||||
/>
|
||||
<ModalSurvey
|
||||
isOpen={this.state.isOpen.survey}
|
||||
onOpenToggle={this.toggleModal.bind(this, 'survey')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
return <AppLayout
|
||||
toolbar={toolbar}
|
||||
layerList={layerList}
|
||||
layerEditor={layerEditor}
|
||||
map={this.mapRenderer()}
|
||||
bottom={bottomPanel}
|
||||
modals={modals}
|
||||
/>
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import ScrollContainer from './ScrollContainer'
|
||||
|
||||
import theme from '../config/theme'
|
||||
import colors from '../config/colors'
|
||||
|
||||
class AppLayout extends React.Component {
|
||||
static propTypes = {
|
||||
toolbar: React.PropTypes.element.isRequired,
|
||||
layerList: React.PropTypes.element.isRequired,
|
||||
layerEditor: React.PropTypes.element,
|
||||
map: React.PropTypes.element.isRequired,
|
||||
toolbar: PropTypes.element.isRequired,
|
||||
layerList: PropTypes.element.isRequired,
|
||||
layerEditor: PropTypes.element,
|
||||
map: PropTypes.element.isRequired,
|
||||
bottom: PropTypes.element,
|
||||
modals: PropTypes.node,
|
||||
}
|
||||
|
||||
static childContextTypes = {
|
||||
reactIconBase: React.PropTypes.object
|
||||
reactIconBase: PropTypes.object
|
||||
}
|
||||
|
||||
getChildContext() {
|
||||
@@ -23,42 +23,22 @@ class AppLayout extends React.Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div style={{
|
||||
fontFamily: theme.fontFamily,
|
||||
color: theme.color,
|
||||
fontWeight: 300
|
||||
}}>
|
||||
return <div className="maputnik-layout">
|
||||
{this.props.toolbar}
|
||||
<div style={{
|
||||
position: 'fixed',
|
||||
bottom: 0,
|
||||
height: "100%",
|
||||
top: 40,
|
||||
left: 0,
|
||||
zIndex: 1,
|
||||
width: 200,
|
||||
overflow: "hidden",
|
||||
backgroundColor: colors.black
|
||||
}}>
|
||||
<ScrollContainer>
|
||||
{this.props.layerList}
|
||||
</ScrollContainer>
|
||||
<div className="maputnik-layout-list">
|
||||
{this.props.layerList}
|
||||
</div>
|
||||
<div style={{
|
||||
position: 'fixed',
|
||||
bottom: 0,
|
||||
height: "100%",
|
||||
top: 40,
|
||||
left: 200,
|
||||
zIndex: 1,
|
||||
width: 300,
|
||||
backgroundColor: colors.black
|
||||
}}>
|
||||
<div className="maputnik-layout-drawer">
|
||||
<ScrollContainer>
|
||||
{this.props.layerEditor}
|
||||
</ScrollContainer>
|
||||
</div>
|
||||
{this.props.map}
|
||||
{this.props.bottom && <div className="maputnik-layout-bottom">
|
||||
{this.props.bottom}
|
||||
</div>
|
||||
}
|
||||
{this.props.modals}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
62
src/components/AppMessagePanel.jsx
Normal file
62
src/components/AppMessagePanel.jsx
Normal file
@@ -0,0 +1,62 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import {formatLayerId} from '../util/format';
|
||||
|
||||
export default class AppMessagePanel extends React.Component {
|
||||
static propTypes = {
|
||||
errors: PropTypes.array,
|
||||
infos: PropTypes.array,
|
||||
mapStyle: PropTypes.object,
|
||||
onLayerSelect: PropTypes.func,
|
||||
currentLayer: PropTypes.object,
|
||||
selectedLayerIndex: PropTypes.number,
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
onLayerSelect: () => {},
|
||||
}
|
||||
|
||||
render() {
|
||||
const {selectedLayerIndex} = this.props;
|
||||
const errors = this.props.errors.map((error, idx) => {
|
||||
let content;
|
||||
if (error.parsed && error.parsed.type === "layer") {
|
||||
const {parsed} = error;
|
||||
const {mapStyle, currentLayer} = this.props;
|
||||
const layerId = mapStyle.layers[parsed.data.index].id;
|
||||
content = (
|
||||
<>
|
||||
Layer <span>{formatLayerId(layerId)}</span>: {parsed.data.message}
|
||||
{selectedLayerIndex !== parsed.data.index &&
|
||||
<>
|
||||
—
|
||||
<button
|
||||
className="maputnik-message-panel__switch-button"
|
||||
onClick={() => this.props.onLayerSelect(parsed.data.index)}
|
||||
>
|
||||
switch to layer
|
||||
</button>
|
||||
</>
|
||||
}
|
||||
</>
|
||||
);
|
||||
}
|
||||
else {
|
||||
content = error.message;
|
||||
}
|
||||
return <p key={"error-"+idx} className="maputnik-message-panel-error">
|
||||
{content}
|
||||
</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>
|
||||
}
|
||||
}
|
||||
|
||||
285
src/components/AppToolbar.jsx
Normal file
285
src/components/AppToolbar.jsx
Normal file
@@ -0,0 +1,285 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import classnames from 'classnames'
|
||||
import {detect} from 'detect-browser';
|
||||
|
||||
import {MdFileDownload, MdOpenInBrowser, MdSettings, MdLayers, MdHelpOutline, MdFindInPage, MdAssignmentTurnedIn} from 'react-icons/md'
|
||||
|
||||
|
||||
import logoImage from 'maputnik-design/logos/logo-color.svg'
|
||||
import pkgJson from '../../package.json'
|
||||
|
||||
|
||||
// This is required because of <https://stackoverflow.com/a/49846426>, there isn't another way to detect support that I'm aware of.
|
||||
const browser = detect();
|
||||
const colorAccessibilityFiltersEnabled = ['chrome', 'firefox'].indexOf(browser.name) > -1;
|
||||
|
||||
|
||||
class IconText extends React.Component {
|
||||
static propTypes = {
|
||||
children: PropTypes.node,
|
||||
}
|
||||
|
||||
render() {
|
||||
return <span className="maputnik-icon-text">{this.props.children}</span>
|
||||
}
|
||||
}
|
||||
|
||||
class ToolbarLink extends React.Component {
|
||||
static propTypes = {
|
||||
className: PropTypes.string,
|
||||
children: PropTypes.node,
|
||||
href: PropTypes.string,
|
||||
onToggleModal: PropTypes.func,
|
||||
}
|
||||
|
||||
render() {
|
||||
return <a
|
||||
className={classnames('maputnik-toolbar-link', this.props.className)}
|
||||
href={this.props.href}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
{this.props.children}
|
||||
</a>
|
||||
}
|
||||
}
|
||||
|
||||
class ToolbarLinkHighlighted extends React.Component {
|
||||
static propTypes = {
|
||||
className: PropTypes.string,
|
||||
children: PropTypes.node,
|
||||
href: PropTypes.string,
|
||||
onToggleModal: PropTypes.func
|
||||
}
|
||||
|
||||
render() {
|
||||
return <a
|
||||
className={classnames('maputnik-toolbar-link', "maputnik-toolbar-link--highlighted", this.props.className)}
|
||||
href={this.props.href}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
<span className="maputnik-toolbar-link-wrapper">
|
||||
{this.props.children}
|
||||
</span>
|
||||
</a>
|
||||
}
|
||||
}
|
||||
|
||||
class ToolbarSelect extends React.Component {
|
||||
static propTypes = {
|
||||
children: PropTypes.node,
|
||||
wdKey: PropTypes.string
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div
|
||||
className='maputnik-toolbar-select'
|
||||
data-wd-key={this.props.wdKey}
|
||||
>
|
||||
{this.props.children}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
class ToolbarAction extends React.Component {
|
||||
static propTypes = {
|
||||
children: PropTypes.node,
|
||||
onClick: PropTypes.func,
|
||||
wdKey: PropTypes.string
|
||||
}
|
||||
|
||||
render() {
|
||||
return <button
|
||||
className='maputnik-toolbar-action'
|
||||
data-wd-key={this.props.wdKey}
|
||||
onClick={this.props.onClick}
|
||||
>
|
||||
{this.props.children}
|
||||
</button>
|
||||
}
|
||||
}
|
||||
|
||||
export default class AppToolbar extends React.Component {
|
||||
static propTypes = {
|
||||
mapStyle: PropTypes.object.isRequired,
|
||||
inspectModeEnabled: PropTypes.bool.isRequired,
|
||||
onStyleChanged: PropTypes.func.isRequired,
|
||||
// A new style has been uploaded
|
||||
onStyleOpen: PropTypes.func.isRequired,
|
||||
// A dict of source id's and the available source layers
|
||||
sources: PropTypes.object.isRequired,
|
||||
children: PropTypes.node,
|
||||
onToggleModal: PropTypes.func,
|
||||
onSetMapState: PropTypes.func,
|
||||
mapState: PropTypes.string,
|
||||
renderer: PropTypes.string,
|
||||
}
|
||||
|
||||
state = {
|
||||
isOpen: {
|
||||
settings: false,
|
||||
sources: false,
|
||||
open: false,
|
||||
add: false,
|
||||
export: false,
|
||||
}
|
||||
}
|
||||
|
||||
handleSelection(val) {
|
||||
this.props.onSetMapState(val);
|
||||
}
|
||||
|
||||
onSkip = (target) => {
|
||||
if (target === "map") {
|
||||
document.querySelector(".mapboxgl-canvas").focus();
|
||||
}
|
||||
else {
|
||||
const el = document.querySelector("#skip-target-"+target);
|
||||
el.focus();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const views = [
|
||||
{
|
||||
id: "map",
|
||||
group: "general",
|
||||
title: "Map",
|
||||
},
|
||||
{
|
||||
id: "inspect",
|
||||
group: "general",
|
||||
title: "Inspect",
|
||||
disabled: this.props.renderer !== 'mbgljs',
|
||||
},
|
||||
{
|
||||
id: "filter-deuteranopia",
|
||||
group: "color-accessibility",
|
||||
title: "Deuteranopia filter",
|
||||
disabled: !colorAccessibilityFiltersEnabled,
|
||||
},
|
||||
{
|
||||
id: "filter-protanopia",
|
||||
group: "color-accessibility",
|
||||
title: "Protanopia filter",
|
||||
disabled: !colorAccessibilityFiltersEnabled,
|
||||
},
|
||||
{
|
||||
id: "filter-tritanopia",
|
||||
group: "color-accessibility",
|
||||
title: "Tritanopia filter",
|
||||
disabled: !colorAccessibilityFiltersEnabled,
|
||||
},
|
||||
{
|
||||
id: "filter-achromatopsia",
|
||||
group: "color-accessibility",
|
||||
title: "Achromatopsia filter",
|
||||
disabled: !colorAccessibilityFiltersEnabled,
|
||||
},
|
||||
];
|
||||
|
||||
const currentView = views.find((view) => {
|
||||
return view.id === this.props.mapState;
|
||||
});
|
||||
|
||||
return <nav className='maputnik-toolbar'>
|
||||
<div className="maputnik-toolbar__inner">
|
||||
<div
|
||||
className="maputnik-toolbar-logo-container"
|
||||
>
|
||||
{/* Keyboard accessible quick links */}
|
||||
<button
|
||||
data-wd-key="root:skip:layer-list"
|
||||
className="maputnik-toolbar-skip"
|
||||
onClick={e => this.onSkip("layer-list")}
|
||||
>
|
||||
Layers list
|
||||
</button>
|
||||
<button
|
||||
data-wd-key="root:skip:layer-editor"
|
||||
className="maputnik-toolbar-skip"
|
||||
onClick={e => this.onSkip("layer-editor")}
|
||||
>
|
||||
Layer editor
|
||||
</button>
|
||||
<button
|
||||
data-wd-key="root:skip:map-view"
|
||||
className="maputnik-toolbar-skip"
|
||||
onClick={e => this.onSkip("map")}
|
||||
>
|
||||
Map view
|
||||
</button>
|
||||
<a
|
||||
className="maputnik-toolbar-logo"
|
||||
target="blank"
|
||||
rel="noreferrer noopener"
|
||||
href="https://github.com/maputnik/editor"
|
||||
>
|
||||
<span dangerouslySetInnerHTML={{__html: logoImage}} />
|
||||
<h1>
|
||||
<span className="maputnik-toolbar-name">{pkgJson.name}</span>
|
||||
<span className="maputnik-toolbar-version">v{pkgJson.version}</span>
|
||||
</h1>
|
||||
</a>
|
||||
</div>
|
||||
<div className="maputnik-toolbar__actions" role="navigation" aria-label="Toolbar">
|
||||
<ToolbarAction wdKey="nav:open" onClick={this.props.onToggleModal.bind(this, 'open')}>
|
||||
<MdOpenInBrowser />
|
||||
<IconText>Open</IconText>
|
||||
</ToolbarAction>
|
||||
<ToolbarAction wdKey="nav:export" onClick={this.props.onToggleModal.bind(this, 'export')}>
|
||||
<MdFileDownload />
|
||||
<IconText>Export</IconText>
|
||||
</ToolbarAction>
|
||||
<ToolbarAction wdKey="nav:sources" onClick={this.props.onToggleModal.bind(this, 'sources')}>
|
||||
<MdLayers />
|
||||
<IconText>Data Sources</IconText>
|
||||
</ToolbarAction>
|
||||
<ToolbarAction wdKey="nav:settings" onClick={this.props.onToggleModal.bind(this, 'settings')}>
|
||||
<MdSettings />
|
||||
<IconText>Style Settings</IconText>
|
||||
</ToolbarAction>
|
||||
|
||||
<ToolbarSelect wdKey="nav:inspect">
|
||||
<MdFindInPage />
|
||||
<label>View
|
||||
<select
|
||||
className="maputnik-select"
|
||||
onChange={(e) => this.handleSelection(e.target.value)}
|
||||
value={currentView.id}
|
||||
>
|
||||
{views.filter(v => v.group === "general").map((item) => {
|
||||
return (
|
||||
<option key={item.id} value={item.id} disabled={item.disabled}>
|
||||
{item.title}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
<optgroup label="Color accessibility">
|
||||
{views.filter(v => v.group === "color-accessibility").map((item) => {
|
||||
return (
|
||||
<option key={item.id} value={item.id} disabled={item.disabled}>
|
||||
{item.title}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</optgroup>
|
||||
</select>
|
||||
</label>
|
||||
</ToolbarSelect>
|
||||
|
||||
<ToolbarLink href={"https://github.com/maputnik/editor/wiki"}>
|
||||
<MdHelpOutline />
|
||||
<IconText>Help</IconText>
|
||||
</ToolbarLink>
|
||||
<ToolbarLinkHighlighted href={"https://gregorywolanski.typeform.com/to/cPgaSY"}>
|
||||
<MdAssignmentTurnedIn />
|
||||
<IconText>Take the Maputnik Survey</IconText>
|
||||
</ToolbarLinkHighlighted>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
}
|
||||
}
|
||||
103
src/components/Block.jsx
Normal file
103
src/components/Block.jsx
Normal file
@@ -0,0 +1,103 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import classnames from 'classnames'
|
||||
import FieldDocLabel from './FieldDocLabel'
|
||||
import Doc from './Doc'
|
||||
|
||||
|
||||
/** Wrap a component with a label */
|
||||
export default class Block extends React.Component {
|
||||
static propTypes = {
|
||||
"data-wd-key": PropTypes.string,
|
||||
label: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.element,
|
||||
]),
|
||||
action: PropTypes.element,
|
||||
children: PropTypes.node.isRequired,
|
||||
style: PropTypes.object,
|
||||
onChange: PropTypes.func,
|
||||
fieldSpec: PropTypes.object,
|
||||
wideMode: PropTypes.bool,
|
||||
error: PropTypes.array,
|
||||
}
|
||||
|
||||
constructor (props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
showDoc: false,
|
||||
}
|
||||
}
|
||||
|
||||
onChange(e) {
|
||||
const value = e.target.value
|
||||
return this.props.onChange(value === "" ? undefined : value)
|
||||
}
|
||||
|
||||
onToggleDoc = (val) => {
|
||||
this.setState({
|
||||
showDoc: val
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Some fields for example <InputColor/> bind click events inside the element
|
||||
* to close the picker. This in turn propagates to the <label/> element
|
||||
* causing the picker to reopen. This causes a scenario where the picker can
|
||||
* never be closed once open.
|
||||
*/
|
||||
onLabelClick = (event) => {
|
||||
const el = event.nativeEvent.target;
|
||||
const nativeEvent = event.nativeEvent;
|
||||
const contains = this._blockEl.contains(el);
|
||||
|
||||
if (event.nativeEvent.target.nodeName !== "INPUT" && !contains) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
render() {
|
||||
const errors = [].concat(this.props.error || []);
|
||||
|
||||
return <label style={this.props.style}
|
||||
data-wd-key={this.props["data-wd-key"]}
|
||||
className={classnames({
|
||||
"maputnik-input-block": true,
|
||||
"maputnik-input-block--wide": this.props.wideMode,
|
||||
"maputnik-action-block": this.props.action
|
||||
})}
|
||||
onClick={this.onLabelClick}
|
||||
>
|
||||
{this.props.fieldSpec &&
|
||||
<div className="maputnik-input-block-label">
|
||||
<FieldDocLabel
|
||||
label={this.props.label}
|
||||
onToggleDoc={this.onToggleDoc}
|
||||
fieldSpec={this.props.fieldSpec}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
{!this.props.fieldSpec &&
|
||||
<div className="maputnik-input-block-label">
|
||||
{this.props.label}
|
||||
</div>
|
||||
}
|
||||
<div className="maputnik-input-block-action">
|
||||
{this.props.action}
|
||||
</div>
|
||||
<div className="maputnik-input-block-content" ref={el => this._blockEl = el}>
|
||||
{this.props.children}
|
||||
</div>
|
||||
{this.props.fieldSpec &&
|
||||
<div
|
||||
className="maputnik-doc-inline"
|
||||
style={{display: this.state.showDoc ? '' : 'none'}}
|
||||
>
|
||||
<Doc fieldSpec={this.props.fieldSpec} />
|
||||
</div>
|
||||
}
|
||||
</label>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
import React from 'react'
|
||||
import colors from '../config/colors'
|
||||
import { margins, fontSizes } from '../config/scales'
|
||||
|
||||
class Button extends React.Component {
|
||||
static propTypes = {
|
||||
onClick: React.PropTypes.func,
|
||||
style: React.PropTypes.object,
|
||||
}
|
||||
|
||||
render() {
|
||||
return <a
|
||||
onClick={this.props.onClick}
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
backgroundColor: colors.midgray,
|
||||
color: colors.lowgray,
|
||||
fontSize: fontSizes[4],
|
||||
padding: margins[1],
|
||||
userSelect: 'none',
|
||||
borderRadius: 2,
|
||||
...this.props.style,
|
||||
}}>
|
||||
{this.props.children}
|
||||
</a>
|
||||
}
|
||||
}
|
||||
|
||||
export default Button
|
||||
34
src/components/Collapse.jsx
Normal file
34
src/components/Collapse.jsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { Collapse as ReactCollapse } from 'react-collapse'
|
||||
import accessibility from '../../libs/accessibility'
|
||||
|
||||
|
||||
export default class Collapse extends React.Component {
|
||||
static propTypes = {
|
||||
isActive: PropTypes.bool.isRequired,
|
||||
children: PropTypes.element.isRequired
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
isActive: true
|
||||
}
|
||||
|
||||
render() {
|
||||
if (accessibility.reducedMotionEnabled()) {
|
||||
return (
|
||||
<div style={{display: this.props.isActive ? "block" : "none"}}>
|
||||
{this.props.children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
else {
|
||||
return (
|
||||
<ReactCollapse isOpened={this.props.isActive}>
|
||||
{this.props.children}
|
||||
</ReactCollapse>
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
20
src/components/Collapser.jsx
Normal file
20
src/components/Collapser.jsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import {MdArrowDropDown, MdArrowDropUp} from 'react-icons/md'
|
||||
|
||||
export default class Collapser extends React.Component {
|
||||
static propTypes = {
|
||||
isCollapsed: PropTypes.bool.isRequired,
|
||||
style: PropTypes.object,
|
||||
}
|
||||
|
||||
render() {
|
||||
const iconStyle = {
|
||||
width: 20,
|
||||
height: 20,
|
||||
...this.props.style,
|
||||
}
|
||||
return this.props.isCollapsed ? <MdArrowDropUp style={iconStyle}/> : <MdArrowDropDown style={iconStyle} />
|
||||
}
|
||||
}
|
||||
|
||||
83
src/components/Doc.jsx
Normal file
83
src/components/Doc.jsx
Normal file
@@ -0,0 +1,83 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
export default class Doc extends React.Component {
|
||||
static propTypes = {
|
||||
fieldSpec: PropTypes.object.isRequired,
|
||||
}
|
||||
|
||||
render () {
|
||||
const {fieldSpec} = this.props;
|
||||
|
||||
const {doc, values} = fieldSpec;
|
||||
const sdkSupport = fieldSpec['sdk-support'];
|
||||
|
||||
const headers = {
|
||||
js: "JS",
|
||||
android: "Android",
|
||||
ios: "iOS",
|
||||
macos: "macOS",
|
||||
};
|
||||
|
||||
const renderValues = (
|
||||
!!values &&
|
||||
// HACK: Currently we merge additional values into the stylespec, so this is required
|
||||
// See <https://github.com/maputnik/editor/blob/master/src/components/fields/PropertyGroup.jsx#L16>
|
||||
!Array.isArray(values)
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{doc &&
|
||||
<div className="SpecDoc">
|
||||
<div className="SpecDoc__doc">{doc}</div>
|
||||
{renderValues &&
|
||||
<ul className="SpecDoc__values">
|
||||
{Object.entries(values).map(([key, value]) => {
|
||||
return (
|
||||
<li key={key}>
|
||||
<code>{JSON.stringify(key)}</code>
|
||||
<div>{value.doc}</div>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
{sdkSupport &&
|
||||
<div className="SpecDoc__sdk-support">
|
||||
<table className="SpecDoc__sdk-support__table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
{Object.values(headers).map(header => {
|
||||
return <th key={header}>{header}</th>;
|
||||
})}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{Object.entries(sdkSupport).map(([key, supportObj]) => {
|
||||
return (
|
||||
<tr key={key}>
|
||||
<td>{key}</td>
|
||||
{Object.keys(headers).map(k => {
|
||||
const value = supportObj[k];
|
||||
if (supportObj.hasOwnProperty(k)) {
|
||||
return <td key={k}>{supportObj[k]}</td>;
|
||||
}
|
||||
else {
|
||||
return <td key={k}>no</td>;
|
||||
}
|
||||
})}
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
21
src/components/FieldArray.jsx
Normal file
21
src/components/FieldArray.jsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import Block from './Block'
|
||||
import InputArray from './InputArray'
|
||||
import Fieldset from './Fieldset'
|
||||
|
||||
export default class FieldArray extends React.Component {
|
||||
static propTypes = {
|
||||
...InputArray.propTypes,
|
||||
name: PropTypes.string,
|
||||
}
|
||||
|
||||
render() {
|
||||
const {props} = this;
|
||||
|
||||
return <Fieldset label={props.label}>
|
||||
<InputArray {...props} />
|
||||
</Fieldset>
|
||||
}
|
||||
}
|
||||
|
||||
20
src/components/FieldAutocomplete.jsx
Normal file
20
src/components/FieldAutocomplete.jsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import Block from './Block'
|
||||
import InputAutocomplete from './InputAutocomplete'
|
||||
|
||||
|
||||
export default class FieldAutocomplete extends React.Component {
|
||||
static propTypes = {
|
||||
...InputAutocomplete.propTypes,
|
||||
}
|
||||
|
||||
render() {
|
||||
const {props} = this;
|
||||
|
||||
return <Block label={props.label}>
|
||||
<InputAutocomplete {...props} />
|
||||
</Block>
|
||||
}
|
||||
}
|
||||
|
||||
20
src/components/FieldCheckbox.jsx
Normal file
20
src/components/FieldCheckbox.jsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import Block from './Block'
|
||||
import InputCheckbox from './InputCheckbox'
|
||||
|
||||
|
||||
export default class FieldCheckbox extends React.Component {
|
||||
static propTypes = {
|
||||
...InputCheckbox.propTypes,
|
||||
}
|
||||
|
||||
render() {
|
||||
const {props} = this;
|
||||
|
||||
return <Block label={this.props.label}>
|
||||
<InputCheckbox {...props} />
|
||||
</Block>
|
||||
}
|
||||
}
|
||||
|
||||
20
src/components/FieldColor.jsx
Normal file
20
src/components/FieldColor.jsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import Block from './Block'
|
||||
import InputColor from './InputColor'
|
||||
|
||||
|
||||
export default class FieldColor extends React.Component {
|
||||
static propTypes = {
|
||||
...InputColor.propTypes,
|
||||
}
|
||||
|
||||
render() {
|
||||
const {props} = this;
|
||||
|
||||
return <Block label={props.label}>
|
||||
<InputColor {...props} />
|
||||
</Block>
|
||||
}
|
||||
}
|
||||
|
||||
31
src/components/FieldComment.jsx
Normal file
31
src/components/FieldComment.jsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import Block from './Block'
|
||||
import InputString from './InputString'
|
||||
|
||||
export default class FieldComment extends React.Component {
|
||||
static propTypes = {
|
||||
value: PropTypes.string,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
render() {
|
||||
const fieldSpec = {
|
||||
doc: "Comments for the current layer. This is non-standard and not in the spec."
|
||||
};
|
||||
|
||||
return <Block
|
||||
label={"Comments"}
|
||||
fieldSpec={fieldSpec}
|
||||
data-wd-key="layer-comment"
|
||||
>
|
||||
<InputString
|
||||
multi={true}
|
||||
value={this.props.value}
|
||||
onChange={this.props.onChange}
|
||||
default="Comment..."
|
||||
/>
|
||||
</Block>
|
||||
}
|
||||
}
|
||||
63
src/components/FieldDocLabel.jsx
Normal file
63
src/components/FieldDocLabel.jsx
Normal file
@@ -0,0 +1,63 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import {MdInfoOutline, MdHighlightOff} from 'react-icons/md'
|
||||
|
||||
export default class FieldDocLabel extends React.Component {
|
||||
static propTypes = {
|
||||
label: PropTypes.oneOfType([
|
||||
PropTypes.object,
|
||||
PropTypes.string
|
||||
]).isRequired,
|
||||
fieldSpec: PropTypes.object,
|
||||
onToggleDoc: PropTypes.func,
|
||||
}
|
||||
|
||||
constructor (props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
open: false,
|
||||
}
|
||||
}
|
||||
|
||||
onToggleDoc = (open) => {
|
||||
this.setState({
|
||||
open,
|
||||
}, () => {
|
||||
if (this.props.onToggleDoc) {
|
||||
this.props.onToggleDoc(this.state.open);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const {label, fieldSpec} = this.props;
|
||||
const {doc} = fieldSpec || {};
|
||||
|
||||
if (doc) {
|
||||
return <label className="maputnik-doc-wrapper">
|
||||
<div className="maputnik-doc-target">
|
||||
{label}
|
||||
{'\xa0'}
|
||||
<button
|
||||
aria-label={this.state.open ? "close property documentation" : "open property documentation"}
|
||||
className={`maputnik-doc-button maputnik-doc-button--${this.state.open ? 'open' : 'closed'}`}
|
||||
onClick={() => this.onToggleDoc(!this.state.open)}
|
||||
>
|
||||
{this.state.open ? <MdHighlightOff /> : <MdInfoOutline />}
|
||||
</button>
|
||||
</div>
|
||||
</label>
|
||||
}
|
||||
else if (label) {
|
||||
return <label className="maputnik-doc-wrapper">
|
||||
<div className="maputnik-doc-target">
|
||||
{label}
|
||||
</div>
|
||||
</label>
|
||||
}
|
||||
else {
|
||||
<div />
|
||||
}
|
||||
}
|
||||
}
|
||||
21
src/components/FieldDynamicArray.jsx
Normal file
21
src/components/FieldDynamicArray.jsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import Block from './Block'
|
||||
import InputDynamicArray from './InputDynamicArray'
|
||||
import Fieldset from './Fieldset'
|
||||
|
||||
export default class FieldDynamicArray extends React.Component {
|
||||
static propTypes = {
|
||||
...InputDynamicArray.propTypes,
|
||||
name: PropTypes.string,
|
||||
}
|
||||
|
||||
render() {
|
||||
const {props} = this;
|
||||
|
||||
return <Fieldset label={props.label}>
|
||||
<InputDynamicArray {...props} />
|
||||
</Fieldset>
|
||||
}
|
||||
}
|
||||
|
||||
20
src/components/FieldEnum.jsx
Normal file
20
src/components/FieldEnum.jsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import InputEnum from './InputEnum'
|
||||
import Block from './Block';
|
||||
import Fieldset from './Fieldset';
|
||||
|
||||
|
||||
export default class FieldEnum extends React.Component {
|
||||
static propTypes = {
|
||||
...InputEnum.propTypes,
|
||||
}
|
||||
|
||||
render() {
|
||||
const {props} = this;
|
||||
|
||||
return <Fieldset label={props.label}>
|
||||
<InputEnum {...props} />
|
||||
</Fieldset>
|
||||
}
|
||||
}
|
||||
410
src/components/FieldFunction.jsx
Normal file
410
src/components/FieldFunction.jsx
Normal file
@@ -0,0 +1,410 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import SpecProperty from './_SpecProperty'
|
||||
import DataProperty from './_DataProperty'
|
||||
import ZoomProperty from './_ZoomProperty'
|
||||
import ExpressionProperty from './_ExpressionProperty'
|
||||
import {function as styleFunction} from '@maplibre/maplibre-gl-style-spec';
|
||||
import {findDefaultFromSpec} from '../util/spec-helper';
|
||||
|
||||
|
||||
function isLiteralExpression (value) {
|
||||
return (Array.isArray(value) && value.length === 2 && value[0] === "literal");
|
||||
}
|
||||
|
||||
function isGetExpression (value) {
|
||||
return (
|
||||
Array.isArray(value) &&
|
||||
value.length === 2 &&
|
||||
value[0] === "get"
|
||||
);
|
||||
}
|
||||
|
||||
function isZoomField(value) {
|
||||
return (
|
||||
typeof(value) === 'object' &&
|
||||
value.stops &&
|
||||
typeof(value.property) === 'undefined' &&
|
||||
Array.isArray(value.stops) &&
|
||||
value.stops.length > 1 &&
|
||||
value.stops.every(stop => {
|
||||
return (
|
||||
Array.isArray(stop) &&
|
||||
stop.length === 2
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function isIdentityProperty (value) {
|
||||
return (
|
||||
typeof(value) === 'object' &&
|
||||
value.type === "identity" &&
|
||||
value.hasOwnProperty("property")
|
||||
);
|
||||
}
|
||||
|
||||
function isDataStopProperty (value) {
|
||||
return (
|
||||
typeof(value) === 'object' &&
|
||||
value.stops &&
|
||||
typeof(value.property) !== 'undefined' &&
|
||||
value.stops.length > 1 &&
|
||||
Array.isArray(value.stops) &&
|
||||
value.stops.every(stop => {
|
||||
return (
|
||||
Array.isArray(stop) &&
|
||||
stop.length === 2 &&
|
||||
typeof(stop[0]) === 'object'
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function isDataField(value) {
|
||||
return (
|
||||
isIdentityProperty(value) ||
|
||||
isDataStopProperty(value)
|
||||
);
|
||||
}
|
||||
|
||||
function isPrimative (value) {
|
||||
const valid = ["string", "boolean", "number"];
|
||||
return valid.includes(typeof(value));
|
||||
}
|
||||
|
||||
function isArrayOfPrimatives (values) {
|
||||
if (Array.isArray(values)) {
|
||||
return values.every(isPrimative);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function getDataType (value, fieldSpec={}) {
|
||||
if (value === undefined) {
|
||||
return "value";
|
||||
}
|
||||
else if (isPrimative(value)) {
|
||||
return "value";
|
||||
}
|
||||
else if (fieldSpec.type === "array" && isArrayOfPrimatives(value)) {
|
||||
return "value";
|
||||
}
|
||||
else if (isZoomField(value)) {
|
||||
return "zoom_function";
|
||||
}
|
||||
else if (isDataField(value)) {
|
||||
return "data_function";
|
||||
}
|
||||
else {
|
||||
return "expression";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/** Supports displaying spec field for zoom function objects
|
||||
* https://www.mapbox.com/mapbox-gl-style-spec/#types-function-zoom-property
|
||||
*/
|
||||
export default class FieldFunction extends React.Component {
|
||||
static propTypes = {
|
||||
onChange: PropTypes.func.isRequired,
|
||||
fieldName: PropTypes.string.isRequired,
|
||||
fieldType: PropTypes.string.isRequired,
|
||||
fieldSpec: PropTypes.object.isRequired,
|
||||
errors: PropTypes.object,
|
||||
|
||||
value: PropTypes.oneOfType([
|
||||
PropTypes.object,
|
||||
PropTypes.string,
|
||||
PropTypes.number,
|
||||
PropTypes.bool,
|
||||
PropTypes.array
|
||||
]),
|
||||
}
|
||||
|
||||
constructor (props) {
|
||||
super();
|
||||
this.state = {
|
||||
dataType: getDataType(props.value, props.fieldSpec),
|
||||
isEditing: false,
|
||||
}
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps(props, state) {
|
||||
// Because otherwise when editing values we end up accidentally changing field type.
|
||||
if (state.isEditing) {
|
||||
return {};
|
||||
}
|
||||
else {
|
||||
return {
|
||||
isEditing: false,
|
||||
dataType: getDataType(props.value, props.fieldSpec)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
getFieldFunctionType(fieldSpec) {
|
||||
if (fieldSpec.expression.interpolated) {
|
||||
return "exponential"
|
||||
}
|
||||
if (fieldSpec.type === "number") {
|
||||
return "interval"
|
||||
}
|
||||
return "categorical"
|
||||
}
|
||||
|
||||
addStop = () => {
|
||||
const stops = this.props.value.stops.slice(0)
|
||||
const lastStop = stops[stops.length - 1]
|
||||
if (typeof lastStop[0] === "object") {
|
||||
stops.push([
|
||||
{zoom: lastStop[0].zoom + 1, value: lastStop[0].value},
|
||||
lastStop[1]
|
||||
])
|
||||
}
|
||||
else {
|
||||
stops.push([lastStop[0] + 1, lastStop[1]])
|
||||
}
|
||||
|
||||
const changedValue = {
|
||||
...this.props.value,
|
||||
stops: stops,
|
||||
}
|
||||
|
||||
this.props.onChange(this.props.fieldName, changedValue)
|
||||
}
|
||||
|
||||
deleteExpression = () => {
|
||||
const {fieldSpec, fieldName} = this.props;
|
||||
this.props.onChange(fieldName, fieldSpec.default);
|
||||
this.setState({
|
||||
dataType: "value",
|
||||
});
|
||||
}
|
||||
|
||||
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 {value} = this.props;
|
||||
|
||||
let zoomFunc;
|
||||
if (typeof(value) === "object") {
|
||||
if (value.stops) {
|
||||
zoomFunc = {
|
||||
base: value.base,
|
||||
stops: value.stops.map(stop => {
|
||||
return [stop[0].zoom, stop[1] || findDefaultFromSpec(this.props.fieldSpec)];
|
||||
})
|
||||
}
|
||||
}
|
||||
else {
|
||||
zoomFunc = {
|
||||
base: value.base,
|
||||
stops: [
|
||||
[6, findDefaultFromSpec(this.props.fieldSpec)],
|
||||
[10, findDefaultFromSpec(this.props.fieldSpec)]
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
zoomFunc = {
|
||||
stops: [
|
||||
[6, value || findDefaultFromSpec(this.props.fieldSpec)],
|
||||
[10, value || findDefaultFromSpec(this.props.fieldSpec)]
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
this.props.onChange(this.props.fieldName, zoomFunc)
|
||||
}
|
||||
|
||||
undoExpression = () => {
|
||||
const {value, fieldName} = this.props;
|
||||
|
||||
if (isGetExpression(value)) {
|
||||
this.props.onChange(fieldName, {
|
||||
"type": "identity",
|
||||
"property": value[1]
|
||||
});
|
||||
this.setState({
|
||||
dataType: "value",
|
||||
});
|
||||
}
|
||||
else if (isLiteralExpression(value)) {
|
||||
this.props.onChange(fieldName, value[1]);
|
||||
this.setState({
|
||||
dataType: "value",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
canUndo = () => {
|
||||
const {value, fieldSpec} = this.props;
|
||||
return (
|
||||
isGetExpression(value) ||
|
||||
isLiteralExpression(value) ||
|
||||
isPrimative(value) ||
|
||||
(Array.isArray(value) && fieldSpec.type === "array")
|
||||
);
|
||||
}
|
||||
|
||||
makeExpression = () => {
|
||||
const {value, fieldSpec} = this.props;
|
||||
let expression;
|
||||
|
||||
if (typeof(value) === "object" && 'stops' in value) {
|
||||
expression = styleFunction.convertFunction(value, fieldSpec);
|
||||
}
|
||||
else if (isIdentityProperty(value)) {
|
||||
expression = ["get", value.property];
|
||||
}
|
||||
else {
|
||||
expression = ["literal", value || this.props.fieldSpec.default];
|
||||
}
|
||||
this.props.onChange(this.props.fieldName, expression);
|
||||
}
|
||||
|
||||
makeDataFunction = () => {
|
||||
const functionType = this.getFieldFunctionType(this.props.fieldSpec);
|
||||
const stopValue = functionType === 'categorical' ? '' : 0;
|
||||
const {value} = this.props;
|
||||
let dataFunc;
|
||||
|
||||
if (typeof(value) === "object") {
|
||||
if (value.stops) {
|
||||
dataFunc = {
|
||||
property: "",
|
||||
type: functionType,
|
||||
base: value.base,
|
||||
stops: value.stops.map(stop => {
|
||||
return [{zoom: stop[0], value: stopValue}, stop[1] || findDefaultFromSpec(this.props.fieldSpec)];
|
||||
})
|
||||
}
|
||||
}
|
||||
else {
|
||||
dataFunc = {
|
||||
property: "",
|
||||
type: functionType,
|
||||
base: value.base,
|
||||
stops: [
|
||||
[{zoom: 6, value: stopValue}, findDefaultFromSpec(this.props.fieldSpec)],
|
||||
[{zoom: 10, value: stopValue}, findDefaultFromSpec(this.props.fieldSpec)]
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
dataFunc = {
|
||||
property: "",
|
||||
type: functionType,
|
||||
base: value.base,
|
||||
stops: [
|
||||
[{zoom: 6, value: stopValue}, this.props.value || findDefaultFromSpec(this.props.fieldSpec)],
|
||||
[{zoom: 10, value: stopValue}, this.props.value || findDefaultFromSpec(this.props.fieldSpec)]
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
this.props.onChange(this.props.fieldName, dataFunc)
|
||||
}
|
||||
|
||||
onMarkEditing = () => {
|
||||
this.setState({isEditing: true});
|
||||
}
|
||||
|
||||
onUnmarkEditing = () => {
|
||||
this.setState({isEditing: false});
|
||||
}
|
||||
|
||||
render() {
|
||||
const {dataType} = this.state;
|
||||
const propClass = this.props.fieldSpec.default === this.props.value ? "maputnik-default-property" : "maputnik-modified-property"
|
||||
let specField;
|
||||
|
||||
if (dataType === "expression") {
|
||||
specField = (
|
||||
<ExpressionProperty
|
||||
errors={this.props.errors}
|
||||
onChange={this.props.onChange.bind(this, this.props.fieldName)}
|
||||
canUndo={this.canUndo}
|
||||
onUndo={this.undoExpression}
|
||||
onDelete={this.deleteExpression}
|
||||
fieldType={this.props.fieldType}
|
||||
fieldName={this.props.fieldName}
|
||||
fieldSpec={this.props.fieldSpec}
|
||||
value={this.props.value}
|
||||
onFocus={this.onMarkEditing}
|
||||
onBlur={this.onUnmarkEditing}
|
||||
/>
|
||||
);
|
||||
}
|
||||
else if (dataType === "zoom_function") {
|
||||
specField = (
|
||||
<ZoomProperty
|
||||
errors={this.props.errors}
|
||||
onChange={this.props.onChange.bind(this)}
|
||||
fieldType={this.props.fieldType}
|
||||
fieldName={this.props.fieldName}
|
||||
fieldSpec={this.props.fieldSpec}
|
||||
value={this.props.value}
|
||||
onDeleteStop={this.deleteStop}
|
||||
onAddStop={this.addStop}
|
||||
onChangeToDataFunction={this.makeDataFunction}
|
||||
onExpressionClick={this.makeExpression}
|
||||
/>
|
||||
)
|
||||
}
|
||||
else if (dataType === "data_function") {
|
||||
// TODO: Rename to FieldFunction **this file** shouldn't be called that
|
||||
specField = (
|
||||
<DataProperty
|
||||
errors={this.props.errors}
|
||||
onChange={this.props.onChange.bind(this)}
|
||||
fieldType={this.props.fieldType}
|
||||
fieldName={this.props.fieldName}
|
||||
fieldSpec={this.props.fieldSpec}
|
||||
value={this.props.value}
|
||||
onDeleteStop={this.deleteStop}
|
||||
onAddStop={this.addStop}
|
||||
onChangeToZoomFunction={this.makeZoomFunction}
|
||||
onExpressionClick={this.makeExpression}
|
||||
/>
|
||||
)
|
||||
}
|
||||
else {
|
||||
specField = (
|
||||
<SpecProperty
|
||||
errors={this.props.errors}
|
||||
onChange={this.props.onChange.bind(this)}
|
||||
fieldType={this.props.fieldType}
|
||||
fieldName={this.props.fieldName}
|
||||
fieldSpec={this.props.fieldSpec}
|
||||
value={this.props.value}
|
||||
onZoomClick={this.makeZoomFunction}
|
||||
onDataClick={this.makeDataFunction}
|
||||
onExpressionClick={this.makeExpression}
|
||||
/>
|
||||
)
|
||||
}
|
||||
return <div className={propClass} data-wd-key={"spec-field:"+this.props.fieldName}>
|
||||
{specField}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
27
src/components/FieldId.jsx
Normal file
27
src/components/FieldId.jsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import {latest} from '@maplibre/maplibre-gl-style-spec'
|
||||
import Block from './Block'
|
||||
import InputString from './InputString'
|
||||
|
||||
export default class FieldId extends React.Component {
|
||||
static propTypes = {
|
||||
value: PropTypes.string.isRequired,
|
||||
wdKey: PropTypes.string.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
error: PropTypes.object,
|
||||
}
|
||||
|
||||
render() {
|
||||
return <Block label={"ID"} fieldSpec={latest.layer.id}
|
||||
data-wd-key={this.props.wdKey}
|
||||
error={this.props.error}
|
||||
>
|
||||
<InputString
|
||||
value={this.props.value}
|
||||
onInput={this.props.onChange}
|
||||
/>
|
||||
</Block>
|
||||
}
|
||||
}
|
||||
16
src/components/FieldJson.jsx
Normal file
16
src/components/FieldJson.jsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import InputJson from './InputJson'
|
||||
|
||||
|
||||
export default class FieldJson extends React.Component {
|
||||
static propTypes = {
|
||||
...InputJson.propTypes,
|
||||
}
|
||||
|
||||
render() {
|
||||
const {props} = this;
|
||||
return <InputJson {...props} />
|
||||
}
|
||||
}
|
||||
|
||||
30
src/components/FieldMaxZoom.jsx
Normal file
30
src/components/FieldMaxZoom.jsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import {latest} from '@maplibre/maplibre-gl-style-spec'
|
||||
import Block from './Block'
|
||||
import InputNumber from './InputNumber'
|
||||
|
||||
export default class FieldMaxZoom extends React.Component {
|
||||
static propTypes = {
|
||||
value: PropTypes.number,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
error: PropTypes.object,
|
||||
}
|
||||
|
||||
render() {
|
||||
return <Block label={"Max Zoom"} fieldSpec={latest.layer.maxzoom}
|
||||
error={this.props.error}
|
||||
data-wd-key="max-zoom"
|
||||
>
|
||||
<InputNumber
|
||||
allowRange={true}
|
||||
value={this.props.value}
|
||||
onChange={this.props.onChange}
|
||||
min={latest.layer.maxzoom.minimum}
|
||||
max={latest.layer.maxzoom.maximum}
|
||||
default={latest.layer.maxzoom.maximum}
|
||||
/>
|
||||
</Block>
|
||||
}
|
||||
}
|
||||
30
src/components/FieldMinZoom.jsx
Normal file
30
src/components/FieldMinZoom.jsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import {latest} from '@maplibre/maplibre-gl-style-spec'
|
||||
import Block from './Block'
|
||||
import InputNumber from './InputNumber'
|
||||
|
||||
export default class FieldMinZoom extends React.Component {
|
||||
static propTypes = {
|
||||
value: PropTypes.number,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
error: PropTypes.object,
|
||||
}
|
||||
|
||||
render() {
|
||||
return <Block label={"Min Zoom"} fieldSpec={latest.layer.minzoom}
|
||||
error={this.props.error}
|
||||
data-wd-key="min-zoom"
|
||||
>
|
||||
<InputNumber
|
||||
allowRange={true}
|
||||
value={this.props.value}
|
||||
onChange={this.props.onChange}
|
||||
min={latest.layer.minzoom.minimum}
|
||||
max={latest.layer.minzoom.maximum}
|
||||
default={latest.layer.minzoom.minimum}
|
||||
/>
|
||||
</Block>
|
||||
}
|
||||
}
|
||||
21
src/components/FieldMultiInput.jsx
Normal file
21
src/components/FieldMultiInput.jsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import Block from './Block'
|
||||
import InputMultiInput from './InputMultiInput'
|
||||
import Fieldset from './Fieldset'
|
||||
|
||||
|
||||
export default class FieldMultiInput extends React.Component {
|
||||
static propTypes = {
|
||||
...InputMultiInput.propTypes,
|
||||
}
|
||||
|
||||
render() {
|
||||
const {props} = this;
|
||||
|
||||
return <Fieldset label={props.label}>
|
||||
<InputMultiInput {...props} />
|
||||
</Fieldset>
|
||||
}
|
||||
}
|
||||
|
||||
19
src/components/FieldNumber.jsx
Normal file
19
src/components/FieldNumber.jsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import InputNumber from './InputNumber'
|
||||
import Block from './Block'
|
||||
|
||||
|
||||
export default class FieldNumber extends React.Component {
|
||||
static propTypes = {
|
||||
...InputNumber.propTypes,
|
||||
}
|
||||
|
||||
render() {
|
||||
const {props} = this;
|
||||
return <Block label={props.label}>
|
||||
<InputNumber {...props} />
|
||||
</Block>
|
||||
}
|
||||
}
|
||||
|
||||
20
src/components/FieldSelect.jsx
Normal file
20
src/components/FieldSelect.jsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import Block from './Block'
|
||||
import InputSelect from './InputSelect'
|
||||
|
||||
|
||||
export default class FieldSelect extends React.Component {
|
||||
static propTypes = {
|
||||
...InputSelect.propTypes,
|
||||
}
|
||||
|
||||
render() {
|
||||
const {props} = this;
|
||||
|
||||
return <Block label={props.label}>
|
||||
<InputSelect {...props}/>
|
||||
</Block>
|
||||
}
|
||||
}
|
||||
|
||||
36
src/components/FieldSource.jsx
Normal file
36
src/components/FieldSource.jsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import {latest} from '@maplibre/maplibre-gl-style-spec'
|
||||
import Block from './Block'
|
||||
import InputAutocomplete from './InputAutocomplete'
|
||||
|
||||
export default class FieldSource extends React.Component {
|
||||
static propTypes = {
|
||||
value: PropTypes.string,
|
||||
wdKey: PropTypes.string,
|
||||
onChange: PropTypes.func,
|
||||
sourceIds: PropTypes.array,
|
||||
error: PropTypes.object,
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
onChange: () => {},
|
||||
sourceIds: [],
|
||||
}
|
||||
|
||||
render() {
|
||||
return <Block
|
||||
label={"Source"}
|
||||
fieldSpec={latest.layer.source}
|
||||
error={this.props.error}
|
||||
data-wd-key={this.props.wdKey}
|
||||
>
|
||||
<InputAutocomplete
|
||||
value={this.props.value}
|
||||
onChange={this.props.onChange}
|
||||
options={this.props.sourceIds.map(src => [src, src])}
|
||||
/>
|
||||
</Block>
|
||||
}
|
||||
}
|
||||
34
src/components/FieldSourceLayer.jsx
Normal file
34
src/components/FieldSourceLayer.jsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import {latest} from '@maplibre/maplibre-gl-style-spec'
|
||||
import Block from './Block'
|
||||
import InputAutocomplete from './InputAutocomplete'
|
||||
|
||||
export default class FieldSourceLayer 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 <Block label={"Source Layer"} fieldSpec={latest.layer['source-layer']}
|
||||
data-wd-key="layer-source-layer"
|
||||
>
|
||||
<InputAutocomplete
|
||||
keepMenuWithinWindowBounds={!!this.props.isFixed}
|
||||
value={this.props.value}
|
||||
onChange={this.props.onChange}
|
||||
options={this.props.sourceLayerIds.map(l => [l, l])}
|
||||
/>
|
||||
</Block>
|
||||
}
|
||||
}
|
||||
20
src/components/FieldString.jsx
Normal file
20
src/components/FieldString.jsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import Block from './Block'
|
||||
import InputString from './InputString'
|
||||
|
||||
export default class FieldString extends React.Component {
|
||||
static propTypes = {
|
||||
...InputString.propTypes,
|
||||
name: PropTypes.string,
|
||||
}
|
||||
|
||||
render() {
|
||||
const {props} = this;
|
||||
|
||||
return <Block label={props.label}>
|
||||
<InputString {...props} />
|
||||
</Block>
|
||||
}
|
||||
}
|
||||
|
||||
52
src/components/FieldType.jsx
Normal file
52
src/components/FieldType.jsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import {latest} from '@maplibre/maplibre-gl-style-spec'
|
||||
import Block from './Block'
|
||||
import InputSelect from './InputSelect'
|
||||
import InputString from './InputString'
|
||||
|
||||
export default class FieldType extends React.Component {
|
||||
static propTypes = {
|
||||
value: PropTypes.string.isRequired,
|
||||
wdKey: PropTypes.string,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
error: PropTypes.object,
|
||||
disabled: PropTypes.bool,
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
disabled: false,
|
||||
}
|
||||
|
||||
render() {
|
||||
return <Block label={"Type"} fieldSpec={latest.layer.type}
|
||||
data-wd-key={this.props.wdKey}
|
||||
error={this.props.error}
|
||||
>
|
||||
{this.props.disabled &&
|
||||
<InputString
|
||||
value={this.props.value}
|
||||
disabled={true}
|
||||
/>
|
||||
}
|
||||
{!this.props.disabled &&
|
||||
<InputSelect
|
||||
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}
|
||||
/>
|
||||
}
|
||||
</Block>
|
||||
}
|
||||
}
|
||||
22
src/components/FieldUrl.jsx
Normal file
22
src/components/FieldUrl.jsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import React, {Fragment} from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import InputUrl from './InputUrl'
|
||||
import Block from './Block'
|
||||
|
||||
|
||||
export default class FieldUrl extends React.Component {
|
||||
static propTypes = {
|
||||
...InputUrl.propTypes,
|
||||
}
|
||||
|
||||
render () {
|
||||
const {props} = this;
|
||||
|
||||
return (
|
||||
<Block label={this.props.label}>
|
||||
<InputUrl {...props} />
|
||||
</Block>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
58
src/components/Fieldset.jsx
Normal file
58
src/components/Fieldset.jsx
Normal file
@@ -0,0 +1,58 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import FieldDocLabel from './FieldDocLabel'
|
||||
import Doc from './Doc'
|
||||
|
||||
|
||||
let IDX = 0;
|
||||
|
||||
export default class Fieldset extends React.Component {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
this._labelId = `fieldset_label_${(IDX++)}`;
|
||||
this.state = {
|
||||
showDoc: false,
|
||||
}
|
||||
}
|
||||
|
||||
onToggleDoc = (val) => {
|
||||
this.setState({
|
||||
showDoc: val
|
||||
});
|
||||
}
|
||||
|
||||
render () {
|
||||
const {props} = this;
|
||||
|
||||
return <div className="maputnik-input-block" role="group" aria-labelledby={this._labelId}>
|
||||
{this.props.fieldSpec &&
|
||||
<div className="maputnik-input-block-label">
|
||||
<FieldDocLabel
|
||||
label={this.props.label}
|
||||
onToggleDoc={this.onToggleDoc}
|
||||
fieldSpec={this.props.fieldSpec}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
{!this.props.fieldSpec &&
|
||||
<div className="maputnik-input-block-label">
|
||||
{props.label}
|
||||
</div>
|
||||
}
|
||||
<div className="maputnik-input-block-action">
|
||||
{this.props.action}
|
||||
</div>
|
||||
<div className="maputnik-input-block-content">
|
||||
{props.children}
|
||||
</div>
|
||||
{this.props.fieldSpec &&
|
||||
<div
|
||||
className="maputnik-doc-inline"
|
||||
style={{display: this.state.showDoc ? '' : 'none'}}
|
||||
>
|
||||
<Doc fieldSpec={this.props.fieldSpec} />
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
310
src/components/FilterEditor.jsx
Normal file
310
src/components/FilterEditor.jsx
Normal file
@@ -0,0 +1,310 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { combiningFilterOps } from '../libs/filterops.js'
|
||||
import {mdiTableRowPlusAfter} from '@mdi/js';
|
||||
import {isEqual} from 'lodash';
|
||||
|
||||
import {latest, migrate, convertFilter} from '@maplibre/maplibre-gl-style-spec'
|
||||
import InputSelect from './InputSelect'
|
||||
import Block from './Block'
|
||||
import SingleFilterEditor from './SingleFilterEditor'
|
||||
import FilterEditorBlock from './FilterEditorBlock'
|
||||
import InputButton from './InputButton'
|
||||
import Doc from './Doc'
|
||||
import ExpressionProperty from './_ExpressionProperty';
|
||||
import {mdiFunctionVariant} from '@mdi/js';
|
||||
|
||||
|
||||
function combiningFilter (props) {
|
||||
let filter = props.filter || ['all'];
|
||||
|
||||
if (!Array.isArray(filter)) {
|
||||
return filter;
|
||||
}
|
||||
|
||||
let combiningOp = filter[0];
|
||||
let filters = filter.slice(1);
|
||||
|
||||
if(combiningFilterOps.indexOf(combiningOp) < 0) {
|
||||
combiningOp = 'all';
|
||||
filters = [filter.slice(0)];
|
||||
}
|
||||
|
||||
return [combiningOp, ...filters];
|
||||
}
|
||||
|
||||
function migrateFilter (filter) {
|
||||
return migrate(createStyleFromFilter(filter)).layers[0].filter;
|
||||
}
|
||||
|
||||
function createStyleFromFilter (filter) {
|
||||
return {
|
||||
"id": "tmp",
|
||||
"version": 8,
|
||||
"name": "Empty Style",
|
||||
"metadata": {"maputnik:renderer": "mbgljs"},
|
||||
"sources": {
|
||||
"tmp": {
|
||||
"type": "geojson",
|
||||
"data": {}
|
||||
}
|
||||
},
|
||||
"sprite": "",
|
||||
"glyphs": "https://orangemug.github.io/font-glyphs/glyphs/{fontstack}/{range}.pbf",
|
||||
"layers": [
|
||||
{
|
||||
id: "tmp",
|
||||
type: "fill",
|
||||
source: "tmp",
|
||||
filter: filter,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
const FILTER_OPS = [
|
||||
"all",
|
||||
"any",
|
||||
"none"
|
||||
];
|
||||
|
||||
// If we convert a filter that is an expression to an expression it'll remain the same in value
|
||||
function checkIfSimpleFilter (filter) {
|
||||
if (filter.length === 1 && FILTER_OPS.includes(filter[0])) {
|
||||
return true;
|
||||
}
|
||||
const expression = convertFilter(filter);
|
||||
return !isEqual(expression, filter);
|
||||
}
|
||||
|
||||
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 FilterEditor extends React.Component {
|
||||
static propTypes = {
|
||||
/** Properties of the vector layer and the available fields */
|
||||
properties: PropTypes.object,
|
||||
filter: PropTypes.array,
|
||||
errors: PropTypes.object,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
filter: ["all"],
|
||||
}
|
||||
|
||||
constructor (props) {
|
||||
super();
|
||||
this.state = {
|
||||
showDoc: false,
|
||||
displaySimpleFilter: checkIfSimpleFilter(combiningFilter(props)),
|
||||
};
|
||||
}
|
||||
|
||||
// Convert filter to combining filter
|
||||
onFilterPartChanged(filterIdx, newPart) {
|
||||
const newFilter = combiningFilter(this.props).slice(0)
|
||||
newFilter[filterIdx] = newPart
|
||||
this.props.onChange(newFilter)
|
||||
}
|
||||
|
||||
deleteFilterItem(filterIdx) {
|
||||
const newFilter = combiningFilter(this.props).slice(0)
|
||||
newFilter.splice(filterIdx + 1, 1)
|
||||
this.props.onChange(newFilter)
|
||||
}
|
||||
|
||||
addFilterItem = () => {
|
||||
const newFilterItem = combiningFilter(this.props).slice(0)
|
||||
newFilterItem.push(['==', 'name', ''])
|
||||
this.props.onChange(newFilterItem)
|
||||
}
|
||||
|
||||
onToggleDoc = (val) => {
|
||||
this.setState({
|
||||
showDoc: val
|
||||
});
|
||||
}
|
||||
|
||||
makeFilter = () => {
|
||||
this.setState({
|
||||
displaySimpleFilter: true,
|
||||
})
|
||||
}
|
||||
|
||||
makeExpression = () => {
|
||||
let filter = combiningFilter(this.props);
|
||||
this.props.onChange(migrateFilter(filter));
|
||||
this.setState({
|
||||
displaySimpleFilter: false,
|
||||
})
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps (props, currentState) {
|
||||
const {filter} = props;
|
||||
const displaySimpleFilter = checkIfSimpleFilter(combiningFilter(props));
|
||||
|
||||
// Upgrade but never downgrade
|
||||
if (!displaySimpleFilter && currentState.displaySimpleFilter === true) {
|
||||
return {
|
||||
displaySimpleFilter: false,
|
||||
valueIsSimpleFilter: false,
|
||||
};
|
||||
}
|
||||
else if (displaySimpleFilter && currentState.displaySimpleFilter === false) {
|
||||
return {
|
||||
valueIsSimpleFilter: true,
|
||||
}
|
||||
}
|
||||
else {
|
||||
return {
|
||||
valueIsSimpleFilter: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {errors} = this.props;
|
||||
const {displaySimpleFilter} = this.state;
|
||||
const fieldSpec={
|
||||
doc: latest.layer.filter.doc + " Combine multiple filters together by using a compound filter."
|
||||
};
|
||||
const defaultFilter = ["all"];
|
||||
|
||||
const isNestedCombiningFilter = displaySimpleFilter && hasNestedCombiningFilter(combiningFilter(this.props));
|
||||
|
||||
if (isNestedCombiningFilter) {
|
||||
return <div className="maputnik-filter-editor-unsupported">
|
||||
<p>
|
||||
Nested filters are not supported.
|
||||
</p>
|
||||
<InputButton
|
||||
onClick={this.makeExpression}
|
||||
title="Convert to expression"
|
||||
>
|
||||
<svg style={{marginRight: "0.2em", width:"14px", height:"14px", verticalAlign: "middle"}} viewBox="0 0 24 24">
|
||||
<path fill="currentColor" d={mdiFunctionVariant} />
|
||||
</svg>
|
||||
Upgrade to expression
|
||||
</InputButton>
|
||||
</div>
|
||||
}
|
||||
else if (displaySimpleFilter) {
|
||||
const filter = combiningFilter(this.props);
|
||||
let combiningOp = filter[0];
|
||||
let filters = filter.slice(1)
|
||||
|
||||
const actions = (
|
||||
<div>
|
||||
<InputButton
|
||||
onClick={this.makeExpression}
|
||||
title="Convert to expression"
|
||||
className="maputnik-make-zoom-function"
|
||||
>
|
||||
<svg style={{width:"14px", height:"14px", verticalAlign: "middle"}} viewBox="0 0 24 24">
|
||||
<path fill="currentColor" d={mdiFunctionVariant} />
|
||||
</svg>
|
||||
</InputButton>
|
||||
</div>
|
||||
);
|
||||
|
||||
const editorBlocks = filters.map((f, idx) => {
|
||||
const error = errors[`filter[${idx+1}]`];
|
||||
|
||||
return (
|
||||
<div key={`block-${idx}`}>
|
||||
<FilterEditorBlock key={idx} onDelete={this.deleteFilterItem.bind(this, idx)}>
|
||||
<SingleFilterEditor
|
||||
properties={this.props.properties}
|
||||
filter={f}
|
||||
onChange={this.onFilterPartChanged.bind(this, idx + 1)}
|
||||
/>
|
||||
</FilterEditorBlock>
|
||||
{error &&
|
||||
<div key="error" className="maputnik-inline-error">{error.message}</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
})
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<Block
|
||||
key="top"
|
||||
fieldSpec={fieldSpec}
|
||||
label={"Filter"}
|
||||
action={actions}
|
||||
>
|
||||
<InputSelect
|
||||
value={combiningOp}
|
||||
onChange={this.onFilterPartChanged.bind(this, 0)}
|
||||
options={[["all", "every filter matches"], ["none", "no filter matches"], ["any", "any filter matches"]]}
|
||||
/>
|
||||
</Block>
|
||||
{editorBlocks}
|
||||
<div
|
||||
key="buttons"
|
||||
className="maputnik-filter-editor-add-wrapper"
|
||||
>
|
||||
<InputButton
|
||||
data-wd-key="layer-filter-button"
|
||||
className="maputnik-add-filter"
|
||||
onClick={this.addFilterItem}
|
||||
>
|
||||
<svg style={{width:"14px", height:"14px", verticalAlign: "text-bottom"}} viewBox="0 0 24 24">
|
||||
<path fill="currentColor" d={mdiTableRowPlusAfter} />
|
||||
</svg> Add filter
|
||||
</InputButton>
|
||||
</div>
|
||||
<div
|
||||
key="doc"
|
||||
className="maputnik-doc-inline"
|
||||
style={{display: this.state.showDoc ? '' : 'none'}}
|
||||
>
|
||||
<Doc fieldSpec={fieldSpec} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
else {
|
||||
let {filter} = this.props;
|
||||
|
||||
return (
|
||||
<>
|
||||
<ExpressionProperty
|
||||
onDelete={() => {
|
||||
this.setState({displaySimpleFilter: true});
|
||||
this.props.onChange(defaultFilter);
|
||||
}}
|
||||
fieldName="filter"
|
||||
fieldSpec={fieldSpec}
|
||||
value={filter}
|
||||
errors={errors}
|
||||
onChange={this.props.onChange}
|
||||
/>
|
||||
{this.state.valueIsSimpleFilter &&
|
||||
<div className="maputnik-expr-infobox">
|
||||
You've entered a old style filter,{' '}
|
||||
<button
|
||||
onClick={this.makeFilter}
|
||||
className="maputnik-expr-infobox__button"
|
||||
>
|
||||
switch to filter editor
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
29
src/components/FilterEditorBlock.jsx
Normal file
29
src/components/FilterEditorBlock.jsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import InputButton from './InputButton'
|
||||
import {MdDelete} from 'react-icons/md'
|
||||
|
||||
export default 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">
|
||||
<InputButton
|
||||
className="maputnik-delete-filter"
|
||||
onClick={this.props.onDelete}
|
||||
title="Delete filter block"
|
||||
>
|
||||
<MdDelete />
|
||||
</InputButton>
|
||||
</div>
|
||||
<div className="maputnik-filter-editor-block-content">
|
||||
{this.props.children}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
import React from 'react'
|
||||
import { fontSizes, margins } from '../config/scales'
|
||||
|
||||
class Heading extends React.Component {
|
||||
static propTypes = {
|
||||
level: React.PropTypes.number.isRequired,
|
||||
style: React.PropTypes.object,
|
||||
}
|
||||
|
||||
render() {
|
||||
const headingProps = {
|
||||
style: {
|
||||
fontWeight: 400,
|
||||
fontSize: fontSizes[this.props.level - 1],
|
||||
marginBottom: margins[1],
|
||||
...this.props.style
|
||||
}
|
||||
}
|
||||
|
||||
switch(this.props.level) {
|
||||
case 1: return <h1 {...headingProps}>{this.props.children}</h1>
|
||||
case 2: return <h2 {...headingProps}>{this.props.children}</h2>
|
||||
case 3: return <h3 {...headingProps}>{this.props.children}</h3>
|
||||
case 4: return <h4 {...headingProps}>{this.props.children}</h4>
|
||||
case 5: return <h5 {...headingProps}>{this.props.children}</h5>
|
||||
default: return <h6 {...headingProps}>{this.props.children}</h6>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default Heading
|
||||
@@ -2,7 +2,7 @@ import React from 'react'
|
||||
import IconBase from 'react-icon-base'
|
||||
|
||||
|
||||
export default class BackgroundIcon extends React.Component {
|
||||
export default class IconBackground extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<IconBase viewBox="0 0 20 20" {...this.props}>
|
||||
15
src/components/IconCircle.jsx
Normal file
15
src/components/IconCircle.jsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import React from 'react'
|
||||
import IconBase from 'react-icon-base'
|
||||
|
||||
|
||||
export default class IconCircle 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>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from 'react'
|
||||
import IconBase from 'react-icon-base'
|
||||
|
||||
|
||||
export default class FillIcon extends React.Component {
|
||||
export default class IconFill extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<IconBase viewBox="0 0 20 20" {...this.props}>
|
||||
33
src/components/IconLayer.jsx
Normal file
33
src/components/IconLayer.jsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import IconLine from './IconLine.jsx'
|
||||
import IconFill from './IconFill.jsx'
|
||||
import IconSymbol from './IconSymbol.jsx'
|
||||
import IconBackground from './IconBackground.jsx'
|
||||
import IconCircle from './IconCircle.jsx'
|
||||
import IconMissing from './IconMissing.jsx'
|
||||
|
||||
export default class IconLayer 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 <IconBackground {...iconProps} />
|
||||
case 'raster': return <IconFill {...iconProps} />
|
||||
case 'hillshade': return <IconFill {...iconProps} />
|
||||
case 'heatmap': return <IconFill {...iconProps} />
|
||||
case 'fill': return <IconFill {...iconProps} />
|
||||
case 'background': return <IconBackground {...iconProps} />
|
||||
case 'line': return <IconLine {...iconProps} />
|
||||
case 'symbol': return <IconSymbol {...iconProps} />
|
||||
case 'circle': return <IconCircle {...iconProps} />
|
||||
default: return <IconMissing {...iconProps} />
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
15
src/components/IconLine.jsx
Normal file
15
src/components/IconLine.jsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import React from 'react'
|
||||
import IconBase from 'react-icon-base'
|
||||
|
||||
|
||||
export default class IconLine 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>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
11
src/components/IconMissing.jsx
Normal file
11
src/components/IconMissing.jsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import React from 'react'
|
||||
import {MdPriorityHigh} from 'react-icons/md'
|
||||
|
||||
|
||||
export default class IconMissing extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<MdPriorityHigh {...this.props} />
|
||||
)
|
||||
}
|
||||
}
|
||||
18
src/components/IconSymbol.jsx
Normal file
18
src/components/IconSymbol.jsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import React from 'react'
|
||||
import IconBase from 'react-icon-base'
|
||||
|
||||
|
||||
export default class IconSymbol 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>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
113
src/components/InputArray.jsx
Normal file
113
src/components/InputArray.jsx
Normal file
@@ -0,0 +1,113 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import InputString from './InputString'
|
||||
import InputNumber from './InputNumber'
|
||||
|
||||
export default class FieldArray extends React.Component {
|
||||
static propTypes = {
|
||||
value: PropTypes.array,
|
||||
type: PropTypes.string,
|
||||
length: PropTypes.number,
|
||||
default: PropTypes.array,
|
||||
onChange: PropTypes.func,
|
||||
'aria-label': PropTypes.string,
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
value: [],
|
||||
default: [],
|
||||
}
|
||||
|
||||
constructor (props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
value: this.props.value.slice(0),
|
||||
// This is so we can compare changes in getDerivedStateFromProps
|
||||
initialPropsValue: this.props.value.slice(0),
|
||||
};
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps(props, state) {
|
||||
const value = [];
|
||||
const initialPropsValue = state.initialPropsValue.slice(0);
|
||||
|
||||
Array(props.length).fill(null).map((_, i) => {
|
||||
if (props.value[i] === state.initialPropsValue[i]) {
|
||||
value[i] = state.value[i];
|
||||
}
|
||||
else {
|
||||
value[i] = state.value[i];
|
||||
initialPropsValue[i] = state.value[i];
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
value,
|
||||
initialPropsValue,
|
||||
};
|
||||
}
|
||||
|
||||
isComplete (value) {
|
||||
return Array(this.props.length).fill(null).every((_, i) => {
|
||||
const val = value[i]
|
||||
return !(val === undefined || val === "");
|
||||
});
|
||||
}
|
||||
|
||||
changeValue(idx, newValue) {
|
||||
const value = this.state.value.slice(0);
|
||||
value[idx] = newValue;
|
||||
|
||||
this.setState({
|
||||
value,
|
||||
}, () => {
|
||||
if (this.isComplete(value)) {
|
||||
this.props.onChange(value);
|
||||
}
|
||||
else {
|
||||
// Unset until complete
|
||||
this.props.onChange(undefined);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const {value} = this.state;
|
||||
|
||||
const containsValues = (
|
||||
value.length > 0 &&
|
||||
!value.every(val => {
|
||||
return (val === "" || val === undefined)
|
||||
})
|
||||
);
|
||||
|
||||
const inputs = Array(this.props.length).fill(null).map((_, i) => {
|
||||
if(this.props.type === 'number') {
|
||||
return <InputNumber
|
||||
key={i}
|
||||
default={containsValues ? undefined : this.props.default[i]}
|
||||
value={value[i]}
|
||||
required={containsValues ? true : false}
|
||||
onChange={this.changeValue.bind(this, i)}
|
||||
aria-label={this.props['aria-label'] || this.props.label}
|
||||
/>
|
||||
} else {
|
||||
return <InputString
|
||||
key={i}
|
||||
default={containsValues ? undefined : this.props.default[i]}
|
||||
value={value[i]}
|
||||
required={containsValues ? true : false}
|
||||
onChange={this.changeValue.bind(this, i)}
|
||||
aria-label={this.props['aria-label'] || this.props.label}
|
||||
/>
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="maputnik-array">
|
||||
{inputs}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
100
src/components/InputAutocomplete.jsx
Normal file
100
src/components/InputAutocomplete.jsx
Normal file
@@ -0,0 +1,100 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import classnames from 'classnames'
|
||||
import Autocomplete from 'react-autocomplete'
|
||||
|
||||
|
||||
const MAX_HEIGHT = 140;
|
||||
|
||||
export default class InputAutocomplete extends React.Component {
|
||||
static propTypes = {
|
||||
value: PropTypes.string,
|
||||
options: PropTypes.array,
|
||||
onChange: PropTypes.func,
|
||||
keepMenuWithinWindowBounds: PropTypes.bool,
|
||||
'aria-label': PropTypes.string,
|
||||
}
|
||||
|
||||
state = {
|
||||
maxHeight: MAX_HEIGHT
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
onChange: () => {},
|
||||
options: [],
|
||||
}
|
||||
|
||||
calcMaxHeight() {
|
||||
if(this.props.keepMenuWithinWindowBounds) {
|
||||
const maxHeight = window.innerHeight - this.autocompleteMenuEl.getBoundingClientRect().top;
|
||||
const limitedMaxHeight = Math.min(maxHeight, MAX_HEIGHT);
|
||||
|
||||
if(limitedMaxHeight != this.state.maxHeight) {
|
||||
this.setState({
|
||||
maxHeight: limitedMaxHeight
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.calcMaxHeight();
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this.calcMaxHeight();
|
||||
}
|
||||
|
||||
onChange (v) {
|
||||
this.props.onChange(v === "" ? undefined : v);
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div
|
||||
ref={(el) => {
|
||||
this.autocompleteMenuEl = el;
|
||||
}}
|
||||
>
|
||||
<Autocomplete
|
||||
menuStyle={{
|
||||
position: "fixed",
|
||||
overflow: "auto",
|
||||
maxHeight: this.state.maxHeight,
|
||||
zIndex: '998'
|
||||
}}
|
||||
wrapperProps={{
|
||||
className: "maputnik-autocomplete",
|
||||
style: null
|
||||
}}
|
||||
inputProps={{
|
||||
'aria-label': this.props['aria-label'],
|
||||
className: "maputnik-string",
|
||||
spellCheck: false
|
||||
}}
|
||||
value={this.props.value}
|
||||
items={this.props.options}
|
||||
getItemValue={(item) => item[0]}
|
||||
onSelect={v => this.onChange(v)}
|
||||
onChange={(e, v) => this.onChange(v)}
|
||||
shouldItemRender={(item, value="") => {
|
||||
if (typeof(value) === "string") {
|
||||
return item[0].toLowerCase().indexOf(value.toLowerCase()) > -1
|
||||
}
|
||||
}}
|
||||
renderItem={(item, isHighlighted) => (
|
||||
<div
|
||||
key={item[0]}
|
||||
className={classnames({
|
||||
"maputnik-autocomplete-menu-item": true,
|
||||
"maputnik-autocomplete-menu-item-selected": isHighlighted,
|
||||
})}
|
||||
>
|
||||
{item[1]}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
35
src/components/InputButton.jsx
Normal file
35
src/components/InputButton.jsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import classnames from 'classnames'
|
||||
|
||||
export default class InputButton extends React.Component {
|
||||
static propTypes = {
|
||||
"data-wd-key": PropTypes.string,
|
||||
"aria-label": PropTypes.string,
|
||||
onClick: PropTypes.func,
|
||||
style: PropTypes.object,
|
||||
className: PropTypes.string,
|
||||
children: PropTypes.node,
|
||||
disabled: PropTypes.bool,
|
||||
type: PropTypes.string,
|
||||
id: PropTypes.string,
|
||||
title: PropTypes.string,
|
||||
}
|
||||
|
||||
render() {
|
||||
return <button
|
||||
id={this.props.id}
|
||||
title={this.props.title}
|
||||
type={this.props.type}
|
||||
onClick={this.props.onClick}
|
||||
disabled={this.props.disabled}
|
||||
aria-label={this.props["aria-label"]}
|
||||
className={classnames("maputnik-button", this.props.className)}
|
||||
data-wd-key={this.props["data-wd-key"]}
|
||||
style={this.props.style}
|
||||
>
|
||||
{this.props.children}
|
||||
</button>
|
||||
}
|
||||
}
|
||||
|
||||
39
src/components/InputCheckbox.jsx
Normal file
39
src/components/InputCheckbox.jsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
export default class InputCheckbox extends React.Component {
|
||||
static propTypes = {
|
||||
value: PropTypes.bool,
|
||||
style: PropTypes.object,
|
||||
onChange: PropTypes.func,
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
value: false,
|
||||
}
|
||||
|
||||
onChange = () => {
|
||||
this.props.onChange(!this.props.value);
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div className="maputnik-checkbox-wrapper">
|
||||
<input
|
||||
className="maputnik-checkbox"
|
||||
type="checkbox"
|
||||
style={this.props.style}
|
||||
onChange={this.onChange}
|
||||
onClick={this.onChange}
|
||||
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>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
135
src/components/InputColor.jsx
Normal file
135
src/components/InputColor.jsx
Normal file
@@ -0,0 +1,135 @@
|
||||
import React from 'react'
|
||||
import Color from 'color'
|
||||
import ChromePicker from 'react-color/lib/components/chrome/Chrome'
|
||||
import PropTypes from 'prop-types'
|
||||
import lodash from 'lodash';
|
||||
|
||||
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*/
|
||||
export default class InputColor extends React.Component {
|
||||
static propTypes = {
|
||||
onChange: PropTypes.func.isRequired,
|
||||
name: PropTypes.string,
|
||||
value: PropTypes.string,
|
||||
doc: PropTypes.string,
|
||||
style: PropTypes.object,
|
||||
default: PropTypes.string,
|
||||
'aria-label': PropTypes.string,
|
||||
}
|
||||
|
||||
state = {
|
||||
pickerOpened: false
|
||||
}
|
||||
|
||||
constructor () {
|
||||
super();
|
||||
this.onChangeNoCheck = lodash.throttle(this.onChangeNoCheck, 1000/30);
|
||||
}
|
||||
|
||||
onChangeNoCheck (v) {
|
||||
this.props.onChange(v);
|
||||
}
|
||||
|
||||
//TODO: I much rather would do this with absolute positioning
|
||||
//but I am too stupid to get it to work together with fixed position
|
||||
//and scrollbars so I have to fallback to JavaScript
|
||||
calcPickerOffset = () => {
|
||||
const elem = this.colorInput
|
||||
if(elem) {
|
||||
const pos = elem.getBoundingClientRect()
|
||||
return {
|
||||
top: pos.top,
|
||||
left: pos.left + 196,
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
top: 160,
|
||||
left: 555,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
togglePicker = () => {
|
||||
this.setState({ pickerOpened: !this.state.pickerOpened })
|
||||
}
|
||||
|
||||
get color() {
|
||||
// Catch invalid color.
|
||||
try {
|
||||
return Color(this.props.value).rgb()
|
||||
}
|
||||
catch(err) {
|
||||
console.warn("Error parsing color: ", err);
|
||||
return Color("rgb(255,255,255)");
|
||||
}
|
||||
}
|
||||
|
||||
onChange (v) {
|
||||
this.props.onChange(v === "" ? undefined : v);
|
||||
}
|
||||
|
||||
render() {
|
||||
const offset = this.calcPickerOffset()
|
||||
var currentColor = this.color.object()
|
||||
currentColor = {
|
||||
r: currentColor.r,
|
||||
g: currentColor.g,
|
||||
b: currentColor.b,
|
||||
// Rename alpha -> a for ChromePicker
|
||||
a: currentColor.alpha
|
||||
}
|
||||
|
||||
const picker = <div
|
||||
className="maputnik-color-picker-offset"
|
||||
style={{
|
||||
position: 'fixed',
|
||||
zIndex: 1,
|
||||
left: offset.left,
|
||||
top: offset.top,
|
||||
}}>
|
||||
<ChromePicker
|
||||
color={currentColor}
|
||||
onChange={c => this.onChangeNoCheck(formatColor(c))}
|
||||
/>
|
||||
<div
|
||||
className="maputnik-color-picker-offset"
|
||||
onClick={this.togglePicker}
|
||||
style={{
|
||||
zIndex: -1,
|
||||
position: 'fixed',
|
||||
top: '0px',
|
||||
right: '0px',
|
||||
bottom: '0px',
|
||||
left: '0px',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
var swatchStyle = {
|
||||
backgroundColor: this.props.value
|
||||
};
|
||||
|
||||
return <div className="maputnik-color-wrapper">
|
||||
{this.state.pickerOpened && picker}
|
||||
<div className="maputnik-color-swatch" style={swatchStyle}></div>
|
||||
<input
|
||||
aria-label={this.props['aria-label']}
|
||||
spellCheck="false"
|
||||
autoComplete="off"
|
||||
className="maputnik-color"
|
||||
ref={(input) => this.colorInput = input}
|
||||
onClick={this.togglePicker}
|
||||
style={this.props.style}
|
||||
name={this.props.name}
|
||||
placeholder={this.props.default}
|
||||
value={this.props.value ? this.props.value : ""}
|
||||
onChange={(e) => this.onChange(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
141
src/components/InputDynamicArray.jsx
Normal file
141
src/components/InputDynamicArray.jsx
Normal file
@@ -0,0 +1,141 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import InputString from './InputString'
|
||||
import InputNumber from './InputNumber'
|
||||
import InputButton from './InputButton'
|
||||
import {MdDelete} from 'react-icons/md'
|
||||
import FieldDocLabel from './FieldDocLabel'
|
||||
import InputEnum from './InputEnum'
|
||||
import capitalize from 'lodash.capitalize'
|
||||
import InputUrl from './InputUrl'
|
||||
|
||||
|
||||
export default class FieldDynamicArray extends React.Component {
|
||||
static propTypes = {
|
||||
value: PropTypes.array,
|
||||
type: PropTypes.string,
|
||||
default: PropTypes.array,
|
||||
onChange: PropTypes.func,
|
||||
style: PropTypes.object,
|
||||
fieldSpec: PropTypes.object,
|
||||
'aria-label': PropTypes.string,
|
||||
}
|
||||
|
||||
changeValue(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 if (this.props.type === 'url') {
|
||||
values.push("");
|
||||
}
|
||||
else if (this.props.type === 'enum') {
|
||||
const {fieldSpec} = this.props;
|
||||
const defaultValue = Object.keys(fieldSpec.values)[0];
|
||||
values.push(defaultValue);
|
||||
} else {
|
||||
values.push("")
|
||||
}
|
||||
|
||||
this.props.onChange(values)
|
||||
}
|
||||
|
||||
deleteValue(valueIdx) {
|
||||
const values = this.values.slice(0)
|
||||
values.splice(valueIdx, 1)
|
||||
|
||||
this.props.onChange(values.length > 0 ? values : undefined);
|
||||
}
|
||||
|
||||
render() {
|
||||
const inputs = this.values.map((v, i) => {
|
||||
const deleteValueBtn= <DeleteValueInputButton onClick={this.deleteValue.bind(this, i)} />
|
||||
let input;
|
||||
if(this.props.type === 'url') {
|
||||
input = <InputUrl
|
||||
value={v}
|
||||
onChange={this.changeValue.bind(this, i)}
|
||||
aria-label={this.props['aria-label'] || this.props.label}
|
||||
/>
|
||||
}
|
||||
else if (this.props.type === 'number') {
|
||||
input = <InputNumber
|
||||
value={v}
|
||||
onChange={this.changeValue.bind(this, i)}
|
||||
aria-label={this.props['aria-label'] || this.props.label}
|
||||
/>
|
||||
}
|
||||
else if (this.props.type === 'enum') {
|
||||
const options = Object.keys(this.props.fieldSpec.values).map(v => [v, capitalize(v)]);
|
||||
input = <InputEnum
|
||||
options={options}
|
||||
value={v}
|
||||
onChange={this.changeValue.bind(this, i)}
|
||||
aria-label={this.props['aria-label'] || this.props.label}
|
||||
/>
|
||||
}
|
||||
else {
|
||||
input = <InputString
|
||||
value={v}
|
||||
onChange={this.changeValue.bind(this, i)}
|
||||
aria-label={this.props['aria-label'] || this.props.label}
|
||||
/>
|
||||
}
|
||||
|
||||
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}
|
||||
<InputButton
|
||||
className="maputnik-array-add-value"
|
||||
onClick={this.addValue}
|
||||
>
|
||||
Add value
|
||||
</InputButton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class DeleteValueInputButton extends React.Component {
|
||||
static propTypes = {
|
||||
onClick: PropTypes.func,
|
||||
}
|
||||
|
||||
render() {
|
||||
return <InputButton
|
||||
className="maputnik-delete-stop"
|
||||
onClick={this.props.onClick}
|
||||
title="Remove array item"
|
||||
>
|
||||
<FieldDocLabel
|
||||
label={<MdDelete />}
|
||||
doc={"Remove array item."}
|
||||
/>
|
||||
</InputButton>
|
||||
}
|
||||
}
|
||||
|
||||
49
src/components/InputEnum.jsx
Normal file
49
src/components/InputEnum.jsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import InputSelect from './InputSelect'
|
||||
import InputMultiInput from './InputMultiInput'
|
||||
|
||||
|
||||
function optionsLabelLength(options) {
|
||||
let sum = 0;
|
||||
options.forEach(([_, label]) => {
|
||||
sum += label.length
|
||||
})
|
||||
return sum
|
||||
}
|
||||
|
||||
|
||||
export default class InputEnum extends React.Component {
|
||||
static propTypes = {
|
||||
"data-wd-key": PropTypes.string,
|
||||
value: PropTypes.string,
|
||||
style: PropTypes.object,
|
||||
default: PropTypes.string,
|
||||
name: PropTypes.string,
|
||||
onChange: PropTypes.func,
|
||||
options: PropTypes.array,
|
||||
'aria-label': PropTypes.string,
|
||||
}
|
||||
|
||||
render() {
|
||||
const {options, value, onChange, name, label} = this.props;
|
||||
|
||||
if(options.length <= 3 && optionsLabelLength(options) <= 20) {
|
||||
return <InputMultiInput
|
||||
name={name}
|
||||
options={options}
|
||||
value={value || this.props.default}
|
||||
onChange={onChange}
|
||||
aria-label={this.props['aria-label'] || label}
|
||||
/>
|
||||
} else {
|
||||
return <InputSelect
|
||||
options={options}
|
||||
value={value || this.props.default}
|
||||
onChange={onChange}
|
||||
aria-label={this.props['aria-label'] || label}
|
||||
/>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
61
src/components/InputFont.jsx
Normal file
61
src/components/InputFont.jsx
Normal file
@@ -0,0 +1,61 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import InputAutocomplete from './InputAutocomplete'
|
||||
|
||||
export default class FieldFont extends React.Component {
|
||||
static propTypes = {
|
||||
value: PropTypes.array,
|
||||
default: PropTypes.array,
|
||||
fonts: PropTypes.array,
|
||||
style: PropTypes.object,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
'aria-label': PropTypes.string,
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
fonts: []
|
||||
}
|
||||
|
||||
get values() {
|
||||
const out = this.props.value || this.props.default || [];
|
||||
|
||||
// Always put a "" in the last field to you can keep adding entries
|
||||
if (out[out.length-1] !== ""){
|
||||
return out.concat("");
|
||||
}
|
||||
else {
|
||||
return out;
|
||||
}
|
||||
}
|
||||
|
||||
changeFont(idx, newValue) {
|
||||
const changedValues = this.values.slice(0)
|
||||
changedValues[idx] = newValue
|
||||
const filteredValues = changedValues
|
||||
.filter(v => v !== undefined)
|
||||
.filter(v => v !== "")
|
||||
|
||||
this.props.onChange(filteredValues);
|
||||
}
|
||||
|
||||
render() {
|
||||
const inputs = this.values.map((value, i) => {
|
||||
return <li
|
||||
key={i}
|
||||
>
|
||||
<InputAutocomplete
|
||||
aria-label={this.props['aria-label'] || this.props.name}
|
||||
value={value}
|
||||
options={this.props.fonts.map(f => [f, f])}
|
||||
onChange={this.changeFont.bind(this, i)}
|
||||
/>
|
||||
</li>
|
||||
})
|
||||
|
||||
return (
|
||||
<ul className="maputnik-font">
|
||||
{inputs}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
}
|
||||
174
src/components/InputJson.jsx
Normal file
174
src/components/InputJson.jsx
Normal file
@@ -0,0 +1,174 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import classnames from 'classnames';
|
||||
|
||||
import Block from './Block'
|
||||
import FieldString from './FieldString'
|
||||
import CodeMirror from 'codemirror';
|
||||
|
||||
import 'codemirror/mode/javascript/javascript'
|
||||
import 'codemirror/addon/lint/lint'
|
||||
import 'codemirror/addon/edit/matchbrackets'
|
||||
import 'codemirror/lib/codemirror.css'
|
||||
import 'codemirror/addon/lint/lint.css'
|
||||
import jsonlint from 'jsonlint'
|
||||
import stringifyPretty from 'json-stringify-pretty-compact'
|
||||
import '../util/codemirror-mgl';
|
||||
|
||||
|
||||
export default class InputJson extends React.Component {
|
||||
static propTypes = {
|
||||
layer: PropTypes.any.isRequired,
|
||||
maxHeight: PropTypes.number,
|
||||
onChange: PropTypes.func,
|
||||
lineNumbers: PropTypes.bool,
|
||||
lineWrapping: PropTypes.bool,
|
||||
getValue: PropTypes.func,
|
||||
gutters: PropTypes.array,
|
||||
className: PropTypes.string,
|
||||
onFocus: PropTypes.func,
|
||||
onBlur: PropTypes.func,
|
||||
onJSONValid: PropTypes.func,
|
||||
onJSONInvalid: PropTypes.func,
|
||||
mode: PropTypes.object,
|
||||
lint: PropTypes.oneOfType([
|
||||
PropTypes.bool,
|
||||
PropTypes.object,
|
||||
]),
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
lineNumbers: true,
|
||||
lineWrapping: false,
|
||||
gutters: ["CodeMirror-lint-markers"],
|
||||
getValue: (data) => {
|
||||
return stringifyPretty(data, {indent: 2, maxLength: 40});
|
||||
},
|
||||
onFocus: () => {},
|
||||
onBlur: () => {},
|
||||
onJSONInvalid: () => {},
|
||||
onJSONValid: () => {},
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this._keyEvent = "keyboard";
|
||||
this.state = {
|
||||
isEditing: false,
|
||||
showMessage: false,
|
||||
prevValue: this.props.getValue(this.props.layer),
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
this._doc = CodeMirror(this._el, {
|
||||
value: this.props.getValue(this.props.layer),
|
||||
mode: this.props.mode || {
|
||||
name: "mgl",
|
||||
},
|
||||
lineWrapping: this.props.lineWrapping,
|
||||
tabSize: 2,
|
||||
theme: 'maputnik',
|
||||
viewportMargin: Infinity,
|
||||
lineNumbers: this.props.lineNumbers,
|
||||
lint: this.props.lint || {
|
||||
context: "layer"
|
||||
},
|
||||
matchBrackets: true,
|
||||
gutters: this.props.gutters,
|
||||
scrollbarStyle: "null",
|
||||
});
|
||||
|
||||
this._doc.on('change', this.onChange);
|
||||
this._doc.on('focus', this.onFocus);
|
||||
this._doc.on('blur', this.onBlur);
|
||||
}
|
||||
|
||||
onPointerDown = (cm, e) => {
|
||||
this._keyEvent = "pointer";
|
||||
}
|
||||
|
||||
onFocus = (cm, e) => {
|
||||
this.props.onFocus();
|
||||
this.setState({
|
||||
isEditing: true,
|
||||
showMessage: (this._keyEvent === "keyboard"),
|
||||
});
|
||||
}
|
||||
|
||||
onBlur = () => {
|
||||
this._keyEvent = "keyboard";
|
||||
this.props.onBlur();
|
||||
this.setState({
|
||||
isEditing: false,
|
||||
showMessage: false,
|
||||
});
|
||||
}
|
||||
|
||||
componentWillUnMount () {
|
||||
this._doc.off('change', this.onChange);
|
||||
this._doc.off('focus', this.onFocus);
|
||||
this._doc.off('blur', this.onBlur);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (!this.state.isEditing && prevProps.layer !== this.props.layer) {
|
||||
this._cancelNextChange = true;
|
||||
this._doc.setValue(
|
||||
this.props.getValue(this.props.layer),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
onChange = (e) => {
|
||||
if (this._cancelNextChange) {
|
||||
this._cancelNextChange = false;
|
||||
this.setState({
|
||||
prevValue: this._doc.getValue(),
|
||||
})
|
||||
return;
|
||||
}
|
||||
const newCode = this._doc.getValue();
|
||||
|
||||
if (this.state.prevValue !== newCode) {
|
||||
let parsedLayer, err;
|
||||
try {
|
||||
parsedLayer = JSON.parse(newCode);
|
||||
} catch(_err) {
|
||||
err = _err;
|
||||
console.warn(_err)
|
||||
}
|
||||
|
||||
if (err) {
|
||||
this.props.onJSONInvalid();
|
||||
}
|
||||
else {
|
||||
this.props.onChange(parsedLayer)
|
||||
this.props.onJSONValid();
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({
|
||||
prevValue: newCode,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const {showMessage} = this.state;
|
||||
const style = {};
|
||||
if (this.props.maxHeight) {
|
||||
style.maxHeight = this.props.maxHeight;
|
||||
}
|
||||
|
||||
return <div className="JSONEditor" onPointerDown={this.onPointerDown} aria-hidden="true">
|
||||
<div className={classnames("JSONEditor__message", {"JSONEditor__message--on": showMessage})}>
|
||||
Press <kbd>ESC</kbd> to lose focus
|
||||
</div>
|
||||
<div
|
||||
className={classnames("codemirror-container", this.props.className)}
|
||||
ref={(el) => this._el = el}
|
||||
style={style}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
42
src/components/InputMultiInput.jsx
Normal file
42
src/components/InputMultiInput.jsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import classnames from 'classnames'
|
||||
import InputButton from './InputButton'
|
||||
|
||||
export default class InputMultiInput extends React.Component {
|
||||
static propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
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 radios = options.map(([val, label])=> {
|
||||
return <label
|
||||
key={val}
|
||||
className={classnames("maputnik-radio-as-button", {"maputnik-button-selected": val === selectedValue})}
|
||||
>
|
||||
<input type="radio"
|
||||
name={this.props.name}
|
||||
onChange={e => this.props.onChange(val)}
|
||||
value={val}
|
||||
checked={val === selectedValue}
|
||||
/>
|
||||
{label}
|
||||
</label>
|
||||
})
|
||||
|
||||
return <fieldset className="maputnik-multibutton" aria-label={this.props['aria-label']}>
|
||||
{radios}
|
||||
</fieldset>
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
234
src/components/InputNumber.jsx
Normal file
234
src/components/InputNumber.jsx
Normal file
@@ -0,0 +1,234 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
let IDX = 0;
|
||||
|
||||
export default class InputNumber extends React.Component {
|
||||
static propTypes = {
|
||||
value: PropTypes.number,
|
||||
default: PropTypes.number,
|
||||
min: PropTypes.number,
|
||||
max: PropTypes.number,
|
||||
onChange: PropTypes.func,
|
||||
allowRange: PropTypes.bool,
|
||||
rangeStep: PropTypes.number,
|
||||
wdKey: PropTypes.string,
|
||||
required: PropTypes.bool,
|
||||
"aria-label": PropTypes.string,
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
rangeStep: 1
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
uuid: IDX++,
|
||||
editing: false,
|
||||
value: props.value,
|
||||
dirtyValue: props.value,
|
||||
}
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps(props, state) {
|
||||
if (!state.editing && props.value !== state.value) {
|
||||
return {
|
||||
value: props.value,
|
||||
dirtyValue: props.value,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
changeValue(newValue) {
|
||||
const value = (newValue === "" || newValue === undefined) ?
|
||||
undefined :
|
||||
parseFloat(newValue);
|
||||
|
||||
const hasChanged = this.props.value !== value;
|
||||
if(this.isValid(value) && hasChanged) {
|
||||
this.props.onChange(value)
|
||||
this.setState({
|
||||
value: newValue,
|
||||
});
|
||||
}
|
||||
else if (!this.isValid(value) && hasChanged) {
|
||||
this.setState({
|
||||
value: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
this.setState({
|
||||
dirtyValue: newValue === "" ? undefined : newValue,
|
||||
})
|
||||
}
|
||||
|
||||
isValid(v) {
|
||||
if (v === undefined) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const value = parseFloat(v)
|
||||
if(isNaN(value)) {
|
||||
return false
|
||||
}
|
||||
|
||||
if(!isNaN(this.props.min) && value < this.props.min) {
|
||||
return false
|
||||
}
|
||||
|
||||
if(!isNaN(this.props.max) && value > this.props.max) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
resetValue = () => {
|
||||
this.setState({editing: false});
|
||||
// Reset explicitly to default value if value has been cleared
|
||||
if(this.state.value === "") {
|
||||
return;
|
||||
}
|
||||
|
||||
// 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)
|
||||
this.setState({dirtyValue: this.props.value});
|
||||
} else {
|
||||
this.changeValue(undefined);
|
||||
this.setState({dirtyValue: undefined});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onChangeRange = (e) => {
|
||||
let value = parseFloat(e.target.value, 10);
|
||||
const step = this.props.rangeStep;
|
||||
let dirtyValue = value;
|
||||
|
||||
if(step) {
|
||||
// Can't do this with the <input/> range step attribute else we won't be able to set a high precision value via the text input.
|
||||
const snap = value % step;
|
||||
|
||||
// Round up/down to step
|
||||
if (this._keyboardEvent) {
|
||||
// If it's keyboard event we might get a low positive/negative value,
|
||||
// for example we might go from 13 to 13.23, however because we know
|
||||
// that came from a keyboard event we always want to increase by a
|
||||
// single step value.
|
||||
if (value < this.state.dirtyValue) {
|
||||
value = this.state.value - step;
|
||||
}
|
||||
else {
|
||||
value = this.state.value + step
|
||||
}
|
||||
dirtyValue = value;
|
||||
}
|
||||
else {
|
||||
if (snap < step/2) {
|
||||
value = value - snap;
|
||||
}
|
||||
else {
|
||||
value = value + (step - snap);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
this._keyboardEvent = false;
|
||||
|
||||
// Clamp between min/max
|
||||
value = Math.max(this.props.min, Math.min(this.props.max, value));
|
||||
|
||||
this.setState({value, dirtyValue});
|
||||
this.props.onChange(value);
|
||||
}
|
||||
|
||||
render() {
|
||||
if(
|
||||
this.props.hasOwnProperty("min") && this.props.hasOwnProperty("max") &&
|
||||
this.props.min !== undefined && this.props.max !== undefined &&
|
||||
this.props.allowRange
|
||||
) {
|
||||
const value = this.state.editing ? this.state.dirtyValue : this.state.value;
|
||||
const defaultValue = this.props.default === undefined ? "" : this.props.default;
|
||||
let inputValue;
|
||||
if (this.state.editingRange) {
|
||||
inputValue = this.state.value;
|
||||
}
|
||||
else {
|
||||
inputValue = value;
|
||||
}
|
||||
|
||||
return <div className="maputnik-number-container">
|
||||
<input
|
||||
className="maputnik-number-range"
|
||||
key="range"
|
||||
type="range"
|
||||
max={this.props.max}
|
||||
min={this.props.min}
|
||||
step="any"
|
||||
spellCheck="false"
|
||||
value={value === undefined ? defaultValue : value}
|
||||
onChange={this.onChangeRange}
|
||||
onKeyDown={() => {
|
||||
this._keyboardEvent = true;
|
||||
}}
|
||||
onPointerDown={() => {
|
||||
this.setState({editing: true, editingRange: true});
|
||||
}}
|
||||
onPointerUp={() => {
|
||||
// Safari doesn't get onBlur event
|
||||
this.setState({editing: false, editingRange: false});
|
||||
}}
|
||||
onBlur={() => {
|
||||
this.setState({
|
||||
editing: false,
|
||||
editingRange: false,
|
||||
dirtyValue: this.state.value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<input
|
||||
key="text"
|
||||
type="text"
|
||||
spellCheck="false"
|
||||
className="maputnik-number"
|
||||
placeholder={this.props.default}
|
||||
value={inputValue === undefined ? "" : inputValue}
|
||||
onFocus={e => {
|
||||
this.setState({editing: true});
|
||||
}}
|
||||
onChange={e => {
|
||||
this.changeValue(e.target.value);
|
||||
}}
|
||||
onBlur={e => {
|
||||
this.setState({editing: false});
|
||||
this.resetValue()
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
else {
|
||||
const value = this.state.editing ? this.state.dirtyValue : this.state.value;
|
||||
|
||||
return <input
|
||||
aria-label={this.props['aria-label']}
|
||||
spellCheck="false"
|
||||
className="maputnik-number"
|
||||
placeholder={this.props.default}
|
||||
value={value === undefined ? "" : value}
|
||||
onChange={e => this.changeValue(e.target.value)}
|
||||
onFocus={() => {
|
||||
this.setState({editing: true});
|
||||
}}
|
||||
onBlur={this.resetValue}
|
||||
required={this.props.required}
|
||||
/>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
36
src/components/InputSelect.jsx
Normal file
36
src/components/InputSelect.jsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
export default class InputSelect 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,
|
||||
title: PropTypes.string,
|
||||
'aria-label': PropTypes.string,
|
||||
}
|
||||
|
||||
|
||||
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}
|
||||
title={this.props.title}
|
||||
value={this.props.value}
|
||||
onChange={e => this.props.onChange(e.target.value)}
|
||||
aria-label={this.props['aria-label']}
|
||||
>
|
||||
{ options.map(([val, label]) => <option key={val} value={val}>{label}</option>) }
|
||||
</select>
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
139
src/components/InputSpec.jsx
Normal file
139
src/components/InputSpec.jsx
Normal file
@@ -0,0 +1,139 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import InputColor from './InputColor'
|
||||
import InputNumber from './InputNumber'
|
||||
import InputCheckbox from './InputCheckbox'
|
||||
import InputString from './InputString'
|
||||
import InputSelect from './InputSelect'
|
||||
import InputMultiInput from './InputMultiInput'
|
||||
import InputArray from './InputArray'
|
||||
import InputDynamicArray from './InputDynamicArray'
|
||||
import InputFont from './InputFont'
|
||||
import InputAutocomplete from './InputAutocomplete'
|
||||
import InputEnum from './InputEnum'
|
||||
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,
|
||||
'aria-label': PropTypes.string,
|
||||
}
|
||||
|
||||
render() {
|
||||
const commonProps = {
|
||||
error: this.props.error,
|
||||
fieldSpec: this.props.fieldSpec,
|
||||
label: this.props.label,
|
||||
action: this.props.action,
|
||||
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),
|
||||
'aria-label': this.props['aria-label'],
|
||||
}
|
||||
|
||||
function childNodes() {
|
||||
switch(this.props.fieldSpec.type) {
|
||||
case 'number': return (
|
||||
<InputNumber
|
||||
{...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)])
|
||||
|
||||
return <InputEnum
|
||||
{...commonProps}
|
||||
options={options}
|
||||
/>
|
||||
case 'resolvedImage':
|
||||
case 'formatted':
|
||||
case 'string':
|
||||
if (iconProperties.indexOf(this.props.fieldName) >= 0) {
|
||||
const options = this.props.fieldSpec.values || [];
|
||||
return <InputAutocomplete
|
||||
{...commonProps}
|
||||
options={options.map(f => [f, f])}
|
||||
/>
|
||||
} else {
|
||||
return <InputString
|
||||
{...commonProps}
|
||||
/>
|
||||
}
|
||||
case 'color': return (
|
||||
<InputColor
|
||||
{...commonProps}
|
||||
/>
|
||||
)
|
||||
case 'boolean': return (
|
||||
<InputCheckbox
|
||||
{...commonProps}
|
||||
/>
|
||||
)
|
||||
case 'array':
|
||||
if(this.props.fieldName === 'text-font') {
|
||||
return <InputFont
|
||||
{...commonProps}
|
||||
fonts={this.props.fieldSpec.values}
|
||||
/>
|
||||
} else {
|
||||
if (this.props.fieldSpec.length) {
|
||||
return <InputArray
|
||||
{...commonProps}
|
||||
type={this.props.fieldSpec.value}
|
||||
length={this.props.fieldSpec.length}
|
||||
/>
|
||||
} else {
|
||||
return <InputDynamicArray
|
||||
{...commonProps}
|
||||
fieldSpec={this.props.fieldSpec}
|
||||
type={this.props.fieldSpec.value}
|
||||
/>
|
||||
}
|
||||
}
|
||||
default: return null
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div data-wd-key={"spec-field:"+this.props.fieldName}>
|
||||
{childNodes.call(this)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
95
src/components/InputString.jsx
Normal file
95
src/components/InputString.jsx
Normal file
@@ -0,0 +1,95 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
export default class InputString extends React.Component {
|
||||
static propTypes = {
|
||||
"data-wd-key": PropTypes.string,
|
||||
value: PropTypes.string,
|
||||
style: PropTypes.object,
|
||||
default: PropTypes.string,
|
||||
onChange: PropTypes.func,
|
||||
onInput: PropTypes.func,
|
||||
multi: PropTypes.bool,
|
||||
required: PropTypes.bool,
|
||||
disabled: PropTypes.bool,
|
||||
spellCheck: PropTypes.bool,
|
||||
'aria-label': PropTypes.string,
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
onInput: () => {},
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
editing: false,
|
||||
value: props.value || ''
|
||||
}
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps(props, state) {
|
||||
if (!state.editing) {
|
||||
return {
|
||||
value: props.value
|
||||
};
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
render() {
|
||||
let tag;
|
||||
let classes;
|
||||
|
||||
if(!!this.props.multi) {
|
||||
tag = "textarea"
|
||||
classes = [
|
||||
"maputnik-string",
|
||||
"maputnik-string--multi"
|
||||
]
|
||||
}
|
||||
else {
|
||||
tag = "input"
|
||||
classes = [
|
||||
"maputnik-string"
|
||||
]
|
||||
}
|
||||
|
||||
if(!!this.props.disabled) {
|
||||
classes.push("maputnik-string--disabled");
|
||||
}
|
||||
|
||||
return React.createElement(tag, {
|
||||
"aria-label": this.props["aria-label"],
|
||||
"data-wd-key": this.props["data-wd-key"],
|
||||
spellCheck: this.props.hasOwnProperty("spellCheck") ? this.props.spellCheck : !(tag === "input"),
|
||||
disabled: this.props.disabled,
|
||||
className: classes.join(" "),
|
||||
style: this.props.style,
|
||||
value: this.state.value === undefined ? "" : this.state.value,
|
||||
placeholder: this.props.default,
|
||||
onChange: e => {
|
||||
this.setState({
|
||||
editing: true,
|
||||
value: e.target.value
|
||||
}, () => {
|
||||
this.props.onInput(this.state.value);
|
||||
});
|
||||
},
|
||||
onBlur: () => {
|
||||
if(this.state.value!==this.props.value) {
|
||||
this.setState({editing: false});
|
||||
this.props.onChange(this.state.value);
|
||||
}
|
||||
},
|
||||
onKeyDown: (e) => {
|
||||
if (e.keyCode === 13) {
|
||||
this.props.onChange(this.state.value);
|
||||
}
|
||||
},
|
||||
required: this.props.required,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
103
src/components/InputUrl.jsx
Normal file
103
src/components/InputUrl.jsx
Normal file
@@ -0,0 +1,103 @@
|
||||
import React, {Fragment} from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import InputString from './InputString'
|
||||
import SmallError from './SmallError'
|
||||
|
||||
|
||||
function validate (url) {
|
||||
if (url === "") {
|
||||
return;
|
||||
}
|
||||
|
||||
let error;
|
||||
const getProtocol = (url) => {
|
||||
try {
|
||||
const urlObj = new URL(url);
|
||||
return urlObj.protocol;
|
||||
}
|
||||
catch (err) {
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
const protocol = getProtocol(url);
|
||||
const isSsl = window.location.protocol === "https:";
|
||||
|
||||
if (!protocol) {
|
||||
error = (
|
||||
<SmallError>
|
||||
Must provide protocol {
|
||||
isSsl
|
||||
? <code>https://</code>
|
||||
: <><code>http://</code> or <code>https://</code></>
|
||||
}
|
||||
</SmallError>
|
||||
);
|
||||
}
|
||||
else if (
|
||||
protocol &&
|
||||
protocol === "http:" &&
|
||||
window.location.protocol === "https:"
|
||||
) {
|
||||
error = (
|
||||
<SmallError>
|
||||
CORS policy won't allow fetching resources served over http from https, use a <code>https://</code> domain
|
||||
</SmallError>
|
||||
);
|
||||
}
|
||||
|
||||
return error;
|
||||
}
|
||||
|
||||
export default class FieldUrl extends React.Component {
|
||||
static propTypes = {
|
||||
"data-wd-key": PropTypes.string,
|
||||
value: PropTypes.string,
|
||||
style: PropTypes.object,
|
||||
default: PropTypes.string,
|
||||
onChange: PropTypes.func,
|
||||
onInput: PropTypes.func,
|
||||
multi: PropTypes.bool,
|
||||
required: PropTypes.bool,
|
||||
'aria-label': PropTypes.string,
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
onInput: () => {},
|
||||
}
|
||||
|
||||
constructor (props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
error: validate(props.value)
|
||||
};
|
||||
}
|
||||
|
||||
onInput = (url) => {
|
||||
this.setState({
|
||||
error: validate(url)
|
||||
});
|
||||
this.props.onInput(url);
|
||||
}
|
||||
|
||||
onChange = (url) => {
|
||||
this.setState({
|
||||
error: validate(url)
|
||||
});
|
||||
this.props.onChange(url);
|
||||
}
|
||||
|
||||
render () {
|
||||
return (
|
||||
<div>
|
||||
<InputString
|
||||
{...this.props}
|
||||
onInput={this.onInput}
|
||||
onChange={this.onChange}
|
||||
aria-label={this.props['aria-label']}
|
||||
/>
|
||||
{this.state.error}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
337
src/components/LayerEditor.jsx
Normal file
337
src/components/LayerEditor.jsx
Normal file
@@ -0,0 +1,337 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { Wrapper, Button, Menu, MenuItem } from 'react-aria-menubutton'
|
||||
|
||||
import FieldJson from './FieldJson'
|
||||
import FilterEditor from './FilterEditor'
|
||||
import PropertyGroup from './PropertyGroup'
|
||||
import LayerEditorGroup from './LayerEditorGroup'
|
||||
import FieldType from './FieldType'
|
||||
import FieldId from './FieldId'
|
||||
import FieldMinZoom from './FieldMinZoom'
|
||||
import FieldMaxZoom from './FieldMaxZoom'
|
||||
import FieldComment from './FieldComment'
|
||||
import FieldSource from './FieldSource'
|
||||
import FieldSourceLayer from './FieldSourceLayer'
|
||||
import {Accordion} from 'react-accessible-accordion';
|
||||
|
||||
import {MdMoreVert} from 'react-icons/md'
|
||||
|
||||
import { changeType, changeProperty } from '../libs/layer'
|
||||
import layout from '../config/layout.json'
|
||||
import {formatLayerId} from '../util/format';
|
||||
|
||||
|
||||
function getLayoutForType (type) {
|
||||
return layout[type] ? layout[type] : layout.invalid;
|
||||
}
|
||||
|
||||
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(getLayoutForType(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,
|
||||
errors: PropTypes.array,
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
onLayerChanged: () => {},
|
||||
onLayerIdChange: () => {},
|
||||
onLayerDestroyed: () => {},
|
||||
}
|
||||
|
||||
static childContextTypes = {
|
||||
reactIconBase: PropTypes.object
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
//TODO: Clean this up and refactor into function
|
||||
const editorGroups = {}
|
||||
layoutGroups(this.props.layer.type).forEach(group => {
|
||||
editorGroups[group.title] = true
|
||||
})
|
||||
|
||||
this.state = { editorGroups }
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps(props, state) {
|
||||
const additionalGroups = { ...state.editorGroups }
|
||||
|
||||
getLayoutForType(props.layer.type).groups.forEach(group => {
|
||||
if(!(group.title in additionalGroups)) {
|
||||
additionalGroups[group.title] = true
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
editorGroups: additionalGroups
|
||||
};
|
||||
}
|
||||
|
||||
getChildContext () {
|
||||
return {
|
||||
reactIconBase: {
|
||||
size: 14,
|
||||
color: '#8e8e8e',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
changeProperty(group, property, newValue) {
|
||||
this.props.onLayerChanged(
|
||||
this.props.layerIndex,
|
||||
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']
|
||||
}
|
||||
const {errors, layerIndex} = this.props;
|
||||
|
||||
const errorData = {};
|
||||
errors.forEach(error => {
|
||||
if (
|
||||
error.parsed &&
|
||||
error.parsed.type === "layer" &&
|
||||
error.parsed.data.index == layerIndex
|
||||
) {
|
||||
errorData[error.parsed.data.key] = {
|
||||
message: error.parsed.data.message
|
||||
};
|
||||
}
|
||||
})
|
||||
|
||||
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>
|
||||
<FieldId
|
||||
value={this.props.layer.id}
|
||||
wdKey="layer-editor.layer-id"
|
||||
error={errorData.id}
|
||||
onChange={newId => this.props.onLayerIdChange(this.props.layerIndex, this.props.layer.id, newId)}
|
||||
/>
|
||||
<FieldType
|
||||
disabled={true}
|
||||
error={errorData.type}
|
||||
value={this.props.layer.type}
|
||||
onChange={newType => this.props.onLayerChanged(
|
||||
this.props.layerIndex,
|
||||
changeType(this.props.layer, newType)
|
||||
)}
|
||||
/>
|
||||
{this.props.layer.type !== 'background' && <FieldSource
|
||||
error={errorData.source}
|
||||
sourceIds={Object.keys(this.props.sources)}
|
||||
value={this.props.layer.source}
|
||||
onChange={v => this.changeProperty(null, 'source', v)}
|
||||
/>
|
||||
}
|
||||
{['background', 'raster', 'hillshade', 'heatmap'].indexOf(this.props.layer.type) < 0 &&
|
||||
<FieldSourceLayer
|
||||
error={errorData['source-layer']}
|
||||
sourceLayerIds={sourceLayerIds}
|
||||
value={this.props.layer['source-layer']}
|
||||
onChange={v => this.changeProperty(null, 'source-layer', v)}
|
||||
/>
|
||||
}
|
||||
<FieldMinZoom
|
||||
error={errorData.minzoom}
|
||||
value={this.props.layer.minzoom}
|
||||
onChange={v => this.changeProperty(null, 'minzoom', v)}
|
||||
/>
|
||||
<FieldMaxZoom
|
||||
error={errorData.maxzoom}
|
||||
value={this.props.layer.maxzoom}
|
||||
onChange={v => this.changeProperty(null, 'maxzoom', v)}
|
||||
/>
|
||||
<FieldComment
|
||||
error={errorData.comment}
|
||||
value={comment}
|
||||
onChange={v => this.changeProperty('metadata', 'maputnik:comment', v == "" ? undefined : v)}
|
||||
/>
|
||||
</div>
|
||||
case 'filter': return <div>
|
||||
<div className="maputnik-filter-editor-wrapper">
|
||||
<FilterEditor
|
||||
errors={errorData}
|
||||
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
|
||||
errors={errorData}
|
||||
layer={this.props.layer}
|
||||
groupFields={fields}
|
||||
spec={this.props.spec}
|
||||
onChange={this.changeProperty.bind(this)}
|
||||
/>
|
||||
case 'jsoneditor':
|
||||
return <FieldJson
|
||||
layer={this.props.layer}
|
||||
onChange={(layer) => {
|
||||
this.props.onLayerChanged(
|
||||
this.props.layerIndex,
|
||||
layer
|
||||
);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
}
|
||||
|
||||
moveLayer(offset) {
|
||||
this.props.onMoveLayer({
|
||||
oldIndex: this.props.layerIndex,
|
||||
newIndex: this.props.layerIndex+offset
|
||||
})
|
||||
}
|
||||
|
||||
render() {
|
||||
const groupIds = [];
|
||||
const layerType = this.props.layer.type
|
||||
const groups = layoutGroups(layerType).filter(group => {
|
||||
return !(layerType === 'background' && group.type === 'source')
|
||||
}).map(group => {
|
||||
const groupId = group.title.replace(/ /g, "_");
|
||||
groupIds.push(groupId);
|
||||
return <LayerEditorGroup
|
||||
data-wd-key={group.title}
|
||||
id={groupId}
|
||||
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.layerIndex)
|
||||
},
|
||||
duplicate: {
|
||||
text: "Duplicate",
|
||||
handler: () => this.props.onLayerCopy(this.props.layerIndex)
|
||||
},
|
||||
hide: {
|
||||
text: (layout.visibility === "none") ? "Show" : "Hide",
|
||||
handler: () => this.props.onLayerVisibilityToggle(this.props.layerIndex)
|
||||
},
|
||||
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 <section className="maputnik-layer-editor"
|
||||
role="main"
|
||||
aria-label="Layer editor"
|
||||
>
|
||||
<header>
|
||||
<div className="layer-header">
|
||||
<h2 className="layer-header__title">
|
||||
Layer: {formatLayerId(this.props.layer.id)}
|
||||
</h2>
|
||||
<div className="layer-header__info">
|
||||
<Wrapper
|
||||
className='more-menu'
|
||||
onSelection={handleSelection}
|
||||
closeOnSelection={false}
|
||||
>
|
||||
<Button id="skip-target-layer-editor" className='more-menu__button' title="Layer options">
|
||||
<MdMoreVert className="more-menu__button__svg" />
|
||||
</Button>
|
||||
<Menu>
|
||||
<ul className="more-menu__menu">
|
||||
{Object.keys(items).map((id, idx) => {
|
||||
const item = items[id];
|
||||
return <li key={id}>
|
||||
<MenuItem value={id} className='more-menu__menu__item'>
|
||||
{item.text}
|
||||
</MenuItem>
|
||||
</li>
|
||||
})}
|
||||
</ul>
|
||||
</Menu>
|
||||
</Wrapper>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</header>
|
||||
<Accordion
|
||||
allowMultipleExpanded={true}
|
||||
allowZeroExpanded={true}
|
||||
preExpanded={groupIds}
|
||||
>
|
||||
{groups}
|
||||
</Accordion>
|
||||
</section>
|
||||
}
|
||||
}
|
||||
51
src/components/LayerEditorGroup.jsx
Normal file
51
src/components/LayerEditorGroup.jsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import Icon from '@mdi/react'
|
||||
import {
|
||||
mdiMenuDown,
|
||||
mdiMenuUp
|
||||
} from '@mdi/js';
|
||||
import {
|
||||
AccordionItem,
|
||||
AccordionItemHeading,
|
||||
AccordionItemButton,
|
||||
AccordionItemPanel,
|
||||
} from 'react-accessible-accordion';
|
||||
|
||||
|
||||
export default class LayerEditorGroup extends React.Component {
|
||||
static propTypes = {
|
||||
"id": PropTypes.string,
|
||||
"data-wd-key": PropTypes.string,
|
||||
title: PropTypes.string.isRequired,
|
||||
isActive: PropTypes.bool.isRequired,
|
||||
children: PropTypes.element.isRequired,
|
||||
onActiveToggle: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
render() {
|
||||
return <AccordionItem uuid={this.props.id}>
|
||||
<AccordionItemHeading className="maputnik-layer-editor-group"
|
||||
data-wd-key={"layer-editor-group:"+this.props["data-wd-key"]}
|
||||
onClick={e => this.props.onActiveToggle(!this.props.isActive)}
|
||||
>
|
||||
<AccordionItemButton className="maputnik-layer-editor-group__button">
|
||||
<span style={{flexGrow: 1}}>{this.props.title}</span>
|
||||
<Icon
|
||||
path={mdiMenuUp}
|
||||
size={1}
|
||||
className="maputnik-layer-editor-group__button__icon maputnik-layer-editor-group__button__icon--up"
|
||||
/>
|
||||
<Icon
|
||||
path={mdiMenuDown}
|
||||
size={1}
|
||||
className="maputnik-layer-editor-group__button__icon maputnik-layer-editor-group__button__icon--down"
|
||||
/>
|
||||
</AccordionItemButton>
|
||||
</AccordionItemHeading>
|
||||
<AccordionItemPanel>
|
||||
{this.props.children}
|
||||
</AccordionItemPanel>
|
||||
</AccordionItem>
|
||||
}
|
||||
}
|
||||
332
src/components/LayerList.jsx
Normal file
332
src/components/LayerList.jsx
Normal file
@@ -0,0 +1,332 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import classnames from 'classnames'
|
||||
import lodash from 'lodash';
|
||||
|
||||
import LayerListGroup from './LayerListGroup'
|
||||
import LayerListItem from './LayerListItem'
|
||||
import ModalAdd from './ModalAdd'
|
||||
|
||||
import {SortableContainer} from 'react-sortable-hoc';
|
||||
|
||||
const layerListPropTypes = {
|
||||
layers: PropTypes.array.isRequired,
|
||||
selectedLayerIndex: PropTypes.number.isRequired,
|
||||
onLayersChange: PropTypes.func.isRequired,
|
||||
onLayerSelect: PropTypes.func,
|
||||
sources: PropTypes.object.isRequired,
|
||||
}
|
||||
|
||||
function layerPrefix(name) {
|
||||
return name.replace(' ', '-').replace('_', '-').split('-')[0]
|
||||
}
|
||||
|
||||
function findClosestCommonPrefix(layers, idx) {
|
||||
const currentLayerPrefix = layerPrefix(layers[idx].id)
|
||||
let closestIdx = idx
|
||||
for (let i = idx; i > 0; i--) {
|
||||
const previousLayerPrefix = layerPrefix(layers[i-1].id)
|
||||
if(previousLayerPrefix === currentLayerPrefix) {
|
||||
closestIdx = i - 1
|
||||
} else {
|
||||
return closestIdx
|
||||
}
|
||||
}
|
||||
return closestIdx
|
||||
}
|
||||
|
||||
let UID = 0;
|
||||
|
||||
// List of collapsible layer editors
|
||||
class LayerListContainer extends React.Component {
|
||||
static propTypes = {...layerListPropTypes}
|
||||
static defaultProps = {
|
||||
onLayerSelect: () => {},
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.selectedItemRef = React.createRef();
|
||||
this.scrollContainerRef = React.createRef();
|
||||
this.state = {
|
||||
collapsedGroups: {},
|
||||
areAllGroupsExpanded: false,
|
||||
keys: {
|
||||
add: UID++,
|
||||
},
|
||||
isOpen: {
|
||||
add: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
toggleModal(modalName) {
|
||||
this.setState({
|
||||
keys: {
|
||||
...this.state.keys,
|
||||
[modalName]: UID++,
|
||||
},
|
||||
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 = []
|
||||
const layerIdCount = new Map();
|
||||
|
||||
for (let i = 0; i < this.props.layers.length; i++) {
|
||||
const origLayer = this.props.layers[i];
|
||||
const previousLayer = this.props.layers[i-1]
|
||||
layerIdCount.set(origLayer.id,
|
||||
layerIdCount.has(origLayer.id) ? layerIdCount.get(origLayer.id) + 1 : 0
|
||||
);
|
||||
const layer = {
|
||||
...origLayer,
|
||||
key: `layers-list-${origLayer.id}-${layerIdCount.get(origLayer.id)}`,
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
shouldComponentUpdate (nextProps, nextState) {
|
||||
// Always update on state change
|
||||
if (this.state !== nextState) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// This component tree only requires id and visibility from the layers
|
||||
// objects
|
||||
function getRequiredProps (layer) {
|
||||
const out = {
|
||||
id: layer.id,
|
||||
};
|
||||
|
||||
if (layer.layout) {
|
||||
out.layout = {
|
||||
visibility: layer.layout.visibility
|
||||
};
|
||||
}
|
||||
return out;
|
||||
}
|
||||
const layersEqual = lodash.isEqual(
|
||||
nextProps.layers.map(getRequiredProps),
|
||||
this.props.layers.map(getRequiredProps),
|
||||
);
|
||||
|
||||
function withoutLayers (props) {
|
||||
const out = {
|
||||
...props
|
||||
};
|
||||
delete out['layers'];
|
||||
return out;
|
||||
}
|
||||
|
||||
// Compare the props without layers because we've already compared them
|
||||
// efficiently above.
|
||||
const propsEqual = lodash.isEqual(
|
||||
withoutLayers(this.props),
|
||||
withoutLayers(nextProps)
|
||||
);
|
||||
|
||||
const propsChanged = !(layersEqual && propsEqual);
|
||||
return propsChanged;
|
||||
}
|
||||
|
||||
componentDidUpdate (prevProps) {
|
||||
if (prevProps.selectedLayerIndex !== this.props.selectedLayerIndex) {
|
||||
const selectedItemNode = this.selectedItemRef.current;
|
||||
if (selectedItemNode && selectedItemNode.node) {
|
||||
const target = selectedItemNode.node;
|
||||
const options = {
|
||||
root: this.scrollContainerRef.current,
|
||||
threshold: 1.0
|
||||
}
|
||||
const observer = new IntersectionObserver(entries => {
|
||||
observer.unobserve(target);
|
||||
if (entries.length > 0 && entries[0].intersectionRatio < 1) {
|
||||
target.scrollIntoView();
|
||||
}
|
||||
}, options);
|
||||
|
||||
observer.observe(target);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
const listItems = []
|
||||
let idx = 0
|
||||
const layersByGroup = this.groupedLayers();
|
||||
layersByGroup.forEach(layers => {
|
||||
const groupPrefix = layerPrefix(layers[0].id)
|
||||
if(layers.length > 1) {
|
||||
const grp = <LayerListGroup
|
||||
data-wd-key={[groupPrefix, idx].join('-')}
|
||||
aria-controls={layers.map(l => l.key).join(" ")}
|
||||
key={`group-${groupPrefix}-${idx}`}
|
||||
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 layerError = this.props.errors.find(error => {
|
||||
return (
|
||||
error.parsed &&
|
||||
error.parsed.type === "layer" &&
|
||||
error.parsed.data.index == idx
|
||||
);
|
||||
});
|
||||
|
||||
const additionalProps = {};
|
||||
if (idx === this.props.selectedLayerIndex) {
|
||||
additionalProps.ref = this.selectedItemRef;
|
||||
}
|
||||
|
||||
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,
|
||||
'maputnik-layer-list-item--error': !!layerError
|
||||
})}
|
||||
index={idx}
|
||||
key={layer.key}
|
||||
id={layer.key}
|
||||
layerId={layer.id}
|
||||
layerIndex={idx}
|
||||
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)}
|
||||
{...additionalProps}
|
||||
/>
|
||||
listItems.push(listItem)
|
||||
idx += 1
|
||||
})
|
||||
})
|
||||
|
||||
return <section
|
||||
className="maputnik-layer-list"
|
||||
role="complementary"
|
||||
aria-label="Layers list"
|
||||
ref={this.scrollContainerRef}
|
||||
>
|
||||
<ModalAdd
|
||||
key={this.state.keys.add}
|
||||
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-target-layer-list"
|
||||
onClick={this.toggleLayers}
|
||||
className="maputnik-button">
|
||||
{this.state.areAllGroupsExpanded === true ? "Collapse" : "Expand"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="maputnik-default-property">
|
||||
<div className="maputnik-multibutton">
|
||||
<button
|
||||
onClick={this.toggleModal.bind(this, 'add')}
|
||||
data-wd-key="layer-list:add-layer"
|
||||
className="maputnik-button maputnik-button-selected">
|
||||
Add Layer
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<div
|
||||
role="navigation"
|
||||
aria-label="Layers list"
|
||||
>
|
||||
<ul className="maputnik-layer-list-container">
|
||||
{listItems}
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
}
|
||||
}
|
||||
|
||||
const LayerListContainerSortable = SortableContainer((props) => <LayerListContainer {...props} />)
|
||||
|
||||
export default class LayerList extends React.Component {
|
||||
static propTypes = {...layerListPropTypes}
|
||||
|
||||
render() {
|
||||
return <LayerListContainerSortable
|
||||
{...this.props}
|
||||
helperClass='sortableHelper'
|
||||
onSortEnd={this.props.onMoveLayer.bind(this)}
|
||||
useDragHandle={true}
|
||||
shouldCancelStart={() => false}
|
||||
/>
|
||||
}
|
||||
}
|
||||
35
src/components/LayerListGroup.jsx
Normal file
35
src/components/LayerListGroup.jsx
Normal file
@@ -0,0 +1,35 @@
|
||||
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,
|
||||
'aria-controls': PropTypes.string,
|
||||
}
|
||||
|
||||
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)}
|
||||
>
|
||||
<button
|
||||
className="maputnik-layer-list-group-title"
|
||||
aria-controls={this.props['aria-controls']}
|
||||
aria-expanded={this.props.isActive}
|
||||
>
|
||||
{this.props.title}
|
||||
</button>
|
||||
<span className="maputnik-space" />
|
||||
<Collapser
|
||||
style={{ height: 14, width: 14 }}
|
||||
isCollapsed={this.props.isActive}
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
}
|
||||
}
|
||||
139
src/components/LayerListItem.jsx
Normal file
139
src/components/LayerListItem.jsx
Normal file
@@ -0,0 +1,139 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import classnames from 'classnames'
|
||||
|
||||
import {MdContentCopy, MdVisibility, MdVisibilityOff, MdDelete} from 'react-icons/md'
|
||||
|
||||
import IconLayer from './IconLayer'
|
||||
import {SortableElement, SortableHandle} from 'react-sortable-hoc'
|
||||
|
||||
|
||||
const DraggableLabel = SortableHandle((props) => {
|
||||
return <div className="maputnik-layer-list-item-handle">
|
||||
<IconLayer
|
||||
className="layer-handle__icon"
|
||||
type={props.layerType}
|
||||
/>
|
||||
<button className="maputnik-layer-list-item-id">
|
||||
{props.layerId}
|
||||
</button>
|
||||
</div>
|
||||
});
|
||||
|
||||
class IconAction extends React.Component {
|
||||
static propTypes = {
|
||||
action: PropTypes.string.isRequired,
|
||||
onClick: PropTypes.func.isRequired,
|
||||
wdKey: PropTypes.string,
|
||||
classBlockName: PropTypes.string,
|
||||
classBlockModifier: PropTypes.string,
|
||||
}
|
||||
|
||||
renderIcon() {
|
||||
switch(this.props.action) {
|
||||
case 'duplicate': return <MdContentCopy />
|
||||
case 'show': return <MdVisibility />
|
||||
case 'hide': return <MdVisibilityOff />
|
||||
case 'delete': return <MdDelete />
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {classBlockName, classBlockModifier} = this.props;
|
||||
|
||||
let classAdditions = '';
|
||||
if (classBlockName) {
|
||||
classAdditions = `maputnik-layer-list-icon-action__${classBlockName}`;
|
||||
|
||||
if (classBlockModifier) {
|
||||
classAdditions += ` maputnik-layer-list-icon-action__${classBlockName}--${classBlockModifier}`;
|
||||
}
|
||||
}
|
||||
|
||||
return <button
|
||||
tabIndex="-1"
|
||||
title={this.props.action}
|
||||
className={`maputnik-layer-list-icon-action ${classAdditions}`}
|
||||
data-wd-key={this.props.wdKey}
|
||||
onClick={this.props.onClick}
|
||||
aria-hidden="true"
|
||||
>
|
||||
{this.renderIcon()}
|
||||
</button>
|
||||
}
|
||||
}
|
||||
|
||||
class LayerListItem extends React.Component {
|
||||
static propTypes = {
|
||||
layerIndex: PropTypes.number.isRequired,
|
||||
layerId: PropTypes.string.isRequired,
|
||||
layerType: PropTypes.string.isRequired,
|
||||
isSelected: PropTypes.bool,
|
||||
visibility: PropTypes.string,
|
||||
className: PropTypes.string,
|
||||
|
||||
onLayerSelect: PropTypes.func.isRequired,
|
||||
onLayerCopy: PropTypes.func,
|
||||
onLayerDestroy: PropTypes.func,
|
||||
onLayerVisibilityToggle: PropTypes.func,
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
isSelected: false,
|
||||
visibility: 'visible',
|
||||
onLayerCopy: () => {},
|
||||
onLayerDestroy: () => {},
|
||||
onLayerVisibilityToggle: () => {},
|
||||
}
|
||||
|
||||
static childContextTypes = {
|
||||
reactIconBase: PropTypes.object
|
||||
}
|
||||
|
||||
getChildContext() {
|
||||
return {
|
||||
reactIconBase: { size: 14 }
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const visibilityAction = this.props.visibility === 'visible' ? 'show' : 'hide';
|
||||
|
||||
return <li
|
||||
id={this.props.id}
|
||||
key={this.props.layerId}
|
||||
onClick={e => this.props.onLayerSelect(this.props.layerIndex)}
|
||||
data-wd-key={"layer-list-item:"+this.props.layerId}
|
||||
className={classnames({
|
||||
"maputnik-layer-list-item": true,
|
||||
"maputnik-layer-list-item-selected": this.props.isSelected,
|
||||
[this.props.className]: true,
|
||||
})}>
|
||||
<DraggableLabel {...this.props} />
|
||||
<span style={{flexGrow: 1}} />
|
||||
<IconAction
|
||||
wdKey={"layer-list-item:"+this.props.layerId+":delete"}
|
||||
action={'delete'}
|
||||
classBlockName="delete"
|
||||
onClick={e => this.props.onLayerDestroy(this.props.layerIndex)}
|
||||
/>
|
||||
<IconAction
|
||||
wdKey={"layer-list-item:"+this.props.layerId+":copy"}
|
||||
action={'duplicate'}
|
||||
classBlockName="duplicate"
|
||||
onClick={e => this.props.onLayerCopy(this.props.layerIndex)}
|
||||
/>
|
||||
<IconAction
|
||||
wdKey={"layer-list-item:"+this.props.layerId+":toggle-visibility"}
|
||||
action={visibilityAction}
|
||||
classBlockName="visibility"
|
||||
classBlockModifier={visibilityAction}
|
||||
onClick={e => this.props.onLayerVisibilityToggle(this.props.layerIndex)}
|
||||
/>
|
||||
</li>
|
||||
}
|
||||
}
|
||||
|
||||
const LayerListItemSortable = SortableElement((props) => <LayerListItem {...props} />);
|
||||
|
||||
export default LayerListItemSortable;
|
||||
247
src/components/MapMapboxGl.jsx
Normal file
247
src/components/MapMapboxGl.jsx
Normal file
@@ -0,0 +1,247 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import ReactDOM from 'react-dom'
|
||||
import MapLibreGl from 'maplibre-gl'
|
||||
import MapboxInspect from 'mapbox-gl-inspect'
|
||||
import MapMapboxGlLayerPopup from './MapMapboxGlLayerPopup'
|
||||
import MapMapboxGlFeaturePropertyPopup from './MapMapboxGlFeaturePropertyPopup'
|
||||
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 'maplibre-gl/dist/maplibre-gl.css'
|
||||
import '../mapboxgl.css'
|
||||
import '../libs/mapbox-rtl'
|
||||
|
||||
|
||||
const IS_SUPPORTED = MapLibreGl.supported();
|
||||
|
||||
function renderPopup(popup, mountNode) {
|
||||
ReactDOM.render(popup, mountNode);
|
||||
return mountNode;
|
||||
}
|
||||
|
||||
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 MapMapboxGl 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,
|
||||
replaceAccessTokens: PropTypes.func.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
onMapLoaded: () => {},
|
||||
onDataChange: () => {},
|
||||
onLayerSelect: () => {},
|
||||
onChange: () => {},
|
||||
mapboxAccessToken: tokens.mapbox,
|
||||
options: {},
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
map: null,
|
||||
inspect: null,
|
||||
}
|
||||
}
|
||||
|
||||
updateMapFromProps(props) {
|
||||
if(!IS_SUPPORTED) return;
|
||||
|
||||
if(!this.state.map) return
|
||||
|
||||
//Mapbox GL now does diffing natively so we don't need to calculate
|
||||
//the necessary operations ourselves!
|
||||
this.state.map.setStyle(
|
||||
this.props.replaceAccessTokens(props.mapStyle),
|
||||
{diff: true}
|
||||
)
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
let should = false;
|
||||
try {
|
||||
should = JSON.stringify(this.props) !== JSON.stringify(nextProps) || JSON.stringify(this.state) !== JSON.stringify(nextState);
|
||||
} catch(e) {
|
||||
// no biggie, carry on
|
||||
}
|
||||
return should;
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState, snapshot) {
|
||||
if(!IS_SUPPORTED) return;
|
||||
|
||||
const map = this.state.map;
|
||||
|
||||
this.updateMapFromProps(this.props);
|
||||
|
||||
if(this.state.inspect && this.props.inspectModeEnabled !== this.state.inspect._showInspectMap) {
|
||||
// HACK: Fix for <https://github.com/maputnik/editor/issues/576>, while we wait for a proper fix.
|
||||
// eslint-disable-next-line
|
||||
this.state.inspect._popupBlocked = false;
|
||||
this.state.inspect.toggleInspector()
|
||||
}
|
||||
if (map) {
|
||||
if (this.props.inspectModeEnabled) {
|
||||
// HACK: We need to work out why we need to do this and what's causing
|
||||
// this error. I'm assuming an issue with mapbox-gl update and
|
||||
// mapbox-gl-inspect.
|
||||
try {
|
||||
this.state.inspect.render();
|
||||
} catch(err) {
|
||||
console.error("FIXME: Caught error", err);
|
||||
}
|
||||
}
|
||||
|
||||
map.showTileBoundaries = this.props.options.showTileBoundaries;
|
||||
map.showCollisionBoxes = this.props.options.showCollisionBoxes;
|
||||
map.showOverdrawInspector = this.props.options.showOverdrawInspector;
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if(!IS_SUPPORTED) return;
|
||||
|
||||
const mapOpts = {
|
||||
...this.props.options,
|
||||
container: this.container,
|
||||
style: this.props.mapStyle,
|
||||
hash: true,
|
||||
maxZoom: 24
|
||||
}
|
||||
|
||||
const map = new MapLibreGl.Map(mapOpts);
|
||||
|
||||
const mapViewChange = () => {
|
||||
const center = map.getCenter();
|
||||
const zoom = map.getZoom();
|
||||
this.props.onChange({center, zoom});
|
||||
}
|
||||
mapViewChange();
|
||||
|
||||
map.showTileBoundaries = mapOpts.showTileBoundaries;
|
||||
map.showCollisionBoxes = mapOpts.showCollisionBoxes;
|
||||
map.showOverdrawInspector = mapOpts.showOverdrawInspector;
|
||||
|
||||
const zoomControl = new ZoomControl;
|
||||
map.addControl(zoomControl, 'top-right');
|
||||
|
||||
const nav = new MapLibreGl.NavigationControl({visualizePitch:true});
|
||||
map.addControl(nav, 'top-right');
|
||||
|
||||
const tmpNode = document.createElement('div');
|
||||
|
||||
const inspect = new MapboxInspect({
|
||||
popup: new MapLibreGl.Popup({
|
||||
closeOnClick: false
|
||||
}),
|
||||
showMapPopup: true,
|
||||
showMapPopupOnHover: false,
|
||||
showInspectMapPopupOnHover: true,
|
||||
showInspectButton: false,
|
||||
blockHoverPopupOnClick: true,
|
||||
assignLayerColor: (layerId, alpha) => {
|
||||
return Color(colors.brightColor(layerId, alpha)).desaturate(0.5).string()
|
||||
},
|
||||
buildInspectStyle: (originalMapStyle, coloredLayers) => buildInspectStyle(originalMapStyle, coloredLayers, this.props.highlightedLayer),
|
||||
renderPopup: features => {
|
||||
if(this.props.inspectModeEnabled) {
|
||||
return renderPopup(<MapMapboxGlFeaturePropertyPopup features={features} />, tmpNode);
|
||||
} else {
|
||||
return renderPopup(<MapMapboxGlLayerPopup features={features} onLayerSelect={this.onLayerSelectById} zoom={this.state.zoom} />, tmpNode);
|
||||
}
|
||||
}
|
||||
})
|
||||
map.addControl(inspect)
|
||||
|
||||
map.on("style.load", () => {
|
||||
this.setState({
|
||||
map,
|
||||
inspect,
|
||||
zoom: map.getZoom()
|
||||
});
|
||||
})
|
||||
|
||||
map.on("data", e => {
|
||||
if(e.dataType !== 'tile') return
|
||||
this.props.onDataChange({
|
||||
map: this.state.map
|
||||
})
|
||||
})
|
||||
|
||||
map.on("error", e => {
|
||||
console.log("ERROR", e);
|
||||
})
|
||||
|
||||
map.on("zoom", e => {
|
||||
this.setState({
|
||||
zoom: map.getZoom()
|
||||
});
|
||||
});
|
||||
|
||||
map.on("dragend", mapViewChange);
|
||||
map.on("zoomend", mapViewChange);
|
||||
}
|
||||
|
||||
onLayerSelectById = (id) => {
|
||||
const index = this.props.mapStyle.layers.findIndex(layer => layer.id === id);
|
||||
this.props.onLayerSelect(index);
|
||||
}
|
||||
|
||||
render() {
|
||||
if(IS_SUPPORTED) {
|
||||
return <div
|
||||
className="maputnik-map__map"
|
||||
role="region"
|
||||
aria-label="Map view"
|
||||
ref={x => this.container = x}
|
||||
></div>
|
||||
}
|
||||
else {
|
||||
return <div
|
||||
className="maputnik-map maputnik-map--error"
|
||||
>
|
||||
<div className="maputnik-map__error-message">
|
||||
Error: Cannot load MapboxGL, WebGL is either unsupported or disabled
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
78
src/components/MapMapboxGlFeaturePropertyPopup.jsx
Normal file
78
src/components/MapMapboxGlFeaturePropertyPopup.jsx
Normal file
@@ -0,0 +1,78 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import Block from './Block'
|
||||
import FieldString from './FieldString'
|
||||
|
||||
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 <Block key={propertyName} label={propertyName}>
|
||||
<FieldString value={displayValue(property)} style={{backgroundColor: 'transparent'}}/>
|
||||
</Block>
|
||||
})
|
||||
}
|
||||
|
||||
function renderFeatureId(feature) {
|
||||
return <Block key={"feature-id"} label={"feature_id"}>
|
||||
<FieldString value={displayValue(feature.id)} style={{backgroundColor: 'transparent'}} />
|
||||
</Block>
|
||||
}
|
||||
|
||||
function renderFeature(feature, idx) {
|
||||
return <div key={`${feature.sourceLayer}-${idx}`}>
|
||||
<div className="maputnik-popup-layer-id">{feature.layer['source']}: {feature.layer['source-layer']}{feature.inspectModeCounter && <span> × {feature.inspectModeCounter}</span>}</div>
|
||||
<Block key={"property-type"} label={"$type"}>
|
||||
<FieldString value={feature.geometry.type} style={{backgroundColor: 'transparent'}} />
|
||||
</Block>
|
||||
{renderFeatureId(feature)}
|
||||
{renderProperties(feature)}
|
||||
</div>
|
||||
}
|
||||
|
||||
function removeDuplicatedFeatures(features) {
|
||||
let uniqueFeatures = [];
|
||||
|
||||
features.forEach(feature => {
|
||||
const featureIndex = uniqueFeatures.findIndex(feature2 => {
|
||||
return feature.layer['source-layer'] === feature2.layer['source-layer']
|
||||
&& JSON.stringify(feature.properties) === JSON.stringify(feature2.properties)
|
||||
})
|
||||
|
||||
if(featureIndex === -1) {
|
||||
uniqueFeatures.push(feature)
|
||||
} else {
|
||||
if(uniqueFeatures[featureIndex].hasOwnProperty('inspectModeCounter')) {
|
||||
uniqueFeatures[featureIndex].inspectModeCounter++
|
||||
} else {
|
||||
uniqueFeatures[featureIndex].inspectModeCounter = 2
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return uniqueFeatures
|
||||
}
|
||||
|
||||
class FeaturePropertyPopup extends React.Component {
|
||||
static propTypes = {
|
||||
features: PropTypes.array
|
||||
}
|
||||
|
||||
render() {
|
||||
const features = removeDuplicatedFeatures(this.props.features)
|
||||
return <div className="maputnik-feature-property-popup">
|
||||
{features.map(renderFeature)}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default FeaturePropertyPopup
|
||||
123
src/components/MapMapboxGlLayerPopup.jsx
Normal file
123
src/components/MapMapboxGlLayerPopup.jsx
Normal file
@@ -0,0 +1,123 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import IconLayer from './IconLayer'
|
||||
import {latest} from '@maplibre/maplibre-gl-style-spec'
|
||||
|
||||
function groupFeaturesBySourceLayer(features) {
|
||||
const sources = {}
|
||||
|
||||
let returnedFeatures = {};
|
||||
|
||||
features.forEach(feature => {
|
||||
if(returnedFeatures.hasOwnProperty(feature.layer.id)) {
|
||||
returnedFeatures[feature.layer.id]++
|
||||
|
||||
const featureObject = sources[feature.layer['source-layer']].find(f => f.layer.id === feature.layer.id)
|
||||
|
||||
featureObject.counter = returnedFeatures[feature.layer.id]
|
||||
} else {
|
||||
sources[feature.layer['source-layer']] = sources[feature.layer['source-layer']] || []
|
||||
sources[feature.layer['source-layer']].push(feature)
|
||||
|
||||
returnedFeatures[feature.layer.id] = 1
|
||||
}
|
||||
})
|
||||
|
||||
return sources
|
||||
}
|
||||
|
||||
class FeatureLayerPopup extends React.Component {
|
||||
static propTypes = {
|
||||
onLayerSelect: PropTypes.func.isRequired,
|
||||
features: PropTypes.array,
|
||||
zoom: PropTypes.number,
|
||||
}
|
||||
|
||||
_getFeatureColor(feature, zoom) {
|
||||
// Guard because openlayers won't have this
|
||||
if (!feature.layer.paint) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const paintProps = feature.layer.paint;
|
||||
let propName;
|
||||
|
||||
if(paintProps.hasOwnProperty("text-color") && paintProps["text-color"]) {
|
||||
propName = "text-color";
|
||||
}
|
||||
else if (paintProps.hasOwnProperty("fill-color") && paintProps["fill-color"]) {
|
||||
propName = "fill-color";
|
||||
}
|
||||
else if (paintProps.hasOwnProperty("line-color") && paintProps["line-color"]) {
|
||||
propName = "line-color";
|
||||
}
|
||||
else if (paintProps.hasOwnProperty("fill-extrusion-color") && paintProps["fill-extrusion-color"]) {
|
||||
propName = "fill-extrusion-color";
|
||||
}
|
||||
|
||||
if(propName) {
|
||||
const propertySpec = latest["paint_"+feature.layer.type][propName];
|
||||
let color = feature.layer.paint[propName];
|
||||
return String(color);
|
||||
}
|
||||
else {
|
||||
// Default color
|
||||
return "black";
|
||||
}
|
||||
}
|
||||
// This is quite complex, just incase there's an edgecase we're missing
|
||||
// always return black if we get an unexpected error.
|
||||
catch (err) {
|
||||
console.warn("Unable to get feature color, error:", err);
|
||||
return "black";
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const sources = groupFeaturesBySourceLayer(this.props.features)
|
||||
|
||||
const items = Object.keys(sources).map(vectorLayerId => {
|
||||
const layers = sources[vectorLayerId].map((feature, idx) => {
|
||||
const featureColor = this._getFeatureColor(feature, this.props.zoom);
|
||||
|
||||
return <div
|
||||
key={idx}
|
||||
className="maputnik-popup-layer"
|
||||
>
|
||||
<div
|
||||
className="maputnik-popup-layer__swatch"
|
||||
style={{background: featureColor}}
|
||||
></div>
|
||||
<label
|
||||
className="maputnik-popup-layer__label"
|
||||
onClick={() => {
|
||||
this.props.onLayerSelect(feature.layer.id)
|
||||
}}
|
||||
>
|
||||
{feature.layer.type &&
|
||||
<IconLayer type={feature.layer.type} style={{
|
||||
width: 14,
|
||||
height: 14,
|
||||
paddingRight: 3
|
||||
}}/>
|
||||
}
|
||||
{feature.layer.id}
|
||||
{feature.counter && <span> × {feature.counter}</span>}
|
||||
</label>
|
||||
</div>
|
||||
})
|
||||
return <div key={vectorLayerId}>
|
||||
<div className="maputnik-popup-layer-id">{vectorLayerId}</div>
|
||||
{layers}
|
||||
</div>
|
||||
})
|
||||
|
||||
return <div className="maputnik-feature-layer-popup">
|
||||
{items}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default FeatureLayerPopup
|
||||
191
src/components/MapOpenLayers.jsx
Normal file
191
src/components/MapOpenLayers.jsx
Normal file
@@ -0,0 +1,191 @@
|
||||
import React from 'react'
|
||||
import {throttle} from 'lodash';
|
||||
import PropTypes from 'prop-types'
|
||||
import { loadJSON } from '../libs/urlopen'
|
||||
|
||||
import MapMapboxGlLayerPopup from './MapMapboxGlLayerPopup';
|
||||
|
||||
import 'ol/ol.css'
|
||||
import {apply} from 'ol-mapbox-style';
|
||||
import {Map, View, Proj, Overlay} from 'ol';
|
||||
|
||||
import {toLonLat} from 'ol/proj';
|
||||
import {toStringHDMS} from 'ol/coordinate';
|
||||
|
||||
|
||||
function renderCoords (coords) {
|
||||
if (!coords || coords.length < 2) {
|
||||
return null;
|
||||
}
|
||||
else {
|
||||
return <span className="maputnik-coords">
|
||||
{coords.map((coord) => String(coord).padStart(7, "\u00A0")).join(', ')}
|
||||
</span>
|
||||
}
|
||||
}
|
||||
|
||||
export default class MapOpenLayers extends React.Component {
|
||||
static propTypes = {
|
||||
onDataChange: PropTypes.func,
|
||||
mapStyle: PropTypes.object.isRequired,
|
||||
accessToken: PropTypes.string,
|
||||
style: PropTypes.object,
|
||||
onLayerSelect: PropTypes.func.isRequired,
|
||||
debugToolbox: PropTypes.bool.isRequired,
|
||||
replaceAccessTokens: PropTypes.func.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
onMapLoaded: () => {},
|
||||
onDataChange: () => {},
|
||||
onLayerSelect: () => {},
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
zoom: 0,
|
||||
rotation: 0,
|
||||
cursor: [],
|
||||
center: [],
|
||||
};
|
||||
this.updateStyle = throttle(this._updateStyle.bind(this), 200);
|
||||
}
|
||||
|
||||
_updateStyle(newMapStyle) {
|
||||
if(!this.map) return;
|
||||
|
||||
// See <https://github.com/openlayers/ol-mapbox-style/issues/215#issuecomment-493198815>
|
||||
this.map.getLayers().clear();
|
||||
apply(this.map, newMapStyle);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (this.props.mapStyle !== prevProps.mapStyle) {
|
||||
this.updateStyle(
|
||||
this.props.replaceAccessTokens(this.props.mapStyle)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.overlay = new Overlay({
|
||||
element: this.popupContainer,
|
||||
autoPan: true,
|
||||
autoPanAnimation: {
|
||||
duration: 250
|
||||
}
|
||||
});
|
||||
|
||||
const map = new Map({
|
||||
target: this.container,
|
||||
overlays: [this.overlay],
|
||||
view: new View({
|
||||
zoom: 1,
|
||||
center: [180, -90],
|
||||
})
|
||||
});
|
||||
|
||||
map.on('pointermove', (evt) => {
|
||||
var coords = toLonLat(evt.coordinate);
|
||||
this.setState({
|
||||
cursor: [
|
||||
coords[0].toFixed(2),
|
||||
coords[1].toFixed(2)
|
||||
]
|
||||
})
|
||||
})
|
||||
|
||||
const onMoveEnd = () => {
|
||||
const zoom = map.getView().getZoom();
|
||||
const center = toLonLat(map.getView().getCenter());
|
||||
|
||||
this.props.onChange({
|
||||
zoom,
|
||||
center: {
|
||||
lng: center[0],
|
||||
lat: center[1],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
onMoveEnd();
|
||||
map.on('moveend', onMoveEnd);
|
||||
|
||||
map.on('postrender', (evt) => {
|
||||
const center = toLonLat(map.getView().getCenter());
|
||||
this.setState({
|
||||
center: [
|
||||
center[0].toFixed(2),
|
||||
center[1].toFixed(2),
|
||||
],
|
||||
rotation: map.getView().getRotation().toFixed(2),
|
||||
zoom: map.getView().getZoom().toFixed(2)
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
this.map = map;
|
||||
this.updateStyle(
|
||||
this.props.replaceAccessTokens(this.props.mapStyle)
|
||||
);
|
||||
}
|
||||
|
||||
closeOverlay = (e) => {
|
||||
e.target.blur();
|
||||
this.overlay.setPosition(undefined);
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div className="maputnik-ol-container">
|
||||
<div
|
||||
ref={x => this.popupContainer = x}
|
||||
style={{background: "black"}}
|
||||
className="maputnik-popup"
|
||||
>
|
||||
<button
|
||||
className="mapboxgl-popup-close-button"
|
||||
onClick={this.closeOverlay}
|
||||
aria-label="Close popup"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
<MapMapboxGlLayerPopup
|
||||
features={this.state.selectedFeatures || []}
|
||||
onLayerSelect={this.props.onLayerSelect}
|
||||
/>
|
||||
</div>
|
||||
<div className="maputnik-ol-zoom">
|
||||
Zoom: {this.state.zoom}
|
||||
</div>
|
||||
{this.props.debugToolbox &&
|
||||
<div className="maputnik-ol-debug">
|
||||
<div>
|
||||
<label>cursor: </label>
|
||||
<span>{renderCoords(this.state.cursor)}</span>
|
||||
</div>
|
||||
<div>
|
||||
<label>center: </label>
|
||||
<span>{renderCoords(this.state.center)}</span>
|
||||
</div>
|
||||
<div>
|
||||
<label>rotation: </label>
|
||||
<span>{this.state.rotation}</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<div
|
||||
className="maputnik-ol"
|
||||
ref={x => this.container = x}
|
||||
role="region"
|
||||
aria-label="Map view"
|
||||
style={{
|
||||
...this.props.style,
|
||||
}}>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
70
src/components/Modal.jsx
Normal file
70
src/components/Modal.jsx
Normal file
@@ -0,0 +1,70 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import {MdClose} from 'react-icons/md'
|
||||
import AriaModal from 'react-aria-modal'
|
||||
import classnames from 'classnames';
|
||||
|
||||
|
||||
export default class Modal extends React.Component {
|
||||
static propTypes = {
|
||||
"data-wd-key": PropTypes.string,
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
onOpenToggle: PropTypes.func.isRequired,
|
||||
children: PropTypes.node,
|
||||
underlayClickExits: PropTypes.bool,
|
||||
underlayProps: PropTypes.object,
|
||||
className: PropTypes.string,
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
underlayClickExits: true
|
||||
}
|
||||
|
||||
// See <https://github.com/maputnik/editor/issues/416>
|
||||
onClose = () => {
|
||||
if (document.activeElement) {
|
||||
document.activeElement.blur();
|
||||
}
|
||||
|
||||
setImmediate(() => {
|
||||
this.props.onOpenToggle(false);
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
if(this.props.isOpen) {
|
||||
return <AriaModal
|
||||
titleText={this.props.title}
|
||||
underlayClickExits={this.props.underlayClickExits}
|
||||
underlayProps={this.props.underlayProps}
|
||||
data-wd-key={this.props["data-wd-key"]}
|
||||
verticallyCenter={true}
|
||||
onExit={this.onClose}
|
||||
>
|
||||
<div className={classnames("maputnik-modal", this.props.className)}
|
||||
data-wd-key={this.props["data-wd-key"]}
|
||||
>
|
||||
<header className="maputnik-modal-header">
|
||||
<h1 className="maputnik-modal-header-title">{this.props.title}</h1>
|
||||
<span className="maputnik-modal-header-space"></span>
|
||||
<button className="maputnik-modal-header-toggle"
|
||||
title="Close modal"
|
||||
onClick={this.onClose}
|
||||
data-wd-key={this.props["data-wd-key"]+".close-modal"}
|
||||
>
|
||||
<MdClose />
|
||||
</button>
|
||||
</header>
|
||||
<div className="maputnik-modal-scroller">
|
||||
<div className="maputnik-modal-content">{this.props.children}</div>
|
||||
</div>
|
||||
</div>
|
||||
</AriaModal>
|
||||
}
|
||||
else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
174
src/components/ModalAdd.jsx
Normal file
174
src/components/ModalAdd.jsx
Normal file
@@ -0,0 +1,174 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import {latest} from '@maplibre/maplibre-gl-style-spec'
|
||||
import InputButton from './InputButton'
|
||||
import Modal from './Modal'
|
||||
|
||||
import FieldType from './FieldType'
|
||||
import FieldId from './FieldId'
|
||||
import FieldSource from './FieldSource'
|
||||
import FieldSourceLayer from './FieldSourceLayer'
|
||||
|
||||
export default class ModalAdd extends React.Component {
|
||||
static propTypes = {
|
||||
layers: PropTypes.array.isRequired,
|
||||
onLayersChange: PropTypes.func.isRequired,
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onOpenToggle: PropTypes.func.isRequired,
|
||||
|
||||
// A dict of source id's and the available source layers
|
||||
sources: PropTypes.object.isRequired,
|
||||
}
|
||||
|
||||
addLayer = () => {
|
||||
const changedLayers = this.props.layers.slice(0)
|
||||
const layer = {
|
||||
id: this.state.id,
|
||||
type: this.state.type,
|
||||
}
|
||||
|
||||
if(this.state.type !== 'background') {
|
||||
layer.source = this.state.source
|
||||
if(this.state.type !== 'raster' && this.state['source-layer']) {
|
||||
layer['source-layer'] = this.state['source-layer']
|
||||
}
|
||||
}
|
||||
|
||||
changedLayers.push(layer)
|
||||
|
||||
this.props.onLayersChange(changedLayers)
|
||||
this.props.onOpenToggle(false)
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
type: 'fill',
|
||||
id: '',
|
||||
}
|
||||
|
||||
if(props.sources.length > 0) {
|
||||
this.state.source = Object.keys(this.props.sources)[0]
|
||||
this.state['source-layer'] = this.props.sources[this.state.source][0]
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
// Check if source is valid for new type
|
||||
const oldType = prevState.type;
|
||||
const newType = this.state.type;
|
||||
|
||||
const availableSourcesOld = this.getSources(oldType);
|
||||
const availableSourcesNew = this.getSources(newType);
|
||||
|
||||
if(
|
||||
// Type has changed
|
||||
oldType !== newType
|
||||
&& prevState.source !== ""
|
||||
// Was a valid source previously
|
||||
&& availableSourcesOld.indexOf(prevState.source) > -1
|
||||
// And is not a valid source now
|
||||
&& availableSourcesNew.indexOf(this.state.source) < 0
|
||||
) {
|
||||
// Clear the source
|
||||
this.setState({
|
||||
source: ""
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
getLayersForSource(source) {
|
||||
const sourceObj = this.props.sources[source] || {};
|
||||
return sourceObj.layers || [];
|
||||
}
|
||||
|
||||
getSources(type) {
|
||||
const sources = [];
|
||||
|
||||
const types = {
|
||||
vector: [
|
||||
"fill",
|
||||
"line",
|
||||
"symbol",
|
||||
"circle",
|
||||
"fill-extrusion",
|
||||
"heatmap"
|
||||
],
|
||||
raster: [
|
||||
"raster"
|
||||
],
|
||||
geojson: [
|
||||
"fill",
|
||||
"line",
|
||||
"symbol",
|
||||
"circle",
|
||||
"fill-extrusion",
|
||||
"heatmap"
|
||||
]
|
||||
}
|
||||
|
||||
for(let [key, val] of Object.entries(this.props.sources)) {
|
||||
if(types[val.type] && types[val.type].indexOf(type) > -1) {
|
||||
sources.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
return sources;
|
||||
}
|
||||
|
||||
|
||||
render() {
|
||||
const sources = this.getSources(this.state.type);
|
||||
const layers = this.getLayersForSource(this.state.source);
|
||||
|
||||
return <Modal
|
||||
isOpen={this.props.isOpen}
|
||||
onOpenToggle={this.props.onOpenToggle}
|
||||
title={'Add Layer'}
|
||||
data-wd-key="modal:add-layer"
|
||||
className="maputnik-add-modal"
|
||||
>
|
||||
<div className="maputnik-add-layer">
|
||||
<FieldId
|
||||
label="ID"
|
||||
fieldSpec={latest.layer.id}
|
||||
value={this.state.id}
|
||||
wdKey="add-layer.layer-id"
|
||||
onChange={v => {
|
||||
this.setState({ id: v })
|
||||
}}
|
||||
/>
|
||||
<FieldType
|
||||
value={this.state.type}
|
||||
wdKey="add-layer.layer-type"
|
||||
onChange={v => this.setState({ type: v })}
|
||||
/>
|
||||
{this.state.type !== 'background' &&
|
||||
<FieldSource
|
||||
sourceIds={sources}
|
||||
wdKey="add-layer.layer-source-block"
|
||||
value={this.state.source}
|
||||
onChange={v => this.setState({ source: v })}
|
||||
/>
|
||||
}
|
||||
{['background', 'raster', 'hillshade', 'heatmap'].indexOf(this.state.type) < 0 &&
|
||||
<FieldSourceLayer
|
||||
isFixed={true}
|
||||
sourceLayerIds={layers}
|
||||
value={this.state['source-layer']}
|
||||
onChange={v => this.setState({ 'source-layer': v })}
|
||||
/>
|
||||
}
|
||||
<InputButton
|
||||
className="maputnik-add-layer-button"
|
||||
onClick={this.addLayer}
|
||||
data-wd-key="add-layer"
|
||||
>
|
||||
Add Layer
|
||||
</InputButton>
|
||||
</div>
|
||||
</Modal>
|
||||
}
|
||||
}
|
||||
|
||||
72
src/components/ModalDebug.jsx
Normal file
72
src/components/ModalDebug.jsx
Normal file
@@ -0,0 +1,72 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import Modal from './Modal'
|
||||
|
||||
|
||||
export default class ModalDebug extends React.Component {
|
||||
static propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
renderer: PropTypes.string.isRequired,
|
||||
onChangeMaboxGlDebug: PropTypes.func.isRequired,
|
||||
onChangeOpenlayersDebug: PropTypes.func.isRequired,
|
||||
onOpenToggle: PropTypes.func.isRequired,
|
||||
mapboxGlDebugOptions: PropTypes.object,
|
||||
openlayersDebugOptions: PropTypes.object,
|
||||
mapView: PropTypes.object,
|
||||
}
|
||||
|
||||
render() {
|
||||
const {mapView} = this.props;
|
||||
|
||||
const osmZoom = Math.round(mapView.zoom)+1;
|
||||
const osmLon = Number.parseFloat(mapView.center.lng).toFixed(5);
|
||||
const osmLat = Number.parseFloat(mapView.center.lat).toFixed(5);
|
||||
|
||||
return <Modal
|
||||
data-wd-key="modal:debug"
|
||||
isOpen={this.props.isOpen}
|
||||
onOpenToggle={this.props.onOpenToggle}
|
||||
title={'Debug'}
|
||||
>
|
||||
<section className="maputnik-modal-section maputnik-modal-shortcuts">
|
||||
<h1>Options</h1>
|
||||
{this.props.renderer === 'mbgljs' &&
|
||||
<ul>
|
||||
{Object.entries(this.props.mapboxGlDebugOptions).map(([key, val]) => {
|
||||
return <li key={key}>
|
||||
<label>
|
||||
<input type="checkbox" checked={val} onClick={(e) => this.props.onChangeMaboxGlDebug(key, e.target.checked)} /> {key}
|
||||
</label>
|
||||
</li>
|
||||
})}
|
||||
</ul>
|
||||
}
|
||||
{this.props.renderer === 'ol' &&
|
||||
<ul>
|
||||
{Object.entries(this.props.openlayersDebugOptions).map(([key, val]) => {
|
||||
return <li key={key}>
|
||||
<label>
|
||||
<input type="checkbox" checked={val} onClick={(e) => this.props.onChangeOpenlayersDebug(key, e.target.checked)} /> {key}
|
||||
</label>
|
||||
</li>
|
||||
})}
|
||||
</ul>
|
||||
}
|
||||
</section>
|
||||
<section className="maputnik-modal-section">
|
||||
<h1>Links</h1>
|
||||
<p>
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href={`https://www.openstreetmap.org/#map=${osmZoom}/${osmLat}/${osmLon}`}
|
||||
>
|
||||
Open in OSM
|
||||
</a> — Opens the current view on openstreetmap.org
|
||||
</p>
|
||||
</section>
|
||||
</Modal>
|
||||
}
|
||||
}
|
||||
|
||||
162
src/components/ModalExport.jsx
Normal file
162
src/components/ModalExport.jsx
Normal file
@@ -0,0 +1,162 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import Slugify from 'slugify'
|
||||
import { saveAs } from 'file-saver'
|
||||
import pkgLockJson from '../../package-lock.json'
|
||||
|
||||
import {format} from '@maplibre/maplibre-gl-style-spec'
|
||||
import FieldString from './FieldString'
|
||||
import FieldCheckbox from './FieldCheckbox'
|
||||
import InputButton from './InputButton'
|
||||
import Modal from './Modal'
|
||||
import {MdFileDownload} from 'react-icons/md'
|
||||
import style from '../libs/style'
|
||||
import fieldSpecAdditional from '../libs/field-spec-additional'
|
||||
|
||||
|
||||
const MAPBOX_GL_VERSION = pkgLockJson.dependencies["mapbox-gl"].version;
|
||||
|
||||
|
||||
export default class ModalExport extends React.Component {
|
||||
static propTypes = {
|
||||
mapStyle: PropTypes.object.isRequired,
|
||||
onStyleChanged: PropTypes.func.isRequired,
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onOpenToggle: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
tokenizedStyle () {
|
||||
return format(
|
||||
style.stripAccessTokens(
|
||||
style.replaceAccessTokens(this.props.mapStyle)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
exportName () {
|
||||
if(this.props.mapStyle.name) {
|
||||
return Slugify(this.props.mapStyle.name, {
|
||||
replacement: '_',
|
||||
remove: /[*\-+~.()'"!:]/g,
|
||||
lower: true
|
||||
});
|
||||
} else {
|
||||
return this.props.mapStyle.id
|
||||
}
|
||||
}
|
||||
|
||||
downloadHtml() {
|
||||
const tokenStyle = this.tokenizedStyle();
|
||||
const htmlTitle = this.props.mapStyle.name || "Map";
|
||||
const html = `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>${htmlTitle}</title>
|
||||
<meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no" />
|
||||
<script src="https://api.mapbox.com/mapbox-gl-js/v${MAPBOX_GL_VERSION}/mapbox-gl.js"></script>
|
||||
<link href="https://api.mapbox.com/mapbox-gl-js/v${MAPBOX_GL_VERSION}/mapbox-gl.css" rel="stylesheet" />
|
||||
<style>
|
||||
body { margin: 0; padding: 0; }
|
||||
#map { position: absolute; top: 0; bottom: 0; width: 100%; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="map"></div>
|
||||
<script>
|
||||
mapboxgl.accessToken = 'access_token';
|
||||
const map = new mapboxgl.Map({
|
||||
container: 'map',
|
||||
style: ${tokenStyle},
|
||||
});
|
||||
map.addControl(new mapboxgl.NavigationControl());
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
|
||||
const blob = new Blob([html], {type: "text/html;charset=utf-8"});
|
||||
const exportName = this.exportName();
|
||||
saveAs(blob, exportName + ".html");
|
||||
}
|
||||
|
||||
downloadStyle() {
|
||||
const tokenStyle = this.tokenizedStyle();
|
||||
const blob = new Blob([tokenStyle], {type: "application/json;charset=utf-8"});
|
||||
const exportName = this.exportName();
|
||||
saveAs(blob, exportName + ".json");
|
||||
}
|
||||
|
||||
changeMetadataProperty(property, value) {
|
||||
const changedStyle = {
|
||||
...this.props.mapStyle,
|
||||
metadata: {
|
||||
...this.props.mapStyle.metadata,
|
||||
[property]: value
|
||||
}
|
||||
}
|
||||
this.props.onStyleChanged(changedStyle)
|
||||
}
|
||||
|
||||
|
||||
render() {
|
||||
return <Modal
|
||||
data-wd-key="modal:export"
|
||||
isOpen={this.props.isOpen}
|
||||
onOpenToggle={this.props.onOpenToggle}
|
||||
title={'Export Style'}
|
||||
className="maputnik-export-modal"
|
||||
>
|
||||
|
||||
<section className="maputnik-modal-section">
|
||||
<h1>Download Style</h1>
|
||||
<p>
|
||||
Download a JSON style to your computer.
|
||||
</p>
|
||||
|
||||
<div>
|
||||
<FieldString
|
||||
label={fieldSpecAdditional.maputnik.mapbox_access_token.label}
|
||||
fieldSpec={fieldSpecAdditional.maputnik.mapbox_access_token}
|
||||
value={(this.props.mapStyle.metadata || {})['maputnik:mapbox_access_token']}
|
||||
onChange={this.changeMetadataProperty.bind(this, "maputnik:mapbox_access_token")}
|
||||
/>
|
||||
<FieldString
|
||||
label={fieldSpecAdditional.maputnik.maptiler_access_token.label}
|
||||
fieldSpec={fieldSpecAdditional.maputnik.maptiler_access_token}
|
||||
value={(this.props.mapStyle.metadata || {})['maputnik:openmaptiles_access_token']}
|
||||
onChange={this.changeMetadataProperty.bind(this, "maputnik:openmaptiles_access_token")}
|
||||
/>
|
||||
<FieldString
|
||||
label={fieldSpecAdditional.maputnik.thunderforest_access_token.label}
|
||||
fieldSpec={fieldSpecAdditional.maputnik.thunderforest_access_token}
|
||||
value={(this.props.mapStyle.metadata || {})['maputnik:thunderforest_access_token']}
|
||||
onChange={this.changeMetadataProperty.bind(this, "maputnik:thunderforest_access_token")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="maputnik-modal-export-buttons">
|
||||
<InputButton
|
||||
onClick={this.downloadStyle.bind(this)}
|
||||
>
|
||||
<MdFileDownload />
|
||||
Download Style
|
||||
</InputButton>
|
||||
|
||||
<InputButton
|
||||
onClick={this.downloadHtml.bind(this)}
|
||||
>
|
||||
<MdFileDownload />
|
||||
Download HTML
|
||||
</InputButton>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</Modal>
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user