Compare commits
1162 commits
Author | SHA1 | Date | |
---|---|---|---|
4b1ffd489f | |||
![]() |
cd613e2fb5 | ||
![]() |
3e521a7d92 | ||
![]() |
d88fbb7c55 | ||
![]() |
6d4ab40b96 | ||
![]() |
5441a10488 | ||
![]() |
8a7d9957fb | ||
![]() |
6d1c617752 | ||
![]() |
3881219fee | ||
![]() |
3110cab18f | ||
![]() |
f2b48acb61 | ||
![]() |
6e0006ffcf | ||
![]() |
6ef12fba6c | ||
![]() |
3d8bf78974 | ||
![]() |
1d60dd6afc | ||
![]() |
7662cb84ce | ||
![]() |
467203e125 | ||
![]() |
9bb270b6c5 | ||
![]() |
f02c63c94c | ||
![]() |
b0a2cefb0e | ||
![]() |
f7be63a35c | ||
![]() |
407c2eb209 | ||
![]() |
4d11796967 | ||
![]() |
68ce971133 | ||
![]() |
5e1e87ac83 | ||
![]() |
5469ac313e | ||
![]() |
7e53bb8ab4 | ||
![]() |
5ce5fa5283 | ||
![]() |
97be9db6b7 | ||
![]() |
3abbb39633 | ||
![]() |
52549e5c3c | ||
![]() |
77b741986f | ||
![]() |
70f954b308 | ||
![]() |
c30d799811 | ||
![]() |
4a783446cd | ||
![]() |
9d222c1dec | ||
![]() |
a2bc9f0cce | ||
![]() |
93f72c1fe7 | ||
![]() |
760653901e | ||
![]() |
6f28feb947 | ||
![]() |
88ded67053 | ||
![]() |
138c9dcfec | ||
![]() |
6cda7a0b38 | ||
![]() |
88c61f14b4 | ||
![]() |
6a34babc07 | ||
![]() |
474103bd55 | ||
![]() |
5a91a2927c | ||
![]() |
d272e17b55 | ||
![]() |
ed2a815110 | ||
![]() |
098b18e888 | ||
![]() |
574374c53d | ||
![]() |
3745af969c | ||
![]() |
5757859952 | ||
![]() |
5eb98f0bee | ||
![]() |
18b4b0d6fc | ||
![]() |
5bfe8c7d3a | ||
![]() |
44cf365d65 | ||
![]() |
e0be79b09d | ||
![]() |
9f7819daea | ||
![]() |
d506f22fb3 | ||
![]() |
16482c5f1e | ||
![]() |
664afe62f4 | ||
![]() |
00d9189ae5 | ||
![]() |
82099e5029 | ||
![]() |
1a9ce46be2 | ||
![]() |
27b2ddfa1b | ||
![]() |
31e802086e | ||
![]() |
cb757b9b6d | ||
![]() |
a8a95e27da | ||
![]() |
b04953370f | ||
![]() |
e26b7c37c1 | ||
![]() |
8a77948fbe | ||
![]() |
b165cfccaa | ||
![]() |
67b440e74c | ||
![]() |
936b70fa02 | ||
![]() |
afa9bff28e | ||
![]() |
513f4ed94d | ||
![]() |
1f5a58035b | ||
![]() |
0943a74d91 | ||
![]() |
5cb00470c7 | ||
![]() |
f846573395 | ||
![]() |
35a5a6e061 | ||
![]() |
49270d101e | ||
![]() |
33e1f2bc9b | ||
![]() |
0684f355db | ||
![]() |
99de546045 | ||
![]() |
1d4c1ff188 | ||
![]() |
f64f95f80b | ||
![]() |
0382d29011 | ||
![]() |
6bc31a0748 | ||
![]() |
3d9701744e | ||
![]() |
2ff4f098af | ||
![]() |
3caf67ce91 | ||
![]() |
fb171e29a3 | ||
![]() |
585ac508f1 | ||
![]() |
28d49d5df3 | ||
![]() |
fa6620f785 | ||
![]() |
3dce758575 | ||
![]() |
e783526ae1 | ||
![]() |
4b7628d0ab | ||
![]() |
231fef540d | ||
![]() |
d8009ef148 | ||
![]() |
1fd6d31717 | ||
![]() |
64152c3a90 | ||
![]() |
33b9bcb8d0 | ||
![]() |
990a0cc842 | ||
![]() |
920c60243f | ||
![]() |
0365b41029 | ||
![]() |
ee7fe0586f | ||
![]() |
b92d14b469 | ||
![]() |
47ffae898e | ||
![]() |
d8c97edb27 | ||
![]() |
583a488264 | ||
![]() |
649d012eef | ||
![]() |
3961d20a68 | ||
![]() |
b02c2ae534 | ||
![]() |
416d78356e | ||
![]() |
a72f74d3a7 | ||
![]() |
67aa0e1d9a | ||
![]() |
29f287be65 | ||
![]() |
785995429d | ||
![]() |
401f891866 | ||
![]() |
fa988ea28b | ||
![]() |
cc4b8f7954 | ||
![]() |
9a1ba10700 | ||
![]() |
0ae45de6e8 | ||
![]() |
bca7f8424f | ||
![]() |
e0f9aba4b7 | ||
![]() |
765c5f38d3 | ||
![]() |
6e5bf6d439 | ||
![]() |
7e65750a43 | ||
![]() |
693d7468bb | ||
![]() |
6f12edccab | ||
![]() |
fef5e00d4a | ||
![]() |
443cf38b65 | ||
![]() |
f7c922f0e4 | ||
![]() |
20a73693fd | ||
![]() |
b1cc2ff121 | ||
![]() |
2a2437c838 | ||
![]() |
f38d1f8743 | ||
![]() |
1efc87cc9c | ||
![]() |
1d6f9ab37a | ||
![]() |
d7653643c2 | ||
![]() |
fad0ee9fb5 | ||
![]() |
1abc17f94e | ||
![]() |
d53bf2fb79 | ||
![]() |
516546c9c2 | ||
![]() |
20fce6d622 | ||
![]() |
31e4afe5f6 | ||
![]() |
68d2f7174e | ||
![]() |
a3947ac60e | ||
![]() |
11941c9a5b | ||
![]() |
a5e5dd735a | ||
![]() |
8730b48c2d | ||
![]() |
c50c38092b | ||
![]() |
f647d9fa87 | ||
![]() |
b4f988299d | ||
![]() |
7d5c633d39 | ||
![]() |
87515e0b40 | ||
![]() |
aff8020cf4 | ||
![]() |
6742c7f45d | ||
![]() |
71dc40c2ce | ||
![]() |
46f3979201 | ||
![]() |
b6a90bb6e0 | ||
![]() |
d495ba3680 | ||
![]() |
d239cef587 | ||
![]() |
38d2f87658 | ||
![]() |
bff699ba4a | ||
![]() |
366f71ca44 | ||
![]() |
66445e8653 | ||
![]() |
3ef69c904f | ||
![]() |
1a17bb1c8a | ||
![]() |
50df4480ca | ||
![]() |
4da85858b7 | ||
![]() |
df67488e6b | ||
![]() |
d84f0f06eb | ||
![]() |
56cca70c1d | ||
![]() |
d4460215b7 | ||
![]() |
f5cf29470e | ||
![]() |
eb9da9f6bb | ||
![]() |
add801bfb1 | ||
![]() |
eed9f0abed | ||
![]() |
a9d6a9a852 | ||
![]() |
c7daf20cef | ||
![]() |
d7e2e8adb2 | ||
![]() |
9fe824d7fd | ||
![]() |
dbc7d21c45 | ||
![]() |
6c22d673c9 | ||
![]() |
f6a9b63fe6 | ||
![]() |
500a81e8b3 | ||
![]() |
e6b38e4575 | ||
![]() |
5c403bbd63 | ||
![]() |
9204d314bd | ||
![]() |
3f98005fd2 | ||
![]() |
f8effb103d | ||
![]() |
6b687407dd | ||
![]() |
0acc5c9723 | ||
![]() |
aadb03a5ab | ||
![]() |
87e5b72ca4 | ||
![]() |
ebad6f9aeb | ||
![]() |
46d8418bc3 | ||
![]() |
30ac5279da | ||
![]() |
902c82606c | ||
![]() |
1e695bf7fe | ||
![]() |
fef29c8b10 | ||
![]() |
f4cedd171f | ||
![]() |
1695066133 | ||
![]() |
33567286ff | ||
![]() |
96952be0b3 | ||
![]() |
7a758b2bcd | ||
![]() |
2f17520437 | ||
![]() |
ffe4363f9c | ||
![]() |
d5a9ce11d8 | ||
![]() |
281bd03971 | ||
![]() |
58e9fc6597 | ||
![]() |
b2e9ac9de2 | ||
![]() |
946d58a10a | ||
![]() |
6aa7a80b4d | ||
![]() |
43ca0ca4ba | ||
![]() |
1aff9a5695 | ||
![]() |
d2fd737149 | ||
![]() |
0fb5af61f6 | ||
![]() |
300925d432 | ||
![]() |
e52dab78a6 | ||
![]() |
fe111ed18f | ||
![]() |
c34c5786bd | ||
![]() |
7cd8e76dd5 | ||
![]() |
80c7019816 | ||
![]() |
4a999e06a3 | ||
![]() |
41974cf613 | ||
![]() |
bf3b1fb24b | ||
![]() |
a727b04617 | ||
![]() |
fe8d2dd82c | ||
![]() |
26d8066cdc | ||
![]() |
e1738cc107 | ||
![]() |
9117012cfe | ||
![]() |
e0ac03ea9f | ||
![]() |
d68ab38442 | ||
![]() |
de57db400a | ||
![]() |
cbf3a41803 | ||
![]() |
c68b147da6 | ||
![]() |
10fadc5d3c | ||
![]() |
c146df5e85 | ||
![]() |
b8f620b174 | ||
![]() |
97c4412d94 | ||
![]() |
dd23233e1d | ||
![]() |
881aa5b401 | ||
![]() |
8ef5ee9974 | ||
![]() |
3533918715 | ||
![]() |
fcd85d99b9 | ||
![]() |
19b469db2a | ||
![]() |
ddfbde1212 | ||
![]() |
5ba0c4c68c | ||
![]() |
dfe69da98b | ||
![]() |
7c16a1b0fa | ||
![]() |
52bcc697c2 | ||
![]() |
2a94190bd1 | ||
![]() |
37d2d8fc41 | ||
![]() |
2f0501dec3 | ||
![]() |
6cd7e5806d | ||
![]() |
dd4c691f79 | ||
![]() |
7457e21b48 | ||
![]() |
d0d3c01ef4 | ||
![]() |
00d40b614c | ||
![]() |
13a424df05 | ||
![]() |
5a8644091d | ||
![]() |
50a22280a3 | ||
![]() |
442ef54e34 | ||
![]() |
e46e06359a | ||
![]() |
6deb7a2d92 | ||
![]() |
3801e5eb9f | ||
![]() |
6372fdaeb5 | ||
![]() |
9d009c9c6d | ||
![]() |
318ad335e6 | ||
![]() |
8362ac9307 | ||
![]() |
f246fc6f51 | ||
![]() |
19f230eef0 | ||
![]() |
aa97a52bc3 | ||
![]() |
ec5714a1e3 | ||
![]() |
c4fbf14d2b | ||
![]() |
d2ad9b68a9 | ||
![]() |
a9a5528068 | ||
![]() |
d9bb7a1fe7 | ||
![]() |
b8f1a9ed83 | ||
![]() |
5315f10186 | ||
![]() |
64feb48cde | ||
![]() |
70cbbaabaa | ||
![]() |
4a2ba11798 | ||
![]() |
0c6c5a591b | ||
![]() |
b4afa86cae | ||
![]() |
19e7ab81c1 | ||
![]() |
037752899c | ||
![]() |
254aafee4c | ||
![]() |
967ee0d988 | ||
![]() |
88efeb2fd0 | ||
![]() |
3c228f4562 | ||
![]() |
f0181c53de | ||
![]() |
75c6c4cc6f | ||
![]() |
ae2dc3c893 | ||
![]() |
da0a96708e | ||
![]() |
40ade063f9 | ||
![]() |
ee602acc4c | ||
![]() |
407f6b2e30 | ||
![]() |
c9aa26a6de | ||
![]() |
b25a6420dd | ||
![]() |
36d3e9d356 | ||
![]() |
9a4ef3dedf | ||
![]() |
f69a2339b8 | ||
![]() |
d9c2128ddf | ||
![]() |
345b831edd | ||
![]() |
526766c8f4 | ||
![]() |
cef150431b | ||
![]() |
7d06f216a8 | ||
![]() |
fba007b60b | ||
![]() |
d7af5235bf | ||
![]() |
29e2379764 | ||
![]() |
94877fde58 | ||
![]() |
a6e7eec140 | ||
![]() |
62e00dbbb7 | ||
![]() |
0b5104ef9c | ||
![]() |
74dba3970e | ||
![]() |
36f9320d50 | ||
![]() |
4600fbe93a | ||
![]() |
7b9f496c5c | ||
![]() |
36f63de686 | ||
![]() |
8045a336f4 | ||
![]() |
a0a7ff9b58 | ||
![]() |
1725eb6014 | ||
![]() |
b4697ecf00 | ||
![]() |
62b9c93640 | ||
![]() |
d3b981fdec | ||
![]() |
a01e3ff769 | ||
![]() |
82cbe74790 | ||
![]() |
0f2c9e7d7c | ||
![]() |
078db09c1b | ||
![]() |
85157ea037 | ||
![]() |
56107da182 | ||
![]() |
c902f15329 | ||
![]() |
278ae074e3 | ||
![]() |
e4420157a3 | ||
![]() |
e65c27d531 | ||
![]() |
29cf78af30 | ||
![]() |
8b7bf4edb0 | ||
![]() |
404acbf05e | ||
![]() |
6f4cdc4227 | ||
![]() |
b87ae5cb80 | ||
![]() |
3f20943134 | ||
![]() |
5761da6228 | ||
![]() |
7d2ad069bf | ||
![]() |
f428ddeea5 | ||
![]() |
410ecdc1cc | ||
![]() |
4bcf6994f3 | ||
![]() |
64e346169d | ||
![]() |
aa17fc97ad | ||
![]() |
d6d3fbec6d | ||
![]() |
36f4b495cd | ||
![]() |
bcabac3a51 | ||
![]() |
87565edc68 | ||
![]() |
8988094d84 | ||
![]() |
a633cc8bb3 | ||
![]() |
f21d37a4dd | ||
![]() |
1e01690df9 | ||
![]() |
a20dba2139 | ||
![]() |
5092298d67 | ||
![]() |
4568027046 | ||
![]() |
471dd15e77 | ||
![]() |
456b5056d3 | ||
![]() |
c161667e0e | ||
![]() |
2a441a0039 | ||
![]() |
55b747db52 | ||
![]() |
ec7a1c8a1c | ||
![]() |
d33584d599 | ||
![]() |
9a9ddb788f | ||
![]() |
883a449a9e | ||
![]() |
a6dadfda28 | ||
![]() |
7d8a6ad338 | ||
![]() |
e8f64e2861 | ||
![]() |
3781054f2d | ||
![]() |
9d5684513e | ||
![]() |
9348350ba3 | ||
![]() |
d4a5cc6074 | ||
![]() |
816ed29a0f | ||
![]() |
592beaab29 | ||
![]() |
5e666758d6 | ||
![]() |
1b97491739 | ||
![]() |
55c21459f2 | ||
![]() |
75d439fe85 | ||
![]() |
18f76d3b2d | ||
![]() |
e71710d850 | ||
![]() |
4ad9cbc645 | ||
![]() |
5add97b9c0 | ||
![]() |
0a8c98681c | ||
![]() |
bd997ff135 | ||
![]() |
02ea9f2dbd | ||
![]() |
7e246d0cd1 | ||
![]() |
d759dd2952 | ||
![]() |
c13e5e6821 | ||
![]() |
775ea74a2a | ||
![]() |
02ee629f30 | ||
![]() |
ad185479f2 | ||
![]() |
254e2a6f54 | ||
![]() |
a219afbb2c | ||
![]() |
d75e949f8b | ||
![]() |
51d1a67925 | ||
![]() |
2de369db26 | ||
![]() |
f14c08524f | ||
![]() |
203035aeb1 | ||
![]() |
92d539b518 | ||
![]() |
ff9417c9cb | ||
![]() |
20369963c5 | ||
![]() |
d0af7f9b00 | ||
![]() |
123d1d608b | ||
![]() |
86d21f6244 | ||
![]() |
44fa77a79d | ||
![]() |
d4e8078aca | ||
![]() |
1920c48b56 | ||
![]() |
5abb734d6c | ||
![]() |
9129a62245 | ||
![]() |
b0af3aee27 | ||
![]() |
0119db6eb9 | ||
![]() |
fb1d98c901 | ||
![]() |
ca5a824b7d | ||
![]() |
2e59f19072 | ||
![]() |
857c8c339a | ||
![]() |
27c97daa7a | ||
![]() |
e485979ab9 | ||
![]() |
2238e2e340 | ||
![]() |
394dcf1f3e | ||
![]() |
37d4f449a9 | ||
![]() |
c3f050a7af | ||
![]() |
0923a97de6 | ||
![]() |
f182193236 | ||
![]() |
887eb8ad97 | ||
![]() |
28e53ce197 | ||
![]() |
42d2e3b736 | ||
![]() |
c5b688a463 | ||
![]() |
a7c6d5bde4 | ||
![]() |
bde8f75894 | ||
![]() |
20c149a1ea | ||
![]() |
de60805921 | ||
![]() |
f7336a7582 | ||
![]() |
a7e5d0b599 | ||
![]() |
873cf366a3 | ||
![]() |
fac07d78b1 | ||
![]() |
424f8dbdc9 | ||
![]() |
842e01fac7 | ||
![]() |
13eeeaffad | ||
![]() |
ca86d3e1a6 | ||
![]() |
1f4474b45a | ||
![]() |
dfed68728b | ||
![]() |
9666aa51f1 | ||
![]() |
642e367347 | ||
![]() |
c900f592a8 | ||
![]() |
b3a5b0d4a7 | ||
![]() |
1befca7ee4 | ||
![]() |
1fa8c53476 | ||
![]() |
8340d6e41f | ||
![]() |
f7bfff1343 | ||
![]() |
17dc7dd93b | ||
![]() |
2c5385824d | ||
![]() |
42cf242904 | ||
![]() |
d0cad1c7d0 | ||
![]() |
320d2ce6cd | ||
![]() |
ef33dd9137 | ||
![]() |
76c40ce10a | ||
![]() |
0f66c56d97 | ||
![]() |
ea1bd406d0 | ||
![]() |
bf5e778f7d | ||
![]() |
73678978ae | ||
![]() |
87fa90cb40 | ||
![]() |
38de5607a7 | ||
![]() |
451fd476a6 | ||
![]() |
cce1454448 | ||
![]() |
33be3f529a | ||
![]() |
4e90b8bdd7 | ||
![]() |
c4590bf2df | ||
![]() |
0e13eb4695 | ||
![]() |
5dab4904f9 | ||
![]() |
ee7228f9d8 | ||
![]() |
4268b73aff | ||
![]() |
941c558d74 | ||
![]() |
d37852a97f | ||
![]() |
659c14e351 | ||
![]() |
e97b6db47e | ||
![]() |
21a9ef1576 | ||
![]() |
b761eab63c | ||
![]() |
53c7a31781 | ||
![]() |
0d669c4980 | ||
![]() |
d499d396c9 | ||
![]() |
adcfa2f734 | ||
![]() |
b6497deee0 | ||
![]() |
05a5b89344 | ||
![]() |
b37bc27bff | ||
![]() |
2cfb67b0d8 | ||
![]() |
ad017b98d6 | ||
![]() |
3a30ea4251 | ||
![]() |
457a8bc45a | ||
![]() |
4cb029f377 | ||
![]() |
492bed144b | ||
![]() |
05a720fee7 | ||
![]() |
0ebce4c91b | ||
![]() |
4c57a736a6 | ||
![]() |
bb7de7b4e4 | ||
![]() |
39d1f8ae31 | ||
![]() |
a957221958 | ||
![]() |
4b468001b5 | ||
![]() |
978574e1d1 | ||
![]() |
8462044906 | ||
![]() |
8962bae5fc | ||
![]() |
cb01b4df82 | ||
![]() |
eb37c2f416 | ||
![]() |
d4ff0473b8 | ||
![]() |
8639cfda60 | ||
![]() |
7ec4856d44 | ||
![]() |
3c39dbab0e | ||
![]() |
74610e098e | ||
![]() |
25c3d625df | ||
![]() |
434939a84f | ||
![]() |
f883756f0c | ||
![]() |
a82118d0bf | ||
![]() |
74ca3ed7e0 | ||
![]() |
32e5f8ce75 | ||
![]() |
8e37598313 | ||
![]() |
6afa0c96a5 | ||
![]() |
3ae7daa6c9 | ||
![]() |
0d6ba22814 | ||
![]() |
259f4387a2 | ||
![]() |
89c6eb7ddf | ||
![]() |
ea0bb2ed6b | ||
![]() |
1a76f374a9 | ||
![]() |
c1ae654737 | ||
![]() |
2d2f114998 | ||
![]() |
c25d16365f | ||
![]() |
9dfc9ff06f | ||
![]() |
6eef3d659c | ||
![]() |
1a56f9c9fc | ||
![]() |
a88f809112 | ||
![]() |
6884dc932b | ||
![]() |
0251a9ade1 | ||
![]() |
b7e3a41090 | ||
![]() |
78a88c1857 | ||
![]() |
5aaa43a70c | ||
![]() |
506b55f9e9 | ||
![]() |
cf85989707 | ||
![]() |
8cf663315c | ||
![]() |
121e335071 | ||
![]() |
94d5bf28c8 | ||
![]() |
ec44840e9c | ||
![]() |
ffe541ef73 | ||
![]() |
c593f4eacf | ||
![]() |
fd0c90047b | ||
![]() |
47856340ed | ||
![]() |
cc9114f519 | ||
![]() |
3aa960ef23 | ||
![]() |
9fa0ba537a | ||
![]() |
415d14e34b | ||
![]() |
64adff0855 | ||
![]() |
b4d06196da | ||
![]() |
e0432b59d0 | ||
![]() |
f13e3fe2a0 | ||
![]() |
fd3034debe | ||
![]() |
7d0d03abb4 | ||
![]() |
a98bbe3c6d | ||
![]() |
989944070f | ||
![]() |
3138f2dafb | ||
![]() |
93344376b7 | ||
![]() |
c2bb67ca1c | ||
![]() |
c35b14d58d | ||
![]() |
12e8cf5c3d | ||
![]() |
2bc7482743 | ||
![]() |
45a0fc48b2 | ||
![]() |
93048196fc | ||
![]() |
dfd0b24afe | ||
![]() |
f242cc1760 | ||
![]() |
acde7b0a30 | ||
![]() |
49f342f877 | ||
![]() |
007551d556 | ||
![]() |
d7ca2adebf | ||
![]() |
b3e707b108 | ||
![]() |
dc4fa1261c | ||
![]() |
bd3a5aad49 | ||
![]() |
3dde743bde | ||
![]() |
25fd0e293b | ||
![]() |
f7a6ddd842 | ||
![]() |
faa4fdc6ee | ||
![]() |
03c919bd58 | ||
![]() |
05c99ec105 | ||
![]() |
a3f0ba9528 | ||
![]() |
db41a619f7 | ||
![]() |
05b17635b1 | ||
![]() |
3dede3bfdc | ||
![]() |
cb70516f90 | ||
![]() |
2616953c0b | ||
![]() |
1a078c10b5 | ||
![]() |
ea186d34df | ||
![]() |
00a536f9fb | ||
![]() |
0e8f9696fc | ||
![]() |
3092ef74f3 | ||
![]() |
eab8280303 | ||
![]() |
faaf1fc46c | ||
![]() |
172cbd6203 | ||
![]() |
e3d212ec90 | ||
![]() |
2996d986e8 | ||
![]() |
d21abf6cfd | ||
![]() |
0519ee7535 | ||
![]() |
a97139693c | ||
![]() |
af51d684f2 | ||
![]() |
0880655f58 | ||
![]() |
ad0d80d93a | ||
![]() |
757cabc1ed | ||
![]() |
f94625ab05 | ||
![]() |
20c8122da3 | ||
![]() |
78c17773db | ||
![]() |
35902b9daf | ||
![]() |
a13edf6c08 | ||
![]() |
2117b396b7 | ||
![]() |
0fb677cca4 | ||
![]() |
8807e48a6b | ||
![]() |
c41fabf534 | ||
![]() |
ab77f16649 | ||
![]() |
4df60fecb4 | ||
![]() |
62a6d6243d | ||
![]() |
d0ead3561a | ||
![]() |
edef41ac50 | ||
![]() |
70d9188e8c | ||
![]() |
bb0cd60e64 | ||
![]() |
646f67e987 | ||
![]() |
4cb3bb84c9 | ||
![]() |
6fdfcc00b3 | ||
![]() |
a9611bf25d | ||
![]() |
73fe12e597 | ||
![]() |
ac74d42071 | ||
![]() |
ab4f0ef4a1 | ||
![]() |
687bbe1606 | ||
![]() |
a890382120 | ||
![]() |
ba70b623a9 | ||
![]() |
2d145168bf | ||
![]() |
97541435bc | ||
![]() |
81fcd5d242 | ||
![]() |
138ac04676 | ||
![]() |
b9df1d29d1 | ||
![]() |
a01df53c4d | ||
![]() |
8ed68ce973 | ||
![]() |
953af22405 | ||
![]() |
5fd9569385 | ||
![]() |
997117f0c0 | ||
![]() |
fe782a73f1 | ||
![]() |
29ab68f5e1 | ||
![]() |
785662f36c | ||
![]() |
a42eec5b43 | ||
![]() |
dc9c5564c7 | ||
![]() |
b4a819942e | ||
![]() |
19d96832b9 | ||
![]() |
4ea5402f5f | ||
![]() |
347993a332 | ||
![]() |
3525f8c177 | ||
![]() |
2a909d57af | ||
![]() |
0bac307937 | ||
![]() |
582954378c | ||
![]() |
e345ebe43a | ||
![]() |
63701471d4 | ||
![]() |
0d506b7ee9 | ||
![]() |
76a89ae8e4 | ||
![]() |
53d65d211a | ||
![]() |
da4a1af51e | ||
![]() |
12974b0553 | ||
![]() |
816e7eba6a | ||
![]() |
2349c1a3aa | ||
![]() |
11f807fb45 | ||
![]() |
d2e7ef4e2c | ||
![]() |
07f3a37e65 | ||
![]() |
52633b1c86 | ||
![]() |
b089b104c9 | ||
![]() |
540d9fb71c | ||
![]() |
08361cee58 | ||
![]() |
e01ba4d7ce | ||
![]() |
e1abfa0758 | ||
![]() |
bb8386478b | ||
![]() |
e5cc8e3845 | ||
![]() |
9cc7e3a45e | ||
![]() |
6e7c3eb3b3 | ||
![]() |
ac3b2f3dce | ||
![]() |
a7af45ee3f | ||
![]() |
c134795b81 | ||
![]() |
0346d5c304 | ||
![]() |
9b64093c42 | ||
![]() |
50201f0a99 | ||
![]() |
d97c217e62 | ||
![]() |
145ea15be3 | ||
![]() |
1243405807 | ||
![]() |
212af8145c | ||
![]() |
b6e2d101eb | ||
![]() |
f3f63498a8 | ||
![]() |
f8a0ab6d3c | ||
![]() |
38604c0a45 | ||
![]() |
7f6d6bc994 | ||
![]() |
b2bd5eaa96 | ||
![]() |
7cfcc413c4 | ||
![]() |
edd36dd251 | ||
![]() |
a5459fb3ba | ||
![]() |
44372784e1 | ||
![]() |
161ac5ff84 | ||
![]() |
9af3bff4f7 | ||
![]() |
031b4cb39a | ||
![]() |
6a8ced5d9d | ||
![]() |
70acddb40c | ||
![]() |
788d8328c4 | ||
![]() |
7ef9730a9c | ||
![]() |
2796b1514d | ||
![]() |
8584a117fe | ||
![]() |
48b4c33bed | ||
![]() |
5a4268fcc1 | ||
![]() |
c1e9dc76ea | ||
![]() |
80d3699383 | ||
![]() |
49fae1e739 | ||
![]() |
208611f630 | ||
![]() |
97f1801923 | ||
![]() |
235b2b7fcd | ||
![]() |
9d2e71fb2e | ||
![]() |
aba436ea98 | ||
![]() |
3136ccd83a | ||
![]() |
efb4b2c915 | ||
![]() |
b4cd42b186 | ||
![]() |
b87a256e20 | ||
![]() |
2362137667 | ||
![]() |
58222b4c7b | ||
![]() |
ba5e8c4392 | ||
![]() |
abe5cd6401 | ||
![]() |
0c6509d959 | ||
![]() |
5be1072fd6 | ||
![]() |
77e23e3283 | ||
![]() |
6c6dd17df6 | ||
![]() |
ef9e11cb25 | ||
![]() |
398a6edbcd | ||
![]() |
773902bbe9 | ||
![]() |
72fdf84292 | ||
![]() |
082243bda9 | ||
![]() |
d951c8be6b | ||
![]() |
edd7c7dd87 | ||
![]() |
a766dfd233 | ||
![]() |
aba60f0c6a | ||
![]() |
6027d89623 | ||
![]() |
8680a8006a | ||
![]() |
09ded526ef | ||
![]() |
c89a5ae029 | ||
![]() |
a3d7f8bcbd | ||
![]() |
7c1420982c | ||
![]() |
51baa9b67b | ||
![]() |
6b3f557b1f | ||
![]() |
8d2ddd8f95 | ||
![]() |
9f59f67087 | ||
![]() |
1d21648969 | ||
![]() |
16de5be673 | ||
![]() |
6b96f224ae | ||
![]() |
6ff4cae9b9 | ||
![]() |
644db6cd8a | ||
![]() |
a98404e921 | ||
![]() |
559cfc462c | ||
![]() |
559c3a913e | ||
![]() |
4036d528ec | ||
![]() |
6ae4116ccb | ||
![]() |
39bb7ffbf1 | ||
![]() |
e249a3f67d | ||
![]() |
04b49d8a98 | ||
![]() |
309d481117 | ||
![]() |
7ce4805cdd | ||
![]() |
5377414710 | ||
![]() |
042b8b986a | ||
![]() |
038bfe29d6 | ||
![]() |
f8563e1f2b | ||
![]() |
10431d70d0 | ||
![]() |
a5a8ae1e95 | ||
![]() |
3c411cd1ac | ||
![]() |
79bd70942b | ||
![]() |
5585f49396 | ||
![]() |
d5a079d8f4 | ||
![]() |
29d3e72dd3 | ||
![]() |
c3bff5ac5f | ||
![]() |
e16de39b93 | ||
![]() |
13415fd29f | ||
![]() |
9a9b920455 | ||
![]() |
b55b8adb63 | ||
![]() |
9e12ee6f8c | ||
![]() |
0e85e0058f | ||
![]() |
3a94a8f9d2 | ||
![]() |
28dbc78264 | ||
![]() |
8f77be3037 | ||
![]() |
f2dc13e298 | ||
![]() |
cf0eedb379 | ||
![]() |
95bb59dcfe | ||
![]() |
224a1300df | ||
![]() |
9fd381640e | ||
![]() |
b7c384f1ee | ||
![]() |
8126b31081 | ||
![]() |
caa641c550 | ||
![]() |
d02ce0663b | ||
![]() |
ea89d11021 | ||
![]() |
aa933e5154 | ||
![]() |
298d09845d | ||
![]() |
d7a34f3a74 | ||
![]() |
cb700181d3 | ||
![]() |
226b979592 | ||
![]() |
13ad268b43 | ||
![]() |
e2387d164b | ||
![]() |
515c295898 | ||
![]() |
cdc7803ad8 | ||
![]() |
b839979351 | ||
![]() |
0a7c403f0b | ||
![]() |
b3d810817d | ||
![]() |
96161fc656 | ||
![]() |
58c769b448 | ||
![]() |
bdd0a5c868 | ||
![]() |
5048388d1f | ||
![]() |
4c7a227e11 | ||
![]() |
7f8be27844 | ||
![]() |
736e8d393a | ||
![]() |
1c24d12b0d | ||
![]() |
f77ccd06af | ||
![]() |
4996848bdc | ||
![]() |
aa7ae575d0 | ||
![]() |
9603703908 | ||
![]() |
1445c545b0 | ||
![]() |
17a73b1d4a | ||
![]() |
e44104254e | ||
![]() |
58b536036f | ||
![]() |
a68e095400 | ||
![]() |
53b28b35f1 | ||
![]() |
58f92ae947 | ||
![]() |
e506014763 | ||
![]() |
7c5e7e94e9 | ||
![]() |
e6747ebb78 | ||
![]() |
2a87ad19c9 | ||
![]() |
ba62f0bf30 | ||
![]() |
08369a666d | ||
![]() |
f771d41788 | ||
![]() |
9dda95ee8e | ||
![]() |
63483a3155 | ||
![]() |
bca5191ad9 | ||
![]() |
245d9765a9 | ||
![]() |
e8134dfeb0 | ||
![]() |
a1a8996d3f | ||
![]() |
bea528ab64 | ||
![]() |
cde0233130 | ||
![]() |
c99a1a5425 | ||
![]() |
243bdbdaa0 | ||
![]() |
0631a76a51 | ||
![]() |
8e1071aad0 | ||
![]() |
e283569897 | ||
![]() |
507da8567e | ||
![]() |
9cd6feb56d | ||
![]() |
059b0f11a0 | ||
![]() |
7acbc93ba9 | ||
![]() |
bc3d79bfdc | ||
![]() |
58168eb0a5 | ||
![]() |
f993b01387 | ||
![]() |
782c28cd64 | ||
![]() |
76a0064f0c | ||
![]() |
55fa8bf8c1 | ||
![]() |
0c42fdd6b3 | ||
![]() |
e8b3c02546 | ||
![]() |
56089c0a62 | ||
![]() |
857ff2d8d7 | ||
![]() |
e310fa5165 | ||
![]() |
71fab2a2a1 | ||
![]() |
1c59e3d742 | ||
![]() |
6e24f6ec8e | ||
![]() |
5d5ed1153f | ||
![]() |
baf01a117b | ||
![]() |
2cc2cc1b3b | ||
![]() |
45ed8d90ca | ||
![]() |
8e66736955 | ||
![]() |
72ea5ab05d | ||
![]() |
9d642fa0b3 | ||
![]() |
1d734f9b42 | ||
![]() |
80146ed7d9 | ||
![]() |
a28df7ef8f | ||
![]() |
c333197a7c | ||
![]() |
45df72df51 | ||
![]() |
e737753891 | ||
![]() |
edff8ce06c | ||
![]() |
df05ce3f2a | ||
![]() |
a74ca20375 | ||
![]() |
1c890b0157 | ||
![]() |
7a9f04a024 | ||
![]() |
1354c7f663 | ||
![]() |
da3cb1e7f5 | ||
![]() |
19e821a8fe | ||
![]() |
659289d85c | ||
![]() |
734d1f01f0 | ||
![]() |
c1055a9647 | ||
![]() |
2afb460191 | ||
![]() |
8eb736b821 | ||
![]() |
83e20b7a4e | ||
![]() |
81f65af3a8 | ||
![]() |
f5ea790878 | ||
![]() |
a0eb5800fd | ||
![]() |
27eb7f0ed8 | ||
![]() |
2f9059d09e | ||
![]() |
e11c8f9315 | ||
![]() |
650718e0f6 | ||
![]() |
5ed632c229 | ||
![]() |
f545076986 | ||
![]() |
3d48485475 | ||
![]() |
c060dedf20 | ||
![]() |
cd1f5fd04a | ||
![]() |
dc6be5047c | ||
![]() |
47ff33166d | ||
![]() |
875521c5a8 | ||
![]() |
82f179b07c | ||
![]() |
51d6ca0880 | ||
![]() |
89878015bb | ||
![]() |
ac948b6dee | ||
![]() |
440775dbbf | ||
![]() |
ea408f8ec9 | ||
![]() |
da646868c3 | ||
![]() |
d60996e3c6 | ||
![]() |
02cf45bbd9 | ||
![]() |
d9f8582279 | ||
![]() |
54f11a2125 | ||
![]() |
598c8c590b | ||
![]() |
438a18eec1 | ||
![]() |
7bdb7afcb9 | ||
![]() |
2e46700cd9 | ||
![]() |
bb09f3df64 | ||
![]() |
42f24c2c99 | ||
![]() |
427a0f0687 | ||
![]() |
da5ea4b426 | ||
![]() |
2208ff1e24 | ||
![]() |
cf521058be | ||
![]() |
a9b38022bb | ||
![]() |
654bdda629 | ||
![]() |
537313840e | ||
![]() |
d30f8464b2 | ||
![]() |
698c527e94 | ||
![]() |
8007f1386c | ||
![]() |
4f2fdf602b | ||
![]() |
6d7397647a | ||
![]() |
8fd7a9b42b | ||
![]() |
95470143b6 | ||
![]() |
de83021c3d | ||
![]() |
7de3d8b9c7 | ||
![]() |
bdc3d20524 | ||
![]() |
5d93b1d4f9 | ||
![]() |
d30027e992 | ||
![]() |
c233d23523 | ||
![]() |
1109c77ec2 | ||
![]() |
88cf9b37a9 | ||
![]() |
2d207f792b | ||
![]() |
d67a57861d | ||
![]() |
27e9dbfb4e | ||
![]() |
a199008fa3 | ||
![]() |
e88b786073 | ||
![]() |
c03b0a12f8 | ||
![]() |
a234041cd1 | ||
![]() |
49a779970e | ||
![]() |
9545c2594e | ||
![]() |
6c23d95feb | ||
![]() |
366380395e | ||
![]() |
8ea665297f | ||
![]() |
f6580c0342 | ||
![]() |
34a139040c | ||
![]() |
66bea8a42b | ||
![]() |
28790fda30 | ||
![]() |
0b16af0084 | ||
![]() |
e1654a51de | ||
![]() |
5372bab6c5 | ||
![]() |
6a960e8593 | ||
![]() |
1577cfa54a | ||
![]() |
640038a115 | ||
![]() |
49a8562441 | ||
![]() |
1e402ed207 | ||
![]() |
f8949c1aa9 | ||
![]() |
7b952ee5c0 | ||
![]() |
0673c8990a | ||
![]() |
37386bfb29 | ||
![]() |
b93bc5fadc | ||
![]() |
ca7a6ac515 | ||
![]() |
9cc3f9df6a | ||
![]() |
e6ebb918fb | ||
![]() |
ba0f441490 | ||
![]() |
e3ac9fbb4e | ||
![]() |
2f2e4b8f42 | ||
![]() |
bfadeb799b | ||
![]() |
2da1bac6c7 | ||
![]() |
6f5a7ed704 | ||
![]() |
3722c653f1 | ||
![]() |
6231f9f7a7 | ||
![]() |
1079ece860 | ||
![]() |
841cb82f85 | ||
![]() |
5c1d396ed2 | ||
![]() |
4992bc4194 | ||
![]() |
f14d8b1e57 | ||
![]() |
14350a4338 | ||
![]() |
01fbaad962 | ||
![]() |
26e6d56d9b | ||
![]() |
66e3a3da1b | ||
![]() |
63482a2a39 | ||
![]() |
58ccc5477c | ||
![]() |
bbbb38d70f | ||
![]() |
2ef0540478 | ||
![]() |
85692e9659 | ||
![]() |
b66ae40a1e | ||
![]() |
88022f9297 | ||
![]() |
b127dacc0d | ||
![]() |
c162a6647d | ||
![]() |
b48f7edc1d | ||
![]() |
6d68c93871 | ||
![]() |
1fef895e67 | ||
![]() |
920b6c19b6 | ||
![]() |
7f280feeb1 | ||
![]() |
1aec8f386e | ||
![]() |
d05606de4d | ||
![]() |
b9930dd195 | ||
![]() |
4082828790 | ||
![]() |
473e7b6f4a | ||
![]() |
3d993458b5 | ||
![]() |
b2dcf97d9a | ||
![]() |
d4ddbec45d | ||
![]() |
d32e46ba2f | ||
![]() |
0ec992eb21 | ||
![]() |
7a55ac9ebb | ||
![]() |
78fcb893f7 | ||
![]() |
19dcd52cc8 | ||
![]() |
557f83121c | ||
![]() |
2d29a21596 | ||
![]() |
945e568b2b | ||
![]() |
e1a996a267 | ||
![]() |
bdfa92532e | ||
![]() |
e14793c54b | ||
![]() |
37db733e02 | ||
![]() |
1c401f41a2 | ||
![]() |
0018ed9524 | ||
![]() |
dfb07b8286 | ||
![]() |
c1de2a7903 | ||
![]() |
509d32da68 | ||
![]() |
bbc14abb4a | ||
![]() |
442baee1ce | ||
![]() |
0e6c9bfeb0 | ||
![]() |
d3a685e51c | ||
![]() |
99af9eae23 | ||
![]() |
d330a9743c | ||
![]() |
becb46ab80 | ||
![]() |
ef3f34f778 | ||
![]() |
1aaabd2dff | ||
![]() |
bcac161a25 | ||
![]() |
9d362a5b5e | ||
![]() |
21883f490f | ||
![]() |
70515947ca | ||
![]() |
63090802ae | ||
![]() |
131b5e2f81 | ||
![]() |
6e085af7cc | ||
![]() |
345d96d5e6 | ||
![]() |
f1c835c21d | ||
![]() |
15ed6d74bf | ||
![]() |
a3d8240aac | ||
![]() |
098f057e55 | ||
![]() |
18ba6f5059 | ||
![]() |
820dbdd3dd | ||
![]() |
acd7683f18 | ||
![]() |
09859c10c1 | ||
![]() |
f201deecdc | ||
![]() |
b49521d2ed | ||
![]() |
30c9bc8979 | ||
![]() |
69340bbc83 | ||
![]() |
ff9d5a8e5d | ||
![]() |
23c2aa54d0 | ||
![]() |
a8fd1b38b7 | ||
![]() |
7c28bf2b21 | ||
![]() |
f9f26f0d65 | ||
![]() |
de60a0a076 | ||
![]() |
90b9af3d95 | ||
![]() |
78aea26318 | ||
![]() |
292b1b6b44 | ||
![]() |
de7f5f0366 | ||
![]() |
161de17803 | ||
![]() |
f9e5afbf49 | ||
![]() |
513e2dac8f | ||
![]() |
e428ab60f1 | ||
![]() |
9d1a2bf995 | ||
![]() |
bbda49589c | ||
![]() |
a571d5f39b | ||
![]() |
68d997a655 | ||
![]() |
7d2f8ab062 | ||
![]() |
4c2157842c | ||
![]() |
25c87ccf23 | ||
![]() |
97457a23f2 | ||
![]() |
e9cad399c9 | ||
![]() |
ee1cb21dfd | ||
![]() |
f524f1465e | ||
![]() |
1004f4cce1 | ||
![]() |
b4d6490e00 | ||
![]() |
3cf8ce9903 | ||
![]() |
f21ee2691b | ||
![]() |
845e31b3f2 | ||
![]() |
c2a18d7329 | ||
![]() |
d120b46966 | ||
![]() |
afda5d00bc | ||
![]() |
b3cb047d3d | ||
![]() |
af47c9c23b | ||
![]() |
2ab9d9e168 | ||
![]() |
8a7f44d4c2 | ||
![]() |
aa9a469bb4 | ||
![]() |
53c1ef5786 | ||
![]() |
8074cfb4c7 | ||
![]() |
ffc72789ae | ||
![]() |
4fc76251c3 | ||
![]() |
b048990e14 | ||
![]() |
b257855e38 | ||
![]() |
fb758be730 | ||
![]() |
7accdfa7da | ||
![]() |
7efe22cf7f | ||
![]() |
5a20bf4fac | ||
![]() |
a0fbf7fb79 | ||
![]() |
ed0af943da | ||
![]() |
e4ce4877b6 | ||
![]() |
a0a086e95a | ||
![]() |
dbaca66b2c | ||
![]() |
d465142275 | ||
![]() |
7f06f09696 | ||
![]() |
f5c5570fca | ||
![]() |
cb10936a1d | ||
![]() |
a43c96f6cc | ||
![]() |
a310c130f3 | ||
![]() |
14f406cc34 | ||
![]() |
3c29cb0f65 | ||
![]() |
d993c805fe | ||
![]() |
6443ade489 | ||
![]() |
eeb27ed481 | ||
![]() |
15bc393aa8 | ||
![]() |
d6e2baef67 | ||
![]() |
6100013718 | ||
![]() |
4eea8a91b8 | ||
![]() |
d57f9f7a60 | ||
![]() |
1307b29ff9 | ||
![]() |
a9443edfc6 | ||
![]() |
e6a1bf407c | ||
![]() |
0ba2dcce53 | ||
![]() |
ebd6662ebb | ||
![]() |
6898be5bea | ||
![]() |
a55b5ad69c | ||
![]() |
1104bf8a57 | ||
![]() |
1d4503b507 | ||
![]() |
2d5dccc1e0 | ||
![]() |
fc32cc24e5 | ||
![]() |
575c1df524 | ||
![]() |
d8f35bb70a | ||
![]() |
4af6fb686f | ||
![]() |
391c21a966 | ||
![]() |
2d2c43aeb5 | ||
![]() |
900ca4ed3e | ||
![]() |
9d63634078 | ||
![]() |
0b98651b48 | ||
![]() |
d3aeab5d89 | ||
![]() |
01bff86c6d | ||
![]() |
9a21382984 | ||
![]() |
9e391562ab | ||
![]() |
646b65d695 | ||
![]() |
c9e21b676c | ||
![]() |
c50da54e64 | ||
![]() |
b8b56846f2 | ||
![]() |
8bb625652d | ||
![]() |
801c523c73 |
|
@ -1,4 +1,8 @@
|
|||
.git
|
||||
node_modules
|
||||
test_data
|
||||
test
|
||||
*
|
||||
!src
|
||||
!public
|
||||
!test
|
||||
!package.json
|
||||
!package-lock.json
|
||||
!docker-entrypoint.sh
|
||||
**.gitignore
|
1
.eslintignore
Normal file
|
@ -0,0 +1 @@
|
|||
public
|
36
.eslintrc.cjs
Normal file
|
@ -0,0 +1,36 @@
|
|||
module.exports = {
|
||||
root: true,
|
||||
env: {
|
||||
browser: true,
|
||||
node: true,
|
||||
es6: true,
|
||||
},
|
||||
parserOptions: {
|
||||
parser: '@typescript-eslint/parser',
|
||||
ecmaVersion: 2020,
|
||||
sourceType: 'module',
|
||||
lib: ['es2020'],
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
tsx: true,
|
||||
},
|
||||
},
|
||||
plugins: ['prettier', 'jsdoc', 'security'],
|
||||
extends: [
|
||||
'prettier',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:@typescript-eslint/eslint-recommended',
|
||||
'plugin:prettier/recommended',
|
||||
'plugin:jsdoc/recommended',
|
||||
'plugin:security/recommended',
|
||||
],
|
||||
// add your custom rules here
|
||||
rules: {
|
||||
'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',
|
||||
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'warn',
|
||||
{ argsIgnorePattern: 'next|err|info|reject' },
|
||||
],
|
||||
},
|
||||
};
|
11
.gitattributes
vendored
Normal file
|
@ -0,0 +1,11 @@
|
|||
###############################################################################
|
||||
# Set default behavior to automatically normalize line endings.
|
||||
###############################################################################
|
||||
* text=auto
|
||||
|
||||
###############################################################################
|
||||
# behavior for Unix scripts
|
||||
#
|
||||
# Unix scripts are treated as binary by default.
|
||||
###############################################################################
|
||||
*.sh eol=lf
|
19
.github/dependabot.yml
vendored
Normal file
|
@ -0,0 +1,19 @@
|
|||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: npm
|
||||
versioning-strategy: increase
|
||||
directory: '/'
|
||||
schedule:
|
||||
interval: daily
|
||||
commit-message:
|
||||
prefix: fix
|
||||
prefix-development: chore
|
||||
include: scope
|
||||
- package-ecosystem: github-actions
|
||||
directory: '/'
|
||||
schedule:
|
||||
interval: weekly
|
||||
commit-message:
|
||||
prefix: fix
|
||||
prefix-development: chore
|
||||
include: scope
|
17
.github/workflows/automerger.yml
vendored
Normal file
|
@ -0,0 +1,17 @@
|
|||
name: 'Auto Merge PRs'
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
automerge:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: fastify/github-action-merge-dependabot@v3
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
target: minor
|
56
.github/workflows/ci.yml
vendored
Normal file
|
@ -0,0 +1,56 @@
|
|||
name: 'Continuous Integration'
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
|
||||
permissions:
|
||||
checks: write
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
ci:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Check out repository ✨ (non-dependabot)
|
||||
if: ${{ github.actor != 'dependabot[bot]' }}
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Check out repository 🎉 (dependabot)
|
||||
if: ${{ github.actor == 'dependabot[bot]' }}
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Install dependencies (Ubuntu) 🚀
|
||||
run: >-
|
||||
sudo apt-get install -qq libcairo2-dev libjpeg8-dev libpango1.0-dev
|
||||
libgif-dev build-essential
|
||||
|
||||
- name: Setup node env 📦
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: 'package.json'
|
||||
check-latest: true
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies 🚀
|
||||
run: npm ci --prefer-offline --no-audit --omit=optional
|
||||
|
||||
- name: Run linter(s) 💅
|
||||
uses: wearerequired/lint-action@v2
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
continue_on_error: false
|
||||
git_name: github-actions[bot]
|
||||
git_email: github-actions[bot]@users.noreply.github.com
|
||||
auto_fix: false
|
||||
eslint: true
|
||||
eslint_extensions: js,cjs,mjs,ts
|
||||
prettier: true
|
||||
prettier_extensions: js,cjs,ts,json
|
||||
|
||||
- name: Run hadolint 🐳
|
||||
uses: hadolint/hadolint-action@v3.1.0
|
||||
with:
|
||||
dockerfile: Dockerfile
|
||||
ignore: DL3008,DL3015
|
37
.github/workflows/codeql.yml
vendored
Normal file
|
@ -0,0 +1,37 @@
|
|||
name: 'CodeQL'
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
schedule:
|
||||
- cron: '45 23 * * 2'
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: [javascript]
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
queries: +security-and-quality
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v3
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3
|
||||
with:
|
||||
category: '/language:${{ matrix.language }}'
|
86
.github/workflows/ct.yml
vendored
Normal file
|
@ -0,0 +1,86 @@
|
|||
name: 'Continuous Testing'
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
|
||||
permissions:
|
||||
checks: write
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
ct:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Check out repository ✨ (non-dependabot)
|
||||
if: ${{ github.actor != 'dependabot[bot]' }}
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Check out repository 🎉 (dependabot)
|
||||
if: ${{ github.actor == 'dependabot[bot]' }}
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Update apt-get 🚀
|
||||
run: sudo apt-get update -qq
|
||||
|
||||
- name: Install dependencies (Ubuntu) 🚀
|
||||
run: >-
|
||||
sudo apt-get install -qq libcairo2-dev libjpeg8-dev libpango1.0-dev
|
||||
libgif-dev build-essential g++ xvfb libgles2-mesa-dev libgbm-dev
|
||||
libxxf86vm-dev
|
||||
|
||||
- name: Setup node env 📦
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: 'package.json'
|
||||
check-latest: true
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies 🚀
|
||||
run: npm ci --prefer-offline --no-audit
|
||||
|
||||
- name: Pull test data 📦
|
||||
run: >-
|
||||
wget -O test_data.zip
|
||||
https://github.com/maptiler/tileserver-gl/releases/download/v1.3.0/test_data.zip
|
||||
|
||||
- name: Prepare test data 📦
|
||||
run: unzip -q test_data.zip -d test_data
|
||||
|
||||
- name: Run tests 🧪
|
||||
run: xvfb-run --server-args="-screen 0 1024x768x24" npm test
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
with:
|
||||
platforms: 'arm64'
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Test Docker Build
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
push: false
|
||||
platforms: linux/arm64,linux/amd64
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
- name: Create Tileserver Light Directory
|
||||
run: node publish.js --no-publish
|
||||
|
||||
- name: Install node dependencies
|
||||
run: npm ci --prefer-offline --no-audit
|
||||
working-directory: ./light
|
||||
|
||||
- name: Test Light Version to Docker Hub
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: ./light
|
||||
file: ./light/Dockerfile
|
||||
push: false
|
||||
platforms: linux/arm64,linux/amd64
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
43
.github/workflows/pipeline.yml
vendored
Normal file
|
@ -0,0 +1,43 @@
|
|||
name: 'The Pipeline'
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
|
||||
concurrency:
|
||||
group: ci-${{ github.ref }}-1
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
extract-branch:
|
||||
name: 'Fetch branch'
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
current_branch: ${{ steps.get-branch.outputs.current_branch }}
|
||||
steps:
|
||||
- name: Extract branch name 🕊
|
||||
id: get-branch
|
||||
run: echo "current_branch=${GITHUB_REF##*/}" >> $GITHUB_OUTPUT
|
||||
ci:
|
||||
name: 'CI'
|
||||
needs:
|
||||
- extract-branch
|
||||
uses: ./.github/workflows/ci.yml
|
||||
ct:
|
||||
name: 'CT'
|
||||
needs:
|
||||
- extract-branch
|
||||
uses: ./.github/workflows/ct.yml
|
||||
automerger:
|
||||
name: 'Automerge Dependabot PRs'
|
||||
needs:
|
||||
- ci
|
||||
- ct
|
||||
- extract-branch
|
||||
if: >
|
||||
github.event_name == 'pull_request' && github.event.pull_request.user.login == 'dependabot[bot]'
|
||||
uses: ./.github/workflows/automerger.yml
|
175
.github/workflows/release.yml
vendored
Normal file
|
@ -0,0 +1,175 @@
|
|||
name: 'Build, Test, Release'
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
docker_user:
|
||||
description: 'Docker Username'
|
||||
required: true
|
||||
docker_token:
|
||||
description: 'Docker Token'
|
||||
required: true
|
||||
npm_token:
|
||||
description: 'NPM Token'
|
||||
required: true
|
||||
|
||||
jobs:
|
||||
release-check:
|
||||
name: Check if version is published
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: 'package.json'
|
||||
check-latest: true
|
||||
cache: 'npm'
|
||||
|
||||
- name: Check if version is published
|
||||
id: check
|
||||
run: |
|
||||
currentVersion="$( node -e "console.log(require('./package.json').version)" )"
|
||||
isPublished="$( npm view tileserver-gl versions --json | jq -c --arg cv "$currentVersion" 'any(. == $cv)' )"
|
||||
echo "version=$currentVersion" >> "$GITHUB_OUTPUT"
|
||||
echo "published=$isPublished" >> "$GITHUB_OUTPUT"
|
||||
echo "currentVersion: $currentVersion"
|
||||
echo "isPublished: $isPublished"
|
||||
outputs:
|
||||
published: ${{ steps.check.outputs.published }}
|
||||
version: ${{ steps.check.outputs.version }}
|
||||
|
||||
release:
|
||||
needs: release-check
|
||||
if: ${{ needs.release-check.outputs.published == 'false' }}
|
||||
name: 'Build, Test, Publish'
|
||||
runs-on: ubuntu-22.04
|
||||
env:
|
||||
PACKAGE_VERSION: ${{ needs.release-check.outputs.version }}
|
||||
steps:
|
||||
- name: Check out repository ✨
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Update apt-get 🚀
|
||||
run: sudo apt-get update -qq
|
||||
|
||||
- name: Install dependencies (Ubuntu) 🚀
|
||||
run: >-
|
||||
sudo apt-get install -qq libcairo2-dev libjpeg8-dev libpango1.0-dev
|
||||
libgif-dev build-essential g++ xvfb libgles2-mesa-dev libgbm-dev
|
||||
libxxf86vm-dev
|
||||
|
||||
- name: Setup node env 📦
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: 'package.json'
|
||||
check-latest: true
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies 🚀
|
||||
run: npm ci --prefer-offline --no-audit
|
||||
|
||||
- name: Pull test data 📦
|
||||
run: >-
|
||||
wget -O test_data.zip
|
||||
https://github.com/maptiler/tileserver-gl/releases/download/v1.3.0/test_data.zip
|
||||
|
||||
- name: Prepare test data 📦
|
||||
run: unzip -q test_data.zip -d test_data
|
||||
|
||||
- name: Run tests 🧪
|
||||
run: xvfb-run --server-args="-screen 0 1024x768x24" npm test
|
||||
|
||||
- name: Remove Test Data
|
||||
run: rm -R test_data*
|
||||
|
||||
- name: Get release type
|
||||
id: prepare_release
|
||||
run: |
|
||||
RELEASE_TYPE="$(node -e "console.log(require('semver').prerelease('${{ needs.release-check.outputs.version }}') ? 'prerelease' : 'regular')")"
|
||||
if [[ $RELEASE_TYPE == 'regular' ]]; then
|
||||
echo "prerelease=false" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "prerelease=true" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Publish to NPM
|
||||
run: |
|
||||
npm config set //registry.npmjs.org/:_authToken ${NPM_TOKEN}
|
||||
npm publish --access public --tag ${{ steps.prepare_release.outputs.prerelease == 'true' && 'next' || 'latest' }}
|
||||
env:
|
||||
NPM_TOKEN: ${{ github.event.inputs.npm_token }}
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
with:
|
||||
platforms: 'arm64'
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ github.event.inputs.docker_user }}
|
||||
password: ${{ github.event.inputs.docker_token }}
|
||||
|
||||
- name: Build and publish Full Version to Docker Hub
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
tags: |
|
||||
maptiler/tileserver-gl:${{ steps.prepare_release.outputs.prerelease == 'true' && 'next' || 'latest' }},
|
||||
maptiler/tileserver-gl:v${{ env.PACKAGE_VERSION }}
|
||||
platforms: linux/arm64,linux/amd64
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
- name: Extract changelog for version
|
||||
run: |
|
||||
awk '/^##/ { p = 0 }; p == 1 { print }; $0 == "## ${{ env.PACKAGE_VERSION }}" { p = 1 };' CHANGELOG.md > changelog_for_version.md
|
||||
cat changelog_for_version.md
|
||||
|
||||
- name: Publish to Github
|
||||
uses: ncipollo/release-action@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
tag: v${{ env.PACKAGE_VERSION }}
|
||||
name: v${{ env.PACKAGE_VERSION }}
|
||||
bodyFile: changelog_for_version.md
|
||||
allowUpdates: true
|
||||
draft: false
|
||||
prerelease: ${{ steps.prepare_release.outputs.prerelease }}
|
||||
|
||||
- name: Create Tileserver Light Directory
|
||||
run: node publish.js --no-publish
|
||||
|
||||
- name: Install node dependencies
|
||||
run: npm ci --prefer-offline --no-audit
|
||||
working-directory: ./light
|
||||
|
||||
- name: Publish to Light Version NPM
|
||||
working-directory: ./light
|
||||
run: |
|
||||
npm config set //registry.npmjs.org/:_authToken ${NPM_TOKEN}
|
||||
npm publish --access public --tag ${{ steps.prepare_release.outputs.prerelease == 'true' && 'next' || 'latest' }}
|
||||
env:
|
||||
NPM_TOKEN: ${{ github.event.inputs.npm_token }}
|
||||
|
||||
- name: Build and publish Light Version to Docker Hub
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: ./light
|
||||
file: ./light/Dockerfile
|
||||
push: true
|
||||
tags: |
|
||||
maptiler/tileserver-gl-light:${{ steps.prepare_release.outputs.prerelease == 'true' && 'next' || 'latest' }},
|
||||
maptiler/tileserver-gl-light:v${{ env.PACKAGE_VERSION }}
|
||||
platforms: linux/arm64,linux/amd64
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
19
.gitignore
vendored
|
@ -1,3 +1,22 @@
|
|||
docs/_build
|
||||
public/resources/leaflet-hash.js
|
||||
public/resources/leaflet.css
|
||||
public/resources/leaflet.js
|
||||
public/resources/leaflet.js.map
|
||||
public/resources/mapbox-gl-rtl-text.js
|
||||
public/resources/maplibre-gl-inspect.css
|
||||
public/resources/maplibre-gl-inspect.js
|
||||
public/resources/maplibre-gl-inspect.js.map
|
||||
public/resources/maplibre-gl.css
|
||||
public/resources/maplibre-gl.js
|
||||
public/resources/maplibre-gl.js.map
|
||||
node_modules
|
||||
test_data
|
||||
test_data.zip
|
||||
data
|
||||
light
|
||||
plugins
|
||||
config.json
|
||||
*.mbtiles
|
||||
styles
|
||||
fonts
|
||||
|
|
3
.hadolint.yml
Normal file
|
@ -0,0 +1,3 @@
|
|||
ignored:
|
||||
- DL3008
|
||||
- DL3015
|
1
.nvmrc
Normal file
|
@ -0,0 +1 @@
|
|||
18
|
1
.prettierignore
Normal file
|
@ -0,0 +1 @@
|
|||
public
|
18
.readthedocs.yaml
Normal file
|
@ -0,0 +1,18 @@
|
|||
# Read the Docs configuration file for Sphinx projects
|
||||
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
|
||||
|
||||
# Required
|
||||
version: 2
|
||||
|
||||
# Set the version of Python and other tools you might need
|
||||
build:
|
||||
os: ubuntu-22.04
|
||||
tools:
|
||||
python: "3.11"
|
||||
|
||||
# Build documentation in the doc/help/ directory with Sphinx
|
||||
sphinx:
|
||||
configuration: docs/conf.py
|
||||
|
||||
formats:
|
||||
- pdf
|
22
.travis.yml
|
@ -1,22 +0,0 @@
|
|||
language: node_js
|
||||
node_js:
|
||||
- "4"
|
||||
env:
|
||||
- CXX=g++-4.8
|
||||
addons:
|
||||
apt:
|
||||
sources:
|
||||
- ubuntu-toolchain-r-test
|
||||
packages:
|
||||
- g++-4.8
|
||||
before_install:
|
||||
- sudo apt-get update -qq
|
||||
- sudo apt-get install -qq xvfb
|
||||
install:
|
||||
- npm install
|
||||
- wget -O test_data.zip https://github.com/klokantech/tileserver-gl-data/archive/v0.0.3.zip
|
||||
- unzip -q test_data.zip -d tmp_test_data
|
||||
- mkdir test_data
|
||||
- mv tmp_test_data/tileserver-gl-data-*/* -t test_data
|
||||
script:
|
||||
- xvfb-run --server-args="-screen 0 1024x768x24" npm test
|
32
CHANGELOG.md
Normal file
|
@ -0,0 +1,32 @@
|
|||
# tileserver-gl changelog
|
||||
|
||||
## 5.2.0
|
||||
* Use npm packages for public/resources (https://github.com/maptiler/tileserver-gl/pull/1427) by @okimiko
|
||||
* use ttf files of googlefonts/opensans (https://github.com/maptiler/tileserver-gl/pull/1447) by @okimiko
|
||||
* Limit Elevation Lat/Long Output Length (https://github.com/maptiler/tileserver-gl/pull/1457) by @okimiko
|
||||
* Fetch style from url (https://github.com/maptiler/tileserver-gl/pull/1462) by @YoelRidgway
|
||||
* fix: memory leak on SIGHUP (https://github.com/maptiler/tileserver-gl/pull/1455) by @okimiko
|
||||
* fix: resolves Unimplemented type: 3 error for geojson format (https://github.com/maptiler/tileserver-gl/pull/1465) by @rjdjohnston
|
||||
* fix: Test light version in ct workflow - fix sqlite build in light (https://github.com/maptiler/tileserver-gl/pull/1477) by @acalcutt
|
||||
* fix: light version docker entrypoint permissions (https://github.com/maptiler/tileserver-gl/pull/1478) by @acalcutt
|
||||
|
||||
## 5.1.3
|
||||
* Fix SIGHUP (broken since 5.1.x) (https://github.com/maptiler/tileserver-gl/pull/1452) by @okimiko
|
||||
|
||||
## 5.1.2
|
||||
* Fix broken light (invalid use of heavy dependencies) (https://github.com/maptiler/tileserver-gl/pull/1449) by @okimiko
|
||||
|
||||
## 5.1.1
|
||||
* Fix wrong node version in Docker image (https://github.com/maptiler/tileserver-gl/pull/1442) by @acalcutt
|
||||
|
||||
## 5.1.0
|
||||
* Update recommended node to v22 + Update docker images to use node 22 (https://github.com/maptiler/tileserver-gl/pull/1438) by @acalcutt
|
||||
* Upgrade Express to v5 + Canvas to v3 + code cleanup (https://github.com/maptiler/tileserver-gl/pull/1429) by @acalcutt
|
||||
* Terrain Preview and simple Elevation Query (https://github.com/maptiler/tileserver-gl/pull/1425 and https://github.com/maptiler/tileserver-gl/pull/1432) by @okimiko
|
||||
* add progressive rendering option for static jpeg images (#1397) by @samuel-git
|
||||
|
||||
## 5.0.0
|
||||
* Update Maplibre-Native to [v6.0.0](https://github.com/maplibre/maplibre-native/releases/tag/node-v6.0.0) release by @acalcutt in https://github.com/maptiler/tileserver-gl/pull/1376 and @dependabot in https://github.com/maptiler/tileserver-gl/pull/1381
|
||||
* This first release that use Metal for rendering instead of OpenGL (ES) for macOS.
|
||||
* This the first release that uses OpenGL (ES) 3.0 on Windows and Linux
|
||||
* Note: Windows users may need to update their [c++ redistributable ](https://learn.microsoft.com/en-us/cpp/windows/latest-supported-vc-redist?view=msvc-170) for maplibre-native v6.0.0
|
118
Dockerfile
|
@ -1,22 +1,110 @@
|
|||
FROM debian:stretch
|
||||
MAINTAINER Petr Sloup <petr.sloup@klokantech.com>
|
||||
FROM ubuntu:jammy AS builder
|
||||
|
||||
RUN apt-get -qq update \
|
||||
&& DEBIAN_FRONTEND=noninteractive apt-get -y install \
|
||||
curl \
|
||||
build-essential \
|
||||
python \
|
||||
xvfb \
|
||||
&& curl -sL https://deb.nodesource.com/setup_4.x | bash - \
|
||||
&& apt-get -y install nodejs \
|
||||
&& apt-get clean
|
||||
ENV NODE_ENV="production"
|
||||
|
||||
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
||||
|
||||
RUN export DEBIAN_FRONTEND=noninteractive && \
|
||||
apt-get update && \
|
||||
apt-get install -y --no-install-recommends --no-install-suggests \
|
||||
build-essential \
|
||||
ca-certificates \
|
||||
curl \
|
||||
gnupg \
|
||||
pkg-config \
|
||||
xvfb \
|
||||
libglfw3-dev \
|
||||
libuv1-dev \
|
||||
libjpeg-turbo8 \
|
||||
libicu70 \
|
||||
libcairo2-dev \
|
||||
libpango1.0-dev \
|
||||
libjpeg-dev \
|
||||
libgif-dev \
|
||||
librsvg2-dev \
|
||||
gir1.2-rsvg-2.0 \
|
||||
librsvg2-2 \
|
||||
librsvg2-common \
|
||||
libcurl4-openssl-dev \
|
||||
libpixman-1-dev \
|
||||
libpixman-1-0 && \
|
||||
mkdir -p /etc/apt/keyrings && \
|
||||
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg && \
|
||||
echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_22.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list && \
|
||||
apt-get -qq update && \
|
||||
apt-get install -y --no-install-recommends --no-install-suggests nodejs && \
|
||||
npm i -g npm@latest && \
|
||||
apt-get -y remove curl gnupg && \
|
||||
apt-get -y --purge autoremove && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN mkdir -p /usr/src/app
|
||||
COPY / /usr/src/app
|
||||
RUN cd /usr/src/app && npm install --production
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
COPY package.json /usr/src/app
|
||||
COPY package-lock.json /usr/src/app
|
||||
|
||||
RUN npm config set maxsockets 1 && \
|
||||
npm config set fetch-retries 5 && \
|
||||
npm config set fetch-retry-mintimeout 100000 && \
|
||||
npm config set fetch-retry-maxtimeout 600000 && \
|
||||
npm ci --omit=dev && \
|
||||
chown -R root:root /usr/src/app
|
||||
|
||||
FROM ubuntu:jammy AS final
|
||||
|
||||
ENV \
|
||||
NODE_ENV="production" \
|
||||
CHOKIDAR_USEPOLLING=1 \
|
||||
CHOKIDAR_INTERVAL=500
|
||||
|
||||
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
||||
|
||||
RUN export DEBIAN_FRONTEND=noninteractive && \
|
||||
groupadd -r node && \
|
||||
useradd -r -g node node && \
|
||||
apt-get -qq update && \
|
||||
apt-get install -y --no-install-recommends --no-install-suggests \
|
||||
ca-certificates \
|
||||
curl \
|
||||
gnupg \
|
||||
xvfb \
|
||||
libglfw3 \
|
||||
libuv1 \
|
||||
libjpeg-turbo8 \
|
||||
libicu70 \
|
||||
libcairo2 \
|
||||
libgif7 \
|
||||
libopengl0 \
|
||||
libpixman-1-0 \
|
||||
libcurl4 \
|
||||
librsvg2-2 \
|
||||
libpango-1.0-0 && \
|
||||
mkdir -p /etc/apt/keyrings && \
|
||||
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg && \
|
||||
echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_22.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list && \
|
||||
apt-get -qq update && \
|
||||
apt-get install -y --no-install-recommends --no-install-suggests nodejs && \
|
||||
npm i -g npm@latest && \
|
||||
apt-get -y remove curl gnupg && \
|
||||
apt-get -y --purge autoremove && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY --from=builder /usr/src/app /usr/src/app
|
||||
|
||||
COPY . /usr/src/app
|
||||
|
||||
RUN mkdir -p /data && chown node:node /data
|
||||
VOLUME /data
|
||||
WORKDIR /data
|
||||
|
||||
EXPOSE 80
|
||||
CMD ["/usr/src/app/run.sh"]
|
||||
EXPOSE 8080
|
||||
|
||||
USER node:node
|
||||
|
||||
ENTRYPOINT ["/usr/src/app/docker-entrypoint.sh"]
|
||||
|
||||
HEALTHCHECK CMD node /usr/src/app/src/healthcheck.js
|
||||
|
|
38
Dockerfile_build
Normal file
|
@ -0,0 +1,38 @@
|
|||
FROM ubuntu:jammy AS builder
|
||||
|
||||
ENV NODE_ENV="devel"
|
||||
|
||||
RUN export DEBIAN_FRONTEND=noninteractive && \
|
||||
apt-get update && \
|
||||
apt-get install -y --no-install-recommends --no-install-suggests \
|
||||
build-essential \
|
||||
ca-certificates \
|
||||
curl \
|
||||
gnupg \
|
||||
pkg-config \
|
||||
xvfb \
|
||||
libglfw3-dev \
|
||||
libuv1-dev \
|
||||
libjpeg-turbo8 \
|
||||
libicu70 \
|
||||
libcairo2-dev \
|
||||
libpango1.0-dev \
|
||||
libjpeg-dev \
|
||||
libgif-dev \
|
||||
librsvg2-dev \
|
||||
gir1.2-rsvg-2.0 \
|
||||
librsvg2-2 \
|
||||
librsvg2-common \
|
||||
libcurl4-openssl-dev \
|
||||
libpixman-1-dev \
|
||||
libpixman-1-0 && \
|
||||
mkdir -p /etc/apt/keyrings && \
|
||||
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg && \
|
||||
echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_22.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list && \
|
||||
apt-get -qq update && \
|
||||
apt-get install -y --no-install-recommends --no-install-suggests nodejs && \
|
||||
npm i -g npm@latest && \
|
||||
apt-get -y remove curl gnupg && \
|
||||
apt-get -y --purge autoremove && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/*
|
84
Dockerfile_light
Normal file
|
@ -0,0 +1,84 @@
|
|||
FROM ubuntu:jammy AS builder
|
||||
|
||||
ENV NODE_ENV="production"
|
||||
|
||||
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
||||
|
||||
RUN export DEBIAN_FRONTEND=noninteractive && \
|
||||
apt-get update && \
|
||||
apt-get install -y --no-install-recommends --no-install-suggests \
|
||||
build-essential \
|
||||
ca-certificates \
|
||||
curl \
|
||||
gnupg && \
|
||||
mkdir -p /etc/apt/keyrings && \
|
||||
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg && \
|
||||
echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_22.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list && \
|
||||
apt-get -qq update && \
|
||||
apt-get install -y --no-install-recommends --no-install-suggests nodejs && \
|
||||
npm i -g npm@latest && \
|
||||
apt-get -y remove curl gnupg && \
|
||||
apt-get -y --purge autoremove && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN mkdir -p /usr/src/app
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
COPY package.json /usr/src/app
|
||||
COPY package-lock.json /usr/src/app
|
||||
|
||||
RUN npm config set maxsockets 1 && \
|
||||
npm config set fetch-retries 5 && \
|
||||
npm config set fetch-retry-mintimeout 100000 && \
|
||||
npm config set fetch-retry-maxtimeout 600000 && \
|
||||
npm ci --omit=dev && \
|
||||
chown -R root:root /usr/src/app
|
||||
|
||||
FROM ubuntu:jammy AS final
|
||||
|
||||
ENV \
|
||||
NODE_ENV="production" \
|
||||
CHOKIDAR_USEPOLLING=1 \
|
||||
CHOKIDAR_INTERVAL=500
|
||||
|
||||
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
||||
|
||||
RUN export DEBIAN_FRONTEND=noninteractive && \
|
||||
groupadd -r node && \
|
||||
useradd -r -g node node && \
|
||||
apt-get -qq update && \
|
||||
apt-get install -y --no-install-recommends --no-install-suggests \
|
||||
ca-certificates \
|
||||
curl \
|
||||
gnupg && \
|
||||
mkdir -p /etc/apt/keyrings && \
|
||||
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg && \
|
||||
echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_22.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list && \
|
||||
apt-get -qq update && \
|
||||
apt-get install -y --no-install-recommends --no-install-suggests nodejs && \
|
||||
npm i -g npm@latest && \
|
||||
apt-get -y remove curl gnupg && \
|
||||
apt-get -y --purge autoremove && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY --from=builder /usr/src/app /usr/src/app
|
||||
|
||||
COPY . /usr/src/app
|
||||
|
||||
RUN mkdir -p /data && \
|
||||
chown node:node /data && \
|
||||
chmod +x /usr/src/app/docker-entrypoint.sh
|
||||
|
||||
VOLUME /data
|
||||
WORKDIR /data
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
USER node:node
|
||||
|
||||
ENTRYPOINT ["/usr/src/app/docker-entrypoint.sh"]
|
||||
|
||||
HEALTHCHECK CMD node /usr/src/app/src/healthcheck.js
|
62
Dockerfile_test
Normal file
|
@ -0,0 +1,62 @@
|
|||
# Run tests inside docker without requiring full installation of dependencies on local machine
|
||||
# Simply run "docker build -f Dockerfile_test ."
|
||||
# WARNING: sometimes it fails with a core dumped exception
|
||||
|
||||
FROM ubuntu:jammy
|
||||
|
||||
ENV NODE_ENV="development"
|
||||
|
||||
RUN set -ex; \
|
||||
export DEBIAN_FRONTEND=noninteractive; \
|
||||
apt-get -qq update; \
|
||||
apt-get -y --no-install-recommends install \
|
||||
unzip \
|
||||
build-essential \
|
||||
ca-certificates \
|
||||
curl \
|
||||
gnupg \
|
||||
pkg-config \
|
||||
xvfb \
|
||||
libglfw3-dev \
|
||||
libuv1-dev \
|
||||
libjpeg-turbo8 \
|
||||
libicu70 \
|
||||
libcairo2-dev \
|
||||
libpango1.0-dev \
|
||||
libjpeg-dev \
|
||||
libgif-dev \
|
||||
librsvg2-dev \
|
||||
gir1.2-rsvg-2.0 \
|
||||
librsvg2-2 \
|
||||
librsvg2-common \
|
||||
libcurl4-openssl-dev \
|
||||
libpixman-1-dev \
|
||||
libpixman-1-0; \
|
||||
apt-get -y --purge autoremove; \
|
||||
apt-get clean; \
|
||||
rm -rf /var/lib/apt/lists/*;
|
||||
|
||||
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
||||
|
||||
RUN mkdir -p /etc/apt/keyrings; \
|
||||
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg; \
|
||||
echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_22.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list; \
|
||||
apt-get -qq update; \
|
||||
apt-get install -y nodejs; \
|
||||
npm i -g npm@latest; \
|
||||
apt-get -y remove gnupg; \
|
||||
apt-get -y --purge autoremove; \
|
||||
apt-get clean; \
|
||||
rm -rf /var/lib/apt/lists/*;
|
||||
|
||||
RUN mkdir -p /usr/src/app
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
RUN curl -L -o test_data.zip https://github.com/maptiler/tileserver-gl/releases/download/v1.3.0/test_data.zip; \
|
||||
unzip -q test_data.zip -d test_data
|
||||
|
||||
COPY package.json .
|
||||
RUN npm install
|
||||
COPY / .
|
||||
|
||||
RUN xvfb-run --server-args="-screen 0 1024x768x24" npm test
|
21
ISSUE_TEMPLATE.md
Normal file
|
@ -0,0 +1,21 @@
|
|||
It is great you want to help us making TileServer GL project better!
|
||||
|
||||
This is the right place only for a software bug report or a new software feature request.
|
||||
|
||||
NOTE: Questions about OpenMapTiles data, OpenMapTiles Server, TileHosting and other software/products do not belong here (and will not be answered)!
|
||||
|
||||
The usage and installation questions belongs to https://stackoverflow.com/questions/tagged/openmaptiles
|
||||
A guaranteed support and consulting from the core developers via https://openmaptiles.com/support/
|
||||
|
||||
Please search this GitHub repo for similar requests before posting (check also closed tickets).
|
||||
|
||||
When reporting a problem you have with the TileServer GL software please provide:
|
||||
|
||||
- Clear description of the problem: What steps will lead to reproducing the error on our computer? What is exactly wrong?
|
||||
- Version of the TileServer GL software you have used
|
||||
- Version and name of the operating system you use or other details of your setup
|
||||
- Information about your used config / styles / vector tiles
|
||||
- URL / link to the specific location in a live map demo where a bug is visible is always great
|
||||
- Screenshot of the problem are cool! Drag&drop an image to the report here...
|
||||
|
||||
We love pull requests! If you are able to code, please send us your fix or code modification via GitHub... Thanks!
|
1461
LICENSE.md
Normal file
112
PUBLISHING.md
Normal file
|
@ -0,0 +1,112 @@
|
|||
# Publishing a New Version
|
||||
|
||||
This document outlines the process for publishing new versions of this project. We use a GitHub workflow for automated releases, but also provide manual steps for specific situations.
|
||||
|
||||
## Automated Publishing via GitHub Workflow (Recommended)
|
||||
|
||||
This is the preferred method for publishing new versions. It automates the entire process, including version bumping, tagging, building, and publishing.
|
||||
|
||||
1. **Prepare the Release:**
|
||||
|
||||
* **Choose the Version Increment:** Determine the appropriate semantic versioning increment (prerelease, prepatch, preminor, premajor, patch, minor, major).
|
||||
|
||||
* **Update `package.json`:** Use the `npm version` command to bump the version number. This command also supports creating pre-release versions using the `--preid pre` flag.
|
||||
|
||||
```bash
|
||||
# Example: Increment to a new patch version
|
||||
npm version patch --no-git-tag-version
|
||||
|
||||
# Example: Increment to a new minor version
|
||||
npm version minor --no-git-tag-version
|
||||
|
||||
# Example: Increment to a new major version
|
||||
npm version major --no-git-tag-version
|
||||
|
||||
# Example: Create a pre-release (e.g., -pre.0) version
|
||||
npm version prepatch --preid pre --no-git-tag-version
|
||||
# OR
|
||||
npm version preminor --preid pre --no-git-tag-version
|
||||
# OR
|
||||
npm version premajor --preid pre --no-git-tag-version
|
||||
|
||||
# Example: Increment an existing pre-release version (e.g., -pre.0 to -pre.1)
|
||||
npm version prerelease --preid pre --no-git-tag-version
|
||||
```
|
||||
|
||||
* `--no-git-tag-version`: This prevents `npm version` from automatically creating a git tag, as the GitHub workflow will handle this later.
|
||||
* `--preid pre`: Specifies that "pre" is the pre-release identifier.
|
||||
|
||||
* **Update the Changelog (`CHANGELOG.md`):** Add a new section for the release. The heading *must* exactly match the format `## <VERSION>`, where `<VERSION>` is the full version number from `package.json`. Describe the changes included in the release.
|
||||
|
||||
```markdown
|
||||
## 1.2.3
|
||||
* Added new feature X.
|
||||
```
|
||||
|
||||
2. **Commit and Push:** Commit the changes to `package.json`, `package-lock.json` and `CHANGELOG.md` to a the `master` branch. Create a pull request (PR) for review. If you are an administrator, you may push directly, but a PR is generally recommended for code review.
|
||||
|
||||
3. **Run the GitHub Workflow:** Once the PR is merged (or changes pushed directly), trigger the "Build, Test, Release" GitHub workflow. This workflow is responsible for:
|
||||
|
||||
* Building the project.
|
||||
* Running tests.
|
||||
* Creating an NPM package.
|
||||
* Building and pushing Docker images.
|
||||
* Creating a GitHub Release.
|
||||
* Creating a Git tag for the release.
|
||||
|
||||
## Manual Publishing (For Special Cases)
|
||||
|
||||
Use the following steps *only* when the automated workflow is not suitable (e.g., for debugging, specific environment needs).
|
||||
|
||||
1. **Update Version in `package.json`:** Modify the `version` field in `package.json` to the desired version number.
|
||||
|
||||
2. **Create and Push Git Tag:**
|
||||
|
||||
```bash
|
||||
git tag vX.X.X # Replace X.X.X with the version number
|
||||
git push origin --tags
|
||||
```
|
||||
|
||||
3. **NPM Publish Options (Choose ONE):**
|
||||
|
||||
* **Option A: Publish only the full `tileserver-gl` version:**
|
||||
|
||||
```bash
|
||||
npm publish --access public
|
||||
```
|
||||
|
||||
* **Option B: Build and Publish both `tileserver-gl` and `tileserver-gl-light` using `publish.js`:**
|
||||
|
||||
```bash
|
||||
# This script builds the light version and publishes both tileserver-gl and tileserver-gl-light to NPM.
|
||||
node publish.js
|
||||
```
|
||||
|
||||
* **Option C: Build only the `tileserver-gl-light` version (no publish):**
|
||||
|
||||
```bash
|
||||
# This script ONLY builds the light version (e.g., for local testing or Docker image creation) without publishing.
|
||||
node publish.js --no-publish
|
||||
```
|
||||
|
||||
4. **Build and Push Docker Images:**
|
||||
|
||||
```bash
|
||||
# Build the main image
|
||||
docker buildx build --platform linux/amd64 -t maptiler/tileserver-gl:latest -t maptiler/tileserver-gl:X.X.X . # Replace X.X.X
|
||||
docker push maptiler/tileserver-gl --all-tags
|
||||
|
||||
# Build the light image
|
||||
cd light
|
||||
docker buildx build --platform linux/amd64 -t maptiler/tileserver-gl-light:latest -t maptiler/tileserver-gl-light:X.X.X . # Replace X.X.X
|
||||
docker push maptiler/tileserver-gl-light --all-tags
|
||||
cd .. # Return to the project root
|
||||
```
|
||||
* Make sure you are logged into the docker registry before pushing. `docker login`
|
||||
|
||||
**Important Considerations for Manual Publishing:**
|
||||
|
||||
* **Consistency:** Ensure the version number in `package.json`, the Git tag, and the Docker image tags are identical.
|
||||
* **Credentials:** Verify that you have the necessary permissions to push Docker images and publish to NPM.
|
||||
* **Cleanliness:** Before building Docker images, ensure your working directory is clean to avoid including unwanted files in the image.
|
||||
* **Error Handling:** Manually publishing is more prone to errors. Double-check each step to avoid issues.
|
140
README.md
|
@ -1,47 +1,115 @@
|
|||
# tileserver-gl
|
||||
[](https://travis-ci.org/klokantech/tileserver-gl)
|
||||

|
||||
|
||||
## Installation
|
||||
# My TileServer GL
|
||||
|
||||
### Docker
|
||||
- `docker run -it -v $(pwd):/data -p 8080:80 klokantech/tileserver-gl`
|
||||
creare un folder dove mettere la mappa e i layers
|
||||
|
||||
### Without docker
|
||||
- Make sure you have Node v4 or higher (`nvm install 4`)
|
||||
- `npm install`
|
||||
- `node src/main.js`
|
||||
Download vector tiles from [OpenMapTiles](https://data.maptiler.com/downloads/planet/).
|
||||
|
||||
## Sample data
|
||||
Sample data can be downloaded at https://github.com/klokantech/tileserver-gl-data/archive/master.zip
|
||||
scaricare i layers
|
||||
|
||||
#### Usage
|
||||
- unpack somewhere and `cd` to the directory
|
||||
- `docker run -it -v $(pwd):/data -p 8080:80 klokantech/tileserver-gl`
|
||||
- (or `node path/to/repo/src/main.js`)
|
||||
wget https://github.com/maptiler/tileserver-gl/releases/download/v1.3.0/test_data.zip
|
||||
unzip test_data.zip
|
||||
|
||||
## Configuration
|
||||
modificare nano config.json per inserire il nome del file .mbtiles (x es planetOSM.mbtiles )
|
||||
|
||||
Create `config.json` file in the root directory.
|
||||
The config file can contain definition of several paths where the tiles will be served.
|
||||
far partire il docker
|
||||
|
||||
### Example configuration file
|
||||
See https://github.com/klokantech/tileserver-gl-data/blob/master/config.json
|
||||
services:
|
||||
tileserver-gl:
|
||||
volumes:
|
||||
- /home/nvme/dockerdata/tileserver:/data
|
||||
ports:
|
||||
- 8115:8080
|
||||
image: maptiler/tileserver-gl:latest
|
||||
|
||||
**Note**: To specify local mbtiles as source of the vector tiles inside the style, use urls with `mbtiles` protocol with path relative to the `cwd + options.paths.root + options.paths.mbtiles`. (For example `mbtiles://switzerland.mbtiles`)
|
||||
oppure
|
||||
|
||||
## Available URLs
|
||||
sudo docker run -d -v /home/nvme/dockerdata/tileserver:/data -p 8115:8080 maptiler/tileserver-gl:latest
|
||||
|
||||
# TileServer GL
|
||||
[](https://github.com/maptiler/tileserver-gl/actions/workflows/pipeline.yml)
|
||||
[](https://hub.docker.com/r/maptiler/tileserver-gl/)
|
||||
|
||||
Vector and raster maps with GL styles. Server-side rendering by MapLibre GL Native. Map tile server for MapLibre GL JS, Android, iOS, Leaflet, OpenLayers, GIS via WMTS, etc.
|
||||
|
||||
Download vector tiles from [OpenMapTiles](https://data.maptiler.com/downloads/planet/).
|
||||
## Getting Started with Node
|
||||
|
||||
Make sure you have Node.js version **18.17.0** or above installed. Node 22 is recommended. (running `node -v` it should output something like `v22.x.x`). Running without docker requires [Native dependencies](https://maptiler-tileserver.readthedocs.io/en/latest/installation.html#npm) to be installed first.
|
||||
|
||||
Install `tileserver-gl` with server-side raster rendering of vector tiles with npm.
|
||||
|
||||
```bash
|
||||
npm install -g tileserver-gl
|
||||
```
|
||||
|
||||
Once installed, you can use it like the following examples.
|
||||
|
||||
using a mbtiles file
|
||||
```bash
|
||||
wget https://github.com/maptiler/tileserver-gl/releases/download/v1.3.0/zurich_switzerland.mbtiles
|
||||
tileserver-gl --file zurich_switzerland.mbtiles
|
||||
[in your browser, visit http://[server ip]:8080]
|
||||
```
|
||||
|
||||
using a config.json + style + mbtiles file
|
||||
```bash
|
||||
wget https://github.com/maptiler/tileserver-gl/releases/download/v1.3.0/test_data.zip
|
||||
unzip test_data.zip
|
||||
tileserver-gl
|
||||
[in your browser, visit http://[server ip]:8080]
|
||||
```
|
||||
|
||||
Alternatively, you can use the `tileserver-gl-light` npm package instead, which is pure javascript, does not have any native dependencies, and can run anywhere, but does not contain rasterization on the server side made with Maplibre GL Native.
|
||||
|
||||
## Getting Started with Docker
|
||||
|
||||
An alternative to npm to start the packed software easier is to install [Docker](https://www.docker.com/) on your computer and then run from the tileserver-gl directory
|
||||
|
||||
Example using a mbtiles file
|
||||
```bash
|
||||
wget https://github.com/maptiler/tileserver-gl/releases/download/v1.3.0/zurich_switzerland.mbtiles
|
||||
docker run --rm -it -v $(pwd):/data -p 8080:8080 maptiler/tileserver-gl:latest --file zurich_switzerland.mbtiles
|
||||
[in your browser, visit http://[server ip]:8080]
|
||||
```
|
||||
|
||||
Example using a config.json + style + mbtiles file
|
||||
```bash
|
||||
wget https://github.com/maptiler/tileserver-gl/releases/download/v1.3.0/test_data.zip
|
||||
unzip test_data.zip
|
||||
docker run --rm -it -v $(pwd):/data -p 8080:8080 maptiler/tileserver-gl:latest
|
||||
[in your browser, visit http://[server ip]:8080]
|
||||
```
|
||||
|
||||
Example using a different path
|
||||
```bash
|
||||
docker run --rm -it -v /your/local/config/path:/data -p 8080:8080 maptiler/tileserver-gl:latest
|
||||
```
|
||||
replace '/your/local/config/path' with the path to your config file
|
||||
|
||||
|
||||
Alternatively, you can use the `maptiler/tileserver-gl-light:latest` docker image instead, which is pure javascript, does not have any native dependencies, and can run anywhere, but does not contain rasterization on the server side made with Maplibre GL Native.
|
||||
|
||||
## Getting Started with Linux cli
|
||||
|
||||
Test from command line
|
||||
```bash
|
||||
wget https://github.com/maptiler/tileserver-gl/releases/download/v1.3.0/test_data.zip
|
||||
unzip -q test_data.zip -d test_data
|
||||
xvfb-run --server-args="-screen 0 1024x768x24" npm test
|
||||
```
|
||||
|
||||
Run from command line
|
||||
```bash
|
||||
xvfb-run --server-args="-screen 0 1024x768x24" node .
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
You can read the full documentation of this project at https://tileserver.readthedocs.io/en/latest/.
|
||||
|
||||
## Alternative
|
||||
|
||||
Discover MapTiler Server if you need a [map server with easy setup and user-friendly interface](https://www.maptiler.com/server/).
|
||||
|
||||
- If you visit the server on the configured port (default 8080) you should see your maps appearing in the browser.
|
||||
- Style is served at `/styles/{id}.json` (+ array at `/styles.json`)
|
||||
- Sprites at `/styles/{id}/sprite[@2x].{format}`
|
||||
- Fonts at `/fonts/{fontstack}/{start}-{end}.pbf`
|
||||
- Rendered tiles are at `/styles/{id}/rendered/{z}/{x}/{y}[@2x].{format}`
|
||||
- The optional `@2x` (or `@3x`) part can be used to render HiDPI (retina) tiles
|
||||
- Available formats: `png`, `jpg` (`jpeg`), `webp`
|
||||
- TileJSON at `/styles/{id}/rendered.json`
|
||||
- Static images are rendered at:
|
||||
- `/styles/{id}/static/{lon},{lat},{zoom}/{width}x{height}[@2x].{format}` (center-based)
|
||||
- `/styles/{id}/static/{minx},{miny},{maxx},{maxy}/{zoom}[@2x].{format}` (area-based)
|
||||
- Source data at `/data/{mbtiles}/{z}/{x}/{y}.{format}`
|
||||
- TileJSON at `/data/{mbtiles}.json`
|
||||
- Array of all TileJSONs at `/index.json` (`/rendered.json`; `/data.json`)
|
||||
|
|
35
README_light.md
Normal file
|
@ -0,0 +1,35 @@
|
|||
# TileServer GL light
|
||||
[](https://travis-ci.org/maptiler/tileserver-gl)
|
||||
[](https://hub.docker.com/r/maptiler/tileserver-gl/)
|
||||
|
||||
Vector maps with GL styles. Map tile server for Mapbox Android, iOS, GL JS, Leaflet, OpenLayers, etc. without server side rendering.
|
||||
|
||||
## Quickstart
|
||||
Use `npm install -g tileserver-gl-light` to install the package from npm.
|
||||
|
||||
Then you can simply run `tileserver-gl-light zurich_switzerland.mbtiles` to start the server for the given mbtiles.
|
||||
|
||||
See also `tileserver-gl` which contains server side rendering.
|
||||
|
||||
Prepared vector tiles can be downloaded from [OpenMapTiles.com](https://openmaptiles.com/downloads/planet/).
|
||||
|
||||
## Building docker image
|
||||
|
||||
You can build TileServer GL light image from source.
|
||||
|
||||
```
|
||||
git clone https://github.com/maptiler/tileserver-gl.git
|
||||
cd tileserver-gl
|
||||
node publish.js --no-publish
|
||||
cd light
|
||||
docker build -t tileserver-gl-light .
|
||||
```
|
||||
|
||||
[Download from OpenMapTiles.com](https://openmaptiles.com/downloads/planet/) or [create](https://github.com/openmaptiles/openmaptiles) your vector tile, and run following in directory contains your *.mbtiles.
|
||||
|
||||
```
|
||||
docker run --rm -it -v $(pwd):/data -p 8080:8080 tileserver-gl-light
|
||||
```
|
||||
|
||||
## Documentation
|
||||
You can read full documentation of this project at https://maptiler-tileserver.readthedocs.io/
|
3
commitlint.config.cjs
Normal file
|
@ -0,0 +1,3 @@
|
|||
module.exports = {
|
||||
extends: ['@commitlint/config-conventional'],
|
||||
};
|
10
docker-entrypoint.sh
Executable file
|
@ -0,0 +1,10 @@
|
|||
#!/bin/sh
|
||||
if ! which -- "${1}"; then
|
||||
# first arg is not an executable
|
||||
if [ -e /tmp/.X99-lock ]; then rm /tmp/.X99-lock -f; fi
|
||||
export DISPLAY=:99
|
||||
Xvfb "${DISPLAY}" -nolisten unix &
|
||||
exec node /usr/src/app/ "$@"
|
||||
fi
|
||||
|
||||
exec "$@"
|
7
docker-entrypoint_light.sh
Normal file
|
@ -0,0 +1,7 @@
|
|||
#!/bin/sh
|
||||
if ! which -- "${1}"; then
|
||||
# first arg is not an executable
|
||||
exec node /usr/src/app/ "$@"
|
||||
fi
|
||||
|
||||
exec "$@"
|
177
docs/Makefile
Normal file
|
@ -0,0 +1,177 @@
|
|||
# Makefile for Sphinx documentation
|
||||
#
|
||||
|
||||
# You can set these variables from the command line.
|
||||
SPHINXOPTS =
|
||||
SPHINXBUILD = sphinx-build
|
||||
PAPER =
|
||||
BUILDDIR = _build
|
||||
|
||||
# User-friendly check for sphinx-build
|
||||
ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1)
|
||||
$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/)
|
||||
endif
|
||||
|
||||
# Internal variables.
|
||||
PAPEROPT_a4 = -D latex_paper_size=a4
|
||||
PAPEROPT_letter = -D latex_paper_size=letter
|
||||
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
|
||||
# the i18n builder cannot share the environment and doctrees with the others
|
||||
I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
|
||||
|
||||
.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext
|
||||
|
||||
help:
|
||||
@echo "Please use \`make <target>' where <target> is one of"
|
||||
@echo " html to make standalone HTML files"
|
||||
@echo " dirhtml to make HTML files named index.html in directories"
|
||||
@echo " singlehtml to make a single large HTML file"
|
||||
@echo " pickle to make pickle files"
|
||||
@echo " json to make JSON files"
|
||||
@echo " htmlhelp to make HTML files and a HTML help project"
|
||||
@echo " qthelp to make HTML files and a qthelp project"
|
||||
@echo " devhelp to make HTML files and a Devhelp project"
|
||||
@echo " epub to make an epub"
|
||||
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
|
||||
@echo " latexpdf to make LaTeX files and run them through pdflatex"
|
||||
@echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
|
||||
@echo " text to make text files"
|
||||
@echo " man to make manual pages"
|
||||
@echo " texinfo to make Texinfo files"
|
||||
@echo " info to make Texinfo files and run them through makeinfo"
|
||||
@echo " gettext to make PO message catalogs"
|
||||
@echo " changes to make an overview of all changed/added/deprecated items"
|
||||
@echo " xml to make Docutils-native XML files"
|
||||
@echo " pseudoxml to make pseudoxml-XML files for display purposes"
|
||||
@echo " linkcheck to check all external links for integrity"
|
||||
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
|
||||
|
||||
clean:
|
||||
rm -rf $(BUILDDIR)/*
|
||||
|
||||
html:
|
||||
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
|
||||
@echo
|
||||
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
|
||||
|
||||
dirhtml:
|
||||
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
|
||||
@echo
|
||||
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
|
||||
|
||||
singlehtml:
|
||||
$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
|
||||
@echo
|
||||
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
|
||||
|
||||
pickle:
|
||||
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
|
||||
@echo
|
||||
@echo "Build finished; now you can process the pickle files."
|
||||
|
||||
json:
|
||||
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
|
||||
@echo
|
||||
@echo "Build finished; now you can process the JSON files."
|
||||
|
||||
htmlhelp:
|
||||
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
|
||||
@echo
|
||||
@echo "Build finished; now you can run HTML Help Workshop with the" \
|
||||
".hhp project file in $(BUILDDIR)/htmlhelp."
|
||||
|
||||
qthelp:
|
||||
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
|
||||
@echo
|
||||
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
|
||||
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
|
||||
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/TileServerGL.qhcp"
|
||||
@echo "To view the help file:"
|
||||
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/TileServerGL.qhc"
|
||||
|
||||
devhelp:
|
||||
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
|
||||
@echo
|
||||
@echo "Build finished."
|
||||
@echo "To view the help file:"
|
||||
@echo "# mkdir -p $$HOME/.local/share/devhelp/TileServerGL"
|
||||
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/TileServerGL"
|
||||
@echo "# devhelp"
|
||||
|
||||
epub:
|
||||
$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
|
||||
@echo
|
||||
@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
|
||||
|
||||
latex:
|
||||
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
|
||||
@echo
|
||||
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
|
||||
@echo "Run \`make' in that directory to run these through (pdf)latex" \
|
||||
"(use \`make latexpdf' here to do that automatically)."
|
||||
|
||||
latexpdf:
|
||||
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
|
||||
@echo "Running LaTeX files through pdflatex..."
|
||||
$(MAKE) -C $(BUILDDIR)/latex all-pdf
|
||||
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
|
||||
|
||||
latexpdfja:
|
||||
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
|
||||
@echo "Running LaTeX files through platex and dvipdfmx..."
|
||||
$(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
|
||||
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
|
||||
|
||||
text:
|
||||
$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
|
||||
@echo
|
||||
@echo "Build finished. The text files are in $(BUILDDIR)/text."
|
||||
|
||||
man:
|
||||
$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
|
||||
@echo
|
||||
@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
|
||||
|
||||
texinfo:
|
||||
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
|
||||
@echo
|
||||
@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
|
||||
@echo "Run \`make' in that directory to run these through makeinfo" \
|
||||
"(use \`make info' here to do that automatically)."
|
||||
|
||||
info:
|
||||
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
|
||||
@echo "Running Texinfo files through makeinfo..."
|
||||
make -C $(BUILDDIR)/texinfo info
|
||||
@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
|
||||
|
||||
gettext:
|
||||
$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
|
||||
@echo
|
||||
@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
|
||||
|
||||
changes:
|
||||
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
|
||||
@echo
|
||||
@echo "The overview file is in $(BUILDDIR)/changes."
|
||||
|
||||
linkcheck:
|
||||
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
|
||||
@echo
|
||||
@echo "Link check complete; look for any errors in the above output " \
|
||||
"or in $(BUILDDIR)/linkcheck/output.txt."
|
||||
|
||||
doctest:
|
||||
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
|
||||
@echo "Testing of doctests in the sources finished, look at the " \
|
||||
"results in $(BUILDDIR)/doctest/output.txt."
|
||||
|
||||
xml:
|
||||
$(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
|
||||
@echo
|
||||
@echo "Build finished. The XML files are in $(BUILDDIR)/xml."
|
||||
|
||||
pseudoxml:
|
||||
$(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
|
||||
@echo
|
||||
@echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."
|
258
docs/conf.py
Normal file
|
@ -0,0 +1,258 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# TileServer GL documentation build configuration file, created by
|
||||
# sphinx-quickstart on Thu Aug 4 23:48:49 2016.
|
||||
#
|
||||
# This file is execfile()d with the current directory set to its
|
||||
# containing dir.
|
||||
#
|
||||
# Note that not all possible configuration values are present in this
|
||||
# autogenerated file.
|
||||
#
|
||||
# All configuration values have a default; values that are commented out
|
||||
# serve to show the default.
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
# If extensions (or modules to document with autodoc) are in another directory,
|
||||
# add these directories to sys.path here. If the directory is relative to the
|
||||
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||
#sys.path.insert(0, os.path.abspath('.'))
|
||||
|
||||
# -- General configuration ------------------------------------------------
|
||||
|
||||
# If your documentation needs a minimal Sphinx version, state it here.
|
||||
#needs_sphinx = '1.0'
|
||||
|
||||
# Add any Sphinx extension module names here, as strings. They can be
|
||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
||||
# ones.
|
||||
extensions = []
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
templates_path = ['_templates']
|
||||
|
||||
# The suffix of source filenames.
|
||||
source_suffix = '.rst'
|
||||
|
||||
# The encoding of source files.
|
||||
#source_encoding = 'utf-8-sig'
|
||||
|
||||
# The master toctree document.
|
||||
master_doc = 'index'
|
||||
|
||||
# General information about the project.
|
||||
project = u'TileServer GL'
|
||||
copyright = u'2023, MapTiler.com'
|
||||
|
||||
# The version info for the project you're documenting, acts as replacement for
|
||||
# |version| and |release|, also used in various other places throughout the
|
||||
# built documents.
|
||||
#
|
||||
# The short X.Y version.
|
||||
version = '1'
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
release = '1.0'
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
#language = None
|
||||
|
||||
# There are two options for replacing |today|: either, you set today to some
|
||||
# non-false value, then it is used:
|
||||
#today = ''
|
||||
# Else, today_fmt is used as the format for a strftime call.
|
||||
#today_fmt = '%B %d, %Y'
|
||||
|
||||
# List of patterns, relative to source directory, that match files and
|
||||
# directories to ignore when looking for source files.
|
||||
exclude_patterns = ['_build']
|
||||
|
||||
# The reST default role (used for this markup: `text`) to use for all
|
||||
# documents.
|
||||
#default_role = None
|
||||
|
||||
# If true, '()' will be appended to :func: etc. cross-reference text.
|
||||
#add_function_parentheses = True
|
||||
|
||||
# If true, the current module name will be prepended to all description
|
||||
# unit titles (such as .. function::).
|
||||
#add_module_names = True
|
||||
|
||||
# If true, sectionauthor and moduleauthor directives will be shown in the
|
||||
# output. They are ignored by default.
|
||||
#show_authors = False
|
||||
|
||||
# The name of the Pygments (syntax highlighting) style to use.
|
||||
pygments_style = 'sphinx'
|
||||
|
||||
# A list of ignored prefixes for module index sorting.
|
||||
#modindex_common_prefix = []
|
||||
|
||||
# If true, keep warnings as "system message" paragraphs in the built documents.
|
||||
#keep_warnings = False
|
||||
|
||||
|
||||
# -- Options for HTML output ----------------------------------------------
|
||||
|
||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||
# a list of builtin themes.
|
||||
html_theme = 'default'
|
||||
|
||||
# Theme options are theme-specific and customize the look and feel of a theme
|
||||
# further. For a list of options available for each theme, see the
|
||||
# documentation.
|
||||
#html_theme_options = {}
|
||||
|
||||
# Add any paths that contain custom themes here, relative to this directory.
|
||||
#html_theme_path = []
|
||||
|
||||
# The name for this set of Sphinx documents. If None, it defaults to
|
||||
# "<project> v<release> documentation".
|
||||
#html_title = None
|
||||
|
||||
# A shorter title for the navigation bar. Default is the same as html_title.
|
||||
#html_short_title = None
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top
|
||||
# of the sidebar.
|
||||
#html_logo = None
|
||||
|
||||
# The name of an image file (within the static path) to use as favicon of the
|
||||
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
|
||||
# pixels large.
|
||||
#html_favicon = None
|
||||
|
||||
# Add any paths that contain custom static files (such as style sheets) here,
|
||||
# relative to this directory. They are copied after the builtin static files,
|
||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||
html_static_path = ['_static']
|
||||
|
||||
# Add any extra paths that contain custom files (such as robots.txt or
|
||||
# .htaccess) here, relative to this directory. These files are copied
|
||||
# directly to the root of the documentation.
|
||||
#html_extra_path = []
|
||||
|
||||
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
|
||||
# using the given strftime format.
|
||||
#html_last_updated_fmt = '%b %d, %Y'
|
||||
|
||||
# If true, SmartyPants will be used to convert quotes and dashes to
|
||||
# typographically correct entities.
|
||||
#html_use_smartypants = True
|
||||
|
||||
# Custom sidebar templates, maps document names to template names.
|
||||
#html_sidebars = {}
|
||||
|
||||
# Additional templates that should be rendered to pages, maps page names to
|
||||
# template names.
|
||||
#html_additional_pages = {}
|
||||
|
||||
# If false, no module index is generated.
|
||||
#html_domain_indices = True
|
||||
|
||||
# If false, no index is generated.
|
||||
#html_use_index = True
|
||||
|
||||
# If true, the index is split into individual pages for each letter.
|
||||
#html_split_index = False
|
||||
|
||||
# If true, links to the reST sources are added to the pages.
|
||||
#html_show_sourcelink = True
|
||||
|
||||
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
|
||||
#html_show_sphinx = True
|
||||
|
||||
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
|
||||
#html_show_copyright = True
|
||||
|
||||
# If true, an OpenSearch description file will be output, and all pages will
|
||||
# contain a <link> tag referring to it. The value of this option must be the
|
||||
# base URL from which the finished HTML is served.
|
||||
#html_use_opensearch = ''
|
||||
|
||||
# This is the file name suffix for HTML files (e.g. ".xhtml").
|
||||
#html_file_suffix = None
|
||||
|
||||
# Output file base name for HTML help builder.
|
||||
htmlhelp_basename = 'TileServerGLdoc'
|
||||
|
||||
|
||||
# -- Options for LaTeX output ---------------------------------------------
|
||||
|
||||
latex_elements = {
|
||||
# The paper size ('letterpaper' or 'a4paper').
|
||||
#'papersize': 'letterpaper',
|
||||
|
||||
# The font size ('10pt', '11pt' or '12pt').
|
||||
#'pointsize': '10pt',
|
||||
|
||||
# Additional stuff for the LaTeX preamble.
|
||||
#'preamble': '',
|
||||
}
|
||||
|
||||
# Grouping the document tree into LaTeX files. List of tuples
|
||||
# (source start file, target name, title,
|
||||
# author, documentclass [howto, manual, or own class]).
|
||||
latex_documents = [
|
||||
('index', 'TileServerGL.tex', u'TileServer GL Documentation',
|
||||
u'MapTiler.com', 'manual'),
|
||||
]
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top of
|
||||
# the title page.
|
||||
#latex_logo = None
|
||||
|
||||
# For "manual" documents, if this is true, then toplevel headings are parts,
|
||||
# not chapters.
|
||||
#latex_use_parts = False
|
||||
|
||||
# If true, show page references after internal links.
|
||||
#latex_show_pagerefs = False
|
||||
|
||||
# If true, show URL addresses after external links.
|
||||
#latex_show_urls = False
|
||||
|
||||
# Documents to append as an appendix to all manuals.
|
||||
#latex_appendices = []
|
||||
|
||||
# If false, no module index is generated.
|
||||
#latex_domain_indices = True
|
||||
|
||||
|
||||
# -- Options for manual page output ---------------------------------------
|
||||
|
||||
# One entry per manual page. List of tuples
|
||||
# (source start file, name, description, authors, manual section).
|
||||
man_pages = [
|
||||
('index', 'tileservergl', u'TileServer GL Documentation',
|
||||
[u'MapTiler.com'], 1)
|
||||
]
|
||||
|
||||
# If true, show URL addresses after external links.
|
||||
#man_show_urls = False
|
||||
|
||||
|
||||
# -- Options for Texinfo output -------------------------------------------
|
||||
|
||||
# Grouping the document tree into Texinfo files. List of tuples
|
||||
# (source start file, target name, title, author,
|
||||
# dir menu entry, description, category)
|
||||
texinfo_documents = [
|
||||
('index', 'TileServerGL', u'TileServer GL Documentation',
|
||||
u'MapTiler.com', 'TileServerGL', 'One line description of project.',
|
||||
'Miscellaneous'),
|
||||
]
|
||||
|
||||
# Documents to append as an appendix to all manuals.
|
||||
#texinfo_appendices = []
|
||||
|
||||
# If false, no module index is generated.
|
||||
#texinfo_domain_indices = True
|
||||
|
||||
# How to display URL addresses: 'footnote', 'no', or 'inline'.
|
||||
#texinfo_show_urls = 'footnote'
|
||||
|
||||
# If true, do not generate a @detailmenu in the "Top" node's menu.
|
||||
#texinfo_no_detailmenu = False
|
340
docs/config.rst
Normal file
|
@ -0,0 +1,340 @@
|
|||
==================
|
||||
Configuration file
|
||||
==================
|
||||
|
||||
The configuration file defines the behavior of the application. It's a regular JSON file.
|
||||
|
||||
Example:
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
{
|
||||
"options": {
|
||||
"paths": {
|
||||
"root": "",
|
||||
"fonts": "fonts",
|
||||
"sprites": "sprites",
|
||||
"icons": "icons",
|
||||
"styles": "styles",
|
||||
"mbtiles": "data",
|
||||
"pmtiles": "data",
|
||||
"files": "files"
|
||||
},
|
||||
"domains": [
|
||||
"localhost:8080",
|
||||
"127.0.0.1:8080"
|
||||
],
|
||||
"formatOptions": {
|
||||
"jpeg": {
|
||||
"quality": 80
|
||||
},
|
||||
"webp": {
|
||||
"quality": 90
|
||||
}
|
||||
},
|
||||
"maxScaleFactor": 3,
|
||||
"maxSize": 2048,
|
||||
"pbfAlias": "pbf",
|
||||
"serveAllFonts": false,
|
||||
"serveAllStyles": false,
|
||||
"serveStaticMaps": true,
|
||||
"allowRemoteMarkerIcons": true,
|
||||
"allowInlineMarkerImages": true,
|
||||
"staticAttributionText": "© OpenMapTiles © OpenStreetMaps",
|
||||
"tileMargin": 0
|
||||
},
|
||||
"styles": {
|
||||
"basic": {
|
||||
"style": "basic.json",
|
||||
"tilejson": {
|
||||
"type": "overlay",
|
||||
"bounds": [8.44806, 47.32023, 8.62537, 47.43468]
|
||||
}
|
||||
},
|
||||
"hybrid": {
|
||||
"style": "satellite-hybrid.json",
|
||||
"serve_rendered": false,
|
||||
"tilejson": {
|
||||
"format": "webp"
|
||||
}
|
||||
},
|
||||
"remote": {
|
||||
"style": "https://demotiles.maplibre.org/style.json"
|
||||
}
|
||||
},
|
||||
"data": {
|
||||
"zurich-vector": {
|
||||
"mbtiles": "zurich.mbtiles"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
``options``
|
||||
===========
|
||||
|
||||
``paths``
|
||||
---------
|
||||
|
||||
Defines where to look for the different types of input data.
|
||||
|
||||
The value of ``root`` is used as prefix for all data types.
|
||||
|
||||
``domains``
|
||||
-----------
|
||||
|
||||
You can use this to optionally specify on what domains the rendered tiles are accessible. This can be used for basic load-balancing or to bypass browser's limit for the number of connections per domain.
|
||||
|
||||
``frontPage``
|
||||
-----------------
|
||||
|
||||
Path to the html (relative to ``root`` path) to use as a front page.
|
||||
|
||||
Use ``true`` (or nothing) to serve the default TileServer GL front page with list of styles and data.
|
||||
Use ``false`` to disable the front page altogether (404).
|
||||
|
||||
``formatOptions``
|
||||
-----------------
|
||||
|
||||
You can use this to specify options for the generation of images in the supported file formats.
|
||||
For WebP, the only supported option is ``quality`` [0-100].
|
||||
For JPEG, the only supported options are ``quality`` [0-100] and ``progressive`` [true, false].
|
||||
For PNG, the full set of options `exposed by the sharp library <https://sharp.pixelplumbing.com/api-output#png>`_ is available, except ``force`` and ``colours`` (use ``colors``). If not set, their values are the defaults from ``sharp``.
|
||||
|
||||
For example::
|
||||
|
||||
"formatOptions": {
|
||||
"png": {
|
||||
"palette": true,
|
||||
"colors": 4
|
||||
}
|
||||
}
|
||||
|
||||
Note: ``formatOptions`` replaced the ``formatQuality`` option in previous versions of TileServer GL.
|
||||
|
||||
``maxScaleFactor``
|
||||
-----------
|
||||
|
||||
Maximum scale factor to allow in raster tile and static maps requests (e.g. ``@3x`` suffix).
|
||||
Also see ``maxSize`` below.
|
||||
Default value is ``3``, maximum ``9``.
|
||||
|
||||
``maxSize``
|
||||
-----------
|
||||
|
||||
Maximum image side length to be allowed to be rendered (including scale factor).
|
||||
Be careful when changing this value since there are hardware limits that need to be considered.
|
||||
Default is ``2048``.
|
||||
|
||||
``tileMargin``
|
||||
--------------
|
||||
|
||||
Additional image side length added during tile rendering that is cropped from the delivered tile. This is useful for resolving the issue with cropped labels,
|
||||
but it does come with a performance degradation, because additional, adjacent vector tiles need to be loaded to generate a single tile.
|
||||
Default is ``0`` to disable this processing.
|
||||
|
||||
``minRendererPoolSizes``
|
||||
------------------------
|
||||
|
||||
Minimum amount of raster tile renderers per scale factor.
|
||||
The value is an array: the first element is the minimum amount of renderers for scale factor one, the second for scale factor two and so on.
|
||||
If the array has less elements than ``maxScaleFactor``, then the last element is used for all remaining scale factors as well.
|
||||
Selecting renderer pool sizes is a trade-off between memory use and speed.
|
||||
A reasonable value will depend on your hardware and your amount of styles and scale factors.
|
||||
If you have plenty of memory, you'll want to set this equal to ``maxRendererPoolSizes`` to avoid increased latency due to renderer destruction and recreation.
|
||||
If you need to conserve memory, you'll want something lower than ``maxRendererPoolSizes``, possibly allocating more renderers to scale factors that are more common.
|
||||
Default is ``[8, 4, 2]``.
|
||||
|
||||
``maxRendererPoolSizes``
|
||||
------------------------
|
||||
|
||||
Maximum amount of raster tile renderers per scale factor.
|
||||
The value and considerations are similar to ``minRendererPoolSizes`` above.
|
||||
If you have plenty of memory, try setting these equal to or slightly above your processor count, e.g. if you have four processors, try a value of ``[6]``.
|
||||
If you need to conserve memory, try lower values for scale factors that are less common.
|
||||
Default is ``[16, 8, 4]``.
|
||||
|
||||
``pbfAlias``
|
||||
------------------------
|
||||
|
||||
Some CDNs did not handle .pbf extension as a static file correctly.
|
||||
The default URLs (with .pbf) are always available, but an alternative can be set.
|
||||
An example extension suffix would be ".pbf.pict".
|
||||
|
||||
``serveAllFonts``
|
||||
------------------------
|
||||
|
||||
If this option is enabled, all the fonts from the ``paths.fonts`` will be served.
|
||||
Otherwise only the fonts referenced by available styles will be served.
|
||||
|
||||
``serveAllStyles``
|
||||
------------------------
|
||||
|
||||
If this option is enabled, all the styles from the ``paths.styles`` will be served. (No recursion, only ``.json`` files are used.)
|
||||
The process will also watch for changes in this directory and remove/add more styles dynamically.
|
||||
It is recommended to also use the ``serveAllFonts`` option when using this option.
|
||||
|
||||
``serveStaticMaps``
|
||||
------------------------
|
||||
|
||||
If this option is enabled, all the static map endpoints will be served.
|
||||
Default is ``true``.
|
||||
|
||||
``watermark``
|
||||
-----------
|
||||
|
||||
Optional string to be rendered into the raster tiles and static maps as watermark (bottom-left corner).
|
||||
Not used by default.
|
||||
|
||||
``staticAttributionText``
|
||||
-----------
|
||||
|
||||
Optional string to be rendered in the static images endpoint. Text will be rendered in the bottom-right corner,
|
||||
and styled similar to attribution on web-based maps (text only, links not supported).
|
||||
Not used by default.
|
||||
|
||||
``allowRemoteMarkerIcons``
|
||||
--------------
|
||||
|
||||
Allows the rendering of marker icons fetched via http(s) hyperlinks.
|
||||
For security reasons only allow this if you can control the origins from where the markers are fetched!
|
||||
Default is to disallow fetching of icons from remote sources.
|
||||
|
||||
``allowInlineMarkerImages``
|
||||
--------------
|
||||
Allows the rendering of inline marker icons or base64 urls.
|
||||
For security reasons only allow this if you can control the origins from where the markers are fetched!
|
||||
Not used by default.
|
||||
|
||||
|
||||
``styles``
|
||||
==========
|
||||
|
||||
Each item in this object defines one style (map). It can have the following options:
|
||||
|
||||
* ``style`` -- name of the style json file or url of a remote hosted style [required]
|
||||
* ``serve_rendered`` -- whether to render the raster tiles for this style or not
|
||||
* ``serve_data`` -- whether to allow access to the original tiles, sprites and required glyphs
|
||||
* ``tilejson`` -- properties to add to the TileJSON created for the raster data
|
||||
|
||||
* ``format`` and ``bounds`` can be especially useful
|
||||
|
||||
``data``
|
||||
========
|
||||
|
||||
Each item specifies one data source which should be made accessible by the server. It has to have one of the following options:
|
||||
|
||||
* ``mbtiles`` -- name of the mbtiles file
|
||||
* ``pmtiles`` -- name of the pmtiles file or url.
|
||||
|
||||
For example::
|
||||
|
||||
"data": {
|
||||
"source1": {
|
||||
"mbtiles": "source1.mbtiles"
|
||||
},
|
||||
"source2": {
|
||||
"pmtiles": "source2.pmtiles"
|
||||
},
|
||||
"source3": {
|
||||
"pmtiles": "https://foo.lan/source3.pmtiles"
|
||||
}
|
||||
}
|
||||
|
||||
The data source does not need to be specified here unless you explicitly want to serve the raw data.
|
||||
|
||||
Serving Terrain Tiles
|
||||
--------------
|
||||
|
||||
If you serve terrain tiles, it is possible to configure an ``encoding`` with ``mapbox`` or ``terrarium`` to enable a terrain preview mode and the ``elevation`` api for the ``data`` endpoint.
|
||||
|
||||
For example::
|
||||
|
||||
"data": {
|
||||
"terrain1": {
|
||||
"mbtiles": "terrain1.mbtiles",
|
||||
"encoding": "mapbox"
|
||||
},
|
||||
"terrain2": {
|
||||
"pmtiles": "terrain2.pmtiles"
|
||||
"encoding": "terrarium"
|
||||
}
|
||||
}
|
||||
|
||||
Referencing local files from style JSON
|
||||
=======================================
|
||||
|
||||
You can link various data sources from the style JSON (for example even remote TileJSONs).
|
||||
|
||||
MBTiles
|
||||
-------
|
||||
|
||||
To specify that you want to use local mbtiles, use to following syntax: ``mbtiles://source1.mbtiles``.
|
||||
TileServer-GL will try to find the file ``source1.mbtiles`` in ``root`` + ``mbtiles`` path.
|
||||
|
||||
For example::
|
||||
|
||||
"sources": {
|
||||
"source1": {
|
||||
"url": "mbtiles://source1.mbtiles",
|
||||
"type": "vector"
|
||||
}
|
||||
}
|
||||
|
||||
Alternatively, you can use ``mbtiles://{source1}`` to reference existing data object from the config.
|
||||
In this case, the server will look into the ``config.json`` to determine what file to use by data id.
|
||||
For the config above, this is equivalent to ``mbtiles://source1.mbtiles``.
|
||||
|
||||
PMTiles
|
||||
-------
|
||||
|
||||
To specify that you want to use local pmtiles, use to following syntax: ``pmtiles://source2.pmtiles``.
|
||||
TileServer-GL will try to find the file ``source2.pmtiles`` in ``root`` + ``pmtiles`` path.
|
||||
|
||||
To specify that you want to use a url based pmtiles, use to following syntax: ``pmtiles://https://foo.lan/source3.pmtiles``.
|
||||
|
||||
For example::
|
||||
|
||||
"sources": {
|
||||
"source2": {
|
||||
"url": "pmtiles://source2.pmtiles",
|
||||
"type": "vector"
|
||||
},
|
||||
"source3": {
|
||||
"url": "pmtiles://https://foo.lan/source3.pmtiles",
|
||||
"type": "vector"
|
||||
}
|
||||
}
|
||||
|
||||
Alternatively, you can use ``pmtiles://{source2}`` to reference existing data object from the config.
|
||||
In this case, the server will look into the ``config.json`` to determine what file to use by data id.
|
||||
For the config above, this is equivalent to ``pmtiles://source2.mbtiles``.
|
||||
|
||||
Sprites
|
||||
-------
|
||||
|
||||
If your style requires any sprites, make sure the style JSON contains proper path in the ``sprite`` property.
|
||||
|
||||
It can be a local path (e.g. ``my-style/sprite``) or remote http(s) location (e.g. ``https://mycdn.com/my-style/sprite``). Several possible extension are added to this path, so the following files should be present:
|
||||
|
||||
* ``sprite.json``
|
||||
* ``sprite.png``
|
||||
* ``sprite@2x.json``
|
||||
* ``sprite@2x.png``
|
||||
|
||||
You can also use the following placeholders in the sprite path for easier use:
|
||||
|
||||
* ``{style}`` -- gets replaced with the name of the style file (``xxx.json``)
|
||||
* ``{styleJsonFolder}`` -- gets replaced with the path to the style file
|
||||
|
||||
Fonts (glyphs)
|
||||
--------------
|
||||
|
||||
Similarly to the sprites, the style JSON also needs to contain proper paths to the font glyphs (in the ``glyphs`` property) and can be both local and remote.
|
||||
|
||||
It should contain the following placeholders:
|
||||
|
||||
* ``{fontstack}`` -- name of the font and variant
|
||||
* ``{range}`` -- range of the glyphs
|
||||
|
||||
For example ``"glyphs": "{fontstack}/{range}.pbf"`` will instruct TileServer-GL to look for the files such as ``fonts/Open Sans/0-255.pbf`` (``fonts`` come from the ``paths`` property of the ``config.json`` example above).
|
137
docs/deployment.rst
Normal file
|
@ -0,0 +1,137 @@
|
|||
==========
|
||||
Deployment
|
||||
==========
|
||||
|
||||
Typically, you should use nginx, lighttpd or apache on the frontend. The tileserver-gl server is hidden behind it in production deployment.
|
||||
|
||||
Caching
|
||||
=======
|
||||
|
||||
There is a plenty of options you can use to create proper caching infrastructure: Varnish, Cloudflare, ...
|
||||
|
||||
Cloudflare Cache Rules
|
||||
-----------
|
||||
|
||||
Cloudflare supports custom rules for configuring caching:
|
||||
https://developers.cloudflare.com/cache/about/cache-rules/
|
||||
|
||||
tileserver-gl renders tiles in multiple formats - ``.png``, ``.jpg (jpeg)``, ``.webp`` for the raster endpoints, ``.pbf`` for vector endpoint. In addition, style information is generated with ``.json`` format.
|
||||
|
||||
Endpoint data can be configured to be cached by Cloudflare. For example to cache vector endpoint you will need to configure Cloudflare rules for the ``.pbf`` and ``.json`` data.
|
||||
|
||||
Create a rule which matches ``hostname (equal)`` and ``URI Path (ends with)`` for ``.pbf`` and ``.json`` fields. Set cache status to eligible for cache to enable the caching and overwrite the ``Edge TTL`` with ``Browser TTL`` to be 7 days (depends on your application usage).
|
||||
|
||||
This will ensure that your tiles are cached on the client side and by Cloudflare for seven days. If the tileserver is down or user has no internet access it will try to use cached tiles.
|
||||
|
||||
Note that ``Browser TTL`` will overwrite expiration dates on the client device. If you rebuild your maps, old tiles will be rendered until it expires or cache is cleared on the client device.
|
||||
|
||||
Nginx Cache
|
||||
-----------
|
||||
|
||||
If you have a reverse proxy setup in front of the tileserver you may want to enable caching as it will greatly offload requests from the application.
|
||||
|
||||
Configure the proxy cache path directive to initialize your cache store:
|
||||
|
||||
::
|
||||
|
||||
proxy_cache_path /var/cache/nginx/tileserver
|
||||
keys_zone=TileserverCache:50m
|
||||
levels=1:2
|
||||
inactive=2w
|
||||
max_size=10g;
|
||||
|
||||
Make sure to give proper permissions for the /var/cache/nginx/tileserver folder. Usually nginx is running with www-data user.
|
||||
Enable caching on specific proxy pass:
|
||||
|
||||
::
|
||||
|
||||
location / {
|
||||
include proxy_params;
|
||||
proxy_pass http://127.0.0.1:8080/;
|
||||
|
||||
proxy_cache TileserverCache;
|
||||
proxy_cache_valid 200 1w;
|
||||
|
||||
# add_header X-Cache-Status $upstream_cache_status;
|
||||
}
|
||||
|
||||
If you need to confirm whether caching works or not, uncomment the X-Cache-Status header. This will return a header on response with `HIT` or `MISS` header value which indicates if nginx cached the response or not.
|
||||
|
||||
Make sure to clean your cache by removing files in the configured directory after you change your styles or tile information. You may experiment with the caching values to fit your needs.
|
||||
|
||||
More about Nginx caching: https://docs.nginx.com/nginx/admin-guide/content-cache/content-caching/
|
||||
|
||||
Securing
|
||||
========
|
||||
|
||||
Nginx can be used to add protection via https, password, referrer, IP address restriction, access keys, etc.
|
||||
|
||||
Running behind a proxy or a load-balancer
|
||||
=========================================
|
||||
|
||||
If you need to run TileServer GL behind a proxy, make sure the proxy sends ``X-Forwarded-*`` headers to the server (most importantly ``X-Forwarded-Host`` and ``X-Forwarded-Proto``) to ensure the URLs generated inside TileJSON, etc. are using the desired domain and protocol.
|
||||
|
||||
Nginx Reverse Proxy
|
||||
-----------
|
||||
|
||||
An example nginx reverse proxy server configuration for HTTPS connections. It enables caching, CORS and Cloudflare Authenticated Pulls.
|
||||
|
||||
::
|
||||
|
||||
proxy_cache_path /var/cache/nginx/tileserver
|
||||
keys_zone=TileserverCache:50m
|
||||
levels=1:2
|
||||
inactive=2w
|
||||
max_size=1g;
|
||||
|
||||
map_hash_bucket_size 128;
|
||||
map $http_origin $allow_origin {
|
||||
https://www.example.com $http_origin;
|
||||
default "";
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
listen [::]:443 ssl http2;
|
||||
|
||||
ssl_certificate /etc/ssl/www.example.com/cert.pem;
|
||||
ssl_certificate_key /etc/ssl/www.example.com/key.pem;
|
||||
|
||||
# https://developers.cloudflare.com/ssl/origin-configuration/authenticated-origin-pull/
|
||||
ssl_client_certificate /etc/ssl/cloudflare.pem;
|
||||
ssl_verify_client on;
|
||||
|
||||
server_name www.example.com example.com;
|
||||
|
||||
# Disable root application access. You may want to allow this in development.
|
||||
location ~ ^/$ {
|
||||
return 404;
|
||||
}
|
||||
|
||||
# Disable root application access. You may want to allow this in development.
|
||||
location /favicon.ico {
|
||||
return 404;
|
||||
}
|
||||
|
||||
location / {
|
||||
# This include directive sets up required headers for proxy and proxy cache.
|
||||
# As well it includes the required ``X-Forwarded-*`` headers for tileserver to properly generate tiles.
|
||||
include proxy_params;
|
||||
|
||||
proxy_pass http://127.0.0.1:8080/;
|
||||
|
||||
# Disable default CORS headers
|
||||
proxy_hide_header Access-Control-Allow-Origin;
|
||||
|
||||
# Enable proxy cache
|
||||
proxy_cache TileserverCache;
|
||||
proxy_cache_valid 200 1w;
|
||||
|
||||
# Set our custom CORS
|
||||
add_header 'Access-Control-Allow-Origin' $allow_origin;
|
||||
|
||||
# If you need to see nginx cache status. Uncomment line below.
|
||||
# add_header X-Cache-Status $upstream_cache_status;
|
||||
}
|
||||
}
|
||||
|
140
docs/endpoints.rst
Normal file
|
@ -0,0 +1,140 @@
|
|||
===================
|
||||
Available endpoints
|
||||
===================
|
||||
|
||||
If you visit the server on the configured port (default 8080) you can see your maps appearing in the browser.
|
||||
|
||||
Styles
|
||||
======
|
||||
* Styles are served at ``/styles/{id}/style.json`` (+ array at ``/styles.json``)
|
||||
|
||||
* Sprites at ``/styles/{id}/sprite[/spriteID][@2x].{format}``
|
||||
* Fonts at ``/fonts/{fontstack}/{start}-{end}.pbf``
|
||||
|
||||
Rendered tiles
|
||||
==============
|
||||
* Rendered tiles are served at ``/styles/{id}[/{tileSize}]/{z}/{x}/{y}[@2x].{format}``
|
||||
|
||||
* The optional ratio ``@2x`` (ex. ``@2x``, ``@3x``, ``@4x``) part can be used to render HiDPI (retina) tiles
|
||||
* The optional tile size ``/{tileSize}`` (ex. ``/256``, ``/512``). if omitted, tileSize defaults to 256.
|
||||
* Available formats: ``png``, ``jpg`` (``jpeg``), ``webp``
|
||||
* TileJSON at ``/styles[/{tileSize}]/{id}.json``
|
||||
|
||||
* The rendered tiles are not available in the ``tileserver-gl-light`` version.
|
||||
|
||||
WMTS Capabilities
|
||||
==============
|
||||
* WMTS Capabilities are served at ``/styles/{id}/wmts.xml``
|
||||
|
||||
Static images
|
||||
=============
|
||||
* Several endpoints:
|
||||
|
||||
* ``/styles/{id}/static/{lon},{lat},{zoom}[@{bearing}[,{pitch}]]/{width}x{height}[@2x].{format}`` (center-based)
|
||||
* ``/styles/{id}/static/{minx},{miny},{maxx},{maxy}/{width}x{height}[@2x].{format}`` (area-based)
|
||||
* ``/styles/{id}/static/auto/{width}x{height}[@2x].{format}`` (autofit path -- see below)
|
||||
|
||||
* All the static image endpoints additionally support following query parameters:
|
||||
|
||||
* ``path`` - ``((fill|stroke|width)\:[^\|]+\|)*(enc:.+|-?\d+(\.\d*)?,-?\d+(\.\d*)?(\|-?\d+(\.\d*)?,-?\d+(\.\d*)?)+)``
|
||||
|
||||
* comma-separated ``lng,lat``, pipe-separated pairs
|
||||
|
||||
* e.g. ``path=5.9,45.8|5.9,47.8|10.5,47.8|10.5,45.8|5.9,45.8``
|
||||
|
||||
* `Google Encoded Polyline Format <https://developers.google.com/maps/documentation/utilities/polylinealgorithm>`_
|
||||
|
||||
* e.g. ``path=enc:_p~iF~ps|U_ulLnnqC_mqNvxq`@``
|
||||
* If 'enc:' is used, the rest of the path parameter is considered to be part of the encoded polyline string -- do not specify the coordinate pairs.
|
||||
|
||||
* With options (fill|stroke|width)
|
||||
|
||||
* e.g. ``path=stroke:yellow|width:2|fill:green|5.9,45.8|5.9,47.8|10.5,47.8|10.5,45.8|5.9,45.8`` or ``path=stroke:blue|width:1|fill:yellow|enc:_p~iF~ps|U_ulLnnqC_mqNvxq`@``
|
||||
|
||||
* can be provided multiple times
|
||||
|
||||
* ``latlng`` - indicates coordinates are in ``lat,lng`` order rather than the usual ``lng,lat`` for paths and markers
|
||||
* ``fill`` - default color to use as the fill (e.g. ``red``, ``rgba(255,255,255,0.5)``, ``#0000ff``) for all paths
|
||||
* ``stroke`` - default color of the path stroke for all paths
|
||||
* ``width`` - default width of the stroke for all paths
|
||||
* ``linecap`` - rendering style for the start and end points of all paths - see https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineCap
|
||||
* ``linejoin`` - rendering style for joining successive segments of all paths when the direction changes - see https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineJoin
|
||||
* ``border`` - color of the optional border stroke for all paths ; the border is like a halo around the stroke
|
||||
* ``borderwidth`` - width of the border stroke (default 10% of stroke width) for all paths
|
||||
* ``marker`` - Marker in format ``lng,lat|iconPath|option|option|...``
|
||||
|
||||
* Will be rendered with the bottom center at the provided location
|
||||
* ``lng,lat`` and ``iconPath`` are mandatory and icons won't be rendered without them
|
||||
* ``iconPath`` is either a link to an image served via http(s) or a path to a file relative to the configured icon path
|
||||
* ``option`` must adhere to the format ``optionName:optionValue`` and supports the following names
|
||||
|
||||
* ``scale`` - Factor to scale image by
|
||||
|
||||
* e.g. ``0.5`` - Scales the image to half it's original size
|
||||
|
||||
* ``offset`` - Image offset as positive or negative pixel value in format ``[offsetX],[offsetY]``
|
||||
|
||||
* scales with ``scale`` parameter since image placement is relative to it's size
|
||||
* e.g. ``2,-4`` - Image will be moved 2 pixel to the right and 4 pixel in the upwards direction from the provided location
|
||||
|
||||
* e.g. ``5.9,45.8|marker-icon.png|scale:0.5|offset:2,-4``
|
||||
* can be provided multiple times
|
||||
|
||||
* ``padding`` - "percentage" padding for fitted endpoints (area-based and path autofit)
|
||||
|
||||
* value of ``0.1`` means "add 10% size to each side to make sure the area of interest is nicely visible"
|
||||
|
||||
* ``maxzoom`` - Maximum zoom level (only for auto endpoint where zoom level is calculated and not provided)
|
||||
|
||||
* You can also use (experimental) ``/styles/{id}/static/raw/...`` endpoints with raw spherical mercator coordinates (EPSG:3857) instead of WGS84.
|
||||
|
||||
* The static images are not available in the ``tileserver-gl-light`` version.
|
||||
|
||||
Source data
|
||||
===========
|
||||
* Source data are served at ``/data/{id}/{z}/{x}/{y}.{format}``
|
||||
|
||||
* Format depends on the source file (usually ``png`` or ``pbf``)
|
||||
|
||||
* ``geojson`` is also available (useful for inspecting the tiles) in case the original format is ``pbf``
|
||||
|
||||
* TileJSON at ``/data/{id}.json``
|
||||
|
||||
* If terrain mbtile data is served and ``encoding`` is configured (see config) the elevation can be queried
|
||||
|
||||
* by ``/data/{id}/elevation/{z}/{x}/{y}`` for the tile
|
||||
|
||||
* or ``/data/{id}/elevation/{z}/{long}/{lat}`` for the coordinate
|
||||
|
||||
* the result will be a json object like ``{"z":7,"x":68,"y":45,"red":134,"green":66,"blue":0,"latitude":11.84069,"longitude":46.04798,"elevation":1602}``
|
||||
|
||||
* The elevation api is not available in the ``tileserver-gl-light`` version.
|
||||
|
||||
Static files
|
||||
===========
|
||||
* Static files are served at ``/files/{filename}``
|
||||
|
||||
* The source folder can be configured (``options.paths.files``), default is ``public/files``
|
||||
|
||||
* This feature can be used to serve ``geojson`` files for styles and rendered tiles.
|
||||
|
||||
* Keep in mind, that each rendered tile loads the whole geojson file, if performance matters a conversion to a tiled format (e.g. with https://github.com/felt/tippecanoe)may be a better approch.
|
||||
|
||||
* Use ``file://{filename}`` to have matching paths for both endoints
|
||||
|
||||
TileJSON arrays
|
||||
===============
|
||||
Array of all TileJSONs is at ``[/{tileSize}]/index.json`` (``[/{tileSize}]/rendered.json``; ``/data.json``)
|
||||
|
||||
* The optional tile size ``/{tileSize}`` (ex. ``/256``, ``/512``). if omitted, tileSize defaults to 256.
|
||||
|
||||
List of available fonts
|
||||
=======================
|
||||
Array of names of the available fonts is at ``/fonts.json``
|
||||
|
||||
Health check
|
||||
============
|
||||
Endpoint reporting health status is at ``/health`` and currently returns:
|
||||
|
||||
* ``503`` Starting - for a short period before everything is initialized
|
||||
* ``200`` OK - when the server is running
|
28
docs/index.rst
Normal file
|
@ -0,0 +1,28 @@
|
|||
.. TileServer GL documentation master file, created by
|
||||
sphinx-quickstart on Thu Aug 4 23:48:49 2016.
|
||||
You can adapt this file completely to your liking, but it should at least
|
||||
contain the root `toctree` directive.
|
||||
|
||||
Welcome to TileServer GL's documentation!
|
||||
=========================================
|
||||
|
||||
Contents:
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
installation
|
||||
usage
|
||||
config
|
||||
deployment
|
||||
endpoints
|
||||
|
||||
|
||||
|
||||
Indices and tables
|
||||
==================
|
||||
|
||||
* :ref:`genindex`
|
||||
* :ref:`modindex`
|
||||
* :ref:`search`
|
||||
|
64
docs/installation.rst
Normal file
|
@ -0,0 +1,64 @@
|
|||
============
|
||||
Installation
|
||||
============
|
||||
|
||||
Docker
|
||||
======
|
||||
|
||||
When running docker image, no special installation is needed -- the docker will automatically download the image if not present.
|
||||
|
||||
Just run ``docker run --rm -it -v $(pwd):/data -p 8080:8080 maptiler/tileserver-gl``.
|
||||
|
||||
Additional options (see :doc:`/usage`) can be passed to the TileServer GL by appending them to the end of this command. You can, for example, do the following:
|
||||
|
||||
* ``docker run ... maptiler/tileserver-gl --file my-tiles.mbtiles`` -- explicitly specify which mbtiles to use (if you have more in the folder)
|
||||
* ``docker run ... maptiler/tileserver-gl --verbose`` -- to see the default config created automatically
|
||||
|
||||
npm
|
||||
===
|
||||
|
||||
npm is supported on the following platforms with `Native Dependencies <#id1>`_ installed.
|
||||
|
||||
- Operating systems:
|
||||
|
||||
- Ubuntu 22.04 (x64/arm64)
|
||||
- macOS 14 (x64/arm64)
|
||||
- Windows (x64)
|
||||
|
||||
- Node.js 18,20
|
||||
|
||||
Install globally from npmjs.
|
||||
------------------------------
|
||||
::
|
||||
|
||||
npm install -g tileserver-gl
|
||||
tileserver-gl
|
||||
|
||||
Install locally from source
|
||||
-------------------
|
||||
::
|
||||
|
||||
git clone https://github.com/maptiler/tileserver-gl.git
|
||||
cd tileserver-gl
|
||||
npm install
|
||||
node .
|
||||
|
||||
Native dependencies
|
||||
-------------------
|
||||
|
||||
Ubuntu 22.04 (x64/arm64)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
- apt install build-essential pkg-config xvfb libglfw3-dev libuv1-dev libjpeg-turbo8 libicu70 libcairo2-dev libpango1.0-dev libjpeg-dev libgif-dev librsvg2-dev gir1.2-rsvg-2.0 librsvg2-2 librsvg2-common libcurl4-openssl-dev libpixman-1-dev libpixman-1-0
|
||||
|
||||
MacOS 14 (x64/arm64)
|
||||
~~~~~~~~~~~~~~~~~~~~~~
|
||||
- brew install pkg-config cairo pango libpng jpeg giflib librsvg
|
||||
|
||||
Windows (x64)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
- `Microsoft Visual C++ Redistributable <https://aka.ms/vs/17/release/vc_redist.x64.exe>`_
|
||||
|
||||
``tileserver-gl-light`` on npm
|
||||
==============================
|
||||
|
||||
Alternatively, you can use ``tileserver-gl-light`` package instead, which is pure javascript (does not have any native dependencies) and can run anywhere, but does not contain rasterization features.
|
48
docs/usage.rst
Normal file
|
@ -0,0 +1,48 @@
|
|||
=====
|
||||
Usage
|
||||
=====
|
||||
|
||||
Getting started
|
||||
======
|
||||
::
|
||||
|
||||
Usage: main.js tileserver-gl [file] [options]
|
||||
|
||||
Options:
|
||||
--file <file> MBTiles or PMTiles file
|
||||
ignored if the configuration file is also specified
|
||||
--mbtiles <file> (DEPRECIATED) MBTiles file
|
||||
ignored if file is also specified
|
||||
ignored if the configuration file is also specified
|
||||
-c, --config <file> Configuration file [config.json] (default: "config.json")
|
||||
-b, --bind <address> Bind address
|
||||
-p, --port <port> Port [8080] (default: 8080)
|
||||
-C|--no-cors Disable Cross-origin resource sharing headers
|
||||
-u|--public_url <url> Enable exposing the server on subpaths, not necessarily the root of the domain
|
||||
-V, --verbose More verbose output
|
||||
-s, --silent Less verbose output
|
||||
-l|--log_file <file> output log file (defaults to standard out)
|
||||
-f|--log_format <format> define the log format: https://github.com/expressjs/morgan#morganformat-options
|
||||
-v, --version output the version number
|
||||
-h, --help display help for command
|
||||
|
||||
|
||||
Default preview style and configuration
|
||||
======
|
||||
|
||||
- If no configuration file is specified, a default preview style (compatible with openmaptiles) is used.
|
||||
- If no data file is specified (and is not found in the current working directory), a sample file is downloaded (showing the Zurich area)
|
||||
|
||||
Reloading the configuration
|
||||
======
|
||||
|
||||
It is possible to reload the configuration file without restarting the whole process by sending a SIGHUP signal to the node process.
|
||||
|
||||
- The `docker kill -s HUP tileserver-gl` command can be used when running the tileserver-gl docker container.
|
||||
- The `docker-compose kill -s HUP tileserver-gl-service-name` can be used when tileserver-gl is run as a docker-compose service.
|
||||
|
||||
Docker and `--port`
|
||||
======
|
||||
|
||||
When running tileserver-gl in a Docker container, using the `--port` option would make the container incorrectly seem unhealthy.
|
||||
Instead, it is advised to use Docker's port mapping and map the default port 8080 to the desired external port.
|
4
lint-staged.config.cjs
Normal file
|
@ -0,0 +1,4 @@
|
|||
module.exports = {
|
||||
'*.{js,ts}': 'npm run lint:js',
|
||||
'*.{yml}': 'npm run lint:yml',
|
||||
};
|
11258
package-lock.json
generated
Normal file
117
package.json
|
@ -1,37 +1,98 @@
|
|||
{
|
||||
"name": "tileserver-gl",
|
||||
"version": "0.0.3",
|
||||
"description": "Map tile server for JSON GL styles - serverside generated raster tiles",
|
||||
"version": "5.2.0",
|
||||
"description": "Map tile server for JSON GL styles - vector and server side generated raster tiles",
|
||||
"main": "src/main.js",
|
||||
"authors": [
|
||||
"Petr Sloup <petr.sloup@klokantech.com>"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/klokantech/tileserver-gl.git"
|
||||
},
|
||||
"bin": "src/main.js",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"test": "mocha test/**.js"
|
||||
"prepare": "npm run copy:maplibre && npm run copy:maplibre-inspect && npm run copy:mapbox-rtl-text && npm run copy:leaflet && npm run copy:leaflet-hash",
|
||||
"copy:maplibre": "copyfiles -EVf node_modules/maplibre-gl/dist/maplibre-gl.js node_modules/maplibre-gl/dist/maplibre-gl.js.map node_modules/maplibre-gl/dist/maplibre-gl.css public/resources/",
|
||||
"copy:maplibre-inspect": "copyfiles -EVf node_modules/@maplibre/maplibre-gl-inspect/dist/maplibre-gl-inspect.js node_modules/@maplibre/maplibre-gl-inspect/dist/maplibre-gl-inspect.js.map node_modules/@maplibre/maplibre-gl-inspect/dist/maplibre-gl-inspect.css public/resources/",
|
||||
"copy:mapbox-rtl-text": "copyfiles -EVf node_modules/@mapbox/mapbox-gl-rtl-text/dist/mapbox-gl-rtl-text.js public/resources/",
|
||||
"copy:leaflet": "copyfiles -EVf node_modules/leaflet/dist/leaflet.js node_modules/leaflet/dist/leaflet.js.map node_modules/leaflet/dist/leaflet.css node_modules/leaflet/dist/leaflet-hash.js public/resources/",
|
||||
"copy:leaflet-hash": "copyfiles -EVf node_modules/leaflet-hash/leaflet-hash.js public/resources/",
|
||||
"test": "mocha test/**.js --timeout 10000 --exit",
|
||||
"test-docker": "xvfb-run npm test",
|
||||
"lint:yml": "yamllint --schema=CORE_SCHEMA *.{yml,yaml}",
|
||||
"lint:js": "npm run lint:eslint && npm run lint:prettier",
|
||||
"lint:js:fix": "npm run lint:eslint:fix && npm run lint:prettier:fix",
|
||||
"lint:eslint": "eslint \"{,!(node_modules|dist|static|public)/**/}*.{js,ts,cjs,mjs}\" --ignore-path .gitignore",
|
||||
"lint:eslint:fix": "eslint --fix \"{,!(node_modules|dist|static|public)/**/}*.{js,ts,cjs,mjs}\" --ignore-path .gitignore",
|
||||
"lint:prettier": "prettier --check \"{,!(node_modules|dist|static|public)/**/}*.{js,ts,cjs,mjs,json}\" --ignore-path .gitignore",
|
||||
"lint:prettier:fix": "prettier --write \"{,!(node_modules|dist|static|public)/**/}*.{js,ts,cjs,mjs,json}\" --ignore-path .gitignore",
|
||||
"docker": "docker build . && docker run --rm -i -p 8080:8080 $(docker build -q .)"
|
||||
},
|
||||
"dependencies": {
|
||||
"async": "1.5.2",
|
||||
"advanced-pool": "0.3.1",
|
||||
"clone": "1.0.2",
|
||||
"color": "0.11.1",
|
||||
"cors": "2.7.1",
|
||||
"express": "4.13.4",
|
||||
"handlebars": "4.0.5",
|
||||
"mapbox-gl-native": "3.1.2",
|
||||
"mbtiles": "0.9.0",
|
||||
"morgan": "1.7.0",
|
||||
"nomnom": "1.8.1",
|
||||
"request": "2.72.0",
|
||||
"sharp": "0.14.1",
|
||||
"sphericalmercator": "1.0.5"
|
||||
"@jsse/pbfont": "^0.2.2",
|
||||
"@mapbox/mapbox-gl-rtl-text": "0.3.0",
|
||||
"@mapbox/mbtiles": "0.12.1",
|
||||
"@mapbox/polyline": "^1.2.1",
|
||||
"@mapbox/sphericalmercator": "1.2.0",
|
||||
"@mapbox/vector-tile": "2.0.3",
|
||||
"@maplibre/maplibre-gl-inspect": "1.7.0",
|
||||
"@maplibre/maplibre-gl-native": "6.0.0",
|
||||
"@maplibre/maplibre-gl-style-spec": "20.3.1",
|
||||
"@sindresorhus/fnv1a": "3.1.0",
|
||||
"advanced-pool": "0.3.3",
|
||||
"axios": "^1.8.2",
|
||||
"canvas": "3.0.1",
|
||||
"chokidar": "3.6.0",
|
||||
"clone": "2.1.2",
|
||||
"color": "4.2.3",
|
||||
"commander": "12.1.0",
|
||||
"copyfiles": "2.4.1",
|
||||
"cors": "2.8.5",
|
||||
"express": "5.0.1",
|
||||
"handlebars": "4.7.8",
|
||||
"http-shutdown": "1.2.2",
|
||||
"leaflet": "1.9.4",
|
||||
"leaflet-hash": "0.2.1",
|
||||
"maplibre-gl": "4.7.1",
|
||||
"morgan": "1.10.0",
|
||||
"pbf": "4.0.1",
|
||||
"pmtiles": "3.0.7",
|
||||
"proj4": "2.12.1",
|
||||
"sanitize-filename": "1.6.3",
|
||||
"semver": "^7.6.3",
|
||||
"sharp": "0.33.5",
|
||||
"tileserver-gl-styles": "2.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"should": "^8.3.0",
|
||||
"mocha": "^2.4.5",
|
||||
"supertest": "^1.2.0"
|
||||
}
|
||||
"@commitlint/cli": "^19.5.0",
|
||||
"@commitlint/config-conventional": "^19.5.0",
|
||||
"@typescript-eslint/eslint-plugin": "^7.18.0",
|
||||
"@typescript-eslint/parser": "^7.18.0",
|
||||
"chai": "5.1.1",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-jsdoc": "^50.2.2",
|
||||
"eslint-plugin-prettier": "^5.2.1",
|
||||
"eslint-plugin-security": "^1.7.1",
|
||||
"lint-staged": "^15.2.10",
|
||||
"mocha": "^10.7.3",
|
||||
"node-addon-api": "^8",
|
||||
"prettier": "^3.3.3",
|
||||
"should": "^13.2.3",
|
||||
"supertest": "^7.0.0",
|
||||
"yaml-lint": "^1.7.0"
|
||||
},
|
||||
"keywords": [
|
||||
"maptiler",
|
||||
"tileserver-gl",
|
||||
"maplibre-gl",
|
||||
"tileserver"
|
||||
],
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=18.17.0 <23"
|
||||
},
|
||||
"repository": {
|
||||
"url": "git+https://github.com/maptiler/tileserver-gl.git",
|
||||
"type": "git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/maptiler/tileserver-gl/issues"
|
||||
},
|
||||
"homepage": "https://github.com/maptiler/tileserver-gl#readme"
|
||||
}
|
||||
|
|
13
prettier.config.cjs
Normal file
|
@ -0,0 +1,13 @@
|
|||
module.exports = {
|
||||
$schema: 'http://json.schemastore.org/prettierrc',
|
||||
semi: true,
|
||||
arrowParens: 'always',
|
||||
singleQuote: true,
|
||||
trailingComma: 'all',
|
||||
bracketSpacing: true,
|
||||
htmlWhitespaceSensitivity: 'css',
|
||||
insertPragma: false,
|
||||
tabWidth: 2,
|
||||
useTabs: false,
|
||||
endOfLine: 'lf',
|
||||
};
|
8
public/files/index.html
Normal file
|
@ -0,0 +1,8 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title></title>
|
||||
</head>
|
||||
<body>
|
||||
</body>
|
||||
</html>
|
51
public/resources/elevation-control.js
Normal file
|
@ -0,0 +1,51 @@
|
|||
class ElevationInfoControl {
|
||||
constructor(options) {
|
||||
this.url = options["url"];
|
||||
}
|
||||
|
||||
getDefaultPosition() {
|
||||
const defaultPosition = "bottom-left";
|
||||
return defaultPosition;
|
||||
}
|
||||
|
||||
onAdd(map) {
|
||||
this.map = map;
|
||||
this.controlContainer = document.createElement("div");
|
||||
this.controlContainer.classList.add("maplibregl-ctrl");
|
||||
this.controlContainer.classList.add("maplibregl-ctrl-group");
|
||||
this.controlContainer.classList.add("maplibre-ctrl-elevation");
|
||||
this.controlContainer.textContent = "Elevation: Click on Map";
|
||||
|
||||
map.on('click', (e) => {
|
||||
var url = this.url;
|
||||
var coord = {"z": Math.floor(map.getZoom()), "x": e.lngLat["lng"].toFixed(7), "y": e.lngLat["lat"].toFixed(7)};
|
||||
for(var key in coord) {
|
||||
url = url.replace(new RegExp('{'+ key +'}','g'), coord[key]);
|
||||
}
|
||||
|
||||
let request = new XMLHttpRequest();
|
||||
request.open("GET", url, true);
|
||||
request.onload = () => {
|
||||
if (request.status !== 200) {
|
||||
this.controlContainer.textContent = "Elevation: No value";
|
||||
} else {
|
||||
this.controlContainer.textContent = `Elevation: ${JSON.parse(request.responseText).elevation} (${JSON.stringify(coord)})`;
|
||||
}
|
||||
}
|
||||
request.send();
|
||||
});
|
||||
return this.controlContainer;
|
||||
}
|
||||
|
||||
onRemove() {
|
||||
if (
|
||||
!this.controlContainer ||
|
||||
!this.controlContainer.parentNode ||
|
||||
!this.map
|
||||
) {
|
||||
return;
|
||||
}
|
||||
this.controlContainer.parentNode.removeChild(this.controlContainer);
|
||||
this.map = undefined;
|
||||
}
|
||||
};
|
BIN
public/resources/favicon.ico
Normal file
After Width: | Height: | Size: 5.3 KiB |
BIN
public/resources/fonts/OpenSans-Bold.ttf
Normal file
BIN
public/resources/fonts/OpenSans-Italic.ttf
Normal file
BIN
public/resources/fonts/OpenSans-Regular.ttf
Normal file
BIN
public/resources/images/header-map-1280px.png
Normal file
After Width: | Height: | Size: 171 KiB |
BIN
public/resources/images/layers-2x.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
public/resources/images/layers.png
Normal file
After Width: | Height: | Size: 696 B |
BIN
public/resources/images/logo.png
Normal file
After Width: | Height: | Size: 6.3 KiB |
67
public/resources/images/maptiler-logo.svg
Normal file
|
@ -0,0 +1,67 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 15.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="2034.203px" height="552.055px" viewBox="0 0 2034.203 552.055" enable-background="new 0 0 2034.203 552.055"
|
||||
xml:space="preserve">
|
||||
<g>
|
||||
<path fill="#3A1888" d="M3.604-242.717"/>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path fill="#3A1888" d="M152.645,436.647c25.674,25.668,94.015,95.335,93.983,95.406c-0.249,0.454,67.892-67.963,95.032-95.087
|
||||
l-94.67-94.665L152.645,436.647z"/>
|
||||
<path fill="#03A1C4" d="M246.99,342.301l94.67,94.665c0.141-0.157,0.314-0.336,0.466-0.477l94.578-94.583l-94.66-94.662
|
||||
L246.99,342.301z"/>
|
||||
<path fill="#05D0DF" d="M436.704,341.907l0.243-0.244c52.317-52.312,52.36-137.096,0.157-189.473l-95.06,95.055L436.704,341.907z"
|
||||
/>
|
||||
<path fill="#761FE8" d="M151.931,247.245l-94.329,94.326c0.027,0.032,0.043,0.064,0.076,0.092l94.811,94.827
|
||||
c0.054,0.049,0.108,0.098,0.157,0.157l94.345-94.346L151.931,247.245z"/>
|
||||
<path fill="#FFAA01" d="M246.99,152.184l95.054,95.061l95.06-95.055c-0.076-0.054-0.103-0.108-0.157-0.162l-94.821-94.816
|
||||
c-0.022-0.027-0.054-0.054-0.082-0.081L246.99,152.184z"/>
|
||||
<path fill="#F1175D" d="M57.201,152.514c-51.852,52.377-51.722,136.848,0.4,189.057l94.329-94.326L57.201,152.514z"/>
|
||||
<path fill="#FB3A1B" d="M246.99,152.184L152.255,57.45l-94.578,94.578c-0.163,0.162-0.309,0.336-0.476,0.486l94.729,94.73
|
||||
L246.99,152.184z"/>
|
||||
<path fill="#FBC935" d="M342.044,57.13C289.663,4.846,204.832,4.874,152.488,57.211l-0.233,0.238l94.735,94.734L342.044,57.13z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path fill="#333359" d="M734.146,365.616v-96.875c0-23.851-12.479-45.492-37.077-45.492c-24.224,0-38.517,21.642-38.517,45.492
|
||||
v96.875h-44.761V184.347h41.46l3.301,22.021c9.542-18.353,30.458-24.949,47.685-24.949c21.669,0,43.306,8.811,53.588,33.754
|
||||
c16.144-25.685,37.066-33.022,60.537-33.022c51.38,0,76.692,31.551,76.692,85.859v97.605h-44.767V268.01
|
||||
c0-23.84-9.904-44.037-34.106-44.037c-24.234,0-39.279,20.917-39.279,44.768v96.875H734.146z"/>
|
||||
<path fill="#333359" d="M1086.026,184.726h42.938v180.89h-42.208l-2.208-26.41c-10.266,21.269-38.516,31.535-58.702,31.914
|
||||
c-53.556,0.368-93.198-32.655-93.198-96.137c0-62.375,41.477-95.029,94.313-94.662c24.212,0,47.321,11.371,57.587,29.354
|
||||
L1086.026,184.726z M977.416,274.983c0,34.479,23.85,55.039,53.573,55.039c70.446,0,70.446-109.713,0-109.713
|
||||
C1001.266,220.309,977.416,240.496,977.416,274.983z"/>
|
||||
<path fill="#333359" d="M1166.756,441.214V184.726h41.839l2.923,24.949c13.951-20.187,38.175-28.991,58.719-28.991
|
||||
c55.753,0,92.835,41.471,92.835,94.667c0,52.847-33.401,94.675-91.374,94.675c-19.065,0-47.332-5.888-60.18-25.695v96.884
|
||||
H1166.756z M1318.305,275.351c0-28.253-19.082-51.378-51.37-51.378c-32.298,0-51.38,23.125-51.38,51.378
|
||||
c0,28.244,20.922,51.38,51.38,51.38C1297.404,326.731,1318.305,303.595,1318.305,275.351z"/>
|
||||
<path fill="#333359" d="M1443.064,129.682v54.665h61.642v15.046h-61.642v110.453c0,24.575,5.146,41.823,33.392,41.823
|
||||
c8.805,0,18.709-2.938,27.882-7.339l6.24,14.666c-11.382,5.521-22.763,9.185-34.122,9.185c-38.527,0-51.002-22.752-51.002-58.335
|
||||
V199.393h-38.538v-15.046h38.538v-52.831L1443.064,129.682z"/>
|
||||
<path fill="#333359" d="M1570.027,125.272c0,19.082-28.986,19.082-28.986,0C1541.041,106.2,1570.027,106.2,1570.027,125.272z
|
||||
M1546.188,183.612v182.004h17.962V183.612H1546.188z"/>
|
||||
<path fill="#333359" d="M1633.503,108.776v256.84h-17.983v-256.84H1633.503z"/>
|
||||
<path fill="#333359" d="M1918.606,184.347l0.73,32.304c11.365-24.603,37.066-34.133,60.181-34.133
|
||||
c13.589-0.367,26.772,3.307,38.896,10.646l-8.08,14.671c-9.525-5.871-20.187-8.441-30.815-8.441
|
||||
c-33.771,0.379-59.817,27.524-59.817,60.553v105.67h-17.979V184.347H1918.606z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path fill="none" d="M1694.655,305.711c0.006,0.016,0.014,0.031,0.02,0.047l146.748-38.832c-0.007-0.055-0.012-0.11-0.018-0.166
|
||||
L1694.655,305.711z"/>
|
||||
<path fill="none" d="M1765.447,197.873c-42.255,0-76.514,34.997-76.514,78.169c0,4.196,0.333,8.312,0.956,12.329l147.452-39.137
|
||||
C1826.633,219.268,1798.486,197.873,1765.447,197.873z"/>
|
||||
<g>
|
||||
<path fill="none" d="M1765.447,198.374c-42.255,0-76.514,34.996-76.514,78.169c0,4.196,0.333,8.312,0.956,12.329l147.452-39.137
|
||||
C1826.633,219.768,1798.486,198.374,1765.447,198.374z"/>
|
||||
<path fill="#333359" d="M1765.447,354.709c-31.946,0-59.308-20.014-70.764-48.431l-0.1,0.004l0.091-0.024
|
||||
c-0.006-0.016-0.014-0.031-0.02-0.047l146.75-38.951c0.006,0.056,0.011,0.111,0.018,0.166l15.616-4.133
|
||||
c-6.306-45.918-44.904-81.253-91.59-81.253c-51.089,0-92.501,42.31-92.501,94.5s41.412,94.501,92.501,94.501
|
||||
c38.213,0,71.011-23.675,85.115-57.448l-14.717-6.398C1824.179,335.126,1797.054,354.709,1765.447,354.709z M1688.934,276.542
|
||||
c0-43.173,34.259-78.169,76.514-78.169c33.039,0,61.186,21.395,71.895,51.361l-147.452,39.137
|
||||
C1689.267,284.854,1688.934,280.739,1688.934,276.542z"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 5 KiB |
BIN
public/resources/images/marker-icon-2x.png
Normal file
After Width: | Height: | Size: 2.4 KiB |
BIN
public/resources/images/marker-icon.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
public/resources/images/marker-shadow.png
Normal file
After Width: | Height: | Size: 618 B |
BIN
public/resources/images/placeholder.png
Normal file
After Width: | Height: | Size: 2 KiB |
|
@ -1,162 +0,0 @@
|
|||
(function(window) {
|
||||
var HAS_HASHCHANGE = (function() {
|
||||
var doc_mode = window.documentMode;
|
||||
return ('onhashchange' in window) &&
|
||||
(doc_mode === undefined || doc_mode > 7);
|
||||
})();
|
||||
|
||||
L.Hash = function(map) {
|
||||
this.onHashChange = L.Util.bind(this.onHashChange, this);
|
||||
|
||||
if (map) {
|
||||
this.init(map);
|
||||
}
|
||||
};
|
||||
|
||||
L.Hash.parseHash = function(hash) {
|
||||
if(hash.indexOf('#') === 0) {
|
||||
hash = hash.substr(1);
|
||||
}
|
||||
var args = hash.split("/");
|
||||
if (args.length == 3) {
|
||||
var zoom = parseInt(args[0], 10),
|
||||
lat = parseFloat(args[1]),
|
||||
lon = parseFloat(args[2]);
|
||||
if (isNaN(zoom) || isNaN(lat) || isNaN(lon)) {
|
||||
return false;
|
||||
} else {
|
||||
return {
|
||||
center: new L.LatLng(lat, lon),
|
||||
zoom: zoom
|
||||
};
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
L.Hash.formatHash = function(map) {
|
||||
var center = map.getCenter(),
|
||||
zoom = map.getZoom(),
|
||||
precision = Math.max(0, Math.ceil(Math.log(zoom) / Math.LN2));
|
||||
|
||||
return "#" + [zoom,
|
||||
center.lat.toFixed(precision),
|
||||
center.lng.toFixed(precision)
|
||||
].join("/");
|
||||
},
|
||||
|
||||
L.Hash.prototype = {
|
||||
map: null,
|
||||
lastHash: null,
|
||||
|
||||
parseHash: L.Hash.parseHash,
|
||||
formatHash: L.Hash.formatHash,
|
||||
|
||||
init: function(map) {
|
||||
this.map = map;
|
||||
|
||||
// reset the hash
|
||||
this.lastHash = null;
|
||||
this.onHashChange();
|
||||
|
||||
if (!this.isListening) {
|
||||
this.startListening();
|
||||
}
|
||||
},
|
||||
|
||||
removeFrom: function(map) {
|
||||
if (this.changeTimeout) {
|
||||
clearTimeout(this.changeTimeout);
|
||||
}
|
||||
|
||||
if (this.isListening) {
|
||||
this.stopListening();
|
||||
}
|
||||
|
||||
this.map = null;
|
||||
},
|
||||
|
||||
onMapMove: function() {
|
||||
// bail if we're moving the map (updating from a hash),
|
||||
// or if the map is not yet loaded
|
||||
|
||||
if (this.movingMap || !this.map._loaded) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var hash = this.formatHash(this.map);
|
||||
if (this.lastHash != hash) {
|
||||
location.replace(hash);
|
||||
this.lastHash = hash;
|
||||
}
|
||||
},
|
||||
|
||||
movingMap: false,
|
||||
update: function() {
|
||||
var hash = location.hash;
|
||||
if (hash === this.lastHash) {
|
||||
return;
|
||||
}
|
||||
var parsed = this.parseHash(hash);
|
||||
if (parsed) {
|
||||
this.movingMap = true;
|
||||
|
||||
this.map.setView(parsed.center, parsed.zoom);
|
||||
|
||||
this.movingMap = false;
|
||||
} else {
|
||||
this.onMapMove(this.map);
|
||||
}
|
||||
},
|
||||
|
||||
// defer hash change updates every 100ms
|
||||
changeDefer: 100,
|
||||
changeTimeout: null,
|
||||
onHashChange: function() {
|
||||
// throttle calls to update() so that they only happen every
|
||||
// `changeDefer` ms
|
||||
if (!this.changeTimeout) {
|
||||
var that = this;
|
||||
this.changeTimeout = setTimeout(function() {
|
||||
that.update();
|
||||
that.changeTimeout = null;
|
||||
}, this.changeDefer);
|
||||
}
|
||||
},
|
||||
|
||||
isListening: false,
|
||||
hashChangeInterval: null,
|
||||
startListening: function() {
|
||||
this.map.on("moveend", this.onMapMove, this);
|
||||
|
||||
if (HAS_HASHCHANGE) {
|
||||
L.DomEvent.addListener(window, "hashchange", this.onHashChange);
|
||||
} else {
|
||||
clearInterval(this.hashChangeInterval);
|
||||
this.hashChangeInterval = setInterval(this.onHashChange, 50);
|
||||
}
|
||||
this.isListening = true;
|
||||
},
|
||||
|
||||
stopListening: function() {
|
||||
this.map.off("moveend", this.onMapMove, this);
|
||||
|
||||
if (HAS_HASHCHANGE) {
|
||||
L.DomEvent.removeListener(window, "hashchange", this.onHashChange);
|
||||
} else {
|
||||
clearInterval(this.hashChangeInterval);
|
||||
}
|
||||
this.isListening = false;
|
||||
}
|
||||
};
|
||||
L.hash = function(map) {
|
||||
return new L.Hash(map);
|
||||
};
|
||||
L.Map.prototype.addHash = function() {
|
||||
this._hash = L.hash(this);
|
||||
};
|
||||
L.Map.prototype.removeHash = function() {
|
||||
this._hash.removeFrom();
|
||||
};
|
||||
})(window);
|
|
@ -1,244 +0,0 @@
|
|||
.mapboxgl-map {
|
||||
font: 12px/20px 'Helvetica Neue', Arial, Helvetica, sans-serif;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
-webkit-tap-highlight-color: rgba(0,0,0,0);
|
||||
}
|
||||
|
||||
.mapboxgl-canvas-container.mapboxgl-interactive,
|
||||
.mapboxgl-ctrl-nav-compass {
|
||||
cursor: -webkit-grab;
|
||||
cursor: -moz-grab;
|
||||
cursor: grab;
|
||||
}
|
||||
.mapboxgl-canvas-container.mapboxgl-interactive:active,
|
||||
.mapboxgl-ctrl-nav-compass:active {
|
||||
cursor: -webkit-grabbing;
|
||||
cursor: -moz-grabbing;
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.mapboxgl-ctrl-top-left,
|
||||
.mapboxgl-ctrl-top-right,
|
||||
.mapboxgl-ctrl-bottom-left,
|
||||
.mapboxgl-ctrl-bottom-right { position:absolute; }
|
||||
.mapboxgl-ctrl-top-left { top:0; left:0; }
|
||||
.mapboxgl-ctrl-top-right { top:0; right:0; }
|
||||
.mapboxgl-ctrl-bottom-left { bottom:0; left:0; }
|
||||
.mapboxgl-ctrl-bottom-right { right:0; bottom:0; }
|
||||
|
||||
.mapboxgl-ctrl { clear:both; }
|
||||
.mapboxgl-ctrl-top-left .mapboxgl-ctrl { margin:10px 0 0 10px; float:left; }
|
||||
.mapboxgl-ctrl-top-right .mapboxgl-ctrl{ margin:10px 10px 0 0; float:right; }
|
||||
.mapboxgl-ctrl-bottom-left .mapboxgl-ctrl { margin:0 0 10px 10px; float:left; }
|
||||
.mapboxgl-ctrl-bottom-right .mapboxgl-ctrl { margin:0 10px 10px 0; float:right; }
|
||||
|
||||
.mapboxgl-ctrl-group {
|
||||
border-radius: 4px;
|
||||
-moz-box-shadow: 0px 0px 2px rgba(0,0,0,0.1);
|
||||
-webkit-box-shadow: 0px 0px 2px rgba(0,0,0,0.1);
|
||||
box-shadow: 0px 0px 0px 2px rgba(0,0,0,0.1);
|
||||
overflow: hidden;
|
||||
background: #fff;
|
||||
}
|
||||
.mapboxgl-ctrl-group > button {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
display: block;
|
||||
padding: 0;
|
||||
outline: none;
|
||||
border: none;
|
||||
border-bottom: 1px solid #ddd;
|
||||
box-sizing: border-box;
|
||||
background-color: rgba(0,0,0,0);
|
||||
cursor: pointer;
|
||||
}
|
||||
/* https://bugzilla.mozilla.org/show_bug.cgi?id=140562 */
|
||||
.mapboxgl-ctrl > button::-moz-focus-inner {
|
||||
border: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.mapboxgl-ctrl > button:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
.mapboxgl-ctrl > button:hover {
|
||||
background-color: rgba(0,0,0,0.05);
|
||||
}
|
||||
.mapboxgl-ctrl-icon,
|
||||
.mapboxgl-ctrl-icon > div.arrow {
|
||||
speak: none;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
.mapboxgl-ctrl-icon.mapboxgl-ctrl-zoom-out {
|
||||
padding: 5px;
|
||||
background-image: url("data:image/svg+xml;charset=utf8,%3Csvg%20viewBox%3D%270%200%2020%2020%27%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%3E%0A%20%20%3Cpath%20style%3D%27fill%3A%23333333%3B%27%20d%3D%27m%207%2C9%20c%20-0.554%2C0%20-1%2C0.446%20-1%2C1%200%2C0.554%200.446%2C1%201%2C1%20l%206%2C0%20c%200.554%2C0%201%2C-0.446%201%2C-1%200%2C-0.554%20-0.446%2C-1%20-1%2C-1%20z%27%20%2F%3E%0A%3C%2Fsvg%3E%0A");
|
||||
}
|
||||
.mapboxgl-ctrl-icon.mapboxgl-ctrl-zoom-in {
|
||||
padding: 5px;
|
||||
background-image: url("data:image/svg+xml;charset=utf8,%3Csvg%20viewBox%3D%270%200%2020%2020%27%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%3E%0A%20%20%3Cpath%20style%3D%27fill%3A%23333333%3B%27%20d%3D%27M%2010%206%20C%209.446%206%209%206.4459904%209%207%20L%209%209%20L%207%209%20C%206.446%209%206%209.446%206%2010%20C%206%2010.554%206.446%2011%207%2011%20L%209%2011%20L%209%2013%20C%209%2013.55401%209.446%2014%2010%2014%20C%2010.554%2014%2011%2013.55401%2011%2013%20L%2011%2011%20L%2013%2011%20C%2013.554%2011%2014%2010.554%2014%2010%20C%2014%209.446%2013.554%209%2013%209%20L%2011%209%20L%2011%207%20C%2011%206.4459904%2010.554%206%2010%206%20z%27%20%2F%3E%0A%3C%2Fsvg%3E%0A");
|
||||
}
|
||||
.mapboxgl-ctrl-icon.mapboxgl-ctrl-compass > div.arrow {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin: 5px;
|
||||
background-image: url("data:image/svg+xml;charset=utf8,%3Csvg%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%20viewBox%3D%270%200%2020%2020%27%3E%0A%09%3Cpolygon%20fill%3D%27%23333333%27%20points%3D%276%2C9%2010%2C1%2014%2C9%27%2F%3E%0A%09%3Cpolygon%20fill%3D%27%23CCCCCC%27%20points%3D%276%2C11%2010%2C19%2014%2C11%20%27%2F%3E%0A%3C%2Fsvg%3E");
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.mapboxgl-ctrl.mapboxgl-ctrl-attrib {
|
||||
padding: 0 5px;
|
||||
background-color: rgba(255,255,255,0.5);
|
||||
margin: 0;
|
||||
}
|
||||
.mapboxgl-ctrl-attrib a {
|
||||
color: rgba(0,0,0,0.75);
|
||||
text-decoration: none;
|
||||
}
|
||||
.mapboxgl-ctrl-attrib a:hover {
|
||||
color: inherit;
|
||||
text-decoration: underline;
|
||||
}
|
||||
.mapboxgl-ctrl-attrib .mapbox-improve-map {
|
||||
font-weight: bold;
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
.mapboxgl-popup {
|
||||
position: absolute;
|
||||
display: -webkit-flex;
|
||||
display: flex;
|
||||
will-change: transform;
|
||||
pointer-events: none;
|
||||
}
|
||||
.mapboxgl-popup-anchor-top,
|
||||
.mapboxgl-popup-anchor-top-left,
|
||||
.mapboxgl-popup-anchor-top-right {
|
||||
-webkit-flex-direction: column;
|
||||
flex-direction: column;
|
||||
}
|
||||
.mapboxgl-popup-anchor-bottom,
|
||||
.mapboxgl-popup-anchor-bottom-left,
|
||||
.mapboxgl-popup-anchor-bottom-right {
|
||||
-webkit-flex-direction: column-reverse;
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
.mapboxgl-popup-anchor-left {
|
||||
-webkit-flex-direction: row;
|
||||
flex-direction: row;
|
||||
}
|
||||
.mapboxgl-popup-anchor-right {
|
||||
-webkit-flex-direction: row-reverse;
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
.mapboxgl-popup-tip {
|
||||
width: 0;
|
||||
height: 0;
|
||||
border: 10px solid transparent;
|
||||
z-index: 1;
|
||||
}
|
||||
.mapboxgl-popup-anchor-top .mapboxgl-popup-tip {
|
||||
-webkit-align-self: center;
|
||||
align-self: center;
|
||||
border-top: none;
|
||||
border-bottom-color: #fff;
|
||||
}
|
||||
.mapboxgl-popup-anchor-top-left .mapboxgl-popup-tip {
|
||||
-webkit-align-self: flex-start;
|
||||
align-self: flex-start;
|
||||
border-top: none;
|
||||
border-left: none;
|
||||
border-bottom-color: #fff;
|
||||
}
|
||||
.mapboxgl-popup-anchor-top-right .mapboxgl-popup-tip {
|
||||
-webkit-align-self: flex-end;
|
||||
align-self: flex-end;
|
||||
border-top: none;
|
||||
border-right: none;
|
||||
border-bottom-color: #fff;
|
||||
}
|
||||
.mapboxgl-popup-anchor-bottom .mapboxgl-popup-tip {
|
||||
-webkit-align-self: center;
|
||||
align-self: center;
|
||||
border-bottom: none;
|
||||
border-top-color: #fff;
|
||||
}
|
||||
.mapboxgl-popup-anchor-bottom-left .mapboxgl-popup-tip {
|
||||
-webkit-align-self: flex-start;
|
||||
align-self: flex-start;
|
||||
border-bottom: none;
|
||||
border-left: none;
|
||||
border-top-color: #fff;
|
||||
}
|
||||
.mapboxgl-popup-anchor-bottom-right .mapboxgl-popup-tip {
|
||||
-webkit-align-self: flex-end;
|
||||
align-self: flex-end;
|
||||
border-bottom: none;
|
||||
border-right: none;
|
||||
border-top-color: #fff;
|
||||
}
|
||||
.mapboxgl-popup-anchor-left .mapboxgl-popup-tip {
|
||||
-webkit-align-self: center;
|
||||
align-self: center;
|
||||
border-left: none;
|
||||
border-right-color: #fff;
|
||||
}
|
||||
.mapboxgl-popup-anchor-right .mapboxgl-popup-tip {
|
||||
-webkit-align-self: center;
|
||||
align-self: center;
|
||||
border-right: none;
|
||||
border-left-color: #fff;
|
||||
}
|
||||
.mapboxgl-popup-close-button {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
border: none;
|
||||
border-radius: 0 3px 0 0;
|
||||
cursor: pointer;
|
||||
background-color: rgba(0,0,0,0);
|
||||
}
|
||||
.mapboxgl-popup-close-button:hover {
|
||||
background-color: rgba(0,0,0,0.05);
|
||||
}
|
||||
.mapboxgl-popup-content {
|
||||
position: relative;
|
||||
background: #fff;
|
||||
border-radius: 3px;
|
||||
box-shadow: 0 1px 2px rgba(0,0,0,0.10);
|
||||
padding: 10px 10px 15px;
|
||||
pointer-events: auto;
|
||||
}
|
||||
.mapboxgl-popup-anchor-top-left .mapboxgl-popup-content {
|
||||
border-top-left-radius: 0;
|
||||
}
|
||||
.mapboxgl-popup-anchor-top-right .mapboxgl-popup-content {
|
||||
border-top-right-radius: 0;
|
||||
}
|
||||
.mapboxgl-popup-anchor-bottom-left .mapboxgl-popup-content {
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
.mapboxgl-popup-anchor-bottom-right .mapboxgl-popup-content {
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
.mapboxgl-crosshair,
|
||||
.mapboxgl-crosshair .mapboxgl-interactive,
|
||||
.mapboxgl-crosshair .mapboxgl-interactive:active {
|
||||
cursor: crosshair;
|
||||
}
|
||||
.mapboxgl-boxzoom {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
background: #fff;
|
||||
border: 2px dotted #202020;
|
||||
opacity: 0.5;
|
||||
}
|
||||
@media print {
|
||||
.mapbox-improve-map {
|
||||
display:none;
|
||||
}
|
||||
}
|
|
@ -2,126 +2,230 @@
|
|||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{{name}} - TileServer GL</title>
|
||||
{{#is_vector}}
|
||||
<link rel="stylesheet" type="text/css" href="/mapbox-gl.css" />
|
||||
<script src="/mapbox-gl.js"></script>
|
||||
{{#use_maplibre}}
|
||||
<link rel="stylesheet" type="text/css" href="{{public_url}}maplibre-gl.css{{&key_query}}" />
|
||||
<link rel="stylesheet" type="text/css" href="{{public_url}}maplibre-gl-inspect.css{{&key_query}}" />
|
||||
<script src="{{public_url}}maplibre-gl.js{{&key_query}}"></script>
|
||||
<script src="{{public_url}}maplibre-gl-inspect.js{{&key_query}}"></script>
|
||||
{{^is_light}}
|
||||
<script src="{{public_url}}elevation-control.js{{&key_query}}"></script>
|
||||
{{/is_light}}
|
||||
<style>
|
||||
body {background:#000;color:#ccc;}
|
||||
body {background:#fff;color:#333;font-family:Arial, sans-serif;}
|
||||
{{^is_terrain}}
|
||||
#map {position:absolute;top:0;left:0;right:250px;bottom:0;}
|
||||
{{/is_terrain}}
|
||||
{{#is_terrain}}
|
||||
#map { position:absolute; top:0; bottom:0; left:0; right:0; }
|
||||
{{/is_terrain}}
|
||||
h1 {position:absolute;top:5px;right:0;width:240px;margin:0;line-height:20px;font-size:20px;}
|
||||
#layerList {position:absolute;top:35px;right:0;bottom:60%;width:240px;overflow:auto;}
|
||||
#layerList {position:absolute;top:35px;right:0;bottom:0;width:240px;overflow:auto;}
|
||||
#layerList div div {width:15px;height:15px;display:inline-block;}
|
||||
#propertyList {position:absolute;top:40%;bottom:0;right:0;width:240px;overflow:auto;color:#fff;}
|
||||
{{^is_light}}
|
||||
.maplibre-ctrl-elevation { padding-left: 5px; padding-right: 5px; }
|
||||
{{/is_light}}
|
||||
</style>
|
||||
{{/is_vector}}
|
||||
{{^is_vector}}
|
||||
<link rel="stylesheet" type="text/css" href="/mapbox.css" />
|
||||
<script src="/mapbox.js"></script>
|
||||
<script src="/leaflet-hash.js"></script>
|
||||
{{/use_maplibre}}
|
||||
{{^use_maplibre}}
|
||||
<link rel="stylesheet" type="text/css" href="{{public_url}}leaflet.css{{&key_query}}" />
|
||||
<script src="{{public_url}}leaflet.js{{&key_query}}"></script>
|
||||
<script src="{{public_url}}leaflet-hash.js{{&key_query}}"></script>
|
||||
<style>
|
||||
body { margin:0; padding:0; }
|
||||
#map { position:absolute; top:0; bottom:0; width:100%; }
|
||||
.leaflet-control-layers-toggle {
|
||||
background-image: url({{public_url}}images/layers.png{{&key_query}});
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
}
|
||||
.leaflet-retina .leaflet-control-layers-toggle {
|
||||
background-image: url({{public_url}}images/layers-2x.png{{&key_query}});
|
||||
background-size: 26px 26px;
|
||||
}
|
||||
.leaflet-default-icon-path { /* used only in path-guessing heuristic, see L.Icon.Default */
|
||||
background-image: url({{public_url}}images/marker-icon.png{{&key_query}});
|
||||
}
|
||||
</style>
|
||||
{{/is_vector}}
|
||||
{{/use_maplibre}}
|
||||
</head>
|
||||
<body>
|
||||
{{#is_vector}}
|
||||
{{#use_maplibre}}
|
||||
<h1>{{name}}</h1>
|
||||
<div id="map"></div>
|
||||
{{^is_terrain}}
|
||||
<div id="layerList"></div>
|
||||
<pre id="propertyList"></pre>
|
||||
{{/is_terrain}}
|
||||
<script>
|
||||
var map = new mapboxgl.Map({
|
||||
container: 'map',
|
||||
hash: true
|
||||
});
|
||||
map.addControl(new mapboxgl.Navigation());
|
||||
var keyMatch = location.search.match(/[\?\&]key=([^&]+)/i);
|
||||
var keyParam = keyMatch ? '?key=' + keyMatch[1] : '';
|
||||
|
||||
function generateColor(str) {
|
||||
var rgb = [0, 0, 0];
|
||||
for (var i = 0; i < str.length; i++) {
|
||||
var v = str.charCodeAt(i);
|
||||
rgb[v % 3] = (rgb[i % 3] + (13*(v%13))) % 12;
|
||||
}
|
||||
var r = 4 + rgb[0];
|
||||
var g = 4 + rgb[1];
|
||||
var b = 4 + rgb[2];
|
||||
r = (r * 16) + r;
|
||||
g = (g * 16) + g;
|
||||
b = (b * 16) + b;
|
||||
return [r, g, b, 1];
|
||||
};
|
||||
|
||||
function initLayer(data) {
|
||||
var layer;
|
||||
var layerList = document.getElementById('layerList');
|
||||
var layers_ = [];
|
||||
data['vector_layers'].forEach(function(el) {
|
||||
var color = generateColor(el['id']);
|
||||
var colorText = 'rgba(' + color[0] + ',' + color[1] + ',' + color[2] + ',' + color[3] + ')';
|
||||
layers_.push({
|
||||
id: el['id'] + Math.random(),
|
||||
source: 'vector_layer_',
|
||||
'source-layer': el['id'],
|
||||
interactive: true,
|
||||
type: 'line',
|
||||
paint: {'line-color': colorText}
|
||||
});
|
||||
var item = document.createElement('div');
|
||||
item.innerHTML = '<div style="' +
|
||||
'background:rgba(' + color[0] + ',' + color[1] + ',' + color[2] + ',1);' +
|
||||
'"></div> ' + el['id'];
|
||||
layerList.appendChild(item);
|
||||
});
|
||||
map.setStyle({
|
||||
{{^is_terrain}}
|
||||
var style = {
|
||||
version: 8,
|
||||
sources: {
|
||||
'vector_layer_': {
|
||||
type: 'vector',
|
||||
tiles: data['tiles'],
|
||||
minzoom: data['minzoom'],
|
||||
maxzoom: data['maxzoom']
|
||||
url: '{{public_url}}data/{{id}}.json' + keyParam
|
||||
}
|
||||
},
|
||||
layers: layers_
|
||||
layers: []
|
||||
};
|
||||
{{/is_terrain}}
|
||||
{{#is_terrain}}
|
||||
|
||||
var style = {
|
||||
version: 8,
|
||||
sources: {
|
||||
"terrain": {
|
||||
"type": "raster-dem",
|
||||
"url": "{{public_url}}data/{{id}}.json",
|
||||
"encoding": "{{terrain_encoding}}"
|
||||
},
|
||||
"hillshade": {
|
||||
"type": "raster-dem",
|
||||
"url": "{{public_url}}data/{{id}}.json",
|
||||
"encoding": "{{terrain_encoding}}"
|
||||
}
|
||||
},
|
||||
"terrain": {
|
||||
"source": "terrain"
|
||||
},
|
||||
"layers": [
|
||||
{
|
||||
"id": "background",
|
||||
"paint": {
|
||||
{{#if is_terrainrgb}}
|
||||
"background-color": "hsl(190, 99%, 63%)"
|
||||
{{else}}
|
||||
"background-color": "hsl(0, 100%, 25%)"
|
||||
{{/if}}
|
||||
},
|
||||
"type": "background"
|
||||
},
|
||||
{
|
||||
"id": "hillshade",
|
||||
"source": "hillshade",
|
||||
"type": "hillshade",
|
||||
"paint": {
|
||||
"hillshade-shadow-color": "hsl(39, 21%, 33%)",
|
||||
"hillshade-illumination-direction": 315,
|
||||
"hillshade-exaggeration": 0.8
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
{{/is_terrain}}
|
||||
|
||||
var map = new maplibregl.Map({
|
||||
container: 'map',
|
||||
hash: true,
|
||||
maxPitch: 85,
|
||||
style: style
|
||||
});
|
||||
|
||||
return layer;
|
||||
}
|
||||
map.addControl(new maplibregl.NavigationControl({
|
||||
visualizePitch: true,
|
||||
showZoom: true,
|
||||
showCompass: true
|
||||
}));
|
||||
{{#is_terrain}}
|
||||
|
||||
var xhttp = new XMLHttpRequest();
|
||||
xhttp.onreadystatechange = function() {
|
||||
if (xhttp.readyState == 4 && xhttp.status == 200) {
|
||||
initLayer(xhttp.response);
|
||||
}
|
||||
};
|
||||
xhttp.responseType = 'json';
|
||||
xhttp.open('GET', '/data/{{id}}.json', true);
|
||||
xhttp.send();
|
||||
map.addControl(
|
||||
new maplibregl.TerrainControl({
|
||||
source: "terrain",
|
||||
})
|
||||
);
|
||||
|
||||
var propertyList = document.getElementById('propertyList');
|
||||
map.on('mousemove', function(e) {
|
||||
propertyList.innerHTML = '';
|
||||
map.featuresAt(e.point, {radius: 3}, function(err, features) {
|
||||
if (err) throw err;
|
||||
if (features[0]) {
|
||||
propertyList.innerHTML = JSON.stringify(features[0].properties, null, 2);
|
||||
}
|
||||
{{^is_light}}
|
||||
map.addControl(
|
||||
new ElevationInfoControl({
|
||||
url: "{{public_url}}data/{{id}}/elevation/{z}/{x}/{y}"
|
||||
})
|
||||
);
|
||||
{{/is_light}}
|
||||
{{/is_terrain}}
|
||||
{{^is_terrain}}
|
||||
|
||||
var inspect = new MaplibreInspect({
|
||||
showInspectMap: true,
|
||||
showInspectButton: false
|
||||
});
|
||||
});
|
||||
map.addControl(inspect);
|
||||
|
||||
map.on('styledata', function() {
|
||||
var layerList = document.getElementById('layerList');
|
||||
layerList.innerHTML = '';
|
||||
Object.keys(inspect.sources).forEach(function(sourceId) {
|
||||
var layerIds = inspect.sources[sourceId];
|
||||
layerIds.forEach(function(layerId) {
|
||||
var item = document.createElement('div');
|
||||
item.innerHTML = '<div style="' +
|
||||
'background:' + inspect.assignLayerColor(layerId) + ';' +
|
||||
'"></div> ' + layerId;
|
||||
layerList.appendChild(item);
|
||||
});
|
||||
})
|
||||
});
|
||||
{{/is_terrain}}
|
||||
</script>
|
||||
{{/is_vector}}
|
||||
{{^is_vector}}
|
||||
{{/use_maplibre}}
|
||||
{{^use_maplibre}}
|
||||
<h1 style="display:none;">{{name}}</h1>
|
||||
<div id='map'></div>
|
||||
<script>
|
||||
var map = L.mapbox.map('map', '/data/{{id}}.json', { zoomControl: false });
|
||||
var keyMatch = location.search.match(/[\?\&]key=([^&]+)/i);
|
||||
var keyParam = keyMatch ? '?key=' + keyMatch[1] : '';
|
||||
var map = L.map('map', { zoomControl: false });
|
||||
new L.Control.Zoom({ position: 'topright' }).addTo(map);
|
||||
|
||||
var tile_urls = [], tile_attribution, tile_minzoom, tile_maxzoom;
|
||||
var url = '{{public_url}}data/{{id}}.json' + keyParam;
|
||||
var req = new XMLHttpRequest();
|
||||
req.overrideMimeType("application/json");
|
||||
req.open('GET', url, true);
|
||||
req.onload = function() {
|
||||
var jsonResponse = JSON.parse(req.responseText);
|
||||
for (key in jsonResponse) {
|
||||
var keyl = key.toLowerCase();
|
||||
switch(keyl) {
|
||||
case "tiles":
|
||||
tile_urls = jsonResponse[key];
|
||||
break;
|
||||
case "attribution":
|
||||
tile_attribution = jsonResponse[key];
|
||||
break;
|
||||
case "minzoom":
|
||||
tile_minzoom = jsonResponse[key];
|
||||
break;
|
||||
case "maxzoom":
|
||||
tile_maxzoom = jsonResponse[key];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for (tile_url in tile_urls) {
|
||||
L.tileLayer(tile_urls[tile_url], {
|
||||
tileSize: 256,
|
||||
minZoom: tile_minzoom,
|
||||
maxZoom: tile_maxzoom,
|
||||
attribution: tile_attribution
|
||||
}).addTo(map);
|
||||
}
|
||||
|
||||
map.eachLayer(function(layer) {
|
||||
// do not add scale prefix even if retina display is detected
|
||||
layer.scalePrefix = '.';
|
||||
});
|
||||
};
|
||||
req.send(null);
|
||||
|
||||
setTimeout(function() {
|
||||
new L.Hash(map);
|
||||
}, 0);
|
||||
</script>
|
||||
{{/is_vector}}
|
||||
{{/use_maplibre}}
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -2,38 +2,128 @@
|
|||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{{name}} - TileServer GL</title>
|
||||
<link rel="stylesheet" type="text/css" href="/mapbox-gl.css" />
|
||||
<script src="/mapbox-gl.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="/mapbox.css" />
|
||||
<script src="/mapbox.js"></script>
|
||||
<script src="/leaflet-hash.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="{{public_url}}maplibre-gl.css{{&key_query}}" />
|
||||
<link rel="stylesheet" type="text/css" href="{{public_url}}maplibre-gl-inspect.css{{&key_query}}" />
|
||||
<link rel="stylesheet" type="text/css" href="{{public_url}}leaflet.css{{&key_query}}" />
|
||||
<script src="{{public_url}}maplibre-gl.js{{&key_query}}"></script>
|
||||
<script src="{{public_url}}maplibre-gl-inspect.js{{&key_query}}"></script>
|
||||
<script src="{{public_url}}leaflet.js{{&key_query}}"></script>
|
||||
<script src="{{public_url}}leaflet-hash.js{{&key_query}}"></script>
|
||||
<style>
|
||||
body { margin:0; padding:0; }
|
||||
#map { position:absolute; top:0; bottom:0; width:100%; }
|
||||
.leaflet-control-layers-toggle {
|
||||
background-image: url({{public_url}}images/layers.png{{&key_query}});
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
}
|
||||
.leaflet-retina .leaflet-control-layers-toggle {
|
||||
background-image: url({{public_url}}images/layers-2x.png{{&key_query}});
|
||||
background-size: 26px 26px;
|
||||
}
|
||||
.leaflet-default-icon-path { /* used only in path-guessing heuristic, see L.Icon.Default */
|
||||
background-image: url({{public_url}}images/marker-icon.png{{&key_query}});
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1 style="display:none;">{{name}}</h1>
|
||||
<div id='map'></div>
|
||||
<script>
|
||||
var preference = (location.search || '').substr(1);
|
||||
if (preference != 'vector' && preference != 'raster') {
|
||||
preference = mapboxgl.supported() ? 'vector' : 'raster';
|
||||
function isWebglSupported() {
|
||||
if (window.WebGLRenderingContext) {
|
||||
const canvas = document.createElement('canvas');
|
||||
try {
|
||||
const context = canvas.getContext('webgl2') || canvas.getContext('webgl');
|
||||
if (context && typeof context.getParameter == 'function') {
|
||||
return true;
|
||||
}
|
||||
} catch (e) {
|
||||
// WebGL is supported, but disabled
|
||||
}
|
||||
return false;
|
||||
}
|
||||
// WebGL not supported
|
||||
return false;
|
||||
}
|
||||
var q = (location.search || '').substr(1).split('&');
|
||||
var preference =
|
||||
q.indexOf('vector') >= 0 ? 'vector' :
|
||||
(q.indexOf('raster') >= 0 ? 'raster' :
|
||||
(isWebglSupported() ? 'vector' : 'raster'));
|
||||
|
||||
var keyMatch = location.search.match(/[\?\&]key=([^&]+)/i);
|
||||
var keyParam = keyMatch ? '?key=' + keyMatch[1] : '';
|
||||
|
||||
if (preference == 'vector') {
|
||||
var map = new mapboxgl.Map({
|
||||
maplibregl.setRTLTextPlugin('{{public_url}}mapbox-gl-rtl-text.js' + keyParam);
|
||||
var map = new maplibregl.Map({
|
||||
container: 'map',
|
||||
style: '/styles/{{id}}.json',
|
||||
hash: true
|
||||
style: '{{public_url}}styles/{{id}}/style.json' + keyParam,
|
||||
hash: true,
|
||||
maxPitch: 85
|
||||
});
|
||||
map.addControl(new mapboxgl.Navigation());
|
||||
map.addControl(new maplibregl.NavigationControl({
|
||||
visualizePitch: true,
|
||||
showZoom: true,
|
||||
showCompass: true
|
||||
}));
|
||||
map.addControl(new MaplibreInspect({
|
||||
showMapPopupOnHover: false,
|
||||
showInspectMapPopupOnHover: false,
|
||||
selectThreshold: 5
|
||||
}));
|
||||
} else {
|
||||
var map = L.mapbox.map('map', '/styles/{{id}}/rendered.json', { zoomControl: false });
|
||||
new L.Control.Zoom({ position: 'topright' }).addTo(map);
|
||||
setTimeout(function() {
|
||||
new L.Hash(map);
|
||||
}, 0);
|
||||
var map = L.map('map', { minZoom: 1, zoomControl: false });
|
||||
new L.Control.Zoom({ position: 'topright' }).addTo(map);
|
||||
|
||||
var tile_urls = [], tile_attribution, tile_minzoom, tile_maxzoom;
|
||||
var url = '{{public_url}}styles/512/{{id}}.json' + keyParam;
|
||||
var req = new XMLHttpRequest();
|
||||
req.overrideMimeType("application/json");
|
||||
req.open('GET', url, true);
|
||||
req.onload = function() {
|
||||
var jsonResponse = JSON.parse(req.responseText);
|
||||
for (key in jsonResponse) {
|
||||
var keyl = key.toLowerCase();
|
||||
switch(keyl) {
|
||||
case "tiles":
|
||||
tile_urls = jsonResponse[key];
|
||||
break;
|
||||
case "attribution":
|
||||
tile_attribution = jsonResponse[key];
|
||||
break;
|
||||
case "minzoom":
|
||||
tile_minzoom = jsonResponse[key];
|
||||
break;
|
||||
case "maxzoom":
|
||||
tile_maxzoom = jsonResponse[key];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for (tile_url in tile_urls) {
|
||||
L.tileLayer(tile_urls[tile_url], {
|
||||
tileSize: 512,
|
||||
zoomOffset: -1,
|
||||
minZoom: tile_minzoom,
|
||||
maxZoom: tile_maxzoom,
|
||||
attribution: tile_attribution
|
||||
}).addTo(map);
|
||||
}
|
||||
|
||||
map.eachLayer(function(layer) {
|
||||
// do not add scale prefix even if retina display is detected
|
||||
layer.scalePrefix = '.';
|
||||
});
|
||||
};
|
||||
req.send(null);
|
||||
|
||||
setTimeout(function() {
|
||||
new L.Hash(map);
|
||||
}, 0);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
|
781
public/templates/wmts.tmpl
Normal file
|
@ -0,0 +1,781 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<Capabilities xmlns="http://www.opengis.net/wmts/1.0" xmlns:ows="http://www.opengis.net/ows/1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:gml="http://www.opengis.net/gml" xsi:schemaLocation="http://www.opengis.net/wmts/1.0 http://schemas.opengis.net/wmts/1.0/wmtsGetCapabilities_response.xsd" version="1.0.0">
|
||||
<!-- Service Identification -->
|
||||
<ows:ServiceIdentification>
|
||||
<ows:Title>TileServer GL</ows:Title>
|
||||
<ows:ServiceType>OGC WMTS</ows:ServiceType>
|
||||
<ows:ServiceTypeVersion>1.0.0</ows:ServiceTypeVersion>
|
||||
</ows:ServiceIdentification>
|
||||
<!-- Operations Metadata -->
|
||||
<ows:OperationsMetadata>
|
||||
<ows:Operation name="GetCapabilities">
|
||||
<ows:DCP>
|
||||
<ows:HTTP>
|
||||
<ows:Get xlink:href="{{baseUrl}}styles/{{id}}/wmts.xml">
|
||||
<ows:Constraint name="GetEncoding">
|
||||
<ows:AllowedValues>
|
||||
<ows:Value>RESTful</ows:Value>
|
||||
</ows:AllowedValues>
|
||||
</ows:Constraint>
|
||||
</ows:Get>
|
||||
</ows:HTTP>
|
||||
</ows:DCP>
|
||||
</ows:Operation>
|
||||
<ows:Operation name="GetTile">
|
||||
<ows:DCP>
|
||||
<ows:HTTP>
|
||||
<ows:Get xlink:href="{{baseUrl}}styles/{{id}}/wmts.xml">
|
||||
<ows:Constraint name="GetEncoding">
|
||||
<ows:AllowedValues>
|
||||
<ows:Value>RESTful</ows:Value>
|
||||
</ows:AllowedValues>
|
||||
</ows:Constraint>
|
||||
</ows:Get>
|
||||
</ows:HTTP>
|
||||
</ows:DCP>
|
||||
</ows:Operation>
|
||||
</ows:OperationsMetadata>
|
||||
<Contents>
|
||||
<Layer>
|
||||
<ows:Title>{{name}}-256</ows:Title>
|
||||
<ows:Identifier>{{id}}-256</ows:Identifier>
|
||||
<ows:WGS84BoundingBox crs="urn:ogc:def:crs:OGC:2:84">
|
||||
<ows:LowerCorner>-180 -85.051128779807</ows:LowerCorner>
|
||||
<ows:UpperCorner>180 85.051128779807</ows:UpperCorner>
|
||||
</ows:WGS84BoundingBox>
|
||||
<Style isDefault="true">
|
||||
<ows:Identifier>default</ows:Identifier>
|
||||
</Style>
|
||||
<Format>image/png</Format>
|
||||
<TileMatrixSetLink>
|
||||
<TileMatrixSet>GoogleMapsCompatible_256</TileMatrixSet>
|
||||
</TileMatrixSetLink>
|
||||
<ResourceURL format="image/png" resourceType="tile" template="{{baseUrl}}styles/{{id}}/256/{TileMatrix}/{TileCol}/{TileRow}.png{{key_query}}"/>
|
||||
</Layer>
|
||||
<Layer>
|
||||
<ows:Title>{{name}}-512</ows:Title>
|
||||
<ows:Identifier>{{id}}-512</ows:Identifier>
|
||||
<ows:WGS84BoundingBox crs="urn:ogc:def:crs:OGC:2:84">
|
||||
<ows:LowerCorner>-180 -85.051128779807</ows:LowerCorner>
|
||||
<ows:UpperCorner>180 85.051128779807</ows:UpperCorner>
|
||||
</ows:WGS84BoundingBox>
|
||||
<Style isDefault="true">
|
||||
<ows:Identifier>default</ows:Identifier>
|
||||
</Style>
|
||||
<Format>image/png</Format>
|
||||
<TileMatrixSetLink>
|
||||
<TileMatrixSet>GoogleMapsCompatible_512</TileMatrixSet>
|
||||
</TileMatrixSetLink>
|
||||
<ResourceURL format="image/png" resourceType="tile" template="{{baseUrl}}styles/{{id}}/512/{TileMatrix}/{TileCol}/{TileRow}.png{{key_query}}"/>
|
||||
</Layer>
|
||||
<TileMatrixSet>
|
||||
<ows:Title>GoogleMapsCompatible_256</ows:Title>
|
||||
<ows:Abstract>GoogleMapsCompatible_256 EPSG:3857</ows:Abstract>
|
||||
<ows:Identifier>GoogleMapsCompatible_256</ows:Identifier>
|
||||
<ows:SupportedCRS>urn:ogc:def:crs:EPSG::3857</ows:SupportedCRS>
|
||||
<TileMatrix>
|
||||
<ows:Identifier>0</ows:Identifier>
|
||||
<ScaleDenominator>559082264.02872</ScaleDenominator>
|
||||
<TopLeftCorner>-20037508.34 20037508.34</TopLeftCorner>
|
||||
<TileWidth>256</TileWidth>
|
||||
<TileHeight>256</TileHeight>
|
||||
<MatrixWidth>1</MatrixWidth>
|
||||
<MatrixHeight>1</MatrixHeight>
|
||||
</TileMatrix>
|
||||
<TileMatrix>
|
||||
<ows:Identifier>1</ows:Identifier>
|
||||
<ScaleDenominator>279541132.01436</ScaleDenominator>
|
||||
<TopLeftCorner>-20037508.34 20037508.34</TopLeftCorner>
|
||||
<TileWidth>256</TileWidth>
|
||||
<TileHeight>256</TileHeight>
|
||||
<MatrixWidth>2</MatrixWidth>
|
||||
<MatrixHeight>2</MatrixHeight>
|
||||
</TileMatrix>
|
||||
<TileMatrix>
|
||||
<ows:Identifier>2</ows:Identifier>
|
||||
<ScaleDenominator>139770566.00718</ScaleDenominator>
|
||||
<TopLeftCorner>-20037508.34 20037508.34</TopLeftCorner>
|
||||
<TileWidth>256</TileWidth>
|
||||
<TileHeight>256</TileHeight>
|
||||
<MatrixWidth>4</MatrixWidth>
|
||||
<MatrixHeight>4</MatrixHeight>
|
||||
</TileMatrix>
|
||||
<TileMatrix>
|
||||
<ows:Identifier>3</ows:Identifier>
|
||||
<ScaleDenominator>69885283.00359</ScaleDenominator>
|
||||
<TopLeftCorner>-20037508.34 20037508.34</TopLeftCorner>
|
||||
<TileWidth>256</TileWidth>
|
||||
<TileHeight>256</TileHeight>
|
||||
<MatrixWidth>8</MatrixWidth>
|
||||
<MatrixHeight>8</MatrixHeight>
|
||||
</TileMatrix>
|
||||
<TileMatrix>
|
||||
<ows:Identifier>4</ows:Identifier>
|
||||
<ScaleDenominator>34942641.501795</ScaleDenominator>
|
||||
<TopLeftCorner>-20037508.34 20037508.34</TopLeftCorner>
|
||||
<TileWidth>256</TileWidth>
|
||||
<TileHeight>256</TileHeight>
|
||||
<MatrixWidth>16</MatrixWidth>
|
||||
<MatrixHeight>16</MatrixHeight>
|
||||
</TileMatrix>
|
||||
<TileMatrix>
|
||||
<ows:Identifier>5</ows:Identifier>
|
||||
<ScaleDenominator>17471320.750897</ScaleDenominator>
|
||||
<TopLeftCorner>-20037508.34 20037508.34</TopLeftCorner>
|
||||
<TileWidth>256</TileWidth>
|
||||
<TileHeight>256</TileHeight>
|
||||
<MatrixWidth>32</MatrixWidth>
|
||||
<MatrixHeight>32</MatrixHeight>
|
||||
</TileMatrix>
|
||||
<TileMatrix>
|
||||
<ows:Identifier>6</ows:Identifier>
|
||||
<ScaleDenominator>8735660.3754487</ScaleDenominator>
|
||||
<TopLeftCorner>-20037508.34 20037508.34</TopLeftCorner>
|
||||
<TileWidth>256</TileWidth>
|
||||
<TileHeight>256</TileHeight>
|
||||
<MatrixWidth>64</MatrixWidth>
|
||||
<MatrixHeight>64</MatrixHeight>
|
||||
</TileMatrix>
|
||||
<TileMatrix>
|
||||
<ows:Identifier>7</ows:Identifier>
|
||||
<ScaleDenominator>4367830.1877244</ScaleDenominator>
|
||||
<TopLeftCorner>-20037508.34 20037508.34</TopLeftCorner>
|
||||
<TileWidth>256</TileWidth>
|
||||
<TileHeight>256</TileHeight>
|
||||
<MatrixWidth>128</MatrixWidth>
|
||||
<MatrixHeight>128</MatrixHeight>
|
||||
</TileMatrix>
|
||||
<TileMatrix>
|
||||
<ows:Identifier>8</ows:Identifier>
|
||||
<ScaleDenominator>2183915.0938622</ScaleDenominator>
|
||||
<TopLeftCorner>-20037508.34 20037508.34</TopLeftCorner>
|
||||
<TileWidth>256</TileWidth>
|
||||
<TileHeight>256</TileHeight>
|
||||
<MatrixWidth>256</MatrixWidth>
|
||||
<MatrixHeight>256</MatrixHeight>
|
||||
</TileMatrix>
|
||||
<TileMatrix>
|
||||
<ows:Identifier>9</ows:Identifier>
|
||||
<ScaleDenominator>1091957.5469311</ScaleDenominator>
|
||||
<TopLeftCorner>-20037508.34 20037508.34</TopLeftCorner>
|
||||
<TileWidth>256</TileWidth>
|
||||
<TileHeight>256</TileHeight>
|
||||
<MatrixWidth>512</MatrixWidth>
|
||||
<MatrixHeight>512</MatrixHeight>
|
||||
</TileMatrix>
|
||||
<TileMatrix>
|
||||
<ows:Identifier>10</ows:Identifier>
|
||||
<ScaleDenominator>545978.77346554</ScaleDenominator>
|
||||
<TopLeftCorner>-20037508.34 20037508.34</TopLeftCorner>
|
||||
<TileWidth>256</TileWidth>
|
||||
<TileHeight>256</TileHeight>
|
||||
<MatrixWidth>1024</MatrixWidth>
|
||||
<MatrixHeight>1024</MatrixHeight>
|
||||
</TileMatrix>
|
||||
<TileMatrix>
|
||||
<ows:Identifier>11</ows:Identifier>
|
||||
<ScaleDenominator>272989.38673277</ScaleDenominator>
|
||||
<TopLeftCorner>-20037508.34 20037508.34</TopLeftCorner>
|
||||
<TileWidth>256</TileWidth>
|
||||
<TileHeight>256</TileHeight>
|
||||
<MatrixWidth>2048</MatrixWidth>
|
||||
<MatrixHeight>2048</MatrixHeight>
|
||||
</TileMatrix>
|
||||
<TileMatrix>
|
||||
<ows:Identifier>12</ows:Identifier>
|
||||
<ScaleDenominator>136494.69336639</ScaleDenominator>
|
||||
<TopLeftCorner>-20037508.34 20037508.34</TopLeftCorner>
|
||||
<TileWidth>256</TileWidth>
|
||||
<TileHeight>256</TileHeight>
|
||||
<MatrixWidth>4096</MatrixWidth>
|
||||
<MatrixHeight>4096</MatrixHeight>
|
||||
</TileMatrix>
|
||||
<TileMatrix>
|
||||
<ows:Identifier>13</ows:Identifier>
|
||||
<ScaleDenominator>68247.346683193</ScaleDenominator>
|
||||
<TopLeftCorner>-20037508.34 20037508.34</TopLeftCorner>
|
||||
<TileWidth>256</TileWidth>
|
||||
<TileHeight>256</TileHeight>
|
||||
<MatrixWidth>8192</MatrixWidth>
|
||||
<MatrixHeight>8192</MatrixHeight>
|
||||
</TileMatrix>
|
||||
<TileMatrix>
|
||||
<ows:Identifier>14</ows:Identifier>
|
||||
<ScaleDenominator>34123.673341597</ScaleDenominator>
|
||||
<TopLeftCorner>-20037508.34 20037508.34</TopLeftCorner>
|
||||
<TileWidth>256</TileWidth>
|
||||
<TileHeight>256</TileHeight>
|
||||
<MatrixWidth>16384</MatrixWidth>
|
||||
<MatrixHeight>16384</MatrixHeight>
|
||||
</TileMatrix>
|
||||
<TileMatrix>
|
||||
<ows:Identifier>15</ows:Identifier>
|
||||
<ScaleDenominator>17061.836670798</ScaleDenominator>
|
||||
<TopLeftCorner>-20037508.34 20037508.34</TopLeftCorner>
|
||||
<TileWidth>256</TileWidth>
|
||||
<TileHeight>256</TileHeight>
|
||||
<MatrixWidth>32768</MatrixWidth>
|
||||
<MatrixHeight>32768</MatrixHeight>
|
||||
</TileMatrix>
|
||||
<TileMatrix>
|
||||
<ows:Identifier>16</ows:Identifier>
|
||||
<ScaleDenominator>8530.9183353991</ScaleDenominator>
|
||||
<TopLeftCorner>-20037508.34 20037508.34</TopLeftCorner>
|
||||
<TileWidth>256</TileWidth>
|
||||
<TileHeight>256</TileHeight>
|
||||
<MatrixWidth>65536</MatrixWidth>
|
||||
<MatrixHeight>65536</MatrixHeight>
|
||||
</TileMatrix>
|
||||
<TileMatrix>
|
||||
<ows:Identifier>17</ows:Identifier>
|
||||
<ScaleDenominator>4265.4591676996</ScaleDenominator>
|
||||
<TopLeftCorner>-20037508.34 20037508.34</TopLeftCorner>
|
||||
<TileWidth>256</TileWidth>
|
||||
<TileHeight>256</TileHeight>
|
||||
<MatrixWidth>131072</MatrixWidth>
|
||||
<MatrixHeight>131072</MatrixHeight>
|
||||
</TileMatrix>
|
||||
<TileMatrix>
|
||||
<ows:Identifier>18</ows:Identifier>
|
||||
<ScaleDenominator>2132.7295838498</ScaleDenominator>
|
||||
<TopLeftCorner>-20037508.34 20037508.34</TopLeftCorner>
|
||||
<TileWidth>256</TileWidth>
|
||||
<TileHeight>256</TileHeight>
|
||||
<MatrixWidth>262144</MatrixWidth>
|
||||
<MatrixHeight>262144</MatrixHeight>
|
||||
</TileMatrix>
|
||||
</TileMatrixSet>
|
||||
<TileMatrixSet>
|
||||
<ows:Title>GoogleMapsCompatible_512</ows:Title>
|
||||
<ows:Abstract>GoogleMapsCompatible_512 EPSG:3857</ows:Abstract>
|
||||
<ows:Identifier>GoogleMapsCompatible_512</ows:Identifier>
|
||||
<ows:SupportedCRS>urn:ogc:def:crs:EPSG::3857</ows:SupportedCRS>
|
||||
<TileMatrix>
|
||||
<ows:Identifier>0</ows:Identifier>
|
||||
<ScaleDenominator>279541132.0143589</ScaleDenominator>
|
||||
<TopLeftCorner>-20037508.34 20037508.34</TopLeftCorner>
|
||||
<TileWidth>512</TileWidth>
|
||||
<TileHeight>512</TileHeight>
|
||||
<MatrixWidth>1</MatrixWidth>
|
||||
<MatrixHeight>1</MatrixHeight>
|
||||
</TileMatrix>
|
||||
<TileMatrix>
|
||||
<ows:Identifier>1</ows:Identifier>
|
||||
<ScaleDenominator>139770566.0071794</ScaleDenominator>
|
||||
<TopLeftCorner>-20037508.34 20037508.34</TopLeftCorner>
|
||||
<TileWidth>512</TileWidth>
|
||||
<TileHeight>512</TileHeight>
|
||||
<MatrixWidth>2</MatrixWidth>
|
||||
<MatrixHeight>2</MatrixHeight>
|
||||
</TileMatrix>
|
||||
<TileMatrix>
|
||||
<ows:Identifier>2</ows:Identifier>
|
||||
<ScaleDenominator>69885283.00358972</ScaleDenominator>
|
||||
<TopLeftCorner>-20037508.34 20037508.34</TopLeftCorner>
|
||||
<TileWidth>512</TileWidth>
|
||||
<TileHeight>512</TileHeight>
|
||||
<MatrixWidth>4</MatrixWidth>
|
||||
<MatrixHeight>4</MatrixHeight>
|
||||
</TileMatrix>
|
||||
<TileMatrix>
|
||||
<ows:Identifier>3</ows:Identifier>
|
||||
<ScaleDenominator>34942641.501795</ScaleDenominator>
|
||||
<TopLeftCorner>-20037508.34 20037508.34</TopLeftCorner>
|
||||
<TileWidth>512</TileWidth>
|
||||
<TileHeight>512</TileHeight>
|
||||
<MatrixWidth>8</MatrixWidth>
|
||||
<MatrixHeight>8</MatrixHeight>
|
||||
</TileMatrix>
|
||||
<TileMatrix>
|
||||
<ows:Identifier>4</ows:Identifier>
|
||||
<ScaleDenominator>17471320.750897</ScaleDenominator>
|
||||
<TopLeftCorner>-20037508.34 20037508.34</TopLeftCorner>
|
||||
<TileWidth>512</TileWidth>
|
||||
<TileHeight>512</TileHeight>
|
||||
<MatrixWidth>16</MatrixWidth>
|
||||
<MatrixHeight>16</MatrixHeight>
|
||||
</TileMatrix>
|
||||
<TileMatrix>
|
||||
<ows:Identifier>5</ows:Identifier>
|
||||
<ScaleDenominator>8735660.3754487</ScaleDenominator>
|
||||
<TopLeftCorner>-20037508.34 20037508.34</TopLeftCorner>
|
||||
<TileWidth>512</TileWidth>
|
||||
<TileHeight>512</TileHeight>
|
||||
<MatrixWidth>32</MatrixWidth>
|
||||
<MatrixHeight>32</MatrixHeight>
|
||||
</TileMatrix>
|
||||
<TileMatrix>
|
||||
<ows:Identifier>6</ows:Identifier>
|
||||
<ScaleDenominator>4367830.1877244</ScaleDenominator>
|
||||
<TopLeftCorner>-20037508.34 20037508.34</TopLeftCorner>
|
||||
<TileWidth>512</TileWidth>
|
||||
<TileHeight>512</TileHeight>
|
||||
<MatrixWidth>64</MatrixWidth>
|
||||
<MatrixHeight>64</MatrixHeight>
|
||||
</TileMatrix>
|
||||
<TileMatrix>
|
||||
<ows:Identifier>7</ows:Identifier>
|
||||
<ScaleDenominator>2183915.0938622</ScaleDenominator>
|
||||
<TopLeftCorner>-20037508.34 20037508.34</TopLeftCorner>
|
||||
<TileWidth>512</TileWidth>
|
||||
<TileHeight>512</TileHeight>
|
||||
<MatrixWidth>128</MatrixWidth>
|
||||
<MatrixHeight>128</MatrixHeight>
|
||||
</TileMatrix>
|
||||
<TileMatrix>
|
||||
<ows:Identifier>8</ows:Identifier>
|
||||
<ScaleDenominator>1091957.5469311</ScaleDenominator>
|
||||
<TopLeftCorner>-20037508.34 20037508.34</TopLeftCorner>
|
||||
<TileWidth>512</TileWidth>
|
||||
<TileHeight>512</TileHeight>
|
||||
<MatrixWidth>256</MatrixWidth>
|
||||
<MatrixHeight>256</MatrixHeight>
|
||||
</TileMatrix>
|
||||
<TileMatrix>
|
||||
<ows:Identifier>9</ows:Identifier>
|
||||
<ScaleDenominator>545978.77346554</ScaleDenominator>
|
||||
<TopLeftCorner>-20037508.34 20037508.34</TopLeftCorner>
|
||||
<TileWidth>512</TileWidth>
|
||||
<TileHeight>512</TileHeight>
|
||||
<MatrixWidth>512</MatrixWidth>
|
||||
<MatrixHeight>512</MatrixHeight>
|
||||
</TileMatrix>
|
||||
<TileMatrix>
|
||||
<ows:Identifier>10</ows:Identifier>
|
||||
<ScaleDenominator>272989.38673277</ScaleDenominator>
|
||||
<TopLeftCorner>-20037508.34 20037508.34</TopLeftCorner>
|
||||
<TileWidth>512</TileWidth>
|
||||
<TileHeight>512</TileHeight>
|
||||
<MatrixWidth>1024</MatrixWidth>
|
||||
<MatrixHeight>1024</MatrixHeight>
|
||||
</TileMatrix>
|
||||
<TileMatrix>
|
||||
<ows:Identifier>11</ows:Identifier>
|
||||
<ScaleDenominator>136494.69336639</ScaleDenominator>
|
||||
<TopLeftCorner>-20037508.34 20037508.34</TopLeftCorner>
|
||||
<TileWidth>512</TileWidth>
|
||||
<TileHeight>512</TileHeight>
|
||||
<MatrixWidth>2048</MatrixWidth>
|
||||
<MatrixHeight>2048</MatrixHeight>
|
||||
</TileMatrix>
|
||||
<TileMatrix>
|
||||
<ows:Identifier>12</ows:Identifier>
|
||||
<ScaleDenominator>68247.346683193</ScaleDenominator>
|
||||
<TopLeftCorner>-20037508.34 20037508.34</TopLeftCorner>
|
||||
<TileWidth>512</TileWidth>
|
||||
<TileHeight>512</TileHeight>
|
||||
<MatrixWidth>4096</MatrixWidth>
|
||||
<MatrixHeight>4096</MatrixHeight>
|
||||
</TileMatrix>
|
||||
<TileMatrix>
|
||||
<ows:Identifier>13</ows:Identifier>
|
||||
<ScaleDenominator>34123.673341597</ScaleDenominator>
|
||||
<TopLeftCorner>-20037508.34 20037508.34</TopLeftCorner>
|
||||
<TileWidth>512</TileWidth>
|
||||
<TileHeight>512</TileHeight>
|
||||
<MatrixWidth>8192</MatrixWidth>
|
||||
<MatrixHeight>8192</MatrixHeight>
|
||||
</TileMatrix>
|
||||
<TileMatrix>
|
||||
<ows:Identifier>14</ows:Identifier>
|
||||
<ScaleDenominator>17061.836670798</ScaleDenominator>
|
||||
<TopLeftCorner>-20037508.34 20037508.34</TopLeftCorner>
|
||||
<TileWidth>512</TileWidth>
|
||||
<TileHeight>512</TileHeight>
|
||||
<MatrixWidth>16384</MatrixWidth>
|
||||
<MatrixHeight>16384</MatrixHeight>
|
||||
</TileMatrix>
|
||||
<TileMatrix>
|
||||
<ows:Identifier>15</ows:Identifier>
|
||||
<ScaleDenominator>8530.9183353991</ScaleDenominator>
|
||||
<TopLeftCorner>-20037508.34 20037508.34</TopLeftCorner>
|
||||
<TileWidth>512</TileWidth>
|
||||
<TileHeight>512</TileHeight>
|
||||
<MatrixWidth>32768</MatrixWidth>
|
||||
<MatrixHeight>32768</MatrixHeight>
|
||||
</TileMatrix>
|
||||
<TileMatrix>
|
||||
<ows:Identifier>16</ows:Identifier>
|
||||
<ScaleDenominator>4265.4591676996</ScaleDenominator>
|
||||
<TopLeftCorner>-20037508.34 20037508.34</TopLeftCorner>
|
||||
<TileWidth>512</TileWidth>
|
||||
<TileHeight>512</TileHeight>
|
||||
<MatrixWidth>65536</MatrixWidth>
|
||||
<MatrixHeight>65536</MatrixHeight>
|
||||
</TileMatrix>
|
||||
<TileMatrix>
|
||||
<ows:Identifier>17</ows:Identifier>
|
||||
<ScaleDenominator>2132.7295838498</ScaleDenominator>
|
||||
<TopLeftCorner>-20037508.34 20037508.34</TopLeftCorner>
|
||||
<TileWidth>512</TileWidth>
|
||||
<TileHeight>512</TileHeight>
|
||||
<MatrixWidth>131072</MatrixWidth>
|
||||
<MatrixHeight>131072</MatrixHeight>
|
||||
</TileMatrix>
|
||||
<TileMatrix>
|
||||
<ows:Identifier>18</ows:Identifier>
|
||||
<ScaleDenominator>1066.364791924892</ScaleDenominator>
|
||||
<TopLeftCorner>-20037508.34 20037508.34</TopLeftCorner>
|
||||
<TileWidth>512</TileWidth>
|
||||
<TileHeight>512</TileHeight>
|
||||
<MatrixWidth>262144</MatrixWidth>
|
||||
<MatrixHeight>262144</MatrixHeight>
|
||||
</TileMatrix>
|
||||
</TileMatrixSet>
|
||||
<TileMatrixSet>
|
||||
<ows:Title>WGS84_256</ows:Title>
|
||||
<ows:Abstract>WGS84_256 EPSG:4326</ows:Abstract>
|
||||
<ows:Identifier>WGS84_256</ows:Identifier>
|
||||
<ows:SupportedCRS>urn:ogc:def:crs:EPSG::4326</ows:SupportedCRS>
|
||||
<TileMatrix>
|
||||
<ows:Identifier>0</ows:Identifier>
|
||||
<ScaleDenominator>279541132.01436</ScaleDenominator>
|
||||
<TopLeftCorner>90 -180</TopLeftCorner>
|
||||
<TileWidth>256</TileWidth>
|
||||
<TileHeight>256</TileHeight>
|
||||
<MatrixWidth>2</MatrixWidth>
|
||||
<MatrixHeight>1</MatrixHeight>
|
||||
</TileMatrix>
|
||||
<TileMatrix>
|
||||
<ows:Identifier>1</ows:Identifier>
|
||||
<ScaleDenominator>139770566.00718</ScaleDenominator>
|
||||
<TopLeftCorner>90 -180</TopLeftCorner>
|
||||
<TileWidth>256</TileWidth>
|
||||
<TileHeight>256</TileHeight>
|
||||
<MatrixWidth>4</MatrixWidth>
|
||||
<MatrixHeight>2</MatrixHeight>
|
||||
</TileMatrix>
|
||||
<TileMatrix>
|
||||
<ows:Identifier>2</ows:Identifier>
|
||||
<ScaleDenominator>69885283.00359</ScaleDenominator>
|
||||
<TopLeftCorner>90 -180</TopLeftCorner>
|
||||
<TileWidth>256</TileWidth>
|
||||
<TileHeight>256</TileHeight>
|
||||
<MatrixWidth>8</MatrixWidth>
|
||||
<MatrixHeight>4</MatrixHeight>
|
||||
</TileMatrix>
|
||||
<TileMatrix>
|
||||
<ows:Identifier>3</ows:Identifier>
|
||||
<ScaleDenominator>34942641.501795</ScaleDenominator>
|
||||
<TopLeftCorner>90 -180</TopLeftCorner>
|
||||
<TileWidth>256</TileWidth>
|
||||
<TileHeight>256</TileHeight>
|
||||
<MatrixWidth>16</MatrixWidth>
|
||||
<MatrixHeight>8</MatrixHeight>
|
||||
</TileMatrix>
|
||||
<TileMatrix>
|
||||
<ows:Identifier>4</ows:Identifier>
|
||||
<ScaleDenominator>17471320.750897</ScaleDenominator>
|
||||
<TopLeftCorner>90 -180</TopLeftCorner>
|
||||
<TileWidth>256</TileWidth>
|
||||
<TileHeight>256</TileHeight>
|
||||
<MatrixWidth>32</MatrixWidth>
|
||||
<MatrixHeight>16</MatrixHeight>
|
||||
</TileMatrix>
|
||||
<TileMatrix>
|
||||
<ows:Identifier>5</ows:Identifier>
|
||||
<ScaleDenominator>8735660.3754487</ScaleDenominator>
|
||||
<TopLeftCorner>90 -180</TopLeftCorner>
|
||||
<TileWidth>256</TileWidth>
|
||||
<TileHeight>256</TileHeight>
|
||||
<MatrixWidth>64</MatrixWidth>
|
||||
<MatrixHeight>32</MatrixHeight>
|
||||
</TileMatrix>
|
||||
<TileMatrix>
|
||||
<ows:Identifier>6</ows:Identifier>
|
||||
<ScaleDenominator>4367830.1877244</ScaleDenominator>
|
||||
<TopLeftCorner>90 -180</TopLeftCorner>
|
||||
<TileWidth>256</TileWidth>
|
||||
<TileHeight>256</TileHeight>
|
||||
<MatrixWidth>128</MatrixWidth>
|
||||
<MatrixHeight>64</MatrixHeight>
|
||||
</TileMatrix>
|
||||
<TileMatrix>
|
||||
<ows:Identifier>7</ows:Identifier>
|
||||
<ScaleDenominator>2183915.0938622</ScaleDenominator>
|
||||
<TopLeftCorner>90 -180</TopLeftCorner>
|
||||
<TileWidth>256</TileWidth>
|
||||
<TileHeight>256</TileHeight>
|
||||
<MatrixWidth>256</MatrixWidth>
|
||||
<MatrixHeight>128</MatrixHeight>
|
||||
</TileMatrix>
|
||||
<TileMatrix>
|
||||
<ows:Identifier>8</ows:Identifier>
|
||||
<ScaleDenominator>1091957.5469311</ScaleDenominator>
|
||||
<TopLeftCorner>90 -180</TopLeftCorner>
|
||||
<TileWidth>256</TileWidth>
|
||||
<TileHeight>256</TileHeight>
|
||||
<MatrixWidth>512</MatrixWidth>
|
||||
<MatrixHeight>256</MatrixHeight>
|
||||
</TileMatrix>
|
||||
<TileMatrix>
|
||||
<ows:Identifier>9</ows:Identifier>
|
||||
<ScaleDenominator>545978.77346554</ScaleDenominator>
|
||||
<TopLeftCorner>90 -180</TopLeftCorner>
|
||||
<TileWidth>256</TileWidth>
|
||||
<TileHeight>256</TileHeight>
|
||||
<MatrixWidth>1024</MatrixWidth>
|
||||
<MatrixHeight>512</MatrixHeight>
|
||||
</TileMatrix>
|
||||
<TileMatrix>
|
||||
<ows:Identifier>10</ows:Identifier>
|
||||
<ScaleDenominator>272989.38673277</ScaleDenominator>
|
||||
<TopLeftCorner>90 -180</TopLeftCorner>
|
||||
<TileWidth>256</TileWidth>
|
||||
<TileHeight>256</TileHeight>
|
||||
<MatrixWidth>2048</MatrixWidth>
|
||||
<MatrixHeight>1024</MatrixHeight>
|
||||
</TileMatrix>
|
||||
<TileMatrix>
|
||||
<ows:Identifier>11</ows:Identifier>
|
||||
<ScaleDenominator>136494.69336639</ScaleDenominator>
|
||||
<TopLeftCorner>90 -180</TopLeftCorner>
|
||||
<TileWidth>256</TileWidth>
|
||||
<TileHeight>256</TileHeight>
|
||||
<MatrixWidth>4096</MatrixWidth>
|
||||
<MatrixHeight>2048</MatrixHeight>
|
||||
</TileMatrix>
|
||||
<TileMatrix>
|
||||
<ows:Identifier>12</ows:Identifier>
|
||||
<ScaleDenominator>68247.346683193</ScaleDenominator>
|
||||
<TopLeftCorner>90 -180</TopLeftCorner>
|
||||
<TileWidth>256</TileWidth>
|
||||
<TileHeight>256</TileHeight>
|
||||
<MatrixWidth>8192</MatrixWidth>
|
||||
<MatrixHeight>4096</MatrixHeight>
|
||||
</TileMatrix>
|
||||
<TileMatrix>
|
||||
<ows:Identifier>13</ows:Identifier>
|
||||
<ScaleDenominator>34123.673341597</ScaleDenominator>
|
||||
<TopLeftCorner>90 -180</TopLeftCorner>
|
||||
<TileWidth>256</TileWidth>
|
||||
<TileHeight>256</TileHeight>
|
||||
<MatrixWidth>16384</MatrixWidth>
|
||||
<MatrixHeight>8192</MatrixHeight>
|
||||
</TileMatrix>
|
||||
<TileMatrix>
|
||||
<ows:Identifier>14</ows:Identifier>
|
||||
<ScaleDenominator>17061.836670798</ScaleDenominator>
|
||||
<TopLeftCorner>90 -180</TopLeftCorner>
|
||||
<TileWidth>256</TileWidth>
|
||||
<TileHeight>256</TileHeight>
|
||||
<MatrixWidth>32768</MatrixWidth>
|
||||
<MatrixHeight>16384</MatrixHeight>
|
||||
</TileMatrix>
|
||||
<TileMatrix>
|
||||
<ows:Identifier>15</ows:Identifier>
|
||||
<ScaleDenominator>8530.9183353991</ScaleDenominator>
|
||||
<TopLeftCorner>90 -180</TopLeftCorner>
|
||||
<TileWidth>256</TileWidth>
|
||||
<TileHeight>256</TileHeight>
|
||||
<MatrixWidth>65536</MatrixWidth>
|
||||
<MatrixHeight>32768</MatrixHeight>
|
||||
</TileMatrix>
|
||||
<TileMatrix>
|
||||
<ows:Identifier>16</ows:Identifier>
|
||||
<ScaleDenominator>4265.4591676996</ScaleDenominator>
|
||||
<TopLeftCorner>90 -180</TopLeftCorner>
|
||||
<TileWidth>256</TileWidth>
|
||||
<TileHeight>256</TileHeight>
|
||||
<MatrixWidth>131072</MatrixWidth>
|
||||
<MatrixHeight>65536</MatrixHeight>
|
||||
</TileMatrix>
|
||||
<TileMatrix>
|
||||
<ows:Identifier>17</ows:Identifier>
|
||||
<ScaleDenominator>2132.7295838498</ScaleDenominator>
|
||||
<TopLeftCorner>90 -180</TopLeftCorner>
|
||||
<TileWidth>256</TileWidth>
|
||||
<TileHeight>256</TileHeight>
|
||||
<MatrixWidth>262144</MatrixWidth>
|
||||
<MatrixHeight>131072</MatrixHeight>
|
||||
</TileMatrix>
|
||||
<TileMatrix>
|
||||
<ows:Identifier>18</ows:Identifier>
|
||||
<ScaleDenominator>1066.3647919249</ScaleDenominator>
|
||||
<TopLeftCorner>90 -180</TopLeftCorner>
|
||||
<TileWidth>256</TileWidth>
|
||||
<TileHeight>256</TileHeight>
|
||||
<MatrixWidth>524288</MatrixWidth>
|
||||
<MatrixHeight>262144</MatrixHeight>
|
||||
</TileMatrix>
|
||||
</TileMatrixSet>
|
||||
<TileMatrixSet>
|
||||
<ows:Title>WGS84_512</ows:Title>
|
||||
<ows:Abstract>WGS84_512 EPSG:4326</ows:Abstract>
|
||||
<ows:Identifier>WGS84_512</ows:Identifier>
|
||||
<ows:SupportedCRS>urn:ogc:def:crs:EPSG::4326</ows:SupportedCRS>
|
||||
<TileMatrix>
|
||||
<ows:Identifier>0</ows:Identifier>
|
||||
<ScaleDenominator>139770566.00718</ScaleDenominator>
|
||||
<TopLeftCorner>90 -180</TopLeftCorner>
|
||||
<TileWidth>512</TileWidth>
|
||||
<TileHeight>512</TileHeight>
|
||||
<MatrixWidth>2</MatrixWidth>
|
||||
<MatrixHeight>1</MatrixHeight>
|
||||
</TileMatrix>
|
||||
<TileMatrix>
|
||||
<ows:Identifier>1</ows:Identifier>
|
||||
<ScaleDenominator>69885283.00359</ScaleDenominator>
|
||||
<TopLeftCorner>90 -180</TopLeftCorner>
|
||||
<TileWidth>512</TileWidth>
|
||||
<TileHeight>512</TileHeight>
|
||||
<MatrixWidth>4</MatrixWidth>
|
||||
<MatrixHeight>2</MatrixHeight>
|
||||
</TileMatrix>
|
||||
<TileMatrix>
|
||||
<ows:Identifier>2</ows:Identifier>
|
||||
<ScaleDenominator>34942641.501795</ScaleDenominator>
|
||||
<TopLeftCorner>90 -180</TopLeftCorner>
|
||||
<TileWidth>512</TileWidth>
|
||||
<TileHeight>512</TileHeight>
|
||||
<MatrixWidth>8</MatrixWidth>
|
||||
<MatrixHeight>4</MatrixHeight>
|
||||
</TileMatrix>
|
||||
<TileMatrix>
|
||||
<ows:Identifier>3</ows:Identifier>
|
||||
<ScaleDenominator>17471320.750897</ScaleDenominator>
|
||||
<TopLeftCorner>90 -180</TopLeftCorner>
|
||||
<TileWidth>512</TileWidth>
|
||||
<TileHeight>512</TileHeight>
|
||||
<MatrixWidth>16</MatrixWidth>
|
||||
<MatrixHeight>8</MatrixHeight>
|
||||
</TileMatrix>
|
||||
<TileMatrix>
|
||||
<ows:Identifier>4</ows:Identifier>
|
||||
<ScaleDenominator>8735660.3754487</ScaleDenominator>
|
||||
<TopLeftCorner>90 -180</TopLeftCorner>
|
||||
<TileWidth>512</TileWidth>
|
||||
<TileHeight>512</TileHeight>
|
||||
<MatrixWidth>32</MatrixWidth>
|
||||
<MatrixHeight>16</MatrixHeight>
|
||||
</TileMatrix>
|
||||
<TileMatrix>
|
||||
<ows:Identifier>5</ows:Identifier>
|
||||
<ScaleDenominator>4367830.1877244</ScaleDenominator>
|
||||
<TopLeftCorner>90 -180</TopLeftCorner>
|
||||
<TileWidth>512</TileWidth>
|
||||
<TileHeight>512</TileHeight>
|
||||
<MatrixWidth>64</MatrixWidth>
|
||||
<MatrixHeight>32</MatrixHeight>
|
||||
</TileMatrix>
|
||||
<TileMatrix>
|
||||
<ows:Identifier>6</ows:Identifier>
|
||||
<ScaleDenominator>2183915.0938622</ScaleDenominator>
|
||||
<TopLeftCorner>90 -180</TopLeftCorner>
|
||||
<TileWidth>512</TileWidth>
|
||||
<TileHeight>512</TileHeight>
|
||||
<MatrixWidth>128</MatrixWidth>
|
||||
<MatrixHeight>64</MatrixHeight>
|
||||
</TileMatrix>
|
||||
<TileMatrix>
|
||||
<ows:Identifier>7</ows:Identifier>
|
||||
<ScaleDenominator>1091957.5469311</ScaleDenominator>
|
||||
<TopLeftCorner>90 -180</TopLeftCorner>
|
||||
<TileWidth>512</TileWidth>
|
||||
<TileHeight>512</TileHeight>
|
||||
<MatrixWidth>256</MatrixWidth>
|
||||
<MatrixHeight>128</MatrixHeight>
|
||||
</TileMatrix>
|
||||
<TileMatrix>
|
||||
<ows:Identifier>8</ows:Identifier>
|
||||
<ScaleDenominator>545978.77346554</ScaleDenominator>
|
||||
<TopLeftCorner>90 -180</TopLeftCorner>
|
||||
<TileWidth>512</TileWidth>
|
||||
<TileHeight>512</TileHeight>
|
||||
<MatrixWidth>512</MatrixWidth>
|
||||
<MatrixHeight>256</MatrixHeight>
|
||||
</TileMatrix>
|
||||
<TileMatrix>
|
||||
<ows:Identifier>9</ows:Identifier>
|
||||
<ScaleDenominator>272989.38673277</ScaleDenominator>
|
||||
<TopLeftCorner>90 -180</TopLeftCorner>
|
||||
<TileWidth>512</TileWidth>
|
||||
<TileHeight>512</TileHeight>
|
||||
<MatrixWidth>1024</MatrixWidth>
|
||||
<MatrixHeight>512</MatrixHeight>
|
||||
</TileMatrix>
|
||||
<TileMatrix>
|
||||
<ows:Identifier>10</ows:Identifier>
|
||||
<ScaleDenominator>136494.69336639</ScaleDenominator>
|
||||
<TopLeftCorner>90 -180</TopLeftCorner>
|
||||
<TileWidth>512</TileWidth>
|
||||
<TileHeight>512</TileHeight>
|
||||
<MatrixWidth>2048</MatrixWidth>
|
||||
<MatrixHeight>1024</MatrixHeight>
|
||||
</TileMatrix>
|
||||
<TileMatrix>
|
||||
<ows:Identifier>11</ows:Identifier>
|
||||
<ScaleDenominator>68247.346683193</ScaleDenominator>
|
||||
<TopLeftCorner>90 -180</TopLeftCorner>
|
||||
<TileWidth>512</TileWidth>
|
||||
<TileHeight>512</TileHeight>
|
||||
<MatrixWidth>4096</MatrixWidth>
|
||||
<MatrixHeight>2048</MatrixHeight>
|
||||
</TileMatrix>
|
||||
<TileMatrix>
|
||||
<ows:Identifier>12</ows:Identifier>
|
||||
<ScaleDenominator>34123.673341597</ScaleDenominator>
|
||||
<TopLeftCorner>90 -180</TopLeftCorner>
|
||||
<TileWidth>512</TileWidth>
|
||||
<TileHeight>512</TileHeight>
|
||||
<MatrixWidth>8192</MatrixWidth>
|
||||
<MatrixHeight>4096</MatrixHeight>
|
||||
</TileMatrix>
|
||||
<TileMatrix>
|
||||
<ows:Identifier>13</ows:Identifier>
|
||||
<ScaleDenominator>17061.836670798</ScaleDenominator>
|
||||
<TopLeftCorner>90 -180</TopLeftCorner>
|
||||
<TileWidth>512</TileWidth>
|
||||
<TileHeight>512</TileHeight>
|
||||
<MatrixWidth>16384</MatrixWidth>
|
||||
<MatrixHeight>8192</MatrixHeight>
|
||||
</TileMatrix>
|
||||
<TileMatrix>
|
||||
<ows:Identifier>14</ows:Identifier>
|
||||
<ScaleDenominator>8530.9183353991</ScaleDenominator>
|
||||
<TopLeftCorner>90 -180</TopLeftCorner>
|
||||
<TileWidth>512</TileWidth>
|
||||
<TileHeight>512</TileHeight>
|
||||
<MatrixWidth>32768</MatrixWidth>
|
||||
<MatrixHeight>16384</MatrixHeight>
|
||||
</TileMatrix>
|
||||
<TileMatrix>
|
||||
<ows:Identifier>15</ows:Identifier>
|
||||
<ScaleDenominator>4265.4591676996</ScaleDenominator>
|
||||
<TopLeftCorner>90 -180</TopLeftCorner>
|
||||
<TileWidth>512</TileWidth>
|
||||
<TileHeight>512</TileHeight>
|
||||
<MatrixWidth>65536</MatrixWidth>
|
||||
<MatrixHeight>32768</MatrixHeight>
|
||||
</TileMatrix>
|
||||
<TileMatrix>
|
||||
<ows:Identifier>16</ows:Identifier>
|
||||
<ScaleDenominator>2132.7295838498</ScaleDenominator>
|
||||
<TopLeftCorner>90 -180</TopLeftCorner>
|
||||
<TileWidth>512</TileWidth>
|
||||
<TileHeight>512</TileHeight>
|
||||
<MatrixWidth>131072</MatrixWidth>
|
||||
<MatrixHeight>65536</MatrixHeight>
|
||||
</TileMatrix>
|
||||
<TileMatrix>
|
||||
<ows:Identifier>17</ows:Identifier>
|
||||
<ScaleDenominator>1066.3647919249</ScaleDenominator>
|
||||
<TopLeftCorner>90 -180</TopLeftCorner>
|
||||
<TileWidth>512</TileWidth>
|
||||
<TileHeight>512</TileHeight>
|
||||
<MatrixWidth>262144</MatrixWidth>
|
||||
<MatrixHeight>131072</MatrixHeight>
|
||||
</TileMatrix>
|
||||
<TileMatrix>
|
||||
<ows:Identifier>18</ows:Identifier>
|
||||
<ScaleDenominator>533.182</ScaleDenominator>
|
||||
<TopLeftCorner>90 -180</TopLeftCorner>
|
||||
<TileWidth>512</TileWidth>
|
||||
<TileHeight>512</TileHeight>
|
||||
<MatrixWidth>524288</MatrixWidth>
|
||||
<MatrixHeight>262144</MatrixHeight>
|
||||
</TileMatrix>
|
||||
</TileMatrixSet>
|
||||
</Contents>
|
||||
<ServiceMetadataURL xlink:href="{{baseUrl}}styles/{{id}}/wmts.xml"/>
|
||||
</Capabilities>
|
68
publish.js
Normal file
|
@ -0,0 +1,68 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
'use strict';
|
||||
|
||||
/*
|
||||
* This script creates `tileserver-gl-light` version
|
||||
* (without native dependencies) and publishes
|
||||
* `tileserver-gl` and `tileserver-gl-light` to npm.
|
||||
*/
|
||||
|
||||
/* CREATE tileserver-gl-light */
|
||||
|
||||
// SYNC THE `light` FOLDER
|
||||
|
||||
import child_process from 'child_process';
|
||||
child_process.execSync(
|
||||
'rsync -av --exclude="light" --exclude=".git" --exclude="node_modules" --delete . light',
|
||||
{
|
||||
stdio: 'inherit',
|
||||
},
|
||||
);
|
||||
|
||||
// PATCH `package.json`
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const packageJson = JSON.parse(
|
||||
fs.readFileSync(__dirname + '/package.json', 'utf8'),
|
||||
);
|
||||
|
||||
packageJson.name += '-light';
|
||||
packageJson.description =
|
||||
'Map tile server for JSON GL styles - serving vector tiles';
|
||||
delete packageJson.dependencies['canvas'];
|
||||
delete packageJson.dependencies['@maplibre/maplibre-gl-native'];
|
||||
delete packageJson.dependencies['sharp'];
|
||||
|
||||
delete packageJson.scripts['prepare'];
|
||||
|
||||
delete packageJson.optionalDependencies;
|
||||
delete packageJson.devDependencies;
|
||||
|
||||
packageJson.engines.node = '>= 14.15.0';
|
||||
|
||||
const str = JSON.stringify(packageJson, undefined, 2);
|
||||
fs.writeFileSync('light/package.json', str);
|
||||
fs.renameSync('light/README_light.md', 'light/README.md');
|
||||
fs.renameSync('light/Dockerfile_light', 'light/Dockerfile');
|
||||
fs.renameSync('light/docker-entrypoint_light.sh', 'light/docker-entrypoint.sh');
|
||||
|
||||
// for Build tileserver-gl-light docker image, don't publish
|
||||
if (process.argv.length > 2 && process.argv[2] == '--no-publish') {
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
/* PUBLISH */
|
||||
|
||||
// tileserver-gl
|
||||
child_process.execSync('npm publish . --access public', {
|
||||
stdio: 'inherit',
|
||||
});
|
||||
|
||||
// tileserver-gl-light
|
||||
child_process.execSync('npm publish ./light --access public', {
|
||||
stdio: 'inherit',
|
||||
});
|
2
run.sh
|
@ -1,2 +0,0 @@
|
|||
#!/bin/bash
|
||||
xvfb-run --server-args="-screen 0 1024x768x24" node /usr/src/app/src/main.js -p 80 -c /data/config.json
|
18
src/healthcheck.js
Normal file
|
@ -0,0 +1,18 @@
|
|||
import * as http from 'http';
|
||||
const options = {
|
||||
timeout: 2000,
|
||||
};
|
||||
const url = 'http://localhost:8080/health';
|
||||
const request = http.request(url, options, (res) => {
|
||||
console.log(`STATUS: ${res.statusCode}`);
|
||||
if (res.statusCode == 200) {
|
||||
process.exit(0);
|
||||
} else {
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
request.on('error', function (err) {
|
||||
console.log('ERROR');
|
||||
process.exit(1);
|
||||
});
|
||||
request.end();
|
314
src/main.js
|
@ -1,28 +1,300 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
'use strict';
|
||||
import os from 'os';
|
||||
|
||||
var opts = require('nomnom')
|
||||
.option('config', {
|
||||
abbr: 'c',
|
||||
default: 'config.json',
|
||||
help: 'Configuration file'
|
||||
})
|
||||
.option('port', {
|
||||
abbr: 'p',
|
||||
default: 8080,
|
||||
help: 'Port'
|
||||
})
|
||||
.option('version', {
|
||||
abbr: 'v',
|
||||
flag: true,
|
||||
help: 'Version info',
|
||||
callback: function() {
|
||||
return 'version ' + require('../package.json').version;
|
||||
const envSize = parseInt(process.env.UV_THREADPOOL_SIZE, 10);
|
||||
process.env.UV_THREADPOOL_SIZE = Math.ceil(
|
||||
Math.max(4, isNaN(envSize) ? os.cpus().length * 1.5 : envSize),
|
||||
);
|
||||
|
||||
import fs from 'node:fs';
|
||||
import fsp from 'node:fs/promises';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import axios from 'axios';
|
||||
import { server } from './server.js';
|
||||
import { isValidHttpUrl } from './utils.js';
|
||||
import { openPMtiles, getPMtilesInfo } from './pmtiles_adapter.js';
|
||||
import { program } from 'commander';
|
||||
import { existsP } from './promises.js';
|
||||
import { openMbTilesWrapper } from './mbtiles_wrapper.js';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const packageJson = JSON.parse(
|
||||
fs.readFileSync(__dirname + '/../package.json', 'utf8'),
|
||||
);
|
||||
|
||||
const args = process.argv;
|
||||
if (args.length >= 3 && args[2][0] !== '-') {
|
||||
args.splice(2, 0, '--mbtiles');
|
||||
}
|
||||
|
||||
program
|
||||
.description('tileserver-gl startup options')
|
||||
.usage('tileserver-gl [mbtiles] [options]')
|
||||
.option(
|
||||
'--file <file>',
|
||||
'MBTiles or PMTiles file\n' +
|
||||
'\t ignored if the configuration file is also specified',
|
||||
)
|
||||
.option(
|
||||
'--mbtiles <file>',
|
||||
'(DEPRECIATED) MBTiles file\n' +
|
||||
'\t ignored if file is also specified' +
|
||||
'\t ignored if the configuration file is also specified',
|
||||
)
|
||||
.option(
|
||||
'-c, --config <file>',
|
||||
'Configuration file [config.json]',
|
||||
'config.json',
|
||||
)
|
||||
.option('-b, --bind <address>', 'Bind address')
|
||||
.option('-p, --port <port>', 'Port [8080]', 8080, parseInt)
|
||||
.option('-C|--no-cors', 'Disable Cross-origin resource sharing headers')
|
||||
.option(
|
||||
'-u|--public_url <url>',
|
||||
'Enable exposing the server on subpaths, not necessarily the root of the domain',
|
||||
)
|
||||
.option('-V, --verbose', 'More verbose output')
|
||||
.option('-s, --silent', 'Less verbose output')
|
||||
.option('-l|--log_file <file>', 'output log file (defaults to standard out)')
|
||||
.option(
|
||||
'-f|--log_format <format>',
|
||||
'define the log format: https://github.com/expressjs/morgan#morganformat-options',
|
||||
)
|
||||
.version(packageJson.version, '-v, --version');
|
||||
program.parse(process.argv);
|
||||
const opts = program.opts();
|
||||
|
||||
console.log(`Starting ${packageJson.name} v${packageJson.version}`);
|
||||
|
||||
const startServer = (configPath, config) => {
|
||||
let publicUrl = opts.public_url;
|
||||
if (publicUrl && publicUrl.lastIndexOf('/') !== publicUrl.length - 1) {
|
||||
publicUrl += '/';
|
||||
}
|
||||
return server({
|
||||
configPath,
|
||||
config,
|
||||
bind: opts.bind,
|
||||
port: opts.port,
|
||||
cors: opts.cors,
|
||||
verbose: opts.verbose,
|
||||
silent: opts.silent,
|
||||
logFile: opts.log_file,
|
||||
logFormat: opts.log_format,
|
||||
publicUrl,
|
||||
});
|
||||
};
|
||||
|
||||
const startWithInputFile = async (inputFile) => {
|
||||
console.log(`[INFO] Automatically creating config file for ${inputFile}`);
|
||||
console.log(`[INFO] Only a basic preview style will be used.`);
|
||||
console.log(
|
||||
`[INFO] See documentation to learn how to create config.json file.`,
|
||||
);
|
||||
|
||||
let inputFilePath;
|
||||
if (isValidHttpUrl(inputFile)) {
|
||||
inputFilePath = process.cwd();
|
||||
} else {
|
||||
inputFile = path.resolve(process.cwd(), inputFile);
|
||||
inputFilePath = path.dirname(inputFile);
|
||||
|
||||
const inputFileStats = await fsp.stat(inputFile);
|
||||
if (!inputFileStats.isFile() || inputFileStats.size === 0) {
|
||||
console.log(`ERROR: Not a valid input file: `);
|
||||
process.exit(1);
|
||||
}
|
||||
}).parse();
|
||||
}
|
||||
|
||||
return require('./server')({
|
||||
config: opts.config,
|
||||
port: opts.port
|
||||
const styleDir = path.resolve(
|
||||
__dirname,
|
||||
'../node_modules/tileserver-gl-styles/',
|
||||
);
|
||||
|
||||
const config = {
|
||||
options: {
|
||||
paths: {
|
||||
root: styleDir,
|
||||
fonts: 'fonts',
|
||||
styles: 'styles',
|
||||
mbtiles: inputFilePath,
|
||||
pmtiles: inputFilePath,
|
||||
},
|
||||
},
|
||||
styles: {},
|
||||
data: {},
|
||||
};
|
||||
|
||||
const extension = inputFile.split('.').pop().toLowerCase();
|
||||
if (extension === 'pmtiles') {
|
||||
const fileOpenInfo = openPMtiles(inputFile);
|
||||
const metadata = await getPMtilesInfo(fileOpenInfo);
|
||||
|
||||
if (
|
||||
metadata.format === 'pbf' &&
|
||||
metadata.name.toLowerCase().indexOf('openmaptiles') > -1
|
||||
) {
|
||||
if (isValidHttpUrl(inputFile)) {
|
||||
config['data'][`v3`] = {
|
||||
pmtiles: inputFile,
|
||||
};
|
||||
} else {
|
||||
config['data'][`v3`] = {
|
||||
pmtiles: path.basename(inputFile),
|
||||
};
|
||||
}
|
||||
|
||||
const styles = await fsp.readdir(path.resolve(styleDir, 'styles'));
|
||||
for (const styleName of styles) {
|
||||
const styleFileRel = styleName + '/style.json';
|
||||
const styleFile = path.resolve(styleDir, 'styles', styleFileRel);
|
||||
if (await existsP(styleFile)) {
|
||||
config['styles'][styleName] = {
|
||||
style: styleFileRel,
|
||||
tilejson: {
|
||||
bounds: metadata.bounds,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.log(
|
||||
`WARN: PMTiles not in "openmaptiles" format. Serving raw data only...`,
|
||||
);
|
||||
if (isValidHttpUrl(inputFile)) {
|
||||
config['data'][(metadata.id || 'pmtiles').replace(/[?/:]/g, '_')] = {
|
||||
pmtiles: inputFile,
|
||||
};
|
||||
} else {
|
||||
config['data'][(metadata.id || 'pmtiles').replace(/[?/:]/g, '_')] = {
|
||||
pmtiles: path.basename(inputFile),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (opts.verbose) {
|
||||
console.log(JSON.stringify(config, undefined, 2));
|
||||
} else {
|
||||
console.log('Run with --verbose to see the config file here.');
|
||||
}
|
||||
|
||||
return startServer(null, config);
|
||||
} else {
|
||||
if (isValidHttpUrl(inputFile)) {
|
||||
console.log(
|
||||
`ERROR: MBTiles does not support web based files. "${inputFile}" is not a valid data file.`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
let info;
|
||||
try {
|
||||
const mbw = await openMbTilesWrapper(inputFile);
|
||||
info = await mbw.getInfo();
|
||||
if (!info) throw new Error('Metadata missing in the MBTiles.');
|
||||
} catch (err) {
|
||||
console.log('ERROR: Unable to open MBTiles or read metadata:', err);
|
||||
console.log(`Make sure ${path.basename(inputFile)} is valid MBTiles.`);
|
||||
process.exit(1);
|
||||
}
|
||||
const bounds = info.bounds;
|
||||
|
||||
if (
|
||||
info.format === 'pbf' &&
|
||||
info.name.toLowerCase().indexOf('openmaptiles') > -1
|
||||
) {
|
||||
config['data'][`v3`] = {
|
||||
mbtiles: path.basename(inputFile),
|
||||
};
|
||||
|
||||
const styles = await fsp.readdir(path.resolve(styleDir, 'styles'));
|
||||
for (const styleName of styles) {
|
||||
const styleFileRel = styleName + '/style.json';
|
||||
const styleFile = path.resolve(styleDir, 'styles', styleFileRel);
|
||||
if (await existsP(styleFile)) {
|
||||
config['styles'][styleName] = {
|
||||
style: styleFileRel,
|
||||
tilejson: {
|
||||
bounds,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.log(
|
||||
`WARN: MBTiles not in "openmaptiles" format. Serving raw data only...`,
|
||||
);
|
||||
config['data'][(info.id || 'mbtiles').replace(/[?/:]/g, '_')] = {
|
||||
mbtiles: path.basename(inputFile),
|
||||
};
|
||||
}
|
||||
|
||||
if (opts.verbose) {
|
||||
console.log(JSON.stringify(config, undefined, 2));
|
||||
} else {
|
||||
console.log('Run with --verbose to see the config file here.');
|
||||
}
|
||||
|
||||
return startServer(null, config);
|
||||
}
|
||||
};
|
||||
|
||||
fs.stat(path.resolve(opts.config), async (err, stats) => {
|
||||
if (err || !stats.isFile() || stats.size === 0) {
|
||||
let inputFile;
|
||||
if (opts.file) {
|
||||
inputFile = opts.file;
|
||||
} else if (opts.mbtiles) {
|
||||
inputFile = opts.mbtiles;
|
||||
}
|
||||
|
||||
if (inputFile) {
|
||||
return startWithInputFile(inputFile);
|
||||
} else {
|
||||
// try to find in the cwd
|
||||
const files = await fsp.readdir(process.cwd());
|
||||
for (const filename of files) {
|
||||
if (filename.endsWith('.mbtiles') || filename.endsWith('.pmtiles')) {
|
||||
const inputFilesStats = await fsp.stat(filename);
|
||||
if (inputFilesStats.isFile() && inputFilesStats.size > 0) {
|
||||
inputFile = filename;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (inputFile) {
|
||||
console.log(`No input file specified, using ${inputFile}`);
|
||||
return startWithInputFile(inputFile);
|
||||
} else {
|
||||
const url =
|
||||
'https://github.com/maptiler/tileserver-gl/releases/download/v1.3.0/zurich_switzerland.mbtiles';
|
||||
const filename = 'zurich_switzerland.mbtiles';
|
||||
const writer = fs.createWriteStream(filename);
|
||||
console.log(`No input file found`);
|
||||
console.log(`[DEMO] Downloading sample data (${filename}) from ${url}`);
|
||||
|
||||
try {
|
||||
const response = await axios({
|
||||
url,
|
||||
method: 'GET',
|
||||
responseType: 'stream',
|
||||
});
|
||||
|
||||
response.data.pipe(writer);
|
||||
writer.on('finish', () => startWithInputFile(filename));
|
||||
writer.on('error', (err) =>
|
||||
console.error(`Error writing file: ${err}`),
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(`Error downloading file: ${error}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.log(`Using specified config file from ${opts.config}`);
|
||||
return startServer(opts.config, null);
|
||||
}
|
||||
});
|
||||
|
|
46
src/mbtiles_wrapper.js
Normal file
|
@ -0,0 +1,46 @@
|
|||
import MBTiles from '@mapbox/mbtiles';
|
||||
import util from 'node:util';
|
||||
|
||||
/**
|
||||
* Promise-ful wrapper around the MBTiles class.
|
||||
*/
|
||||
class MBTilesWrapper {
|
||||
constructor(mbtiles) {
|
||||
this._mbtiles = mbtiles;
|
||||
this._getInfoP = util.promisify(mbtiles.getInfo.bind(mbtiles));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the underlying MBTiles object.
|
||||
* @returns {MBTiles}
|
||||
*/
|
||||
getMbTiles() {
|
||||
return this._mbtiles;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the MBTiles metadata object.
|
||||
* @returns {Promise<object>}
|
||||
*/
|
||||
getInfo() {
|
||||
return this._getInfoP();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the given MBTiles file and return a promise that resolves with a
|
||||
* MBTilesWrapper instance.
|
||||
* @param inputFile Input file
|
||||
* @returns {Promise<MBTilesWrapper>}
|
||||
*/
|
||||
export function openMbTilesWrapper(inputFile) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const mbtiles = new MBTiles(inputFile + '?mode=ro', (err) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
resolve(new MBTilesWrapper(mbtiles));
|
||||
});
|
||||
});
|
||||
}
|
151
src/pmtiles_adapter.js
Normal file
|
@ -0,0 +1,151 @@
|
|||
import fs from 'node:fs';
|
||||
import { PMTiles, FetchSource } from 'pmtiles';
|
||||
import { isValidHttpUrl } from './utils.js';
|
||||
|
||||
class PMTilesFileSource {
|
||||
constructor(fd) {
|
||||
this.fd = fd;
|
||||
}
|
||||
getKey() {
|
||||
return this.fd;
|
||||
}
|
||||
async getBytes(offset, length) {
|
||||
const buffer = Buffer.alloc(length);
|
||||
await readFileBytes(this.fd, buffer, offset);
|
||||
const ab = buffer.buffer.slice(
|
||||
buffer.byteOffset,
|
||||
buffer.byteOffset + buffer.byteLength,
|
||||
);
|
||||
return { data: ab };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param fd
|
||||
* @param buffer
|
||||
* @param offset
|
||||
*/
|
||||
async function readFileBytes(fd, buffer, offset) {
|
||||
return new Promise((resolve, reject) => {
|
||||
fs.read(fd, buffer, 0, buffer.length, offset, (err) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param FilePath
|
||||
*/
|
||||
export function openPMtiles(FilePath) {
|
||||
let pmtiles = undefined;
|
||||
|
||||
if (isValidHttpUrl(FilePath)) {
|
||||
const source = new FetchSource(FilePath);
|
||||
pmtiles = new PMTiles(source);
|
||||
} else {
|
||||
const fd = fs.openSync(FilePath, 'r');
|
||||
const source = new PMTilesFileSource(fd);
|
||||
pmtiles = new PMTiles(source);
|
||||
}
|
||||
return pmtiles;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param pmtiles
|
||||
*/
|
||||
export async function getPMtilesInfo(pmtiles) {
|
||||
const header = await pmtiles.getHeader();
|
||||
const metadata = await pmtiles.getMetadata();
|
||||
|
||||
//Add missing metadata from header
|
||||
metadata['format'] = getPmtilesTileType(header.tileType).type;
|
||||
metadata['minzoom'] = header.minZoom;
|
||||
metadata['maxzoom'] = header.maxZoom;
|
||||
|
||||
if (header.minLon && header.minLat && header.maxLon && header.maxLat) {
|
||||
metadata['bounds'] = [
|
||||
header.minLon,
|
||||
header.minLat,
|
||||
header.maxLon,
|
||||
header.maxLat,
|
||||
];
|
||||
} else {
|
||||
metadata['bounds'] = [-180, -85.05112877980659, 180, 85.0511287798066];
|
||||
}
|
||||
|
||||
if (header.centerZoom) {
|
||||
metadata['center'] = [
|
||||
header.centerLon,
|
||||
header.centerLat,
|
||||
header.centerZoom,
|
||||
];
|
||||
} else {
|
||||
metadata['center'] = [
|
||||
header.centerLon,
|
||||
header.centerLat,
|
||||
parseInt(metadata['maxzoom']) / 2,
|
||||
];
|
||||
}
|
||||
|
||||
return metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param pmtiles
|
||||
* @param z
|
||||
* @param x
|
||||
* @param y
|
||||
*/
|
||||
export async function getPMtilesTile(pmtiles, z, x, y) {
|
||||
const header = await pmtiles.getHeader();
|
||||
const tileType = getPmtilesTileType(header.tileType);
|
||||
let zxyTile = await pmtiles.getZxy(z, x, y);
|
||||
if (zxyTile && zxyTile.data) {
|
||||
zxyTile = Buffer.from(zxyTile.data);
|
||||
} else {
|
||||
zxyTile = undefined;
|
||||
}
|
||||
return { data: zxyTile, header: tileType.header };
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param typenum
|
||||
*/
|
||||
function getPmtilesTileType(typenum) {
|
||||
let head = {};
|
||||
let tileType;
|
||||
switch (typenum) {
|
||||
case 0:
|
||||
tileType = 'Unknown';
|
||||
break;
|
||||
case 1:
|
||||
tileType = 'pbf';
|
||||
head['Content-Type'] = 'application/x-protobuf';
|
||||
break;
|
||||
case 2:
|
||||
tileType = 'png';
|
||||
head['Content-Type'] = 'image/png';
|
||||
break;
|
||||
case 3:
|
||||
tileType = 'jpeg';
|
||||
head['Content-Type'] = 'image/jpeg';
|
||||
break;
|
||||
case 4:
|
||||
tileType = 'webp';
|
||||
head['Content-Type'] = 'image/webp';
|
||||
break;
|
||||
case 5:
|
||||
tileType = 'avif';
|
||||
head['Content-Type'] = 'image/avif';
|
||||
break;
|
||||
}
|
||||
return { type: tileType, header: head };
|
||||
}
|
14
src/promises.js
Normal file
|
@ -0,0 +1,14 @@
|
|||
import util from 'node:util';
|
||||
import fsp from 'node:fs/promises';
|
||||
import zlib from 'zlib';
|
||||
|
||||
export const gzipP = util.promisify(zlib.gzip);
|
||||
export const gunzipP = util.promisify(zlib.gunzip);
|
||||
export const existsP = async (path) => {
|
||||
try {
|
||||
await fsp.access(path); // Defaults to F_OK: indicating that the file is visible to the calling process
|
||||
return true;
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
};
|
303
src/render.js
Normal file
|
@ -0,0 +1,303 @@
|
|||
'use strict';
|
||||
|
||||
import { createCanvas, Image } from 'canvas';
|
||||
|
||||
import SphericalMercator from '@mapbox/sphericalmercator';
|
||||
|
||||
const mercator = new SphericalMercator();
|
||||
|
||||
/**
|
||||
* Transforms coordinates to pixels.
|
||||
* @param {List[Number]} ll Longitude/Latitude coordinate pair.
|
||||
* @param {number} zoom Map zoom level.
|
||||
*/
|
||||
const precisePx = (ll, zoom) => {
|
||||
const px = mercator.px(ll, 20);
|
||||
const scale = Math.pow(2, zoom - 20);
|
||||
return [px[0] * scale, px[1] * scale];
|
||||
};
|
||||
|
||||
/**
|
||||
* Draws a marker in canvas context.
|
||||
* @param {object} ctx Canvas context object.
|
||||
* @param {object} marker Marker object parsed by extractMarkersFromQuery.
|
||||
* @param {number} z Map zoom level.
|
||||
*/
|
||||
const drawMarker = (ctx, marker, z) => {
|
||||
return new Promise((resolve) => {
|
||||
const img = new Image();
|
||||
const pixelCoords = precisePx(marker.location, z);
|
||||
|
||||
const getMarkerCoordinates = (imageWidth, imageHeight, scale) => {
|
||||
// Images are placed with their top-left corner at the provided location
|
||||
// within the canvas but we expect icons to be centered and above it.
|
||||
|
||||
// Substract half of the images width from the x-coordinate to center
|
||||
// the image in relation to the provided location
|
||||
let xCoordinate = pixelCoords[0] - imageWidth / 2;
|
||||
// Substract the images height from the y-coordinate to place it above
|
||||
// the provided location
|
||||
let yCoordinate = pixelCoords[1] - imageHeight;
|
||||
|
||||
// Since image placement is dependent on the size offsets have to be
|
||||
// scaled as well. Additionally offsets are provided as either positive or
|
||||
// negative values so we always add them
|
||||
if (marker.offsetX) {
|
||||
xCoordinate = xCoordinate + marker.offsetX * scale;
|
||||
}
|
||||
if (marker.offsetY) {
|
||||
yCoordinate = yCoordinate + marker.offsetY * scale;
|
||||
}
|
||||
|
||||
return {
|
||||
x: xCoordinate,
|
||||
y: yCoordinate,
|
||||
};
|
||||
};
|
||||
|
||||
const drawOnCanvas = () => {
|
||||
// Check if the images should be resized before beeing drawn
|
||||
const defaultScale = 1;
|
||||
const scale = marker.scale ? marker.scale : defaultScale;
|
||||
|
||||
// Calculate scaled image sizes
|
||||
const imageWidth = img.width * scale;
|
||||
const imageHeight = img.height * scale;
|
||||
|
||||
// Pass the desired sizes to get correlating coordinates
|
||||
const coords = getMarkerCoordinates(imageWidth, imageHeight, scale);
|
||||
|
||||
// Draw the image on canvas
|
||||
if (scale != defaultScale) {
|
||||
ctx.drawImage(img, coords.x, coords.y, imageWidth, imageHeight);
|
||||
} else {
|
||||
ctx.drawImage(img, coords.x, coords.y);
|
||||
}
|
||||
// Resolve the promise when image has been drawn
|
||||
resolve();
|
||||
};
|
||||
|
||||
img.onload = drawOnCanvas;
|
||||
img.onerror = (err) => {
|
||||
throw err;
|
||||
};
|
||||
img.src = marker.icon;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Draws a list of markers onto a canvas.
|
||||
* Wraps drawing of markers into list of promises and awaits them.
|
||||
* It's required because images are expected to load asynchronous in canvas js
|
||||
* even when provided from a local disk.
|
||||
* @param {object} ctx Canvas context object.
|
||||
* @param {List[Object]} markers Marker objects parsed by extractMarkersFromQuery.
|
||||
* @param {number} z Map zoom level.
|
||||
*/
|
||||
const drawMarkers = async (ctx, markers, z) => {
|
||||
const markerPromises = [];
|
||||
|
||||
for (const marker of markers) {
|
||||
// Begin drawing marker
|
||||
markerPromises.push(drawMarker(ctx, marker, z));
|
||||
}
|
||||
|
||||
// Await marker drawings before continuing
|
||||
await Promise.all(markerPromises);
|
||||
};
|
||||
|
||||
/**
|
||||
* Draws a list of coordinates onto a canvas and styles the resulting path.
|
||||
* @param {object} ctx Canvas context object.
|
||||
* @param {List[Number]} path List of coordinates.
|
||||
* @param {object} query Request query parameters.
|
||||
* @param {string} pathQuery Path query parameter.
|
||||
* @param {number} z Map zoom level.
|
||||
*/
|
||||
const drawPath = (ctx, path, query, pathQuery, z) => {
|
||||
const splitPaths = pathQuery.split('|');
|
||||
|
||||
if (!path || path.length < 2) {
|
||||
return null;
|
||||
}
|
||||
|
||||
ctx.beginPath();
|
||||
|
||||
// Transform coordinates to pixel on canvas and draw lines between points
|
||||
for (const pair of path) {
|
||||
const px = precisePx(pair, z);
|
||||
ctx.lineTo(px[0], px[1]);
|
||||
}
|
||||
|
||||
// Check if first coordinate matches last coordinate
|
||||
if (
|
||||
path[0][0] === path[path.length - 1][0] &&
|
||||
path[0][1] === path[path.length - 1][1]
|
||||
) {
|
||||
ctx.closePath();
|
||||
}
|
||||
|
||||
// Optionally fill drawn shape with a rgba color from query
|
||||
const pathHasFill = splitPaths.filter((x) => x.startsWith('fill')).length > 0;
|
||||
if (query.fill !== undefined || pathHasFill) {
|
||||
if ('fill' in query) {
|
||||
ctx.fillStyle = query.fill || 'rgba(255,255,255,0.4)';
|
||||
}
|
||||
if (pathHasFill) {
|
||||
ctx.fillStyle = splitPaths
|
||||
.find((x) => x.startsWith('fill:'))
|
||||
.replace('fill:', '');
|
||||
}
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
// Get line width from query and fall back to 1 if not provided
|
||||
const pathHasWidth =
|
||||
splitPaths.filter((x) => x.startsWith('width')).length > 0;
|
||||
if (query.width !== undefined || pathHasWidth) {
|
||||
let lineWidth = 1;
|
||||
// Get line width from query
|
||||
if ('width' in query) {
|
||||
lineWidth = Number(query.width);
|
||||
}
|
||||
// Get line width from path in query
|
||||
if (pathHasWidth) {
|
||||
lineWidth = Number(
|
||||
splitPaths.find((x) => x.startsWith('width:')).replace('width:', ''),
|
||||
);
|
||||
}
|
||||
// Get border width from query and fall back to 10% of line width
|
||||
const borderWidth =
|
||||
query.borderwidth !== undefined
|
||||
? parseFloat(query.borderwidth)
|
||||
: lineWidth * 0.1;
|
||||
|
||||
// Set rendering style for the start and end points of the path
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineCap
|
||||
ctx.lineCap = query.linecap || 'butt';
|
||||
|
||||
// Set rendering style for overlapping segments of the path with differing directions
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineJoin
|
||||
ctx.lineJoin = query.linejoin || 'miter';
|
||||
|
||||
// In order to simulate a border we draw the path two times with the first
|
||||
// beeing the wider border part.
|
||||
if (query.border !== undefined && borderWidth > 0) {
|
||||
// We need to double the desired border width and add it to the line width
|
||||
// in order to get the desired border on each side of the line.
|
||||
ctx.lineWidth = lineWidth + borderWidth * 2;
|
||||
// Set border style as rgba
|
||||
ctx.strokeStyle = query.border;
|
||||
ctx.stroke();
|
||||
}
|
||||
ctx.lineWidth = lineWidth;
|
||||
}
|
||||
|
||||
const pathHasStroke =
|
||||
splitPaths.filter((x) => x.startsWith('stroke')).length > 0;
|
||||
if (query.stroke !== undefined || pathHasStroke) {
|
||||
if ('stroke' in query) {
|
||||
ctx.strokeStyle = query.stroke;
|
||||
}
|
||||
// Path Stroke gets higher priority
|
||||
if (pathHasStroke) {
|
||||
ctx.strokeStyle = splitPaths
|
||||
.find((x) => x.startsWith('stroke:'))
|
||||
.replace('stroke:', '');
|
||||
}
|
||||
} else {
|
||||
ctx.strokeStyle = 'rgba(0,64,255,0.7)';
|
||||
}
|
||||
ctx.stroke();
|
||||
};
|
||||
|
||||
export const renderOverlay = async (
|
||||
z,
|
||||
x,
|
||||
y,
|
||||
bearing,
|
||||
pitch,
|
||||
w,
|
||||
h,
|
||||
scale,
|
||||
paths,
|
||||
markers,
|
||||
query,
|
||||
) => {
|
||||
if ((!paths || paths.length === 0) && (!markers || markers.length === 0)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const center = precisePx([x, y], z);
|
||||
|
||||
const mapHeight = 512 * (1 << z);
|
||||
const maxEdge = center[1] + h / 2;
|
||||
const minEdge = center[1] - h / 2;
|
||||
if (maxEdge > mapHeight) {
|
||||
center[1] -= maxEdge - mapHeight;
|
||||
} else if (minEdge < 0) {
|
||||
center[1] -= minEdge;
|
||||
}
|
||||
|
||||
const canvas = createCanvas(scale * w, scale * h);
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.scale(scale, scale);
|
||||
if (bearing) {
|
||||
ctx.translate(w / 2, h / 2);
|
||||
ctx.rotate((-bearing / 180) * Math.PI);
|
||||
ctx.translate(-center[0], -center[1]);
|
||||
} else {
|
||||
// optimized path
|
||||
ctx.translate(-center[0] + w / 2, -center[1] + h / 2);
|
||||
}
|
||||
|
||||
// Draw provided paths if any
|
||||
paths.forEach((path, i) => {
|
||||
const pathQuery = Array.isArray(query.path) ? query.path.at(i) : query.path;
|
||||
drawPath(ctx, path, query, pathQuery, z);
|
||||
});
|
||||
|
||||
// Await drawing of markers before rendering the canvas
|
||||
await drawMarkers(ctx, markers, z);
|
||||
|
||||
return canvas.toBuffer();
|
||||
};
|
||||
|
||||
export const renderWatermark = (width, height, scale, text) => {
|
||||
const canvas = createCanvas(scale * width, scale * height);
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.scale(scale, scale);
|
||||
|
||||
ctx.font = '10px sans-serif';
|
||||
ctx.strokeWidth = '1px';
|
||||
ctx.strokeStyle = 'rgba(255,255,255,.4)';
|
||||
ctx.strokeText(text, 5, height - 5);
|
||||
ctx.fillStyle = 'rgba(0,0,0,.4)';
|
||||
ctx.fillText(text, 5, height - 5);
|
||||
|
||||
return canvas;
|
||||
};
|
||||
|
||||
export const renderAttribution = (width, height, scale, text) => {
|
||||
const canvas = createCanvas(scale * width, scale * height);
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.scale(scale, scale);
|
||||
|
||||
ctx.font = '10px sans-serif';
|
||||
const textMetrics = ctx.measureText(text);
|
||||
const textWidth = textMetrics.width;
|
||||
const textHeight = 14;
|
||||
|
||||
const padding = 6;
|
||||
ctx.fillStyle = 'rgba(255, 255, 255, 0.8)';
|
||||
ctx.fillRect(
|
||||
width - textWidth - padding,
|
||||
height - textHeight - padding,
|
||||
textWidth + padding,
|
||||
textHeight + padding,
|
||||
);
|
||||
ctx.fillStyle = 'rgba(0,0,0,.8)';
|
||||
ctx.fillText(text, width - textWidth - padding / 2, height - textHeight + 8);
|
||||
|
||||
return canvas;
|
||||
};
|
|
@ -1,87 +1,423 @@
|
|||
'use strict';
|
||||
|
||||
var crypto = require('crypto'),
|
||||
fs = require('fs'),
|
||||
path = require('path');
|
||||
import fsp from 'node:fs/promises';
|
||||
import path from 'path';
|
||||
|
||||
var clone = require('clone'),
|
||||
express = require('express'),
|
||||
mbtiles = require('mbtiles');
|
||||
import clone from 'clone';
|
||||
import express from 'express';
|
||||
import Pbf from 'pbf';
|
||||
import { VectorTile } from '@mapbox/vector-tile';
|
||||
import SphericalMercator from '@mapbox/sphericalmercator';
|
||||
|
||||
var utils = require('./utils');
|
||||
import {
|
||||
fixTileJSONCenter,
|
||||
getTileUrls,
|
||||
isValidHttpUrl,
|
||||
fetchTileData,
|
||||
} from './utils.js';
|
||||
import { getPMtilesInfo, openPMtiles } from './pmtiles_adapter.js';
|
||||
import { gunzipP, gzipP } from './promises.js';
|
||||
import { openMbTilesWrapper } from './mbtiles_wrapper.js';
|
||||
|
||||
module.exports = function(options, repo, params, id) {
|
||||
var app = express().disable('x-powered-by');
|
||||
import fs from 'node:fs';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
var mbtilesFile = path.join(options.paths.mbtiles, params.mbtiles);
|
||||
var tileJSON = {
|
||||
'tiles': params.domains || options.domains
|
||||
};
|
||||
const packageJson = JSON.parse(
|
||||
fs.readFileSync(
|
||||
path.dirname(fileURLToPath(import.meta.url)) + '/../package.json',
|
||||
'utf8',
|
||||
),
|
||||
);
|
||||
|
||||
repo[id] = tileJSON;
|
||||
const isLight = packageJson.name.slice(-6) === '-light';
|
||||
const serve_rendered = (
|
||||
await import(`${!isLight ? `./serve_rendered.js` : `./serve_light.js`}`)
|
||||
).serve_rendered;
|
||||
|
||||
var source = new mbtiles(mbtilesFile, function(err) {
|
||||
source.getInfo(function(err, info) {
|
||||
export const serve_data = {
|
||||
/**
|
||||
* Initializes the serve_data module.
|
||||
* @param {object} options Configuration options.
|
||||
* @param {object} repo Repository object.
|
||||
* @param {object} programOpts - An object containing the program options
|
||||
* @returns {express.Application} The initialized Express application.
|
||||
*/
|
||||
init: function (options, repo, programOpts) {
|
||||
const { verbose } = programOpts;
|
||||
const app = express().disable('x-powered-by');
|
||||
|
||||
/**
|
||||
* Handles requests for tile data, responding with the tile image.
|
||||
* @param {object} req - Express request object.
|
||||
* @param {object} res - Express response object.
|
||||
* @param {string} req.params.id - ID of the tile.
|
||||
* @param {string} req.params.z - Z coordinate of the tile.
|
||||
* @param {string} req.params.x - X coordinate of the tile.
|
||||
* @param {string} req.params.y - Y coordinate of the tile.
|
||||
* @param {string} req.params.format - Format of the tile.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
app.get('/:id/:z/:x/:y.:format', async (req, res) => {
|
||||
if (verbose) {
|
||||
console.log(
|
||||
`Handling tile request for: /data/%s/%s/%s/%s.%s`,
|
||||
String(req.params.id).replace(/\n|\r/g, ''),
|
||||
String(req.params.z).replace(/\n|\r/g, ''),
|
||||
String(req.params.x).replace(/\n|\r/g, ''),
|
||||
String(req.params.y).replace(/\n|\r/g, ''),
|
||||
String(req.params.format).replace(/\n|\r/g, ''),
|
||||
);
|
||||
}
|
||||
const item = repo[req.params.id];
|
||||
if (!item) {
|
||||
return res.sendStatus(404);
|
||||
}
|
||||
const tileJSONFormat = item.tileJSON.format;
|
||||
const z = parseInt(req.params.z, 10);
|
||||
const x = parseInt(req.params.x, 10);
|
||||
const y = parseInt(req.params.y, 10);
|
||||
if (isNaN(z) || isNaN(x) || isNaN(y)) {
|
||||
return res.status(404).send('Invalid Tile');
|
||||
}
|
||||
|
||||
let format = req.params.format;
|
||||
if (format === options.pbfAlias) {
|
||||
format = 'pbf';
|
||||
}
|
||||
if (
|
||||
format !== tileJSONFormat &&
|
||||
!(format === 'geojson' && tileJSONFormat === 'pbf')
|
||||
) {
|
||||
return res.status(404).send('Invalid format');
|
||||
}
|
||||
if (
|
||||
z < item.tileJSON.minzoom ||
|
||||
x < 0 ||
|
||||
y < 0 ||
|
||||
z > item.tileJSON.maxzoom ||
|
||||
x >= Math.pow(2, z) ||
|
||||
y >= Math.pow(2, z)
|
||||
) {
|
||||
return res.status(404).send('Out of bounds');
|
||||
}
|
||||
|
||||
const fetchTile = await fetchTileData(
|
||||
item.source,
|
||||
item.sourceType,
|
||||
z,
|
||||
x,
|
||||
y,
|
||||
);
|
||||
if (fetchTile == null) return res.status(204).send();
|
||||
|
||||
let data = fetchTile.data;
|
||||
let headers = fetchTile.headers;
|
||||
let isGzipped = data.slice(0, 2).indexOf(Buffer.from([0x1f, 0x8b])) === 0;
|
||||
|
||||
if (isGzipped) {
|
||||
data = await gunzipP(data);
|
||||
isGzipped = false;
|
||||
}
|
||||
|
||||
if (tileJSONFormat === 'pbf') {
|
||||
if (options.dataDecoratorFunc) {
|
||||
data = options.dataDecoratorFunc(
|
||||
req.params.id,
|
||||
'data',
|
||||
data,
|
||||
z,
|
||||
x,
|
||||
y,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (format === 'pbf') {
|
||||
headers['Content-Type'] = 'application/x-protobuf';
|
||||
} else if (format === 'geojson') {
|
||||
headers['Content-Type'] = 'application/json';
|
||||
const tile = new VectorTile(new Pbf(data));
|
||||
const geojson = {
|
||||
type: 'FeatureCollection',
|
||||
features: [],
|
||||
};
|
||||
for (const layerName in tile.layers) {
|
||||
const layer = tile.layers[layerName];
|
||||
for (let i = 0; i < layer.length; i++) {
|
||||
const feature = layer.feature(i);
|
||||
const featureGeoJSON = feature.toGeoJSON(x, y, z);
|
||||
featureGeoJSON.properties.layer = layerName;
|
||||
geojson.features.push(featureGeoJSON);
|
||||
}
|
||||
}
|
||||
data = JSON.stringify(geojson);
|
||||
}
|
||||
if (headers) {
|
||||
delete headers['ETag'];
|
||||
}
|
||||
headers['Content-Encoding'] = 'gzip';
|
||||
res.set(headers);
|
||||
|
||||
if (!isGzipped) {
|
||||
data = await gzipP(data);
|
||||
}
|
||||
|
||||
return res.status(200).send(data);
|
||||
});
|
||||
|
||||
/**
|
||||
* Handles requests for elevation data.
|
||||
* @param {object} req - Express request object.
|
||||
* @param {object} res - Express response object.
|
||||
* @param {string} req.params.id - ID of the elevation data.
|
||||
* @param {string} req.params.z - Z coordinate of the tile.
|
||||
* @param {string} req.params.x - X coordinate of the tile (either integer or float).
|
||||
* @param {string} req.params.y - Y coordinate of the tile (either integer or float).
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
app.get('/:id/elevation/:z/:x/:y', async (req, res, next) => {
|
||||
try {
|
||||
if (verbose) {
|
||||
console.log(
|
||||
`Handling elevation request for: /data/%s/elevation/%s/%s/%s`,
|
||||
String(req.params.id).replace(/\n|\r/g, ''),
|
||||
String(req.params.z).replace(/\n|\r/g, ''),
|
||||
String(req.params.x).replace(/\n|\r/g, ''),
|
||||
String(req.params.y).replace(/\n|\r/g, ''),
|
||||
);
|
||||
}
|
||||
const item = repo?.[req.params.id];
|
||||
if (!item) return res.sendStatus(404);
|
||||
if (!item.source) return res.status(404).send('Missing source');
|
||||
if (!item.tileJSON) return res.status(404).send('Missing tileJSON');
|
||||
if (!item.sourceType) return res.status(404).send('Missing sourceType');
|
||||
const { source, tileJSON, sourceType } = item;
|
||||
if (sourceType !== 'pmtiles' && sourceType !== 'mbtiles') {
|
||||
return res
|
||||
.status(400)
|
||||
.send('Invalid sourceType. Must be pmtiles or mbtiles.');
|
||||
}
|
||||
const encoding = tileJSON?.encoding;
|
||||
if (encoding == null) {
|
||||
return res.status(400).send('Missing tileJSON.encoding');
|
||||
} else if (encoding !== 'terrarium' && encoding !== 'mapbox') {
|
||||
return res
|
||||
.status(400)
|
||||
.send('Invalid encoding. Must be terrarium or mapbox.');
|
||||
}
|
||||
const format = tileJSON?.format;
|
||||
if (format == null) {
|
||||
return res.status(400).send('Missing tileJSON.format');
|
||||
} else if (format !== 'webp' && format !== 'png') {
|
||||
return res.status(400).send('Invalid format. Must be webp or png.');
|
||||
}
|
||||
const z = parseInt(req.params.z, 10);
|
||||
const x = parseFloat(req.params.x);
|
||||
const y = parseFloat(req.params.y);
|
||||
if (tileJSON.minzoom == null || tileJSON.maxzoom == null) {
|
||||
return res.status(404).send(JSON.stringify(tileJSON));
|
||||
}
|
||||
const TILE_SIZE = tileJSON.tileSize || 512;
|
||||
let bbox;
|
||||
let xy;
|
||||
var zoom = z;
|
||||
|
||||
if (Number.isInteger(x) && Number.isInteger(y)) {
|
||||
const intX = parseInt(req.params.x, 10);
|
||||
const intY = parseInt(req.params.y, 10);
|
||||
if (
|
||||
zoom < tileJSON.minzoom ||
|
||||
zoom > tileJSON.maxzoom ||
|
||||
intX < 0 ||
|
||||
intY < 0 ||
|
||||
intX >= Math.pow(2, zoom) ||
|
||||
intY >= Math.pow(2, zoom)
|
||||
) {
|
||||
return res.status(404).send('Out of bounds');
|
||||
}
|
||||
xy = [intX, intY];
|
||||
bbox = new SphericalMercator().bbox(intX, intY, zoom);
|
||||
} else {
|
||||
//no zoom limit with coordinates
|
||||
if (zoom < tileJSON.minzoom) {
|
||||
zoom = tileJSON.minzoom;
|
||||
}
|
||||
if (zoom > tileJSON.maxzoom) {
|
||||
zoom = tileJSON.maxzoom;
|
||||
}
|
||||
bbox = [x, y, x + 0.1, y + 0.1];
|
||||
const { minX, minY } = new SphericalMercator().xyz(bbox, zoom);
|
||||
xy = [minX, minY];
|
||||
}
|
||||
|
||||
const fetchTile = await fetchTileData(
|
||||
source,
|
||||
sourceType,
|
||||
zoom,
|
||||
xy[0],
|
||||
xy[1],
|
||||
);
|
||||
if (fetchTile == null) return res.status(204).send();
|
||||
|
||||
let data = fetchTile.data;
|
||||
var param = {
|
||||
long: bbox[0].toFixed(7),
|
||||
lat: bbox[1].toFixed(7),
|
||||
encoding,
|
||||
format,
|
||||
tile_size: TILE_SIZE,
|
||||
z: zoom,
|
||||
x: xy[0],
|
||||
y: xy[1],
|
||||
};
|
||||
|
||||
res
|
||||
.status(200)
|
||||
.send(await serve_rendered.getTerrainElevation(data, param));
|
||||
} catch (err) {
|
||||
return res
|
||||
.status(500)
|
||||
.header('Content-Type', 'text/plain')
|
||||
.send(err.message);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Handles requests for tilejson for the data tiles.
|
||||
* @param {object} req - Express request object.
|
||||
* @param {object} res - Express response object.
|
||||
* @param {string} req.params.id - ID of the data source.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
app.get('/:id.json', (req, res) => {
|
||||
if (verbose) {
|
||||
console.log(
|
||||
`Handling tilejson request for: /data/%s.json`,
|
||||
String(req.params.id).replace(/\n|\r/g, ''),
|
||||
);
|
||||
}
|
||||
const item = repo[req.params.id];
|
||||
if (!item) {
|
||||
return res.sendStatus(404);
|
||||
}
|
||||
const tileSize = undefined;
|
||||
const info = clone(item.tileJSON);
|
||||
info.tiles = getTileUrls(
|
||||
req,
|
||||
info.tiles,
|
||||
`data/${req.params.id}`,
|
||||
tileSize,
|
||||
info.format,
|
||||
item.publicUrl,
|
||||
{
|
||||
pbf: options.pbfAlias,
|
||||
},
|
||||
);
|
||||
return res.send(info);
|
||||
});
|
||||
|
||||
return app;
|
||||
},
|
||||
/**
|
||||
* Adds a new data source to the repository.
|
||||
* @param {object} options Configuration options.
|
||||
* @param {object} repo Repository object.
|
||||
* @param {object} params Parameters object.
|
||||
* @param {string} id ID of the data source.
|
||||
* @param {object} programOpts - An object containing the program options
|
||||
* @param {string} programOpts.publicUrl Public URL for the data.
|
||||
* @param {boolean} programOpts.verbose Whether verbose logging should be used.
|
||||
* @param {Function} dataResolver Function to resolve data.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
add: async function (options, repo, params, id, programOpts) {
|
||||
const { publicUrl } = programOpts;
|
||||
let inputFile;
|
||||
let inputType;
|
||||
if (params.pmtiles) {
|
||||
inputType = 'pmtiles';
|
||||
if (isValidHttpUrl(params.pmtiles)) {
|
||||
inputFile = params.pmtiles;
|
||||
} else {
|
||||
inputFile = path.resolve(options.paths.pmtiles, params.pmtiles);
|
||||
}
|
||||
} else if (params.mbtiles) {
|
||||
inputType = 'mbtiles';
|
||||
if (isValidHttpUrl(params.mbtiles)) {
|
||||
console.log(
|
||||
`ERROR: MBTiles does not support web based files. "${params.mbtiles}" is not a valid data file.`,
|
||||
);
|
||||
process.exit(1);
|
||||
} else {
|
||||
inputFile = path.resolve(options.paths.mbtiles, params.mbtiles);
|
||||
}
|
||||
}
|
||||
|
||||
let tileJSON = {
|
||||
tiles: params.domains || options.domains,
|
||||
};
|
||||
|
||||
if (!isValidHttpUrl(inputFile)) {
|
||||
const inputFileStats = await fsp.stat(inputFile);
|
||||
if (!inputFileStats.isFile() || inputFileStats.size === 0) {
|
||||
throw Error(`Not valid input file: "${inputFile}"`);
|
||||
}
|
||||
}
|
||||
|
||||
let source;
|
||||
let sourceType;
|
||||
if (inputType === 'pmtiles') {
|
||||
source = openPMtiles(inputFile);
|
||||
sourceType = 'pmtiles';
|
||||
const metadata = await getPMtilesInfo(source);
|
||||
|
||||
tileJSON['encoding'] = params['encoding'];
|
||||
tileJSON['tileSize'] = params['tileSize'];
|
||||
tileJSON['name'] = id;
|
||||
tileJSON['format'] = 'pbf';
|
||||
Object.assign(tileJSON, metadata);
|
||||
|
||||
tileJSON['tilejson'] = '2.0.0';
|
||||
delete tileJSON['filesize'];
|
||||
delete tileJSON['mtime'];
|
||||
delete tileJSON['scheme'];
|
||||
|
||||
Object.assign(tileJSON, params.tilejson || {});
|
||||
fixTileJSONCenter(tileJSON);
|
||||
|
||||
if (options.dataDecoratorFunc) {
|
||||
tileJSON = options.dataDecoratorFunc(id, 'tilejson', tileJSON);
|
||||
}
|
||||
} else if (inputType === 'mbtiles') {
|
||||
sourceType = 'mbtiles';
|
||||
const mbw = await openMbTilesWrapper(inputFile);
|
||||
const info = await mbw.getInfo();
|
||||
source = mbw.getMbTiles();
|
||||
tileJSON['encoding'] = params['encoding'];
|
||||
tileJSON['tileSize'] = params['tileSize'];
|
||||
tileJSON['name'] = id;
|
||||
tileJSON['format'] = 'pbf';
|
||||
|
||||
Object.assign(tileJSON, info);
|
||||
|
||||
tileJSON['tilejson'] = '2.0.0';
|
||||
tileJSON['basename'] = id;
|
||||
tileJSON['filesize'] = fs.statSync(mbtilesFile)['size'];
|
||||
delete tileJSON['filesize'];
|
||||
delete tileJSON['mtime'];
|
||||
delete tileJSON['scheme'];
|
||||
|
||||
Object.assign(tileJSON, params.tilejson || {});
|
||||
utils.fixTileJSONCenter(tileJSON);
|
||||
});
|
||||
});
|
||||
fixTileJSONCenter(tileJSON);
|
||||
|
||||
var tilePattern = '/' + id + '/:z(\\d+)/:x(\\d+)/:y(\\d+).:format([\\w]+)';
|
||||
|
||||
app.get(tilePattern, function(req, res, next) {
|
||||
var z = req.params.z | 0,
|
||||
x = req.params.x | 0,
|
||||
y = req.params.y | 0;
|
||||
if (req.params.format != tileJSON.format) {
|
||||
return res.status(404).send('Invalid format');
|
||||
}
|
||||
if (z < tileJSON.minzoom || 0 || x < 0 || y < 0 ||
|
||||
z > tileJSON.maxzoom ||
|
||||
x >= Math.pow(2, z) || y >= Math.pow(2, z)) {
|
||||
return res.status(404).send('Out of bounds');
|
||||
}
|
||||
source.getTile(z, x, y, function(err, data, headers) {
|
||||
if (err) {
|
||||
if (/does not exist/.test(err.message)) {
|
||||
return res.status(404).send(err.message);
|
||||
} else {
|
||||
return res.status(500).send(err.message);
|
||||
}
|
||||
} else {
|
||||
var md5 = crypto.createHash('md5').update(data).digest('base64');
|
||||
headers['content-md5'] = md5;
|
||||
if (tileJSON['format'] == 'pbf') {
|
||||
headers['content-type'] = 'application/x-protobuf';
|
||||
headers['content-encoding'] = 'gzip';
|
||||
}
|
||||
res.set(headers);
|
||||
|
||||
if (data == null) {
|
||||
return res.status(404).send('Not found');
|
||||
} else {
|
||||
return res.status(200).send(data);
|
||||
}
|
||||
if (options.dataDecoratorFunc) {
|
||||
tileJSON = options.dataDecoratorFunc(id, 'tilejson', tileJSON);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
app.get('/' + id + '.json', function(req, res, next) {
|
||||
var info = clone(tileJSON);
|
||||
info.tiles = utils.getTileUrls(req, info.tiles,
|
||||
'data/' + id, info.format);
|
||||
return res.send(info);
|
||||
});
|
||||
|
||||
return app;
|
||||
repo[id] = {
|
||||
tileJSON,
|
||||
publicUrl,
|
||||
source,
|
||||
sourceType,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
|
|
@ -1,60 +1,101 @@
|
|||
'use strict';
|
||||
|
||||
var async = require('async'),
|
||||
path = require('path'),
|
||||
fs = require('fs');
|
||||
import express from 'express';
|
||||
|
||||
var clone = require('clone'),
|
||||
express = require('express');
|
||||
import { getFontsPbf, listFonts } from './utils.js';
|
||||
|
||||
/**
|
||||
* Initializes and returns an Express app that serves font files.
|
||||
* @param {object} options - Configuration options for the server.
|
||||
* @param {object} allowedFonts - An object containing allowed fonts.
|
||||
* @param {object} programOpts - An object containing the program options.
|
||||
* @returns {Promise<express.Application>} - A promise that resolves to the Express app.
|
||||
*/
|
||||
export async function serve_font(options, allowedFonts, programOpts) {
|
||||
const { verbose } = programOpts;
|
||||
const app = express().disable('x-powered-by');
|
||||
|
||||
module.exports = function(options, allowedFonts) {
|
||||
var app = express().disable('x-powered-by');
|
||||
const lastModified = new Date().toUTCString();
|
||||
|
||||
var fontPath = options.paths.fonts;
|
||||
const fontPath = options.paths.fonts;
|
||||
|
||||
var getFontPbf = function(name, range, callback) {
|
||||
// if some of the files failed to load (does not exist or not allowed),
|
||||
// return empty buffer so the other fonts can still work
|
||||
if (allowedFonts[name]) {
|
||||
var filename = path.join(fontPath, name, range + '.pbf');
|
||||
return fs.readFile(filename, function(err, data) {
|
||||
if (err) {
|
||||
console.log('Font load error:', filename);
|
||||
return callback(null, new Buffer([]));
|
||||
} else {
|
||||
return callback(null, data);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
return callback(null, new Buffer([]));
|
||||
const existingFonts = {};
|
||||
|
||||
/**
|
||||
* Handles requests for a font file.
|
||||
* @param {object} req - Express request object.
|
||||
* @param {object} res - Express response object.
|
||||
* @param {string} req.params.fontstack - Name of the font stack.
|
||||
* @param {string} req.params.range - The range of the font (e.g. 0-255).
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
app.get('/fonts/:fontstack/:range.pbf', async (req, res) => {
|
||||
const sRange = String(req.params.range).replace(/\n|\r/g, '');
|
||||
const sFontStack = String(decodeURI(req.params.fontstack)).replace(
|
||||
/\n|\r/g,
|
||||
'',
|
||||
);
|
||||
|
||||
if (verbose) {
|
||||
console.log(
|
||||
`Handling font request for: /fonts/%s/%s.pbf`,
|
||||
sFontStack,
|
||||
sRange,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
app.get('/:fontstack/:range([\\d]+-[\\d]+).pbf',
|
||||
function(req, res, next) {
|
||||
var fontstack = decodeURI(req.params.fontstack);
|
||||
var range = req.params.range;
|
||||
|
||||
var fonts = fontstack.split(',');
|
||||
|
||||
var queue = [];
|
||||
fonts.forEach(function(font) {
|
||||
queue.push(function(callback) {
|
||||
getFontPbf(font, range, callback);
|
||||
});
|
||||
});
|
||||
|
||||
return async.parallel(queue, function(err, results) {
|
||||
var concated = Buffer.concat(results);
|
||||
if (err || concated.length == 0) {
|
||||
return res.status(400).send('');
|
||||
} else {
|
||||
res.header('Content-type', 'application/x-protobuf');
|
||||
return res.send(concated);
|
||||
const modifiedSince = req.get('if-modified-since');
|
||||
const cc = req.get('cache-control');
|
||||
if (modifiedSince && (!cc || cc.indexOf('no-cache') === -1)) {
|
||||
if (
|
||||
new Date(lastModified).getTime() === new Date(modifiedSince).getTime()
|
||||
) {
|
||||
return res.sendStatus(304);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const concatenated = await getFontsPbf(
|
||||
options.serveAllFonts ? null : allowedFonts,
|
||||
fontPath,
|
||||
sFontStack,
|
||||
sRange,
|
||||
existingFonts,
|
||||
);
|
||||
res.header('Content-type', 'application/x-protobuf');
|
||||
res.header('Last-Modified', lastModified);
|
||||
return res.send(concatenated);
|
||||
} catch (err) {
|
||||
console.error(
|
||||
`Error serving font: %s/%s.pbf, Error: %s`,
|
||||
sFontStack,
|
||||
sRange,
|
||||
String(err),
|
||||
);
|
||||
return res
|
||||
.status(400)
|
||||
.header('Content-Type', 'text/plain')
|
||||
.send('Error serving font');
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Handles requests for a list of all available fonts.
|
||||
* @param {object} req - Express request object.
|
||||
* @param {object} res - Express response object.
|
||||
* @returns {void}
|
||||
*/
|
||||
app.get('/fonts.json', (req, res) => {
|
||||
if (verbose) {
|
||||
console.log('Handling list font request for /fonts.json');
|
||||
}
|
||||
res.header('Content-type', 'application/json');
|
||||
return res.send(
|
||||
Object.keys(options.serveAllFonts ? existingFonts : allowedFonts).sort(),
|
||||
);
|
||||
});
|
||||
|
||||
const fonts = await listFonts(options.paths.fonts);
|
||||
Object.assign(existingFonts, fonts);
|
||||
return app;
|
||||
};
|
||||
}
|
||||
|
|
14
src/serve_light.js
Normal file
|
@ -0,0 +1,14 @@
|
|||
/* eslint-disable @typescript-eslint/no-empty-function */
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
'use strict';
|
||||
|
||||
export const serve_rendered = {
|
||||
init: (options, repo, programOpts) => {},
|
||||
add: (options, repo, params, id, programOpts, dataResolver) => {},
|
||||
remove: (repo, id) => {},
|
||||
clear: (repo) => {},
|
||||
getTerrainElevation: (data, param) => {
|
||||
param['elevation'] = 'not supported in light';
|
||||
return param;
|
||||
},
|
||||
};
|
|
@ -1,82 +1,325 @@
|
|||
'use strict';
|
||||
|
||||
var path = require('path'),
|
||||
fs = require('fs');
|
||||
import path from 'path';
|
||||
import fs from 'node:fs';
|
||||
|
||||
var clone = require('clone'),
|
||||
express = require('express');
|
||||
import clone from 'clone';
|
||||
import express from 'express';
|
||||
import { validateStyleMin } from '@maplibre/maplibre-gl-style-spec';
|
||||
|
||||
import {
|
||||
allowedSpriteScales,
|
||||
allowedSpriteFormats,
|
||||
fixUrl,
|
||||
readFile,
|
||||
} from './utils.js';
|
||||
|
||||
module.exports = function(options, repo, params, id, reportTiles, reportFont) {
|
||||
var app = express().disable('x-powered-by');
|
||||
const httpTester = /^https?:\/\//i;
|
||||
|
||||
var styleFile = path.join(options.paths.styles, params.style);
|
||||
|
||||
var styleJSON = clone(require(styleFile));
|
||||
Object.keys(styleJSON.sources).forEach(function(name) {
|
||||
var source = styleJSON.sources[name];
|
||||
var url = source.url;
|
||||
if (url.lastIndexOf('mbtiles:', 0) === 0) {
|
||||
var mbtiles = url.substring('mbtiles://'.length);
|
||||
var identifier = reportTiles(mbtiles);
|
||||
source.url = 'local://data/' + identifier + '.json';
|
||||
}
|
||||
});
|
||||
|
||||
var findFontReferences = function(obj) {
|
||||
Object.keys(obj).forEach(function(key) {
|
||||
var value = obj[key];
|
||||
if (key == 'text-font') {
|
||||
if (value && value.length > 0) {
|
||||
value.forEach(reportFont);
|
||||
export const serve_style = {
|
||||
/**
|
||||
* Initializes the serve_style module.
|
||||
* @param {object} options Configuration options.
|
||||
* @param {object} repo Repository object.
|
||||
* @param {object} programOpts - An object containing the program options.
|
||||
* @returns {express.Application} The initialized Express application.
|
||||
*/
|
||||
init: function (options, repo, programOpts) {
|
||||
const { verbose } = programOpts;
|
||||
const app = express().disable('x-powered-by');
|
||||
/**
|
||||
* Handles requests for style.json files.
|
||||
* @param {express.Request} req - Express request object.
|
||||
* @param {express.Response} res - Express response object.
|
||||
* @param {express.NextFunction} next - Express next function.
|
||||
* @param {string} req.params.id - ID of the style.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
app.get('/:id/style.json', (req, res, next) => {
|
||||
const { id } = req.params;
|
||||
if (verbose) {
|
||||
console.log(
|
||||
'Handling style request for: /styles/%s/style.json',
|
||||
String(id).replace(/\n|\r/g, ''),
|
||||
);
|
||||
}
|
||||
try {
|
||||
const item = repo[id];
|
||||
if (!item) {
|
||||
return res.sendStatus(404);
|
||||
}
|
||||
} else if (value && typeof value == 'object') {
|
||||
findFontReferences(value);
|
||||
const styleJSON_ = clone(item.styleJSON);
|
||||
for (const name of Object.keys(styleJSON_.sources)) {
|
||||
const source = styleJSON_.sources[name];
|
||||
source.url = fixUrl(req, source.url, item.publicUrl);
|
||||
if (typeof source.data == 'string') {
|
||||
source.data = fixUrl(req, source.data, item.publicUrl);
|
||||
}
|
||||
}
|
||||
if (styleJSON_.sprite) {
|
||||
if (Array.isArray(styleJSON_.sprite)) {
|
||||
styleJSON_.sprite.forEach((spriteItem) => {
|
||||
spriteItem.url = fixUrl(req, spriteItem.url, item.publicUrl);
|
||||
});
|
||||
} else {
|
||||
styleJSON_.sprite = fixUrl(req, styleJSON_.sprite, item.publicUrl);
|
||||
}
|
||||
}
|
||||
if (styleJSON_.glyphs) {
|
||||
styleJSON_.glyphs = fixUrl(req, styleJSON_.glyphs, item.publicUrl);
|
||||
}
|
||||
return res.send(styleJSON_);
|
||||
} catch (e) {
|
||||
next(e);
|
||||
}
|
||||
});
|
||||
};
|
||||
styleJSON.layers.forEach(findFontReferences);
|
||||
|
||||
var spritePath = path.join(options.paths.sprites,
|
||||
path.basename(styleFile, '.json'));
|
||||
/**
|
||||
* Handles GET requests for sprite images and JSON files.
|
||||
* @param {express.Request} req - Express request object.
|
||||
* @param {express.Response} res - Express response object.
|
||||
* @param {express.NextFunction} next - Express next function.
|
||||
* @param {string} req.params.id - ID of the sprite.
|
||||
* @param {string} [req.params.spriteID='default'] - ID of the specific sprite image, defaults to 'default'.
|
||||
* @param {string} [req.params.scale] - Scale of the sprite image, defaults to ''.
|
||||
* @param {string} req.params.format - Format of the sprite file, 'png' or 'json'.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
app.get(
|
||||
`/:id/sprite{/:spriteID}{@:scale}{.:format}`,
|
||||
async (req, res, next) => {
|
||||
const { spriteID = 'default', id, format, scale } = req.params;
|
||||
const sanitizedId = String(id).replace(/\n|\r/g, '');
|
||||
const sanitizedScale = scale ? String(scale).replace(/\n|\r/g, '') : '';
|
||||
const sanitizedSpriteID = String(spriteID).replace(/\n|\r/g, '');
|
||||
const sanitizedFormat = format
|
||||
? '.' + String(format).replace(/\n|\r/g, '')
|
||||
: '';
|
||||
if (verbose) {
|
||||
console.log(
|
||||
`Handling sprite request for: /styles/%s/sprite/%s%s%s`,
|
||||
sanitizedId,
|
||||
sanitizedSpriteID,
|
||||
sanitizedScale,
|
||||
sanitizedFormat,
|
||||
);
|
||||
}
|
||||
const item = repo[id];
|
||||
const validatedFormat = allowedSpriteFormats(format);
|
||||
if (!item || !validatedFormat) {
|
||||
if (verbose)
|
||||
console.error(
|
||||
`Sprite item or format not found for: /styles/%s/sprite/%s%s%s`,
|
||||
sanitizedId,
|
||||
sanitizedSpriteID,
|
||||
sanitizedScale,
|
||||
sanitizedFormat,
|
||||
);
|
||||
return res.sendStatus(404);
|
||||
}
|
||||
const sprite = item.spritePaths.find(
|
||||
(sprite) => sprite.id === spriteID,
|
||||
);
|
||||
const spriteScale = allowedSpriteScales(scale);
|
||||
if (!sprite || spriteScale === null) {
|
||||
if (verbose)
|
||||
console.error(
|
||||
`Bad Sprite ID or Scale for: /styles/%s/sprite/%s%s%s`,
|
||||
sanitizedId,
|
||||
sanitizedSpriteID,
|
||||
sanitizedScale,
|
||||
sanitizedFormat,
|
||||
);
|
||||
return res.status(400).send('Bad Sprite ID or Scale');
|
||||
}
|
||||
|
||||
styleJSON.sprite = 'local://styles/' + id + '/sprite';
|
||||
styleJSON.glyphs = 'local://fonts/{fontstack}/{range}.pbf';
|
||||
const modifiedSince = req.get('if-modified-since');
|
||||
const cc = req.get('cache-control');
|
||||
if (modifiedSince && (!cc || cc.indexOf('no-cache') === -1)) {
|
||||
if (
|
||||
new Date(item.lastModified).getTime() ===
|
||||
new Date(modifiedSince).getTime()
|
||||
) {
|
||||
return res.sendStatus(304);
|
||||
}
|
||||
}
|
||||
|
||||
repo[id] = styleJSON;
|
||||
const sanitizedSpritePath = sprite.path.replace(/^(\.\.\/)+/, '');
|
||||
const filename = `${sanitizedSpritePath}${spriteScale}.${validatedFormat}`;
|
||||
if (verbose) console.log(`Loading sprite from: %s`, filename);
|
||||
try {
|
||||
const data = await readFile(filename);
|
||||
|
||||
app.get('/' + id + '.json', function(req, res, next) {
|
||||
var fixUrl = function(url) {
|
||||
return url.replace(
|
||||
'local://', req.protocol + '://' + req.headers.host + '/');
|
||||
if (validatedFormat === 'json') {
|
||||
res.header('Content-type', 'application/json');
|
||||
} else if (validatedFormat === 'png') {
|
||||
res.header('Content-type', 'image/png');
|
||||
}
|
||||
if (verbose)
|
||||
console.log(
|
||||
`Responding with sprite data for /styles/%s/sprite/%s%s%s`,
|
||||
sanitizedId,
|
||||
sanitizedSpriteID,
|
||||
sanitizedScale,
|
||||
sanitizedFormat,
|
||||
);
|
||||
res.set({ 'Last-Modified': item.lastModified });
|
||||
return res.send(data);
|
||||
} catch (err) {
|
||||
if (verbose) {
|
||||
console.error(
|
||||
'Sprite load error: %s, Error: %s',
|
||||
filename,
|
||||
String(err),
|
||||
);
|
||||
}
|
||||
return res.sendStatus(404);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
return app;
|
||||
},
|
||||
/**
|
||||
* Removes an item from the repository.
|
||||
* @param {object} repo Repository object.
|
||||
* @param {string} id ID of the item to remove.
|
||||
* @returns {void}
|
||||
*/
|
||||
remove: function (repo, id) {
|
||||
delete repo[id];
|
||||
},
|
||||
/**
|
||||
* Adds a new style to the repository.
|
||||
* @param {object} options Configuration options.
|
||||
* @param {object} repo Repository object.
|
||||
* @param {object} params Parameters object containing style path
|
||||
* @param {string} id ID of the style.
|
||||
* @param {object} programOpts - An object containing the program options
|
||||
* @param {object} style pre-fetched/read StyleJSON object.
|
||||
* @param {Function} reportTiles Function for reporting tile sources.
|
||||
* @param {Function} reportFont Function for reporting font usage
|
||||
* @returns {boolean} true if add is successful
|
||||
*/
|
||||
add: function (
|
||||
options,
|
||||
repo,
|
||||
params,
|
||||
id,
|
||||
programOpts,
|
||||
style,
|
||||
reportTiles,
|
||||
reportFont,
|
||||
) {
|
||||
const { publicUrl } = programOpts;
|
||||
const styleFile = path.resolve(options.paths.styles, params.style);
|
||||
const styleJSON = clone(style);
|
||||
|
||||
const validationErrors = validateStyleMin(styleJSON);
|
||||
if (validationErrors.length > 0) {
|
||||
console.log(`The file "${params.style}" is not a valid style file:`);
|
||||
for (const err of validationErrors) {
|
||||
console.log(`${err.line}: ${err.message}`);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const name of Object.keys(styleJSON.sources)) {
|
||||
const source = styleJSON.sources[name];
|
||||
let url = source.url;
|
||||
if (
|
||||
url &&
|
||||
(url.startsWith('pmtiles://') || url.startsWith('mbtiles://'))
|
||||
) {
|
||||
const protocol = url.split(':')[0];
|
||||
|
||||
let dataId = url.replace('pmtiles://', '').replace('mbtiles://', '');
|
||||
if (dataId.startsWith('{') && dataId.endsWith('}')) {
|
||||
dataId = dataId.slice(1, -1);
|
||||
}
|
||||
|
||||
const mapsTo = (params.mapping || {})[dataId];
|
||||
if (mapsTo) {
|
||||
dataId = mapsTo;
|
||||
}
|
||||
|
||||
const identifier = reportTiles(dataId, protocol);
|
||||
if (!identifier) {
|
||||
return false;
|
||||
}
|
||||
source.url = `local://data/${identifier}.json`;
|
||||
}
|
||||
|
||||
let data = source.data;
|
||||
if (data && typeof data == 'string' && data.startsWith('file://')) {
|
||||
source.data =
|
||||
'local://files' +
|
||||
path.resolve(
|
||||
'/',
|
||||
data.replace('file://', '').replace(options.paths.files, ''),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for (const obj of styleJSON.layers) {
|
||||
if (obj['type'] === 'symbol') {
|
||||
const fonts = (obj['layout'] || {})['text-font'];
|
||||
if (fonts && fonts.length) {
|
||||
fonts.forEach(reportFont);
|
||||
} else {
|
||||
reportFont('Open Sans Regular');
|
||||
reportFont('Arial Unicode MS Regular');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let spritePaths = [];
|
||||
if (styleJSON.sprite) {
|
||||
if (!Array.isArray(styleJSON.sprite)) {
|
||||
if (!httpTester.test(styleJSON.sprite)) {
|
||||
let spritePath = path.join(
|
||||
options.paths.sprites,
|
||||
styleJSON.sprite
|
||||
.replace('{style}', path.basename(styleFile, '.json'))
|
||||
.replace(
|
||||
'{styleJsonFolder}',
|
||||
path.relative(options.paths.sprites, path.dirname(styleFile)),
|
||||
),
|
||||
);
|
||||
styleJSON.sprite = `local://styles/${id}/sprite`;
|
||||
spritePaths.push({ id: 'default', path: spritePath });
|
||||
}
|
||||
} else {
|
||||
for (let spriteItem of styleJSON.sprite) {
|
||||
if (!httpTester.test(spriteItem.url)) {
|
||||
let spritePath = path.join(
|
||||
options.paths.sprites,
|
||||
spriteItem.url
|
||||
.replace('{style}', path.basename(styleFile, '.json'))
|
||||
.replace(
|
||||
'{styleJsonFolder}',
|
||||
path.relative(options.paths.sprites, path.dirname(styleFile)),
|
||||
),
|
||||
);
|
||||
spriteItem.url = `local://styles/${id}/sprite/` + spriteItem.id;
|
||||
spritePaths.push({ id: spriteItem.id, path: spritePath });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (styleJSON.glyphs && !httpTester.test(styleJSON.glyphs)) {
|
||||
styleJSON.glyphs = 'local://fonts/{fontstack}/{range}.pbf';
|
||||
}
|
||||
|
||||
repo[id] = {
|
||||
styleJSON,
|
||||
spritePaths,
|
||||
publicUrl,
|
||||
name: styleJSON.name,
|
||||
lastModified: new Date().toUTCString(),
|
||||
};
|
||||
|
||||
var styleJSON_ = clone(styleJSON);
|
||||
Object.keys(styleJSON_.sources).forEach(function(name) {
|
||||
var source = styleJSON_.sources[name];
|
||||
source.url = fixUrl(source.url);
|
||||
});
|
||||
styleJSON_.sprite = fixUrl(styleJSON_.sprite);
|
||||
styleJSON_.glyphs = fixUrl(styleJSON_.glyphs);
|
||||
return res.send(styleJSON_);
|
||||
});
|
||||
|
||||
app.get('/' + id + '/sprite:scale(@[23]x)?\.:format([\\w]+)',
|
||||
function(req, res, next) {
|
||||
var scale = req.params.scale,
|
||||
format = req.params.format;
|
||||
var filename = spritePath + (scale || '') + '.' + format;
|
||||
return fs.readFile(filename, function(err, data) {
|
||||
if (err) {
|
||||
console.log('Sprite load error:', filename);
|
||||
return res.status(404).send('File not found');
|
||||
} else {
|
||||
if (format == 'json') res.header('Content-type', 'application/json');
|
||||
if (format == 'png') res.header('Content-type', 'image/png');
|
||||
return res.send(data);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return app;
|
||||
return true;
|
||||
},
|
||||
};
|
||||
|
|
903
src/server.js
|
@ -1,222 +1,620 @@
|
|||
#!/usr/bin/env node
|
||||
'use strict';
|
||||
|
||||
process.env.UV_THREADPOOL_SIZE =
|
||||
Math.ceil(Math.max(4, require('os').cpus().length * 1.5));
|
||||
import fs from 'node:fs';
|
||||
import path from 'path';
|
||||
import fnv1a from '@sindresorhus/fnv1a';
|
||||
import chokidar from 'chokidar';
|
||||
import clone from 'clone';
|
||||
import cors from 'cors';
|
||||
import enableShutdown from 'http-shutdown';
|
||||
import express from 'express';
|
||||
import handlebars from 'handlebars';
|
||||
import SphericalMercator from '@mapbox/sphericalmercator';
|
||||
const mercator = new SphericalMercator();
|
||||
import morgan from 'morgan';
|
||||
import { serve_data } from './serve_data.js';
|
||||
import { serve_style } from './serve_style.js';
|
||||
import { serve_font } from './serve_font.js';
|
||||
import {
|
||||
allowedTileSizes,
|
||||
getTileUrls,
|
||||
getPublicUrl,
|
||||
isValidHttpUrl,
|
||||
} from './utils.js';
|
||||
|
||||
var fs = require('fs'),
|
||||
path = require('path');
|
||||
import { fileURLToPath } from 'url';
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const packageJson = JSON.parse(
|
||||
fs.readFileSync(__dirname + '/../package.json', 'utf8'),
|
||||
);
|
||||
const isLight = packageJson.name.slice(-6) === '-light';
|
||||
|
||||
var clone = require('clone'),
|
||||
cors = require('cors'),
|
||||
express = require('express'),
|
||||
handlebars = require('handlebars'),
|
||||
mercator = new (require('sphericalmercator'))(),
|
||||
morgan = require('morgan');
|
||||
const serve_rendered = (
|
||||
await import(`${!isLight ? `./serve_rendered.js` : `./serve_light.js`}`)
|
||||
).serve_rendered;
|
||||
|
||||
var serve_font = require('./serve_font'),
|
||||
serve_rendered = require('./serve_rendered'),
|
||||
serve_style = require('./serve_style'),
|
||||
serve_data = require('./serve_data'),
|
||||
utils = require('./utils');
|
||||
/**
|
||||
* Starts the server.
|
||||
* @param {object} opts - Configuration options for the server.
|
||||
* @returns {Promise<object>} - A promise that resolves to the server object.
|
||||
*/
|
||||
async function start(opts) {
|
||||
console.log('Starting server');
|
||||
|
||||
module.exports = function(opts, callback) {
|
||||
var app = express().disable('x-powered-by'),
|
||||
serving = {
|
||||
styles: {},
|
||||
rendered: {},
|
||||
data: {},
|
||||
fonts: { // default fonts, always expose these (if they exist)
|
||||
'Open Sans Regular': true,
|
||||
'Arial Unicode MS Regular': true
|
||||
}
|
||||
};
|
||||
const app = express().disable('x-powered-by');
|
||||
const serving = {
|
||||
styles: {},
|
||||
rendered: {},
|
||||
data: {},
|
||||
fonts: {},
|
||||
};
|
||||
|
||||
app.enable('trust proxy');
|
||||
|
||||
callback = callback || function() {};
|
||||
|
||||
if (process.env.NODE_ENV !== 'production' &&
|
||||
process.env.NODE_ENV !== 'test') {
|
||||
app.use(morgan('dev'));
|
||||
if (process.env.NODE_ENV !== 'test') {
|
||||
const defaultLogFormat =
|
||||
process.env.NODE_ENV === 'production' ? 'tiny' : 'dev';
|
||||
const logFormat = opts.logFormat || defaultLogFormat;
|
||||
app.use(
|
||||
morgan(logFormat, {
|
||||
stream: opts.logFile
|
||||
? fs.createWriteStream(opts.logFile, { flags: 'a' })
|
||||
: process.stdout,
|
||||
skip: (req, res) =>
|
||||
opts.silent && (res.statusCode === 200 || res.statusCode === 304),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
var configPath = path.resolve(opts.config);
|
||||
|
||||
var config;
|
||||
try {
|
||||
config = require(configPath);
|
||||
} catch (e) {
|
||||
console.log('ERROR: Config file not found or invalid!');
|
||||
console.log(' See README.md for instructions and sample data.');
|
||||
let config = opts.config || null;
|
||||
let configPath = null;
|
||||
if (opts.configPath) {
|
||||
configPath = path.resolve(opts.configPath);
|
||||
try {
|
||||
config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
||||
} catch (e) {
|
||||
console.log('ERROR: Config file not found or invalid!');
|
||||
console.log(' See README.md for instructions and sample data.');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
if (!config) {
|
||||
console.log('ERROR: No config file not specified!');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
var options = config.options || {};
|
||||
var paths = options.paths || {};
|
||||
const options = config.options || {};
|
||||
const paths = options.paths || {};
|
||||
options.paths = paths;
|
||||
paths.root = path.resolve(process.cwd(), paths.root || '');
|
||||
paths.root = path.resolve(
|
||||
configPath ? path.dirname(configPath) : process.cwd(),
|
||||
paths.root || '',
|
||||
);
|
||||
paths.styles = path.resolve(paths.root, paths.styles || '');
|
||||
paths.fonts = path.resolve(paths.root, paths.fonts || '');
|
||||
paths.sprites = path.resolve(paths.root, paths.sprites || '');
|
||||
paths.mbtiles = path.resolve(paths.root, paths.mbtiles || '');
|
||||
paths.pmtiles = path.resolve(paths.root, paths.pmtiles || '');
|
||||
paths.icons = paths.icons
|
||||
? path.resolve(paths.root, paths.icons)
|
||||
: path.resolve(__dirname, '../public/resources/images');
|
||||
paths.files = paths.files
|
||||
? path.resolve(paths.root, paths.files)
|
||||
: path.resolve(__dirname, '../public/files');
|
||||
|
||||
var data = clone(config.data || {});
|
||||
const startupPromises = [];
|
||||
|
||||
Object.keys(config.styles || {}).forEach(function(id) {
|
||||
var item = config.styles[id];
|
||||
if (!item.style || item.style.length == 0) {
|
||||
console.log('Missing "style" property for ' + id);
|
||||
return;
|
||||
for (const type of Object.keys(paths)) {
|
||||
if (!fs.existsSync(paths[type])) {
|
||||
console.error(
|
||||
`The specified path for "${type}" does not exist (${paths[type]}).`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively get all files within a directory.
|
||||
* Inspired by https://stackoverflow.com/a/45130990/10133863
|
||||
* @param {string} directory Absolute path to a directory to get files from.
|
||||
* @returns {Promise<string[]>} - A promise that resolves to an array of file paths relative to the icon directory.
|
||||
*/
|
||||
async function getFiles(directory) {
|
||||
// Fetch all entries of the directory and attach type information
|
||||
const dirEntries = await fs.promises.readdir(directory, {
|
||||
withFileTypes: true,
|
||||
});
|
||||
|
||||
// Iterate through entries and return the relative file-path to the icon directory if it is not a directory
|
||||
// otherwise initiate a recursive call
|
||||
const files = await Promise.all(
|
||||
dirEntries.map((dirEntry) => {
|
||||
const entryPath = path.resolve(directory, dirEntry.name);
|
||||
return dirEntry.isDirectory()
|
||||
? getFiles(entryPath)
|
||||
: entryPath.replace(paths.icons + path.sep, '');
|
||||
}),
|
||||
);
|
||||
|
||||
// Flatten the list of files to a single array
|
||||
return files.flat();
|
||||
}
|
||||
|
||||
// Load all available icons into a settings object
|
||||
startupPromises.push(
|
||||
getFiles(paths.icons).then((files) => {
|
||||
paths.availableIcons = files;
|
||||
}),
|
||||
);
|
||||
|
||||
if (options.dataDecorator) {
|
||||
try {
|
||||
options.dataDecoratorFunc = require(
|
||||
path.resolve(paths.root, options.dataDecorator),
|
||||
);
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
const data = clone(config.data || {});
|
||||
|
||||
if (opts.cors) {
|
||||
app.use(cors());
|
||||
}
|
||||
|
||||
app.use('/data/', serve_data.init(options, serving.data, opts));
|
||||
app.use('/files/', express.static(paths.files));
|
||||
app.use('/styles/', serve_style.init(options, serving.styles, opts));
|
||||
if (!isLight) {
|
||||
startupPromises.push(
|
||||
serve_rendered.init(options, serving.rendered, opts).then((sub) => {
|
||||
app.use('/styles/', sub);
|
||||
}),
|
||||
);
|
||||
}
|
||||
/**
|
||||
* Adds a style to the server.
|
||||
* @param {string} id - The ID of the style.
|
||||
* @param {object} item - The style configuration object.
|
||||
* @param {boolean} allowMoreData - Whether to allow adding more data sources.
|
||||
* @param {boolean} reportFonts - Whether to report fonts.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function addStyle(id, item, allowMoreData, reportFonts) {
|
||||
let success = true;
|
||||
|
||||
let styleJSON;
|
||||
try {
|
||||
if (isValidHttpUrl(item.style)) {
|
||||
const res = await fetch(item.style);
|
||||
if (!res.ok) {
|
||||
throw new Error(`fetch error ${res.status}`);
|
||||
}
|
||||
styleJSON = await res.json();
|
||||
} else {
|
||||
const styleFile = path.resolve(options.paths.styles, item.style);
|
||||
const styleFileData = await fs.promises.readFile(styleFile);
|
||||
styleJSON = JSON.parse(styleFileData);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(`Error getting style file "${item.style}"`);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (item.serve_data !== false) {
|
||||
app.use('/styles/', serve_style(options, serving.styles, item, id,
|
||||
function(mbtiles) {
|
||||
var dataItemId;
|
||||
Object.keys(data).forEach(function(id) {
|
||||
if (data[id].mbtiles == mbtiles) {
|
||||
success = serve_style.add(
|
||||
options,
|
||||
serving.styles,
|
||||
item,
|
||||
id,
|
||||
opts,
|
||||
styleJSON,
|
||||
(styleSourceId, protocol) => {
|
||||
let dataItemId;
|
||||
for (const id of Object.keys(data)) {
|
||||
if (id === styleSourceId) {
|
||||
// Style id was found in data ids, return that id
|
||||
dataItemId = id;
|
||||
} else {
|
||||
const fileType = Object.keys(data[id])[0];
|
||||
if (data[id][fileType] === styleSourceId) {
|
||||
// Style id was found in data filename, return the id that filename belong to
|
||||
dataItemId = id;
|
||||
}
|
||||
}
|
||||
});
|
||||
if (dataItemId) { // mbtiles exist in the data config
|
||||
}
|
||||
if (dataItemId) {
|
||||
// input files exists in the data config, return found id
|
||||
return dataItemId;
|
||||
} else {
|
||||
var id = mbtiles.substr(0, mbtiles.lastIndexOf('.')) || mbtiles;
|
||||
while (data[id]) id += '_';
|
||||
data[id] = {
|
||||
'mbtiles': mbtiles
|
||||
};
|
||||
return id;
|
||||
}
|
||||
}, function(font) {
|
||||
serving.fonts[font] = true;
|
||||
}));
|
||||
}
|
||||
if (item.serve_rendered !== false) {
|
||||
app.use('/styles/' + id + '/',
|
||||
serve_rendered(options, serving.rendered, item, id));
|
||||
}
|
||||
});
|
||||
if (!allowMoreData) {
|
||||
console.log(
|
||||
`ERROR: style "${item.style}" using unknown file "${styleSourceId}"! Skipping...`,
|
||||
);
|
||||
return undefined;
|
||||
} else {
|
||||
let id =
|
||||
styleSourceId.substr(0, styleSourceId.lastIndexOf('.')) ||
|
||||
styleSourceId;
|
||||
if (isValidHttpUrl(styleSourceId)) {
|
||||
id =
|
||||
fnv1a(styleSourceId) + '_' + id.replace(/^.*\/(.*)$/, '$1');
|
||||
}
|
||||
while (data[id]) id += '_'; //if the data source id already exists, add a "_" untill it doesn't
|
||||
//Add the new data source to the data array.
|
||||
data[id] = {
|
||||
[protocol]: styleSourceId,
|
||||
};
|
||||
|
||||
if (Object.keys(serving.styles).length > 0) {
|
||||
// serve fonts only if serving some styles
|
||||
app.use('/fonts/', serve_font(options, serving.fonts));
|
||||
return id;
|
||||
}
|
||||
}
|
||||
},
|
||||
(font) => {
|
||||
if (reportFonts) {
|
||||
serving.fonts[font] = true;
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
if (success && item.serve_rendered !== false) {
|
||||
if (!isLight) {
|
||||
startupPromises.push(
|
||||
serve_rendered.add(
|
||||
options,
|
||||
serving.rendered,
|
||||
item,
|
||||
id,
|
||||
opts,
|
||||
styleJSON,
|
||||
function dataResolver(styleSourceId) {
|
||||
let fileType;
|
||||
let inputFile;
|
||||
for (const id of Object.keys(data)) {
|
||||
fileType = Object.keys(data[id])[0];
|
||||
if (styleSourceId == id) {
|
||||
inputFile = data[id][fileType];
|
||||
break;
|
||||
} else if (data[id][fileType] == styleSourceId) {
|
||||
inputFile = data[id][fileType];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!isValidHttpUrl(inputFile)) {
|
||||
inputFile = path.resolve(options.paths[fileType], inputFile);
|
||||
}
|
||||
|
||||
return { inputFile, fileType };
|
||||
},
|
||||
),
|
||||
);
|
||||
} else {
|
||||
item.serve_rendered = false;
|
||||
}
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
app.use(cors());
|
||||
|
||||
Object.keys(data).forEach(function(id) {
|
||||
var item = data[id];
|
||||
if (!item.mbtiles || item.mbtiles.length == 0) {
|
||||
console.log('Missing "mbtiles" property for ' + id);
|
||||
return;
|
||||
for (const id of Object.keys(config.styles || {})) {
|
||||
const item = config.styles[id];
|
||||
if (!item.style || item.style.length === 0) {
|
||||
console.log(`Missing "style" property for ${id}`);
|
||||
continue;
|
||||
}
|
||||
startupPromises.push(addStyle(id, item, true, true));
|
||||
}
|
||||
startupPromises.push(
|
||||
serve_font(options, serving.fonts, opts).then((sub) => {
|
||||
app.use('/', sub);
|
||||
}),
|
||||
);
|
||||
for (const id of Object.keys(data)) {
|
||||
const item = data[id];
|
||||
const fileType = Object.keys(data[id])[0];
|
||||
if (!fileType || !(fileType === 'pmtiles' || fileType === 'mbtiles')) {
|
||||
console.log(
|
||||
`Missing "pmtiles" or "mbtiles" property for ${id} data source`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
startupPromises.push(serve_data.add(options, serving.data, item, id, opts));
|
||||
}
|
||||
if (options.serveAllStyles) {
|
||||
fs.readdir(options.paths.styles, { withFileTypes: true }, (err, files) => {
|
||||
if (err) {
|
||||
return;
|
||||
}
|
||||
for (const file of files) {
|
||||
if (file.isFile() && path.extname(file.name).toLowerCase() == '.json') {
|
||||
const id = path.basename(file.name, '.json');
|
||||
const item = {
|
||||
style: file.name,
|
||||
};
|
||||
addStyle(id, item, false, false);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
app.use('/data/', serve_data(options, serving.data, item, id));
|
||||
});
|
||||
const watcher = chokidar.watch(
|
||||
path.join(options.paths.styles, '*.json'),
|
||||
{},
|
||||
);
|
||||
watcher.on('all', (eventType, filename) => {
|
||||
if (filename) {
|
||||
const id = path.basename(filename, '.json');
|
||||
console.log(`Style "${id}" changed, updating...`);
|
||||
|
||||
app.get('/styles.json', function(req, res, next) {
|
||||
var result = [];
|
||||
Object.keys(serving.styles).forEach(function(id) {
|
||||
var styleJSON = serving.styles[id];
|
||||
serve_style.remove(serving.styles, id);
|
||||
if (!isLight) {
|
||||
serve_rendered.remove(serving.rendered, id);
|
||||
}
|
||||
|
||||
if (eventType == 'add' || eventType == 'change') {
|
||||
const item = {
|
||||
style: filename,
|
||||
};
|
||||
addStyle(id, item, false, false);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Handles requests for a list of available styles.
|
||||
* @param {object} req - Express request object.
|
||||
* @param {object} res - Express response object.
|
||||
* @param {string} [req.query.key] - Optional API key.
|
||||
* @returns {void}
|
||||
*/
|
||||
app.get('/styles.json', (req, res, next) => {
|
||||
const result = [];
|
||||
const query = req.query.key
|
||||
? `?key=${encodeURIComponent(req.query.key)}`
|
||||
: '';
|
||||
for (const id of Object.keys(serving.styles)) {
|
||||
const styleJSON = serving.styles[id].styleJSON;
|
||||
result.push({
|
||||
version: styleJSON.version,
|
||||
name: styleJSON.name,
|
||||
id: id,
|
||||
url: req.protocol + '://' + req.headers.host + '/styles/' + id + '.json'
|
||||
id,
|
||||
url: `${getPublicUrl(
|
||||
opts.publicUrl,
|
||||
req,
|
||||
)}styles/${id}/style.json${query}`,
|
||||
});
|
||||
});
|
||||
}
|
||||
res.send(result);
|
||||
});
|
||||
|
||||
var addTileJSONs = function(arr, req, type) {
|
||||
Object.keys(serving[type]).forEach(function(id) {
|
||||
var info = clone(serving[type][id]);
|
||||
info.tiles = utils.getTileUrls(req, info.tiles,
|
||||
type + '/' + id, info.format);
|
||||
/**
|
||||
* Adds TileJSON metadata to an array.
|
||||
* @param {Array} arr - The array to add TileJSONs to
|
||||
* @param {object} req - The express request object.
|
||||
* @param {string} type - The type of resource
|
||||
* @param {number} tileSize - The tile size.
|
||||
* @returns {Array} - An array of TileJSON objects.
|
||||
*/
|
||||
function addTileJSONs(arr, req, type, tileSize) {
|
||||
for (const id of Object.keys(serving[type])) {
|
||||
const info = clone(serving[type][id].tileJSON);
|
||||
let path = '';
|
||||
if (type === 'rendered') {
|
||||
path = `styles/${id}`;
|
||||
} else {
|
||||
path = `${type}/${id}`;
|
||||
}
|
||||
info.tiles = getTileUrls(
|
||||
req,
|
||||
info.tiles,
|
||||
path,
|
||||
tileSize,
|
||||
info.format,
|
||||
opts.publicUrl,
|
||||
{
|
||||
pbf: options.pbfAlias,
|
||||
},
|
||||
);
|
||||
arr.push(info);
|
||||
});
|
||||
}
|
||||
return arr;
|
||||
};
|
||||
}
|
||||
|
||||
app.get('/rendered.json', function(req, res, next) {
|
||||
res.send(addTileJSONs([], req, 'rendered'));
|
||||
});
|
||||
app.get('/data.json', function(req, res, next) {
|
||||
res.send(addTileJSONs([], req, 'data'));
|
||||
});
|
||||
app.get('/index.json', function(req, res, next) {
|
||||
res.send(addTileJSONs(addTileJSONs([], req, 'rendered'), req, 'data'));
|
||||
/**
|
||||
* Handles requests for a rendered tilejson endpoint.
|
||||
* @param {object} req - Express request object.
|
||||
* @param {object} res - Express response object.
|
||||
* @param {string} req.params.tileSize - Optional tile size parameter.
|
||||
* @returns {void}
|
||||
*/
|
||||
app.get('{/:tileSize}/rendered.json', (req, res, next) => {
|
||||
const tileSize = allowedTileSizes(req.params['tileSize']);
|
||||
res.send(addTileJSONs([], req, 'rendered', parseInt(tileSize, 10)));
|
||||
});
|
||||
|
||||
//------------------------------------
|
||||
/**
|
||||
* Handles requests for a data tilejson endpoint.
|
||||
* @param {object} req - Express request object.
|
||||
* @param {object} res - Express response object.
|
||||
* @returns {void}
|
||||
*/
|
||||
app.get('/data.json', (req, res) => {
|
||||
res.send(addTileJSONs([], req, 'data', undefined));
|
||||
});
|
||||
|
||||
/**
|
||||
* Handles requests for a combined rendered and data tilejson endpoint.
|
||||
* @param {object} req - Express request object.
|
||||
* @param {object} res - Express response object.
|
||||
* @param {string} req.params.tileSize - Optional tile size parameter.
|
||||
* @returns {void}
|
||||
*/
|
||||
app.get('{/:tileSize}/index.json', (req, res, next) => {
|
||||
const tileSize = allowedTileSizes(req.params['tileSize']);
|
||||
res.send(
|
||||
addTileJSONs(
|
||||
addTileJSONs([], req, 'rendered', parseInt(tileSize, 10)),
|
||||
req,
|
||||
'data',
|
||||
undefined,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
// ------------------------------------
|
||||
// serve web presentations
|
||||
app.use('/', express.static(path.join(__dirname, '../public/resources')));
|
||||
|
||||
var templates = path.join(__dirname, '../public/templates');
|
||||
var serveTemplate = function(path, template, dataGetter) {
|
||||
fs.readFile(templates + '/' + template + '.tmpl', function(err, content) {
|
||||
if (err) {
|
||||
console.log('Template not found:', err);
|
||||
}
|
||||
var compiled = handlebars.compile(content.toString());
|
||||
const templates = path.join(__dirname, '../public/templates');
|
||||
|
||||
app.use(path, function(req, res, next) {
|
||||
var data = {};
|
||||
/**
|
||||
* Serves a Handlebars template.
|
||||
* @param {string} urlPath - The URL path to serve the template at
|
||||
* @param {string} template - The name of the template file
|
||||
* @param {Function} dataGetter - A function to get data to be passed to the template.
|
||||
* @returns {void}
|
||||
*/
|
||||
function serveTemplate(urlPath, template, dataGetter) {
|
||||
let templateFile = `${templates}/${template}.tmpl`;
|
||||
if (template === 'index') {
|
||||
if (options.frontPage === false) {
|
||||
return;
|
||||
} else if (
|
||||
options.frontPage &&
|
||||
options.frontPage.constructor === String
|
||||
) {
|
||||
templateFile = path.resolve(paths.root, options.frontPage);
|
||||
}
|
||||
}
|
||||
try {
|
||||
const content = fs.readFileSync(templateFile, 'utf-8');
|
||||
const compiled = handlebars.compile(content.toString());
|
||||
app.get(urlPath, (req, res, next) => {
|
||||
if (opts.verbose) {
|
||||
console.log(`Serving template at path: ${urlPath}`);
|
||||
}
|
||||
let data = {};
|
||||
if (dataGetter) {
|
||||
data = dataGetter(req.params);
|
||||
if (!data) {
|
||||
return res.status(404).send('Not found');
|
||||
data = dataGetter(req);
|
||||
if (data) {
|
||||
data['server_version'] =
|
||||
`${packageJson.name} v${packageJson.version}`;
|
||||
data['public_url'] = opts.publicUrl || '/';
|
||||
data['is_light'] = isLight;
|
||||
data['key_query_part'] = req.query.key
|
||||
? `key=${encodeURIComponent(req.query.key)}&`
|
||||
: '';
|
||||
data['key_query'] = req.query.key
|
||||
? `?key=${encodeURIComponent(req.query.key)}`
|
||||
: '';
|
||||
if (template === 'wmts') res.set('Content-Type', 'text/xml');
|
||||
return res.status(200).send(compiled(data));
|
||||
} else {
|
||||
if (opts.verbose) {
|
||||
console.log(`Forwarding request for: ${urlPath} to next route`);
|
||||
}
|
||||
next('route');
|
||||
}
|
||||
}
|
||||
return res.status(200).send(compiled(data));
|
||||
});
|
||||
});
|
||||
};
|
||||
} catch (err) {
|
||||
console.error(`Error reading template file: ${templateFile}`, err);
|
||||
throw new Error(`Template not found: ${err.message}`); //throw an error so that the server doesnt start
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles requests for the index page, providing a list of available styles and data.
|
||||
* @param {object} req - Express request object.
|
||||
* @param {object} res - Express response object.
|
||||
* @returns {void}
|
||||
*/
|
||||
serveTemplate('/', 'index', (req) => {
|
||||
let styles = {};
|
||||
for (const id of Object.keys(serving.styles || {})) {
|
||||
let style = {
|
||||
...serving.styles[id],
|
||||
serving_data: serving.styles[id],
|
||||
serving_rendered: serving.rendered[id],
|
||||
};
|
||||
|
||||
serveTemplate('/$', 'index', function() {
|
||||
var styles = clone(config.styles || {});
|
||||
Object.keys(styles).forEach(function(id) {
|
||||
var style = styles[id];
|
||||
style.name = (serving.styles[id] || serving.rendered[id] || {}).name;
|
||||
style.serving_data = serving.styles[id];
|
||||
style.serving_rendered = serving.rendered[id];
|
||||
if (style.serving_rendered) {
|
||||
var center = style.serving_rendered.center;
|
||||
const { center } = style.serving_rendered.tileJSON;
|
||||
if (center) {
|
||||
style.viewer_hash = '#' + center[2] + '/' +
|
||||
center[1].toFixed(5) + '/' +
|
||||
center[0].toFixed(5);
|
||||
style.viewer_hash = `#${center[2]}/${center[1].toFixed(
|
||||
5,
|
||||
)}/${center[0].toFixed(5)}`;
|
||||
|
||||
var centerPx = mercator.px([center[0], center[1]], center[2]);
|
||||
style.thumbnail = center[2] + '/' +
|
||||
Math.floor(centerPx[0] / 256) + '/' +
|
||||
Math.floor(centerPx[1] / 256) + '.png';
|
||||
const centerPx = mercator.px([center[0], center[1]], center[2]);
|
||||
// Set thumbnail default size to be 256px x 256px
|
||||
style.thumbnail = `${Math.floor(center[2])}/${Math.floor(
|
||||
centerPx[0] / 256,
|
||||
)}/${Math.floor(centerPx[1] / 256)}.png`;
|
||||
}
|
||||
|
||||
const tileSize = 512;
|
||||
style.xyz_link = getTileUrls(
|
||||
req,
|
||||
style.serving_rendered.tileJSON.tiles,
|
||||
`styles/${id}`,
|
||||
tileSize,
|
||||
style.serving_rendered.tileJSON.format,
|
||||
opts.publicUrl,
|
||||
)[0];
|
||||
}
|
||||
});
|
||||
var data = clone(serving.data || {});
|
||||
Object.keys(data).forEach(function(id) {
|
||||
var data_ = data[id];
|
||||
var center = data_.center;
|
||||
|
||||
styles[id] = style;
|
||||
}
|
||||
let datas = {};
|
||||
for (const id of Object.keys(serving.data || {})) {
|
||||
let data = Object.assign({}, serving.data[id]);
|
||||
|
||||
const { tileJSON } = serving.data[id];
|
||||
const { center } = tileJSON;
|
||||
|
||||
if (center) {
|
||||
data_.viewer_hash = '#' + center[2] + '/' +
|
||||
center[1].toFixed(5) + '/' +
|
||||
center[0].toFixed(5);
|
||||
data.viewer_hash = `#${center[2]}/${center[1].toFixed(
|
||||
5,
|
||||
)}/${center[0].toFixed(5)}`;
|
||||
}
|
||||
data_.is_vector = data_.format == 'pbf';
|
||||
if (!data_.is_vector) {
|
||||
|
||||
const tileSize = undefined;
|
||||
data.xyz_link = getTileUrls(
|
||||
req,
|
||||
tileJSON.tiles,
|
||||
`data/${id}`,
|
||||
tileSize,
|
||||
tileJSON.format,
|
||||
opts.publicUrl,
|
||||
{
|
||||
pbf: options.pbfAlias,
|
||||
},
|
||||
)[0];
|
||||
|
||||
data.is_vector = tileJSON.format === 'pbf';
|
||||
if (!data.is_vector) {
|
||||
if (
|
||||
tileJSON.encoding === 'terrarium' ||
|
||||
tileJSON.encoding === 'mapbox'
|
||||
) {
|
||||
if (!isLight) {
|
||||
data.elevation_link = getTileUrls(
|
||||
req,
|
||||
tileJSON.tiles,
|
||||
`data/${id}/elevation`,
|
||||
)[0];
|
||||
}
|
||||
data.is_terrain = true;
|
||||
}
|
||||
if (center) {
|
||||
var centerPx = mercator.px([center[0], center[1]], center[2]);
|
||||
data_.thumbnail = center[2] + '/' +
|
||||
Math.floor(centerPx[0] / 256) + '/' +
|
||||
Math.floor(centerPx[1] / 256) + '.' + data_.format;
|
||||
const centerPx = mercator.px([center[0], center[1]], center[2]);
|
||||
data.thumbnail = `${Math.floor(center[2])}/${Math.floor(
|
||||
centerPx[0] / 256,
|
||||
)}/${Math.floor(centerPx[1] / 256)}.${tileJSON.format}`;
|
||||
}
|
||||
}
|
||||
if (data_.filesize) {
|
||||
var suffix = 'kB';
|
||||
var size = parseInt(data_.filesize, 10) / 1024;
|
||||
|
||||
if (data.filesize) {
|
||||
let suffix = 'kB';
|
||||
let size = parseInt(tileJSON.filesize, 10) / 1024;
|
||||
if (size > 1024) {
|
||||
suffix = 'MB';
|
||||
size /= 1024;
|
||||
|
@ -225,55 +623,190 @@ module.exports = function(opts, callback) {
|
|||
suffix = 'GB';
|
||||
size /= 1024;
|
||||
}
|
||||
data_.formatted_filesize = size.toFixed(2) + ' ' + suffix;
|
||||
data.formatted_filesize = `${size.toFixed(2)} ${suffix}`;
|
||||
}
|
||||
});
|
||||
datas[id] = data;
|
||||
}
|
||||
return {
|
||||
styles: styles,
|
||||
data: data
|
||||
styles: Object.keys(styles).length ? styles : null,
|
||||
data: Object.keys(datas).length ? datas : null,
|
||||
};
|
||||
});
|
||||
|
||||
serveTemplate('/styles/:id/$', 'viewer', function(params) {
|
||||
var id = params.id;
|
||||
var style = clone((config.styles || {})[id]);
|
||||
/**
|
||||
* Handles requests for a map viewer template for a specific style.
|
||||
* @param {object} req - Express request object.
|
||||
* @param {object} res - Express response object.
|
||||
* @param {string} req.params.id - ID of the style.
|
||||
* @returns {void}
|
||||
*/
|
||||
serveTemplate('/styles/:id/', 'viewer', (req) => {
|
||||
const { id } = req.params;
|
||||
const style = clone(((serving.styles || {})[id] || {}).styleJSON);
|
||||
|
||||
if (!style) {
|
||||
return null;
|
||||
}
|
||||
style.id = id;
|
||||
style.name = (serving.styles[id] || serving.rendered[id]).name;
|
||||
style.serving_data = serving.styles[id];
|
||||
style.serving_rendered = serving.rendered[id];
|
||||
return style;
|
||||
return {
|
||||
...style,
|
||||
id,
|
||||
name: (serving.styles[id] || serving.rendered[id]).name,
|
||||
serving_data: serving.styles[id],
|
||||
serving_rendered: serving.rendered[id],
|
||||
};
|
||||
});
|
||||
|
||||
/*
|
||||
app.use('/rendered/:id/$', function(req, res, next) {
|
||||
return res.redirect(301, '/styles/' + req.params.id + '/');
|
||||
});
|
||||
*/
|
||||
/**
|
||||
* Handles requests for a Web Map Tile Service (WMTS) XML template.
|
||||
* @param {object} req - Express request object.
|
||||
* @param {object} res - Express response object.
|
||||
* @param {string} req.params.id - ID of the style.
|
||||
* @returns {void}
|
||||
*/
|
||||
serveTemplate('/styles/:id/wmts.xml', 'wmts', (req) => {
|
||||
const { id } = req.params;
|
||||
const wmts = clone((serving.styles || {})[id]);
|
||||
|
||||
if (!wmts) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (wmts.hasOwnProperty('serve_rendered') && !wmts.serve_rendered) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let baseUrl;
|
||||
if (opts.publicUrl) {
|
||||
baseUrl = opts.publicUrl;
|
||||
} else {
|
||||
baseUrl = `${
|
||||
req.get('X-Forwarded-Protocol')
|
||||
? req.get('X-Forwarded-Protocol')
|
||||
: req.protocol
|
||||
}://${req.get('host')}/`;
|
||||
}
|
||||
|
||||
return {
|
||||
...wmts,
|
||||
id,
|
||||
name: (serving.styles[id] || serving.rendered[id]).name,
|
||||
baseUrl,
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* Handles requests for a data view template for a specific data source.
|
||||
* @param {object} req - Express request object.
|
||||
* @param {object} res - Express response object.
|
||||
* @param {string} req.params.id - ID of the data source.
|
||||
* @param {string} [req.params.view] - Optional view type.
|
||||
* @returns {void}
|
||||
*/
|
||||
serveTemplate('/data{/:view}/:id/', 'data', (req) => {
|
||||
const { id, view } = req.params;
|
||||
const data = serving.data[id];
|
||||
|
||||
serveTemplate('/data/:id/$', 'data', function(params) {
|
||||
var id = params.id;
|
||||
var data = clone(serving.data[id]);
|
||||
if (!data) {
|
||||
return null;
|
||||
}
|
||||
data.id = id;
|
||||
data.is_vector = data.format == 'pbf';
|
||||
return data;
|
||||
const is_terrain =
|
||||
(data.tileJSON.encoding === 'terrarium' ||
|
||||
data.tileJSON.encoding === 'mapbox') &&
|
||||
view === 'preview';
|
||||
|
||||
return {
|
||||
...data,
|
||||
id,
|
||||
use_maplibre: data.tileJSON.format === 'pbf' || is_terrain,
|
||||
is_terrain: is_terrain,
|
||||
is_terrainrgb: data.tileJSON.encoding === 'mapbox',
|
||||
terrain_encoding: data.tileJSON.encoding,
|
||||
is_light: isLight,
|
||||
};
|
||||
});
|
||||
|
||||
var server = app.listen(process.env.PORT || opts.port, function() {
|
||||
console.log('Listening at http://%s:%d/',
|
||||
this.address().address, this.address().port);
|
||||
|
||||
return callback();
|
||||
let startupComplete = false;
|
||||
const startupPromise = Promise.all(startupPromises).then(() => {
|
||||
console.log('Startup complete');
|
||||
startupComplete = true;
|
||||
});
|
||||
|
||||
setTimeout(callback, 1000);
|
||||
/**
|
||||
* Handles requests to see the health of the server.
|
||||
* @param {object} req - Express request object.
|
||||
* @param {object} res - Express response object.
|
||||
* @returns {void}
|
||||
*/
|
||||
app.get('/health', (req, res) => {
|
||||
if (startupComplete) {
|
||||
return res.status(200).send('OK');
|
||||
} else {
|
||||
return res.status(503).send('Starting');
|
||||
}
|
||||
});
|
||||
|
||||
const server = app.listen(
|
||||
process.env.PORT || opts.port,
|
||||
process.env.BIND || opts.bind,
|
||||
function () {
|
||||
let address = this.address().address;
|
||||
if (address.indexOf('::') === 0) {
|
||||
address = `[${address}]`; // literal IPv6 address
|
||||
}
|
||||
console.log(`Listening at http://${address}:${this.address().port}/`);
|
||||
},
|
||||
);
|
||||
|
||||
// add server.shutdown() to gracefully stop serving
|
||||
enableShutdown(server);
|
||||
|
||||
return {
|
||||
app: app,
|
||||
server: server
|
||||
app,
|
||||
server,
|
||||
startupPromise,
|
||||
serving,
|
||||
};
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Stop the server gracefully
|
||||
* @param {string} signal Name of the received signal
|
||||
* @returns {void}
|
||||
*/
|
||||
function stopGracefully(signal) {
|
||||
console.log(`Caught signal ${signal}, stopping gracefully`);
|
||||
process.exit();
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts and manages the server
|
||||
* @param {object} opts - Configuration options for the server.
|
||||
* @returns {Promise<object>} - A promise that resolves to the running server
|
||||
*/
|
||||
export async function server(opts) {
|
||||
const running = await start(opts);
|
||||
|
||||
running.startupPromise.catch((err) => {
|
||||
console.error(err.message);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
process.on('SIGINT', stopGracefully);
|
||||
process.on('SIGTERM', stopGracefully);
|
||||
|
||||
process.on('SIGHUP', (signal) => {
|
||||
console.log(`Caught signal ${signal}, refreshing`);
|
||||
console.log('Stopping server and reloading config');
|
||||
|
||||
running.server.shutdown(async () => {
|
||||
const restarted = await start(opts);
|
||||
if (!isLight) {
|
||||
serve_rendered.clear(running.serving.rendered);
|
||||
}
|
||||
running.server = restarted.server;
|
||||
running.app = restarted.app;
|
||||
running.startupPromise = restarted.startupPromise;
|
||||
running.serving = restarted.serving;
|
||||
});
|
||||
});
|
||||
return running;
|
||||
}
|
||||
|
|
423
src/utils.js
|
@ -1,39 +1,430 @@
|
|||
'use strict';
|
||||
|
||||
module.exports.getTileUrls = function(req, domains, path, format) {
|
||||
import path from 'path';
|
||||
import fsPromises from 'fs/promises';
|
||||
import fs from 'node:fs';
|
||||
import clone from 'clone';
|
||||
import { combine } from '@jsse/pbfont';
|
||||
import { existsP } from './promises.js';
|
||||
import { getPMtilesTile } from './pmtiles_adapter.js';
|
||||
|
||||
export const allowedSpriteFormats = allowedOptions(['png', 'json']);
|
||||
|
||||
export const allowedTileSizes = allowedOptions(['256', '512']);
|
||||
|
||||
/**
|
||||
* Restrict user input to an allowed set of options.
|
||||
* @param {string[]} opts - An array of allowed option strings.
|
||||
* @param {object} [config] - Optional configuration object.
|
||||
* @param {string} [config.defaultValue] - The default value to return if input doesn't match.
|
||||
* @returns {function(string): string} - A function that takes a value and returns it if valid or a default.
|
||||
*/
|
||||
export function allowedOptions(opts, { defaultValue } = {}) {
|
||||
const values = Object.fromEntries(opts.map((key) => [key, key]));
|
||||
return (value) => values[value] || defaultValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a scale string to a number.
|
||||
* @param {string} scale The scale string (e.g., '2x', '4x').
|
||||
* @param {number} maxScale Maximum allowed scale digit.
|
||||
* @returns {number|null} The parsed scale as a number or null if invalid.
|
||||
*/
|
||||
export function allowedScales(scale, maxScale = 9) {
|
||||
if (scale === undefined) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line security/detect-non-literal-regexp
|
||||
const regex = new RegExp(`^[2-${maxScale}]x$`);
|
||||
if (!regex.test(scale)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return parseInt(scale.slice(0, -1), 10);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a string is a valid sprite scale and returns it if it is within the allowed range, and null if it does not conform.
|
||||
* @param {string} scale - The scale string to validate (e.g., '2x', '3x').
|
||||
* @param {number} [maxScale] - The maximum scale value. If no value is passed in, it defaults to a value of 3.
|
||||
* @returns {string|null} - The valid scale string or null if invalid.
|
||||
*/
|
||||
export function allowedSpriteScales(scale, maxScale = 3) {
|
||||
if (!scale) {
|
||||
return '';
|
||||
}
|
||||
const match = scale?.match(/^([2-9]\d*)x$/);
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
const parsedScale = parseInt(match[1], 10);
|
||||
if (parsedScale <= maxScale) {
|
||||
return `@${parsedScale}x`;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces local:// URLs with public http(s):// URLs.
|
||||
* @param {object} req - Express request object.
|
||||
* @param {string} url - The URL string to fix.
|
||||
* @param {string} publicUrl - The public URL prefix to use for replacements.
|
||||
* @returns {string} - The fixed URL string.
|
||||
*/
|
||||
export function fixUrl(req, url, publicUrl) {
|
||||
if (!url || typeof url !== 'string' || url.indexOf('local://') !== 0) {
|
||||
return url;
|
||||
}
|
||||
const queryParams = [];
|
||||
if (req.query.key) {
|
||||
queryParams.unshift(`key=${encodeURIComponent(req.query.key)}`);
|
||||
}
|
||||
let query = '';
|
||||
if (queryParams.length) {
|
||||
query = `?${queryParams.join('&')}`;
|
||||
}
|
||||
return url.replace('local://', getPublicUrl(publicUrl, req)) + query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a new URL object from the Express request.
|
||||
* @param {object} req - Express request object.
|
||||
* @returns {URL} - URL object with correct host and optionally path.
|
||||
*/
|
||||
function getUrlObject(req) {
|
||||
const urlObject = new URL(`${req.protocol}://${req.headers.host}/`);
|
||||
// support overriding hostname by sending X-Forwarded-Host http header
|
||||
urlObject.hostname = req.hostname;
|
||||
|
||||
// support overriding port by sending X-Forwarded-Port http header
|
||||
const xForwardedPort = req.get('X-Forwarded-Port');
|
||||
if (xForwardedPort) {
|
||||
urlObject.port = xForwardedPort;
|
||||
}
|
||||
|
||||
// support add url prefix by sending X-Forwarded-Path http header
|
||||
const xForwardedPath = req.get('X-Forwarded-Path');
|
||||
if (xForwardedPath) {
|
||||
urlObject.pathname = path.posix.join(xForwardedPath, urlObject.pathname);
|
||||
}
|
||||
return urlObject;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the public URL, either from a provided publicUrl or generated from the request.
|
||||
* @param {string} publicUrl - The optional public URL to use.
|
||||
* @param {object} req - The Express request object.
|
||||
* @returns {string} - The final public URL string.
|
||||
*/
|
||||
export function getPublicUrl(publicUrl, req) {
|
||||
if (publicUrl) {
|
||||
return publicUrl;
|
||||
}
|
||||
return getUrlObject(req).toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an array of tile URLs based on given parameters.
|
||||
* @param {object} req - Express request object.
|
||||
* @param {string | string[]} domains - Domain(s) to use for tile URLs.
|
||||
* @param {string} path - The base path for the tiles.
|
||||
* @param {number} [tileSize] - The size of the tile (optional).
|
||||
* @param {string} format - The format of the tiles (e.g., 'png', 'jpg').
|
||||
* @param {string} publicUrl - The public URL to use (if not using domains).
|
||||
* @param {object} [aliases] - Aliases for format extensions.
|
||||
* @returns {string[]} An array of tile URL strings.
|
||||
*/
|
||||
export function getTileUrls(
|
||||
req,
|
||||
domains,
|
||||
path,
|
||||
tileSize,
|
||||
format,
|
||||
publicUrl,
|
||||
aliases,
|
||||
) {
|
||||
const urlObject = getUrlObject(req);
|
||||
if (domains) {
|
||||
if (domains.constructor === String && domains.length > 0) {
|
||||
domains = domains.split(',');
|
||||
}
|
||||
const hostParts = urlObject.host.split('.');
|
||||
const relativeSubdomainsUsable =
|
||||
hostParts.length > 1 &&
|
||||
!/^([0-9]{1,3}\.){3}[0-9]{1,3}(\:[0-9]+)?$/.test(urlObject.host);
|
||||
const newDomains = [];
|
||||
for (const domain of domains) {
|
||||
if (domain.indexOf('*') !== -1) {
|
||||
if (relativeSubdomainsUsable) {
|
||||
const newParts = hostParts.slice(1);
|
||||
newParts.unshift(domain.replace('*', hostParts[0]));
|
||||
newDomains.push(newParts.join('.'));
|
||||
}
|
||||
} else {
|
||||
newDomains.push(domain);
|
||||
}
|
||||
}
|
||||
domains = newDomains;
|
||||
}
|
||||
if (!domains || domains.length == 0) {
|
||||
domains = [req.headers.host];
|
||||
domains = [urlObject.host];
|
||||
}
|
||||
|
||||
var key = req.query.key;
|
||||
var query = (key && key.length > 0) ? ('?key=' + key) : '';
|
||||
const queryParams = [];
|
||||
if (req.query.key) {
|
||||
queryParams.push(`key=${encodeURIComponent(req.query.key)}`);
|
||||
}
|
||||
if (req.query.style) {
|
||||
queryParams.push(`style=${encodeURIComponent(req.query.style)}`);
|
||||
}
|
||||
const query = queryParams.length > 0 ? `?${queryParams.join('&')}` : '';
|
||||
|
||||
var uris = [];
|
||||
domains.forEach(function(domain) {
|
||||
uris.push(req.protocol + '://' + domain + '/' + path +
|
||||
'/{z}/{x}/{y}.' + format + query);
|
||||
});
|
||||
if (aliases && aliases[format]) {
|
||||
format = aliases[format];
|
||||
}
|
||||
|
||||
let tileParams = `{z}/{x}/{y}`;
|
||||
if (tileSize && ['png', 'jpg', 'jpeg', 'webp'].includes(format)) {
|
||||
tileParams = `${tileSize}/{z}/{x}/{y}`;
|
||||
}
|
||||
|
||||
if (format && format != '') {
|
||||
format = `.${format}`;
|
||||
} else {
|
||||
format = '';
|
||||
}
|
||||
|
||||
const uris = [];
|
||||
if (!publicUrl) {
|
||||
let xForwardedPath = `${req.get('X-Forwarded-Path') ? '/' + req.get('X-Forwarded-Path') : ''}`;
|
||||
for (const domain of domains) {
|
||||
uris.push(
|
||||
`${req.protocol}://${domain}${xForwardedPath}/${path}/${tileParams}${format}${query}`,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
uris.push(`${publicUrl}${path}/${tileParams}${format}${query}`);
|
||||
}
|
||||
|
||||
return uris;
|
||||
};
|
||||
}
|
||||
|
||||
module.exports.fixTileJSONCenter = function(tileJSON) {
|
||||
/**
|
||||
* Fixes the center in the tileJSON if no center is available.
|
||||
* @param {object} tileJSON - The tileJSON object to process.
|
||||
* @returns {void}
|
||||
*/
|
||||
export function fixTileJSONCenter(tileJSON) {
|
||||
if (tileJSON.bounds && !tileJSON.center) {
|
||||
var fitWidth = 1024;
|
||||
var tiles = fitWidth / 256;
|
||||
const fitWidth = 1024;
|
||||
const tiles = fitWidth / 256;
|
||||
tileJSON.center = [
|
||||
(tileJSON.bounds[0] + tileJSON.bounds[2]) / 2,
|
||||
(tileJSON.bounds[1] + tileJSON.bounds[3]) / 2,
|
||||
Math.round(
|
||||
-Math.log((tileJSON.bounds[2] - tileJSON.bounds[0]) / 360 / tiles) /
|
||||
Math.LN2
|
||||
)
|
||||
Math.LN2,
|
||||
),
|
||||
];
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a file and returns a Promise with the file data.
|
||||
* @param {string} filename - Path to the file to read.
|
||||
* @returns {Promise<Buffer>} - A Promise that resolves with the file data as a Buffer or rejects with an error.
|
||||
*/
|
||||
export function readFile(filename) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const sanitizedFilename = path.normalize(filename); // Normalize path, remove ..
|
||||
// eslint-disable-next-line security/detect-non-literal-fs-filename
|
||||
fs.readFile(String(sanitizedFilename), (err, data) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(data);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves font data for a given font and range.
|
||||
* @param {object} allowedFonts - An object of allowed fonts.
|
||||
* @param {string} fontPath - The path to the font directory.
|
||||
* @param {string} name - The name of the font.
|
||||
* @param {string} range - The range (e.g., '0-255') of the font to load.
|
||||
* @param {object} [fallbacks] - Optional fallback font list.
|
||||
* @returns {Promise<Buffer>} A promise that resolves with the font data Buffer or rejects with an error.
|
||||
*/
|
||||
async function getFontPbf(allowedFonts, fontPath, name, range, fallbacks) {
|
||||
if (!allowedFonts || (allowedFonts[name] && fallbacks)) {
|
||||
const fontMatch = name?.match(/^[\p{L}\p{N} \-\.~!*'()@&=+,#$\[\]]+$/u);
|
||||
const sanitizedName = fontMatch?.[0] || 'invalid';
|
||||
if (!name || typeof name !== 'string' || name.trim() === '' || !fontMatch) {
|
||||
console.error(
|
||||
'ERROR: Invalid font name: %s',
|
||||
sanitizedName.replace(/\n|\r/g, ''),
|
||||
);
|
||||
throw new Error('Invalid font name');
|
||||
}
|
||||
|
||||
const rangeMatch = range?.match(/^[\d-]+$/);
|
||||
const sanitizedRange = rangeMatch?.[0] || 'invalid';
|
||||
if (!/^\d+-\d+$/.test(range)) {
|
||||
console.error(
|
||||
'ERROR: Invalid range: %s',
|
||||
sanitizedRange.replace(/\n|\r/g, ''),
|
||||
);
|
||||
throw new Error('Invalid range');
|
||||
}
|
||||
const filename = path.join(
|
||||
fontPath,
|
||||
sanitizedName,
|
||||
`${sanitizedRange}.pbf`,
|
||||
);
|
||||
|
||||
if (!fallbacks) {
|
||||
fallbacks = clone(allowedFonts || {});
|
||||
}
|
||||
delete fallbacks[name];
|
||||
|
||||
try {
|
||||
const data = await readFile(filename);
|
||||
return data;
|
||||
} catch (err) {
|
||||
console.error(
|
||||
'ERROR: Font not found: %s, Error: %s',
|
||||
filename.replace(/\n|\r/g, ''),
|
||||
String(err),
|
||||
);
|
||||
if (fallbacks && Object.keys(fallbacks).length) {
|
||||
let fallbackName;
|
||||
|
||||
let fontStyle = name.split(' ').pop();
|
||||
if (['Regular', 'Bold', 'Italic'].indexOf(fontStyle) < 0) {
|
||||
fontStyle = 'Regular';
|
||||
}
|
||||
fallbackName = `Noto Sans ${fontStyle}`;
|
||||
if (!fallbacks[fallbackName]) {
|
||||
fallbackName = `Open Sans ${fontStyle}`;
|
||||
if (!fallbacks[fallbackName]) {
|
||||
fallbackName = Object.keys(fallbacks)[0];
|
||||
}
|
||||
}
|
||||
console.error(
|
||||
`ERROR: Trying to use %s as a fallback for: %s`,
|
||||
fallbackName,
|
||||
sanitizedName,
|
||||
);
|
||||
delete fallbacks[fallbackName];
|
||||
return getFontPbf(null, fontPath, fallbackName, range, fallbacks);
|
||||
} else {
|
||||
throw new Error('Font load error');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw new Error('Font not allowed');
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Combines multiple font pbf buffers into one.
|
||||
* @param {object} allowedFonts - An object of allowed fonts.
|
||||
* @param {string} fontPath - The path to the font directory.
|
||||
* @param {string} names - Comma-separated font names.
|
||||
* @param {string} range - The range of the font (e.g., '0-255').
|
||||
* @param {object} [fallbacks] - Fallback font list.
|
||||
* @returns {Promise<Buffer>} - A promise that resolves to the combined font data buffer.
|
||||
*/
|
||||
export async function getFontsPbf(
|
||||
allowedFonts,
|
||||
fontPath,
|
||||
names,
|
||||
range,
|
||||
fallbacks,
|
||||
) {
|
||||
const fonts = names.split(',');
|
||||
const queue = [];
|
||||
for (const font of fonts) {
|
||||
queue.push(
|
||||
getFontPbf(
|
||||
allowedFonts,
|
||||
fontPath,
|
||||
font,
|
||||
range,
|
||||
clone(allowedFonts || fallbacks),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
const combined = combine(await Promise.all(queue), names);
|
||||
return Buffer.from(combined.buffer, 0, combined.buffer.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Lists available fonts in a given font directory.
|
||||
* @param {string} fontPath - The path to the font directory.
|
||||
* @returns {Promise<object>} - Promise that resolves with an object where keys are the font names.
|
||||
*/
|
||||
export async function listFonts(fontPath) {
|
||||
const existingFonts = {};
|
||||
|
||||
const files = await fsPromises.readdir(fontPath);
|
||||
for (const file of files) {
|
||||
const stats = await fsPromises.stat(path.join(fontPath, file));
|
||||
if (
|
||||
stats.isDirectory() &&
|
||||
(await existsP(path.join(fontPath, file, '0-255.pbf')))
|
||||
) {
|
||||
existingFonts[path.basename(file)] = true;
|
||||
}
|
||||
}
|
||||
|
||||
return existingFonts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a string is a valid HTTP or HTTPS URL.
|
||||
* @param {string} string - The string to validate.
|
||||
* @returns {boolean} True if the string is a valid HTTP/HTTPS URL, false otherwise.
|
||||
*/
|
||||
export function isValidHttpUrl(string) {
|
||||
let url;
|
||||
|
||||
try {
|
||||
url = new URL(string);
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return url.protocol === 'http:' || url.protocol === 'https:';
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches tile data from either PMTiles or MBTiles source.
|
||||
* @param {object} source - The source object, which may contain a mbtiles object, or pmtiles object.
|
||||
* @param {string} sourceType - The source type, which should be `pmtiles` or `mbtiles`
|
||||
* @param {number} z - The zoom level.
|
||||
* @param {number} x - The x coordinate of the tile.
|
||||
* @param {number} y - The y coordinate of the tile.
|
||||
* @returns {Promise<object | null>} - A promise that resolves to an object with data and headers or null if no data is found.
|
||||
*/
|
||||
export async function fetchTileData(source, sourceType, z, x, y) {
|
||||
if (sourceType === 'pmtiles') {
|
||||
return await new Promise(async (resolve) => {
|
||||
const tileinfo = await getPMtilesTile(source, z, x, y);
|
||||
if (!tileinfo?.data) return resolve(null);
|
||||
resolve({ data: tileinfo.data, headers: tileinfo.header });
|
||||
});
|
||||
} else if (sourceType === 'mbtiles') {
|
||||
return await new Promise((resolve) => {
|
||||
source.getTile(z, x, y, (err, tileData, tileHeader) => {
|
||||
if (err) {
|
||||
return resolve(null);
|
||||
}
|
||||
resolve({ data: tileData, headers: tileHeader });
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,69 +1,77 @@
|
|||
var testTileJSONArray = function(url) {
|
||||
describe(url + ' is array of TileJSONs', function() {
|
||||
it('is json', function(done) {
|
||||
const testTileJSONArray = function (url) {
|
||||
describe(url + ' is array of TileJSONs', function () {
|
||||
it('is json', function (done) {
|
||||
supertest(app)
|
||||
.get(url)
|
||||
.expect(200)
|
||||
.expect('Content-Type', /application\/json/, done);
|
||||
});
|
||||
|
||||
it('is non-empty array', function(done) {
|
||||
it('is non-empty array', function (done) {
|
||||
supertest(app)
|
||||
.get(url)
|
||||
.expect(function(res) {
|
||||
res.body.should.be.Array();
|
||||
res.body.length.should.be.greaterThan(0);
|
||||
}).end(done);
|
||||
.expect(function (res) {
|
||||
expect(res.body).to.be.a('array');
|
||||
expect(res.body.length).to.be.greaterThan(0);
|
||||
})
|
||||
.end(done);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
var testTileJSON = function(url, basename) {
|
||||
describe(url + ' is TileJSON', function() {
|
||||
it('is json', function(done) {
|
||||
const testTileJSON = function (url) {
|
||||
describe(url + ' is TileJSON', function () {
|
||||
it('is json', function (done) {
|
||||
supertest(app)
|
||||
.get(url)
|
||||
.expect(200)
|
||||
.expect('Content-Type', /application\/json/, done);
|
||||
});
|
||||
|
||||
it('has valid basename and tiles', function(done) {
|
||||
it('has valid tiles', function (done) {
|
||||
supertest(app)
|
||||
.get(url)
|
||||
.expect(function(res) {
|
||||
res.body.basename.should.equal(basename);
|
||||
res.body.tiles.length.should.be.greaterThan(0);
|
||||
}).end(done);
|
||||
.expect(function (res) {
|
||||
expect(res.body.tiles.length).to.be.greaterThan(0);
|
||||
})
|
||||
.end(done);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
describe('Metadata', function() {
|
||||
describe('Metadata', function () {
|
||||
describe('/health', function () {
|
||||
it('returns 200', function (done) {
|
||||
supertest(app).get('/health').expect(200, done);
|
||||
});
|
||||
});
|
||||
|
||||
testTileJSONArray('/index.json');
|
||||
testTileJSONArray('/rendered.json');
|
||||
testTileJSONArray('/data.json');
|
||||
|
||||
describe('/styles.json is valid array', function() {
|
||||
it('is json', function(done) {
|
||||
describe('/styles.json is valid array', function () {
|
||||
it('is json', function (done) {
|
||||
supertest(app)
|
||||
.get('/styles.json')
|
||||
.expect(200)
|
||||
.expect('Content-Type', /application\/json/, done);
|
||||
});
|
||||
|
||||
it('contains valid item', function(done) {
|
||||
it('contains valid item', function (done) {
|
||||
supertest(app)
|
||||
.get('/styles.json')
|
||||
.expect(function(res) {
|
||||
res.body.should.be.Array();
|
||||
res.body.length.should.be.greaterThan(0);
|
||||
res.body[0].version.should.equal(8);
|
||||
res.body[0].id.should.be.String();
|
||||
res.body[0].name.should.be.String();
|
||||
}).end(done);
|
||||
.expect(function (res) {
|
||||
expect(res.body).to.be.a('array');
|
||||
expect(res.body.length).to.be.greaterThan(0);
|
||||
expect(res.body[0].version).to.be.equal(8);
|
||||
expect(res.body[0].id).to.be.a('string');
|
||||
expect(res.body[0].name).to.be.a('string');
|
||||
})
|
||||
.end(done);
|
||||
});
|
||||
});
|
||||
|
||||
testTileJSON('/styles/test/rendered.json', 'test');
|
||||
testTileJSON('/data/zurich-vector.json', 'zurich-vector');
|
||||
testTileJSON('/styles/test-style.json');
|
||||
testTileJSON('/data/openmaptiles.json');
|
||||
});
|
||||
|
|
|
@ -1,20 +1,28 @@
|
|||
process.env.NODE_ENV = 'test';
|
||||
|
||||
global.should = require('should');
|
||||
global.supertest = require('supertest');
|
||||
import { expect } from 'chai';
|
||||
import supertest from 'supertest';
|
||||
import { server } from '../src/server.js';
|
||||
|
||||
before(function() {
|
||||
global.expect = expect;
|
||||
global.supertest = supertest;
|
||||
|
||||
before(async function () {
|
||||
console.log('global setup');
|
||||
process.chdir('test_data');
|
||||
var running = require('../src/server')({
|
||||
config: 'config.json',
|
||||
port: 8888
|
||||
const running = await server({
|
||||
configPath: 'config.json',
|
||||
port: 8888,
|
||||
publicUrl: '/test/',
|
||||
});
|
||||
global.app = running.app;
|
||||
global.server = running.server;
|
||||
return running.startupPromise;
|
||||
});
|
||||
|
||||
after(function() {
|
||||
after(function () {
|
||||
console.log('global teardown');
|
||||
global.server.close(function() { console.log('Done'); });
|
||||
global.server.close(function () {
|
||||
console.log('Done');
|
||||
});
|
||||
});
|
||||
|
|
230
test/static.js
|
@ -1,80 +1,210 @@
|
|||
var testStatic = function(prefix, q, format, status, scale, type) {
|
||||
const testStatic = function (prefix, q, format, status, scale, type, query) {
|
||||
if (scale) q += '@' + scale + 'x';
|
||||
var path = '/styles/' + prefix + '/static/' + q + '.' + format;
|
||||
it(path + ' returns ' + status, function(done) {
|
||||
var test = supertest(app).get(path);
|
||||
let path = '/styles/' + prefix + '/static/' + q + '.' + format;
|
||||
if (query) {
|
||||
path += query;
|
||||
}
|
||||
it(path + ' returns ' + status, function (done) {
|
||||
const test = supertest(app).get(path);
|
||||
if (status) test.expect(status);
|
||||
if (type) test.expect('Content-Type', type);
|
||||
test.end(done);
|
||||
});
|
||||
};
|
||||
|
||||
describe('Static endpoints', function() {
|
||||
describe('center-based', function() {
|
||||
describe('valid requests', function() {
|
||||
describe('various formats', function() {
|
||||
testStatic('test', '0,0,0/256x256', 'png', 200, undefined, /image\/png/);
|
||||
testStatic('test', '0,0,0/256x256', 'jpg', 200, undefined, /image\/jpeg/);
|
||||
testStatic('test', '0,0,0/256x256', 'jpeg', 200, undefined, /image\/jpeg/);
|
||||
testStatic('test', '0,0,0/256x256', 'webp', 200, undefined, /image\/webp/);
|
||||
const prefix = 'test-style';
|
||||
|
||||
describe('Static endpoints', function () {
|
||||
describe('center-based', function () {
|
||||
describe('valid requests', function () {
|
||||
describe('various formats', function () {
|
||||
testStatic(
|
||||
prefix,
|
||||
'0,0,0/256x256',
|
||||
'png',
|
||||
200,
|
||||
undefined,
|
||||
/image\/png/,
|
||||
);
|
||||
testStatic(
|
||||
prefix,
|
||||
'0,0,0/256x256',
|
||||
'jpg',
|
||||
200,
|
||||
undefined,
|
||||
/image\/jpeg/,
|
||||
);
|
||||
testStatic(
|
||||
prefix,
|
||||
'0,0,0/256x256',
|
||||
'jpeg',
|
||||
200,
|
||||
undefined,
|
||||
/image\/jpeg/,
|
||||
);
|
||||
testStatic(
|
||||
prefix,
|
||||
'0,0,0/256x256',
|
||||
'webp',
|
||||
200,
|
||||
undefined,
|
||||
/image\/webp/,
|
||||
);
|
||||
});
|
||||
|
||||
describe('different parameters', function() {
|
||||
testStatic('test', '0,0,0/300x300', 'png', 200, 2);
|
||||
testStatic('test', '0,0,0/300x300', 'png', 200, 3);
|
||||
describe('different parameters', function () {
|
||||
testStatic(prefix, '0,0,0/300x300', 'png', 200, 2);
|
||||
testStatic(prefix, '0,0,0/300x300', 'png', 200, 3);
|
||||
|
||||
testStatic('test', '80,40,20/600x300', 'png', 200, 3);
|
||||
testStatic('test', '8.5,40.5,20/300x150', 'png', 200, 3);
|
||||
testStatic('test', '-8.5,-40.5,20/300x150', 'png', 200, 3);
|
||||
testStatic(prefix, '0,0,1.5/256x256', 'png', 200);
|
||||
|
||||
testStatic('test', '8,40,2,0,0/300x150', 'png', 200);
|
||||
testStatic('test', '8,40,2,180,45/300x150', 'png', 200, 2);
|
||||
testStatic('test', '8,40,2,10/300x150', 'png', 200, 3);
|
||||
testStatic('test', '8,40,2,10.3,20.4/300x300', 'png', 200);
|
||||
testStatic('test', '0,0,2,390,120/300x300', 'png', 200);
|
||||
testStatic(prefix, '80,40,20/600x300', 'png', 200, 3);
|
||||
testStatic(prefix, '8.5,40.5,20/300x150', 'png', 200, 3);
|
||||
testStatic(prefix, '-8.5,-40.5,20/300x150', 'png', 200, 3);
|
||||
|
||||
testStatic(prefix, '8,40,2@0,0/300x150', 'png', 200);
|
||||
testStatic(prefix, '8,40,2@180,45/300x150', 'png', 200, 2);
|
||||
testStatic(prefix, '8,40,2@10/300x150', 'png', 200, 3);
|
||||
testStatic(prefix, '8,40,2@10.3,20.4/300x300', 'png', 200);
|
||||
testStatic(prefix, '0,0,2@390,120/300x300', 'png', 200);
|
||||
});
|
||||
});
|
||||
|
||||
describe('invalid requests return 4xx', function() {
|
||||
testStatic('test', '190,0,0/256x256', 'png', 400);
|
||||
testStatic('test', '0,86,0/256x256', 'png', 400);
|
||||
testStatic('test', '80,40,20/0x0', 'png', 400);
|
||||
testStatic('test', '0,0,0/256x256', 'gif', 400);
|
||||
testStatic('test', '0,0,0/256x256', 'png', 404, 1);
|
||||
describe('invalid requests return 4xx', function () {
|
||||
testStatic(prefix, '190,0,0/256x256', 'png', 400);
|
||||
testStatic(prefix, '0,86,0/256x256', 'png', 400);
|
||||
testStatic(prefix, '80,40,20/0x0', 'png', 400);
|
||||
testStatic(prefix, '0,0,0/256x256', 'gif', 400);
|
||||
testStatic(prefix, '0,0,0/256x256', 'png', 404, 1);
|
||||
|
||||
testStatic('test', '0,0,-1/256x256', 'png', 404);
|
||||
testStatic('test', '0,0,1.5/256x256', 'png', 404);
|
||||
testStatic('test', '0,0,0/256.5x256.5', 'png', 404);
|
||||
testStatic(prefix, '0,0,-1/256x256', 'png', 404);
|
||||
testStatic(prefix, '0,0,0/256.5x256.5', 'png', 400);
|
||||
|
||||
testStatic('test', '0,0,0,/256x256', 'png', 404);
|
||||
testStatic('test', '0,0,0,0,/256x256', 'png', 404);
|
||||
testStatic(prefix, '0,0,0,/256x256', 'png', 404);
|
||||
testStatic(prefix, '0,0,0,0,/256x256', 'png', 404);
|
||||
});
|
||||
});
|
||||
|
||||
describe('area-based', function() {
|
||||
describe('valid requests', function() {
|
||||
describe('various formats', function() {
|
||||
testStatic('test', '-180,-80,180,80/0', 'png', 200, undefined, /image\/png/);
|
||||
testStatic('test', '-180,-80,180,80/0', 'jpg', 200, undefined, /image\/jpeg/);
|
||||
testStatic('test', '-180,-80,180,80/0', 'jpeg', 200, undefined, /image\/jpeg/);
|
||||
testStatic('test', '-180,-80,180,80/0', 'webp', 200, undefined, /image\/webp/);
|
||||
describe('area-based', function () {
|
||||
describe('valid requests', function () {
|
||||
describe('various formats', function () {
|
||||
testStatic(
|
||||
prefix,
|
||||
'-180,-80,180,80/10x10',
|
||||
'png',
|
||||
200,
|
||||
undefined,
|
||||
/image\/png/,
|
||||
);
|
||||
testStatic(
|
||||
prefix,
|
||||
'-180,-80,180,80/10x10',
|
||||
'jpg',
|
||||
200,
|
||||
undefined,
|
||||
/image\/jpeg/,
|
||||
);
|
||||
testStatic(
|
||||
prefix,
|
||||
'-180,-80,180,80/10x10',
|
||||
'jpeg',
|
||||
200,
|
||||
undefined,
|
||||
/image\/jpeg/,
|
||||
);
|
||||
testStatic(
|
||||
prefix,
|
||||
'-180,-80,180,80/10x10',
|
||||
'webp',
|
||||
200,
|
||||
undefined,
|
||||
/image\/webp/,
|
||||
);
|
||||
});
|
||||
|
||||
describe('different parameters', function() {
|
||||
testStatic('test', '-180,-90,180,90/0', 'png', 200, 2);
|
||||
testStatic('test', '0,0,1,1/3', 'png', 200, 3);
|
||||
describe('different parameters', function () {
|
||||
testStatic(prefix, '-180,-90,180,90/20x20', 'png', 200, 2);
|
||||
testStatic(prefix, '0,0,1,1/200x200', 'png', 200, 3);
|
||||
|
||||
testStatic('test', '-280,-80,0,80/0', 'png', 200);
|
||||
testStatic(prefix, '-280,-80,0,80/280x160', 'png', 200);
|
||||
});
|
||||
});
|
||||
|
||||
describe('invalid requests return 4xx', function() {
|
||||
testStatic('test', '0,87,1,88/5', 'png', 400);
|
||||
describe('invalid requests return 4xx', function () {
|
||||
testStatic(prefix, '0,87,1,88/5x2', 'png', 400);
|
||||
|
||||
testStatic('test', '18,-9,-18,9/0', 'png', 400);
|
||||
testStatic('test', '0,0,1,1/1', 'gif', 400);
|
||||
testStatic(prefix, '0,0,1,1/1x1', 'gif', 400);
|
||||
|
||||
testStatic('test', '-180,-80,180,80/0.5', 'png', 404);
|
||||
testStatic(prefix, '-180,-80,180,80/0.5x2.6', 'png', 400);
|
||||
});
|
||||
});
|
||||
|
||||
describe('autofit path', function () {
|
||||
describe('valid requests', function () {
|
||||
testStatic(
|
||||
prefix,
|
||||
'auto/256x256',
|
||||
'png',
|
||||
200,
|
||||
undefined,
|
||||
/image\/png/,
|
||||
'?path=10,10|20,20',
|
||||
);
|
||||
|
||||
describe('different parameters', function () {
|
||||
testStatic(
|
||||
prefix,
|
||||
'auto/20x20',
|
||||
'png',
|
||||
200,
|
||||
2,
|
||||
/image\/png/,
|
||||
'?path=10,10|20,20',
|
||||
);
|
||||
testStatic(
|
||||
prefix,
|
||||
'auto/200x200',
|
||||
'png',
|
||||
200,
|
||||
3,
|
||||
/image\/png/,
|
||||
'?path=-10,-10|-20,-20',
|
||||
);
|
||||
});
|
||||
|
||||
describe('encoded path', function () {
|
||||
testStatic(
|
||||
prefix,
|
||||
'auto/20x20',
|
||||
'png',
|
||||
200,
|
||||
2,
|
||||
/image\/png/,
|
||||
'?path=' + encodeURIComponent('enc:{{biGwvyGoUi@s_A|{@'),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('invalid requests return 4xx', function () {
|
||||
testStatic(prefix, 'auto/256x256', 'png', 400);
|
||||
testStatic(
|
||||
prefix,
|
||||
'auto/256x256',
|
||||
'png',
|
||||
400,
|
||||
undefined,
|
||||
undefined,
|
||||
'?path=invalid',
|
||||
);
|
||||
testStatic(
|
||||
prefix,
|
||||
'auto/2560x2560',
|
||||
'png',
|
||||
400,
|
||||
undefined,
|
||||
undefined,
|
||||
'?path=10,10|20,20',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,48 +1,66 @@
|
|||
var testIs = function(url, type, status) {
|
||||
it(url + ' return ' + (status || 200) + ' and is ' + type.toString(),
|
||||
function(done) {
|
||||
supertest(app)
|
||||
.get(url)
|
||||
.expect(status || 200)
|
||||
.expect('Content-Type', type, done);
|
||||
});
|
||||
const testIs = function (url, type, status) {
|
||||
it(
|
||||
url + ' return ' + (status || 200) + ' and is ' + type.toString(),
|
||||
function (done) {
|
||||
supertest(app)
|
||||
.get(url)
|
||||
.expect(status || 200)
|
||||
.expect('Content-Type', type, done);
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
describe('Styles', function() {
|
||||
describe('/styles/test.json is valid style', function() {
|
||||
testIs('/styles/test.json', /application\/json/);
|
||||
const prefix = 'test-style';
|
||||
|
||||
it('contains expected properties', function(done) {
|
||||
describe('Styles', function () {
|
||||
describe('/styles/' + prefix + '/style.json is valid style', function () {
|
||||
testIs('/styles/' + prefix + '/style.json', /application\/json/);
|
||||
|
||||
it('contains expected properties', function (done) {
|
||||
supertest(app)
|
||||
.get('/styles/test.json')
|
||||
.expect(function(res) {
|
||||
res.body.version.should.equal(8);
|
||||
res.body.name.should.be.String();
|
||||
res.body.sources.should.be.Object();
|
||||
res.body.glyphs.should.be.String();
|
||||
res.body.sprite.should.be.String();
|
||||
res.body.layers.should.be.Array();
|
||||
}).end(done);
|
||||
.get('/styles/' + prefix + '/style.json')
|
||||
.expect(function (res) {
|
||||
expect(res.body.version).to.be.equal(8);
|
||||
expect(res.body.name).to.be.a('string');
|
||||
expect(res.body.sources).to.be.a('object');
|
||||
expect(res.body.glyphs).to.be.a('string');
|
||||
expect(res.body.sprite).to.be.a('string');
|
||||
expect(res.body.sprite).to.be.equal('/test/styles/test-style/sprite');
|
||||
expect(res.body.layers).to.be.a('array');
|
||||
})
|
||||
.end(done);
|
||||
});
|
||||
});
|
||||
describe('/styles/streets.json is not served', function() {
|
||||
testIs('/styles/streets.json', /./, 404);
|
||||
describe('/styles/streets/style.json is not served', function () {
|
||||
testIs('/styles/streets/style.json', /./, 404);
|
||||
});
|
||||
|
||||
describe('/styles/test/sprite[@2x].{format}', function() {
|
||||
testIs('/styles/test/sprite.json', /application\/json/);
|
||||
testIs('/styles/test/sprite@2x.json', /application\/json/);
|
||||
testIs('/styles/test/sprite.png', /image\/png/);
|
||||
testIs('/styles/test/sprite@2x.png', /image\/png/);
|
||||
describe('/styles/' + prefix + '/sprite[@2x].{format}', function () {
|
||||
testIs('/styles/' + prefix + '/sprite.json', /application\/json/);
|
||||
testIs('/styles/' + prefix + '/sprite@2x.json', /application\/json/);
|
||||
testIs('/styles/' + prefix + '/sprite.png', /image\/png/);
|
||||
testIs('/styles/' + prefix + '/sprite@2x.png', /image\/png/);
|
||||
});
|
||||
|
||||
describe('/styles/' + prefix + '/sprite/default[@2x].{format}', function () {
|
||||
testIs('/styles/' + prefix + '/sprite/default.json', /application\/json/);
|
||||
testIs(
|
||||
'/styles/' + prefix + '/sprite/default@2x.json',
|
||||
/application\/json/,
|
||||
);
|
||||
testIs('/styles/' + prefix + '/sprite/default.png', /image\/png/);
|
||||
testIs('/styles/' + prefix + '/sprite/default@2x.png', /image\/png/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Fonts', function() {
|
||||
describe('Fonts', function () {
|
||||
testIs('/fonts/Open Sans Bold/0-255.pbf', /application\/x-protobuf/);
|
||||
testIs('/fonts/Open Sans Regular/65280-65533.pbf', /application\/x-protobuf/);
|
||||
testIs('/fonts/Open Sans Bold,Open Sans Regular/0-255.pbf',
|
||||
/application\/x-protobuf/);
|
||||
testIs('/fonts/Nonsense,Open Sans Bold/0-255.pbf', /application\/x-protobuf/);
|
||||
testIs('/fonts/Open Sans Regular/65280-65535.pbf', /application\/x-protobuf/);
|
||||
testIs(
|
||||
'/fonts/Open Sans Bold,Open Sans Regular/0-255.pbf',
|
||||
/application\/x-protobuf/,
|
||||
);
|
||||
testIs('/fonts/Nonsense,Open Sans Bold/0-255.pbf', /./, 400);
|
||||
|
||||
testIs('/fonts/Nonsense/0-255.pbf', /./, 400);
|
||||
testIs('/fonts/Nonsense1,Nonsense2/0-255.pbf', /./, 400);
|
||||
|
|
|
@ -1,28 +1,28 @@
|
|||
var testTile = function(prefix, z, x, y, status) {
|
||||
var path = '/data/' + prefix + '/' + z + '/' + x + '/' + y + '.pbf';
|
||||
it(path + ' returns ' + status, function(done) {
|
||||
var test = supertest(app).get(path);
|
||||
const testTile = function (prefix, z, x, y, status) {
|
||||
const path = '/data/' + prefix + '/' + z + '/' + x + '/' + y + '.pbf';
|
||||
it(path + ' returns ' + status, function (done) {
|
||||
const test = supertest(app).get(path);
|
||||
if (status) test.expect(status);
|
||||
if (status == 200) test.expect('Content-Type', /application\/x-protobuf/);
|
||||
test.end(done);
|
||||
});
|
||||
};
|
||||
|
||||
var prefix = 'zurich-vector';
|
||||
const prefix = 'openmaptiles';
|
||||
|
||||
describe('Vector tiles', function() {
|
||||
describe('existing tiles', function() {
|
||||
describe('Vector tiles', function () {
|
||||
describe('existing tiles', function () {
|
||||
testTile(prefix, 0, 0, 0, 200);
|
||||
testTile(prefix, 14, 8581, 5738, 200);
|
||||
});
|
||||
|
||||
describe('non-existent requests return 4xx', function() {
|
||||
describe('non-existent requests return 4xx', function () {
|
||||
testTile('non_existent', 0, 0, 0, 404);
|
||||
testTile(prefix, -1, 0, 0, 404); // err zoom
|
||||
testTile(prefix, 20, 0, 0, 404); // zoom out of bounds
|
||||
testTile(prefix, 0, 1, 0, 404);
|
||||
testTile(prefix, 0, 0, 1, 404);
|
||||
|
||||
testTile(prefix, 14, 0, 0, 404); // non existent tile
|
||||
testTile(prefix, 14, 0, 0, 204); // non existent tile
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,7 +1,29 @@
|
|||
var testTile = function(prefix, z, x, y, format, status, scale, type) {
|
||||
var testTile = function (
|
||||
prefix,
|
||||
tileSize = 256,
|
||||
z,
|
||||
x,
|
||||
y,
|
||||
format,
|
||||
status,
|
||||
scale,
|
||||
type,
|
||||
) {
|
||||
if (scale) y += '@' + scale + 'x';
|
||||
var path = '/styles/' + prefix + '/rendered/' + z + '/' + x + '/' + y + '.' + format;
|
||||
it(path + ' returns ' + status, function(done) {
|
||||
var path =
|
||||
'/styles/' +
|
||||
prefix +
|
||||
'/' +
|
||||
tileSize +
|
||||
'/' +
|
||||
z +
|
||||
'/' +
|
||||
x +
|
||||
'/' +
|
||||
y +
|
||||
'.' +
|
||||
format;
|
||||
it(path + ' returns ' + status, function (done) {
|
||||
var test = supertest(app).get(path);
|
||||
test.expect(status);
|
||||
if (type) test.expect('Content-Type', type);
|
||||
|
@ -9,36 +31,45 @@ var testTile = function(prefix, z, x, y, format, status, scale, type) {
|
|||
});
|
||||
};
|
||||
|
||||
describe('Raster tiles', function() {
|
||||
describe('valid requests', function() {
|
||||
describe('various formats', function() {
|
||||
testTile('test', 0, 0, 0, 'png', 200, undefined, /image\/png/);
|
||||
testTile('test', 0, 0, 0, 'jpg', 200, undefined, /image\/jpeg/);
|
||||
testTile('test', 0, 0, 0, 'jpeg', 200, undefined, /image\/jpeg/);
|
||||
testTile('test', 0, 0, 0, 'webp', 200, undefined, /image\/webp/);
|
||||
const prefix = 'test-style';
|
||||
|
||||
describe('Raster tiles', function () {
|
||||
describe('valid requests', function () {
|
||||
describe('various formats', function () {
|
||||
testTile(prefix, 256, 0, 0, 0, 'png', 200, undefined, /image\/png/);
|
||||
testTile(prefix, 512, 0, 0, 0, 'png', 200, undefined, /image\/png/);
|
||||
testTile(prefix, 256, 0, 0, 0, 'jpg', 200, undefined, /image\/jpeg/);
|
||||
testTile(prefix, 512, 0, 0, 0, 'jpg', 200, undefined, /image\/jpeg/);
|
||||
testTile(prefix, 256, 0, 0, 0, 'jpeg', 200, undefined, /image\/jpeg/);
|
||||
testTile(prefix, 512, 0, 0, 0, 'jpeg', 200, undefined, /image\/jpeg/);
|
||||
testTile(prefix, 256, 0, 0, 0, 'webp', 200, undefined, /image\/webp/);
|
||||
testTile(prefix, 512, 0, 0, 0, 'webp', 200, undefined, /image\/webp/);
|
||||
});
|
||||
|
||||
describe('different coordinates and scales', function() {
|
||||
testTile('test', 1, 1, 1, 'png', 200);
|
||||
|
||||
testTile('test', 0, 0, 0, 'png', 200, 2);
|
||||
testTile('test', 0, 0, 0, 'png', 200, 3);
|
||||
testTile('test', 2, 1, 1, 'png', 200, 3);
|
||||
describe('different coordinates and scales', function () {
|
||||
testTile(prefix, 256, 1, 0, 0, 'png', 200);
|
||||
testTile(prefix, 512, 1, 0, 0, 'png', 200);
|
||||
testTile(prefix, 256, 0, 0, 0, 'png', 200, 2);
|
||||
testTile(prefix, 512, 0, 0, 0, 'png', 200, 2);
|
||||
testTile(prefix, 256, 0, 0, 0, 'png', 200, 3);
|
||||
testTile(prefix, 512, 0, 0, 0, 'png', 200, 3);
|
||||
testTile(prefix, 256, 2, 1, 1, 'png', 200, 3);
|
||||
testTile(prefix, 512, 2, 1, 1, 'png', 200, 3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('invalid requests return 4xx', function() {
|
||||
testTile('non_existent', 0, 0, 0, 'png', 404);
|
||||
testTile('test', -1, 0, 0, 'png', 404);
|
||||
testTile('test', 25, 0, 0, 'png', 404);
|
||||
testTile('test', 0, 1, 0, 'png', 404);
|
||||
testTile('test', 0, 0, 1, 'png', 404);
|
||||
testTile('test', 0, 0, 0, 'gif', 400);
|
||||
testTile('test', 0, 0, 0, 'pbf', 400);
|
||||
describe('invalid requests return 4xx', function () {
|
||||
testTile('non_existent', 256, 0, 0, 0, 'png', 404);
|
||||
testTile(prefix, 256, -1, 0, 0, 'png', 400);
|
||||
testTile(prefix, 256, 25, 0, 0, 'png', 400);
|
||||
testTile(prefix, 256, 0, 1, 0, 'png', 400);
|
||||
testTile(prefix, 256, 0, 0, 1, 'png', 400);
|
||||
testTile(prefix, 256, 0, 0, 0, 'gif', 400);
|
||||
testTile(prefix, 256, 0, 0, 0, 'pbf', 400);
|
||||
|
||||
testTile('test', 0, 0, 0, 'png', 404, 1);
|
||||
testTile('test', 0, 0, 0, 'png', 404, 4);
|
||||
testTile(prefix, 256, 0, 0, 0, 'png', 400, 1);
|
||||
testTile(prefix, 256, 0, 0, 0, 'png', 400, 5);
|
||||
|
||||
//testTile('hybrid', 0, 0, 0, 'png', 404); //TODO: test this
|
||||
testTile(prefix, 300, 0, 0, 0, 'png', 400);
|
||||
});
|
||||
});
|
||||
|
|