Compare commits
371 Commits
v3.1.1
...
61e830901b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
61e830901b | ||
|
|
84930b82ca | ||
|
|
5767fff19d | ||
|
|
d44e288a31 | ||
|
|
b05a5f6f42 | ||
|
|
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 | ||
|
|
f2cc5bf563 | ||
|
|
1ab22d9908 | ||
|
|
d2d2d38daf | ||
|
|
5d5be67efc | ||
|
|
edd7c7dd87 | ||
|
|
a766dfd233 | ||
|
|
aba60f0c6a | ||
|
|
6027d89623 | ||
|
|
8680a8006a | ||
|
|
09ded526ef |
@@ -1,6 +1,8 @@
|
|||||||
*
|
*
|
||||||
!src
|
!src
|
||||||
!public
|
!public
|
||||||
|
!test
|
||||||
!package.json
|
!package.json
|
||||||
|
!mapbox-mbtiles-0.12.2.tgz
|
||||||
!package-lock.json
|
!package-lock.json
|
||||||
!docker-entrypoint.sh
|
!docker-entrypoint.sh
|
||||||
|
|||||||
1
.eslintignore
Normal file
1
.eslintignore
Normal file
@@ -0,0 +1 @@
|
|||||||
|
public
|
||||||
32
.eslintrc.cjs
Normal file
32
.eslintrc.cjs
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
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',
|
||||||
|
},
|
||||||
|
};
|
||||||
11
.gitattributes
vendored
Normal file
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
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
|
||||||
16
.github/workflows/automerger.yml
vendored
Normal file
16
.github/workflows/automerger.yml
vendored
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
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 }}
|
||||||
51
.github/workflows/ci.yml
vendored
Normal file
51
.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
name: 'Continuous Integration'
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
checks: write
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
ci:
|
||||||
|
runs-on: ubuntu-20.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: Setup node env 📦
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
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
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@v2
|
||||||
|
with:
|
||||||
|
languages: ${{ matrix.language }}
|
||||||
|
queries: +security-and-quality
|
||||||
|
- name: Autobuild
|
||||||
|
uses: github/codeql-action/autobuild@v2
|
||||||
|
- name: Perform CodeQL Analysis
|
||||||
|
uses: github/codeql-action/analyze@v2
|
||||||
|
with:
|
||||||
|
category: '/language:${{ matrix.language }}'
|
||||||
70
.github/workflows/ct.yml
vendored
Normal file
70
.github/workflows/ct.yml
vendored
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
name: 'Continuous Testing'
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
checks: write
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
ct:
|
||||||
|
runs-on: ubuntu-20.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@v3
|
||||||
|
with:
|
||||||
|
node-version-file: 'package.json'
|
||||||
|
check-latest: true
|
||||||
|
cache: 'npm'
|
||||||
|
|
||||||
|
- name: Install dependencies 🚀
|
||||||
|
run: npm ci --prefer-offline --no-audit --omit=optional
|
||||||
|
|
||||||
|
- 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@v2
|
||||||
|
with:
|
||||||
|
platforms: 'arm64'
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v2
|
||||||
|
|
||||||
|
- name: Test Docker Build
|
||||||
|
uses: docker/build-push-action@v4
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
push: false
|
||||||
|
platforms: linux/arm64,linux/amd64
|
||||||
|
# experimental: https://github.com/docker/build-push-action/blob/master/docs/advanced/cache.md#cache-backend-api
|
||||||
|
cache-from: type=gha
|
||||||
|
cache-to: type=gha,mode=max
|
||||||
43
.github/workflows/pipeline.yml
vendored
Normal file
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
|
||||||
118
.github/workflows/release.yml
vendored
Normal file
118
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
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:
|
||||||
|
name: 'Build, Test, Publish'
|
||||||
|
runs-on: ubuntu-20.04
|
||||||
|
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@v3
|
||||||
|
with:
|
||||||
|
node-version-file: 'package.json'
|
||||||
|
check-latest: true
|
||||||
|
cache: 'npm'
|
||||||
|
|
||||||
|
- name: Install dependencies 🚀
|
||||||
|
run: npm ci --prefer-offline --no-audit --omit=optional
|
||||||
|
|
||||||
|
- 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: Publish to Full Version NPM
|
||||||
|
run: |
|
||||||
|
npm config set //registry.npmjs.org/:_authToken ${NPM_TOKEN}
|
||||||
|
npm publish --access public
|
||||||
|
env:
|
||||||
|
NPM_TOKEN: ${{ github.event.inputs.npm_token }}
|
||||||
|
|
||||||
|
- name: Get version
|
||||||
|
run: |
|
||||||
|
echo "PACKAGE_VERSION=$(grep '"version"' package.json | cut -d '"' -f 4 | head -n 1)" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v2
|
||||||
|
with:
|
||||||
|
platforms: 'arm64'
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v2
|
||||||
|
|
||||||
|
- 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@v4
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
push: true
|
||||||
|
tags: maptiler/tileserver-gl:latest, maptiler/tileserver-gl:v${{ env.PACKAGE_VERSION }}
|
||||||
|
platforms: linux/arm64,linux/amd64
|
||||||
|
# experimental: https://github.com/docker/build-push-action/blob/master/docs/advanced/cache.md#cache-backend-api
|
||||||
|
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 install
|
||||||
|
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
|
||||||
|
env:
|
||||||
|
NPM_TOKEN: ${{ github.event.inputs.npm_token }}
|
||||||
|
|
||||||
|
- name: Build and publish Light Version to Docker Hub
|
||||||
|
uses: docker/build-push-action@v4
|
||||||
|
with:
|
||||||
|
context: ./light
|
||||||
|
file: ./light/Dockerfile
|
||||||
|
push: true
|
||||||
|
tags: maptiler/tileserver-gl-light:latest, maptiler/tileserver-gl-light:v${{ env.PACKAGE_VERSION }}
|
||||||
|
platforms: linux/arm64,linux/amd64
|
||||||
|
# experimental: https://github.com/docker/build-push-action/blob/master/docs/advanced/cache.md#cache-backend-api
|
||||||
|
cache-from: type=gha
|
||||||
|
cache-to: type=gha,mode=max
|
||||||
3
.hadolint.yml
Normal file
3
.hadolint.yml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
ignored:
|
||||||
|
- DL3008
|
||||||
|
- DL3015
|
||||||
21
.husky/commit-msg
Executable file
21
.husky/commit-msg
Executable file
@@ -0,0 +1,21 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
. "$(dirname "$0")/_/husky.sh"
|
||||||
|
|
||||||
|
NAME=$(git config user.name)
|
||||||
|
EMAIL=$(git config user.email)
|
||||||
|
|
||||||
|
if [ -z "$NAME" ]; then
|
||||||
|
echo "empty git config user.name"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$EMAIL" ]; then
|
||||||
|
echo "empty git config user.email"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
git interpret-trailers --if-exists doNothing --trailer \
|
||||||
|
"Signed-off-by: $NAME <$EMAIL>" \
|
||||||
|
--in-place "$1"
|
||||||
|
|
||||||
|
npm exec --no -- commitlint --edit $1
|
||||||
4
.husky/pre-push
Executable file
4
.husky/pre-push
Executable file
@@ -0,0 +1,4 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
. "$(dirname "$0")/_/husky.sh"
|
||||||
|
|
||||||
|
npm exec --no -- lint-staged --no-stash
|
||||||
1
.prettierignore
Normal file
1
.prettierignore
Normal file
@@ -0,0 +1 @@
|
|||||||
|
public
|
||||||
18
.readthedocs.yaml
Normal file
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
|
||||||
21
.travis.yml
21
.travis.yml
@@ -1,21 +0,0 @@
|
|||||||
language: node_js
|
|
||||||
node_js:
|
|
||||||
- "10"
|
|
||||||
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 libcairo2-dev libjpeg8-dev libpango1.0-dev libgif-dev build-essential g++
|
|
||||||
- sudo apt-get install -qq xvfb libgles2-mesa-dev libgbm-dev libxxf86vm-dev
|
|
||||||
install:
|
|
||||||
- npm install
|
|
||||||
- wget -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
|
|
||||||
script:
|
|
||||||
- xvfb-run --server-args="-screen 0 1024x768x24" npm test
|
|
||||||
138
Dockerfile
138
Dockerfile
@@ -1,55 +1,109 @@
|
|||||||
FROM node:10-buster AS builder
|
FROM ubuntu:focal AS builder
|
||||||
|
|
||||||
RUN export DEBIAN_FRONTEND=noninteractive \
|
ENV NODE_ENV="production"
|
||||||
&& apt-get -qq update \
|
|
||||||
&& apt-get -y --no-install-recommends install \
|
RUN set -ex; \
|
||||||
apt-transport-https \
|
export DEBIAN_FRONTEND=noninteractive; \
|
||||||
curl \
|
apt-get -qq update; \
|
||||||
unzip \
|
apt-get -y --no-install-recommends install \
|
||||||
build-essential \
|
build-essential \
|
||||||
python \
|
ca-certificates \
|
||||||
|
wget \
|
||||||
|
pkg-config \
|
||||||
|
xvfb \
|
||||||
|
libglfw3-dev \
|
||||||
|
libuv1-dev \
|
||||||
|
libjpeg-turbo8 \
|
||||||
|
libicu66 \
|
||||||
libcairo2-dev \
|
libcairo2-dev \
|
||||||
libgles2-mesa-dev \
|
libpango1.0-dev \
|
||||||
libgbm-dev \
|
libjpeg-dev \
|
||||||
libllvm7 \
|
libgif-dev \
|
||||||
libprotobuf-dev \
|
librsvg2-dev \
|
||||||
&& apt-get -y --purge autoremove \
|
gir1.2-rsvg-2.0 \
|
||||||
&& apt-get clean \
|
librsvg2-2 \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
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 wget -qO- https://deb.nodesource.com/setup_18.x | bash; \
|
||||||
|
apt-get install -y nodejs; \
|
||||||
|
npm i -g npm@latest; \
|
||||||
|
apt-get -y remove wget; \
|
||||||
|
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 mapbox-mbtiles-0.12.2.tgz /usr/src/app
|
||||||
|
COPY package.json /usr/src/app
|
||||||
|
COPY package-lock.json /usr/src/app
|
||||||
|
|
||||||
|
RUN npm ci --omit=dev; \
|
||||||
|
chown -R root:root /usr/src/app;
|
||||||
|
|
||||||
|
FROM ubuntu:focal AS final
|
||||||
|
|
||||||
|
ENV \
|
||||||
|
NODE_ENV="production" \
|
||||||
|
CHOKIDAR_USEPOLLING=1 \
|
||||||
|
CHOKIDAR_INTERVAL=500
|
||||||
|
|
||||||
|
RUN set -ex; \
|
||||||
|
export DEBIAN_FRONTEND=noninteractive; \
|
||||||
|
groupadd -r node; \
|
||||||
|
useradd -r -g node node; \
|
||||||
|
apt-get -qq update; \
|
||||||
|
apt-get -y --no-install-recommends install \
|
||||||
|
ca-certificates \
|
||||||
|
wget \
|
||||||
|
xvfb \
|
||||||
|
libglfw3 \
|
||||||
|
libuv1 \
|
||||||
|
libjpeg-turbo8 \
|
||||||
|
libicu66 \
|
||||||
|
libcairo2 \
|
||||||
|
libgif7 \
|
||||||
|
libopengl0 \
|
||||||
|
libpixman-1-0 \
|
||||||
|
libcurl4 \
|
||||||
|
librsvg2-2 \
|
||||||
|
libpango-1.0-0; \
|
||||||
|
apt-get -y --purge autoremove; \
|
||||||
|
apt-get clean; \
|
||||||
|
rm -rf /var/lib/apt/lists/*;
|
||||||
|
|
||||||
|
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
||||||
|
|
||||||
|
RUN wget -qO- https://deb.nodesource.com/setup_18.x | bash; \
|
||||||
|
apt-get install -y nodejs; \
|
||||||
|
npm i -g npm@latest; \
|
||||||
|
apt-get -y remove wget; \
|
||||||
|
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
|
COPY . /usr/src/app
|
||||||
|
|
||||||
ENV NODE_ENV="production"
|
RUN mkdir -p /data && chown node:node /data
|
||||||
|
|
||||||
RUN cd /usr/src/app && npm install --production
|
|
||||||
|
|
||||||
|
|
||||||
FROM node:10-buster-slim AS final
|
|
||||||
|
|
||||||
RUN export DEBIAN_FRONTEND=noninteractive \
|
|
||||||
&& apt-get -qq update \
|
|
||||||
&& apt-get -y --no-install-recommends install \
|
|
||||||
libgles2-mesa \
|
|
||||||
libegl1 \
|
|
||||||
xvfb \
|
|
||||||
xauth \
|
|
||||||
&& apt-get -y --purge autoremove \
|
|
||||||
&& apt-get clean \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
COPY --from=builder /usr/src/app /app
|
|
||||||
|
|
||||||
ENV NODE_ENV="production"
|
|
||||||
ENV CHOKIDAR_USEPOLLING=1
|
|
||||||
ENV CHOKIDAR_INTERVAL=500
|
|
||||||
|
|
||||||
VOLUME /data
|
VOLUME /data
|
||||||
WORKDIR /data
|
WORKDIR /data
|
||||||
|
|
||||||
EXPOSE 80
|
EXPOSE 8080
|
||||||
|
|
||||||
USER node:node
|
USER node:node
|
||||||
|
|
||||||
ENTRYPOINT ["/app/docker-entrypoint.sh"]
|
ENTRYPOINT ["/usr/src/app/docker-entrypoint.sh"]
|
||||||
|
|
||||||
CMD ["-p", "80"]
|
HEALTHCHECK CMD node /usr/src/app/src/healthcheck.js
|
||||||
|
|||||||
@@ -1,13 +1,44 @@
|
|||||||
FROM node:10-stretch
|
FROM ubuntu:focal
|
||||||
|
|
||||||
|
ENV \
|
||||||
|
NODE_ENV="production" \
|
||||||
|
CHOKIDAR_USEPOLLING=1 \
|
||||||
|
CHOKIDAR_INTERVAL=500
|
||||||
|
|
||||||
|
RUN set -ex; \
|
||||||
|
export DEBIAN_FRONTEND=noninteractive; \
|
||||||
|
groupadd -r node; \
|
||||||
|
useradd -r -g node node; \
|
||||||
|
apt-get -qq update; \
|
||||||
|
apt-get -y --no-install-recommends install \
|
||||||
|
ca-certificates \
|
||||||
|
wget; \
|
||||||
|
wget -qO- https://deb.nodesource.com/setup_18.x | bash; \
|
||||||
|
apt-get install -y nodejs; \
|
||||||
|
apt-get -y remove wget; \
|
||||||
|
apt-get -y --purge autoremove; \
|
||||||
|
apt-get clean; \
|
||||||
|
rm -rf /var/lib/apt/lists/*;
|
||||||
|
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
RUN mkdir -p /data; \
|
||||||
|
chown node:node /data; \
|
||||||
|
mkdir -p /usr/src/app;
|
||||||
|
|
||||||
ENV NODE_ENV="production"
|
|
||||||
ENV CHOKIDAR_USEPOLLING=1
|
|
||||||
ENV CHOKIDAR_INTERVAL=500
|
|
||||||
EXPOSE 80
|
|
||||||
VOLUME /data
|
VOLUME /data
|
||||||
WORKDIR /data
|
|
||||||
ENTRYPOINT ["node", "/usr/src/app/", "-p", "80"]
|
|
||||||
|
|
||||||
RUN mkdir -p /usr/src/app
|
WORKDIR /data
|
||||||
|
|
||||||
COPY / /usr/src/app
|
COPY / /usr/src/app
|
||||||
RUN cd /usr/src/app && npm install --production
|
|
||||||
|
RUN cd /usr/src/app; \
|
||||||
|
npm install --omit=dev; \
|
||||||
|
chown -R root:root /usr/src/app; \
|
||||||
|
chmod +x /usr/src/app/docker-entrypoint.sh;
|
||||||
|
|
||||||
|
USER node:node
|
||||||
|
|
||||||
|
ENTRYPOINT ["/usr/src/app/docker-entrypoint.sh"]
|
||||||
|
|
||||||
|
HEALTHCHECK CMD node /usr/src/app/src/healthcheck.js
|
||||||
@@ -2,33 +2,43 @@
|
|||||||
# Simply run "docker build -f Dockerfile_test ."
|
# Simply run "docker build -f Dockerfile_test ."
|
||||||
# WARNING: sometimes it fails with a core dumped exception
|
# WARNING: sometimes it fails with a core dumped exception
|
||||||
|
|
||||||
FROM node:10-stretch
|
FROM ubuntu:focal
|
||||||
|
|
||||||
RUN apt-get -qq update \
|
ENV NODE_ENV="development"
|
||||||
&& DEBIAN_FRONTEND=noninteractive apt-get -y install \
|
|
||||||
apt-transport-https \
|
RUN set -ex; \
|
||||||
curl \
|
export DEBIAN_FRONTEND=noninteractive; \
|
||||||
unzip \
|
apt-get -qq update; \
|
||||||
build-essential \
|
apt-get -y --no-install-recommends install \
|
||||||
python \
|
unzip \
|
||||||
libcairo2-dev \
|
build-essential \
|
||||||
libgles2-mesa-dev \
|
ca-certificates \
|
||||||
libgbm-dev \
|
wget \
|
||||||
libllvm3.9 \
|
pkg-config \
|
||||||
libprotobuf-dev \
|
xvfb \
|
||||||
libxxf86vm-dev \
|
libglfw3-dev \
|
||||||
xvfb \
|
libuv1-dev \
|
||||||
&& apt-get clean
|
libjpeg-turbo8 \
|
||||||
|
libicu66 \
|
||||||
|
libcairo2-dev \
|
||||||
|
libpango1.0-dev \
|
||||||
|
libjpeg-dev \
|
||||||
|
libgif-dev \
|
||||||
|
librsvg2-dev \
|
||||||
|
libcurl4-openssl-dev \
|
||||||
|
libpixman-1-dev; \
|
||||||
|
wget -qO- https://deb.nodesource.com/setup_18.x | bash; \
|
||||||
|
apt-get install -y nodejs; \
|
||||||
|
apt-get clean;
|
||||||
|
|
||||||
RUN mkdir -p /usr/src/app
|
RUN mkdir -p /usr/src/app
|
||||||
WORKDIR /usr/src/app
|
WORKDIR /usr/src/app
|
||||||
|
|
||||||
RUN wget -O test_data.zip https://github.com/maptiler/tileserver-gl/releases/download/v1.3.0/test_data.zip
|
RUN wget -O test_data.zip https://github.com/maptiler/tileserver-gl/releases/download/v1.3.0/test_data.zip; \
|
||||||
RUN unzip -q test_data.zip -d test_data
|
unzip -q test_data.zip -d test_data
|
||||||
|
|
||||||
ENV NODE_ENV="test"
|
|
||||||
|
|
||||||
COPY package.json .
|
COPY package.json .
|
||||||
RUN npm install
|
RUN npm install
|
||||||
COPY / .
|
COPY / .
|
||||||
|
|
||||||
RUN xvfb-run --server-args="-screen 0 1024x768x24" npm test
|
RUN xvfb-run --server-args="-screen 0 1024x768x24" npm test
|
||||||
|
|||||||
901
LICENSE.md
901
LICENSE.md
File diff suppressed because it is too large
Load Diff
@@ -3,4 +3,11 @@
|
|||||||
- Update version in `package.json`
|
- Update version in `package.json`
|
||||||
- `git tag vx.x.x`
|
- `git tag vx.x.x`
|
||||||
- `git push --tags`
|
- `git push --tags`
|
||||||
- `node publish.js` (publishes packages to npm)
|
- `docker buildx build --platform linux/amd64 -t maptiler/tileserver-gl:latest -t maptiler/tileserver-gl:[version] .`
|
||||||
|
- `docker push maptiler/tileserver-gl --all-tags`
|
||||||
|
- `npm publish --access public` or `node publish.js`
|
||||||
|
- `node publish.js --no-publish`
|
||||||
|
- `cd light`
|
||||||
|
- `docker buildx build --platform linux/amd64 -t maptiler/tileserver-gl-light:latest -t maptiler/tileserver-gl-light:[version] .`
|
||||||
|
- `docker push maptiler/tileserver-gl-light --all-tags`
|
||||||
|
- `npm publish --access public`
|
||||||
|
|||||||
75
README.md
75
README.md
@@ -2,47 +2,88 @@
|
|||||||
|
|
||||||
|
|
||||||
# TileServer GL
|
# TileServer GL
|
||||||
[](https://travis-ci.org/maptiler/tileserver-gl)
|
[](https://github.com/maptiler/tileserver-gl/actions/workflows/pipeline.yml)
|
||||||
[](https://hub.docker.com/r/maptiler/tileserver-gl/)
|
[](https://hub.docker.com/r/maptiler/tileserver-gl/)
|
||||||
|
|
||||||
Vector and raster maps with GL styles. Server side rendering by Mapbox GL Native. Map tile server for Mapbox GL JS, Android, iOS, Leaflet, OpenLayers, GIS via WMTS, etc.
|
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.
|
||||||
|
|
||||||
## Get Started
|
Download vector tiles from [OpenMapTiles](https://data.maptiler.com/downloads/planet/).
|
||||||
|
## Getting Started with Node
|
||||||
|
|
||||||
Make sure you have Node.js version **10** installed (running `node -v` it should output something like `v10.17.0`).
|
Make sure you have Node.js version **14.20.0** or above installed. Node 18 is recommended. (running `node -v` it should output something like `v18.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
|
Install `tileserver-gl` with server-side raster rendering of vector tiles with npm.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm install -g tileserver-gl
|
npm install -g tileserver-gl
|
||||||
```
|
```
|
||||||
|
|
||||||
Now download vector tiles from [OpenMapTiles](https://openmaptiles.org/downloads/).
|
Once installed, you can use it like the following examples.
|
||||||
|
|
||||||
|
using a mbtiles file
|
||||||
```bash
|
```bash
|
||||||
curl -o zurich_switzerland.mbtiles https://[GET-YOUR-LINK]/extracts/zurich_switzerland.mbtiles
|
wget https://github.com/maptiler/tileserver-gl/releases/download/v1.3.0/zurich_switzerland.mbtiles
|
||||||
|
tileserver-gl --mbtiles zurich_switzerland.mbtiles
|
||||||
|
[in your browser, visit http://[server ip]:8080]
|
||||||
```
|
```
|
||||||
|
|
||||||
Start `tileserver-gl` with the downloaded vector tiles.
|
using a config.json + style + mbtiles file
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
tileserver-gl zurich_switzerland.mbtiles
|
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` 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 MapBox GL Native.
|
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.
|
||||||
|
|
||||||
## Using Docker
|
## 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 in the directory with the downloaded MBTiles the command:
|
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
|
```bash
|
||||||
docker run --rm -it -v $(pwd):/data -p 8080:80 maptiler/tileserver-gl
|
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 --mbtiles zurich_switzerland.mbtiles
|
||||||
|
[in your browser, visit http://[server ip]:8080]
|
||||||
```
|
```
|
||||||
|
|
||||||
This will download and start a ready to use container on your computer and the maps are going to be available in webbrowser on localhost: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
|
||||||
|
[in your browser, visit http://[server ip]:8080]
|
||||||
|
```
|
||||||
|
|
||||||
On laptop you can use [Docker Kitematic](https://kitematic.com/) and search "tileserver-gl" and run it, then drop in the 'data' folder the MBTiles.
|
Example using a different path
|
||||||
|
```bash
|
||||||
|
docker run --rm -it -v /your/local/config/path:/data -p 8080:8080 maptiler/tileserver-gl
|
||||||
|
```
|
||||||
|
replace '/your/local/config/path' with the path to your config file
|
||||||
|
|
||||||
|
|
||||||
|
Alternatively, you can use the `maptiler/tileserver-gl-light` 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
|
## Documentation
|
||||||
|
|
||||||
You can read full documentation of this project at https://tileserver.readthedocs.io/.
|
You can read the full documentation of this project at https://maptiler-tileserver.readthedocs.io/.
|
||||||
|
|
||||||
|
## Alternative
|
||||||
|
|
||||||
|
Discover MapTiler Server if you need a [map server with easy setup and user-friendly interface](https://www.maptiler.com/server/).
|
||||||
|
|
||||||
|
|||||||
3
commitlint.config.cjs
Normal file
3
commitlint.config.cjs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
module.exports = {
|
||||||
|
extends: ['@commitlint/config-conventional'],
|
||||||
|
};
|
||||||
@@ -1,23 +1,10 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
handle() {
|
|
||||||
SIGNAL=$(( $? - 128 ))
|
|
||||||
echo "Caught signal ${SIGNAL}, stopping gracefully"
|
|
||||||
kill -s ${SIGNAL} $(pidof node) 2>/dev/null
|
|
||||||
}
|
|
||||||
|
|
||||||
trap handle INT TERM
|
|
||||||
|
|
||||||
if ! which -- "${1}"; then
|
if ! which -- "${1}"; then
|
||||||
# first arg is not an executable
|
# first arg is not an executable
|
||||||
xvfb-run -a --server-args="-screen 0 1024x768x24" -- node /app/ "$@" &
|
if [ -e /tmp/.X99-lock ]; then rm /tmp/.X99-lock -f; fi
|
||||||
# Wait exits immediately on signals which have traps set. Store return value and wait
|
export DISPLAY=:99
|
||||||
# again for all jobs to actually complete before continuing.
|
Xvfb "${DISPLAY}" -nolisten unix &
|
||||||
wait $! || RETVAL=$?
|
exec node /usr/src/app/ "$@"
|
||||||
wait
|
|
||||||
exit ${RETVAL}
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
exec "$@"
|
exec "$@"
|
||||||
|
|||||||
7
docker-entrypoint_light.sh
Normal file
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 "$@"
|
||||||
@@ -44,7 +44,7 @@ master_doc = 'index'
|
|||||||
|
|
||||||
# General information about the project.
|
# General information about the project.
|
||||||
project = u'TileServer GL'
|
project = u'TileServer GL'
|
||||||
copyright = u'2016, Klokan Technologies GmbH'
|
copyright = u'2023, MapTiler.com'
|
||||||
|
|
||||||
# The version info for the project you're documenting, acts as replacement for
|
# The version info for the project you're documenting, acts as replacement for
|
||||||
# |version| and |release|, also used in various other places throughout the
|
# |version| and |release|, also used in various other places throughout the
|
||||||
@@ -197,7 +197,7 @@ latex_elements = {
|
|||||||
# author, documentclass [howto, manual, or own class]).
|
# author, documentclass [howto, manual, or own class]).
|
||||||
latex_documents = [
|
latex_documents = [
|
||||||
('index', 'TileServerGL.tex', u'TileServer GL Documentation',
|
('index', 'TileServerGL.tex', u'TileServer GL Documentation',
|
||||||
u'Klokan Technologies GmbH', 'manual'),
|
u'MapTiler.com', 'manual'),
|
||||||
]
|
]
|
||||||
|
|
||||||
# The name of an image file (relative to this directory) to place at the top of
|
# The name of an image file (relative to this directory) to place at the top of
|
||||||
@@ -227,7 +227,7 @@ latex_documents = [
|
|||||||
# (source start file, name, description, authors, manual section).
|
# (source start file, name, description, authors, manual section).
|
||||||
man_pages = [
|
man_pages = [
|
||||||
('index', 'tileservergl', u'TileServer GL Documentation',
|
('index', 'tileservergl', u'TileServer GL Documentation',
|
||||||
[u'Klokan Technologies GmbH'], 1)
|
[u'MapTiler.com'], 1)
|
||||||
]
|
]
|
||||||
|
|
||||||
# If true, show URL addresses after external links.
|
# If true, show URL addresses after external links.
|
||||||
@@ -241,7 +241,7 @@ man_pages = [
|
|||||||
# dir menu entry, description, category)
|
# dir menu entry, description, category)
|
||||||
texinfo_documents = [
|
texinfo_documents = [
|
||||||
('index', 'TileServerGL', u'TileServer GL Documentation',
|
('index', 'TileServerGL', u'TileServer GL Documentation',
|
||||||
u'Klokan Technologies GmbH', 'TileServerGL', 'One line description of project.',
|
u'MapTiler.com', 'TileServerGL', 'One line description of project.',
|
||||||
'Miscellaneous'),
|
'Miscellaneous'),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ Example:
|
|||||||
"root": "",
|
"root": "",
|
||||||
"fonts": "fonts",
|
"fonts": "fonts",
|
||||||
"sprites": "sprites",
|
"sprites": "sprites",
|
||||||
|
"icons": "icons",
|
||||||
"styles": "styles",
|
"styles": "styles",
|
||||||
"mbtiles": ""
|
"mbtiles": ""
|
||||||
},
|
},
|
||||||
@@ -31,6 +32,7 @@ Example:
|
|||||||
"serveAllFonts": false,
|
"serveAllFonts": false,
|
||||||
"serveAllStyles": false,
|
"serveAllStyles": false,
|
||||||
"serveStaticMaps": true,
|
"serveStaticMaps": true,
|
||||||
|
"allowRemoteMarkerIcons": true,
|
||||||
"tileMargin": 0
|
"tileMargin": 0
|
||||||
},
|
},
|
||||||
"styles": {
|
"styles": {
|
||||||
@@ -141,6 +143,13 @@ Optional string to be rendered into the raster tiles (and static maps) as waterm
|
|||||||
Can be used for hard-coding attributions etc. (can also be specified per-style).
|
Can be used for hard-coding attributions etc. (can also be specified per-style).
|
||||||
Not used by default.
|
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.
|
||||||
|
|
||||||
``styles``
|
``styles``
|
||||||
==========
|
==========
|
||||||
|
|
||||||
|
|||||||
@@ -2,12 +2,64 @@
|
|||||||
Deployment
|
Deployment
|
||||||
==========
|
==========
|
||||||
|
|
||||||
Typically - you should use nginx/lighttpd/apache on the frontend - and the tileserver-gl server is hidden behind it in production deployment.
|
Typically, you should use nginx, lighttpd or apache on the frontend. The tileserver-gl server is hidden behind it in production deployment.
|
||||||
|
|
||||||
Caching
|
Caching
|
||||||
=======
|
=======
|
||||||
|
|
||||||
There is a plenty of options you can use to create proper caching infrastructure: Varnish, CloudFlare, ...
|
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
|
Securing
|
||||||
========
|
========
|
||||||
@@ -17,4 +69,69 @@ Nginx can be used to add protection via https, password, referrer, IP address re
|
|||||||
Running behind a proxy or a load-balancer
|
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 ensures the URLs generated inside TileJSON etc. are using the desired domain and protocol.
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -35,18 +35,56 @@ Static images
|
|||||||
|
|
||||||
* All the static image endpoints additionally support following query parameters:
|
* All the static image endpoints additionally support following query parameters:
|
||||||
|
|
||||||
* ``path`` - comma-separated ``lng,lat``, pipe-separated pairs
|
* ``path`` - ``((fill|stroke|width)\:[^\|]+\|)*((enc:.+)|((-?\d+\.?\d*,-?\d+\.?\d*\|)+(-?\d+\.?\d*,-?\d+\.?\d*)))``
|
||||||
|
|
||||||
* e.g. ``5.9,45.8|5.9,47.8|10.5,47.8|10.5,45.8|5.9,45.8``
|
* comma-separated ``lng,lat``, pipe-separated pairs
|
||||||
|
|
||||||
* ``latlng`` - indicates the ``path`` coordinates are in ``lat,lng`` order rather than the usual ``lng,lat``
|
* 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``
|
||||||
* ``fill`` - color to use as the fill (e.g. ``red``, ``rgba(255,255,255,0.5)``, ``#0000ff``)
|
* ``fill`` - color to use as the fill (e.g. ``red``, ``rgba(255,255,255,0.5)``, ``#0000ff``)
|
||||||
* ``stroke`` - color of the path stroke
|
* ``stroke`` - color of the path stroke
|
||||||
* ``width`` - width of the stroke
|
* ``width`` - width of the stroke
|
||||||
|
* ``linecap`` - rendering style for the start and end points of the path
|
||||||
|
* ``linejoin`` - rendering style for overlapping segments of the path with differing directions
|
||||||
|
* ``border`` - color of the optional border path stroke
|
||||||
|
* ``borderwidth`` - width of the border stroke (default 10% of width)
|
||||||
|
* ``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-start.svg|scale:0.5|offset:2,-4``
|
||||||
|
* can be provided multiple times
|
||||||
|
|
||||||
* ``padding`` - "percentage" padding for fitted endpoints (area-based and path autofit)
|
* ``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"
|
* 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.
|
* 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.
|
* The static images are not available in the ``tileserver-gl-light`` version.
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ Docker
|
|||||||
|
|
||||||
When running docker image, no special installation is needed -- the docker will automatically download the image if not present.
|
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:80 maptiler/tileserver-gl``.
|
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:
|
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:
|
||||||
|
|
||||||
@@ -26,11 +26,30 @@ Native dependencies
|
|||||||
There are some native dependencies that you need to make sure are installed if you plan to run the TileServer GL natively without docker.
|
There are some native dependencies that you need to make sure are installed if you plan to run the TileServer GL natively without docker.
|
||||||
The precise package names you need to install may differ on various platforms.
|
The precise package names you need to install may differ on various platforms.
|
||||||
|
|
||||||
These are required on Debian 9:
|
These are required on Debian 11:
|
||||||
* ``build-essential``
|
* ``libgles2-mesa``
|
||||||
* ``libcairo2-dev``
|
* ``libegl1``
|
||||||
* ``libprotobuf-dev``
|
* ``xvfb``
|
||||||
|
* ``xauth``
|
||||||
|
* ``libopengl0``
|
||||||
|
* ``libcurl4``
|
||||||
|
* ``curl``
|
||||||
|
* ``libuv1-dev``
|
||||||
|
* ``libc6-dev``
|
||||||
|
* ``http://archive.ubuntu.com/ubuntu/pool/main/libj/libjpeg-turbo/libjpeg-turbo8_2.0.3-0ubuntu1_amd64.deb``
|
||||||
|
* ``http://archive.ubuntu.com/ubuntu/pool/main/i/icu/libicu66_66.1-2ubuntu2_amd64.deb``
|
||||||
|
|
||||||
|
These are required on Ubuntu 20.04:
|
||||||
|
* ``libcairo2-dev``
|
||||||
|
* ``libjpeg8-dev``
|
||||||
|
* ``libpango1.0-dev``
|
||||||
|
* ``libgif-dev``
|
||||||
|
* ``build-essential``
|
||||||
|
* ``g++``
|
||||||
|
* ``xvfb``
|
||||||
|
* ``libgles2-mesa-dev``
|
||||||
|
* ``libgbm-dev``
|
||||||
|
* ``libxxf86vm-dev``
|
||||||
|
|
||||||
``tileserver-gl-light`` on npm
|
``tileserver-gl-light`` on npm
|
||||||
==============================
|
==============================
|
||||||
@@ -41,7 +60,7 @@ Alternatively, you can use ``tileserver-gl-light`` package instead, which is pur
|
|||||||
From source
|
From source
|
||||||
===========
|
===========
|
||||||
|
|
||||||
Make sure you have Node v10 (nvm install 10) and run::
|
Make sure you have Node v18 (nvm install 18) and run::
|
||||||
|
|
||||||
npm install
|
npm install
|
||||||
node .
|
node .
|
||||||
|
|||||||
@@ -29,8 +29,16 @@ Default preview style and configuration
|
|||||||
- If no configuration file is specified, a default preview style (compatible with openmaptiles) is used.
|
- If no configuration file is specified, a default preview style (compatible with openmaptiles) is used.
|
||||||
- If no mbtiles file is specified (and is not found in the current working directory), a sample file is downloaded (showing the Zurich area)
|
- If no mbtiles file is specified (and is not found in the current working directory), a sample file is downloaded (showing the Zurich area)
|
||||||
|
|
||||||
Reloading configuration
|
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.
|
It is possible to reload the configuration file without restarting the whole process by sending a SIGHUP signal to the node process.
|
||||||
However, this does not currently work when running the tileserver-gl docker container (the signal is not passed to the subprocess, see https://github.com/maptiler/tileserver-gl/issues/420#issuecomment-597507663).
|
|
||||||
|
- 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
4
lint-staged.config.cjs
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
module.exports = {
|
||||||
|
'*.{js,ts}': 'npm run lint:js',
|
||||||
|
'*.{yml}': 'npm run lint:yml',
|
||||||
|
};
|
||||||
BIN
mapbox-mbtiles-0.12.2.tgz
Normal file
BIN
mapbox-mbtiles-0.12.2.tgz
Normal file
Binary file not shown.
8934
package-lock.json
generated
Normal file
8934
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
88
package.json
88
package.json
@@ -1,49 +1,83 @@
|
|||||||
{
|
{
|
||||||
"name": "tileserver-gl",
|
"name": "tileserver-gl",
|
||||||
"version": "3.1.1",
|
"version": "4.5.1",
|
||||||
"description": "Map tile server for JSON GL styles - vector and server side generated raster tiles",
|
"description": "Map tile server for JSON GL styles - vector and server side generated raster tiles",
|
||||||
"main": "src/main.js",
|
"main": "src/main.js",
|
||||||
"bin": "src/main.js",
|
"bin": "src/main.js",
|
||||||
"repository": {
|
"type": "module",
|
||||||
"type": "git",
|
|
||||||
"url": "https://github.com/maptiler/tileserver-gl.git"
|
|
||||||
},
|
|
||||||
"license": "BSD-2-Clause",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10 <11"
|
|
||||||
},
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "mocha test/**.js --timeout 10000",
|
"test": "mocha test/**.js --timeout 10000",
|
||||||
"docker": "docker build -f Dockerfile . && docker run --rm -i -p 8080:80 $(docker build -q .)"
|
"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 -f Dockerfile . && docker run --rm -i -p 8080:8080 $(docker build -q .)",
|
||||||
|
"prepare": "node -e \"if (process.env.NODE_ENV !== 'production'){ process.exit(1) } \" || husky install"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mapbox/glyph-pbf-composite": "0.0.3",
|
"@mapbox/glyph-pbf-composite": "0.0.3",
|
||||||
"@mapbox/mapbox-gl-native": "5.0.2",
|
"@mapbox/mbtiles": "file:/./mapbox-mbtiles-0.12.2.tgz",
|
||||||
"@mapbox/mapbox-gl-style-spec": "13.12.0",
|
"@mapbox/polyline": "^1.2.1",
|
||||||
"@mapbox/mbtiles": "0.11.0",
|
"@mapbox/sphericalmercator": "1.2.0",
|
||||||
"@mapbox/sphericalmercator": "1.1.0",
|
|
||||||
"@mapbox/vector-tile": "1.3.1",
|
"@mapbox/vector-tile": "1.3.1",
|
||||||
|
"@maplibre/maplibre-gl-native": "5.2.0",
|
||||||
|
"@maplibre/maplibre-gl-style-spec": "18.0.0",
|
||||||
"advanced-pool": "0.3.3",
|
"advanced-pool": "0.3.3",
|
||||||
"canvas": "2.6.1",
|
"canvas": "2.11.2",
|
||||||
"chokidar": "3.3.1",
|
"chokidar": "3.5.3",
|
||||||
"clone": "2.1.2",
|
"clone": "2.1.2",
|
||||||
"color": "3.1.2",
|
"color": "4.2.3",
|
||||||
"commander": "4.1.1",
|
"commander": "11.0.0",
|
||||||
"cors": "2.8.5",
|
"cors": "2.8.5",
|
||||||
"esm": "3.2.25",
|
"express": "4.18.2",
|
||||||
"express": "4.17.1",
|
"handlebars": "4.7.8",
|
||||||
"handlebars": "4.7.3",
|
|
||||||
"http-shutdown": "1.2.2",
|
"http-shutdown": "1.2.2",
|
||||||
"morgan": "1.9.1",
|
"morgan": "1.10.0",
|
||||||
"pbf": "3.2.1",
|
"pbf": "3.2.1",
|
||||||
"proj4": "2.6.0",
|
"proj4": "2.9.0",
|
||||||
"request": "2.88.2",
|
"request": "2.88.2",
|
||||||
"sharp": "0.26.2",
|
"sanitize-filename": "1.6.3",
|
||||||
|
"sharp": "0.32.6",
|
||||||
"tileserver-gl-styles": "2.0.0"
|
"tileserver-gl-styles": "2.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"mocha": "^7.1.0",
|
"@commitlint/cli": "^17.7.2",
|
||||||
|
"@commitlint/config-conventional": "^17.7.0",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^5.61.0",
|
||||||
|
"@typescript-eslint/parser": "^5.62.0",
|
||||||
|
"chai": "4.3.10",
|
||||||
|
"eslint": "^8.50.0",
|
||||||
|
"eslint-config-prettier": "^9.0.0",
|
||||||
|
"eslint-plugin-jsdoc": "^46.8.2",
|
||||||
|
"eslint-plugin-prettier": "^4.2.1",
|
||||||
|
"eslint-plugin-security": "^1.7.1",
|
||||||
|
"husky": "^8.0.3",
|
||||||
|
"lint-staged": "^14.0.1",
|
||||||
|
"mocha": "^10.2.0",
|
||||||
|
"prettier": "^2.8.8",
|
||||||
"should": "^13.2.3",
|
"should": "^13.2.3",
|
||||||
"supertest": "^4.0.2"
|
"supertest": "^6.3.3",
|
||||||
}
|
"yaml-lint": "^1.7.0"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"maptiler",
|
||||||
|
"tileserver-gl",
|
||||||
|
"maplibre-gl",
|
||||||
|
"tileserver"
|
||||||
|
],
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.20.0 <19"
|
||||||
|
},
|
||||||
|
"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
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',
|
||||||
|
};
|
||||||
243
public/resources/L.TileLayer.NoGap.js
Normal file
243
public/resources/L.TileLayer.NoGap.js
Normal file
@@ -0,0 +1,243 @@
|
|||||||
|
// @class TileLayer
|
||||||
|
|
||||||
|
L.TileLayer.mergeOptions({
|
||||||
|
// @option keepBuffer
|
||||||
|
// The amount of tiles outside the visible map area to be kept in the stitched
|
||||||
|
// `TileLayer`.
|
||||||
|
|
||||||
|
// @option dumpToCanvas: Boolean = true
|
||||||
|
// Whether to dump loaded tiles to a `<canvas>` to prevent some rendering
|
||||||
|
// artifacts. (Disabled by default in IE)
|
||||||
|
dumpToCanvas: L.Browser.canvas && !L.Browser.ie,
|
||||||
|
});
|
||||||
|
|
||||||
|
L.TileLayer.include({
|
||||||
|
_onUpdateLevel: function(z, zoom) {
|
||||||
|
if (this.options.dumpToCanvas) {
|
||||||
|
this._levels[z].canvas.style.zIndex =
|
||||||
|
this.options.maxZoom - Math.abs(zoom - z);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_onRemoveLevel: function(z) {
|
||||||
|
if (this.options.dumpToCanvas) {
|
||||||
|
L.DomUtil.remove(this._levels[z].canvas);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_onCreateLevel: function(level) {
|
||||||
|
if (this.options.dumpToCanvas) {
|
||||||
|
level.canvas = L.DomUtil.create(
|
||||||
|
"canvas",
|
||||||
|
"leaflet-tile-container leaflet-zoom-animated",
|
||||||
|
this._container
|
||||||
|
);
|
||||||
|
level.ctx = level.canvas.getContext("2d");
|
||||||
|
this._resetCanvasSize(level);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_removeTile: function(key) {
|
||||||
|
if (this.options.dumpToCanvas) {
|
||||||
|
var tile = this._tiles[key];
|
||||||
|
var level = this._levels[tile.coords.z];
|
||||||
|
var tileSize = this.getTileSize();
|
||||||
|
|
||||||
|
if (level) {
|
||||||
|
// Where in the canvas should this tile go?
|
||||||
|
var offset = L.point(tile.coords.x, tile.coords.y)
|
||||||
|
.subtract(level.canvasRange.min)
|
||||||
|
.scaleBy(this.getTileSize());
|
||||||
|
|
||||||
|
level.ctx.clearRect(offset.x, offset.y, tileSize.x, tileSize.y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
L.GridLayer.prototype._removeTile.call(this, key);
|
||||||
|
},
|
||||||
|
|
||||||
|
_resetCanvasSize: function(level) {
|
||||||
|
var buff = this.options.keepBuffer,
|
||||||
|
pixelBounds = this._getTiledPixelBounds(this._map.getCenter()),
|
||||||
|
tileRange = this._pxBoundsToTileRange(pixelBounds),
|
||||||
|
tileSize = this.getTileSize();
|
||||||
|
|
||||||
|
tileRange.min = tileRange.min.subtract([buff, buff]); // This adds the no-prune buffer
|
||||||
|
tileRange.max = tileRange.max.add([buff + 1, buff + 1]);
|
||||||
|
|
||||||
|
var pixelRange = L.bounds(
|
||||||
|
tileRange.min.scaleBy(tileSize),
|
||||||
|
tileRange.max.add([1, 1]).scaleBy(tileSize) // This prevents an off-by-one when checking if tiles are inside
|
||||||
|
),
|
||||||
|
mustRepositionCanvas = false,
|
||||||
|
neededSize = pixelRange.max.subtract(pixelRange.min);
|
||||||
|
|
||||||
|
// Resize the canvas, if needed, and only to make it bigger.
|
||||||
|
if (
|
||||||
|
neededSize.x > level.canvas.width ||
|
||||||
|
neededSize.y > level.canvas.height
|
||||||
|
) {
|
||||||
|
// Resizing canvases erases the currently drawn content, I'm afraid.
|
||||||
|
// To keep it, dump the pixels to another canvas, then display it on
|
||||||
|
// top. This could be done with getImageData/putImageData, but that
|
||||||
|
// would break for tainted canvases (in non-CORS tilesets)
|
||||||
|
var oldSize = { x: level.canvas.width, y: level.canvas.height };
|
||||||
|
// console.info('Resizing canvas from ', oldSize, 'to ', neededSize);
|
||||||
|
|
||||||
|
var tmpCanvas = L.DomUtil.create("canvas");
|
||||||
|
tmpCanvas.style.width = (tmpCanvas.width = oldSize.x) + "px";
|
||||||
|
tmpCanvas.style.height = (tmpCanvas.height = oldSize.y) + "px";
|
||||||
|
tmpCanvas.getContext("2d").drawImage(level.canvas, 0, 0);
|
||||||
|
// var data = level.ctx.getImageData(0, 0, oldSize.x, oldSize.y);
|
||||||
|
|
||||||
|
level.canvas.style.width = (level.canvas.width = neededSize.x) + "px";
|
||||||
|
level.canvas.style.height = (level.canvas.height = neededSize.y) + "px";
|
||||||
|
level.ctx.drawImage(tmpCanvas, 0, 0);
|
||||||
|
// level.ctx.putImageData(data, 0, 0, 0, 0, oldSize.x, oldSize.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Translate the canvas contents if it's moved around
|
||||||
|
if (level.canvasRange) {
|
||||||
|
var offset = level.canvasRange.min
|
||||||
|
.subtract(tileRange.min)
|
||||||
|
.scaleBy(this.getTileSize());
|
||||||
|
|
||||||
|
// console.info('Offsetting by ', offset);
|
||||||
|
|
||||||
|
if (!L.Browser.safari) {
|
||||||
|
// By default, canvases copy things "on top of" existing pixels, but we want
|
||||||
|
// this to *replace* the existing pixels when doing a drawImage() call.
|
||||||
|
// This will also clear the sides, so no clearRect() calls are needed to make room
|
||||||
|
// for the new tiles.
|
||||||
|
level.ctx.globalCompositeOperation = "copy";
|
||||||
|
level.ctx.drawImage(level.canvas, offset.x, offset.y);
|
||||||
|
level.ctx.globalCompositeOperation = "source-over";
|
||||||
|
} else {
|
||||||
|
// Safari clears the canvas when copying from itself :-(
|
||||||
|
if (!this._tmpCanvas) {
|
||||||
|
var t = (this._tmpCanvas = L.DomUtil.create("canvas"));
|
||||||
|
t.width = level.canvas.width;
|
||||||
|
t.height = level.canvas.height;
|
||||||
|
this._tmpContext = t.getContext("2d");
|
||||||
|
}
|
||||||
|
this._tmpContext.clearRect(
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
level.canvas.width,
|
||||||
|
level.canvas.height
|
||||||
|
);
|
||||||
|
this._tmpContext.drawImage(level.canvas, 0, 0);
|
||||||
|
level.ctx.clearRect(0, 0, level.canvas.width, level.canvas.height);
|
||||||
|
level.ctx.drawImage(this._tmpCanvas, offset.x, offset.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
mustRepositionCanvas = true; // Wait until new props are set
|
||||||
|
}
|
||||||
|
|
||||||
|
level.canvasRange = tileRange;
|
||||||
|
level.canvasPxRange = pixelRange;
|
||||||
|
level.canvasOrigin = pixelRange.min;
|
||||||
|
|
||||||
|
// console.log('Canvas tile range: ', level, tileRange.min, tileRange.max );
|
||||||
|
// console.log('Canvas pixel range: ', pixelRange.min, pixelRange.max );
|
||||||
|
// console.log('Level origin: ', level.origin );
|
||||||
|
|
||||||
|
if (mustRepositionCanvas) {
|
||||||
|
this._setCanvasZoomTransform(
|
||||||
|
level,
|
||||||
|
this._map.getCenter(),
|
||||||
|
this._map.getZoom()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/// set transform/position of canvas, in addition to the transform/position of the individual tile container
|
||||||
|
_setZoomTransform: function(level, center, zoom) {
|
||||||
|
L.GridLayer.prototype._setZoomTransform.call(this, level, center, zoom);
|
||||||
|
if (this.options.dumpToCanvas) {
|
||||||
|
this._setCanvasZoomTransform(level, center, zoom);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// This will get called twice:
|
||||||
|
// * From _setZoomTransform
|
||||||
|
// * When the canvas has shifted due to a new tile being loaded
|
||||||
|
_setCanvasZoomTransform: function(level, center, zoom) {
|
||||||
|
// console.log('_setCanvasZoomTransform', level, center, zoom);
|
||||||
|
if (!level.canvasOrigin) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var scale = this._map.getZoomScale(zoom, level.zoom),
|
||||||
|
translate = level.canvasOrigin
|
||||||
|
.multiplyBy(scale)
|
||||||
|
.subtract(this._map._getNewPixelOrigin(center, zoom))
|
||||||
|
.round();
|
||||||
|
|
||||||
|
if (L.Browser.any3d) {
|
||||||
|
L.DomUtil.setTransform(level.canvas, translate, scale);
|
||||||
|
} else {
|
||||||
|
L.DomUtil.setPosition(level.canvas, translate);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_onOpaqueTile: function(tile) {
|
||||||
|
if (!this.options.dumpToCanvas) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Guard against an NS_ERROR_NOT_AVAILABLE (or similar) exception
|
||||||
|
// when a non-image-tile has been loaded (e.g. a WMS error).
|
||||||
|
// Checking for tile.el.complete is not enough, as it has been
|
||||||
|
// already marked as loaded and ready somehow.
|
||||||
|
try {
|
||||||
|
this.dumpPixels(tile.coords, tile.el);
|
||||||
|
} catch (ex) {
|
||||||
|
return this.fire("tileerror", {
|
||||||
|
error: "Could not copy tile pixels: " + ex,
|
||||||
|
tile: tile,
|
||||||
|
coods: tile.coords,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// If dumping the pixels was successful, then hide the tile.
|
||||||
|
// Do not remove the tile itself, as it is needed to check if the whole
|
||||||
|
// level (and its canvas) should be removed (via level.el.children.length)
|
||||||
|
tile.el.style.display = "none";
|
||||||
|
},
|
||||||
|
|
||||||
|
// @section Extension methods
|
||||||
|
// @uninheritable
|
||||||
|
|
||||||
|
// @method dumpPixels(coords: Object, imageSource: CanvasImageSource): this
|
||||||
|
// Dumps pixels from the given `CanvasImageSource` into the layer, into
|
||||||
|
// the space for the tile represented by the `coords` tile coordinates (an object
|
||||||
|
// like `{x: Number, y: Number, z: Number}`; the image source must have the
|
||||||
|
// same size as the `tileSize` option for the layer. Has no effect if `dumpToCanvas`
|
||||||
|
// is `false`.
|
||||||
|
dumpPixels: function(coords, imageSource) {
|
||||||
|
var level = this._levels[coords.z],
|
||||||
|
tileSize = this.getTileSize();
|
||||||
|
|
||||||
|
if (!level.canvasRange || !this.options.dumpToCanvas) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the tile is inside the currently visible map bounds
|
||||||
|
// There is a possible race condition when tiles are loaded after they
|
||||||
|
// have been panned outside of the map.
|
||||||
|
if (!level.canvasRange.contains(coords)) {
|
||||||
|
this._resetCanvasSize(level);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Where in the canvas should this tile go?
|
||||||
|
var offset = L.point(coords.x, coords.y)
|
||||||
|
.subtract(level.canvasRange.min)
|
||||||
|
.scaleBy(this.getTileSize());
|
||||||
|
|
||||||
|
level.ctx.drawImage(imageSource, offset.x, offset.y, tileSize.x, tileSize.y);
|
||||||
|
|
||||||
|
// TODO: Clear the pixels of other levels' canvases where they overlap
|
||||||
|
// this newly dumped tile.
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
});
|
||||||
BIN
public/resources/images/layers-2x.png
Normal file
BIN
public/resources/images/layers-2x.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.2 KiB |
BIN
public/resources/images/layers.png
Normal file
BIN
public/resources/images/layers.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 696 B |
@@ -17,38 +17,38 @@
|
|||||||
font-style: normal;
|
font-style: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
body{
|
body {
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
color: #212121;
|
color: #212121;
|
||||||
font-family:'OpenSans', sans-serif, Arial;
|
font-family: 'OpenSans', sans-serif, Arial;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
margin:0;
|
margin: 0;
|
||||||
background-repeat:no-repeat !important;
|
background-repeat: no-repeat !important;
|
||||||
background-size: contain !important;
|
background-size: contain !important;
|
||||||
background-image: url(/images/header-map-1280px.png);
|
background-image: url(/images/header-map-1280px.png);
|
||||||
}
|
}
|
||||||
a{
|
a {
|
||||||
color: #499DCE;
|
color: #499dce;
|
||||||
transition: color .2s;
|
transition: color 0.2s;
|
||||||
}
|
}
|
||||||
a:hover {
|
a:hover {
|
||||||
color: #395D73;
|
color: #395d73;
|
||||||
}
|
}
|
||||||
.title {
|
.title {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
font-size: 32px;
|
font-size: 32px;
|
||||||
text-align:center;
|
text-align: center;
|
||||||
margin:90px 0 0 0;
|
margin: 90px 0 0 0;
|
||||||
position:relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
.title.light:after {
|
.title.light:after {
|
||||||
content: "light";
|
content: 'light';
|
||||||
display: block;
|
display: block;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
bottom: -5px;
|
bottom: -5px;
|
||||||
color: #499DCE;
|
color: #499dce;
|
||||||
font-size:.8em;
|
font-size: 0.8em;
|
||||||
}
|
}
|
||||||
section {
|
section {
|
||||||
margin: 15px auto;
|
margin: 15px auto;
|
||||||
@@ -60,27 +60,27 @@ section {
|
|||||||
}
|
}
|
||||||
.subtitle {
|
.subtitle {
|
||||||
font-size: 26px;
|
font-size: 26px;
|
||||||
font-weight:normal;
|
font-weight: normal;
|
||||||
text-align:center;
|
text-align: center;
|
||||||
margin:10px 0 95px 0;
|
margin: 10px 0 95px 0;
|
||||||
}
|
}
|
||||||
.box-header {
|
.box-header {
|
||||||
text-align:left;
|
text-align: left;
|
||||||
text-transform:uppercase;
|
text-transform: uppercase;
|
||||||
border:1px solid #ededed;
|
border: 1px solid #ededed;
|
||||||
margin:25px 0 0 0;
|
margin: 25px 0 0 0;
|
||||||
padding:12px 30px;
|
padding: 12px 30px;
|
||||||
font-size:20px;
|
font-size: 20px;
|
||||||
background:#fff;
|
background: #fff;
|
||||||
}
|
}
|
||||||
.item {
|
.item {
|
||||||
background:#fff;
|
background: #fff;
|
||||||
height: 191px;
|
height: 191px;
|
||||||
border: 1px solid #ededed;
|
border: 1px solid #ededed;
|
||||||
border-top:none;
|
border-top: none;
|
||||||
}
|
}
|
||||||
.item:nth-child(odd) {
|
.item:nth-child(odd) {
|
||||||
background-color:#fbfbfb;
|
background-color: #fbfbfb;
|
||||||
}
|
}
|
||||||
.item img {
|
.item img {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -91,27 +91,27 @@ section {
|
|||||||
border: 1px solid #ccc;
|
border: 1px solid #ccc;
|
||||||
}
|
}
|
||||||
.details {
|
.details {
|
||||||
float:left;
|
float: left;
|
||||||
height: 128px;
|
height: 128px;
|
||||||
padding: 20px 30px 20px 188px;
|
padding: 20px 30px 20px 188px;
|
||||||
}
|
}
|
||||||
.details h3 {
|
.details h3 {
|
||||||
font-size:18px;
|
font-size: 18px;
|
||||||
margin-top: 25px;
|
margin-top: 25px;
|
||||||
}
|
}
|
||||||
.details p {
|
.details p {
|
||||||
padding:0;
|
padding: 0;
|
||||||
margin:18px 0;
|
margin: 18px 0;
|
||||||
}
|
}
|
||||||
.viewers {
|
.viewers {
|
||||||
float:right;
|
float: right;
|
||||||
text-align:center;
|
text-align: center;
|
||||||
width: 120px;
|
width: 120px;
|
||||||
margin-top: 25px;
|
margin-top: 25px;
|
||||||
padding-right: 30px;
|
padding-right: 30px;
|
||||||
}
|
}
|
||||||
.btn {
|
.btn {
|
||||||
display:block;
|
display: block;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
line-height: 36px;
|
line-height: 36px;
|
||||||
}
|
}
|
||||||
@@ -119,57 +119,58 @@ section {
|
|||||||
position: relative;
|
position: relative;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border-radius:4px;
|
border-radius: 4px;
|
||||||
background-color: #499DCE;
|
background-color: #499dce;
|
||||||
background: linear-gradient(90deg, #5aaad8, #4a9ecf);
|
background: linear-gradient(90deg, #5aaad8, #4a9ecf);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
.btn:first-child:hover {
|
.btn:first-child:hover {
|
||||||
background: #395D73;
|
background: #395d73;
|
||||||
}
|
}
|
||||||
footer {
|
footer {
|
||||||
width:100%;
|
width: 100%;
|
||||||
border-top:1px solid #ededed;
|
border-top: 1px solid #ededed;
|
||||||
text-align:center;
|
text-align: center;
|
||||||
color:#d3d3d3;
|
color: #d3d3d3;
|
||||||
padding-top:10px;
|
padding-top: 10px;
|
||||||
font-size:12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
footer img {
|
footer img {
|
||||||
width: 113px;
|
width: 113px;
|
||||||
height: 31px;
|
height: 31px;
|
||||||
}
|
}
|
||||||
footer .t {
|
footer .t {
|
||||||
display:none;
|
display: none;
|
||||||
}
|
}
|
||||||
footer p {
|
footer p {
|
||||||
margin-top:0;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
footer a {
|
footer a {
|
||||||
color: #787878;
|
color: #787878;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
.details h3, .identifier {
|
.details h3,
|
||||||
|
.identifier {
|
||||||
max-width: 550px;
|
max-width: 550px;
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Responsive */
|
/* Responsive */
|
||||||
@media (max-width: 950px) {
|
@media (max-width: 950px) {
|
||||||
section{
|
section {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
width: 96%;
|
width: 96%;
|
||||||
padding: 2%;
|
padding: 2%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@media (max-width: 600px) {
|
@media (max-width: 600px) {
|
||||||
.title{
|
.title {
|
||||||
margin: 25px 0 0 0;
|
margin: 25px 0 0 0;
|
||||||
}
|
}
|
||||||
.title.light:after {
|
.title.light:after {
|
||||||
font-size:.6em;
|
font-size: 0.6em;
|
||||||
}
|
}
|
||||||
.title img {
|
.title img {
|
||||||
width: 200px;
|
width: 200px;
|
||||||
|
|||||||
656
public/resources/leaflet.css
Normal file
656
public/resources/leaflet.css
Normal file
@@ -0,0 +1,656 @@
|
|||||||
|
/* required styles */
|
||||||
|
|
||||||
|
.leaflet-pane,
|
||||||
|
.leaflet-tile,
|
||||||
|
.leaflet-marker-icon,
|
||||||
|
.leaflet-marker-shadow,
|
||||||
|
.leaflet-tile-container,
|
||||||
|
.leaflet-pane > svg,
|
||||||
|
.leaflet-pane > canvas,
|
||||||
|
.leaflet-zoom-box,
|
||||||
|
.leaflet-image-layer,
|
||||||
|
.leaflet-layer {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
.leaflet-container {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.leaflet-tile,
|
||||||
|
.leaflet-marker-icon,
|
||||||
|
.leaflet-marker-shadow {
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-drag: none;
|
||||||
|
}
|
||||||
|
/* Prevents IE11 from highlighting tiles in blue */
|
||||||
|
.leaflet-tile::selection {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
/* Safari renders non-retina tile on retina better with this, but Chrome is worse */
|
||||||
|
.leaflet-safari .leaflet-tile {
|
||||||
|
image-rendering: -webkit-optimize-contrast;
|
||||||
|
}
|
||||||
|
/* hack that prevents hw layers "stretching" when loading new tiles */
|
||||||
|
.leaflet-safari .leaflet-tile-container {
|
||||||
|
width: 1600px;
|
||||||
|
height: 1600px;
|
||||||
|
-webkit-transform-origin: 0 0;
|
||||||
|
}
|
||||||
|
.leaflet-marker-icon,
|
||||||
|
.leaflet-marker-shadow {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
/* .leaflet-container svg: reset svg max-width decleration shipped in Joomla! (joomla.org) 3.x */
|
||||||
|
/* .leaflet-container img: map is broken in FF if you have max-width: 100% on tiles */
|
||||||
|
.leaflet-container .leaflet-overlay-pane svg {
|
||||||
|
max-width: none !important;
|
||||||
|
max-height: none !important;
|
||||||
|
}
|
||||||
|
.leaflet-container .leaflet-marker-pane img,
|
||||||
|
.leaflet-container .leaflet-shadow-pane img,
|
||||||
|
.leaflet-container .leaflet-tile-pane img,
|
||||||
|
.leaflet-container img.leaflet-image-layer,
|
||||||
|
.leaflet-container .leaflet-tile {
|
||||||
|
max-width: none !important;
|
||||||
|
max-height: none !important;
|
||||||
|
width: auto;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-container.leaflet-touch-zoom {
|
||||||
|
-ms-touch-action: pan-x pan-y;
|
||||||
|
touch-action: pan-x pan-y;
|
||||||
|
}
|
||||||
|
.leaflet-container.leaflet-touch-drag {
|
||||||
|
-ms-touch-action: pinch-zoom;
|
||||||
|
/* Fallback for FF which doesn't support pinch-zoom */
|
||||||
|
touch-action: none;
|
||||||
|
touch-action: pinch-zoom;
|
||||||
|
}
|
||||||
|
.leaflet-container.leaflet-touch-drag.leaflet-touch-zoom {
|
||||||
|
-ms-touch-action: none;
|
||||||
|
touch-action: none;
|
||||||
|
}
|
||||||
|
.leaflet-container {
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
}
|
||||||
|
.leaflet-container a {
|
||||||
|
-webkit-tap-highlight-color: rgba(51, 181, 229, 0.4);
|
||||||
|
}
|
||||||
|
.leaflet-tile {
|
||||||
|
filter: inherit;
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
.leaflet-tile-loaded {
|
||||||
|
visibility: inherit;
|
||||||
|
}
|
||||||
|
.leaflet-zoom-box {
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
-moz-box-sizing: border-box;
|
||||||
|
box-sizing: border-box;
|
||||||
|
z-index: 800;
|
||||||
|
}
|
||||||
|
/* workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=888319 */
|
||||||
|
.leaflet-overlay-pane svg {
|
||||||
|
-moz-user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-pane { z-index: 400; }
|
||||||
|
|
||||||
|
.leaflet-tile-pane { z-index: 200; }
|
||||||
|
.leaflet-overlay-pane { z-index: 400; }
|
||||||
|
.leaflet-shadow-pane { z-index: 500; }
|
||||||
|
.leaflet-marker-pane { z-index: 600; }
|
||||||
|
.leaflet-tooltip-pane { z-index: 650; }
|
||||||
|
.leaflet-popup-pane { z-index: 700; }
|
||||||
|
|
||||||
|
.leaflet-map-pane canvas { z-index: 100; }
|
||||||
|
.leaflet-map-pane svg { z-index: 200; }
|
||||||
|
|
||||||
|
.leaflet-vml-shape {
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
}
|
||||||
|
.lvml {
|
||||||
|
behavior: url(#default#VML);
|
||||||
|
display: inline-block;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* control positioning */
|
||||||
|
|
||||||
|
.leaflet-control {
|
||||||
|
position: relative;
|
||||||
|
z-index: 800;
|
||||||
|
pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
.leaflet-top,
|
||||||
|
.leaflet-bottom {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 1000;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.leaflet-top {
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
.leaflet-right {
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
.leaflet-bottom {
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
.leaflet-left {
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
.leaflet-control {
|
||||||
|
float: left;
|
||||||
|
clear: both;
|
||||||
|
}
|
||||||
|
.leaflet-right .leaflet-control {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
.leaflet-top .leaflet-control {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
.leaflet-bottom .leaflet-control {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.leaflet-left .leaflet-control {
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
.leaflet-right .leaflet-control {
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* zoom and fade animations */
|
||||||
|
|
||||||
|
.leaflet-fade-anim .leaflet-popup {
|
||||||
|
opacity: 0;
|
||||||
|
-webkit-transition: opacity 0.2s linear;
|
||||||
|
-moz-transition: opacity 0.2s linear;
|
||||||
|
transition: opacity 0.2s linear;
|
||||||
|
}
|
||||||
|
.leaflet-fade-anim .leaflet-map-pane .leaflet-popup {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
.leaflet-zoom-animated {
|
||||||
|
-webkit-transform-origin: 0 0;
|
||||||
|
-ms-transform-origin: 0 0;
|
||||||
|
transform-origin: 0 0;
|
||||||
|
}
|
||||||
|
svg.leaflet-zoom-animated {
|
||||||
|
will-change: transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-zoom-anim .leaflet-zoom-animated {
|
||||||
|
-webkit-transition: -webkit-transform 0.25s cubic-bezier(0,0,0.25,1);
|
||||||
|
-moz-transition: -moz-transform 0.25s cubic-bezier(0,0,0.25,1);
|
||||||
|
transition: transform 0.25s cubic-bezier(0,0,0.25,1);
|
||||||
|
}
|
||||||
|
.leaflet-zoom-anim .leaflet-tile,
|
||||||
|
.leaflet-pan-anim .leaflet-tile {
|
||||||
|
-webkit-transition: none;
|
||||||
|
-moz-transition: none;
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-zoom-anim .leaflet-zoom-hide {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* cursors */
|
||||||
|
|
||||||
|
.leaflet-interactive {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.leaflet-grab {
|
||||||
|
cursor: -webkit-grab;
|
||||||
|
cursor: -moz-grab;
|
||||||
|
cursor: grab;
|
||||||
|
}
|
||||||
|
.leaflet-crosshair,
|
||||||
|
.leaflet-crosshair .leaflet-interactive {
|
||||||
|
cursor: crosshair;
|
||||||
|
}
|
||||||
|
.leaflet-popup-pane,
|
||||||
|
.leaflet-control {
|
||||||
|
cursor: auto;
|
||||||
|
}
|
||||||
|
.leaflet-dragging .leaflet-grab,
|
||||||
|
.leaflet-dragging .leaflet-grab .leaflet-interactive,
|
||||||
|
.leaflet-dragging .leaflet-marker-draggable {
|
||||||
|
cursor: move;
|
||||||
|
cursor: -webkit-grabbing;
|
||||||
|
cursor: -moz-grabbing;
|
||||||
|
cursor: grabbing;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* marker & overlays interactivity */
|
||||||
|
.leaflet-marker-icon,
|
||||||
|
.leaflet-marker-shadow,
|
||||||
|
.leaflet-image-layer,
|
||||||
|
.leaflet-pane > svg path,
|
||||||
|
.leaflet-tile-container {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-marker-icon.leaflet-interactive,
|
||||||
|
.leaflet-image-layer.leaflet-interactive,
|
||||||
|
.leaflet-pane > svg path.leaflet-interactive,
|
||||||
|
svg.leaflet-image-layer.leaflet-interactive path {
|
||||||
|
pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* visual tweaks */
|
||||||
|
|
||||||
|
.leaflet-container {
|
||||||
|
background: #ddd;
|
||||||
|
outline-offset: 1px;
|
||||||
|
}
|
||||||
|
.leaflet-container a {
|
||||||
|
color: #0078A8;
|
||||||
|
}
|
||||||
|
.leaflet-zoom-box {
|
||||||
|
border: 2px dotted #38f;
|
||||||
|
background: rgba(255,255,255,0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* general typography */
|
||||||
|
.leaflet-container {
|
||||||
|
font-family: "Helvetica Neue", Arial, Helvetica, sans-serif;
|
||||||
|
font-size: 12px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* general toolbar styles */
|
||||||
|
|
||||||
|
.leaflet-bar {
|
||||||
|
box-shadow: 0 1px 5px rgba(0,0,0,0.65);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.leaflet-bar a {
|
||||||
|
background-color: #fff;
|
||||||
|
border-bottom: 1px solid #ccc;
|
||||||
|
width: 26px;
|
||||||
|
height: 26px;
|
||||||
|
line-height: 26px;
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
text-decoration: none;
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
.leaflet-bar a,
|
||||||
|
.leaflet-control-layers-toggle {
|
||||||
|
background-position: 50% 50%;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.leaflet-bar a:hover,
|
||||||
|
.leaflet-bar a:focus {
|
||||||
|
background-color: #f4f4f4;
|
||||||
|
}
|
||||||
|
.leaflet-bar a:first-child {
|
||||||
|
border-top-left-radius: 4px;
|
||||||
|
border-top-right-radius: 4px;
|
||||||
|
}
|
||||||
|
.leaflet-bar a:last-child {
|
||||||
|
border-bottom-left-radius: 4px;
|
||||||
|
border-bottom-right-radius: 4px;
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
.leaflet-bar a.leaflet-disabled {
|
||||||
|
cursor: default;
|
||||||
|
background-color: #f4f4f4;
|
||||||
|
color: #bbb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-touch .leaflet-bar a {
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
line-height: 30px;
|
||||||
|
}
|
||||||
|
.leaflet-touch .leaflet-bar a:first-child {
|
||||||
|
border-top-left-radius: 2px;
|
||||||
|
border-top-right-radius: 2px;
|
||||||
|
}
|
||||||
|
.leaflet-touch .leaflet-bar a:last-child {
|
||||||
|
border-bottom-left-radius: 2px;
|
||||||
|
border-bottom-right-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* zoom control */
|
||||||
|
|
||||||
|
.leaflet-control-zoom-in,
|
||||||
|
.leaflet-control-zoom-out {
|
||||||
|
font: bold 18px 'Lucida Console', Monaco, monospace;
|
||||||
|
text-indent: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-touch .leaflet-control-zoom-in, .leaflet-touch .leaflet-control-zoom-out {
|
||||||
|
font-size: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* layers control */
|
||||||
|
|
||||||
|
.leaflet-control-layers {
|
||||||
|
box-shadow: 0 1px 5px rgba(0,0,0,0.4);
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
.leaflet-control-layers-toggle {
|
||||||
|
background-image: url(images/layers.png);
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
}
|
||||||
|
.leaflet-retina .leaflet-control-layers-toggle {
|
||||||
|
background-image: url(images/layers-2x.png);
|
||||||
|
background-size: 26px 26px;
|
||||||
|
}
|
||||||
|
.leaflet-touch .leaflet-control-layers-toggle {
|
||||||
|
width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
}
|
||||||
|
.leaflet-control-layers .leaflet-control-layers-list,
|
||||||
|
.leaflet-control-layers-expanded .leaflet-control-layers-toggle {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.leaflet-control-layers-expanded .leaflet-control-layers-list {
|
||||||
|
display: block;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.leaflet-control-layers-expanded {
|
||||||
|
padding: 6px 10px 6px 6px;
|
||||||
|
color: #333;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
.leaflet-control-layers-scrollbar {
|
||||||
|
overflow-y: scroll;
|
||||||
|
overflow-x: hidden;
|
||||||
|
padding-right: 5px;
|
||||||
|
}
|
||||||
|
.leaflet-control-layers-selector {
|
||||||
|
margin-top: 2px;
|
||||||
|
position: relative;
|
||||||
|
top: 1px;
|
||||||
|
}
|
||||||
|
.leaflet-control-layers label {
|
||||||
|
display: block;
|
||||||
|
font-size: 13px;
|
||||||
|
font-size: 1.08333em;
|
||||||
|
}
|
||||||
|
.leaflet-control-layers-separator {
|
||||||
|
height: 0;
|
||||||
|
border-top: 1px solid #ddd;
|
||||||
|
margin: 5px -10px 5px -6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Default icon URLs */
|
||||||
|
.leaflet-default-icon-path { /* used only in path-guessing heuristic, see L.Icon.Default */
|
||||||
|
background-image: url(images/marker-icon.png);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* attribution and scale controls */
|
||||||
|
|
||||||
|
.leaflet-container .leaflet-control-attribution {
|
||||||
|
background: #fff;
|
||||||
|
background: rgba(255, 255, 255, 0.8);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.leaflet-control-attribution,
|
||||||
|
.leaflet-control-scale-line {
|
||||||
|
padding: 0 5px;
|
||||||
|
color: #333;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
.leaflet-control-attribution a {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.leaflet-control-attribution a:hover,
|
||||||
|
.leaflet-control-attribution a:focus {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
.leaflet-attribution-flag {
|
||||||
|
display: inline !important;
|
||||||
|
vertical-align: baseline !important;
|
||||||
|
width: 1em;
|
||||||
|
height: 0.6669em;
|
||||||
|
}
|
||||||
|
.leaflet-left .leaflet-control-scale {
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
|
.leaflet-bottom .leaflet-control-scale {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
.leaflet-control-scale-line {
|
||||||
|
border: 2px solid #777;
|
||||||
|
border-top: none;
|
||||||
|
line-height: 1.1;
|
||||||
|
padding: 2px 5px 1px;
|
||||||
|
white-space: nowrap;
|
||||||
|
-moz-box-sizing: border-box;
|
||||||
|
box-sizing: border-box;
|
||||||
|
background: rgba(255, 255, 255, 0.8);
|
||||||
|
text-shadow: 1px 1px #fff;
|
||||||
|
}
|
||||||
|
.leaflet-control-scale-line:not(:first-child) {
|
||||||
|
border-top: 2px solid #777;
|
||||||
|
border-bottom: none;
|
||||||
|
margin-top: -2px;
|
||||||
|
}
|
||||||
|
.leaflet-control-scale-line:not(:first-child):not(:last-child) {
|
||||||
|
border-bottom: 2px solid #777;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-touch .leaflet-control-attribution,
|
||||||
|
.leaflet-touch .leaflet-control-layers,
|
||||||
|
.leaflet-touch .leaflet-bar {
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
.leaflet-touch .leaflet-control-layers,
|
||||||
|
.leaflet-touch .leaflet-bar {
|
||||||
|
border: 2px solid rgba(0,0,0,0.2);
|
||||||
|
background-clip: padding-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* popup */
|
||||||
|
|
||||||
|
.leaflet-popup {
|
||||||
|
position: absolute;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.leaflet-popup-content-wrapper {
|
||||||
|
padding: 1px;
|
||||||
|
text-align: left;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
.leaflet-popup-content {
|
||||||
|
margin: 13px 24px 13px 20px;
|
||||||
|
line-height: 1.3;
|
||||||
|
font-size: 13px;
|
||||||
|
font-size: 1.08333em;
|
||||||
|
min-height: 1px;
|
||||||
|
}
|
||||||
|
.leaflet-popup-content p {
|
||||||
|
margin: 17px 0;
|
||||||
|
margin: 1.3em 0;
|
||||||
|
}
|
||||||
|
.leaflet-popup-tip-container {
|
||||||
|
width: 40px;
|
||||||
|
height: 20px;
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
margin-top: -1px;
|
||||||
|
margin-left: -20px;
|
||||||
|
overflow: hidden;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.leaflet-popup-tip {
|
||||||
|
width: 17px;
|
||||||
|
height: 17px;
|
||||||
|
padding: 1px;
|
||||||
|
|
||||||
|
margin: -10px auto 0;
|
||||||
|
pointer-events: auto;
|
||||||
|
|
||||||
|
-webkit-transform: rotate(45deg);
|
||||||
|
-moz-transform: rotate(45deg);
|
||||||
|
-ms-transform: rotate(45deg);
|
||||||
|
transform: rotate(45deg);
|
||||||
|
}
|
||||||
|
.leaflet-popup-content-wrapper,
|
||||||
|
.leaflet-popup-tip {
|
||||||
|
background: white;
|
||||||
|
color: #333;
|
||||||
|
box-shadow: 0 3px 14px rgba(0,0,0,0.4);
|
||||||
|
}
|
||||||
|
.leaflet-container a.leaflet-popup-close-button {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
border: none;
|
||||||
|
text-align: center;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
font: 16px/24px Tahoma, Verdana, sans-serif;
|
||||||
|
color: #757575;
|
||||||
|
text-decoration: none;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
.leaflet-container a.leaflet-popup-close-button:hover,
|
||||||
|
.leaflet-container a.leaflet-popup-close-button:focus {
|
||||||
|
color: #585858;
|
||||||
|
}
|
||||||
|
.leaflet-popup-scrolled {
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-oldie .leaflet-popup-content-wrapper {
|
||||||
|
-ms-zoom: 1;
|
||||||
|
}
|
||||||
|
.leaflet-oldie .leaflet-popup-tip {
|
||||||
|
width: 24px;
|
||||||
|
margin: 0 auto;
|
||||||
|
|
||||||
|
-ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)";
|
||||||
|
filter: progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678);
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-oldie .leaflet-control-zoom,
|
||||||
|
.leaflet-oldie .leaflet-control-layers,
|
||||||
|
.leaflet-oldie .leaflet-popup-content-wrapper,
|
||||||
|
.leaflet-oldie .leaflet-popup-tip {
|
||||||
|
border: 1px solid #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* div icon */
|
||||||
|
|
||||||
|
.leaflet-div-icon {
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Tooltip */
|
||||||
|
/* Base styles for the element that has a tooltip */
|
||||||
|
.leaflet-tooltip {
|
||||||
|
position: absolute;
|
||||||
|
padding: 6px;
|
||||||
|
background-color: #fff;
|
||||||
|
border: 1px solid #fff;
|
||||||
|
border-radius: 3px;
|
||||||
|
color: #222;
|
||||||
|
white-space: nowrap;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
-ms-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
pointer-events: none;
|
||||||
|
box-shadow: 0 1px 3px rgba(0,0,0,0.4);
|
||||||
|
}
|
||||||
|
.leaflet-tooltip.leaflet-interactive {
|
||||||
|
cursor: pointer;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
.leaflet-tooltip-top:before,
|
||||||
|
.leaflet-tooltip-bottom:before,
|
||||||
|
.leaflet-tooltip-left:before,
|
||||||
|
.leaflet-tooltip-right:before {
|
||||||
|
position: absolute;
|
||||||
|
pointer-events: none;
|
||||||
|
border: 6px solid transparent;
|
||||||
|
background: transparent;
|
||||||
|
content: "";
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Directions */
|
||||||
|
|
||||||
|
.leaflet-tooltip-bottom {
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
.leaflet-tooltip-top {
|
||||||
|
margin-top: -6px;
|
||||||
|
}
|
||||||
|
.leaflet-tooltip-bottom:before,
|
||||||
|
.leaflet-tooltip-top:before {
|
||||||
|
left: 50%;
|
||||||
|
margin-left: -6px;
|
||||||
|
}
|
||||||
|
.leaflet-tooltip-top:before {
|
||||||
|
bottom: 0;
|
||||||
|
margin-bottom: -12px;
|
||||||
|
border-top-color: #fff;
|
||||||
|
}
|
||||||
|
.leaflet-tooltip-bottom:before {
|
||||||
|
top: 0;
|
||||||
|
margin-top: -12px;
|
||||||
|
margin-left: -6px;
|
||||||
|
border-bottom-color: #fff;
|
||||||
|
}
|
||||||
|
.leaflet-tooltip-left {
|
||||||
|
margin-left: -6px;
|
||||||
|
}
|
||||||
|
.leaflet-tooltip-right {
|
||||||
|
margin-left: 6px;
|
||||||
|
}
|
||||||
|
.leaflet-tooltip-left:before,
|
||||||
|
.leaflet-tooltip-right:before {
|
||||||
|
top: 50%;
|
||||||
|
margin-top: -6px;
|
||||||
|
}
|
||||||
|
.leaflet-tooltip-left:before {
|
||||||
|
right: 0;
|
||||||
|
margin-right: -12px;
|
||||||
|
border-left-color: #fff;
|
||||||
|
}
|
||||||
|
.leaflet-tooltip-right:before {
|
||||||
|
left: 0;
|
||||||
|
margin-left: -12px;
|
||||||
|
border-right-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Printing */
|
||||||
|
|
||||||
|
@media print {
|
||||||
|
/* Prevent printers from removing background-images of controls. */
|
||||||
|
.leaflet-control {
|
||||||
|
-webkit-print-color-adjust: exact;
|
||||||
|
print-color-adjust: exact;
|
||||||
|
}
|
||||||
|
}
|
||||||
6
public/resources/leaflet.js
Normal file
6
public/resources/leaflet.js
Normal file
File diff suppressed because one or more lines are too long
1
public/resources/leaflet.js.map
Normal file
1
public/resources/leaflet.js.map
Normal file
File diff suppressed because one or more lines are too long
@@ -1,40 +0,0 @@
|
|||||||
.mapbox-gl-inspect_popup {
|
|
||||||
color: #333;
|
|
||||||
display: table;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mapbox-gl-inspect_feature:not(:last-child) {
|
|
||||||
border-bottom: 1px solid #ccc;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mapbox-gl-inspect_layer:before {
|
|
||||||
content: '#';
|
|
||||||
}
|
|
||||||
|
|
||||||
.mapbox-gl-inspect_layer {
|
|
||||||
display: block;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mapbox-gl-inspect_property {
|
|
||||||
display: table-row;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mapbox-gl-inspect_property-value {
|
|
||||||
display: table-cell;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
.mapbox-gl-inspect_property-name {
|
|
||||||
display: table-cell;
|
|
||||||
padding-right: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mapboxgl-ctrl-inspect {
|
|
||||||
background-image: url('data:image/svg+xml;charset=utf8,<svg%20xmlns="http://www.w3.org/2000/svg"%20fill="#333333%22%20preserveAspectRatio=%22xMidYMid%20meet%22%20viewBox=%22-10%20-10%2060%2060%22%3E%3Cg%3E%3Cpath%20d=%22m15%2021.6q0-2%201.5-3.5t3.5-1.5%203.5%201.5%201.5%203.5-1.5%203.6-3.5%201.4-3.5-1.4-1.5-3.6z%20m18.4%2011.1l-6.4-6.5q1.4-2.1%201.4-4.6%200-3.4-2.5-5.8t-5.9-2.4-5.9%202.4-2.5%205.8%202.5%205.9%205.9%202.5q2.4%200%204.6-1.4l7.4%207.4q-0.9%200.6-2%200.6h-20q-1.3%200-2.3-0.9t-1.1-2.3l0.1-26.8q0-1.3%201-2.3t2.3-0.9h13.4l10%2010v19.3z%22%3E%3C/path%3E%3C/g%3E%3C/svg%3E');
|
|
||||||
}
|
|
||||||
|
|
||||||
.mapboxgl-ctrl-map {
|
|
||||||
background-image: url('data:image/svg+xml;charset=utf8,<svg%20xmlns="http://www.w3.org/2000/svg"%20fill="#333333%22%20viewBox=%22-10%20-10%2060%2060%22%20preserveAspectRatio=%22xMidYMid%20meet%22%3E%3Cg%3E%3Cpath%20d=%22m25%2031.640000000000004v-19.766666666666673l-10-3.511666666666663v19.766666666666666z%20m9.140000000000008-26.640000000000004q0.8599999999999923%200%200.8599999999999923%200.8600000000000003v25.156666666666666q0%200.625-0.625%200.783333333333335l-9.375%203.1999999999999993-10-3.5133333333333354-8.906666666666668%203.4383333333333326-0.2333333333333334%200.07833333333333314q-0.8616666666666664%200-0.8616666666666664-0.8599999999999994v-25.156666666666663q0-0.625%200.6233333333333331-0.7833333333333332l9.378333333333334-3.198333333333334%2010%203.5133333333333336%208.905000000000001-3.4383333333333344z%22%3E%3C/path%3E%3C/g%3E%3C/svg%3E');
|
|
||||||
}
|
|
||||||
|
|
||||||
1
public/resources/mapbox-gl-inspect.min.js
vendored
1
public/resources/mapbox-gl-inspect.min.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
40
public/resources/maplibre-gl-inspect.css
Normal file
40
public/resources/maplibre-gl-inspect.css
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
.maplibregl-inspect_popup {
|
||||||
|
color: #333;
|
||||||
|
display: table;
|
||||||
|
}
|
||||||
|
|
||||||
|
.maplibregl-inspect_feature:not(:last-child) {
|
||||||
|
border-bottom: 1px solid #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.maplibregl-inspect_layer:before {
|
||||||
|
content: '#';
|
||||||
|
}
|
||||||
|
|
||||||
|
.maplibregl-inspect_layer {
|
||||||
|
display: block;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.maplibregl-inspect_property {
|
||||||
|
display: table-row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.maplibregl-inspect_property-value {
|
||||||
|
display: table-cell;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.maplibregl-inspect_property-name {
|
||||||
|
display: table-cell;
|
||||||
|
padding-right: 10px;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.maplibregl-ctrl-inspect {
|
||||||
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23333333' preserveAspectRatio='xMidYMid meet' viewBox='-10 -10 60 60'%3E%3Cg%3E%3Cpath d='m15 21.6q0-2 1.5-3.5t3.5-1.5 3.5 1.5 1.5 3.5-1.5 3.6-3.5 1.4-3.5-1.4-1.5-3.6z m18.4 11.1l-6.4-6.5q1.4-2.1 1.4-4.6 0-3.4-2.5-5.8t-5.9-2.4-5.9 2.4-2.5 5.8 2.5 5.9 5.9 2.5q2.4 0 4.6-1.4l7.4 7.4q-0.9 0.6-2 0.6h-20q-1.3 0-2.3-0.9t-1.1-2.3l0.1-26.8q0-1.3 1-2.3t2.3-0.9h13.4l10 10v19.3z'%3E%3C/path%3E%3C/g%3E%3C/svg%3E");
|
||||||
|
}
|
||||||
|
|
||||||
|
.maplibregl-ctrl-map {
|
||||||
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23333333' viewBox='-10 -10 60 60' preserveAspectRatio='xMidYMid meet'%3E%3Cg%3E%3Cpath d='m25 31.640000000000004v-19.766666666666673l-10-3.511666666666663v19.766666666666666z m9.140000000000008-26.640000000000004q0.8599999999999923 0 0.8599999999999923 0.8600000000000003v25.156666666666666q0 0.625-0.625 0.783333333333335l-9.375 3.1999999999999993-10-3.5133333333333354-8.906666666666668 3.4383333333333326-0.2333333333333334 0.07833333333333314q-0.8616666666666664 0-0.8616666666666664-0.8599999999999994v-25.156666666666663q0-0.625 0.6233333333333331-0.7833333333333332l9.378333333333334-3.198333333333334 10 3.5133333333333336 8.905000000000001-3.4383333333333344z'%3E%3C/path%3E%3C/g%3E%3C/svg%3E");
|
||||||
|
}
|
||||||
1
public/resources/maplibre-gl-inspect.min.js
vendored
Normal file
1
public/resources/maplibre-gl-inspect.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
public/resources/maplibre-gl.css
Normal file
1
public/resources/maplibre-gl.css
Normal file
File diff suppressed because one or more lines are too long
48
public/resources/maplibre-gl.js
Normal file
48
public/resources/maplibre-gl.js
Normal file
File diff suppressed because one or more lines are too long
1
public/resources/maplibre-gl.js.map
Normal file
1
public/resources/maplibre-gl.js.map
Normal file
File diff suppressed because one or more lines are too long
@@ -5,10 +5,10 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>{{name}} - TileServer GL</title>
|
<title>{{name}} - TileServer GL</title>
|
||||||
{{#is_vector}}
|
{{#is_vector}}
|
||||||
<link rel="stylesheet" type="text/css" href="{{public_url}}mapbox-gl.css{{&key_query}}" />
|
<link rel="stylesheet" type="text/css" href="{{public_url}}maplibre-gl.css{{&key_query}}" />
|
||||||
<link rel="stylesheet" type="text/css" href="{{public_url}}mapbox-gl-inspect.css{{&key_query}}" />
|
<link rel="stylesheet" type="text/css" href="{{public_url}}maplibre-gl-inspect.css{{&key_query}}" />
|
||||||
<script src="{{public_url}}mapbox-gl.js{{&key_query}}"></script>
|
<script src="{{public_url}}maplibre-gl.js{{&key_query}}"></script>
|
||||||
<script src="{{public_url}}mapbox-gl-inspect.min.js{{&key_query}}"></script>
|
<script src="{{public_url}}maplibre-gl-inspect.min.js{{&key_query}}"></script>
|
||||||
<style>
|
<style>
|
||||||
body {background:#fff;color:#333;font-family:Arial, sans-serif;}
|
body {background:#fff;color:#333;font-family:Arial, sans-serif;}
|
||||||
#map {position:absolute;top:0;left:0;right:250px;bottom:0;}
|
#map {position:absolute;top:0;left:0;right:250px;bottom:0;}
|
||||||
@@ -18,12 +18,25 @@
|
|||||||
</style>
|
</style>
|
||||||
{{/is_vector}}
|
{{/is_vector}}
|
||||||
{{^is_vector}}
|
{{^is_vector}}
|
||||||
<link rel="stylesheet" type="text/css" href="{{public_url}}mapbox.css{{&key_query}}" />
|
<link rel="stylesheet" type="text/css" href="{{public_url}}leaflet.css{{&key_query}}" />
|
||||||
<script src="{{public_url}}mapbox.js{{&key_query}}"></script>
|
<script src="{{public_url}}leaflet.js{{&key_query}}"></script>
|
||||||
<script src="{{public_url}}leaflet-hash.js{{&key_query}}"></script>
|
<script src="{{public_url}}leaflet-hash.js{{&key_query}}"></script>
|
||||||
|
<script src="{{public_url}}L.TileLayer.NoGap.js{{&key_query}}"></script>
|
||||||
<style>
|
<style>
|
||||||
body { margin:0; padding:0; }
|
body { margin:0; padding:0; }
|
||||||
#map { position:absolute; top:0; bottom:0; width:100%; }
|
#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>
|
</style>
|
||||||
{{/is_vector}}
|
{{/is_vector}}
|
||||||
</head>
|
</head>
|
||||||
@@ -34,22 +47,27 @@
|
|||||||
<div id="layerList"></div>
|
<div id="layerList"></div>
|
||||||
<pre id="propertyList"></pre>
|
<pre id="propertyList"></pre>
|
||||||
<script>
|
<script>
|
||||||
var map = new mapboxgl.Map({
|
var keyMatch = location.search.match(/[\?\&]key=([^&]+)/i);
|
||||||
|
var keyParam = keyMatch ? '?key=' + keyMatch[1] : '';
|
||||||
|
|
||||||
|
var map = new maplibregl.Map({
|
||||||
container: 'map',
|
container: 'map',
|
||||||
hash: true,
|
hash: true,
|
||||||
|
maplibreLogo: true,
|
||||||
|
maxPitch: 85,
|
||||||
style: {
|
style: {
|
||||||
version: 8,
|
version: 8,
|
||||||
sources: {
|
sources: {
|
||||||
'vector_layer_': {
|
'vector_layer_': {
|
||||||
type: 'vector',
|
type: 'vector',
|
||||||
url: '{{public_url}}data/{{id}}.json{{&key_query}}'
|
url: '{{public_url}}data/{{id}}.json' + keyParam
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
layers: []
|
layers: []
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
map.addControl(new mapboxgl.NavigationControl());
|
map.addControl(new maplibregl.NavigationControl());
|
||||||
var inspect = new MapboxInspect({
|
var inspect = new MaplibreInspect({
|
||||||
showInspectMap: true,
|
showInspectMap: true,
|
||||||
showInspectButton: false
|
showInspectButton: false
|
||||||
});
|
});
|
||||||
@@ -74,12 +92,52 @@
|
|||||||
<h1 style="display:none;">{{name}}</h1>
|
<h1 style="display:none;">{{name}}</h1>
|
||||||
<div id='map'></div>
|
<div id='map'></div>
|
||||||
<script>
|
<script>
|
||||||
var map = L.mapbox.map('map', '{{public_url}}data/{{id}}.json{{&key_query}}', { zoomControl: false });
|
var keyMatch = location.search.match(/[\?\&]key=([^&]+)/i);
|
||||||
map.eachLayer(function(layer) {
|
var keyParam = keyMatch ? '?key=' + keyMatch[1] : '';
|
||||||
// do not add scale prefix even if retina display is detected
|
|
||||||
layer.scalePrefix = '.';
|
var map = L.map('map', { zoomControl: false });
|
||||||
});
|
|
||||||
new L.Control.Zoom({ position: 'topright' }).addTo(map);
|
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], {
|
||||||
|
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() {
|
setTimeout(function() {
|
||||||
new L.Hash(map);
|
new L.Hash(map);
|
||||||
}, 0);
|
}, 0);
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<section>
|
<section>
|
||||||
<h1 class="title {{#if is_light}}light{{/if}}"><img src="{{public_url}}images/logo.png" alt="TileServer GL" /></h1>
|
<h1 class="title {{#if is_light}}light{{/if}}"><img src="{{public_url}}images/logo.png{{&key_query}}" alt="TileServer GL" /></h1>
|
||||||
<h2 class="subtitle">Vector {{#if is_light}}<s>and raster</s>{{else}}and raster{{/if}} maps with GL styles</h2>
|
<h2 class="subtitle">Vector {{#if is_light}}<s>and raster</s>{{else}}and raster{{/if}} maps with GL styles</h2>
|
||||||
{{#if styles}}
|
{{#if styles}}
|
||||||
<h2 class="box-header">Styles</h2>
|
<h2 class="box-header">Styles</h2>
|
||||||
@@ -27,7 +27,7 @@
|
|||||||
{{#if thumbnail}}
|
{{#if thumbnail}}
|
||||||
<img src="{{public_url}}styles/{{@key}}/{{thumbnail}}{{&../key_query}}" alt="{{name}} preview" />
|
<img src="{{public_url}}styles/{{@key}}/{{thumbnail}}{{&../key_query}}" alt="{{name}} preview" />
|
||||||
{{else}}
|
{{else}}
|
||||||
<img src="{{public_url}}images/placeholder.png" alt="{{name}} preview" />
|
<img src="{{public_url}}images/placeholder.png{{&key_query}}" alt="{{name}} preview" />
|
||||||
{{/if}}
|
{{/if}}
|
||||||
<div class="details">
|
<div class="details">
|
||||||
<h3>{{name}}</h3>
|
<h3>{{name}}</h3>
|
||||||
@@ -41,7 +41,7 @@
|
|||||||
{{#if serving_data}}| {{/if}}<a href="{{public_url}}styles/{{@key}}.json{{&../key_query}}">TileJSON</a>
|
{{#if serving_data}}| {{/if}}<a href="{{public_url}}styles/{{@key}}.json{{&../key_query}}">TileJSON</a>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{#if serving_rendered}}
|
{{#if serving_rendered}}
|
||||||
| <a href="/styles/{{@key}}/wmts.xml{{&../key_query}}">WMTS</a>
|
| <a href="{{public_url}}styles/{{@key}}/wmts.xml{{&../key_query}}">WMTS</a>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{#if xyz_link}}
|
{{#if xyz_link}}
|
||||||
| <a href="#" onclick="return toggle_xyz('xyz_style_{{@key}}');">XYZ</a>
|
| <a href="#" onclick="return toggle_xyz('xyz_style_{{@key}}');">XYZ</a>
|
||||||
@@ -74,7 +74,7 @@
|
|||||||
{{#if thumbnail}}
|
{{#if thumbnail}}
|
||||||
<img src="{{public_url}}data/{{@key}}/{{thumbnail}}{{&../key_query}}" alt="{{name}} preview" />
|
<img src="{{public_url}}data/{{@key}}/{{thumbnail}}{{&../key_query}}" alt="{{name}} preview" />
|
||||||
{{else}}
|
{{else}}
|
||||||
<img src="{{public_url}}images/placeholder.png" alt="{{name}} preview" />
|
<img src="{{public_url}}images/placeholder.png{{&key_query}}" alt="{{name}} preview" />
|
||||||
{{/if}}
|
{{/if}}
|
||||||
<div class="details">
|
<div class="details">
|
||||||
<h3>{{name}}</h3>
|
<h3>{{name}}</h3>
|
||||||
@@ -104,10 +104,10 @@
|
|||||||
{{/if}}
|
{{/if}}
|
||||||
</section>
|
</section>
|
||||||
<footer>
|
<footer>
|
||||||
<a href="https://www.maptiler.com/" target="_blank"><img src="{{public_url}}images/maptiler-logo.svg" /></a>
|
<a href="https://www.maptiler.com/" target="_blank"><img src="{{public_url}}images/maptiler-logo.svg{{&key_query}}" /></a>
|
||||||
<p>
|
<p>
|
||||||
<a href="https://github.com/maptiler/tileserver-gl" target="_blank">Powered by TileServer GL ({{server_version}})</a> – <a href="https://www.maptiler.com/" target="_blank">an open-source project from MapTiler.</a>
|
<a href="https://github.com/maptiler/tileserver-gl" target="_blank">Powered by TileServer GL ({{server_version}})</a> – <a href="https://www.maptiler.com/" target="_blank">an open-source project from MapTiler.</a>
|
||||||
</p>
|
</p>
|
||||||
</footer>
|
</footer>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -4,39 +4,126 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>{{name}} - TileServer GL</title>
|
<title>{{name}} - TileServer GL</title>
|
||||||
<link rel="stylesheet" type="text/css" href="{{public_url}}mapbox-gl.css{{&key_query}}" />
|
<link rel="stylesheet" type="text/css" href="{{public_url}}maplibre-gl.css{{&key_query}}" />
|
||||||
<script src="{{public_url}}mapbox-gl.js{{&key_query}}"></script>
|
<link rel="stylesheet" type="text/css" href="{{public_url}}maplibre-gl-inspect.css{{&key_query}}" />
|
||||||
<link rel="stylesheet" type="text/css" href="{{public_url}}mapbox.css{{&key_query}}" />
|
<link rel="stylesheet" type="text/css" href="{{public_url}}leaflet.css{{&key_query}}" />
|
||||||
<script src="{{public_url}}mapbox.js{{&key_query}}"></script>
|
<script src="{{public_url}}maplibre-gl.js{{&key_query}}"></script>
|
||||||
|
<script src="{{public_url}}maplibre-gl-inspect.min.js{{&key_query}}"></script>
|
||||||
|
<script src="{{public_url}}leaflet.js{{&key_query}}"></script>
|
||||||
<script src="{{public_url}}leaflet-hash.js{{&key_query}}"></script>
|
<script src="{{public_url}}leaflet-hash.js{{&key_query}}"></script>
|
||||||
|
<script src="{{public_url}}L.TileLayer.NoGap.js{{&key_query}}"></script>
|
||||||
<style>
|
<style>
|
||||||
body { margin:0; padding:0; }
|
body { margin:0; padding:0; }
|
||||||
#map { position:absolute; top:0; bottom:0; width:100%; }
|
#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>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1 style="display:none;">{{name}}</h1>
|
<h1 style="display:none;">{{name}}</h1>
|
||||||
<div id='map'></div>
|
<div id='map'></div>
|
||||||
<script>
|
<script>
|
||||||
|
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 q = (location.search || '').substr(1).split('&');
|
||||||
var preference =
|
var preference =
|
||||||
q.indexOf('vector') >= 0 ? 'vector' :
|
q.indexOf('vector') >= 0 ? 'vector' :
|
||||||
(q.indexOf('raster') >= 0 ? 'raster' :
|
(q.indexOf('raster') >= 0 ? 'raster' :
|
||||||
(mapboxgl.supported() ? 'vector' : 'raster'));
|
(isWebglSupported() ? 'vector' : 'raster'));
|
||||||
|
|
||||||
|
var keyMatch = location.search.match(/[\?\&]key=([^&]+)/i);
|
||||||
|
var keyParam = keyMatch ? '?key=' + keyMatch[1] : '';
|
||||||
|
|
||||||
if (preference == 'vector') {
|
if (preference == 'vector') {
|
||||||
mapboxgl.setRTLTextPlugin('{{public_url}}mapbox-gl-rtl-text.js{{&key_query}}');
|
maplibregl.setRTLTextPlugin('{{public_url}}mapbox-gl-rtl-text.js' + keyParam);
|
||||||
var map = new mapboxgl.Map({
|
var map = new maplibregl.Map({
|
||||||
container: 'map',
|
container: 'map',
|
||||||
style: '{{public_url}}styles/{{id}}/style.json{{&key_query}}',
|
style: '{{public_url}}styles/{{id}}/style.json' + keyParam,
|
||||||
hash: true
|
hash: true,
|
||||||
|
maplibreLogo: true,
|
||||||
|
maxPitch: 85
|
||||||
});
|
});
|
||||||
map.addControl(new mapboxgl.NavigationControl());
|
map.addControl(new maplibregl.NavigationControl({
|
||||||
|
visualizePitch: true,
|
||||||
|
showZoom: true,
|
||||||
|
showCompass: true
|
||||||
|
}));
|
||||||
|
map.addControl(new MaplibreInspect({
|
||||||
|
showMapPopupOnHover: false,
|
||||||
|
showInspectMapPopupOnHover: false,
|
||||||
|
selectThreshold: 5
|
||||||
|
}));
|
||||||
} else {
|
} else {
|
||||||
var map = L.mapbox.map('map', '{{public_url}}styles/{{id}}.json{{&key_query}}', { zoomControl: false });
|
var map = L.map('map', { zoomControl: false });
|
||||||
new L.Control.Zoom({ position: 'topright' }).addTo(map);
|
new L.Control.Zoom({ position: 'topright' }).addTo(map);
|
||||||
setTimeout(function() {
|
|
||||||
new L.Hash(map);
|
var tile_urls = [], tile_attribution, tile_minzoom, tile_maxzoom;
|
||||||
}, 0);
|
var url = '{{public_url}}styles/{{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], {
|
||||||
|
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>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
<ows:Operation name="GetCapabilities">
|
<ows:Operation name="GetCapabilities">
|
||||||
<ows:DCP>
|
<ows:DCP>
|
||||||
<ows:HTTP>
|
<ows:HTTP>
|
||||||
<ows:Get xlink:href="{{baseUrl}}/wmts/{{id}}/">
|
<ows:Get xlink:href="{{baseUrl}}wmts/{{id}}/">
|
||||||
<ows:Constraint name="GetEncoding">
|
<ows:Constraint name="GetEncoding">
|
||||||
<ows:AllowedValues>
|
<ows:AllowedValues>
|
||||||
<ows:Value>RESTful</ows:Value>
|
<ows:Value>RESTful</ows:Value>
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
<ows:Operation name="GetTile">
|
<ows:Operation name="GetTile">
|
||||||
<ows:DCP>
|
<ows:DCP>
|
||||||
<ows:HTTP>
|
<ows:HTTP>
|
||||||
<ows:Get xlink:href="{{baseUrl}}/styles/">
|
<ows:Get xlink:href="{{baseUrl}}styles/">
|
||||||
<ows:Constraint name="GetEncoding">
|
<ows:Constraint name="GetEncoding">
|
||||||
<ows:AllowedValues>
|
<ows:AllowedValues>
|
||||||
<ows:Value>RESTful</ows:Value>
|
<ows:Value>RESTful</ows:Value>
|
||||||
@@ -50,7 +50,7 @@
|
|||||||
<TileMatrixSetLink>
|
<TileMatrixSetLink>
|
||||||
<TileMatrixSet>GoogleMapsCompatible</TileMatrixSet>
|
<TileMatrixSet>GoogleMapsCompatible</TileMatrixSet>
|
||||||
</TileMatrixSetLink>
|
</TileMatrixSetLink>
|
||||||
<ResourceURL format="image/png" resourceType="tile" template="{{baseUrl}}/styles/{{id}}/{TileMatrix}/{TileCol}/{TileRow}.png{{key_query}}"/>
|
<ResourceURL format="image/png" resourceType="tile" template="{{baseUrl}}styles/{{id}}/{TileMatrix}/{TileCol}/{TileRow}.png{{key_query}}"/>
|
||||||
</Layer><TileMatrixSet>
|
</Layer><TileMatrixSet>
|
||||||
<ows:Title>GoogleMapsCompatible</ows:Title>
|
<ows:Title>GoogleMapsCompatible</ows:Title>
|
||||||
<ows:Abstract>GoogleMapsCompatible EPSG:3857</ows:Abstract>
|
<ows:Abstract>GoogleMapsCompatible EPSG:3857</ows:Abstract>
|
||||||
@@ -403,5 +403,5 @@
|
|||||||
<MatrixHeight>262144</MatrixHeight>
|
<MatrixHeight>262144</MatrixHeight>
|
||||||
</TileMatrix></TileMatrixSet>
|
</TileMatrix></TileMatrixSet>
|
||||||
</Contents>
|
</Contents>
|
||||||
<ServiceMetadataURL xlink:href="{{baseUrl}}/wmts/{{id}}/"/>
|
<ServiceMetadataURL xlink:href="{{baseUrl}}wmts/{{id}}/"/>
|
||||||
</Capabilities>
|
</Capabilities>
|
||||||
|
|||||||
45
publish.js
45
publish.js
@@ -11,43 +11,58 @@
|
|||||||
/* CREATE tileserver-gl-light */
|
/* CREATE tileserver-gl-light */
|
||||||
|
|
||||||
// SYNC THE `light` FOLDER
|
// SYNC THE `light` FOLDER
|
||||||
require('child_process').execSync('rsync -av --exclude="light" --exclude=".git" --exclude="node_modules" --delete . light', {
|
|
||||||
stdio: 'inherit'
|
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`
|
// PATCH `package.json`
|
||||||
var fs = require('fs');
|
import fs from 'fs';
|
||||||
var packageJson = require('./package');
|
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.name += '-light';
|
||||||
packageJson.description = 'Map tile server for JSON GL styles - serving vector tiles';
|
packageJson.description =
|
||||||
|
'Map tile server for JSON GL styles - serving vector tiles';
|
||||||
delete packageJson.dependencies['canvas'];
|
delete packageJson.dependencies['canvas'];
|
||||||
delete packageJson.dependencies['@mapbox/mapbox-gl-native'];
|
delete packageJson.dependencies['@maplibre/maplibre-gl-native'];
|
||||||
delete packageJson.dependencies['sharp'];
|
delete packageJson.dependencies['sharp'];
|
||||||
|
|
||||||
|
delete packageJson.scripts['prepare'];
|
||||||
|
|
||||||
delete packageJson.optionalDependencies;
|
delete packageJson.optionalDependencies;
|
||||||
delete packageJson.devDependencies;
|
delete packageJson.devDependencies;
|
||||||
|
|
||||||
packageJson.engines.node = '>= 10';
|
packageJson.engines.node = '>= 14.15.0';
|
||||||
|
|
||||||
var str = JSON.stringify(packageJson, undefined, 2);
|
const str = JSON.stringify(packageJson, undefined, 2);
|
||||||
fs.writeFileSync('light/package.json', str);
|
fs.writeFileSync('light/package.json', str);
|
||||||
fs.renameSync('light/README_light.md', 'light/README.md');
|
fs.renameSync('light/README_light.md', 'light/README.md');
|
||||||
fs.renameSync('light/Dockerfile_light', 'light/Dockerfile');
|
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
|
// for Build tileserver-gl-light docker image, don't publish
|
||||||
if (process.argv.length > 2 && process.argv[2] == "--no-publish") {
|
if (process.argv.length > 2 && process.argv[2] == '--no-publish') {
|
||||||
process.exit(0)
|
process.exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* PUBLISH */
|
/* PUBLISH */
|
||||||
|
|
||||||
// tileserver-gl
|
// tileserver-gl
|
||||||
require('child_process').execSync('npm publish .', {
|
child_process.execSync('npm publish . --access public', {
|
||||||
stdio: 'inherit'
|
stdio: 'inherit',
|
||||||
});
|
});
|
||||||
|
|
||||||
// tileserver-gl-light
|
// tileserver-gl-light
|
||||||
require('child_process').execSync('npm publish light', {
|
child_process.execSync('npm publish ./light --access public', {
|
||||||
stdio: 'inherit'
|
stdio: 'inherit',
|
||||||
});
|
});
|
||||||
|
|||||||
37
run.sh
37
run.sh
@@ -1,37 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
_term() {
|
|
||||||
echo "Caught signal, stopping gracefully"
|
|
||||||
kill -TERM "$child" 2>/dev/null
|
|
||||||
}
|
|
||||||
|
|
||||||
trap _term SIGTERM
|
|
||||||
trap _term SIGINT
|
|
||||||
|
|
||||||
xvfbMaxStartWaitTime=60
|
|
||||||
displayNumber=99
|
|
||||||
screenNumber=0
|
|
||||||
|
|
||||||
# Delete files if they were not cleaned by last run
|
|
||||||
rm -rf /tmp/.X11-unix /tmp/.X${displayNumber}-lock ~/xvfb.pid
|
|
||||||
|
|
||||||
echo "Starting Xvfb on display ${displayNumber}"
|
|
||||||
start-stop-daemon --start --pidfile ~/xvfb.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :${displayNumber} -screen ${screenNumber} 1024x768x24 -ac +extension GLX +render -noreset
|
|
||||||
|
|
||||||
# Wait to be able to connect to the port. This will exit if it cannot in 1 minute.
|
|
||||||
timeout ${xvfbMaxStartWaitTime} bash -c "while ! xdpyinfo -display :${displayNumber} >/dev/null; do sleep 0.5; done"
|
|
||||||
if [ $? -ne 0 ]; then
|
|
||||||
echo "Could not connect to display ${displayNumber} in ${xvfbMaxStartWaitTime} seconds time."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
export DISPLAY=:${displayNumber}.${screenNumber}
|
|
||||||
|
|
||||||
echo
|
|
||||||
cd /data
|
|
||||||
node /usr/src/app/ -p 80 "$@" &
|
|
||||||
child=$!
|
|
||||||
wait "$child"
|
|
||||||
|
|
||||||
start-stop-daemon --stop --retry 5 --pidfile ~/xvfb.pid # stop xvfb when exiting
|
|
||||||
rm ~/xvfb.pid
|
|
||||||
18
src/healthcheck.js
Normal file
18
src/healthcheck.js
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import * as http from 'http';
|
||||||
|
var options = {
|
||||||
|
timeout: 2000,
|
||||||
|
};
|
||||||
|
var url = 'http://localhost:8080/health';
|
||||||
|
var 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();
|
||||||
149
src/main.js
149
src/main.js
@@ -2,73 +2,56 @@
|
|||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
require = require('esm')(module);
|
import fs from 'node:fs';
|
||||||
|
import path from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
import request from 'request';
|
||||||
|
import { server } from './server.js';
|
||||||
|
|
||||||
const fs = require('fs');
|
import MBTiles from '@mapbox/mbtiles';
|
||||||
const path = require('path');
|
|
||||||
const request = require('request');
|
|
||||||
|
|
||||||
const MBTiles = require('@mapbox/mbtiles');
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = path.dirname(__filename);
|
||||||
const packageJson = require('../package');
|
const packageJson = JSON.parse(
|
||||||
|
fs.readFileSync(__dirname + '/../package.json', 'utf8'),
|
||||||
|
);
|
||||||
|
|
||||||
const args = process.argv;
|
const args = process.argv;
|
||||||
if (args.length >= 3 && args[2][0] !== '-') {
|
if (args.length >= 3 && args[2][0] !== '-') {
|
||||||
args.splice(2, 0, '--mbtiles');
|
args.splice(2, 0, '--mbtiles');
|
||||||
}
|
}
|
||||||
|
|
||||||
const opts = require('commander')
|
import { program } from 'commander';
|
||||||
|
program
|
||||||
.description('tileserver-gl startup options')
|
.description('tileserver-gl startup options')
|
||||||
.usage('tileserver-gl [mbtiles] [options]')
|
.usage('tileserver-gl [mbtiles] [options]')
|
||||||
.option(
|
.option(
|
||||||
'--mbtiles <file>',
|
'--mbtiles <file>',
|
||||||
'MBTiles file (uses demo configuration);\n' +
|
'MBTiles file (uses demo configuration);\n' +
|
||||||
'\t ignored if the configuration file is also specified'
|
'\t ignored if the configuration file is also specified',
|
||||||
)
|
)
|
||||||
.option(
|
.option(
|
||||||
'-c, --config <file>',
|
'-c, --config <file>',
|
||||||
'Configuration file [config.json]',
|
'Configuration file [config.json]',
|
||||||
'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('-b, --bind <address>', 'Bind address')
|
||||||
|
.option('-p, --port <port>', 'Port [8080]', 8080, parseInt)
|
||||||
|
.option('-C|--no-cors', 'Disable Cross-origin resource sharing headers')
|
||||||
.option(
|
.option(
|
||||||
'-u|--public_url <url>',
|
'-u|--public_url <url>',
|
||||||
'Enable exposing the server on subpaths, not necessarily the root of the domain'
|
'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('-V, --verbose', 'More verbose output')
|
||||||
|
.option('-s, --silent', 'Less verbose output')
|
||||||
|
.option('-l|--log_file <file>', 'output log file (defaults to standard out)')
|
||||||
.option(
|
.option(
|
||||||
'-f|--log_format <format>',
|
'-f|--log_format <format>',
|
||||||
'define the log format: https://github.com/expressjs/morgan#morganformat-options'
|
'define the log format: https://github.com/expressjs/morgan#morganformat-options',
|
||||||
)
|
)
|
||||||
.version(
|
.version(packageJson.version, '-v, --version');
|
||||||
packageJson.version,
|
program.parse(process.argv);
|
||||||
'-v, --version'
|
const opts = program.opts();
|
||||||
)
|
|
||||||
.parse(args);
|
|
||||||
|
|
||||||
console.log(`Starting ${packageJson.name} v${packageJson.version}`);
|
console.log(`Starting ${packageJson.name} v${packageJson.version}`);
|
||||||
|
|
||||||
@@ -77,7 +60,7 @@ const startServer = (configPath, config) => {
|
|||||||
if (publicUrl && publicUrl.lastIndexOf('/') !== publicUrl.length - 1) {
|
if (publicUrl && publicUrl.lastIndexOf('/') !== publicUrl.length - 1) {
|
||||||
publicUrl += '/';
|
publicUrl += '/';
|
||||||
}
|
}
|
||||||
return require('./server')({
|
return server({
|
||||||
configPath: configPath,
|
configPath: configPath,
|
||||||
config: config,
|
config: config,
|
||||||
bind: opts.bind,
|
bind: opts.bind,
|
||||||
@@ -87,14 +70,16 @@ const startServer = (configPath, config) => {
|
|||||||
silent: opts.silent,
|
silent: opts.silent,
|
||||||
logFile: opts.log_file,
|
logFile: opts.log_file,
|
||||||
logFormat: opts.log_format,
|
logFormat: opts.log_format,
|
||||||
publicUrl: publicUrl
|
publicUrl: publicUrl,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const startWithMBTiles = (mbtilesFile) => {
|
const startWithMBTiles = (mbtilesFile) => {
|
||||||
console.log(`[INFO] Automatically creating config file for ${mbtilesFile}`);
|
console.log(`[INFO] Automatically creating config file for ${mbtilesFile}`);
|
||||||
console.log(`[INFO] Only a basic preview style will be used.`);
|
console.log(`[INFO] Only a basic preview style will be used.`);
|
||||||
console.log(`[INFO] See documentation to learn how to create config.json file.`);
|
console.log(
|
||||||
|
`[INFO] See documentation to learn how to create config.json file.`,
|
||||||
|
);
|
||||||
|
|
||||||
mbtilesFile = path.resolve(process.cwd(), mbtilesFile);
|
mbtilesFile = path.resolve(process.cwd(), mbtilesFile);
|
||||||
|
|
||||||
@@ -103,64 +88,73 @@ const startWithMBTiles = (mbtilesFile) => {
|
|||||||
console.log(`ERROR: Not valid MBTiles file: ${mbtilesFile}`);
|
console.log(`ERROR: Not valid MBTiles file: ${mbtilesFile}`);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
const instance = new MBTiles(mbtilesFile, (err) => {
|
const instance = new MBTiles(mbtilesFile + '?mode=ro', (err) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
console.log('ERROR: Unable to open MBTiles.');
|
console.log('ERROR: Unable to open MBTiles.');
|
||||||
console.log(` Make sure ${path.basename(mbtilesFile)} is valid MBTiles.`);
|
console.log(`Make sure ${path.basename(mbtilesFile)} is valid MBTiles.`);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
instance.getInfo((err, info) => {
|
instance.getInfo((err, info) => {
|
||||||
if (err || !info) {
|
if (err || !info) {
|
||||||
console.log('ERROR: Metadata missing in the MBTiles.');
|
console.log('ERROR: Metadata missing in the MBTiles.');
|
||||||
console.log(` Make sure ${path.basename(mbtilesFile)} is valid MBTiles.`);
|
console.log(
|
||||||
|
`Make sure ${path.basename(mbtilesFile)} is valid MBTiles.`,
|
||||||
|
);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
const bounds = info.bounds;
|
const bounds = info.bounds;
|
||||||
|
|
||||||
const styleDir = path.resolve(__dirname, "../node_modules/tileserver-gl-styles/");
|
const styleDir = path.resolve(
|
||||||
|
__dirname,
|
||||||
|
'../node_modules/tileserver-gl-styles/',
|
||||||
|
);
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
"options": {
|
options: {
|
||||||
"paths": {
|
paths: {
|
||||||
"root": styleDir,
|
root: styleDir,
|
||||||
"fonts": "fonts",
|
fonts: 'fonts',
|
||||||
"styles": "styles",
|
styles: 'styles',
|
||||||
"mbtiles": path.dirname(mbtilesFile)
|
mbtiles: path.dirname(mbtilesFile),
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
"styles": {},
|
styles: {},
|
||||||
"data": {}
|
data: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
if (info.format === 'pbf' &&
|
if (
|
||||||
info.name.toLowerCase().indexOf('openmaptiles') > -1) {
|
info.format === 'pbf' &&
|
||||||
|
info.name.toLowerCase().indexOf('openmaptiles') > -1
|
||||||
|
) {
|
||||||
config['data'][`v3`] = {
|
config['data'][`v3`] = {
|
||||||
"mbtiles": path.basename(mbtilesFile)
|
mbtiles: path.basename(mbtilesFile),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const styles = fs.readdirSync(path.resolve(styleDir, 'styles'));
|
const styles = fs.readdirSync(path.resolve(styleDir, 'styles'));
|
||||||
for (let styleName of styles) {
|
for (const styleName of styles) {
|
||||||
const styleFileRel = styleName + '/style.json';
|
const styleFileRel = styleName + '/style.json';
|
||||||
const styleFile = path.resolve(styleDir, 'styles', styleFileRel);
|
const styleFile = path.resolve(styleDir, 'styles', styleFileRel);
|
||||||
if (fs.existsSync(styleFile)) {
|
if (fs.existsSync(styleFile)) {
|
||||||
config['styles'][styleName] = {
|
config['styles'][styleName] = {
|
||||||
"style": styleFileRel,
|
style: styleFileRel,
|
||||||
"tilejson": {
|
tilejson: {
|
||||||
"bounds": bounds
|
bounds: bounds,
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log(`WARN: MBTiles not in "openmaptiles" format. Serving raw data only...`);
|
console.log(
|
||||||
config['data'][(info.id || 'mbtiles')
|
`WARN: MBTiles not in "openmaptiles" format. Serving raw data only...`,
|
||||||
.replace(/\//g, '_')
|
);
|
||||||
.replace(/:/g, '_')
|
config['data'][
|
||||||
.replace(/\?/g, '_')] = {
|
(info.id || 'mbtiles')
|
||||||
"mbtiles": path.basename(mbtilesFile)
|
.replace(/\//g, '_')
|
||||||
|
.replace(/:/g, '_')
|
||||||
|
.replace(/\?/g, '_')
|
||||||
|
] = {
|
||||||
|
mbtiles: path.basename(mbtilesFile),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,7 +175,7 @@ fs.stat(path.resolve(opts.config), (err, stats) => {
|
|||||||
if (!mbtiles) {
|
if (!mbtiles) {
|
||||||
// try to find in the cwd
|
// try to find in the cwd
|
||||||
const files = fs.readdirSync(process.cwd());
|
const files = fs.readdirSync(process.cwd());
|
||||||
for (let filename of files) {
|
for (const filename of files) {
|
||||||
if (filename.endsWith('.mbtiles')) {
|
if (filename.endsWith('.mbtiles')) {
|
||||||
const mbTilesStats = fs.statSync(filename);
|
const mbTilesStats = fs.statSync(filename);
|
||||||
if (mbTilesStats.isFile() && mbTilesStats.size > 0) {
|
if (mbTilesStats.isFile() && mbTilesStats.size > 0) {
|
||||||
@@ -194,7 +188,8 @@ fs.stat(path.resolve(opts.config), (err, stats) => {
|
|||||||
console.log(`No MBTiles specified, using ${mbtiles}`);
|
console.log(`No MBTiles specified, using ${mbtiles}`);
|
||||||
return startWithMBTiles(mbtiles);
|
return startWithMBTiles(mbtiles);
|
||||||
} else {
|
} else {
|
||||||
const url = 'https://github.com/maptiler/tileserver-gl/releases/download/v1.3.0/zurich_switzerland.mbtiles';
|
const url =
|
||||||
|
'https://github.com/maptiler/tileserver-gl/releases/download/v1.3.0/zurich_switzerland.mbtiles';
|
||||||
const filename = 'zurich_switzerland.mbtiles';
|
const filename = 'zurich_switzerland.mbtiles';
|
||||||
const stream = fs.createWriteStream(filename);
|
const stream = fs.createWriteStream(filename);
|
||||||
console.log(`No MBTiles found`);
|
console.log(`No MBTiles found`);
|
||||||
|
|||||||
@@ -1,27 +1,128 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const fs = require('fs');
|
import fs from 'node:fs';
|
||||||
const path = require('path');
|
import path from 'path';
|
||||||
const zlib = require('zlib');
|
import zlib from 'zlib';
|
||||||
|
|
||||||
const clone = require('clone');
|
import clone from 'clone';
|
||||||
const express = require('express');
|
import express from 'express';
|
||||||
const MBTiles = require('@mapbox/mbtiles');
|
import MBTiles from '@mapbox/mbtiles';
|
||||||
const Pbf = require('pbf');
|
import Pbf from 'pbf';
|
||||||
const VectorTile = require('@mapbox/vector-tile').VectorTile;
|
import { VectorTile } from '@mapbox/vector-tile';
|
||||||
|
|
||||||
const utils = require('./utils');
|
import { getTileUrls, fixTileJSONCenter } from './utils.js';
|
||||||
|
|
||||||
module.exports = {
|
export const serve_data = {
|
||||||
init: (options, repo) => {
|
init: (options, repo) => {
|
||||||
const app = express().disable('x-powered-by');
|
const app = express().disable('x-powered-by');
|
||||||
|
|
||||||
app.get('/:id/:z(\\d+)/:x(\\d+)/:y(\\d+).:format([\\w.]+)', (req, res, next) => {
|
app.get(
|
||||||
|
'/:id/:z(\\d+)/:x(\\d+)/:y(\\d+).:format([\\w.]+)',
|
||||||
|
(req, res, next) => {
|
||||||
|
const item = repo[req.params.id];
|
||||||
|
if (!item) {
|
||||||
|
return res.sendStatus(404);
|
||||||
|
}
|
||||||
|
const tileJSONFormat = item.tileJSON.format;
|
||||||
|
const z = req.params.z | 0;
|
||||||
|
const x = req.params.x | 0;
|
||||||
|
const y = req.params.y | 0;
|
||||||
|
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 ||
|
||||||
|
0 ||
|
||||||
|
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');
|
||||||
|
}
|
||||||
|
item.source.getTile(z, x, y, (err, data, headers) => {
|
||||||
|
let isGzipped;
|
||||||
|
if (err) {
|
||||||
|
if (/does not exist/.test(err.message)) {
|
||||||
|
return res.status(204).send();
|
||||||
|
} else {
|
||||||
|
return res
|
||||||
|
.status(500)
|
||||||
|
.header('Content-Type', 'text/plain')
|
||||||
|
.send(err.message);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (data == null) {
|
||||||
|
return res.status(404).send('Not found');
|
||||||
|
} else {
|
||||||
|
if (tileJSONFormat === 'pbf') {
|
||||||
|
isGzipped =
|
||||||
|
data.slice(0, 2).indexOf(Buffer.from([0x1f, 0x8b])) === 0;
|
||||||
|
if (options.dataDecoratorFunc) {
|
||||||
|
if (isGzipped) {
|
||||||
|
data = zlib.unzipSync(data);
|
||||||
|
isGzipped = false;
|
||||||
|
}
|
||||||
|
data = options.dataDecoratorFunc(id, 'data', data, z, x, y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (format === 'pbf') {
|
||||||
|
headers['Content-Type'] = 'application/x-protobuf';
|
||||||
|
} else if (format === 'geojson') {
|
||||||
|
headers['Content-Type'] = 'application/json';
|
||||||
|
|
||||||
|
if (isGzipped) {
|
||||||
|
data = zlib.unzipSync(data);
|
||||||
|
isGzipped = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
delete headers['ETag']; // do not trust the tile ETag -- regenerate
|
||||||
|
headers['Content-Encoding'] = 'gzip';
|
||||||
|
res.set(headers);
|
||||||
|
|
||||||
|
if (!isGzipped) {
|
||||||
|
data = zlib.gzipSync(data);
|
||||||
|
isGzipped = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).send(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
app.get('/:id/:t(\\d+)/:z(\\d+)/:x(\\d+)/:y(\\d+).:format([\\w.]+)', (req, res, next) => {
|
||||||
const item = repo[req.params.id];
|
const item = repo[req.params.id];
|
||||||
if (!item) {
|
if (!item) {
|
||||||
return res.sendStatus(404);
|
return res.sendStatus(404);
|
||||||
}
|
}
|
||||||
let tileJSONFormat = item.tileJSON.format;
|
let tileJSONFormat = item.tileJSON.format;
|
||||||
|
const t = req.params.t | 0;
|
||||||
const z = req.params.z | 0;
|
const z = req.params.z | 0;
|
||||||
const x = req.params.x | 0;
|
const x = req.params.x | 0;
|
||||||
const y = req.params.y | 0;
|
const y = req.params.y | 0;
|
||||||
@@ -30,15 +131,16 @@ module.exports = {
|
|||||||
format = 'pbf';
|
format = 'pbf';
|
||||||
}
|
}
|
||||||
if (format !== tileJSONFormat &&
|
if (format !== tileJSONFormat &&
|
||||||
!(format === 'geojson' && tileJSONFormat === 'pbf')) {
|
!(format === 'geojson' && tileJSONFormat === 'pbf') && format !== 'png') {
|
||||||
return res.status(404).send('Invalid format');
|
return res.status(404).send('Invalid format');
|
||||||
}
|
}
|
||||||
if (z < item.tileJSON.minzoom || 0 || x < 0 || y < 0 ||
|
if (z < item.tileJSON.minzoom || 0 || x < 0 || y < 0 ||
|
||||||
z > item.tileJSON.maxzoom ||
|
z > item.tileJSON.maxzoom ||
|
||||||
x >= Math.pow(2, z) || y >= Math.pow(2, z)) {
|
x >= Math.pow(2, z) || y >= Math.pow(2, z) ||
|
||||||
|
t < 0) {
|
||||||
return res.status(404).send('Out of bounds');
|
return res.status(404).send('Out of bounds');
|
||||||
}
|
}
|
||||||
item.source.getTile(z, x, y, (err, data, headers) => {
|
item.source.getTileT(t, z, x, y, (err, data, headers) => {
|
||||||
let isGzipped;
|
let isGzipped;
|
||||||
if (err) {
|
if (err) {
|
||||||
if (/does not exist/.test(err.message)) {
|
if (/does not exist/.test(err.message)) {
|
||||||
@@ -63,6 +165,8 @@ module.exports = {
|
|||||||
}
|
}
|
||||||
if (format === 'pbf') {
|
if (format === 'pbf') {
|
||||||
headers['Content-Type'] = 'application/x-protobuf';
|
headers['Content-Type'] = 'application/x-protobuf';
|
||||||
|
} else if (format === 'png') {
|
||||||
|
headers['Content-Type'] = 'image/png';
|
||||||
} else if (format === 'geojson') {
|
} else if (format === 'geojson') {
|
||||||
headers['Content-Type'] = 'application/json';
|
headers['Content-Type'] = 'application/json';
|
||||||
|
|
||||||
@@ -108,10 +212,16 @@ module.exports = {
|
|||||||
return res.sendStatus(404);
|
return res.sendStatus(404);
|
||||||
}
|
}
|
||||||
const info = clone(item.tileJSON);
|
const info = clone(item.tileJSON);
|
||||||
info.tiles = utils.getTileUrls(req, info.tiles,
|
info.tiles = getTileUrls(
|
||||||
`data/${req.params.id}`, info.format, item.publicUrl, {
|
req,
|
||||||
'pbf': options.pbfAlias
|
info.tiles,
|
||||||
});
|
`data/${req.params.id}`,
|
||||||
|
info.format,
|
||||||
|
item.publicUrl,
|
||||||
|
{
|
||||||
|
pbf: options.pbfAlias,
|
||||||
|
},
|
||||||
|
);
|
||||||
return res.send(info);
|
return res.send(info);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -120,7 +230,7 @@ module.exports = {
|
|||||||
add: (options, repo, params, id, publicUrl) => {
|
add: (options, repo, params, id, publicUrl) => {
|
||||||
const mbtilesFile = path.resolve(options.paths.mbtiles, params.mbtiles);
|
const mbtilesFile = path.resolve(options.paths.mbtiles, params.mbtiles);
|
||||||
let tileJSON = {
|
let tileJSON = {
|
||||||
'tiles': params.domains || options.domains
|
tiles: params.domains || options.domains,
|
||||||
};
|
};
|
||||||
|
|
||||||
const mbtilesFileStats = fs.statSync(mbtilesFile);
|
const mbtilesFileStats = fs.statSync(mbtilesFile);
|
||||||
@@ -129,7 +239,7 @@ module.exports = {
|
|||||||
}
|
}
|
||||||
let source;
|
let source;
|
||||||
const sourceInfoPromise = new Promise((resolve, reject) => {
|
const sourceInfoPromise = new Promise((resolve, reject) => {
|
||||||
source = new MBTiles(mbtilesFile, err => {
|
source = new MBTiles(mbtilesFile + '?mode=ro', (err) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
reject(err);
|
reject(err);
|
||||||
return;
|
return;
|
||||||
@@ -150,7 +260,7 @@ module.exports = {
|
|||||||
delete tileJSON['scheme'];
|
delete tileJSON['scheme'];
|
||||||
|
|
||||||
Object.assign(tileJSON, params.tilejson || {});
|
Object.assign(tileJSON, params.tilejson || {});
|
||||||
utils.fixTileJSONCenter(tileJSON);
|
fixTileJSONCenter(tileJSON);
|
||||||
|
|
||||||
if (options.dataDecoratorFunc) {
|
if (options.dataDecoratorFunc) {
|
||||||
tileJSON = options.dataDecoratorFunc(id, 'tilejson', tileJSON);
|
tileJSON = options.dataDecoratorFunc(id, 'tilejson', tileJSON);
|
||||||
@@ -164,8 +274,8 @@ module.exports = {
|
|||||||
repo[id] = {
|
repo[id] = {
|
||||||
tileJSON,
|
tileJSON,
|
||||||
publicUrl,
|
publicUrl,
|
||||||
source
|
source,
|
||||||
}
|
};
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const express = require('express');
|
import express from 'express';
|
||||||
const fs = require('fs');
|
import fs from 'node:fs';
|
||||||
const path = require('path');
|
import path from 'path';
|
||||||
|
|
||||||
const utils = require('./utils');
|
import { getFontsPbf } from './utils.js';
|
||||||
|
|
||||||
module.exports = (options, allowedFonts) => {
|
export const serve_font = (options, allowedFonts) => {
|
||||||
const app = express().disable('x-powered-by');
|
const app = express().disable('x-powered-by');
|
||||||
|
|
||||||
const lastModified = new Date().toUTCString();
|
const lastModified = new Date().toUTCString();
|
||||||
@@ -26,8 +26,10 @@ module.exports = (options, allowedFonts) => {
|
|||||||
reject(err);
|
reject(err);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (stats.isDirectory() &&
|
if (
|
||||||
fs.existsSync(path.join(fontPath, file, '0-255.pbf'))) {
|
stats.isDirectory() &&
|
||||||
|
fs.existsSync(path.join(fontPath, file, '0-255.pbf'))
|
||||||
|
) {
|
||||||
existingFonts[path.basename(file)] = true;
|
existingFonts[path.basename(file)] = true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -40,19 +42,26 @@ module.exports = (options, allowedFonts) => {
|
|||||||
const fontstack = decodeURI(req.params.fontstack);
|
const fontstack = decodeURI(req.params.fontstack);
|
||||||
const range = req.params.range;
|
const range = req.params.range;
|
||||||
|
|
||||||
utils.getFontsPbf(options.serveAllFonts ? null : allowedFonts,
|
getFontsPbf(
|
||||||
fontPath, fontstack, range, existingFonts).then(concated => {
|
options.serveAllFonts ? null : allowedFonts,
|
||||||
|
fontPath,
|
||||||
|
fontstack,
|
||||||
|
range,
|
||||||
|
existingFonts,
|
||||||
|
).then(
|
||||||
|
(concated) => {
|
||||||
res.header('Content-type', 'application/x-protobuf');
|
res.header('Content-type', 'application/x-protobuf');
|
||||||
res.header('Last-Modified', lastModified);
|
res.header('Last-Modified', lastModified);
|
||||||
return res.send(concated);
|
return res.send(concated);
|
||||||
}, err => res.status(400).send(err)
|
},
|
||||||
|
(err) => res.status(400).header('Content-Type', 'text/plain').send(err),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
app.get('/fonts.json', (req, res, next) => {
|
app.get('/fonts.json', (req, res, next) => {
|
||||||
res.header('Content-type', 'application/json');
|
res.header('Content-type', 'application/json');
|
||||||
return res.send(
|
return res.send(
|
||||||
Object.keys(options.serveAllFonts ? existingFonts : allowedFonts).sort()
|
Object.keys(options.serveAllFonts ? existingFonts : allowedFonts).sort(),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
9
src/serve_light.js
Normal file
9
src/serve_light.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-empty-function */
|
||||||
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
export const serve_rendered = {
|
||||||
|
init: (options, repo) => {},
|
||||||
|
add: (options, repo, params, id, publicUrl, dataResolver) => {},
|
||||||
|
remove: (repo, id) => {},
|
||||||
|
};
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,18 +1,18 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const path = require('path');
|
import path from 'path';
|
||||||
const fs = require('fs');
|
import fs from 'node:fs';
|
||||||
|
|
||||||
const clone = require('clone');
|
import clone from 'clone';
|
||||||
const express = require('express');
|
import express from 'express';
|
||||||
import {validate} from '@mapbox/mapbox-gl-style-spec';
|
import { validate } from '@maplibre/maplibre-gl-style-spec';
|
||||||
|
|
||||||
const utils = require('./utils');
|
import { getPublicUrl } from './utils.js';
|
||||||
|
|
||||||
const httpTester = /^(http(s)?:)?\/\//;
|
const httpTester = /^(http(s)?:)?\/\//;
|
||||||
|
|
||||||
const fixUrl = (req, url, publicUrl, opt_nokey) => {
|
const fixUrl = (req, url, publicUrl, opt_nokey) => {
|
||||||
if (!url || (typeof url !== 'string') || url.indexOf('local://') !== 0) {
|
if (!url || typeof url !== 'string' || url.indexOf('local://') !== 0) {
|
||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
const queryParams = [];
|
const queryParams = [];
|
||||||
@@ -23,11 +23,10 @@ const fixUrl = (req, url, publicUrl, opt_nokey) => {
|
|||||||
if (queryParams.length) {
|
if (queryParams.length) {
|
||||||
query = `?${queryParams.join('&')}`;
|
query = `?${queryParams.join('&')}`;
|
||||||
}
|
}
|
||||||
return url.replace(
|
return url.replace('local://', getPublicUrl(publicUrl, req)) + query;
|
||||||
'local://', utils.getPublicUrl(publicUrl, req)) + query;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = {
|
export const serve_style = {
|
||||||
init: (options, repo) => {
|
init: (options, repo) => {
|
||||||
const app = express().disable('x-powered-by');
|
const app = express().disable('x-powered-by');
|
||||||
|
|
||||||
@@ -43,10 +42,20 @@ module.exports = {
|
|||||||
}
|
}
|
||||||
// mapbox-gl-js viewer cannot handle sprite urls with query
|
// mapbox-gl-js viewer cannot handle sprite urls with query
|
||||||
if (styleJSON_.sprite) {
|
if (styleJSON_.sprite) {
|
||||||
styleJSON_.sprite = fixUrl(req, styleJSON_.sprite, item.publicUrl, false);
|
styleJSON_.sprite = fixUrl(
|
||||||
|
req,
|
||||||
|
styleJSON_.sprite,
|
||||||
|
item.publicUrl,
|
||||||
|
false,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (styleJSON_.glyphs) {
|
if (styleJSON_.glyphs) {
|
||||||
styleJSON_.glyphs = fixUrl(req, styleJSON_.glyphs, item.publicUrl, false);
|
styleJSON_.glyphs = fixUrl(
|
||||||
|
req,
|
||||||
|
styleJSON_.glyphs,
|
||||||
|
item.publicUrl,
|
||||||
|
false,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return res.send(styleJSON_);
|
return res.send(styleJSON_);
|
||||||
});
|
});
|
||||||
@@ -56,8 +65,8 @@ module.exports = {
|
|||||||
if (!item || !item.spritePath) {
|
if (!item || !item.spritePath) {
|
||||||
return res.sendStatus(404);
|
return res.sendStatus(404);
|
||||||
}
|
}
|
||||||
const scale = req.params.scale,
|
const scale = req.params.scale;
|
||||||
format = req.params.format;
|
const format = req.params.format;
|
||||||
const filename = `${item.spritePath + (scale || '')}.${format}`;
|
const filename = `${item.spritePath + (scale || '')}.${format}`;
|
||||||
return fs.readFile(filename, (err, data) => {
|
return fs.readFile(filename, (err, data) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
@@ -87,23 +96,25 @@ module.exports = {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
let validationErrors = validate(styleFileData);
|
const validationErrors = validate(styleFileData);
|
||||||
if (validationErrors.length > 0) {
|
if (validationErrors.length > 0) {
|
||||||
console.log(`The file "${params.style}" is not valid a valid style file:`);
|
console.log(
|
||||||
|
`The file "${params.style}" is not valid a valid style file:`,
|
||||||
|
);
|
||||||
for (const err of validationErrors) {
|
for (const err of validationErrors) {
|
||||||
console.log(`${err.line}: ${err.message}`);
|
console.log(`${err.line}: ${err.message}`);
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
let styleJSON = JSON.parse(styleFileData);
|
const styleJSON = JSON.parse(styleFileData);
|
||||||
|
|
||||||
for (const name of Object.keys(styleJSON.sources)) {
|
for (const name of Object.keys(styleJSON.sources)) {
|
||||||
const source = styleJSON.sources[name];
|
const source = styleJSON.sources[name];
|
||||||
const url = source.url;
|
const url = source.url;
|
||||||
if (url && url.lastIndexOf('mbtiles:', 0) === 0) {
|
if (url && url.lastIndexOf('mbtiles:', 0) === 0) {
|
||||||
let mbtilesFile = url.substring('mbtiles://'.length);
|
let mbtilesFile = url.substring('mbtiles://'.length);
|
||||||
const fromData = mbtilesFile[0] === '{' &&
|
const fromData =
|
||||||
mbtilesFile[mbtilesFile.length - 1] === '}';
|
mbtilesFile[0] === '{' && mbtilesFile[mbtilesFile.length - 1] === '}';
|
||||||
|
|
||||||
if (fromData) {
|
if (fromData) {
|
||||||
mbtilesFile = mbtilesFile.substr(1, mbtilesFile.length - 2);
|
mbtilesFile = mbtilesFile.substr(1, mbtilesFile.length - 2);
|
||||||
@@ -120,7 +131,7 @@ module.exports = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let obj of styleJSON.layers) {
|
for (const obj of styleJSON.layers) {
|
||||||
if (obj['type'] === 'symbol') {
|
if (obj['type'] === 'symbol') {
|
||||||
const fonts = (obj['layout'] || {})['text-font'];
|
const fonts = (obj['layout'] || {})['text-font'];
|
||||||
if (fonts && fonts.length) {
|
if (fonts && fonts.length) {
|
||||||
@@ -135,10 +146,14 @@ module.exports = {
|
|||||||
let spritePath;
|
let spritePath;
|
||||||
|
|
||||||
if (styleJSON.sprite && !httpTester.test(styleJSON.sprite)) {
|
if (styleJSON.sprite && !httpTester.test(styleJSON.sprite)) {
|
||||||
spritePath = path.join(options.paths.sprites,
|
spritePath = path.join(
|
||||||
|
options.paths.sprites,
|
||||||
styleJSON.sprite
|
styleJSON.sprite
|
||||||
.replace('{style}', path.basename(styleFile, '.json'))
|
.replace('{style}', path.basename(styleFile, '.json'))
|
||||||
.replace('{styleJsonFolder}', path.relative(options.paths.sprites, path.dirname(styleFile)))
|
.replace(
|
||||||
|
'{styleJsonFolder}',
|
||||||
|
path.relative(options.paths.sprites, path.dirname(styleFile)),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
styleJSON.sprite = `local://styles/${id}/sprite`;
|
styleJSON.sprite = `local://styles/${id}/sprite`;
|
||||||
}
|
}
|
||||||
@@ -150,9 +165,9 @@ module.exports = {
|
|||||||
styleJSON,
|
styleJSON,
|
||||||
spritePath,
|
spritePath,
|
||||||
publicUrl,
|
publicUrl,
|
||||||
name: styleJSON.name
|
name: styleJSON.name,
|
||||||
};
|
};
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
504
src/server.js
504
src/server.js
@@ -1,54 +1,68 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
process.env.UV_THREADPOOL_SIZE =
|
import os from 'os';
|
||||||
Math.ceil(Math.max(4, require('os').cpus().length * 1.5));
|
process.env.UV_THREADPOOL_SIZE = Math.ceil(Math.max(4, os.cpus().length * 1.5));
|
||||||
|
|
||||||
const fs = require('fs');
|
import fs from 'node:fs';
|
||||||
const path = require('path');
|
import path from 'path';
|
||||||
|
|
||||||
const chokidar = require('chokidar');
|
import chokidar from 'chokidar';
|
||||||
const clone = require('clone');
|
import clone from 'clone';
|
||||||
const cors = require('cors');
|
import cors from 'cors';
|
||||||
const enableShutdown = require('http-shutdown');
|
import enableShutdown from 'http-shutdown';
|
||||||
const express = require('express');
|
import express from 'express';
|
||||||
const handlebars = require('handlebars');
|
import handlebars from 'handlebars';
|
||||||
const mercator = new (require('@mapbox/sphericalmercator'))();
|
import SphericalMercator from '@mapbox/sphericalmercator';
|
||||||
const morgan = require('morgan');
|
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 { getTileUrls, getPublicUrl } from './utils.js';
|
||||||
|
|
||||||
const packageJson = require('../package');
|
import { fileURLToPath } from 'url';
|
||||||
const serve_font = require('./serve_font');
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const serve_style = require('./serve_style');
|
const __dirname = path.dirname(__filename);
|
||||||
const serve_data = require('./serve_data');
|
const packageJson = JSON.parse(
|
||||||
const utils = require('./utils');
|
fs.readFileSync(__dirname + '/../package.json', 'utf8'),
|
||||||
|
);
|
||||||
|
|
||||||
let serve_rendered = null;
|
|
||||||
const isLight = packageJson.name.slice(-6) === '-light';
|
const isLight = packageJson.name.slice(-6) === '-light';
|
||||||
if (!isLight) {
|
const serve_rendered = (
|
||||||
// do not require `serve_rendered` in the light package
|
await import(`${!isLight ? `./serve_rendered.js` : `./serve_light.js`}`)
|
||||||
serve_rendered = require('./serve_rendered');
|
).serve_rendered;
|
||||||
}
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param opts
|
||||||
|
*/
|
||||||
function start(opts) {
|
function start(opts) {
|
||||||
console.log('Starting server');
|
console.log('Starting server');
|
||||||
|
|
||||||
const app = express().disable('x-powered-by'),
|
const app = express().disable('x-powered-by');
|
||||||
serving = {
|
const serving = {
|
||||||
styles: {},
|
styles: {},
|
||||||
rendered: {},
|
rendered: {},
|
||||||
data: {},
|
data: {},
|
||||||
fonts: {}
|
fonts: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
app.enable('trust proxy');
|
app.enable('trust proxy');
|
||||||
|
|
||||||
if (process.env.NODE_ENV !== 'test') {
|
if (process.env.NODE_ENV !== 'test') {
|
||||||
const defaultLogFormat = process.env.NODE_ENV === 'production' ? 'tiny' : 'dev';
|
const defaultLogFormat =
|
||||||
|
process.env.NODE_ENV === 'production' ? 'tiny' : 'dev';
|
||||||
const logFormat = opts.logFormat || defaultLogFormat;
|
const logFormat = opts.logFormat || defaultLogFormat;
|
||||||
app.use(morgan(logFormat, {
|
app.use(
|
||||||
stream: opts.logFile ? fs.createWriteStream(opts.logFile, { flags: 'a' }) : process.stdout,
|
morgan(logFormat, {
|
||||||
skip: (req, res) => opts.silent && (res.statusCode === 200 || res.statusCode === 304)
|
stream: opts.logFile
|
||||||
}));
|
? fs.createWriteStream(opts.logFile, { flags: 'a' })
|
||||||
|
: process.stdout,
|
||||||
|
skip: (req, res) =>
|
||||||
|
opts.silent && (res.statusCode === 200 || res.statusCode === 304),
|
||||||
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let config = opts.config || null;
|
let config = opts.config || null;
|
||||||
@@ -56,7 +70,7 @@ function start(opts) {
|
|||||||
if (opts.configPath) {
|
if (opts.configPath) {
|
||||||
configPath = path.resolve(opts.configPath);
|
configPath = path.resolve(opts.configPath);
|
||||||
try {
|
try {
|
||||||
config = clone(require(configPath));
|
config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log('ERROR: Config file not found or invalid!');
|
console.log('ERROR: Config file not found or invalid!');
|
||||||
console.log(' See README.md for instructions and sample data.');
|
console.log(' See README.md for instructions and sample data.');
|
||||||
@@ -73,17 +87,21 @@ function start(opts) {
|
|||||||
options.paths = paths;
|
options.paths = paths;
|
||||||
paths.root = path.resolve(
|
paths.root = path.resolve(
|
||||||
configPath ? path.dirname(configPath) : process.cwd(),
|
configPath ? path.dirname(configPath) : process.cwd(),
|
||||||
paths.root || '');
|
paths.root || '',
|
||||||
|
);
|
||||||
paths.styles = path.resolve(paths.root, paths.styles || '');
|
paths.styles = path.resolve(paths.root, paths.styles || '');
|
||||||
paths.fonts = path.resolve(paths.root, paths.fonts || '');
|
paths.fonts = path.resolve(paths.root, paths.fonts || '');
|
||||||
paths.sprites = path.resolve(paths.root, paths.sprites || '');
|
paths.sprites = path.resolve(paths.root, paths.sprites || '');
|
||||||
paths.mbtiles = path.resolve(paths.root, paths.mbtiles || '');
|
paths.mbtiles = path.resolve(paths.root, paths.mbtiles || '');
|
||||||
|
paths.icons = path.resolve(paths.root, paths.icons || '');
|
||||||
|
|
||||||
const startupPromises = [];
|
const startupPromises = [];
|
||||||
|
|
||||||
const checkPath = type => {
|
const checkPath = (type) => {
|
||||||
if (!fs.existsSync(paths[type])) {
|
if (!fs.existsSync(paths[type])) {
|
||||||
console.error(`The specified path for "${type}" does not exist (${paths[type]}).`);
|
console.error(
|
||||||
|
`The specified path for "${type}" does not exist (${paths[type]}).`,
|
||||||
|
);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -91,10 +109,50 @@ function start(opts) {
|
|||||||
checkPath('fonts');
|
checkPath('fonts');
|
||||||
checkPath('sprites');
|
checkPath('sprites');
|
||||||
checkPath('mbtiles');
|
checkPath('mbtiles');
|
||||||
|
checkPath('icons');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
const getFiles = async (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(
|
||||||
|
new Promise((resolve) => {
|
||||||
|
getFiles(paths.icons).then((files) => {
|
||||||
|
paths.availableIcons = files;
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
if (options.dataDecorator) {
|
if (options.dataDecorator) {
|
||||||
try {
|
try {
|
||||||
options.dataDecoratorFunc = require(path.resolve(paths.root, options.dataDecorator));
|
options.dataDecoratorFunc = require(path.resolve(
|
||||||
|
paths.root,
|
||||||
|
options.dataDecorator,
|
||||||
|
));
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,72 +164,93 @@ function start(opts) {
|
|||||||
|
|
||||||
app.use('/data/', serve_data.init(options, serving.data));
|
app.use('/data/', serve_data.init(options, serving.data));
|
||||||
app.use('/styles/', serve_style.init(options, serving.styles));
|
app.use('/styles/', serve_style.init(options, serving.styles));
|
||||||
if (serve_rendered) {
|
if (!isLight) {
|
||||||
startupPromises.push(
|
startupPromises.push(
|
||||||
serve_rendered.init(options, serving.rendered)
|
serve_rendered.init(options, serving.rendered).then((sub) => {
|
||||||
.then(sub => {
|
app.use('/styles/', sub);
|
||||||
app.use('/styles/', sub);
|
}),
|
||||||
})
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let addStyle = (id, item, allowMoreData, reportFonts) => {
|
const addStyle = (id, item, allowMoreData, reportFonts) => {
|
||||||
let success = true;
|
let success = true;
|
||||||
if (item.serve_data !== false) {
|
if (item.serve_data !== false) {
|
||||||
success = serve_style.add(options, serving.styles, item, id, opts.publicUrl,
|
success = serve_style.add(
|
||||||
|
options,
|
||||||
|
serving.styles,
|
||||||
|
item,
|
||||||
|
id,
|
||||||
|
opts.publicUrl,
|
||||||
(mbtiles, fromData) => {
|
(mbtiles, fromData) => {
|
||||||
let dataItemId;
|
let dataItemId;
|
||||||
for (const id of Object.keys(data)) {
|
for (const id of Object.keys(serving.data)) {
|
||||||
if (fromData) {
|
if (fromData) {
|
||||||
if (id === mbtiles) {
|
if (id === mbtiles) {
|
||||||
dataItemId = id;
|
dataItemId = id;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (data[id].mbtiles === mbtiles) {
|
if (serving.data[id].mbtiles === mbtiles) {
|
||||||
dataItemId = id;
|
dataItemId = id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (dataItemId) { // mbtiles exist in the data config
|
if (dataItemId) {
|
||||||
|
// mbtiles exist in the data config
|
||||||
return dataItemId;
|
return dataItemId;
|
||||||
} else {
|
} else {
|
||||||
if (fromData || !allowMoreData) {
|
if (fromData || !allowMoreData) {
|
||||||
console.log(`ERROR: style "${item.style}" using unknown mbtiles "${mbtiles}"! Skipping...`);
|
console.log(
|
||||||
|
`ERROR: style "${item.style}" using unknown mbtiles "${mbtiles}"! Skipping...`,
|
||||||
|
);
|
||||||
return undefined;
|
return undefined;
|
||||||
} else {
|
} else {
|
||||||
let id = mbtiles.substr(0, mbtiles.lastIndexOf('.')) || mbtiles;
|
let id = mbtiles.substr(0, mbtiles.lastIndexOf('.')) || mbtiles;
|
||||||
while (data[id]) id += '_';
|
//while (data[id]) id += '_';
|
||||||
data[id] = {
|
data[id] = {
|
||||||
'mbtiles': mbtiles
|
mbtiles: mbtiles,
|
||||||
};
|
};
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, font => {
|
},
|
||||||
|
(font) => {
|
||||||
if (reportFonts) {
|
if (reportFonts) {
|
||||||
serving.fonts[font] = true;
|
serving.fonts[font] = true;
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (success && item.serve_rendered !== false) {
|
if (success && item.serve_rendered !== false) {
|
||||||
if (serve_rendered) {
|
if (!isLight) {
|
||||||
startupPromises.push(serve_rendered.add(options, serving.rendered, item, id, opts.publicUrl,
|
startupPromises.push(
|
||||||
mbtiles => {
|
serve_rendered.add(
|
||||||
let mbtilesFile;
|
options,
|
||||||
for (const id of Object.keys(data)) {
|
serving.rendered,
|
||||||
if (id === mbtiles) {
|
item,
|
||||||
mbtilesFile = data[id].mbtiles;
|
id,
|
||||||
|
opts.publicUrl,
|
||||||
|
(mbtiles) => {
|
||||||
|
let mbtilesFile;
|
||||||
|
for (const id of Object.keys(data)) {
|
||||||
|
if (id === mbtiles) {
|
||||||
|
mbtilesFile = data[id].mbtiles;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
return mbtilesFile;
|
||||||
return mbtilesFile;
|
},
|
||||||
}
|
),
|
||||||
));
|
);
|
||||||
} else {
|
} else {
|
||||||
item.serve_rendered = false;
|
item.serve_rendered = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let addData = (id, item) => {
|
||||||
|
console.log(`Add data ${id}`);
|
||||||
|
Promise.all([serve_data.add(options, serving.data, item, id, opts.publicUrl)]);
|
||||||
|
};
|
||||||
|
|
||||||
for (const id of Object.keys(config.styles || {})) {
|
for (const id of Object.keys(config.styles || {})) {
|
||||||
const item = config.styles[id];
|
const item = config.styles[id];
|
||||||
if (!item.style || item.style.length === 0) {
|
if (!item.style || item.style.length === 0) {
|
||||||
@@ -183,9 +262,9 @@ function start(opts) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
startupPromises.push(
|
startupPromises.push(
|
||||||
serve_font(options, serving.fonts).then(sub => {
|
serve_font(options, serving.fonts).then((sub) => {
|
||||||
app.use('/', sub);
|
app.use('/', sub);
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
for (const id of Object.keys(data)) {
|
for (const id of Object.keys(data)) {
|
||||||
@@ -196,61 +275,97 @@ function start(opts) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
startupPromises.push(
|
startupPromises.push(
|
||||||
serve_data.add(options, serving.data, item, id, opts.publicUrl)
|
serve_data.add(options, serving.data, item, id, opts.publicUrl),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.serveAllStyles) {
|
if (options.serveAllStyles) {
|
||||||
fs.readdir(options.paths.styles, {withFileTypes: true}, (err, files) => {
|
fs.readdir(options.paths.mbtiles, {withFileTypes: true}, (err, files) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
if (file.isFile() &&
|
if (file.isFile() && path.extname(file.name).toLowerCase() == '.mbtiles') {
|
||||||
path.extname(file.name).toLowerCase() == '.json') {
|
let id = path.basename(file.name, '.mbtiles');
|
||||||
let id = path.basename(file.name, '.json');
|
|
||||||
let item = {
|
let item = {
|
||||||
style: file.name
|
mbtiles: file.name
|
||||||
};
|
};
|
||||||
addStyle(id, item, false, false);
|
addData(id, item);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const watcher = chokidar.watch(path.join(options.paths.styles, '*.json'),
|
fs.readdir(options.paths.styles, { withFileTypes: true }, (err, files) => {
|
||||||
{
|
if (err) {
|
||||||
});
|
return;
|
||||||
watcher.on('all',
|
}
|
||||||
(eventType, filename) => {
|
for (const file of files) {
|
||||||
if (filename) {
|
if (file.isFile() && path.extname(file.name).toLowerCase() == '.json') {
|
||||||
let id = path.basename(filename, '.json');
|
const id = path.basename(file.name, '.json');
|
||||||
console.log(`Style "${id}" changed, updating...`);
|
const item = {
|
||||||
|
style: file.name,
|
||||||
serve_style.remove(serving.styles, id);
|
};
|
||||||
if (serve_rendered) {
|
addStyle(id, item, true, true);
|
||||||
serve_rendered.remove(serving.rendered, id);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (eventType == "add" || eventType == "change") {
|
|
||||||
let item = {
|
|
||||||
style: filename
|
|
||||||
};
|
|
||||||
addStyle(id, item, false, false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const watcherData = chokidar.watch(path.join(options.paths.mbtiles, '*.mbtiles'),
|
||||||
|
{
|
||||||
|
});
|
||||||
|
watcherData.on('all', (eventType, filename) => {
|
||||||
|
if (filename) {
|
||||||
|
let id = path.basename(filename, '.mbtiles');
|
||||||
|
console.log(`Data "${id}" added`);
|
||||||
|
|
||||||
|
if (eventType == "add") {
|
||||||
|
let item = {
|
||||||
|
mbtiles: filename
|
||||||
|
};
|
||||||
|
addData(id, item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
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...`);
|
||||||
|
|
||||||
|
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, true, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
app.get('/styles.json', (req, res, next) => {
|
app.get('/styles.json', (req, res, next) => {
|
||||||
const result = [];
|
const result = [];
|
||||||
const query = req.query.key ? (`?key=${encodeURIComponent(req.query.key)}`) : '';
|
const query = req.query.key
|
||||||
|
? `?key=${encodeURIComponent(req.query.key)}`
|
||||||
|
: '';
|
||||||
for (const id of Object.keys(serving.styles)) {
|
for (const id of Object.keys(serving.styles)) {
|
||||||
const styleJSON = serving.styles[id].styleJSON;
|
const styleJSON = serving.styles[id].styleJSON;
|
||||||
result.push({
|
result.push({
|
||||||
version: styleJSON.version,
|
version: styleJSON.version,
|
||||||
name: styleJSON.name,
|
name: styleJSON.name,
|
||||||
id: id,
|
id: id,
|
||||||
url: `${utils.getPublicUrl(opts.publicUrl, req)}styles/${id}/style.json${query}`
|
url: `${getPublicUrl(
|
||||||
|
opts.publicUrl,
|
||||||
|
req,
|
||||||
|
)}styles/${id}/style.json${query}`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
res.send(result);
|
res.send(result);
|
||||||
@@ -265,9 +380,16 @@ function start(opts) {
|
|||||||
} else {
|
} else {
|
||||||
path = `${type}/${id}`;
|
path = `${type}/${id}`;
|
||||||
}
|
}
|
||||||
info.tiles = utils.getTileUrls(req, info.tiles, path, info.format, opts.publicUrl, {
|
info.tiles = getTileUrls(
|
||||||
'pbf': options.pbfAlias
|
req,
|
||||||
});
|
info.tiles,
|
||||||
|
path,
|
||||||
|
info.format,
|
||||||
|
opts.publicUrl,
|
||||||
|
{
|
||||||
|
pbf: options.pbfAlias,
|
||||||
|
},
|
||||||
|
);
|
||||||
arr.push(info);
|
arr.push(info);
|
||||||
}
|
}
|
||||||
return arr;
|
return arr;
|
||||||
@@ -283,7 +405,7 @@ function start(opts) {
|
|||||||
res.send(addTileJSONs(addTileJSONs([], req, 'rendered'), req, 'data'));
|
res.send(addTileJSONs(addTileJSONs([], req, 'rendered'), req, 'data'));
|
||||||
});
|
});
|
||||||
|
|
||||||
//------------------------------------
|
// ------------------------------------
|
||||||
// serve web presentations
|
// serve web presentations
|
||||||
app.use('/', express.static(path.join(__dirname, '../public/resources')));
|
app.use('/', express.static(path.join(__dirname, '../public/resources')));
|
||||||
|
|
||||||
@@ -293,43 +415,52 @@ function start(opts) {
|
|||||||
if (template === 'index') {
|
if (template === 'index') {
|
||||||
if (options.frontPage === false) {
|
if (options.frontPage === false) {
|
||||||
return;
|
return;
|
||||||
} else if (options.frontPage &&
|
} else if (
|
||||||
options.frontPage.constructor === String) {
|
options.frontPage &&
|
||||||
|
options.frontPage.constructor === String
|
||||||
|
) {
|
||||||
templateFile = path.resolve(paths.root, options.frontPage);
|
templateFile = path.resolve(paths.root, options.frontPage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
startupPromises.push(new Promise((resolve, reject) => {
|
startupPromises.push(
|
||||||
fs.readFile(templateFile, (err, content) => {
|
new Promise((resolve, reject) => {
|
||||||
if (err) {
|
fs.readFile(templateFile, (err, content) => {
|
||||||
err = new Error(`Template not found: ${err.message}`);
|
if (err) {
|
||||||
reject(err);
|
err = new Error(`Template not found: ${err.message}`);
|
||||||
return;
|
reject(err);
|
||||||
}
|
return;
|
||||||
const compiled = handlebars.compile(content.toString());
|
|
||||||
|
|
||||||
app.use(urlPath, (req, res, next) => {
|
|
||||||
let data = {};
|
|
||||||
if (dataGetter) {
|
|
||||||
data = dataGetter(req);
|
|
||||||
if (!data) {
|
|
||||||
return res.status(404).send('Not found');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
data['server_version'] = `${packageJson.name} v${packageJson.version}`;
|
const compiled = handlebars.compile(content.toString());
|
||||||
data['public_url'] = opts.publicUrl || '/';
|
|
||||||
data['is_light'] = isLight;
|
app.use(urlPath, (req, res, next) => {
|
||||||
data['key_query_part'] =
|
let data = {};
|
||||||
req.query.key ? `key=${encodeURIComponent(req.query.key)}&` : '';
|
if (dataGetter) {
|
||||||
data['key_query'] = req.query.key ? `?key=${encodeURIComponent(req.query.key)}` : '';
|
data = dataGetter(req);
|
||||||
if (template === 'wmts') res.set('Content-Type', 'text/xml');
|
if (!data) {
|
||||||
return res.status(200).send(compiled(data));
|
return res.status(404).send('Not found');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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));
|
||||||
|
});
|
||||||
|
resolve();
|
||||||
});
|
});
|
||||||
resolve();
|
}),
|
||||||
});
|
);
|
||||||
}));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
serveTemplate('/$', 'index', req => {
|
serveTemplate('/$', 'index', (req) => {
|
||||||
const styles = clone(serving.styles || {});
|
const styles = clone(serving.styles || {});
|
||||||
for (const id of Object.keys(styles)) {
|
for (const id of Object.keys(styles)) {
|
||||||
const style = styles[id];
|
const style = styles[id];
|
||||||
@@ -339,15 +470,23 @@ function start(opts) {
|
|||||||
if (style.serving_rendered) {
|
if (style.serving_rendered) {
|
||||||
const center = style.serving_rendered.tileJSON.center;
|
const center = style.serving_rendered.tileJSON.center;
|
||||||
if (center) {
|
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)}`;
|
||||||
|
|
||||||
const centerPx = mercator.px([center[0], center[1]], center[2]);
|
const centerPx = mercator.px([center[0], center[1]], center[2]);
|
||||||
style.thumbnail = `${center[2]}/${Math.floor(centerPx[0] / 256)}/${Math.floor(centerPx[1] / 256)}.png`;
|
style.thumbnail = `${center[2]}/${Math.floor(
|
||||||
|
centerPx[0] / 256,
|
||||||
|
)}/${Math.floor(centerPx[1] / 256)}.png`;
|
||||||
}
|
}
|
||||||
|
|
||||||
style.xyz_link = utils.getTileUrls(
|
style.xyz_link = getTileUrls(
|
||||||
req, style.serving_rendered.tileJSON.tiles,
|
req,
|
||||||
`styles/${id}`, style.serving_rendered.tileJSON.format, opts.publicUrl)[0];
|
style.serving_rendered.tileJSON.tiles,
|
||||||
|
`styles/${id}`,
|
||||||
|
style.serving_rendered.tileJSON.format,
|
||||||
|
opts.publicUrl,
|
||||||
|
)[0];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const data = clone(serving.data || {});
|
const data = clone(serving.data || {});
|
||||||
@@ -356,19 +495,29 @@ function start(opts) {
|
|||||||
const tilejson = data[id].tileJSON;
|
const tilejson = data[id].tileJSON;
|
||||||
const center = tilejson.center;
|
const center = tilejson.center;
|
||||||
if (center) {
|
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 = tilejson.format === 'pbf';
|
data_.is_vector = tilejson.format === 'pbf';
|
||||||
if (!data_.is_vector) {
|
if (!data_.is_vector) {
|
||||||
if (center) {
|
if (center) {
|
||||||
const centerPx = mercator.px([center[0], center[1]], center[2]);
|
const centerPx = mercator.px([center[0], center[1]], center[2]);
|
||||||
data_.thumbnail = `${center[2]}/${Math.floor(centerPx[0] / 256)}/${Math.floor(centerPx[1] / 256)}.${data_.tileJSON.format}`;
|
data_.thumbnail = `${center[2]}/${Math.floor(
|
||||||
|
centerPx[0] / 256,
|
||||||
|
)}/${Math.floor(centerPx[1] / 256)}.${data_.tileJSON.format}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
data_.xyz_link = utils.getTileUrls(
|
data_.xyz_link = getTileUrls(
|
||||||
req, tilejson.tiles, `data/${id}`, tilejson.format, opts.publicUrl, {
|
req,
|
||||||
'pbf': options.pbfAlias
|
tilejson.tiles,
|
||||||
})[0];
|
`data/${id}`,
|
||||||
|
tilejson.format,
|
||||||
|
opts.publicUrl,
|
||||||
|
{
|
||||||
|
pbf: options.pbfAlias,
|
||||||
|
},
|
||||||
|
)[0];
|
||||||
}
|
}
|
||||||
if (data_.filesize) {
|
if (data_.filesize) {
|
||||||
let suffix = 'kB';
|
let suffix = 'kB';
|
||||||
@@ -386,11 +535,11 @@ function start(opts) {
|
|||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
styles: Object.keys(styles).length ? styles : null,
|
styles: Object.keys(styles).length ? styles : null,
|
||||||
data: Object.keys(data).length ? data : null
|
data: Object.keys(data).length ? data : null,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
serveTemplate('/styles/:id/$', 'viewer', req => {
|
serveTemplate('/styles/:id/$', 'viewer', (req) => {
|
||||||
const id = req.params.id;
|
const id = req.params.id;
|
||||||
const style = clone(((serving.styles || {})[id] || {}).styleJSON);
|
const style = clone(((serving.styles || {})[id] || {}).styleJSON);
|
||||||
if (!style) {
|
if (!style) {
|
||||||
@@ -408,22 +557,30 @@ function start(opts) {
|
|||||||
return res.redirect(301, '/styles/' + req.params.id + '/');
|
return res.redirect(301, '/styles/' + req.params.id + '/');
|
||||||
});
|
});
|
||||||
*/
|
*/
|
||||||
serveTemplate('/styles/:id/wmts.xml', 'wmts', req => {
|
serveTemplate('/styles/:id/wmts.xml', 'wmts', (req) => {
|
||||||
const id = req.params.id;
|
const id = req.params.id;
|
||||||
const wmts = clone((serving.styles || {})[id]);
|
const wmts = clone((serving.styles || {})[id]);
|
||||||
if (!wmts) {
|
if (!wmts) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (wmts.hasOwnProperty("serve_rendered") && !wmts.serve_rendered) {
|
if (wmts.hasOwnProperty('serve_rendered') && !wmts.serve_rendered) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
wmts.id = id;
|
wmts.id = id;
|
||||||
wmts.name = (serving.styles[id] || serving.rendered[id]).name;
|
wmts.name = (serving.styles[id] || serving.rendered[id]).name;
|
||||||
wmts.baseUrl = `${req.get('X-Forwarded-Protocol') ? req.get('X-Forwarded-Protocol') : req.protocol}://${req.get('host')}`;
|
if (opts.publicUrl) {
|
||||||
|
wmts.baseUrl = opts.publicUrl;
|
||||||
|
} else {
|
||||||
|
wmts.baseUrl = `${
|
||||||
|
req.get('X-Forwarded-Protocol')
|
||||||
|
? req.get('X-Forwarded-Protocol')
|
||||||
|
: req.protocol
|
||||||
|
}://${req.get('host')}/`;
|
||||||
|
}
|
||||||
return wmts;
|
return wmts;
|
||||||
});
|
});
|
||||||
|
|
||||||
serveTemplate('/data/:id/$', 'data', req => {
|
serveTemplate('/data/:id/$', 'data', (req) => {
|
||||||
const id = req.params.id;
|
const id = req.params.id;
|
||||||
const data = clone(serving.data[id]);
|
const data = clone(serving.data[id]);
|
||||||
if (!data) {
|
if (!data) {
|
||||||
@@ -447,13 +604,17 @@ function start(opts) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const server = app.listen(process.env.PORT || opts.port, process.env.BIND || opts.bind, function () {
|
const server = app.listen(
|
||||||
let address = this.address().address;
|
process.env.PORT || opts.port,
|
||||||
if (address.indexOf('::') === 0) {
|
process.env.BIND || opts.bind,
|
||||||
address = `[${address}]`; // literal IPv6 address
|
function () {
|
||||||
}
|
let address = this.address().address;
|
||||||
console.log(`Listening at http://${address}:${this.address().port}/`);
|
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
|
// add server.shutdown() to gracefully stop serving
|
||||||
enableShutdown(server);
|
enableShutdown(server);
|
||||||
@@ -461,30 +622,39 @@ function start(opts) {
|
|||||||
return {
|
return {
|
||||||
app: app,
|
app: app,
|
||||||
server: server,
|
server: server,
|
||||||
startupPromise: startupPromise
|
startupPromise: startupPromise,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = opts => {
|
/**
|
||||||
|
* Stop the server gracefully
|
||||||
|
* @param {string} signal Name of the received signal
|
||||||
|
*/
|
||||||
|
function stopGracefully(signal) {
|
||||||
|
console.log(`Caught signal ${signal}, stopping gracefully`);
|
||||||
|
process.exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param opts
|
||||||
|
*/
|
||||||
|
export function server(opts) {
|
||||||
const running = start(opts);
|
const running = start(opts);
|
||||||
|
|
||||||
running.startupPromise.catch(err => {
|
running.startupPromise.catch((err) => {
|
||||||
console.error(err.message);
|
console.error(err.message);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
process.on('SIGINT', () => {
|
process.on('SIGINT', stopGracefully);
|
||||||
process.exit();
|
process.on('SIGTERM', stopGracefully);
|
||||||
});
|
|
||||||
|
|
||||||
process.on('SIGHUP', () => {
|
process.on('SIGHUP', (signal) => {
|
||||||
|
console.log(`Caught signal ${signal}, refreshing`);
|
||||||
console.log('Stopping server and reloading config');
|
console.log('Stopping server and reloading config');
|
||||||
|
|
||||||
running.server.shutdown(() => {
|
running.server.shutdown(() => {
|
||||||
for (const key in require.cache) {
|
|
||||||
delete require.cache[key];
|
|
||||||
}
|
|
||||||
|
|
||||||
const restarted = start(opts);
|
const restarted = start(opts);
|
||||||
running.server = restarted.server;
|
running.server = restarted.server;
|
||||||
running.app = restarted.app;
|
running.app = restarted.app;
|
||||||
@@ -492,4 +662,4 @@ module.exports = opts => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return running;
|
return running;
|
||||||
};
|
}
|
||||||
|
|||||||
168
src/utils.js
168
src/utils.js
@@ -1,24 +1,57 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const path = require('path');
|
import path from 'path';
|
||||||
const fs = require('fs');
|
import fs from 'node:fs';
|
||||||
|
|
||||||
const clone = require('clone');
|
import clone from 'clone';
|
||||||
const glyphCompose = require('@mapbox/glyph-pbf-composite');
|
import glyphCompose from '@mapbox/glyph-pbf-composite';
|
||||||
|
|
||||||
|
|
||||||
module.exports.getPublicUrl = (publicUrl, req) => publicUrl || `${req.protocol}://${req.headers.host}/`;
|
var EARTH_RADIUS = 6371.0088;
|
||||||
|
|
||||||
module.exports.getTileUrls = (req, domains, path, format, publicUrl, aliases) => {
|
function degToRad(degrees) {
|
||||||
|
return degrees * (Math.PI / 180);
|
||||||
|
}
|
||||||
|
|
||||||
|
function tileToLon(tileX, zoom) {
|
||||||
|
return ((tileX / 2**zoom) * 360.0) - 180.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function tileToLat(tileY, zoom) {
|
||||||
|
var n = Math.PI - 2 * Math.PI * tileY / 2**zoom;
|
||||||
|
return (180.0 / Math.PI) * Math.atan(0.5 * (Math.exp(n) - Math.exp(-n)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate new URL object
|
||||||
|
* @param req
|
||||||
|
* @params {object} req - Express request
|
||||||
|
* @returns {URL} object
|
||||||
|
*/
|
||||||
|
const 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;
|
||||||
|
return urlObject;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getPublicUrl = (publicUrl, req) => {
|
||||||
|
if (publicUrl) {
|
||||||
|
return publicUrl;
|
||||||
|
}
|
||||||
|
return getUrlObject(req).toString();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getTileUrls = (req, domains, path, format, publicUrl, aliases) => {
|
||||||
|
const urlObject = getUrlObject(req);
|
||||||
if (domains) {
|
if (domains) {
|
||||||
if (domains.constructor === String && domains.length > 0) {
|
if (domains.constructor === String && domains.length > 0) {
|
||||||
domains = domains.split(',');
|
domains = domains.split(',');
|
||||||
}
|
}
|
||||||
const host = req.headers.host;
|
const hostParts = urlObject.host.split('.');
|
||||||
const hostParts = host.split('.');
|
const relativeSubdomainsUsable =
|
||||||
const relativeSubdomainsUsable = hostParts.length > 1 &&
|
hostParts.length > 1 &&
|
||||||
!/^([0-9]{1,3}\.){3}[0-9]{1,3}(\:[0-9]+)?$/.test(host);
|
!/^([0-9]{1,3}\.){3}[0-9]{1,3}(\:[0-9]+)?$/.test(urlObject.host);
|
||||||
const newDomains = [];
|
const newDomains = [];
|
||||||
for (const domain of domains) {
|
for (const domain of domains) {
|
||||||
if (domain.indexOf('*') !== -1) {
|
if (domain.indexOf('*') !== -1) {
|
||||||
@@ -34,10 +67,9 @@ module.exports.getTileUrls = (req, domains, path, format, publicUrl, aliases) =>
|
|||||||
domains = newDomains;
|
domains = newDomains;
|
||||||
}
|
}
|
||||||
if (!domains || domains.length == 0) {
|
if (!domains || domains.length == 0) {
|
||||||
domains = [req.headers.host];
|
domains = [urlObject.host];
|
||||||
}
|
}
|
||||||
|
|
||||||
const key = req.query.key;
|
|
||||||
const queryParams = [];
|
const queryParams = [];
|
||||||
if (req.query.key) {
|
if (req.query.key) {
|
||||||
queryParams.push(`key=${encodeURIComponent(req.query.key)}`);
|
queryParams.push(`key=${encodeURIComponent(req.query.key)}`);
|
||||||
@@ -45,7 +77,7 @@ module.exports.getTileUrls = (req, domains, path, format, publicUrl, aliases) =>
|
|||||||
if (req.query.style) {
|
if (req.query.style) {
|
||||||
queryParams.push(`style=${encodeURIComponent(req.query.style)}`);
|
queryParams.push(`style=${encodeURIComponent(req.query.style)}`);
|
||||||
}
|
}
|
||||||
const query = queryParams.length > 0 ? (`?${queryParams.join('&')}`) : '';
|
const query = queryParams.length > 0 ? `?${queryParams.join('&')}` : '';
|
||||||
|
|
||||||
if (aliases && aliases[format]) {
|
if (aliases && aliases[format]) {
|
||||||
format = aliases[format];
|
format = aliases[format];
|
||||||
@@ -54,16 +86,18 @@ module.exports.getTileUrls = (req, domains, path, format, publicUrl, aliases) =>
|
|||||||
const uris = [];
|
const uris = [];
|
||||||
if (!publicUrl) {
|
if (!publicUrl) {
|
||||||
for (const domain of domains) {
|
for (const domain of domains) {
|
||||||
uris.push(`${req.protocol}://${domain}/${path}/{z}/{x}/{y}.${format}${query}`);
|
uris.push(
|
||||||
|
`${req.protocol}://${domain}/${path}/{z}/{x}/{y}.${format}${query}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
uris.push(`${publicUrl}${path}/{z}/{x}/{y}.${format}${query}`)
|
uris.push(`${publicUrl}${path}/{z}/{x}/{y}.${format}${query}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return uris;
|
return uris;
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports.fixTileJSONCenter = tileJSON => {
|
export const fixTileJSONCenter = (tileJSON) => {
|
||||||
if (tileJSON.bounds && !tileJSON.center) {
|
if (tileJSON.bounds && !tileJSON.center) {
|
||||||
const fitWidth = 1024;
|
const fitWidth = 1024;
|
||||||
const tiles = fitWidth / 256;
|
const tiles = fitWidth / 256;
|
||||||
@@ -72,60 +106,76 @@ module.exports.fixTileJSONCenter = tileJSON => {
|
|||||||
(tileJSON.bounds[1] + tileJSON.bounds[3]) / 2,
|
(tileJSON.bounds[1] + tileJSON.bounds[3]) / 2,
|
||||||
Math.round(
|
Math.round(
|
||||||
-Math.log((tileJSON.bounds[2] - tileJSON.bounds[0]) / 360 / tiles) /
|
-Math.log((tileJSON.bounds[2] - tileJSON.bounds[0]) / 360 / tiles) /
|
||||||
Math.LN2
|
Math.LN2,
|
||||||
)
|
),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getFontPbf = (allowedFonts, fontPath, name, range, fallbacks) => new Promise((resolve, reject) => {
|
const getFontPbf = (allowedFonts, fontPath, name, range, fallbacks) =>
|
||||||
if (!allowedFonts || (allowedFonts[name] && fallbacks)) {
|
new Promise((resolve, reject) => {
|
||||||
const filename = path.join(fontPath, name, `${range}.pbf`);
|
if (!allowedFonts || (allowedFonts[name] && fallbacks)) {
|
||||||
if (!fallbacks) {
|
const filename = path.join(fontPath, name, `${range}.pbf`);
|
||||||
fallbacks = clone(allowedFonts || {});
|
if (!fallbacks) {
|
||||||
}
|
fallbacks = clone(allowedFonts || {});
|
||||||
delete fallbacks[name];
|
|
||||||
fs.readFile(filename, (err, data) => {
|
|
||||||
if (err) {
|
|
||||||
console.error(`ERROR: Font not found: ${name}`);
|
|
||||||
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 ${fallbackName} as a fallback`);
|
|
||||||
delete fallbacks[fallbackName];
|
|
||||||
getFontPbf(null, fontPath, fallbackName, range, fallbacks).then(resolve, reject);
|
|
||||||
} else {
|
|
||||||
reject(`Font load error: ${name}`);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
resolve(data);
|
|
||||||
}
|
}
|
||||||
});
|
delete fallbacks[name];
|
||||||
} else {
|
fs.readFile(filename, (err, data) => {
|
||||||
reject(`Font not allowed: ${name}`);
|
if (err) {
|
||||||
}
|
console.error(`ERROR: Font not found: ${name}`);
|
||||||
});
|
if (fallbacks && Object.keys(fallbacks).length) {
|
||||||
|
let fallbackName;
|
||||||
|
|
||||||
module.exports.getFontsPbf = (allowedFonts, fontPath, names, range, fallbacks) => {
|
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 ${fallbackName} as a fallback`);
|
||||||
|
delete fallbacks[fallbackName];
|
||||||
|
getFontPbf(null, fontPath, fallbackName, range, fallbacks).then(
|
||||||
|
resolve,
|
||||||
|
reject,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
reject(`Font load error: ${name}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
resolve(data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
reject(`Font not allowed: ${name}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export const getFontsPbf = (
|
||||||
|
allowedFonts,
|
||||||
|
fontPath,
|
||||||
|
names,
|
||||||
|
range,
|
||||||
|
fallbacks,
|
||||||
|
) => {
|
||||||
const fonts = names.split(',');
|
const fonts = names.split(',');
|
||||||
const queue = [];
|
const queue = [];
|
||||||
for (const font of fonts) {
|
for (const font of fonts) {
|
||||||
queue.push(
|
queue.push(
|
||||||
getFontPbf(allowedFonts, fontPath, font, range, clone(allowedFonts || fallbacks))
|
getFontPbf(
|
||||||
|
allowedFonts,
|
||||||
|
fontPath,
|
||||||
|
font,
|
||||||
|
range,
|
||||||
|
clone(allowedFonts || fallbacks),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Promise.all(queue).then(values => glyphCompose.combine(values));
|
return Promise.all(queue).then((values) => glyphCompose.combine(values));
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,48 +1,48 @@
|
|||||||
var testTileJSONArray = function(url) {
|
const testTileJSONArray = function (url) {
|
||||||
describe(url + ' is array of TileJSONs', function() {
|
describe(url + ' is array of TileJSONs', function () {
|
||||||
it('is json', function(done) {
|
it('is json', function (done) {
|
||||||
supertest(app)
|
supertest(app)
|
||||||
.get(url)
|
.get(url)
|
||||||
.expect(200)
|
.expect(200)
|
||||||
.expect('Content-Type', /application\/json/, done);
|
.expect('Content-Type', /application\/json/, done);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('is non-empty array', function(done) {
|
it('is non-empty array', function (done) {
|
||||||
supertest(app)
|
supertest(app)
|
||||||
.get(url)
|
.get(url)
|
||||||
.expect(function(res) {
|
.expect(function (res) {
|
||||||
res.body.should.be.Array();
|
expect(res.body).to.be.a('array');
|
||||||
res.body.length.should.be.greaterThan(0);
|
expect(res.body.length).to.be.greaterThan(0);
|
||||||
}).end(done);
|
})
|
||||||
|
.end(done);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
var testTileJSON = function(url) {
|
const testTileJSON = function (url) {
|
||||||
describe(url + ' is TileJSON', function() {
|
describe(url + ' is TileJSON', function () {
|
||||||
it('is json', function(done) {
|
it('is json', function (done) {
|
||||||
supertest(app)
|
supertest(app)
|
||||||
.get(url)
|
.get(url)
|
||||||
.expect(200)
|
.expect(200)
|
||||||
.expect('Content-Type', /application\/json/, done);
|
.expect('Content-Type', /application\/json/, done);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('has valid tiles', function(done) {
|
it('has valid tiles', function (done) {
|
||||||
supertest(app)
|
supertest(app)
|
||||||
.get(url)
|
.get(url)
|
||||||
.expect(function(res) {
|
.expect(function (res) {
|
||||||
res.body.tiles.length.should.be.greaterThan(0);
|
expect(res.body.tiles.length).to.be.greaterThan(0);
|
||||||
}).end(done);
|
})
|
||||||
|
.end(done);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('Metadata', function() {
|
describe('Metadata', function () {
|
||||||
describe('/health', function() {
|
describe('/health', function () {
|
||||||
it('returns 200', function(done) {
|
it('returns 200', function (done) {
|
||||||
supertest(app)
|
supertest(app).get('/health').expect(200, done);
|
||||||
.get('/health')
|
|
||||||
.expect(200, done);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -50,24 +50,25 @@ describe('Metadata', function() {
|
|||||||
testTileJSONArray('/rendered.json');
|
testTileJSONArray('/rendered.json');
|
||||||
testTileJSONArray('/data.json');
|
testTileJSONArray('/data.json');
|
||||||
|
|
||||||
describe('/styles.json is valid array', function() {
|
describe('/styles.json is valid array', function () {
|
||||||
it('is json', function(done) {
|
it('is json', function (done) {
|
||||||
supertest(app)
|
supertest(app)
|
||||||
.get('/styles.json')
|
.get('/styles.json')
|
||||||
.expect(200)
|
.expect(200)
|
||||||
.expect('Content-Type', /application\/json/, done);
|
.expect('Content-Type', /application\/json/, done);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('contains valid item', function(done) {
|
it('contains valid item', function (done) {
|
||||||
supertest(app)
|
supertest(app)
|
||||||
.get('/styles.json')
|
.get('/styles.json')
|
||||||
.expect(function(res) {
|
.expect(function (res) {
|
||||||
res.body.should.be.Array();
|
expect(res.body).to.be.a('array');
|
||||||
res.body.length.should.be.greaterThan(0);
|
expect(res.body.length).to.be.greaterThan(0);
|
||||||
res.body[0].version.should.equal(8);
|
expect(res.body[0].version).to.be.equal(8);
|
||||||
res.body[0].id.should.be.String();
|
expect(res.body[0].id).to.be.a('string');
|
||||||
res.body[0].name.should.be.String();
|
expect(res.body[0].name).to.be.a('string');
|
||||||
}).end(done);
|
})
|
||||||
|
.end(done);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,24 +1,29 @@
|
|||||||
process.env.NODE_ENV = 'test';
|
process.env.NODE_ENV = 'test';
|
||||||
|
|
||||||
global.should = require('should');
|
import { expect } from 'chai';
|
||||||
global.supertest = require('supertest');
|
import supertest from 'supertest';
|
||||||
|
import { server } from '../src/server.js';
|
||||||
|
|
||||||
require = require('esm')(module);
|
global.expect = expect;
|
||||||
|
global.supertest = supertest;
|
||||||
|
|
||||||
before(function() {
|
before(function () {
|
||||||
console.log('global setup');
|
console.log('global setup');
|
||||||
process.chdir('test_data');
|
process.chdir('test_data');
|
||||||
var running = require('../src/server')({
|
const running = server({
|
||||||
configPath: 'config.json',
|
configPath: 'config.json',
|
||||||
port: 8888,
|
port: 8888,
|
||||||
publicUrl: '/test/'
|
publicUrl: '/test/',
|
||||||
});
|
});
|
||||||
global.app = running.app;
|
global.app = running.app;
|
||||||
global.server = running.server;
|
global.server = running.server;
|
||||||
return running.startupPromise;
|
return running.startupPromise;
|
||||||
});
|
});
|
||||||
|
|
||||||
after(function() {
|
after(function () {
|
||||||
console.log('global teardown');
|
console.log('global teardown');
|
||||||
global.server.close(function() { console.log('Done'); process.exit(); });
|
global.server.close(function () {
|
||||||
|
console.log('Done');
|
||||||
|
process.exit();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
174
test/static.js
174
test/static.js
@@ -1,30 +1,58 @@
|
|||||||
var testStatic = function(prefix, q, format, status, scale, type, query) {
|
const testStatic = function (prefix, q, format, status, scale, type, query) {
|
||||||
if (scale) q += '@' + scale + 'x';
|
if (scale) q += '@' + scale + 'x';
|
||||||
var path = '/styles/' + prefix + '/static/' + q + '.' + format;
|
let path = '/styles/' + prefix + '/static/' + q + '.' + format;
|
||||||
if (query) {
|
if (query) {
|
||||||
path += query;
|
path += query;
|
||||||
}
|
}
|
||||||
it(path + ' returns ' + status, function(done) {
|
it(path + ' returns ' + status, function (done) {
|
||||||
var test = supertest(app).get(path);
|
const test = supertest(app).get(path);
|
||||||
if (status) test.expect(status);
|
if (status) test.expect(status);
|
||||||
if (type) test.expect('Content-Type', type);
|
if (type) test.expect('Content-Type', type);
|
||||||
test.end(done);
|
test.end(done);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
var prefix = 'test-style';
|
const prefix = 'test-style';
|
||||||
|
|
||||||
describe('Static endpoints', function() {
|
describe('Static endpoints', function () {
|
||||||
describe('center-based', function() {
|
describe('center-based', function () {
|
||||||
describe('valid requests', function() {
|
describe('valid requests', function () {
|
||||||
describe('various formats', function() {
|
describe('various formats', function () {
|
||||||
testStatic(prefix, '0,0,0/256x256', 'png', 200, undefined, /image\/png/);
|
testStatic(
|
||||||
testStatic(prefix, '0,0,0/256x256', 'jpg', 200, undefined, /image\/jpeg/);
|
prefix,
|
||||||
testStatic(prefix, '0,0,0/256x256', 'jpeg', 200, undefined, /image\/jpeg/);
|
'0,0,0/256x256',
|
||||||
testStatic(prefix, '0,0,0/256x256', 'webp', 200, undefined, /image\/webp/);
|
'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() {
|
describe('different parameters', function () {
|
||||||
testStatic(prefix, '0,0,0/300x300', 'png', 200, 2);
|
testStatic(prefix, '0,0,0/300x300', 'png', 200, 2);
|
||||||
testStatic(prefix, '0,0,0/300x300', 'png', 200, 3);
|
testStatic(prefix, '0,0,0/300x300', 'png', 200, 3);
|
||||||
|
|
||||||
@@ -42,7 +70,7 @@ describe('Static endpoints', function() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('invalid requests return 4xx', function() {
|
describe('invalid requests return 4xx', function () {
|
||||||
testStatic(prefix, '190,0,0/256x256', 'png', 400);
|
testStatic(prefix, '190,0,0/256x256', 'png', 400);
|
||||||
testStatic(prefix, '0,86,0/256x256', 'png', 400);
|
testStatic(prefix, '0,86,0/256x256', 'png', 400);
|
||||||
testStatic(prefix, '80,40,20/0x0', 'png', 400);
|
testStatic(prefix, '80,40,20/0x0', 'png', 400);
|
||||||
@@ -57,16 +85,44 @@ describe('Static endpoints', function() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('area-based', function() {
|
describe('area-based', function () {
|
||||||
describe('valid requests', function() {
|
describe('valid requests', function () {
|
||||||
describe('various formats', function() {
|
describe('various formats', function () {
|
||||||
testStatic(prefix, '-180,-80,180,80/10x10', 'png', 200, undefined, /image\/png/);
|
testStatic(
|
||||||
testStatic(prefix, '-180,-80,180,80/10x10', 'jpg', 200, undefined, /image\/jpeg/);
|
prefix,
|
||||||
testStatic(prefix, '-180,-80,180,80/10x10', 'jpeg', 200, undefined, /image\/jpeg/);
|
'-180,-80,180,80/10x10',
|
||||||
testStatic(prefix, '-180,-80,180,80/10x10', 'webp', 200, undefined, /image\/webp/);
|
'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() {
|
describe('different parameters', function () {
|
||||||
testStatic(prefix, '-180,-90,180,90/20x20', 'png', 200, 2);
|
testStatic(prefix, '-180,-90,180,90/20x20', 'png', 200, 2);
|
||||||
testStatic(prefix, '0,0,1,1/200x200', 'png', 200, 3);
|
testStatic(prefix, '0,0,1,1/200x200', 'png', 200, 3);
|
||||||
|
|
||||||
@@ -74,7 +130,7 @@ describe('Static endpoints', function() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('invalid requests return 4xx', function() {
|
describe('invalid requests return 4xx', function () {
|
||||||
testStatic(prefix, '0,87,1,88/5x2', 'png', 400);
|
testStatic(prefix, '0,87,1,88/5x2', 'png', 400);
|
||||||
|
|
||||||
testStatic(prefix, '0,0,1,1/1x1', 'gif', 400);
|
testStatic(prefix, '0,0,1,1/1x1', 'gif', 400);
|
||||||
@@ -83,20 +139,72 @@ describe('Static endpoints', function() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('autofit path', function() {
|
describe('autofit path', function () {
|
||||||
describe('valid requests', function() {
|
describe('valid requests', function () {
|
||||||
testStatic(prefix, 'auto/256x256', 'png', 200, undefined, /image\/png/, '?path=10,10|20,20');
|
testStatic(
|
||||||
|
prefix,
|
||||||
|
'auto/256x256',
|
||||||
|
'png',
|
||||||
|
200,
|
||||||
|
undefined,
|
||||||
|
/image\/png/,
|
||||||
|
'?path=10,10|20,20',
|
||||||
|
);
|
||||||
|
|
||||||
describe('different parameters', function() {
|
describe('different parameters', function () {
|
||||||
testStatic(prefix, 'auto/20x20', 'png', 200, 2, /image\/png/, '?path=10,10|20,20');
|
testStatic(
|
||||||
testStatic(prefix, 'auto/200x200', 'png', 200, 3, /image\/png/, '?path=-10,-10|-20,-20');
|
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=' + decodeURIComponent('enc:{{biGwvyGoUi@s_A|{@'),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('invalid requests return 4xx', function() {
|
describe('invalid requests return 4xx', function () {
|
||||||
testStatic(prefix, 'auto/256x256', 'png', 400);
|
testStatic(prefix, 'auto/256x256', 'png', 400);
|
||||||
testStatic(prefix, 'auto/256x256', 'png', 400, undefined, undefined, '?path=10,10');
|
testStatic(
|
||||||
testStatic(prefix, 'auto/2560x2560', 'png', 400, undefined, undefined, '?path=10,10|20,20');
|
prefix,
|
||||||
|
'auto/256x256',
|
||||||
|
'png',
|
||||||
|
400,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
'?path=invalid',
|
||||||
|
);
|
||||||
|
testStatic(
|
||||||
|
prefix,
|
||||||
|
'auto/2560x2560',
|
||||||
|
'png',
|
||||||
|
400,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
'?path=10,10|20,20',
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,38 +1,41 @@
|
|||||||
var testIs = function(url, type, status) {
|
const testIs = function (url, type, status) {
|
||||||
it(url + ' return ' + (status || 200) + ' and is ' + type.toString(),
|
it(
|
||||||
function(done) {
|
url + ' return ' + (status || 200) + ' and is ' + type.toString(),
|
||||||
supertest(app)
|
function (done) {
|
||||||
.get(url)
|
supertest(app)
|
||||||
.expect(status || 200)
|
.get(url)
|
||||||
.expect('Content-Type', type, done);
|
.expect(status || 200)
|
||||||
});
|
.expect('Content-Type', type, done);
|
||||||
|
},
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
var prefix = 'test-style';
|
const prefix = 'test-style';
|
||||||
|
|
||||||
describe('Styles', function() {
|
describe('Styles', function () {
|
||||||
describe('/styles/' + prefix + '/style.json is valid style', function() {
|
describe('/styles/' + prefix + '/style.json is valid style', function () {
|
||||||
testIs('/styles/' + prefix + '/style.json', /application\/json/);
|
testIs('/styles/' + prefix + '/style.json', /application\/json/);
|
||||||
|
|
||||||
it('contains expected properties', function(done) {
|
it('contains expected properties', function (done) {
|
||||||
supertest(app)
|
supertest(app)
|
||||||
.get('/styles/' + prefix + '/style.json')
|
.get('/styles/' + prefix + '/style.json')
|
||||||
.expect(function(res) {
|
.expect(function (res) {
|
||||||
res.body.version.should.equal(8);
|
expect(res.body.version).to.be.equal(8);
|
||||||
res.body.name.should.be.String();
|
expect(res.body.name).to.be.a('string');
|
||||||
res.body.sources.should.be.Object();
|
expect(res.body.sources).to.be.a('object');
|
||||||
res.body.glyphs.should.be.String();
|
expect(res.body.glyphs).to.be.a('string');
|
||||||
res.body.sprite.should.be.String();
|
expect(res.body.sprite).to.be.a('string');
|
||||||
res.body.sprite.should.equal('/test/styles/test-style/sprite');
|
expect(res.body.sprite).to.be.equal('/test/styles/test-style/sprite');
|
||||||
res.body.layers.should.be.Array();
|
expect(res.body.layers).to.be.a('array');
|
||||||
}).end(done);
|
})
|
||||||
|
.end(done);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('/styles/streets/style.json is not served', function() {
|
describe('/styles/streets/style.json is not served', function () {
|
||||||
testIs('/styles/streets/style.json', /./, 404);
|
testIs('/styles/streets/style.json', /./, 404);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('/styles/' + prefix + '/sprite[@2x].{format}', function() {
|
describe('/styles/' + prefix + '/sprite[@2x].{format}', function () {
|
||||||
testIs('/styles/' + prefix + '/sprite.json', /application\/json/);
|
testIs('/styles/' + prefix + '/sprite.json', /application\/json/);
|
||||||
testIs('/styles/' + prefix + '/sprite@2x.json', /application\/json/);
|
testIs('/styles/' + prefix + '/sprite@2x.json', /application\/json/);
|
||||||
testIs('/styles/' + prefix + '/sprite.png', /image\/png/);
|
testIs('/styles/' + prefix + '/sprite.png', /image\/png/);
|
||||||
@@ -40,11 +43,13 @@ describe('Styles', function() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Fonts', function() {
|
describe('Fonts', function () {
|
||||||
testIs('/fonts/Open Sans Bold/0-255.pbf', /application\/x-protobuf/);
|
testIs('/fonts/Open Sans Bold/0-255.pbf', /application\/x-protobuf/);
|
||||||
testIs('/fonts/Open Sans Regular/65280-65535.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',
|
testIs(
|
||||||
/application\/x-protobuf/);
|
'/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,Open Sans Bold/0-255.pbf', /./, 400);
|
||||||
|
|
||||||
testIs('/fonts/Nonsense/0-255.pbf', /./, 400);
|
testIs('/fonts/Nonsense/0-255.pbf', /./, 400);
|
||||||
|
|||||||
@@ -1,22 +1,22 @@
|
|||||||
var testTile = function(prefix, z, x, y, status) {
|
const testTile = function (prefix, z, x, y, status) {
|
||||||
var path = '/data/' + prefix + '/' + z + '/' + x + '/' + y + '.pbf';
|
const path = '/data/' + prefix + '/' + z + '/' + x + '/' + y + '.pbf';
|
||||||
it(path + ' returns ' + status, function(done) {
|
it(path + ' returns ' + status, function (done) {
|
||||||
var test = supertest(app).get(path);
|
const test = supertest(app).get(path);
|
||||||
if (status) test.expect(status);
|
if (status) test.expect(status);
|
||||||
if (status == 200) test.expect('Content-Type', /application\/x-protobuf/);
|
if (status == 200) test.expect('Content-Type', /application\/x-protobuf/);
|
||||||
test.end(done);
|
test.end(done);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
var prefix = 'openmaptiles';
|
const prefix = 'openmaptiles';
|
||||||
|
|
||||||
describe('Vector tiles', function() {
|
describe('Vector tiles', function () {
|
||||||
describe('existing tiles', function() {
|
describe('existing tiles', function () {
|
||||||
testTile(prefix, 0, 0, 0, 200);
|
testTile(prefix, 0, 0, 0, 200);
|
||||||
testTile(prefix, 14, 8581, 5738, 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('non_existent', 0, 0, 0, 404);
|
||||||
testTile(prefix, -1, 0, 0, 404); // err zoom
|
testTile(prefix, -1, 0, 0, 404); // err zoom
|
||||||
testTile(prefix, 20, 0, 0, 404); // zoom out of bounds
|
testTile(prefix, 20, 0, 0, 404); // zoom out of bounds
|
||||||
|
|||||||
@@ -1,26 +1,26 @@
|
|||||||
var testTile = function(prefix, z, x, y, format, status, scale, type) {
|
const testTile = function (prefix, z, x, y, format, status, scale, type) {
|
||||||
if (scale) y += '@' + scale + 'x';
|
if (scale) y += '@' + scale + 'x';
|
||||||
var path = '/styles/' + prefix + '/' + z + '/' + x + '/' + y + '.' + format;
|
const path = '/styles/' + prefix + '/' + z + '/' + x + '/' + y + '.' + format;
|
||||||
it(path + ' returns ' + status, function(done) {
|
it(path + ' returns ' + status, function (done) {
|
||||||
var test = supertest(app).get(path);
|
const test = supertest(app).get(path);
|
||||||
test.expect(status);
|
test.expect(status);
|
||||||
if (type) test.expect('Content-Type', type);
|
if (type) test.expect('Content-Type', type);
|
||||||
test.end(done);
|
test.end(done);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
var prefix = 'test-style';
|
const prefix = 'test-style';
|
||||||
|
|
||||||
describe('Raster tiles', function() {
|
describe('Raster tiles', function () {
|
||||||
describe('valid requests', function() {
|
describe('valid requests', function () {
|
||||||
describe('various formats', function() {
|
describe('various formats', function () {
|
||||||
testTile(prefix, 0, 0, 0, 'png', 200, undefined, /image\/png/);
|
testTile(prefix, 0, 0, 0, 'png', 200, undefined, /image\/png/);
|
||||||
testTile(prefix, 0, 0, 0, 'jpg', 200, undefined, /image\/jpeg/);
|
testTile(prefix, 0, 0, 0, 'jpg', 200, undefined, /image\/jpeg/);
|
||||||
testTile(prefix, 0, 0, 0, 'jpeg', 200, undefined, /image\/jpeg/);
|
testTile(prefix, 0, 0, 0, 'jpeg', 200, undefined, /image\/jpeg/);
|
||||||
testTile(prefix, 0, 0, 0, 'webp', 200, undefined, /image\/webp/);
|
testTile(prefix, 0, 0, 0, 'webp', 200, undefined, /image\/webp/);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('different coordinates and scales', function() {
|
describe('different coordinates and scales', function () {
|
||||||
testTile(prefix, 1, 1, 1, 'png', 200);
|
testTile(prefix, 1, 1, 1, 'png', 200);
|
||||||
|
|
||||||
testTile(prefix, 0, 0, 0, 'png', 200, 2);
|
testTile(prefix, 0, 0, 0, 'png', 200, 2);
|
||||||
@@ -29,7 +29,7 @@ describe('Raster tiles', function() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('invalid requests return 4xx', function() {
|
describe('invalid requests return 4xx', function () {
|
||||||
testTile('non_existent', 0, 0, 0, 'png', 404);
|
testTile('non_existent', 0, 0, 0, 'png', 404);
|
||||||
testTile(prefix, -1, 0, 0, 'png', 404);
|
testTile(prefix, -1, 0, 0, 'png', 404);
|
||||||
testTile(prefix, 25, 0, 0, 'png', 404);
|
testTile(prefix, 25, 0, 0, 'png', 404);
|
||||||
@@ -41,6 +41,6 @@ describe('Raster tiles', function() {
|
|||||||
testTile(prefix, 0, 0, 0, 'png', 404, 1);
|
testTile(prefix, 0, 0, 0, 'png', 404, 1);
|
||||||
testTile(prefix, 0, 0, 0, 'png', 404, 5);
|
testTile(prefix, 0, 0, 0, 'png', 404, 5);
|
||||||
|
|
||||||
//testTile('hybrid', 0, 0, 0, 'png', 404); //TODO: test this
|
// testTile('hybrid', 0, 0, 0, 'png', 404); //TODO: test this
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user