Compare commits
30 commits
acalcutt-p
...
master
Author | SHA1 | Date | |
---|---|---|---|
4b1ffd489f | |||
![]() |
cd613e2fb5 | ||
![]() |
3e521a7d92 | ||
![]() |
d88fbb7c55 | ||
![]() |
6d4ab40b96 | ||
![]() |
5441a10488 | ||
![]() |
8a7d9957fb | ||
![]() |
6d1c617752 | ||
![]() |
3881219fee | ||
![]() |
3110cab18f | ||
![]() |
f2b48acb61 | ||
![]() |
6e0006ffcf | ||
![]() |
6ef12fba6c | ||
![]() |
3d8bf78974 | ||
![]() |
1d60dd6afc | ||
![]() |
7662cb84ce | ||
![]() |
467203e125 | ||
![]() |
9bb270b6c5 | ||
![]() |
f02c63c94c | ||
![]() |
b0a2cefb0e | ||
![]() |
f7be63a35c | ||
![]() |
407c2eb209 | ||
![]() |
4d11796967 | ||
![]() |
68ce971133 | ||
![]() |
5e1e87ac83 | ||
![]() |
5469ac313e | ||
![]() |
7e53bb8ab4 | ||
![]() |
5ce5fa5283 | ||
![]() |
97be9db6b7 | ||
![]() |
3abbb39633 |
44 changed files with 6213 additions and 6439 deletions
1
.github/workflows/automerger.yml
vendored
1
.github/workflows/automerger.yml
vendored
|
@ -14,3 +14,4 @@ jobs:
|
|||
- uses: fastify/github-action-merge-dependabot@v3
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
target: minor
|
||||
|
|
5
.github/workflows/ci.yml
vendored
5
.github/workflows/ci.yml
vendored
|
@ -21,6 +21,11 @@ jobs:
|
|||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Install dependencies (Ubuntu) 🚀
|
||||
run: >-
|
||||
sudo apt-get install -qq libcairo2-dev libjpeg8-dev libpango1.0-dev
|
||||
libgif-dev build-essential
|
||||
|
||||
- name: Setup node env 📦
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
|
|
18
.github/workflows/ct.yml
vendored
18
.github/workflows/ct.yml
vendored
|
@ -65,6 +65,22 @@ jobs:
|
|||
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
|
||||
|
||||
- name: Create Tileserver Light Directory
|
||||
run: node publish.js --no-publish
|
||||
|
||||
- name: Install node dependencies
|
||||
run: npm ci --prefer-offline --no-audit
|
||||
working-directory: ./light
|
||||
|
||||
- name: Test Light Version to Docker Hub
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: ./light
|
||||
file: ./light/Dockerfile
|
||||
push: false
|
||||
platforms: linux/arm64,linux/amd64
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
|
10
.github/workflows/release.yml
vendored
10
.github/workflows/release.yml
vendored
|
@ -99,7 +99,7 @@ jobs:
|
|||
- name: Publish to NPM
|
||||
run: |
|
||||
npm config set //registry.npmjs.org/:_authToken ${NPM_TOKEN}
|
||||
npm publish --access public --tag ${{ needs.release-check.outputs.prerelease == 'true' && 'next' || 'latest' }}
|
||||
npm publish --access public --tag ${{ steps.prepare_release.outputs.prerelease == 'true' && 'next' || 'latest' }}
|
||||
env:
|
||||
NPM_TOKEN: ${{ github.event.inputs.npm_token }}
|
||||
|
||||
|
@ -123,7 +123,7 @@ jobs:
|
|||
context: .
|
||||
push: true
|
||||
tags: |
|
||||
maptiler/tileserver-gl:${{ needs.release-check.outputs.prerelease == 'true' && 'next' || 'latest' }},
|
||||
maptiler/tileserver-gl:${{ steps.prepare_release.outputs.prerelease == 'true' && 'next' || 'latest' }},
|
||||
maptiler/tileserver-gl:v${{ env.PACKAGE_VERSION }}
|
||||
platforms: linux/arm64,linux/amd64
|
||||
cache-from: type=gha
|
||||
|
@ -144,7 +144,7 @@ jobs:
|
|||
bodyFile: changelog_for_version.md
|
||||
allowUpdates: true
|
||||
draft: false
|
||||
prerelease: ${{ env.PRERELEASE }}
|
||||
prerelease: ${{ steps.prepare_release.outputs.prerelease }}
|
||||
|
||||
- name: Create Tileserver Light Directory
|
||||
run: node publish.js --no-publish
|
||||
|
@ -157,7 +157,7 @@ jobs:
|
|||
working-directory: ./light
|
||||
run: |
|
||||
npm config set //registry.npmjs.org/:_authToken ${NPM_TOKEN}
|
||||
npm publish --access public --tag ${{ needs.release-check.outputs.prerelease == 'true' && 'next' || 'latest' }}
|
||||
npm publish --access public --tag ${{ steps.prepare_release.outputs.prerelease == 'true' && 'next' || 'latest' }}
|
||||
env:
|
||||
NPM_TOKEN: ${{ github.event.inputs.npm_token }}
|
||||
|
||||
|
@ -168,7 +168,7 @@ jobs:
|
|||
file: ./light/Dockerfile
|
||||
push: true
|
||||
tags: |
|
||||
maptiler/tileserver-gl-light:${{ needs.release-check.outputs.prerelease == 'true' && 'next' || 'latest' }},
|
||||
maptiler/tileserver-gl-light:${{ steps.prepare_release.outputs.prerelease == 'true' && 'next' || 'latest' }},
|
||||
maptiler/tileserver-gl-light:v${{ env.PACKAGE_VERSION }}
|
||||
platforms: linux/arm64,linux/amd64
|
||||
cache-from: type=gha
|
||||
|
|
11
.gitignore
vendored
11
.gitignore
vendored
|
@ -1,4 +1,15 @@
|
|||
docs/_build
|
||||
public/resources/leaflet-hash.js
|
||||
public/resources/leaflet.css
|
||||
public/resources/leaflet.js
|
||||
public/resources/leaflet.js.map
|
||||
public/resources/mapbox-gl-rtl-text.js
|
||||
public/resources/maplibre-gl-inspect.css
|
||||
public/resources/maplibre-gl-inspect.js
|
||||
public/resources/maplibre-gl-inspect.js.map
|
||||
public/resources/maplibre-gl.css
|
||||
public/resources/maplibre-gl.js
|
||||
public/resources/maplibre-gl.js.map
|
||||
node_modules
|
||||
test_data
|
||||
test_data.zip
|
||||
|
|
25
CHANGELOG.md
25
CHANGELOG.md
|
@ -1,5 +1,30 @@
|
|||
# tileserver-gl changelog
|
||||
|
||||
## 5.2.0
|
||||
* Use npm packages for public/resources (https://github.com/maptiler/tileserver-gl/pull/1427) by @okimiko
|
||||
* use ttf files of googlefonts/opensans (https://github.com/maptiler/tileserver-gl/pull/1447) by @okimiko
|
||||
* Limit Elevation Lat/Long Output Length (https://github.com/maptiler/tileserver-gl/pull/1457) by @okimiko
|
||||
* Fetch style from url (https://github.com/maptiler/tileserver-gl/pull/1462) by @YoelRidgway
|
||||
* fix: memory leak on SIGHUP (https://github.com/maptiler/tileserver-gl/pull/1455) by @okimiko
|
||||
* fix: resolves Unimplemented type: 3 error for geojson format (https://github.com/maptiler/tileserver-gl/pull/1465) by @rjdjohnston
|
||||
* fix: Test light version in ct workflow - fix sqlite build in light (https://github.com/maptiler/tileserver-gl/pull/1477) by @acalcutt
|
||||
* fix: light version docker entrypoint permissions (https://github.com/maptiler/tileserver-gl/pull/1478) by @acalcutt
|
||||
|
||||
## 5.1.3
|
||||
* Fix SIGHUP (broken since 5.1.x) (https://github.com/maptiler/tileserver-gl/pull/1452) by @okimiko
|
||||
|
||||
## 5.1.2
|
||||
* Fix broken light (invalid use of heavy dependencies) (https://github.com/maptiler/tileserver-gl/pull/1449) by @okimiko
|
||||
|
||||
## 5.1.1
|
||||
* Fix wrong node version in Docker image (https://github.com/maptiler/tileserver-gl/pull/1442) by @acalcutt
|
||||
|
||||
## 5.1.0
|
||||
* Update recommended node to v22 + Update docker images to use node 22 (https://github.com/maptiler/tileserver-gl/pull/1438) by @acalcutt
|
||||
* Upgrade Express to v5 + Canvas to v3 + code cleanup (https://github.com/maptiler/tileserver-gl/pull/1429) by @acalcutt
|
||||
* Terrain Preview and simple Elevation Query (https://github.com/maptiler/tileserver-gl/pull/1425 and https://github.com/maptiler/tileserver-gl/pull/1432) by @okimiko
|
||||
* add progressive rendering option for static jpeg images (#1397) by @samuel-git
|
||||
|
||||
## 5.0.0
|
||||
* Update Maplibre-Native to [v6.0.0](https://github.com/maplibre/maplibre-native/releases/tag/node-v6.0.0) release by @acalcutt in https://github.com/maptiler/tileserver-gl/pull/1376 and @dependabot in https://github.com/maptiler/tileserver-gl/pull/1381
|
||||
* This first release that use Metal for rendering instead of OpenGL (ES) for macOS.
|
||||
|
|
90
Dockerfile
90
Dockerfile
|
@ -2,10 +2,11 @@ FROM ubuntu:jammy AS builder
|
|||
|
||||
ENV NODE_ENV="production"
|
||||
|
||||
RUN set -ex; \
|
||||
export DEBIAN_FRONTEND=noninteractive; \
|
||||
apt-get -qq update; \
|
||||
apt-get -y --no-install-recommends install \
|
||||
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
||||
|
||||
RUN export DEBIAN_FRONTEND=noninteractive && \
|
||||
apt-get update && \
|
||||
apt-get install -y --no-install-recommends --no-install-suggests \
|
||||
build-essential \
|
||||
ca-certificates \
|
||||
curl \
|
||||
|
@ -26,23 +27,17 @@ RUN set -ex; \
|
|||
librsvg2-common \
|
||||
libcurl4-openssl-dev \
|
||||
libpixman-1-dev \
|
||||
libpixman-1-0; \
|
||||
apt-get -y --purge autoremove; \
|
||||
apt-get clean; \
|
||||
rm -rf /var/lib/apt/lists/*;
|
||||
|
||||
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
||||
|
||||
RUN mkdir -p /etc/apt/keyrings; \
|
||||
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg; \
|
||||
echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_20.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list; \
|
||||
apt-get -qq update; \
|
||||
apt-get install -y nodejs; \
|
||||
npm i -g npm@latest; \
|
||||
apt-get -y remove curl gnupg; \
|
||||
apt-get -y --purge autoremove; \
|
||||
apt-get clean; \
|
||||
rm -rf /var/lib/apt/lists/*;
|
||||
libpixman-1-0 && \
|
||||
mkdir -p /etc/apt/keyrings && \
|
||||
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg && \
|
||||
echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_22.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list && \
|
||||
apt-get -qq update && \
|
||||
apt-get install -y --no-install-recommends --no-install-suggests nodejs && \
|
||||
npm i -g npm@latest && \
|
||||
apt-get -y remove curl gnupg && \
|
||||
apt-get -y --purge autoremove && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN mkdir -p /usr/src/app
|
||||
|
||||
|
@ -51,12 +46,12 @@ WORKDIR /usr/src/app
|
|||
COPY package.json /usr/src/app
|
||||
COPY package-lock.json /usr/src/app
|
||||
|
||||
RUN npm config set maxsockets 1; \
|
||||
npm config set fetch-retries 5; \
|
||||
npm config set fetch-retry-mintimeout 100000; \
|
||||
npm config set fetch-retry-maxtimeout 600000; \
|
||||
npm ci --omit=dev; \
|
||||
chown -R root:root /usr/src/app;
|
||||
RUN npm config set maxsockets 1 && \
|
||||
npm config set fetch-retries 5 && \
|
||||
npm config set fetch-retry-mintimeout 100000 && \
|
||||
npm config set fetch-retry-maxtimeout 600000 && \
|
||||
npm ci --omit=dev && \
|
||||
chown -R root:root /usr/src/app
|
||||
|
||||
FROM ubuntu:jammy AS final
|
||||
|
||||
|
@ -65,12 +60,13 @@ ENV \
|
|||
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 \
|
||||
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
||||
|
||||
RUN export DEBIAN_FRONTEND=noninteractive && \
|
||||
groupadd -r node && \
|
||||
useradd -r -g node node && \
|
||||
apt-get -qq update && \
|
||||
apt-get install -y --no-install-recommends --no-install-suggests \
|
||||
ca-certificates \
|
||||
curl \
|
||||
gnupg \
|
||||
|
@ -85,23 +81,17 @@ RUN set -ex; \
|
|||
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 mkdir -p /etc/apt/keyrings; \
|
||||
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg; \
|
||||
echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_20.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list; \
|
||||
apt-get -qq update; \
|
||||
apt-get install -y nodejs; \
|
||||
npm i -g npm@latest; \
|
||||
apt-get -y remove curl gnupg; \
|
||||
apt-get -y --purge autoremove; \
|
||||
apt-get clean; \
|
||||
rm -rf /var/lib/apt/lists/*;
|
||||
libpango-1.0-0 && \
|
||||
mkdir -p /etc/apt/keyrings && \
|
||||
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg && \
|
||||
echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_22.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list && \
|
||||
apt-get -qq update && \
|
||||
apt-get install -y --no-install-recommends --no-install-suggests nodejs && \
|
||||
npm i -g npm@latest && \
|
||||
apt-get -y remove curl gnupg && \
|
||||
apt-get -y --purge autoremove && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY --from=builder /usr/src/app /usr/src/app
|
||||
|
||||
|
|
38
Dockerfile_build
Normal file
38
Dockerfile_build
Normal file
|
@ -0,0 +1,38 @@
|
|||
FROM ubuntu:jammy AS builder
|
||||
|
||||
ENV NODE_ENV="devel"
|
||||
|
||||
RUN export DEBIAN_FRONTEND=noninteractive && \
|
||||
apt-get update && \
|
||||
apt-get install -y --no-install-recommends --no-install-suggests \
|
||||
build-essential \
|
||||
ca-certificates \
|
||||
curl \
|
||||
gnupg \
|
||||
pkg-config \
|
||||
xvfb \
|
||||
libglfw3-dev \
|
||||
libuv1-dev \
|
||||
libjpeg-turbo8 \
|
||||
libicu70 \
|
||||
libcairo2-dev \
|
||||
libpango1.0-dev \
|
||||
libjpeg-dev \
|
||||
libgif-dev \
|
||||
librsvg2-dev \
|
||||
gir1.2-rsvg-2.0 \
|
||||
librsvg2-2 \
|
||||
librsvg2-common \
|
||||
libcurl4-openssl-dev \
|
||||
libpixman-1-dev \
|
||||
libpixman-1-0 && \
|
||||
mkdir -p /etc/apt/keyrings && \
|
||||
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg && \
|
||||
echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_22.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list && \
|
||||
apt-get -qq update && \
|
||||
apt-get install -y --no-install-recommends --no-install-suggests nodejs && \
|
||||
npm i -g npm@latest && \
|
||||
apt-get -y remove curl gnupg && \
|
||||
apt-get -y --purge autoremove && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/*
|
|
@ -1,50 +1,81 @@
|
|||
FROM ubuntu:jammy
|
||||
FROM ubuntu:jammy AS builder
|
||||
|
||||
ENV NODE_ENV="production"
|
||||
|
||||
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
||||
|
||||
RUN export DEBIAN_FRONTEND=noninteractive && \
|
||||
apt-get update && \
|
||||
apt-get install -y --no-install-recommends --no-install-suggests \
|
||||
build-essential \
|
||||
ca-certificates \
|
||||
curl \
|
||||
gnupg && \
|
||||
mkdir -p /etc/apt/keyrings && \
|
||||
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg && \
|
||||
echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_22.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list && \
|
||||
apt-get -qq update && \
|
||||
apt-get install -y --no-install-recommends --no-install-suggests nodejs && \
|
||||
npm i -g npm@latest && \
|
||||
apt-get -y remove curl gnupg && \
|
||||
apt-get -y --purge autoremove && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN mkdir -p /usr/src/app
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
COPY package.json /usr/src/app
|
||||
COPY package-lock.json /usr/src/app
|
||||
|
||||
RUN npm config set maxsockets 1 && \
|
||||
npm config set fetch-retries 5 && \
|
||||
npm config set fetch-retry-mintimeout 100000 && \
|
||||
npm config set fetch-retry-maxtimeout 600000 && \
|
||||
npm ci --omit=dev && \
|
||||
chown -R root:root /usr/src/app
|
||||
|
||||
FROM ubuntu:jammy AS final
|
||||
|
||||
ENV \
|
||||
NODE_ENV="production" \
|
||||
CHOKIDAR_USEPOLLING=1 \
|
||||
CHOKIDAR_INTERVAL=500
|
||||
|
||||
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 \
|
||||
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
||||
|
||||
RUN export DEBIAN_FRONTEND=noninteractive && \
|
||||
groupadd -r node && \
|
||||
useradd -r -g node node && \
|
||||
apt-get -qq update && \
|
||||
apt-get install -y --no-install-recommends --no-install-suggests \
|
||||
ca-certificates \
|
||||
curl \
|
||||
gnupg; \
|
||||
mkdir -p /etc/apt/keyrings; \
|
||||
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg; \
|
||||
echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_20.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list; \
|
||||
apt-get -qq update; \
|
||||
apt-get install -y nodejs; \
|
||||
npm i -g npm@latest; \
|
||||
apt-get -y remove curl gnupg; \
|
||||
apt-get -y --purge autoremove; \
|
||||
apt-get clean; \
|
||||
rm -rf /var/lib/apt/lists/*;
|
||||
gnupg && \
|
||||
mkdir -p /etc/apt/keyrings && \
|
||||
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg && \
|
||||
echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_22.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list && \
|
||||
apt-get -qq update && \
|
||||
apt-get install -y --no-install-recommends --no-install-suggests nodejs && \
|
||||
npm i -g npm@latest && \
|
||||
apt-get -y remove curl gnupg && \
|
||||
apt-get -y --purge autoremove && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
EXPOSE 8080
|
||||
COPY --from=builder /usr/src/app /usr/src/app
|
||||
|
||||
RUN mkdir -p /data; \
|
||||
chown node:node /data; \
|
||||
mkdir -p /usr/src/app;
|
||||
COPY . /usr/src/app
|
||||
|
||||
RUN mkdir -p /data && \
|
||||
chown node:node /data && \
|
||||
chmod +x /usr/src/app/docker-entrypoint.sh
|
||||
|
||||
VOLUME /data
|
||||
|
||||
WORKDIR /data
|
||||
|
||||
COPY / /usr/src/app
|
||||
|
||||
RUN cd /usr/src/app; \
|
||||
npm config set maxsockets 1; \
|
||||
npm config set fetch-retries 5; \
|
||||
npm config set fetch-retry-mintimeout 100000; \
|
||||
npm config set fetch-retry-maxtimeout 600000; \
|
||||
npm install --omit=dev; \
|
||||
chown -R root:root /usr/src/app; \
|
||||
chmod +x /usr/src/app/docker-entrypoint.sh;
|
||||
EXPOSE 8080
|
||||
|
||||
USER node:node
|
||||
|
||||
|
|
|
@ -13,7 +13,8 @@ RUN set -ex; \
|
|||
unzip \
|
||||
build-essential \
|
||||
ca-certificates \
|
||||
wget \
|
||||
curl \
|
||||
gnupg \
|
||||
pkg-config \
|
||||
xvfb \
|
||||
libglfw3-dev \
|
||||
|
@ -25,16 +26,33 @@ RUN set -ex; \
|
|||
libjpeg-dev \
|
||||
libgif-dev \
|
||||
librsvg2-dev \
|
||||
gir1.2-rsvg-2.0 \
|
||||
librsvg2-2 \
|
||||
librsvg2-common \
|
||||
libcurl4-openssl-dev \
|
||||
libpixman-1-dev; \
|
||||
wget -qO- https://deb.nodesource.com/setup_18.x | bash; \
|
||||
libpixman-1-dev \
|
||||
libpixman-1-0; \
|
||||
apt-get -y --purge autoremove; \
|
||||
apt-get clean; \
|
||||
rm -rf /var/lib/apt/lists/*;
|
||||
|
||||
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
||||
|
||||
RUN mkdir -p /etc/apt/keyrings; \
|
||||
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg; \
|
||||
echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_22.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list; \
|
||||
apt-get -qq update; \
|
||||
apt-get install -y nodejs; \
|
||||
apt-get clean;
|
||||
npm i -g npm@latest; \
|
||||
apt-get -y remove gnupg; \
|
||||
apt-get -y --purge autoremove; \
|
||||
apt-get clean; \
|
||||
rm -rf /var/lib/apt/lists/*;
|
||||
|
||||
RUN mkdir -p /usr/src/app
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
RUN wget -O test_data.zip https://github.com/maptiler/tileserver-gl/releases/download/v1.3.0/test_data.zip; \
|
||||
RUN curl -L -o test_data.zip https://github.com/maptiler/tileserver-gl/releases/download/v1.3.0/test_data.zip; \
|
||||
unzip -q test_data.zip -d test_data
|
||||
|
||||
COPY package.json .
|
||||
|
|
115
PUBLISHING.md
115
PUBLISHING.md
|
@ -1,19 +1,112 @@
|
|||
# Publishing new version
|
||||
# Publishing a New Version
|
||||
|
||||
1.) Change the version number in package.json. Run the following command in the package root directory, replacing <update_type> with one of the semantic versioning release types (prerelease, prepatch, preminor, premajor, patch, minor, major):
|
||||
npm version <update_type> --preid pre --no-git-tag-version
|
||||
This document outlines the process for publishing new versions of this project. We use a GitHub workflow for automated releases, but also provide manual steps for specific situations.
|
||||
|
||||
--preid specifies which suffix to use in the release such as pre, next, beta, rc, etc.
|
||||
## Automated Publishing via GitHub Workflow (Recommended)
|
||||
|
||||
prepatch, preminor, and premajor start a new series of pre-releases while bumping the patch, minor, or major version. E.g. premajor with --preid pre would do a prerelease for a new major using the -pre suffix (i.e. it would be a new major with -pre.0)
|
||||
This is the preferred method for publishing new versions. It automates the entire process, including version bumping, tagging, building, and publishing.
|
||||
|
||||
You can use prerelease to bump the version for a new pre-release version. E.g. you could run npm version prerelease --preid pre --no-git-tag-version to go from -pre.0 to -pre.1.
|
||||
1. **Prepare the Release:**
|
||||
|
||||
For regular versions, you can use patch, minor, or major. E.g. npm version major --no-git-tag-version.
|
||||
* **Choose the Version Increment:** Determine the appropriate semantic versioning increment (prerelease, prepatch, preminor, premajor, patch, minor, major).
|
||||
|
||||
2.) Update the changelog, which can be found in CHANGELOG.md. The heading must match ## <VERSION> exactly, or it will not be picked up. For example, for version 5.0.0:
|
||||
## 5.0.0
|
||||
* **Update `package.json`:** Use the `npm version` command to bump the version number. This command also supports creating pre-release versions using the `--preid pre` flag.
|
||||
|
||||
3.) Commit and push the changes.
|
||||
```bash
|
||||
# Example: Increment to a new patch version
|
||||
npm version patch --no-git-tag-version
|
||||
|
||||
4.) Run the 'Build, Test, Release' github workflow. The workflow will create a NPM, Docker, and Github release and Tag.
|
||||
# Example: Increment to a new minor version
|
||||
npm version minor --no-git-tag-version
|
||||
|
||||
# Example: Increment to a new major version
|
||||
npm version major --no-git-tag-version
|
||||
|
||||
# Example: Create a pre-release (e.g., -pre.0) version
|
||||
npm version prepatch --preid pre --no-git-tag-version
|
||||
# OR
|
||||
npm version preminor --preid pre --no-git-tag-version
|
||||
# OR
|
||||
npm version premajor --preid pre --no-git-tag-version
|
||||
|
||||
# Example: Increment an existing pre-release version (e.g., -pre.0 to -pre.1)
|
||||
npm version prerelease --preid pre --no-git-tag-version
|
||||
```
|
||||
|
||||
* `--no-git-tag-version`: This prevents `npm version` from automatically creating a git tag, as the GitHub workflow will handle this later.
|
||||
* `--preid pre`: Specifies that "pre" is the pre-release identifier.
|
||||
|
||||
* **Update the Changelog (`CHANGELOG.md`):** Add a new section for the release. The heading *must* exactly match the format `## <VERSION>`, where `<VERSION>` is the full version number from `package.json`. Describe the changes included in the release.
|
||||
|
||||
```markdown
|
||||
## 1.2.3
|
||||
* Added new feature X.
|
||||
```
|
||||
|
||||
2. **Commit and Push:** Commit the changes to `package.json`, `package-lock.json` and `CHANGELOG.md` to a the `master` branch. Create a pull request (PR) for review. If you are an administrator, you may push directly, but a PR is generally recommended for code review.
|
||||
|
||||
3. **Run the GitHub Workflow:** Once the PR is merged (or changes pushed directly), trigger the "Build, Test, Release" GitHub workflow. This workflow is responsible for:
|
||||
|
||||
* Building the project.
|
||||
* Running tests.
|
||||
* Creating an NPM package.
|
||||
* Building and pushing Docker images.
|
||||
* Creating a GitHub Release.
|
||||
* Creating a Git tag for the release.
|
||||
|
||||
## Manual Publishing (For Special Cases)
|
||||
|
||||
Use the following steps *only* when the automated workflow is not suitable (e.g., for debugging, specific environment needs).
|
||||
|
||||
1. **Update Version in `package.json`:** Modify the `version` field in `package.json` to the desired version number.
|
||||
|
||||
2. **Create and Push Git Tag:**
|
||||
|
||||
```bash
|
||||
git tag vX.X.X # Replace X.X.X with the version number
|
||||
git push origin --tags
|
||||
```
|
||||
|
||||
3. **NPM Publish Options (Choose ONE):**
|
||||
|
||||
* **Option A: Publish only the full `tileserver-gl` version:**
|
||||
|
||||
```bash
|
||||
npm publish --access public
|
||||
```
|
||||
|
||||
* **Option B: Build and Publish both `tileserver-gl` and `tileserver-gl-light` using `publish.js`:**
|
||||
|
||||
```bash
|
||||
# This script builds the light version and publishes both tileserver-gl and tileserver-gl-light to NPM.
|
||||
node publish.js
|
||||
```
|
||||
|
||||
* **Option C: Build only the `tileserver-gl-light` version (no publish):**
|
||||
|
||||
```bash
|
||||
# This script ONLY builds the light version (e.g., for local testing or Docker image creation) without publishing.
|
||||
node publish.js --no-publish
|
||||
```
|
||||
|
||||
4. **Build and Push Docker Images:**
|
||||
|
||||
```bash
|
||||
# Build the main image
|
||||
docker buildx build --platform linux/amd64 -t maptiler/tileserver-gl:latest -t maptiler/tileserver-gl:X.X.X . # Replace X.X.X
|
||||
docker push maptiler/tileserver-gl --all-tags
|
||||
|
||||
# Build the light image
|
||||
cd light
|
||||
docker buildx build --platform linux/amd64 -t maptiler/tileserver-gl-light:latest -t maptiler/tileserver-gl-light:X.X.X . # Replace X.X.X
|
||||
docker push maptiler/tileserver-gl-light --all-tags
|
||||
cd .. # Return to the project root
|
||||
```
|
||||
* Make sure you are logged into the docker registry before pushing. `docker login`
|
||||
|
||||
**Important Considerations for Manual Publishing:**
|
||||
|
||||
* **Consistency:** Ensure the version number in `package.json`, the Git tag, and the Docker image tags are identical.
|
||||
* **Credentials:** Verify that you have the necessary permissions to push Docker images and publish to NPM.
|
||||
* **Cleanliness:** Before building Docker images, ensure your working directory is clean to avoid including unwanted files in the image.
|
||||
* **Error Handling:** Manually publishing is more prone to errors. Double-check each step to avoid issues.
|
||||
|
|
37
README.md
37
README.md
|
@ -1,5 +1,32 @@
|
|||

|
||||
|
||||
# My TileServer GL
|
||||
|
||||
creare un folder dove mettere la mappa e i layers
|
||||
|
||||
Download vector tiles from [OpenMapTiles](https://data.maptiler.com/downloads/planet/).
|
||||
|
||||
scaricare i layers
|
||||
|
||||
wget https://github.com/maptiler/tileserver-gl/releases/download/v1.3.0/test_data.zip
|
||||
unzip test_data.zip
|
||||
|
||||
modificare nano config.json per inserire il nome del file .mbtiles (x es planetOSM.mbtiles )
|
||||
|
||||
far partire il docker
|
||||
|
||||
services:
|
||||
tileserver-gl:
|
||||
volumes:
|
||||
- /home/nvme/dockerdata/tileserver:/data
|
||||
ports:
|
||||
- 8115:8080
|
||||
image: maptiler/tileserver-gl:latest
|
||||
|
||||
oppure
|
||||
|
||||
sudo docker run -d -v /home/nvme/dockerdata/tileserver:/data -p 8115:8080 maptiler/tileserver-gl:latest
|
||||
|
||||
# TileServer GL
|
||||
[](https://github.com/maptiler/tileserver-gl/actions/workflows/pipeline.yml)
|
||||
[](https://hub.docker.com/r/maptiler/tileserver-gl/)
|
||||
|
@ -9,7 +36,7 @@ Vector and raster maps with GL styles. Server-side rendering by MapLibre GL Nati
|
|||
Download vector tiles from [OpenMapTiles](https://data.maptiler.com/downloads/planet/).
|
||||
## Getting Started with Node
|
||||
|
||||
Make sure you have Node.js version **18.17.0** or above installed. Node 20 is recommended. (running `node -v` it should output something like `v20.x.x`). Running without docker requires [Native dependencies](https://maptiler-tileserver.readthedocs.io/en/latest/installation.html#npm) to be installed first.
|
||||
Make sure you have Node.js version **18.17.0** or above installed. Node 22 is recommended. (running `node -v` it should output something like `v22.x.x`). Running without docker requires [Native dependencies](https://maptiler-tileserver.readthedocs.io/en/latest/installation.html#npm) to be installed first.
|
||||
|
||||
Install `tileserver-gl` with server-side raster rendering of vector tiles with npm.
|
||||
|
||||
|
@ -43,7 +70,7 @@ An alternative to npm to start the packed software easier is to install [Docker]
|
|||
Example using a mbtiles file
|
||||
```bash
|
||||
wget https://github.com/maptiler/tileserver-gl/releases/download/v1.3.0/zurich_switzerland.mbtiles
|
||||
docker run --rm -it -v $(pwd):/data -p 8080:8080 maptiler/tileserver-gl --file zurich_switzerland.mbtiles
|
||||
docker run --rm -it -v $(pwd):/data -p 8080:8080 maptiler/tileserver-gl:latest --file zurich_switzerland.mbtiles
|
||||
[in your browser, visit http://[server ip]:8080]
|
||||
```
|
||||
|
||||
|
@ -51,18 +78,18 @@ 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
|
||||
docker run --rm -it -v $(pwd):/data -p 8080:8080 maptiler/tileserver-gl:latest
|
||||
[in your browser, visit http://[server ip]:8080]
|
||||
```
|
||||
|
||||
Example using a different path
|
||||
```bash
|
||||
docker run --rm -it -v /your/local/config/path:/data -p 8080:8080 maptiler/tileserver-gl
|
||||
docker run --rm -it -v /your/local/config/path:/data -p 8080:8080 maptiler/tileserver-gl:latest
|
||||
```
|
||||
replace '/your/local/config/path' with the path to your config file
|
||||
|
||||
|
||||
Alternatively, you can use the `maptiler/tileserver-gl-light` 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.
|
||||
Alternatively, you can use the `maptiler/tileserver-gl-light:latest` docker image instead, which is pure javascript, does not have any native dependencies, and can run anywhere, but does not contain rasterization on the server side made with Maplibre GL Native.
|
||||
|
||||
## Getting Started with Linux cli
|
||||
|
||||
|
|
|
@ -57,6 +57,9 @@ Example:
|
|||
"tilejson": {
|
||||
"format": "webp"
|
||||
}
|
||||
},
|
||||
"remote": {
|
||||
"style": "https://demotiles.maplibre.org/style.json"
|
||||
}
|
||||
},
|
||||
"data": {
|
||||
|
@ -209,7 +212,7 @@ Not used by default.
|
|||
|
||||
Each item in this object defines one style (map). It can have the following options:
|
||||
|
||||
* ``style`` -- name of the style json file [required]
|
||||
* ``style`` -- name of the style json file or url of a remote hosted style [required]
|
||||
* ``serve_rendered`` -- whether to render the raster tiles for this style or not
|
||||
* ``serve_data`` -- whether to allow access to the original tiles, sprites and required glyphs
|
||||
* ``tilejson`` -- properties to add to the TileJSON created for the raster data
|
||||
|
|
|
@ -108,6 +108,8 @@ Source data
|
|||
|
||||
* the result will be a json object like ``{"z":7,"x":68,"y":45,"red":134,"green":66,"blue":0,"latitude":11.84069,"longitude":46.04798,"elevation":1602}``
|
||||
|
||||
* The elevation api is not available in the ``tileserver-gl-light`` version.
|
||||
|
||||
Static files
|
||||
===========
|
||||
* Static files are served at ``/files/{filename}``
|
||||
|
|
5592
package-lock.json
generated
5592
package-lock.json
generated
File diff suppressed because it is too large
Load diff
22
package.json
22
package.json
|
@ -1,11 +1,17 @@
|
|||
{
|
||||
"name": "tileserver-gl",
|
||||
"version": "5.0.0",
|
||||
"version": "5.2.0",
|
||||
"description": "Map tile server for JSON GL styles - vector and server side generated raster tiles",
|
||||
"main": "src/main.js",
|
||||
"bin": "src/main.js",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"prepare": "npm run copy:maplibre && npm run copy:maplibre-inspect && npm run copy:mapbox-rtl-text && npm run copy:leaflet && npm run copy:leaflet-hash",
|
||||
"copy:maplibre": "copyfiles -EVf node_modules/maplibre-gl/dist/maplibre-gl.js node_modules/maplibre-gl/dist/maplibre-gl.js.map node_modules/maplibre-gl/dist/maplibre-gl.css public/resources/",
|
||||
"copy:maplibre-inspect": "copyfiles -EVf node_modules/@maplibre/maplibre-gl-inspect/dist/maplibre-gl-inspect.js node_modules/@maplibre/maplibre-gl-inspect/dist/maplibre-gl-inspect.js.map node_modules/@maplibre/maplibre-gl-inspect/dist/maplibre-gl-inspect.css public/resources/",
|
||||
"copy:mapbox-rtl-text": "copyfiles -EVf node_modules/@mapbox/mapbox-gl-rtl-text/dist/mapbox-gl-rtl-text.js public/resources/",
|
||||
"copy:leaflet": "copyfiles -EVf node_modules/leaflet/dist/leaflet.js node_modules/leaflet/dist/leaflet.js.map node_modules/leaflet/dist/leaflet.css node_modules/leaflet/dist/leaflet-hash.js public/resources/",
|
||||
"copy:leaflet-hash": "copyfiles -EVf node_modules/leaflet-hash/leaflet-hash.js public/resources/",
|
||||
"test": "mocha test/**.js --timeout 10000 --exit",
|
||||
"test-docker": "xvfb-run npm test",
|
||||
"lint:yml": "yamllint --schema=CORE_SCHEMA *.{yml,yaml}",
|
||||
|
@ -19,24 +25,30 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@jsse/pbfont": "^0.2.2",
|
||||
"@mapbox/mapbox-gl-rtl-text": "0.3.0",
|
||||
"@mapbox/mbtiles": "0.12.1",
|
||||
"@mapbox/polyline": "^1.2.1",
|
||||
"@mapbox/sphericalmercator": "1.2.0",
|
||||
"@mapbox/vector-tile": "2.0.3",
|
||||
"@maplibre/maplibre-gl-inspect": "1.7.0",
|
||||
"@maplibre/maplibre-gl-native": "6.0.0",
|
||||
"@maplibre/maplibre-gl-style-spec": "20.3.1",
|
||||
"@sindresorhus/fnv1a": "3.1.0",
|
||||
"advanced-pool": "0.3.3",
|
||||
"axios": "^1.7.7",
|
||||
"canvas": "2.11.2",
|
||||
"axios": "^1.8.2",
|
||||
"canvas": "3.0.1",
|
||||
"chokidar": "3.6.0",
|
||||
"clone": "2.1.2",
|
||||
"color": "4.2.3",
|
||||
"commander": "12.1.0",
|
||||
"copyfiles": "2.4.1",
|
||||
"cors": "2.8.5",
|
||||
"express": "4.19.2",
|
||||
"express": "5.0.1",
|
||||
"handlebars": "4.7.8",
|
||||
"http-shutdown": "1.2.2",
|
||||
"leaflet": "1.9.4",
|
||||
"leaflet-hash": "0.2.1",
|
||||
"maplibre-gl": "4.7.1",
|
||||
"morgan": "1.10.0",
|
||||
"pbf": "4.0.1",
|
||||
"pmtiles": "3.0.7",
|
||||
|
@ -73,7 +85,7 @@
|
|||
],
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=18.17.0 <21"
|
||||
"node": ">=18.17.0 <23"
|
||||
},
|
||||
"repository": {
|
||||
"url": "git+https://github.com/maptiler/tileserver-gl.git",
|
||||
|
|
|
@ -18,7 +18,7 @@ class ElevationInfoControl {
|
|||
|
||||
map.on('click', (e) => {
|
||||
var url = this.url;
|
||||
var coord = {"z": Math.floor(map.getZoom()), "x": e.lngLat["lng"], "y": e.lngLat["lat"]};
|
||||
var coord = {"z": Math.floor(map.getZoom()), "x": e.lngLat["lng"].toFixed(7), "y": e.lngLat["lat"].toFixed(7)};
|
||||
for(var key in coord) {
|
||||
url = url.replace(new RegExp('{'+ key +'}','g'), coord[key]);
|
||||
}
|
||||
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -1,162 +0,0 @@
|
|||
(function(window) {
|
||||
var HAS_HASHCHANGE = (function() {
|
||||
var doc_mode = window.documentMode;
|
||||
return ('onhashchange' in window) &&
|
||||
(doc_mode === undefined || doc_mode > 7);
|
||||
})();
|
||||
|
||||
L.Hash = function(map) {
|
||||
this.onHashChange = L.Util.bind(this.onHashChange, this);
|
||||
|
||||
if (map) {
|
||||
this.init(map);
|
||||
}
|
||||
};
|
||||
|
||||
L.Hash.parseHash = function(hash) {
|
||||
if(hash.indexOf('#') === 0) {
|
||||
hash = hash.substr(1);
|
||||
}
|
||||
var args = hash.split("/");
|
||||
if (args.length == 3) {
|
||||
var zoom = parseInt(args[0], 10),
|
||||
lat = parseFloat(args[1]),
|
||||
lon = parseFloat(args[2]);
|
||||
if (isNaN(zoom) || isNaN(lat) || isNaN(lon)) {
|
||||
return false;
|
||||
} else {
|
||||
return {
|
||||
center: new L.LatLng(lat, lon),
|
||||
zoom: zoom
|
||||
};
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
L.Hash.formatHash = function(map) {
|
||||
var center = map.getCenter(),
|
||||
zoom = map.getZoom(),
|
||||
precision = Math.max(0, Math.ceil(Math.log(zoom) / Math.LN2));
|
||||
|
||||
return "#" + [zoom,
|
||||
center.lat.toFixed(precision),
|
||||
center.lng.toFixed(precision)
|
||||
].join("/");
|
||||
},
|
||||
|
||||
L.Hash.prototype = {
|
||||
map: null,
|
||||
lastHash: null,
|
||||
|
||||
parseHash: L.Hash.parseHash,
|
||||
formatHash: L.Hash.formatHash,
|
||||
|
||||
init: function(map) {
|
||||
this.map = map;
|
||||
|
||||
// reset the hash
|
||||
this.lastHash = null;
|
||||
this.onHashChange();
|
||||
|
||||
if (!this.isListening) {
|
||||
this.startListening();
|
||||
}
|
||||
},
|
||||
|
||||
removeFrom: function(map) {
|
||||
if (this.changeTimeout) {
|
||||
clearTimeout(this.changeTimeout);
|
||||
}
|
||||
|
||||
if (this.isListening) {
|
||||
this.stopListening();
|
||||
}
|
||||
|
||||
this.map = null;
|
||||
},
|
||||
|
||||
onMapMove: function() {
|
||||
// bail if we're moving the map (updating from a hash),
|
||||
// or if the map is not yet loaded
|
||||
|
||||
if (this.movingMap || !this.map._loaded) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var hash = this.formatHash(this.map);
|
||||
if (this.lastHash != hash) {
|
||||
location.replace(hash);
|
||||
this.lastHash = hash;
|
||||
}
|
||||
},
|
||||
|
||||
movingMap: false,
|
||||
update: function() {
|
||||
var hash = location.hash;
|
||||
if (hash === this.lastHash) {
|
||||
return;
|
||||
}
|
||||
var parsed = this.parseHash(hash);
|
||||
if (parsed) {
|
||||
this.movingMap = true;
|
||||
|
||||
this.map.setView(parsed.center, parsed.zoom);
|
||||
|
||||
this.movingMap = false;
|
||||
} else {
|
||||
this.onMapMove(this.map);
|
||||
}
|
||||
},
|
||||
|
||||
// defer hash change updates every 100ms
|
||||
changeDefer: 100,
|
||||
changeTimeout: null,
|
||||
onHashChange: function() {
|
||||
// throttle calls to update() so that they only happen every
|
||||
// `changeDefer` ms
|
||||
if (!this.changeTimeout) {
|
||||
var that = this;
|
||||
this.changeTimeout = setTimeout(function() {
|
||||
that.update();
|
||||
that.changeTimeout = null;
|
||||
}, this.changeDefer);
|
||||
}
|
||||
},
|
||||
|
||||
isListening: false,
|
||||
hashChangeInterval: null,
|
||||
startListening: function() {
|
||||
this.map.on("moveend", this.onMapMove, this);
|
||||
|
||||
if (HAS_HASHCHANGE) {
|
||||
L.DomEvent.addListener(window, "hashchange", this.onHashChange);
|
||||
} else {
|
||||
clearInterval(this.hashChangeInterval);
|
||||
this.hashChangeInterval = setInterval(this.onHashChange, 50);
|
||||
}
|
||||
this.isListening = true;
|
||||
},
|
||||
|
||||
stopListening: function() {
|
||||
this.map.off("moveend", this.onMapMove, this);
|
||||
|
||||
if (HAS_HASHCHANGE) {
|
||||
L.DomEvent.removeListener(window, "hashchange", this.onHashChange);
|
||||
} else {
|
||||
clearInterval(this.hashChangeInterval);
|
||||
}
|
||||
this.isListening = false;
|
||||
}
|
||||
};
|
||||
L.hash = function(map) {
|
||||
return new L.Hash(map);
|
||||
};
|
||||
L.Map.prototype.addHash = function() {
|
||||
this._hash = L.hash(this);
|
||||
};
|
||||
L.Map.prototype.removeHash = function() {
|
||||
this._hash.removeFrom();
|
||||
};
|
||||
})(window);
|
|
@ -1,661 +0,0 @@
|
|||
/* 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 img.leaflet-tile {
|
||||
/* See: https://bugs.chromium.org/p/chromium/issues/detail?id=600120 */
|
||||
mix-blend-mode: plus-lighter;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
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
|
@ -1,40 +0,0 @@
|
|||
.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");
|
||||
}
|
File diff suppressed because it is too large
Load diff
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
|
@ -9,7 +9,9 @@
|
|||
<link rel="stylesheet" type="text/css" href="{{public_url}}maplibre-gl-inspect.css{{&key_query}}" />
|
||||
<script src="{{public_url}}maplibre-gl.js{{&key_query}}"></script>
|
||||
<script src="{{public_url}}maplibre-gl-inspect.js{{&key_query}}"></script>
|
||||
{{^is_light}}
|
||||
<script src="{{public_url}}elevation-control.js{{&key_query}}"></script>
|
||||
{{/is_light}}
|
||||
<style>
|
||||
body {background:#fff;color:#333;font-family:Arial, sans-serif;}
|
||||
{{^is_terrain}}
|
||||
|
@ -21,7 +23,9 @@
|
|||
h1 {position:absolute;top:5px;right:0;width:240px;margin:0;line-height:20px;font-size:20px;}
|
||||
#layerList {position:absolute;top:35px;right:0;bottom:0;width:240px;overflow:auto;}
|
||||
#layerList div div {width:15px;height:15px;display:inline-block;}
|
||||
{{^is_light}}
|
||||
.maplibre-ctrl-elevation { padding-left: 5px; padding-right: 5px; }
|
||||
{{/is_light}}
|
||||
</style>
|
||||
{{/use_maplibre}}
|
||||
{{^use_maplibre}}
|
||||
|
@ -135,11 +139,13 @@
|
|||
})
|
||||
);
|
||||
|
||||
{{^is_light}}
|
||||
map.addControl(
|
||||
new ElevationInfoControl({
|
||||
url: "{{public_url}}data/{{id}}/elevation/{z}/{x}/{y}"
|
||||
})
|
||||
);
|
||||
{{/is_light}}
|
||||
{{/is_terrain}}
|
||||
{{^is_terrain}}
|
||||
|
||||
|
|
|
@ -124,9 +124,9 @@
|
|||
{{/is_vector}}
|
||||
{{^is_vector}}
|
||||
<a class="btn" href="{{public_url}}data/{{@key}}/{{&../key_query}}{{viewer_hash}}">View</a>
|
||||
{{#elevation_link}}
|
||||
{{#is_terrain}}
|
||||
<a class="btn" href="{{public_url}}data/preview/{{@key}}/{{&../key_query}}{{viewer_hash}}">Preview Terrain</a>
|
||||
{{/elevation_link}}
|
||||
{{/is_terrain}}
|
||||
{{/is_vector}}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,6 +1,12 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
'use strict';
|
||||
import os from 'os';
|
||||
|
||||
const envSize = parseInt(process.env.UV_THREADPOOL_SIZE, 10);
|
||||
process.env.UV_THREADPOOL_SIZE = Math.ceil(
|
||||
Math.max(4, isNaN(envSize) ? os.cpus().length * 1.5 : envSize),
|
||||
);
|
||||
|
||||
import fs from 'node:fs';
|
||||
import fsp from 'node:fs/promises';
|
||||
|
|
|
@ -8,33 +8,78 @@ import express from 'express';
|
|||
import Pbf from 'pbf';
|
||||
import { VectorTile } from '@mapbox/vector-tile';
|
||||
import SphericalMercator from '@mapbox/sphericalmercator';
|
||||
import { Image, createCanvas } from 'canvas';
|
||||
import sharp from 'sharp';
|
||||
|
||||
import { fixTileJSONCenter, getTileUrls, isValidHttpUrl } from './utils.js';
|
||||
import {
|
||||
getPMtilesInfo,
|
||||
getPMtilesTile,
|
||||
openPMtiles,
|
||||
} from './pmtiles_adapter.js';
|
||||
fixTileJSONCenter,
|
||||
getTileUrls,
|
||||
isValidHttpUrl,
|
||||
fetchTileData,
|
||||
} from './utils.js';
|
||||
import { getPMtilesInfo, openPMtiles } from './pmtiles_adapter.js';
|
||||
import { gunzipP, gzipP } from './promises.js';
|
||||
import { openMbTilesWrapper } from './mbtiles_wrapper.js';
|
||||
|
||||
import fs from 'node:fs';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const packageJson = JSON.parse(
|
||||
fs.readFileSync(
|
||||
path.dirname(fileURLToPath(import.meta.url)) + '/../package.json',
|
||||
'utf8',
|
||||
),
|
||||
);
|
||||
|
||||
const isLight = packageJson.name.slice(-6) === '-light';
|
||||
const serve_rendered = (
|
||||
await import(`${!isLight ? `./serve_rendered.js` : `./serve_light.js`}`)
|
||||
).serve_rendered;
|
||||
|
||||
export const serve_data = {
|
||||
init: (options, repo) => {
|
||||
/**
|
||||
* Initializes the serve_data module.
|
||||
* @param {object} options Configuration options.
|
||||
* @param {object} repo Repository object.
|
||||
* @param {object} programOpts - An object containing the program options
|
||||
* @returns {express.Application} The initialized Express application.
|
||||
*/
|
||||
init: function (options, repo, programOpts) {
|
||||
const { verbose } = programOpts;
|
||||
const app = express().disable('x-powered-by');
|
||||
|
||||
app.get(
|
||||
'/:id/:z(\\d+)/:x(\\d+)/:y(\\d+).:format([\\w.]+)',
|
||||
async (req, res, next) => {
|
||||
/**
|
||||
* Handles requests for tile data, responding with the tile image.
|
||||
* @param {object} req - Express request object.
|
||||
* @param {object} res - Express response object.
|
||||
* @param {string} req.params.id - ID of the tile.
|
||||
* @param {string} req.params.z - Z coordinate of the tile.
|
||||
* @param {string} req.params.x - X coordinate of the tile.
|
||||
* @param {string} req.params.y - Y coordinate of the tile.
|
||||
* @param {string} req.params.format - Format of the tile.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
app.get('/:id/:z/:x/:y.:format', async (req, res) => {
|
||||
if (verbose) {
|
||||
console.log(
|
||||
`Handling tile request for: /data/%s/%s/%s/%s.%s`,
|
||||
String(req.params.id).replace(/\n|\r/g, ''),
|
||||
String(req.params.z).replace(/\n|\r/g, ''),
|
||||
String(req.params.x).replace(/\n|\r/g, ''),
|
||||
String(req.params.y).replace(/\n|\r/g, ''),
|
||||
String(req.params.format).replace(/\n|\r/g, ''),
|
||||
);
|
||||
}
|
||||
const item = repo[req.params.id];
|
||||
if (!item) {
|
||||
return res.sendStatus(404);
|
||||
}
|
||||
const tileJSONFormat = item.tileJSON.format;
|
||||
const z = req.params.z | 0;
|
||||
const x = req.params.x | 0;
|
||||
const y = req.params.y | 0;
|
||||
const z = parseInt(req.params.z, 10);
|
||||
const x = parseInt(req.params.x, 10);
|
||||
const y = parseInt(req.params.y, 10);
|
||||
if (isNaN(z) || isNaN(x) || isNaN(y)) {
|
||||
return res.status(404).send('Invalid Tile');
|
||||
}
|
||||
|
||||
let format = req.params.format;
|
||||
if (format === options.pbfAlias) {
|
||||
format = 'pbf';
|
||||
|
@ -47,7 +92,6 @@ export const serve_data = {
|
|||
}
|
||||
if (
|
||||
z < item.tileJSON.minzoom ||
|
||||
0 ||
|
||||
x < 0 ||
|
||||
y < 0 ||
|
||||
z > item.tileJSON.maxzoom ||
|
||||
|
@ -56,18 +100,38 @@ export const serve_data = {
|
|||
) {
|
||||
return res.status(404).send('Out of bounds');
|
||||
}
|
||||
if (item.sourceType === 'pmtiles') {
|
||||
let tileinfo = await getPMtilesTile(item.source, z, x, y);
|
||||
if (tileinfo == undefined || tileinfo.data == undefined) {
|
||||
return res.status(404).send('Not found');
|
||||
} else {
|
||||
let data = tileinfo.data;
|
||||
let headers = tileinfo.header;
|
||||
|
||||
const fetchTile = await fetchTileData(
|
||||
item.source,
|
||||
item.sourceType,
|
||||
z,
|
||||
x,
|
||||
y,
|
||||
);
|
||||
if (fetchTile == null) return res.status(204).send();
|
||||
|
||||
let data = fetchTile.data;
|
||||
let headers = fetchTile.headers;
|
||||
let isGzipped = data.slice(0, 2).indexOf(Buffer.from([0x1f, 0x8b])) === 0;
|
||||
|
||||
if (isGzipped) {
|
||||
data = await gunzipP(data);
|
||||
isGzipped = false;
|
||||
}
|
||||
|
||||
if (tileJSONFormat === 'pbf') {
|
||||
if (options.dataDecoratorFunc) {
|
||||
data = options.dataDecoratorFunc(id, 'data', data, z, x, y);
|
||||
data = options.dataDecoratorFunc(
|
||||
req.params.id,
|
||||
'data',
|
||||
data,
|
||||
z,
|
||||
x,
|
||||
y,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (format === 'pbf') {
|
||||
headers['Content-Type'] = 'application/x-protobuf';
|
||||
} else if (format === 'geojson') {
|
||||
|
@ -88,68 +152,9 @@ export const serve_data = {
|
|||
}
|
||||
data = JSON.stringify(geojson);
|
||||
}
|
||||
delete headers['ETag']; // do not trust the tile ETag -- regenerate
|
||||
headers['Content-Encoding'] = 'gzip';
|
||||
res.set(headers);
|
||||
|
||||
data = await gzipP(data);
|
||||
|
||||
return res.status(200).send(data);
|
||||
if (headers) {
|
||||
delete headers['ETag'];
|
||||
}
|
||||
} else if (item.sourceType === 'mbtiles') {
|
||||
item.source.getTile(z, x, y, async (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 = await gunzipP(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 = await gunzipP(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);
|
||||
|
||||
|
@ -158,32 +163,40 @@ export const serve_data = {
|
|||
}
|
||||
|
||||
return res.status(200).send(data);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
app.get(
|
||||
'^/:id/elevation/:z([0-9]+)/:x([-.0-9]+)/:y([-.0-9]+)',
|
||||
async (req, res, next) => {
|
||||
/**
|
||||
* Handles requests for elevation data.
|
||||
* @param {object} req - Express request object.
|
||||
* @param {object} res - Express response object.
|
||||
* @param {string} req.params.id - ID of the elevation data.
|
||||
* @param {string} req.params.z - Z coordinate of the tile.
|
||||
* @param {string} req.params.x - X coordinate of the tile (either integer or float).
|
||||
* @param {string} req.params.y - Y coordinate of the tile (either integer or float).
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
app.get('/:id/elevation/:z/:x/:y', async (req, res, next) => {
|
||||
try {
|
||||
if (verbose) {
|
||||
console.log(
|
||||
`Handling elevation request for: /data/%s/elevation/%s/%s/%s`,
|
||||
String(req.params.id).replace(/\n|\r/g, ''),
|
||||
String(req.params.z).replace(/\n|\r/g, ''),
|
||||
String(req.params.x).replace(/\n|\r/g, ''),
|
||||
String(req.params.y).replace(/\n|\r/g, ''),
|
||||
);
|
||||
}
|
||||
const item = repo?.[req.params.id];
|
||||
if (!item) return res.sendStatus(404);
|
||||
if (!item.source) return res.status(404).send('Missing source');
|
||||
if (!item.tileJSON) return res.status(404).send('Missing tileJSON');
|
||||
if (!item.sourceType)
|
||||
return res.status(404).send('Missing sourceType');
|
||||
|
||||
if (!item.sourceType) return res.status(404).send('Missing sourceType');
|
||||
const { source, tileJSON, sourceType } = item;
|
||||
|
||||
if (sourceType !== 'pmtiles' && sourceType !== 'mbtiles') {
|
||||
return res
|
||||
.status(400)
|
||||
.send('Invalid sourceType. Must be pmtiles or mbtiles.');
|
||||
}
|
||||
|
||||
const encoding = tileJSON?.encoding;
|
||||
if (encoding == null) {
|
||||
return res.status(400).send('Missing tileJSON.encoding');
|
||||
|
@ -192,22 +205,18 @@ export const serve_data = {
|
|||
.status(400)
|
||||
.send('Invalid encoding. Must be terrarium or mapbox.');
|
||||
}
|
||||
|
||||
const format = tileJSON?.format;
|
||||
if (format == null) {
|
||||
return res.status(400).send('Missing tileJSON.format');
|
||||
} else if (format !== 'webp' && format !== 'png') {
|
||||
return res.status(400).send('Invalid format. Must be webp or png.');
|
||||
}
|
||||
|
||||
const z = parseInt(req.params.z, 10);
|
||||
const x = parseFloat(req.params.x);
|
||||
const y = parseFloat(req.params.y);
|
||||
|
||||
if (tileJSON.minzoom == null || tileJSON.maxzoom == null) {
|
||||
return res.status(404).send(JSON.stringify(tileJSON));
|
||||
}
|
||||
|
||||
const TILE_SIZE = tileJSON.tileSize || 512;
|
||||
let bbox;
|
||||
let xy;
|
||||
|
@ -216,7 +225,6 @@ export const serve_data = {
|
|||
if (Number.isInteger(x) && Number.isInteger(y)) {
|
||||
const intX = parseInt(req.params.x, 10);
|
||||
const intY = parseInt(req.params.y, 10);
|
||||
|
||||
if (
|
||||
zoom < tileJSON.minzoom ||
|
||||
zoom > tileJSON.maxzoom ||
|
||||
|
@ -237,124 +245,57 @@ export const serve_data = {
|
|||
if (zoom > tileJSON.maxzoom) {
|
||||
zoom = tileJSON.maxzoom;
|
||||
}
|
||||
|
||||
bbox = [x, y, x + 0.1, y + 0.1];
|
||||
const { minX, minY } = new SphericalMercator().xyz(bbox, zoom);
|
||||
xy = [minX, minY];
|
||||
}
|
||||
|
||||
let data;
|
||||
if (sourceType === 'pmtiles') {
|
||||
const tileinfo = await getPMtilesTile(source, zoom, xy[0], xy[1]);
|
||||
if (!tileinfo?.data) return res.status(204).send();
|
||||
data = tileinfo.data;
|
||||
} else {
|
||||
data = await new Promise((resolve, reject) => {
|
||||
source.getTile(zoom, xy[0], xy[1], (err, tileData) => {
|
||||
if (err) {
|
||||
return /does not exist/.test(err.message)
|
||||
? resolve(null)
|
||||
: reject(err);
|
||||
}
|
||||
resolve(tileData);
|
||||
});
|
||||
});
|
||||
}
|
||||
if (data == null) return res.status(204).send();
|
||||
if (!data) return res.status(404).send('Not found');
|
||||
const fetchTile = await fetchTileData(
|
||||
source,
|
||||
sourceType,
|
||||
zoom,
|
||||
xy[0],
|
||||
xy[1],
|
||||
);
|
||||
if (fetchTile == null) return res.status(204).send();
|
||||
|
||||
const image = new Image();
|
||||
await new Promise(async (resolve, reject) => {
|
||||
image.onload = async () => {
|
||||
const canvas = createCanvas(TILE_SIZE, TILE_SIZE);
|
||||
const context = canvas.getContext('2d');
|
||||
context.drawImage(image, 0, 0);
|
||||
|
||||
const long = bbox[0];
|
||||
const lat = bbox[1];
|
||||
|
||||
// calculate pixel coordinate of tile,
|
||||
// see https://developers.google.com/maps/documentation/javascript/examples/map-coordinates
|
||||
let siny = Math.sin((lat * Math.PI) / 180);
|
||||
// Truncating to 0.9999 effectively limits latitude to 89.189. This is
|
||||
// about a third of a tile past the edge of the world tile.
|
||||
siny = Math.min(Math.max(siny, -0.9999), 0.9999);
|
||||
|
||||
const xWorld = TILE_SIZE * (0.5 + long / 360);
|
||||
const yWorld =
|
||||
TILE_SIZE *
|
||||
(0.5 - Math.log((1 + siny) / (1 - siny)) / (4 * Math.PI));
|
||||
|
||||
const scale = 1 << zoom;
|
||||
|
||||
const xTile = Math.floor((xWorld * scale) / TILE_SIZE);
|
||||
const yTile = Math.floor((yWorld * scale) / TILE_SIZE);
|
||||
|
||||
const xPixel = Math.floor(xWorld * scale) - xTile * TILE_SIZE;
|
||||
const yPixel = Math.floor(yWorld * scale) - yTile * TILE_SIZE;
|
||||
|
||||
if (
|
||||
xPixel < 0 ||
|
||||
yPixel < 0 ||
|
||||
xPixel >= TILE_SIZE ||
|
||||
yPixel >= TILE_SIZE
|
||||
) {
|
||||
return reject('Pixel is out of bounds');
|
||||
}
|
||||
|
||||
const imgdata = context.getImageData(xPixel, yPixel, 1, 1);
|
||||
const red = imgdata.data[0];
|
||||
const green = imgdata.data[1];
|
||||
const blue = imgdata.data[2];
|
||||
|
||||
let elevation;
|
||||
if (encoding === 'mapbox') {
|
||||
elevation =
|
||||
-10000 + (red * 256 * 256 + green * 256 + blue) * 0.1;
|
||||
} else if (encoding === 'terrarium') {
|
||||
elevation = red * 256 + green + blue / 256 - 32768;
|
||||
} else {
|
||||
elevation = 'invalid encoding';
|
||||
}
|
||||
|
||||
resolve(
|
||||
res.status(200).send({
|
||||
let data = fetchTile.data;
|
||||
var param = {
|
||||
long: bbox[0].toFixed(7),
|
||||
lat: bbox[1].toFixed(7),
|
||||
encoding,
|
||||
format,
|
||||
tile_size: TILE_SIZE,
|
||||
z: zoom,
|
||||
x: xy[0],
|
||||
y: xy[1],
|
||||
red,
|
||||
green,
|
||||
blue,
|
||||
latitude: lat,
|
||||
longitude: long,
|
||||
elevation,
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
image.onerror = (err) => reject(err);
|
||||
|
||||
if (format === 'webp') {
|
||||
try {
|
||||
const img = await sharp(data).toFormat('png').toBuffer();
|
||||
image.src = img;
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
} else {
|
||||
image.src = data;
|
||||
}
|
||||
});
|
||||
res
|
||||
.status(200)
|
||||
.send(await serve_rendered.getTerrainElevation(data, param));
|
||||
} catch (err) {
|
||||
return res
|
||||
.status(500)
|
||||
.header('Content-Type', 'text/plain')
|
||||
.send(err.message);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
app.get('/:id.json', (req, res, next) => {
|
||||
/**
|
||||
* Handles requests for tilejson for the data tiles.
|
||||
* @param {object} req - Express request object.
|
||||
* @param {object} res - Express response object.
|
||||
* @param {string} req.params.id - ID of the data source.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
app.get('/:id.json', (req, res) => {
|
||||
if (verbose) {
|
||||
console.log(
|
||||
`Handling tilejson request for: /data/%s.json`,
|
||||
String(req.params.id).replace(/\n|\r/g, ''),
|
||||
);
|
||||
}
|
||||
const item = repo[req.params.id];
|
||||
if (!item) {
|
||||
return res.sendStatus(404);
|
||||
|
@ -377,7 +318,20 @@ export const serve_data = {
|
|||
|
||||
return app;
|
||||
},
|
||||
add: async (options, repo, params, id, publicUrl) => {
|
||||
/**
|
||||
* Adds a new data source to the repository.
|
||||
* @param {object} options Configuration options.
|
||||
* @param {object} repo Repository object.
|
||||
* @param {object} params Parameters object.
|
||||
* @param {string} id ID of the data source.
|
||||
* @param {object} programOpts - An object containing the program options
|
||||
* @param {string} programOpts.publicUrl Public URL for the data.
|
||||
* @param {boolean} programOpts.verbose Whether verbose logging should be used.
|
||||
* @param {Function} dataResolver Function to resolve data.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
add: async function (options, repo, params, id, programOpts) {
|
||||
const { publicUrl } = programOpts;
|
||||
let inputFile;
|
||||
let inputType;
|
||||
if (params.pmtiles) {
|
||||
|
|
|
@ -4,7 +4,15 @@ import express from 'express';
|
|||
|
||||
import { getFontsPbf, listFonts } from './utils.js';
|
||||
|
||||
export const serve_font = async (options, allowedFonts) => {
|
||||
/**
|
||||
* Initializes and returns an Express app that serves font files.
|
||||
* @param {object} options - Configuration options for the server.
|
||||
* @param {object} allowedFonts - An object containing allowed fonts.
|
||||
* @param {object} programOpts - An object containing the program options.
|
||||
* @returns {Promise<express.Application>} - A promise that resolves to the Express app.
|
||||
*/
|
||||
export async function serve_font(options, allowedFonts, programOpts) {
|
||||
const { verbose } = programOpts;
|
||||
const app = express().disable('x-powered-by');
|
||||
|
||||
const lastModified = new Date().toUTCString();
|
||||
|
@ -13,31 +21,74 @@ export const serve_font = async (options, allowedFonts) => {
|
|||
|
||||
const existingFonts = {};
|
||||
|
||||
app.get(
|
||||
'/fonts/:fontstack/:range([\\d]+-[\\d]+).pbf',
|
||||
async (req, res, next) => {
|
||||
const fontstack = decodeURI(req.params.fontstack);
|
||||
const range = req.params.range;
|
||||
/**
|
||||
* Handles requests for a font file.
|
||||
* @param {object} req - Express request object.
|
||||
* @param {object} res - Express response object.
|
||||
* @param {string} req.params.fontstack - Name of the font stack.
|
||||
* @param {string} req.params.range - The range of the font (e.g. 0-255).
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
app.get('/fonts/:fontstack/:range.pbf', async (req, res) => {
|
||||
const sRange = String(req.params.range).replace(/\n|\r/g, '');
|
||||
const sFontStack = String(decodeURI(req.params.fontstack)).replace(
|
||||
/\n|\r/g,
|
||||
'',
|
||||
);
|
||||
|
||||
if (verbose) {
|
||||
console.log(
|
||||
`Handling font request for: /fonts/%s/%s.pbf`,
|
||||
sFontStack,
|
||||
sRange,
|
||||
);
|
||||
}
|
||||
|
||||
const modifiedSince = req.get('if-modified-since');
|
||||
const cc = req.get('cache-control');
|
||||
if (modifiedSince && (!cc || cc.indexOf('no-cache') === -1)) {
|
||||
if (
|
||||
new Date(lastModified).getTime() === new Date(modifiedSince).getTime()
|
||||
) {
|
||||
return res.sendStatus(304);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const concatenated = await getFontsPbf(
|
||||
options.serveAllFonts ? null : allowedFonts,
|
||||
fontPath,
|
||||
fontstack,
|
||||
range,
|
||||
sFontStack,
|
||||
sRange,
|
||||
existingFonts,
|
||||
);
|
||||
|
||||
res.header('Content-type', 'application/x-protobuf');
|
||||
res.header('Last-Modified', lastModified);
|
||||
return res.send(concatenated);
|
||||
} catch (err) {
|
||||
res.status(400).header('Content-Type', 'text/plain').send(err);
|
||||
}
|
||||
},
|
||||
console.error(
|
||||
`Error serving font: %s/%s.pbf, Error: %s`,
|
||||
sFontStack,
|
||||
sRange,
|
||||
String(err),
|
||||
);
|
||||
return res
|
||||
.status(400)
|
||||
.header('Content-Type', 'text/plain')
|
||||
.send('Error serving font');
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/fonts.json', (req, res, next) => {
|
||||
/**
|
||||
* Handles requests for a list of all available fonts.
|
||||
* @param {object} req - Express request object.
|
||||
* @param {object} res - Express response object.
|
||||
* @returns {void}
|
||||
*/
|
||||
app.get('/fonts.json', (req, res) => {
|
||||
if (verbose) {
|
||||
console.log('Handling list font request for /fonts.json');
|
||||
}
|
||||
res.header('Content-type', 'application/json');
|
||||
return res.send(
|
||||
Object.keys(options.serveAllFonts ? existingFonts : allowedFonts).sort(),
|
||||
|
@ -47,4 +98,4 @@ export const serve_font = async (options, allowedFonts) => {
|
|||
const fonts = await listFonts(options.paths.fonts);
|
||||
Object.assign(existingFonts, fonts);
|
||||
return app;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -3,7 +3,12 @@
|
|||
'use strict';
|
||||
|
||||
export const serve_rendered = {
|
||||
init: (options, repo) => {},
|
||||
add: (options, repo, params, id, publicUrl, dataResolver) => {},
|
||||
init: (options, repo, programOpts) => {},
|
||||
add: (options, repo, params, id, programOpts, dataResolver) => {},
|
||||
remove: (repo, id) => {},
|
||||
clear: (repo) => {},
|
||||
getTerrainElevation: (data, param) => {
|
||||
param['elevation'] = 'not supported in light';
|
||||
return param;
|
||||
},
|
||||
};
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -7,18 +7,44 @@ import clone from 'clone';
|
|||
import express from 'express';
|
||||
import { validateStyleMin } from '@maplibre/maplibre-gl-style-spec';
|
||||
|
||||
import { fixUrl, allowedOptions } from './utils.js';
|
||||
import {
|
||||
allowedSpriteScales,
|
||||
allowedSpriteFormats,
|
||||
fixUrl,
|
||||
readFile,
|
||||
} from './utils.js';
|
||||
|
||||
const httpTester = /^https?:\/\//i;
|
||||
const allowedSpriteScales = allowedOptions(['', '@2x', '@3x']);
|
||||
const allowedSpriteFormats = allowedOptions(['png', 'json']);
|
||||
|
||||
export const serve_style = {
|
||||
init: (options, repo) => {
|
||||
/**
|
||||
* Initializes the serve_style module.
|
||||
* @param {object} options Configuration options.
|
||||
* @param {object} repo Repository object.
|
||||
* @param {object} programOpts - An object containing the program options.
|
||||
* @returns {express.Application} The initialized Express application.
|
||||
*/
|
||||
init: function (options, repo, programOpts) {
|
||||
const { verbose } = programOpts;
|
||||
const app = express().disable('x-powered-by');
|
||||
|
||||
/**
|
||||
* Handles requests for style.json files.
|
||||
* @param {express.Request} req - Express request object.
|
||||
* @param {express.Response} res - Express response object.
|
||||
* @param {express.NextFunction} next - Express next function.
|
||||
* @param {string} req.params.id - ID of the style.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
app.get('/:id/style.json', (req, res, next) => {
|
||||
const item = repo[req.params.id];
|
||||
const { id } = req.params;
|
||||
if (verbose) {
|
||||
console.log(
|
||||
'Handling style request for: /styles/%s/style.json',
|
||||
String(id).replace(/\n|\r/g, ''),
|
||||
);
|
||||
}
|
||||
try {
|
||||
const item = repo[id];
|
||||
if (!item) {
|
||||
return res.sendStatus(404);
|
||||
}
|
||||
|
@ -30,7 +56,6 @@ export const serve_style = {
|
|||
source.data = fixUrl(req, source.data, item.publicUrl);
|
||||
}
|
||||
}
|
||||
// mapbox-gl-js viewer cannot handle sprite urls with query
|
||||
if (styleJSON_.sprite) {
|
||||
if (Array.isArray(styleJSON_.sprite)) {
|
||||
styleJSON_.sprite.forEach((spriteItem) => {
|
||||
|
@ -44,59 +69,152 @@ export const serve_style = {
|
|||
styleJSON_.glyphs = fixUrl(req, styleJSON_.glyphs, item.publicUrl);
|
||||
}
|
||||
return res.send(styleJSON_);
|
||||
} catch (e) {
|
||||
next(e);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Handles GET requests for sprite images and JSON files.
|
||||
* @param {express.Request} req - Express request object.
|
||||
* @param {express.Response} res - Express response object.
|
||||
* @param {express.NextFunction} next - Express next function.
|
||||
* @param {string} req.params.id - ID of the sprite.
|
||||
* @param {string} [req.params.spriteID='default'] - ID of the specific sprite image, defaults to 'default'.
|
||||
* @param {string} [req.params.scale] - Scale of the sprite image, defaults to ''.
|
||||
* @param {string} req.params.format - Format of the sprite file, 'png' or 'json'.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
app.get(
|
||||
'/:id/sprite(/:spriteID)?:scale(@[23]x)?.:format([\\w]+)',
|
||||
(req, res, next) => {
|
||||
const { spriteID = 'default', id } = req.params;
|
||||
const scale = allowedSpriteScales(req.params.scale) || '';
|
||||
const format = allowedSpriteFormats(req.params.format);
|
||||
|
||||
if (format) {
|
||||
`/:id/sprite{/:spriteID}{@:scale}{.:format}`,
|
||||
async (req, res, next) => {
|
||||
const { spriteID = 'default', id, format, scale } = req.params;
|
||||
const sanitizedId = String(id).replace(/\n|\r/g, '');
|
||||
const sanitizedScale = scale ? String(scale).replace(/\n|\r/g, '') : '';
|
||||
const sanitizedSpriteID = String(spriteID).replace(/\n|\r/g, '');
|
||||
const sanitizedFormat = format
|
||||
? '.' + String(format).replace(/\n|\r/g, '')
|
||||
: '';
|
||||
if (verbose) {
|
||||
console.log(
|
||||
`Handling sprite request for: /styles/%s/sprite/%s%s%s`,
|
||||
sanitizedId,
|
||||
sanitizedSpriteID,
|
||||
sanitizedScale,
|
||||
sanitizedFormat,
|
||||
);
|
||||
}
|
||||
const item = repo[id];
|
||||
const validatedFormat = allowedSpriteFormats(format);
|
||||
if (!item || !validatedFormat) {
|
||||
if (verbose)
|
||||
console.error(
|
||||
`Sprite item or format not found for: /styles/%s/sprite/%s%s%s`,
|
||||
sanitizedId,
|
||||
sanitizedSpriteID,
|
||||
sanitizedScale,
|
||||
sanitizedFormat,
|
||||
);
|
||||
return res.sendStatus(404);
|
||||
}
|
||||
const sprite = item.spritePaths.find(
|
||||
(sprite) => sprite.id === spriteID,
|
||||
);
|
||||
if (sprite) {
|
||||
const filename = `${sprite.path + scale}.${format}`;
|
||||
return fs.readFile(filename, (err, data) => {
|
||||
if (err) {
|
||||
console.log('Sprite load error:', filename);
|
||||
return res.sendStatus(404);
|
||||
} else {
|
||||
if (format === 'json')
|
||||
res.header('Content-type', 'application/json');
|
||||
if (format === 'png') res.header('Content-type', 'image/png');
|
||||
return res.send(data);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
const spriteScale = allowedSpriteScales(scale);
|
||||
if (!sprite || spriteScale === null) {
|
||||
if (verbose)
|
||||
console.error(
|
||||
`Bad Sprite ID or Scale for: /styles/%s/sprite/%s%s%s`,
|
||||
sanitizedId,
|
||||
sanitizedSpriteID,
|
||||
sanitizedScale,
|
||||
sanitizedFormat,
|
||||
);
|
||||
return res.status(400).send('Bad Sprite ID or Scale');
|
||||
}
|
||||
} else {
|
||||
return res.status(400).send('Bad Sprite Format');
|
||||
|
||||
const modifiedSince = req.get('if-modified-since');
|
||||
const cc = req.get('cache-control');
|
||||
if (modifiedSince && (!cc || cc.indexOf('no-cache') === -1)) {
|
||||
if (
|
||||
new Date(item.lastModified).getTime() ===
|
||||
new Date(modifiedSince).getTime()
|
||||
) {
|
||||
return res.sendStatus(304);
|
||||
}
|
||||
}
|
||||
|
||||
const sanitizedSpritePath = sprite.path.replace(/^(\.\.\/)+/, '');
|
||||
const filename = `${sanitizedSpritePath}${spriteScale}.${validatedFormat}`;
|
||||
if (verbose) console.log(`Loading sprite from: %s`, filename);
|
||||
try {
|
||||
const data = await readFile(filename);
|
||||
|
||||
if (validatedFormat === 'json') {
|
||||
res.header('Content-type', 'application/json');
|
||||
} else if (validatedFormat === 'png') {
|
||||
res.header('Content-type', 'image/png');
|
||||
}
|
||||
if (verbose)
|
||||
console.log(
|
||||
`Responding with sprite data for /styles/%s/sprite/%s%s%s`,
|
||||
sanitizedId,
|
||||
sanitizedSpriteID,
|
||||
sanitizedScale,
|
||||
sanitizedFormat,
|
||||
);
|
||||
res.set({ 'Last-Modified': item.lastModified });
|
||||
return res.send(data);
|
||||
} catch (err) {
|
||||
if (verbose) {
|
||||
console.error(
|
||||
'Sprite load error: %s, Error: %s',
|
||||
filename,
|
||||
String(err),
|
||||
);
|
||||
}
|
||||
return res.sendStatus(404);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
return app;
|
||||
},
|
||||
remove: (repo, id) => {
|
||||
/**
|
||||
* Removes an item from the repository.
|
||||
* @param {object} repo Repository object.
|
||||
* @param {string} id ID of the item to remove.
|
||||
* @returns {void}
|
||||
*/
|
||||
remove: function (repo, id) {
|
||||
delete repo[id];
|
||||
},
|
||||
add: (options, repo, params, id, publicUrl, reportTiles, reportFont) => {
|
||||
/**
|
||||
* Adds a new style to the repository.
|
||||
* @param {object} options Configuration options.
|
||||
* @param {object} repo Repository object.
|
||||
* @param {object} params Parameters object containing style path
|
||||
* @param {string} id ID of the style.
|
||||
* @param {object} programOpts - An object containing the program options
|
||||
* @param {object} style pre-fetched/read StyleJSON object.
|
||||
* @param {Function} reportTiles Function for reporting tile sources.
|
||||
* @param {Function} reportFont Function for reporting font usage
|
||||
* @returns {boolean} true if add is successful
|
||||
*/
|
||||
add: function (
|
||||
options,
|
||||
repo,
|
||||
params,
|
||||
id,
|
||||
programOpts,
|
||||
style,
|
||||
reportTiles,
|
||||
reportFont,
|
||||
) {
|
||||
const { publicUrl } = programOpts;
|
||||
const styleFile = path.resolve(options.paths.styles, params.style);
|
||||
const styleJSON = clone(style);
|
||||
|
||||
let styleFileData;
|
||||
try {
|
||||
styleFileData = fs.readFileSync(styleFile); // TODO: could be made async if this function was
|
||||
} catch (e) {
|
||||
console.log(`Error reading style file "${params.style}"`);
|
||||
return false;
|
||||
}
|
||||
|
||||
const styleJSON = JSON.parse(styleFileData);
|
||||
const validationErrors = validateStyleMin(styleJSON);
|
||||
if (validationErrors.length > 0) {
|
||||
console.log(`The file "${params.style}" is not a valid style file:`);
|
||||
|
@ -199,6 +317,7 @@ export const serve_style = {
|
|||
spritePaths,
|
||||
publicUrl,
|
||||
name: styleJSON.name,
|
||||
lastModified: new Date().toUTCString(),
|
||||
};
|
||||
|
||||
return true;
|
||||
|
|
282
src/server.js
282
src/server.js
|
@ -1,9 +1,6 @@
|
|||
#!/usr/bin/env node
|
||||
'use strict';
|
||||
|
||||
import os from 'os';
|
||||
process.env.UV_THREADPOOL_SIZE = Math.ceil(Math.max(4, os.cpus().length * 1.5));
|
||||
|
||||
import fs from 'node:fs';
|
||||
import path from 'path';
|
||||
import fnv1a from '@sindresorhus/fnv1a';
|
||||
|
@ -19,25 +16,30 @@ 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, isValidHttpUrl } from './utils.js';
|
||||
import {
|
||||
allowedTileSizes,
|
||||
getTileUrls,
|
||||
getPublicUrl,
|
||||
isValidHttpUrl,
|
||||
} from './utils.js';
|
||||
|
||||
import { fileURLToPath } from 'url';
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const packageJson = JSON.parse(
|
||||
fs.readFileSync(__dirname + '/../package.json', 'utf8'),
|
||||
);
|
||||
|
||||
const isLight = packageJson.name.slice(-6) === '-light';
|
||||
|
||||
const serve_rendered = (
|
||||
await import(`${!isLight ? `./serve_rendered.js` : `./serve_light.js`}`)
|
||||
).serve_rendered;
|
||||
|
||||
/**
|
||||
*
|
||||
* @param opts
|
||||
* Starts the server.
|
||||
* @param {object} opts - Configuration options for the server.
|
||||
* @returns {Promise<object>} - A promise that resolves to the server object.
|
||||
*/
|
||||
function start(opts) {
|
||||
async function start(opts) {
|
||||
console.log('Starting server');
|
||||
|
||||
const app = express().disable('x-powered-by');
|
||||
|
@ -116,8 +118,9 @@ function start(opts) {
|
|||
* Recursively get all files within a directory.
|
||||
* Inspired by https://stackoverflow.com/a/45130990/10133863
|
||||
* @param {string} directory Absolute path to a directory to get files from.
|
||||
* @returns {Promise<string[]>} - A promise that resolves to an array of file paths relative to the icon directory.
|
||||
*/
|
||||
const getFiles = async (directory) => {
|
||||
async function getFiles(directory) {
|
||||
// Fetch all entries of the directory and attach type information
|
||||
const dirEntries = await fs.promises.readdir(directory, {
|
||||
withFileTypes: true,
|
||||
|
@ -136,7 +139,7 @@ function start(opts) {
|
|||
|
||||
// Flatten the list of files to a single array
|
||||
return files.flat();
|
||||
};
|
||||
}
|
||||
|
||||
// Load all available icons into a settings object
|
||||
startupPromises.push(
|
||||
|
@ -159,26 +162,53 @@ function start(opts) {
|
|||
app.use(cors());
|
||||
}
|
||||
|
||||
app.use('/data/', serve_data.init(options, serving.data));
|
||||
app.use('/data/', serve_data.init(options, serving.data, opts));
|
||||
app.use('/files/', express.static(paths.files));
|
||||
app.use('/styles/', serve_style.init(options, serving.styles));
|
||||
app.use('/styles/', serve_style.init(options, serving.styles, opts));
|
||||
if (!isLight) {
|
||||
startupPromises.push(
|
||||
serve_rendered.init(options, serving.rendered).then((sub) => {
|
||||
serve_rendered.init(options, serving.rendered, opts).then((sub) => {
|
||||
app.use('/styles/', sub);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
const addStyle = (id, item, allowMoreData, reportFonts) => {
|
||||
/**
|
||||
* Adds a style to the server.
|
||||
* @param {string} id - The ID of the style.
|
||||
* @param {object} item - The style configuration object.
|
||||
* @param {boolean} allowMoreData - Whether to allow adding more data sources.
|
||||
* @param {boolean} reportFonts - Whether to report fonts.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function addStyle(id, item, allowMoreData, reportFonts) {
|
||||
let success = true;
|
||||
|
||||
let styleJSON;
|
||||
try {
|
||||
if (isValidHttpUrl(item.style)) {
|
||||
const res = await fetch(item.style);
|
||||
if (!res.ok) {
|
||||
throw new Error(`fetch error ${res.status}`);
|
||||
}
|
||||
styleJSON = await res.json();
|
||||
} else {
|
||||
const styleFile = path.resolve(options.paths.styles, item.style);
|
||||
const styleFileData = await fs.promises.readFile(styleFile);
|
||||
styleJSON = JSON.parse(styleFileData);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(`Error getting style file "${item.style}"`);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (item.serve_data !== false) {
|
||||
success = serve_style.add(
|
||||
options,
|
||||
serving.styles,
|
||||
item,
|
||||
id,
|
||||
opts.publicUrl,
|
||||
opts,
|
||||
styleJSON,
|
||||
(styleSourceId, protocol) => {
|
||||
let dataItemId;
|
||||
for (const id of Object.keys(data)) {
|
||||
|
@ -235,7 +265,8 @@ function start(opts) {
|
|||
serving.rendered,
|
||||
item,
|
||||
id,
|
||||
opts.publicUrl,
|
||||
opts,
|
||||
styleJSON,
|
||||
function dataResolver(styleSourceId) {
|
||||
let fileType;
|
||||
let inputFile;
|
||||
|
@ -261,7 +292,8 @@ function start(opts) {
|
|||
item.serve_rendered = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
return success;
|
||||
}
|
||||
|
||||
for (const id of Object.keys(config.styles || {})) {
|
||||
const item = config.styles[id];
|
||||
|
@ -269,16 +301,13 @@ function start(opts) {
|
|||
console.log(`Missing "style" property for ${id}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
addStyle(id, item, true, true);
|
||||
startupPromises.push(addStyle(id, item, true, true));
|
||||
}
|
||||
|
||||
startupPromises.push(
|
||||
serve_font(options, serving.fonts).then((sub) => {
|
||||
serve_font(options, serving.fonts, opts).then((sub) => {
|
||||
app.use('/', sub);
|
||||
}),
|
||||
);
|
||||
|
||||
for (const id of Object.keys(data)) {
|
||||
const item = data[id];
|
||||
const fileType = Object.keys(data[id])[0];
|
||||
|
@ -288,12 +317,8 @@ function start(opts) {
|
|||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
startupPromises.push(
|
||||
serve_data.add(options, serving.data, item, id, opts.publicUrl),
|
||||
);
|
||||
startupPromises.push(serve_data.add(options, serving.data, item, id, opts));
|
||||
}
|
||||
|
||||
if (options.serveAllStyles) {
|
||||
fs.readdir(options.paths.styles, { withFileTypes: true }, (err, files) => {
|
||||
if (err) {
|
||||
|
@ -333,7 +358,13 @@ function start(opts) {
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles requests for a list of available styles.
|
||||
* @param {object} req - Express request object.
|
||||
* @param {object} res - Express response object.
|
||||
* @param {string} [req.query.key] - Optional API key.
|
||||
* @returns {void}
|
||||
*/
|
||||
app.get('/styles.json', (req, res, next) => {
|
||||
const result = [];
|
||||
const query = req.query.key
|
||||
|
@ -354,7 +385,15 @@ function start(opts) {
|
|||
res.send(result);
|
||||
});
|
||||
|
||||
const addTileJSONs = (arr, req, type, tileSize) => {
|
||||
/**
|
||||
* Adds TileJSON metadata to an array.
|
||||
* @param {Array} arr - The array to add TileJSONs to
|
||||
* @param {object} req - The express request object.
|
||||
* @param {string} type - The type of resource
|
||||
* @param {number} tileSize - The tile size.
|
||||
* @returns {Array} - An array of TileJSON objects.
|
||||
*/
|
||||
function addTileJSONs(arr, req, type, tileSize) {
|
||||
for (const id of Object.keys(serving[type])) {
|
||||
const info = clone(serving[type][id].tileJSON);
|
||||
let path = '';
|
||||
|
@ -377,20 +416,42 @@ function start(opts) {
|
|||
arr.push(info);
|
||||
}
|
||||
return arr;
|
||||
};
|
||||
}
|
||||
|
||||
app.get('/(:tileSize(256|512)/)?rendered.json', (req, res, next) => {
|
||||
const tileSize = parseInt(req.params.tileSize, 10) || undefined;
|
||||
res.send(addTileJSONs([], req, 'rendered', tileSize));
|
||||
/**
|
||||
* Handles requests for a rendered tilejson endpoint.
|
||||
* @param {object} req - Express request object.
|
||||
* @param {object} res - Express response object.
|
||||
* @param {string} req.params.tileSize - Optional tile size parameter.
|
||||
* @returns {void}
|
||||
*/
|
||||
app.get('{/:tileSize}/rendered.json', (req, res, next) => {
|
||||
const tileSize = allowedTileSizes(req.params['tileSize']);
|
||||
res.send(addTileJSONs([], req, 'rendered', parseInt(tileSize, 10)));
|
||||
});
|
||||
app.get('/data.json', (req, res, next) => {
|
||||
|
||||
/**
|
||||
* Handles requests for a data tilejson endpoint.
|
||||
* @param {object} req - Express request object.
|
||||
* @param {object} res - Express response object.
|
||||
* @returns {void}
|
||||
*/
|
||||
app.get('/data.json', (req, res) => {
|
||||
res.send(addTileJSONs([], req, 'data', undefined));
|
||||
});
|
||||
app.get('/(:tileSize(256|512)/)?index.json', (req, res, next) => {
|
||||
const tileSize = parseInt(req.params.tileSize, 10) || undefined;
|
||||
|
||||
/**
|
||||
* Handles requests for a combined rendered and data tilejson endpoint.
|
||||
* @param {object} req - Express request object.
|
||||
* @param {object} res - Express response object.
|
||||
* @param {string} req.params.tileSize - Optional tile size parameter.
|
||||
* @returns {void}
|
||||
*/
|
||||
app.get('{/:tileSize}/index.json', (req, res, next) => {
|
||||
const tileSize = allowedTileSizes(req.params['tileSize']);
|
||||
res.send(
|
||||
addTileJSONs(
|
||||
addTileJSONs([], req, 'rendered', tileSize),
|
||||
addTileJSONs([], req, 'rendered', parseInt(tileSize, 10)),
|
||||
req,
|
||||
'data',
|
||||
undefined,
|
||||
|
@ -403,7 +464,15 @@ function start(opts) {
|
|||
app.use('/', express.static(path.join(__dirname, '../public/resources')));
|
||||
|
||||
const templates = path.join(__dirname, '../public/templates');
|
||||
const serveTemplate = (urlPath, template, dataGetter) => {
|
||||
|
||||
/**
|
||||
* Serves a Handlebars template.
|
||||
* @param {string} urlPath - The URL path to serve the template at
|
||||
* @param {string} template - The name of the template file
|
||||
* @param {Function} dataGetter - A function to get data to be passed to the template.
|
||||
* @returns {void}
|
||||
*/
|
||||
function serveTemplate(urlPath, template, dataGetter) {
|
||||
let templateFile = `${templates}/${template}.tmpl`;
|
||||
if (template === 'index') {
|
||||
if (options.frontPage === false) {
|
||||
|
@ -415,24 +484,17 @@ function start(opts) {
|
|||
templateFile = path.resolve(paths.root, options.frontPage);
|
||||
}
|
||||
}
|
||||
startupPromises.push(
|
||||
new Promise((resolve, reject) => {
|
||||
fs.readFile(templateFile, (err, content) => {
|
||||
if (err) {
|
||||
err = new Error(`Template not found: ${err.message}`);
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const content = fs.readFileSync(templateFile, 'utf-8');
|
||||
const compiled = handlebars.compile(content.toString());
|
||||
|
||||
app.use(urlPath, (req, res, next) => {
|
||||
app.get(urlPath, (req, res, next) => {
|
||||
if (opts.verbose) {
|
||||
console.log(`Serving template at path: ${urlPath}`);
|
||||
}
|
||||
let data = {};
|
||||
if (dataGetter) {
|
||||
data = dataGetter(req);
|
||||
if (!data) {
|
||||
return res.status(404).send('Not found');
|
||||
}
|
||||
}
|
||||
if (data) {
|
||||
data['server_version'] =
|
||||
`${packageJson.name} v${packageJson.version}`;
|
||||
data['public_url'] = opts.publicUrl || '/';
|
||||
|
@ -445,14 +507,27 @@ function start(opts) {
|
|||
: '';
|
||||
if (template === 'wmts') res.set('Content-Type', 'text/xml');
|
||||
return res.status(200).send(compiled(data));
|
||||
} else {
|
||||
if (opts.verbose) {
|
||||
console.log(`Forwarding request for: ${urlPath} to next route`);
|
||||
}
|
||||
next('route');
|
||||
}
|
||||
}
|
||||
});
|
||||
resolve();
|
||||
});
|
||||
}),
|
||||
);
|
||||
};
|
||||
} catch (err) {
|
||||
console.error(`Error reading template file: ${templateFile}`, err);
|
||||
throw new Error(`Template not found: ${err.message}`); //throw an error so that the server doesnt start
|
||||
}
|
||||
}
|
||||
|
||||
serveTemplate('/$', 'index', (req) => {
|
||||
/**
|
||||
* Handles requests for the index page, providing a list of available styles and data.
|
||||
* @param {object} req - Express request object.
|
||||
* @param {object} res - Express response object.
|
||||
* @returns {void}
|
||||
*/
|
||||
serveTemplate('/', 'index', (req) => {
|
||||
let styles = {};
|
||||
for (const id of Object.keys(serving.styles || {})) {
|
||||
let style = {
|
||||
|
@ -464,11 +539,15 @@ function start(opts) {
|
|||
if (style.serving_rendered) {
|
||||
const { center } = style.serving_rendered.tileJSON;
|
||||
if (center) {
|
||||
style.viewer_hash = `#${center[2]}/${center[1].toFixed(5)}/${center[0].toFixed(5)}`;
|
||||
style.viewer_hash = `#${center[2]}/${center[1].toFixed(
|
||||
5,
|
||||
)}/${center[0].toFixed(5)}`;
|
||||
|
||||
const centerPx = mercator.px([center[0], center[1]], center[2]);
|
||||
// Set thumbnail default size to be 256px x 256px
|
||||
style.thumbnail = `${Math.floor(center[2])}/${Math.floor(centerPx[0] / 256)}/${Math.floor(centerPx[1] / 256)}.png`;
|
||||
style.thumbnail = `${Math.floor(center[2])}/${Math.floor(
|
||||
centerPx[0] / 256,
|
||||
)}/${Math.floor(centerPx[1] / 256)}.png`;
|
||||
}
|
||||
|
||||
const tileSize = 512;
|
||||
|
@ -484,7 +563,6 @@ function start(opts) {
|
|||
|
||||
styles[id] = style;
|
||||
}
|
||||
|
||||
let datas = {};
|
||||
for (const id of Object.keys(serving.data || {})) {
|
||||
let data = Object.assign({}, serving.data[id]);
|
||||
|
@ -517,15 +595,20 @@ function start(opts) {
|
|||
tileJSON.encoding === 'terrarium' ||
|
||||
tileJSON.encoding === 'mapbox'
|
||||
) {
|
||||
if (!isLight) {
|
||||
data.elevation_link = getTileUrls(
|
||||
req,
|
||||
tileJSON.tiles,
|
||||
`data/${id}/elevation`,
|
||||
)[0];
|
||||
}
|
||||
data.is_terrain = true;
|
||||
}
|
||||
if (center) {
|
||||
const centerPx = mercator.px([center[0], center[1]], center[2]);
|
||||
data.thumbnail = `${Math.floor(center[2])}/${Math.floor(centerPx[0] / 256)}/${Math.floor(centerPx[1] / 256)}.${tileJSON.format}`;
|
||||
data.thumbnail = `${Math.floor(center[2])}/${Math.floor(
|
||||
centerPx[0] / 256,
|
||||
)}/${Math.floor(centerPx[1] / 256)}.${tileJSON.format}`;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -542,24 +625,28 @@ function start(opts) {
|
|||
}
|
||||
data.formatted_filesize = `${size.toFixed(2)} ${suffix}`;
|
||||
}
|
||||
|
||||
datas[id] = data;
|
||||
}
|
||||
|
||||
return {
|
||||
styles: Object.keys(styles).length ? styles : null,
|
||||
data: Object.keys(datas).length ? datas : null,
|
||||
};
|
||||
});
|
||||
|
||||
serveTemplate('/styles/:id/$', 'viewer', (req) => {
|
||||
/**
|
||||
* Handles requests for a map viewer template for a specific style.
|
||||
* @param {object} req - Express request object.
|
||||
* @param {object} res - Express response object.
|
||||
* @param {string} req.params.id - ID of the style.
|
||||
* @returns {void}
|
||||
*/
|
||||
serveTemplate('/styles/:id/', 'viewer', (req) => {
|
||||
const { id } = req.params;
|
||||
const style = clone(((serving.styles || {})[id] || {}).styleJSON);
|
||||
|
||||
if (!style) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
...style,
|
||||
id,
|
||||
|
@ -569,10 +656,12 @@ function start(opts) {
|
|||
};
|
||||
});
|
||||
|
||||
/*
|
||||
app.use('/rendered/:id/$', function(req, res, next) {
|
||||
return res.redirect(301, '/styles/' + req.params.id + '/');
|
||||
});
|
||||
/**
|
||||
* Handles requests for a Web Map Tile Service (WMTS) XML template.
|
||||
* @param {object} req - Express request object.
|
||||
* @param {object} res - Express response object.
|
||||
* @param {string} req.params.id - ID of the style.
|
||||
* @returns {void}
|
||||
*/
|
||||
serveTemplate('/styles/:id/wmts.xml', 'wmts', (req) => {
|
||||
const { id } = req.params;
|
||||
|
@ -605,9 +694,16 @@ function start(opts) {
|
|||
};
|
||||
});
|
||||
|
||||
serveTemplate('^/data/(:preview(preview)/)?:id/$', 'data', (req) => {
|
||||
const id = req.params.id;
|
||||
const preview = req.params.preview || undefined;
|
||||
/**
|
||||
* Handles requests for a data view template for a specific data source.
|
||||
* @param {object} req - Express request object.
|
||||
* @param {object} res - Express response object.
|
||||
* @param {string} req.params.id - ID of the data source.
|
||||
* @param {string} [req.params.view] - Optional view type.
|
||||
* @returns {void}
|
||||
*/
|
||||
serveTemplate('/data{/:view}/:id/', 'data', (req) => {
|
||||
const { id, view } = req.params;
|
||||
const data = serving.data[id];
|
||||
|
||||
if (!data) {
|
||||
|
@ -616,7 +712,8 @@ function start(opts) {
|
|||
const is_terrain =
|
||||
(data.tileJSON.encoding === 'terrarium' ||
|
||||
data.tileJSON.encoding === 'mapbox') &&
|
||||
preview === 'preview';
|
||||
view === 'preview';
|
||||
|
||||
return {
|
||||
...data,
|
||||
id,
|
||||
|
@ -624,6 +721,7 @@ function start(opts) {
|
|||
is_terrain: is_terrain,
|
||||
is_terrainrgb: data.tileJSON.encoding === 'mapbox',
|
||||
terrain_encoding: data.tileJSON.encoding,
|
||||
is_light: isLight,
|
||||
};
|
||||
});
|
||||
|
||||
|
@ -633,7 +731,13 @@ function start(opts) {
|
|||
startupComplete = true;
|
||||
});
|
||||
|
||||
app.get('/health', (req, res, next) => {
|
||||
/**
|
||||
* Handles requests to see the health of the server.
|
||||
* @param {object} req - Express request object.
|
||||
* @param {object} res - Express response object.
|
||||
* @returns {void}
|
||||
*/
|
||||
app.get('/health', (req, res) => {
|
||||
if (startupComplete) {
|
||||
return res.status(200).send('OK');
|
||||
} else {
|
||||
|
@ -660,12 +764,13 @@ function start(opts) {
|
|||
app,
|
||||
server,
|
||||
startupPromise,
|
||||
serving,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the server gracefully
|
||||
* @param {string} signal Name of the received signal
|
||||
* @returns {void}
|
||||
*/
|
||||
function stopGracefully(signal) {
|
||||
console.log(`Caught signal ${signal}, stopping gracefully`);
|
||||
|
@ -673,11 +778,12 @@ function stopGracefully(signal) {
|
|||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param opts
|
||||
* Starts and manages the server
|
||||
* @param {object} opts - Configuration options for the server.
|
||||
* @returns {Promise<object>} - A promise that resolves to the running server
|
||||
*/
|
||||
export function server(opts) {
|
||||
const running = start(opts);
|
||||
export async function server(opts) {
|
||||
const running = await start(opts);
|
||||
|
||||
running.startupPromise.catch((err) => {
|
||||
console.error(err.message);
|
||||
|
@ -691,12 +797,16 @@ export function server(opts) {
|
|||
console.log(`Caught signal ${signal}, refreshing`);
|
||||
console.log('Stopping server and reloading config');
|
||||
|
||||
running.server.shutdown(() => {
|
||||
const restarted = start(opts);
|
||||
running.server.shutdown(async () => {
|
||||
const restarted = await start(opts);
|
||||
if (!isLight) {
|
||||
serve_rendered.clear(running.serving.rendered);
|
||||
}
|
||||
running.server = restarted.server;
|
||||
running.app = restarted.app;
|
||||
running.startupPromise = restarted.startupPromise;
|
||||
running.serving = restarted.serving;
|
||||
});
|
||||
});
|
||||
|
||||
return running;
|
||||
}
|
||||
|
|
262
src/utils.js
262
src/utils.js
|
@ -6,12 +6,18 @@ import fs from 'node:fs';
|
|||
import clone from 'clone';
|
||||
import { combine } from '@jsse/pbfont';
|
||||
import { existsP } from './promises.js';
|
||||
import { getPMtilesTile } from './pmtiles_adapter.js';
|
||||
|
||||
export const allowedSpriteFormats = allowedOptions(['png', 'json']);
|
||||
|
||||
export const allowedTileSizes = allowedOptions(['256', '512']);
|
||||
|
||||
/**
|
||||
* Restrict user input to an allowed set of options.
|
||||
* @param opts
|
||||
* @param root0
|
||||
* @param root0.defaultValue
|
||||
* @param {string[]} opts - An array of allowed option strings.
|
||||
* @param {object} [config] - Optional configuration object.
|
||||
* @param {string} [config.defaultValue] - The default value to return if input doesn't match.
|
||||
* @returns {function(string): string} - A function that takes a value and returns it if valid or a default.
|
||||
*/
|
||||
export function allowedOptions(opts, { defaultValue } = {}) {
|
||||
const values = Object.fromEntries(opts.map((key) => [key, key]));
|
||||
|
@ -19,10 +25,52 @@ export function allowedOptions(opts, { defaultValue } = {}) {
|
|||
}
|
||||
|
||||
/**
|
||||
* Replace local:// urls with public http(s):// urls
|
||||
* @param req
|
||||
* @param url
|
||||
* @param publicUrl
|
||||
* Parses a scale string to a number.
|
||||
* @param {string} scale The scale string (e.g., '2x', '4x').
|
||||
* @param {number} maxScale Maximum allowed scale digit.
|
||||
* @returns {number|null} The parsed scale as a number or null if invalid.
|
||||
*/
|
||||
export function allowedScales(scale, maxScale = 9) {
|
||||
if (scale === undefined) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line security/detect-non-literal-regexp
|
||||
const regex = new RegExp(`^[2-${maxScale}]x$`);
|
||||
if (!regex.test(scale)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return parseInt(scale.slice(0, -1), 10);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a string is a valid sprite scale and returns it if it is within the allowed range, and null if it does not conform.
|
||||
* @param {string} scale - The scale string to validate (e.g., '2x', '3x').
|
||||
* @param {number} [maxScale] - The maximum scale value. If no value is passed in, it defaults to a value of 3.
|
||||
* @returns {string|null} - The valid scale string or null if invalid.
|
||||
*/
|
||||
export function allowedSpriteScales(scale, maxScale = 3) {
|
||||
if (!scale) {
|
||||
return '';
|
||||
}
|
||||
const match = scale?.match(/^([2-9]\d*)x$/);
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
const parsedScale = parseInt(match[1], 10);
|
||||
if (parsedScale <= maxScale) {
|
||||
return `@${parsedScale}x`;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces local:// URLs with public http(s):// URLs.
|
||||
* @param {object} req - Express request object.
|
||||
* @param {string} url - The URL string to fix.
|
||||
* @param {string} publicUrl - The public URL prefix to use for replacements.
|
||||
* @returns {string} - The fixed URL string.
|
||||
*/
|
||||
export function fixUrl(req, url, publicUrl) {
|
||||
if (!url || typeof url !== 'string' || url.indexOf('local://') !== 0) {
|
||||
|
@ -40,12 +88,11 @@ export function fixUrl(req, url, publicUrl) {
|
|||
}
|
||||
|
||||
/**
|
||||
* Generate new URL object
|
||||
* @param req
|
||||
* @params {object} req - Express request
|
||||
* @returns {URL} object
|
||||
* Generates a new URL object from the Express request.
|
||||
* @param {object} req - Express request object.
|
||||
* @returns {URL} - URL object with correct host and optionally path.
|
||||
*/
|
||||
const getUrlObject = (req) => {
|
||||
function getUrlObject(req) {
|
||||
const urlObject = new URL(`${req.protocol}://${req.headers.host}/`);
|
||||
// support overriding hostname by sending X-Forwarded-Host http header
|
||||
urlObject.hostname = req.hostname;
|
||||
|
@ -62,16 +109,33 @@ const getUrlObject = (req) => {
|
|||
urlObject.pathname = path.posix.join(xForwardedPath, urlObject.pathname);
|
||||
}
|
||||
return urlObject;
|
||||
};
|
||||
}
|
||||
|
||||
export const getPublicUrl = (publicUrl, req) => {
|
||||
/**
|
||||
* Gets the public URL, either from a provided publicUrl or generated from the request.
|
||||
* @param {string} publicUrl - The optional public URL to use.
|
||||
* @param {object} req - The Express request object.
|
||||
* @returns {string} - The final public URL string.
|
||||
*/
|
||||
export function getPublicUrl(publicUrl, req) {
|
||||
if (publicUrl) {
|
||||
return publicUrl;
|
||||
}
|
||||
return getUrlObject(req).toString();
|
||||
};
|
||||
}
|
||||
|
||||
export const getTileUrls = (
|
||||
/**
|
||||
* Generates an array of tile URLs based on given parameters.
|
||||
* @param {object} req - Express request object.
|
||||
* @param {string | string[]} domains - Domain(s) to use for tile URLs.
|
||||
* @param {string} path - The base path for the tiles.
|
||||
* @param {number} [tileSize] - The size of the tile (optional).
|
||||
* @param {string} format - The format of the tiles (e.g., 'png', 'jpg').
|
||||
* @param {string} publicUrl - The public URL to use (if not using domains).
|
||||
* @param {object} [aliases] - Aliases for format extensions.
|
||||
* @returns {string[]} An array of tile URL strings.
|
||||
*/
|
||||
export function getTileUrls(
|
||||
req,
|
||||
domains,
|
||||
path,
|
||||
|
@ -79,7 +143,7 @@ export const getTileUrls = (
|
|||
format,
|
||||
publicUrl,
|
||||
aliases,
|
||||
) => {
|
||||
) {
|
||||
const urlObject = getUrlObject(req);
|
||||
if (domains) {
|
||||
if (domains.constructor === String && domains.length > 0) {
|
||||
|
@ -144,9 +208,14 @@ export const getTileUrls = (
|
|||
}
|
||||
|
||||
return uris;
|
||||
};
|
||||
}
|
||||
|
||||
export const fixTileJSONCenter = (tileJSON) => {
|
||||
/**
|
||||
* Fixes the center in the tileJSON if no center is available.
|
||||
* @param {object} tileJSON - The tileJSON object to process.
|
||||
* @returns {void}
|
||||
*/
|
||||
export function fixTileJSONCenter(tileJSON) {
|
||||
if (tileJSON.bounds && !tileJSON.center) {
|
||||
const fitWidth = 1024;
|
||||
const tiles = fitWidth / 256;
|
||||
|
@ -159,19 +228,77 @@ export const fixTileJSONCenter = (tileJSON) => {
|
|||
),
|
||||
];
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const getFontPbf = (allowedFonts, fontPath, name, range, fallbacks) =>
|
||||
new Promise((resolve, reject) => {
|
||||
/**
|
||||
* Reads a file and returns a Promise with the file data.
|
||||
* @param {string} filename - Path to the file to read.
|
||||
* @returns {Promise<Buffer>} - A Promise that resolves with the file data as a Buffer or rejects with an error.
|
||||
*/
|
||||
export function readFile(filename) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const sanitizedFilename = path.normalize(filename); // Normalize path, remove ..
|
||||
// eslint-disable-next-line security/detect-non-literal-fs-filename
|
||||
fs.readFile(String(sanitizedFilename), (err, data) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(data);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves font data for a given font and range.
|
||||
* @param {object} allowedFonts - An object of allowed fonts.
|
||||
* @param {string} fontPath - The path to the font directory.
|
||||
* @param {string} name - The name of the font.
|
||||
* @param {string} range - The range (e.g., '0-255') of the font to load.
|
||||
* @param {object} [fallbacks] - Optional fallback font list.
|
||||
* @returns {Promise<Buffer>} A promise that resolves with the font data Buffer or rejects with an error.
|
||||
*/
|
||||
async function getFontPbf(allowedFonts, fontPath, name, range, fallbacks) {
|
||||
if (!allowedFonts || (allowedFonts[name] && fallbacks)) {
|
||||
const filename = path.join(fontPath, name, `${range}.pbf`);
|
||||
const fontMatch = name?.match(/^[\p{L}\p{N} \-\.~!*'()@&=+,#$\[\]]+$/u);
|
||||
const sanitizedName = fontMatch?.[0] || 'invalid';
|
||||
if (!name || typeof name !== 'string' || name.trim() === '' || !fontMatch) {
|
||||
console.error(
|
||||
'ERROR: Invalid font name: %s',
|
||||
sanitizedName.replace(/\n|\r/g, ''),
|
||||
);
|
||||
throw new Error('Invalid font name');
|
||||
}
|
||||
|
||||
const rangeMatch = range?.match(/^[\d-]+$/);
|
||||
const sanitizedRange = rangeMatch?.[0] || 'invalid';
|
||||
if (!/^\d+-\d+$/.test(range)) {
|
||||
console.error(
|
||||
'ERROR: Invalid range: %s',
|
||||
sanitizedRange.replace(/\n|\r/g, ''),
|
||||
);
|
||||
throw new Error('Invalid range');
|
||||
}
|
||||
const filename = path.join(
|
||||
fontPath,
|
||||
sanitizedName,
|
||||
`${sanitizedRange}.pbf`,
|
||||
);
|
||||
|
||||
if (!fallbacks) {
|
||||
fallbacks = clone(allowedFonts || {});
|
||||
}
|
||||
delete fallbacks[name];
|
||||
fs.readFile(filename, (err, data) => {
|
||||
if (err) {
|
||||
console.error(`ERROR: Font not found: ${name}`);
|
||||
|
||||
try {
|
||||
const data = await readFile(filename);
|
||||
return data;
|
||||
} catch (err) {
|
||||
console.error(
|
||||
'ERROR: Font not found: %s, Error: %s',
|
||||
filename.replace(/\n|\r/g, ''),
|
||||
String(err),
|
||||
);
|
||||
if (fallbacks && Object.keys(fallbacks).length) {
|
||||
let fallbackName;
|
||||
|
||||
|
@ -186,32 +313,37 @@ const getFontPbf = (allowedFonts, fontPath, name, range, fallbacks) =>
|
|||
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,
|
||||
console.error(
|
||||
`ERROR: Trying to use %s as a fallback for: %s`,
|
||||
fallbackName,
|
||||
sanitizedName,
|
||||
);
|
||||
delete fallbacks[fallbackName];
|
||||
return getFontPbf(null, fontPath, fallbackName, range, fallbacks);
|
||||
} else {
|
||||
reject(`Font load error: ${name}`);
|
||||
throw new Error('Font load error');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
resolve(data);
|
||||
throw new Error('Font not allowed');
|
||||
}
|
||||
});
|
||||
} else {
|
||||
reject(`Font not allowed: ${name}`);
|
||||
}
|
||||
});
|
||||
|
||||
export const getFontsPbf = async (
|
||||
/**
|
||||
* Combines multiple font pbf buffers into one.
|
||||
* @param {object} allowedFonts - An object of allowed fonts.
|
||||
* @param {string} fontPath - The path to the font directory.
|
||||
* @param {string} names - Comma-separated font names.
|
||||
* @param {string} range - The range of the font (e.g., '0-255').
|
||||
* @param {object} [fallbacks] - Fallback font list.
|
||||
* @returns {Promise<Buffer>} - A promise that resolves to the combined font data buffer.
|
||||
*/
|
||||
export async function getFontsPbf(
|
||||
allowedFonts,
|
||||
fontPath,
|
||||
names,
|
||||
range,
|
||||
fallbacks,
|
||||
) => {
|
||||
) {
|
||||
const fonts = names.split(',');
|
||||
const queue = [];
|
||||
for (const font of fonts) {
|
||||
|
@ -228,9 +360,14 @@ export const getFontsPbf = async (
|
|||
|
||||
const combined = combine(await Promise.all(queue), names);
|
||||
return Buffer.from(combined.buffer, 0, combined.buffer.length);
|
||||
};
|
||||
}
|
||||
|
||||
export const listFonts = async (fontPath) => {
|
||||
/**
|
||||
* Lists available fonts in a given font directory.
|
||||
* @param {string} fontPath - The path to the font directory.
|
||||
* @returns {Promise<object>} - Promise that resolves with an object where keys are the font names.
|
||||
*/
|
||||
export async function listFonts(fontPath) {
|
||||
const existingFonts = {};
|
||||
|
||||
const files = await fsPromises.readdir(fontPath);
|
||||
|
@ -245,9 +382,14 @@ export const listFonts = async (fontPath) => {
|
|||
}
|
||||
|
||||
return existingFonts;
|
||||
};
|
||||
}
|
||||
|
||||
export const isValidHttpUrl = (string) => {
|
||||
/**
|
||||
* Checks if a string is a valid HTTP or HTTPS URL.
|
||||
* @param {string} string - The string to validate.
|
||||
* @returns {boolean} True if the string is a valid HTTP/HTTPS URL, false otherwise.
|
||||
*/
|
||||
export function isValidHttpUrl(string) {
|
||||
let url;
|
||||
|
||||
try {
|
||||
|
@ -257,4 +399,32 @@ export const isValidHttpUrl = (string) => {
|
|||
}
|
||||
|
||||
return url.protocol === 'http:' || url.protocol === 'https:';
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches tile data from either PMTiles or MBTiles source.
|
||||
* @param {object} source - The source object, which may contain a mbtiles object, or pmtiles object.
|
||||
* @param {string} sourceType - The source type, which should be `pmtiles` or `mbtiles`
|
||||
* @param {number} z - The zoom level.
|
||||
* @param {number} x - The x coordinate of the tile.
|
||||
* @param {number} y - The y coordinate of the tile.
|
||||
* @returns {Promise<object | null>} - A promise that resolves to an object with data and headers or null if no data is found.
|
||||
*/
|
||||
export async function fetchTileData(source, sourceType, z, x, y) {
|
||||
if (sourceType === 'pmtiles') {
|
||||
return await new Promise(async (resolve) => {
|
||||
const tileinfo = await getPMtilesTile(source, z, x, y);
|
||||
if (!tileinfo?.data) return resolve(null);
|
||||
resolve({ data: tileinfo.data, headers: tileinfo.header });
|
||||
});
|
||||
} else if (sourceType === 'mbtiles') {
|
||||
return await new Promise((resolve) => {
|
||||
source.getTile(z, x, y, (err, tileData, tileHeader) => {
|
||||
if (err) {
|
||||
return resolve(null);
|
||||
}
|
||||
resolve({ data: tileData, headers: tileHeader });
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,10 +7,10 @@ import { server } from '../src/server.js';
|
|||
global.expect = expect;
|
||||
global.supertest = supertest;
|
||||
|
||||
before(function () {
|
||||
before(async function () {
|
||||
console.log('global setup');
|
||||
process.chdir('test_data');
|
||||
const running = server({
|
||||
const running = await server({
|
||||
configPath: 'config.json',
|
||||
port: 8888,
|
||||
publicUrl: '/test/',
|
||||
|
|
|
@ -78,7 +78,7 @@ describe('Static endpoints', function () {
|
|||
testStatic(prefix, '0,0,0/256x256', 'png', 404, 1);
|
||||
|
||||
testStatic(prefix, '0,0,-1/256x256', 'png', 404);
|
||||
testStatic(prefix, '0,0,0/256.5x256.5', 'png', 404);
|
||||
testStatic(prefix, '0,0,0/256.5x256.5', 'png', 400);
|
||||
|
||||
testStatic(prefix, '0,0,0,/256x256', 'png', 404);
|
||||
testStatic(prefix, '0,0,0,0,/256x256', 'png', 404);
|
||||
|
@ -135,7 +135,7 @@ describe('Static endpoints', function () {
|
|||
|
||||
testStatic(prefix, '0,0,1,1/1x1', 'gif', 400);
|
||||
|
||||
testStatic(prefix, '-180,-80,180,80/0.5x2.6', 'png', 404);
|
||||
testStatic(prefix, '-180,-80,180,80/0.5x2.6', 'png', 400);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -60,16 +60,16 @@ describe('Raster tiles', function () {
|
|||
|
||||
describe('invalid requests return 4xx', function () {
|
||||
testTile('non_existent', 256, 0, 0, 0, 'png', 404);
|
||||
testTile(prefix, 256, -1, 0, 0, 'png', 404);
|
||||
testTile(prefix, 256, 25, 0, 0, 'png', 404);
|
||||
testTile(prefix, 256, 0, 1, 0, 'png', 404);
|
||||
testTile(prefix, 256, 0, 0, 1, 'png', 404);
|
||||
testTile(prefix, 256, -1, 0, 0, 'png', 400);
|
||||
testTile(prefix, 256, 25, 0, 0, 'png', 400);
|
||||
testTile(prefix, 256, 0, 1, 0, 'png', 400);
|
||||
testTile(prefix, 256, 0, 0, 1, 'png', 400);
|
||||
testTile(prefix, 256, 0, 0, 0, 'gif', 400);
|
||||
testTile(prefix, 256, 0, 0, 0, 'pbf', 400);
|
||||
|
||||
testTile(prefix, 256, 0, 0, 0, 'png', 404, 1);
|
||||
testTile(prefix, 256, 0, 0, 0, 'png', 404, 5);
|
||||
testTile(prefix, 256, 0, 0, 0, 'png', 400, 1);
|
||||
testTile(prefix, 256, 0, 0, 0, 'png', 400, 5);
|
||||
|
||||
testTile(prefix, 300, 0, 0, 0, 'png', 404);
|
||||
testTile(prefix, 300, 0, 0, 0, 'png', 400);
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue