Merge remote-tracking branch 'okimiko/terrain-elevation-contour' into terrain-elevation-contour
This commit is contained in:
commit
4b51f87dd7
29 changed files with 2774 additions and 1368 deletions
81
.github/workflows/release.yml
vendored
81
.github/workflows/release.yml
vendored
|
@ -14,9 +14,41 @@ 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
|
||||||
|
@ -54,17 +86,23 @@ jobs:
|
||||||
- name: Remove Test Data
|
- name: Remove Test Data
|
||||||
run: rm -R test_data*
|
run: rm -R test_data*
|
||||||
|
|
||||||
- name: Publish to Full Version NPM
|
- name: Get release type
|
||||||
|
id: prepare_release
|
||||||
|
run: |
|
||||||
|
RELEASE_TYPE="$(node -e "console.log(require('semver').prerelease('${{ needs.release-check.outputs.version }}') ? 'prerelease' : 'regular')")"
|
||||||
|
if [[ $RELEASE_TYPE == 'regular' ]]; then
|
||||||
|
echo "prerelease=false" >> "$GITHUB_OUTPUT"
|
||||||
|
else
|
||||||
|
echo "prerelease=true" >> "$GITHUB_OUTPUT"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Publish to NPM
|
||||||
run: |
|
run: |
|
||||||
npm config set //registry.npmjs.org/:_authToken ${NPM_TOKEN}
|
npm config set //registry.npmjs.org/:_authToken ${NPM_TOKEN}
|
||||||
npm publish --access public
|
npm publish --access public --tag ${{ steps.prepare_release.outputs.prerelease == 'true' && 'next' || 'latest' }}
|
||||||
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:
|
||||||
|
@ -84,24 +122,42 @@ jobs:
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
push: true
|
push: true
|
||||||
tags: maptiler/tileserver-gl:latest, maptiler/tileserver-gl:v${{ env.PACKAGE_VERSION }}
|
tags: |
|
||||||
|
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 install
|
run: npm ci --prefer-offline --no-audit
|
||||||
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
|
npm publish --access public --tag ${{ steps.prepare_release.outputs.prerelease == 'true' && 'next' || 'latest' }}
|
||||||
env:
|
env:
|
||||||
NPM_TOKEN: ${{ github.event.inputs.npm_token }}
|
NPM_TOKEN: ${{ github.event.inputs.npm_token }}
|
||||||
|
|
||||||
|
@ -111,8 +167,9 @@ jobs:
|
||||||
context: ./light
|
context: ./light
|
||||||
file: ./light/Dockerfile
|
file: ./light/Dockerfile
|
||||||
push: true
|
push: true
|
||||||
tags: maptiler/tileserver-gl-light:latest, maptiler/tileserver-gl-light:v${{ env.PACKAGE_VERSION }}
|
tags: |
|
||||||
|
maptiler/tileserver-gl-light:${{ steps.prepare_release.outputs.prerelease == 'true' && 'next' || 'latest' }},
|
||||||
|
maptiler/tileserver-gl-light:v${{ env.PACKAGE_VERSION }}
|
||||||
platforms: linux/arm64,linux/amd64
|
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
|
||||||
|
|
22
CHANGELOG.md
Normal file
22
CHANGELOG.md
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
# tileserver-gl changelog
|
||||||
|
|
||||||
|
## 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
|
|
@ -35,7 +35,7 @@ SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
||||||
|
|
||||||
RUN mkdir -p /etc/apt/keyrings; \
|
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; \
|
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg; \
|
||||||
echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_20.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list; \
|
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 -qq update; \
|
||||||
apt-get install -y nodejs; \
|
apt-get install -y nodejs; \
|
||||||
npm i -g npm@latest; \
|
npm i -g npm@latest; \
|
||||||
|
@ -94,7 +94,7 @@ SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
||||||
|
|
||||||
RUN mkdir -p /etc/apt/keyrings; \
|
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; \
|
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg; \
|
||||||
echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_20.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list; \
|
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 -qq update; \
|
||||||
apt-get install -y nodejs; \
|
apt-get install -y nodejs; \
|
||||||
npm i -g npm@latest; \
|
npm i -g npm@latest; \
|
||||||
|
|
|
@ -16,7 +16,7 @@ RUN set -ex; \
|
||||||
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_20.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_22.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list; \
|
||||||
apt-get -qq update; \
|
apt-get -qq update; \
|
||||||
apt-get install -y nodejs; \
|
apt-get install -y nodejs; \
|
||||||
npm i -g npm@latest; \
|
npm i -g npm@latest; \
|
||||||
|
|
|
@ -13,7 +13,8 @@ RUN set -ex; \
|
||||||
unzip \
|
unzip \
|
||||||
build-essential \
|
build-essential \
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
wget \
|
curl \
|
||||||
|
gnupg \
|
||||||
pkg-config \
|
pkg-config \
|
||||||
xvfb \
|
xvfb \
|
||||||
libglfw3-dev \
|
libglfw3-dev \
|
||||||
|
@ -25,16 +26,33 @@ 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 \
|
||||||
wget -qO- https://deb.nodesource.com/setup_18.x | bash; \
|
libpixman-1-0; \
|
||||||
|
apt-get -y --purge autoremove; \
|
||||||
|
apt-get clean; \
|
||||||
|
rm -rf /var/lib/apt/lists/*;
|
||||||
|
|
||||||
|
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
||||||
|
|
||||||
|
RUN mkdir -p /etc/apt/keyrings; \
|
||||||
|
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg; \
|
||||||
|
echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_22.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list; \
|
||||||
|
apt-get -qq update; \
|
||||||
apt-get install -y nodejs; \
|
apt-get install -y nodejs; \
|
||||||
apt-get clean;
|
npm i -g npm@latest; \
|
||||||
|
apt-get -y remove gnupg; \
|
||||||
|
apt-get -y --purge autoremove; \
|
||||||
|
apt-get clean; \
|
||||||
|
rm -rf /var/lib/apt/lists/*;
|
||||||
|
|
||||||
RUN mkdir -p /usr/src/app
|
RUN mkdir -p /usr/src/app
|
||||||
WORKDIR /usr/src/app
|
WORKDIR /usr/src/app
|
||||||
|
|
||||||
RUN wget -O test_data.zip https://github.com/maptiler/tileserver-gl/releases/download/v1.3.0/test_data.zip; \
|
RUN curl -L -o test_data.zip https://github.com/maptiler/tileserver-gl/releases/download/v1.3.0/test_data.zip; \
|
||||||
unzip -q test_data.zip -d test_data
|
unzip -q test_data.zip -d test_data
|
||||||
|
|
||||||
COPY package.json .
|
COPY package.json .
|
||||||
|
|
|
@ -1,13 +1,19 @@
|
||||||
# Publishing new version
|
# Publishing new version
|
||||||
|
|
||||||
- Update version in `package.json`
|
1.) Change the version number in package.json. Run the following command in the package root directory, replacing <update_type> with one of the semantic versioning release types (prerelease, prepatch, preminor, premajor, patch, minor, major):
|
||||||
- `git tag vx.x.x`
|
npm version <update_type> --preid pre --no-git-tag-version
|
||||||
- `git push --tags`
|
|
||||||
- `docker buildx build --platform linux/amd64 -t maptiler/tileserver-gl:latest -t maptiler/tileserver-gl:[version] .`
|
--preid specifies which suffix to use in the release such as pre, next, beta, rc, etc.
|
||||||
- `docker push maptiler/tileserver-gl --all-tags`
|
|
||||||
- `npm publish --access public` or `node publish.js`
|
prepatch, preminor, and premajor start a new series of pre-releases while bumping the patch, minor, or major version. E.g. premajor with --preid pre would do a prerelease for a new major using the -pre suffix (i.e. it would be a new major with -pre.0)
|
||||||
- `node publish.js --no-publish`
|
|
||||||
- `cd light`
|
You can use prerelease to bump the version for a new pre-release version. E.g. you could run npm version prerelease --preid pre --no-git-tag-version to go from -pre.0 to -pre.1.
|
||||||
- `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`
|
For regular versions, you can use patch, minor, or major. E.g. npm version major --no-git-tag-version.
|
||||||
- `npm publish --access public`
|
|
||||||
|
2.) Update the changelog, which can be found in CHANGELOG.md. The heading must match ## <VERSION> exactly, or it will not be picked up. For example, for version 5.0.0:
|
||||||
|
```## 5.0.0```
|
||||||
|
|
||||||
|
3.) Commit and push the changes.
|
||||||
|
|
||||||
|
4.) Run the 'Build, Test, Release' github workflow. The workflow will create a NPM, Docker, and Github release and Tag.
|
||||||
|
|
10
README.md
10
README.md
|
@ -9,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 20 is recommended. (running `node -v` it should output something like `v20.x.x`). Running without docker requires [Native dependencies](https://maptiler-tileserver.readthedocs.io/en/latest/installation.html#npm) to be installed first.
|
Make sure you have Node.js version **18.17.0** or above installed. Node 22 is recommended. (running `node -v` it should output something like `v22.x.x`). Running without docker requires [Native dependencies](https://maptiler-tileserver.readthedocs.io/en/latest/installation.html#npm) to be installed first.
|
||||||
|
|
||||||
Install `tileserver-gl` with server-side raster rendering of vector tiles with npm.
|
Install `tileserver-gl` with server-side raster rendering of vector tiles with npm.
|
||||||
|
|
||||||
|
@ -43,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 --file zurich_switzerland.mbtiles
|
docker run --rm -it -v $(pwd):/data -p 8080:8080 maptiler/tileserver-gl:latest --file zurich_switzerland.mbtiles
|
||||||
[in your browser, visit http://[server ip]:8080]
|
[in your browser, visit http://[server ip]:8080]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -51,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
|
docker run --rm -it -v $(pwd):/data -p 8080:8080 maptiler/tileserver-gl:latest
|
||||||
[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
|
docker run --rm -it -v /your/local/config/path:/data -p 8080:8080 maptiler/tileserver-gl:latest
|
||||||
```
|
```
|
||||||
replace '/your/local/config/path' with the path to your config file
|
replace '/your/local/config/path' with the path to your config file
|
||||||
|
|
||||||
|
|
||||||
Alternatively, you can use the `maptiler/tileserver-gl-light` docker image instead, which is pure javascript, does not have any native dependencies, and can run anywhere, but does not contain rasterization on the server side made with Maplibre GL Native.
|
Alternatively, you can use the `maptiler/tileserver-gl-light:latest` docker image instead, which is pure javascript, does not have any native dependencies, and can run anywhere, but does not contain rasterization on the server side made with Maplibre GL Native.
|
||||||
|
|
||||||
## Getting Started with Linux cli
|
## Getting Started with Linux cli
|
||||||
|
|
||||||
|
|
|
@ -108,6 +108,8 @@ Source data
|
||||||
|
|
||||||
* the result will be a json object like ``{"z":7,"x":68,"y":45,"red":134,"green":66,"blue":0,"latitude":11.84069,"longitude":46.04798,"elevation":1602}``
|
* the 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
|
||||||
===========
|
===========
|
||||||
* Static files are served at ``/files/{filename}``
|
* Static files are served at ``/files/{filename}``
|
||||||
|
|
857
package-lock.json
generated
857
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "tileserver-gl",
|
"name": "tileserver-gl",
|
||||||
"version": "5.0.0",
|
"version": "5.1.3",
|
||||||
"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",
|
||||||
|
@ -28,13 +28,13 @@
|
||||||
"@sindresorhus/fnv1a": "3.1.0",
|
"@sindresorhus/fnv1a": "3.1.0",
|
||||||
"advanced-pool": "0.3.3",
|
"advanced-pool": "0.3.3",
|
||||||
"axios": "^1.7.7",
|
"axios": "^1.7.7",
|
||||||
"canvas": "2.11.2",
|
"canvas": "3.0.1",
|
||||||
"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",
|
||||||
"cors": "2.8.5",
|
"cors": "2.8.5",
|
||||||
"express": "4.19.2",
|
"express": "5.0.1",
|
||||||
"handlebars": "4.7.8",
|
"handlebars": "4.7.8",
|
||||||
"http-shutdown": "1.2.2",
|
"http-shutdown": "1.2.2",
|
||||||
"maplibre-contour": "^0.1.0",
|
"maplibre-contour": "^0.1.0",
|
||||||
|
@ -43,6 +43,7 @@
|
||||||
"pmtiles": "3.0.7",
|
"pmtiles": "3.0.7",
|
||||||
"proj4": "2.12.1",
|
"proj4": "2.12.1",
|
||||||
"sanitize-filename": "1.6.3",
|
"sanitize-filename": "1.6.3",
|
||||||
|
"semver": "^7.6.3",
|
||||||
"sharp": "0.33.5",
|
"sharp": "0.33.5",
|
||||||
"tileserver-gl-styles": "2.0.0"
|
"tileserver-gl-styles": "2.0.0"
|
||||||
},
|
},
|
||||||
|
@ -73,7 +74,7 @@
|
||||||
],
|
],
|
||||||
"license": "BSD-2-Clause",
|
"license": "BSD-2-Clause",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.17.0 <21"
|
"node": ">=18.17.0 <23"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"url": "git+https://github.com/maptiler/tileserver-gl.git",
|
"url": "git+https://github.com/maptiler/tileserver-gl.git",
|
||||||
|
|
65
public/resources/contour-control.js
Normal file
65
public/resources/contour-control.js
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
class MaplibreContourControl {
|
||||||
|
constructor(options) {
|
||||||
|
this.source = options["source"];
|
||||||
|
this.confLayers = options["layers"];
|
||||||
|
this.visibility = options["visibility"];
|
||||||
|
}
|
||||||
|
|
||||||
|
getDefaultPosition() {
|
||||||
|
const defaultPosition = "top-right";
|
||||||
|
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.contourButton = document.createElement("button");
|
||||||
|
this.contourButton.type = "button";
|
||||||
|
this.contourButton.textContent = "C";
|
||||||
|
|
||||||
|
this.map.on("style.load", () => {
|
||||||
|
this.confLayers.forEach(layer => {
|
||||||
|
this.map.setLayoutProperty(layer, "visibility", this.visibility ? "visible" : "none");
|
||||||
|
if (this.visibility) {
|
||||||
|
this.controlContainer.classList.add("maplibre-ctrl-contour-active");
|
||||||
|
this.contourButton.title = "Disable Contours";
|
||||||
|
} else {
|
||||||
|
this.contourButton.title = "Enable Contours";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
this.contourButton.addEventListener("click", () => {
|
||||||
|
this.confLayers.forEach(layer => {
|
||||||
|
var visibility = this.map.getLayoutProperty(layer, "visibility");
|
||||||
|
if (visibility === "visible") {
|
||||||
|
this.map.setLayoutProperty(layer, "visibility", "none");
|
||||||
|
this.controlContainer.classList.remove("maplibre-ctrl-contour-active");
|
||||||
|
this.contourButton.title = "Disable Contours";
|
||||||
|
} else {
|
||||||
|
this.controlContainer.classList.add("maplibre-ctrl-contour-active");
|
||||||
|
this.map.setLayoutProperty(layer, "visibility", "visible");
|
||||||
|
this.contourButton.title = "Enable Contours";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
this.controlContainer.appendChild(this.contourButton);
|
||||||
|
return this.controlContainer;
|
||||||
|
}
|
||||||
|
|
||||||
|
onRemove() {
|
||||||
|
if (
|
||||||
|
!this.controlContainer ||
|
||||||
|
!this.controlContainer.parentNode ||
|
||||||
|
!this.map ||
|
||||||
|
!this.contourButton
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.contourButton.removeEventListener("click");
|
||||||
|
this.controlContainer.parentNode.removeChild(this.controlContainer);
|
||||||
|
this.map = undefined;
|
||||||
|
}
|
||||||
|
};
|
51
public/resources/elevation-control.js
Normal file
51
public/resources/elevation-control.js
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
class ElevationInfoControl {
|
||||||
|
constructor(options) {
|
||||||
|
this.url = options["url"];
|
||||||
|
}
|
||||||
|
|
||||||
|
getDefaultPosition() {
|
||||||
|
const defaultPosition = "bottom-left";
|
||||||
|
return defaultPosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
onAdd(map) {
|
||||||
|
this.map = map;
|
||||||
|
this.controlContainer = document.createElement("div");
|
||||||
|
this.controlContainer.classList.add("maplibregl-ctrl");
|
||||||
|
this.controlContainer.classList.add("maplibregl-ctrl-group");
|
||||||
|
this.controlContainer.classList.add("maplibre-ctrl-elevation");
|
||||||
|
this.controlContainer.textContent = "Elevation: Click on Map";
|
||||||
|
|
||||||
|
map.on('click', (e) => {
|
||||||
|
var url = this.url;
|
||||||
|
var coord = {"z": Math.floor(map.getZoom()), "x": e.lngLat["lng"], "y": e.lngLat["lat"]};
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
};
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -9,17 +9,25 @@
|
||||||
<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>
|
||||||
|
<script src="{{public_url}}contour-control.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}}
|
{{^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}}
|
||||||
{{#is_terrain}}
|
{{#is_terrain}}
|
||||||
#map { position:absolute; top:0; bottom:0; width:100%; }
|
#map { position:absolute; top:0; bottom:0; left:0; right:0; }
|
||||||
{{/is_terrain}}
|
{{/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;}
|
||||||
|
.maplibre-ctrl-contour-active button { color: #33b5e5; font-weight: bold; }
|
||||||
|
{{^is_light}}
|
||||||
|
.maplibre-ctrl-elevation { padding-left: 5px; padding-right: 5px; }
|
||||||
|
{{/is_light}}
|
||||||
</style>
|
</style>
|
||||||
{{/use_maplibre}}
|
{{/use_maplibre}}
|
||||||
{{^use_maplibre}}
|
{{^use_maplibre}}
|
||||||
|
@ -69,10 +77,9 @@
|
||||||
};
|
};
|
||||||
{{/is_terrain}}
|
{{/is_terrain}}
|
||||||
{{#is_terrain}}
|
{{#is_terrain}}
|
||||||
|
|
||||||
let baseUrl = window.location.origin;
|
let baseUrl = window.location.origin;
|
||||||
console.log(baseUrl);
|
|
||||||
baseUrl = baseUrl + "/data/{{id}}/contour/{z}/{x}/{y}"
|
|
||||||
console.log(baseUrl);
|
|
||||||
var style = {
|
var style = {
|
||||||
version: 8,
|
version: 8,
|
||||||
sources: {
|
sources: {
|
||||||
|
@ -88,9 +95,10 @@
|
||||||
},
|
},
|
||||||
"contour": {
|
"contour": {
|
||||||
"type": "vector",
|
"type": "vector",
|
||||||
"tiles": [ baseUrl ],
|
"tiles": [ baseUrl + "/data/{{id}}/contour/{z}/{x}/{y}" ],
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"glyphs": "/fonts/{fontstack}/{range}.pbf",
|
||||||
"terrain": {
|
"terrain": {
|
||||||
"source": "terrain"
|
"source": "terrain"
|
||||||
},
|
},
|
||||||
|
@ -98,7 +106,11 @@
|
||||||
{
|
{
|
||||||
"id": "background",
|
"id": "background",
|
||||||
"paint": {
|
"paint": {
|
||||||
|
{{#if is_terrainrgb}}
|
||||||
"background-color": "hsl(190, 99%, 63%)"
|
"background-color": "hsl(190, 99%, 63%)"
|
||||||
|
{{else}}
|
||||||
|
"background-color": "hsl(0, 100%, 25%)"
|
||||||
|
{{/if}}
|
||||||
},
|
},
|
||||||
"type": "background"
|
"type": "background"
|
||||||
},
|
},
|
||||||
|
@ -118,9 +130,26 @@
|
||||||
"source": "contour",
|
"source": "contour",
|
||||||
"source-layer": "contours",
|
"source-layer": "contours",
|
||||||
"paint": {
|
"paint": {
|
||||||
"line-opacity": 0.5,
|
"line-opacity": 1,
|
||||||
"line-width": ["match", ["get", "level"], 1, 1, 0.5]
|
"line-width": ["match", ["get", "level"], 1, 1, 0.5]
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "contour-label",
|
||||||
|
"type": "symbol",
|
||||||
|
"source": "contour",
|
||||||
|
"source-layer": "contours",
|
||||||
|
"filter": [">", ["get", "ele"], 0 ],
|
||||||
|
"paint": {
|
||||||
|
"text-halo-color": "white",
|
||||||
|
"text-halo-width": 1
|
||||||
|
},
|
||||||
|
"layout": {
|
||||||
|
"symbol-placement": "line",
|
||||||
|
"text-size": 10,
|
||||||
|
"text-field": "{ele}",
|
||||||
|
"text-font": ["Noto Sans Bold"]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
@ -132,24 +161,44 @@
|
||||||
maxPitch: 85,
|
maxPitch: 85,
|
||||||
style: style
|
style: style
|
||||||
});
|
});
|
||||||
|
|
||||||
map.addControl(new maplibregl.NavigationControl({
|
map.addControl(new maplibregl.NavigationControl({
|
||||||
visualizePitch: true,
|
visualizePitch: true,
|
||||||
showZoom: true,
|
showZoom: true,
|
||||||
showCompass: true
|
showCompass: true
|
||||||
}));
|
}));
|
||||||
{{#is_terrain}}
|
{{#is_terrain}}
|
||||||
|
|
||||||
map.addControl(
|
map.addControl(
|
||||||
new maplibregl.TerrainControl({
|
new maplibregl.TerrainControl({
|
||||||
source: "terrain",
|
source: "terrain",
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
map.addControl(
|
||||||
|
new MaplibreContourControl({
|
||||||
|
source: "contour",
|
||||||
|
visibility: false,
|
||||||
|
layers: [ "contours", "contour-label" ]
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
{{^is_light}}
|
||||||
|
map.addControl(
|
||||||
|
new ElevationInfoControl({
|
||||||
|
url: "{{public_url}}data/{{id}}/elevation/{z}/{x}/{y}"
|
||||||
|
})
|
||||||
|
);
|
||||||
|
{{/is_light}}
|
||||||
{{/is_terrain}}
|
{{/is_terrain}}
|
||||||
{{^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 = '';
|
||||||
|
|
|
@ -124,9 +124,9 @@
|
||||||
{{/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>
|
||||||
{{#elevation_link}}
|
{{#is_terrain}}
|
||||||
<a class="btn" href="{{public_url}}data/preview/{{@key}}/{{&../key_query}}{{viewer_hash}}">Preview</a>
|
<a class="btn" href="{{public_url}}data/preview/{{@key}}/{{&../key_query}}{{viewer_hash}}">Preview Terrain</a>
|
||||||
{{/elevation_link}}
|
{{/is_terrain}}
|
||||||
{{/is_vector}}
|
{{/is_vector}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -107,7 +107,7 @@ export class LocalDemManager {
|
||||||
* @throws If an error occurs fetching or processing the tile.
|
* @throws If an error occurs fetching or processing the tile.
|
||||||
*/
|
*/
|
||||||
async GetTile(url, abortController) {
|
async GetTile(url, abortController) {
|
||||||
console.log(url);
|
//console.log(url);
|
||||||
const $zxy = this.extractZXYFromUrlTrim(url);
|
const $zxy = this.extractZXYFromUrlTrim(url);
|
||||||
if (!$zxy) {
|
if (!$zxy) {
|
||||||
throw new Error(`Could not extract zxy from $url`);
|
throw new Error(`Could not extract zxy from $url`);
|
||||||
|
@ -171,7 +171,7 @@ export class LocalDemManager {
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.name === 'AbortError') {
|
if (error.name === 'AbortError') {
|
||||||
console.log('fetch cancelled');
|
console.log('fetch canceled');
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
throw error;
|
throw error;
|
||||||
|
|
|
@ -1,6 +1,12 @@
|
||||||
#!/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 fsp from 'node:fs/promises';
|
||||||
|
|
|
@ -8,34 +8,78 @@ import express from 'express';
|
||||||
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 SphericalMercator from '@mapbox/sphericalmercator';
|
||||||
import { Image, createCanvas } from 'canvas';
|
|
||||||
import sharp from 'sharp';
|
|
||||||
|
|
||||||
import { LocalDemManager } from './contour.js';
|
import { LocalDemManager } from './contour.js';
|
||||||
import { fixTileJSONCenter, getTileUrls, isValidHttpUrl } from './utils.js';
|
|
||||||
import {
|
import {
|
||||||
getPMtilesInfo,
|
fixTileJSONCenter,
|
||||||
getPMtilesTile,
|
getTileUrls,
|
||||||
openPMtiles,
|
isValidHttpUrl,
|
||||||
} from './pmtiles_adapter.js';
|
fetchTileData,
|
||||||
|
} from './utils.js';
|
||||||
|
import { getPMtilesInfo, openPMtiles } from './pmtiles_adapter.js';
|
||||||
import { gunzipP, gzipP } from './promises.js';
|
import { gunzipP, gzipP } from './promises.js';
|
||||||
import { openMbTilesWrapper } from './mbtiles_wrapper.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(
|
/**
|
||||||
'/:id/:z(\\d+)/:x(\\d+)/:y(\\d+).:format([\\w.]+)',
|
* Handles requests for tile data, responding with the tile image.
|
||||||
async (req, res, next) => {
|
* @param {object} req - Express request object.
|
||||||
|
* @param {object} res - Express response object.
|
||||||
|
* @param {string} req.params.id - ID of the tile.
|
||||||
|
* @param {string} req.params.z - Z coordinate of the tile.
|
||||||
|
* @param {string} req.params.x - X coordinate of the tile.
|
||||||
|
* @param {string} req.params.y - Y coordinate of the tile.
|
||||||
|
* @param {string} req.params.format - Format of the tile.
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
app.get('/:id/:z/:x/:y.:format', async (req, res) => {
|
||||||
|
if (verbose) {
|
||||||
|
console.log(
|
||||||
|
`Handling tile request for: /data/%s/%s/%s/%s.%s`,
|
||||||
|
String(req.params.id).replace(/\n|\r/g, ''),
|
||||||
|
String(req.params.z).replace(/\n|\r/g, ''),
|
||||||
|
String(req.params.x).replace(/\n|\r/g, ''),
|
||||||
|
String(req.params.y).replace(/\n|\r/g, ''),
|
||||||
|
String(req.params.format).replace(/\n|\r/g, ''),
|
||||||
|
);
|
||||||
|
}
|
||||||
const item = repo[req.params.id];
|
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 = req.params.z | 0;
|
const z = parseInt(req.params.z, 10);
|
||||||
const x = req.params.x | 0;
|
const x = parseInt(req.params.x, 10);
|
||||||
const y = req.params.y | 0;
|
const y = parseInt(req.params.y, 10);
|
||||||
|
if (isNaN(z) || isNaN(x) || isNaN(y)) {
|
||||||
|
return res.status(404).send('Invalid Tile');
|
||||||
|
}
|
||||||
|
|
||||||
let format = req.params.format;
|
let format = req.params.format;
|
||||||
if (format === options.pbfAlias) {
|
if (format === options.pbfAlias) {
|
||||||
format = 'pbf';
|
format = 'pbf';
|
||||||
|
@ -48,7 +92,6 @@ 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 ||
|
||||||
|
@ -57,18 +100,37 @@ export const serve_data = {
|
||||||
) {
|
) {
|
||||||
return res.status(404).send('Out of bounds');
|
return res.status(404).send('Out of bounds');
|
||||||
}
|
}
|
||||||
if (item.sourceType === 'pmtiles') {
|
|
||||||
let tileinfo = await getPMtilesTile(item.source, z, x, y);
|
const fetchTile = await fetchTileData(
|
||||||
if (tileinfo == undefined || tileinfo.data == undefined) {
|
item.source,
|
||||||
return res.status(404).send('Not found');
|
item.sourceType,
|
||||||
} else {
|
z,
|
||||||
let data = tileinfo.data;
|
x,
|
||||||
let headers = tileinfo.header;
|
y,
|
||||||
|
);
|
||||||
|
if (fetchTile == null) return res.status(204).send();
|
||||||
|
|
||||||
|
let data = fetchTile.data;
|
||||||
|
let headers = fetchTile.headers;
|
||||||
|
let isGzipped = data.slice(0, 2).indexOf(Buffer.from([0x1f, 0x8b])) === 0;
|
||||||
|
|
||||||
if (tileJSONFormat === 'pbf') {
|
if (tileJSONFormat === 'pbf') {
|
||||||
if (options.dataDecoratorFunc) {
|
if (options.dataDecoratorFunc) {
|
||||||
data = options.dataDecoratorFunc(id, 'data', data, z, x, y);
|
if (isGzipped) {
|
||||||
|
data = await gunzipP(data);
|
||||||
|
isGzipped = false;
|
||||||
|
}
|
||||||
|
data = options.dataDecoratorFunc(
|
||||||
|
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') {
|
||||||
|
@ -89,68 +151,9 @@ export const serve_data = {
|
||||||
}
|
}
|
||||||
data = JSON.stringify(geojson);
|
data = JSON.stringify(geojson);
|
||||||
}
|
}
|
||||||
delete headers['ETag']; // do not trust the tile ETag -- regenerate
|
if (headers) {
|
||||||
headers['Content-Encoding'] = 'gzip';
|
delete headers['ETag'];
|
||||||
res.set(headers);
|
|
||||||
|
|
||||||
data = await gzipP(data);
|
|
||||||
|
|
||||||
return res.status(200).send(data);
|
|
||||||
}
|
}
|
||||||
} else if (item.sourceType === 'mbtiles') {
|
|
||||||
item.source.getTile(z, x, y, async (err, data, headers) => {
|
|
||||||
let isGzipped;
|
|
||||||
if (err) {
|
|
||||||
if (/does not exist/.test(err.message)) {
|
|
||||||
return res.status(204).send();
|
|
||||||
} else {
|
|
||||||
return res
|
|
||||||
.status(500)
|
|
||||||
.header('Content-Type', 'text/plain')
|
|
||||||
.send(err.message);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (data == null) {
|
|
||||||
return res.status(404).send('Not found');
|
|
||||||
} else {
|
|
||||||
if (tileJSONFormat === 'pbf') {
|
|
||||||
isGzipped =
|
|
||||||
data.slice(0, 2).indexOf(Buffer.from([0x1f, 0x8b])) === 0;
|
|
||||||
if (options.dataDecoratorFunc) {
|
|
||||||
if (isGzipped) {
|
|
||||||
data = await gunzipP(data);
|
|
||||||
isGzipped = false;
|
|
||||||
}
|
|
||||||
data = options.dataDecoratorFunc(id, 'data', data, z, x, y);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (format === 'pbf') {
|
|
||||||
headers['Content-Type'] = 'application/x-protobuf';
|
|
||||||
} else if (format === 'geojson') {
|
|
||||||
headers['Content-Type'] = 'application/json';
|
|
||||||
|
|
||||||
if (isGzipped) {
|
|
||||||
data = await gunzipP(data);
|
|
||||||
isGzipped = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const tile = new VectorTile(new Pbf(data));
|
|
||||||
const geojson = {
|
|
||||||
type: 'FeatureCollection',
|
|
||||||
features: [],
|
|
||||||
};
|
|
||||||
for (const layerName in tile.layers) {
|
|
||||||
const layer = tile.layers[layerName];
|
|
||||||
for (let i = 0; i < layer.length; i++) {
|
|
||||||
const feature = layer.feature(i);
|
|
||||||
const featureGeoJSON = feature.toGeoJSON(x, y, z);
|
|
||||||
featureGeoJSON.properties.layer = layerName;
|
|
||||||
geojson.features.push(featureGeoJSON);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
data = JSON.stringify(geojson);
|
|
||||||
}
|
|
||||||
delete headers['ETag']; // do not trust the tile ETag -- regenerate
|
|
||||||
headers['Content-Encoding'] = 'gzip';
|
headers['Content-Encoding'] = 'gzip';
|
||||||
res.set(headers);
|
res.set(headers);
|
||||||
|
|
||||||
|
@ -159,23 +162,34 @@ export const serve_data = {
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.status(200).send(data);
|
return res.status(200).send(data);
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
app.get(
|
/**
|
||||||
'^/:id/contour/:z([0-9]+)/:x([-.0-9]+)/:y([-.0-9]+)',
|
* Handles requests for contour data.
|
||||||
async (req, res, next) => {
|
* @param {object} req - Express request object.
|
||||||
|
* @param {object} res - Express response object.
|
||||||
|
* @param {string} req.params.id - ID of the contour 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/contour/:z/:x/:y', async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
|
if (verbose) {
|
||||||
|
console.log(
|
||||||
|
`Handling contour request for: /data/%s/contour/%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];
|
const item = repo?.[req.params.id];
|
||||||
if (!item) return res.sendStatus(404);
|
if (!item) return res.sendStatus(404);
|
||||||
if (!item.source) return res.status(404).send('Missing source');
|
if (!item.source) return res.status(404).send('Missing source');
|
||||||
if (!item.tileJSON) return res.status(404).send('Missing tileJSON');
|
if (!item.tileJSON) return res.status(404).send('Missing tileJSON');
|
||||||
if (!item.sourceType)
|
if (!item.sourceType) return res.status(404).send('Missing sourceType');
|
||||||
return res.status(404).send('Missing sourceType');
|
|
||||||
|
|
||||||
const { source, tileJSON, sourceType } = item;
|
const { source, tileJSON, sourceType } = item;
|
||||||
|
|
||||||
|
@ -246,7 +260,6 @@ export const serve_data = {
|
||||||
{ levels: [levels] },
|
{ levels: [levels] },
|
||||||
new AbortController(),
|
new AbortController(),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Set the Content-Type header here
|
// Set the Content-Type header here
|
||||||
res.setHeader('Content-Type', 'application/x-protobuf');
|
res.setHeader('Content-Type', 'application/x-protobuf');
|
||||||
res.setHeader('Content-Encoding', 'gzip');
|
res.setHeader('Content-Encoding', 'gzip');
|
||||||
|
@ -259,28 +272,40 @@ export const serve_data = {
|
||||||
.header('Content-Type', 'text/plain')
|
.header('Content-Type', 'text/plain')
|
||||||
.send(err.message);
|
.send(err.message);
|
||||||
}
|
}
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
app.get(
|
/**
|
||||||
'^/:id/elevation/:z([0-9]+)/:x([-.0-9]+)/:y([-.0-9]+)',
|
* Handles requests for elevation data.
|
||||||
async (req, res, next) => {
|
* @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 {
|
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];
|
const item = repo?.[req.params.id];
|
||||||
if (!item) return res.sendStatus(404);
|
if (!item) return res.sendStatus(404);
|
||||||
if (!item.source) return res.status(404).send('Missing source');
|
if (!item.source) return res.status(404).send('Missing source');
|
||||||
if (!item.tileJSON) return res.status(404).send('Missing tileJSON');
|
if (!item.tileJSON) return res.status(404).send('Missing tileJSON');
|
||||||
if (!item.sourceType)
|
if (!item.sourceType) return res.status(404).send('Missing sourceType');
|
||||||
return res.status(404).send('Missing sourceType');
|
|
||||||
|
|
||||||
const { source, tileJSON, sourceType } = item;
|
const { source, tileJSON, sourceType } = item;
|
||||||
|
|
||||||
if (sourceType !== 'pmtiles' && sourceType !== 'mbtiles') {
|
if (sourceType !== 'pmtiles' && sourceType !== 'mbtiles') {
|
||||||
return res
|
return res
|
||||||
.status(400)
|
.status(400)
|
||||||
.send('Invalid sourceType. Must be pmtiles or mbtiles.');
|
.send('Invalid sourceType. Must be pmtiles or mbtiles.');
|
||||||
}
|
}
|
||||||
|
|
||||||
const encoding = tileJSON?.encoding;
|
const encoding = tileJSON?.encoding;
|
||||||
if (encoding == null) {
|
if (encoding == null) {
|
||||||
return res.status(400).send('Missing tileJSON.encoding');
|
return res.status(400).send('Missing tileJSON.encoding');
|
||||||
|
@ -289,157 +314,97 @@ export const serve_data = {
|
||||||
.status(400)
|
.status(400)
|
||||||
.send('Invalid encoding. Must be terrarium or mapbox.');
|
.send('Invalid encoding. Must be terrarium or mapbox.');
|
||||||
}
|
}
|
||||||
|
|
||||||
const format = tileJSON?.format;
|
const format = tileJSON?.format;
|
||||||
if (format == null) {
|
if (format == null) {
|
||||||
return res.status(400).send('Missing tileJSON.format');
|
return res.status(400).send('Missing tileJSON.format');
|
||||||
} else if (format !== 'webp' && format !== 'png') {
|
} else if (format !== 'webp' && format !== 'png') {
|
||||||
return res.status(400).send('Invalid format. Must be webp or png.');
|
return res.status(400).send('Invalid format. Must be webp or png.');
|
||||||
}
|
}
|
||||||
|
|
||||||
const z = parseInt(req.params.z, 10);
|
const z = parseInt(req.params.z, 10);
|
||||||
const x = parseFloat(req.params.x);
|
const x = parseFloat(req.params.x);
|
||||||
const y = parseFloat(req.params.y);
|
const y = parseFloat(req.params.y);
|
||||||
|
|
||||||
if (tileJSON.minzoom == null || tileJSON.maxzoom == null) {
|
if (tileJSON.minzoom == null || tileJSON.maxzoom == null) {
|
||||||
return res.status(404).send(JSON.stringify(tileJSON));
|
return res.status(404).send(JSON.stringify(tileJSON));
|
||||||
}
|
}
|
||||||
|
const TILE_SIZE = tileJSON.tileSize || 512;
|
||||||
const TILE_SIZE = 256;
|
let bbox;
|
||||||
let tileCenter;
|
|
||||||
let xy;
|
let xy;
|
||||||
|
var zoom = z;
|
||||||
|
|
||||||
if (Number.isInteger(x) && Number.isInteger(y)) {
|
if (Number.isInteger(x) && Number.isInteger(y)) {
|
||||||
const intX = parseInt(req.params.x, 10);
|
const intX = parseInt(req.params.x, 10);
|
||||||
const intY = parseInt(req.params.y, 10);
|
const intY = parseInt(req.params.y, 10);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
z < tileJSON.minzoom ||
|
zoom < tileJSON.minzoom ||
|
||||||
z > tileJSON.maxzoom ||
|
zoom > tileJSON.maxzoom ||
|
||||||
intX < 0 ||
|
intX < 0 ||
|
||||||
intY < 0 ||
|
intY < 0 ||
|
||||||
intX >= Math.pow(2, z) ||
|
intX >= Math.pow(2, zoom) ||
|
||||||
intY >= Math.pow(2, z)
|
intY >= Math.pow(2, zoom)
|
||||||
) {
|
) {
|
||||||
return res.status(404).send('Out of bounds');
|
return res.status(404).send('Out of bounds');
|
||||||
}
|
}
|
||||||
xy = [intX, intY];
|
xy = [intX, intY];
|
||||||
tileCenter = new SphericalMercator().bbox(intX, intY, z);
|
bbox = new SphericalMercator().bbox(intX, intY, zoom);
|
||||||
} else {
|
} else {
|
||||||
if (
|
//no zoom limit with coordinates
|
||||||
z < tileJSON.minzoom ||
|
if (zoom < tileJSON.minzoom) {
|
||||||
z > tileJSON.maxzoom ||
|
zoom = tileJSON.minzoom;
|
||||||
x < -180 ||
|
|
||||||
y < -90 ||
|
|
||||||
x > 180 ||
|
|
||||||
y > 90
|
|
||||||
) {
|
|
||||||
return res.status(404).send('Out of bounds');
|
|
||||||
}
|
}
|
||||||
|
if (zoom > tileJSON.maxzoom) {
|
||||||
tileCenter = [y, x, y + 0.1, x + 0.1];
|
zoom = tileJSON.maxzoom;
|
||||||
const { minX, minY } = new SphericalMercator().xyz(tileCenter, z);
|
}
|
||||||
|
bbox = [x, y, x + 0.1, y + 0.1];
|
||||||
|
const { minX, minY } = new SphericalMercator().xyz(bbox, zoom);
|
||||||
xy = [minX, minY];
|
xy = [minX, minY];
|
||||||
}
|
}
|
||||||
|
|
||||||
let data;
|
const fetchTile = await fetchTileData(
|
||||||
if (sourceType === 'pmtiles') {
|
source,
|
||||||
const tileinfo = await getPMtilesTile(source, z, x, y);
|
sourceType,
|
||||||
if (!tileinfo?.data) return res.status(204).send();
|
zoom,
|
||||||
data = tileinfo.data;
|
xy[0],
|
||||||
} else {
|
xy[1],
|
||||||
data = await new Promise((resolve, reject) => {
|
);
|
||||||
source.getTile(z, xy[0], xy[1], (err, tileData) => {
|
if (fetchTile == null) return res.status(204).send();
|
||||||
if (err) {
|
|
||||||
return /does not exist/.test(err.message)
|
|
||||||
? resolve(null)
|
|
||||||
: reject(err);
|
|
||||||
}
|
|
||||||
resolve(tileData);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (data == null) return res.status(204).send();
|
|
||||||
if (!data) return res.status(404).send('Not found');
|
|
||||||
|
|
||||||
const image = new Image();
|
let data = fetchTile.data;
|
||||||
await new Promise(async (resolve, reject) => {
|
var param = {
|
||||||
image.onload = async () => {
|
long: bbox[0],
|
||||||
const canvas = createCanvas(TILE_SIZE, TILE_SIZE);
|
lat: bbox[1],
|
||||||
const context = canvas.getContext('2d');
|
encoding,
|
||||||
context.drawImage(image, 0, 0);
|
format,
|
||||||
const imgdata = context.getImageData(0, 0, TILE_SIZE, TILE_SIZE);
|
tile_size: TILE_SIZE,
|
||||||
|
z: zoom,
|
||||||
const arrayWidth = imgdata.width;
|
|
||||||
const arrayHeight = imgdata.height;
|
|
||||||
const bytesPerPixel = 4;
|
|
||||||
|
|
||||||
const xPixel = Math.floor(xy[0]);
|
|
||||||
const yPixel = Math.floor(xy[1]);
|
|
||||||
|
|
||||||
if (
|
|
||||||
xPixel < 0 ||
|
|
||||||
yPixel < 0 ||
|
|
||||||
xPixel >= arrayWidth ||
|
|
||||||
yPixel >= arrayHeight
|
|
||||||
) {
|
|
||||||
return reject('Out of bounds Pixel');
|
|
||||||
}
|
|
||||||
|
|
||||||
const index = (yPixel * arrayWidth + xPixel) * bytesPerPixel;
|
|
||||||
|
|
||||||
const red = imgdata.data[index];
|
|
||||||
const green = imgdata.data[index + 1];
|
|
||||||
const blue = imgdata.data[index + 2];
|
|
||||||
|
|
||||||
let elevation;
|
|
||||||
if (encoding === 'mapbox') {
|
|
||||||
elevation =
|
|
||||||
-10000 + (red * 256 * 256 + green * 256 + blue) * 0.1;
|
|
||||||
} else if (encoding === 'terrarium') {
|
|
||||||
elevation = red * 256 + green + blue / 256 - 32768;
|
|
||||||
} else {
|
|
||||||
elevation = 'invalid encoding';
|
|
||||||
}
|
|
||||||
|
|
||||||
resolve(
|
|
||||||
res.status(200).send({
|
|
||||||
z,
|
|
||||||
x: xy[0],
|
x: xy[0],
|
||||||
y: xy[1],
|
y: xy[1],
|
||||||
red,
|
|
||||||
green,
|
|
||||||
blue,
|
|
||||||
latitude: tileCenter[0],
|
|
||||||
longitude: tileCenter[1],
|
|
||||||
elevation,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
image.onerror = (err) => reject(err);
|
res
|
||||||
|
.status(200)
|
||||||
if (format === 'webp') {
|
.send(await serve_rendered.getTerrainElevation(data, param));
|
||||||
try {
|
|
||||||
const img = await sharp(data).toFormat('png').toBuffer();
|
|
||||||
image.src = img;
|
|
||||||
} catch (err) {
|
|
||||||
reject(err);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
image.src = data;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (err) {
|
} 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);
|
||||||
}
|
}
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
app.get('/:id.json', (req, res, next) => {
|
/**
|
||||||
|
* Handles requests for tilejson for the data tiles.
|
||||||
|
* @param {object} req - Express request object.
|
||||||
|
* @param {object} res - Express response object.
|
||||||
|
* @param {string} req.params.id - ID of the data source.
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
app.get('/:id.json', (req, res) => {
|
||||||
|
if (verbose) {
|
||||||
|
console.log(
|
||||||
|
`Handling tilejson request for: /data/%s.json`,
|
||||||
|
String(req.params.id).replace(/\n|\r/g, ''),
|
||||||
|
);
|
||||||
|
}
|
||||||
const item = repo[req.params.id];
|
const item = repo[req.params.id];
|
||||||
if (!item) {
|
if (!item) {
|
||||||
return res.sendStatus(404);
|
return res.sendStatus(404);
|
||||||
|
@ -462,7 +427,20 @@ 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) {
|
||||||
|
@ -503,6 +481,7 @@ export const serve_data = {
|
||||||
const metadata = await getPMtilesInfo(source);
|
const metadata = await getPMtilesInfo(source);
|
||||||
|
|
||||||
tileJSON['encoding'] = params['encoding'];
|
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);
|
||||||
|
@ -524,6 +503,7 @@ export const serve_data = {
|
||||||
const info = await mbw.getInfo();
|
const info = await mbw.getInfo();
|
||||||
source = mbw.getMbTiles();
|
source = mbw.getMbTiles();
|
||||||
tileJSON['encoding'] = params['encoding'];
|
tileJSON['encoding'] = params['encoding'];
|
||||||
|
tileJSON['tileSize'] = params['tileSize'];
|
||||||
tileJSON['name'] = id;
|
tileJSON['name'] = id;
|
||||||
tileJSON['format'] = 'pbf';
|
tileJSON['format'] = 'pbf';
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,15 @@ 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();
|
||||||
|
@ -13,31 +21,74 @@ export const serve_font = async (options, allowedFonts) => {
|
||||||
|
|
||||||
const existingFonts = {};
|
const existingFonts = {};
|
||||||
|
|
||||||
app.get(
|
/**
|
||||||
'/fonts/:fontstack/:range([\\d]+-[\\d]+).pbf',
|
* Handles requests for a font file.
|
||||||
async (req, res, next) => {
|
* @param {object} req - Express request object.
|
||||||
const fontstack = decodeURI(req.params.fontstack);
|
* @param {object} res - Express response object.
|
||||||
const range = req.params.range;
|
* @param {string} req.params.fontstack - Name of the font stack.
|
||||||
|
* @param {string} req.params.range - The range of the font (e.g. 0-255).
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
app.get('/fonts/:fontstack/:range.pbf', async (req, res) => {
|
||||||
|
const sRange = String(req.params.range).replace(/\n|\r/g, '');
|
||||||
|
const sFontStack = String(decodeURI(req.params.fontstack)).replace(
|
||||||
|
/\n|\r/g,
|
||||||
|
'',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (verbose) {
|
||||||
|
console.log(
|
||||||
|
`Handling font request for: /fonts/%s/%s.pbf`,
|
||||||
|
sFontStack,
|
||||||
|
sRange,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const modifiedSince = req.get('if-modified-since');
|
||||||
|
const cc = req.get('cache-control');
|
||||||
|
if (modifiedSince && (!cc || cc.indexOf('no-cache') === -1)) {
|
||||||
|
if (
|
||||||
|
new Date(lastModified).getTime() === new Date(modifiedSince).getTime()
|
||||||
|
) {
|
||||||
|
return res.sendStatus(304);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const concatenated = await getFontsPbf(
|
const concatenated = await getFontsPbf(
|
||||||
options.serveAllFonts ? null : allowedFonts,
|
options.serveAllFonts ? null : allowedFonts,
|
||||||
fontPath,
|
fontPath,
|
||||||
fontstack,
|
sFontStack,
|
||||||
range,
|
sRange,
|
||||||
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) {
|
||||||
res.status(400).header('Content-Type', 'text/plain').send(err);
|
console.error(
|
||||||
}
|
`Error serving font: %s/%s.pbf, Error: %s`,
|
||||||
},
|
sFontStack,
|
||||||
|
sRange,
|
||||||
|
String(err),
|
||||||
);
|
);
|
||||||
|
return res
|
||||||
|
.status(400)
|
||||||
|
.header('Content-Type', 'text/plain')
|
||||||
|
.send('Error serving font');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
app.get('/fonts.json', (req, res, next) => {
|
/**
|
||||||
|
* Handles requests for a list of all available fonts.
|
||||||
|
* @param {object} req - Express request object.
|
||||||
|
* @param {object} res - Express response object.
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
app.get('/fonts.json', (req, res) => {
|
||||||
|
if (verbose) {
|
||||||
|
console.log('Handling list font request for /fonts.json');
|
||||||
|
}
|
||||||
res.header('Content-type', 'application/json');
|
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(),
|
||||||
|
@ -47,4 +98,4 @@ export const serve_font = async (options, allowedFonts) => {
|
||||||
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;
|
||||||
};
|
}
|
||||||
|
|
|
@ -3,7 +3,11 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
export const serve_rendered = {
|
export const serve_rendered = {
|
||||||
init: (options, repo) => {},
|
init: (options, repo, programOpts) => {},
|
||||||
add: (options, repo, params, id, publicUrl, dataResolver) => {},
|
add: (options, repo, params, id, programOpts, dataResolver) => {},
|
||||||
remove: (repo, id) => {},
|
remove: (repo, id) => {},
|
||||||
|
getTerrainElevation: (data, param) => {
|
||||||
|
param['elevation'] = 'not supported in light';
|
||||||
|
return param;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -7,18 +7,44 @@ import clone from 'clone';
|
||||||
import express from 'express';
|
import express from 'express';
|
||||||
import { validateStyleMin } from '@maplibre/maplibre-gl-style-spec';
|
import { validateStyleMin } from '@maplibre/maplibre-gl-style-spec';
|
||||||
|
|
||||||
import { fixUrl, allowedOptions } from './utils.js';
|
import {
|
||||||
|
allowedSpriteScales,
|
||||||
|
allowedSpriteFormats,
|
||||||
|
fixUrl,
|
||||||
|
readFile,
|
||||||
|
} from './utils.js';
|
||||||
|
|
||||||
const httpTester = /^https?:\/\//i;
|
const 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 item = repo[req.params.id];
|
const { id } = req.params;
|
||||||
|
if (verbose) {
|
||||||
|
console.log(
|
||||||
|
'Handling style request for: /styles/%s/style.json',
|
||||||
|
String(id).replace(/\n|\r/g, ''),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const item = repo[id];
|
||||||
if (!item) {
|
if (!item) {
|
||||||
return res.sendStatus(404);
|
return res.sendStatus(404);
|
||||||
}
|
}
|
||||||
|
@ -30,7 +56,6 @@ export const serve_style = {
|
||||||
source.data = fixUrl(req, source.data, item.publicUrl);
|
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) => {
|
||||||
|
@ -44,48 +69,147 @@ 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(@[23]x)?.:format([\\w]+)',
|
`/:id/sprite{/:spriteID}{@:scale}{.:format}`,
|
||||||
(req, res, next) => {
|
async (req, res, next) => {
|
||||||
const { spriteID = 'default', id } = req.params;
|
const { spriteID = 'default', id, format, scale } = req.params;
|
||||||
const scale = allowedSpriteScales(req.params.scale) || '';
|
const sanitizedId = String(id).replace(/\n|\r/g, '');
|
||||||
const format = allowedSpriteFormats(req.params.format);
|
const sanitizedScale = scale ? String(scale).replace(/\n|\r/g, '') : '';
|
||||||
|
const sanitizedSpriteID = String(spriteID).replace(/\n|\r/g, '');
|
||||||
if (format) {
|
const sanitizedFormat = format
|
||||||
|
? '.' + String(format).replace(/\n|\r/g, '')
|
||||||
|
: '';
|
||||||
|
if (verbose) {
|
||||||
|
console.log(
|
||||||
|
`Handling sprite request for: /styles/%s/sprite/%s%s%s`,
|
||||||
|
sanitizedId,
|
||||||
|
sanitizedSpriteID,
|
||||||
|
sanitizedScale,
|
||||||
|
sanitizedFormat,
|
||||||
|
);
|
||||||
|
}
|
||||||
const item = repo[id];
|
const 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,
|
||||||
);
|
);
|
||||||
if (sprite) {
|
const spriteScale = allowedSpriteScales(scale);
|
||||||
const filename = `${sprite.path + scale}.${format}`;
|
if (!sprite || spriteScale === null) {
|
||||||
return fs.readFile(filename, (err, data) => {
|
if (verbose)
|
||||||
if (err) {
|
console.error(
|
||||||
console.log('Sprite load error:', filename);
|
`Bad Sprite ID or Scale for: /styles/%s/sprite/%s%s%s`,
|
||||||
return res.sendStatus(404);
|
sanitizedId,
|
||||||
} else {
|
sanitizedSpriteID,
|
||||||
if (format === 'json')
|
sanitizedScale,
|
||||||
res.header('Content-type', 'application/json');
|
sanitizedFormat,
|
||||||
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 {
|
|
||||||
return res.status(400).send('Bad Sprite Format');
|
const modifiedSince = req.get('if-modified-since');
|
||||||
|
const cc = req.get('cache-control');
|
||||||
|
if (modifiedSince && (!cc || cc.indexOf('no-cache') === -1)) {
|
||||||
|
if (
|
||||||
|
new Date(item.lastModified).getTime() ===
|
||||||
|
new Date(modifiedSince).getTime()
|
||||||
|
) {
|
||||||
|
return res.sendStatus(304);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const sanitizedSpritePath = sprite.path.replace(/^(\.\.\/)+/, '');
|
||||||
|
const filename = `${sanitizedSpritePath}${spriteScale}.${validatedFormat}`;
|
||||||
|
if (verbose) console.log(`Loading sprite from: %s`, filename);
|
||||||
|
try {
|
||||||
|
const data = await readFile(filename);
|
||||||
|
|
||||||
|
if (validatedFormat === 'json') {
|
||||||
|
res.header('Content-type', 'application/json');
|
||||||
|
} else if (validatedFormat === 'png') {
|
||||||
|
res.header('Content-type', 'image/png');
|
||||||
|
}
|
||||||
|
if (verbose)
|
||||||
|
console.log(
|
||||||
|
`Responding with sprite data for /styles/%s/sprite/%s%s%s`,
|
||||||
|
sanitizedId,
|
||||||
|
sanitizedSpriteID,
|
||||||
|
sanitizedScale,
|
||||||
|
sanitizedFormat,
|
||||||
|
);
|
||||||
|
res.set({ 'Last-Modified': item.lastModified });
|
||||||
|
return res.send(data);
|
||||||
|
} catch (err) {
|
||||||
|
if (verbose) {
|
||||||
|
console.error(
|
||||||
|
'Sprite load error: %s, Error: %s',
|
||||||
|
filename,
|
||||||
|
String(err),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return res.sendStatus(404);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
return app;
|
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 {Function} reportTiles Function for reporting tile sources.
|
||||||
|
* @param {Function} reportFont Function for reporting font usage
|
||||||
|
* @returns {boolean} true if add is succesful
|
||||||
|
*/
|
||||||
|
add: function (
|
||||||
|
options,
|
||||||
|
repo,
|
||||||
|
params,
|
||||||
|
id,
|
||||||
|
programOpts,
|
||||||
|
reportTiles,
|
||||||
|
reportFont,
|
||||||
|
) {
|
||||||
|
const { publicUrl } = programOpts;
|
||||||
const styleFile = path.resolve(options.paths.styles, params.style);
|
const styleFile = path.resolve(options.paths.styles, params.style);
|
||||||
|
|
||||||
let styleFileData;
|
let styleFileData;
|
||||||
|
@ -199,6 +323,7 @@ export const serve_style = {
|
||||||
spritePaths,
|
spritePaths,
|
||||||
publicUrl,
|
publicUrl,
|
||||||
name: styleJSON.name,
|
name: styleJSON.name,
|
||||||
|
lastModified: new Date().toUTCString(),
|
||||||
};
|
};
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|
251
src/server.js
251
src/server.js
|
@ -1,9 +1,6 @@
|
||||||
#!/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';
|
||||||
|
@ -19,25 +16,30 @@ 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 { getTileUrls, getPublicUrl, isValidHttpUrl } from './utils.js';
|
import {
|
||||||
|
allowedTileSizes,
|
||||||
|
getTileUrls,
|
||||||
|
getPublicUrl,
|
||||||
|
isValidHttpUrl,
|
||||||
|
} from './utils.js';
|
||||||
|
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __dirname = path.dirname(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 opts
|
* @param {object} opts - Configuration options for the server.
|
||||||
|
* @returns {Promise<object>} - A promise that resolves to the server object.
|
||||||
*/
|
*/
|
||||||
function start(opts) {
|
async function start(opts) {
|
||||||
console.log('Starting server');
|
console.log('Starting server');
|
||||||
|
|
||||||
const app = express().disable('x-powered-by');
|
const app = express().disable('x-powered-by');
|
||||||
|
@ -116,8 +118,9 @@ function start(opts) {
|
||||||
* 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.
|
||||||
*/
|
*/
|
||||||
const getFiles = async (directory) => {
|
async function getFiles(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,
|
||||||
|
@ -136,7 +139,7 @@ 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(
|
||||||
|
@ -159,18 +162,25 @@ function start(opts) {
|
||||||
app.use(cors());
|
app.use(cors());
|
||||||
}
|
}
|
||||||
|
|
||||||
app.use('/data/', serve_data.init(options, serving.data));
|
app.use('/data/', serve_data.init(options, serving.data, opts));
|
||||||
app.use('/files/', express.static(paths.files));
|
app.use('/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).then((sub) => {
|
serve_rendered.init(options, serving.rendered, opts).then((sub) => {
|
||||||
app.use('/styles/', sub);
|
app.use('/styles/', sub);
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
const addStyle = (id, item, allowMoreData, reportFonts) => {
|
* Adds a style to the server.
|
||||||
|
* @param {string} id - The ID of the style.
|
||||||
|
* @param {object} item - The style configuration object.
|
||||||
|
* @param {boolean} allowMoreData - Whether to allow adding more data sources.
|
||||||
|
* @param {boolean} reportFonts - Whether to report fonts.
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
function addStyle(id, item, allowMoreData, reportFonts) {
|
||||||
let success = true;
|
let success = true;
|
||||||
if (item.serve_data !== false) {
|
if (item.serve_data !== false) {
|
||||||
success = serve_style.add(
|
success = serve_style.add(
|
||||||
|
@ -178,7 +188,7 @@ function start(opts) {
|
||||||
serving.styles,
|
serving.styles,
|
||||||
item,
|
item,
|
||||||
id,
|
id,
|
||||||
opts.publicUrl,
|
opts,
|
||||||
(styleSourceId, protocol) => {
|
(styleSourceId, protocol) => {
|
||||||
let dataItemId;
|
let dataItemId;
|
||||||
for (const id of Object.keys(data)) {
|
for (const id of Object.keys(data)) {
|
||||||
|
@ -235,7 +245,7 @@ function start(opts) {
|
||||||
serving.rendered,
|
serving.rendered,
|
||||||
item,
|
item,
|
||||||
id,
|
id,
|
||||||
opts.publicUrl,
|
opts,
|
||||||
function dataResolver(styleSourceId) {
|
function dataResolver(styleSourceId) {
|
||||||
let fileType;
|
let fileType;
|
||||||
let inputFile;
|
let inputFile;
|
||||||
|
@ -261,7 +271,7 @@ function start(opts) {
|
||||||
item.serve_rendered = false;
|
item.serve_rendered = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
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];
|
||||||
|
@ -272,13 +282,11 @@ function start(opts) {
|
||||||
|
|
||||||
addStyle(id, item, true, true);
|
addStyle(id, item, true, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
startupPromises.push(
|
startupPromises.push(
|
||||||
serve_font(options, serving.fonts).then((sub) => {
|
serve_font(options, serving.fonts, opts).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];
|
||||||
|
@ -288,12 +296,8 @@ 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) {
|
||||||
|
@ -333,7 +337,13 @@ function start(opts) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* Handles requests for a list of available styles.
|
||||||
|
* @param {object} req - Express request object.
|
||||||
|
* @param {object} res - Express response object.
|
||||||
|
* @param {string} [req.query.key] - Optional API key.
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
app.get('/styles.json', (req, res, next) => {
|
app.get('/styles.json', (req, res, next) => {
|
||||||
const result = [];
|
const result = [];
|
||||||
const query = req.query.key
|
const query = req.query.key
|
||||||
|
@ -354,7 +364,15 @@ 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 = '';
|
||||||
|
@ -377,20 +395,42 @@ function start(opts) {
|
||||||
arr.push(info);
|
arr.push(info);
|
||||||
}
|
}
|
||||||
return arr;
|
return arr;
|
||||||
};
|
}
|
||||||
|
|
||||||
app.get('/(:tileSize(256|512)/)?rendered.json', (req, res, next) => {
|
/**
|
||||||
const tileSize = parseInt(req.params.tileSize, 10) || undefined;
|
* Handles requests for a rendered tilejson endpoint.
|
||||||
res.send(addTileJSONs([], req, 'rendered', tileSize));
|
* @param {object} req - Express request object.
|
||||||
|
* @param {object} res - Express response object.
|
||||||
|
* @param {string} req.params.tileSize - Optional tile size parameter.
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
app.get('{/:tileSize}/rendered.json', (req, res, next) => {
|
||||||
|
const tileSize = allowedTileSizes(req.params['tileSize']);
|
||||||
|
res.send(addTileJSONs([], req, 'rendered', parseInt(tileSize, 10)));
|
||||||
});
|
});
|
||||||
app.get('/data.json', (req, res, next) => {
|
|
||||||
|
/**
|
||||||
|
* Handles requests for a data tilejson endpoint.
|
||||||
|
* @param {object} req - Express request object.
|
||||||
|
* @param {object} res - Express response object.
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
app.get('/data.json', (req, res) => {
|
||||||
res.send(addTileJSONs([], req, 'data', undefined));
|
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', tileSize),
|
addTileJSONs([], req, 'rendered', parseInt(tileSize, 10)),
|
||||||
req,
|
req,
|
||||||
'data',
|
'data',
|
||||||
undefined,
|
undefined,
|
||||||
|
@ -403,7 +443,15 @@ 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) {
|
||||||
|
@ -415,24 +463,17 @@ function start(opts) {
|
||||||
templateFile = path.resolve(paths.root, options.frontPage);
|
templateFile = path.resolve(paths.root, options.frontPage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
startupPromises.push(
|
try {
|
||||||
new Promise((resolve, reject) => {
|
const content = fs.readFileSync(templateFile, 'utf-8');
|
||||||
fs.readFile(templateFile, (err, content) => {
|
|
||||||
if (err) {
|
|
||||||
err = new Error(`Template not found: ${err.message}`);
|
|
||||||
reject(err);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const compiled = handlebars.compile(content.toString());
|
const compiled = handlebars.compile(content.toString());
|
||||||
|
app.get(urlPath, (req, res, next) => {
|
||||||
app.use(urlPath, (req, res, next) => {
|
if (opts.verbose) {
|
||||||
|
console.log(`Serving template at path: ${urlPath}`);
|
||||||
|
}
|
||||||
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 || '/';
|
||||||
|
@ -445,14 +486,27 @@ 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');
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
resolve();
|
} catch (err) {
|
||||||
});
|
console.error(`Error reading template file: ${templateFile}`, err);
|
||||||
}),
|
throw new Error(`Template not found: ${err.message}`); //throw an error so that the server doesnt start
|
||||||
);
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
serveTemplate('/$', 'index', (req) => {
|
/**
|
||||||
|
* Handles requests for the index page, providing a list of available styles and data.
|
||||||
|
* @param {object} req - Express request object.
|
||||||
|
* @param {object} res - Express response object.
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
serveTemplate('/', 'index', (req) => {
|
||||||
let styles = {};
|
let styles = {};
|
||||||
for (const id of Object.keys(serving.styles || {})) {
|
for (const id of Object.keys(serving.styles || {})) {
|
||||||
let style = {
|
let style = {
|
||||||
|
@ -464,11 +518,15 @@ 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(5)}/${center[0].toFixed(5)}`;
|
style.viewer_hash = `#${center[2]}/${center[1].toFixed(
|
||||||
|
5,
|
||||||
|
)}/${center[0].toFixed(5)}`;
|
||||||
|
|
||||||
const centerPx = mercator.px([center[0], center[1]], center[2]);
|
const centerPx = mercator.px([center[0], center[1]], center[2]);
|
||||||
// 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(centerPx[0] / 256)}/${Math.floor(centerPx[1] / 256)}.png`;
|
style.thumbnail = `${Math.floor(center[2])}/${Math.floor(
|
||||||
|
centerPx[0] / 256,
|
||||||
|
)}/${Math.floor(centerPx[1] / 256)}.png`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const tileSize = 512;
|
const tileSize = 512;
|
||||||
|
@ -484,7 +542,6 @@ 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]);
|
||||||
|
@ -517,15 +574,20 @@ function start(opts) {
|
||||||
tileJSON.encoding === 'terrarium' ||
|
tileJSON.encoding === 'terrarium' ||
|
||||||
tileJSON.encoding === 'mapbox'
|
tileJSON.encoding === 'mapbox'
|
||||||
) {
|
) {
|
||||||
|
if (!isLight) {
|
||||||
data.elevation_link = getTileUrls(
|
data.elevation_link = getTileUrls(
|
||||||
req,
|
req,
|
||||||
tileJSON.tiles,
|
tileJSON.tiles,
|
||||||
`data/${id}/elevation`,
|
`data/${id}/elevation`,
|
||||||
)[0];
|
)[0];
|
||||||
}
|
}
|
||||||
|
data.is_terrain = true;
|
||||||
|
}
|
||||||
if (center) {
|
if (center) {
|
||||||
const centerPx = mercator.px([center[0], center[1]], center[2]);
|
const centerPx = mercator.px([center[0], center[1]], center[2]);
|
||||||
data.thumbnail = `${Math.floor(center[2])}/${Math.floor(centerPx[0] / 256)}/${Math.floor(centerPx[1] / 256)}.${tileJSON.format}`;
|
data.thumbnail = `${Math.floor(center[2])}/${Math.floor(
|
||||||
|
centerPx[0] / 256,
|
||||||
|
)}/${Math.floor(centerPx[1] / 256)}.${tileJSON.format}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -542,24 +604,28 @@ 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,
|
||||||
|
@ -569,10 +635,12 @@ function start(opts) {
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
/*
|
/**
|
||||||
app.use('/rendered/:id/$', function(req, res, next) {
|
* Handles requests for a Web Map Tile Service (WMTS) XML template.
|
||||||
return res.redirect(301, '/styles/' + req.params.id + '/');
|
* @param {object} req - Express request object.
|
||||||
});
|
* @param {object} res - Express response object.
|
||||||
|
* @param {string} req.params.id - ID of the style.
|
||||||
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
serveTemplate('/styles/:id/wmts.xml', 'wmts', (req) => {
|
serveTemplate('/styles/:id/wmts.xml', 'wmts', (req) => {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
|
@ -605,9 +673,16 @@ function start(opts) {
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
serveTemplate('^/data/(:preview(preview)/)?:id/$', 'data', (req) => {
|
/**
|
||||||
const id = req.params.id;
|
* Handles requests for a data view template for a specific data source.
|
||||||
const preview = req.params.preview || undefined;
|
* @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) {
|
||||||
|
@ -616,7 +691,8 @@ function start(opts) {
|
||||||
const is_terrain =
|
const is_terrain =
|
||||||
(data.tileJSON.encoding === 'terrarium' ||
|
(data.tileJSON.encoding === 'terrarium' ||
|
||||||
data.tileJSON.encoding === 'mapbox') &&
|
data.tileJSON.encoding === 'mapbox') &&
|
||||||
preview === 'preview';
|
view === 'preview';
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...data,
|
...data,
|
||||||
id,
|
id,
|
||||||
|
@ -624,6 +700,7 @@ function start(opts) {
|
||||||
is_terrain: is_terrain,
|
is_terrain: is_terrain,
|
||||||
is_terrainrgb: data.tileJSON.encoding === 'mapbox',
|
is_terrainrgb: data.tileJSON.encoding === 'mapbox',
|
||||||
terrain_encoding: data.tileJSON.encoding,
|
terrain_encoding: data.tileJSON.encoding,
|
||||||
|
is_light: isLight,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -633,7 +710,13 @@ 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 {
|
||||||
|
@ -662,10 +745,10 @@ function start(opts) {
|
||||||
startupPromise,
|
startupPromise,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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`);
|
||||||
|
@ -673,11 +756,12 @@ function stopGracefully(signal) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
* Starts and manages the server
|
||||||
* @param opts
|
* @param {object} opts - Configuration options for the server.
|
||||||
|
* @returns {Promise<object>} - A promise that resolves to the running server
|
||||||
*/
|
*/
|
||||||
export function server(opts) {
|
export async function server(opts) {
|
||||||
const running = start(opts);
|
const running = await start(opts);
|
||||||
|
|
||||||
running.startupPromise.catch((err) => {
|
running.startupPromise.catch((err) => {
|
||||||
console.error(err.message);
|
console.error(err.message);
|
||||||
|
@ -691,12 +775,11 @@ export 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(() => {
|
running.server.shutdown(async () => {
|
||||||
const restarted = start(opts);
|
const restarted = await start(opts);
|
||||||
running.server = restarted.server;
|
running.server = restarted.server;
|
||||||
running.app = restarted.app;
|
running.app = restarted.app;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
return running;
|
return running;
|
||||||
}
|
}
|
||||||
|
|
270
src/utils.js
270
src/utils.js
|
@ -6,12 +6,18 @@ import fs 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 { 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 opts
|
* @param {string[]} opts - An array of allowed option strings.
|
||||||
* @param root0
|
* @param {object} [config] - Optional configuration object.
|
||||||
* @param root0.defaultValue
|
* @param {string} [config.defaultValue] - The default value to return if input doesn't match.
|
||||||
|
* @returns {function(string): string} - A function that takes a value and returns it if valid or a default.
|
||||||
*/
|
*/
|
||||||
export function allowedOptions(opts, { defaultValue } = {}) {
|
export function allowedOptions(opts, { defaultValue } = {}) {
|
||||||
const values = Object.fromEntries(opts.map((key) => [key, key]));
|
const values = Object.fromEntries(opts.map((key) => [key, key]));
|
||||||
|
@ -19,10 +25,52 @@ export function allowedOptions(opts, { defaultValue } = {}) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Replace local:// urls with public http(s):// urls
|
* Parses a scale string to a number.
|
||||||
* @param req
|
* @param {string} scale The scale string (e.g., '2x', '4x').
|
||||||
* @param url
|
* @param {number} maxScale Maximum allowed scale digit.
|
||||||
* @param publicUrl
|
* @returns {number|null} The parsed scale as a number or null if invalid.
|
||||||
|
*/
|
||||||
|
export function allowedScales(scale, maxScale = 9) {
|
||||||
|
if (scale === undefined) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line security/detect-non-literal-regexp
|
||||||
|
const regex = new RegExp(`^[2-${maxScale}]x$`);
|
||||||
|
if (!regex.test(scale)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return parseInt(scale.slice(0, -1), 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a string is a valid sprite scale and returns it if it is within the allowed range, and null if it does not conform.
|
||||||
|
* @param {string} scale - The scale string to validate (e.g., '2x', '3x').
|
||||||
|
* @param {number} [maxScale] - The maximum scale value. If no value is passed in, it defaults to a value of 3.
|
||||||
|
* @returns {string|null} - The valid scale string or null if invalid.
|
||||||
|
*/
|
||||||
|
export function allowedSpriteScales(scale, maxScale = 3) {
|
||||||
|
if (!scale) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
const match = scale?.match(/^([2-9]\d*)x$/);
|
||||||
|
if (!match) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const parsedScale = parseInt(match[1], 10);
|
||||||
|
if (parsedScale <= maxScale) {
|
||||||
|
return `@${parsedScale}x`;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replaces local:// URLs with public http(s):// URLs.
|
||||||
|
* @param {object} req - Express request object.
|
||||||
|
* @param {string} url - The URL string to fix.
|
||||||
|
* @param {string} publicUrl - The public URL prefix to use for replacements.
|
||||||
|
* @returns {string} - The fixed URL string.
|
||||||
*/
|
*/
|
||||||
export function fixUrl(req, url, publicUrl) {
|
export function fixUrl(req, url, publicUrl) {
|
||||||
if (!url || typeof url !== 'string' || url.indexOf('local://') !== 0) {
|
if (!url || typeof url !== 'string' || url.indexOf('local://') !== 0) {
|
||||||
|
@ -40,32 +88,54 @@ export function fixUrl(req, url, publicUrl) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate new URL object
|
* Generates a new URL object from the Express request.
|
||||||
* @param req
|
* @param {object} req - Express request object.
|
||||||
* @params {object} req - Express request
|
* @returns {URL} - URL object with correct host and optionally path.
|
||||||
* @returns {URL} object
|
|
||||||
*/
|
*/
|
||||||
const getUrlObject = (req) => {
|
function 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,
|
||||||
|
@ -73,7 +143,7 @@ export const 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) {
|
||||||
|
@ -138,9 +208,14 @@ export const getTileUrls = (
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
||||||
|
@ -153,19 +228,77 @@ export const fixTileJSONCenter = (tileJSON) => {
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
const getFontPbf = (allowedFonts, fontPath, name, range, fallbacks) =>
|
/**
|
||||||
new Promise((resolve, reject) => {
|
* Reads a file and returns a Promise with the file data.
|
||||||
|
* @param {string} filename - Path to the file to read.
|
||||||
|
* @returns {Promise<Buffer>} - A Promise that resolves with the file data as a Buffer or rejects with an error.
|
||||||
|
*/
|
||||||
|
export function readFile(filename) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const sanitizedFilename = path.normalize(filename); // Normalize path, remove ..
|
||||||
|
// eslint-disable-next-line security/detect-non-literal-fs-filename
|
||||||
|
fs.readFile(String(sanitizedFilename), (err, data) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
} else {
|
||||||
|
resolve(data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves font data for a given font and range.
|
||||||
|
* @param {object} allowedFonts - An object of allowed fonts.
|
||||||
|
* @param {string} fontPath - The path to the font directory.
|
||||||
|
* @param {string} name - The name of the font.
|
||||||
|
* @param {string} range - The range (e.g., '0-255') of the font to load.
|
||||||
|
* @param {object} [fallbacks] - Optional fallback font list.
|
||||||
|
* @returns {Promise<Buffer>} A promise that resolves with the font data Buffer or rejects with an error.
|
||||||
|
*/
|
||||||
|
async function getFontPbf(allowedFonts, fontPath, name, range, fallbacks) {
|
||||||
if (!allowedFonts || (allowedFonts[name] && fallbacks)) {
|
if (!allowedFonts || (allowedFonts[name] && fallbacks)) {
|
||||||
const filename = path.join(fontPath, name, `${range}.pbf`);
|
const fontMatch = name?.match(/^[\p{L}\p{N} \-\.~!*'()@&=+,#$\[\]]+$/u);
|
||||||
|
const sanitizedName = fontMatch?.[0] || 'invalid';
|
||||||
|
if (!name || typeof name !== 'string' || name.trim() === '' || !fontMatch) {
|
||||||
|
console.error(
|
||||||
|
'ERROR: Invalid font name: %s',
|
||||||
|
sanitizedName.replace(/\n|\r/g, ''),
|
||||||
|
);
|
||||||
|
throw new Error('Invalid font name');
|
||||||
|
}
|
||||||
|
|
||||||
|
const rangeMatch = range?.match(/^[\d-]+$/);
|
||||||
|
const sanitizedRange = rangeMatch?.[0] || 'invalid';
|
||||||
|
if (!/^\d+-\d+$/.test(range)) {
|
||||||
|
console.error(
|
||||||
|
'ERROR: Invalid range: %s',
|
||||||
|
sanitizedRange.replace(/\n|\r/g, ''),
|
||||||
|
);
|
||||||
|
throw new Error('Invalid range');
|
||||||
|
}
|
||||||
|
const filename = path.join(
|
||||||
|
fontPath,
|
||||||
|
sanitizedName,
|
||||||
|
`${sanitizedRange}.pbf`,
|
||||||
|
);
|
||||||
|
|
||||||
if (!fallbacks) {
|
if (!fallbacks) {
|
||||||
fallbacks = clone(allowedFonts || {});
|
fallbacks = clone(allowedFonts || {});
|
||||||
}
|
}
|
||||||
delete fallbacks[name];
|
delete fallbacks[name];
|
||||||
fs.readFile(filename, (err, data) => {
|
|
||||||
if (err) {
|
try {
|
||||||
console.error(`ERROR: Font not found: ${name}`);
|
const data = await readFile(filename);
|
||||||
|
return data;
|
||||||
|
} catch (err) {
|
||||||
|
console.error(
|
||||||
|
'ERROR: Font not found: %s, Error: %s',
|
||||||
|
filename.replace(/\n|\r/g, ''),
|
||||||
|
String(err),
|
||||||
|
);
|
||||||
if (fallbacks && Object.keys(fallbacks).length) {
|
if (fallbacks && Object.keys(fallbacks).length) {
|
||||||
let fallbackName;
|
let fallbackName;
|
||||||
|
|
||||||
|
@ -180,32 +313,37 @@ const getFontPbf = (allowedFonts, fontPath, name, range, fallbacks) =>
|
||||||
fallbackName = Object.keys(fallbacks)[0];
|
fallbackName = Object.keys(fallbacks)[0];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
console.error(
|
||||||
console.error(`ERROR: Trying to use ${fallbackName} as a fallback`);
|
`ERROR: Trying to use %s as a fallback for: %s`,
|
||||||
delete fallbacks[fallbackName];
|
fallbackName,
|
||||||
getFontPbf(null, fontPath, fallbackName, range, fallbacks).then(
|
sanitizedName,
|
||||||
resolve,
|
|
||||||
reject,
|
|
||||||
);
|
);
|
||||||
|
delete fallbacks[fallbackName];
|
||||||
|
return getFontPbf(null, fontPath, fallbackName, range, fallbacks);
|
||||||
} else {
|
} else {
|
||||||
reject(`Font load error: ${name}`);
|
throw new Error('Font load error');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
resolve(data);
|
throw new Error('Font not allowed');
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
} else {
|
/**
|
||||||
reject(`Font not allowed: ${name}`);
|
* Combines multiple font pbf buffers into one.
|
||||||
}
|
* @param {object} allowedFonts - An object of allowed fonts.
|
||||||
});
|
* @param {string} fontPath - The path to the font directory.
|
||||||
|
* @param {string} names - Comma-separated font names.
|
||||||
export const getFontsPbf = async (
|
* @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) {
|
||||||
|
@ -222,9 +360,14 @@ export const getFontsPbf = async (
|
||||||
|
|
||||||
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);
|
||||||
|
@ -239,9 +382,14 @@ export const listFonts = async (fontPath) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
||||||
|
@ -251,4 +399,32 @@ export const 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 });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -7,10 +7,10 @@ import { server } from '../src/server.js';
|
||||||
global.expect = expect;
|
global.expect = expect;
|
||||||
global.supertest = supertest;
|
global.supertest = supertest;
|
||||||
|
|
||||||
before(function () {
|
before(async function () {
|
||||||
console.log('global setup');
|
console.log('global setup');
|
||||||
process.chdir('test_data');
|
process.chdir('test_data');
|
||||||
const running = server({
|
const running = await server({
|
||||||
configPath: 'config.json',
|
configPath: 'config.json',
|
||||||
port: 8888,
|
port: 8888,
|
||||||
publicUrl: '/test/',
|
publicUrl: '/test/',
|
||||||
|
|
|
@ -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', 404);
|
testStatic(prefix, '0,0,0/256.5x256.5', 'png', 400);
|
||||||
|
|
||||||
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', 404);
|
testStatic(prefix, '-180,-80,180,80/0.5x2.6', 'png', 400);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -60,16 +60,16 @@ describe('Raster tiles', function () {
|
||||||
|
|
||||||
describe('invalid requests return 4xx', function () {
|
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', 404);
|
testTile(prefix, 256, -1, 0, 0, 'png', 400);
|
||||||
testTile(prefix, 256, 25, 0, 0, 'png', 404);
|
testTile(prefix, 256, 25, 0, 0, 'png', 400);
|
||||||
testTile(prefix, 256, 0, 1, 0, 'png', 404);
|
testTile(prefix, 256, 0, 1, 0, 'png', 400);
|
||||||
testTile(prefix, 256, 0, 0, 1, 'png', 404);
|
testTile(prefix, 256, 0, 0, 1, 'png', 400);
|
||||||
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', 404, 1);
|
testTile(prefix, 256, 0, 0, 0, 'png', 400, 1);
|
||||||
testTile(prefix, 256, 0, 0, 0, 'png', 404, 5);
|
testTile(prefix, 256, 0, 0, 0, 'png', 400, 5);
|
||||||
|
|
||||||
testTile(prefix, 300, 0, 0, 0, 'png', 404);
|
testTile(prefix, 300, 0, 0, 0, 'png', 400);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in a new issue