Compare commits

..

1 commit

Author SHA1 Message Date
dependabot[bot]
839f4190e5
chore(deps-dev): bump eslint-plugin-security from 1.7.1 to 3.0.1
Bumps [eslint-plugin-security](https://github.com/eslint-community/eslint-plugin-security) from 1.7.1 to 3.0.1.
- [Release notes](https://github.com/eslint-community/eslint-plugin-security/releases)
- [Changelog](https://github.com/eslint-community/eslint-plugin-security/blob/main/CHANGELOG.md)
- [Commits](https://github.com/eslint-community/eslint-plugin-security/compare/v1.7.1...v3.0.1)

---
updated-dependencies:
- dependency-name: eslint-plugin-security
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-12 23:07:30 +00:00
52 changed files with 6926 additions and 7248 deletions

View file

@ -5,4 +5,3 @@
!package.json !package.json
!package-lock.json !package-lock.json
!docker-entrypoint.sh !docker-entrypoint.sh
**.gitignore

View file

@ -14,4 +14,3 @@ jobs:
- uses: fastify/github-action-merge-dependabot@v3 - uses: fastify/github-action-merge-dependabot@v3
with: with:
github-token: ${{ secrets.GITHUB_TOKEN }} github-token: ${{ secrets.GITHUB_TOKEN }}
target: minor

View file

@ -21,11 +21,6 @@ jobs:
with: with:
ref: ${{ github.event.pull_request.head.sha }} 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 📦 - name: Setup node env 📦
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:

View file

@ -65,22 +65,6 @@ jobs:
context: . context: .
push: false push: false
platforms: linux/arm64,linux/amd64 platforms: linux/arm64,linux/amd64
cache-from: type=gha # experimental: https://github.com/docker/build-push-action/blob/master/docs/advanced/cache.md#cache-backend-api
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-from: type=gha
cache-to: type=gha,mode=max cache-to: type=gha,mode=max

View file

@ -14,41 +14,9 @@ on:
required: true required: true
jobs: jobs:
release-check:
name: Check if version is published
runs-on: ubuntu-latest
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: 'package.json'
check-latest: true
cache: 'npm'
- name: Check if version is published
id: check
run: |
currentVersion="$( node -e "console.log(require('./package.json').version)" )"
isPublished="$( npm view tileserver-gl versions --json | jq -c --arg cv "$currentVersion" 'any(. == $cv)' )"
echo "version=$currentVersion" >> "$GITHUB_OUTPUT"
echo "published=$isPublished" >> "$GITHUB_OUTPUT"
echo "currentVersion: $currentVersion"
echo "isPublished: $isPublished"
outputs:
published: ${{ steps.check.outputs.published }}
version: ${{ steps.check.outputs.version }}
release: release:
needs: release-check
if: ${{ needs.release-check.outputs.published == 'false' }}
name: 'Build, Test, Publish' name: 'Build, Test, Publish'
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
env:
PACKAGE_VERSION: ${{ needs.release-check.outputs.version }}
steps: steps:
- name: Check out repository ✨ - name: Check out repository ✨
uses: actions/checkout@v4 uses: actions/checkout@v4
@ -86,23 +54,17 @@ jobs:
- name: Remove Test Data - name: Remove Test Data
run: rm -R test_data* run: rm -R test_data*
- name: Get release type - name: Publish to Full Version NPM
id: prepare_release
run: |
RELEASE_TYPE="$(node -e "console.log(require('semver').prerelease('${{ needs.release-check.outputs.version }}') ? 'prerelease' : 'regular')")"
if [[ $RELEASE_TYPE == 'regular' ]]; then
echo "prerelease=false" >> "$GITHUB_OUTPUT"
else
echo "prerelease=true" >> "$GITHUB_OUTPUT"
fi
- name: Publish to NPM
run: | run: |
npm config set //registry.npmjs.org/:_authToken ${NPM_TOKEN} npm config set //registry.npmjs.org/:_authToken ${NPM_TOKEN}
npm publish --access public --tag ${{ steps.prepare_release.outputs.prerelease == 'true' && 'next' || 'latest' }} npm publish --access public
env: env:
NPM_TOKEN: ${{ github.event.inputs.npm_token }} NPM_TOKEN: ${{ github.event.inputs.npm_token }}
- name: Get version
run: |
echo "PACKAGE_VERSION=$(grep '"version"' package.json | cut -d '"' -f 4 | head -n 1)" >> $GITHUB_ENV
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v3 uses: docker/setup-qemu-action@v3
with: with:
@ -122,42 +84,24 @@ jobs:
with: with:
context: . context: .
push: true push: true
tags: | tags: maptiler/tileserver-gl:latest, maptiler/tileserver-gl:v${{ env.PACKAGE_VERSION }}
maptiler/tileserver-gl:${{ steps.prepare_release.outputs.prerelease == 'true' && 'next' || 'latest' }},
maptiler/tileserver-gl:v${{ env.PACKAGE_VERSION }}
platforms: linux/arm64,linux/amd64 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-from: type=gha
cache-to: type=gha,mode=max cache-to: type=gha,mode=max
- name: Extract changelog for version
run: |
awk '/^##/ { p = 0 }; p == 1 { print }; $0 == "## ${{ env.PACKAGE_VERSION }}" { p = 1 };' CHANGELOG.md > changelog_for_version.md
cat changelog_for_version.md
- name: Publish to Github
uses: ncipollo/release-action@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag: v${{ env.PACKAGE_VERSION }}
name: v${{ env.PACKAGE_VERSION }}
bodyFile: changelog_for_version.md
allowUpdates: true
draft: false
prerelease: ${{ steps.prepare_release.outputs.prerelease }}
- name: Create Tileserver Light Directory - name: Create Tileserver Light Directory
run: node publish.js --no-publish run: node publish.js --no-publish
- name: Install node dependencies - name: Install node dependencies
run: npm ci --prefer-offline --no-audit run: npm install
working-directory: ./light working-directory: ./light
- name: Publish to Light Version NPM - name: Publish to Light Version NPM
working-directory: ./light working-directory: ./light
run: | run: |
npm config set //registry.npmjs.org/:_authToken ${NPM_TOKEN} npm config set //registry.npmjs.org/:_authToken ${NPM_TOKEN}
npm publish --access public --tag ${{ steps.prepare_release.outputs.prerelease == 'true' && 'next' || 'latest' }} npm publish --access public
env: env:
NPM_TOKEN: ${{ github.event.inputs.npm_token }} NPM_TOKEN: ${{ github.event.inputs.npm_token }}
@ -167,9 +111,8 @@ jobs:
context: ./light context: ./light
file: ./light/Dockerfile file: ./light/Dockerfile
push: true push: true
tags: | tags: maptiler/tileserver-gl-light:latest, maptiler/tileserver-gl-light:v${{ env.PACKAGE_VERSION }}
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 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-from: type=gha
cache-to: type=gha,mode=max cache-to: type=gha,mode=max

14
.gitignore vendored
View file

@ -1,22 +1,8 @@
docs/_build 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 node_modules
test_data test_data
test_data.zip
data data
light light
plugins plugins
config.json config.json
*.mbtiles *.mbtiles
styles
fonts

18
.husky/commit-msg Executable file
View file

@ -0,0 +1,18 @@
NAME=$(git config user.name)
EMAIL=$(git config user.email)
if [ -z "$NAME" ]; then
echo "empty git config user.name"
exit 1
fi
if [ -z "$EMAIL" ]; then
echo "empty git config user.email"
exit 1
fi
git interpret-trailers --if-exists doNothing --trailer \
"Signed-off-by: $NAME <$EMAIL>" \
--in-place "$1"
npm exec --no -- commitlint --edit $1

1
.husky/pre-push Executable file
View file

@ -0,0 +1 @@
npm exec --no -- lint-staged --no-stash

View file

@ -1,32 +0,0 @@
# tileserver-gl changelog
## 5.2.0
* Use npm packages for public/resources (https://github.com/maptiler/tileserver-gl/pull/1427) by @okimiko
* use ttf files of googlefonts/opensans (https://github.com/maptiler/tileserver-gl/pull/1447) by @okimiko
* Limit Elevation Lat/Long Output Length (https://github.com/maptiler/tileserver-gl/pull/1457) by @okimiko
* Fetch style from url (https://github.com/maptiler/tileserver-gl/pull/1462) by @YoelRidgway
* fix: memory leak on SIGHUP (https://github.com/maptiler/tileserver-gl/pull/1455) by @okimiko
* fix: resolves Unimplemented type: 3 error for geojson format (https://github.com/maptiler/tileserver-gl/pull/1465) by @rjdjohnston
* fix: Test light version in ct workflow - fix sqlite build in light (https://github.com/maptiler/tileserver-gl/pull/1477) by @acalcutt
* fix: light version docker entrypoint permissions (https://github.com/maptiler/tileserver-gl/pull/1478) by @acalcutt
## 5.1.3
* Fix SIGHUP (broken since 5.1.x) (https://github.com/maptiler/tileserver-gl/pull/1452) by @okimiko
## 5.1.2
* Fix broken light (invalid use of heavy dependencies) (https://github.com/maptiler/tileserver-gl/pull/1449) by @okimiko
## 5.1.1
* Fix wrong node version in Docker image (https://github.com/maptiler/tileserver-gl/pull/1442) by @acalcutt
## 5.1.0
* Update recommended node to v22 + Update docker images to use node 22 (https://github.com/maptiler/tileserver-gl/pull/1438) by @acalcutt
* Upgrade Express to v5 + Canvas to v3 + code cleanup (https://github.com/maptiler/tileserver-gl/pull/1429) by @acalcutt
* Terrain Preview and simple Elevation Query (https://github.com/maptiler/tileserver-gl/pull/1425 and https://github.com/maptiler/tileserver-gl/pull/1432) by @okimiko
* add progressive rendering option for static jpeg images (#1397) by @samuel-git
## 5.0.0
* Update Maplibre-Native to [v6.0.0](https://github.com/maplibre/maplibre-native/releases/tag/node-v6.0.0) release by @acalcutt in https://github.com/maptiler/tileserver-gl/pull/1376 and @dependabot in https://github.com/maptiler/tileserver-gl/pull/1381
* This first release that use Metal for rendering instead of OpenGL (ES) for macOS.
* This the first release that uses OpenGL (ES) 3.0 on Windows and Linux
* Note: Windows users may need to update their [c++ redistributable ](https://learn.microsoft.com/en-us/cpp/windows/latest-supported-vc-redist?view=msvc-170) for maplibre-native v6.0.0

View file

@ -2,11 +2,10 @@ FROM ubuntu:jammy AS builder
ENV NODE_ENV="production" ENV NODE_ENV="production"
SHELL ["/bin/bash", "-o", "pipefail", "-c"] RUN set -ex; \
export DEBIAN_FRONTEND=noninteractive; \
RUN export DEBIAN_FRONTEND=noninteractive && \ apt-get -qq update; \
apt-get update && \ apt-get -y --no-install-recommends install \
apt-get install -y --no-install-recommends --no-install-suggests \
build-essential \ build-essential \
ca-certificates \ ca-certificates \
curl \ curl \
@ -27,17 +26,23 @@ RUN export DEBIAN_FRONTEND=noninteractive && \
librsvg2-common \ librsvg2-common \
libcurl4-openssl-dev \ libcurl4-openssl-dev \
libpixman-1-dev \ libpixman-1-dev \
libpixman-1-0 && \ libpixman-1-0; \
mkdir -p /etc/apt/keyrings && \ apt-get -y --purge autoremove; \
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg && \ apt-get clean; \
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 && \ rm -rf /var/lib/apt/lists/*;
apt-get -qq update && \
apt-get install -y --no-install-recommends --no-install-suggests nodejs && \ SHELL ["/bin/bash", "-o", "pipefail", "-c"]
npm i -g npm@latest && \
apt-get -y remove curl gnupg && \ RUN mkdir -p /etc/apt/keyrings; \
apt-get -y --purge autoremove && \ curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg; \
apt-get clean && \ 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; \
rm -rf /var/lib/apt/lists/* 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/*;
RUN mkdir -p /usr/src/app RUN mkdir -p /usr/src/app
@ -46,12 +51,12 @@ WORKDIR /usr/src/app
COPY package.json /usr/src/app COPY package.json /usr/src/app
COPY package-lock.json /usr/src/app COPY package-lock.json /usr/src/app
RUN npm config set maxsockets 1 && \ RUN npm config set maxsockets 1; \
npm config set fetch-retries 5 && \ npm config set fetch-retries 5; \
npm config set fetch-retry-mintimeout 100000 && \ npm config set fetch-retry-mintimeout 100000; \
npm config set fetch-retry-maxtimeout 600000 && \ npm config set fetch-retry-maxtimeout 600000; \
npm ci --omit=dev && \ npm ci --omit=dev; \
chown -R root:root /usr/src/app chown -R root:root /usr/src/app;
FROM ubuntu:jammy AS final FROM ubuntu:jammy AS final
@ -60,13 +65,12 @@ ENV \
CHOKIDAR_USEPOLLING=1 \ CHOKIDAR_USEPOLLING=1 \
CHOKIDAR_INTERVAL=500 CHOKIDAR_INTERVAL=500
SHELL ["/bin/bash", "-o", "pipefail", "-c"] RUN set -ex; \
export DEBIAN_FRONTEND=noninteractive; \
RUN export DEBIAN_FRONTEND=noninteractive && \ groupadd -r node; \
groupadd -r node && \ useradd -r -g node node; \
useradd -r -g node node && \ apt-get -qq update; \
apt-get -qq update && \ apt-get -y --no-install-recommends install \
apt-get install -y --no-install-recommends --no-install-suggests \
ca-certificates \ ca-certificates \
curl \ curl \
gnupg \ gnupg \
@ -81,17 +85,23 @@ RUN export DEBIAN_FRONTEND=noninteractive && \
libpixman-1-0 \ libpixman-1-0 \
libcurl4 \ libcurl4 \
librsvg2-2 \ librsvg2-2 \
libpango-1.0-0 && \ libpango-1.0-0; \
mkdir -p /etc/apt/keyrings && \ apt-get -y --purge autoremove; \
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg && \ apt-get clean; \
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 && \ rm -rf /var/lib/apt/lists/*;
apt-get -qq update && \
apt-get install -y --no-install-recommends --no-install-suggests nodejs && \ SHELL ["/bin/bash", "-o", "pipefail", "-c"]
npm i -g npm@latest && \
apt-get -y remove curl gnupg && \ RUN mkdir -p /etc/apt/keyrings; \
apt-get -y --purge autoremove && \ curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg; \
apt-get clean && \ 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; \
rm -rf /var/lib/apt/lists/* 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/*;
COPY --from=builder /usr/src/app /usr/src/app COPY --from=builder /usr/src/app /usr/src/app

View file

@ -1,38 +0,0 @@
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/*

View file

@ -1,82 +1,51 @@
FROM ubuntu:jammy AS builder FROM ubuntu:jammy
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 \ ENV \
NODE_ENV="production" \ NODE_ENV="production" \
CHOKIDAR_USEPOLLING=1 \ CHOKIDAR_USEPOLLING=1 \
CHOKIDAR_INTERVAL=500 CHOKIDAR_INTERVAL=500
SHELL ["/bin/bash", "-o", "pipefail", "-c"] RUN set -ex; \
export DEBIAN_FRONTEND=noninteractive; \
RUN export DEBIAN_FRONTEND=noninteractive && \ groupadd -r node; \
groupadd -r node && \ useradd -r -g node node; \
useradd -r -g node node && \ apt-get -qq update; \
apt-get -qq update && \ apt-get -y --no-install-recommends install \
apt-get install -y --no-install-recommends --no-install-suggests \
ca-certificates \ ca-certificates \
curl \ curl \
gnupg && \ gnupg; \
mkdir -p /etc/apt/keyrings && \ mkdir -p /etc/apt/keyrings; \
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg && \ 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 && \ 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 -qq update; \
apt-get install -y --no-install-recommends --no-install-suggests nodejs && \ apt-get install -y nodejs; \
npm i -g npm@latest && \ npm i -g npm@latest; \
apt-get -y remove curl gnupg && \ apt-get -y remove curl gnupg; \
apt-get -y --purge autoremove && \ apt-get -y --purge autoremove; \
apt-get clean && \ apt-get clean; \
rm -rf /var/lib/apt/lists/* rm -rf /var/lib/apt/lists/*;
COPY --from=builder /usr/src/app /usr/src/app
COPY . /usr/src/app
RUN mkdir -p /data && \
chown node:node /data && \
chmod +x /usr/src/app/docker-entrypoint.sh
VOLUME /data
WORKDIR /data
EXPOSE 8080 EXPOSE 8080
RUN mkdir -p /data; \
chown node:node /data; \
mkdir -p /usr/src/app;
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;
USER node:node USER node:node
ENTRYPOINT ["/usr/src/app/docker-entrypoint.sh"] ENTRYPOINT ["/usr/src/app/docker-entrypoint.sh"]

View file

@ -13,8 +13,7 @@ RUN set -ex; \
unzip \ unzip \
build-essential \ build-essential \
ca-certificates \ ca-certificates \
curl \ wget \
gnupg \
pkg-config \ pkg-config \
xvfb \ xvfb \
libglfw3-dev \ libglfw3-dev \
@ -26,33 +25,16 @@ RUN set -ex; \
libjpeg-dev \ libjpeg-dev \
libgif-dev \ libgif-dev \
librsvg2-dev \ librsvg2-dev \
gir1.2-rsvg-2.0 \
librsvg2-2 \
librsvg2-common \
libcurl4-openssl-dev \ libcurl4-openssl-dev \
libpixman-1-dev \ libpixman-1-dev; \
libpixman-1-0; \ wget -qO- https://deb.nodesource.com/setup_18.x | bash; \
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 install -y nodejs; \
npm i -g npm@latest; \ apt-get clean;
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 RUN mkdir -p /usr/src/app
WORKDIR /usr/src/app WORKDIR /usr/src/app
RUN curl -L -o test_data.zip https://github.com/maptiler/tileserver-gl/releases/download/v1.3.0/test_data.zip; \ RUN wget -O test_data.zip https://github.com/maptiler/tileserver-gl/releases/download/v1.3.0/test_data.zip; \
unzip -q test_data.zip -d test_data unzip -q test_data.zip -d test_data
COPY package.json . COPY package.json .

View file

@ -1,112 +1,13 @@
# Publishing a New Version # Publishing new version
This document outlines the process for publishing new versions of this project. We use a GitHub workflow for automated releases, but also provide manual steps for specific situations. - Update version in `package.json`
- `git tag vx.x.x`
## Automated Publishing via GitHub Workflow (Recommended) - `git push --tags`
- `docker buildx build --platform linux/amd64 -t maptiler/tileserver-gl:latest -t maptiler/tileserver-gl:[version] .`
This is the preferred method for publishing new versions. It automates the entire process, including version bumping, tagging, building, and publishing. - `docker push maptiler/tileserver-gl --all-tags`
- `npm publish --access public` or `node publish.js`
1. **Prepare the Release:** - `node publish.js --no-publish`
- `cd light`
* **Choose the Version Increment:** Determine the appropriate semantic versioning increment (prerelease, prepatch, preminor, premajor, patch, minor, major). - `docker buildx build --platform linux/amd64 -t maptiler/tileserver-gl-light:latest -t maptiler/tileserver-gl-light:[version] .`
- `docker push maptiler/tileserver-gl-light --all-tags`
* **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. - `npm publish --access public`
```bash
# Example: Increment to a new patch version
npm version patch --no-git-tag-version
# Example: Increment to a new minor version
npm version minor --no-git-tag-version
# Example: Increment to a new major version
npm version major --no-git-tag-version
# Example: Create a pre-release (e.g., -pre.0) version
npm version prepatch --preid pre --no-git-tag-version
# OR
npm version preminor --preid pre --no-git-tag-version
# OR
npm version premajor --preid pre --no-git-tag-version
# Example: Increment an existing pre-release version (e.g., -pre.0 to -pre.1)
npm version prerelease --preid pre --no-git-tag-version
```
* `--no-git-tag-version`: This prevents `npm version` from automatically creating a git tag, as the GitHub workflow will handle this later.
* `--preid pre`: Specifies that "pre" is the pre-release identifier.
* **Update the Changelog (`CHANGELOG.md`):** Add a new section for the release. The heading *must* exactly match the format `## <VERSION>`, where `<VERSION>` is the full version number from `package.json`. Describe the changes included in the release.
```markdown
## 1.2.3
* Added new feature X.
```
2. **Commit and Push:** Commit the changes to `package.json`, `package-lock.json` and `CHANGELOG.md` to a the `master` branch. Create a pull request (PR) for review. If you are an administrator, you may push directly, but a PR is generally recommended for code review.
3. **Run the GitHub Workflow:** Once the PR is merged (or changes pushed directly), trigger the "Build, Test, Release" GitHub workflow. This workflow is responsible for:
* Building the project.
* Running tests.
* Creating an NPM package.
* Building and pushing Docker images.
* Creating a GitHub Release.
* Creating a Git tag for the release.
## Manual Publishing (For Special Cases)
Use the following steps *only* when the automated workflow is not suitable (e.g., for debugging, specific environment needs).
1. **Update Version in `package.json`:** Modify the `version` field in `package.json` to the desired version number.
2. **Create and Push Git Tag:**
```bash
git tag vX.X.X # Replace X.X.X with the version number
git push origin --tags
```
3. **NPM Publish Options (Choose ONE):**
* **Option A: Publish only the full `tileserver-gl` version:**
```bash
npm publish --access public
```
* **Option B: Build and Publish both `tileserver-gl` and `tileserver-gl-light` using `publish.js`:**
```bash
# This script builds the light version and publishes both tileserver-gl and tileserver-gl-light to NPM.
node publish.js
```
* **Option C: Build only the `tileserver-gl-light` version (no publish):**
```bash
# This script ONLY builds the light version (e.g., for local testing or Docker image creation) without publishing.
node publish.js --no-publish
```
4. **Build and Push Docker Images:**
```bash
# Build the main image
docker buildx build --platform linux/amd64 -t maptiler/tileserver-gl:latest -t maptiler/tileserver-gl:X.X.X . # Replace X.X.X
docker push maptiler/tileserver-gl --all-tags
# Build the light image
cd light
docker buildx build --platform linux/amd64 -t maptiler/tileserver-gl-light:latest -t maptiler/tileserver-gl-light:X.X.X . # Replace X.X.X
docker push maptiler/tileserver-gl-light --all-tags
cd .. # Return to the project root
```
* Make sure you are logged into the docker registry before pushing. `docker login`
**Important Considerations for Manual Publishing:**
* **Consistency:** Ensure the version number in `package.json`, the Git tag, and the Docker image tags are identical.
* **Credentials:** Verify that you have the necessary permissions to push Docker images and publish to NPM.
* **Cleanliness:** Before building Docker images, ensure your working directory is clean to avoid including unwanted files in the image.
* **Error Handling:** Manually publishing is more prone to errors. Double-check each step to avoid issues.

View file

@ -1,32 +1,5 @@
![tileserver-gl](https://cloud.githubusercontent.com/assets/59284/18173467/fa3aa2ca-7069-11e6-86b1-0f1266befeb6.jpeg) ![tileserver-gl](https://cloud.githubusercontent.com/assets/59284/18173467/fa3aa2ca-7069-11e6-86b1-0f1266befeb6.jpeg)
# 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 # TileServer GL
[![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/maptiler/tileserver-gl/pipeline.yml)](https://github.com/maptiler/tileserver-gl/actions/workflows/pipeline.yml) [![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/maptiler/tileserver-gl/pipeline.yml)](https://github.com/maptiler/tileserver-gl/actions/workflows/pipeline.yml)
[![Docker Hub](https://img.shields.io/badge/docker-hub-blue.svg)](https://hub.docker.com/r/maptiler/tileserver-gl/) [![Docker Hub](https://img.shields.io/badge/docker-hub-blue.svg)](https://hub.docker.com/r/maptiler/tileserver-gl/)
@ -36,7 +9,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/). Download vector tiles from [OpenMapTiles](https://data.maptiler.com/downloads/planet/).
## Getting Started with Node ## Getting Started with Node
Make sure you have Node.js version **18.17.0** or above installed. Node 22 is recommended. (running `node -v` it should output something like `v22.x.x`). Running without docker requires [Native dependencies](https://maptiler-tileserver.readthedocs.io/en/latest/installation.html#npm) to be installed first. 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.
Install `tileserver-gl` with server-side raster rendering of vector tiles with npm. Install `tileserver-gl` with server-side raster rendering of vector tiles with npm.
@ -70,7 +43,7 @@ An alternative to npm to start the packed software easier is to install [Docker]
Example using a mbtiles file Example using a mbtiles file
```bash ```bash
wget https://github.com/maptiler/tileserver-gl/releases/download/v1.3.0/zurich_switzerland.mbtiles wget https://github.com/maptiler/tileserver-gl/releases/download/v1.3.0/zurich_switzerland.mbtiles
docker run --rm -it -v $(pwd):/data -p 8080:8080 maptiler/tileserver-gl:latest --file zurich_switzerland.mbtiles docker run --rm -it -v $(pwd):/data -p 8080:8080 maptiler/tileserver-gl --file zurich_switzerland.mbtiles
[in your browser, visit http://[server ip]:8080] [in your browser, visit http://[server ip]:8080]
``` ```
@ -78,18 +51,18 @@ Example using a config.json + style + mbtiles file
```bash ```bash
wget https://github.com/maptiler/tileserver-gl/releases/download/v1.3.0/test_data.zip wget https://github.com/maptiler/tileserver-gl/releases/download/v1.3.0/test_data.zip
unzip test_data.zip unzip test_data.zip
docker run --rm -it -v $(pwd):/data -p 8080:8080 maptiler/tileserver-gl:latest docker run --rm -it -v $(pwd):/data -p 8080:8080 maptiler/tileserver-gl
[in your browser, visit http://[server ip]:8080] [in your browser, visit http://[server ip]:8080]
``` ```
Example using a different path Example using a different path
```bash ```bash
docker run --rm -it -v /your/local/config/path:/data -p 8080:8080 maptiler/tileserver-gl:latest docker run --rm -it -v /your/local/config/path:/data -p 8080:8080 maptiler/tileserver-gl
``` ```
replace '/your/local/config/path' with the path to your config file replace '/your/local/config/path' with the path to your config file
Alternatively, you can use the `maptiler/tileserver-gl-light:latest` docker image instead, which is pure javascript, does not have any native dependencies, and can run anywhere, but does not contain rasterization on the server side made with Maplibre GL Native. Alternatively, you can use the `maptiler/tileserver-gl-light` docker image instead, which is pure javascript, does not have any native dependencies, and can run anywhere, but does not contain rasterization on the server side made with Maplibre GL Native.
## Getting Started with Linux cli ## Getting Started with Linux cli

View file

@ -17,20 +17,15 @@ Example:
"icons": "icons", "icons": "icons",
"styles": "styles", "styles": "styles",
"mbtiles": "data", "mbtiles": "data",
"pmtiles": "data", "pmtiles": "data"
"files": "files"
}, },
"domains": [ "domains": [
"localhost:8080", "localhost:8080",
"127.0.0.1:8080" "127.0.0.1:8080"
], ],
"formatOptions": { "formatQuality": {
"jpeg": { "jpeg": 80,
"quality": 80 "webp": 90
},
"webp": {
"quality": 90
}
}, },
"maxScaleFactor": 3, "maxScaleFactor": 3,
"maxSize": 2048, "maxSize": 2048,
@ -57,9 +52,6 @@ Example:
"tilejson": { "tilejson": {
"format": "webp" "format": "webp"
} }
},
"remote": {
"style": "https://demotiles.maplibre.org/style.json"
} }
}, },
"data": { "data": {
@ -93,24 +85,10 @@ Path to the html (relative to ``root`` path) to use as a front page.
Use ``true`` (or nothing) to serve the default TileServer GL front page with list of styles and data. Use ``true`` (or nothing) to serve the default TileServer GL front page with list of styles and data.
Use ``false`` to disable the front page altogether (404). Use ``false`` to disable the front page altogether (404).
``formatOptions`` ``formatQuality``
----------------- -----------------
You can use this to specify options for the generation of images in the supported file formats. Quality of the compression of individual image formats. [0-100]
For WebP, the only supported option is ``quality`` [0-100].
For JPEG, the only supported options are ``quality`` [0-100] and ``progressive`` [true, false].
For PNG, the full set of options `exposed by the sharp library <https://sharp.pixelplumbing.com/api-output#png>`_ is available, except ``force`` and ``colours`` (use ``colors``). If not set, their values are the defaults from ``sharp``.
For example::
"formatOptions": {
"png": {
"palette": true,
"colors": 4
}
}
Note: ``formatOptions`` replaced the ``formatQuality`` option in previous versions of TileServer GL.
``maxScaleFactor`` ``maxScaleFactor``
----------- -----------
@ -212,7 +190,7 @@ Not used by default.
Each item in this object defines one style (map). It can have the following options: Each item in this object defines one style (map). It can have the following options:
* ``style`` -- name of the style json file or url of a remote hosted style [required] * ``style`` -- name of the style json file [required]
* ``serve_rendered`` -- whether to render the raster tiles for this style or not * ``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 * ``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 * ``tilejson`` -- properties to add to the TileJSON created for the raster data
@ -241,26 +219,9 @@ For example::
} }
} }
The data source does not need to be specified here unless you explicitly want to serve the raw data. The data source does not need to be specified here unless you explicitly want to serve the raw data.
Serving Terrain Tiles
--------------
If you serve terrain tiles, it is possible to configure an ``encoding`` with ``mapbox`` or ``terrarium`` to enable a terrain preview mode and the ``elevation`` api for the ``data`` endpoint.
For example::
"data": {
"terrain1": {
"mbtiles": "terrain1.mbtiles",
"encoding": "mapbox"
},
"terrain2": {
"pmtiles": "terrain2.pmtiles"
"encoding": "terrarium"
}
}
Referencing local files from style JSON Referencing local files from style JSON
======================================= =======================================
@ -303,7 +264,7 @@ For example::
"source3": { "source3": {
"url": "pmtiles://https://foo.lan/source3.pmtiles", "url": "pmtiles://https://foo.lan/source3.pmtiles",
"type": "vector" "type": "vector"
} },
} }
Alternatively, you can use ``pmtiles://{source2}`` to reference existing data object from the config. Alternatively, you can use ``pmtiles://{source2}`` to reference existing data object from the config.

View file

@ -77,7 +77,7 @@ Static images
* scales with ``scale`` parameter since image placement is relative to it's size * scales with ``scale`` parameter since image placement is relative to it's size
* e.g. ``2,-4`` - Image will be moved 2 pixel to the right and 4 pixel in the upwards direction from the provided location * e.g. ``2,-4`` - Image will be moved 2 pixel to the right and 4 pixel in the upwards direction from the provided location
* e.g. ``5.9,45.8|marker-icon.png|scale:0.5|offset:2,-4`` * e.g. ``5.9,45.8|marker-start.svg|scale:0.5|offset:2,-4``
* can be provided multiple times * can be provided multiple times
* ``padding`` - "percentage" padding for fitted endpoints (area-based and path autofit) * ``padding`` - "percentage" padding for fitted endpoints (area-based and path autofit)
@ -100,28 +100,6 @@ Source data
* TileJSON at ``/data/{id}.json`` * TileJSON at ``/data/{id}.json``
* If terrain mbtile data is served and ``encoding`` is configured (see config) the elevation can be queried
* by ``/data/{id}/elevation/{z}/{x}/{y}`` for the tile
* or ``/data/{id}/elevation/{z}/{long}/{lat}`` for the coordinate
* the result will be a json object like ``{"z":7,"x":68,"y":45,"red":134,"green":66,"blue":0,"latitude":11.84069,"longitude":46.04798,"elevation":1602}``
* The elevation api is not available in the ``tileserver-gl-light`` version.
Static files
===========
* Static files are served at ``/files/{filename}``
* The source folder can be configured (``options.paths.files``), default is ``public/files``
* This feature can be used to serve ``geojson`` files for styles and rendered tiles.
* Keep in mind, that each rendered tile loads the whole geojson file, if performance matters a conversion to a tiled format (e.g. with https://github.com/felt/tippecanoe)may be a better approch.
* Use ``file://{filename}`` to have matching paths for both endoints
TileJSON arrays TileJSON arrays
=============== ===============
Array of all TileJSONs is at ``[/{tileSize}]/index.json`` (``[/{tileSize}]/rendered.json``; ``/data.json``) Array of all TileJSONs is at ``[/{tileSize}]/index.json`` (``[/{tileSize}]/rendered.json``; ``/data.json``)

View file

@ -22,7 +22,7 @@ npm is supported on the following platforms with `Native Dependencies <#id1>`_ i
- Operating systems: - Operating systems:
- Ubuntu 22.04 (x64/arm64) - Ubuntu 22.04 (x64/arm64)
- macOS 14 (x64/arm64) - macOS 12 (x64/arm64)
- Windows (x64) - Windows (x64)
- Node.js 18,20 - Node.js 18,20
@ -50,13 +50,13 @@ Ubuntu 22.04 (x64/arm64)
~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~
- apt install build-essential pkg-config xvfb libglfw3-dev libuv1-dev libjpeg-turbo8 libicu70 libcairo2-dev libpango1.0-dev libjpeg-dev libgif-dev librsvg2-dev gir1.2-rsvg-2.0 librsvg2-2 librsvg2-common libcurl4-openssl-dev libpixman-1-dev libpixman-1-0 - apt install build-essential pkg-config xvfb libglfw3-dev libuv1-dev libjpeg-turbo8 libicu70 libcairo2-dev libpango1.0-dev libjpeg-dev libgif-dev librsvg2-dev gir1.2-rsvg-2.0 librsvg2-2 librsvg2-common libcurl4-openssl-dev libpixman-1-dev libpixman-1-0
MacOS 14 (x64/arm64) MacOS 12 (x64/arm64)
~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~
- brew install pkg-config cairo pango libpng jpeg giflib librsvg - brew install pkg-config cairo libpng jpeg giflib
Windows (x64) Windows (x64)
~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~
- `Microsoft Visual C++ Redistributable <https://aka.ms/vs/17/release/vc_redist.x64.exe>`_ - `Microsoft Visual C++ 2015-2022 Redistributable <https://aka.ms/vs/17/release/vc_redist.x64.exe>`_
``tileserver-gl-light`` on npm ``tileserver-gl-light`` on npm
============================== ==============================

6332
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,19 +1,12 @@
{ {
"name": "tileserver-gl", "name": "tileserver-gl",
"version": "5.2.0", "version": "4.12.0",
"description": "Map tile server for JSON GL styles - vector and server side generated raster tiles", "description": "Map tile server for JSON GL styles - vector and server side generated raster tiles",
"main": "src/main.js", "main": "src/main.js",
"bin": "src/main.js", "bin": "src/main.js",
"type": "module", "type": "module",
"scripts": { "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": "mocha test/**.js --timeout 10000 --exit",
"test-docker": "xvfb-run npm test",
"lint:yml": "yamllint --schema=CORE_SCHEMA *.{yml,yaml}", "lint:yml": "yamllint --schema=CORE_SCHEMA *.{yml,yaml}",
"lint:js": "npm run lint:eslint && npm run lint:prettier", "lint:js": "npm run lint:eslint && npm run lint:prettier",
"lint:js:fix": "npm run lint:eslint:fix && npm run lint:prettier:fix", "lint:js:fix": "npm run lint:eslint:fix && npm run lint:prettier:fix",
@ -21,58 +14,53 @@
"lint:eslint:fix": "eslint --fix \"{,!(node_modules|dist|static|public)/**/}*.{js,ts,cjs,mjs}\" --ignore-path .gitignore", "lint:eslint:fix": "eslint --fix \"{,!(node_modules|dist|static|public)/**/}*.{js,ts,cjs,mjs}\" --ignore-path .gitignore",
"lint:prettier": "prettier --check \"{,!(node_modules|dist|static|public)/**/}*.{js,ts,cjs,mjs,json}\" --ignore-path .gitignore", "lint:prettier": "prettier --check \"{,!(node_modules|dist|static|public)/**/}*.{js,ts,cjs,mjs,json}\" --ignore-path .gitignore",
"lint:prettier:fix": "prettier --write \"{,!(node_modules|dist|static|public)/**/}*.{js,ts,cjs,mjs,json}\" --ignore-path .gitignore", "lint:prettier:fix": "prettier --write \"{,!(node_modules|dist|static|public)/**/}*.{js,ts,cjs,mjs,json}\" --ignore-path .gitignore",
"docker": "docker build . && docker run --rm -i -p 8080:8080 $(docker build -q .)" "docker": "docker build . && docker run --rm -i -p 8080:8080 $(docker build -q .)",
"prepare": "node -e \"if (process.env.NODE_ENV !== 'production'){ process.exit(1) } \" || husky"
}, },
"dependencies": { "dependencies": {
"@jsse/pbfont": "^0.2.2", "@jsse/pbfont": "^0.1.2",
"@mapbox/mapbox-gl-rtl-text": "0.3.0",
"@mapbox/mbtiles": "0.12.1", "@mapbox/mbtiles": "0.12.1",
"@mapbox/polyline": "^1.2.1", "@mapbox/polyline": "^1.2.1",
"@mapbox/sphericalmercator": "1.2.0", "@mapbox/sphericalmercator": "1.2.0",
"@mapbox/vector-tile": "2.0.3", "@mapbox/vector-tile": "2.0.2",
"@maplibre/maplibre-gl-inspect": "1.7.0", "@maplibre/maplibre-gl-native": "5.4.0",
"@maplibre/maplibre-gl-native": "6.0.0", "@maplibre/maplibre-gl-style-spec": "20.3.0",
"@maplibre/maplibre-gl-style-spec": "20.3.1",
"@sindresorhus/fnv1a": "3.1.0", "@sindresorhus/fnv1a": "3.1.0",
"advanced-pool": "0.3.3", "advanced-pool": "0.3.3",
"axios": "^1.8.2", "axios": "^1.7.2",
"canvas": "3.0.1", "canvas": "2.11.2",
"chokidar": "3.6.0", "chokidar": "3.6.0",
"clone": "2.1.2", "clone": "2.1.2",
"color": "4.2.3", "color": "4.2.3",
"commander": "12.1.0", "commander": "12.1.0",
"copyfiles": "2.4.1",
"cors": "2.8.5", "cors": "2.8.5",
"express": "5.0.1", "express": "4.19.2",
"handlebars": "4.7.8", "handlebars": "4.7.8",
"http-shutdown": "1.2.2", "http-shutdown": "1.2.2",
"leaflet": "1.9.4",
"leaflet-hash": "0.2.1",
"maplibre-gl": "4.7.1",
"morgan": "1.10.0", "morgan": "1.10.0",
"pbf": "4.0.1", "pbf": "4.0.1",
"pmtiles": "3.0.7", "pmtiles": "3.0.6",
"proj4": "2.12.1", "proj4": "2.11.0",
"sanitize-filename": "1.6.3", "sanitize-filename": "1.6.3",
"semver": "^7.6.3", "sharp": "0.33.4",
"sharp": "0.33.5",
"tileserver-gl-styles": "2.0.0" "tileserver-gl-styles": "2.0.0"
}, },
"devDependencies": { "devDependencies": {
"@commitlint/cli": "^19.5.0", "@commitlint/cli": "^19.3.0",
"@commitlint/config-conventional": "^19.5.0", "@commitlint/config-conventional": "^19.2.2",
"@typescript-eslint/eslint-plugin": "^7.18.0", "@typescript-eslint/eslint-plugin": "^7.16.0",
"@typescript-eslint/parser": "^7.18.0", "@typescript-eslint/parser": "^7.16.0",
"chai": "5.1.1", "chai": "5.1.1",
"eslint": "^8.57.0", "eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0", "eslint-config-prettier": "^9.1.0",
"eslint-plugin-jsdoc": "^50.2.2", "eslint-plugin-jsdoc": "^48.7.0",
"eslint-plugin-prettier": "^5.2.1", "eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-security": "^1.7.1", "eslint-plugin-security": "^3.0.1",
"lint-staged": "^15.2.10", "husky": "^9.0.11",
"mocha": "^10.7.3", "lint-staged": "^15.2.7",
"mocha": "^10.6.0",
"node-addon-api": "^8", "node-addon-api": "^8",
"prettier": "^3.3.3", "prettier": "^3.3.2",
"should": "^13.2.3", "should": "^13.2.3",
"supertest": "^7.0.0", "supertest": "^7.0.0",
"yaml-lint": "^1.7.0" "yaml-lint": "^1.7.0"
@ -85,7 +73,7 @@
], ],
"license": "BSD-2-Clause", "license": "BSD-2-Clause",
"engines": { "engines": {
"node": ">=18.17.0 <23" "node": ">=18.17.0 <21"
}, },
"repository": { "repository": {
"url": "git+https://github.com/maptiler/tileserver-gl.git", "url": "git+https://github.com/maptiler/tileserver-gl.git",

View file

@ -1,8 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title></title>
</head>
<body>
</body>
</html>

View file

@ -1,51 +0,0 @@
class ElevationInfoControl {
constructor(options) {
this.url = options["url"];
}
getDefaultPosition() {
const defaultPosition = "bottom-left";
return defaultPosition;
}
onAdd(map) {
this.map = map;
this.controlContainer = document.createElement("div");
this.controlContainer.classList.add("maplibregl-ctrl");
this.controlContainer.classList.add("maplibregl-ctrl-group");
this.controlContainer.classList.add("maplibre-ctrl-elevation");
this.controlContainer.textContent = "Elevation: Click on Map";
map.on('click', (e) => {
var url = this.url;
var coord = {"z": Math.floor(map.getZoom()), "x": e.lngLat["lng"].toFixed(7), "y": e.lngLat["lat"].toFixed(7)};
for(var key in coord) {
url = url.replace(new RegExp('{'+ key +'}','g'), coord[key]);
}
let request = new XMLHttpRequest();
request.open("GET", url, true);
request.onload = () => {
if (request.status !== 200) {
this.controlContainer.textContent = "Elevation: No value";
} else {
this.controlContainer.textContent = `Elevation: ${JSON.parse(request.responseText).elevation} (${JSON.stringify(coord)})`;
}
}
request.send();
});
return this.controlContainer;
}
onRemove() {
if (
!this.controlContainer ||
!this.controlContainer.parentNode ||
!this.map
) {
return;
}
this.controlContainer.parentNode.removeChild(this.controlContainer);
this.map = undefined;
}
};

View file

@ -1,18 +1,18 @@
@font-face { @font-face {
font-family: 'OpenSans'; font-family: 'OpenSans';
src: url('./fonts/OpenSans-Regular.ttf'); src: url('/fonts/OpenSans-Regular.ttf');
font-weight: normal; font-weight: normal;
font-style: normal; font-style: normal;
} }
@font-face { @font-face {
font-family: 'OpenSans'; font-family: 'OpenSans';
src: url('./fonts/OpenSans-Italic.ttf'); src: url('/fonts/OpenSans-Italic.ttf');
font-weight: normal; font-weight: normal;
font-style: italic; font-style: italic;
} }
@font-face { @font-face {
font-family: 'OpenSans'; font-family: 'OpenSans';
src: url('./fonts/OpenSans-Bold.ttf'); src: url('/fonts/OpenSans-Bold.ttf');
font-weight: bold; font-weight: bold;
font-style: normal; font-style: normal;
} }
@ -25,7 +25,7 @@ body {
margin: 0; margin: 0;
background-repeat: no-repeat !important; background-repeat: no-repeat !important;
background-size: contain !important; background-size: contain !important;
background-image: url(./images/header-map-1280px.png); background-image: url(/images/header-map-1280px.png);
} }
a { a {
color: #499dce; color: #499dce;
@ -73,29 +73,12 @@ section {
font-size: 20px; font-size: 20px;
background: #fff; background: #fff;
} }
.filter-details {
padding: 20px 30px;
}
.box input { /* Filter text input */
padding: 10px;
font-size: 16px;
border: 1px solid #ededed;
border-radius: 4px;
/* fill out to parent */
width: 100%;
}
.item { .item {
background: #fff; background: #fff;
height: 191px; height: 191px;
border: 1px solid #ededed; border: 1px solid #ededed;
border-top: none; border-top: none;
} }
.filter-item {
background: #fbfbfb;
height: 150px;
border: 1px solid #ededed;
border-top: none;
}
.item:nth-child(odd) { .item:nth-child(odd) {
background-color: #fbfbfb; background-color: #fbfbfb;
} }
@ -114,7 +97,7 @@ section {
} }
.details h3 { .details h3 {
font-size: 18px; font-size: 18px;
margin-top: 5px; margin-top: 25px;
} }
.details p { .details p {
padding: 0; padding: 0;

View file

@ -0,0 +1,162 @@
(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);

View file

@ -0,0 +1,661 @@
/* 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

View file

@ -0,0 +1,40 @@
.maplibregl-inspect_popup {
color: #333;
display: table;
}
.maplibregl-inspect_feature:not(:last-child) {
border-bottom: 1px solid #ccc;
}
.maplibregl-inspect_layer:before {
content: '#';
}
.maplibregl-inspect_layer {
display: block;
font-weight: bold;
}
.maplibregl-inspect_property {
display: table-row;
}
.maplibregl-inspect_property-value {
display: table-cell;
word-break: break-all;
}
.maplibregl-inspect_property-name {
display: table-cell;
padding-right: 10px;
word-break: break-all;
}
.maplibregl-ctrl-inspect {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23333333' preserveAspectRatio='xMidYMid meet' viewBox='-10 -10 60 60'%3E%3Cg%3E%3Cpath d='m15 21.6q0-2 1.5-3.5t3.5-1.5 3.5 1.5 1.5 3.5-1.5 3.6-3.5 1.4-3.5-1.4-1.5-3.6z m18.4 11.1l-6.4-6.5q1.4-2.1 1.4-4.6 0-3.4-2.5-5.8t-5.9-2.4-5.9 2.4-2.5 5.8 2.5 5.9 5.9 2.5q2.4 0 4.6-1.4l7.4 7.4q-0.9 0.6-2 0.6h-20q-1.3 0-2.3-0.9t-1.1-2.3l0.1-26.8q0-1.3 1-2.3t2.3-0.9h13.4l10 10v19.3z'%3E%3C/path%3E%3C/g%3E%3C/svg%3E");
}
.maplibregl-ctrl-map {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23333333' viewBox='-10 -10 60 60' preserveAspectRatio='xMidYMid meet'%3E%3Cg%3E%3Cpath d='m25 31.640000000000004v-19.766666666666673l-10-3.511666666666663v19.766666666666666z m9.140000000000008-26.640000000000004q0.8599999999999923 0 0.8599999999999923 0.8600000000000003v25.156666666666666q0 0.625-0.625 0.783333333333335l-9.375 3.1999999999999993-10-3.5133333333333354-8.906666666666668 3.4383333333333326-0.2333333333333334 0.07833333333333314q-0.8616666666666664 0-0.8616666666666664-0.8599999999999994v-25.156666666666663q0-0.625 0.6233333333333331-0.7833333333333332l9.378333333333334-3.198333333333334 10 3.5133333333333336 8.905000000000001-3.4383333333333344z'%3E%3C/path%3E%3C/g%3E%3C/svg%3E");
}

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

View file

@ -4,31 +4,20 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{name}} - TileServer GL</title> <title>{{name}} - TileServer GL</title>
{{#use_maplibre}} {{#is_vector}}
<link rel="stylesheet" type="text/css" href="{{public_url}}maplibre-gl.css{{&key_query}}" /> <link rel="stylesheet" type="text/css" href="{{public_url}}maplibre-gl.css{{&key_query}}" />
<link rel="stylesheet" type="text/css" href="{{public_url}}maplibre-gl-inspect.css{{&key_query}}" /> <link rel="stylesheet" type="text/css" href="{{public_url}}maplibre-gl-inspect.css{{&key_query}}" />
<script src="{{public_url}}maplibre-gl.js{{&key_query}}"></script> <script src="{{public_url}}maplibre-gl.js{{&key_query}}"></script>
<script src="{{public_url}}maplibre-gl-inspect.js{{&key_query}}"></script> <script src="{{public_url}}maplibre-gl-inspect.js{{&key_query}}"></script>
{{^is_light}}
<script src="{{public_url}}elevation-control.js{{&key_query}}"></script>
{{/is_light}}
<style> <style>
body {background:#fff;color:#333;font-family:Arial, sans-serif;} body {background:#fff;color:#333;font-family:Arial, sans-serif;}
{{^is_terrain}}
#map {position:absolute;top:0;left:0;right:250px;bottom:0;} #map {position:absolute;top:0;left:0;right:250px;bottom:0;}
{{/is_terrain}}
{{#is_terrain}}
#map { position:absolute; top:0; bottom:0; left:0; right:0; }
{{/is_terrain}}
h1 {position:absolute;top:5px;right:0;width:240px;margin:0;line-height:20px;font-size:20px;} 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 {position:absolute;top:35px;right:0;bottom:0;width:240px;overflow:auto;}
#layerList div div {width:15px;height:15px;display:inline-block;} #layerList div div {width:15px;height:15px;display:inline-block;}
{{^is_light}}
.maplibre-ctrl-elevation { padding-left: 5px; padding-right: 5px; }
{{/is_light}}
</style> </style>
{{/use_maplibre}} {{/is_vector}}
{{^use_maplibre}} {{^is_vector}}
<link rel="stylesheet" type="text/css" href="{{public_url}}leaflet.css{{&key_query}}" /> <link rel="stylesheet" type="text/css" href="{{public_url}}leaflet.css{{&key_query}}" />
<script src="{{public_url}}leaflet.js{{&key_query}}"></script> <script src="{{public_url}}leaflet.js{{&key_query}}"></script>
<script src="{{public_url}}leaflet-hash.js{{&key_query}}"></script> <script src="{{public_url}}leaflet-hash.js{{&key_query}}"></script>
@ -48,22 +37,23 @@
background-image: url({{public_url}}images/marker-icon.png{{&key_query}}); background-image: url({{public_url}}images/marker-icon.png{{&key_query}});
} }
</style> </style>
{{/use_maplibre}} {{/is_vector}}
</head> </head>
<body> <body>
{{#use_maplibre}} {{#is_vector}}
<h1>{{name}}</h1> <h1>{{name}}</h1>
<div id="map"></div> <div id="map"></div>
{{^is_terrain}}
<div id="layerList"></div> <div id="layerList"></div>
<pre id="propertyList"></pre> <pre id="propertyList"></pre>
{{/is_terrain}}
<script> <script>
var keyMatch = location.search.match(/[\?\&]key=([^&]+)/i); var keyMatch = location.search.match(/[\?\&]key=([^&]+)/i);
var keyParam = keyMatch ? '?key=' + keyMatch[1] : ''; var keyParam = keyMatch ? '?key=' + keyMatch[1] : '';
{{^is_terrain}} var map = new maplibregl.Map({
var style = { container: 'map',
hash: true,
maxPitch: 85,
style: {
version: 8, version: 8,
sources: { sources: {
'vector_layer_': { 'vector_layer_': {
@ -72,89 +62,14 @@
} }
}, },
layers: [] layers: []
};
{{/is_terrain}}
{{#is_terrain}}
var style = {
version: 8,
sources: {
"terrain": {
"type": "raster-dem",
"url": "{{public_url}}data/{{id}}.json",
"encoding": "{{terrain_encoding}}"
},
"hillshade": {
"type": "raster-dem",
"url": "{{public_url}}data/{{id}}.json",
"encoding": "{{terrain_encoding}}"
} }
},
"terrain": {
"source": "terrain"
},
"layers": [
{
"id": "background",
"paint": {
{{#if is_terrainrgb}}
"background-color": "hsl(190, 99%, 63%)"
{{else}}
"background-color": "hsl(0, 100%, 25%)"
{{/if}}
},
"type": "background"
},
{
"id": "hillshade",
"source": "hillshade",
"type": "hillshade",
"paint": {
"hillshade-shadow-color": "hsl(39, 21%, 33%)",
"hillshade-illumination-direction": 315,
"hillshade-exaggeration": 0.8
}
}
]
};
{{/is_terrain}}
var map = new maplibregl.Map({
container: 'map',
hash: true,
maxPitch: 85,
style: style
}); });
map.addControl(new maplibregl.NavigationControl());
map.addControl(new maplibregl.NavigationControl({
visualizePitch: true,
showZoom: true,
showCompass: true
}));
{{#is_terrain}}
map.addControl(
new maplibregl.TerrainControl({
source: "terrain",
})
);
{{^is_light}}
map.addControl(
new ElevationInfoControl({
url: "{{public_url}}data/{{id}}/elevation/{z}/{x}/{y}"
})
);
{{/is_light}}
{{/is_terrain}}
{{^is_terrain}}
var inspect = new MaplibreInspect({ var inspect = new MaplibreInspect({
showInspectMap: true, showInspectMap: true,
showInspectButton: false showInspectButton: false
}); });
map.addControl(inspect); map.addControl(inspect);
map.on('styledata', function() { map.on('styledata', function() {
var layerList = document.getElementById('layerList'); var layerList = document.getElementById('layerList');
layerList.innerHTML = ''; layerList.innerHTML = '';
@ -169,15 +84,15 @@
}); });
}) })
}); });
{{/is_terrain}}
</script> </script>
{{/use_maplibre}} {{/is_vector}}
{{^use_maplibre}} {{^is_vector}}
<h1 style="display:none;">{{name}}</h1> <h1 style="display:none;">{{name}}</h1>
<div id='map'></div> <div id='map'></div>
<script> <script>
var keyMatch = location.search.match(/[\?\&]key=([^&]+)/i); var keyMatch = location.search.match(/[\?\&]key=([^&]+)/i);
var keyParam = keyMatch ? '?key=' + keyMatch[1] : ''; var keyParam = keyMatch ? '?key=' + keyMatch[1] : '';
var map = L.map('map', { zoomControl: false }); var map = L.map('map', { zoomControl: false });
new L.Control.Zoom({ position: 'topright' }).addTo(map); new L.Control.Zoom({ position: 'topright' }).addTo(map);
@ -226,6 +141,6 @@
new L.Hash(map); new L.Hash(map);
}, 0); }, 0);
</script> </script>
{{/use_maplibre}} {{/is_vector}}
</body> </body>
</html> </html>

View file

@ -6,51 +6,24 @@
<title>TileServer GL - Server for vector and raster maps with GL styles</title> <title>TileServer GL - Server for vector and raster maps with GL styles</title>
<link rel="stylesheet" type="text/css" href="{{public_url}}index.css{{&key_query}}" /> <link rel="stylesheet" type="text/css" href="{{public_url}}index.css{{&key_query}}" />
<script> <script>
function toggle_link(id, link) { function toggle_xyz(id) {
var el = document.getElementById(id); var el = document.getElementById(id);
var s = el.style; var s = el.style;
if (s.display == 'none') { s.display = s.display == 'none' ? 'inline-block' : 'none';
s.display = 'inline-block';
} else if (el.value == link) {
s.display = 'none';
}
el.value = link;
el.setSelectionRange(0, el.value.length); el.setSelectionRange(0, el.value.length);
return false; return false;
} }
function filter() {
var filter = document.getElementById('filter').value.toLowerCase();
var items = document.getElementsByClassName('item');
for (var i = 0; i < items.length; i++) {
var item = items[i];
var dataName = item.getAttribute('data-name')?.toLowerCase() ?? '';
var dataIdentifier = item.getAttribute('data-id')?.toLowerCase() ?? '';
item.hidden = !(dataName.indexOf(filter) > -1 || dataIdentifier.indexOf(filter) > -1);
}
}
</script> </script>
</head> </head>
<body> <body>
<section> <section>
<h1 class="title {{#if is_light}}light{{/if}}"><img src="{{public_url}}images/logo.png{{&key_query}}" alt="TileServer GL" /></h1> <h1 class="title {{#if is_light}}light{{/if}}"><img src="{{public_url}}images/logo.png{{&key_query}}" alt="TileServer GL" /></h1>
<h2 class="subtitle">Vector {{#if is_light}}<s>and raster</s>{{else}}and raster{{/if}} maps with GL styles</h2> <h2 class="subtitle">Vector {{#if is_light}}<s>and raster</s>{{else}}and raster{{/if}} maps with GL styles</h2>
<h2 class="box-header">Filter</h2>
<!-- filter box -->
<div class="box">
<div class="filter-item">
<div class="filter-details">
<h3>Filter styles and data by name or identifier</h3>
<!-- filter input , needs to call filter() when content changes...-->
<input id="filter" type="text" oninput="filter()" placeholder="Start typing name or identifier" autofocus />
</div>
</div>
</div>
{{#if styles}} {{#if styles}}
<h2 class="box-header">Styles</h2> <h2 class="box-header">Styles</h2>
<div class="box"> <div class="box">
{{#each styles}} {{#each styles}}
<div class="item" data-id="{{@key}}" data-name="{{name}}"> <div class="item">
{{#if thumbnail}} {{#if thumbnail}}
<img src="{{public_url}}styles/{{@key}}/{{thumbnail}}{{&../key_query}}" alt="{{name}} preview" /> <img src="{{public_url}}styles/{{@key}}/{{thumbnail}}{{&../key_query}}" alt="{{name}} preview" />
{{else}} {{else}}
@ -71,8 +44,8 @@
| <a href="{{public_url}}styles/{{@key}}/wmts.xml{{&../key_query}}">WMTS</a> | <a href="{{public_url}}styles/{{@key}}/wmts.xml{{&../key_query}}">WMTS</a>
{{/if}} {{/if}}
{{#if xyz_link}} {{#if xyz_link}}
| <a href="#" onclick="return toggle_link('xyz_style_{{@key}}', '{{&xyz_link}}');">XYZ</a> | <a href="#" onclick="return toggle_xyz('xyz_style_{{@key}}');">XYZ</a>
<input id="xyz_style_{{@key}}" type="text" value="" style="display:none;" /> <input id="xyz_style_{{@key}}" type="text" value="{{&xyz_link}}" style="display:none;" />
{{/if}} {{/if}}
</p> </p>
</div> </div>
@ -97,7 +70,7 @@
<h2 class="box-header">Data</h2> <h2 class="box-header">Data</h2>
<div class="box"> <div class="box">
{{#each data}} {{#each data}}
<div class="item" data-id="{{@key}}" data-name="{{tileJSON.name}}"> <div class="item">
{{#if thumbnail}} {{#if thumbnail}}
<img src="{{public_url}}data/{{@key}}/{{thumbnail}}{{&../key_query}}" alt="{{name}} preview" /> <img src="{{public_url}}data/{{@key}}/{{thumbnail}}{{&../key_query}}" alt="{{name}} preview" />
{{else}} {{else}}
@ -110,12 +83,9 @@
<p class="services"> <p class="services">
services: <a href="{{public_url}}data/{{@key}}.json{{&../key_query}}">TileJSON</a> services: <a href="{{public_url}}data/{{@key}}.json{{&../key_query}}">TileJSON</a>
{{#if xyz_link}} {{#if xyz_link}}
| <a href="#" onclick="return toggle_link('link_data_{{@key}}', '{{&xyz_link}}');">XYZ</a> | <a href="#" onclick="return toggle_xyz('xyz_data_{{@key}}');">XYZ</a>
<input id="xyz_data_{{@key}}" type="text" value="{{&xyz_link}}" style="display:none;" />
{{/if}} {{/if}}
{{#if elevation_link}}
| <a href="#" onclick="return toggle_link('link_data_{{@key}}', '{{&elevation_link}}');">Elevation</a>
{{/if}}
<input id="link_data_{{@key}}" type="text" value="" style="display:none;" />
</p> </p>
</div> </div>
<div class="viewers"> <div class="viewers">
@ -124,9 +94,6 @@
{{/is_vector}} {{/is_vector}}
{{^is_vector}} {{^is_vector}}
<a class="btn" href="{{public_url}}data/{{@key}}/{{&../key_query}}{{viewer_hash}}">View</a> <a class="btn" href="{{public_url}}data/{{@key}}/{{&../key_query}}{{viewer_hash}}">View</a>
{{#is_terrain}}
<a class="btn" href="{{public_url}}data/preview/{{@key}}/{{&../key_query}}{{viewer_hash}}">Preview Terrain</a>
{{/is_terrain}}
{{/is_vector}} {{/is_vector}}
</div> </div>
</div> </div>

View file

@ -1,24 +1,15 @@
#!/usr/bin/env node #!/usr/bin/env node
'use strict'; '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 fs from 'node:fs';
import fsp from 'node:fs/promises';
import path from 'path'; import path from 'path';
import { fileURLToPath } from 'url'; import { fileURLToPath } from 'url';
import axios from 'axios'; import axios from 'axios';
import { server } from './server.js'; import { server } from './server.js';
import MBTiles from '@mapbox/mbtiles';
import { isValidHttpUrl } from './utils.js'; import { isValidHttpUrl } from './utils.js';
import { openPMtiles, getPMtilesInfo } from './pmtiles_adapter.js'; import { openPMtiles, getPMtilesInfo } from './pmtiles_adapter.js';
import { program } from 'commander';
import { existsP } from './promises.js';
import { openMbTilesWrapper } from './mbtiles_wrapper.js';
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename); const __dirname = path.dirname(__filename);
@ -31,6 +22,7 @@ if (args.length >= 3 && args[2][0] !== '-') {
args.splice(2, 0, '--mbtiles'); args.splice(2, 0, '--mbtiles');
} }
import { program } from 'commander';
program program
.description('tileserver-gl startup options') .description('tileserver-gl startup options')
.usage('tileserver-gl [mbtiles] [options]') .usage('tileserver-gl [mbtiles] [options]')
@ -103,7 +95,7 @@ const startWithInputFile = async (inputFile) => {
inputFile = path.resolve(process.cwd(), inputFile); inputFile = path.resolve(process.cwd(), inputFile);
inputFilePath = path.dirname(inputFile); inputFilePath = path.dirname(inputFile);
const inputFileStats = await fsp.stat(inputFile); const inputFileStats = fs.statSync(inputFile);
if (!inputFileStats.isFile() || inputFileStats.size === 0) { if (!inputFileStats.isFile() || inputFileStats.size === 0) {
console.log(`ERROR: Not a valid input file: `); console.log(`ERROR: Not a valid input file: `);
process.exit(1); process.exit(1);
@ -148,11 +140,11 @@ const startWithInputFile = async (inputFile) => {
}; };
} }
const styles = await fsp.readdir(path.resolve(styleDir, 'styles')); const styles = fs.readdirSync(path.resolve(styleDir, 'styles'));
for (const styleName of styles) { for (const styleName of styles) {
const styleFileRel = styleName + '/style.json'; const styleFileRel = styleName + '/style.json';
const styleFile = path.resolve(styleDir, 'styles', styleFileRel); const styleFile = path.resolve(styleDir, 'styles', styleFileRel);
if (await existsP(styleFile)) { if (fs.existsSync(styleFile)) {
config['styles'][styleName] = { config['styles'][styleName] = {
style: styleFileRel, style: styleFileRel,
tilejson: { tilejson: {
@ -190,16 +182,21 @@ const startWithInputFile = async (inputFile) => {
); );
process.exit(1); process.exit(1);
} }
let info; const instance = new MBTiles(inputFile + '?mode=ro', (err) => {
try { if (err) {
const mbw = await openMbTilesWrapper(inputFile); console.log('ERROR: Unable to open MBTiles.');
info = await mbw.getInfo();
if (!info) throw new Error('Metadata missing in the MBTiles.');
} catch (err) {
console.log('ERROR: Unable to open MBTiles or read metadata:', err);
console.log(`Make sure ${path.basename(inputFile)} is valid MBTiles.`); console.log(`Make sure ${path.basename(inputFile)} is valid MBTiles.`);
process.exit(1); process.exit(1);
} }
instance.getInfo((err, info) => {
if (err || !info) {
console.log('ERROR: Metadata missing in the MBTiles.');
console.log(
`Make sure ${path.basename(inputFile)} is valid MBTiles.`,
);
process.exit(1);
}
const bounds = info.bounds; const bounds = info.bounds;
if ( if (
@ -210,11 +207,11 @@ const startWithInputFile = async (inputFile) => {
mbtiles: path.basename(inputFile), mbtiles: path.basename(inputFile),
}; };
const styles = await fsp.readdir(path.resolve(styleDir, 'styles')); const styles = fs.readdirSync(path.resolve(styleDir, 'styles'));
for (const styleName of styles) { for (const styleName of styles) {
const styleFileRel = styleName + '/style.json'; const styleFileRel = styleName + '/style.json';
const styleFile = path.resolve(styleDir, 'styles', styleFileRel); const styleFile = path.resolve(styleDir, 'styles', styleFileRel);
if (await existsP(styleFile)) { if (fs.existsSync(styleFile)) {
config['styles'][styleName] = { config['styles'][styleName] = {
style: styleFileRel, style: styleFileRel,
tilejson: { tilejson: {
@ -239,6 +236,8 @@ const startWithInputFile = async (inputFile) => {
} }
return startServer(null, config); return startServer(null, config);
});
});
} }
}; };
@ -255,10 +254,10 @@ fs.stat(path.resolve(opts.config), async (err, stats) => {
return startWithInputFile(inputFile); return startWithInputFile(inputFile);
} else { } else {
// try to find in the cwd // try to find in the cwd
const files = await fsp.readdir(process.cwd()); const files = fs.readdirSync(process.cwd());
for (const filename of files) { for (const filename of files) {
if (filename.endsWith('.mbtiles') || filename.endsWith('.pmtiles')) { if (filename.endsWith('.mbtiles') || filename.endsWith('.pmtiles')) {
const inputFilesStats = await fsp.stat(filename); const inputFilesStats = fs.statSync(filename);
if (inputFilesStats.isFile() && inputFilesStats.size > 0) { if (inputFilesStats.isFile() && inputFilesStats.size > 0) {
inputFile = filename; inputFile = filename;
break; break;

View file

@ -1,46 +0,0 @@
import MBTiles from '@mapbox/mbtiles';
import util from 'node:util';
/**
* Promise-ful wrapper around the MBTiles class.
*/
class MBTilesWrapper {
constructor(mbtiles) {
this._mbtiles = mbtiles;
this._getInfoP = util.promisify(mbtiles.getInfo.bind(mbtiles));
}
/**
* Get the underlying MBTiles object.
* @returns {MBTiles}
*/
getMbTiles() {
return this._mbtiles;
}
/**
* Get the MBTiles metadata object.
* @returns {Promise<object>}
*/
getInfo() {
return this._getInfoP();
}
}
/**
* Open the given MBTiles file and return a promise that resolves with a
* MBTilesWrapper instance.
* @param inputFile Input file
* @returns {Promise<MBTilesWrapper>}
*/
export function openMbTilesWrapper(inputFile) {
return new Promise((resolve, reject) => {
const mbtiles = new MBTiles(inputFile + '?mode=ro', (err) => {
if (err) {
reject(err);
return;
}
resolve(new MBTilesWrapper(mbtiles));
});
});
}

View file

@ -1,14 +0,0 @@
import util from 'node:util';
import fsp from 'node:fs/promises';
import zlib from 'zlib';
export const gzipP = util.promisify(zlib.gzip);
export const gunzipP = util.promisify(zlib.gunzip);
export const existsP = async (path) => {
try {
await fsp.access(path); // Defaults to F_OK: indicating that the file is visible to the calling process
return true;
} catch (err) {
return false;
}
};

View file

@ -1,85 +1,37 @@
'use strict'; 'use strict';
import fsp from 'node:fs/promises'; import fs from 'node:fs';
import path from 'path'; import path from 'path';
import zlib from 'zlib';
import clone from 'clone'; import clone from 'clone';
import express from 'express'; import express from 'express';
import MBTiles from '@mapbox/mbtiles';
import Pbf from 'pbf'; import Pbf from 'pbf';
import { VectorTile } from '@mapbox/vector-tile'; import { VectorTile } from '@mapbox/vector-tile';
import SphericalMercator from '@mapbox/sphericalmercator';
import { getTileUrls, isValidHttpUrl, fixTileJSONCenter } from './utils.js';
import { import {
fixTileJSONCenter, openPMtiles,
getTileUrls, getPMtilesInfo,
isValidHttpUrl, getPMtilesTile,
fetchTileData, } from './pmtiles_adapter.js';
} 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 = { 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'); const app = express().disable('x-powered-by');
/** app.get(
* Handles requests for tile data, responding with the tile image. '/:id/:z(\\d+)/:x(\\d+)/:y(\\d+).:format([\\w.]+)',
* @param {object} req - Express request object. async (req, res, next) => {
* @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]; const item = repo[req.params.id];
if (!item) { if (!item) {
return res.sendStatus(404); return res.sendStatus(404);
} }
const tileJSONFormat = item.tileJSON.format; const tileJSONFormat = item.tileJSON.format;
const z = parseInt(req.params.z, 10); const z = req.params.z | 0;
const x = parseInt(req.params.x, 10); const x = req.params.x | 0;
const y = parseInt(req.params.y, 10); const y = req.params.y | 0;
if (isNaN(z) || isNaN(x) || isNaN(y)) {
return res.status(404).send('Invalid Tile');
}
let format = req.params.format; let format = req.params.format;
if (format === options.pbfAlias) { if (format === options.pbfAlias) {
format = 'pbf'; format = 'pbf';
@ -92,6 +44,7 @@ export const serve_data = {
} }
if ( if (
z < item.tileJSON.minzoom || z < item.tileJSON.minzoom ||
0 ||
x < 0 || x < 0 ||
y < 0 || y < 0 ||
z > item.tileJSON.maxzoom || z > item.tileJSON.maxzoom ||
@ -100,38 +53,18 @@ export const serve_data = {
) { ) {
return res.status(404).send('Out of bounds'); return res.status(404).send('Out of bounds');
} }
if (item.sourceType === 'pmtiles') {
const fetchTile = await fetchTileData( let tileinfo = await getPMtilesTile(item.source, z, x, y);
item.source, if (tileinfo == undefined || tileinfo.data == undefined) {
item.sourceType, return res.status(404).send('Not found');
z, } else {
x, let data = tileinfo.data;
y, let headers = tileinfo.header;
);
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 (tileJSONFormat === 'pbf') {
if (options.dataDecoratorFunc) { if (options.dataDecoratorFunc) {
data = options.dataDecoratorFunc( data = options.dataDecoratorFunc(id, 'data', data, z, x, y);
req.params.id,
'data',
data,
z,
x,
y,
);
} }
} }
if (format === 'pbf') { if (format === 'pbf') {
headers['Content-Type'] = 'application/x-protobuf'; headers['Content-Type'] = 'application/x-protobuf';
} else if (format === 'geojson') { } else if (format === 'geojson') {
@ -152,150 +85,84 @@ export const serve_data = {
} }
data = JSON.stringify(geojson); data = JSON.stringify(geojson);
} }
if (headers) { delete headers['ETag']; // do not trust the tile ETag -- regenerate
delete headers['ETag'];
}
headers['Content-Encoding'] = 'gzip'; headers['Content-Encoding'] = 'gzip';
res.set(headers); res.set(headers);
if (!isGzipped) { data = zlib.gzipSync(data);
data = await gzipP(data);
}
return res.status(200).send(data); return res.status(200).send(data);
});
/**
* Handles requests for elevation data.
* @param {object} req - Express request object.
* @param {object} res - Express response object.
* @param {string} req.params.id - ID of the elevation data.
* @param {string} req.params.z - Z coordinate of the tile.
* @param {string} req.params.x - X coordinate of the tile (either integer or float).
* @param {string} req.params.y - Y coordinate of the tile (either integer or float).
* @returns {Promise<void>}
*/
app.get('/:id/elevation/:z/:x/:y', async (req, res, next) => {
try {
if (verbose) {
console.log(
`Handling elevation request for: /data/%s/elevation/%s/%s/%s`,
String(req.params.id).replace(/\n|\r/g, ''),
String(req.params.z).replace(/\n|\r/g, ''),
String(req.params.x).replace(/\n|\r/g, ''),
String(req.params.y).replace(/\n|\r/g, ''),
);
} }
const item = repo?.[req.params.id]; } else if (item.sourceType === 'mbtiles') {
if (!item) return res.sendStatus(404); item.source.getTile(z, x, y, (err, data, headers) => {
if (!item.source) return res.status(404).send('Missing source'); let isGzipped;
if (!item.tileJSON) return res.status(404).send('Missing tileJSON'); if (err) {
if (!item.sourceType) return res.status(404).send('Missing sourceType'); if (/does not exist/.test(err.message)) {
const { source, tileJSON, sourceType } = item; return res.status(204).send();
if (sourceType !== 'pmtiles' && sourceType !== 'mbtiles') {
return res
.status(400)
.send('Invalid sourceType. Must be pmtiles or mbtiles.');
}
const encoding = tileJSON?.encoding;
if (encoding == null) {
return res.status(400).send('Missing tileJSON.encoding');
} else if (encoding !== 'terrarium' && encoding !== 'mapbox') {
return res
.status(400)
.send('Invalid encoding. Must be terrarium or mapbox.');
}
const format = tileJSON?.format;
if (format == null) {
return res.status(400).send('Missing tileJSON.format');
} else if (format !== 'webp' && format !== 'png') {
return res.status(400).send('Invalid format. Must be webp or png.');
}
const z = parseInt(req.params.z, 10);
const x = parseFloat(req.params.x);
const y = parseFloat(req.params.y);
if (tileJSON.minzoom == null || tileJSON.maxzoom == null) {
return res.status(404).send(JSON.stringify(tileJSON));
}
const TILE_SIZE = tileJSON.tileSize || 512;
let bbox;
let xy;
var zoom = z;
if (Number.isInteger(x) && Number.isInteger(y)) {
const intX = parseInt(req.params.x, 10);
const intY = parseInt(req.params.y, 10);
if (
zoom < tileJSON.minzoom ||
zoom > tileJSON.maxzoom ||
intX < 0 ||
intY < 0 ||
intX >= Math.pow(2, zoom) ||
intY >= Math.pow(2, zoom)
) {
return res.status(404).send('Out of bounds');
}
xy = [intX, intY];
bbox = new SphericalMercator().bbox(intX, intY, zoom);
} else { } else {
//no zoom limit with coordinates
if (zoom < tileJSON.minzoom) {
zoom = tileJSON.minzoom;
}
if (zoom > tileJSON.maxzoom) {
zoom = tileJSON.maxzoom;
}
bbox = [x, y, x + 0.1, y + 0.1];
const { minX, minY } = new SphericalMercator().xyz(bbox, zoom);
xy = [minX, minY];
}
const fetchTile = await fetchTileData(
source,
sourceType,
zoom,
xy[0],
xy[1],
);
if (fetchTile == null) return res.status(204).send();
let data = fetchTile.data;
var param = {
long: bbox[0].toFixed(7),
lat: bbox[1].toFixed(7),
encoding,
format,
tile_size: TILE_SIZE,
z: zoom,
x: xy[0],
y: xy[1],
};
res
.status(200)
.send(await serve_rendered.getTerrainElevation(data, param));
} catch (err) {
return res return res
.status(500) .status(500)
.header('Content-Type', 'text/plain') .header('Content-Type', 'text/plain')
.send(err.message); .send(err.message);
} }
}); } else {
if (data == null) {
/** return res.status(404).send('Not found');
* Handles requests for tilejson for the data tiles. } else {
* @param {object} req - Express request object. if (tileJSONFormat === 'pbf') {
* @param {object} res - Express response object. isGzipped =
* @param {string} req.params.id - ID of the data source. data.slice(0, 2).indexOf(Buffer.from([0x1f, 0x8b])) === 0;
* @returns {Promise<void>} if (options.dataDecoratorFunc) {
*/ if (isGzipped) {
app.get('/:id.json', (req, res) => { data = zlib.unzipSync(data);
if (verbose) { isGzipped = false;
console.log(
`Handling tilejson request for: /data/%s.json`,
String(req.params.id).replace(/\n|\r/g, ''),
);
} }
data = options.dataDecoratorFunc(id, 'data', data, z, x, y);
}
}
if (format === 'pbf') {
headers['Content-Type'] = 'application/x-protobuf';
} else if (format === 'geojson') {
headers['Content-Type'] = 'application/json';
if (isGzipped) {
data = zlib.unzipSync(data);
isGzipped = false;
}
const tile = new VectorTile(new Pbf(data));
const geojson = {
type: 'FeatureCollection',
features: [],
};
for (const layerName in tile.layers) {
const layer = tile.layers[layerName];
for (let i = 0; i < layer.length; i++) {
const feature = layer.feature(i);
const featureGeoJSON = feature.toGeoJSON(x, y, z);
featureGeoJSON.properties.layer = layerName;
geojson.features.push(featureGeoJSON);
}
}
data = JSON.stringify(geojson);
}
delete headers['ETag']; // do not trust the tile ETag -- regenerate
headers['Content-Encoding'] = 'gzip';
res.set(headers);
if (!isGzipped) {
data = zlib.gzipSync(data);
}
return res.status(200).send(data);
}
}
});
}
},
);
app.get('/:id.json', (req, res, next) => {
const item = repo[req.params.id]; const item = repo[req.params.id];
if (!item) { if (!item) {
return res.sendStatus(404); return res.sendStatus(404);
@ -318,20 +185,7 @@ export const serve_data = {
return app; 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 inputFile;
let inputType; let inputType;
if (params.pmtiles) { if (params.pmtiles) {
@ -358,7 +212,7 @@ export const serve_data = {
}; };
if (!isValidHttpUrl(inputFile)) { if (!isValidHttpUrl(inputFile)) {
const inputFileStats = await fsp.stat(inputFile); const inputFileStats = fs.statSync(inputFile);
if (!inputFileStats.isFile() || inputFileStats.size === 0) { if (!inputFileStats.isFile() || inputFileStats.size === 0) {
throw Error(`Not valid input file: "${inputFile}"`); throw Error(`Not valid input file: "${inputFile}"`);
} }
@ -371,8 +225,6 @@ export const serve_data = {
sourceType = 'pmtiles'; sourceType = 'pmtiles';
const metadata = await getPMtilesInfo(source); const metadata = await getPMtilesInfo(source);
tileJSON['encoding'] = params['encoding'];
tileJSON['tileSize'] = params['tileSize'];
tileJSON['name'] = id; tileJSON['name'] = id;
tileJSON['format'] = 'pbf'; tileJSON['format'] = 'pbf';
Object.assign(tileJSON, metadata); Object.assign(tileJSON, metadata);
@ -390,11 +242,17 @@ export const serve_data = {
} }
} else if (inputType === 'mbtiles') { } else if (inputType === 'mbtiles') {
sourceType = 'mbtiles'; sourceType = 'mbtiles';
const mbw = await openMbTilesWrapper(inputFile); const sourceInfoPromise = new Promise((resolve, reject) => {
const info = await mbw.getInfo(); source = new MBTiles(inputFile + '?mode=ro', (err) => {
source = mbw.getMbTiles(); if (err) {
tileJSON['encoding'] = params['encoding']; reject(err);
tileJSON['tileSize'] = params['tileSize']; return;
}
source.getInfo((err, info) => {
if (err) {
reject(err);
return;
}
tileJSON['name'] = id; tileJSON['name'] = id;
tileJSON['format'] = 'pbf'; tileJSON['format'] = 'pbf';
@ -411,6 +269,12 @@ export const serve_data = {
if (options.dataDecoratorFunc) { if (options.dataDecoratorFunc) {
tileJSON = options.dataDecoratorFunc(id, 'tilejson', tileJSON); tileJSON = options.dataDecoratorFunc(id, 'tilejson', tileJSON);
} }
resolve();
});
});
});
await sourceInfoPromise;
} }
repo[id] = { repo[id] = {

View file

@ -4,15 +4,7 @@ import express from 'express';
import { getFontsPbf, listFonts } from './utils.js'; 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 app = express().disable('x-powered-by');
const lastModified = new Date().toUTCString(); const lastModified = new Date().toUTCString();
@ -21,74 +13,31 @@ export async function serve_font(options, allowedFonts, programOpts) {
const existingFonts = {}; const existingFonts = {};
/** app.get(
* Handles requests for a font file. '/fonts/:fontstack/:range([\\d]+-[\\d]+).pbf',
* @param {object} req - Express request object. async (req, res, next) => {
* @param {object} res - Express response object. const fontstack = decodeURI(req.params.fontstack);
* @param {string} req.params.fontstack - Name of the font stack. const range = req.params.range;
* @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 { try {
const concatenated = await getFontsPbf( const concatenated = await getFontsPbf(
options.serveAllFonts ? null : allowedFonts, options.serveAllFonts ? null : allowedFonts,
fontPath, fontPath,
sFontStack, fontstack,
sRange, range,
existingFonts, existingFonts,
); );
res.header('Content-type', 'application/x-protobuf'); res.header('Content-type', 'application/x-protobuf');
res.header('Last-Modified', lastModified); res.header('Last-Modified', lastModified);
return res.send(concatenated); return res.send(concatenated);
} catch (err) { } catch (err) {
console.error( res.status(400).header('Content-Type', 'text/plain').send(err);
`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'); res.header('Content-type', 'application/json');
return res.send( return res.send(
Object.keys(options.serveAllFonts ? existingFonts : allowedFonts).sort(), Object.keys(options.serveAllFonts ? existingFonts : allowedFonts).sort(),
@ -98,4 +47,4 @@ export async function serve_font(options, allowedFonts, programOpts) {
const fonts = await listFonts(options.paths.fonts); const fonts = await listFonts(options.paths.fonts);
Object.assign(existingFonts, fonts); Object.assign(existingFonts, fonts);
return app; return app;
} };

View file

@ -3,12 +3,7 @@
'use strict'; 'use strict';
export const serve_rendered = { export const serve_rendered = {
init: (options, repo, programOpts) => {}, init: (options, repo) => {},
add: (options, repo, params, id, programOpts, dataResolver) => {}, add: (options, repo, params, id, publicUrl, dataResolver) => {},
remove: (repo, id) => {}, 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

View file

@ -7,44 +7,18 @@ import clone from 'clone';
import express from 'express'; import express from 'express';
import { validateStyleMin } from '@maplibre/maplibre-gl-style-spec'; import { validateStyleMin } from '@maplibre/maplibre-gl-style-spec';
import { import { fixUrl, allowedOptions } from './utils.js';
allowedSpriteScales,
allowedSpriteFormats,
fixUrl,
readFile,
} from './utils.js';
const httpTester = /^https?:\/\//i; const httpTester = /^https?:\/\//i;
const allowedSpriteScales = allowedOptions(['', '@2x', '@3x']);
const allowedSpriteFormats = allowedOptions(['png', 'json']);
export const serve_style = { 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'); 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) => { app.get('/:id/style.json', (req, res, next) => {
const { id } = req.params; const item = repo[req.params.id];
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) { if (!item) {
return res.sendStatus(404); return res.sendStatus(404);
} }
@ -52,10 +26,8 @@ export const serve_style = {
for (const name of Object.keys(styleJSON_.sources)) { for (const name of Object.keys(styleJSON_.sources)) {
const source = styleJSON_.sources[name]; const source = styleJSON_.sources[name];
source.url = fixUrl(req, source.url, item.publicUrl); source.url = fixUrl(req, source.url, item.publicUrl);
if (typeof source.data == 'string') {
source.data = fixUrl(req, source.data, item.publicUrl);
}
} }
// mapbox-gl-js viewer cannot handle sprite urls with query
if (styleJSON_.sprite) { if (styleJSON_.sprite) {
if (Array.isArray(styleJSON_.sprite)) { if (Array.isArray(styleJSON_.sprite)) {
styleJSON_.sprite.forEach((spriteItem) => { styleJSON_.sprite.forEach((spriteItem) => {
@ -69,152 +41,59 @@ export const serve_style = {
styleJSON_.glyphs = fixUrl(req, styleJSON_.glyphs, item.publicUrl); styleJSON_.glyphs = fixUrl(req, styleJSON_.glyphs, item.publicUrl);
} }
return res.send(styleJSON_); 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( app.get(
`/:id/sprite{/:spriteID}{@:scale}{.:format}`, '/:id/sprite(/:spriteID)?:scale(@[23]x)?.:format([\\w]+)',
async (req, res, next) => { (req, res, next) => {
const { spriteID = 'default', id, format, scale } = req.params; const { spriteID = 'default', id } = req.params;
const sanitizedId = String(id).replace(/\n|\r/g, ''); const scale = allowedSpriteScales(req.params.scale) || '';
const sanitizedScale = scale ? String(scale).replace(/\n|\r/g, '') : ''; const format = allowedSpriteFormats(req.params.format);
const sanitizedSpriteID = String(spriteID).replace(/\n|\r/g, '');
const sanitizedFormat = format if (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 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( const sprite = item.spritePaths.find(
(sprite) => sprite.id === spriteID, (sprite) => sprite.id === spriteID,
); );
const spriteScale = allowedSpriteScales(scale); if (sprite) {
if (!sprite || spriteScale === null) { const filename = `${sprite.path + scale}.${format}`;
if (verbose) return fs.readFile(filename, (err, data) => {
console.error( if (err) {
`Bad Sprite ID or Scale for: /styles/%s/sprite/%s%s%s`, console.log('Sprite load error:', filename);
sanitizedId, return res.sendStatus(404);
sanitizedSpriteID, } else {
sanitizedScale, if (format === 'json')
sanitizedFormat, res.header('Content-type', 'application/json');
); if (format === 'png') res.header('Content-type', 'image/png');
return res.send(data);
}
});
} else {
return res.status(400).send('Bad Sprite ID or Scale'); return res.status(400).send('Bad Sprite ID or Scale');
} }
} else {
const modifiedSince = req.get('if-modified-since'); return res.status(400).send('Bad Sprite Format');
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; 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]; 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 styleFile = path.resolve(options.paths.styles, params.style);
const styleJSON = clone(style);
let styleFileData;
try {
styleFileData = fs.readFileSync(styleFile);
} catch (e) {
console.log('Error reading style file');
return false;
}
const styleJSON = JSON.parse(styleFileData);
const validationErrors = validateStyleMin(styleJSON); const validationErrors = validateStyleMin(styleJSON);
if (validationErrors.length > 0) { if (validationErrors.length > 0) {
console.log(`The file "${params.style}" is not a valid style file:`); console.log(`The file "${params.style}" is not a valid style file:`);
@ -249,16 +128,6 @@ export const serve_style = {
} }
source.url = `local://data/${identifier}.json`; source.url = `local://data/${identifier}.json`;
} }
let data = source.data;
if (data && typeof data == 'string' && data.startsWith('file://')) {
source.data =
'local://files' +
path.resolve(
'/',
data.replace('file://', '').replace(options.paths.files, ''),
);
}
} }
for (const obj of styleJSON.layers) { for (const obj of styleJSON.layers) {
@ -317,7 +186,6 @@ export const serve_style = {
spritePaths, spritePaths,
publicUrl, publicUrl,
name: styleJSON.name, name: styleJSON.name,
lastModified: new Date().toUTCString(),
}; };
return true; return true;

View file

@ -1,6 +1,9 @@
#!/usr/bin/env node #!/usr/bin/env node
'use strict'; '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 fs from 'node:fs';
import path from 'path'; import path from 'path';
import fnv1a from '@sindresorhus/fnv1a'; import fnv1a from '@sindresorhus/fnv1a';
@ -16,30 +19,25 @@ import morgan from 'morgan';
import { serve_data } from './serve_data.js'; import { serve_data } from './serve_data.js';
import { serve_style } from './serve_style.js'; import { serve_style } from './serve_style.js';
import { serve_font } from './serve_font.js'; import { serve_font } from './serve_font.js';
import { import { getTileUrls, getPublicUrl, isValidHttpUrl } from './utils.js';
allowedTileSizes,
getTileUrls,
getPublicUrl,
isValidHttpUrl,
} from './utils.js';
import { fileURLToPath } from 'url'; import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url)); const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const packageJson = JSON.parse( const packageJson = JSON.parse(
fs.readFileSync(__dirname + '/../package.json', 'utf8'), fs.readFileSync(__dirname + '/../package.json', 'utf8'),
); );
const isLight = packageJson.name.slice(-6) === '-light';
const isLight = packageJson.name.slice(-6) === '-light';
const serve_rendered = ( const serve_rendered = (
await import(`${!isLight ? `./serve_rendered.js` : `./serve_light.js`}`) await import(`${!isLight ? `./serve_rendered.js` : `./serve_light.js`}`)
).serve_rendered; ).serve_rendered;
/** /**
* Starts the server. *
* @param {object} opts - Configuration options for the server. * @param opts
* @returns {Promise<object>} - A promise that resolves to the server object.
*/ */
async function start(opts) { function start(opts) {
console.log('Starting server'); console.log('Starting server');
const app = express().disable('x-powered-by'); const app = express().disable('x-powered-by');
@ -96,31 +94,31 @@ async function start(opts) {
paths.sprites = path.resolve(paths.root, paths.sprites || ''); paths.sprites = path.resolve(paths.root, paths.sprites || '');
paths.mbtiles = path.resolve(paths.root, paths.mbtiles || ''); paths.mbtiles = path.resolve(paths.root, paths.mbtiles || '');
paths.pmtiles = path.resolve(paths.root, paths.pmtiles || ''); paths.pmtiles = path.resolve(paths.root, paths.pmtiles || '');
paths.icons = paths.icons paths.icons = path.resolve(paths.root, paths.icons || '');
? path.resolve(paths.root, paths.icons)
: path.resolve(__dirname, '../public/resources/images');
paths.files = paths.files
? path.resolve(paths.root, paths.files)
: path.resolve(__dirname, '../public/files');
const startupPromises = []; const startupPromises = [];
for (const type of Object.keys(paths)) { const checkPath = (type) => {
if (!fs.existsSync(paths[type])) { if (!fs.existsSync(paths[type])) {
console.error( console.error(
`The specified path for "${type}" does not exist (${paths[type]}).`, `The specified path for "${type}" does not exist (${paths[type]}).`,
); );
process.exit(1); process.exit(1);
} }
} };
checkPath('styles');
checkPath('fonts');
checkPath('sprites');
checkPath('mbtiles');
checkPath('pmtiles');
checkPath('icons');
/** /**
* Recursively get all files within a directory. * Recursively get all files within a directory.
* Inspired by https://stackoverflow.com/a/45130990/10133863 * Inspired by https://stackoverflow.com/a/45130990/10133863
* @param {string} directory Absolute path to a directory to get files from. * @param {string} directory Absolute path to a directory to get files from.
* @returns {Promise<string[]>} - A promise that resolves to an array of file paths relative to the icon directory.
*/ */
async function getFiles(directory) { const getFiles = async (directory) => {
// Fetch all entries of the directory and attach type information // Fetch all entries of the directory and attach type information
const dirEntries = await fs.promises.readdir(directory, { const dirEntries = await fs.promises.readdir(directory, {
withFileTypes: true, withFileTypes: true,
@ -139,7 +137,7 @@ async function start(opts) {
// Flatten the list of files to a single array // Flatten the list of files to a single array
return files.flat(); return files.flat();
} };
// Load all available icons into a settings object // Load all available icons into a settings object
startupPromises.push( startupPromises.push(
@ -162,53 +160,25 @@ async function start(opts) {
app.use(cors()); app.use(cors());
} }
app.use('/data/', serve_data.init(options, serving.data, opts)); app.use('/data/', serve_data.init(options, serving.data));
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) { if (!isLight) {
startupPromises.push( startupPromises.push(
serve_rendered.init(options, serving.rendered, opts).then((sub) => { serve_rendered.init(options, serving.rendered).then((sub) => {
app.use('/styles/', sub); app.use('/styles/', sub);
}), }),
); );
} }
/**
* Adds a style to the server. const addStyle = (id, item, allowMoreData, reportFonts) => {
* @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 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) { if (item.serve_data !== false) {
success = serve_style.add( success = serve_style.add(
options, options,
serving.styles, serving.styles,
item, item,
id, id,
opts, opts.publicUrl,
styleJSON,
(styleSourceId, protocol) => { (styleSourceId, protocol) => {
let dataItemId; let dataItemId;
for (const id of Object.keys(data)) { for (const id of Object.keys(data)) {
@ -265,8 +235,7 @@ async function start(opts) {
serving.rendered, serving.rendered,
item, item,
id, id,
opts, opts.publicUrl,
styleJSON,
function dataResolver(styleSourceId) { function dataResolver(styleSourceId) {
let fileType; let fileType;
let inputFile; let inputFile;
@ -292,8 +261,7 @@ async function start(opts) {
item.serve_rendered = false; item.serve_rendered = false;
} }
} }
return success; };
}
for (const id of Object.keys(config.styles || {})) { for (const id of Object.keys(config.styles || {})) {
const item = config.styles[id]; const item = config.styles[id];
@ -301,13 +269,16 @@ async function start(opts) {
console.log(`Missing "style" property for ${id}`); console.log(`Missing "style" property for ${id}`);
continue; continue;
} }
startupPromises.push(addStyle(id, item, true, true));
addStyle(id, item, true, true);
} }
startupPromises.push( startupPromises.push(
serve_font(options, serving.fonts, opts).then((sub) => { serve_font(options, serving.fonts).then((sub) => {
app.use('/', sub); app.use('/', sub);
}), }),
); );
for (const id of Object.keys(data)) { for (const id of Object.keys(data)) {
const item = data[id]; const item = data[id];
const fileType = Object.keys(data[id])[0]; const fileType = Object.keys(data[id])[0];
@ -317,8 +288,12 @@ async function start(opts) {
); );
continue; continue;
} }
startupPromises.push(serve_data.add(options, serving.data, item, id, opts));
startupPromises.push(
serve_data.add(options, serving.data, item, id, opts.publicUrl),
);
} }
if (options.serveAllStyles) { if (options.serveAllStyles) {
fs.readdir(options.paths.styles, { withFileTypes: true }, (err, files) => { fs.readdir(options.paths.styles, { withFileTypes: true }, (err, files) => {
if (err) { if (err) {
@ -358,13 +333,7 @@ async 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) => { app.get('/styles.json', (req, res, next) => {
const result = []; const result = [];
const query = req.query.key const query = req.query.key
@ -385,15 +354,7 @@ async function start(opts) {
res.send(result); 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])) { for (const id of Object.keys(serving[type])) {
const info = clone(serving[type][id].tileJSON); const info = clone(serving[type][id].tileJSON);
let path = ''; let path = '';
@ -416,42 +377,20 @@ async function start(opts) {
arr.push(info); arr.push(info);
} }
return arr; return arr;
} };
/** app.get('/(:tileSize(256|512)/)?rendered.json', (req, res, next) => {
* Handles requests for a rendered tilejson endpoint. const tileSize = parseInt(req.params.tileSize, 10) || undefined;
* @param {object} req - Express request object. res.send(addTileJSONs([], req, 'rendered', tileSize));
* @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)); 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( res.send(
addTileJSONs( addTileJSONs(
addTileJSONs([], req, 'rendered', parseInt(tileSize, 10)), addTileJSONs([], req, 'rendered', tileSize),
req, req,
'data', 'data',
undefined, undefined,
@ -464,15 +403,7 @@ async function start(opts) {
app.use('/', express.static(path.join(__dirname, '../public/resources'))); app.use('/', express.static(path.join(__dirname, '../public/resources')));
const templates = path.join(__dirname, '../public/templates'); 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`; let templateFile = `${templates}/${template}.tmpl`;
if (template === 'index') { if (template === 'index') {
if (options.frontPage === false) { if (options.frontPage === false) {
@ -484,17 +415,24 @@ async function start(opts) {
templateFile = path.resolve(paths.root, options.frontPage); templateFile = path.resolve(paths.root, options.frontPage);
} }
} }
try { startupPromises.push(
const content = fs.readFileSync(templateFile, 'utf-8'); new Promise((resolve, reject) => {
const compiled = handlebars.compile(content.toString()); fs.readFile(templateFile, (err, content) => {
app.get(urlPath, (req, res, next) => { if (err) {
if (opts.verbose) { err = new Error(`Template not found: ${err.message}`);
console.log(`Serving template at path: ${urlPath}`); reject(err);
return;
} }
const compiled = handlebars.compile(content.toString());
app.use(urlPath, (req, res, next) => {
let data = {}; let data = {};
if (dataGetter) { if (dataGetter) {
data = dataGetter(req); data = dataGetter(req);
if (data) { if (!data) {
return res.status(404).send('Not found');
}
}
data['server_version'] = data['server_version'] =
`${packageJson.name} v${packageJson.version}`; `${packageJson.name} v${packageJson.version}`;
data['public_url'] = opts.publicUrl || '/'; data['public_url'] = opts.publicUrl || '/';
@ -507,27 +445,14 @@ async function start(opts) {
: ''; : '';
if (template === 'wmts') res.set('Content-Type', 'text/xml'); if (template === 'wmts') res.set('Content-Type', 'text/xml');
return res.status(200).send(compiled(data)); return res.status(200).send(compiled(data));
} else {
if (opts.verbose) {
console.log(`Forwarding request for: ${urlPath} to next route`);
}
next('route');
}
}
}); });
} catch (err) { resolve();
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 = {}; let styles = {};
for (const id of Object.keys(serving.styles || {})) { for (const id of Object.keys(serving.styles || {})) {
let style = { let style = {
@ -539,15 +464,11 @@ async function start(opts) {
if (style.serving_rendered) { if (style.serving_rendered) {
const { center } = style.serving_rendered.tileJSON; const { center } = style.serving_rendered.tileJSON;
if (center) { if (center) {
style.viewer_hash = `#${center[2]}/${center[1].toFixed( style.viewer_hash = `#${center[2]}/${center[1].toFixed(5)}/${center[0].toFixed(5)}`;
5,
)}/${center[0].toFixed(5)}`;
const centerPx = mercator.px([center[0], center[1]], center[2]); const centerPx = mercator.px([center[0], center[1]], center[2]);
// Set thumbnail default size to be 256px x 256px // Set thumbnail default size to be 256px x 256px
style.thumbnail = `${Math.floor(center[2])}/${Math.floor( style.thumbnail = `${center[2]}/${Math.floor(centerPx[0] / 256)}/${Math.floor(centerPx[1] / 256)}.png`;
centerPx[0] / 256,
)}/${Math.floor(centerPx[1] / 256)}.png`;
} }
const tileSize = 512; const tileSize = 512;
@ -563,6 +484,7 @@ async function start(opts) {
styles[id] = style; styles[id] = style;
} }
let datas = {}; let datas = {};
for (const id of Object.keys(serving.data || {})) { for (const id of Object.keys(serving.data || {})) {
let data = Object.assign({}, serving.data[id]); let data = Object.assign({}, serving.data[id]);
@ -576,6 +498,14 @@ async function start(opts) {
)}/${center[0].toFixed(5)}`; )}/${center[0].toFixed(5)}`;
} }
data.is_vector = tileJSON.format === 'pbf';
if (!data.is_vector) {
if (center) {
const centerPx = mercator.px([center[0], center[1]], center[2]);
data.thumbnail = `${center[2]}/${Math.floor(centerPx[0] / 256)}/${Math.floor(centerPx[1] / 256)}.${tileJSON.format}`;
}
}
const tileSize = undefined; const tileSize = undefined;
data.xyz_link = getTileUrls( data.xyz_link = getTileUrls(
req, req,
@ -589,29 +519,6 @@ async function start(opts) {
}, },
)[0]; )[0];
data.is_vector = tileJSON.format === 'pbf';
if (!data.is_vector) {
if (
tileJSON.encoding === 'terrarium' ||
tileJSON.encoding === 'mapbox'
) {
if (!isLight) {
data.elevation_link = getTileUrls(
req,
tileJSON.tiles,
`data/${id}/elevation`,
)[0];
}
data.is_terrain = true;
}
if (center) {
const centerPx = mercator.px([center[0], center[1]], center[2]);
data.thumbnail = `${Math.floor(center[2])}/${Math.floor(
centerPx[0] / 256,
)}/${Math.floor(centerPx[1] / 256)}.${tileJSON.format}`;
}
}
if (data.filesize) { if (data.filesize) {
let suffix = 'kB'; let suffix = 'kB';
let size = parseInt(tileJSON.filesize, 10) / 1024; let size = parseInt(tileJSON.filesize, 10) / 1024;
@ -625,28 +532,24 @@ async function start(opts) {
} }
data.formatted_filesize = `${size.toFixed(2)} ${suffix}`; data.formatted_filesize = `${size.toFixed(2)} ${suffix}`;
} }
datas[id] = data; datas[id] = data;
} }
return { return {
styles: Object.keys(styles).length ? styles : null, styles: Object.keys(styles).length ? styles : null,
data: Object.keys(datas).length ? datas : 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 { id } = req.params;
const style = clone(((serving.styles || {})[id] || {}).styleJSON); const style = clone(((serving.styles || {})[id] || {}).styleJSON);
if (!style) { if (!style) {
return null; return null;
} }
return { return {
...style, ...style,
id, id,
@ -656,12 +559,10 @@ async function start(opts) {
}; };
}); });
/** /*
* Handles requests for a Web Map Tile Service (WMTS) XML template. app.use('/rendered/:id/$', function(req, res, next) {
* @param {object} req - Express request object. return res.redirect(301, '/styles/' + req.params.id + '/');
* @param {object} res - Express response object. });
* @param {string} req.params.id - ID of the style.
* @returns {void}
*/ */
serveTemplate('/styles/:id/wmts.xml', 'wmts', (req) => { serveTemplate('/styles/:id/wmts.xml', 'wmts', (req) => {
const { id } = req.params; const { id } = req.params;
@ -694,34 +595,18 @@ async function start(opts) {
}; };
}); });
/** serveTemplate('/data/:id/$', 'data', (req) => {
* Handles requests for a data view template for a specific data source. const { id } = req.params;
* @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]; const data = serving.data[id];
if (!data) { if (!data) {
return null; return null;
} }
const is_terrain =
(data.tileJSON.encoding === 'terrarium' ||
data.tileJSON.encoding === 'mapbox') &&
view === 'preview';
return { return {
...data, ...data,
id, id,
use_maplibre: data.tileJSON.format === 'pbf' || is_terrain, is_vector: data.tileJSON.format === 'pbf',
is_terrain: is_terrain,
is_terrainrgb: data.tileJSON.encoding === 'mapbox',
terrain_encoding: data.tileJSON.encoding,
is_light: isLight,
}; };
}); });
@ -731,13 +616,7 @@ async function start(opts) {
startupComplete = true; 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) { if (startupComplete) {
return res.status(200).send('OK'); return res.status(200).send('OK');
} else { } else {
@ -764,13 +643,12 @@ async function start(opts) {
app, app,
server, server,
startupPromise, startupPromise,
serving,
}; };
} }
/** /**
* Stop the server gracefully * Stop the server gracefully
* @param {string} signal Name of the received signal * @param {string} signal Name of the received signal
* @returns {void}
*/ */
function stopGracefully(signal) { function stopGracefully(signal) {
console.log(`Caught signal ${signal}, stopping gracefully`); console.log(`Caught signal ${signal}, stopping gracefully`);
@ -778,12 +656,11 @@ function stopGracefully(signal) {
} }
/** /**
* Starts and manages the server *
* @param {object} opts - Configuration options for the server. * @param opts
* @returns {Promise<object>} - A promise that resolves to the running server
*/ */
export async function server(opts) { export function server(opts) {
const running = await start(opts); const running = start(opts);
running.startupPromise.catch((err) => { running.startupPromise.catch((err) => {
console.error(err.message); console.error(err.message);
@ -797,16 +674,12 @@ export async function server(opts) {
console.log(`Caught signal ${signal}, refreshing`); console.log(`Caught signal ${signal}, refreshing`);
console.log('Stopping server and reloading config'); console.log('Stopping server and reloading config');
running.server.shutdown(async () => { running.server.shutdown(() => {
const restarted = await start(opts); const restarted = start(opts);
if (!isLight) {
serve_rendered.clear(running.serving.rendered);
}
running.server = restarted.server; running.server = restarted.server;
running.app = restarted.app; running.app = restarted.app;
running.startupPromise = restarted.startupPromise;
running.serving = restarted.serving;
}); });
}); });
return running; return running;
} }

View file

@ -2,22 +2,15 @@
import path from 'path'; import path from 'path';
import fsPromises from 'fs/promises'; import fsPromises from 'fs/promises';
import fs from 'node:fs'; import fs, { existsSync } from 'node:fs';
import clone from 'clone'; import clone from 'clone';
import { combine } from '@jsse/pbfont'; 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. * Restrict user input to an allowed set of options.
* @param {string[]} opts - An array of allowed option strings. * @param opts
* @param {object} [config] - Optional configuration object. * @param root0
* @param {string} [config.defaultValue] - The default value to return if input doesn't match. * @param root0.defaultValue
* @returns {function(string): string} - A function that takes a value and returns it if valid or a default.
*/ */
export function allowedOptions(opts, { defaultValue } = {}) { export function allowedOptions(opts, { defaultValue } = {}) {
const values = Object.fromEntries(opts.map((key) => [key, key])); const values = Object.fromEntries(opts.map((key) => [key, key]));
@ -25,52 +18,10 @@ export function allowedOptions(opts, { defaultValue } = {}) {
} }
/** /**
* Parses a scale string to a number. * Replace local:// urls with public http(s):// urls
* @param {string} scale The scale string (e.g., '2x', '4x'). * @param req
* @param {number} maxScale Maximum allowed scale digit. * @param url
* @returns {number|null} The parsed scale as a number or null if invalid. * @param publicUrl
*/
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) { export function fixUrl(req, url, publicUrl) {
if (!url || typeof url !== 'string' || url.indexOf('local://') !== 0) { if (!url || typeof url !== 'string' || url.indexOf('local://') !== 0) {
@ -88,54 +39,32 @@ export function fixUrl(req, url, publicUrl) {
} }
/** /**
* Generates a new URL object from the Express request. * Generate new URL object
* @param {object} req - Express request object. * @param req
* @returns {URL} - URL object with correct host and optionally path. * @params {object} req - Express request
* @returns {URL} object
*/ */
function getUrlObject(req) { const getUrlObject = (req) => {
const urlObject = new URL(`${req.protocol}://${req.headers.host}/`); const urlObject = new URL(`${req.protocol}://${req.headers.host}/`);
// support overriding hostname by sending X-Forwarded-Host http header // support overriding hostname by sending X-Forwarded-Host http header
urlObject.hostname = req.hostname; urlObject.hostname = req.hostname;
// support overriding port by sending X-Forwarded-Port http header
const xForwardedPort = req.get('X-Forwarded-Port');
if (xForwardedPort) {
urlObject.port = xForwardedPort;
}
// support add url prefix by sending X-Forwarded-Path http header // support add url prefix by sending X-Forwarded-Path http header
const xForwardedPath = req.get('X-Forwarded-Path'); const xForwardedPath = req.get('X-Forwarded-Path');
if (xForwardedPath) { if (xForwardedPath) {
urlObject.pathname = path.posix.join(xForwardedPath, urlObject.pathname); urlObject.pathname = path.posix.join(xForwardedPath, urlObject.pathname);
} }
return urlObject; 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) { if (publicUrl) {
return publicUrl; return publicUrl;
} }
return getUrlObject(req).toString(); 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, req,
domains, domains,
path, path,
@ -143,7 +72,7 @@ export function getTileUrls(
format, format,
publicUrl, publicUrl,
aliases, aliases,
) { ) => {
const urlObject = getUrlObject(req); const urlObject = getUrlObject(req);
if (domains) { if (domains) {
if (domains.constructor === String && domains.length > 0) { if (domains.constructor === String && domains.length > 0) {
@ -189,33 +118,22 @@ export function getTileUrls(
tileParams = `${tileSize}/{z}/{x}/{y}`; tileParams = `${tileSize}/{z}/{x}/{y}`;
} }
if (format && format != '') {
format = `.${format}`;
} else {
format = '';
}
const uris = []; const uris = [];
if (!publicUrl) { if (!publicUrl) {
let xForwardedPath = `${req.get('X-Forwarded-Path') ? '/' + req.get('X-Forwarded-Path') : ''}`; let xForwardedPath = `${req.get('X-Forwarded-Path') ? '/' + req.get('X-Forwarded-Path') : ''}`;
for (const domain of domains) { for (const domain of domains) {
uris.push( uris.push(
`${req.protocol}://${domain}${xForwardedPath}/${path}/${tileParams}${format}${query}`, `${req.protocol}://${domain}${xForwardedPath}/${path}/${tileParams}.${format}${query}`,
); );
} }
} else { } else {
uris.push(`${publicUrl}${path}/${tileParams}${format}${query}`); uris.push(`${publicUrl}${path}/${tileParams}.${format}${query}`);
} }
return uris; 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) { if (tileJSON.bounds && !tileJSON.center) {
const fitWidth = 1024; const fitWidth = 1024;
const tiles = fitWidth / 256; const tiles = fitWidth / 256;
@ -228,77 +146,19 @@ export function fixTileJSONCenter(tileJSON) {
), ),
]; ];
} }
} };
/** const getFontPbf = (allowedFonts, fontPath, name, range, fallbacks) =>
* Reads a file and returns a Promise with the file data. new Promise((resolve, reject) => {
* @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)) { if (!allowedFonts || (allowedFonts[name] && fallbacks)) {
const fontMatch = name?.match(/^[\p{L}\p{N} \-\.~!*'()@&=+,#$\[\]]+$/u); const filename = path.join(fontPath, name, `${range}.pbf`);
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) { if (!fallbacks) {
fallbacks = clone(allowedFonts || {}); fallbacks = clone(allowedFonts || {});
} }
delete fallbacks[name]; delete fallbacks[name];
fs.readFile(filename, (err, data) => {
try { if (err) {
const data = await readFile(filename); console.error(`ERROR: Font not found: ${name}`);
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) { if (fallbacks && Object.keys(fallbacks).length) {
let fallbackName; let fallbackName;
@ -313,37 +173,32 @@ async function getFontPbf(allowedFonts, fontPath, name, range, fallbacks) {
fallbackName = Object.keys(fallbacks)[0]; fallbackName = Object.keys(fallbacks)[0];
} }
} }
console.error(
`ERROR: Trying to use %s as a fallback for: %s`, console.error(`ERROR: Trying to use ${fallbackName} as a fallback`);
fallbackName,
sanitizedName,
);
delete fallbacks[fallbackName]; delete fallbacks[fallbackName];
return getFontPbf(null, fontPath, fallbackName, range, fallbacks); getFontPbf(null, fontPath, fallbackName, range, fallbacks).then(
resolve,
reject,
);
} else { } else {
throw new Error('Font load error'); reject(`Font load error: ${name}`);
}
} }
} else { } else {
throw new Error('Font not allowed'); resolve(data);
} }
});
} else {
reject(`Font not allowed: ${name}`);
} }
/** });
* Combines multiple font pbf buffers into one.
* @param {object} allowedFonts - An object of allowed fonts. export const getFontsPbf = async (
* @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, allowedFonts,
fontPath, fontPath,
names, names,
range, range,
fallbacks, fallbacks,
) { ) => {
const fonts = names.split(','); const fonts = names.split(',');
const queue = []; const queue = [];
for (const font of fonts) { for (const font of fonts) {
@ -360,14 +215,9 @@ export async function getFontsPbf(
const combined = combine(await Promise.all(queue), names); const combined = combine(await Promise.all(queue), names);
return Buffer.from(combined.buffer, 0, combined.buffer.length); 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 existingFonts = {};
const files = await fsPromises.readdir(fontPath); const files = await fsPromises.readdir(fontPath);
@ -375,21 +225,16 @@ export async function listFonts(fontPath) {
const stats = await fsPromises.stat(path.join(fontPath, file)); const stats = await fsPromises.stat(path.join(fontPath, file));
if ( if (
stats.isDirectory() && stats.isDirectory() &&
(await existsP(path.join(fontPath, file, '0-255.pbf'))) existsSync(path.join(fontPath, file, '0-255.pbf'))
) { ) {
existingFonts[path.basename(file)] = true; existingFonts[path.basename(file)] = true;
} }
} }
return existingFonts; 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; let url;
try { try {
@ -399,32 +244,4 @@ export function isValidHttpUrl(string) {
} }
return url.protocol === 'http:' || url.protocol === 'https:'; 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 });
});
});
}
}

View file

@ -7,10 +7,10 @@ import { server } from '../src/server.js';
global.expect = expect; global.expect = expect;
global.supertest = supertest; global.supertest = supertest;
before(async function () { before(function () {
console.log('global setup'); console.log('global setup');
process.chdir('test_data'); process.chdir('test_data');
const running = await server({ const running = server({
configPath: 'config.json', configPath: 'config.json',
port: 8888, port: 8888,
publicUrl: '/test/', publicUrl: '/test/',

View file

@ -78,7 +78,7 @@ describe('Static endpoints', function () {
testStatic(prefix, '0,0,0/256x256', 'png', 404, 1); testStatic(prefix, '0,0,0/256x256', 'png', 404, 1);
testStatic(prefix, '0,0,-1/256x256', 'png', 404); testStatic(prefix, '0,0,-1/256x256', 'png', 404);
testStatic(prefix, '0,0,0/256.5x256.5', 'png', 400); testStatic(prefix, '0,0,0/256.5x256.5', 'png', 404);
testStatic(prefix, '0,0,0,/256x256', 'png', 404); testStatic(prefix, '0,0,0,/256x256', 'png', 404);
testStatic(prefix, '0,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, '0,0,1,1/1x1', 'gif', 400);
testStatic(prefix, '-180,-80,180,80/0.5x2.6', 'png', 400); testStatic(prefix, '-180,-80,180,80/0.5x2.6', 'png', 404);
}); });
}); });

View file

@ -60,16 +60,16 @@ describe('Raster tiles', function () {
describe('invalid requests return 4xx', function () { describe('invalid requests return 4xx', function () {
testTile('non_existent', 256, 0, 0, 0, 'png', 404); testTile('non_existent', 256, 0, 0, 0, 'png', 404);
testTile(prefix, 256, -1, 0, 0, 'png', 400); testTile(prefix, 256, -1, 0, 0, 'png', 404);
testTile(prefix, 256, 25, 0, 0, 'png', 400); testTile(prefix, 256, 25, 0, 0, 'png', 404);
testTile(prefix, 256, 0, 1, 0, 'png', 400); testTile(prefix, 256, 0, 1, 0, 'png', 404);
testTile(prefix, 256, 0, 0, 1, 'png', 400); testTile(prefix, 256, 0, 0, 1, 'png', 404);
testTile(prefix, 256, 0, 0, 0, 'gif', 400); testTile(prefix, 256, 0, 0, 0, 'gif', 400);
testTile(prefix, 256, 0, 0, 0, 'pbf', 400); testTile(prefix, 256, 0, 0, 0, 'pbf', 400);
testTile(prefix, 256, 0, 0, 0, 'png', 400, 1); testTile(prefix, 256, 0, 0, 0, 'png', 404, 1);
testTile(prefix, 256, 0, 0, 0, 'png', 400, 5); testTile(prefix, 256, 0, 0, 0, 'png', 404, 5);
testTile(prefix, 300, 0, 0, 0, 'png', 400); testTile(prefix, 300, 0, 0, 0, 'png', 404);
}); });
}); });