mirror of
https://github.com/maputnik/editor.git
synced 2025-12-06 14:20:02 +00:00
Compare commits
650 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4dd34e99fd | ||
|
|
3773acd5be | ||
|
|
6887d70194 | ||
|
|
66c5a5c953 | ||
|
|
8184ac8393 | ||
|
|
6a0d2e8ee5 | ||
|
|
58edd262b0 | ||
|
|
35840409b8 | ||
|
|
d0f6e0fadb | ||
|
|
0de304ca3e | ||
|
|
48bf25c1b0 | ||
|
|
c82c6158e6 | ||
|
|
41cd7dfad1 | ||
|
|
7591b031ce | ||
|
|
95b5324fd3 | ||
|
|
f34529ef06 | ||
|
|
a73b11805d | ||
|
|
ff15b77b7f | ||
|
|
355b663e7e | ||
|
|
3c043fd5e0 | ||
|
|
5f54dd0ccf | ||
|
|
3727f5da48 | ||
|
|
079c5f67cc | ||
|
|
a304d4e060 | ||
|
|
7ac1b03b5a | ||
|
|
b9e32894b3 | ||
|
|
bc5ecfade6 | ||
|
|
c84c7a7b96 | ||
|
|
cb77c6b4e2 | ||
|
|
ea42f434eb | ||
|
|
6f82c12861 | ||
|
|
3b95b25777 | ||
|
|
1da65f2116 | ||
|
|
a62db148cd | ||
|
|
6ed10a862f | ||
|
|
123e84f19b | ||
|
|
d9b1b6f3ae | ||
|
|
e0cef99c07 | ||
|
|
eb48bed32a | ||
|
|
87cf81d1c9 | ||
|
|
8e35ed97e6 | ||
|
|
124ae98bf3 | ||
|
|
b784bf2b84 | ||
|
|
09a1f3f87b | ||
|
|
a22476cab2 | ||
|
|
a324ddb654 | ||
|
|
656264f2bc | ||
|
|
974dd7bfd9 | ||
|
|
fa182e66fa | ||
|
|
3bf0e510e6 | ||
|
|
e8d07fa694 | ||
|
|
4d1e2e6893 | ||
|
|
f219ff1e17 | ||
|
|
8eabfa5519 | ||
|
|
ad69cbdb20 | ||
|
|
17eaa3f204 | ||
|
|
1df2e36dbb | ||
|
|
0c46affda9 | ||
|
|
18f45e932b | ||
|
|
31d56c9fae | ||
|
|
b7838ad6e1 | ||
|
|
c92fd12854 | ||
|
|
eb55796461 | ||
|
|
4b97f82980 | ||
|
|
5d0b6e3201 | ||
|
|
5ab9be9fdb | ||
|
|
9659d41b83 | ||
|
|
52f949e152 | ||
|
|
e4d559f953 | ||
|
|
577663f706 | ||
|
|
393f4a38b9 | ||
|
|
3727c9ad5e | ||
|
|
a99cbc00ba | ||
|
|
fe5f7e8b8c | ||
|
|
3ed4b8f2d7 | ||
|
|
f17c2e8112 | ||
|
|
2be447f105 | ||
|
|
2fe6fa2be6 | ||
|
|
7265bf0aa4 | ||
|
|
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 | ||
|
|
c9504fcaed | ||
|
|
b7ef0943f4 | ||
|
|
4661677387 | ||
|
|
77ed14a340 | ||
|
|
e24d390f7c | ||
|
|
698fdfc958 | ||
|
|
77b3655c3c | ||
|
|
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 | ||
|
|
c264cd1771 | ||
|
|
1495d11462 | ||
|
|
af92aac7ec | ||
|
|
90dfbf37e0 | ||
|
|
e21f412933 | ||
|
|
da297fe82c | ||
|
|
624ccb5b00 | ||
|
|
9f0e5641ab | ||
|
|
fda11e52e7 | ||
|
|
d9e3aa6ac4 | ||
|
|
aeca95a27f | ||
|
|
7dfcdac202 | ||
|
|
4f156ee3fd | ||
|
|
6d00214f55 | ||
|
|
1e7b6e809c | ||
|
|
cdcc61e234 | ||
|
|
d07b40ccef | ||
|
|
e0abd8251d | ||
|
|
324452e714 | ||
|
|
8d3ad6b1a1 | ||
|
|
9c63172d36 | ||
|
|
6f21fd8dff | ||
|
|
0e788c5841 | ||
|
|
7229df704a | ||
|
|
686fd27b35 | ||
|
|
293342e4fb | ||
|
|
03d9b946e7 | ||
|
|
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 | ||
|
|
4a3825fa89 | ||
|
|
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 | ||
|
|
5371b0f9fb | ||
|
|
38bf12701e | ||
|
|
e4ec1d155a | ||
|
|
538cea7f45 | ||
|
|
361f083687 | ||
|
|
c1a59200e2 | ||
|
|
6e0432ff5e | ||
|
|
1c83de08c1 | ||
|
|
0af828543b | ||
|
|
369cc23a30 | ||
|
|
db56ad8b2e | ||
|
|
7fa17d81ac | ||
|
|
019c6a0086 | ||
|
|
c1bee74b57 | ||
|
|
b794279304 | ||
|
|
935dfa1704 | ||
|
|
bda7a0e659 | ||
|
|
fdfc470ccc | ||
|
|
8d1cc340b8 | ||
|
|
338c6b59a8 | ||
|
|
021f8ab400 | ||
|
|
3153eea1da | ||
|
|
f305db9e3e | ||
|
|
8471d0af3d | ||
|
|
e916b25594 | ||
|
|
1ce2d59b9b | ||
|
|
2a832955c4 | ||
|
|
608b836fe0 | ||
|
|
de9c4fcc4a | ||
|
|
1fec89b69e | ||
|
|
911549aca3 | ||
|
|
41329ec2f8 | ||
|
|
15cdfbc980 | ||
|
|
5053058c32 | ||
|
|
8a8cfad303 | ||
|
|
b1d097a40f | ||
|
|
d951256b1c | ||
|
|
ec753869d5 | ||
|
|
2e58be1c90 | ||
|
|
562a4f7322 | ||
|
|
e52a63e1dd | ||
|
|
4533fd06ed | ||
|
|
b4fc62632c | ||
|
|
e3a9a8a38c | ||
|
|
7c4e982fb3 | ||
|
|
85dd22b09a | ||
|
|
18e15eeb5c | ||
|
|
3a45b8dd41 | ||
|
|
5b8412765b | ||
|
|
69519df82f | ||
|
|
8052701021 | ||
|
|
35c0150522 | ||
|
|
c55278e7da | ||
|
|
3a0fc6eeac | ||
|
|
b456b59c44 | ||
|
|
e18d304313 | ||
|
|
fe0df2a4ef | ||
|
|
a51fdb8435 | ||
|
|
cdd5d27908 | ||
|
|
d3ecef3de6 | ||
|
|
3ef0a90de4 | ||
|
|
87290889fd | ||
|
|
1997e31b6b | ||
|
|
5b21a2fa4f | ||
|
|
d314add6a9 | ||
|
|
50d61cdb0e | ||
|
|
4ffea21c5f | ||
|
|
d29a79e79f | ||
|
|
004177a3c8 | ||
|
|
b9e70a943f | ||
|
|
de853eb2d7 | ||
|
|
f242c2c015 | ||
|
|
d895cf079c | ||
|
|
147cbc1580 | ||
|
|
3e46c0d3ba | ||
|
|
d233e0a14d | ||
|
|
a73b2fd7e1 | ||
|
|
69f63f2844 | ||
|
|
10136c07db | ||
|
|
5e3156ab21 | ||
|
|
6be8959951 | ||
|
|
d2cd84de2b | ||
|
|
60bea1777a | ||
|
|
ce9216b2d5 | ||
|
|
35ed202cd0 | ||
|
|
0d77518a02 | ||
|
|
11375008fa | ||
|
|
009f4e105d | ||
|
|
b1af4917e5 | ||
|
|
5b712d74ae | ||
|
|
ca2df37c79 | ||
|
|
7d1890156d | ||
|
|
ecab640a9a | ||
|
|
f8cb0619f3 | ||
|
|
d874b2503b | ||
|
|
3d09f2a0f3 | ||
|
|
1f1580276d | ||
|
|
0421a7f099 | ||
|
|
8b722fc967 | ||
|
|
66e3ce8743 | ||
|
|
45eb3a01e6 |
19
.babelrc
19
.babelrc
@@ -1,19 +0,0 @@
|
||||
{
|
||||
"presets": [
|
||||
"@babel/preset-env",
|
||||
"@babel/preset-react"
|
||||
],
|
||||
"plugins": [
|
||||
"react-hot-loader/babel",
|
||||
"@babel/plugin-proposal-class-properties"
|
||||
],
|
||||
"env": {
|
||||
"test": {
|
||||
"plugins": [
|
||||
["istanbul", {
|
||||
"exclude": ["node_modules/**", "test/**"]
|
||||
}]
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
version: 2
|
||||
templates:
|
||||
# Test the build **only** no webdriver
|
||||
build-steps: &build-steps
|
||||
- checkout
|
||||
- run:
|
||||
name: "Create artifacts directory"
|
||||
command: mkdir /tmp/artifacts
|
||||
- restore_cache:
|
||||
key: v1-dependencies-{{ arch }}-{{ checksum "package.json" }}
|
||||
|
||||
- run: npm install
|
||||
|
||||
- save_cache:
|
||||
paths:
|
||||
- node_modules
|
||||
key: v1-dependencies-{{ arch }}-{{ checksum "package.json" }}
|
||||
|
||||
- run: mkdir -p /tmp/artifacts/logs
|
||||
- run: npm run build
|
||||
- run: npm run lint
|
||||
- run: npm run lint-styles
|
||||
- store_artifacts:
|
||||
path: /tmp/artifacts
|
||||
destination: /artifacts
|
||||
# Test in webdriver
|
||||
wdio-steps: &wdio-steps
|
||||
- checkout
|
||||
- run:
|
||||
name: "Create artifacts directory"
|
||||
command: mkdir /tmp/artifacts
|
||||
- restore_cache:
|
||||
key: v1-dependencies-{{ arch }}-{{ checksum "package.json" }}
|
||||
|
||||
- run: npm install
|
||||
|
||||
- save_cache:
|
||||
paths:
|
||||
- node_modules
|
||||
key: v1-dependencies-{{ arch }}-{{ checksum "package.json" }}
|
||||
|
||||
- run: mkdir -p /tmp/artifacts/logs
|
||||
- run: npm run build
|
||||
- run: npm run lint
|
||||
- run: npm run lint-styles
|
||||
- run: DOCKER_HOST=localhost npm test
|
||||
- run: ./node_modules/.bin/istanbul report --include /tmp/artifacts/coverage/coverage.json --dir /tmp/artifacts/coverage html lcov
|
||||
- store_artifacts:
|
||||
path: /tmp/artifacts
|
||||
destination: /artifacts
|
||||
jobs:
|
||||
build-linux-node-v8:
|
||||
docker:
|
||||
- image: node:8
|
||||
working_directory: ~/repo-linux-node-v8
|
||||
steps: *build-steps
|
||||
build-linux-node-v10:
|
||||
docker:
|
||||
- image: node:10
|
||||
- image: selenium/standalone-chrome:3.141.59
|
||||
working_directory: ~/repo-linux-node-v10
|
||||
steps: *wdio-steps
|
||||
build-linux-node-v12:
|
||||
docker:
|
||||
- image: node:12
|
||||
working_directory: ~/repo-linux-node-v12
|
||||
steps: *build-steps
|
||||
build-osx-node-v8:
|
||||
macos:
|
||||
xcode: "9.0"
|
||||
dependencies:
|
||||
override:
|
||||
- brew install node@8
|
||||
working_directory: ~/repo-osx-node-v8
|
||||
steps: *build-steps
|
||||
build-osx-node-v10:
|
||||
macos:
|
||||
xcode: "9.0"
|
||||
dependencies:
|
||||
override:
|
||||
- brew install node@10
|
||||
working_directory: ~/repo-osx-node-v10
|
||||
steps: *build-steps
|
||||
build-osx-node-v12:
|
||||
macos:
|
||||
xcode: "9.0"
|
||||
dependencies:
|
||||
override:
|
||||
- brew install node@12
|
||||
working_directory: ~/repo-osx-node-v12
|
||||
steps: *build-steps
|
||||
|
||||
workflows:
|
||||
version: 2
|
||||
build:
|
||||
jobs:
|
||||
- build-linux-node-v8
|
||||
- build-linux-node-v10
|
||||
- build-linux-node-v12
|
||||
- build-osx-node-v8
|
||||
- build-osx-node-v10
|
||||
- build-osx-node-v12
|
||||
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
|
||||
46
.eslintrc
Normal file
46
.eslintrc
Normal file
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"root": true,
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es2020": true
|
||||
},
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:react/recommended",
|
||||
"plugin:react/jsx-runtime",
|
||||
"plugin:react-hooks/recommended",
|
||||
],
|
||||
"ignorePatterns": [
|
||||
"dist"
|
||||
],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": "latest",
|
||||
"sourceType": "module"
|
||||
},
|
||||
"settings": {
|
||||
"react": { "version": "16.4" }
|
||||
},
|
||||
"plugins": [
|
||||
"@typescript-eslint",
|
||||
"react-refresh"],
|
||||
"rules": {
|
||||
"react-refresh/only-export-components": [
|
||||
"warn",
|
||||
{ "allowConstantExport": true }
|
||||
],
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
"warn",
|
||||
{ "argsIgnorePattern": "^_" }
|
||||
],
|
||||
"no-unused-vars": "off",
|
||||
"react/prop-types": ["off"],
|
||||
// Disable no-undef. It's covered by @typescript-eslint
|
||||
"no-undef": "off",
|
||||
"indent": ["error", 2],
|
||||
"no-var": ["error"]
|
||||
},
|
||||
"globals": {
|
||||
"global": "readonly"
|
||||
}
|
||||
}
|
||||
2
.github/FUNDING.yml
vendored
Normal file
2
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
github: [maplibre]
|
||||
open_collective: maplibre
|
||||
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, main -->
|
||||
**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) -->
|
||||
|
||||
11
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
11
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
## Launch Checklist
|
||||
|
||||
<!-- Thanks for the PR! Feel free to add or remove items from the checklist. -->
|
||||
|
||||
|
||||
- [ ] Briefly describe the changes in this PR.
|
||||
- [ ] Link to related issues.
|
||||
- [ ] Include before/after visuals or gifs if this PR includes visual changes.
|
||||
- [ ] Write tests for all new functionality.
|
||||
- [ ] Add an entry to `CHANGELOG.md` under the `## main` section.
|
||||
|
||||
14
.github/dependabot.yml
vendored
Normal file
14
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
open-pull-requests-limit: 2
|
||||
versioning-strategy: increase
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
open-pull-requests-limit: 2
|
||||
versioning-strategy: increase
|
||||
113
.github/workflows/ci.yml
vendored
Normal file
113
.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,113 @@
|
||||
name: ci
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
push:
|
||||
branches: [ main ]
|
||||
|
||||
jobs:
|
||||
build-docker:
|
||||
name: build docker
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request' }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- run: docker build -t test-docker-image-build .
|
||||
|
||||
# build the editor
|
||||
build-node:
|
||||
name: "build on ${{ 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]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
- run: npm ci
|
||||
- run: npm run build
|
||||
- run: npm run lint
|
||||
- run: npm run lint-css
|
||||
|
||||
|
||||
|
||||
build-artifacts:
|
||||
name: "build artifacts"
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request' }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
- run: npm ci
|
||||
- run: npm run build
|
||||
- name: artifacts/maputnik
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: maputnik
|
||||
path: dist
|
||||
|
||||
# Build and upload desktop CLI artifacts
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ^1.23.x
|
||||
cache-dependency-path: desktop/go.sum
|
||||
id: go
|
||||
|
||||
- name: Build desktop artifacts
|
||||
run: npm run build-desktop
|
||||
|
||||
- name: Artifacts/linux
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: maputnik-linux
|
||||
path: ./desktop/bin/linux/
|
||||
|
||||
- name: Artifacts/darwin
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: maputnik-darwin
|
||||
path: ./desktop/bin/darwin/
|
||||
|
||||
- name: Artifacts/windows
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: maputnik-windows
|
||||
path: ./desktop/bin/windows/
|
||||
|
||||
e2e-tests:
|
||||
name: "E2E tests using ${{ matrix.browser }}"
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
browser: [chrome, firefox]
|
||||
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- run: npm ci
|
||||
- name: Cypress run
|
||||
uses: cypress-io/github-action@v6
|
||||
with:
|
||||
build: npm run build
|
||||
start: npm run start
|
||||
browser: ${{ matrix.browser }}
|
||||
- name: Upload coverage reports to Codecov
|
||||
uses: codecov/codecov-action@v3
|
||||
with:
|
||||
files: ${{ github.workspace }}/.nyc_output/out.json
|
||||
verbose: true
|
||||
39
.github/workflows/create-bump-version-pr.yml
vendored
Normal file
39
.github/workflows/create-bump-version-pr.yml
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
name: Create bump version PR
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: Version to change to.
|
||||
required: true
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
bump-version-pr:
|
||||
name: Bump version PR
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: main
|
||||
|
||||
- name: Use Node.js from nvmrc
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
|
||||
- name: Bump version
|
||||
run: |
|
||||
npm version --commit-hooks false --git-tag-version false ${{ inputs.version }}
|
||||
./build/bump-version-changelog.js ${{ inputs.version }}
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v6
|
||||
with:
|
||||
commit-message: Bump version to ${{ inputs.version }}
|
||||
branch: bump-version-to-${{ inputs.version }}
|
||||
title: Bump version to ${{ inputs.version }}
|
||||
51
.github/workflows/deploy.yml
vendored
Normal file
51
.github/workflows/deploy.yml
vendored
Normal file
@@ -0,0 +1,51 @@
|
||||
name: deploy
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
|
||||
jobs:
|
||||
deploy-pages:
|
||||
name: deploy/pages
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event_name == 'push' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Use Node.js from nvmrc
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
|
||||
- name: Install
|
||||
run: npm ci
|
||||
|
||||
- name: Build
|
||||
run: npm run build
|
||||
|
||||
- name: Upload to GitHub Pages
|
||||
uses: peaceiris/actions-gh-pages@v3
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
publish_dir: dist
|
||||
|
||||
# publish docker to GitHub registry
|
||||
deploy-docker:
|
||||
name: deploy/docker
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
if: ${{ github.event_name == 'push' }}
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
|
||||
steps:
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- uses: actions/checkout@v4
|
||||
- run: docker build -t ghcr.io/maplibre/maputnik:main .
|
||||
- run: docker push ghcr.io/maplibre/maputnik:main
|
||||
104
.github/workflows/release.yml
vendored
Normal file
104
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,104 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
release-check:
|
||||
name: Check if version changed
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: main
|
||||
|
||||
- name: Use Node.js from nvmrc
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
|
||||
- name: Check if version has been updated
|
||||
id: check
|
||||
uses: EndBug/version-check@v2
|
||||
|
||||
outputs:
|
||||
publish: ${{ steps.check.outputs.changed }}
|
||||
|
||||
release:
|
||||
name: Release
|
||||
needs: release-check
|
||||
if: ${{ needs.release-check.outputs.publish == 'true' }}
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: main
|
||||
|
||||
- name: Use Node.js from nvmrc
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
|
||||
- name: Set up Go for desktop build
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ^1.23.x
|
||||
cache-dependency-path: desktop/go.sum
|
||||
id: go
|
||||
|
||||
- name: Get version
|
||||
id: package-version
|
||||
uses: martinbeentjes/npm-get-version-action@v1.3.1
|
||||
|
||||
- name: Install
|
||||
run: npm ci
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
npm run build
|
||||
npm run build-desktop
|
||||
|
||||
- name: Tag commit and push
|
||||
id: tag_version
|
||||
uses: mathieudutour/github-tag-action@v6.2
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
custom_tag: ${{ steps.package-version.outputs.current-version }}
|
||||
|
||||
- name: Create Archives
|
||||
run: |
|
||||
zip -r dist dist
|
||||
zip -r desktop desktop/bin/
|
||||
|
||||
- name: Build Release Notes
|
||||
id: release_notes
|
||||
run: |
|
||||
RELEASE_NOTES_PATH="${PWD}/release_notes.txt"
|
||||
./build/release-notes.js > ${RELEASE_NOTES_PATH}
|
||||
echo "release_notes=${RELEASE_NOTES_PATH}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Create GitHub Release
|
||||
id: create_regular_release
|
||||
uses: ncipollo/release-action@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
tag: ${{ steps.tag_version.outputs.new_tag }}
|
||||
name: ${{ steps.tag_version.outputs.new_tag }}
|
||||
bodyFile: ${{ steps.release_notes.outputs.release_notes }}
|
||||
artifacts: "dist.zip,desktop.zip"
|
||||
allowUpdates: true
|
||||
draft: false
|
||||
prerelease: false
|
||||
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -14,6 +14,7 @@ lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
@@ -32,4 +33,6 @@ node_modules
|
||||
public
|
||||
/errorShots
|
||||
/old
|
||||
/build
|
||||
/cypress/screenshots
|
||||
/dist/
|
||||
/desktop/version.go
|
||||
|
||||
18
.nycrc.json
Normal file
18
.nycrc.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"all": true,
|
||||
"extends": "@istanbuljs/nyc-config-typescript",
|
||||
"check-coverage": false,
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx"],
|
||||
"exclude": [
|
||||
"cypress/**/*.*",
|
||||
"**/*.d.ts",
|
||||
"**/*.cy.tsx",
|
||||
"**/*.cy.ts",
|
||||
"./coverage/**",
|
||||
"./cypress/**",
|
||||
"./dist/**",
|
||||
"node_modules"
|
||||
],
|
||||
"report-dir": "coverage",
|
||||
"reporter": ["json", "lcov", "json-summary"]
|
||||
}
|
||||
24
CHANGELOG.md
Normal file
24
CHANGELOG.md
Normal file
@@ -0,0 +1,24 @@
|
||||
## main
|
||||
|
||||
### ✨ Features and improvements
|
||||
- _...Add new stuff here..._
|
||||
|
||||
### 🐞 Bug fixes
|
||||
- _...Add new stuff here..._
|
||||
|
||||
## 2.1.0
|
||||
|
||||
### ✨ Features and improvements
|
||||
|
||||
- Add GitHub workflows for releasing new versions
|
||||
- Update desktop build to pull from this repo (#922)
|
||||
|
||||
## 2.0.0
|
||||
|
||||
- Update MapLibre to version 4 (#872)
|
||||
- Start continuous deployment of maputnik website
|
||||
|
||||
## 1.7.0
|
||||
|
||||
- See release notes at https://maputnik.github.io/blog/2020/04/23/release-v1.7.0
|
||||
|
||||
2
CODE_OF_CONDUCT.md
Normal file
2
CODE_OF_CONDUCT.md
Normal file
@@ -0,0 +1,2 @@
|
||||
# Contributor Covenant
|
||||
[](https://github.com/maplibre/maplibre/blob/main/CODE_OF_CONDUCT.md)
|
||||
29
Dockerfile
29
Dockerfile
@@ -1,21 +1,16 @@
|
||||
FROM node:10-slim
|
||||
FROM node:18 as builder
|
||||
WORKDIR /maputnik
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
git \
|
||||
python \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
# Only copy package.json to prevent npm install from running on every build
|
||||
COPY package.json package-lock.json .npmrc ./
|
||||
RUN npm ci
|
||||
|
||||
EXPOSE 8888
|
||||
# Build maputnik
|
||||
COPY . .
|
||||
RUN npx vite build
|
||||
|
||||
ENV HOME /maputnik
|
||||
RUN mkdir ${HOME}
|
||||
#---------------------------------------------------------------------------
|
||||
# Create a clean nginx-alpine slim image with just the build results
|
||||
FROM nginx:alpine-slim
|
||||
|
||||
COPY . ${HOME}/
|
||||
|
||||
WORKDIR ${HOME}
|
||||
|
||||
RUN npm install -d
|
||||
RUN npm run build
|
||||
|
||||
WORKDIR ${HOME}/build/build
|
||||
CMD python -m SimpleHTTPServer 8888
|
||||
COPY --from=builder /maputnik/dist /usr/share/nginx/html/
|
||||
|
||||
1
LICENSE
1
LICENSE
@@ -1,6 +1,7 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 Lukas Martinelli
|
||||
Copyright (c) 2024 MapLibre contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
151
README.md
151
README.md
@@ -1,69 +1,61 @@
|
||||
# Maputnik
|
||||
<img width="200" alt="Maputnik logo" src="https://cdn.jsdelivr.net/gh/maputnik/design/logos/logo-color.png" />
|
||||
|
||||
[][circleci]
|
||||
[][appveyor]
|
||||
[][dm-prod]
|
||||
[][dm-dev]
|
||||
# Maputnik
|
||||
[][github-action-ci]
|
||||
[][license]
|
||||
|
||||
[circleci]: https://circleci.com/gh/maputnik/editor/tree/master
|
||||
[appveyor]: https://ci.appveyor.com/project/lukasmartinelli/editor
|
||||
[dm-prod]: https://david-dm.org/maputnik/editor
|
||||
[dm-dev]: https://david-dm.org/maputnik/editor?type=dev
|
||||
[license]: https://tldrlegal.com/license/mit-license
|
||||
[github-action-ci]: https://github.com/maplibre/maputnik/actions?query=workflow%3Aci
|
||||
[license]: https://tldrlegal.com/license/mit-license
|
||||
|
||||
<img width="200" align="right" alt="Maputnik" src="https://cdn.jsdelivr.net/gh/maputnik/editor@1.5.0/src/img/maputnik.png" />
|
||||
|
||||
A free and open visual editor for the [Mapbox GL styles](https://www.mapbox.com/mapbox-gl-style-spec/)
|
||||
A free and open visual editor for the [MapLibre GL styles](https://maplibre.org/maplibre-style-spec/)
|
||||
targeted at developers and map designers.
|
||||
|
||||
- :link: Design your maps online at **<https://maputnik.github.io/editor/>** (all in local storage)
|
||||
- :link: Try out the v1.6.0-beta release at: https://maputnik.github.io/releases/v1.6.0-beta/
|
||||
- :link: Use the [Maputnik CLI](https://github.com/maputnik/editor/wiki/Maputnik-CLI) for local style development
|
||||
|
||||
Mapbox has built one of the best and most amazing OSS ecosystems. A key component to ensure its longevity and independence is an OSS map designer.
|
||||
## Usage
|
||||
|
||||
- :link: Design your maps online at **<https://www.maplibre.org/maputnik/>** (all in local storage)
|
||||
- :link: Use the [Maputnik CLI](https://github.com/maplibre/maputnik/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.
|
||||
|
||||
## Donations
|
||||
If you or your organisation has seen value from Maputnik, please consider donating at <https://maputnik.github.io/donate>
|
||||
|
||||
```bash
|
||||
docker run -it --rm -p 8888:80 ghcr.io/maplibre/maputnik:main
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
The documentation can be found in the [Wiki](https://github.com/maputnik/editor/wiki). You are welcome to collaborate!
|
||||
The documentation can be found in the [Wiki](https://github.com/maplibre/maputnik/wiki). You are welcome to collaborate!
|
||||
|
||||
- :link: **Study the [Maputnik Wiki](https://github.com/maputnik/editor/wiki)**
|
||||
- :link: **Study the [Maputnik Wiki](https://github.com/maplibre/maputnik/wiki)**
|
||||
- :video_camera: Design a map from Scratch https://youtu.be/XoDh0gEnBQo
|
||||
|
||||
[](https://youtu.be/XoDh0gEnBQo)
|
||||
|
||||
## Develop
|
||||
|
||||
Maputnik is written in ES6 and is using [React](https://github.com/facebook/react) and [Mapbox GL JS](https://www.mapbox.com/mapbox-gl-js/api/).
|
||||
Maputnik is written in typescript and is using [React](https://github.com/facebook/react) and [MapLibre GL JS](https://maplibre.org/projects/maplibre-gl-js/).
|
||||
|
||||
We ensure building and developing Maputnik works with the [current active LTS Node.js version and above](https://github.com/nodejs/Release#release-schedule).
|
||||
|
||||
### Getting Involved
|
||||
Join the #maplibre or #maputnik slack channel at OSMUS: get an invite at https://slack.openstreetmap.us/ Read the the below guide in order to get familiar with how we do things around here.
|
||||
|
||||
Install the deps, start the dev server and open the web browser on `http://localhost:8888/`.
|
||||
|
||||
```bash
|
||||
# install dependencies
|
||||
npm install
|
||||
# start dev server
|
||||
npm start
|
||||
npm run start
|
||||
```
|
||||
|
||||
If you want Maputnik to be accessible externally use the [`--host` option](https://webpack.js.org/configuration/dev-server/#devserverhost):
|
||||
If you want Maputnik to be accessible externally use the [`--host` option](https://vitejs.dev/config/server-options.html#server-host):
|
||||
|
||||
```bash
|
||||
# start externally accessible dev server
|
||||
npm start -- --host 0.0.0.0
|
||||
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.
|
||||
The build process will watch for changes to the filesystem, rebuild and autoreload the editor.
|
||||
|
||||
```
|
||||
npm run build
|
||||
@@ -74,98 +66,51 @@ Lint the JavaScript code.
|
||||
```
|
||||
# run linter
|
||||
npm run lint
|
||||
npm run lint-styles
|
||||
npm run lint-css
|
||||
npm run sort-styles
|
||||
```
|
||||
|
||||
|
||||
## Tests
|
||||
For testing we use [webdriverio](http://webdriver.io) and [selenium-standalone](https://github.com/vvo/selenium-standalone)
|
||||
For E2E testing we use [Cypress](https://www.cypress.io/)
|
||||
|
||||
[selenium-standalone](https://github.com/vvo/selenium-standalone) starts a server that will launch browsers on your local machine. We use chrome so you **must** have chrome installed on your machine.
|
||||
[Cypress](https://www.cypress.io/) doesn't starts a server so you'll need to start one manually by running `npm run start`.
|
||||
|
||||
Now open a terminal and run the following. This will install the drivers on your local machine
|
||||
Now open a terminal and run the following using *chrome*:
|
||||
|
||||
```
|
||||
./node_modules/.bin/selenium-standalone install
|
||||
npm run test
|
||||
```
|
||||
or *firefox*:
|
||||
```
|
||||
npm run test -- --browser firefox
|
||||
```
|
||||
|
||||
Now start the standalone server
|
||||
See the following docs for more info: (Launching Browsers)[https://docs.cypress.io/guides/guides/launching-browsers]
|
||||
|
||||
You can also see the tests as they run or select which suites to run by executing:
|
||||
|
||||
```
|
||||
./node_modules/.bin/selenium-standalone start
|
||||
npm run cy:open
|
||||
```
|
||||
|
||||
Then open another terminal and run
|
||||
## Release process
|
||||
|
||||
```
|
||||
npm test
|
||||
```
|
||||
1. Review [`CHANGELOG.md`](/CHANGELOG.md)
|
||||
- Double-check that all changes included in the release are appropriately documented.
|
||||
- To-be-released changes should be under the "main" header.
|
||||
- Commit any final changes to the changelog.
|
||||
2. Run [Create bump version PR](https://github.com/maplibre/maputnik/actions/workflows/create-bump-version-pr.yml) by manual workflow dispatch and set the version number in the input. This will create a PR that changes the changelog and `package.json` file to review and merge.
|
||||
3. Once merged, an automatic process will kick in and creates a GitHub release and uploads release assets.
|
||||
|
||||
After some time you should see a browser launch which will be automated by the test runner.
|
||||
|
||||
|
||||
## Related Projects
|
||||
|
||||
- [maputnik-dev-server](https://github.com/nycplanning/labs-maputnik-dev-server) - An express.js server that allows for quickly loading the style from any mapboxGL map into mapuntnik.
|
||||
|
||||
## Sponsors
|
||||
|
||||
Thanks to the supporters of the **[Kickstarter campaign](https://www.kickstarter.com/projects/174808720/maputnik-visual-map-editor-for-mapbox-gl)**. This project would not be possible without these commercial and individual sponsors.
|
||||
|
||||
### Gold
|
||||
|
||||
- [Wemap](https://getwemap.com/)
|
||||
- [Orbicon Informatik](https://www.orbiconinformatik.dk/)
|
||||
- [Terranodo](http://terranodo.io/)
|
||||
|
||||
<a href="https://getwemap.com/">
|
||||
<img width="33%" alt="Wemap" style="display:inline" src="https://cdn.jsdelivr.net/gh/maputnik/editor@1.5.0/media/sponsors/wemap.jpg" />
|
||||
</a>
|
||||
<a href="http://terranodo.io/">
|
||||
<img width="33%" alt="Terranodo" style="display:inline" src="https://cdn.jsdelivr.net/gh/maputnik/editor@1.5.0/media/sponsors/terranodo.png" />
|
||||
</a>
|
||||
<a href="https://www.orbiconinformatik.dk/">
|
||||
<img width="32%" alt="Terranodo" style="display:inline" src="https://cdn.jsdelivr.net/gh/maputnik/editor@1.5.0/media/sponsors/orbicon_informatik.png" />
|
||||
</a>
|
||||
|
||||
<br/>
|
||||
|
||||
### Silver
|
||||
|
||||
- [Klokan Technologies](https://www.klokantech.com/)
|
||||
- [Geofabrik](http://www.geofabrik.de/)
|
||||
- [Dreipol](https://www.dreipol.ch/)
|
||||
|
||||
<a href="https://www.klokantech.com/">
|
||||
<img width="18%" alt="Klokan Technologies" style="display:inline-block" src="https://cdn.jsdelivr.net/gh/maputnik/editor@1.5.0/media/sponsors/klokantech.png" />
|
||||
</a>
|
||||
<a href="http://www.geofabrik.de/">
|
||||
<img width="18%" alt="Geofabrik" style="display:inline-block" src="https://cdn.jsdelivr.net/gh/maputnik/editor@1.5.0/media/sponsors/geofabrik.png" />
|
||||
</a>
|
||||
<a href="https://www.dreipol.ch/">
|
||||
<img width="18%" alt="Dreipol" style="display:inline-block" src="https://cdn.jsdelivr.net/gh/maputnik/editor@1.5.0/media/sponsors/dreipol.png" />
|
||||
</a>
|
||||
|
||||
<br/>
|
||||
|
||||
### Individuals
|
||||
|
||||
**Influential Stakeholder**
|
||||
|
||||
Alan McConchie, Odi, Mats Norén, Uli [geOps](http://geops.ch/), Helge Fahrnberger ([Toursprung](http://www.toursprung.com/)), Kirusanth Poopalasingam
|
||||
|
||||
**Stakeholder**
|
||||
|
||||
Brian Flood, Vasile Coțovanu, Andreas Kalkbrenner, Christian Mäder, Gregor Wassmann, Lee Armstrong, Rafel, Jon Burgess, Lukas Lehmann, Joachim Ungar, Alois Ackermann, Zsolt Ero, Jordan Meek
|
||||
|
||||
**Supporter**
|
||||
|
||||
Sina Martinelli, Nicholas Doiron, Neil Cawse, Urs42, Benedikt Groß, Manuel Roth, Janko Mihelić, Moritz Stefaner, Sebastian Ahoi, Juerg Uhlmann, Tom Wider, Nadia Panchaud, Oliver Snowden, Stephan Heuel, Tobin Bradley, Adrian Herzog, Antti Lehto, Pascal Mages, Marc Gehling, Imre Samu, Lauri K., Visahavel Parthasarathy, Christophe Waterlot-Buisine, Max Galka, ubahnverleih, Wouter van Dam, Jakob Lobensteiner, Samuel Kurath, Brian Bancroft
|
||||
You can see this file's history for previous sponsors of the original Maputnik repo.
|
||||
Read more about the MapLibre Sponsorship Program at https://maplibre.org/sponsors/.
|
||||
|
||||
## License
|
||||
|
||||
Maputnik is [licensed under MIT](LICENSE) and is Copyright (c) Lukas Martinelli and contributors.
|
||||
|
||||
**Disclaimer** This project is not affiliated with Mapbox or Mapbox Studio. It is an independent style editor for the
|
||||
open source technology in the Mapbox GL ecosystem.
|
||||
As contributor please take extra care of not violating any Mapbox trademarks. Do not get inspired by Mapbox Studio and make your own decisions for a good style editor.
|
||||
Maputnik is [licensed under MIT](LICENSE) and is Copyright (c) Lukas Martinelli and Maplibre contributors.
|
||||
As contributor please take extra care of not violating any Mapbox trademarks. Do not get inspired by other map studios and make your own decisions for a good style editor.
|
||||
|
||||
2
SECURITY_POLICY.txt
Normal file
2
SECURITY_POLICY.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
For an up-to-date policy refer to
|
||||
https://github.com/maplibre/maplibre/blob/main/SECURITY_POLICY.txt
|
||||
26
appveyor.yml
26
appveyor.yml
@@ -1,26 +0,0 @@
|
||||
image: Visual Studio 2015
|
||||
environment:
|
||||
matrix:
|
||||
- nodejs_version: "8"
|
||||
- nodejs_version: "10"
|
||||
- nodejs_version: "12"
|
||||
platform:
|
||||
- x86
|
||||
- x64
|
||||
install:
|
||||
# https://github.com/appveyor/ci/issues/2921#issuecomment-501016533
|
||||
- ps: |
|
||||
try {
|
||||
Install-Product node $env:nodejs_version $env:platform
|
||||
} catch {
|
||||
echo "Unable to install node $env:nodejs_version, trying update..."
|
||||
Update-NodeJsInstallation (Get-NodeJsLatestBuild $env:nodejs_version) $env:platform
|
||||
}
|
||||
- md public
|
||||
- npm --vs2015 install --global windows-build-tools
|
||||
- npm install
|
||||
build_script:
|
||||
- npm run build
|
||||
test_script:
|
||||
- npm run lint
|
||||
- npm run lint-styles
|
||||
11
build/README.md
Normal file
11
build/README.md
Normal file
@@ -0,0 +1,11 @@
|
||||
# Build Scripts
|
||||
|
||||
This folder holds common build scripts used by some of the Github workflows.
|
||||
|
||||
The scripts are borrowed from [maplibre/maplibre-gl-js](https://github.com/maplibre/maplibre-gl-js/tree/bc70bc559cea5c987fa1b79fd44766cef68bbe28/build).
|
||||
|
||||
## Generate Release Notes
|
||||
|
||||
`bump-version-changelog.js` Used to update the changelog with the current notes, and set up a space for new notes
|
||||
|
||||
`release-notes.js` Used to generate release notes when releasing a new version
|
||||
29
build/bump-version-changelog.js
Executable file
29
build/bump-version-changelog.js
Executable file
@@ -0,0 +1,29 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* This script updates the changelog.md file with the version given in the arguments
|
||||
* It replaces ## main with ## <version>
|
||||
* Removes _...Add new stuff here..._
|
||||
* And adds on top a ## main with add stuff here.
|
||||
*
|
||||
* Copied from maplibre/maplibre-gl-js
|
||||
* https://github.com/maplibre/maplibre-gl-js/blob/bc70bc559cea5c987fa1b79fd44766cef68bbe28/build/release-notes.js
|
||||
*/
|
||||
|
||||
import * as fs from 'fs';
|
||||
|
||||
const changelogPath = 'CHANGELOG.md';
|
||||
let changelog = fs.readFileSync(changelogPath, 'utf8');
|
||||
changelog = changelog.replace('## main', `## ${process.argv[2]}`);
|
||||
changelog = changelog.replaceAll('- _...Add new stuff here..._\n', '');
|
||||
changelog = `## main
|
||||
|
||||
### ✨ Features and improvements
|
||||
- _...Add new stuff here..._
|
||||
|
||||
### 🐞 Bug fixes
|
||||
- _...Add new stuff here..._
|
||||
|
||||
` + changelog;
|
||||
|
||||
fs.writeFileSync(changelogPath, changelog, 'utf8');
|
||||
48
build/release-notes.js
Executable file
48
build/release-notes.js
Executable file
@@ -0,0 +1,48 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
// Copied from maplibre/maplibre-gl-js
|
||||
// https://github.com/maplibre/maplibre-gl-js/blob/bc70bc559cea5c987fa1b79fd44766cef68bbe28/build/release-notes.js
|
||||
|
||||
import * as fs from 'fs';
|
||||
|
||||
const changelogPath = 'CHANGELOG.md';
|
||||
const changelog = fs.readFileSync(changelogPath, 'utf8');
|
||||
|
||||
/*
|
||||
Parse the raw changelog text and split it into individual releases.
|
||||
|
||||
This regular expression:
|
||||
- Matches lines starting with "## x.x.x".
|
||||
- Groups the version number.
|
||||
- Skips the (optional) release date.
|
||||
- Groups the changelog content.
|
||||
- Ends when another "## x.x.x" is found.
|
||||
*/
|
||||
const regex = /^## (\d+\.\d+\.\d+.*?)\n(.+?)(?=\n^## \d+\.\d+\.\d+.*?\n)/gms;
|
||||
|
||||
let releaseNotes = [];
|
||||
let match;
|
||||
// eslint-disable-next-line no-cond-assign
|
||||
while (match = regex.exec(changelog)) {
|
||||
releaseNotes.push({
|
||||
'version': match[1],
|
||||
'changelog': match[2].trim(),
|
||||
});
|
||||
}
|
||||
|
||||
const latest = releaseNotes[0];
|
||||
const previous = releaseNotes[1];
|
||||
|
||||
// Print the release notes template.
|
||||
|
||||
let header = 'Changes since previous version'
|
||||
if (previous) {
|
||||
header = `https://github.com/maplibre/maputnik
|
||||
[Changes](https://github.com/maplibre/maputnik/compare/v${previous.version}...v${latest.version}) since [Maputnik v${previous.version}](https://github.com/maplibre/maputnik/releases/tag/v${previous.version})`
|
||||
}
|
||||
const templatedReleaseNotes = `${header}
|
||||
|
||||
${latest.changelog}
|
||||
|
||||
// eslint-disable-next-line eol-last
|
||||
process.stdout.write(templatedReleaseNotes.trimEnd());
|
||||
@@ -1,288 +0,0 @@
|
||||
var webpack = require("webpack");
|
||||
var WebpackDevServer = require("webpack-dev-server");
|
||||
var webpackConfig = require("./webpack.config");
|
||||
var testConfig = require("../test/config/specs");
|
||||
var artifacts = require("../test/artifacts");
|
||||
var isDocker = require("is-docker");
|
||||
|
||||
|
||||
var server;
|
||||
var SCREENSHOT_PATH = artifacts.pathSync("screenshots");
|
||||
|
||||
exports.config = {
|
||||
//
|
||||
// ====================
|
||||
// Runner Configuration
|
||||
// ====================
|
||||
//
|
||||
// WebdriverIO allows it to run your tests in arbitrary locations (e.g. locally or
|
||||
// on a remote machine).
|
||||
runner: 'local',
|
||||
//
|
||||
// ==================
|
||||
// Specify Test Files
|
||||
// ==================
|
||||
// Define which test specs should run. The pattern is relative to the directory
|
||||
// from which `wdio` was called. Notice that, if you are calling `wdio` from an
|
||||
// NPM script (see https://docs.npmjs.com/cli/run-script) then the current working
|
||||
// directory is where your package.json resides, so `wdio` will be called from there.
|
||||
//
|
||||
specs: [
|
||||
'./test/functional/index.js'
|
||||
],
|
||||
// Patterns to exclude.
|
||||
exclude: [
|
||||
// 'path/to/excluded/files'
|
||||
],
|
||||
//
|
||||
// ============
|
||||
// Capabilities
|
||||
// ============
|
||||
// Define your capabilities here. WebdriverIO can run multiple capabilities at the same
|
||||
// time. Depending on the number of capabilities, WebdriverIO launches several test
|
||||
// sessions. Within your capabilities you can overwrite the spec and exclude options in
|
||||
// order to group specific specs to a specific capability.
|
||||
//
|
||||
// First, you can define how many instances should be started at the same time. Let's
|
||||
// say you have 3 different capabilities (Chrome, Firefox, and Safari) and you have
|
||||
// set maxInstances to 1; wdio will spawn 3 processes. Therefore, if you have 10 spec
|
||||
// files and you set maxInstances to 10, all spec files will get tested at the same time
|
||||
// and 30 processes will get spawned. The property handles how many capabilities
|
||||
// from the same test should run tests.
|
||||
//
|
||||
maxInstances: 10,
|
||||
//
|
||||
// If you have trouble getting all important capabilities together, check out the
|
||||
// Sauce Labs platform configurator - a great tool to configure your capabilities:
|
||||
// https://docs.saucelabs.com/reference/platforms-configurator
|
||||
//
|
||||
capabilities: [{
|
||||
// maxInstances can get overwritten per capability. So if you have an in-house Selenium
|
||||
// grid with only 5 firefox instances available you can make sure that not more than
|
||||
// 5 instances get started at a time.
|
||||
maxInstances: 5,
|
||||
//
|
||||
browserName: 'chrome',
|
||||
// If outputDir is provided WebdriverIO can capture driver session logs
|
||||
// it is possible to configure which logTypes to include/exclude.
|
||||
// excludeDriverLogs: ['*'], // pass '*' to exclude all driver session logs
|
||||
// excludeDriverLogs: ['bugreport', 'server'],
|
||||
}],
|
||||
//
|
||||
// ===================
|
||||
// Test Configurations
|
||||
// ===================
|
||||
// Define all options that are relevant for the WebdriverIO instance here
|
||||
//
|
||||
// Level of logging verbosity: trace | debug | info | warn | error | silent
|
||||
logLevel: 'info',
|
||||
//
|
||||
// Set specific log levels per logger
|
||||
// loggers:
|
||||
// - webdriver, webdriverio
|
||||
// - @wdio/applitools-service, @wdio/browserstack-service, @wdio/devtools-service, @wdio/sauce-service
|
||||
// - @wdio/mocha-framework, @wdio/jasmine-framework
|
||||
// - @wdio/local-runner, @wdio/lambda-runner
|
||||
// - @wdio/sumologic-reporter
|
||||
// - @wdio/cli, @wdio/config, @wdio/sync, @wdio/utils
|
||||
// Level of logging verbosity: trace | debug | info | warn | error | silent
|
||||
// logLevels: {
|
||||
// webdriver: 'debug',
|
||||
// '@wdio/applitools-service': 'info'
|
||||
// },
|
||||
//
|
||||
// If you only want to run your tests until a specific amount of tests have failed use
|
||||
// bail (default is 0 - don't bail, run all tests).
|
||||
bail: 0,
|
||||
//
|
||||
screenshotPath: SCREENSHOT_PATH,
|
||||
// Note: This is here because @orangemug currently runs Maputnik inside a docker container.
|
||||
host: process.env.DOCKER_HOST || "0.0.0.0",
|
||||
// Set a base URL in order to shorten url command calls. If your `url` parameter starts
|
||||
// with `/`, the base url gets prepended, not including the path portion of your baseUrl.
|
||||
// If your `url` parameter starts without a scheme or `/` (like `some/path`), the base url
|
||||
// gets prepended directly.
|
||||
baseUrl: 'http://localhost',
|
||||
//
|
||||
// Default timeout for all waitFor* commands.
|
||||
waitforTimeout: 10000,
|
||||
//
|
||||
// Default timeout in milliseconds for request
|
||||
// if Selenium Grid doesn't send response
|
||||
connectionRetryTimeout: 90000,
|
||||
//
|
||||
// Default request retries count
|
||||
connectionRetryCount: 3,
|
||||
//
|
||||
// Test runner services
|
||||
// Services take over a specific job you don't want to take care of. They enhance
|
||||
// your test setup with almost no effort. Unlike plugins, they don't add new
|
||||
// commands. Instead, they hook themselves up into the test process.
|
||||
services: ['selenium-standalone'],
|
||||
//
|
||||
// Framework you want to run your specs with.
|
||||
// The following are supported: Mocha, Jasmine, and Cucumber
|
||||
// see also: https://webdriver.io/docs/frameworks.html
|
||||
//
|
||||
// Make sure you have the wdio adapter package for the specific framework installed
|
||||
// before running any tests.
|
||||
framework: 'mocha',
|
||||
//
|
||||
// The number of times to retry the entire specfile when it fails as a whole
|
||||
// specFileRetries: 1,
|
||||
//
|
||||
// Test reporter for stdout.
|
||||
// The only one supported by default is 'dot'
|
||||
// see also: https://webdriver.io/docs/dot-reporter.html
|
||||
reporters: ['spec'],
|
||||
|
||||
//
|
||||
// Options to be passed to Mocha.
|
||||
// See the full list at http://mochajs.org/
|
||||
mochaOpts: {
|
||||
ui: 'bdd',
|
||||
// Because we don't know how long the initial build will take...
|
||||
timeout: 4*60*1000
|
||||
},
|
||||
onPrepare: function (config, capabilities) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
var compiler = webpack(webpackConfig);
|
||||
server = new WebpackDevServer(compiler, {
|
||||
stats: {
|
||||
colors: true
|
||||
}
|
||||
});
|
||||
server.listen(testConfig.port, (isDocker() ? "0.0.0.0" : "localhost"), function(err) {
|
||||
if(err) {
|
||||
reject(err);
|
||||
}
|
||||
else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
})
|
||||
},
|
||||
onComplete: function(exitCode) {
|
||||
server.close()
|
||||
}
|
||||
//
|
||||
// =====
|
||||
// Hooks
|
||||
// =====
|
||||
// WebdriverIO provides several hooks you can use to interfere with the test process in order to enhance
|
||||
// it and to build services around it. You can either apply a single function or an array of
|
||||
// methods to it. If one of them returns with a promise, WebdriverIO will wait until that promise got
|
||||
// resolved to continue.
|
||||
/**
|
||||
* Gets executed once before all workers get launched.
|
||||
* @param {Object} config wdio configuration object
|
||||
* @param {Array.<Object>} capabilities list of capabilities details
|
||||
*/
|
||||
// onPrepare: function (config, capabilities) {
|
||||
// },
|
||||
/**
|
||||
* Gets executed just before initialising the webdriver session and test framework. It allows you
|
||||
* to manipulate configurations depending on the capability or spec.
|
||||
* @param {Object} config wdio configuration object
|
||||
* @param {Array.<Object>} capabilities list of capabilities details
|
||||
* @param {Array.<String>} specs List of spec file paths that are to be run
|
||||
*/
|
||||
// beforeSession: function (config, capabilities, specs) {
|
||||
// },
|
||||
/**
|
||||
* Gets executed before test execution begins. At this point you can access to all global
|
||||
* variables like `browser`. It is the perfect place to define custom commands.
|
||||
* @param {Array.<Object>} capabilities list of capabilities details
|
||||
* @param {Array.<String>} specs List of spec file paths that are to be run
|
||||
*/
|
||||
// before: function (capabilities, specs) {
|
||||
// },
|
||||
/**
|
||||
* Runs before a WebdriverIO command gets executed.
|
||||
* @param {String} commandName hook command name
|
||||
* @param {Array} args arguments that command would receive
|
||||
*/
|
||||
// beforeCommand: function (commandName, args) {
|
||||
// },
|
||||
|
||||
/**
|
||||
* Hook that gets executed before the suite starts
|
||||
* @param {Object} suite suite details
|
||||
*/
|
||||
// beforeSuite: function (suite) {
|
||||
// },
|
||||
/**
|
||||
* Function to be executed before a test (in Mocha/Jasmine) or a step (in Cucumber) starts.
|
||||
* @param {Object} test test details
|
||||
*/
|
||||
// beforeTest: function (test) {
|
||||
// },
|
||||
/**
|
||||
* Hook that gets executed _before_ a hook within the suite starts (e.g. runs before calling
|
||||
* beforeEach in Mocha)
|
||||
*/
|
||||
// beforeHook: function () {
|
||||
// },
|
||||
/**
|
||||
* Hook that gets executed _after_ a hook within the suite starts (e.g. runs after calling
|
||||
* afterEach in Mocha)
|
||||
*/
|
||||
// afterHook: function () {
|
||||
// },
|
||||
/**
|
||||
* Function to be executed after a test (in Mocha/Jasmine) or a step (in Cucumber) starts.
|
||||
* @param {Object} test test details
|
||||
*/
|
||||
// afterTest: function (test) {
|
||||
// },
|
||||
/**
|
||||
* Hook that gets executed after the suite has ended
|
||||
* @param {Object} suite suite details
|
||||
*/
|
||||
// afterSuite: function (suite) {
|
||||
// },
|
||||
|
||||
/**
|
||||
* Runs after a WebdriverIO command gets executed
|
||||
* @param {String} commandName hook command name
|
||||
* @param {Array} args arguments that command would receive
|
||||
* @param {Number} result 0 - command success, 1 - command error
|
||||
* @param {Object} error error object if any
|
||||
*/
|
||||
// afterCommand: function (commandName, args, result, error) {
|
||||
// },
|
||||
/**
|
||||
* Gets executed after all tests are done. You still have access to all global variables from
|
||||
* the test.
|
||||
* @param {Number} result 0 - test pass, 1 - test fail
|
||||
* @param {Array.<Object>} capabilities list of capabilities details
|
||||
* @param {Array.<String>} specs List of spec file paths that ran
|
||||
*/
|
||||
// after: function (result, capabilities, specs) {
|
||||
// },
|
||||
/**
|
||||
* Gets executed right after terminating the webdriver session.
|
||||
* @param {Object} config wdio configuration object
|
||||
* @param {Array.<Object>} capabilities list of capabilities details
|
||||
* @param {Array.<String>} specs List of spec file paths that ran
|
||||
*/
|
||||
// afterSession: function (config, capabilities, specs) {
|
||||
// },
|
||||
/**
|
||||
* Gets executed after all workers got shut down and the process is about to exit. An error
|
||||
* thrown in the onComplete hook will result in the test run failing.
|
||||
* @param {Object} exitCode 0 - success, 1 - fail
|
||||
* @param {Object} config wdio configuration object
|
||||
* @param {Array.<Object>} capabilities list of capabilities details
|
||||
* @param {<Object>} results object containing test results
|
||||
*/
|
||||
// onComplete: function(exitCode, config, capabilities, results) {
|
||||
// },
|
||||
/**
|
||||
* Gets executed when a refresh happens.
|
||||
* @param {String} oldSessionId session ID of the old session
|
||||
* @param {String} newSessionId session ID of the new session
|
||||
*/
|
||||
//onReload: function(oldSessionId, newSessionId) {
|
||||
//}
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
"use strict";
|
||||
var webpack = require('webpack');
|
||||
var path = require('path');
|
||||
var rules = require('./webpack.rules');
|
||||
var HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
var CopyWebpackPlugin = require('copy-webpack-plugin');
|
||||
|
||||
const HOST = process.env.HOST || "127.0.0.1";
|
||||
const PORT = process.env.PORT || "8888";
|
||||
|
||||
module.exports = {
|
||||
target: 'web',
|
||||
mode: 'development',
|
||||
entry: [
|
||||
`webpack-dev-server/client?http://${HOST}:${PORT}`,
|
||||
`webpack/hot/only-dev-server`,
|
||||
`./src/index.jsx` // Your appʼs entry point
|
||||
],
|
||||
devtool: process.env.WEBPACK_DEVTOOL || 'cheap-module-source-map',
|
||||
output: {
|
||||
path: path.join(__dirname, '..', 'public'),
|
||||
filename: 'bundle.js'
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.js', '.jsx']
|
||||
},
|
||||
module: {
|
||||
noParse: [
|
||||
/mapbox-gl\/dist\/mapbox-gl.js/
|
||||
],
|
||||
rules: rules
|
||||
},
|
||||
node: {
|
||||
fs: "empty",
|
||||
net: 'empty',
|
||||
tls: 'empty'
|
||||
},
|
||||
devServer: {
|
||||
contentBase: "./public",
|
||||
// do not print bundle build stats
|
||||
noInfo: true,
|
||||
// enable HMR
|
||||
hot: true,
|
||||
// embed the webpack-dev-server runtime into the bundle
|
||||
inline: true,
|
||||
// serve index.html in place of 404 responses to allow HTML5 history
|
||||
historyApiFallback: true,
|
||||
port: PORT,
|
||||
host: HOST,
|
||||
watchOptions: {
|
||||
// Disabled polling by default as it causes lots of CPU usage and hence drains laptop batteries. To enable polling add WEBPACK_DEV_SERVER_POLLING to your environment
|
||||
// See <https://webpack.js.org/configuration/watch/#watchoptions-poll> for details
|
||||
poll: (!!process.env.WEBPACK_DEV_SERVER_POLLING ? true : false),
|
||||
watch: false
|
||||
}
|
||||
},
|
||||
plugins: [
|
||||
new webpack.NoEmitOnErrorsPlugin(),
|
||||
new webpack.HotModuleReplacementPlugin(),
|
||||
new HtmlWebpackPlugin({
|
||||
title: 'Maputnik',
|
||||
template: './src/template.html'
|
||||
}),
|
||||
new CopyWebpackPlugin([
|
||||
{
|
||||
from: './src/manifest.json',
|
||||
to: 'manifest.json'
|
||||
}
|
||||
])
|
||||
]
|
||||
};
|
||||
@@ -1,62 +0,0 @@
|
||||
var webpack = require('webpack');
|
||||
var path = require('path');
|
||||
var rules = require('./webpack.rules');
|
||||
var HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
var WebpackCleanupPlugin = require('webpack-cleanup-plugin');
|
||||
var BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
|
||||
var CopyWebpackPlugin = require('copy-webpack-plugin');
|
||||
var artifacts = require("../test/artifacts");
|
||||
|
||||
var OUTPATH = artifacts.pathSync("/build");
|
||||
|
||||
module.exports = {
|
||||
entry: {
|
||||
app: './src/index.jsx',
|
||||
},
|
||||
output: {
|
||||
path: OUTPATH,
|
||||
filename: '[name].[contenthash].js',
|
||||
chunkFilename: '[contenthash].js'
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.js', '.jsx']
|
||||
},
|
||||
module: {
|
||||
noParse: [
|
||||
/mapbox-gl\/dist\/mapbox-gl.js/
|
||||
],
|
||||
rules: rules
|
||||
},
|
||||
node: {
|
||||
fs: "empty",
|
||||
net: 'empty',
|
||||
tls: 'empty'
|
||||
},
|
||||
plugins: [
|
||||
new webpack.NoEmitOnErrorsPlugin(),
|
||||
new WebpackCleanupPlugin(),
|
||||
new webpack.DefinePlugin({
|
||||
'process.env': {
|
||||
NODE_ENV: '"production"'
|
||||
}
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
template: './src/template.html',
|
||||
title: 'Maputnik'
|
||||
}),
|
||||
new CopyWebpackPlugin([
|
||||
{
|
||||
from: './src/manifest.json',
|
||||
to: 'manifest.json'
|
||||
}
|
||||
]),
|
||||
new BundleAnalyzerPlugin({
|
||||
analyzerMode: 'static',
|
||||
defaultSizes: 'gzip',
|
||||
openAnalyzer: false,
|
||||
generateStatsFile: true,
|
||||
reportFilename: 'bundle-stats.html',
|
||||
statsFilename: 'bundle-stats.json',
|
||||
})
|
||||
]
|
||||
};
|
||||
@@ -1,38 +0,0 @@
|
||||
const path = require("path");
|
||||
|
||||
module.exports = [
|
||||
{
|
||||
test: /\.jsx?$/,
|
||||
exclude: [
|
||||
path.resolve(__dirname, '../node_modules')
|
||||
],
|
||||
use: 'babel-loader'
|
||||
},
|
||||
{
|
||||
test: /\.(eot|ttf|woff|woff2)$/,
|
||||
use: 'file-loader?name=fonts/[name].[ext]'
|
||||
},
|
||||
{
|
||||
test: /\.ico$/,
|
||||
use: 'file-loader?name=[name].[ext]'
|
||||
},
|
||||
{
|
||||
test: /\.(svg|gif|jpg|png)$/,
|
||||
use: 'file-loader?name=img/[name].[ext]'
|
||||
},
|
||||
{
|
||||
test: /[\/\\](node_modules|global|src)[\/\\].*\.scss$/,
|
||||
use: [
|
||||
'style-loader',
|
||||
"css-loader",
|
||||
"sass-loader"
|
||||
]
|
||||
},
|
||||
{
|
||||
test: /[\/\\](node_modules|global|src)[\/\\].*\.css$/,
|
||||
use: [
|
||||
'style-loader',
|
||||
'css-loader'
|
||||
]
|
||||
}
|
||||
];
|
||||
23
cypress.config.ts
Normal file
23
cypress.config.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { defineConfig } from "cypress";
|
||||
import { createRequire } from "module";
|
||||
const require = createRequire(import.meta.url);
|
||||
|
||||
export default defineConfig({
|
||||
env: {
|
||||
codeCoverage: {
|
||||
exclude: "cypress/**/*.*",
|
||||
},
|
||||
},
|
||||
e2e: {
|
||||
setupNodeEvents(on, config) {
|
||||
// implement node event listeners here
|
||||
require("@cypress/code-coverage/task")(on, config);
|
||||
return config;
|
||||
},
|
||||
baseUrl: "http://localhost:8888",
|
||||
retries: {
|
||||
runMode: 2,
|
||||
openMode: 0,
|
||||
},
|
||||
},
|
||||
});
|
||||
40
cypress/e2e/accessibility.cy.ts
Normal file
40
cypress/e2e/accessibility.cy.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { MaputnikDriver } from "./maputnik-driver";
|
||||
|
||||
describe("accessibility", () => {
|
||||
let { beforeAndAfter, get, when, then } = new MaputnikDriver();
|
||||
beforeAndAfter();
|
||||
|
||||
describe("skip links", () => {
|
||||
beforeEach(() => {
|
||||
when.setStyle("layer");
|
||||
});
|
||||
|
||||
it("skip link to layer list", () => {
|
||||
const selector = "root:skip:layer-list";
|
||||
then(get.elementByTestId(selector)).shouldExist();
|
||||
when.tab();
|
||||
then(get.elementByTestId(selector)).shouldBeFocused();
|
||||
when.click(selector);
|
||||
then(get.skipTargetLayerList()).shouldBeFocused();
|
||||
});
|
||||
|
||||
// This fails for some reason only in Chrome, but passes in firefox. Adding a skip here to allow merge and later on we'll decide if we want to fix this or not.
|
||||
it.skip("skip link to layer editor", () => {
|
||||
const selector = "root:skip:layer-editor";
|
||||
then(get.elementByTestId(selector)).shouldExist();
|
||||
when.tab().tab();
|
||||
then(get.elementByTestId(selector)).shouldBeFocused();
|
||||
when.click(selector);
|
||||
then(get.skipTargetLayerEditor()).shouldBeFocused();
|
||||
});
|
||||
|
||||
it("skip link to map view", () => {
|
||||
const selector = "root:skip:map-view";
|
||||
then(get.elementByTestId(selector)).shouldExist();
|
||||
when.tab().tab().tab();
|
||||
then(get.elementByTestId(selector)).shouldBeFocused();
|
||||
when.click(selector);
|
||||
then(get.canvas()).shouldBeFocused();
|
||||
});
|
||||
});
|
||||
});
|
||||
125
cypress/e2e/history.cy.ts
Normal file
125
cypress/e2e/history.cy.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
import { MaputnikDriver } from "./maputnik-driver";
|
||||
|
||||
describe("history", () => {
|
||||
let { beforeAndAfter, when, get, then } = new MaputnikDriver();
|
||||
beforeAndAfter();
|
||||
|
||||
let undoKeyCombo: string;
|
||||
let redoKeyCombo: string;
|
||||
|
||||
before(() => {
|
||||
const isMac = get.isMac();
|
||||
undoKeyCombo = isMac ? "{meta}z" : "{ctrl}z";
|
||||
redoKeyCombo = isMac ? "{meta}{shift}z" : "{ctrl}y";
|
||||
});
|
||||
|
||||
it("undo/redo", () => {
|
||||
when.setStyle("geojson");
|
||||
when.modal.open();
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({ layers: [] });
|
||||
|
||||
when.modal.fillLayers({
|
||||
id: "step 1",
|
||||
type: "background",
|
||||
});
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
{
|
||||
id: "step 1",
|
||||
type: "background",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
when.modal.open();
|
||||
when.modal.fillLayers({
|
||||
id: "step 2",
|
||||
type: "background",
|
||||
});
|
||||
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
{
|
||||
id: "step 1",
|
||||
type: "background",
|
||||
},
|
||||
{
|
||||
id: "step 2",
|
||||
type: "background",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
when.typeKeys(undoKeyCombo);
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
{
|
||||
id: "step 1",
|
||||
type: "background",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
when.typeKeys(undoKeyCombo);
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({ layers: [] });
|
||||
|
||||
when.typeKeys(redoKeyCombo);
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
{
|
||||
id: "step 1",
|
||||
type: "background",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
when.typeKeys(redoKeyCombo);
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
{
|
||||
id: "step 1",
|
||||
type: "background",
|
||||
},
|
||||
{
|
||||
id: "step 2",
|
||||
type: "background",
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("should not redo after undo and value change", () => {
|
||||
when.setStyle("geojson");
|
||||
when.modal.open();
|
||||
when.modal.fillLayers({
|
||||
id: "step 1",
|
||||
type: "background",
|
||||
});
|
||||
|
||||
when.modal.open();
|
||||
when.modal.fillLayers({
|
||||
id: "step 2",
|
||||
type: "background",
|
||||
});
|
||||
|
||||
when.typeKeys(undoKeyCombo);
|
||||
when.typeKeys(undoKeyCombo);
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({ layers: [] });
|
||||
|
||||
when.modal.open();
|
||||
when.modal.fillLayers({
|
||||
id: "step 3",
|
||||
type: "background",
|
||||
});
|
||||
|
||||
when.typeKeys(redoKeyCombo);
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
{
|
||||
id: "step 3",
|
||||
type: "background",
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
35
cypress/e2e/i18n.cy.ts
Normal file
35
cypress/e2e/i18n.cy.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { MaputnikDriver } from "./maputnik-driver";
|
||||
|
||||
describe("i18n", () => {
|
||||
let { beforeAndAfter, get, when, then } = new MaputnikDriver();
|
||||
beforeAndAfter();
|
||||
|
||||
describe("language detector", () => {
|
||||
it("English", () => {
|
||||
const url = "?lng=en";
|
||||
when.visit(url);
|
||||
then(get.elementByTestId("maputnik-lang-select")).shouldHaveValue("en");
|
||||
});
|
||||
|
||||
it("Japanese", () => {
|
||||
const url = "?lng=ja";
|
||||
when.visit(url);
|
||||
then(get.elementByTestId("maputnik-lang-select")).shouldHaveValue("ja");
|
||||
});
|
||||
});
|
||||
|
||||
describe("language switcher", () => {
|
||||
beforeEach(() => {
|
||||
when.setStyle("layer");
|
||||
});
|
||||
|
||||
it("the language switcher switches to Japanese", () => {
|
||||
const selector = "maputnik-lang-select";
|
||||
then(get.elementByTestId(selector)).shouldExist();
|
||||
when.select(selector, "ja");
|
||||
then(get.elementByTestId(selector)).shouldHaveValue("ja");
|
||||
|
||||
then(get.elementByTestId("nav:settings")).shouldHaveText("スタイル設定");
|
||||
});
|
||||
});
|
||||
});
|
||||
60
cypress/e2e/keyboard.cy.ts
Normal file
60
cypress/e2e/keyboard.cy.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { MaputnikDriver } from "./maputnik-driver";
|
||||
|
||||
describe("keyboard", () => {
|
||||
let { beforeAndAfter, given, when, get, then } = new MaputnikDriver();
|
||||
beforeAndAfter();
|
||||
describe("shortcuts", () => {
|
||||
beforeEach(() => {
|
||||
given.setupMockBackedResponses();
|
||||
when.setStyle("");
|
||||
});
|
||||
|
||||
it("ESC should unfocus", () => {
|
||||
const targetSelector = "maputnik-select";
|
||||
when.focus(targetSelector);
|
||||
then(get.elementByTestId(targetSelector)).shouldBeFocused();
|
||||
when.typeKeys("{esc}");
|
||||
then(get.elementByTestId(targetSelector)).shouldNotBeFocused();
|
||||
});
|
||||
|
||||
it("'?' should show shortcuts modal", () => {
|
||||
when.typeKeys("?");
|
||||
then(get.elementByTestId("modal:shortcuts")).shouldBeVisible();
|
||||
});
|
||||
|
||||
it("'o' should show open modal", () => {
|
||||
when.typeKeys("o");
|
||||
then(get.elementByTestId("modal:open")).shouldBeVisible();
|
||||
});
|
||||
|
||||
it("'e' should show export modal", () => {
|
||||
when.typeKeys("e");
|
||||
then(get.elementByTestId("modal:export")).shouldBeVisible();
|
||||
});
|
||||
|
||||
it("'d' should show sources modal", () => {
|
||||
when.typeKeys("d");
|
||||
then(get.elementByTestId("modal:sources")).shouldBeVisible();
|
||||
});
|
||||
|
||||
it("'s' should show settings modal", () => {
|
||||
when.typeKeys("s");
|
||||
then(get.elementByTestId("modal:settings")).shouldBeVisible();
|
||||
});
|
||||
|
||||
it("'i' should change map to inspect mode", () => {
|
||||
when.typeKeys("i");
|
||||
then(get.inputValue("maputnik-select")).shouldEqual("inspect");
|
||||
});
|
||||
|
||||
it("'m' should focus map", () => {
|
||||
when.typeKeys("m");
|
||||
then(get.canvas()).shouldBeFocused();
|
||||
});
|
||||
|
||||
it("'!' should show debug modal", () => {
|
||||
when.typeKeys("!");
|
||||
then(get.elementByTestId("modal:debug")).shouldBeVisible();
|
||||
});
|
||||
});
|
||||
});
|
||||
497
cypress/e2e/layers.cy.ts
Normal file
497
cypress/e2e/layers.cy.ts
Normal file
@@ -0,0 +1,497 @@
|
||||
import { v1 as uuid } from "uuid";
|
||||
import { MaputnikDriver } from "./maputnik-driver";
|
||||
|
||||
describe("layers", () => {
|
||||
let { beforeAndAfter, get, when, then } = new MaputnikDriver();
|
||||
beforeAndAfter();
|
||||
beforeEach(() => {
|
||||
when.setStyle("both");
|
||||
when.modal.open();
|
||||
});
|
||||
|
||||
describe("ops", () => {
|
||||
let id: string;
|
||||
beforeEach(() => {
|
||||
id = when.modal.fillLayers({
|
||||
type: "background",
|
||||
});
|
||||
});
|
||||
|
||||
it("should update layers in local storage", () => {
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
{
|
||||
id: id,
|
||||
type: "background",
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
describe("when clicking delete", () => {
|
||||
beforeEach(() => {
|
||||
when.click("layer-list-item:" + id + ":delete");
|
||||
});
|
||||
it("should empty layers in local storage", () => {
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("when clicking duplicate", () => {
|
||||
beforeEach(() => {
|
||||
when.click("layer-list-item:" + id + ":copy");
|
||||
});
|
||||
it("should add copy layer in local storage", () => {
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
{
|
||||
id: id + "-copy",
|
||||
type: "background",
|
||||
},
|
||||
{
|
||||
id: id,
|
||||
type: "background",
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("when clicking hide", () => {
|
||||
beforeEach(() => {
|
||||
when.click("layer-list-item:" + id + ":toggle-visibility");
|
||||
});
|
||||
|
||||
it("should update visibility to none in local storage", () => {
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
{
|
||||
id: id,
|
||||
type: "background",
|
||||
layout: {
|
||||
visibility: "none",
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
describe("when clicking show", () => {
|
||||
beforeEach(() => {
|
||||
when.click("layer-list-item:" + id + ":toggle-visibility");
|
||||
});
|
||||
|
||||
it("should update visibility to visible in local storage", () => {
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
{
|
||||
id: id,
|
||||
type: "background",
|
||||
layout: {
|
||||
visibility: "visible",
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
describe("background", () => {
|
||||
it("add", () => {
|
||||
let id = when.modal.fillLayers({
|
||||
type: "background",
|
||||
});
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
{
|
||||
id: id,
|
||||
type: "background",
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
describe("modify", () => {
|
||||
function createBackground() {
|
||||
// Setup
|
||||
let id = uuid();
|
||||
|
||||
when.selectWithin("add-layer.layer-type", "background");
|
||||
when.setValue("add-layer.layer-id.input", "background:" + id);
|
||||
|
||||
when.click("add-layer");
|
||||
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
{
|
||||
id: "background:" + id,
|
||||
type: "background",
|
||||
},
|
||||
],
|
||||
});
|
||||
return id;
|
||||
}
|
||||
|
||||
// ====> THESE SHOULD BE FROM THE SPEC
|
||||
describe("layer", () => {
|
||||
it("expand/collapse");
|
||||
it("id", () => {
|
||||
let bgId = createBackground();
|
||||
|
||||
when.click("layer-list-item:background:" + bgId);
|
||||
|
||||
let id = uuid();
|
||||
when.setValue("layer-editor.layer-id.input", "foobar:" + id);
|
||||
when.click("min-zoom");
|
||||
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
{
|
||||
id: "foobar:" + id,
|
||||
type: "background",
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
describe("min-zoom", () => {
|
||||
let bgId: string;
|
||||
|
||||
beforeEach(() => {
|
||||
bgId = createBackground();
|
||||
when.click("layer-list-item:background:" + bgId);
|
||||
when.setValue("min-zoom.input-text", "1");
|
||||
when.click("layer-editor.layer-id");
|
||||
});
|
||||
|
||||
it("should update min-zoom in local storage", () => {
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
{
|
||||
id: "background:" + bgId,
|
||||
type: "background",
|
||||
minzoom: 1,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("when clicking next layer should update style on local storage", () => {
|
||||
when.type("min-zoom.input-text", "{backspace}");
|
||||
when.click("max-zoom.input-text");
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
{
|
||||
id: "background:" + bgId,
|
||||
type: "background",
|
||||
minzoom: 1,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("max-zoom", () => {
|
||||
let bgId: string;
|
||||
|
||||
beforeEach(() => {
|
||||
bgId = createBackground();
|
||||
when.click("layer-list-item:background:" + bgId);
|
||||
when.setValue("max-zoom.input-text", "1");
|
||||
when.click("layer-editor.layer-id");
|
||||
});
|
||||
|
||||
it("should update style in local storage", () => {
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
{
|
||||
id: "background:" + bgId,
|
||||
type: "background",
|
||||
maxzoom: 1,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("comments", () => {
|
||||
let bgId: string;
|
||||
let comment = "42";
|
||||
|
||||
beforeEach(() => {
|
||||
bgId = createBackground();
|
||||
when.click("layer-list-item:background:" + bgId);
|
||||
when.setValue("layer-comment.input", comment);
|
||||
when.click("layer-editor.layer-id");
|
||||
});
|
||||
|
||||
it("should update style in local storage", () => {
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
{
|
||||
id: "background:" + bgId,
|
||||
type: "background",
|
||||
metadata: {
|
||||
"maputnik:comment": comment,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
describe("when unsetting", () => {
|
||||
beforeEach(() => {
|
||||
when.clear("layer-comment.input");
|
||||
when.click("min-zoom.input-text");
|
||||
});
|
||||
|
||||
it("should update style in local storage", () => {
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
{
|
||||
id: "background:" + bgId,
|
||||
type: "background",
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("color", () => {
|
||||
let bgId: string;
|
||||
beforeEach(() => {
|
||||
bgId = createBackground();
|
||||
when.click("layer-list-item:background:" + bgId);
|
||||
when.click("spec-field:background-color");
|
||||
});
|
||||
|
||||
it("should update style in local storage", () => {
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
{
|
||||
id: "background:" + bgId,
|
||||
type: "background",
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("opacity", () => {
|
||||
let bgId: string;
|
||||
beforeEach(() => {
|
||||
bgId = createBackground();
|
||||
when.click("layer-list-item:background:" + bgId);
|
||||
when.type("spec-field-input:background-opacity", "0.");
|
||||
});
|
||||
|
||||
it("should keep '.' in the input field", () => {
|
||||
then(get.elementByTestId("spec-field-input:background-opacity")).shouldHaveValue("0.");
|
||||
});
|
||||
|
||||
it("should revert to a valid value when focus out", () => {
|
||||
when.click("layer-list-item:background:" + bgId);
|
||||
then(get.elementByTestId("spec-field-input:background-opacity")).shouldHaveValue('0');
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe("filter", () => {
|
||||
it("expand/collapse");
|
||||
it("compound filter");
|
||||
});
|
||||
|
||||
describe("paint", () => {
|
||||
it("expand/collapse");
|
||||
it("color");
|
||||
it("pattern");
|
||||
it("opacity");
|
||||
});
|
||||
// <=====
|
||||
|
||||
describe("json-editor", () => {
|
||||
it("expand/collapse");
|
||||
it("modify");
|
||||
|
||||
// TODO
|
||||
it.skip("parse error", () => {
|
||||
let bgId = createBackground();
|
||||
|
||||
when.click("layer-list-item:background:" + bgId);
|
||||
|
||||
let errorSelector = ".CodeMirror-lint-marker-error";
|
||||
then(get.elementByTestId(errorSelector)).shouldNotExist();
|
||||
|
||||
when.click(".CodeMirror");
|
||||
when.typeKeys(
|
||||
"\uE013\uE013\uE013\uE013\uE013\uE013\uE013\uE013\uE013\uE013\uE013\uE013 {"
|
||||
);
|
||||
then(get.elementByTestId(errorSelector)).shouldExist();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("fill", () => {
|
||||
it("add", () => {
|
||||
let id = when.modal.fillLayers({
|
||||
type: "fill",
|
||||
layer: "example",
|
||||
});
|
||||
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
{
|
||||
id: id,
|
||||
type: "fill",
|
||||
source: "example",
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
// TODO: Change source
|
||||
it("change source");
|
||||
});
|
||||
|
||||
describe("line", () => {
|
||||
it("add", () => {
|
||||
let id = when.modal.fillLayers({
|
||||
type: "line",
|
||||
layer: "example",
|
||||
});
|
||||
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
{
|
||||
id: id,
|
||||
type: "line",
|
||||
source: "example",
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("groups", () => {
|
||||
// TODO
|
||||
// Click each of the layer groups.
|
||||
});
|
||||
});
|
||||
|
||||
describe("symbol", () => {
|
||||
it("add", () => {
|
||||
let id = when.modal.fillLayers({
|
||||
type: "symbol",
|
||||
layer: "example",
|
||||
});
|
||||
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
{
|
||||
id: id,
|
||||
type: "symbol",
|
||||
source: "example",
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("raster", () => {
|
||||
it("add", () => {
|
||||
let id = when.modal.fillLayers({
|
||||
type: "raster",
|
||||
layer: "raster",
|
||||
});
|
||||
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
{
|
||||
id: id,
|
||||
type: "raster",
|
||||
source: "raster",
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("circle", () => {
|
||||
it("add", () => {
|
||||
let id = when.modal.fillLayers({
|
||||
type: "circle",
|
||||
layer: "example",
|
||||
});
|
||||
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
{
|
||||
id: id,
|
||||
type: "circle",
|
||||
source: "example",
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("fill extrusion", () => {
|
||||
it("add", () => {
|
||||
let id = when.modal.fillLayers({
|
||||
type: "fill-extrusion",
|
||||
layer: "example",
|
||||
});
|
||||
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
{
|
||||
id: id,
|
||||
type: "fill-extrusion",
|
||||
source: "example",
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("groups", () => {
|
||||
it("simple", () => {
|
||||
when.setStyle("geojson");
|
||||
|
||||
when.modal.open();
|
||||
when.modal.fillLayers({
|
||||
id: "foo",
|
||||
type: "background",
|
||||
});
|
||||
|
||||
when.modal.open();
|
||||
when.modal.fillLayers({
|
||||
id: "foo_bar",
|
||||
type: "background",
|
||||
});
|
||||
|
||||
when.modal.open();
|
||||
when.modal.fillLayers({
|
||||
id: "foo_bar_baz",
|
||||
type: "background",
|
||||
});
|
||||
|
||||
then(get.elementByTestId("layer-list-item:foo")).shouldBeVisible();
|
||||
then(get.elementByTestId("layer-list-item:foo_bar")).shouldNotBeVisible();
|
||||
then(
|
||||
get.elementByTestId("layer-list-item:foo_bar_baz")
|
||||
).shouldNotBeVisible();
|
||||
when.click("layer-list-group:foo-0");
|
||||
then(get.elementByTestId("layer-list-item:foo")).shouldBeVisible();
|
||||
then(get.elementByTestId("layer-list-item:foo_bar")).shouldBeVisible();
|
||||
then(
|
||||
get.elementByTestId("layer-list-item:foo_bar_baz")
|
||||
).shouldBeVisible();
|
||||
});
|
||||
});
|
||||
});
|
||||
32
cypress/e2e/map.cy.ts
Normal file
32
cypress/e2e/map.cy.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { MaputnikDriver } from "./maputnik-driver";
|
||||
|
||||
describe("map", () => {
|
||||
let { beforeAndAfter, get, when, then } = new MaputnikDriver();
|
||||
beforeAndAfter();
|
||||
describe("zoom level", () => {
|
||||
it("via url", () => {
|
||||
let zoomLevel = 12.37;
|
||||
when.setStyle("geojson", zoomLevel);
|
||||
then(get.elementByTestId("maplibre:ctrl-zoom")).shouldBeVisible();
|
||||
then(get.elementByTestId("maplibre:ctrl-zoom")).shouldContainText(
|
||||
"Zoom: " + zoomLevel
|
||||
);
|
||||
});
|
||||
|
||||
it("via map controls", () => {
|
||||
let zoomLevel = 12.37;
|
||||
when.setStyle("geojson", zoomLevel);
|
||||
then(get.elementByTestId("maplibre:ctrl-zoom")).shouldBeVisible();
|
||||
when.clickZoomIn();
|
||||
then(get.elementByTestId("maplibre:ctrl-zoom")).shouldContainText(
|
||||
"Zoom: " + (zoomLevel + 1)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("search", () => {
|
||||
it('should exist', () => {
|
||||
then(get.searchControl()).shouldBeVisible();
|
||||
});
|
||||
});
|
||||
});
|
||||
19
cypress/e2e/maputnik-cypress-helper.ts
Normal file
19
cypress/e2e/maputnik-cypress-helper.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { CypressHelper } from "@shellygo/cypress-test-utils";
|
||||
|
||||
export default class MaputnikCypressHelper {
|
||||
private helper = new CypressHelper({ defaultDataAttribute: "data-wd-key" });
|
||||
|
||||
public given = {
|
||||
...this.helper.given,
|
||||
};
|
||||
|
||||
public get = {
|
||||
...this.helper.get,
|
||||
};
|
||||
|
||||
public when = {
|
||||
...this.helper.when,
|
||||
};
|
||||
|
||||
public beforeAndAfter = this.helper.beforeAndAfter;
|
||||
}
|
||||
186
cypress/e2e/maputnik-driver.ts
Normal file
186
cypress/e2e/maputnik-driver.ts
Normal file
@@ -0,0 +1,186 @@
|
||||
/// <reference types="cypress-plugin-tab" />
|
||||
|
||||
import { CypressHelper } from "@shellygo/cypress-test-utils";
|
||||
import { Assertable, then } from "@shellygo/cypress-test-utils/assertable";
|
||||
import MaputnikCypressHelper from "./maputnik-cypress-helper";
|
||||
import ModalDriver from "./modal-driver";
|
||||
const baseUrl = "http://localhost:8888/";
|
||||
|
||||
const styleFromWindow = (win: Window) => {
|
||||
const styleId = win.localStorage.getItem("maputnik:latest_style");
|
||||
const styleItem = win.localStorage.getItem(`maputnik:style:${styleId}`);
|
||||
const obj = JSON.parse(styleItem || "");
|
||||
return obj;
|
||||
};
|
||||
|
||||
export class MaputnikAssertable<T> extends Assertable<T> {
|
||||
shouldEqualToStoredStyle = () =>
|
||||
then(
|
||||
new CypressHelper().get.window().then((win: Window) => {
|
||||
const style = styleFromWindow(win);
|
||||
then(this.chainable).shouldDeepNestedInclude(style);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export class MaputnikDriver {
|
||||
private helper = new MaputnikCypressHelper();
|
||||
private modalDriver = new ModalDriver();
|
||||
|
||||
public beforeAndAfter = () => {
|
||||
beforeEach(() => {
|
||||
this.given.setupMockBackedResponses();
|
||||
this.when.setStyle("both");
|
||||
});
|
||||
};
|
||||
|
||||
public then = (chainable: Cypress.Chainable<any>) =>
|
||||
new MaputnikAssertable(chainable);
|
||||
|
||||
public given = {
|
||||
...this.helper.given,
|
||||
setupMockBackedResponses: () => {
|
||||
this.helper.given.interceptAndMockResponse({
|
||||
method: "GET",
|
||||
url: baseUrl + "example-style.json",
|
||||
response: {
|
||||
fixture: "example-style.json",
|
||||
},
|
||||
alias: "example-style.json",
|
||||
});
|
||||
this.helper.given.interceptAndMockResponse({
|
||||
method: "GET",
|
||||
url: baseUrl + "example-layer-style.json",
|
||||
response: {
|
||||
fixture: "example-layer-style.json",
|
||||
},
|
||||
});
|
||||
this.helper.given.interceptAndMockResponse({
|
||||
method: "GET",
|
||||
url: baseUrl + "geojson-style.json",
|
||||
response: {
|
||||
fixture: "geojson-style.json",
|
||||
},
|
||||
});
|
||||
this.helper.given.interceptAndMockResponse({
|
||||
method: "GET",
|
||||
url: baseUrl + "raster-style.json",
|
||||
response: {
|
||||
fixture: "raster-style.json",
|
||||
},
|
||||
});
|
||||
this.helper.given.interceptAndMockResponse({
|
||||
method: "GET",
|
||||
url: baseUrl + "geojson-raster-style.json",
|
||||
response: {
|
||||
fixture: "geojson-raster-style.json",
|
||||
},
|
||||
});
|
||||
this.helper.given.interceptAndMockResponse({
|
||||
method: "GET",
|
||||
url: "*example.local/*",
|
||||
response: [],
|
||||
});
|
||||
this.helper.given.interceptAndMockResponse({
|
||||
method: "GET",
|
||||
url: "*example.com/*",
|
||||
response: [],
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
public when = {
|
||||
...this.helper.when,
|
||||
modal: this.modalDriver.when,
|
||||
within: (selector: string, fn: () => void) => {
|
||||
this.helper.when.within(fn, selector);
|
||||
},
|
||||
tab: () => this.helper.get.element("body").tab(),
|
||||
waitForExampleFileResponse: () => {
|
||||
this.helper.when.waitForResponse("example-style.json");
|
||||
},
|
||||
chooseExampleFile: () => {
|
||||
this.helper.get
|
||||
.bySelector("type", "file")
|
||||
.selectFile("cypress/fixtures/example-style.json", { force: true });
|
||||
},
|
||||
setStyle: (
|
||||
styleProperties: "geojson" | "raster" | "both" | "layer" | "",
|
||||
zoom?: number
|
||||
) => {
|
||||
let url = "?debug";
|
||||
switch (styleProperties) {
|
||||
case "geojson":
|
||||
url += `&style=${baseUrl}geojson-style.json`;
|
||||
break;
|
||||
case "raster":
|
||||
url += `&style=${baseUrl}raster-style.json`;
|
||||
break;
|
||||
case "both":
|
||||
url += `&style=${baseUrl}geojson-raster-style.json`;
|
||||
break;
|
||||
case "layer":
|
||||
url += `&style=${baseUrl}/example-layer-style.json`;
|
||||
break;
|
||||
}
|
||||
if (zoom) {
|
||||
url += `#${zoom}/41.3805/2.1635`;
|
||||
}
|
||||
this.helper.when.visit(baseUrl + url);
|
||||
if (styleProperties) {
|
||||
this.helper.when.acceptConfirm();
|
||||
}
|
||||
// when methods should not include assertions
|
||||
const toolbarLink = this.helper.get.elementByTestId("toolbar:link")
|
||||
toolbarLink.scrollIntoView();
|
||||
toolbarLink.should("be.visible");
|
||||
},
|
||||
|
||||
typeKeys: (keys: string) => this.helper.get.element("body").type(keys),
|
||||
|
||||
clickZoomIn: () => {
|
||||
this.helper.get.element(".maplibregl-ctrl-zoom-in").click();
|
||||
},
|
||||
|
||||
selectWithin: (selector: string, value: string) => {
|
||||
this.when.within(selector, () => {
|
||||
this.helper.get.element("select").select(value);
|
||||
});
|
||||
},
|
||||
|
||||
select: (selector: string, value: string) => {
|
||||
this.helper.get.elementByTestId(selector).select(value);
|
||||
},
|
||||
|
||||
focus: (selector: string) => {
|
||||
this.helper.when.focus(selector);
|
||||
},
|
||||
|
||||
setValue: (selector: string, text: string) => {
|
||||
this.helper.get
|
||||
.elementByTestId(selector)
|
||||
.clear()
|
||||
.type(text, { parseSpecialCharSequences: false });
|
||||
},
|
||||
};
|
||||
|
||||
public get = {
|
||||
...this.helper.get,
|
||||
isMac: () => {
|
||||
return Cypress.platform === "darwin";
|
||||
},
|
||||
|
||||
styleFromLocalStorage: () =>
|
||||
this.helper.get.window().then((win) => styleFromWindow(win)),
|
||||
|
||||
exampleFileUrl: () => {
|
||||
return baseUrl + "example-style.json";
|
||||
},
|
||||
skipTargetLayerList: () =>
|
||||
this.helper.get.elementByTestId("skip-target-layer-list"),
|
||||
skipTargetLayerEditor: () =>
|
||||
this.helper.get.elementByTestId("skip-target-layer-editor"),
|
||||
canvas: () => this.helper.get.element("canvas"),
|
||||
searchControl: () => this.helper.get.element('.maplibregl-ctrl-geocoder')
|
||||
};
|
||||
}
|
||||
40
cypress/e2e/modal-driver.ts
Normal file
40
cypress/e2e/modal-driver.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { v1 as uuid } from "uuid";
|
||||
import MaputnikCypressHelper from "./maputnik-cypress-helper";
|
||||
|
||||
export default class ModalDriver {
|
||||
private helper = new MaputnikCypressHelper();
|
||||
|
||||
public when = {
|
||||
fillLayers: (opts: { type: string; layer?: string; id?: string }) => {
|
||||
// Having logic in test code is an anti pattern.
|
||||
// This should be splitted to multiple single responsibility functions
|
||||
let type = opts.type;
|
||||
let layer = opts.layer;
|
||||
let id;
|
||||
if (opts.id) {
|
||||
id = opts.id;
|
||||
} else {
|
||||
id = `${type}:${uuid()}`;
|
||||
}
|
||||
this.helper.when.selectOption("add-layer.layer-type.select", type);
|
||||
this.helper.when.type("add-layer.layer-id.input", id);
|
||||
|
||||
if (layer) {
|
||||
this.helper.when.within(() => {
|
||||
this.helper.get.element("input").type(layer!);
|
||||
}, "add-layer.layer-source-block");
|
||||
}
|
||||
this.helper.when.click("add-layer");
|
||||
|
||||
return id;
|
||||
},
|
||||
|
||||
open: () => {
|
||||
this.helper.when.click("layer-list:add-layer");
|
||||
},
|
||||
|
||||
close: (key: string) => {
|
||||
this.helper.when.click(key + ".close-modal");
|
||||
},
|
||||
};
|
||||
}
|
||||
180
cypress/e2e/modals.cy.ts
Normal file
180
cypress/e2e/modals.cy.ts
Normal file
@@ -0,0 +1,180 @@
|
||||
import { MaputnikDriver } from "./maputnik-driver";
|
||||
|
||||
describe("modals", () => {
|
||||
let { beforeAndAfter, when, get, then } = new MaputnikDriver();
|
||||
beforeAndAfter();
|
||||
|
||||
beforeEach(() => {
|
||||
when.setStyle("");
|
||||
});
|
||||
describe("open", () => {
|
||||
beforeEach(() => {
|
||||
when.click("nav:open");
|
||||
});
|
||||
|
||||
it("close", () => {
|
||||
when.modal.close("modal:open");
|
||||
then(get.elementByTestId("modal:open")).shouldNotExist();
|
||||
});
|
||||
|
||||
it.skip("upload", () => {
|
||||
// HM: I was not able to make the following choose file actually to select a file and close the modal...
|
||||
when.chooseExampleFile();
|
||||
then(get.responseBody("example-style.json")).shouldEqualToStoredStyle();
|
||||
});
|
||||
|
||||
describe("when click open url", () => {
|
||||
beforeEach(() => {
|
||||
let styleFileUrl = get.exampleFileUrl();
|
||||
|
||||
when.setValue("modal:open.url.input", styleFileUrl);
|
||||
when.click("modal:open.url.button");
|
||||
when.wait(200);
|
||||
});
|
||||
it("load from url", () => {
|
||||
then(get.responseBody("example-style.json")).shouldEqualToStoredStyle();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("shortcuts", () => {
|
||||
it("open/close", () => {
|
||||
when.setStyle("");
|
||||
when.typeKeys("?");
|
||||
when.modal.close("modal:shortcuts");
|
||||
then(get.elementByTestId("modal:shortcuts")).shouldNotExist();
|
||||
});
|
||||
});
|
||||
|
||||
describe("export", () => {
|
||||
beforeEach(() => {
|
||||
when.click("nav:export");
|
||||
});
|
||||
|
||||
it("close", () => {
|
||||
when.modal.close("modal:export");
|
||||
then(get.elementByTestId("modal:export")).shouldNotExist();
|
||||
});
|
||||
|
||||
// TODO: Work out how to download a file and check the contents
|
||||
it("download");
|
||||
});
|
||||
|
||||
describe("sources", () => {
|
||||
it("active sources");
|
||||
it("public source");
|
||||
it("add new source");
|
||||
});
|
||||
|
||||
describe("inspect", () => {
|
||||
it("toggle", () => {
|
||||
// There is no assertion in this test
|
||||
when.setStyle("geojson");
|
||||
when.select("maputnik-select", "inspect");
|
||||
});
|
||||
});
|
||||
|
||||
describe("style settings", () => {
|
||||
beforeEach(() => {
|
||||
when.click("nav:settings");
|
||||
});
|
||||
|
||||
describe("when click name filed spec information", () => {
|
||||
beforeEach(() => {
|
||||
when.click("field-doc-button-Name");
|
||||
});
|
||||
|
||||
it("should show the spec information", () => {
|
||||
then(get.elementsText("spec-field-doc")).shouldInclude(
|
||||
"name for the style"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when set name and click owner", () => {
|
||||
beforeEach(() => {
|
||||
when.setValue("modal:settings.name", "foobar");
|
||||
when.click("modal:settings.owner");
|
||||
when.wait(200);
|
||||
});
|
||||
|
||||
it("show name specifications", () => {
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
name: "foobar",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("when set owner and click name", () => {
|
||||
beforeEach(() => {
|
||||
when.setValue("modal:settings.owner", "foobar");
|
||||
when.click("modal:settings.name");
|
||||
when.wait(200);
|
||||
});
|
||||
it("should update owner in local storage", () => {
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
owner: "foobar",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("sprite url", () => {
|
||||
when.setValue("modal:settings.sprite", "http://example.com");
|
||||
when.click("modal:settings.name");
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
sprite: "http://example.com",
|
||||
});
|
||||
});
|
||||
it("glyphs url", () => {
|
||||
let glyphsUrl = "http://example.com/{fontstack}/{range}.pbf";
|
||||
when.setValue("modal:settings.glyphs", glyphsUrl);
|
||||
when.click("modal:settings.name");
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
glyphs: glyphsUrl,
|
||||
});
|
||||
});
|
||||
|
||||
it("maptiler access token", () => {
|
||||
let apiKey = "testing123";
|
||||
when.setValue(
|
||||
"modal:settings.maputnik:openmaptiles_access_token",
|
||||
apiKey
|
||||
);
|
||||
when.click("modal:settings.name");
|
||||
then(
|
||||
get.styleFromLocalStorage().then((style) => style.metadata)
|
||||
).shouldInclude({
|
||||
"maputnik:openmaptiles_access_token": apiKey,
|
||||
});
|
||||
});
|
||||
|
||||
it("thunderforest access token", () => {
|
||||
let apiKey = "testing123";
|
||||
when.setValue(
|
||||
"modal:settings.maputnik:thunderforest_access_token",
|
||||
apiKey
|
||||
);
|
||||
when.click("modal:settings.name");
|
||||
then(
|
||||
get.styleFromLocalStorage().then((style) => style.metadata)
|
||||
).shouldInclude({ "maputnik:thunderforest_access_token": apiKey });
|
||||
});
|
||||
|
||||
it("style renderer", () => {
|
||||
cy.on("uncaught:exception", () => false); // this is due to the fact that this is an invalid style for openlayers
|
||||
when.select("modal:settings.maputnik:renderer", "ol");
|
||||
then(get.inputValue("modal:settings.maputnik:renderer")).shouldEqual(
|
||||
"ol"
|
||||
);
|
||||
|
||||
when.click("modal:settings.name");
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
metadata: { "maputnik:renderer": "ol" },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("sources", () => {
|
||||
it("toggle");
|
||||
});
|
||||
});
|
||||
18
cypress/fixtures/example-layer-style.json
Normal file
18
cypress/fixtures/example-layer-style.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"id": "test-style",
|
||||
"version": 8,
|
||||
"name": "Test Style",
|
||||
"metadata": {
|
||||
"maputnik:renderer": "mlgljs"
|
||||
},
|
||||
"sources": {},
|
||||
"glyphs": "https://example.local/fonts/{fontstack}/{range}.pbf",
|
||||
"sprites": "https://example.local/fonts/{fontstack}/{range}.pbf",
|
||||
"layers": [
|
||||
{
|
||||
"id": "background",
|
||||
"type": "background"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"version": 8,
|
||||
"name": "Test Style",
|
||||
"metadata": {
|
||||
"maputnik:renderer": "mbgljs"
|
||||
"maputnik:renderer": "mlgljs"
|
||||
},
|
||||
"sources": {},
|
||||
"glyphs": "https://example.local/fonts/{fontstack}/{range}.pbf",
|
||||
34
cypress/fixtures/geojson-raster-style.json
Normal file
34
cypress/fixtures/geojson-raster-style.json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"id": "test-style",
|
||||
"version": 8,
|
||||
"name": "Test Style",
|
||||
"metadata": {
|
||||
"maputnik:renderer": "mlgljs"
|
||||
},
|
||||
"sources": {
|
||||
"example": {
|
||||
"type": "vector",
|
||||
"data": {
|
||||
"type": "FeatureCollection",
|
||||
"features":[{
|
||||
"type": "Feature",
|
||||
"properties": {
|
||||
"name": "Dinagat Islands"
|
||||
},
|
||||
"geometry":{
|
||||
"type": "Point",
|
||||
"coordinates": [125.6, 10.1]
|
||||
}
|
||||
}]
|
||||
}
|
||||
},
|
||||
"raster": {
|
||||
"tileSize": 256,
|
||||
"tiles": ["http://localhost/example/{x}/{y}/{z}"],
|
||||
"type": "raster"
|
||||
}
|
||||
},
|
||||
"glyphs": "https://example.local/fonts/{fontstack}/{range}.pbf",
|
||||
"sprites": "https://example.local/fonts/{fontstack}/{range}.pbf",
|
||||
"layers": []
|
||||
}
|
||||
29
cypress/fixtures/geojson-style.json
Normal file
29
cypress/fixtures/geojson-style.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"id": "test-style",
|
||||
"version": 8,
|
||||
"name": "Test Style",
|
||||
"metadata": {
|
||||
"maputnik:renderer": "mlgljs"
|
||||
},
|
||||
"sources": {
|
||||
"example": {
|
||||
"type": "vector",
|
||||
"data": {
|
||||
"type": "FeatureCollection",
|
||||
"features":[{
|
||||
"type": "Feature",
|
||||
"properties": {
|
||||
"name": "Dinagat Islands"
|
||||
},
|
||||
"geometry":{
|
||||
"type": "Point",
|
||||
"coordinates": [125.6, 10.1]
|
||||
}
|
||||
}]
|
||||
}
|
||||
}
|
||||
},
|
||||
"glyphs": "https://example.local/fonts/{fontstack}/{range}.pbf",
|
||||
"sprites": "https://example.local/fonts/{fontstack}/{range}.pbf",
|
||||
"layers": []
|
||||
}
|
||||
18
cypress/fixtures/raster-style.json
Normal file
18
cypress/fixtures/raster-style.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"id": "test-style",
|
||||
"version": 8,
|
||||
"name": "Test Style",
|
||||
"metadata": {
|
||||
"maputnik:renderer": "mlgljs"
|
||||
},
|
||||
"sources": {
|
||||
"raster": {
|
||||
"tileSize": 256,
|
||||
"tiles": ["http://localhost/example/{x}/{y}/{z}"],
|
||||
"type": "raster"
|
||||
}
|
||||
},
|
||||
"glyphs": "https://example.local/fonts/{fontstack}/{range}.pbf",
|
||||
"sprites": "https://example.local/fonts/{fontstack}/{range}.pbf",
|
||||
"layers": []
|
||||
}
|
||||
37
cypress/support/commands.ts
Normal file
37
cypress/support/commands.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
/// <reference types="cypress" />
|
||||
// ***********************************************
|
||||
// This example commands.ts shows you how to
|
||||
// create various custom commands and overwrite
|
||||
// existing commands.
|
||||
//
|
||||
// For more comprehensive examples of custom
|
||||
// commands please read more here:
|
||||
// https://on.cypress.io/custom-commands
|
||||
// ***********************************************
|
||||
//
|
||||
//
|
||||
// -- This is a parent command --
|
||||
// Cypress.Commands.add('login', (email, password) => { ... })
|
||||
//
|
||||
//
|
||||
// -- This is a child command --
|
||||
// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
|
||||
//
|
||||
//
|
||||
// -- This is a dual command --
|
||||
// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
|
||||
//
|
||||
//
|
||||
// -- This will overwrite an existing command --
|
||||
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
|
||||
//
|
||||
// declare global {
|
||||
// namespace Cypress {
|
||||
// interface Chainable {
|
||||
// login(email: string, password: string): Chainable<void>
|
||||
// drag(subject: string, options?: Partial<TypeOptions>): Chainable<Element>
|
||||
// dismiss(subject: string, options?: Partial<TypeOptions>): Chainable<Element>
|
||||
// visit(originalFn: CommandOriginalFn, url: string, options: Partial<VisitOptions>): Chainable<Element>
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
22
cypress/support/e2e.ts
Normal file
22
cypress/support/e2e.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
// ***********************************************************
|
||||
// This example support/e2e.ts is processed and
|
||||
// loaded automatically before your test files.
|
||||
//
|
||||
// This is a great place to put global configuration and
|
||||
// behavior that modifies Cypress.
|
||||
//
|
||||
// You can change the location of this file or turn off
|
||||
// automatically serving support files with the
|
||||
// 'supportFile' configuration option.
|
||||
//
|
||||
// You can read more here:
|
||||
// https://on.cypress.io/configuration
|
||||
// ***********************************************************
|
||||
|
||||
// Import commands.js using ES2015 syntax:
|
||||
import "@cypress/code-coverage/support";
|
||||
import "cypress-plugin-tab";
|
||||
import "./commands";
|
||||
|
||||
// Alternatively you can use CommonJS syntax:
|
||||
// require('./commands')
|
||||
31
desktop/.gitignore
vendored
Normal file
31
desktop/.gitignore
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
# Compiled Object files, Static and Dynamic libs (Shared Objects)
|
||||
*.o
|
||||
*.a
|
||||
*.so
|
||||
|
||||
# Folders
|
||||
_obj
|
||||
_test
|
||||
editor
|
||||
|
||||
# Architecture specific extensions/prefixes
|
||||
*.[568vq]
|
||||
[568vq].out
|
||||
|
||||
*.cgo1.go
|
||||
*.cgo2.c
|
||||
_cgo_defun.c
|
||||
_cgo_gotypes.go
|
||||
_cgo_export.*
|
||||
|
||||
_testmain.go
|
||||
|
||||
*.exe
|
||||
*.test
|
||||
*.prof
|
||||
|
||||
# Binary version of pubilic/editor
|
||||
rice-box.go
|
||||
|
||||
# Built binary
|
||||
maputnik
|
||||
21
desktop/LICENSE
Normal file
21
desktop/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2016 Maputnik
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
39
desktop/Makefile
Normal file
39
desktop/Makefile
Normal file
@@ -0,0 +1,39 @@
|
||||
SOURCEDIR=.
|
||||
SOURCES := $(shell find $(SOURCEDIR) -name '*.go')
|
||||
BINARY=maputnik
|
||||
DESKTOP_VERSION := 1.1.1
|
||||
EDITOR_VERSION := $(shell node -p "require('../package.json').version")
|
||||
GOPATH := $(if $(GOPATH),$(GOPATH),$(HOME)/go)
|
||||
GOBIN := $(if $(GOBIN),$(GOBIN),$(HOME)/go/bin)
|
||||
|
||||
all: $(BINARY)
|
||||
|
||||
$(BINARY): $(GOBIN)/gox $(SOURCES) version.go rice-box.go
|
||||
$(GOBIN)/gox -osarch "windows/amd64 linux/amd64 darwin/amd64" -output "bin/{{.OS}}/${BINARY}"
|
||||
|
||||
# Copy the current release into ./editor/maputnik so it can be
|
||||
# embedded in the binary
|
||||
editor/pull_release:
|
||||
mkdir -p editor
|
||||
cp -r ../dist/* editor
|
||||
|
||||
$(GOBIN)/gox:
|
||||
go install github.com/mitchellh/gox@v1.0.1
|
||||
|
||||
$(GOBIN)/rice:
|
||||
go install github.com/GeertJohan/go.rice/rice@v1.0.3
|
||||
|
||||
# Embed the current version numbers in the executable by writing version.go
|
||||
.PHONY: version.go
|
||||
version.go:
|
||||
@echo "// DO NOT EDIT: Autogenerated by Makefile\n" > version.go
|
||||
@echo "package main\n" >> version.go
|
||||
@echo "const DesktopVersion = \"$(DESKTOP_VERSION)\"" >> version.go
|
||||
@echo "const EditorVersion = \"$(EDITOR_VERSION)\"" >> version.go
|
||||
|
||||
rice-box.go: $(GOBIN)/rice editor/pull_release
|
||||
$(GOBIN)/rice embed-go
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
rm -rf editor && rm -f rice-box.go && rm -rf bin
|
||||
72
desktop/README.md
Normal file
72
desktop/README.md
Normal file
@@ -0,0 +1,72 @@
|
||||
# Maputnik Desktop [][github-action-ci]
|
||||
---
|
||||
|
||||
A Golang based cross platform executable for integrating Maputnik locally.
|
||||
This binary packages up the JavaScript and CSS bundle produced by maputnik
|
||||
and embeds it in the program for easy distribution. It also allows
|
||||
exposing a local style file and work on it both in Maputnik and with your favorite
|
||||
editor.
|
||||
|
||||
Report issues on [maplibre/maputnik](https://github.com/maplibre/maputnik).
|
||||
|
||||
## Install
|
||||
|
||||
You can download a single binary for Linux, OSX or Windows from [the latest releases of **maplibre/maputnik**](https://github.com/maplibre/maputnik/editor/releases/latest).
|
||||
|
||||
### Usage
|
||||
|
||||
Simply start up a web server and access the Maputnik editor GUI at `localhost:8000`.
|
||||
|
||||
```bash
|
||||
maputnik
|
||||
```
|
||||
|
||||
Expose a local style file to Maputnik allowing the web based editor
|
||||
to save to the local filesystem.
|
||||
|
||||
```bash
|
||||
maputnik --file basic-v9.json
|
||||
```
|
||||
|
||||
Watch the local style for changes and inform the editor via web socket.
|
||||
This makes it possible to edit the style with a local text editor and still
|
||||
use Maputnik.
|
||||
|
||||
```bash
|
||||
maputnik --watch --file basic-v9.json
|
||||
```
|
||||
|
||||
Choose a local port to listen on, instead of using the default port 8000.
|
||||
|
||||
```bash
|
||||
maputnik --port 8001
|
||||
```
|
||||
|
||||
Specify a path to a directory which, if it exists, will be served under http://localhost:8000/static/ .
|
||||
Could be used to serve sprites and glyphs.
|
||||
|
||||
```bash
|
||||
maputnik --static ./localFolder
|
||||
```
|
||||
|
||||
### API
|
||||
|
||||
`maputnik` exposes the configured styles via a HTTP API.
|
||||
|
||||
| Method | Description
|
||||
|---------------------------------|---------------------------------------
|
||||
| `GET /styles` | List the ID of all configured style files
|
||||
| `GET /styles/{filename}` | Get contents of a single style file
|
||||
| `PUT /styles/{filename}` | Update contents of a style file
|
||||
| `WEBSOCKET /ws` | Listen to change events for the configured style files
|
||||
|
||||
### Build
|
||||
|
||||
From the root of the [maplibre/maputnik](https://github.com/maplibre/maputnik) project, install the deps and run the desktop-build command.
|
||||
|
||||
```
|
||||
npm install
|
||||
npm run build-desktop
|
||||
```
|
||||
|
||||
You should now find the `maputnik` binary in your `bin` directory.
|
||||
81
desktop/api.go
Normal file
81
desktop/api.go
Normal file
@@ -0,0 +1,81 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
func StyleFileAccessor(filename string) styleFileAccessor {
|
||||
return styleFileAccessor{filename, styleId(filename)}
|
||||
}
|
||||
|
||||
func styleId(filename string) string {
|
||||
raw, err := ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
log.Panicln(err)
|
||||
}
|
||||
|
||||
var spec styleSpec
|
||||
err = json.Unmarshal(raw, &spec)
|
||||
if err != nil {
|
||||
log.Panicln(err)
|
||||
}
|
||||
|
||||
if spec.Id == "" {
|
||||
fmt.Println("No id in style")
|
||||
}
|
||||
return spec.Id
|
||||
}
|
||||
|
||||
type styleSpec struct {
|
||||
Id string `json:"id"`
|
||||
}
|
||||
|
||||
// Allows access to a single style file
|
||||
type styleFileAccessor struct {
|
||||
filename string
|
||||
id string
|
||||
}
|
||||
|
||||
func (fa styleFileAccessor) ListFiles(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
encoder := json.NewEncoder(w)
|
||||
encoder.Encode([]string{fa.id})
|
||||
}
|
||||
|
||||
func (fa styleFileAccessor) ReadFile(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
_ = vars["styleId"]
|
||||
|
||||
//TODO: Choose right file
|
||||
// right now we just return the single file we know of
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
raw, err := ioutil.ReadFile(fa.filename)
|
||||
if err != nil {
|
||||
log.Panicln(err)
|
||||
}
|
||||
w.Write(raw)
|
||||
}
|
||||
|
||||
func (fa styleFileAccessor) SaveFile(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
_ = vars["styleId"]
|
||||
|
||||
//TODO: Save to right file
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
body, _ := ioutil.ReadAll(r.Body)
|
||||
var out bytes.Buffer
|
||||
json.Indent(&out, body, "", " ")
|
||||
|
||||
if err := ioutil.WriteFile(fa.filename, out.Bytes(), 0666); err != nil {
|
||||
log.Fatalf("Can not copy from request to file: %s", err.Error())
|
||||
}
|
||||
}
|
||||
69
desktop/filewatch/filewatch.go
Normal file
69
desktop/filewatch/filewatch.go
Normal file
@@ -0,0 +1,69 @@
|
||||
package filewatch
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/fsnotify/fsnotify"
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
var upgrader = websocket.Upgrader{
|
||||
ReadBufferSize: 1024,
|
||||
WriteBufferSize: 1024,
|
||||
CheckOrigin: func(r *http.Request) bool { return true },
|
||||
}
|
||||
|
||||
func writer(ws *websocket.Conn, filename string) {
|
||||
watcher, err := fsnotify.NewWatcher()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer watcher.Close()
|
||||
|
||||
done := make(chan bool)
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case event := <-watcher.Events:
|
||||
if event.Op&fsnotify.Write == fsnotify.Write {
|
||||
log.Println("Modified file:", event.Name)
|
||||
var p []byte
|
||||
var err error
|
||||
|
||||
p, err = ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if p != nil {
|
||||
if err := ws.WriteMessage(websocket.TextMessage, p); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
case err := <-watcher.Errors:
|
||||
log.Println("Watch error:", err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
if err = watcher.Add(filename); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
<-done
|
||||
}
|
||||
|
||||
func ServeWebsocketFileWatcher(filename string, w http.ResponseWriter, r *http.Request) {
|
||||
ws, err := upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
if _, ok := err.(websocket.HandshakeError); !ok {
|
||||
log.Println(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
writer(ws, filename)
|
||||
defer ws.Close()
|
||||
}
|
||||
27
desktop/go.mod
Normal file
27
desktop/go.mod
Normal file
@@ -0,0 +1,27 @@
|
||||
module maputnik/desktop
|
||||
|
||||
go 1.19
|
||||
|
||||
require (
|
||||
github.com/GeertJohan/go.rice v1.0.3
|
||||
github.com/fsnotify/fsnotify v1.6.0
|
||||
github.com/gorilla/handlers v1.5.1
|
||||
github.com/gorilla/mux v1.8.0
|
||||
github.com/gorilla/websocket v1.5.0
|
||||
github.com/maputnik/desktop v1.0.7
|
||||
github.com/urfave/cli v1.22.12
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/GeertJohan/go.incremental v1.0.0 // indirect
|
||||
github.com/akavel/rsrc v0.8.0 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
|
||||
github.com/daaku/go.zipexe v1.0.2 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.1 // indirect
|
||||
github.com/jessevdk/go-flags v1.4.0 // indirect
|
||||
github.com/nkovacs/streamquote v1.0.0 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasttemplate v1.0.1 // indirect
|
||||
golang.org/x/sys v0.0.0-20220908164124-27713097b956 // indirect
|
||||
)
|
||||
54
desktop/go.sum
Normal file
54
desktop/go.sum
Normal file
@@ -0,0 +1,54 @@
|
||||
github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||
github.com/GeertJohan/go.incremental v1.0.0 h1:7AH+pY1XUgQE4Y1HcXYaMqAI0m9yrFqo/jt0CW30vsg=
|
||||
github.com/GeertJohan/go.incremental v1.0.0/go.mod h1:6fAjUhbVuX1KcMD3c8TEgVUqmo4seqhv0i0kdATSkM0=
|
||||
github.com/GeertJohan/go.rice v1.0.3 h1:k5viR+xGtIhF61125vCE1cmJ5957RQGXG6dmbaWZSmI=
|
||||
github.com/GeertJohan/go.rice v1.0.3/go.mod h1:XVdrU4pW00M4ikZed5q56tPf1v2KwnIKeIdc9CBYNt4=
|
||||
github.com/akavel/rsrc v0.8.0 h1:zjWn7ukO9Kc5Q62DOJCcxGpXC18RawVtYAGdz2aLlfw=
|
||||
github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/daaku/go.zipexe v1.0.2 h1:Zg55YLYTr7M9wjKn8SY/WcpuuEi+kR2u4E8RhvpyXmk=
|
||||
github.com/daaku/go.zipexe v1.0.2/go.mod h1:5xWogtqlYnfBXkSB1o9xysukNP9GTvaNkqzUZbt3Bw8=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ=
|
||||
github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
|
||||
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
|
||||
github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4=
|
||||
github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q=
|
||||
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
|
||||
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
||||
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
||||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA=
|
||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
github.com/maputnik/desktop v1.0.7 h1:rdFg7emIJOT3YsZpwqSChmWtMOvu+T4h6WwVQAZP9n4=
|
||||
github.com/maputnik/desktop v1.0.7/go.mod h1:wmDjHUztx9jOBz0I22589yWguAGdV/sEM57YANpN8oQ=
|
||||
github.com/nkovacs/streamquote v1.0.0 h1:PmVIV08Zlx2lZK5fFZlMZ04eHcDTIFJCv/5/0twVUow=
|
||||
github.com/nkovacs/streamquote v1.0.0/go.mod h1:BN+NaZ2CmdKqUuTUXUEm9j95B2TRbpOWpxbJYzzgUsc=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/urfave/cli v1.22.12 h1:igJgVw1JdKH+trcLWLeLwZjU9fEfPesQ+9/e4MQ44S8=
|
||||
github.com/urfave/cli v1.22.12/go.mod h1:sSBEIC79qR6OvcmsD4U3KABeOTxDqQtdDnaFuUN30b8=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasttemplate v1.0.1 h1:tY9CJiPnMXf1ERmG2EyK7gNUd+c6RKGD0IfU8WdUSz8=
|
||||
github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
|
||||
golang.org/x/sys v0.0.0-20220908164124-27713097b956 h1:XeJjHH1KiLpKGb6lvMiksZ9l0fVUh+AmGcm0nOMEBOY=
|
||||
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
80
desktop/maputnik.go
Normal file
80
desktop/maputnik.go
Normal file
@@ -0,0 +1,80 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/GeertJohan/go.rice"
|
||||
"github.com/gorilla/handlers"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/maputnik/desktop/filewatch"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
func main() {
|
||||
app := cli.NewApp()
|
||||
app.Name = "maputnik"
|
||||
app.Usage = "Server for integrating Maputnik locally"
|
||||
app.Version = "Editor: " + EditorVersion + "; Desktop: " + DesktopVersion
|
||||
|
||||
app.Flags = []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "file, f",
|
||||
Usage: "Allow access to JSON style from web client",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "watch",
|
||||
Usage: "Notify web client about JSON style file changes",
|
||||
},
|
||||
&cli.IntFlag{
|
||||
Name: "port",
|
||||
Value: 8000,
|
||||
Usage: "TCP port to listen on",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "static",
|
||||
Usage: "Serve directory under /static/",
|
||||
},
|
||||
}
|
||||
|
||||
app.Action = func(c *cli.Context) error {
|
||||
gui := http.FileServer(rice.MustFindBox("editor").HTTPBox())
|
||||
|
||||
router := mux.NewRouter().StrictSlash(true)
|
||||
|
||||
filename := c.String("file")
|
||||
if filename != "" {
|
||||
fmt.Printf("%s is accessible via Maputnik\n", filename)
|
||||
// Allow access to reading and writing file on the local system
|
||||
path, _ := filepath.Abs(filename)
|
||||
accessor := StyleFileAccessor(path)
|
||||
router.Path("/styles").Methods("GET").HandlerFunc(accessor.ListFiles)
|
||||
router.Path("/styles/{styleId}").Methods("GET").HandlerFunc(accessor.ReadFile)
|
||||
router.Path("/styles/{styleId}").Methods("PUT").HandlerFunc(accessor.SaveFile)
|
||||
|
||||
// Register websocket to notify we clients about file changes
|
||||
if c.Bool("watch") {
|
||||
router.Path("/ws").HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
filewatch.ServeWebsocketFileWatcher(filename, w, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
staticDir := c.String("static")
|
||||
if staticDir != "" {
|
||||
h := http.StripPrefix("/static/", http.FileServer(http.Dir(staticDir)))
|
||||
router.PathPrefix("/static/").Handler(h)
|
||||
}
|
||||
|
||||
router.PathPrefix("/").Handler(http.StripPrefix("/", gui))
|
||||
loggedRouter := handlers.LoggingHandler(os.Stdout, router)
|
||||
corsRouter := handlers.CORS(handlers.AllowedHeaders([]string{"Content-Type"}), handlers.AllowedMethods([]string{"GET", "PUT"}), handlers.AllowedOrigins([]string{"*"}), handlers.AllowCredentials())(loggedRouter)
|
||||
|
||||
fmt.Printf("Exposing Maputnik on http://localhost:%d\n", c.Int("port"))
|
||||
return http.ListenAndServe(fmt.Sprintf(":%d", c.Int("port")), corsRouter)
|
||||
}
|
||||
|
||||
app.Run(os.Args)
|
||||
}
|
||||
17
i18next-parser.config.ts
Normal file
17
i18next-parser.config.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
export default {
|
||||
output: 'src/locales/$LOCALE/$NAMESPACE.json',
|
||||
locales: [ 'ja', 'he','zh' ],
|
||||
|
||||
// Because some keys are dynamically generated, i18next-parser can't detect them.
|
||||
// We add these keys manually, so we don't want to remove them.
|
||||
keepRemoved: true,
|
||||
|
||||
// We use plain English keys, so we disable key and namespace separators.
|
||||
keySeparator: false,
|
||||
namespaceSeparator: false,
|
||||
|
||||
defaultValue: (locale, ns, key) => {
|
||||
// The default value is a string that indicates that the string is not translated.
|
||||
return '__STRING_NOT_TRANSLATED__';
|
||||
}
|
||||
}
|
||||
@@ -2,76 +2,46 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title><%= htmlWebpackPlugin.options.title %></title>
|
||||
<title>Maputnik</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="manifest" href="manifest.json">
|
||||
<link rel="manifest" href="src/manifest.json">
|
||||
<link rel="icon" href="src/favicon.ico" type="image/x-icon" />
|
||||
<style>
|
||||
html {
|
||||
background-color: rgb(28, 31, 36);
|
||||
}
|
||||
#loader,
|
||||
#loader:before,
|
||||
#loader:after {
|
||||
border-radius: 50%;
|
||||
width: 2.5em;
|
||||
height: 2.5em;
|
||||
-webkit-animation-fill-mode: both;
|
||||
animation-fill-mode: both;
|
||||
-webkit-animation: pulseload 1.8s infinite ease-in-out;
|
||||
animation: pulseload 1.8s infinite ease-in-out;
|
||||
}
|
||||
#loader {
|
||||
color: #ffffff;
|
||||
font-size: 10px;
|
||||
margin: 80px auto;
|
||||
position: relative;
|
||||
text-indent: -9999em;
|
||||
-webkit-transform: translateZ(0);
|
||||
-ms-transform: translateZ(0);
|
||||
transform: translateZ(0);
|
||||
-webkit-animation-delay: -0.16s;
|
||||
animation-delay: -0.16s;
|
||||
}
|
||||
#loader:before,
|
||||
#loader:after {
|
||||
content: '';
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
position: absolute;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
top: 0;
|
||||
left: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
#loader:before {
|
||||
left: -3.5em;
|
||||
-webkit-animation-delay: -0.32s;
|
||||
animation-delay: -0.32s;
|
||||
|
||||
.loading__logo img {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
}
|
||||
#loader:after {
|
||||
left: 3.5em;
|
||||
}
|
||||
@-webkit-keyframes pulseload {
|
||||
0%,
|
||||
80%,
|
||||
100% {
|
||||
box-shadow: 0 2.5em 0 -1.3em;
|
||||
}
|
||||
40% {
|
||||
box-shadow: 0 2.5em 0 0;
|
||||
}
|
||||
}
|
||||
@keyframes pulseload {
|
||||
0%,
|
||||
80%,
|
||||
100% {
|
||||
box-shadow: 0 2.5em 0 -1.3em;
|
||||
}
|
||||
40% {
|
||||
box-shadow: 0 2.5em 0 0;
|
||||
}
|
||||
|
||||
.loading__text {
|
||||
font-family: sans-serif;
|
||||
color: white;
|
||||
font-size: 1.2em;
|
||||
padding-bottom: 2em;
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- TODO: Import dynamically -->
|
||||
<!-- From <https://github.com/hail2u/color-blindness-emulation> -->
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
version="1.1">
|
||||
<defs>
|
||||
@@ -150,8 +120,13 @@
|
||||
</defs>
|
||||
</svg>
|
||||
|
||||
<div id="app">
|
||||
<div id="loader">Loading...</div>
|
||||
<div id="app"></div>
|
||||
<div class="loading">
|
||||
<div class="loading__logo">
|
||||
<img inline src="node_modules/maputnik-design/logos/logo-loading.svg" />
|
||||
</div>
|
||||
<div class="loading__text">Loading…</div>
|
||||
</div>
|
||||
<script type="module" src="/src/index.jsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 16 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 8.2 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 24 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 65 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 37 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 14 KiB |
25806
package-lock.json
generated
25806
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
211
package.json
211
package.json
@@ -1,63 +1,82 @@
|
||||
{
|
||||
"name": "maputnik",
|
||||
"version": "1.6.0",
|
||||
"description": "A MapboxGL visual style editor",
|
||||
"version": "2.1.0",
|
||||
"description": "A MapLibre GL visual style editor",
|
||||
"type": "module",
|
||||
"main": "''",
|
||||
"scripts": {
|
||||
"stats": "webpack --config config/webpack.production.config.js --profile --json > stats.json",
|
||||
"build": "webpack --config config/webpack.production.config.js --progress --profile --colors",
|
||||
"test": "cross-env NODE_ENV=test wdio config/wdio.conf.js",
|
||||
"test-watch": "cross-env NODE_ENV=test wdio config/wdio.conf.js --watch",
|
||||
"start": "webpack-dev-server --progress --profile --colors --config config/webpack.config.js",
|
||||
"lint": "eslint --ext js --ext jsx src test",
|
||||
"lint-styles": "stylelint \"src/styles/*.scss\""
|
||||
"start": "vite",
|
||||
"build": "tsc && vite build --base=/maputnik/",
|
||||
"build-desktop": "tsc && vite build --base=/ && cd desktop && make",
|
||||
"i18n:refresh": "i18next 'src/**/*.{ts,tsx,js,jsx}'",
|
||||
"lint": "eslint ./src ./cypress --ext ts,tsx,js,jsx --report-unused-disable-directives --max-warnings 0",
|
||||
"test": "cypress run",
|
||||
"cy:open": "cypress open",
|
||||
"lint-css": "stylelint \"src/styles/*.scss\"",
|
||||
"sort-styles": "jq 'sort_by(.id)' src/config/styles.json > tmp.json && mv tmp.json src/config/styles.json"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/maputnik/editor"
|
||||
"url": "https://github.com/maplibre/maputnik"
|
||||
},
|
||||
"author": "Lukas Martinelli",
|
||||
"license": "MIT",
|
||||
"homepage": "https://github.com/maputnik/editor#readme",
|
||||
"homepage": "https://github.com/maplibre/maputnik#readme",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.1.2",
|
||||
"@mapbox/mapbox-gl-rtl-text": "^0.2.2",
|
||||
"@mapbox/mapbox-gl-style-spec": "^13.7.2",
|
||||
"classnames": "^2.2.6",
|
||||
"codemirror": "^5.40.2",
|
||||
"color": "^3.0.0",
|
||||
"detect-browser": "^4.5.0",
|
||||
"file-saver": "^1.3.8",
|
||||
"@mapbox/mapbox-gl-rtl-text": "^0.2.3",
|
||||
"@maplibre/maplibre-gl-geocoder": "^1.6.0",
|
||||
"@maplibre/maplibre-gl-inspect": "^1.6.3",
|
||||
"@maplibre/maplibre-gl-style-spec": "^20.1.1",
|
||||
"@mdi/js": "^7.4.47",
|
||||
"@mdi/react": "^1.6.1",
|
||||
"@typescript-eslint/eslint-plugin": "^7.3.1",
|
||||
"@typescript-eslint/parser": "^7.3.1",
|
||||
"array-move": "^4.0.0",
|
||||
"buffer": "^6.0.3",
|
||||
"classnames": "^2.5.1",
|
||||
"codemirror": "^5.65.2",
|
||||
"color": "^4.2.3",
|
||||
"cypress-plugin-tab": "^1.0.5",
|
||||
"detect-browser": "^5.3.0",
|
||||
"events": "^3.3.0",
|
||||
"file-saver": "^2.0.5",
|
||||
"i18next": "^23.12.2",
|
||||
"i18next-browser-languagedetector": "^8.0.0",
|
||||
"i18next-resources-to-backend": "^1.2.1",
|
||||
"json-stringify-pretty-compact": "^4.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",
|
||||
"mapbox-gl": "^1.2.0",
|
||||
"mapbox-gl-inspect": "^1.3.1",
|
||||
"maputnik-design": "github:maputnik/design",
|
||||
"ol": "^6.0.0-beta.8",
|
||||
"ol-mapbox-style": "^5.0.0-beta.2",
|
||||
"prop-types": "^15.6.2",
|
||||
"react": "^16.5.2",
|
||||
"react-aria-menubutton": "^6.0.1",
|
||||
"react-aria-modal": "^3.0.0",
|
||||
"maplibre-gl": "^4.1.2",
|
||||
"maputnik-design": "github:maputnik/design#172b06c",
|
||||
"ol": "^6.14.1",
|
||||
"ol-mapbox-style": "^7.1.1",
|
||||
"prop-types": "^15.8.1",
|
||||
"react": "^18.2.0",
|
||||
"react-accessible-accordion": "^5.0.0",
|
||||
"react-aria-menubutton": "^7.0.3",
|
||||
"react-aria-modal": "^5.0.2",
|
||||
"react-autobind": "^1.0.6",
|
||||
"react-autocomplete": "^1.8.1",
|
||||
"react-codemirror2": "^5.1.0",
|
||||
"react-collapse": "^4.0.3",
|
||||
"react-color": "^2.14.1",
|
||||
"react-dom": "^16.5.2",
|
||||
"react-collapse": "^5.1.1",
|
||||
"react-color": "^2.19.3",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-file-reader-input": "^2.0.0",
|
||||
"react-i18next": "^15.0.1",
|
||||
"react-icon-base": "^2.1.2",
|
||||
"react-icons": "^3.1.0",
|
||||
"react-motion": "^0.5.2",
|
||||
"react-sortable-hoc": "^0.8.3",
|
||||
"reconnecting-websocket": "^3.2.2",
|
||||
"slugify": "^1.3.1",
|
||||
"url": "^0.11.0"
|
||||
"react-icons": "^5.0.1",
|
||||
"react-sortable-hoc": "^2.0.0",
|
||||
"reconnecting-websocket": "^4.4.0",
|
||||
"sass": "^1.72.0",
|
||||
"slugify": "^1.6.6",
|
||||
"string-hash": "^1.1.3",
|
||||
"url": "^0.11.3"
|
||||
},
|
||||
"jshintConfig": {
|
||||
"esversion": 6
|
||||
@@ -76,75 +95,55 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"eslintConfig": {
|
||||
"plugins": [
|
||||
"react"
|
||||
],
|
||||
"extends": [
|
||||
"plugin:react/recommended"
|
||||
],
|
||||
"env": {
|
||||
"browser": true,
|
||||
"node": true,
|
||||
"es6": true
|
||||
},
|
||||
"parser": "babel-eslint",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 6,
|
||||
"sourceType": "module",
|
||||
"ecmaFeatures": {
|
||||
"impliedStrict": true,
|
||||
"experimentalObjectRestSpread": true,
|
||||
"jsx": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.1.2",
|
||||
"@babel/plugin-proposal-class-properties": "^7.1.0",
|
||||
"@babel/plugin-transform-runtime": "^7.1.0",
|
||||
"@babel/preset-env": "^7.1.0",
|
||||
"@babel/preset-flow": "^7.0.0",
|
||||
"@babel/preset-react": "^7.0.0",
|
||||
"@wdio/cli": "^5.10.4",
|
||||
"@wdio/local-runner": "^5.10.4",
|
||||
"@wdio/mocha-framework": "^5.10.1",
|
||||
"@wdio/selenium-standalone-service": "^5.9.3",
|
||||
"@wdio/spec-reporter": "^5.9.3",
|
||||
"@wdio/sync": "^5.10.1",
|
||||
"babel-eslint": "^10.0.1",
|
||||
"babel-loader": "8.0.4",
|
||||
"babel-plugin-istanbul": "^5.0.1",
|
||||
"copy-webpack-plugin": "^4.5.2",
|
||||
"cors": "^2.8.4",
|
||||
"cross-env": "^5.2.0",
|
||||
"css-loader": "^1.0.0",
|
||||
"eslint": "^5.6.1",
|
||||
"eslint-plugin-react": "^7.11.1",
|
||||
"express": "^4.17.1",
|
||||
"file-loader": "^2.0.0",
|
||||
"html-webpack-plugin": "^3.2.0",
|
||||
"is-docker": "^2.0.0",
|
||||
"@cypress/code-coverage": "^3.12.30",
|
||||
"@istanbuljs/nyc-config-typescript": "^1.0.2",
|
||||
"@rollup/plugin-replace": "^5.0.5",
|
||||
"@shellygo/cypress-test-utils": "^2.1.9",
|
||||
"@types/codemirror": "^5.60.15",
|
||||
"@types/color": "^3.0.6",
|
||||
"@types/cors": "^2.8.17",
|
||||
"@types/file-saver": "^2.0.7",
|
||||
"@types/geojson": "^7946.0.14",
|
||||
"@types/json-to-ast": "^2.1.4",
|
||||
"@types/lodash.capitalize": "^4.2.9",
|
||||
"@types/lodash.clamp": "^4.0.9",
|
||||
"@types/lodash.clonedeep": "^4.5.9",
|
||||
"@types/lodash.get": "^4.4.9",
|
||||
"@types/lodash.isequal": "^4.5.8",
|
||||
"@types/lodash.throttle": "^4.1.9",
|
||||
"@types/mocha": "^10.0.6",
|
||||
"@types/randomcolor": "^0.5.9",
|
||||
"@types/react": "^18.2.67",
|
||||
"@types/react-aria-menubutton": "^6.2.14",
|
||||
"@types/react-aria-modal": "^4.0.10",
|
||||
"@types/react-autocomplete": "^1.8.10",
|
||||
"@types/react-collapse": "^5.0.4",
|
||||
"@types/react-color": "^3.0.12",
|
||||
"@types/react-dom": "^18.2.22",
|
||||
"@types/react-file-reader-input": "^2.0.4",
|
||||
"@types/react-icon-base": "^2.1.6",
|
||||
"@types/string-hash": "^1.1.3",
|
||||
"@types/uuid": "^9.0.8",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"cors": "^2.8.5",
|
||||
"cypress": "^13.13.0",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-plugin-react": "^7.34.1",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.6",
|
||||
"i18next-parser": "^9.0.1",
|
||||
"istanbul": "^0.4.5",
|
||||
"istanbul-lib-coverage": "^2.0.1",
|
||||
"mkdirp": "^0.5.1",
|
||||
"mocha": "^6.1.4",
|
||||
"node-sass": "^4.12.0",
|
||||
"raw-loader": "^0.5.1",
|
||||
"react-hot-loader": "^4.3.11",
|
||||
"sass-loader": "^7.1.0",
|
||||
"selenium-standalone": "^6.16.0",
|
||||
"style-loader": "^0.23.0",
|
||||
"stylelint": "^10.0.0",
|
||||
"stylelint-config-recommended-scss": "^3.2.0",
|
||||
"stylelint-scss": "^3.5.4",
|
||||
"transform-loader": "^0.2.4",
|
||||
"uuid": "^3.3.2",
|
||||
"webdriverio": "^5.10.4",
|
||||
"webpack": "^4.20.2",
|
||||
"webpack-bundle-analyzer": "^3.0.2",
|
||||
"webpack-cleanup-plugin": "^0.5.1",
|
||||
"webpack-cli": "^3.1.2",
|
||||
"webpack-dev-server": "^3.1.9"
|
||||
"istanbul-lib-coverage": "^3.2.2",
|
||||
"mocha": "^10.3.0",
|
||||
"postcss": "^8.4.38",
|
||||
"react-hot-loader": "^4.13.1",
|
||||
"stylelint": "^16.2.1",
|
||||
"stylelint-config-recommended-scss": "^14.0.0",
|
||||
"stylelint-scss": "^6.2.1",
|
||||
"typescript": "^5.4.3",
|
||||
"uuid": "^9.0.1",
|
||||
"vite": "^5.2.6",
|
||||
"vite-plugin-istanbul": "^6.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,724 +0,0 @@
|
||||
import autoBind from 'react-autobind';
|
||||
import React from 'react'
|
||||
import cloneDeep from 'lodash.clonedeep'
|
||||
import clamp from 'lodash.clamp'
|
||||
import get from 'lodash.get'
|
||||
import {arrayMove} from 'react-sortable-hoc'
|
||||
import url from 'url'
|
||||
|
||||
import MapboxGlMap from './map/MapboxGlMap'
|
||||
import OpenLayersMap from './map/OpenLayersMap'
|
||||
import LayerList from './layers/LayerList'
|
||||
import LayerEditor from './layers/LayerEditor'
|
||||
import Toolbar from './Toolbar'
|
||||
import AppLayout from './AppLayout'
|
||||
import MessagePanel from './MessagePanel'
|
||||
|
||||
import SettingsModal from './modals/SettingsModal'
|
||||
import ExportModal from './modals/ExportModal'
|
||||
import SourcesModal from './modals/SourcesModal'
|
||||
import OpenModal from './modals/OpenModal'
|
||||
import ShortcutsModal from './modals/ShortcutsModal'
|
||||
import SurveyModal from './modals/SurveyModal'
|
||||
import DebugModal from './modals/DebugModal'
|
||||
|
||||
import { downloadGlyphsMetadata, downloadSpriteMetadata } from '../libs/metadata'
|
||||
import {latest, validate} from '@mapbox/mapbox-gl-style-spec'
|
||||
import style from '../libs/style'
|
||||
import { initialStyleUrl, loadStyleUrl, removeStyleQuerystring } from '../libs/urlopen'
|
||||
import { undoMessages, redoMessages } from '../libs/diffmessage'
|
||||
import { StyleStore } from '../libs/stylestore'
|
||||
import { ApiStyleStore } from '../libs/apistore'
|
||||
import { RevisionStore } from '../libs/revisions'
|
||||
import LayerWatcher from '../libs/layerwatcher'
|
||||
import tokens from '../config/tokens.json'
|
||||
import isEqual from 'lodash.isequal'
|
||||
import Debug from '../libs/debug'
|
||||
import queryUtil from '../libs/query-util'
|
||||
|
||||
import MapboxGl from 'mapbox-gl'
|
||||
|
||||
|
||||
// Similar functionality as <https://github.com/mapbox/mapbox-gl-js/blob/7e30aadf5177486c2cfa14fe1790c60e217b5e56/src/util/mapbox.js>
|
||||
function normalizeSourceURL (url, apiToken="") {
|
||||
const matches = url.match(/^mapbox:\/\/(.*)/);
|
||||
if (matches) {
|
||||
// mapbox://mapbox.mapbox-streets-v7
|
||||
return `https://api.mapbox.com/v4/${matches[1]}.json?secure&access_token=${apiToken}`
|
||||
}
|
||||
else {
|
||||
return url;
|
||||
}
|
||||
}
|
||||
|
||||
function setFetchAccessToken(url, mapStyle) {
|
||||
const matchesTilehosting = url.match(/\.tilehosting\.com/);
|
||||
const matchesMaptiler = url.match(/\.maptiler\.com/);
|
||||
const matchesThunderforest = url.match(/\.thunderforest\.com/);
|
||||
if (matchesTilehosting || matchesMaptiler) {
|
||||
const accessToken = style.getAccessToken("openmaptiles", mapStyle, {allowFallback: true})
|
||||
if (accessToken) {
|
||||
return url.replace('{key}', accessToken)
|
||||
}
|
||||
}
|
||||
else if (matchesThunderforest) {
|
||||
const accessToken = style.getAccessToken("thunderforest", mapStyle, {allowFallback: true})
|
||||
if (accessToken) {
|
||||
return url.replace('{key}', accessToken)
|
||||
}
|
||||
}
|
||||
else {
|
||||
return url;
|
||||
}
|
||||
}
|
||||
|
||||
function updateRootSpec(spec, fieldName, newValues) {
|
||||
return {
|
||||
...spec,
|
||||
$root: {
|
||||
...spec.$root,
|
||||
[fieldName]: {
|
||||
...spec.$root[fieldName],
|
||||
values: newValues
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default class App extends React.Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
autoBind(this);
|
||||
|
||||
this.revisionStore = new RevisionStore()
|
||||
const params = new URLSearchParams(window.location.search.substring(1))
|
||||
let port = params.get("localport")
|
||||
if (port == null && (window.location.port != 80 && window.location.port != 443)) {
|
||||
port = window.location.port
|
||||
}
|
||||
this.styleStore = new ApiStyleStore({
|
||||
onLocalStyleChange: mapStyle => this.onStyleChanged(mapStyle, false),
|
||||
port: port,
|
||||
host: params.get("localhost")
|
||||
})
|
||||
|
||||
|
||||
const shortcuts = [
|
||||
{
|
||||
key: "?",
|
||||
handler: () => {
|
||||
this.toggleModal("shortcuts");
|
||||
}
|
||||
},
|
||||
{
|
||||
key: "o",
|
||||
handler: () => {
|
||||
this.toggleModal("open");
|
||||
}
|
||||
},
|
||||
{
|
||||
key: "e",
|
||||
handler: () => {
|
||||
this.toggleModal("export");
|
||||
}
|
||||
},
|
||||
{
|
||||
key: "d",
|
||||
handler: () => {
|
||||
this.toggleModal("sources");
|
||||
}
|
||||
},
|
||||
{
|
||||
key: "s",
|
||||
handler: () => {
|
||||
this.toggleModal("settings");
|
||||
}
|
||||
},
|
||||
{
|
||||
key: "i",
|
||||
handler: () => {
|
||||
this.setMapState(
|
||||
this.state.mapState === "map" ? "inspect" : "map"
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
key: "m",
|
||||
handler: () => {
|
||||
document.querySelector(".mapboxgl-canvas").focus();
|
||||
}
|
||||
},
|
||||
{
|
||||
key: "!",
|
||||
handler: () => {
|
||||
this.toggleModal("debug");
|
||||
}
|
||||
},
|
||||
]
|
||||
|
||||
document.body.addEventListener("keyup", (e) => {
|
||||
if(e.key === "Escape") {
|
||||
e.target.blur();
|
||||
document.body.focus();
|
||||
}
|
||||
else if(this.state.isOpen.shortcuts || document.activeElement === document.body) {
|
||||
const shortcut = shortcuts.find((shortcut) => {
|
||||
return (shortcut.key === e.key)
|
||||
})
|
||||
|
||||
if(shortcut) {
|
||||
this.setModal("shortcuts", false);
|
||||
shortcut.handler(e);
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const styleUrl = initialStyleUrl()
|
||||
if(styleUrl && window.confirm("Load style from URL: " + styleUrl + " and discard current changes?")) {
|
||||
this.styleStore = new StyleStore()
|
||||
loadStyleUrl(styleUrl, mapStyle => this.onStyleChanged(mapStyle))
|
||||
removeStyleQuerystring()
|
||||
} else {
|
||||
if(styleUrl) {
|
||||
removeStyleQuerystring()
|
||||
}
|
||||
this.styleStore.init(err => {
|
||||
if(err) {
|
||||
console.log('Falling back to local storage for storing styles')
|
||||
this.styleStore = new StyleStore()
|
||||
}
|
||||
this.styleStore.latestStyle(mapStyle => this.onStyleChanged(mapStyle))
|
||||
|
||||
if(Debug.enabled()) {
|
||||
Debug.set("maputnik", "styleStore", this.styleStore);
|
||||
Debug.set("maputnik", "revisionStore", this.revisionStore);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if(Debug.enabled()) {
|
||||
Debug.set("maputnik", "revisionStore", this.revisionStore);
|
||||
Debug.set("maputnik", "styleStore", this.styleStore);
|
||||
}
|
||||
|
||||
const queryObj = url.parse(window.location.href, true).query;
|
||||
|
||||
this.state = {
|
||||
errors: [],
|
||||
infos: [],
|
||||
mapStyle: style.emptyStyle,
|
||||
selectedLayerIndex: 0,
|
||||
sources: {},
|
||||
vectorLayers: {},
|
||||
mapState: "map",
|
||||
spec: latest,
|
||||
isOpen: {
|
||||
settings: false,
|
||||
sources: false,
|
||||
open: false,
|
||||
shortcuts: false,
|
||||
export: false,
|
||||
survey: localStorage.hasOwnProperty('survey') ? false : true,
|
||||
debug: false,
|
||||
},
|
||||
mapboxGlDebugOptions: {
|
||||
showTileBoundaries: false,
|
||||
showCollisionBoxes: false,
|
||||
showOverdrawInspector: false,
|
||||
},
|
||||
openlayersDebugOptions: {
|
||||
debugToolbox: false,
|
||||
},
|
||||
}
|
||||
|
||||
this.layerWatcher = new LayerWatcher({
|
||||
onVectorLayersChange: v => this.setState({ vectorLayers: v })
|
||||
})
|
||||
}
|
||||
|
||||
handleKeyPress = (e) => {
|
||||
if(navigator.platform.toUpperCase().indexOf('MAC') >= 0) {
|
||||
if(e.metaKey && e.shiftKey && e.keyCode === 90) {
|
||||
e.preventDefault();
|
||||
this.onRedo(e);
|
||||
}
|
||||
else if(e.metaKey && e.keyCode === 90) {
|
||||
e.preventDefault();
|
||||
this.onUndo(e);
|
||||
}
|
||||
}
|
||||
else {
|
||||
if(e.ctrlKey && e.keyCode === 90) {
|
||||
e.preventDefault();
|
||||
this.onUndo(e);
|
||||
}
|
||||
else if(e.ctrlKey && e.keyCode === 89) {
|
||||
e.preventDefault();
|
||||
this.onRedo(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
window.addEventListener("keydown", this.handleKeyPress);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener("keydown", this.handleKeyPress);
|
||||
}
|
||||
|
||||
saveStyle(snapshotStyle) {
|
||||
this.styleStore.save(snapshotStyle)
|
||||
}
|
||||
|
||||
updateFonts(urlTemplate) {
|
||||
const metadata = this.state.mapStyle.metadata || {}
|
||||
const accessToken = metadata['maputnik:openmaptiles_access_token'] || tokens.openmaptiles
|
||||
|
||||
let glyphUrl = (typeof urlTemplate === 'string')? urlTemplate.replace('{key}', accessToken): urlTemplate;
|
||||
downloadGlyphsMetadata(glyphUrl, fonts => {
|
||||
this.setState({ spec: updateRootSpec(this.state.spec, 'glyphs', fonts)})
|
||||
})
|
||||
}
|
||||
|
||||
updateIcons(baseUrl) {
|
||||
downloadSpriteMetadata(baseUrl, icons => {
|
||||
this.setState({ spec: updateRootSpec(this.state.spec, 'sprite', icons)})
|
||||
})
|
||||
}
|
||||
|
||||
onChangeMetadataProperty = (property, value) => {
|
||||
// If we're changing renderer reset the map state.
|
||||
if (
|
||||
property === 'maputnik:renderer' &&
|
||||
value !== get(this.state.mapStyle, ['metadata', 'maputnik:renderer'], 'mbgljs')
|
||||
) {
|
||||
this.setState({
|
||||
mapState: 'map'
|
||||
});
|
||||
}
|
||||
|
||||
const changedStyle = {
|
||||
...this.state.mapStyle,
|
||||
metadata: {
|
||||
...this.state.mapStyle.metadata,
|
||||
[property]: value
|
||||
}
|
||||
}
|
||||
this.onStyleChanged(changedStyle)
|
||||
}
|
||||
|
||||
onStyleChanged = (newStyle, save=true) => {
|
||||
|
||||
const errors = validate(newStyle, latest)
|
||||
if(errors.length === 0) {
|
||||
|
||||
if(newStyle.glyphs !== this.state.mapStyle.glyphs) {
|
||||
this.updateFonts(newStyle.glyphs)
|
||||
}
|
||||
if(newStyle.sprite !== this.state.mapStyle.sprite) {
|
||||
this.updateIcons(newStyle.sprite)
|
||||
}
|
||||
|
||||
this.revisionStore.addRevision(newStyle)
|
||||
if(save) this.saveStyle(newStyle)
|
||||
this.setState({
|
||||
mapStyle: newStyle,
|
||||
errors: [],
|
||||
})
|
||||
} else {
|
||||
this.setState({
|
||||
errors: errors.map(err => err.message)
|
||||
})
|
||||
}
|
||||
|
||||
this.fetchSources();
|
||||
}
|
||||
|
||||
onUndo = () => {
|
||||
const activeStyle = this.revisionStore.undo()
|
||||
const messages = undoMessages(this.state.mapStyle, activeStyle)
|
||||
this.saveStyle(activeStyle)
|
||||
this.setState({
|
||||
mapStyle: activeStyle,
|
||||
infos: messages,
|
||||
})
|
||||
}
|
||||
|
||||
onRedo = () => {
|
||||
const activeStyle = this.revisionStore.redo()
|
||||
const messages = redoMessages(this.state.mapStyle, activeStyle)
|
||||
this.saveStyle(activeStyle)
|
||||
this.setState({
|
||||
mapStyle: activeStyle,
|
||||
infos: messages,
|
||||
})
|
||||
}
|
||||
|
||||
onMoveLayer = (move) => {
|
||||
let { oldIndex, newIndex } = move;
|
||||
let layers = this.state.mapStyle.layers;
|
||||
oldIndex = clamp(oldIndex, 0, layers.length-1);
|
||||
newIndex = clamp(newIndex, 0, layers.length-1);
|
||||
if(oldIndex === newIndex) return;
|
||||
|
||||
if (oldIndex === this.state.selectedLayerIndex) {
|
||||
this.setState({
|
||||
selectedLayerIndex: newIndex
|
||||
});
|
||||
}
|
||||
|
||||
layers = layers.slice(0);
|
||||
layers = arrayMove(layers, oldIndex, newIndex);
|
||||
this.onLayersChange(layers);
|
||||
}
|
||||
|
||||
onLayersChange = (changedLayers) => {
|
||||
const changedStyle = {
|
||||
...this.state.mapStyle,
|
||||
layers: changedLayers
|
||||
}
|
||||
this.onStyleChanged(changedStyle)
|
||||
}
|
||||
|
||||
onLayerDestroy = (layerId) => {
|
||||
let layers = this.state.mapStyle.layers;
|
||||
const remainingLayers = layers.slice(0);
|
||||
const idx = style.indexOfLayer(remainingLayers, layerId)
|
||||
remainingLayers.splice(idx, 1);
|
||||
this.onLayersChange(remainingLayers);
|
||||
}
|
||||
|
||||
onLayerCopy = (layerId) => {
|
||||
let layers = this.state.mapStyle.layers;
|
||||
const changedLayers = layers.slice(0)
|
||||
const idx = style.indexOfLayer(changedLayers, layerId)
|
||||
|
||||
const clonedLayer = cloneDeep(changedLayers[idx])
|
||||
clonedLayer.id = clonedLayer.id + "-copy"
|
||||
changedLayers.splice(idx, 0, clonedLayer)
|
||||
this.onLayersChange(changedLayers)
|
||||
}
|
||||
|
||||
onLayerVisibilityToggle = (layerId) => {
|
||||
let layers = this.state.mapStyle.layers;
|
||||
const changedLayers = layers.slice(0)
|
||||
const idx = style.indexOfLayer(changedLayers, layerId)
|
||||
|
||||
const layer = { ...changedLayers[idx] }
|
||||
const changedLayout = 'layout' in layer ? {...layer.layout} : {}
|
||||
changedLayout.visibility = changedLayout.visibility === 'none' ? 'visible' : 'none'
|
||||
|
||||
layer.layout = changedLayout
|
||||
changedLayers[idx] = layer
|
||||
this.onLayersChange(changedLayers)
|
||||
}
|
||||
|
||||
|
||||
onLayerIdChange = (oldId, newId) => {
|
||||
const changedLayers = this.state.mapStyle.layers.slice(0)
|
||||
const idx = style.indexOfLayer(changedLayers, oldId)
|
||||
|
||||
changedLayers[idx] = {
|
||||
...changedLayers[idx],
|
||||
id: newId
|
||||
}
|
||||
|
||||
this.onLayersChange(changedLayers)
|
||||
}
|
||||
|
||||
onLayerChanged = (layer) => {
|
||||
const changedLayers = this.state.mapStyle.layers.slice(0)
|
||||
const idx = style.indexOfLayer(changedLayers, layer.id)
|
||||
changedLayers[idx] = layer
|
||||
|
||||
this.onLayersChange(changedLayers)
|
||||
}
|
||||
|
||||
setMapState = (newState) => {
|
||||
this.setState({
|
||||
mapState: newState
|
||||
})
|
||||
}
|
||||
|
||||
setDefaultValues = (styleObj) => {
|
||||
const metadata = styleObj.metadata || {}
|
||||
if(metadata['maputnik:renderer'] === undefined) {
|
||||
const changedStyle = {
|
||||
...styleObj,
|
||||
metadata: {
|
||||
...styleObj.metadata,
|
||||
'maputnik:renderer': 'mbgljs'
|
||||
}
|
||||
}
|
||||
return changedStyle
|
||||
} else {
|
||||
return styleObj
|
||||
}
|
||||
}
|
||||
|
||||
openStyle = (styleObj) => {
|
||||
styleObj = this.setDefaultValues(styleObj)
|
||||
this.onStyleChanged(styleObj)
|
||||
}
|
||||
|
||||
fetchSources() {
|
||||
const sourceList = {...this.state.sources};
|
||||
|
||||
for(let [key, val] of Object.entries(this.state.mapStyle.sources)) {
|
||||
if(sourceList.hasOwnProperty(key)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
sourceList[key] = {
|
||||
type: val.type,
|
||||
layers: []
|
||||
};
|
||||
|
||||
if(!this.state.sources.hasOwnProperty(key) && val.type === "vector" && val.hasOwnProperty("url")) {
|
||||
let url = val.url;
|
||||
try {
|
||||
url = normalizeSourceURL(url, MapboxGl.accessToken);
|
||||
} catch(err) {
|
||||
console.warn("Failed to normalizeSourceURL: ", err);
|
||||
}
|
||||
|
||||
try {
|
||||
url = setFetchAccessToken(url, this.state.mapStyle)
|
||||
} catch(err) {
|
||||
console.warn("Failed to setFetchAccessToken: ", err);
|
||||
}
|
||||
|
||||
fetch(url, {
|
||||
mode: 'cors',
|
||||
})
|
||||
.then((response) => {
|
||||
return response.json();
|
||||
})
|
||||
.then((json) => {
|
||||
if(!json.hasOwnProperty("vector_layers")) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create new objects before setState
|
||||
const sources = Object.assign({}, this.state.sources);
|
||||
|
||||
for(let layer of json.vector_layers) {
|
||||
sources[key].layers.push(layer.id)
|
||||
}
|
||||
|
||||
console.debug("Updating source: "+key);
|
||||
this.setState({
|
||||
sources: sources
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("Failed to process sources for '%s'", url, err);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if(!isEqual(this.state.sources, sourceList)) {
|
||||
console.debug("Setting sources");
|
||||
this.setState({
|
||||
sources: sourceList
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
_getRenderer () {
|
||||
const metadata = this.state.mapStyle.metadata || {};
|
||||
return metadata['maputnik:renderer'] || 'mbgljs';
|
||||
}
|
||||
|
||||
mapRenderer() {
|
||||
const metadata = this.state.mapStyle.metadata || {};
|
||||
|
||||
const mapProps = {
|
||||
mapStyle: style.replaceAccessTokens(this.state.mapStyle, {allowFallback: true}),
|
||||
onDataChange: (e) => {
|
||||
this.layerWatcher.analyzeMap(e.map)
|
||||
this.fetchSources();
|
||||
},
|
||||
}
|
||||
|
||||
const renderer = this._getRenderer();
|
||||
|
||||
let mapElement;
|
||||
|
||||
// Check if OL code has been loaded?
|
||||
if(renderer === 'ol') {
|
||||
mapElement = <OpenLayersMap
|
||||
{...mapProps}
|
||||
debugToolbox={this.state.openlayersDebugOptions.debugToolbox}
|
||||
onLayerSelect={this.onLayerSelect}
|
||||
/>
|
||||
} else {
|
||||
mapElement = <MapboxGlMap {...mapProps}
|
||||
options={this.state.mapboxGlDebugOptions}
|
||||
inspectModeEnabled={this.state.mapState === "inspect"}
|
||||
highlightedLayer={this.state.mapStyle.layers[this.state.selectedLayerIndex]}
|
||||
onLayerSelect={this.onLayerSelect} />
|
||||
}
|
||||
|
||||
let filterName;
|
||||
if(this.state.mapState.match(/^filter-/)) {
|
||||
filterName = this.state.mapState.replace(/^filter-/, "");
|
||||
}
|
||||
const elementStyle = {};
|
||||
if (filterName) {
|
||||
elementStyle.filter = `url('#${filterName}')`;
|
||||
};
|
||||
|
||||
return <div style={elementStyle} className="maputnik-map__container">
|
||||
{mapElement}
|
||||
</div>
|
||||
}
|
||||
|
||||
onLayerSelect = (layerId) => {
|
||||
const idx = style.indexOfLayer(this.state.mapStyle.layers, layerId)
|
||||
this.setState({ selectedLayerIndex: idx })
|
||||
}
|
||||
|
||||
setModal(modalName, value) {
|
||||
if(modalName === 'survey' && value === false) {
|
||||
localStorage.setItem('survey', '');
|
||||
}
|
||||
|
||||
this.setState({
|
||||
isOpen: {
|
||||
...this.state.isOpen,
|
||||
[modalName]: value
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
toggleModal(modalName) {
|
||||
this.setModal(modalName, !this.state.isOpen[modalName]);
|
||||
}
|
||||
|
||||
onChangeOpenlayersDebug = (key, value) => {
|
||||
this.setState({
|
||||
openlayersDebugOptions: {
|
||||
...this.state.openlayersDebugOptions,
|
||||
[key]: value,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onChangeMaboxGlDebug = (key, value) => {
|
||||
this.setState({
|
||||
mapboxGlDebugOptions: {
|
||||
...this.state.mapboxGlDebugOptions,
|
||||
[key]: value,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const layers = this.state.mapStyle.layers || []
|
||||
const selectedLayer = layers.length > 0 ? layers[this.state.selectedLayerIndex] : null
|
||||
const metadata = this.state.mapStyle.metadata || {}
|
||||
|
||||
const toolbar = <Toolbar
|
||||
renderer={this._getRenderer()}
|
||||
mapState={this.state.mapState}
|
||||
mapStyle={this.state.mapStyle}
|
||||
inspectModeEnabled={this.state.mapState === "inspect"}
|
||||
sources={this.state.sources}
|
||||
onStyleChanged={this.onStyleChanged}
|
||||
onStyleOpen={this.onStyleChanged}
|
||||
onSetMapState={this.setMapState}
|
||||
onToggleModal={this.toggleModal.bind(this)}
|
||||
/>
|
||||
|
||||
const layerList = <LayerList
|
||||
onMoveLayer={this.onMoveLayer}
|
||||
onLayerDestroy={this.onLayerDestroy}
|
||||
onLayerCopy={this.onLayerCopy}
|
||||
onLayerVisibilityToggle={this.onLayerVisibilityToggle}
|
||||
onLayersChange={this.onLayersChange}
|
||||
onLayerSelect={this.onLayerSelect}
|
||||
selectedLayerIndex={this.state.selectedLayerIndex}
|
||||
layers={layers}
|
||||
sources={this.state.sources}
|
||||
/>
|
||||
|
||||
const layerEditor = selectedLayer ? <LayerEditor
|
||||
layer={selectedLayer}
|
||||
layerIndex={this.state.selectedLayerIndex}
|
||||
isFirstLayer={this.state.selectedLayerIndex < 1}
|
||||
isLastLayer={this.state.selectedLayerIndex === this.state.mapStyle.layers.length-1}
|
||||
sources={this.state.sources}
|
||||
vectorLayers={this.state.vectorLayers}
|
||||
spec={this.state.spec}
|
||||
onMoveLayer={this.onMoveLayer}
|
||||
onLayerChanged={this.onLayerChanged}
|
||||
onLayerDestroy={this.onLayerDestroy}
|
||||
onLayerCopy={this.onLayerCopy}
|
||||
onLayerVisibilityToggle={this.onLayerVisibilityToggle}
|
||||
onLayerIdChange={this.onLayerIdChange}
|
||||
/> : null
|
||||
|
||||
const bottomPanel = (this.state.errors.length + this.state.infos.length) > 0 ? <MessagePanel
|
||||
errors={this.state.errors}
|
||||
infos={this.state.infos}
|
||||
/> : null
|
||||
|
||||
|
||||
const modals = <div>
|
||||
<DebugModal
|
||||
renderer={this._getRenderer()}
|
||||
mapboxGlDebugOptions={this.state.mapboxGlDebugOptions}
|
||||
openlayersDebugOptions={this.state.openlayersDebugOptions}
|
||||
onChangeMaboxGlDebug={this.onChangeMaboxGlDebug}
|
||||
onChangeOpenlayersDebug={this.onChangeOpenlayersDebug}
|
||||
isOpen={this.state.isOpen.debug}
|
||||
onOpenToggle={this.toggleModal.bind(this, 'debug')}
|
||||
/>
|
||||
<ShortcutsModal
|
||||
ref={(el) => this.shortcutEl = el}
|
||||
isOpen={this.state.isOpen.shortcuts}
|
||||
onOpenToggle={this.toggleModal.bind(this, 'shortcuts')}
|
||||
/>
|
||||
<SettingsModal
|
||||
mapStyle={this.state.mapStyle}
|
||||
onStyleChanged={this.onStyleChanged}
|
||||
onChangeMetadataProperty={this.onChangeMetadataProperty}
|
||||
isOpen={this.state.isOpen.settings}
|
||||
onOpenToggle={this.toggleModal.bind(this, 'settings')}
|
||||
openlayersDebugOptions={this.state.openlayersDebugOptions}
|
||||
/>
|
||||
<ExportModal
|
||||
mapStyle={this.state.mapStyle}
|
||||
onStyleChanged={this.onStyleChanged}
|
||||
isOpen={this.state.isOpen.export}
|
||||
onOpenToggle={this.toggleModal.bind(this, 'export')}
|
||||
/>
|
||||
<OpenModal
|
||||
isOpen={this.state.isOpen.open}
|
||||
onStyleOpen={this.openStyle}
|
||||
onOpenToggle={this.toggleModal.bind(this, 'open')}
|
||||
/>
|
||||
<SourcesModal
|
||||
mapStyle={this.state.mapStyle}
|
||||
onStyleChanged={this.onStyleChanged}
|
||||
isOpen={this.state.isOpen.sources}
|
||||
onOpenToggle={this.toggleModal.bind(this, 'sources')}
|
||||
/>
|
||||
<SurveyModal
|
||||
isOpen={this.state.isOpen.survey}
|
||||
onOpenToggle={this.toggleModal.bind(this, 'survey')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
return <AppLayout
|
||||
toolbar={toolbar}
|
||||
layerList={layerList}
|
||||
layerEditor={layerEditor}
|
||||
map={this.mapRenderer()}
|
||||
bottom={bottomPanel}
|
||||
modals={modals}
|
||||
/>
|
||||
}
|
||||
}
|
||||
975
src/components/App.tsx
Normal file
975
src/components/App.tsx
Normal file
@@ -0,0 +1,975 @@
|
||||
// @ts-ignore - this can be easily replaced with arrow functions
|
||||
import autoBind from 'react-autobind';
|
||||
import React from 'react'
|
||||
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 hash from "string-hash";
|
||||
import {Map, LayerSpecification, StyleSpecification, ValidationError, SourceSpecification} from 'maplibre-gl'
|
||||
import {latest, validateStyleMin} from '@maplibre/maplibre-gl-style-spec'
|
||||
|
||||
import MapMaplibreGl from './MapMaplibreGl'
|
||||
import MapOpenLayers from './MapOpenLayers'
|
||||
import LayerList from './LayerList'
|
||||
import LayerEditor from './LayerEditor'
|
||||
import AppToolbar, { MapState } from './AppToolbar'
|
||||
import AppLayout from './AppLayout'
|
||||
import MessagePanel from './AppMessagePanel'
|
||||
|
||||
import ModalSettings from './ModalSettings'
|
||||
import ModalExport from './ModalExport'
|
||||
import ModalSources from './ModalSources'
|
||||
import ModalOpen from './ModalOpen'
|
||||
import ModalShortcuts from './ModalShortcuts'
|
||||
import ModalDebug from './ModalDebug'
|
||||
|
||||
import {downloadGlyphsMetadata, downloadSpriteMetadata} from '../libs/metadata'
|
||||
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 { SortEnd } from 'react-sortable-hoc';
|
||||
import { MapOptions } from 'maplibre-gl';
|
||||
|
||||
// Buffer must be defined globally for @maplibre/maplibre-gl-style-spec validate() function to succeed.
|
||||
window.Buffer = buffer.Buffer;
|
||||
|
||||
function setFetchAccessToken(url: string, mapStyle: StyleSpecification) {
|
||||
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: any, fieldName: string, newValues: any) {
|
||||
return {
|
||||
...spec,
|
||||
$root: {
|
||||
...spec.$root,
|
||||
[fieldName]: {
|
||||
...spec.$root[fieldName],
|
||||
values: newValues
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type OnStyleChangedOpts = {
|
||||
save?: boolean
|
||||
addRevision?: boolean
|
||||
initialLoad?: boolean
|
||||
}
|
||||
|
||||
type MappedErrors = {
|
||||
message: string
|
||||
parsed?: {
|
||||
type: string
|
||||
data: {
|
||||
index: number
|
||||
key: string
|
||||
message: string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type AppState = {
|
||||
errors: MappedErrors[],
|
||||
infos: string[],
|
||||
mapStyle: StyleSpecification & {id: string},
|
||||
dirtyMapStyle?: StyleSpecification,
|
||||
selectedLayerIndex: number,
|
||||
selectedLayerOriginalId?: string,
|
||||
sources: {[key: string]: SourceSpecification},
|
||||
vectorLayers: {},
|
||||
spec: any,
|
||||
mapView: {
|
||||
zoom: number,
|
||||
center: {
|
||||
lng: number,
|
||||
lat: number,
|
||||
},
|
||||
},
|
||||
maplibreGlDebugOptions: Partial<MapOptions> & {
|
||||
showTileBoundaries: boolean,
|
||||
showCollisionBoxes: boolean,
|
||||
showOverdrawInspector: boolean,
|
||||
},
|
||||
openlayersDebugOptions: {
|
||||
debugToolbox: boolean,
|
||||
},
|
||||
mapState: MapState
|
||||
isOpen: {
|
||||
settings: boolean
|
||||
sources: boolean
|
||||
open: boolean
|
||||
shortcuts: boolean
|
||||
export: boolean
|
||||
debug: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export default class App extends React.Component<any, AppState> {
|
||||
revisionStore: RevisionStore;
|
||||
styleStore: StyleStore | ApiStyleStore;
|
||||
layerWatcher: LayerWatcher;
|
||||
|
||||
constructor(props: any) {
|
||||
super(props)
|
||||
autoBind(this);
|
||||
|
||||
this.revisionStore = new RevisionStore()
|
||||
const params = new URLSearchParams(window.location.search.substring(1))
|
||||
let port = params.get("localport")
|
||||
if (port == null && (window.location.port !== "80" && window.location.port !== "443")) {
|
||||
port = window.location.port
|
||||
}
|
||||
this.styleStore = new ApiStyleStore({
|
||||
onLocalStyleChange: mapStyle => this.onStyleChanged(mapStyle, {save: false}),
|
||||
port: port,
|
||||
host: params.get("localhost")
|
||||
})
|
||||
|
||||
|
||||
const shortcuts = [
|
||||
{
|
||||
key: "?",
|
||||
handler: () => {
|
||||
this.toggleModal("shortcuts");
|
||||
}
|
||||
},
|
||||
{
|
||||
key: "o",
|
||||
handler: () => {
|
||||
this.toggleModal("open");
|
||||
}
|
||||
},
|
||||
{
|
||||
key: "e",
|
||||
handler: () => {
|
||||
this.toggleModal("export");
|
||||
}
|
||||
},
|
||||
{
|
||||
key: "d",
|
||||
handler: () => {
|
||||
this.toggleModal("sources");
|
||||
}
|
||||
},
|
||||
{
|
||||
key: "s",
|
||||
handler: () => {
|
||||
this.toggleModal("settings");
|
||||
}
|
||||
},
|
||||
{
|
||||
key: "i",
|
||||
handler: () => {
|
||||
this.setMapState(
|
||||
this.state.mapState === "map" ? "inspect" : "map"
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
key: "m",
|
||||
handler: () => {
|
||||
(document.querySelector(".maplibregl-canvas") as HTMLCanvasElement).focus();
|
||||
}
|
||||
},
|
||||
{
|
||||
key: "!",
|
||||
handler: () => {
|
||||
this.toggleModal("debug");
|
||||
}
|
||||
},
|
||||
]
|
||||
|
||||
document.body.addEventListener("keyup", (e) => {
|
||||
if(e.key === "Escape") {
|
||||
(e.target as HTMLElement).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();
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
this.state = {
|
||||
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
|
||||
debug: false,
|
||||
},
|
||||
maplibreGlDebugOptions: {
|
||||
showTileBoundaries: false,
|
||||
showCollisionBoxes: false,
|
||||
showOverdrawInspector: false,
|
||||
},
|
||||
openlayersDebugOptions: {
|
||||
debugToolbox: false,
|
||||
},
|
||||
}
|
||||
|
||||
this.layerWatcher = new LayerWatcher({
|
||||
onVectorLayersChange: v => this.setState({ vectorLayers: v })
|
||||
})
|
||||
}
|
||||
|
||||
handleKeyPress = (e: KeyboardEvent) => {
|
||||
if(navigator.platform.toUpperCase().indexOf('MAC') >= 0) {
|
||||
if(e.metaKey && e.shiftKey && e.keyCode === 90) {
|
||||
e.preventDefault();
|
||||
this.onRedo();
|
||||
}
|
||||
else if(e.metaKey && e.keyCode === 90) {
|
||||
e.preventDefault();
|
||||
this.onUndo();
|
||||
}
|
||||
}
|
||||
else {
|
||||
if(e.ctrlKey && e.keyCode === 90) {
|
||||
e.preventDefault();
|
||||
this.onUndo();
|
||||
}
|
||||
else if(e.ctrlKey && e.keyCode === 89) {
|
||||
e.preventDefault();
|
||||
this.onRedo();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
window.addEventListener("keydown", this.handleKeyPress);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener("keydown", this.handleKeyPress);
|
||||
}
|
||||
|
||||
saveStyle(snapshotStyle: StyleSpecification & {id: string}) {
|
||||
this.styleStore.save(snapshotStyle)
|
||||
}
|
||||
|
||||
updateFonts(urlTemplate: string) {
|
||||
const metadata: {[key: string]: string} = this.state.mapStyle.metadata || {} as any
|
||||
const accessToken = metadata['maputnik:openmaptiles_access_token'] || tokens.openmaptiles
|
||||
|
||||
const glyphUrl = (typeof urlTemplate === 'string')? urlTemplate.replace('{key}', accessToken): urlTemplate;
|
||||
downloadGlyphsMetadata(glyphUrl, fonts => {
|
||||
this.setState({ spec: updateRootSpec(this.state.spec, 'glyphs', fonts)})
|
||||
})
|
||||
}
|
||||
|
||||
updateIcons(baseUrl: string) {
|
||||
downloadSpriteMetadata(baseUrl, icons => {
|
||||
this.setState({ spec: updateRootSpec(this.state.spec, 'sprite', icons)})
|
||||
})
|
||||
}
|
||||
|
||||
onChangeMetadataProperty = (property: string, value: any) => {
|
||||
// If we're changing renderer reset the map state.
|
||||
if (
|
||||
property === 'maputnik:renderer' &&
|
||||
value !== get(this.state.mapStyle, ['metadata', 'maputnik:renderer'], 'mlgljs')
|
||||
) {
|
||||
this.setState({
|
||||
mapState: 'map'
|
||||
});
|
||||
}
|
||||
|
||||
const changedStyle = {
|
||||
...this.state.mapStyle,
|
||||
metadata: {
|
||||
...(this.state.mapStyle as any).metadata,
|
||||
[property]: value
|
||||
}
|
||||
}
|
||||
this.onStyleChanged(changedStyle)
|
||||
}
|
||||
|
||||
onStyleChanged = (newStyle: StyleSpecification & {id: string}, opts: OnStyleChangedOpts={}) => {
|
||||
opts = {
|
||||
save: true,
|
||||
addRevision: true,
|
||||
initialLoad: false,
|
||||
...opts,
|
||||
};
|
||||
|
||||
if (opts.initialLoad) {
|
||||
this.getInitialStateFromUrl(newStyle);
|
||||
}
|
||||
|
||||
const errors: ValidationError[] = validateStyleMin(newStyle) || [];
|
||||
|
||||
// The validate function doesn't give us errors for duplicate error with
|
||||
// empty string for layer.id, manually deal with that here.
|
||||
const layerErrors: (Error | ValidationError)[] = [];
|
||||
if (newStyle && newStyle.layers) {
|
||||
const foundLayers = new global.Map();
|
||||
newStyle.layers.forEach((layer, index) => {
|
||||
if (layer.id === "" && foundLayers.has(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 [, 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 [, 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 [, 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: StyleSpecification | undefined = 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 as string)
|
||||
}
|
||||
if(newStyle.sprite !== this.state.mapStyle.sprite) {
|
||||
this.updateIcons(newStyle.sprite as string)
|
||||
}
|
||||
|
||||
if (opts.addRevision) {
|
||||
this.revisionStore.addRevision(newStyle);
|
||||
}
|
||||
if (opts.save) {
|
||||
this.saveStyle(newStyle as StyleSpecification & {id: string});
|
||||
}
|
||||
|
||||
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: SortEnd) => {
|
||||
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: LayerSpecification[]) => {
|
||||
const changedStyle = {
|
||||
...this.state.mapStyle,
|
||||
layers: changedLayers
|
||||
}
|
||||
this.onStyleChanged(changedStyle)
|
||||
}
|
||||
|
||||
onLayerDestroy = (index: number) => {
|
||||
const layers = this.state.mapStyle.layers;
|
||||
const remainingLayers = layers.slice(0);
|
||||
remainingLayers.splice(index, 1);
|
||||
this.onLayersChange(remainingLayers);
|
||||
}
|
||||
|
||||
onLayerCopy = (index: number) => {
|
||||
const 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: number) => {
|
||||
const 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: number, _oldId: string, newId: string) => {
|
||||
const changedLayers = this.state.mapStyle.layers.slice(0)
|
||||
changedLayers[index] = {
|
||||
...changedLayers[index],
|
||||
id: newId
|
||||
}
|
||||
|
||||
this.onLayersChange(changedLayers)
|
||||
}
|
||||
|
||||
onLayerChanged = (index: number, layer: LayerSpecification) => {
|
||||
const changedLayers = this.state.mapStyle.layers.slice(0)
|
||||
changedLayers[index] = layer
|
||||
|
||||
this.onLayersChange(changedLayers)
|
||||
}
|
||||
|
||||
setMapState = (newState: MapState) => {
|
||||
this.setState({
|
||||
mapState: newState
|
||||
}, this.setStateInUrl);
|
||||
}
|
||||
|
||||
setDefaultValues = (styleObj: StyleSpecification & {id: string}) => {
|
||||
const metadata: {[key: string]: string} = styleObj.metadata || {} as any
|
||||
if(metadata['maputnik:renderer'] === undefined) {
|
||||
const changedStyle = {
|
||||
...styleObj,
|
||||
metadata: {
|
||||
...styleObj.metadata as any,
|
||||
'maputnik:renderer': 'mlgljs'
|
||||
}
|
||||
}
|
||||
return changedStyle
|
||||
} else {
|
||||
return styleObj
|
||||
}
|
||||
}
|
||||
|
||||
openStyle = (styleObj: StyleSpecification & {id: string}) => {
|
||||
styleObj = this.setDefaultValues(styleObj)
|
||||
this.onStyleChanged(styleObj)
|
||||
}
|
||||
|
||||
fetchSources() {
|
||||
const sourceList: {[key: string]: any} = {};
|
||||
|
||||
for(const [key, val] of Object.entries(this.state.mapStyle.sources)) {
|
||||
if(
|
||||
!Object.prototype.hasOwnProperty.call(this.state.sources, key) &&
|
||||
val.type === "vector" &&
|
||||
Object.prototype.hasOwnProperty.call(val, "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(!Object.prototype.hasOwnProperty.call(json, "vector_layers")) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create new objects before setState
|
||||
const sources = Object.assign({}, {
|
||||
[key]: this.state.sources[key],
|
||||
});
|
||||
|
||||
for(const layer of json.vector_layers) {
|
||||
(sources[key] as any).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: {[key:string]: string} = this.state.mapStyle.metadata || {} as any;
|
||||
return metadata['maputnik:renderer'] || 'mlgljs';
|
||||
}
|
||||
|
||||
onMapChange = (mapView: {
|
||||
zoom: number,
|
||||
center: {
|
||||
lng: number,
|
||||
lat: number,
|
||||
},
|
||||
}) => {
|
||||
this.setState({
|
||||
mapView,
|
||||
});
|
||||
}
|
||||
|
||||
mapRenderer() {
|
||||
const {mapStyle, dirtyMapStyle} = this.state;
|
||||
|
||||
const mapProps = {
|
||||
mapStyle: (dirtyMapStyle || mapStyle),
|
||||
replaceAccessTokens: (mapStyle: StyleSpecification) => {
|
||||
return style.replaceAccessTokens(mapStyle, {
|
||||
allowFallback: true
|
||||
});
|
||||
},
|
||||
onDataChange: (e: {map: Map}) => {
|
||||
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 = <MapMaplibreGl {...mapProps}
|
||||
onChange={this.onMapChange}
|
||||
options={this.state.maplibreGlDebugOptions}
|
||||
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: {filter?: string} = {};
|
||||
if (filterName) {
|
||||
elementStyle.filter = `url('#${filterName}')`;
|
||||
}
|
||||
|
||||
return <div style={elementStyle} className="maputnik-map__container" data-wd-key="maplibre: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: StyleSpecification) => {
|
||||
const url = new URL(location.href);
|
||||
const modalParam = url.searchParams.get("modal");
|
||||
if (modalParam && modalParam !== "") {
|
||||
const modals = modalParam.split(",");
|
||||
const modalObj: {[key: string]: boolean} = {};
|
||||
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 as MapState);
|
||||
}
|
||||
|
||||
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: number) => {
|
||||
this.setState({
|
||||
selectedLayerIndex: index,
|
||||
selectedLayerOriginalId: this.state.mapStyle.layers[index].id,
|
||||
}, this.setStateInUrl);
|
||||
}
|
||||
|
||||
setModal(modalName: keyof AppState["isOpen"], value: boolean) {
|
||||
this.setState({
|
||||
isOpen: {
|
||||
...this.state.isOpen,
|
||||
[modalName]: value
|
||||
}
|
||||
}, this.setStateInUrl)
|
||||
}
|
||||
|
||||
toggleModal(modalName: keyof AppState["isOpen"]) {
|
||||
this.setModal(modalName, !this.state.isOpen[modalName]);
|
||||
}
|
||||
|
||||
onChangeOpenlayersDebug = (key: keyof AppState["openlayersDebugOptions"], value: boolean) => {
|
||||
this.setState({
|
||||
openlayersDebugOptions: {
|
||||
...this.state.openlayersDebugOptions,
|
||||
[key]: value,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onChangeMaplibreGlDebug = (key: keyof AppState["maplibreGlDebugOptions"], value: any) => {
|
||||
this.setState({
|
||||
maplibreGlDebugOptions: {
|
||||
...this.state.maplibreGlDebugOptions,
|
||||
[key]: value,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const layers = this.state.mapStyle.layers || []
|
||||
const selectedLayer = layers.length > 0 ? layers[this.state.selectedLayerIndex] : undefined
|
||||
|
||||
const toolbar = <AppToolbar
|
||||
renderer={this._getRenderer()}
|
||||
mapState={this.state.mapState}
|
||||
mapStyle={this.state.mapStyle}
|
||||
inspectModeEnabled={this.state.mapState === "inspect"}
|
||||
sources={this.state.sources}
|
||||
onStyleChanged={this.onStyleChanged}
|
||||
onStyleOpen={this.onStyleChanged}
|
||||
onSetMapState={this.setMapState}
|
||||
onToggleModal={this.toggleModal.bind(this)}
|
||||
/>
|
||||
|
||||
const layerList = <LayerList
|
||||
onMoveLayer={this.onMoveLayer}
|
||||
onLayerDestroy={this.onLayerDestroy}
|
||||
onLayerCopy={this.onLayerCopy}
|
||||
onLayerVisibilityToggle={this.onLayerVisibilityToggle}
|
||||
onLayersChange={this.onLayersChange}
|
||||
onLayerSelect={this.onLayerSelect}
|
||||
selectedLayerIndex={this.state.selectedLayerIndex}
|
||||
layers={layers}
|
||||
sources={this.state.sources}
|
||||
errors={this.state.errors}
|
||||
/>
|
||||
|
||||
const layerEditor = selectedLayer ? <LayerEditor
|
||||
key={this.state.selectedLayerOriginalId}
|
||||
layer={selectedLayer}
|
||||
layerIndex={this.state.selectedLayerIndex}
|
||||
isFirstLayer={this.state.selectedLayerIndex < 1}
|
||||
isLastLayer={this.state.selectedLayerIndex === this.state.mapStyle.layers.length-1}
|
||||
sources={this.state.sources}
|
||||
vectorLayers={this.state.vectorLayers}
|
||||
spec={this.state.spec}
|
||||
onMoveLayer={this.onMoveLayer}
|
||||
onLayerChanged={this.onLayerChanged}
|
||||
onLayerDestroy={this.onLayerDestroy}
|
||||
onLayerCopy={this.onLayerCopy}
|
||||
onLayerVisibilityToggle={this.onLayerVisibilityToggle}
|
||||
onLayerIdChange={this.onLayerIdChange}
|
||||
errors={this.state.errors}
|
||||
/> : undefined
|
||||
|
||||
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}
|
||||
/> : undefined
|
||||
|
||||
|
||||
const modals = <div>
|
||||
<ModalDebug
|
||||
renderer={this._getRenderer()}
|
||||
maplibreGlDebugOptions={this.state.maplibreGlDebugOptions}
|
||||
openlayersDebugOptions={this.state.openlayersDebugOptions}
|
||||
onChangeMaplibreGlDebug={this.onChangeMaplibreGlDebug}
|
||||
onChangeOpenlayersDebug={this.onChangeOpenlayersDebug}
|
||||
isOpen={this.state.isOpen.debug}
|
||||
onOpenToggle={this.toggleModal.bind(this, 'debug')}
|
||||
mapView={this.state.mapView}
|
||||
/>
|
||||
<ModalShortcuts
|
||||
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')}
|
||||
/>
|
||||
<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')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
return <AppLayout
|
||||
toolbar={toolbar}
|
||||
layerList={layerList}
|
||||
layerEditor={layerEditor}
|
||||
map={this.mapRenderer()}
|
||||
bottom={bottomPanel}
|
||||
modals={modals}
|
||||
/>
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import ScrollContainer from './ScrollContainer'
|
||||
|
||||
class AppLayout extends React.Component {
|
||||
static propTypes = {
|
||||
toolbar: PropTypes.element.isRequired,
|
||||
layerList: PropTypes.element.isRequired,
|
||||
layerEditor: PropTypes.element,
|
||||
map: PropTypes.element.isRequired,
|
||||
bottom: PropTypes.element,
|
||||
modals: PropTypes.node,
|
||||
}
|
||||
|
||||
static childContextTypes = {
|
||||
reactIconBase: PropTypes.object
|
||||
}
|
||||
|
||||
getChildContext() {
|
||||
return {
|
||||
reactIconBase: { size: 14 }
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div className="maputnik-layout">
|
||||
{this.props.toolbar}
|
||||
<div className="maputnik-layout-list">
|
||||
<ScrollContainer>
|
||||
{this.props.layerList}
|
||||
</ScrollContainer>
|
||||
</div>
|
||||
<div className="maputnik-layout-drawer">
|
||||
<ScrollContainer>
|
||||
{this.props.layerEditor}
|
||||
</ScrollContainer>
|
||||
</div>
|
||||
{this.props.map}
|
||||
{this.props.bottom && <div className="maputnik-layout-bottom">
|
||||
{this.props.bottom}
|
||||
</div>
|
||||
}
|
||||
{this.props.modals}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
export default AppLayout
|
||||
52
src/components/AppLayout.tsx
Normal file
52
src/components/AppLayout.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import ScrollContainer from './ScrollContainer'
|
||||
import { WithTranslation, withTranslation } from 'react-i18next';
|
||||
|
||||
type AppLayoutInternalProps = {
|
||||
toolbar: React.ReactElement
|
||||
layerList: React.ReactElement
|
||||
layerEditor?: React.ReactElement
|
||||
map: React.ReactElement
|
||||
bottom?: React.ReactElement
|
||||
modals?: React.ReactNode
|
||||
} & WithTranslation;
|
||||
|
||||
class AppLayoutInternal extends React.Component<AppLayoutInternalProps> {
|
||||
static childContextTypes = {
|
||||
reactIconBase: PropTypes.object
|
||||
}
|
||||
|
||||
getChildContext() {
|
||||
return {
|
||||
reactIconBase: { size: 14 }
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
document.body.dir = this.props.i18n.dir();
|
||||
|
||||
return <div className="maputnik-layout">
|
||||
{this.props.toolbar}
|
||||
<div className="maputnik-layout-main">
|
||||
<div className="maputnik-layout-list">
|
||||
{this.props.layerList}
|
||||
</div>
|
||||
<div className="maputnik-layout-drawer">
|
||||
<ScrollContainer>
|
||||
{this.props.layerEditor}
|
||||
</ScrollContainer>
|
||||
</div>
|
||||
{this.props.map}
|
||||
</div>
|
||||
{this.props.bottom && <div className="maputnik-layout-bottom">
|
||||
{this.props.bottom}
|
||||
</div>
|
||||
}
|
||||
{this.props.modals}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
const AppLayout = withTranslation()(AppLayoutInternal);
|
||||
export default AppLayout;
|
||||
66
src/components/AppMessagePanel.tsx
Normal file
66
src/components/AppMessagePanel.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
import React from 'react'
|
||||
import {formatLayerId} from '../libs/format';
|
||||
import {LayerSpecification, StyleSpecification} from 'maplibre-gl';
|
||||
import { Trans, WithTranslation, withTranslation } from 'react-i18next';
|
||||
|
||||
type AppMessagePanelInternalProps = {
|
||||
errors?: unknown[]
|
||||
infos?: string[]
|
||||
mapStyle?: StyleSpecification
|
||||
onLayerSelect?(...args: unknown[]): unknown
|
||||
currentLayer?: LayerSpecification
|
||||
selectedLayerIndex?: number
|
||||
} & WithTranslation;
|
||||
|
||||
class AppMessagePanelInternal extends React.Component<AppMessagePanelInternalProps> {
|
||||
static defaultProps = {
|
||||
onLayerSelect: () => {},
|
||||
}
|
||||
|
||||
render() {
|
||||
const {t, selectedLayerIndex} = this.props;
|
||||
const errors = this.props.errors?.map((error: any, idx) => {
|
||||
let content;
|
||||
if (error.parsed && error.parsed.type === "layer") {
|
||||
const {parsed} = error;
|
||||
const layerId = this.props.mapStyle?.layers[parsed.data.index].id;
|
||||
content = (
|
||||
<>
|
||||
<Trans t={t}>
|
||||
Layer <span>{formatLayerId(layerId)}</span>: {parsed.data.message}
|
||||
</Trans>
|
||||
{selectedLayerIndex !== parsed.data.index &&
|
||||
<>
|
||||
—
|
||||
<button
|
||||
className="maputnik-message-panel__switch-button"
|
||||
onClick={() => this.props.onLayerSelect!(parsed.data.index)}
|
||||
>
|
||||
{t("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>
|
||||
}
|
||||
}
|
||||
|
||||
const AppMessagePanel = withTranslation()(AppMessagePanelInternal);
|
||||
export default AppMessagePanel;
|
||||
291
src/components/AppToolbar.tsx
Normal file
291
src/components/AppToolbar.tsx
Normal file
@@ -0,0 +1,291 @@
|
||||
import React from 'react'
|
||||
import classnames from 'classnames'
|
||||
import {detect} from 'detect-browser';
|
||||
|
||||
import {MdFileDownload, MdOpenInBrowser, MdSettings, MdLayers, MdHelpOutline, MdFindInPage, MdLanguage} from 'react-icons/md'
|
||||
import pkgJson from '../../package.json'
|
||||
//@ts-ignore
|
||||
import maputnikLogo from 'maputnik-design/logos/logo-color.svg?inline'
|
||||
import { withTranslation, WithTranslation } from 'react-i18next';
|
||||
import { supportedLanguages } from '../i18n';
|
||||
|
||||
// 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;
|
||||
|
||||
|
||||
type IconTextProps = {
|
||||
children?: React.ReactNode
|
||||
};
|
||||
|
||||
|
||||
class IconText extends React.Component<IconTextProps> {
|
||||
render() {
|
||||
return <span className="maputnik-icon-text">{this.props.children}</span>
|
||||
}
|
||||
}
|
||||
|
||||
type ToolbarLinkProps = {
|
||||
className?: string
|
||||
children?: React.ReactNode
|
||||
href?: string
|
||||
onToggleModal?(...args: unknown[]): unknown
|
||||
};
|
||||
|
||||
class ToolbarLink extends React.Component<ToolbarLinkProps> {
|
||||
render() {
|
||||
return <a
|
||||
className={classnames('maputnik-toolbar-link', this.props.className)}
|
||||
href={this.props.href}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
data-wd-key="toolbar:link"
|
||||
>
|
||||
{this.props.children}
|
||||
</a>
|
||||
}
|
||||
}
|
||||
|
||||
type ToolbarSelectProps = {
|
||||
children?: React.ReactNode
|
||||
wdKey?: string
|
||||
};
|
||||
|
||||
class ToolbarSelect extends React.Component<ToolbarSelectProps> {
|
||||
render() {
|
||||
return <div
|
||||
className='maputnik-toolbar-select'
|
||||
data-wd-key={this.props.wdKey}
|
||||
>
|
||||
{this.props.children}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
type ToolbarActionProps = {
|
||||
children?: React.ReactNode
|
||||
onClick?(...args: unknown[]): unknown
|
||||
wdKey?: string
|
||||
};
|
||||
|
||||
class ToolbarAction extends React.Component<ToolbarActionProps> {
|
||||
render() {
|
||||
return <button
|
||||
className='maputnik-toolbar-action'
|
||||
data-wd-key={this.props.wdKey}
|
||||
onClick={this.props.onClick}
|
||||
>
|
||||
{this.props.children}
|
||||
</button>
|
||||
}
|
||||
}
|
||||
|
||||
export type MapState = "map" | "inspect" | "filter-achromatopsia" | "filter-deuteranopia" | "filter-protanopia" | "filter-tritanopia";
|
||||
|
||||
type AppToolbarInternalProps = {
|
||||
mapStyle: object
|
||||
inspectModeEnabled: boolean
|
||||
onStyleChanged(...args: unknown[]): unknown
|
||||
// A new style has been uploaded
|
||||
onStyleOpen(...args: unknown[]): unknown
|
||||
// A dict of source id's and the available source layers
|
||||
sources: object
|
||||
children?: React.ReactNode
|
||||
onToggleModal(...args: unknown[]): unknown
|
||||
onSetMapState(mapState: MapState): unknown
|
||||
mapState?: MapState
|
||||
renderer?: string
|
||||
} & WithTranslation;
|
||||
|
||||
class AppToolbarInternal extends React.Component<AppToolbarInternalProps> {
|
||||
state = {
|
||||
isOpen: {
|
||||
settings: false,
|
||||
sources: false,
|
||||
open: false,
|
||||
add: false,
|
||||
export: false,
|
||||
}
|
||||
}
|
||||
|
||||
handleSelection(val: MapState) {
|
||||
this.props.onSetMapState(val);
|
||||
}
|
||||
|
||||
handleLanguageChange(val: string) {
|
||||
this.props.i18n.changeLanguage(val);
|
||||
}
|
||||
|
||||
onSkip = (target: string) => {
|
||||
if (target === "map") {
|
||||
(document.querySelector(".maplibregl-canvas") as HTMLCanvasElement).focus();
|
||||
}
|
||||
else {
|
||||
const el = document.querySelector("#skip-target-"+target) as HTMLButtonElement;
|
||||
el.focus();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
const views = [
|
||||
{
|
||||
id: "map",
|
||||
group: "general",
|
||||
title: t("Map"),
|
||||
},
|
||||
{
|
||||
id: "inspect",
|
||||
group: "general",
|
||||
title: t("Inspect"),
|
||||
disabled: this.props.renderer === 'ol',
|
||||
},
|
||||
{
|
||||
id: "filter-deuteranopia",
|
||||
group: "color-accessibility",
|
||||
title: t("Deuteranopia filter"),
|
||||
disabled: !colorAccessibilityFiltersEnabled,
|
||||
},
|
||||
{
|
||||
id: "filter-protanopia",
|
||||
group: "color-accessibility",
|
||||
title: t("Protanopia filter"),
|
||||
disabled: !colorAccessibilityFiltersEnabled,
|
||||
},
|
||||
{
|
||||
id: "filter-tritanopia",
|
||||
group: "color-accessibility",
|
||||
title: t("Tritanopia filter"),
|
||||
disabled: !colorAccessibilityFiltersEnabled,
|
||||
},
|
||||
{
|
||||
id: "filter-achromatopsia",
|
||||
group: "color-accessibility",
|
||||
title: t("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")}
|
||||
>
|
||||
{t("Layers list")}
|
||||
</button>
|
||||
<button
|
||||
data-wd-key="root:skip:layer-editor"
|
||||
className="maputnik-toolbar-skip"
|
||||
onClick={_e => this.onSkip("layer-editor")}
|
||||
>
|
||||
{t("Layer editor")}
|
||||
</button>
|
||||
<button
|
||||
data-wd-key="root:skip:map-view"
|
||||
className="maputnik-toolbar-skip"
|
||||
onClick={_e => this.onSkip("map")}
|
||||
>
|
||||
{t("Map view")}
|
||||
</button>
|
||||
<a
|
||||
className="maputnik-toolbar-logo"
|
||||
target="blank"
|
||||
rel="noreferrer noopener"
|
||||
href="https://github.com/maplibre/maputnik"
|
||||
>
|
||||
<img src={maputnikLogo} alt={t("Maputnik on GitHub")} />
|
||||
<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>{t("Open")}</IconText>
|
||||
</ToolbarAction>
|
||||
<ToolbarAction wdKey="nav:export" onClick={this.props.onToggleModal.bind(this, 'export')}>
|
||||
<MdFileDownload />
|
||||
<IconText>{t("Export")}</IconText>
|
||||
</ToolbarAction>
|
||||
<ToolbarAction wdKey="nav:sources" onClick={this.props.onToggleModal.bind(this, 'sources')}>
|
||||
<MdLayers />
|
||||
<IconText>{t("Data Sources")}</IconText>
|
||||
</ToolbarAction>
|
||||
<ToolbarAction wdKey="nav:settings" onClick={this.props.onToggleModal.bind(this, 'settings')}>
|
||||
<MdSettings />
|
||||
<IconText>{t("Style Settings")}</IconText>
|
||||
</ToolbarAction>
|
||||
|
||||
<ToolbarSelect wdKey="nav:inspect">
|
||||
<MdFindInPage />
|
||||
<label>{t("View")}
|
||||
<select
|
||||
className="maputnik-select"
|
||||
data-wd-key="maputnik-select"
|
||||
onChange={(e) => this.handleSelection(e.target.value as MapState)}
|
||||
value={currentView?.id}
|
||||
>
|
||||
{views.filter(v => v.group === "general").map((item) => {
|
||||
return (
|
||||
<option key={item.id} value={item.id} disabled={item.disabled} data-wd-key={item.id}>
|
||||
{item.title}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
<optgroup label={t("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>
|
||||
|
||||
<ToolbarSelect wdKey="nav:language">
|
||||
<MdLanguage />
|
||||
<label>{t("Language")}
|
||||
<select
|
||||
className="maputnik-select"
|
||||
data-wd-key="maputnik-lang-select"
|
||||
onChange={(e) => this.handleLanguageChange(e.target.value)}
|
||||
value={this.props.i18n.language}
|
||||
>
|
||||
{Object.entries(supportedLanguages).map(([code, name]) => {
|
||||
return (
|
||||
<option key={code} value={code}>
|
||||
{name}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
</label>
|
||||
</ToolbarSelect>
|
||||
|
||||
<ToolbarLink href={"https://github.com/maplibre/maputnik/wiki"}>
|
||||
<MdHelpOutline />
|
||||
<IconText>{t("Help")}</IconText>
|
||||
</ToolbarLink>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
}
|
||||
}
|
||||
|
||||
const AppToolbar = withTranslation()(AppToolbarInternal);
|
||||
export default AppToolbar;
|
||||
103
src/components/Block.tsx
Normal file
103
src/components/Block.tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
import React, {PropsWithChildren, SyntheticEvent} from 'react'
|
||||
import classnames from 'classnames'
|
||||
import FieldDocLabel from './FieldDocLabel'
|
||||
import Doc from './Doc'
|
||||
|
||||
|
||||
type BlockProps = PropsWithChildren & {
|
||||
"data-wd-key"?: string
|
||||
label?: string
|
||||
action?: React.ReactElement
|
||||
style?: object
|
||||
onChange?(...args: unknown[]): unknown
|
||||
fieldSpec?: object
|
||||
wideMode?: boolean
|
||||
error?: {message: string}
|
||||
};
|
||||
|
||||
type BlockState = {
|
||||
showDoc: boolean
|
||||
};
|
||||
|
||||
/** Wrap a component with a label */
|
||||
export default class Block extends React.Component<BlockProps, BlockState> {
|
||||
_blockEl: HTMLDivElement | null = null;
|
||||
|
||||
constructor (props: BlockProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
showDoc: false,
|
||||
}
|
||||
}
|
||||
|
||||
onChange(e: React.BaseSyntheticEvent<Event, HTMLInputElement, HTMLInputElement>) {
|
||||
const value = e.target.value
|
||||
if (this.props.onChange) {
|
||||
return this.props.onChange(value === "" ? undefined : value)
|
||||
}
|
||||
}
|
||||
|
||||
onToggleDoc = (val: boolean) => {
|
||||
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: SyntheticEvent<any, any>) => {
|
||||
const el = event.nativeEvent.target;
|
||||
const contains = this._blockEl?.contains(el);
|
||||
|
||||
if (event.nativeEvent.target.nodeName !== "INPUT" && !contains) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
render() {
|
||||
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 PropTypes from 'prop-types'
|
||||
import classnames from 'classnames'
|
||||
|
||||
class Button extends React.Component {
|
||||
static propTypes = {
|
||||
"data-wd-key": PropTypes.string,
|
||||
"aria-label": PropTypes.string,
|
||||
onClick: PropTypes.func,
|
||||
style: PropTypes.object,
|
||||
className: PropTypes.string,
|
||||
children: PropTypes.node,
|
||||
disabled: PropTypes.bool,
|
||||
}
|
||||
|
||||
render() {
|
||||
return <button
|
||||
onClick={this.props.onClick}
|
||||
disabled={this.props.disabled}
|
||||
aria-label={this.props["aria-label"]}
|
||||
className={classnames("maputnik-button", this.props.className)}
|
||||
data-wd-key={this.props["data-wd-key"]}
|
||||
style={this.props.style}>
|
||||
{this.props.children}
|
||||
</button>
|
||||
}
|
||||
}
|
||||
|
||||
export default Button
|
||||
34
src/components/Collapse.tsx
Normal file
34
src/components/Collapse.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import React from 'react'
|
||||
import { Collapse as ReactCollapse } from 'react-collapse'
|
||||
import {reducedMotionEnabled} from '../libs/accessibility'
|
||||
|
||||
|
||||
type CollapseProps = {
|
||||
isActive: boolean
|
||||
children: React.ReactElement
|
||||
};
|
||||
|
||||
|
||||
export default class Collapse extends React.Component<CollapseProps> {
|
||||
static defaultProps = {
|
||||
isActive: true
|
||||
}
|
||||
|
||||
render() {
|
||||
if (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>
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
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,
|
||||
}
|
||||
type CollapserProps = {
|
||||
isCollapsed: boolean
|
||||
style?: object
|
||||
};
|
||||
|
||||
export default class Collapser extends React.Component<CollapserProps> {
|
||||
render() {
|
||||
const iconStyle = {
|
||||
width: 20,
|
||||
91
src/components/Doc.tsx
Normal file
91
src/components/Doc.tsx
Normal file
@@ -0,0 +1,91 @@
|
||||
import React from 'react'
|
||||
|
||||
const headers = {
|
||||
js: "JS",
|
||||
android: "Android",
|
||||
ios: "iOS",
|
||||
macos: "macOS",
|
||||
};
|
||||
|
||||
type DocProps = {
|
||||
fieldSpec: {
|
||||
doc?: string
|
||||
values?: {
|
||||
[key: string]: {
|
||||
doc?: string
|
||||
}
|
||||
}
|
||||
'sdk-support'?: {
|
||||
[key: string]: typeof headers
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default class Doc extends React.Component<DocProps> {
|
||||
render () {
|
||||
const {fieldSpec} = this.props;
|
||||
|
||||
const {doc, values} = fieldSpec;
|
||||
const sdkSupport = fieldSpec['sdk-support'];
|
||||
|
||||
const renderValues = (
|
||||
!!values &&
|
||||
// HACK: Currently we merge additional values into the style spec, so this is required
|
||||
// See <https://github.com/maplibre/maputnik/blob/main/src/components/PropertyGroup.jsx#L16>
|
||||
!Array.isArray(values)
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{doc &&
|
||||
<div className="SpecDoc">
|
||||
<div className="SpecDoc__doc" data-wd-key='spec-field-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) => {
|
||||
if (Object.prototype.hasOwnProperty.call(supportObj, k)) {
|
||||
return <td key={k}>{supportObj[k as keyof typeof headers]}</td>;
|
||||
}
|
||||
else {
|
||||
return <td key={k}>no</td>;
|
||||
}
|
||||
})}
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
19
src/components/FieldArray.tsx
Normal file
19
src/components/FieldArray.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import React from 'react'
|
||||
import InputArray, { FieldArrayProps as InputArrayProps } from './InputArray'
|
||||
import Fieldset from './Fieldset'
|
||||
|
||||
type FieldArrayProps = InputArrayProps & {
|
||||
name?: string
|
||||
fieldSpec?: {
|
||||
doc: string
|
||||
}
|
||||
};
|
||||
|
||||
export default class FieldArray extends React.Component<FieldArrayProps> {
|
||||
render() {
|
||||
return <Fieldset label={this.props.label} fieldSpec={this.props.fieldSpec}>
|
||||
<InputArray {...this.props} />
|
||||
</Fieldset>
|
||||
}
|
||||
}
|
||||
|
||||
18
src/components/FieldAutocomplete.tsx
Normal file
18
src/components/FieldAutocomplete.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import React from 'react'
|
||||
import Block from './Block'
|
||||
import InputAutocomplete, { InputAutocompleteProps } from './InputAutocomplete'
|
||||
|
||||
|
||||
type FieldAutocompleteProps = InputAutocompleteProps & {
|
||||
label?: string;
|
||||
};
|
||||
|
||||
|
||||
export default class FieldAutocomplete extends React.Component<FieldAutocompleteProps> {
|
||||
render() {
|
||||
return <Block label={this.props.label}>
|
||||
<InputAutocomplete {...this.props} />
|
||||
</Block>
|
||||
}
|
||||
}
|
||||
|
||||
18
src/components/FieldCheckbox.tsx
Normal file
18
src/components/FieldCheckbox.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import React from 'react'
|
||||
import Block from './Block'
|
||||
import InputCheckbox, {InputCheckboxProps} from './InputCheckbox'
|
||||
|
||||
|
||||
type FieldCheckboxProps = InputCheckboxProps & {
|
||||
label?: string;
|
||||
};
|
||||
|
||||
|
||||
export default class FieldCheckbox extends React.Component<FieldCheckboxProps> {
|
||||
render() {
|
||||
return <Block label={this.props.label}>
|
||||
<InputCheckbox {...this.props} />
|
||||
</Block>
|
||||
}
|
||||
}
|
||||
|
||||
21
src/components/FieldColor.tsx
Normal file
21
src/components/FieldColor.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import React from 'react'
|
||||
import Block from './Block'
|
||||
import InputColor, {InputColorProps} from './InputColor'
|
||||
|
||||
|
||||
type FieldColorProps = InputColorProps & {
|
||||
label?: string
|
||||
fieldSpec?: {
|
||||
doc: string
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
export default class FieldColor extends React.Component<FieldColorProps> {
|
||||
render() {
|
||||
return <Block label={this.props.label} fieldSpec={this.props.fieldSpec}>
|
||||
<InputColor {...this.props} />
|
||||
</Block>
|
||||
}
|
||||
}
|
||||
|
||||
38
src/components/FieldComment.tsx
Normal file
38
src/components/FieldComment.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import React from 'react'
|
||||
|
||||
import Block from './Block'
|
||||
import InputString from './InputString'
|
||||
import { WithTranslation, withTranslation } from 'react-i18next';
|
||||
|
||||
type FieldCommentInternalProps = {
|
||||
value?: string
|
||||
onChange(value: string | undefined): unknown
|
||||
error: {message: string}
|
||||
} & WithTranslation;
|
||||
|
||||
class FieldCommentInternal extends React.Component<FieldCommentInternalProps> {
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
const fieldSpec = {
|
||||
doc: t("Comments for the current layer. This is non-standard and not in the spec."),
|
||||
};
|
||||
|
||||
return <Block
|
||||
label={t("Comments")}
|
||||
fieldSpec={fieldSpec}
|
||||
data-wd-key="layer-comment"
|
||||
error={this.props.error}
|
||||
>
|
||||
<InputString
|
||||
multi={true}
|
||||
value={this.props.value}
|
||||
onChange={this.props.onChange}
|
||||
default={t("Comment...")}
|
||||
data-wd-key="layer-comment.input"
|
||||
/>
|
||||
</Block>
|
||||
}
|
||||
}
|
||||
|
||||
const FieldComment = withTranslation()(FieldCommentInternal);
|
||||
export default FieldComment;
|
||||
65
src/components/FieldDocLabel.tsx
Normal file
65
src/components/FieldDocLabel.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
import React from 'react'
|
||||
import {MdInfoOutline, MdHighlightOff} from 'react-icons/md'
|
||||
|
||||
type FieldDocLabelProps = {
|
||||
label: JSX.Element | string | undefined
|
||||
fieldSpec?: {
|
||||
doc?: string
|
||||
}
|
||||
onToggleDoc?(...args: unknown[]): unknown
|
||||
};
|
||||
|
||||
type FieldDocLabelState = {
|
||||
open: boolean
|
||||
};
|
||||
|
||||
export default class FieldDocLabel extends React.Component<FieldDocLabelProps, FieldDocLabelState> {
|
||||
constructor (props: FieldDocLabelProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
open: false,
|
||||
}
|
||||
}
|
||||
|
||||
onToggleDoc = (open: boolean) => {
|
||||
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)}
|
||||
data-wd-key={'field-doc-button-'+label}
|
||||
>
|
||||
{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 />
|
||||
}
|
||||
}
|
||||
}
|
||||
16
src/components/FieldDynamicArray.tsx
Normal file
16
src/components/FieldDynamicArray.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import React from 'react'
|
||||
import InputDynamicArray, {FieldDynamicArrayProps as InputDynamicArrayProps} from './InputDynamicArray'
|
||||
import Fieldset from './Fieldset'
|
||||
|
||||
type FieldDynamicArrayProps = InputDynamicArrayProps & {
|
||||
name?: string
|
||||
};
|
||||
|
||||
export default class FieldDynamicArray extends React.Component<FieldDynamicArrayProps> {
|
||||
render() {
|
||||
return <Fieldset label={this.props.label}>
|
||||
<InputDynamicArray {...this.props} />
|
||||
</Fieldset>
|
||||
}
|
||||
}
|
||||
|
||||
20
src/components/FieldEnum.tsx
Normal file
20
src/components/FieldEnum.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import React from 'react'
|
||||
import InputEnum, {InputEnumProps} from './InputEnum'
|
||||
import Fieldset from './Fieldset';
|
||||
|
||||
|
||||
type FieldEnumProps = InputEnumProps & {
|
||||
label?: string;
|
||||
fieldSpec?: {
|
||||
doc: string
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
export default class FieldEnum extends React.Component<FieldEnumProps> {
|
||||
render() {
|
||||
return <Fieldset label={this.props.label} fieldSpec={this.props.fieldSpec}>
|
||||
<InputEnum {...this.props} />
|
||||
</Fieldset>
|
||||
}
|
||||
}
|
||||
407
src/components/FieldFunction.tsx
Normal file
407
src/components/FieldFunction.tsx
Normal file
@@ -0,0 +1,407 @@
|
||||
import React from 'react'
|
||||
|
||||
import SpecProperty from './_SpecProperty'
|
||||
import DataProperty, { Stop } from './_DataProperty'
|
||||
import ZoomProperty from './_ZoomProperty'
|
||||
import ExpressionProperty from './_ExpressionProperty'
|
||||
import {function as styleFunction} from '@maplibre/maplibre-gl-style-spec';
|
||||
import {findDefaultFromSpec} from '../libs/spec-helper';
|
||||
|
||||
|
||||
function isLiteralExpression(value: any) {
|
||||
return (Array.isArray(value) && value.length === 2 && value[0] === "literal");
|
||||
}
|
||||
|
||||
function isGetExpression(value: any) {
|
||||
return (
|
||||
Array.isArray(value) &&
|
||||
value.length === 2 &&
|
||||
value[0] === "get"
|
||||
);
|
||||
}
|
||||
|
||||
function isZoomField(value: any) {
|
||||
return (
|
||||
typeof(value) === 'object' &&
|
||||
value.stops &&
|
||||
typeof(value.property) === 'undefined' &&
|
||||
Array.isArray(value.stops) &&
|
||||
value.stops.length > 1 &&
|
||||
value.stops.every((stop: Stop) => {
|
||||
return (
|
||||
Array.isArray(stop) &&
|
||||
stop.length === 2
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function isIdentityProperty(value: any) {
|
||||
return (
|
||||
typeof(value) === 'object' &&
|
||||
value.type === "identity" &&
|
||||
Object.prototype.hasOwnProperty.call(value, "property")
|
||||
);
|
||||
}
|
||||
|
||||
function isDataStopProperty(value: any) {
|
||||
return (
|
||||
typeof(value) === 'object' &&
|
||||
value.stops &&
|
||||
typeof(value.property) !== 'undefined' &&
|
||||
value.stops.length > 1 &&
|
||||
Array.isArray(value.stops) &&
|
||||
value.stops.every((stop: Stop) => {
|
||||
return (
|
||||
Array.isArray(stop) &&
|
||||
stop.length === 2 &&
|
||||
typeof(stop[0]) === 'object'
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function isDataField(value: any) {
|
||||
return (
|
||||
isIdentityProperty(value) ||
|
||||
isDataStopProperty(value)
|
||||
);
|
||||
}
|
||||
|
||||
function isPrimative(value: any): value is string | boolean | number {
|
||||
const valid = ["string", "boolean", "number"];
|
||||
return valid.includes(typeof(value));
|
||||
}
|
||||
|
||||
function isArrayOfPrimatives(values: any): values is Array<string | boolean | number> {
|
||||
if (Array.isArray(values)) {
|
||||
return values.every(isPrimative);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function getDataType(value: any, fieldSpec={} as any) {
|
||||
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";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
type FieldFunctionProps = {
|
||||
onChange(fieldName: string, value: any): unknown
|
||||
fieldName: string
|
||||
fieldType: string
|
||||
fieldSpec: any
|
||||
errors?: {[key: string]: {message: string}}
|
||||
value?: any
|
||||
};
|
||||
|
||||
type FieldFunctionState = {
|
||||
dataType: string
|
||||
isEditing: boolean
|
||||
}
|
||||
|
||||
/** 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<FieldFunctionProps, FieldFunctionState> {
|
||||
constructor (props: FieldFunctionProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
dataType: getDataType(props.value, props.fieldSpec),
|
||||
isEditing: false,
|
||||
}
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps(props: Readonly<FieldFunctionProps>, state: FieldFunctionState) {
|
||||
// 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: any) {
|
||||
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: number) => {
|
||||
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: 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: 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-container:"+this.props.fieldName}>
|
||||
{specField}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
28
src/components/FieldId.tsx
Normal file
28
src/components/FieldId.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import React from 'react'
|
||||
|
||||
import latest from '@maplibre/maplibre-gl-style-spec/dist/latest.json'
|
||||
import Block from './Block'
|
||||
import InputString from './InputString'
|
||||
|
||||
type FieldIdProps = {
|
||||
value: string
|
||||
wdKey: string
|
||||
onChange(value: string | undefined): unknown
|
||||
error?: {message: string}
|
||||
};
|
||||
|
||||
export default class FieldId extends React.Component<FieldIdProps> {
|
||||
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}
|
||||
data-wd-key={this.props.wdKey + ".input"}
|
||||
/>
|
||||
</Block>
|
||||
}
|
||||
}
|
||||
13
src/components/FieldJson.tsx
Normal file
13
src/components/FieldJson.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import React from 'react'
|
||||
import InputJson, {InputJsonProps} from './InputJson'
|
||||
|
||||
|
||||
type FieldJsonProps = InputJsonProps & {};
|
||||
|
||||
|
||||
export default class FieldJson extends React.Component<FieldJsonProps> {
|
||||
render() {
|
||||
return <InputJson {...this.props} />
|
||||
}
|
||||
}
|
||||
|
||||
35
src/components/FieldMaxZoom.tsx
Normal file
35
src/components/FieldMaxZoom.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import React from 'react'
|
||||
|
||||
import latest from '@maplibre/maplibre-gl-style-spec/dist/latest.json'
|
||||
import Block from './Block'
|
||||
import InputNumber from './InputNumber'
|
||||
import { WithTranslation, withTranslation } from 'react-i18next';
|
||||
|
||||
type FieldMaxZoomInternalProps = {
|
||||
value?: number
|
||||
onChange(value: number | undefined): unknown
|
||||
error?: {message: string}
|
||||
} & WithTranslation;
|
||||
|
||||
class FieldMaxZoomInternal extends React.Component<FieldMaxZoomInternalProps> {
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
return <Block label={t("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}
|
||||
data-wd-key="max-zoom.input"
|
||||
/>
|
||||
</Block>
|
||||
}
|
||||
}
|
||||
|
||||
const FieldMaxZoom = withTranslation()(FieldMaxZoomInternal);
|
||||
export default FieldMaxZoom;
|
||||
35
src/components/FieldMinZoom.tsx
Normal file
35
src/components/FieldMinZoom.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import React from 'react'
|
||||
|
||||
import latest from '@maplibre/maplibre-gl-style-spec/dist/latest.json'
|
||||
import Block from './Block'
|
||||
import InputNumber from './InputNumber'
|
||||
import { WithTranslation, withTranslation } from 'react-i18next';
|
||||
|
||||
type FieldMinZoomInternalProps = {
|
||||
value?: number
|
||||
onChange(...args: unknown[]): unknown
|
||||
error?: {message: string}
|
||||
} & WithTranslation;
|
||||
|
||||
class FieldMinZoomInternal extends React.Component<FieldMinZoomInternalProps> {
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
return <Block label={t("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}
|
||||
data-wd-key='min-zoom.input'
|
||||
/>
|
||||
</Block>
|
||||
}
|
||||
}
|
||||
|
||||
const FieldMinZoom = withTranslation()(FieldMinZoomInternal);
|
||||
export default FieldMinZoom;
|
||||
18
src/components/FieldMultiInput.tsx
Normal file
18
src/components/FieldMultiInput.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import React from 'react'
|
||||
import InputMultiInput, {InputMultiInputProps} from './InputMultiInput'
|
||||
import Fieldset from './Fieldset'
|
||||
|
||||
|
||||
type FieldMultiInputProps = InputMultiInputProps & {
|
||||
label?: string
|
||||
};
|
||||
|
||||
|
||||
export default class FieldMultiInput extends React.Component<FieldMultiInputProps> {
|
||||
render() {
|
||||
return <Fieldset label={this.props.label}>
|
||||
<InputMultiInput {...this.props} />
|
||||
</Fieldset>
|
||||
}
|
||||
}
|
||||
|
||||
20
src/components/FieldNumber.tsx
Normal file
20
src/components/FieldNumber.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import React from 'react'
|
||||
import InputNumber, {InputNumberProps} from './InputNumber'
|
||||
import Block from './Block'
|
||||
|
||||
|
||||
type FieldNumberProps = InputNumberProps & {
|
||||
label?: string
|
||||
fieldSpec?: {
|
||||
doc: string
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
export default class FieldNumber extends React.Component<FieldNumberProps> {
|
||||
render() {
|
||||
return <Block label={this.props.label} fieldSpec={this.props.fieldSpec}>
|
||||
<InputNumber {...this.props} />
|
||||
</Block>
|
||||
}
|
||||
}
|
||||
22
src/components/FieldSelect.tsx
Normal file
22
src/components/FieldSelect.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import React from 'react'
|
||||
import Block from './Block'
|
||||
import InputSelect, {InputSelectProps} from './InputSelect'
|
||||
|
||||
|
||||
type FieldSelectProps = InputSelectProps & {
|
||||
label?: string
|
||||
fieldSpec?: {
|
||||
doc: string
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
export default class FieldSelect extends React.Component<FieldSelectProps> {
|
||||
render() {
|
||||
return <Block label={this.props.label} fieldSpec={this.props.fieldSpec}>
|
||||
<InputSelect {...this.props}/>
|
||||
</Block>
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
40
src/components/FieldSource.tsx
Normal file
40
src/components/FieldSource.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import React from 'react'
|
||||
|
||||
import latest from '@maplibre/maplibre-gl-style-spec/dist/latest.json'
|
||||
import Block from './Block'
|
||||
import InputAutocomplete from './InputAutocomplete'
|
||||
import { WithTranslation, withTranslation } from 'react-i18next';
|
||||
|
||||
type FieldSourceInternalProps = {
|
||||
value?: string
|
||||
wdKey?: string
|
||||
onChange?(value: string| undefined): unknown
|
||||
sourceIds?: unknown[]
|
||||
error?: {message: string}
|
||||
} & WithTranslation;
|
||||
|
||||
class FieldSourceInternal extends React.Component<FieldSourceInternalProps> {
|
||||
static defaultProps = {
|
||||
onChange: () => {},
|
||||
sourceIds: [],
|
||||
}
|
||||
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
return <Block
|
||||
label={t("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>
|
||||
}
|
||||
}
|
||||
|
||||
const FieldSource = withTranslation()(FieldSourceInternal);
|
||||
export default FieldSource;
|
||||
42
src/components/FieldSourceLayer.tsx
Normal file
42
src/components/FieldSourceLayer.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import React from 'react'
|
||||
|
||||
import {latest} from '@maplibre/maplibre-gl-style-spec'
|
||||
import Block from './Block'
|
||||
import InputAutocomplete from './InputAutocomplete'
|
||||
import { WithTranslation, withTranslation } from 'react-i18next';
|
||||
|
||||
type FieldSourceLayerInternalProps = {
|
||||
value?: string
|
||||
onChange?(...args: unknown[]): unknown
|
||||
sourceLayerIds?: unknown[]
|
||||
isFixed?: boolean
|
||||
error?: {message: string}
|
||||
} & WithTranslation;
|
||||
|
||||
class FieldSourceLayerInternal extends React.Component<FieldSourceLayerInternalProps> {
|
||||
static defaultProps = {
|
||||
onChange: () => {},
|
||||
sourceLayerIds: [],
|
||||
isFixed: false
|
||||
}
|
||||
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
return <Block
|
||||
label={t("Source Layer")}
|
||||
fieldSpec={latest.layer['source-layer']}
|
||||
data-wd-key="layer-source-layer"
|
||||
error={this.props.error}
|
||||
>
|
||||
<InputAutocomplete
|
||||
keepMenuWithinWindowBounds={!!this.props.isFixed}
|
||||
value={this.props.value}
|
||||
onChange={this.props.onChange}
|
||||
options={this.props.sourceLayerIds?.map(l => [l, l])}
|
||||
/>
|
||||
</Block>
|
||||
}
|
||||
}
|
||||
|
||||
const FieldSourceLayer = withTranslation()(FieldSourceLayerInternal);
|
||||
export default FieldSourceLayer;
|
||||
19
src/components/FieldString.tsx
Normal file
19
src/components/FieldString.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import React from 'react'
|
||||
import Block from './Block'
|
||||
import InputString, {InputStringProps} from './InputString'
|
||||
|
||||
type FieldStringProps = InputStringProps & {
|
||||
name?: string
|
||||
label?: string
|
||||
fieldSpec?: {
|
||||
doc: string
|
||||
}
|
||||
};
|
||||
|
||||
export default class FieldString extends React.Component<FieldStringProps> {
|
||||
render() {
|
||||
return <Block label={this.props.label} fieldSpec={this.props.fieldSpec}>
|
||||
<InputString {...this.props} />
|
||||
</Block>
|
||||
}
|
||||
}
|
||||
57
src/components/FieldType.tsx
Normal file
57
src/components/FieldType.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import React from 'react'
|
||||
|
||||
import latest from '@maplibre/maplibre-gl-style-spec/dist/latest.json'
|
||||
import Block from './Block'
|
||||
import InputSelect from './InputSelect'
|
||||
import InputString from './InputString'
|
||||
import { WithTranslation, withTranslation } from 'react-i18next';
|
||||
|
||||
type FieldTypeInternalProps = {
|
||||
value: string
|
||||
wdKey?: string
|
||||
onChange(value: string): unknown
|
||||
error?: {message: string}
|
||||
disabled?: boolean
|
||||
} & WithTranslation;
|
||||
|
||||
class FieldTypeInternal extends React.Component<FieldTypeInternalProps> {
|
||||
static defaultProps = {
|
||||
disabled: false,
|
||||
}
|
||||
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
return <Block label={t("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}
|
||||
data-wd-key={this.props.wdKey + ".select"}
|
||||
/>
|
||||
}
|
||||
</Block>
|
||||
}
|
||||
}
|
||||
|
||||
const FieldType = withTranslation()(FieldTypeInternal);
|
||||
export default FieldType;
|
||||
23
src/components/FieldUrl.tsx
Normal file
23
src/components/FieldUrl.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import React from 'react'
|
||||
import InputUrl, {FieldUrlProps as InputUrlProps} from './InputUrl'
|
||||
import Block from './Block'
|
||||
|
||||
|
||||
type FieldUrlProps = InputUrlProps & {
|
||||
label: string;
|
||||
fieldSpec?: {
|
||||
doc: string
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
export default class FieldUrl extends React.Component<FieldUrlProps> {
|
||||
render () {
|
||||
return (
|
||||
<Block label={this.props.label} fieldSpec={this.props.fieldSpec}>
|
||||
<InputUrl {...this.props} />
|
||||
</Block>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user