mirror of
https://github.com/maputnik/editor.git
synced 2025-12-06 06:10:00 +00:00
Compare commits
1097 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 | ||
|
|
5f1e212759 | ||
|
|
2b7db498ef | ||
|
|
1ce2d59b9b | ||
|
|
e6464790f6 | ||
|
|
13ddf9f754 | ||
|
|
30edb881ed | ||
|
|
b30bbdc248 | ||
|
|
824616f6bd | ||
|
|
2a832955c4 | ||
|
|
608b836fe0 | ||
|
|
de9c4fcc4a | ||
|
|
109198a524 | ||
|
|
920e4fe630 | ||
|
|
5e143e0a8e | ||
|
|
57f803d52c | ||
|
|
c55d342c7e | ||
|
|
e9065635cd | ||
|
|
6057721249 | ||
|
|
975487d271 | ||
|
|
46b2fd5978 | ||
|
|
f61313449f | ||
|
|
366ad4d7df | ||
|
|
b5cfb44cf0 | ||
|
|
050cc9cea9 | ||
|
|
b2f194eeee | ||
|
|
97b0b8541d | ||
|
|
b5eb74fe20 | ||
|
|
0500172d42 | ||
|
|
0e7bd98485 | ||
|
|
ff0ece5149 | ||
|
|
db9ad86ac2 | ||
|
|
a066710bfb | ||
|
|
52740483b6 | ||
|
|
518a624e20 | ||
|
|
4ba71c8bd5 | ||
|
|
ceeb628784 | ||
|
|
2ec6a90dc3 | ||
|
|
4e37d834ed | ||
|
|
a7922d894d | ||
|
|
eeda3296ab | ||
|
|
acd26e0162 | ||
|
|
fbf828e202 | ||
|
|
af9015f529 | ||
|
|
7a172b2022 | ||
|
|
a609dc4029 | ||
|
|
92bfee4bcc | ||
|
|
559d4618d1 | ||
|
|
5c391ee287 | ||
|
|
db74cfeb2a | ||
|
|
726b825e4c | ||
|
|
84d56b2606 | ||
|
|
e9a8b094a2 | ||
|
|
924b14621a | ||
|
|
b072e3a98c | ||
|
|
827bd5fa24 | ||
|
|
9e0410afe6 | ||
|
|
ef08a9347e | ||
|
|
9b732540a6 | ||
|
|
24c52074b8 | ||
|
|
cb6c6e0d9f | ||
|
|
884dc6fa49 | ||
|
|
efe42021f1 | ||
|
|
470277c253 | ||
|
|
c1cab38c7a | ||
|
|
1cf36ddb08 | ||
|
|
1fec89b69e | ||
|
|
911549aca3 | ||
|
|
41329ec2f8 | ||
|
|
15cdfbc980 | ||
|
|
5053058c32 | ||
|
|
8a8cfad303 | ||
|
|
cc3c17078d | ||
|
|
47965d5f57 | ||
|
|
c947d9e3ed | ||
|
|
8318180e96 | ||
|
|
87daf6fb76 | ||
|
|
5d254ac2ff | ||
|
|
482f322d9f | ||
|
|
b1d097a40f | ||
|
|
da456b08fe | ||
|
|
2776ac3ce0 | ||
|
|
eb2fc4c715 | ||
|
|
d2ffc3a0b1 | ||
|
|
40a9978f31 | ||
|
|
22688933b3 | ||
|
|
7cf01f0c12 | ||
|
|
1e87765f95 | ||
|
|
cc8fe4e02e | ||
|
|
979fc98e70 | ||
|
|
c3c0c35d8a | ||
|
|
f9913cad63 | ||
|
|
0ed64f94e8 | ||
|
|
1deecd4e2a | ||
|
|
e0e9201b46 | ||
|
|
362c7b437e | ||
|
|
efd0b547e9 | ||
|
|
afbaaa66bc | ||
|
|
de8c687dfb | ||
|
|
1bb079f078 | ||
|
|
d951256b1c | ||
|
|
b35322522f | ||
|
|
6a605571e0 | ||
|
|
c4ec77c911 | ||
|
|
f7643cee7e | ||
|
|
cd95202dcc | ||
|
|
facba3998b | ||
|
|
1f9cc2ce33 | ||
|
|
ad505378ab | ||
|
|
fb7b30c81d | ||
|
|
cb3f93c67d | ||
|
|
76cb5a6b7c | ||
|
|
990e47cb24 | ||
|
|
8be7465428 | ||
|
|
e2b7a6a517 | ||
|
|
284e00b665 | ||
|
|
1fbfa8428b | ||
|
|
b13b89c7ce | ||
|
|
3fa31e2a2e | ||
|
|
0e7d2d8ff5 | ||
|
|
2def3820dd | ||
|
|
e1a489f318 | ||
|
|
760839eb83 | ||
|
|
7136d2dea3 | ||
|
|
21f4d26b50 | ||
|
|
627ea268c9 | ||
|
|
65f77db4d6 | ||
|
|
03b4f1eb8d | ||
|
|
82d1fc0a8b | ||
|
|
3b16de5df4 | ||
|
|
ec753869d5 | ||
|
|
2e58be1c90 | ||
|
|
562a4f7322 | ||
|
|
e52a63e1dd | ||
|
|
4533fd06ed | ||
|
|
b4fc62632c | ||
|
|
e3a9a8a38c | ||
|
|
7c4e982fb3 | ||
|
|
85dd22b09a | ||
|
|
18e15eeb5c | ||
|
|
3a45b8dd41 | ||
|
|
5b8412765b | ||
|
|
69519df82f | ||
|
|
8052701021 | ||
|
|
35c0150522 | ||
|
|
c55278e7da | ||
|
|
94d4070653 | ||
|
|
d856a1cd8e | ||
|
|
7c1d0f7bee | ||
|
|
d02036321f | ||
|
|
1edadcc6bb | ||
|
|
aa92091d2d | ||
|
|
14e0385575 | ||
|
|
6cf861d44e | ||
|
|
1375240bfa | ||
|
|
8f391d7d52 | ||
|
|
84654e81af | ||
|
|
7ff0524bb7 | ||
|
|
06c3c92fd6 | ||
|
|
4c2941e9b6 | ||
|
|
ed7a25646e | ||
|
|
8c821176cf | ||
|
|
98ded98583 | ||
|
|
10ae69e41f | ||
|
|
04531b4305 | ||
|
|
7ab4b2481c | ||
|
|
6fa88e6869 | ||
|
|
5b90c31645 | ||
|
|
5eba11faee | ||
|
|
54b4fc473c | ||
|
|
fe8595cdc9 | ||
|
|
2565a89474 | ||
|
|
92b970377d | ||
|
|
dd3a550ec3 | ||
|
|
7d5d9e2d82 | ||
|
|
9daa71befc | ||
|
|
f10a2d28df | ||
|
|
d52c6c70bb | ||
|
|
6e2f46a0da | ||
|
|
2d6f91d0cd | ||
|
|
f409079d93 | ||
|
|
7004259867 | ||
|
|
f3128cb6d2 | ||
|
|
1dfd4d8d48 | ||
|
|
2f30eb6cbe | ||
|
|
cf45c04069 | ||
|
|
05d149bcfa | ||
|
|
3acbc3291c | ||
|
|
24aa2fd5fa | ||
|
|
cc7d7a56f5 | ||
|
|
da17646b8d | ||
|
|
f9233a1e31 | ||
|
|
c94f536e5a | ||
|
|
b712e7f184 | ||
|
|
b8ab802de5 | ||
|
|
a74eb2989c | ||
|
|
3a0fc6eeac | ||
|
|
0f0684e701 | ||
|
|
e9b5bfb572 | ||
|
|
b456b59c44 | ||
|
|
59ad91fdf8 | ||
|
|
e18d304313 | ||
|
|
5de5281b49 | ||
|
|
fe0df2a4ef | ||
|
|
deec7894dd | ||
|
|
c9a0c0400e | ||
|
|
419e62f69b | ||
|
|
9ffbe3a7a2 | ||
|
|
c8e548e3be | ||
|
|
b9160bd333 | ||
|
|
8ad8b4cdea | ||
|
|
c51c40a20e | ||
|
|
2ccb1a8e0a | ||
|
|
008bb75c04 | ||
|
|
a51fdb8435 | ||
|
|
cdd5d27908 | ||
|
|
82d3c934c8 | ||
|
|
f2f0270936 | ||
|
|
00f646d489 | ||
|
|
90a02df45c | ||
|
|
ad40a15a77 | ||
|
|
d6809cb504 | ||
|
|
ff8d3055b4 | ||
|
|
1f81449e3c | ||
|
|
c59e0cb046 | ||
|
|
2d1675c181 | ||
|
|
48ebca6236 | ||
|
|
bce8e8b807 | ||
|
|
1f4dfa7603 | ||
|
|
c23af0063d | ||
|
|
9f6250c489 | ||
|
|
cf6c6f1c17 | ||
|
|
c6163b6ba2 | ||
|
|
7121a680b4 | ||
|
|
cf391031f0 | ||
|
|
9cac5305cd | ||
|
|
b0adb8cd3d | ||
|
|
3d2a1d5d19 | ||
|
|
3c93c41de1 | ||
|
|
4baed5d8ab | ||
|
|
f17b02b1fe | ||
|
|
3c72d07a88 | ||
|
|
7495c0dfcf | ||
|
|
b0c877d4ae | ||
|
|
e1fd0f8014 | ||
|
|
cb2198b661 | ||
|
|
68beeeb599 | ||
|
|
218ce148d5 | ||
|
|
d0cafb06ee | ||
|
|
5671a58704 | ||
|
|
9cf74ca405 | ||
|
|
1c6e3648eb | ||
|
|
b3a4628a79 | ||
|
|
941cc37c87 | ||
|
|
906d7ac3d5 | ||
|
|
588b18d10e | ||
|
|
90024c5ec7 | ||
|
|
889005de6c | ||
|
|
843d3df8bc | ||
|
|
825b9044b9 | ||
|
|
92a1be83b6 | ||
|
|
36e35eb208 | ||
|
|
2fcdb47fe5 | ||
|
|
012e4b670e | ||
|
|
492cc244d8 | ||
|
|
d17d6b43c0 | ||
|
|
1bf10cd6d6 | ||
|
|
b0cd9140be | ||
|
|
cc6196969f | ||
|
|
802a7eb1be | ||
|
|
a666f86be0 | ||
|
|
44fad76d45 | ||
|
|
c8d23a534e | ||
|
|
cf3650c8cd | ||
|
|
1a8349f821 | ||
|
|
855771a6b6 | ||
|
|
b711168e44 | ||
|
|
4134919dde | ||
|
|
158153e366 | ||
|
|
6b890d162a | ||
|
|
1525807d06 | ||
|
|
a356bfd601 | ||
|
|
e6d2a6d5ff | ||
|
|
c8a004422f | ||
|
|
df457fc7bf | ||
|
|
6e03f1f077 | ||
|
|
4c13350c14 | ||
|
|
63a2495c68 | ||
|
|
f9de73e18a | ||
|
|
e6e2be61f0 | ||
|
|
060f7aa42c | ||
|
|
8b0ae178b8 | ||
|
|
6b94e9b78b | ||
|
|
b171bf3127 | ||
|
|
0c6a179cec | ||
|
|
aa50785c12 | ||
|
|
252403b1e3 | ||
|
|
bc1d0de057 | ||
|
|
4a0b9fd0de | ||
|
|
e8777e1857 | ||
|
|
94a2a16330 | ||
|
|
004d135d93 | ||
|
|
0973dcee8a | ||
|
|
c908f7dcd0 | ||
|
|
b7fd889fcd | ||
|
|
35600c253d | ||
|
|
673465af77 | ||
|
|
cc5d0dc4fe | ||
|
|
e6da977c48 | ||
|
|
e4aa016713 | ||
|
|
8b67499a64 | ||
|
|
bcdc7c6811 | ||
|
|
8f07a79a49 | ||
|
|
cdcf16196c | ||
|
|
a0ed6a379b | ||
|
|
7ffb44f604 | ||
|
|
225d0388ce | ||
|
|
0468db8cc2 | ||
|
|
695f612110 | ||
|
|
9c07852b87 | ||
|
|
9ef198fb86 | ||
|
|
fd34e31462 | ||
|
|
8eb49427fd | ||
|
|
ebafb3c3dd | ||
|
|
09c6154949 | ||
|
|
53c8661cd3 | ||
|
|
3d5eec897e | ||
|
|
3763ec3737 | ||
|
|
f1216795d2 | ||
|
|
0ac70df00f | ||
|
|
7d0a985f1d | ||
|
|
c5ff67b6e0 | ||
|
|
db6b9ac176 | ||
|
|
77475af3c6 | ||
|
|
805133d10c | ||
|
|
fff8fb72c5 | ||
|
|
e02b18cea3 | ||
|
|
b8e9307ce2 | ||
|
|
00b22eb902 | ||
|
|
be954143c3 | ||
|
|
b314642586 | ||
|
|
b5fc315b37 | ||
|
|
26ff9f63bb | ||
|
|
7e5fb4d42f | ||
|
|
762bb786be | ||
|
|
cec87765fc | ||
|
|
b966fae926 | ||
|
|
f1ddf4e57e | ||
|
|
64e65dc7d3 | ||
|
|
1e07a88aed | ||
|
|
6e49cc65a9 | ||
|
|
06d579118a | ||
|
|
f0e4b5b930 | ||
|
|
088127a9a5 | ||
|
|
a4b4d077fa | ||
|
|
bc2ec4d0b7 | ||
|
|
e4de101553 | ||
|
|
6207416b32 | ||
|
|
f0202241f4 | ||
|
|
0e8c94af1e | ||
|
|
922ee616ec | ||
|
|
98c235bc21 | ||
|
|
70f1f9ffac | ||
|
|
409f81f0d8 | ||
|
|
1aa90bef37 | ||
|
|
c5ea9494df | ||
|
|
9a34db7008 | ||
|
|
988b7fca0f | ||
|
|
bdc6489db4 | ||
|
|
49b096b601 | ||
|
|
31d83f6a26 | ||
|
|
03e52b7a72 | ||
|
|
551e950c39 | ||
|
|
a7620f83a6 | ||
|
|
0384181ee1 | ||
|
|
fd59f42819 | ||
|
|
cc51774259 | ||
|
|
5a19245ee0 | ||
|
|
45f45b7547 | ||
|
|
530bfaf3b3 | ||
|
|
6ea70ab9cf | ||
|
|
a0e2d68dae | ||
|
|
1447e8bfb5 | ||
|
|
c0480a50ea | ||
|
|
09ba2be416 | ||
|
|
115ce3305d | ||
|
|
960b2022ed | ||
|
|
252b442ca9 | ||
|
|
03b9ddda9c | ||
|
|
968d7d7fda | ||
|
|
b211f1cd12 | ||
|
|
870d4349f4 | ||
|
|
d88bc59720 | ||
|
|
7c00775515 | ||
|
|
4b5536b282 | ||
|
|
fb84cfee1c | ||
|
|
9132262106 | ||
|
|
5de9e708e9 | ||
|
|
4df63c7287 | ||
|
|
a88ca031d0 | ||
|
|
452706f35c | ||
|
|
8b0aa194cf | ||
|
|
b9aa7e9206 | ||
|
|
e35f106482 | ||
|
|
b7a97cf8ee | ||
|
|
85a28999fb | ||
|
|
9208115981 | ||
|
|
afbdaecd0a | ||
|
|
558f3d649d | ||
|
|
417511d577 | ||
|
|
df350534ce | ||
|
|
7167235146 | ||
|
|
7a7f2eb7de | ||
|
|
cd28a53f6a | ||
|
|
1fe31ac0ec | ||
|
|
ffce8e3ba5 | ||
|
|
a28a417ebc | ||
|
|
6cdb56d13f | ||
|
|
0516e587b4 | ||
|
|
5b4063105b | ||
|
|
d9a5548762 | ||
|
|
cae6cffb7b | ||
|
|
ede782abed | ||
|
|
00afbad7ac | ||
|
|
edd09ef585 | ||
|
|
1e09066779 | ||
|
|
32edb48e16 | ||
|
|
b116eef147 | ||
|
|
74d1cd2d01 | ||
|
|
fd48d82e42 | ||
|
|
480d54c2d8 | ||
|
|
ab9c39b862 | ||
|
|
dd122d1bac | ||
|
|
f9f5e8f925 | ||
|
|
aa2f4a091c | ||
|
|
13fc699d4a | ||
|
|
f5e8d473ad | ||
|
|
35353d75f5 | ||
|
|
0f103c3c00 | ||
|
|
019428a241 | ||
|
|
6200edea25 | ||
|
|
fc7395df96 | ||
|
|
272f662a34 | ||
|
|
d59d9cde95 | ||
|
|
c71fbcf436 | ||
|
|
54c79445db | ||
|
|
a82ba26f86 | ||
|
|
28af87391d | ||
|
|
0aabd33538 | ||
|
|
bd9076c4ff | ||
|
|
1aed761893 | ||
|
|
a2a6f6dcab | ||
|
|
db5dd0f6ee | ||
|
|
42c3dcf258 | ||
|
|
51a115d65a | ||
|
|
fc0fbd6a37 | ||
|
|
d80d76724c | ||
|
|
77da0a6d30 | ||
|
|
79b251d8b9 | ||
|
|
4f19f6a08c | ||
|
|
d2a6eab1e6 | ||
|
|
c7cf051502 | ||
|
|
6e21503e6b | ||
|
|
78d71a4e7e | ||
|
|
b8f32d46cf | ||
|
|
443782decf | ||
|
|
54e79e5eb8 | ||
|
|
221cd4ffd2 | ||
|
|
354b2fb3cb | ||
|
|
7cb2c36ac9 | ||
|
|
11d73595fc | ||
|
|
c241a6e280 | ||
|
|
198ff143f6 | ||
|
|
7b8b797f9c | ||
|
|
a41b25eea7 | ||
|
|
06eac68f9d | ||
|
|
8abf84ebc0 | ||
|
|
e9aa1f6dd6 | ||
|
|
8e7b838bf7 | ||
|
|
32db3c3c9b | ||
|
|
d3ecef3de6 | ||
|
|
45bdf53a41 | ||
|
|
00e94212bd | ||
|
|
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 |
13
.babelrc
13
.babelrc
@@ -1,13 +0,0 @@
|
|||||||
{
|
|
||||||
"presets": ["env", "react"],
|
|
||||||
"plugins": ["transform-object-rest-spread", "transform-class-properties"],
|
|
||||||
"env": {
|
|
||||||
"test": {
|
|
||||||
"plugins": [
|
|
||||||
["istanbul", {
|
|
||||||
exclude: ["node_modules/**", "test/**"]
|
|
||||||
}]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,84 +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-{{ checksum "package.json" }}
|
|
||||||
|
|
||||||
- run: npm install
|
|
||||||
|
|
||||||
- save_cache:
|
|
||||||
paths:
|
|
||||||
- node_modules
|
|
||||||
key: v1-dependencies-{{ 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-{{ checksum "package.json" }}
|
|
||||||
|
|
||||||
- run: npm install
|
|
||||||
|
|
||||||
- save_cache:
|
|
||||||
paths:
|
|
||||||
- node_modules
|
|
||||||
key: v1-dependencies-{{ checksum "package.json" }}
|
|
||||||
|
|
||||||
- run: mkdir -p /tmp/artifacts/logs
|
|
||||||
- run: npm run build
|
|
||||||
- run: npm run lint
|
|
||||||
- run: npm run lint-styles
|
|
||||||
- run: DOCKER_HOST=localhost npm test
|
|
||||||
- run: ./node_modules/.bin/istanbul report --include /tmp/artifacts/coverage/coverage.json --dir /tmp/artifacts/coverage html lcov
|
|
||||||
- store_artifacts:
|
|
||||||
path: /tmp/artifacts
|
|
||||||
destination: /artifacts
|
|
||||||
jobs:
|
|
||||||
build-linux-node-v6:
|
|
||||||
docker:
|
|
||||||
- image: node:6
|
|
||||||
working_directory: ~/repo-linux-node-v6
|
|
||||||
steps: *build-steps
|
|
||||||
build-linux-node-v8:
|
|
||||||
docker:
|
|
||||||
- image: node:8
|
|
||||||
- image: selenium/standalone-chrome:3.8.1
|
|
||||||
working_directory: ~/repo-linux-node-v8
|
|
||||||
steps: *wdio-steps
|
|
||||||
build-linux-node-v9:
|
|
||||||
docker:
|
|
||||||
- image: node:9
|
|
||||||
working_directory: ~/repo-linux-node-v9
|
|
||||||
steps: *build-steps
|
|
||||||
build-osx-node-v9:
|
|
||||||
macos:
|
|
||||||
xcode: "9.0"
|
|
||||||
dependencies:
|
|
||||||
override:
|
|
||||||
- brew install node@9
|
|
||||||
working_directory: ~/repo-linux-node-v9
|
|
||||||
steps: *build-steps
|
|
||||||
|
|
||||||
workflows:
|
|
||||||
version: 2
|
|
||||||
build:
|
|
||||||
jobs:
|
|
||||||
- build-linux-node-v6
|
|
||||||
- build-linux-node-v8
|
|
||||||
- build-linux-node-v9
|
|
||||||
|
|
||||||
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 directory used by tools like istanbul
|
||||||
coverage
|
coverage
|
||||||
|
.nyc_output
|
||||||
|
|
||||||
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||||
.grunt
|
.grunt
|
||||||
@@ -32,4 +33,6 @@ node_modules
|
|||||||
public
|
public
|
||||||
/errorShots
|
/errorShots
|
||||||
/old
|
/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"]
|
||||||
|
}
|
||||||
15
.topissuesrc
Normal file
15
.topissuesrc
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"labels": {
|
||||||
|
"bug": 5,
|
||||||
|
"maintenance": 3,
|
||||||
|
"mentioned in the 1st survey": 2
|
||||||
|
},
|
||||||
|
"reactions": {
|
||||||
|
"+1": 2,
|
||||||
|
"-1": -1,
|
||||||
|
"laugh": 1,
|
||||||
|
"hooray": 2,
|
||||||
|
"confused": 1,
|
||||||
|
"heart": 2
|
||||||
|
}
|
||||||
|
}
|
||||||
22
.travis.yml
22
.travis.yml
@@ -1,22 +0,0 @@
|
|||||||
language: node_js
|
|
||||||
matrix:
|
|
||||||
include:
|
|
||||||
- os: osx
|
|
||||||
node_js: "6"
|
|
||||||
- os: osx
|
|
||||||
node_js: "8"
|
|
||||||
- os: osx
|
|
||||||
node_js: "9"
|
|
||||||
install:
|
|
||||||
- npm install
|
|
||||||
script:
|
|
||||||
- mkdir public
|
|
||||||
- node --stack_size=100000 $(which npm) run build
|
|
||||||
- npm run lint
|
|
||||||
- npm run lint-styles
|
|
||||||
addons:
|
|
||||||
apt:
|
|
||||||
sources:
|
|
||||||
- ubuntu-toolchain-r-test
|
|
||||||
packages:
|
|
||||||
- g++-4.8
|
|
||||||
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)
|
||||||
23
Dockerfile
23
Dockerfile
@@ -1,15 +1,16 @@
|
|||||||
FROM nodesource/xenial:6.1.0
|
FROM node:18 as builder
|
||||||
|
WORKDIR /maputnik
|
||||||
|
|
||||||
EXPOSE 8888
|
# Only copy package.json to prevent npm install from running on every build
|
||||||
|
COPY package.json package-lock.json .npmrc ./
|
||||||
|
RUN npm ci
|
||||||
|
|
||||||
ENV HOME /maputnik
|
# Build maputnik
|
||||||
RUN mkdir ${HOME}
|
COPY . .
|
||||||
|
RUN npx vite build
|
||||||
|
|
||||||
COPY . ${HOME}/
|
#---------------------------------------------------------------------------
|
||||||
|
# Create a clean nginx-alpine slim image with just the build results
|
||||||
|
FROM nginx:alpine-slim
|
||||||
|
|
||||||
WORKDIR ${HOME}
|
COPY --from=builder /maputnik/dist /usr/share/nginx/html/
|
||||||
|
|
||||||
RUN npm install -d --dev
|
|
||||||
RUN npm run build
|
|
||||||
|
|
||||||
CMD npm run start -- --host 0.0.0.0
|
|
||||||
|
|||||||
1
LICENSE
1
LICENSE
@@ -1,6 +1,7 @@
|
|||||||
The MIT License (MIT)
|
The MIT License (MIT)
|
||||||
|
|
||||||
Copyright (c) 2015 Lukas Martinelli
|
Copyright (c) 2015 Lukas Martinelli
|
||||||
|
Copyright (c) 2024 MapLibre contributors
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|||||||
152
README.md
152
README.md
@@ -1,44 +1,43 @@
|
|||||||
# Maputnik
|
<img width="200" alt="Maputnik logo" src="https://cdn.jsdelivr.net/gh/maputnik/design/logos/logo-color.png" />
|
||||||
|
|
||||||
[][travis]
|
# Maputnik
|
||||||
[][appveyor]
|
[][github-action-ci]
|
||||||
[][dm-prod]
|
|
||||||
[][dm-dev]
|
|
||||||
[][license]
|
[][license]
|
||||||
|
|
||||||
[travis]: https://travis-ci.org/maputnik/editor
|
[github-action-ci]: https://github.com/maplibre/maputnik/actions?query=workflow%3Aci
|
||||||
[appveyor]: https://ci.appveyor.com/project/lukasmartinelli/editor
|
[license]: https://tldrlegal.com/license/mit-license
|
||||||
[dm-prod]: https://david-dm.org/maputnik/editor
|
|
||||||
[dm-dev]: https://david-dm.org/maputnik/editor#info=devDependencies
|
|
||||||
[license]: https://tldrlegal.com/license/mit-license
|
|
||||||
|
|
||||||
<img width="200" align="right" alt="Maputnik" src="src/img/maputnik.png" />
|
A free and open visual editor for the [MapLibre GL styles](https://maplibre.org/maplibre-style-spec/)
|
||||||
|
|
||||||
A free and open visual editor for the [Mapbox GL styles](https://www.mapbox.com/mapbox-gl-style-spec/)
|
|
||||||
targeted at developers and map designers.
|
targeted at developers and map designers.
|
||||||
|
|
||||||
- :link: Design your maps online at **<https://maputnik.github.io/editor/>** (all in local storage)
|
|
||||||
- :link: Use the [Maputnik CLI](https://github.com/maputnik/editor/wiki/Maputnik-CLI) for local style development
|
|
||||||
|
|
||||||
Mapbox has built one of the best and most amazing OSS ecosystems. A key component to ensure its longevity and independance is an OSS map designer.
|
## 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.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -it --rm -p 8888:80 ghcr.io/maplibre/maputnik:main
|
||||||
|
```
|
||||||
|
|
||||||
## Documentation
|
## 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
|
- :video_camera: Design a map from Scratch https://youtu.be/XoDh0gEnBQo
|
||||||
|
|
||||||
[](https://youtu.be/XoDh0gEnBQo)
|
[](https://youtu.be/XoDh0gEnBQo)
|
||||||
|
|
||||||
## Develop
|
## 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
|
We ensure building and developing Maputnik works with the [current active LTS Node.js version and above](https://github.com/nodejs/Release#release-schedule).
|
||||||
|
|
||||||
- Linux, OSX and Windows
|
### Getting Involved
|
||||||
- Node >4
|
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/`.
|
Install the deps, start the dev server and open the web browser on `http://localhost:8888/`.
|
||||||
|
|
||||||
@@ -46,15 +45,17 @@ Install the deps, start the dev server and open the web browser on `http://local
|
|||||||
# install dependencies
|
# install dependencies
|
||||||
npm install
|
npm install
|
||||||
# start dev server
|
# start dev server
|
||||||
npm start
|
npm run start
|
||||||
```
|
```
|
||||||
|
|
||||||
The build process will watch for changes to the filesystem, rebuild and autoreload the editor. However note this from the webpack-dev-server docs
|
If you want Maputnik to be accessible externally use the [`--host` option](https://vitejs.dev/config/server-options.html#server-host):
|
||||||
|
|
||||||
> 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.
|
```bash
|
||||||
Snippet from <https://webpack.js.org/configuration/dev-server/#devserver-watchoptions->
|
# start externally accessible dev server
|
||||||
|
npm run start -- --host 0.0.0.0
|
||||||
|
```
|
||||||
|
|
||||||
To enable polling add `export WEBPACK_DEV_SERVER_POLLING=1` to your enviroment.
|
The build process will watch for changes to the filesystem, rebuild and autoreload the editor.
|
||||||
|
|
||||||
```
|
```
|
||||||
npm run build
|
npm run build
|
||||||
@@ -65,98 +66,51 @@ Lint the JavaScript code.
|
|||||||
```
|
```
|
||||||
# run linter
|
# run linter
|
||||||
npm run lint
|
npm run lint
|
||||||
npm run lint-styles
|
npm run lint-css
|
||||||
|
npm run sort-styles
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
## Tests
|
## 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 and 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
|
||||||
|
|
||||||
```
|
1. Review [`CHANGELOG.md`](/CHANGELOG.md)
|
||||||
npm test
|
- 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
|
## 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.
|
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.
|
||||||
|
You can see this file's history for previous sponsors of the original Maputnik repo.
|
||||||
### Gold
|
Read more about the MapLibre Sponsorship Program at https://maplibre.org/sponsors/.
|
||||||
|
|
||||||
- [Wemap](https://getwemap.com/)
|
|
||||||
- [Orbicon Informatik](https://www.orbiconinformatik.dk/)
|
|
||||||
- [Terranodo](http://terranodo.io/)
|
|
||||||
|
|
||||||
<a href="https://getwemap.com/">
|
|
||||||
<img width="33%" alt="Wemap" style="display:inline" src="media/sponsors/wemap.jpg" />
|
|
||||||
</a>
|
|
||||||
<a href="http://terranodo.io/">
|
|
||||||
<img width="33%" alt="Terranodo" style="display:inline" src="media/sponsors/terranodo.png" />
|
|
||||||
</a>
|
|
||||||
<a href="https://www.orbiconinformatik.dk/">
|
|
||||||
<img width="32%" alt="Terranodo" style="display:inline" src="media/sponsors/orbicon_informatik.png" />
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<br/>
|
|
||||||
|
|
||||||
### Silver
|
|
||||||
|
|
||||||
- [Klokan Technologies](https://www.klokantech.com/)
|
|
||||||
- [Geofabrik](http://www.geofabrik.de/)
|
|
||||||
- [Dreipol](https://www.dreipol.ch/)
|
|
||||||
|
|
||||||
<a href="https://www.klokantech.com/">
|
|
||||||
<img width="18%" alt="Klokan Technologies" style="display:inline-block" src="media/sponsors/klokantech.png" />
|
|
||||||
</a>
|
|
||||||
<a href="http://www.geofabrik.de/">
|
|
||||||
<img width="18%" alt="Geofabrik" style="display:inline-block" src="media/sponsors/geofabrik.png" />
|
|
||||||
</a>
|
|
||||||
<a href="https://www.dreipol.ch/">
|
|
||||||
<img width="18%" alt="Dreipol" style="display:inline-block" src="media/sponsors/dreipol.png" />
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<br/>
|
|
||||||
|
|
||||||
### Individuals
|
|
||||||
|
|
||||||
**Influential Stakeholder**
|
|
||||||
|
|
||||||
Alan McConchie, Odi, Mats Norén, Uli [geOps](http://geops.ch/), Helge Fahrnberger ([Toursprung](http://www.toursprung.com/)), Kirusanth Poopalasingam
|
|
||||||
|
|
||||||
**Stakeholder**
|
|
||||||
|
|
||||||
Brian Flood, Vasile Coțovanu, Andreas Kalkbrenner, Christian Mäder, Gregor Wassmann, Lee Armstrong, Rafel, Jon Burgess, Lukas Lehmann, Joachim Ungar, Alois Ackermann, Zsolt Ero, Jordan Meek
|
|
||||||
|
|
||||||
**Supporter**
|
|
||||||
|
|
||||||
Sina Martinelli, Nicholas Doiron, Neil Cawse, Urs42, Benedikt Groß, Manuel Roth, Janko Mihelić, Moritz Stefaner, Sebastian Ahoi, Juerg Uhlmann, Tom Wider, Nadia Panchaud, Oliver Snowden, Stephan Heuel, Tobin Bradley, Adrian Herzog, Antti Lehto, Pascal Mages, Marc Gehling, Imre Samu, Lauri K., Visahavel Parthasarathy, Christophe Waterlot-Buisine, Max Galka, ubahnverleih, Wouter van Dam, Jakob Lobensteiner, Samuel Kurath, Brian Bancroft
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Maputnik is [licensed under MIT](LICENSE) and is Copyright (c) Lukas Martinelli and contributors.
|
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.
|
||||||
**Disclaimer** This project is not affiliated with Mapbox or Mapbox Studio. It is a independent style editor for the
|
|
||||||
open source technology in the Mapbox GL ecosystem.
|
|
||||||
As contributor please take extra care of not violating any Mapbox trademarks. Do not get inspired by Mapbox Studio and make your own decisions for a good style editor.
|
|
||||||
|
|||||||
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
|
||||||
17
appveyor.yml
17
appveyor.yml
@@ -1,17 +0,0 @@
|
|||||||
environment:
|
|
||||||
matrix:
|
|
||||||
- nodejs_version: "6"
|
|
||||||
- nodejs_version: "8"
|
|
||||||
- nodejs_version: "9"
|
|
||||||
platform:
|
|
||||||
- x86
|
|
||||||
- x64
|
|
||||||
install:
|
|
||||||
- ps: Install-Product node $env:nodejs_version
|
|
||||||
- md public
|
|
||||||
- npm install --global --production windows-build-tools
|
|
||||||
- npm install
|
|
||||||
build_script:
|
|
||||||
- npm run build
|
|
||||||
test_script:
|
|
||||||
- npm run lint
|
|
||||||
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,62 +0,0 @@
|
|||||||
var webpack = require("webpack");
|
|
||||||
var WebpackDevServer = require("webpack-dev-server");
|
|
||||||
var webpackConfig = require("./webpack.config");
|
|
||||||
var testConfig = require("../test/config/specs");
|
|
||||||
var artifacts = require("../test/artifacts");
|
|
||||||
var isDocker = require("is-docker");
|
|
||||||
|
|
||||||
|
|
||||||
var server;
|
|
||||||
var SCREENSHOT_PATH = artifacts.pathSync("screenshots");
|
|
||||||
|
|
||||||
exports.config = {
|
|
||||||
specs: [
|
|
||||||
'./test/functional/index.js'
|
|
||||||
],
|
|
||||||
exclude: [
|
|
||||||
],
|
|
||||||
maxInstances: 10,
|
|
||||||
capabilities: [{
|
|
||||||
maxInstances: 5,
|
|
||||||
browserName: 'chrome'
|
|
||||||
}],
|
|
||||||
sync: true,
|
|
||||||
logLevel: 'verbose',
|
|
||||||
coloredLogs: true,
|
|
||||||
bail: 0,
|
|
||||||
screenshotPath: SCREENSHOT_PATH,
|
|
||||||
// Note: This is here because @orangemug currently runs Maputnik inside a docker container.
|
|
||||||
host: process.env.DOCKER_HOST || "0.0.0.0",
|
|
||||||
baseUrl: 'http://localhost',
|
|
||||||
waitforTimeout: 10000,
|
|
||||||
connectionRetryTimeout: 90000,
|
|
||||||
connectionRetryCount: 3,
|
|
||||||
framework: 'mocha',
|
|
||||||
reporters: ['spec'],
|
|
||||||
mochaOpts: {
|
|
||||||
ui: 'bdd',
|
|
||||||
// Because we don't know how long the initial build will take...
|
|
||||||
timeout: 4*60*1000
|
|
||||||
},
|
|
||||||
onPrepare: function (config, capabilities) {
|
|
||||||
return new Promise(function(resolve, reject) {
|
|
||||||
var compiler = webpack(webpackConfig);
|
|
||||||
server = new WebpackDevServer(compiler, {
|
|
||||||
stats: {
|
|
||||||
colors: true
|
|
||||||
}
|
|
||||||
});
|
|
||||||
server.listen(testConfig.port, (isDocker() ? "0.0.0.0" : "localhost"), function(err) {
|
|
||||||
if(err) {
|
|
||||||
reject(err);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
resolve();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
})
|
|
||||||
},
|
|
||||||
onComplete: function(exitCode) {
|
|
||||||
server.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
"use strict";
|
|
||||||
var webpack = require('webpack');
|
|
||||||
var path = require('path');
|
|
||||||
var loaders = require('./webpack.loaders');
|
|
||||||
var HtmlWebpackPlugin = require('html-webpack-plugin');
|
|
||||||
var CopyWebpackPlugin = require('copy-webpack-plugin');
|
|
||||||
|
|
||||||
const HOST = process.env.HOST || "127.0.0.1";
|
|
||||||
const PORT = process.env.PORT || "8888";
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
target: 'web',
|
|
||||||
entry: [
|
|
||||||
`webpack-dev-server/client?http://${HOST}:${PORT}`,
|
|
||||||
`webpack/hot/only-dev-server`,
|
|
||||||
`./src/index.jsx` // Your appʼs entry point
|
|
||||||
],
|
|
||||||
devtool: process.env.WEBPACK_DEVTOOL || 'cheap-module-source-map',
|
|
||||||
output: {
|
|
||||||
path: path.join(__dirname, '..', 'public'),
|
|
||||||
filename: 'bundle.js'
|
|
||||||
},
|
|
||||||
resolve: {
|
|
||||||
extensions: ['.js', '.jsx']
|
|
||||||
},
|
|
||||||
module: {
|
|
||||||
noParse: [
|
|
||||||
/mapbox-gl\/dist\/mapbox-gl.js/
|
|
||||||
],
|
|
||||||
loaders: loaders
|
|
||||||
},
|
|
||||||
node: {
|
|
||||||
fs: "empty",
|
|
||||||
net: 'empty',
|
|
||||||
tls: 'empty'
|
|
||||||
},
|
|
||||||
devServer: {
|
|
||||||
contentBase: "./public",
|
|
||||||
// do not print bundle build stats
|
|
||||||
noInfo: true,
|
|
||||||
// enable HMR
|
|
||||||
hot: true,
|
|
||||||
// embed the webpack-dev-server runtime into the bundle
|
|
||||||
inline: true,
|
|
||||||
// serve index.html in place of 404 responses to allow HTML5 history
|
|
||||||
historyApiFallback: true,
|
|
||||||
port: PORT,
|
|
||||||
host: HOST,
|
|
||||||
watchOptions: {
|
|
||||||
// Disabled polling by default as it causes lots of CPU usage and hence drains laptop batteries. To enable polling add WEBPACK_DEV_SERVER_POLLING to your environment
|
|
||||||
// See <https://webpack.js.org/configuration/watch/#watchoptions-poll> for details
|
|
||||||
poll: (!!process.env.WEBPACK_DEV_SERVER_POLLING ? true : false),
|
|
||||||
watch: false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
plugins: [
|
|
||||||
new webpack.NoEmitOnErrorsPlugin(),
|
|
||||||
new webpack.HotModuleReplacementPlugin(),
|
|
||||||
new HtmlWebpackPlugin({
|
|
||||||
title: 'Maputnik',
|
|
||||||
template: './src/template.html'
|
|
||||||
}),
|
|
||||||
new CopyWebpackPlugin([
|
|
||||||
{
|
|
||||||
from: './src/manifest.json',
|
|
||||||
to: 'manifest.json'
|
|
||||||
}
|
|
||||||
])
|
|
||||||
]
|
|
||||||
};
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
module.exports = [
|
|
||||||
{
|
|
||||||
test: /\.jsx?$/,
|
|
||||||
exclude: /(node_modules|bower_components|public)/,
|
|
||||||
loaders: ['react-hot-loader/webpack']
|
|
||||||
},
|
|
||||||
// HACK: This is a massive hack and reaches into the mapbox-gl private API.
|
|
||||||
// We have to include this for access to `normalizeSourceURL`. We should
|
|
||||||
// remove this ASAP, see <https://github.com/mapbox/mapbox-gl-js/issues/2416>
|
|
||||||
{
|
|
||||||
test: /.*node_modules[\/\\]mapbox-gl[\/\\]src[\/\\]util[\/\\].*\.js/,
|
|
||||||
loader: 'babel-loader',
|
|
||||||
query: {
|
|
||||||
presets: ['env', 'react', 'flow'],
|
|
||||||
plugins: ['transform-runtime', 'transform-decorators-legacy', 'transform-class-properties'],
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.jsx?$/,
|
|
||||||
// Note: These modules aren't ES5 therefore we much compile them.
|
|
||||||
exclude: /(.*node_modules(?)|bower_components|public)/,
|
|
||||||
loader: 'babel-loader',
|
|
||||||
query: {
|
|
||||||
presets: ['env', 'react'],
|
|
||||||
plugins: ['transform-runtime', 'transform-decorators-legacy', 'transform-class-properties'],
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.(eot|ttf|woff|woff2)$/,
|
|
||||||
loader: 'file-loader?name=fonts/[name].[ext]'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.ico$/,
|
|
||||||
loader: 'file-loader?name=[name].[ext]'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.(svg|gif|jpg|png)$/,
|
|
||||||
loader: 'file-loader?name=img/[name].[ext]'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.json$/,
|
|
||||||
loader: 'json-loader'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /[\/\\](node_modules|global|src)[\/\\].*\.scss$/,
|
|
||||||
loaders: ["style-loader", "css-loader", "sass-loader"]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /[\/\\](node_modules|global|src)[\/\\].*\.css$/,
|
|
||||||
loaders: [
|
|
||||||
'style-loader?sourceMap',
|
|
||||||
'css-loader'
|
|
||||||
]
|
|
||||||
}
|
|
||||||
];
|
|
||||||
@@ -1,88 +0,0 @@
|
|||||||
var webpack = require('webpack');
|
|
||||||
var path = require('path');
|
|
||||||
var loaders = require('./webpack.loaders');
|
|
||||||
var ExtractTextPlugin = require('extract-text-webpack-plugin');
|
|
||||||
var HtmlWebpackPlugin = require('html-webpack-plugin');
|
|
||||||
var WebpackCleanupPlugin = require('webpack-cleanup-plugin');
|
|
||||||
var BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
|
|
||||||
var CopyWebpackPlugin = require('copy-webpack-plugin');
|
|
||||||
var artifacts = require("../test/artifacts");
|
|
||||||
var UglifyJsPlugin = require('uglifyjs-webpack-plugin');
|
|
||||||
|
|
||||||
var OUTPATH = artifacts.pathSync("/build");
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
entry: {
|
|
||||||
app: './src/index.jsx',
|
|
||||||
vendor: [
|
|
||||||
'file-saver',
|
|
||||||
'mapbox-gl/dist/mapbox-gl.js',
|
|
||||||
"lodash.clonedeep",
|
|
||||||
"lodash.throttle",
|
|
||||||
'color',
|
|
||||||
'react',
|
|
||||||
"react-dom",
|
|
||||||
"react-color",
|
|
||||||
"react-file-reader-input",
|
|
||||||
"react-collapse",
|
|
||||||
"react-height",
|
|
||||||
"react-icon-base",
|
|
||||||
"react-motion",
|
|
||||||
"react-sortable-hoc",
|
|
||||||
"request",
|
|
||||||
//TODO: Icons raise multi vendor errors?
|
|
||||||
//"react-icons",
|
|
||||||
]
|
|
||||||
},
|
|
||||||
output: {
|
|
||||||
path: OUTPATH,
|
|
||||||
filename: '[name].[chunkhash].js',
|
|
||||||
chunkFilename: '[chunkhash].js'
|
|
||||||
},
|
|
||||||
resolve: {
|
|
||||||
extensions: ['.js', '.jsx']
|
|
||||||
},
|
|
||||||
module: {
|
|
||||||
noParse: [
|
|
||||||
/mapbox-gl\/dist\/mapbox-gl.js/
|
|
||||||
],
|
|
||||||
loaders
|
|
||||||
},
|
|
||||||
node: {
|
|
||||||
fs: "empty",
|
|
||||||
net: 'empty',
|
|
||||||
tls: 'empty'
|
|
||||||
},
|
|
||||||
plugins: [
|
|
||||||
new webpack.NoEmitOnErrorsPlugin(),
|
|
||||||
new webpack.optimize.CommonsChunkPlugin({ name: 'vendor', filename: '[chunkhash].vendor.js' }),
|
|
||||||
new WebpackCleanupPlugin(),
|
|
||||||
new webpack.DefinePlugin({
|
|
||||||
'process.env': {
|
|
||||||
NODE_ENV: '"production"'
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
new UglifyJsPlugin(),
|
|
||||||
new ExtractTextPlugin('[contenthash].css', {
|
|
||||||
allChunks: true
|
|
||||||
}),
|
|
||||||
new HtmlWebpackPlugin({
|
|
||||||
template: './src/template.html',
|
|
||||||
title: 'Maputnik'
|
|
||||||
}),
|
|
||||||
new CopyWebpackPlugin([
|
|
||||||
{
|
|
||||||
from: './src/manifest.json',
|
|
||||||
to: 'manifest.json'
|
|
||||||
}
|
|
||||||
]),
|
|
||||||
new BundleAnalyzerPlugin({
|
|
||||||
analyzerMode: 'static',
|
|
||||||
defaultSizes: 'gzip',
|
|
||||||
openAnalyzer: false,
|
|
||||||
generateStatsFile: true,
|
|
||||||
reportFilename: 'bundle-stats.html',
|
|
||||||
statsFilename: 'bundle-stats.json',
|
|
||||||
})
|
|
||||||
]
|
|
||||||
};
|
|
||||||
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,
|
"version": 8,
|
||||||
"name": "Test Style",
|
"name": "Test Style",
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"maputnik:renderer": "mbgljs"
|
"maputnik:renderer": "mlgljs"
|
||||||
},
|
},
|
||||||
"sources": {},
|
"sources": {},
|
||||||
"glyphs": "https://example.local/fonts/{fontstack}/{range}.pbf",
|
"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__';
|
||||||
|
}
|
||||||
|
}
|
||||||
132
index.html
Normal file
132
index.html
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Maputnik</title>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<link rel="manifest" href="src/manifest.json">
|
||||||
|
<link rel="icon" href="src/favicon.ico" type="image/x-icon" />
|
||||||
|
<style>
|
||||||
|
html {
|
||||||
|
background-color: rgb(28, 31, 36);
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading {
|
||||||
|
text-align: center;
|
||||||
|
position: absolute;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading__logo img {
|
||||||
|
width: 200px;
|
||||||
|
height: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading__text {
|
||||||
|
font-family: sans-serif;
|
||||||
|
color: white;
|
||||||
|
font-size: 1.2em;
|
||||||
|
padding-bottom: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- From <https://github.com/hail2u/color-blindness-emulation> -->
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
version="1.1">
|
||||||
|
<defs>
|
||||||
|
<filter id="protanopia">
|
||||||
|
<feColorMatrix
|
||||||
|
in="SourceGraphic"
|
||||||
|
type="matrix"
|
||||||
|
values="0.567, 0.433, 0, 0, 0
|
||||||
|
0.558, 0.442, 0, 0, 0
|
||||||
|
0, 0.242, 0.758, 0, 0
|
||||||
|
0, 0, 0, 1, 0"/>
|
||||||
|
</filter>
|
||||||
|
<filter id="protanomaly">
|
||||||
|
<feColorMatrix
|
||||||
|
in="SourceGraphic"
|
||||||
|
type="matrix"
|
||||||
|
values="0.817, 0.183, 0, 0, 0
|
||||||
|
0.333, 0.667, 0, 0, 0
|
||||||
|
0, 0.125, 0.875, 0, 0
|
||||||
|
0, 0, 0, 1, 0"/>
|
||||||
|
</filter>
|
||||||
|
<filter id="deuteranopia">
|
||||||
|
<feColorMatrix
|
||||||
|
in="SourceGraphic"
|
||||||
|
type="matrix"
|
||||||
|
values="0.625, 0.375, 0, 0, 0
|
||||||
|
0.7, 0.3, 0, 0, 0
|
||||||
|
0, 0.3, 0.7, 0, 0
|
||||||
|
0, 0, 0, 1, 0"/>
|
||||||
|
</filter>
|
||||||
|
<filter id="deuteranomaly">
|
||||||
|
<feColorMatrix
|
||||||
|
in="SourceGraphic"
|
||||||
|
type="matrix"
|
||||||
|
values="0.8, 0.2, 0, 0, 0
|
||||||
|
0.258, 0.742, 0, 0, 0
|
||||||
|
0, 0.142, 0.858, 0, 0
|
||||||
|
0, 0, 0, 1, 0"/>
|
||||||
|
</filter>
|
||||||
|
<filter id="tritanopia">
|
||||||
|
<feColorMatrix
|
||||||
|
in="SourceGraphic"
|
||||||
|
type="matrix"
|
||||||
|
values="0.95, 0.05, 0, 0, 0
|
||||||
|
0, 0.433, 0.567, 0, 0
|
||||||
|
0, 0.475, 0.525, 0, 0
|
||||||
|
0, 0, 0, 1, 0"/>
|
||||||
|
</filter>
|
||||||
|
<filter id="tritanomaly">
|
||||||
|
<feColorMatrix
|
||||||
|
in="SourceGraphic"
|
||||||
|
type="matrix"
|
||||||
|
values="0.967, 0.033, 0, 0, 0
|
||||||
|
0, 0.733, 0.267, 0, 0
|
||||||
|
0, 0.183, 0.817, 0, 0
|
||||||
|
0, 0, 0, 1, 0"/>
|
||||||
|
</filter>
|
||||||
|
<filter id="achromatopsia">
|
||||||
|
<feColorMatrix
|
||||||
|
in="SourceGraphic"
|
||||||
|
type="matrix"
|
||||||
|
values="0.299, 0.587, 0.114, 0, 0
|
||||||
|
0.299, 0.587, 0.114, 0, 0
|
||||||
|
0.299, 0.587, 0.114, 0, 0
|
||||||
|
0, 0, 0, 1, 0"/>
|
||||||
|
</filter>
|
||||||
|
<filter id="achromatomaly">
|
||||||
|
<feColorMatrix
|
||||||
|
in="SourceGraphic"
|
||||||
|
type="matrix"
|
||||||
|
values="0.618, 0.320, 0.062, 0, 0
|
||||||
|
0.163, 0.775, 0.062, 0, 0
|
||||||
|
0.163, 0.320, 0.516, 0, 0
|
||||||
|
0, 0, 0, 1, 0"/>
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<div id="app"></div>
|
||||||
|
<div class="loading">
|
||||||
|
<div class="loading__logo">
|
||||||
|
<img inline src="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 |
27041
package-lock.json
generated
27041
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
239
package.json
239
package.json
@@ -1,62 +1,82 @@
|
|||||||
{
|
{
|
||||||
"name": "maputnik",
|
"name": "maputnik",
|
||||||
"version": "1.2.0",
|
"version": "2.1.0",
|
||||||
"description": "A MapboxGL visual style editor",
|
"description": "A MapLibre GL visual style editor",
|
||||||
|
"type": "module",
|
||||||
"main": "''",
|
"main": "''",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"stats": "webpack --config config/webpack.production.config.js --profile --json > stats.json",
|
"start": "vite",
|
||||||
"build": "webpack --config config/webpack.production.config.js --progress --profile --colors",
|
"build": "tsc && vite build --base=/maputnik/",
|
||||||
"test": "cross-env NODE_ENV=test wdio config/wdio.conf.js",
|
"build-desktop": "tsc && vite build --base=/ && cd desktop && make",
|
||||||
"test-watch": "cross-env NODE_ENV=test wdio config/wdio.conf.js --watch",
|
"i18n:refresh": "i18next 'src/**/*.{ts,tsx,js,jsx}'",
|
||||||
"start": "webpack-dev-server --progress --profile --colors --config config/webpack.config.js",
|
"lint": "eslint ./src ./cypress --ext ts,tsx,js,jsx --report-unused-disable-directives --max-warnings 0",
|
||||||
"lint": "eslint --ext js --ext jsx {src,test}",
|
"test": "cypress run",
|
||||||
"lint-styles": "stylelint 'src/styles/*.scss'",
|
"cy:open": "cypress open",
|
||||||
"nsp": "nsp check --reporter summary"
|
"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": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/maputnik/editor"
|
"url": "https://github.com/maplibre/maputnik"
|
||||||
},
|
},
|
||||||
"author": "Lukas Martinelli",
|
"author": "Lukas Martinelli",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"homepage": "https://github.com/maputnik/editor#readme",
|
"homepage": "https://github.com/maplibre/maputnik#readme",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mapbox/mapbox-gl-rtl-text": "^0.1.1",
|
"@mapbox/mapbox-gl-rtl-text": "^0.2.3",
|
||||||
"@mapbox/mapbox-gl-style-spec": "^11.1.1",
|
"@maplibre/maplibre-gl-geocoder": "^1.6.0",
|
||||||
"classnames": "^2.2.5",
|
"@maplibre/maplibre-gl-inspect": "^1.6.3",
|
||||||
"codemirror": "^5.36.0",
|
"@maplibre/maplibre-gl-style-spec": "^20.1.1",
|
||||||
"color": "^3.0.0",
|
"@mdi/js": "^7.4.47",
|
||||||
"file-saver": "^1.3.8",
|
"@mdi/react": "^1.6.1",
|
||||||
"github-api": "^3.0.0",
|
"@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",
|
"jsonlint": "github:josdejong/jsonlint#85a19d7",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
"lodash.capitalize": "^4.2.1",
|
"lodash.capitalize": "^4.2.1",
|
||||||
|
"lodash.clamp": "^4.0.3",
|
||||||
"lodash.clonedeep": "^4.5.0",
|
"lodash.clonedeep": "^4.5.0",
|
||||||
|
"lodash.get": "^4.4.2",
|
||||||
"lodash.isequal": "^4.5.0",
|
"lodash.isequal": "^4.5.0",
|
||||||
"lodash.throttle": "^4.1.1",
|
"lodash.throttle": "^4.1.1",
|
||||||
"mapbox-gl": "^0.44.2",
|
"maplibre-gl": "^4.1.2",
|
||||||
"mapbox-gl-inspect": "^1.3.1",
|
"maputnik-design": "github:maputnik/design#172b06c",
|
||||||
"maputnik-design": "github:maputnik/design",
|
"ol": "^6.14.1",
|
||||||
"mousetrap": "^1.6.1",
|
"ol-mapbox-style": "^7.1.1",
|
||||||
"ol-mapbox-style": "^2.10.1",
|
"prop-types": "^15.8.1",
|
||||||
"ol": "^4.6.5",
|
"react": "^18.2.0",
|
||||||
"prop-types": "^15.6.0",
|
"react-accessible-accordion": "^5.0.0",
|
||||||
"react": "^16.3.2",
|
"react-aria-menubutton": "^7.0.3",
|
||||||
"react-addons-pure-render-mixin": "^15.6.2",
|
"react-aria-modal": "^5.0.2",
|
||||||
"react-autocomplete": "^1.7.2",
|
"react-autobind": "^1.0.6",
|
||||||
"react-codemirror2": "^4.2.1",
|
"react-autocomplete": "^1.8.1",
|
||||||
"react-collapse": "^4.0.3",
|
"react-collapse": "^5.1.1",
|
||||||
"react-color": "^2.14.1",
|
"react-color": "^2.19.3",
|
||||||
"react-copy-to-clipboard": "^5.0.1",
|
"react-dom": "^18.2.0",
|
||||||
"react-dom": "^16.3.2",
|
"react-file-reader-input": "^2.0.0",
|
||||||
"react-file-reader-input": "^1.1.4",
|
"react-i18next": "^15.0.1",
|
||||||
"react-height": "^3.0.0",
|
"react-icon-base": "^2.1.2",
|
||||||
"react-icon-base": "^2.1.1",
|
"react-icons": "^5.0.1",
|
||||||
"react-icons": "^2.2.7",
|
"react-sortable-hoc": "^2.0.0",
|
||||||
"react-motion": "^0.5.2",
|
"reconnecting-websocket": "^4.4.0",
|
||||||
"react-sortable-hoc": "^0.6.8",
|
"sass": "^1.72.0",
|
||||||
"reconnecting-websocket": "^3.2.2",
|
"slugify": "^1.6.6",
|
||||||
"request": "^2.85.0",
|
"string-hash": "^1.1.3",
|
||||||
"url": "^0.11.0"
|
"url": "^0.11.3"
|
||||||
},
|
},
|
||||||
"jshintConfig": {
|
"jshintConfig": {
|
||||||
"esversion": 6
|
"esversion": 6
|
||||||
@@ -64,85 +84,66 @@
|
|||||||
"stylelint": {
|
"stylelint": {
|
||||||
"extends": "stylelint-config-recommended-scss",
|
"extends": "stylelint-config-recommended-scss",
|
||||||
"rules": {
|
"rules": {
|
||||||
"no-descending-specificity": null
|
"no-descending-specificity": null,
|
||||||
}
|
"media-feature-name-no-unknown": [
|
||||||
},
|
true,
|
||||||
"eslintConfig": {
|
{
|
||||||
"plugins": [
|
"ignoreMediaFeatureNames": [
|
||||||
"react"
|
"prefers-reduced-motion"
|
||||||
],
|
]
|
||||||
"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": {
|
"devDependencies": {
|
||||||
"babel-core": "^6.26.0",
|
"@cypress/code-coverage": "^3.12.30",
|
||||||
"babel-eslint": "^8.2.3",
|
"@istanbuljs/nyc-config-typescript": "^1.0.2",
|
||||||
"babel-loader": "7.1.4",
|
"@rollup/plugin-replace": "^5.0.5",
|
||||||
"babel-plugin-istanbul": "^4.1.6",
|
"@shellygo/cypress-test-utils": "^2.1.9",
|
||||||
"babel-plugin-transform-class-properties": "^6.24.1",
|
"@types/codemirror": "^5.60.15",
|
||||||
"babel-plugin-transform-decorators-legacy": "^1.3.4",
|
"@types/color": "^3.0.6",
|
||||||
"babel-plugin-transform-flow-strip-types": "^6.22.0",
|
"@types/cors": "^2.8.17",
|
||||||
"babel-plugin-transform-object-rest-spread": "^6.26.0",
|
"@types/file-saver": "^2.0.7",
|
||||||
"babel-plugin-transform-runtime": "^6.23.0",
|
"@types/geojson": "^7946.0.14",
|
||||||
"babel-preset-env": "^1.6.1",
|
"@types/json-to-ast": "^2.1.4",
|
||||||
"babel-preset-es2015": "^6.24.1",
|
"@types/lodash.capitalize": "^4.2.9",
|
||||||
"babel-preset-flow": "^6.23.0",
|
"@types/lodash.clamp": "^4.0.9",
|
||||||
"babel-preset-react": "^6.24.1",
|
"@types/lodash.clonedeep": "^4.5.9",
|
||||||
"babel-register": "^6.26.0",
|
"@types/lodash.get": "^4.4.9",
|
||||||
"babel-runtime": "^6.26.0",
|
"@types/lodash.isequal": "^4.5.8",
|
||||||
"base64-loader": "^1.0.0",
|
"@types/lodash.throttle": "^4.1.9",
|
||||||
"copy-webpack-plugin": "^4.5.1",
|
"@types/mocha": "^10.0.6",
|
||||||
"cors": "^2.8.4",
|
"@types/randomcolor": "^0.5.9",
|
||||||
"cross-env": "^5.1.4",
|
"@types/react": "^18.2.67",
|
||||||
"css-loader": "^0.28.11",
|
"@types/react-aria-menubutton": "^6.2.14",
|
||||||
"eslint": "^4.19.1",
|
"@types/react-aria-modal": "^4.0.10",
|
||||||
"eslint-plugin-react": "^7.4.0",
|
"@types/react-autocomplete": "^1.8.10",
|
||||||
"express": "^4.16.3",
|
"@types/react-collapse": "^5.0.4",
|
||||||
"extract-text-webpack-plugin": "^3.0.2",
|
"@types/react-color": "^3.0.12",
|
||||||
"file-loader": "^1.1.5",
|
"@types/react-dom": "^18.2.22",
|
||||||
"html-webpack-plugin": "^3.2.0",
|
"@types/react-file-reader-input": "^2.0.4",
|
||||||
"is-docker": "^1.1.0",
|
"@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": "^0.4.5",
|
||||||
"istanbul-lib-coverage": "^1.2.0",
|
"istanbul-lib-coverage": "^3.2.2",
|
||||||
"json-loader": "^0.5.7",
|
"mocha": "^10.3.0",
|
||||||
"mkdirp": "^0.5.1",
|
"postcss": "^8.4.38",
|
||||||
"mocha": "^5.1.0",
|
"react-hot-loader": "^4.13.1",
|
||||||
"node-sass": "^4.8.3",
|
"stylelint": "^16.2.1",
|
||||||
"nsp": "^3.1.0",
|
"stylelint-config-recommended-scss": "^14.0.0",
|
||||||
"react-hot-loader": "^3.1.1",
|
"stylelint-scss": "^6.2.1",
|
||||||
"sass-loader": "^7.0.1",
|
"typescript": "^5.4.3",
|
||||||
"selenium-standalone": "^6.14.0",
|
"uuid": "^9.0.1",
|
||||||
"style-loader": "^0.20.3",
|
"vite": "^5.2.6",
|
||||||
"stylelint": "^9.2.0",
|
"vite-plugin-istanbul": "^6.0.0"
|
||||||
"stylelint-config-recommended-scss": "^3.2.0",
|
|
||||||
"stylelint-scss": "^3.0.0",
|
|
||||||
"transform-loader": "^0.2.4",
|
|
||||||
"uglifyjs-webpack-plugin": "^1.2.4",
|
|
||||||
"uuid": "^3.1.0",
|
|
||||||
"wdio-mocha-framework": "^0.5.13",
|
|
||||||
"wdio-phantomjs-service": "^0.2.2",
|
|
||||||
"wdio-selenium-standalone-service": "0.0.10",
|
|
||||||
"wdio-spec-reporter": "^0.1.2",
|
|
||||||
"webdriverio": "^4.12.0",
|
|
||||||
"webpack": "^3.8.1",
|
|
||||||
"webpack-bundle-analyzer": "^2.9.0",
|
|
||||||
"webpack-cleanup-plugin": "^0.5.1",
|
|
||||||
"webpack-dev-server": "^2.9.4"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,329 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import Mousetrap from 'mousetrap'
|
|
||||||
|
|
||||||
import MapboxGlMap from './map/MapboxGlMap'
|
|
||||||
import OpenLayers3Map from './map/OpenLayers3Map'
|
|
||||||
import LayerList from './layers/LayerList'
|
|
||||||
import LayerEditor from './layers/LayerEditor'
|
|
||||||
import Toolbar from './Toolbar'
|
|
||||||
import AppLayout from './AppLayout'
|
|
||||||
import MessagePanel from './MessagePanel'
|
|
||||||
|
|
||||||
import { downloadGlyphsMetadata, downloadSpriteMetadata } from '../libs/metadata'
|
|
||||||
import styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
|
|
||||||
import style from '../libs/style.js'
|
|
||||||
import { initialStyleUrl, loadStyleUrl } from '../libs/urlopen'
|
|
||||||
import { undoMessages, redoMessages } from '../libs/diffmessage'
|
|
||||||
import { loadDefaultStyle, StyleStore } from '../libs/stylestore'
|
|
||||||
import { ApiStyleStore } from '../libs/apistore'
|
|
||||||
import { RevisionStore } from '../libs/revisions'
|
|
||||||
import LayerWatcher from '../libs/layerwatcher'
|
|
||||||
import tokens from '../config/tokens.json'
|
|
||||||
import isEqual from 'lodash.isequal'
|
|
||||||
import Debug from '../libs/debug'
|
|
||||||
|
|
||||||
import MapboxGl from 'mapbox-gl'
|
|
||||||
import mapboxUtil from 'mapbox-gl/src/util/mapbox'
|
|
||||||
|
|
||||||
|
|
||||||
function updateRootSpec(spec, fieldName, newValues) {
|
|
||||||
return {
|
|
||||||
...spec,
|
|
||||||
$root: {
|
|
||||||
...spec.$root,
|
|
||||||
[fieldName]: {
|
|
||||||
...spec.$root[fieldName],
|
|
||||||
values: newValues
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class App extends React.Component {
|
|
||||||
constructor(props) {
|
|
||||||
super(props)
|
|
||||||
this.revisionStore = new RevisionStore()
|
|
||||||
this.styleStore = new ApiStyleStore({
|
|
||||||
onLocalStyleChange: mapStyle => this.onStyleChanged(mapStyle, false)
|
|
||||||
})
|
|
||||||
|
|
||||||
const styleUrl = initialStyleUrl()
|
|
||||||
if(styleUrl) {
|
|
||||||
this.styleStore = new StyleStore()
|
|
||||||
loadStyleUrl(styleUrl, mapStyle => this.onStyleChanged(mapStyle))
|
|
||||||
} else {
|
|
||||||
this.styleStore.init(err => {
|
|
||||||
if(err) {
|
|
||||||
console.log('Falling back to local storage for storing styles')
|
|
||||||
this.styleStore = new StyleStore()
|
|
||||||
}
|
|
||||||
this.styleStore.latestStyle(mapStyle => this.onStyleChanged(mapStyle))
|
|
||||||
|
|
||||||
if(Debug.enabled()) {
|
|
||||||
Debug.set("maputnik", "styleStore", this.styleStore);
|
|
||||||
Debug.set("maputnik", "revisionStore", this.revisionStore);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if(Debug.enabled()) {
|
|
||||||
Debug.set("maputnik", "revisionStore", this.revisionStore);
|
|
||||||
Debug.set("maputnik", "styleStore", this.styleStore);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
errors: [],
|
|
||||||
infos: [],
|
|
||||||
mapStyle: style.emptyStyle,
|
|
||||||
selectedLayerIndex: 0,
|
|
||||||
sources: {},
|
|
||||||
vectorLayers: {},
|
|
||||||
inspectModeEnabled: false,
|
|
||||||
spec: styleSpec.latest,
|
|
||||||
}
|
|
||||||
|
|
||||||
this.layerWatcher = new LayerWatcher({
|
|
||||||
onVectorLayersChange: v => this.setState({ vectorLayers: v })
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
Mousetrap.bind(['mod+z'], this.onUndo.bind(this));
|
|
||||||
Mousetrap.bind(['mod+y', 'mod+shift+z'], this.onRedo.bind(this));
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
Mousetrap.unbind(['mod+z'], this.onUndo.bind(this));
|
|
||||||
Mousetrap.unbind(['mod+y', 'mod+shift+z'], this.onRedo.bind(this));
|
|
||||||
}
|
|
||||||
|
|
||||||
saveStyle(snapshotStyle) {
|
|
||||||
this.styleStore.save(snapshotStyle)
|
|
||||||
}
|
|
||||||
|
|
||||||
updateFonts(urlTemplate) {
|
|
||||||
const metadata = this.state.mapStyle.metadata || {}
|
|
||||||
const accessToken = metadata['maputnik:openmaptiles_access_token'] || tokens.openmaptiles
|
|
||||||
|
|
||||||
let glyphUrl = (typeof urlTemplate === 'string')? urlTemplate.replace('{key}', accessToken): urlTemplate;
|
|
||||||
downloadGlyphsMetadata(glyphUrl, fonts => {
|
|
||||||
this.setState({ spec: updateRootSpec(this.state.spec, 'glyphs', fonts)})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
updateIcons(baseUrl) {
|
|
||||||
downloadSpriteMetadata(baseUrl, icons => {
|
|
||||||
this.setState({ spec: updateRootSpec(this.state.spec, 'sprite', icons)})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
onStyleChanged(newStyle, save=true) {
|
|
||||||
|
|
||||||
const errors = styleSpec.validate(newStyle, styleSpec.latest)
|
|
||||||
if(errors.length === 0) {
|
|
||||||
|
|
||||||
if(newStyle.glyphs !== this.state.mapStyle.glyphs) {
|
|
||||||
this.updateFonts(newStyle.glyphs)
|
|
||||||
}
|
|
||||||
if(newStyle.sprite !== this.state.mapStyle.sprite) {
|
|
||||||
this.updateIcons(newStyle.sprite)
|
|
||||||
}
|
|
||||||
|
|
||||||
this.revisionStore.addRevision(newStyle)
|
|
||||||
if(save) this.saveStyle(newStyle)
|
|
||||||
this.setState({
|
|
||||||
mapStyle: newStyle,
|
|
||||||
errors: [],
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
this.setState({
|
|
||||||
errors: errors.map(err => err.message)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
this.fetchSources();
|
|
||||||
}
|
|
||||||
|
|
||||||
onUndo() {
|
|
||||||
const activeStyle = this.revisionStore.undo()
|
|
||||||
const messages = undoMessages(this.state.mapStyle, activeStyle)
|
|
||||||
this.saveStyle(activeStyle)
|
|
||||||
this.setState({
|
|
||||||
mapStyle: activeStyle,
|
|
||||||
infos: messages,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
onRedo() {
|
|
||||||
const activeStyle = this.revisionStore.redo()
|
|
||||||
const messages = redoMessages(this.state.mapStyle, activeStyle)
|
|
||||||
this.saveStyle(activeStyle)
|
|
||||||
this.setState({
|
|
||||||
mapStyle: activeStyle,
|
|
||||||
infos: messages,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
onLayersChange(changedLayers) {
|
|
||||||
const changedStyle = {
|
|
||||||
...this.state.mapStyle,
|
|
||||||
layers: changedLayers
|
|
||||||
}
|
|
||||||
this.onStyleChanged(changedStyle)
|
|
||||||
}
|
|
||||||
|
|
||||||
onLayerIdChange(oldId, newId) {
|
|
||||||
const changedLayers = this.state.mapStyle.layers.slice(0)
|
|
||||||
const idx = style.indexOfLayer(changedLayers, oldId)
|
|
||||||
|
|
||||||
changedLayers[idx] = {
|
|
||||||
...changedLayers[idx],
|
|
||||||
id: newId
|
|
||||||
}
|
|
||||||
|
|
||||||
this.onLayersChange(changedLayers)
|
|
||||||
}
|
|
||||||
|
|
||||||
onLayerChanged(layer) {
|
|
||||||
const changedLayers = this.state.mapStyle.layers.slice(0)
|
|
||||||
const idx = style.indexOfLayer(changedLayers, layer.id)
|
|
||||||
changedLayers[idx] = layer
|
|
||||||
|
|
||||||
this.onLayersChange(changedLayers)
|
|
||||||
}
|
|
||||||
|
|
||||||
changeInspectMode() {
|
|
||||||
this.setState({
|
|
||||||
inspectModeEnabled: !this.state.inspectModeEnabled
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fetchSources() {
|
|
||||||
const sourceList = {...this.state.sources};
|
|
||||||
|
|
||||||
for(let [key, val] of Object.entries(this.state.mapStyle.sources)) {
|
|
||||||
if(sourceList.hasOwnProperty(key)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
sourceList[key] = {
|
|
||||||
type: val.type,
|
|
||||||
layers: []
|
|
||||||
};
|
|
||||||
|
|
||||||
if(!this.state.sources.hasOwnProperty(key) && val.type === "vector" && val.hasOwnProperty("url")) {
|
|
||||||
let url = val.url;
|
|
||||||
try {
|
|
||||||
url = mapboxUtil.normalizeSourceURL(url, MapboxGl.accessToken);
|
|
||||||
} catch(err) {
|
|
||||||
console.warn("Failed to normalizeSourceURL: ", err);
|
|
||||||
}
|
|
||||||
|
|
||||||
fetch(url)
|
|
||||||
.then((response) => {
|
|
||||||
return response.json();
|
|
||||||
})
|
|
||||||
.then((json) => {
|
|
||||||
if(!json.hasOwnProperty("vector_layers")) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create new objects before setState
|
|
||||||
const sources = Object.assign({}, this.state.sources);
|
|
||||||
|
|
||||||
for(let layer of json.vector_layers) {
|
|
||||||
sources[key].layers.push(layer.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
console.debug("Updating source: "+key);
|
|
||||||
this.setState({
|
|
||||||
sources: sources
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
console.error("Failed to process sources for '%s'", url, err);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!isEqual(this.state.sources, sourceList)) {
|
|
||||||
console.debug("Setting sources");
|
|
||||||
this.setState({
|
|
||||||
sources: sourceList
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mapRenderer() {
|
|
||||||
const mapProps = {
|
|
||||||
mapStyle: style.replaceAccessToken(this.state.mapStyle, {allowFallback: true}),
|
|
||||||
onDataChange: (e) => {
|
|
||||||
this.layerWatcher.analyzeMap(e.map)
|
|
||||||
this.fetchSources();
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
const metadata = this.state.mapStyle.metadata || {}
|
|
||||||
const renderer = metadata['maputnik:renderer'] || 'mbgljs'
|
|
||||||
|
|
||||||
// Check if OL3 code has been loaded?
|
|
||||||
if(renderer === 'ol3') {
|
|
||||||
return <OpenLayers3Map {...mapProps} />
|
|
||||||
} else {
|
|
||||||
return <MapboxGlMap {...mapProps}
|
|
||||||
inspectModeEnabled={this.state.inspectModeEnabled}
|
|
||||||
highlightedLayer={this.state.mapStyle.layers[this.state.selectedLayerIndex]}
|
|
||||||
onLayerSelect={this.onLayerSelect.bind(this)} />
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onLayerSelect(layerId) {
|
|
||||||
const idx = style.indexOfLayer(this.state.mapStyle.layers, layerId)
|
|
||||||
this.setState({ selectedLayerIndex: idx })
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const layers = this.state.mapStyle.layers || []
|
|
||||||
const selectedLayer = layers.length > 0 ? layers[this.state.selectedLayerIndex] : null
|
|
||||||
const metadata = this.state.mapStyle.metadata || {}
|
|
||||||
|
|
||||||
const toolbar = <Toolbar
|
|
||||||
mapStyle={this.state.mapStyle}
|
|
||||||
inspectModeEnabled={this.state.inspectModeEnabled}
|
|
||||||
sources={this.state.sources}
|
|
||||||
onStyleChanged={this.onStyleChanged.bind(this)}
|
|
||||||
onStyleOpen={this.onStyleChanged.bind(this)}
|
|
||||||
onInspectModeToggle={this.changeInspectMode.bind(this)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
const layerList = <LayerList
|
|
||||||
onLayersChange={this.onLayersChange.bind(this)}
|
|
||||||
onLayerSelect={this.onLayerSelect.bind(this)}
|
|
||||||
selectedLayerIndex={this.state.selectedLayerIndex}
|
|
||||||
layers={layers}
|
|
||||||
sources={this.state.sources}
|
|
||||||
/>
|
|
||||||
|
|
||||||
const layerEditor = selectedLayer ? <LayerEditor
|
|
||||||
layer={selectedLayer}
|
|
||||||
sources={this.state.sources}
|
|
||||||
vectorLayers={this.state.vectorLayers}
|
|
||||||
spec={this.state.spec}
|
|
||||||
onLayerChanged={this.onLayerChanged.bind(this)}
|
|
||||||
onLayerIdChange={this.onLayerIdChange.bind(this)}
|
|
||||||
/> : null
|
|
||||||
|
|
||||||
const bottomPanel = (this.state.errors.length + this.state.infos.length) > 0 ? <MessagePanel
|
|
||||||
errors={this.state.errors}
|
|
||||||
infos={this.state.infos}
|
|
||||||
/> : null
|
|
||||||
|
|
||||||
return <AppLayout
|
|
||||||
toolbar={toolbar}
|
|
||||||
layerList={layerList}
|
|
||||||
layerEditor={layerEditor}
|
|
||||||
map={this.mapRenderer()}
|
|
||||||
bottom={bottomPanel}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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,46 +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,
|
|
||||||
}
|
|
||||||
|
|
||||||
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>
|
|
||||||
}
|
|
||||||
</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,25 +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,
|
|
||||||
onClick: PropTypes.func,
|
|
||||||
style: PropTypes.object,
|
|
||||||
className: PropTypes.string,
|
|
||||||
children: PropTypes.node
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return <a
|
|
||||||
onClick={this.props.onClick}
|
|
||||||
className={classnames("maputnik-button", this.props.className)}
|
|
||||||
data-wd-key={this.props["data-wd-key"]}
|
|
||||||
style={this.props.style}>
|
|
||||||
{this.props.children}
|
|
||||||
</a>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
19
src/components/Collapser.tsx
Normal file
19
src/components/Collapser.tsx
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import {MdArrowDropDown, MdArrowDropUp} from 'react-icons/md'
|
||||||
|
|
||||||
|
type CollapserProps = {
|
||||||
|
isCollapsed: boolean
|
||||||
|
style?: object
|
||||||
|
};
|
||||||
|
|
||||||
|
export default class Collapser extends React.Component<CollapserProps> {
|
||||||
|
render() {
|
||||||
|
const iconStyle = {
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
...this.props.style,
|
||||||
|
}
|
||||||
|
return this.props.isCollapsed ? <MdArrowDropUp style={iconStyle}/> : <MdArrowDropDown style={iconStyle} />
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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>
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user