Compare commits
1 commit
master
...
revert-829
Author | SHA1 | Date | |
---|---|---|---|
![]() |
3ccde8d2df |
72 changed files with 6417 additions and 10141 deletions
|
@ -5,4 +5,3 @@
|
|||
!package.json
|
||||
!package-lock.json
|
||||
!docker-entrypoint.sh
|
||||
**.gitignore
|
|
@ -28,9 +28,5 @@ module.exports = {
|
|||
rules: {
|
||||
'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',
|
||||
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'warn',
|
||||
{ argsIgnorePattern: 'next|err|info|reject' },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
|
1
.github/workflows/automerger.yml
vendored
1
.github/workflows/automerger.yml
vendored
|
@ -14,4 +14,3 @@ jobs:
|
|||
- uses: fastify/github-action-merge-dependabot@v3
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
target: minor
|
||||
|
|
13
.github/workflows/ci.yml
vendored
13
.github/workflows/ci.yml
vendored
|
@ -9,25 +9,20 @@ permissions:
|
|||
|
||||
jobs:
|
||||
ci:
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- name: Check out repository ✨ (non-dependabot)
|
||||
if: ${{ github.actor != 'dependabot[bot]' }}
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Check out repository 🎉 (dependabot)
|
||||
if: ${{ github.actor == 'dependabot[bot]' }}
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Install dependencies (Ubuntu) 🚀
|
||||
run: >-
|
||||
sudo apt-get install -qq libcairo2-dev libjpeg8-dev libpango1.0-dev
|
||||
libgif-dev build-essential
|
||||
|
||||
- name: Setup node env 📦
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version-file: 'package.json'
|
||||
check-latest: true
|
||||
|
|
8
.github/workflows/codeql.yml
vendored
8
.github/workflows/codeql.yml
vendored
|
@ -23,15 +23,15 @@ jobs:
|
|||
language: [javascript]
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v3
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
uses: github/codeql-action/init@v2
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
queries: +security-and-quality
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v3
|
||||
uses: github/codeql-action/autobuild@v2
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3
|
||||
uses: github/codeql-action/analyze@v2
|
||||
with:
|
||||
category: '/language:${{ matrix.language }}'
|
||||
|
|
34
.github/workflows/ct.yml
vendored
34
.github/workflows/ct.yml
vendored
|
@ -9,15 +9,15 @@ permissions:
|
|||
|
||||
jobs:
|
||||
ct:
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- name: Check out repository ✨ (non-dependabot)
|
||||
if: ${{ github.actor != 'dependabot[bot]' }}
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Check out repository 🎉 (dependabot)
|
||||
if: ${{ github.actor == 'dependabot[bot]' }}
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
|
@ -31,14 +31,14 @@ jobs:
|
|||
libxxf86vm-dev
|
||||
|
||||
- name: Setup node env 📦
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version-file: 'package.json'
|
||||
check-latest: true
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies 🚀
|
||||
run: npm ci --prefer-offline --no-audit
|
||||
run: npm ci --prefer-offline --no-audit --omit=optional
|
||||
|
||||
- name: Pull test data 📦
|
||||
run: >-
|
||||
|
@ -52,35 +52,19 @@ jobs:
|
|||
run: xvfb-run --server-args="-screen 0 1024x768x24" npm test
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
uses: docker/setup-qemu-action@v2
|
||||
with:
|
||||
platforms: 'arm64'
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Test Docker Build
|
||||
uses: docker/build-push-action@v6
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
push: false
|
||||
platforms: linux/arm64,linux/amd64
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
- name: Create Tileserver Light Directory
|
||||
run: node publish.js --no-publish
|
||||
|
||||
- name: Install node dependencies
|
||||
run: npm ci --prefer-offline --no-audit
|
||||
working-directory: ./light
|
||||
|
||||
- name: Test Light Version to Docker Hub
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: ./light
|
||||
file: ./light/Dockerfile
|
||||
push: false
|
||||
platforms: linux/arm64,linux/amd64
|
||||
# experimental: https://github.com/docker/build-push-action/blob/master/docs/advanced/cache.md#cache-backend-api
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
|
97
.github/workflows/release.yml
vendored
97
.github/workflows/release.yml
vendored
|
@ -14,44 +14,12 @@ on:
|
|||
required: true
|
||||
|
||||
jobs:
|
||||
release-check:
|
||||
name: Check if version is published
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: 'package.json'
|
||||
check-latest: true
|
||||
cache: 'npm'
|
||||
|
||||
- name: Check if version is published
|
||||
id: check
|
||||
run: |
|
||||
currentVersion="$( node -e "console.log(require('./package.json').version)" )"
|
||||
isPublished="$( npm view tileserver-gl versions --json | jq -c --arg cv "$currentVersion" 'any(. == $cv)' )"
|
||||
echo "version=$currentVersion" >> "$GITHUB_OUTPUT"
|
||||
echo "published=$isPublished" >> "$GITHUB_OUTPUT"
|
||||
echo "currentVersion: $currentVersion"
|
||||
echo "isPublished: $isPublished"
|
||||
outputs:
|
||||
published: ${{ steps.check.outputs.published }}
|
||||
version: ${{ steps.check.outputs.version }}
|
||||
|
||||
release:
|
||||
needs: release-check
|
||||
if: ${{ needs.release-check.outputs.published == 'false' }}
|
||||
name: 'Build, Test, Publish'
|
||||
runs-on: ubuntu-22.04
|
||||
env:
|
||||
PACKAGE_VERSION: ${{ needs.release-check.outputs.version }}
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- name: Check out repository ✨
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Update apt-get 🚀
|
||||
run: sudo apt-get update -qq
|
||||
|
@ -63,14 +31,14 @@ jobs:
|
|||
libxxf86vm-dev
|
||||
|
||||
- name: Setup node env 📦
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version-file: 'package.json'
|
||||
check-latest: true
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies 🚀
|
||||
run: npm ci --prefer-offline --no-audit
|
||||
run: npm ci --prefer-offline --no-audit --omit=optional
|
||||
|
||||
- name: Pull test data 📦
|
||||
run: >-
|
||||
|
@ -86,30 +54,24 @@ jobs:
|
|||
- name: Remove Test Data
|
||||
run: rm -R test_data*
|
||||
|
||||
- name: Get release type
|
||||
id: prepare_release
|
||||
run: |
|
||||
RELEASE_TYPE="$(node -e "console.log(require('semver').prerelease('${{ needs.release-check.outputs.version }}') ? 'prerelease' : 'regular')")"
|
||||
if [[ $RELEASE_TYPE == 'regular' ]]; then
|
||||
echo "prerelease=false" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "prerelease=true" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Publish to NPM
|
||||
- name: Publish to Full Version NPM
|
||||
run: |
|
||||
npm config set //registry.npmjs.org/:_authToken ${NPM_TOKEN}
|
||||
npm publish --access public --tag ${{ steps.prepare_release.outputs.prerelease == 'true' && 'next' || 'latest' }}
|
||||
npm publish --access public
|
||||
env:
|
||||
NPM_TOKEN: ${{ github.event.inputs.npm_token }}
|
||||
|
||||
- name: Get version
|
||||
run: |
|
||||
echo "PACKAGE_VERSION=$(grep '"version"' package.json | cut -d '"' -f 4 | head -n 1)" >> $GITHUB_ENV
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
uses: docker/setup-qemu-action@v2
|
||||
with:
|
||||
platforms: 'arm64'
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v2
|
||||
|
@ -118,58 +80,39 @@ jobs:
|
|||
password: ${{ github.event.inputs.docker_token }}
|
||||
|
||||
- name: Build and publish Full Version to Docker Hub
|
||||
uses: docker/build-push-action@v6
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
tags: |
|
||||
maptiler/tileserver-gl:${{ steps.prepare_release.outputs.prerelease == 'true' && 'next' || 'latest' }},
|
||||
maptiler/tileserver-gl:v${{ env.PACKAGE_VERSION }}
|
||||
tags: maptiler/tileserver-gl:latest, maptiler/tileserver-gl:v${{ env.PACKAGE_VERSION }}
|
||||
platforms: linux/arm64,linux/amd64
|
||||
# experimental: https://github.com/docker/build-push-action/blob/master/docs/advanced/cache.md#cache-backend-api
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
- name: Extract changelog for version
|
||||
run: |
|
||||
awk '/^##/ { p = 0 }; p == 1 { print }; $0 == "## ${{ env.PACKAGE_VERSION }}" { p = 1 };' CHANGELOG.md > changelog_for_version.md
|
||||
cat changelog_for_version.md
|
||||
|
||||
- name: Publish to Github
|
||||
uses: ncipollo/release-action@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
tag: v${{ env.PACKAGE_VERSION }}
|
||||
name: v${{ env.PACKAGE_VERSION }}
|
||||
bodyFile: changelog_for_version.md
|
||||
allowUpdates: true
|
||||
draft: false
|
||||
prerelease: ${{ steps.prepare_release.outputs.prerelease }}
|
||||
|
||||
- name: Create Tileserver Light Directory
|
||||
run: node publish.js --no-publish
|
||||
|
||||
- name: Install node dependencies
|
||||
run: npm ci --prefer-offline --no-audit
|
||||
run: npm install
|
||||
working-directory: ./light
|
||||
|
||||
- name: Publish to Light Version NPM
|
||||
working-directory: ./light
|
||||
run: |
|
||||
npm config set //registry.npmjs.org/:_authToken ${NPM_TOKEN}
|
||||
npm publish --access public --tag ${{ steps.prepare_release.outputs.prerelease == 'true' && 'next' || 'latest' }}
|
||||
npm publish --access public
|
||||
env:
|
||||
NPM_TOKEN: ${{ github.event.inputs.npm_token }}
|
||||
|
||||
- name: Build and publish Light Version to Docker Hub
|
||||
uses: docker/build-push-action@v6
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: ./light
|
||||
file: ./light/Dockerfile
|
||||
push: true
|
||||
tags: |
|
||||
maptiler/tileserver-gl-light:${{ steps.prepare_release.outputs.prerelease == 'true' && 'next' || 'latest' }},
|
||||
maptiler/tileserver-gl-light:v${{ env.PACKAGE_VERSION }}
|
||||
tags: maptiler/tileserver-gl-light:latest, maptiler/tileserver-gl-light:v${{ env.PACKAGE_VERSION }}
|
||||
platforms: linux/arm64,linux/amd64
|
||||
# experimental: https://github.com/docker/build-push-action/blob/master/docs/advanced/cache.md#cache-backend-api
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
|
14
.gitignore
vendored
14
.gitignore
vendored
|
@ -1,22 +1,8 @@
|
|||
docs/_build
|
||||
public/resources/leaflet-hash.js
|
||||
public/resources/leaflet.css
|
||||
public/resources/leaflet.js
|
||||
public/resources/leaflet.js.map
|
||||
public/resources/mapbox-gl-rtl-text.js
|
||||
public/resources/maplibre-gl-inspect.css
|
||||
public/resources/maplibre-gl-inspect.js
|
||||
public/resources/maplibre-gl-inspect.js.map
|
||||
public/resources/maplibre-gl.css
|
||||
public/resources/maplibre-gl.js
|
||||
public/resources/maplibre-gl.js.map
|
||||
node_modules
|
||||
test_data
|
||||
test_data.zip
|
||||
data
|
||||
light
|
||||
plugins
|
||||
config.json
|
||||
*.mbtiles
|
||||
styles
|
||||
fonts
|
||||
|
|
21
.husky/commit-msg
Executable file
21
.husky/commit-msg
Executable file
|
@ -0,0 +1,21 @@
|
|||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
NAME=$(git config user.name)
|
||||
EMAIL=$(git config user.email)
|
||||
|
||||
if [ -z "$NAME" ]; then
|
||||
echo "empty git config user.name"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$EMAIL" ]; then
|
||||
echo "empty git config user.email"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
git interpret-trailers --if-exists doNothing --trailer \
|
||||
"Signed-off-by: $NAME <$EMAIL>" \
|
||||
--in-place "$1"
|
||||
|
||||
npm exec --no -- commitlint --edit $1
|
4
.husky/pre-push
Executable file
4
.husky/pre-push
Executable file
|
@ -0,0 +1,4 @@
|
|||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
npm exec --no -- lint-staged --no-stash
|
|
@ -1,18 +0,0 @@
|
|||
# Read the Docs configuration file for Sphinx projects
|
||||
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
|
||||
|
||||
# Required
|
||||
version: 2
|
||||
|
||||
# Set the version of Python and other tools you might need
|
||||
build:
|
||||
os: ubuntu-22.04
|
||||
tools:
|
||||
python: "3.11"
|
||||
|
||||
# Build documentation in the doc/help/ directory with Sphinx
|
||||
sphinx:
|
||||
configuration: docs/conf.py
|
||||
|
||||
formats:
|
||||
- pdf
|
32
CHANGELOG.md
32
CHANGELOG.md
|
@ -1,32 +0,0 @@
|
|||
# tileserver-gl changelog
|
||||
|
||||
## 5.2.0
|
||||
* Use npm packages for public/resources (https://github.com/maptiler/tileserver-gl/pull/1427) by @okimiko
|
||||
* use ttf files of googlefonts/opensans (https://github.com/maptiler/tileserver-gl/pull/1447) by @okimiko
|
||||
* Limit Elevation Lat/Long Output Length (https://github.com/maptiler/tileserver-gl/pull/1457) by @okimiko
|
||||
* Fetch style from url (https://github.com/maptiler/tileserver-gl/pull/1462) by @YoelRidgway
|
||||
* fix: memory leak on SIGHUP (https://github.com/maptiler/tileserver-gl/pull/1455) by @okimiko
|
||||
* fix: resolves Unimplemented type: 3 error for geojson format (https://github.com/maptiler/tileserver-gl/pull/1465) by @rjdjohnston
|
||||
* fix: Test light version in ct workflow - fix sqlite build in light (https://github.com/maptiler/tileserver-gl/pull/1477) by @acalcutt
|
||||
* fix: light version docker entrypoint permissions (https://github.com/maptiler/tileserver-gl/pull/1478) by @acalcutt
|
||||
|
||||
## 5.1.3
|
||||
* Fix SIGHUP (broken since 5.1.x) (https://github.com/maptiler/tileserver-gl/pull/1452) by @okimiko
|
||||
|
||||
## 5.1.2
|
||||
* Fix broken light (invalid use of heavy dependencies) (https://github.com/maptiler/tileserver-gl/pull/1449) by @okimiko
|
||||
|
||||
## 5.1.1
|
||||
* Fix wrong node version in Docker image (https://github.com/maptiler/tileserver-gl/pull/1442) by @acalcutt
|
||||
|
||||
## 5.1.0
|
||||
* Update recommended node to v22 + Update docker images to use node 22 (https://github.com/maptiler/tileserver-gl/pull/1438) by @acalcutt
|
||||
* Upgrade Express to v5 + Canvas to v3 + code cleanup (https://github.com/maptiler/tileserver-gl/pull/1429) by @acalcutt
|
||||
* Terrain Preview and simple Elevation Query (https://github.com/maptiler/tileserver-gl/pull/1425 and https://github.com/maptiler/tileserver-gl/pull/1432) by @okimiko
|
||||
* add progressive rendering option for static jpeg images (#1397) by @samuel-git
|
||||
|
||||
## 5.0.0
|
||||
* Update Maplibre-Native to [v6.0.0](https://github.com/maplibre/maplibre-native/releases/tag/node-v6.0.0) release by @acalcutt in https://github.com/maptiler/tileserver-gl/pull/1376 and @dependabot in https://github.com/maptiler/tileserver-gl/pull/1381
|
||||
* This first release that use Metal for rendering instead of OpenGL (ES) for macOS.
|
||||
* This the first release that uses OpenGL (ES) 3.0 on Windows and Linux
|
||||
* Note: Windows users may need to update their [c++ redistributable ](https://learn.microsoft.com/en-us/cpp/windows/latest-supported-vc-redist?view=msvc-170) for maplibre-native v6.0.0
|
94
Dockerfile
94
Dockerfile
|
@ -1,22 +1,20 @@
|
|||
FROM ubuntu:jammy AS builder
|
||||
FROM ubuntu:focal AS builder
|
||||
|
||||
ENV NODE_ENV="production"
|
||||
|
||||
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
||||
|
||||
RUN export DEBIAN_FRONTEND=noninteractive && \
|
||||
apt-get update && \
|
||||
apt-get install -y --no-install-recommends --no-install-suggests \
|
||||
RUN set -ex; \
|
||||
export DEBIAN_FRONTEND=noninteractive; \
|
||||
apt-get -qq update; \
|
||||
apt-get -y --no-install-recommends install \
|
||||
build-essential \
|
||||
ca-certificates \
|
||||
curl \
|
||||
gnupg \
|
||||
wget \
|
||||
pkg-config \
|
||||
xvfb \
|
||||
libglfw3-dev \
|
||||
libuv1-dev \
|
||||
libjpeg-turbo8 \
|
||||
libicu70 \
|
||||
libicu66 \
|
||||
libcairo2-dev \
|
||||
libpango1.0-dev \
|
||||
libjpeg-dev \
|
||||
|
@ -27,17 +25,20 @@ RUN export DEBIAN_FRONTEND=noninteractive && \
|
|||
librsvg2-common \
|
||||
libcurl4-openssl-dev \
|
||||
libpixman-1-dev \
|
||||
libpixman-1-0 && \
|
||||
mkdir -p /etc/apt/keyrings && \
|
||||
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg && \
|
||||
echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_22.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list && \
|
||||
apt-get -qq update && \
|
||||
apt-get install -y --no-install-recommends --no-install-suggests nodejs && \
|
||||
npm i -g npm@latest && \
|
||||
apt-get -y remove curl gnupg && \
|
||||
apt-get -y --purge autoremove && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
libpixman-1-0; \
|
||||
apt-get -y --purge autoremove; \
|
||||
apt-get clean; \
|
||||
rm -rf /var/lib/apt/lists/*;
|
||||
|
||||
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
||||
|
||||
RUN wget -qO- https://deb.nodesource.com/setup_18.x | bash; \
|
||||
apt-get install -y nodejs; \
|
||||
npm i -g npm@latest; \
|
||||
apt-get -y remove wget; \
|
||||
apt-get -y --purge autoremove; \
|
||||
apt-get clean; \
|
||||
rm -rf /var/lib/apt/lists/*;
|
||||
|
||||
RUN mkdir -p /usr/src/app
|
||||
|
||||
|
@ -46,52 +47,49 @@ WORKDIR /usr/src/app
|
|||
COPY package.json /usr/src/app
|
||||
COPY package-lock.json /usr/src/app
|
||||
|
||||
RUN npm config set maxsockets 1 && \
|
||||
npm config set fetch-retries 5 && \
|
||||
npm config set fetch-retry-mintimeout 100000 && \
|
||||
npm config set fetch-retry-maxtimeout 600000 && \
|
||||
npm ci --omit=dev && \
|
||||
chown -R root:root /usr/src/app
|
||||
RUN npm ci --omit=dev; \
|
||||
chown -R root:root /usr/src/app;
|
||||
|
||||
FROM ubuntu:jammy AS final
|
||||
FROM ubuntu:focal AS final
|
||||
|
||||
ENV \
|
||||
NODE_ENV="production" \
|
||||
CHOKIDAR_USEPOLLING=1 \
|
||||
CHOKIDAR_INTERVAL=500
|
||||
|
||||
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
||||
|
||||
RUN export DEBIAN_FRONTEND=noninteractive && \
|
||||
groupadd -r node && \
|
||||
useradd -r -g node node && \
|
||||
apt-get -qq update && \
|
||||
apt-get install -y --no-install-recommends --no-install-suggests \
|
||||
RUN set -ex; \
|
||||
export DEBIAN_FRONTEND=noninteractive; \
|
||||
groupadd -r node; \
|
||||
useradd -r -g node node; \
|
||||
apt-get -qq update; \
|
||||
apt-get -y --no-install-recommends install \
|
||||
ca-certificates \
|
||||
curl \
|
||||
gnupg \
|
||||
wget \
|
||||
xvfb \
|
||||
libglfw3 \
|
||||
libuv1 \
|
||||
libjpeg-turbo8 \
|
||||
libicu70 \
|
||||
libicu66 \
|
||||
libcairo2 \
|
||||
libgif7 \
|
||||
libopengl0 \
|
||||
libpixman-1-0 \
|
||||
libcurl4 \
|
||||
librsvg2-2 \
|
||||
libpango-1.0-0 && \
|
||||
mkdir -p /etc/apt/keyrings && \
|
||||
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg && \
|
||||
echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_22.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list && \
|
||||
apt-get -qq update && \
|
||||
apt-get install -y --no-install-recommends --no-install-suggests nodejs && \
|
||||
npm i -g npm@latest && \
|
||||
apt-get -y remove curl gnupg && \
|
||||
apt-get -y --purge autoremove && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
libpango-1.0-0; \
|
||||
apt-get -y --purge autoremove; \
|
||||
apt-get clean; \
|
||||
rm -rf /var/lib/apt/lists/*;
|
||||
|
||||
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
||||
|
||||
RUN wget -qO- https://deb.nodesource.com/setup_18.x | bash; \
|
||||
apt-get install -y nodejs; \
|
||||
npm i -g npm@latest; \
|
||||
apt-get -y remove wget; \
|
||||
apt-get -y --purge autoremove; \
|
||||
apt-get clean; \
|
||||
rm -rf /var/lib/apt/lists/*;
|
||||
|
||||
COPY --from=builder /usr/src/app /usr/src/app
|
||||
|
||||
|
|
|
@ -1,38 +0,0 @@
|
|||
FROM ubuntu:jammy AS builder
|
||||
|
||||
ENV NODE_ENV="devel"
|
||||
|
||||
RUN export DEBIAN_FRONTEND=noninteractive && \
|
||||
apt-get update && \
|
||||
apt-get install -y --no-install-recommends --no-install-suggests \
|
||||
build-essential \
|
||||
ca-certificates \
|
||||
curl \
|
||||
gnupg \
|
||||
pkg-config \
|
||||
xvfb \
|
||||
libglfw3-dev \
|
||||
libuv1-dev \
|
||||
libjpeg-turbo8 \
|
||||
libicu70 \
|
||||
libcairo2-dev \
|
||||
libpango1.0-dev \
|
||||
libjpeg-dev \
|
||||
libgif-dev \
|
||||
librsvg2-dev \
|
||||
gir1.2-rsvg-2.0 \
|
||||
librsvg2-2 \
|
||||
librsvg2-common \
|
||||
libcurl4-openssl-dev \
|
||||
libpixman-1-dev \
|
||||
libpixman-1-0 && \
|
||||
mkdir -p /etc/apt/keyrings && \
|
||||
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg && \
|
||||
echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_22.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list && \
|
||||
apt-get -qq update && \
|
||||
apt-get install -y --no-install-recommends --no-install-suggests nodejs && \
|
||||
npm i -g npm@latest && \
|
||||
apt-get -y remove curl gnupg && \
|
||||
apt-get -y --purge autoremove && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/*
|
|
@ -1,82 +1,42 @@
|
|||
FROM ubuntu:jammy AS builder
|
||||
|
||||
ENV NODE_ENV="production"
|
||||
|
||||
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
||||
|
||||
RUN export DEBIAN_FRONTEND=noninteractive && \
|
||||
apt-get update && \
|
||||
apt-get install -y --no-install-recommends --no-install-suggests \
|
||||
build-essential \
|
||||
ca-certificates \
|
||||
curl \
|
||||
gnupg && \
|
||||
mkdir -p /etc/apt/keyrings && \
|
||||
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg && \
|
||||
echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_22.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list && \
|
||||
apt-get -qq update && \
|
||||
apt-get install -y --no-install-recommends --no-install-suggests nodejs && \
|
||||
npm i -g npm@latest && \
|
||||
apt-get -y remove curl gnupg && \
|
||||
apt-get -y --purge autoremove && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN mkdir -p /usr/src/app
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
COPY package.json /usr/src/app
|
||||
COPY package-lock.json /usr/src/app
|
||||
|
||||
RUN npm config set maxsockets 1 && \
|
||||
npm config set fetch-retries 5 && \
|
||||
npm config set fetch-retry-mintimeout 100000 && \
|
||||
npm config set fetch-retry-maxtimeout 600000 && \
|
||||
npm ci --omit=dev && \
|
||||
chown -R root:root /usr/src/app
|
||||
|
||||
FROM ubuntu:jammy AS final
|
||||
FROM ubuntu:focal
|
||||
|
||||
ENV \
|
||||
NODE_ENV="production" \
|
||||
CHOKIDAR_USEPOLLING=1 \
|
||||
CHOKIDAR_INTERVAL=500
|
||||
|
||||
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
||||
|
||||
RUN export DEBIAN_FRONTEND=noninteractive && \
|
||||
groupadd -r node && \
|
||||
useradd -r -g node node && \
|
||||
apt-get -qq update && \
|
||||
apt-get install -y --no-install-recommends --no-install-suggests \
|
||||
RUN set -ex; \
|
||||
export DEBIAN_FRONTEND=noninteractive; \
|
||||
groupadd -r node; \
|
||||
useradd -r -g node node; \
|
||||
apt-get -qq update; \
|
||||
apt-get -y --no-install-recommends install \
|
||||
ca-certificates \
|
||||
curl \
|
||||
gnupg && \
|
||||
mkdir -p /etc/apt/keyrings && \
|
||||
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg && \
|
||||
echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_22.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list && \
|
||||
apt-get -qq update && \
|
||||
apt-get install -y --no-install-recommends --no-install-suggests nodejs && \
|
||||
npm i -g npm@latest && \
|
||||
apt-get -y remove curl gnupg && \
|
||||
apt-get -y --purge autoremove && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY --from=builder /usr/src/app /usr/src/app
|
||||
|
||||
COPY . /usr/src/app
|
||||
|
||||
RUN mkdir -p /data && \
|
||||
chown node:node /data && \
|
||||
chmod +x /usr/src/app/docker-entrypoint.sh
|
||||
|
||||
VOLUME /data
|
||||
WORKDIR /data
|
||||
wget; \
|
||||
wget -qO- https://deb.nodesource.com/setup_18.x | bash; \
|
||||
apt-get install -y nodejs; \
|
||||
apt-get -y remove wget; \
|
||||
apt-get -y --purge autoremove; \
|
||||
apt-get clean; \
|
||||
rm -rf /var/lib/apt/lists/*;
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
RUN mkdir -p /data; \
|
||||
chown node:node /data; \
|
||||
mkdir -p /usr/src/app;
|
||||
|
||||
VOLUME /data
|
||||
|
||||
WORKDIR /data
|
||||
|
||||
COPY / /usr/src/app
|
||||
|
||||
RUN cd /usr/src/app; \
|
||||
npm install --omit=dev; \
|
||||
chown -R root:root /usr/src/app; \
|
||||
chmod +x /usr/src/app/docker-entrypoint.sh;
|
||||
|
||||
USER node:node
|
||||
|
||||
ENTRYPOINT ["/usr/src/app/docker-entrypoint.sh"]
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
# Simply run "docker build -f Dockerfile_test ."
|
||||
# WARNING: sometimes it fails with a core dumped exception
|
||||
|
||||
FROM ubuntu:jammy
|
||||
FROM ubuntu:focal
|
||||
|
||||
ENV NODE_ENV="development"
|
||||
|
||||
|
@ -13,46 +13,28 @@ RUN set -ex; \
|
|||
unzip \
|
||||
build-essential \
|
||||
ca-certificates \
|
||||
curl \
|
||||
gnupg \
|
||||
wget \
|
||||
pkg-config \
|
||||
xvfb \
|
||||
libglfw3-dev \
|
||||
libuv1-dev \
|
||||
libjpeg-turbo8 \
|
||||
libicu70 \
|
||||
libicu66 \
|
||||
libcairo2-dev \
|
||||
libpango1.0-dev \
|
||||
libjpeg-dev \
|
||||
libgif-dev \
|
||||
librsvg2-dev \
|
||||
gir1.2-rsvg-2.0 \
|
||||
librsvg2-2 \
|
||||
librsvg2-common \
|
||||
libcurl4-openssl-dev \
|
||||
libpixman-1-dev \
|
||||
libpixman-1-0; \
|
||||
apt-get -y --purge autoremove; \
|
||||
apt-get clean; \
|
||||
rm -rf /var/lib/apt/lists/*;
|
||||
|
||||
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
||||
|
||||
RUN mkdir -p /etc/apt/keyrings; \
|
||||
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg; \
|
||||
echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_22.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list; \
|
||||
apt-get -qq update; \
|
||||
libpixman-1-dev; \
|
||||
wget -qO- https://deb.nodesource.com/setup_18.x | bash; \
|
||||
apt-get install -y nodejs; \
|
||||
npm i -g npm@latest; \
|
||||
apt-get -y remove gnupg; \
|
||||
apt-get -y --purge autoremove; \
|
||||
apt-get clean; \
|
||||
rm -rf /var/lib/apt/lists/*;
|
||||
apt-get clean;
|
||||
|
||||
RUN mkdir -p /usr/src/app
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
RUN curl -L -o test_data.zip https://github.com/maptiler/tileserver-gl/releases/download/v1.3.0/test_data.zip; \
|
||||
RUN wget -O test_data.zip https://github.com/maptiler/tileserver-gl/releases/download/v1.3.0/test_data.zip; \
|
||||
unzip -q test_data.zip -d test_data
|
||||
|
||||
COPY package.json .
|
||||
|
|
32
LICENSE.md
32
LICENSE.md
|
@ -1427,35 +1427,3 @@ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
```
|
||||
---
|
||||
|
||||
### [PMTiles](https://github.com/protomaps/pmtiles)
|
||||
```
|
||||
BSD 3-Clause License
|
||||
Copyright 2021 Protomaps LLC
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder nor the names of its contributors
|
||||
may be used to endorse or promote products derived from this software
|
||||
without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
```
|
123
PUBLISHING.md
123
PUBLISHING.md
|
@ -1,112 +1,13 @@
|
|||
# Publishing a New Version
|
||||
# Publishing new version
|
||||
|
||||
This document outlines the process for publishing new versions of this project. We use a GitHub workflow for automated releases, but also provide manual steps for specific situations.
|
||||
|
||||
## Automated Publishing via GitHub Workflow (Recommended)
|
||||
|
||||
This is the preferred method for publishing new versions. It automates the entire process, including version bumping, tagging, building, and publishing.
|
||||
|
||||
1. **Prepare the Release:**
|
||||
|
||||
* **Choose the Version Increment:** Determine the appropriate semantic versioning increment (prerelease, prepatch, preminor, premajor, patch, minor, major).
|
||||
|
||||
* **Update `package.json`:** Use the `npm version` command to bump the version number. This command also supports creating pre-release versions using the `--preid pre` flag.
|
||||
|
||||
```bash
|
||||
# Example: Increment to a new patch version
|
||||
npm version patch --no-git-tag-version
|
||||
|
||||
# Example: Increment to a new minor version
|
||||
npm version minor --no-git-tag-version
|
||||
|
||||
# Example: Increment to a new major version
|
||||
npm version major --no-git-tag-version
|
||||
|
||||
# Example: Create a pre-release (e.g., -pre.0) version
|
||||
npm version prepatch --preid pre --no-git-tag-version
|
||||
# OR
|
||||
npm version preminor --preid pre --no-git-tag-version
|
||||
# OR
|
||||
npm version premajor --preid pre --no-git-tag-version
|
||||
|
||||
# Example: Increment an existing pre-release version (e.g., -pre.0 to -pre.1)
|
||||
npm version prerelease --preid pre --no-git-tag-version
|
||||
```
|
||||
|
||||
* `--no-git-tag-version`: This prevents `npm version` from automatically creating a git tag, as the GitHub workflow will handle this later.
|
||||
* `--preid pre`: Specifies that "pre" is the pre-release identifier.
|
||||
|
||||
* **Update the Changelog (`CHANGELOG.md`):** Add a new section for the release. The heading *must* exactly match the format `## <VERSION>`, where `<VERSION>` is the full version number from `package.json`. Describe the changes included in the release.
|
||||
|
||||
```markdown
|
||||
## 1.2.3
|
||||
* Added new feature X.
|
||||
```
|
||||
|
||||
2. **Commit and Push:** Commit the changes to `package.json`, `package-lock.json` and `CHANGELOG.md` to a the `master` branch. Create a pull request (PR) for review. If you are an administrator, you may push directly, but a PR is generally recommended for code review.
|
||||
|
||||
3. **Run the GitHub Workflow:** Once the PR is merged (or changes pushed directly), trigger the "Build, Test, Release" GitHub workflow. This workflow is responsible for:
|
||||
|
||||
* Building the project.
|
||||
* Running tests.
|
||||
* Creating an NPM package.
|
||||
* Building and pushing Docker images.
|
||||
* Creating a GitHub Release.
|
||||
* Creating a Git tag for the release.
|
||||
|
||||
## Manual Publishing (For Special Cases)
|
||||
|
||||
Use the following steps *only* when the automated workflow is not suitable (e.g., for debugging, specific environment needs).
|
||||
|
||||
1. **Update Version in `package.json`:** Modify the `version` field in `package.json` to the desired version number.
|
||||
|
||||
2. **Create and Push Git Tag:**
|
||||
|
||||
```bash
|
||||
git tag vX.X.X # Replace X.X.X with the version number
|
||||
git push origin --tags
|
||||
```
|
||||
|
||||
3. **NPM Publish Options (Choose ONE):**
|
||||
|
||||
* **Option A: Publish only the full `tileserver-gl` version:**
|
||||
|
||||
```bash
|
||||
npm publish --access public
|
||||
```
|
||||
|
||||
* **Option B: Build and Publish both `tileserver-gl` and `tileserver-gl-light` using `publish.js`:**
|
||||
|
||||
```bash
|
||||
# This script builds the light version and publishes both tileserver-gl and tileserver-gl-light to NPM.
|
||||
node publish.js
|
||||
```
|
||||
|
||||
* **Option C: Build only the `tileserver-gl-light` version (no publish):**
|
||||
|
||||
```bash
|
||||
# This script ONLY builds the light version (e.g., for local testing or Docker image creation) without publishing.
|
||||
node publish.js --no-publish
|
||||
```
|
||||
|
||||
4. **Build and Push Docker Images:**
|
||||
|
||||
```bash
|
||||
# Build the main image
|
||||
docker buildx build --platform linux/amd64 -t maptiler/tileserver-gl:latest -t maptiler/tileserver-gl:X.X.X . # Replace X.X.X
|
||||
docker push maptiler/tileserver-gl --all-tags
|
||||
|
||||
# Build the light image
|
||||
cd light
|
||||
docker buildx build --platform linux/amd64 -t maptiler/tileserver-gl-light:latest -t maptiler/tileserver-gl-light:X.X.X . # Replace X.X.X
|
||||
docker push maptiler/tileserver-gl-light --all-tags
|
||||
cd .. # Return to the project root
|
||||
```
|
||||
* Make sure you are logged into the docker registry before pushing. `docker login`
|
||||
|
||||
**Important Considerations for Manual Publishing:**
|
||||
|
||||
* **Consistency:** Ensure the version number in `package.json`, the Git tag, and the Docker image tags are identical.
|
||||
* **Credentials:** Verify that you have the necessary permissions to push Docker images and publish to NPM.
|
||||
* **Cleanliness:** Before building Docker images, ensure your working directory is clean to avoid including unwanted files in the image.
|
||||
* **Error Handling:** Manually publishing is more prone to errors. Double-check each step to avoid issues.
|
||||
- Update version in `package.json`
|
||||
- `git tag vx.x.x`
|
||||
- `git push --tags`
|
||||
- `docker buildx build --platform linux/amd64 -t maptiler/tileserver-gl:latest -t maptiler/tileserver-gl:[version] .`
|
||||
- `docker push maptiler/tileserver-gl --all-tags`
|
||||
- `npm publish --access public` or `node publish.js`
|
||||
- `node publish.js --no-publish`
|
||||
- `cd light`
|
||||
- `docker buildx build --platform linux/amd64 -t maptiler/tileserver-gl-light:latest -t maptiler/tileserver-gl-light:[version] .`
|
||||
- `docker push maptiler/tileserver-gl-light --all-tags`
|
||||
- `npm publish --access public`
|
||||
|
|
40
README.md
40
README.md
|
@ -1,31 +1,5 @@
|
|||

|
||||
|
||||
# My TileServer GL
|
||||
|
||||
creare un folder dove mettere la mappa e i layers
|
||||
|
||||
Download vector tiles from [OpenMapTiles](https://data.maptiler.com/downloads/planet/).
|
||||
|
||||
scaricare i layers
|
||||
|
||||
wget https://github.com/maptiler/tileserver-gl/releases/download/v1.3.0/test_data.zip
|
||||
unzip test_data.zip
|
||||
|
||||
modificare nano config.json per inserire il nome del file .mbtiles (x es planetOSM.mbtiles )
|
||||
|
||||
far partire il docker
|
||||
|
||||
services:
|
||||
tileserver-gl:
|
||||
volumes:
|
||||
- /home/nvme/dockerdata/tileserver:/data
|
||||
ports:
|
||||
- 8115:8080
|
||||
image: maptiler/tileserver-gl:latest
|
||||
|
||||
oppure
|
||||
|
||||
sudo docker run -d -v /home/nvme/dockerdata/tileserver:/data -p 8115:8080 maptiler/tileserver-gl:latest
|
||||
|
||||
# TileServer GL
|
||||
[](https://github.com/maptiler/tileserver-gl/actions/workflows/pipeline.yml)
|
||||
|
@ -36,7 +10,7 @@ Vector and raster maps with GL styles. Server-side rendering by MapLibre GL Nati
|
|||
Download vector tiles from [OpenMapTiles](https://data.maptiler.com/downloads/planet/).
|
||||
## Getting Started with Node
|
||||
|
||||
Make sure you have Node.js version **18.17.0** or above installed. Node 22 is recommended. (running `node -v` it should output something like `v22.x.x`). Running without docker requires [Native dependencies](https://maptiler-tileserver.readthedocs.io/en/latest/installation.html#npm) to be installed first.
|
||||
Make sure you have Node.js version **14.20.0** or above installed. Node 18 is recommended. (running `node -v` it should output something like `v18.x.x`). Running without docker requires [Native dependencies](https://maptiler-tileserver.readthedocs.io/en/latest/installation.html#npm) to be installed first.
|
||||
|
||||
Install `tileserver-gl` with server-side raster rendering of vector tiles with npm.
|
||||
|
||||
|
@ -49,7 +23,7 @@ Once installed, you can use it like the following examples.
|
|||
using a mbtiles file
|
||||
```bash
|
||||
wget https://github.com/maptiler/tileserver-gl/releases/download/v1.3.0/zurich_switzerland.mbtiles
|
||||
tileserver-gl --file zurich_switzerland.mbtiles
|
||||
tileserver-gl --mbtiles zurich_switzerland.mbtiles
|
||||
[in your browser, visit http://[server ip]:8080]
|
||||
```
|
||||
|
||||
|
@ -70,7 +44,7 @@ An alternative to npm to start the packed software easier is to install [Docker]
|
|||
Example using a mbtiles file
|
||||
```bash
|
||||
wget https://github.com/maptiler/tileserver-gl/releases/download/v1.3.0/zurich_switzerland.mbtiles
|
||||
docker run --rm -it -v $(pwd):/data -p 8080:8080 maptiler/tileserver-gl:latest --file zurich_switzerland.mbtiles
|
||||
docker run --rm -it -v $(pwd):/data -p 8080:8080 maptiler/tileserver-gl --mbtiles zurich_switzerland.mbtiles
|
||||
[in your browser, visit http://[server ip]:8080]
|
||||
```
|
||||
|
||||
|
@ -78,18 +52,18 @@ Example using a config.json + style + mbtiles file
|
|||
```bash
|
||||
wget https://github.com/maptiler/tileserver-gl/releases/download/v1.3.0/test_data.zip
|
||||
unzip test_data.zip
|
||||
docker run --rm -it -v $(pwd):/data -p 8080:8080 maptiler/tileserver-gl:latest
|
||||
docker run --rm -it -v $(pwd):/data -p 8080:8080 maptiler/tileserver-gl
|
||||
[in your browser, visit http://[server ip]:8080]
|
||||
```
|
||||
|
||||
Example using a different path
|
||||
```bash
|
||||
docker run --rm -it -v /your/local/config/path:/data -p 8080:8080 maptiler/tileserver-gl:latest
|
||||
docker run --rm -it -v /your/local/config/path:/data -p 8080:8080 maptiler/tileserver-gl
|
||||
```
|
||||
replace '/your/local/config/path' with the path to your config file
|
||||
|
||||
|
||||
Alternatively, you can use the `maptiler/tileserver-gl-light:latest` docker image instead, which is pure javascript, does not have any native dependencies, and can run anywhere, but does not contain rasterization on the server side made with Maplibre GL Native.
|
||||
Alternatively, you can use the `maptiler/tileserver-gl-light` docker image instead, which is pure javascript, does not have any native dependencies, and can run anywhere, but does not contain rasterization on the server side made with Maplibre GL Native.
|
||||
|
||||
## Getting Started with Linux cli
|
||||
|
||||
|
@ -107,7 +81,7 @@ xvfb-run --server-args="-screen 0 1024x768x24" node .
|
|||
|
||||
## Documentation
|
||||
|
||||
You can read the full documentation of this project at https://tileserver.readthedocs.io/en/latest/.
|
||||
You can read the full documentation of this project at https://maptiler-tileserver.readthedocs.io/.
|
||||
|
||||
## Alternative
|
||||
|
||||
|
|
|
@ -28,8 +28,8 @@ docker build -t tileserver-gl-light .
|
|||
[Download from OpenMapTiles.com](https://openmaptiles.com/downloads/planet/) or [create](https://github.com/openmaptiles/openmaptiles) your vector tile, and run following in directory contains your *.mbtiles.
|
||||
|
||||
```
|
||||
docker run --rm -it -v $(pwd):/data -p 8080:8080 tileserver-gl-light
|
||||
docker run --rm -it -v $(pwd):/data -p 8000:80 tileserver-gl-light
|
||||
```
|
||||
|
||||
## Documentation
|
||||
You can read full documentation of this project at https://maptiler-tileserver.readthedocs.io/
|
||||
You can read full documentation of this project at https://tileserver.readthedocs.io/.
|
151
docs/config.rst
151
docs/config.rst
|
@ -16,21 +16,15 @@ Example:
|
|||
"sprites": "sprites",
|
||||
"icons": "icons",
|
||||
"styles": "styles",
|
||||
"mbtiles": "data",
|
||||
"pmtiles": "data",
|
||||
"files": "files"
|
||||
"mbtiles": ""
|
||||
},
|
||||
"domains": [
|
||||
"localhost:8080",
|
||||
"127.0.0.1:8080"
|
||||
],
|
||||
"formatOptions": {
|
||||
"jpeg": {
|
||||
"quality": 80
|
||||
},
|
||||
"webp": {
|
||||
"quality": 90
|
||||
}
|
||||
"formatQuality": {
|
||||
"jpeg": 80,
|
||||
"webp": 90
|
||||
},
|
||||
"maxScaleFactor": 3,
|
||||
"maxSize": 2048,
|
||||
|
@ -39,8 +33,6 @@ Example:
|
|||
"serveAllStyles": false,
|
||||
"serveStaticMaps": true,
|
||||
"allowRemoteMarkerIcons": true,
|
||||
"allowInlineMarkerImages": true,
|
||||
"staticAttributionText": "© OpenMapTiles © OpenStreetMaps",
|
||||
"tileMargin": 0
|
||||
},
|
||||
"styles": {
|
||||
|
@ -57,9 +49,6 @@ Example:
|
|||
"tilejson": {
|
||||
"format": "webp"
|
||||
}
|
||||
},
|
||||
"remote": {
|
||||
"style": "https://demotiles.maplibre.org/style.json"
|
||||
}
|
||||
},
|
||||
"data": {
|
||||
|
@ -93,24 +82,10 @@ Path to the html (relative to ``root`` path) to use as a front page.
|
|||
Use ``true`` (or nothing) to serve the default TileServer GL front page with list of styles and data.
|
||||
Use ``false`` to disable the front page altogether (404).
|
||||
|
||||
``formatOptions``
|
||||
``formatQuality``
|
||||
-----------------
|
||||
|
||||
You can use this to specify options for the generation of images in the supported file formats.
|
||||
For WebP, the only supported option is ``quality`` [0-100].
|
||||
For JPEG, the only supported options are ``quality`` [0-100] and ``progressive`` [true, false].
|
||||
For PNG, the full set of options `exposed by the sharp library <https://sharp.pixelplumbing.com/api-output#png>`_ is available, except ``force`` and ``colours`` (use ``colors``). If not set, their values are the defaults from ``sharp``.
|
||||
|
||||
For example::
|
||||
|
||||
"formatOptions": {
|
||||
"png": {
|
||||
"palette": true,
|
||||
"colors": 4
|
||||
}
|
||||
}
|
||||
|
||||
Note: ``formatOptions`` replaced the ``formatQuality`` option in previous versions of TileServer GL.
|
||||
Quality of the compression of individual image formats. [0-100]
|
||||
|
||||
``maxScaleFactor``
|
||||
-----------
|
||||
|
@ -154,19 +129,6 @@ If you have plenty of memory, try setting these equal to or slightly above your
|
|||
If you need to conserve memory, try lower values for scale factors that are less common.
|
||||
Default is ``[16, 8, 4]``.
|
||||
|
||||
``pbfAlias``
|
||||
------------------------
|
||||
|
||||
Some CDNs did not handle .pbf extension as a static file correctly.
|
||||
The default URLs (with .pbf) are always available, but an alternative can be set.
|
||||
An example extension suffix would be ".pbf.pict".
|
||||
|
||||
``serveAllFonts``
|
||||
------------------------
|
||||
|
||||
If this option is enabled, all the fonts from the ``paths.fonts`` will be served.
|
||||
Otherwise only the fonts referenced by available styles will be served.
|
||||
|
||||
``serveAllStyles``
|
||||
------------------------
|
||||
|
||||
|
@ -174,23 +136,11 @@ If this option is enabled, all the styles from the ``paths.styles`` will be serv
|
|||
The process will also watch for changes in this directory and remove/add more styles dynamically.
|
||||
It is recommended to also use the ``serveAllFonts`` option when using this option.
|
||||
|
||||
``serveStaticMaps``
|
||||
------------------------
|
||||
|
||||
If this option is enabled, all the static map endpoints will be served.
|
||||
Default is ``true``.
|
||||
|
||||
``watermark``
|
||||
-----------
|
||||
|
||||
Optional string to be rendered into the raster tiles and static maps as watermark (bottom-left corner).
|
||||
Not used by default.
|
||||
|
||||
``staticAttributionText``
|
||||
-----------
|
||||
|
||||
Optional string to be rendered in the static images endpoint. Text will be rendered in the bottom-right corner,
|
||||
and styled similar to attribution on web-based maps (text only, links not supported).
|
||||
Optional string to be rendered into the raster tiles (and static maps) as watermark (bottom-left corner).
|
||||
Can be used for hard-coding attributions etc. (can also be specified per-style).
|
||||
Not used by default.
|
||||
|
||||
``allowRemoteMarkerIcons``
|
||||
|
@ -200,19 +150,12 @@ Allows the rendering of marker icons fetched via http(s) hyperlinks.
|
|||
For security reasons only allow this if you can control the origins from where the markers are fetched!
|
||||
Default is to disallow fetching of icons from remote sources.
|
||||
|
||||
``allowInlineMarkerImages``
|
||||
--------------
|
||||
Allows the rendering of inline marker icons or base64 urls.
|
||||
For security reasons only allow this if you can control the origins from where the markers are fetched!
|
||||
Not used by default.
|
||||
|
||||
|
||||
``styles``
|
||||
==========
|
||||
|
||||
Each item in this object defines one style (map). It can have the following options:
|
||||
|
||||
* ``style`` -- name of the style json file or url of a remote hosted style [required]
|
||||
* ``style`` -- name of the style json file [required]
|
||||
* ``serve_rendered`` -- whether to render the raster tiles for this style or not
|
||||
* ``serve_data`` -- whether to allow access to the original tiles, sprites and required glyphs
|
||||
* ``tilejson`` -- properties to add to the TileJSON created for the raster data
|
||||
|
@ -222,44 +165,11 @@ Each item in this object defines one style (map). It can have the following opti
|
|||
``data``
|
||||
========
|
||||
|
||||
Each item specifies one data source which should be made accessible by the server. It has to have one of the following options:
|
||||
Each item specifies one data source which should be made accessible by the server. It has the following options:
|
||||
|
||||
* ``mbtiles`` -- name of the mbtiles file
|
||||
* ``pmtiles`` -- name of the pmtiles file or url.
|
||||
* ``mbtiles`` -- name of the mbtiles file [required]
|
||||
|
||||
For example::
|
||||
|
||||
"data": {
|
||||
"source1": {
|
||||
"mbtiles": "source1.mbtiles"
|
||||
},
|
||||
"source2": {
|
||||
"pmtiles": "source2.pmtiles"
|
||||
},
|
||||
"source3": {
|
||||
"pmtiles": "https://foo.lan/source3.pmtiles"
|
||||
}
|
||||
}
|
||||
|
||||
The data source does not need to be specified here unless you explicitly want to serve the raw data.
|
||||
|
||||
Serving Terrain Tiles
|
||||
--------------
|
||||
|
||||
If you serve terrain tiles, it is possible to configure an ``encoding`` with ``mapbox`` or ``terrarium`` to enable a terrain preview mode and the ``elevation`` api for the ``data`` endpoint.
|
||||
|
||||
For example::
|
||||
|
||||
"data": {
|
||||
"terrain1": {
|
||||
"mbtiles": "terrain1.mbtiles",
|
||||
"encoding": "mapbox"
|
||||
},
|
||||
"terrain2": {
|
||||
"pmtiles": "terrain2.pmtiles"
|
||||
"encoding": "terrarium"
|
||||
}
|
||||
}
|
||||
The mbtiles file does not need to be specified here unless you explicitly want to serve the raw data.
|
||||
|
||||
Referencing local files from style JSON
|
||||
=======================================
|
||||
|
@ -269,46 +179,21 @@ You can link various data sources from the style JSON (for example even remote T
|
|||
MBTiles
|
||||
-------
|
||||
|
||||
To specify that you want to use local mbtiles, use to following syntax: ``mbtiles://source1.mbtiles``.
|
||||
TileServer-GL will try to find the file ``source1.mbtiles`` in ``root`` + ``mbtiles`` path.
|
||||
To specify that you want to use local mbtiles, use to following syntax: ``mbtiles://switzerland.mbtiles``.
|
||||
The TileServer-GL will try to find the file ``switzerland.mbtiles`` in ``root`` + ``mbtiles`` path.
|
||||
|
||||
For example::
|
||||
|
||||
"sources": {
|
||||
"source1": {
|
||||
"url": "mbtiles://source1.mbtiles",
|
||||
"url": "mbtiles://switzerland.mbtiles",
|
||||
"type": "vector"
|
||||
}
|
||||
}
|
||||
|
||||
Alternatively, you can use ``mbtiles://{source1}`` to reference existing data object from the config.
|
||||
In this case, the server will look into the ``config.json`` to determine what file to use by data id.
|
||||
For the config above, this is equivalent to ``mbtiles://source1.mbtiles``.
|
||||
|
||||
PMTiles
|
||||
-------
|
||||
|
||||
To specify that you want to use local pmtiles, use to following syntax: ``pmtiles://source2.pmtiles``.
|
||||
TileServer-GL will try to find the file ``source2.pmtiles`` in ``root`` + ``pmtiles`` path.
|
||||
|
||||
To specify that you want to use a url based pmtiles, use to following syntax: ``pmtiles://https://foo.lan/source3.pmtiles``.
|
||||
|
||||
For example::
|
||||
|
||||
"sources": {
|
||||
"source2": {
|
||||
"url": "pmtiles://source2.pmtiles",
|
||||
"type": "vector"
|
||||
},
|
||||
"source3": {
|
||||
"url": "pmtiles://https://foo.lan/source3.pmtiles",
|
||||
"type": "vector"
|
||||
}
|
||||
}
|
||||
|
||||
Alternatively, you can use ``pmtiles://{source2}`` to reference existing data object from the config.
|
||||
In this case, the server will look into the ``config.json`` to determine what file to use by data id.
|
||||
For the config above, this is equivalent to ``pmtiles://source2.mbtiles``.
|
||||
Alternatively, you can use ``mbtiles://{zurich-vector}`` to reference existing data object from the config.
|
||||
In this case, the server will look into the ``config.json`` to determine what mbtiles file to use.
|
||||
For the config above, this is equivalent to ``mbtiles://zurich.mbtiles``.
|
||||
|
||||
Sprites
|
||||
-------
|
||||
|
|
|
@ -21,7 +21,7 @@ Endpoint data can be configured to be cached by Cloudflare. For example to cache
|
|||
|
||||
Create a rule which matches ``hostname (equal)`` and ``URI Path (ends with)`` for ``.pbf`` and ``.json`` fields. Set cache status to eligible for cache to enable the caching and overwrite the ``Edge TTL`` with ``Browser TTL`` to be 7 days (depends on your application usage).
|
||||
|
||||
This will ensure that your tiles are cached on the client side and by Cloudflare for seven days. If the tileserver is down or user has no internet access it will try to use cached tiles.
|
||||
This will ensure that Cloudflare will cache your tiles on Cloudflare side for seven days aswell on the client side. If the tileserver is down or user has no internet access it will try to use cached tiles from Cloudflare or local.
|
||||
|
||||
Note that ``Browser TTL`` will overwrite expiration dates on the client device. If you rebuild your maps, old tiles will be rendered until it expires or cache is cleared on the client device.
|
||||
|
||||
|
@ -115,7 +115,7 @@ An example nginx reverse proxy server configuration for HTTPS connections. It en
|
|||
|
||||
location / {
|
||||
# This include directive sets up required headers for proxy and proxy cache.
|
||||
# As well it includes the required ``X-Forwarded-*`` headers for tileserver to properly generate tiles.
|
||||
# Aswell it includes the required ``X-Forwarded-*`` headers for tileserver to propely generate tiles.
|
||||
include proxy_params;
|
||||
|
||||
proxy_pass http://127.0.0.1:8080/;
|
||||
|
|
|
@ -8,17 +8,16 @@ Styles
|
|||
======
|
||||
* Styles are served at ``/styles/{id}/style.json`` (+ array at ``/styles.json``)
|
||||
|
||||
* Sprites at ``/styles/{id}/sprite[/spriteID][@2x].{format}``
|
||||
* Sprites at ``/styles/{id}/sprite[@2x].{format}``
|
||||
* Fonts at ``/fonts/{fontstack}/{start}-{end}.pbf``
|
||||
|
||||
Rendered tiles
|
||||
==============
|
||||
* Rendered tiles are served at ``/styles/{id}[/{tileSize}]/{z}/{x}/{y}[@2x].{format}``
|
||||
* Rendered tiles are served at ``/styles/{id}/{z}/{x}/{y}[@2x].{format}``
|
||||
|
||||
* The optional ratio ``@2x`` (ex. ``@2x``, ``@3x``, ``@4x``) part can be used to render HiDPI (retina) tiles
|
||||
* The optional tile size ``/{tileSize}`` (ex. ``/256``, ``/512``). if omitted, tileSize defaults to 256.
|
||||
* The optional ``@2x`` (or ``@3x``, ``@4x``) part can be used to render HiDPI (retina) tiles
|
||||
* Available formats: ``png``, ``jpg`` (``jpeg``), ``webp``
|
||||
* TileJSON at ``/styles[/{tileSize}]/{id}.json``
|
||||
* TileJSON at ``/styles/{id}.json``
|
||||
|
||||
* The rendered tiles are not available in the ``tileserver-gl-light`` version.
|
||||
|
||||
|
@ -36,31 +35,21 @@ Static images
|
|||
|
||||
* All the static image endpoints additionally support following query parameters:
|
||||
|
||||
* ``path`` - ``((fill|stroke|width)\:[^\|]+\|)*(enc:.+|-?\d+(\.\d*)?,-?\d+(\.\d*)?(\|-?\d+(\.\d*)?,-?\d+(\.\d*)?)+)``
|
||||
|
||||
* comma-separated ``lng,lat``, pipe-separated pairs
|
||||
|
||||
* e.g. ``path=5.9,45.8|5.9,47.8|10.5,47.8|10.5,45.8|5.9,45.8``
|
||||
|
||||
* `Google Encoded Polyline Format <https://developers.google.com/maps/documentation/utilities/polylinealgorithm>`_
|
||||
|
||||
* e.g. ``path=enc:_p~iF~ps|U_ulLnnqC_mqNvxq`@``
|
||||
* If 'enc:' is used, the rest of the path parameter is considered to be part of the encoded polyline string -- do not specify the coordinate pairs.
|
||||
|
||||
* With options (fill|stroke|width)
|
||||
|
||||
* e.g. ``path=stroke:yellow|width:2|fill:green|5.9,45.8|5.9,47.8|10.5,47.8|10.5,45.8|5.9,45.8`` or ``path=stroke:blue|width:1|fill:yellow|enc:_p~iF~ps|U_ulLnnqC_mqNvxq`@``
|
||||
* ``path`` - comma-separated ``lng,lat``, pipe-separated pairs
|
||||
|
||||
* e.g. ``5.9,45.8|5.9,47.8|10.5,47.8|10.5,45.8|5.9,45.8``
|
||||
* can be provided multiple times
|
||||
* or pass the path as per [Maptiler Cloud API](https://docs.maptiler.com/cloud/api/static-maps/)
|
||||
* Match pattern: ((fill|stroke|width)\:[^\|]+\|)*((enc:.+)|((-?\d+\.?\d*,-?\d+\.?\d*\|)+(-?\d+\.?\d*,-?\d+\.?\d*)))
|
||||
|
||||
* ``latlng`` - indicates coordinates are in ``lat,lng`` order rather than the usual ``lng,lat`` for paths and markers
|
||||
* ``fill`` - default color to use as the fill (e.g. ``red``, ``rgba(255,255,255,0.5)``, ``#0000ff``) for all paths
|
||||
* ``stroke`` - default color of the path stroke for all paths
|
||||
* ``width`` - default width of the stroke for all paths
|
||||
* ``linecap`` - rendering style for the start and end points of all paths - see https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineCap
|
||||
* ``linejoin`` - rendering style for joining successive segments of all paths when the direction changes - see https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineJoin
|
||||
* ``border`` - color of the optional border stroke for all paths ; the border is like a halo around the stroke
|
||||
* ``borderwidth`` - width of the border stroke (default 10% of stroke width) for all paths
|
||||
* ``latlng`` - indicates coordinates are in ``lat,lng`` order rather than the usual ``lng,lat``
|
||||
* ``fill`` - color to use as the fill (e.g. ``red``, ``rgba(255,255,255,0.5)``, ``#0000ff``)
|
||||
* ``stroke`` - color of the path stroke
|
||||
* ``width`` - width of the stroke
|
||||
* ``linecap`` - rendering style for the start and end points of the path
|
||||
* ``linejoin`` - rendering style for overlapping segments of the path with differing directions
|
||||
* ``border`` - color of the optional border path stroke
|
||||
* ``borderwidth`` - width of the border stroke (default 10% of width)
|
||||
* ``marker`` - Marker in format ``lng,lat|iconPath|option|option|...``
|
||||
|
||||
* Will be rendered with the bottom center at the provided location
|
||||
|
@ -77,7 +66,7 @@ Static images
|
|||
* scales with ``scale`` parameter since image placement is relative to it's size
|
||||
* e.g. ``2,-4`` - Image will be moved 2 pixel to the right and 4 pixel in the upwards direction from the provided location
|
||||
|
||||
* e.g. ``5.9,45.8|marker-icon.png|scale:0.5|offset:2,-4``
|
||||
* e.g. ``5.9,45.8|marker-start.svg|scale:0.5|offset:2,-4``
|
||||
* can be provided multiple times
|
||||
|
||||
* ``padding`` - "percentage" padding for fitted endpoints (area-based and path autofit)
|
||||
|
@ -92,41 +81,17 @@ Static images
|
|||
|
||||
Source data
|
||||
===========
|
||||
* Source data are served at ``/data/{id}/{z}/{x}/{y}.{format}``
|
||||
* Source data are served at ``/data/{mbtiles}/{z}/{x}/{y}.{format}``
|
||||
|
||||
* Format depends on the source file (usually ``png`` or ``pbf``)
|
||||
|
||||
* ``geojson`` is also available (useful for inspecting the tiles) in case the original format is ``pbf``
|
||||
|
||||
* TileJSON at ``/data/{id}.json``
|
||||
|
||||
* If terrain mbtile data is served and ``encoding`` is configured (see config) the elevation can be queried
|
||||
|
||||
* by ``/data/{id}/elevation/{z}/{x}/{y}`` for the tile
|
||||
|
||||
* or ``/data/{id}/elevation/{z}/{long}/{lat}`` for the coordinate
|
||||
|
||||
* the result will be a json object like ``{"z":7,"x":68,"y":45,"red":134,"green":66,"blue":0,"latitude":11.84069,"longitude":46.04798,"elevation":1602}``
|
||||
|
||||
* The elevation api is not available in the ``tileserver-gl-light`` version.
|
||||
|
||||
Static files
|
||||
===========
|
||||
* Static files are served at ``/files/{filename}``
|
||||
|
||||
* The source folder can be configured (``options.paths.files``), default is ``public/files``
|
||||
|
||||
* This feature can be used to serve ``geojson`` files for styles and rendered tiles.
|
||||
|
||||
* Keep in mind, that each rendered tile loads the whole geojson file, if performance matters a conversion to a tiled format (e.g. with https://github.com/felt/tippecanoe)may be a better approch.
|
||||
|
||||
* Use ``file://{filename}`` to have matching paths for both endoints
|
||||
* TileJSON at ``/data/{mbtiles}.json``
|
||||
|
||||
TileJSON arrays
|
||||
===============
|
||||
Array of all TileJSONs is at ``[/{tileSize}]/index.json`` (``[/{tileSize}]/rendered.json``; ``/data.json``)
|
||||
|
||||
* The optional tile size ``/{tileSize}`` (ex. ``/256``, ``/512``). if omitted, tileSize defaults to 256.
|
||||
Array of all TileJSONs is at ``/index.json`` (``/rendered.json``; ``/data.json``)
|
||||
|
||||
List of available fonts
|
||||
=======================
|
||||
|
|
|
@ -11,54 +11,67 @@ Just run ``docker run --rm -it -v $(pwd):/data -p 8080:8080 maptiler/tileserver-
|
|||
|
||||
Additional options (see :doc:`/usage`) can be passed to the TileServer GL by appending them to the end of this command. You can, for example, do the following:
|
||||
|
||||
* ``docker run ... maptiler/tileserver-gl --file my-tiles.mbtiles`` -- explicitly specify which mbtiles to use (if you have more in the folder)
|
||||
* ``docker run ... maptiler/tileserver-gl --mbtiles my-tiles.mbtiles`` -- explicitly specify which mbtiles to use (if you have more in the folder)
|
||||
* ``docker run ... maptiler/tileserver-gl --verbose`` -- to see the default config created automatically
|
||||
|
||||
npm
|
||||
===
|
||||
|
||||
npm is supported on the following platforms with `Native Dependencies <#id1>`_ installed.
|
||||
Just run ``npm install -g tileserver-gl``.
|
||||
|
||||
- Operating systems:
|
||||
|
||||
- Ubuntu 22.04 (x64/arm64)
|
||||
- macOS 14 (x64/arm64)
|
||||
- Windows (x64)
|
||||
|
||||
- Node.js 18,20
|
||||
|
||||
Install globally from npmjs.
|
||||
------------------------------
|
||||
::
|
||||
|
||||
npm install -g tileserver-gl
|
||||
tileserver-gl
|
||||
|
||||
Install locally from source
|
||||
-------------------
|
||||
::
|
||||
|
||||
git clone https://github.com/maptiler/tileserver-gl.git
|
||||
cd tileserver-gl
|
||||
npm install
|
||||
node .
|
||||
|
||||
Native dependencies
|
||||
-------------------
|
||||
|
||||
Ubuntu 22.04 (x64/arm64)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
- apt install build-essential pkg-config xvfb libglfw3-dev libuv1-dev libjpeg-turbo8 libicu70 libcairo2-dev libpango1.0-dev libjpeg-dev libgif-dev librsvg2-dev gir1.2-rsvg-2.0 librsvg2-2 librsvg2-common libcurl4-openssl-dev libpixman-1-dev libpixman-1-0
|
||||
There are some native dependencies that you need to make sure are installed if you plan to run the TileServer GL natively without docker.
|
||||
The precise package names you need to install may differ on various platforms.
|
||||
|
||||
MacOS 14 (x64/arm64)
|
||||
~~~~~~~~~~~~~~~~~~~~~~
|
||||
- brew install pkg-config cairo pango libpng jpeg giflib librsvg
|
||||
These are required on Debian 11:
|
||||
* ``libgles2-mesa``
|
||||
* ``libegl1``
|
||||
* ``xvfb``
|
||||
* ``xauth``
|
||||
* ``libopengl0``
|
||||
* ``libcurl4``
|
||||
* ``curl``
|
||||
* ``libuv1-dev``
|
||||
* ``libc6-dev``
|
||||
* ``http://archive.ubuntu.com/ubuntu/pool/main/libj/libjpeg-turbo/libjpeg-turbo8_2.0.3-0ubuntu1_amd64.deb``
|
||||
* ``http://archive.ubuntu.com/ubuntu/pool/main/i/icu/libicu66_66.1-2ubuntu2_amd64.deb``
|
||||
|
||||
Windows (x64)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
- `Microsoft Visual C++ Redistributable <https://aka.ms/vs/17/release/vc_redist.x64.exe>`_
|
||||
These are required on Ubuntu 20.04:
|
||||
* ``libcairo2-dev``
|
||||
* ``libjpeg8-dev``
|
||||
* ``libpango1.0-dev``
|
||||
* ``libgif-dev``
|
||||
* ``build-essential``
|
||||
* ``g++``
|
||||
* ``xvfb``
|
||||
* ``libgles2-mesa-dev``
|
||||
* ``libgbm-dev``
|
||||
* ``libxxf86vm-dev``
|
||||
|
||||
``tileserver-gl-light`` on npm
|
||||
==============================
|
||||
|
||||
Alternatively, you can use ``tileserver-gl-light`` package instead, which is pure javascript (does not have any native dependencies) and can run anywhere, but does not contain rasterization features.
|
||||
|
||||
|
||||
From source
|
||||
===========
|
||||
|
||||
Make sure you have Node v18 (nvm install 18) and run::
|
||||
|
||||
npm install
|
||||
node .
|
||||
|
||||
|
||||
On OSX
|
||||
======
|
||||
|
||||
Make sure to have dependencies of canvas_ package installed::
|
||||
|
||||
brew install pkg-config cairo libpng jpeg giflib
|
||||
|
||||
|
||||
.. _canvas: https://www.npmjs.com/package/canvas
|
||||
|
|
|
@ -6,32 +6,28 @@ Getting started
|
|||
======
|
||||
::
|
||||
|
||||
Usage: main.js tileserver-gl [file] [options]
|
||||
Usage: main.js tileserver-gl [mbtiles] [options]
|
||||
|
||||
Options:
|
||||
--file <file> MBTiles or PMTiles file
|
||||
|
||||
-h, --help output usage information
|
||||
--mbtiles <file> MBTiles file (uses demo configuration);
|
||||
ignored if the configuration file is also specified
|
||||
--mbtiles <file> (DEPRECIATED) MBTiles file
|
||||
ignored if file is also specified
|
||||
ignored if the configuration file is also specified
|
||||
-c, --config <file> Configuration file [config.json] (default: "config.json")
|
||||
-c, --config <file> Configuration file [config.json]
|
||||
-b, --bind <address> Bind address
|
||||
-p, --port <port> Port [8080] (default: 8080)
|
||||
-p, --port <port> Port [8080]
|
||||
-C|--no-cors Disable Cross-origin resource sharing headers
|
||||
-u|--public_url <url> Enable exposing the server on subpaths, not necessarily the root of the domain
|
||||
-V, --verbose More verbose output
|
||||
-s, --silent Less verbose output
|
||||
-l|--log_file <file> output log file (defaults to standard out)
|
||||
-f|--log_format <format> define the log format: https://github.com/expressjs/morgan#morganformat-options
|
||||
-v, --version output the version number
|
||||
-h, --help display help for command
|
||||
-v, --version Version info
|
||||
|
||||
|
||||
Default preview style and configuration
|
||||
======
|
||||
|
||||
- If no configuration file is specified, a default preview style (compatible with openmaptiles) is used.
|
||||
- If no data file is specified (and is not found in the current working directory), a sample file is downloaded (showing the Zurich area)
|
||||
- If no mbtiles file is specified (and is not found in the current working directory), a sample file is downloaded (showing the Zurich area)
|
||||
|
||||
Reloading the configuration
|
||||
======
|
||||
|
|
8335
package-lock.json
generated
8335
package-lock.json
generated
File diff suppressed because it is too large
Load diff
79
package.json
79
package.json
|
@ -1,19 +1,12 @@
|
|||
{
|
||||
"name": "tileserver-gl",
|
||||
"version": "5.2.0",
|
||||
"version": "4.4.10",
|
||||
"description": "Map tile server for JSON GL styles - vector and server side generated raster tiles",
|
||||
"main": "src/main.js",
|
||||
"bin": "src/main.js",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"prepare": "npm run copy:maplibre && npm run copy:maplibre-inspect && npm run copy:mapbox-rtl-text && npm run copy:leaflet && npm run copy:leaflet-hash",
|
||||
"copy:maplibre": "copyfiles -EVf node_modules/maplibre-gl/dist/maplibre-gl.js node_modules/maplibre-gl/dist/maplibre-gl.js.map node_modules/maplibre-gl/dist/maplibre-gl.css public/resources/",
|
||||
"copy:maplibre-inspect": "copyfiles -EVf node_modules/@maplibre/maplibre-gl-inspect/dist/maplibre-gl-inspect.js node_modules/@maplibre/maplibre-gl-inspect/dist/maplibre-gl-inspect.js.map node_modules/@maplibre/maplibre-gl-inspect/dist/maplibre-gl-inspect.css public/resources/",
|
||||
"copy:mapbox-rtl-text": "copyfiles -EVf node_modules/@mapbox/mapbox-gl-rtl-text/dist/mapbox-gl-rtl-text.js public/resources/",
|
||||
"copy:leaflet": "copyfiles -EVf node_modules/leaflet/dist/leaflet.js node_modules/leaflet/dist/leaflet.js.map node_modules/leaflet/dist/leaflet.css node_modules/leaflet/dist/leaflet-hash.js public/resources/",
|
||||
"copy:leaflet-hash": "copyfiles -EVf node_modules/leaflet-hash/leaflet-hash.js public/resources/",
|
||||
"test": "mocha test/**.js --timeout 10000 --exit",
|
||||
"test-docker": "xvfb-run npm test",
|
||||
"test": "mocha test/**.js --timeout 10000",
|
||||
"lint:yml": "yamllint --schema=CORE_SCHEMA *.{yml,yaml}",
|
||||
"lint:js": "npm run lint:eslint && npm run lint:prettier",
|
||||
"lint:js:fix": "npm run lint:eslint:fix && npm run lint:prettier:fix",
|
||||
|
@ -21,60 +14,52 @@
|
|||
"lint:eslint:fix": "eslint --fix \"{,!(node_modules|dist|static|public)/**/}*.{js,ts,cjs,mjs}\" --ignore-path .gitignore",
|
||||
"lint:prettier": "prettier --check \"{,!(node_modules|dist|static|public)/**/}*.{js,ts,cjs,mjs,json}\" --ignore-path .gitignore",
|
||||
"lint:prettier:fix": "prettier --write \"{,!(node_modules|dist|static|public)/**/}*.{js,ts,cjs,mjs,json}\" --ignore-path .gitignore",
|
||||
"docker": "docker build . && docker run --rm -i -p 8080:8080 $(docker build -q .)"
|
||||
"docker": "docker build -f Dockerfile . && docker run --rm -i -p 8080:8080 $(docker build -q .)",
|
||||
"prepare": "node -e \"if (process.env.NODE_ENV !== 'production'){ process.exit(1) } \" || husky install"
|
||||
},
|
||||
"dependencies": {
|
||||
"@jsse/pbfont": "^0.2.2",
|
||||
"@mapbox/mapbox-gl-rtl-text": "0.3.0",
|
||||
"@mapbox/glyph-pbf-composite": "0.0.3",
|
||||
"@mapbox/mbtiles": "0.12.1",
|
||||
"@mapbox/polyline": "^1.2.1",
|
||||
"@mapbox/polyline": "^1.2.0",
|
||||
"@mapbox/sphericalmercator": "1.2.0",
|
||||
"@mapbox/vector-tile": "2.0.3",
|
||||
"@maplibre/maplibre-gl-inspect": "1.7.0",
|
||||
"@maplibre/maplibre-gl-native": "6.0.0",
|
||||
"@maplibre/maplibre-gl-style-spec": "20.3.1",
|
||||
"@sindresorhus/fnv1a": "3.1.0",
|
||||
"@mapbox/vector-tile": "1.3.1",
|
||||
"@maplibre/maplibre-gl-native": "5.2.0",
|
||||
"@maplibre/maplibre-gl-style-spec": "18.0.0",
|
||||
"advanced-pool": "0.3.3",
|
||||
"axios": "^1.8.2",
|
||||
"canvas": "3.0.1",
|
||||
"chokidar": "3.6.0",
|
||||
"canvas": "2.11.2",
|
||||
"chokidar": "3.5.3",
|
||||
"clone": "2.1.2",
|
||||
"color": "4.2.3",
|
||||
"commander": "12.1.0",
|
||||
"copyfiles": "2.4.1",
|
||||
"commander": "11.0.0",
|
||||
"cors": "2.8.5",
|
||||
"express": "5.0.1",
|
||||
"express": "4.18.2",
|
||||
"handlebars": "4.7.8",
|
||||
"http-shutdown": "1.2.2",
|
||||
"leaflet": "1.9.4",
|
||||
"leaflet-hash": "0.2.1",
|
||||
"maplibre-gl": "4.7.1",
|
||||
"morgan": "1.10.0",
|
||||
"pbf": "4.0.1",
|
||||
"pmtiles": "3.0.7",
|
||||
"proj4": "2.12.1",
|
||||
"pbf": "3.2.1",
|
||||
"proj4": "2.9.0",
|
||||
"request": "2.88.2",
|
||||
"sanitize-filename": "1.6.3",
|
||||
"semver": "^7.6.3",
|
||||
"sharp": "0.33.5",
|
||||
"sharp": "0.32.4",
|
||||
"tileserver-gl-styles": "2.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "^19.5.0",
|
||||
"@commitlint/config-conventional": "^19.5.0",
|
||||
"@typescript-eslint/eslint-plugin": "^7.18.0",
|
||||
"@typescript-eslint/parser": "^7.18.0",
|
||||
"chai": "5.1.1",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-jsdoc": "^50.2.2",
|
||||
"eslint-plugin-prettier": "^5.2.1",
|
||||
"@commitlint/cli": "^17.6.7",
|
||||
"@commitlint/config-conventional": "^17.6.7",
|
||||
"@typescript-eslint/eslint-plugin": "^5.61.0",
|
||||
"@typescript-eslint/parser": "^5.62.0",
|
||||
"chai": "4.3.7",
|
||||
"eslint": "^8.46.0",
|
||||
"eslint-config-prettier": "^8.10.0",
|
||||
"eslint-plugin-jsdoc": "^46.4.6",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"eslint-plugin-security": "^1.7.1",
|
||||
"lint-staged": "^15.2.10",
|
||||
"mocha": "^10.7.3",
|
||||
"node-addon-api": "^8",
|
||||
"prettier": "^3.3.3",
|
||||
"husky": "^8.0.3",
|
||||
"lint-staged": "^13.2.3",
|
||||
"mocha": "^10.2.0",
|
||||
"prettier": "^2.8.8",
|
||||
"should": "^13.2.3",
|
||||
"supertest": "^7.0.0",
|
||||
"supertest": "^6.3.3",
|
||||
"yaml-lint": "^1.7.0"
|
||||
},
|
||||
"keywords": [
|
||||
|
@ -85,7 +70,7 @@
|
|||
],
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=18.17.0 <23"
|
||||
"node": ">=14.20.0 <19"
|
||||
},
|
||||
"repository": {
|
||||
"url": "git+https://github.com/maptiler/tileserver-gl.git",
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title></title>
|
||||
</head>
|
||||
<body>
|
||||
</body>
|
||||
</html>
|
243
public/resources/L.TileLayer.NoGap.js
Normal file
243
public/resources/L.TileLayer.NoGap.js
Normal file
|
@ -0,0 +1,243 @@
|
|||
// @class TileLayer
|
||||
|
||||
L.TileLayer.mergeOptions({
|
||||
// @option keepBuffer
|
||||
// The amount of tiles outside the visible map area to be kept in the stitched
|
||||
// `TileLayer`.
|
||||
|
||||
// @option dumpToCanvas: Boolean = true
|
||||
// Whether to dump loaded tiles to a `<canvas>` to prevent some rendering
|
||||
// artifacts. (Disabled by default in IE)
|
||||
dumpToCanvas: L.Browser.canvas && !L.Browser.ie,
|
||||
});
|
||||
|
||||
L.TileLayer.include({
|
||||
_onUpdateLevel: function(z, zoom) {
|
||||
if (this.options.dumpToCanvas) {
|
||||
this._levels[z].canvas.style.zIndex =
|
||||
this.options.maxZoom - Math.abs(zoom - z);
|
||||
}
|
||||
},
|
||||
|
||||
_onRemoveLevel: function(z) {
|
||||
if (this.options.dumpToCanvas) {
|
||||
L.DomUtil.remove(this._levels[z].canvas);
|
||||
}
|
||||
},
|
||||
|
||||
_onCreateLevel: function(level) {
|
||||
if (this.options.dumpToCanvas) {
|
||||
level.canvas = L.DomUtil.create(
|
||||
"canvas",
|
||||
"leaflet-tile-container leaflet-zoom-animated",
|
||||
this._container
|
||||
);
|
||||
level.ctx = level.canvas.getContext("2d");
|
||||
this._resetCanvasSize(level);
|
||||
}
|
||||
},
|
||||
|
||||
_removeTile: function(key) {
|
||||
if (this.options.dumpToCanvas) {
|
||||
var tile = this._tiles[key];
|
||||
var level = this._levels[tile.coords.z];
|
||||
var tileSize = this.getTileSize();
|
||||
|
||||
if (level) {
|
||||
// Where in the canvas should this tile go?
|
||||
var offset = L.point(tile.coords.x, tile.coords.y)
|
||||
.subtract(level.canvasRange.min)
|
||||
.scaleBy(this.getTileSize());
|
||||
|
||||
level.ctx.clearRect(offset.x, offset.y, tileSize.x, tileSize.y);
|
||||
}
|
||||
}
|
||||
|
||||
L.GridLayer.prototype._removeTile.call(this, key);
|
||||
},
|
||||
|
||||
_resetCanvasSize: function(level) {
|
||||
var buff = this.options.keepBuffer,
|
||||
pixelBounds = this._getTiledPixelBounds(this._map.getCenter()),
|
||||
tileRange = this._pxBoundsToTileRange(pixelBounds),
|
||||
tileSize = this.getTileSize();
|
||||
|
||||
tileRange.min = tileRange.min.subtract([buff, buff]); // This adds the no-prune buffer
|
||||
tileRange.max = tileRange.max.add([buff + 1, buff + 1]);
|
||||
|
||||
var pixelRange = L.bounds(
|
||||
tileRange.min.scaleBy(tileSize),
|
||||
tileRange.max.add([1, 1]).scaleBy(tileSize) // This prevents an off-by-one when checking if tiles are inside
|
||||
),
|
||||
mustRepositionCanvas = false,
|
||||
neededSize = pixelRange.max.subtract(pixelRange.min);
|
||||
|
||||
// Resize the canvas, if needed, and only to make it bigger.
|
||||
if (
|
||||
neededSize.x > level.canvas.width ||
|
||||
neededSize.y > level.canvas.height
|
||||
) {
|
||||
// Resizing canvases erases the currently drawn content, I'm afraid.
|
||||
// To keep it, dump the pixels to another canvas, then display it on
|
||||
// top. This could be done with getImageData/putImageData, but that
|
||||
// would break for tainted canvases (in non-CORS tilesets)
|
||||
var oldSize = { x: level.canvas.width, y: level.canvas.height };
|
||||
// console.info('Resizing canvas from ', oldSize, 'to ', neededSize);
|
||||
|
||||
var tmpCanvas = L.DomUtil.create("canvas");
|
||||
tmpCanvas.style.width = (tmpCanvas.width = oldSize.x) + "px";
|
||||
tmpCanvas.style.height = (tmpCanvas.height = oldSize.y) + "px";
|
||||
tmpCanvas.getContext("2d").drawImage(level.canvas, 0, 0);
|
||||
// var data = level.ctx.getImageData(0, 0, oldSize.x, oldSize.y);
|
||||
|
||||
level.canvas.style.width = (level.canvas.width = neededSize.x) + "px";
|
||||
level.canvas.style.height = (level.canvas.height = neededSize.y) + "px";
|
||||
level.ctx.drawImage(tmpCanvas, 0, 0);
|
||||
// level.ctx.putImageData(data, 0, 0, 0, 0, oldSize.x, oldSize.y);
|
||||
}
|
||||
|
||||
// Translate the canvas contents if it's moved around
|
||||
if (level.canvasRange) {
|
||||
var offset = level.canvasRange.min
|
||||
.subtract(tileRange.min)
|
||||
.scaleBy(this.getTileSize());
|
||||
|
||||
// console.info('Offsetting by ', offset);
|
||||
|
||||
if (!L.Browser.safari) {
|
||||
// By default, canvases copy things "on top of" existing pixels, but we want
|
||||
// this to *replace* the existing pixels when doing a drawImage() call.
|
||||
// This will also clear the sides, so no clearRect() calls are needed to make room
|
||||
// for the new tiles.
|
||||
level.ctx.globalCompositeOperation = "copy";
|
||||
level.ctx.drawImage(level.canvas, offset.x, offset.y);
|
||||
level.ctx.globalCompositeOperation = "source-over";
|
||||
} else {
|
||||
// Safari clears the canvas when copying from itself :-(
|
||||
if (!this._tmpCanvas) {
|
||||
var t = (this._tmpCanvas = L.DomUtil.create("canvas"));
|
||||
t.width = level.canvas.width;
|
||||
t.height = level.canvas.height;
|
||||
this._tmpContext = t.getContext("2d");
|
||||
}
|
||||
this._tmpContext.clearRect(
|
||||
0,
|
||||
0,
|
||||
level.canvas.width,
|
||||
level.canvas.height
|
||||
);
|
||||
this._tmpContext.drawImage(level.canvas, 0, 0);
|
||||
level.ctx.clearRect(0, 0, level.canvas.width, level.canvas.height);
|
||||
level.ctx.drawImage(this._tmpCanvas, offset.x, offset.y);
|
||||
}
|
||||
|
||||
mustRepositionCanvas = true; // Wait until new props are set
|
||||
}
|
||||
|
||||
level.canvasRange = tileRange;
|
||||
level.canvasPxRange = pixelRange;
|
||||
level.canvasOrigin = pixelRange.min;
|
||||
|
||||
// console.log('Canvas tile range: ', level, tileRange.min, tileRange.max );
|
||||
// console.log('Canvas pixel range: ', pixelRange.min, pixelRange.max );
|
||||
// console.log('Level origin: ', level.origin );
|
||||
|
||||
if (mustRepositionCanvas) {
|
||||
this._setCanvasZoomTransform(
|
||||
level,
|
||||
this._map.getCenter(),
|
||||
this._map.getZoom()
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
/// set transform/position of canvas, in addition to the transform/position of the individual tile container
|
||||
_setZoomTransform: function(level, center, zoom) {
|
||||
L.GridLayer.prototype._setZoomTransform.call(this, level, center, zoom);
|
||||
if (this.options.dumpToCanvas) {
|
||||
this._setCanvasZoomTransform(level, center, zoom);
|
||||
}
|
||||
},
|
||||
|
||||
// This will get called twice:
|
||||
// * From _setZoomTransform
|
||||
// * When the canvas has shifted due to a new tile being loaded
|
||||
_setCanvasZoomTransform: function(level, center, zoom) {
|
||||
// console.log('_setCanvasZoomTransform', level, center, zoom);
|
||||
if (!level.canvasOrigin) {
|
||||
return;
|
||||
}
|
||||
var scale = this._map.getZoomScale(zoom, level.zoom),
|
||||
translate = level.canvasOrigin
|
||||
.multiplyBy(scale)
|
||||
.subtract(this._map._getNewPixelOrigin(center, zoom))
|
||||
.round();
|
||||
|
||||
if (L.Browser.any3d) {
|
||||
L.DomUtil.setTransform(level.canvas, translate, scale);
|
||||
} else {
|
||||
L.DomUtil.setPosition(level.canvas, translate);
|
||||
}
|
||||
},
|
||||
|
||||
_onOpaqueTile: function(tile) {
|
||||
if (!this.options.dumpToCanvas) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Guard against an NS_ERROR_NOT_AVAILABLE (or similar) exception
|
||||
// when a non-image-tile has been loaded (e.g. a WMS error).
|
||||
// Checking for tile.el.complete is not enough, as it has been
|
||||
// already marked as loaded and ready somehow.
|
||||
try {
|
||||
this.dumpPixels(tile.coords, tile.el);
|
||||
} catch (ex) {
|
||||
return this.fire("tileerror", {
|
||||
error: "Could not copy tile pixels: " + ex,
|
||||
tile: tile,
|
||||
coods: tile.coords,
|
||||
});
|
||||
}
|
||||
|
||||
// If dumping the pixels was successful, then hide the tile.
|
||||
// Do not remove the tile itself, as it is needed to check if the whole
|
||||
// level (and its canvas) should be removed (via level.el.children.length)
|
||||
tile.el.style.display = "none";
|
||||
},
|
||||
|
||||
// @section Extension methods
|
||||
// @uninheritable
|
||||
|
||||
// @method dumpPixels(coords: Object, imageSource: CanvasImageSource): this
|
||||
// Dumps pixels from the given `CanvasImageSource` into the layer, into
|
||||
// the space for the tile represented by the `coords` tile coordinates (an object
|
||||
// like `{x: Number, y: Number, z: Number}`; the image source must have the
|
||||
// same size as the `tileSize` option for the layer. Has no effect if `dumpToCanvas`
|
||||
// is `false`.
|
||||
dumpPixels: function(coords, imageSource) {
|
||||
var level = this._levels[coords.z],
|
||||
tileSize = this.getTileSize();
|
||||
|
||||
if (!level.canvasRange || !this.options.dumpToCanvas) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the tile is inside the currently visible map bounds
|
||||
// There is a possible race condition when tiles are loaded after they
|
||||
// have been panned outside of the map.
|
||||
if (!level.canvasRange.contains(coords)) {
|
||||
this._resetCanvasSize(level);
|
||||
}
|
||||
|
||||
// Where in the canvas should this tile go?
|
||||
var offset = L.point(coords.x, coords.y)
|
||||
.subtract(level.canvasRange.min)
|
||||
.scaleBy(this.getTileSize());
|
||||
|
||||
level.ctx.drawImage(imageSource, offset.x, offset.y, tileSize.x, tileSize.y);
|
||||
|
||||
// TODO: Clear the pixels of other levels' canvases where they overlap
|
||||
// this newly dumped tile.
|
||||
return this;
|
||||
},
|
||||
});
|
|
@ -1,51 +0,0 @@
|
|||
class ElevationInfoControl {
|
||||
constructor(options) {
|
||||
this.url = options["url"];
|
||||
}
|
||||
|
||||
getDefaultPosition() {
|
||||
const defaultPosition = "bottom-left";
|
||||
return defaultPosition;
|
||||
}
|
||||
|
||||
onAdd(map) {
|
||||
this.map = map;
|
||||
this.controlContainer = document.createElement("div");
|
||||
this.controlContainer.classList.add("maplibregl-ctrl");
|
||||
this.controlContainer.classList.add("maplibregl-ctrl-group");
|
||||
this.controlContainer.classList.add("maplibre-ctrl-elevation");
|
||||
this.controlContainer.textContent = "Elevation: Click on Map";
|
||||
|
||||
map.on('click', (e) => {
|
||||
var url = this.url;
|
||||
var coord = {"z": Math.floor(map.getZoom()), "x": e.lngLat["lng"].toFixed(7), "y": e.lngLat["lat"].toFixed(7)};
|
||||
for(var key in coord) {
|
||||
url = url.replace(new RegExp('{'+ key +'}','g'), coord[key]);
|
||||
}
|
||||
|
||||
let request = new XMLHttpRequest();
|
||||
request.open("GET", url, true);
|
||||
request.onload = () => {
|
||||
if (request.status !== 200) {
|
||||
this.controlContainer.textContent = "Elevation: No value";
|
||||
} else {
|
||||
this.controlContainer.textContent = `Elevation: ${JSON.parse(request.responseText).elevation} (${JSON.stringify(coord)})`;
|
||||
}
|
||||
}
|
||||
request.send();
|
||||
});
|
||||
return this.controlContainer;
|
||||
}
|
||||
|
||||
onRemove() {
|
||||
if (
|
||||
!this.controlContainer ||
|
||||
!this.controlContainer.parentNode ||
|
||||
!this.map
|
||||
) {
|
||||
return;
|
||||
}
|
||||
this.controlContainer.parentNode.removeChild(this.controlContainer);
|
||||
this.map = undefined;
|
||||
}
|
||||
};
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Before Width: | Height: | Size: 2.4 KiB |
Binary file not shown.
Before Width: | Height: | Size: 1.4 KiB |
Binary file not shown.
Before Width: | Height: | Size: 618 B |
|
@ -1,18 +1,18 @@
|
|||
@font-face {
|
||||
font-family: 'OpenSans';
|
||||
src: url('./fonts/OpenSans-Regular.ttf');
|
||||
src: url('/fonts/OpenSans-Regular.ttf');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'OpenSans';
|
||||
src: url('./fonts/OpenSans-Italic.ttf');
|
||||
src: url('/fonts/OpenSans-Italic.ttf');
|
||||
font-weight: normal;
|
||||
font-style: italic;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'OpenSans';
|
||||
src: url('./fonts/OpenSans-Bold.ttf');
|
||||
src: url('/fonts/OpenSans-Bold.ttf');
|
||||
font-weight: bold;
|
||||
font-style: normal;
|
||||
}
|
||||
|
@ -25,7 +25,7 @@ body {
|
|||
margin: 0;
|
||||
background-repeat: no-repeat !important;
|
||||
background-size: contain !important;
|
||||
background-image: url(./images/header-map-1280px.png);
|
||||
background-image: url(/images/header-map-1280px.png);
|
||||
}
|
||||
a {
|
||||
color: #499dce;
|
||||
|
@ -73,29 +73,12 @@ section {
|
|||
font-size: 20px;
|
||||
background: #fff;
|
||||
}
|
||||
.filter-details {
|
||||
padding: 20px 30px;
|
||||
}
|
||||
.box input { /* Filter text input */
|
||||
padding: 10px;
|
||||
font-size: 16px;
|
||||
border: 1px solid #ededed;
|
||||
border-radius: 4px;
|
||||
/* fill out to parent */
|
||||
width: 100%;
|
||||
}
|
||||
.item {
|
||||
background: #fff;
|
||||
height: 191px;
|
||||
border: 1px solid #ededed;
|
||||
border-top: none;
|
||||
}
|
||||
.filter-item {
|
||||
background: #fbfbfb;
|
||||
height: 150px;
|
||||
border: 1px solid #ededed;
|
||||
border-top: none;
|
||||
}
|
||||
.item:nth-child(odd) {
|
||||
background-color: #fbfbfb;
|
||||
}
|
||||
|
@ -114,7 +97,7 @@ section {
|
|||
}
|
||||
.details h3 {
|
||||
font-size: 18px;
|
||||
margin-top: 5px;
|
||||
margin-top: 25px;
|
||||
}
|
||||
.details p {
|
||||
padding: 0;
|
||||
|
|
162
public/resources/leaflet-hash.js
Normal file
162
public/resources/leaflet-hash.js
Normal file
|
@ -0,0 +1,162 @@
|
|||
(function(window) {
|
||||
var HAS_HASHCHANGE = (function() {
|
||||
var doc_mode = window.documentMode;
|
||||
return ('onhashchange' in window) &&
|
||||
(doc_mode === undefined || doc_mode > 7);
|
||||
})();
|
||||
|
||||
L.Hash = function(map) {
|
||||
this.onHashChange = L.Util.bind(this.onHashChange, this);
|
||||
|
||||
if (map) {
|
||||
this.init(map);
|
||||
}
|
||||
};
|
||||
|
||||
L.Hash.parseHash = function(hash) {
|
||||
if(hash.indexOf('#') === 0) {
|
||||
hash = hash.substr(1);
|
||||
}
|
||||
var args = hash.split("/");
|
||||
if (args.length == 3) {
|
||||
var zoom = parseInt(args[0], 10),
|
||||
lat = parseFloat(args[1]),
|
||||
lon = parseFloat(args[2]);
|
||||
if (isNaN(zoom) || isNaN(lat) || isNaN(lon)) {
|
||||
return false;
|
||||
} else {
|
||||
return {
|
||||
center: new L.LatLng(lat, lon),
|
||||
zoom: zoom
|
||||
};
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
L.Hash.formatHash = function(map) {
|
||||
var center = map.getCenter(),
|
||||
zoom = map.getZoom(),
|
||||
precision = Math.max(0, Math.ceil(Math.log(zoom) / Math.LN2));
|
||||
|
||||
return "#" + [zoom,
|
||||
center.lat.toFixed(precision),
|
||||
center.lng.toFixed(precision)
|
||||
].join("/");
|
||||
},
|
||||
|
||||
L.Hash.prototype = {
|
||||
map: null,
|
||||
lastHash: null,
|
||||
|
||||
parseHash: L.Hash.parseHash,
|
||||
formatHash: L.Hash.formatHash,
|
||||
|
||||
init: function(map) {
|
||||
this.map = map;
|
||||
|
||||
// reset the hash
|
||||
this.lastHash = null;
|
||||
this.onHashChange();
|
||||
|
||||
if (!this.isListening) {
|
||||
this.startListening();
|
||||
}
|
||||
},
|
||||
|
||||
removeFrom: function(map) {
|
||||
if (this.changeTimeout) {
|
||||
clearTimeout(this.changeTimeout);
|
||||
}
|
||||
|
||||
if (this.isListening) {
|
||||
this.stopListening();
|
||||
}
|
||||
|
||||
this.map = null;
|
||||
},
|
||||
|
||||
onMapMove: function() {
|
||||
// bail if we're moving the map (updating from a hash),
|
||||
// or if the map is not yet loaded
|
||||
|
||||
if (this.movingMap || !this.map._loaded) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var hash = this.formatHash(this.map);
|
||||
if (this.lastHash != hash) {
|
||||
location.replace(hash);
|
||||
this.lastHash = hash;
|
||||
}
|
||||
},
|
||||
|
||||
movingMap: false,
|
||||
update: function() {
|
||||
var hash = location.hash;
|
||||
if (hash === this.lastHash) {
|
||||
return;
|
||||
}
|
||||
var parsed = this.parseHash(hash);
|
||||
if (parsed) {
|
||||
this.movingMap = true;
|
||||
|
||||
this.map.setView(parsed.center, parsed.zoom);
|
||||
|
||||
this.movingMap = false;
|
||||
} else {
|
||||
this.onMapMove(this.map);
|
||||
}
|
||||
},
|
||||
|
||||
// defer hash change updates every 100ms
|
||||
changeDefer: 100,
|
||||
changeTimeout: null,
|
||||
onHashChange: function() {
|
||||
// throttle calls to update() so that they only happen every
|
||||
// `changeDefer` ms
|
||||
if (!this.changeTimeout) {
|
||||
var that = this;
|
||||
this.changeTimeout = setTimeout(function() {
|
||||
that.update();
|
||||
that.changeTimeout = null;
|
||||
}, this.changeDefer);
|
||||
}
|
||||
},
|
||||
|
||||
isListening: false,
|
||||
hashChangeInterval: null,
|
||||
startListening: function() {
|
||||
this.map.on("moveend", this.onMapMove, this);
|
||||
|
||||
if (HAS_HASHCHANGE) {
|
||||
L.DomEvent.addListener(window, "hashchange", this.onHashChange);
|
||||
} else {
|
||||
clearInterval(this.hashChangeInterval);
|
||||
this.hashChangeInterval = setInterval(this.onHashChange, 50);
|
||||
}
|
||||
this.isListening = true;
|
||||
},
|
||||
|
||||
stopListening: function() {
|
||||
this.map.off("moveend", this.onMapMove, this);
|
||||
|
||||
if (HAS_HASHCHANGE) {
|
||||
L.DomEvent.removeListener(window, "hashchange", this.onHashChange);
|
||||
} else {
|
||||
clearInterval(this.hashChangeInterval);
|
||||
}
|
||||
this.isListening = false;
|
||||
}
|
||||
};
|
||||
L.hash = function(map) {
|
||||
return new L.Hash(map);
|
||||
};
|
||||
L.Map.prototype.addHash = function() {
|
||||
this._hash = L.hash(this);
|
||||
};
|
||||
L.Map.prototype.removeHash = function() {
|
||||
this._hash.removeFrom();
|
||||
};
|
||||
})(window);
|
656
public/resources/leaflet.css
Normal file
656
public/resources/leaflet.css
Normal file
|
@ -0,0 +1,656 @@
|
|||
/* required styles */
|
||||
|
||||
.leaflet-pane,
|
||||
.leaflet-tile,
|
||||
.leaflet-marker-icon,
|
||||
.leaflet-marker-shadow,
|
||||
.leaflet-tile-container,
|
||||
.leaflet-pane > svg,
|
||||
.leaflet-pane > canvas,
|
||||
.leaflet-zoom-box,
|
||||
.leaflet-image-layer,
|
||||
.leaflet-layer {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
.leaflet-container {
|
||||
overflow: hidden;
|
||||
}
|
||||
.leaflet-tile,
|
||||
.leaflet-marker-icon,
|
||||
.leaflet-marker-shadow {
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
user-select: none;
|
||||
-webkit-user-drag: none;
|
||||
}
|
||||
/* Prevents IE11 from highlighting tiles in blue */
|
||||
.leaflet-tile::selection {
|
||||
background: transparent;
|
||||
}
|
||||
/* Safari renders non-retina tile on retina better with this, but Chrome is worse */
|
||||
.leaflet-safari .leaflet-tile {
|
||||
image-rendering: -webkit-optimize-contrast;
|
||||
}
|
||||
/* hack that prevents hw layers "stretching" when loading new tiles */
|
||||
.leaflet-safari .leaflet-tile-container {
|
||||
width: 1600px;
|
||||
height: 1600px;
|
||||
-webkit-transform-origin: 0 0;
|
||||
}
|
||||
.leaflet-marker-icon,
|
||||
.leaflet-marker-shadow {
|
||||
display: block;
|
||||
}
|
||||
/* .leaflet-container svg: reset svg max-width decleration shipped in Joomla! (joomla.org) 3.x */
|
||||
/* .leaflet-container img: map is broken in FF if you have max-width: 100% on tiles */
|
||||
.leaflet-container .leaflet-overlay-pane svg {
|
||||
max-width: none !important;
|
||||
max-height: none !important;
|
||||
}
|
||||
.leaflet-container .leaflet-marker-pane img,
|
||||
.leaflet-container .leaflet-shadow-pane img,
|
||||
.leaflet-container .leaflet-tile-pane img,
|
||||
.leaflet-container img.leaflet-image-layer,
|
||||
.leaflet-container .leaflet-tile {
|
||||
max-width: none !important;
|
||||
max-height: none !important;
|
||||
width: auto;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.leaflet-container.leaflet-touch-zoom {
|
||||
-ms-touch-action: pan-x pan-y;
|
||||
touch-action: pan-x pan-y;
|
||||
}
|
||||
.leaflet-container.leaflet-touch-drag {
|
||||
-ms-touch-action: pinch-zoom;
|
||||
/* Fallback for FF which doesn't support pinch-zoom */
|
||||
touch-action: none;
|
||||
touch-action: pinch-zoom;
|
||||
}
|
||||
.leaflet-container.leaflet-touch-drag.leaflet-touch-zoom {
|
||||
-ms-touch-action: none;
|
||||
touch-action: none;
|
||||
}
|
||||
.leaflet-container {
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
.leaflet-container a {
|
||||
-webkit-tap-highlight-color: rgba(51, 181, 229, 0.4);
|
||||
}
|
||||
.leaflet-tile {
|
||||
filter: inherit;
|
||||
visibility: hidden;
|
||||
}
|
||||
.leaflet-tile-loaded {
|
||||
visibility: inherit;
|
||||
}
|
||||
.leaflet-zoom-box {
|
||||
width: 0;
|
||||
height: 0;
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
z-index: 800;
|
||||
}
|
||||
/* workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=888319 */
|
||||
.leaflet-overlay-pane svg {
|
||||
-moz-user-select: none;
|
||||
}
|
||||
|
||||
.leaflet-pane { z-index: 400; }
|
||||
|
||||
.leaflet-tile-pane { z-index: 200; }
|
||||
.leaflet-overlay-pane { z-index: 400; }
|
||||
.leaflet-shadow-pane { z-index: 500; }
|
||||
.leaflet-marker-pane { z-index: 600; }
|
||||
.leaflet-tooltip-pane { z-index: 650; }
|
||||
.leaflet-popup-pane { z-index: 700; }
|
||||
|
||||
.leaflet-map-pane canvas { z-index: 100; }
|
||||
.leaflet-map-pane svg { z-index: 200; }
|
||||
|
||||
.leaflet-vml-shape {
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
}
|
||||
.lvml {
|
||||
behavior: url(#default#VML);
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
|
||||
/* control positioning */
|
||||
|
||||
.leaflet-control {
|
||||
position: relative;
|
||||
z-index: 800;
|
||||
pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
|
||||
pointer-events: auto;
|
||||
}
|
||||
.leaflet-top,
|
||||
.leaflet-bottom {
|
||||
position: absolute;
|
||||
z-index: 1000;
|
||||
pointer-events: none;
|
||||
}
|
||||
.leaflet-top {
|
||||
top: 0;
|
||||
}
|
||||
.leaflet-right {
|
||||
right: 0;
|
||||
}
|
||||
.leaflet-bottom {
|
||||
bottom: 0;
|
||||
}
|
||||
.leaflet-left {
|
||||
left: 0;
|
||||
}
|
||||
.leaflet-control {
|
||||
float: left;
|
||||
clear: both;
|
||||
}
|
||||
.leaflet-right .leaflet-control {
|
||||
float: right;
|
||||
}
|
||||
.leaflet-top .leaflet-control {
|
||||
margin-top: 10px;
|
||||
}
|
||||
.leaflet-bottom .leaflet-control {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.leaflet-left .leaflet-control {
|
||||
margin-left: 10px;
|
||||
}
|
||||
.leaflet-right .leaflet-control {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
|
||||
/* zoom and fade animations */
|
||||
|
||||
.leaflet-fade-anim .leaflet-popup {
|
||||
opacity: 0;
|
||||
-webkit-transition: opacity 0.2s linear;
|
||||
-moz-transition: opacity 0.2s linear;
|
||||
transition: opacity 0.2s linear;
|
||||
}
|
||||
.leaflet-fade-anim .leaflet-map-pane .leaflet-popup {
|
||||
opacity: 1;
|
||||
}
|
||||
.leaflet-zoom-animated {
|
||||
-webkit-transform-origin: 0 0;
|
||||
-ms-transform-origin: 0 0;
|
||||
transform-origin: 0 0;
|
||||
}
|
||||
svg.leaflet-zoom-animated {
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.leaflet-zoom-anim .leaflet-zoom-animated {
|
||||
-webkit-transition: -webkit-transform 0.25s cubic-bezier(0,0,0.25,1);
|
||||
-moz-transition: -moz-transform 0.25s cubic-bezier(0,0,0.25,1);
|
||||
transition: transform 0.25s cubic-bezier(0,0,0.25,1);
|
||||
}
|
||||
.leaflet-zoom-anim .leaflet-tile,
|
||||
.leaflet-pan-anim .leaflet-tile {
|
||||
-webkit-transition: none;
|
||||
-moz-transition: none;
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.leaflet-zoom-anim .leaflet-zoom-hide {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
|
||||
/* cursors */
|
||||
|
||||
.leaflet-interactive {
|
||||
cursor: pointer;
|
||||
}
|
||||
.leaflet-grab {
|
||||
cursor: -webkit-grab;
|
||||
cursor: -moz-grab;
|
||||
cursor: grab;
|
||||
}
|
||||
.leaflet-crosshair,
|
||||
.leaflet-crosshair .leaflet-interactive {
|
||||
cursor: crosshair;
|
||||
}
|
||||
.leaflet-popup-pane,
|
||||
.leaflet-control {
|
||||
cursor: auto;
|
||||
}
|
||||
.leaflet-dragging .leaflet-grab,
|
||||
.leaflet-dragging .leaflet-grab .leaflet-interactive,
|
||||
.leaflet-dragging .leaflet-marker-draggable {
|
||||
cursor: move;
|
||||
cursor: -webkit-grabbing;
|
||||
cursor: -moz-grabbing;
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
/* marker & overlays interactivity */
|
||||
.leaflet-marker-icon,
|
||||
.leaflet-marker-shadow,
|
||||
.leaflet-image-layer,
|
||||
.leaflet-pane > svg path,
|
||||
.leaflet-tile-container {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.leaflet-marker-icon.leaflet-interactive,
|
||||
.leaflet-image-layer.leaflet-interactive,
|
||||
.leaflet-pane > svg path.leaflet-interactive,
|
||||
svg.leaflet-image-layer.leaflet-interactive path {
|
||||
pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
/* visual tweaks */
|
||||
|
||||
.leaflet-container {
|
||||
background: #ddd;
|
||||
outline-offset: 1px;
|
||||
}
|
||||
.leaflet-container a {
|
||||
color: #0078A8;
|
||||
}
|
||||
.leaflet-zoom-box {
|
||||
border: 2px dotted #38f;
|
||||
background: rgba(255,255,255,0.5);
|
||||
}
|
||||
|
||||
|
||||
/* general typography */
|
||||
.leaflet-container {
|
||||
font-family: "Helvetica Neue", Arial, Helvetica, sans-serif;
|
||||
font-size: 12px;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
|
||||
/* general toolbar styles */
|
||||
|
||||
.leaflet-bar {
|
||||
box-shadow: 0 1px 5px rgba(0,0,0,0.65);
|
||||
border-radius: 4px;
|
||||
}
|
||||
.leaflet-bar a {
|
||||
background-color: #fff;
|
||||
border-bottom: 1px solid #ccc;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
line-height: 26px;
|
||||
display: block;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
color: black;
|
||||
}
|
||||
.leaflet-bar a,
|
||||
.leaflet-control-layers-toggle {
|
||||
background-position: 50% 50%;
|
||||
background-repeat: no-repeat;
|
||||
display: block;
|
||||
}
|
||||
.leaflet-bar a:hover,
|
||||
.leaflet-bar a:focus {
|
||||
background-color: #f4f4f4;
|
||||
}
|
||||
.leaflet-bar a:first-child {
|
||||
border-top-left-radius: 4px;
|
||||
border-top-right-radius: 4px;
|
||||
}
|
||||
.leaflet-bar a:last-child {
|
||||
border-bottom-left-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
border-bottom: none;
|
||||
}
|
||||
.leaflet-bar a.leaflet-disabled {
|
||||
cursor: default;
|
||||
background-color: #f4f4f4;
|
||||
color: #bbb;
|
||||
}
|
||||
|
||||
.leaflet-touch .leaflet-bar a {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
}
|
||||
.leaflet-touch .leaflet-bar a:first-child {
|
||||
border-top-left-radius: 2px;
|
||||
border-top-right-radius: 2px;
|
||||
}
|
||||
.leaflet-touch .leaflet-bar a:last-child {
|
||||
border-bottom-left-radius: 2px;
|
||||
border-bottom-right-radius: 2px;
|
||||
}
|
||||
|
||||
/* zoom control */
|
||||
|
||||
.leaflet-control-zoom-in,
|
||||
.leaflet-control-zoom-out {
|
||||
font: bold 18px 'Lucida Console', Monaco, monospace;
|
||||
text-indent: 1px;
|
||||
}
|
||||
|
||||
.leaflet-touch .leaflet-control-zoom-in, .leaflet-touch .leaflet-control-zoom-out {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
|
||||
/* layers control */
|
||||
|
||||
.leaflet-control-layers {
|
||||
box-shadow: 0 1px 5px rgba(0,0,0,0.4);
|
||||
background: #fff;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.leaflet-control-layers-toggle {
|
||||
background-image: url(images/layers.png);
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
}
|
||||
.leaflet-retina .leaflet-control-layers-toggle {
|
||||
background-image: url(images/layers-2x.png);
|
||||
background-size: 26px 26px;
|
||||
}
|
||||
.leaflet-touch .leaflet-control-layers-toggle {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
}
|
||||
.leaflet-control-layers .leaflet-control-layers-list,
|
||||
.leaflet-control-layers-expanded .leaflet-control-layers-toggle {
|
||||
display: none;
|
||||
}
|
||||
.leaflet-control-layers-expanded .leaflet-control-layers-list {
|
||||
display: block;
|
||||
position: relative;
|
||||
}
|
||||
.leaflet-control-layers-expanded {
|
||||
padding: 6px 10px 6px 6px;
|
||||
color: #333;
|
||||
background: #fff;
|
||||
}
|
||||
.leaflet-control-layers-scrollbar {
|
||||
overflow-y: scroll;
|
||||
overflow-x: hidden;
|
||||
padding-right: 5px;
|
||||
}
|
||||
.leaflet-control-layers-selector {
|
||||
margin-top: 2px;
|
||||
position: relative;
|
||||
top: 1px;
|
||||
}
|
||||
.leaflet-control-layers label {
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
font-size: 1.08333em;
|
||||
}
|
||||
.leaflet-control-layers-separator {
|
||||
height: 0;
|
||||
border-top: 1px solid #ddd;
|
||||
margin: 5px -10px 5px -6px;
|
||||
}
|
||||
|
||||
/* Default icon URLs */
|
||||
.leaflet-default-icon-path { /* used only in path-guessing heuristic, see L.Icon.Default */
|
||||
background-image: url(images/marker-icon.png);
|
||||
}
|
||||
|
||||
|
||||
/* attribution and scale controls */
|
||||
|
||||
.leaflet-container .leaflet-control-attribution {
|
||||
background: #fff;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
margin: 0;
|
||||
}
|
||||
.leaflet-control-attribution,
|
||||
.leaflet-control-scale-line {
|
||||
padding: 0 5px;
|
||||
color: #333;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.leaflet-control-attribution a {
|
||||
text-decoration: none;
|
||||
}
|
||||
.leaflet-control-attribution a:hover,
|
||||
.leaflet-control-attribution a:focus {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.leaflet-attribution-flag {
|
||||
display: inline !important;
|
||||
vertical-align: baseline !important;
|
||||
width: 1em;
|
||||
height: 0.6669em;
|
||||
}
|
||||
.leaflet-left .leaflet-control-scale {
|
||||
margin-left: 5px;
|
||||
}
|
||||
.leaflet-bottom .leaflet-control-scale {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.leaflet-control-scale-line {
|
||||
border: 2px solid #777;
|
||||
border-top: none;
|
||||
line-height: 1.1;
|
||||
padding: 2px 5px 1px;
|
||||
white-space: nowrap;
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
text-shadow: 1px 1px #fff;
|
||||
}
|
||||
.leaflet-control-scale-line:not(:first-child) {
|
||||
border-top: 2px solid #777;
|
||||
border-bottom: none;
|
||||
margin-top: -2px;
|
||||
}
|
||||
.leaflet-control-scale-line:not(:first-child):not(:last-child) {
|
||||
border-bottom: 2px solid #777;
|
||||
}
|
||||
|
||||
.leaflet-touch .leaflet-control-attribution,
|
||||
.leaflet-touch .leaflet-control-layers,
|
||||
.leaflet-touch .leaflet-bar {
|
||||
box-shadow: none;
|
||||
}
|
||||
.leaflet-touch .leaflet-control-layers,
|
||||
.leaflet-touch .leaflet-bar {
|
||||
border: 2px solid rgba(0,0,0,0.2);
|
||||
background-clip: padding-box;
|
||||
}
|
||||
|
||||
|
||||
/* popup */
|
||||
|
||||
.leaflet-popup {
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.leaflet-popup-content-wrapper {
|
||||
padding: 1px;
|
||||
text-align: left;
|
||||
border-radius: 12px;
|
||||
}
|
||||
.leaflet-popup-content {
|
||||
margin: 13px 24px 13px 20px;
|
||||
line-height: 1.3;
|
||||
font-size: 13px;
|
||||
font-size: 1.08333em;
|
||||
min-height: 1px;
|
||||
}
|
||||
.leaflet-popup-content p {
|
||||
margin: 17px 0;
|
||||
margin: 1.3em 0;
|
||||
}
|
||||
.leaflet-popup-tip-container {
|
||||
width: 40px;
|
||||
height: 20px;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
margin-top: -1px;
|
||||
margin-left: -20px;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
}
|
||||
.leaflet-popup-tip {
|
||||
width: 17px;
|
||||
height: 17px;
|
||||
padding: 1px;
|
||||
|
||||
margin: -10px auto 0;
|
||||
pointer-events: auto;
|
||||
|
||||
-webkit-transform: rotate(45deg);
|
||||
-moz-transform: rotate(45deg);
|
||||
-ms-transform: rotate(45deg);
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
.leaflet-popup-content-wrapper,
|
||||
.leaflet-popup-tip {
|
||||
background: white;
|
||||
color: #333;
|
||||
box-shadow: 0 3px 14px rgba(0,0,0,0.4);
|
||||
}
|
||||
.leaflet-container a.leaflet-popup-close-button {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
border: none;
|
||||
text-align: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
font: 16px/24px Tahoma, Verdana, sans-serif;
|
||||
color: #757575;
|
||||
text-decoration: none;
|
||||
background: transparent;
|
||||
}
|
||||
.leaflet-container a.leaflet-popup-close-button:hover,
|
||||
.leaflet-container a.leaflet-popup-close-button:focus {
|
||||
color: #585858;
|
||||
}
|
||||
.leaflet-popup-scrolled {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.leaflet-oldie .leaflet-popup-content-wrapper {
|
||||
-ms-zoom: 1;
|
||||
}
|
||||
.leaflet-oldie .leaflet-popup-tip {
|
||||
width: 24px;
|
||||
margin: 0 auto;
|
||||
|
||||
-ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)";
|
||||
filter: progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678);
|
||||
}
|
||||
|
||||
.leaflet-oldie .leaflet-control-zoom,
|
||||
.leaflet-oldie .leaflet-control-layers,
|
||||
.leaflet-oldie .leaflet-popup-content-wrapper,
|
||||
.leaflet-oldie .leaflet-popup-tip {
|
||||
border: 1px solid #999;
|
||||
}
|
||||
|
||||
|
||||
/* div icon */
|
||||
|
||||
.leaflet-div-icon {
|
||||
background: #fff;
|
||||
border: 1px solid #666;
|
||||
}
|
||||
|
||||
|
||||
/* Tooltip */
|
||||
/* Base styles for the element that has a tooltip */
|
||||
.leaflet-tooltip {
|
||||
position: absolute;
|
||||
padding: 6px;
|
||||
background-color: #fff;
|
||||
border: 1px solid #fff;
|
||||
border-radius: 3px;
|
||||
color: #222;
|
||||
white-space: nowrap;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
pointer-events: none;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.4);
|
||||
}
|
||||
.leaflet-tooltip.leaflet-interactive {
|
||||
cursor: pointer;
|
||||
pointer-events: auto;
|
||||
}
|
||||
.leaflet-tooltip-top:before,
|
||||
.leaflet-tooltip-bottom:before,
|
||||
.leaflet-tooltip-left:before,
|
||||
.leaflet-tooltip-right:before {
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
border: 6px solid transparent;
|
||||
background: transparent;
|
||||
content: "";
|
||||
}
|
||||
|
||||
/* Directions */
|
||||
|
||||
.leaflet-tooltip-bottom {
|
||||
margin-top: 6px;
|
||||
}
|
||||
.leaflet-tooltip-top {
|
||||
margin-top: -6px;
|
||||
}
|
||||
.leaflet-tooltip-bottom:before,
|
||||
.leaflet-tooltip-top:before {
|
||||
left: 50%;
|
||||
margin-left: -6px;
|
||||
}
|
||||
.leaflet-tooltip-top:before {
|
||||
bottom: 0;
|
||||
margin-bottom: -12px;
|
||||
border-top-color: #fff;
|
||||
}
|
||||
.leaflet-tooltip-bottom:before {
|
||||
top: 0;
|
||||
margin-top: -12px;
|
||||
margin-left: -6px;
|
||||
border-bottom-color: #fff;
|
||||
}
|
||||
.leaflet-tooltip-left {
|
||||
margin-left: -6px;
|
||||
}
|
||||
.leaflet-tooltip-right {
|
||||
margin-left: 6px;
|
||||
}
|
||||
.leaflet-tooltip-left:before,
|
||||
.leaflet-tooltip-right:before {
|
||||
top: 50%;
|
||||
margin-top: -6px;
|
||||
}
|
||||
.leaflet-tooltip-left:before {
|
||||
right: 0;
|
||||
margin-right: -12px;
|
||||
border-left-color: #fff;
|
||||
}
|
||||
.leaflet-tooltip-right:before {
|
||||
left: 0;
|
||||
margin-left: -12px;
|
||||
border-right-color: #fff;
|
||||
}
|
||||
|
||||
/* Printing */
|
||||
|
||||
@media print {
|
||||
/* Prevent printers from removing background-images of controls. */
|
||||
.leaflet-control {
|
||||
-webkit-print-color-adjust: exact;
|
||||
print-color-adjust: exact;
|
||||
}
|
||||
}
|
6
public/resources/leaflet.js
Normal file
6
public/resources/leaflet.js
Normal file
File diff suppressed because one or more lines are too long
1
public/resources/leaflet.js.map
Normal file
1
public/resources/leaflet.js.map
Normal file
File diff suppressed because one or more lines are too long
6
public/resources/mapbox-gl-rtl-text.js
Normal file
6
public/resources/mapbox-gl-rtl-text.js
Normal file
File diff suppressed because one or more lines are too long
1
public/resources/maplibre-gl-compat.css
Normal file
1
public/resources/maplibre-gl-compat.css
Normal file
File diff suppressed because one or more lines are too long
45
public/resources/maplibre-gl-compat.js
Normal file
45
public/resources/maplibre-gl-compat.js
Normal file
File diff suppressed because one or more lines are too long
1
public/resources/maplibre-gl-compat.js.map
Normal file
1
public/resources/maplibre-gl-compat.js.map
Normal file
|
@ -0,0 +1 @@
|
|||
{"version":3,"file":"maplibre-gl-compat.js","sources":["../rollup/maplibregl.js"],"sourcesContent":["//\n// Our custom intro provides a specialized \"define()\" function, called by the\n// AMD modules below, that sets up the worker blob URL and then executes the\n// main module, storing its exported value as 'maplibregl'\n\n// The three \"chunks\" imported here are produced by a first Rollup pass,\n// which outputs them as AMD modules.\n\n// Shared dependencies, i.e.:\n/*\ndefine(['exports'], function (exports) {\n // Code for all common dependencies\n // Each module's exports are attached attached to 'exports' (with\n // names rewritten to avoid collisions, etc.)\n})\n*/\nimport './build/maplibregl/shared';\n\n// Worker and its unique dependencies, i.e.:\n/*\ndefine(['./shared.js'], function (__shared__js) {\n // Code for worker script and its unique dependencies.\n // Expects the output of 'shared' module to be passed in as an argument,\n // since all references to common deps look like, e.g.,\n // __shared__js.shapeText().\n});\n*/\n// When this wrapper function is passed to our custom define() above,\n// it gets stringified, together with the shared wrapper (using\n// Function.toString()), and the resulting string of code is made into a\n// Blob URL that gets used by the main module to create the web workers.\nimport './build/maplibregl/worker';\n\n// Main module and its unique dependencies\n/*\ndefine(['./shared.js'], function (__shared__js) {\n // Code for main GL JS module and its unique dependencies.\n // Expects the output of 'shared' module to be passed in as an argument,\n // since all references to common deps look like, e.g.,\n // __shared__js.shapeText().\n //\n // Returns the actual maplibregl (i.e. src/index.js)\n});\n*/\nimport './build/maplibregl/index';\n\nexport default maplibregl;\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AA6CA;AACA,mBAAe,UAAU;;;;;;;;"}
|
1
public/resources/maplibre-gl-inspect-compat.min.js
vendored
Normal file
1
public/resources/maplibre-gl-inspect-compat.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
40
public/resources/maplibre-gl-inspect.css
Normal file
40
public/resources/maplibre-gl-inspect.css
Normal file
|
@ -0,0 +1,40 @@
|
|||
.maplibregl-inspect_popup {
|
||||
color: #333;
|
||||
display: table;
|
||||
}
|
||||
|
||||
.maplibregl-inspect_feature:not(:last-child) {
|
||||
border-bottom: 1px solid #ccc;
|
||||
}
|
||||
|
||||
.maplibregl-inspect_layer:before {
|
||||
content: '#';
|
||||
}
|
||||
|
||||
.maplibregl-inspect_layer {
|
||||
display: block;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.maplibregl-inspect_property {
|
||||
display: table-row;
|
||||
}
|
||||
|
||||
.maplibregl-inspect_property-value {
|
||||
display: table-cell;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.maplibregl-inspect_property-name {
|
||||
display: table-cell;
|
||||
padding-right: 10px;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.maplibregl-ctrl-inspect {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23333333' preserveAspectRatio='xMidYMid meet' viewBox='-10 -10 60 60'%3E%3Cg%3E%3Cpath d='m15 21.6q0-2 1.5-3.5t3.5-1.5 3.5 1.5 1.5 3.5-1.5 3.6-3.5 1.4-3.5-1.4-1.5-3.6z m18.4 11.1l-6.4-6.5q1.4-2.1 1.4-4.6 0-3.4-2.5-5.8t-5.9-2.4-5.9 2.4-2.5 5.8 2.5 5.9 5.9 2.5q2.4 0 4.6-1.4l7.4 7.4q-0.9 0.6-2 0.6h-20q-1.3 0-2.3-0.9t-1.1-2.3l0.1-26.8q0-1.3 1-2.3t2.3-0.9h13.4l10 10v19.3z'%3E%3C/path%3E%3C/g%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.maplibregl-ctrl-map {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23333333' viewBox='-10 -10 60 60' preserveAspectRatio='xMidYMid meet'%3E%3Cg%3E%3Cpath d='m25 31.640000000000004v-19.766666666666673l-10-3.511666666666663v19.766666666666666z m9.140000000000008-26.640000000000004q0.8599999999999923 0 0.8599999999999923 0.8600000000000003v25.156666666666666q0 0.625-0.625 0.783333333333335l-9.375 3.1999999999999993-10-3.5133333333333354-8.906666666666668 3.4383333333333326-0.2333333333333334 0.07833333333333314q-0.8616666666666664 0-0.8616666666666664-0.8599999999999994v-25.156666666666663q0-0.625 0.6233333333333331-0.7833333333333332l9.378333333333334-3.198333333333334 10 3.5133333333333336 8.905000000000001-3.4383333333333344z'%3E%3C/path%3E%3C/g%3E%3C/svg%3E");
|
||||
}
|
1
public/resources/maplibre-gl-inspect.min.js
vendored
Normal file
1
public/resources/maplibre-gl-inspect.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
public/resources/maplibre-gl.css
Normal file
1
public/resources/maplibre-gl.css
Normal file
File diff suppressed because one or more lines are too long
44
public/resources/maplibre-gl.js
Normal file
44
public/resources/maplibre-gl.js
Normal file
File diff suppressed because one or more lines are too long
1
public/resources/maplibre-gl.js.map
Normal file
1
public/resources/maplibre-gl.js.map
Normal file
|
@ -0,0 +1 @@
|
|||
{"version":3,"file":"maplibre-gl.js","sources":["../rollup/maplibregl.js"],"sourcesContent":["//\n// Our custom intro provides a specialized \"define()\" function, called by the\n// AMD modules below, that sets up the worker blob URL and then executes the\n// main module, storing its exported value as 'maplibregl'\n\n// The three \"chunks\" imported here are produced by a first Rollup pass,\n// which outputs them as AMD modules.\n\n// Shared dependencies, i.e.:\n/*\ndefine(['exports'], function (exports) {\n // Code for all common dependencies\n // Each module's exports are attached attached to 'exports' (with\n // names rewritten to avoid collisions, etc.)\n})\n*/\nimport './build/maplibregl/shared';\n\n// Worker and its unique dependencies, i.e.:\n/*\ndefine(['./shared.js'], function (__shared__js) {\n // Code for worker script and its unique dependencies.\n // Expects the output of 'shared' module to be passed in as an argument,\n // since all references to common deps look like, e.g.,\n // __shared__js.shapeText().\n});\n*/\n// When this wrapper function is passed to our custom define() above,\n// it gets stringified, together with the shared wrapper (using\n// Function.toString()), and the resulting string of code is made into a\n// Blob URL that gets used by the main module to create the web workers.\nimport './build/maplibregl/worker';\n\n// Main module and its unique dependencies\n/*\ndefine(['./shared.js'], function (__shared__js) {\n // Code for main GL JS module and its unique dependencies.\n // Expects the output of 'shared' module to be passed in as an argument,\n // since all references to common deps look like, e.g.,\n // __shared__js.shapeText().\n //\n // Returns the actual maplibregl (i.e. src/index.js)\n});\n*/\nimport './build/maplibregl/index';\n\nexport default maplibregl;\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AA6CA;AACA,mBAAe,UAAU;;;;;;;;"}
|
|
@ -4,34 +4,24 @@
|
|||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{{name}} - TileServer GL</title>
|
||||
{{#use_maplibre}}
|
||||
{{#is_vector}}
|
||||
<link rel="stylesheet" type="text/css" href="{{public_url}}maplibre-gl.css{{&key_query}}" />
|
||||
<link rel="stylesheet" type="text/css" href="{{public_url}}maplibre-gl-inspect.css{{&key_query}}" />
|
||||
<script src="{{public_url}}maplibre-gl.js{{&key_query}}"></script>
|
||||
<script src="{{public_url}}maplibre-gl-inspect.js{{&key_query}}"></script>
|
||||
{{^is_light}}
|
||||
<script src="{{public_url}}elevation-control.js{{&key_query}}"></script>
|
||||
{{/is_light}}
|
||||
<script>if (typeof Symbol !== 'undefined') { document.write('<script src="{{public_url}}maplibre-gl.js{{&key_query}}"><\/script>'); } else { document.write('<script src="{{public_url}}maplibre-gl-compat.js{{&key_query}}"><\/script>'); }</script>
|
||||
<script>if (typeof Symbol !== 'undefined') { document.write('<script src="{{public_url}}maplibre-gl-inspect.min.js{{&key_query}}"><\/script>'); } else { document.write('<script src="{{public_url}}maplibre-gl-inspect-compat.min.js{{&key_query}}"><\/script>'); }</script>
|
||||
<style>
|
||||
body {background:#fff;color:#333;font-family:Arial, sans-serif;}
|
||||
{{^is_terrain}}
|
||||
#map {position:absolute;top:0;left:0;right:250px;bottom:0;}
|
||||
{{/is_terrain}}
|
||||
{{#is_terrain}}
|
||||
#map { position:absolute; top:0; bottom:0; left:0; right:0; }
|
||||
{{/is_terrain}}
|
||||
h1 {position:absolute;top:5px;right:0;width:240px;margin:0;line-height:20px;font-size:20px;}
|
||||
#layerList {position:absolute;top:35px;right:0;bottom:0;width:240px;overflow:auto;}
|
||||
#layerList div div {width:15px;height:15px;display:inline-block;}
|
||||
{{^is_light}}
|
||||
.maplibre-ctrl-elevation { padding-left: 5px; padding-right: 5px; }
|
||||
{{/is_light}}
|
||||
</style>
|
||||
{{/use_maplibre}}
|
||||
{{^use_maplibre}}
|
||||
{{/is_vector}}
|
||||
{{^is_vector}}
|
||||
<link rel="stylesheet" type="text/css" href="{{public_url}}leaflet.css{{&key_query}}" />
|
||||
<script src="{{public_url}}leaflet.js{{&key_query}}"></script>
|
||||
<script src="{{public_url}}leaflet-hash.js{{&key_query}}"></script>
|
||||
<script src="{{public_url}}L.TileLayer.NoGap.js{{&key_query}}"></script>
|
||||
<style>
|
||||
body { margin:0; padding:0; }
|
||||
#map { position:absolute; top:0; bottom:0; width:100%; }
|
||||
|
@ -48,22 +38,23 @@
|
|||
background-image: url({{public_url}}images/marker-icon.png{{&key_query}});
|
||||
}
|
||||
</style>
|
||||
{{/use_maplibre}}
|
||||
{{/is_vector}}
|
||||
</head>
|
||||
<body>
|
||||
{{#use_maplibre}}
|
||||
{{#is_vector}}
|
||||
<h1>{{name}}</h1>
|
||||
<div id="map"></div>
|
||||
{{^is_terrain}}
|
||||
<div id="layerList"></div>
|
||||
<pre id="propertyList"></pre>
|
||||
{{/is_terrain}}
|
||||
<script>
|
||||
var keyMatch = location.search.match(/[\?\&]key=([^&]+)/i);
|
||||
var keyParam = keyMatch ? '?key=' + keyMatch[1] : '';
|
||||
|
||||
{{^is_terrain}}
|
||||
var style = {
|
||||
var map = new maplibregl.Map({
|
||||
container: 'map',
|
||||
hash: true,
|
||||
maplibreLogo: true,
|
||||
style: {
|
||||
version: 8,
|
||||
sources: {
|
||||
'vector_layer_': {
|
||||
|
@ -72,89 +63,14 @@
|
|||
}
|
||||
},
|
||||
layers: []
|
||||
};
|
||||
{{/is_terrain}}
|
||||
{{#is_terrain}}
|
||||
|
||||
var style = {
|
||||
version: 8,
|
||||
sources: {
|
||||
"terrain": {
|
||||
"type": "raster-dem",
|
||||
"url": "{{public_url}}data/{{id}}.json",
|
||||
"encoding": "{{terrain_encoding}}"
|
||||
},
|
||||
"hillshade": {
|
||||
"type": "raster-dem",
|
||||
"url": "{{public_url}}data/{{id}}.json",
|
||||
"encoding": "{{terrain_encoding}}"
|
||||
}
|
||||
},
|
||||
"terrain": {
|
||||
"source": "terrain"
|
||||
},
|
||||
"layers": [
|
||||
{
|
||||
"id": "background",
|
||||
"paint": {
|
||||
{{#if is_terrainrgb}}
|
||||
"background-color": "hsl(190, 99%, 63%)"
|
||||
{{else}}
|
||||
"background-color": "hsl(0, 100%, 25%)"
|
||||
{{/if}}
|
||||
},
|
||||
"type": "background"
|
||||
},
|
||||
{
|
||||
"id": "hillshade",
|
||||
"source": "hillshade",
|
||||
"type": "hillshade",
|
||||
"paint": {
|
||||
"hillshade-shadow-color": "hsl(39, 21%, 33%)",
|
||||
"hillshade-illumination-direction": 315,
|
||||
"hillshade-exaggeration": 0.8
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
{{/is_terrain}}
|
||||
|
||||
var map = new maplibregl.Map({
|
||||
container: 'map',
|
||||
hash: true,
|
||||
maxPitch: 85,
|
||||
style: style
|
||||
});
|
||||
|
||||
map.addControl(new maplibregl.NavigationControl({
|
||||
visualizePitch: true,
|
||||
showZoom: true,
|
||||
showCompass: true
|
||||
}));
|
||||
{{#is_terrain}}
|
||||
|
||||
map.addControl(
|
||||
new maplibregl.TerrainControl({
|
||||
source: "terrain",
|
||||
})
|
||||
);
|
||||
|
||||
{{^is_light}}
|
||||
map.addControl(
|
||||
new ElevationInfoControl({
|
||||
url: "{{public_url}}data/{{id}}/elevation/{z}/{x}/{y}"
|
||||
})
|
||||
);
|
||||
{{/is_light}}
|
||||
{{/is_terrain}}
|
||||
{{^is_terrain}}
|
||||
|
||||
map.addControl(new maplibregl.NavigationControl());
|
||||
var inspect = new MaplibreInspect({
|
||||
showInspectMap: true,
|
||||
showInspectButton: false
|
||||
});
|
||||
map.addControl(inspect);
|
||||
|
||||
map.on('styledata', function() {
|
||||
var layerList = document.getElementById('layerList');
|
||||
layerList.innerHTML = '';
|
||||
|
@ -169,15 +85,15 @@
|
|||
});
|
||||
})
|
||||
});
|
||||
{{/is_terrain}}
|
||||
</script>
|
||||
{{/use_maplibre}}
|
||||
{{^use_maplibre}}
|
||||
{{/is_vector}}
|
||||
{{^is_vector}}
|
||||
<h1 style="display:none;">{{name}}</h1>
|
||||
<div id='map'></div>
|
||||
<script>
|
||||
var keyMatch = location.search.match(/[\?\&]key=([^&]+)/i);
|
||||
var keyParam = keyMatch ? '?key=' + keyMatch[1] : '';
|
||||
|
||||
var map = L.map('map', { zoomControl: false });
|
||||
new L.Control.Zoom({ position: 'topright' }).addTo(map);
|
||||
|
||||
|
@ -208,7 +124,6 @@
|
|||
|
||||
for (tile_url in tile_urls) {
|
||||
L.tileLayer(tile_urls[tile_url], {
|
||||
tileSize: 256,
|
||||
minZoom: tile_minzoom,
|
||||
maxZoom: tile_maxzoom,
|
||||
attribution: tile_attribution
|
||||
|
@ -226,6 +141,6 @@
|
|||
new L.Hash(map);
|
||||
}, 0);
|
||||
</script>
|
||||
{{/use_maplibre}}
|
||||
{{/is_vector}}
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -6,51 +6,24 @@
|
|||
<title>TileServer GL - Server for vector and raster maps with GL styles</title>
|
||||
<link rel="stylesheet" type="text/css" href="{{public_url}}index.css{{&key_query}}" />
|
||||
<script>
|
||||
function toggle_link(id, link) {
|
||||
function toggle_xyz(id) {
|
||||
var el = document.getElementById(id);
|
||||
var s = el.style;
|
||||
if (s.display == 'none') {
|
||||
s.display = 'inline-block';
|
||||
} else if (el.value == link) {
|
||||
s.display = 'none';
|
||||
}
|
||||
el.value = link;
|
||||
s.display = s.display == 'none' ? 'inline-block' : 'none';
|
||||
el.setSelectionRange(0, el.value.length);
|
||||
return false;
|
||||
}
|
||||
|
||||
function filter() {
|
||||
var filter = document.getElementById('filter').value.toLowerCase();
|
||||
var items = document.getElementsByClassName('item');
|
||||
for (var i = 0; i < items.length; i++) {
|
||||
var item = items[i];
|
||||
var dataName = item.getAttribute('data-name')?.toLowerCase() ?? '';
|
||||
var dataIdentifier = item.getAttribute('data-id')?.toLowerCase() ?? '';
|
||||
item.hidden = !(dataName.indexOf(filter) > -1 || dataIdentifier.indexOf(filter) > -1);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<section>
|
||||
<h1 class="title {{#if is_light}}light{{/if}}"><img src="{{public_url}}images/logo.png{{&key_query}}" alt="TileServer GL" /></h1>
|
||||
<h2 class="subtitle">Vector {{#if is_light}}<s>and raster</s>{{else}}and raster{{/if}} maps with GL styles</h2>
|
||||
<h2 class="box-header">Filter</h2>
|
||||
<!-- filter box -->
|
||||
<div class="box">
|
||||
<div class="filter-item">
|
||||
<div class="filter-details">
|
||||
<h3>Filter styles and data by name or identifier</h3>
|
||||
<!-- filter input , needs to call filter() when content changes...-->
|
||||
<input id="filter" type="text" oninput="filter()" placeholder="Start typing name or identifier" autofocus />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{#if styles}}
|
||||
<h2 class="box-header">Styles</h2>
|
||||
<div class="box">
|
||||
{{#each styles}}
|
||||
<div class="item" data-id="{{@key}}" data-name="{{name}}">
|
||||
<div class="item">
|
||||
{{#if thumbnail}}
|
||||
<img src="{{public_url}}styles/{{@key}}/{{thumbnail}}{{&../key_query}}" alt="{{name}} preview" />
|
||||
{{else}}
|
||||
|
@ -65,14 +38,14 @@
|
|||
<a href="{{public_url}}styles/{{@key}}/style.json{{&../key_query}}">GL Style</a>
|
||||
{{/if}}
|
||||
{{#if serving_rendered}}
|
||||
{{#if serving_data}}| {{/if}}<a href="{{public_url}}styles/512/{{@key}}.json{{&../key_query}}">TileJSON</a>
|
||||
{{#if serving_data}}| {{/if}}<a href="{{public_url}}styles/{{@key}}.json{{&../key_query}}">TileJSON</a>
|
||||
{{/if}}
|
||||
{{#if serving_rendered}}
|
||||
| <a href="{{public_url}}styles/{{@key}}/wmts.xml{{&../key_query}}">WMTS</a>
|
||||
{{/if}}
|
||||
{{#if xyz_link}}
|
||||
| <a href="#" onclick="return toggle_link('xyz_style_{{@key}}', '{{&xyz_link}}');">XYZ</a>
|
||||
<input id="xyz_style_{{@key}}" type="text" value="" style="display:none;" />
|
||||
| <a href="#" onclick="return toggle_xyz('xyz_style_{{@key}}');">XYZ</a>
|
||||
<input id="xyz_style_{{@key}}" type="text" value="{{&xyz_link}}" style="display:none;" />
|
||||
{{/if}}
|
||||
</p>
|
||||
</div>
|
||||
|
@ -97,25 +70,24 @@
|
|||
<h2 class="box-header">Data</h2>
|
||||
<div class="box">
|
||||
{{#each data}}
|
||||
<div class="item" data-id="{{@key}}" data-name="{{tileJSON.name}}">
|
||||
<div class="item">
|
||||
{{#if thumbnail}}
|
||||
<img src="{{public_url}}data/{{@key}}/{{thumbnail}}{{&../key_query}}" alt="{{name}} preview" />
|
||||
{{else}}
|
||||
<img src="{{public_url}}images/placeholder.png{{&key_query}}" alt="{{name}} preview" />
|
||||
{{/if}}
|
||||
<div class="details">
|
||||
<h3>{{tileJSON.name}}</h3>
|
||||
<div class="identifier">identifier: {{@key}}{{#if formatted_filesize}} | size: {{formatted_filesize}}{{/if}}</div>
|
||||
<div class="identifier">type: {{#is_vector}}vector{{/is_vector}}{{^is_vector}}raster{{/is_vector}} data {{#if sourceType}} | ext: {{sourceType}}{{/if}}</div>
|
||||
<h3>{{name}}</h3>
|
||||
<p class="identifier">identifier: {{@key}}{{#if formatted_filesize}} | size: {{formatted_filesize}}{{/if}} | type: {{#is_vector}}vector{{/is_vector}}{{^is_vector}}raster{{/is_vector}} data</p>
|
||||
<p class="services">
|
||||
services: <a href="{{public_url}}data/{{@key}}.json{{&../key_query}}">TileJSON</a>
|
||||
{{#if wmts_link}}
|
||||
| <a href="{{&wmts_link}}">WMTS</a>
|
||||
{{/if}}
|
||||
{{#if xyz_link}}
|
||||
| <a href="#" onclick="return toggle_link('link_data_{{@key}}', '{{&xyz_link}}');">XYZ</a>
|
||||
| <a href="#" onclick="return toggle_xyz('xyz_data_{{@key}}');">XYZ</a>
|
||||
<input id="xyz_data_{{@key}}" type="text" value="{{&xyz_link}}" style="display:none;" />
|
||||
{{/if}}
|
||||
{{#if elevation_link}}
|
||||
| <a href="#" onclick="return toggle_link('link_data_{{@key}}', '{{&elevation_link}}');">Elevation</a>
|
||||
{{/if}}
|
||||
<input id="link_data_{{@key}}" type="text" value="" style="display:none;" />
|
||||
</p>
|
||||
</div>
|
||||
<div class="viewers">
|
||||
|
@ -124,9 +96,6 @@
|
|||
{{/is_vector}}
|
||||
{{^is_vector}}
|
||||
<a class="btn" href="{{public_url}}data/{{@key}}/{{&../key_query}}{{viewer_hash}}">View</a>
|
||||
{{#is_terrain}}
|
||||
<a class="btn" href="{{public_url}}data/preview/{{@key}}/{{&../key_query}}{{viewer_hash}}">Preview Terrain</a>
|
||||
{{/is_terrain}}
|
||||
{{/is_vector}}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -7,10 +7,11 @@
|
|||
<link rel="stylesheet" type="text/css" href="{{public_url}}maplibre-gl.css{{&key_query}}" />
|
||||
<link rel="stylesheet" type="text/css" href="{{public_url}}maplibre-gl-inspect.css{{&key_query}}" />
|
||||
<link rel="stylesheet" type="text/css" href="{{public_url}}leaflet.css{{&key_query}}" />
|
||||
<script src="{{public_url}}maplibre-gl.js{{&key_query}}"></script>
|
||||
<script src="{{public_url}}maplibre-gl-inspect.js{{&key_query}}"></script>
|
||||
<script>if (typeof Symbol !== 'undefined') { document.write('<script src="{{public_url}}maplibre-gl.js{{&key_query}}"><\/script>'); } else { document.write('<script src="{{public_url}}maplibre-gl-compat.js{{&key_query}}"><\/script>'); }</script>
|
||||
<script>if (typeof Symbol !== 'undefined') { document.write('<script src="{{public_url}}maplibre-gl-inspect.min.js{{&key_query}}"><\/script>'); } else { document.write('<script src="{{public_url}}maplibre-gl-inspect-compat.min.js{{&key_query}}"><\/script>'); }</script>
|
||||
<script src="{{public_url}}leaflet.js{{&key_query}}"></script>
|
||||
<script src="{{public_url}}leaflet-hash.js{{&key_query}}"></script>
|
||||
<script src="{{public_url}}L.TileLayer.NoGap.js{{&key_query}}"></script>
|
||||
<style>
|
||||
body { margin:0; padding:0; }
|
||||
#map { position:absolute; top:0; bottom:0; width:100%; }
|
||||
|
@ -32,27 +33,11 @@
|
|||
<h1 style="display:none;">{{name}}</h1>
|
||||
<div id='map'></div>
|
||||
<script>
|
||||
function isWebglSupported() {
|
||||
if (window.WebGLRenderingContext) {
|
||||
const canvas = document.createElement('canvas');
|
||||
try {
|
||||
const context = canvas.getContext('webgl2') || canvas.getContext('webgl');
|
||||
if (context && typeof context.getParameter == 'function') {
|
||||
return true;
|
||||
}
|
||||
} catch (e) {
|
||||
// WebGL is supported, but disabled
|
||||
}
|
||||
return false;
|
||||
}
|
||||
// WebGL not supported
|
||||
return false;
|
||||
}
|
||||
var q = (location.search || '').substr(1).split('&');
|
||||
var preference =
|
||||
q.indexOf('vector') >= 0 ? 'vector' :
|
||||
(q.indexOf('raster') >= 0 ? 'raster' :
|
||||
(isWebglSupported() ? 'vector' : 'raster'));
|
||||
(maplibregl.supported() ? 'vector' : 'raster'));
|
||||
|
||||
var keyMatch = location.search.match(/[\?\&]key=([^&]+)/i);
|
||||
var keyParam = keyMatch ? '?key=' + keyMatch[1] : '';
|
||||
|
@ -63,7 +48,7 @@
|
|||
container: 'map',
|
||||
style: '{{public_url}}styles/{{id}}/style.json' + keyParam,
|
||||
hash: true,
|
||||
maxPitch: 85
|
||||
maplibreLogo: true
|
||||
});
|
||||
map.addControl(new maplibregl.NavigationControl({
|
||||
visualizePitch: true,
|
||||
|
@ -76,11 +61,11 @@
|
|||
selectThreshold: 5
|
||||
}));
|
||||
} else {
|
||||
var map = L.map('map', { minZoom: 1, zoomControl: false });
|
||||
var map = L.map('map', { zoomControl: false });
|
||||
new L.Control.Zoom({ position: 'topright' }).addTo(map);
|
||||
|
||||
var tile_urls = [], tile_attribution, tile_minzoom, tile_maxzoom;
|
||||
var url = '{{public_url}}styles/512/{{id}}.json' + keyParam;
|
||||
var url = '{{public_url}}styles/{{id}}.json' + keyParam;
|
||||
var req = new XMLHttpRequest();
|
||||
req.overrideMimeType("application/json");
|
||||
req.open('GET', url, true);
|
||||
|
@ -106,8 +91,6 @@
|
|||
|
||||
for (tile_url in tile_urls) {
|
||||
L.tileLayer(tile_urls[tile_url], {
|
||||
tileSize: 512,
|
||||
zoomOffset: -1,
|
||||
minZoom: tile_minzoom,
|
||||
maxZoom: tile_maxzoom,
|
||||
attribution: tile_attribution
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
<ows:Operation name="GetCapabilities">
|
||||
<ows:DCP>
|
||||
<ows:HTTP>
|
||||
<ows:Get xlink:href="{{baseUrl}}styles/{{id}}/wmts.xml">
|
||||
<ows:Get xlink:href="{{baseUrl}}wmts/{{id}}/">
|
||||
<ows:Constraint name="GetEncoding">
|
||||
<ows:AllowedValues>
|
||||
<ows:Value>RESTful</ows:Value>
|
||||
|
@ -24,7 +24,7 @@
|
|||
<ows:Operation name="GetTile">
|
||||
<ows:DCP>
|
||||
<ows:HTTP>
|
||||
<ows:Get xlink:href="{{baseUrl}}styles/{{id}}/wmts.xml">
|
||||
<ows:Get xlink:href="{{baseUrl}}styles/">
|
||||
<ows:Constraint name="GetEncoding">
|
||||
<ows:AllowedValues>
|
||||
<ows:Value>RESTful</ows:Value>
|
||||
|
@ -37,8 +37,8 @@
|
|||
</ows:OperationsMetadata>
|
||||
<Contents>
|
||||
<Layer>
|
||||
<ows:Title>{{name}}-256</ows:Title>
|
||||
<ows:Identifier>{{id}}-256</ows:Identifier>
|
||||
<ows:Title>{{name}}</ows:Title>
|
||||
<ows:Identifier>{{id}}</ows:Identifier>
|
||||
<ows:WGS84BoundingBox crs="urn:ogc:def:crs:OGC:2:84">
|
||||
<ows:LowerCorner>-180 -85.051128779807</ows:LowerCorner>
|
||||
<ows:UpperCorner>180 85.051128779807</ows:UpperCorner>
|
||||
|
@ -48,30 +48,13 @@
|
|||
</Style>
|
||||
<Format>image/png</Format>
|
||||
<TileMatrixSetLink>
|
||||
<TileMatrixSet>GoogleMapsCompatible_256</TileMatrixSet>
|
||||
<TileMatrixSet>GoogleMapsCompatible</TileMatrixSet>
|
||||
</TileMatrixSetLink>
|
||||
<ResourceURL format="image/png" resourceType="tile" template="{{baseUrl}}styles/{{id}}/256/{TileMatrix}/{TileCol}/{TileRow}.png{{key_query}}"/>
|
||||
</Layer>
|
||||
<Layer>
|
||||
<ows:Title>{{name}}-512</ows:Title>
|
||||
<ows:Identifier>{{id}}-512</ows:Identifier>
|
||||
<ows:WGS84BoundingBox crs="urn:ogc:def:crs:OGC:2:84">
|
||||
<ows:LowerCorner>-180 -85.051128779807</ows:LowerCorner>
|
||||
<ows:UpperCorner>180 85.051128779807</ows:UpperCorner>
|
||||
</ows:WGS84BoundingBox>
|
||||
<Style isDefault="true">
|
||||
<ows:Identifier>default</ows:Identifier>
|
||||
</Style>
|
||||
<Format>image/png</Format>
|
||||
<TileMatrixSetLink>
|
||||
<TileMatrixSet>GoogleMapsCompatible_512</TileMatrixSet>
|
||||
</TileMatrixSetLink>
|
||||
<ResourceURL format="image/png" resourceType="tile" template="{{baseUrl}}styles/{{id}}/512/{TileMatrix}/{TileCol}/{TileRow}.png{{key_query}}"/>
|
||||
</Layer>
|
||||
<TileMatrixSet>
|
||||
<ows:Title>GoogleMapsCompatible_256</ows:Title>
|
||||
<ows:Abstract>GoogleMapsCompatible_256 EPSG:3857</ows:Abstract>
|
||||
<ows:Identifier>GoogleMapsCompatible_256</ows:Identifier>
|
||||
<ResourceURL format="image/png" resourceType="tile" template="{{baseUrl}}styles/{{id}}/{TileMatrix}/{TileCol}/{TileRow}.png{{key_query}}"/>
|
||||
</Layer><TileMatrixSet>
|
||||
<ows:Title>GoogleMapsCompatible</ows:Title>
|
||||
<ows:Abstract>GoogleMapsCompatible EPSG:3857</ows:Abstract>
|
||||
<ows:Identifier>GoogleMapsCompatible</ows:Identifier>
|
||||
<ows:SupportedCRS>urn:ogc:def:crs:EPSG::3857</ows:SupportedCRS>
|
||||
<TileMatrix>
|
||||
<ows:Identifier>0</ows:Identifier>
|
||||
|
@ -243,189 +226,10 @@
|
|||
<TileHeight>256</TileHeight>
|
||||
<MatrixWidth>262144</MatrixWidth>
|
||||
<MatrixHeight>262144</MatrixHeight>
|
||||
</TileMatrix>
|
||||
</TileMatrixSet>
|
||||
<TileMatrixSet>
|
||||
<ows:Title>GoogleMapsCompatible_512</ows:Title>
|
||||
<ows:Abstract>GoogleMapsCompatible_512 EPSG:3857</ows:Abstract>
|
||||
<ows:Identifier>GoogleMapsCompatible_512</ows:Identifier>
|
||||
<ows:SupportedCRS>urn:ogc:def:crs:EPSG::3857</ows:SupportedCRS>
|
||||
<TileMatrix>
|
||||
<ows:Identifier>0</ows:Identifier>
|
||||
<ScaleDenominator>279541132.0143589</ScaleDenominator>
|
||||
<TopLeftCorner>-20037508.34 20037508.34</TopLeftCorner>
|
||||
<TileWidth>512</TileWidth>
|
||||
<TileHeight>512</TileHeight>
|
||||
<MatrixWidth>1</MatrixWidth>
|
||||
<MatrixHeight>1</MatrixHeight>
|
||||
</TileMatrix>
|
||||
<TileMatrix>
|
||||
<ows:Identifier>1</ows:Identifier>
|
||||
<ScaleDenominator>139770566.0071794</ScaleDenominator>
|
||||
<TopLeftCorner>-20037508.34 20037508.34</TopLeftCorner>
|
||||
<TileWidth>512</TileWidth>
|
||||
<TileHeight>512</TileHeight>
|
||||
<MatrixWidth>2</MatrixWidth>
|
||||
<MatrixHeight>2</MatrixHeight>
|
||||
</TileMatrix>
|
||||
<TileMatrix>
|
||||
<ows:Identifier>2</ows:Identifier>
|
||||
<ScaleDenominator>69885283.00358972</ScaleDenominator>
|
||||
<TopLeftCorner>-20037508.34 20037508.34</TopLeftCorner>
|
||||
<TileWidth>512</TileWidth>
|
||||
<TileHeight>512</TileHeight>
|
||||
<MatrixWidth>4</MatrixWidth>
|
||||
<MatrixHeight>4</MatrixHeight>
|
||||
</TileMatrix>
|
||||
<TileMatrix>
|
||||
<ows:Identifier>3</ows:Identifier>
|
||||
<ScaleDenominator>34942641.501795</ScaleDenominator>
|
||||
<TopLeftCorner>-20037508.34 20037508.34</TopLeftCorner>
|
||||
<TileWidth>512</TileWidth>
|
||||
<TileHeight>512</TileHeight>
|
||||
<MatrixWidth>8</MatrixWidth>
|
||||
<MatrixHeight>8</MatrixHeight>
|
||||
</TileMatrix>
|
||||
<TileMatrix>
|
||||
<ows:Identifier>4</ows:Identifier>
|
||||
<ScaleDenominator>17471320.750897</ScaleDenominator>
|
||||
<TopLeftCorner>-20037508.34 20037508.34</TopLeftCorner>
|
||||
<TileWidth>512</TileWidth>
|
||||
<TileHeight>512</TileHeight>
|
||||
<MatrixWidth>16</MatrixWidth>
|
||||
<MatrixHeight>16</MatrixHeight>
|
||||
</TileMatrix>
|
||||
<TileMatrix>
|
||||
<ows:Identifier>5</ows:Identifier>
|
||||
<ScaleDenominator>8735660.3754487</ScaleDenominator>
|
||||
<TopLeftCorner>-20037508.34 20037508.34</TopLeftCorner>
|
||||
<TileWidth>512</TileWidth>
|
||||
<TileHeight>512</TileHeight>
|
||||
<MatrixWidth>32</MatrixWidth>
|
||||
<MatrixHeight>32</MatrixHeight>
|
||||
</TileMatrix>
|
||||
<TileMatrix>
|
||||
<ows:Identifier>6</ows:Identifier>
|
||||
<ScaleDenominator>4367830.1877244</ScaleDenominator>
|
||||
<TopLeftCorner>-20037508.34 20037508.34</TopLeftCorner>
|
||||
<TileWidth>512</TileWidth>
|
||||
<TileHeight>512</TileHeight>
|
||||
<MatrixWidth>64</MatrixWidth>
|
||||
<MatrixHeight>64</MatrixHeight>
|
||||
</TileMatrix>
|
||||
<TileMatrix>
|
||||
<ows:Identifier>7</ows:Identifier>
|
||||
<ScaleDenominator>2183915.0938622</ScaleDenominator>
|
||||
<TopLeftCorner>-20037508.34 20037508.34</TopLeftCorner>
|
||||
<TileWidth>512</TileWidth>
|
||||
<TileHeight>512</TileHeight>
|
||||
<MatrixWidth>128</MatrixWidth>
|
||||
<MatrixHeight>128</MatrixHeight>
|
||||
</TileMatrix>
|
||||
<TileMatrix>
|
||||
<ows:Identifier>8</ows:Identifier>
|
||||
<ScaleDenominator>1091957.5469311</ScaleDenominator>
|
||||
<TopLeftCorner>-20037508.34 20037508.34</TopLeftCorner>
|
||||
<TileWidth>512</TileWidth>
|
||||
<TileHeight>512</TileHeight>
|
||||
<MatrixWidth>256</MatrixWidth>
|
||||
<MatrixHeight>256</MatrixHeight>
|
||||
</TileMatrix>
|
||||
<TileMatrix>
|
||||
<ows:Identifier>9</ows:Identifier>
|
||||
<ScaleDenominator>545978.77346554</ScaleDenominator>
|
||||
<TopLeftCorner>-20037508.34 20037508.34</TopLeftCorner>
|
||||
<TileWidth>512</TileWidth>
|
||||
<TileHeight>512</TileHeight>
|
||||
<MatrixWidth>512</MatrixWidth>
|
||||
<MatrixHeight>512</MatrixHeight>
|
||||
</TileMatrix>
|
||||
<TileMatrix>
|
||||
<ows:Identifier>10</ows:Identifier>
|
||||
<ScaleDenominator>272989.38673277</ScaleDenominator>
|
||||
<TopLeftCorner>-20037508.34 20037508.34</TopLeftCorner>
|
||||
<TileWidth>512</TileWidth>
|
||||
<TileHeight>512</TileHeight>
|
||||
<MatrixWidth>1024</MatrixWidth>
|
||||
<MatrixHeight>1024</MatrixHeight>
|
||||
</TileMatrix>
|
||||
<TileMatrix>
|
||||
<ows:Identifier>11</ows:Identifier>
|
||||
<ScaleDenominator>136494.69336639</ScaleDenominator>
|
||||
<TopLeftCorner>-20037508.34 20037508.34</TopLeftCorner>
|
||||
<TileWidth>512</TileWidth>
|
||||
<TileHeight>512</TileHeight>
|
||||
<MatrixWidth>2048</MatrixWidth>
|
||||
<MatrixHeight>2048</MatrixHeight>
|
||||
</TileMatrix>
|
||||
<TileMatrix>
|
||||
<ows:Identifier>12</ows:Identifier>
|
||||
<ScaleDenominator>68247.346683193</ScaleDenominator>
|
||||
<TopLeftCorner>-20037508.34 20037508.34</TopLeftCorner>
|
||||
<TileWidth>512</TileWidth>
|
||||
<TileHeight>512</TileHeight>
|
||||
<MatrixWidth>4096</MatrixWidth>
|
||||
<MatrixHeight>4096</MatrixHeight>
|
||||
</TileMatrix>
|
||||
<TileMatrix>
|
||||
<ows:Identifier>13</ows:Identifier>
|
||||
<ScaleDenominator>34123.673341597</ScaleDenominator>
|
||||
<TopLeftCorner>-20037508.34 20037508.34</TopLeftCorner>
|
||||
<TileWidth>512</TileWidth>
|
||||
<TileHeight>512</TileHeight>
|
||||
<MatrixWidth>8192</MatrixWidth>
|
||||
<MatrixHeight>8192</MatrixHeight>
|
||||
</TileMatrix>
|
||||
<TileMatrix>
|
||||
<ows:Identifier>14</ows:Identifier>
|
||||
<ScaleDenominator>17061.836670798</ScaleDenominator>
|
||||
<TopLeftCorner>-20037508.34 20037508.34</TopLeftCorner>
|
||||
<TileWidth>512</TileWidth>
|
||||
<TileHeight>512</TileHeight>
|
||||
<MatrixWidth>16384</MatrixWidth>
|
||||
<MatrixHeight>16384</MatrixHeight>
|
||||
</TileMatrix>
|
||||
<TileMatrix>
|
||||
<ows:Identifier>15</ows:Identifier>
|
||||
<ScaleDenominator>8530.9183353991</ScaleDenominator>
|
||||
<TopLeftCorner>-20037508.34 20037508.34</TopLeftCorner>
|
||||
<TileWidth>512</TileWidth>
|
||||
<TileHeight>512</TileHeight>
|
||||
<MatrixWidth>32768</MatrixWidth>
|
||||
<MatrixHeight>32768</MatrixHeight>
|
||||
</TileMatrix>
|
||||
<TileMatrix>
|
||||
<ows:Identifier>16</ows:Identifier>
|
||||
<ScaleDenominator>4265.4591676996</ScaleDenominator>
|
||||
<TopLeftCorner>-20037508.34 20037508.34</TopLeftCorner>
|
||||
<TileWidth>512</TileWidth>
|
||||
<TileHeight>512</TileHeight>
|
||||
<MatrixWidth>65536</MatrixWidth>
|
||||
<MatrixHeight>65536</MatrixHeight>
|
||||
</TileMatrix>
|
||||
<TileMatrix>
|
||||
<ows:Identifier>17</ows:Identifier>
|
||||
<ScaleDenominator>2132.7295838498</ScaleDenominator>
|
||||
<TopLeftCorner>-20037508.34 20037508.34</TopLeftCorner>
|
||||
<TileWidth>512</TileWidth>
|
||||
<TileHeight>512</TileHeight>
|
||||
<MatrixWidth>131072</MatrixWidth>
|
||||
<MatrixHeight>131072</MatrixHeight>
|
||||
</TileMatrix>
|
||||
<TileMatrix>
|
||||
<ows:Identifier>18</ows:Identifier>
|
||||
<ScaleDenominator>1066.364791924892</ScaleDenominator>
|
||||
<TopLeftCorner>-20037508.34 20037508.34</TopLeftCorner>
|
||||
<TileWidth>512</TileWidth>
|
||||
<TileHeight>512</TileHeight>
|
||||
<MatrixWidth>262144</MatrixWidth>
|
||||
<MatrixHeight>262144</MatrixHeight>
|
||||
</TileMatrix>
|
||||
</TileMatrixSet>
|
||||
<TileMatrixSet>
|
||||
<ows:Title>WGS84_256</ows:Title>
|
||||
<ows:Abstract>WGS84_256 EPSG:4326</ows:Abstract>
|
||||
<ows:Identifier>WGS84_256</ows:Identifier>
|
||||
</TileMatrix></TileMatrixSet><TileMatrixSet>
|
||||
<ows:Title>WGS84</ows:Title>
|
||||
<ows:Abstract>WGS84 EPSG:4326</ows:Abstract>
|
||||
<ows:Identifier>WGS84</ows:Identifier>
|
||||
<ows:SupportedCRS>urn:ogc:def:crs:EPSG::4326</ows:SupportedCRS>
|
||||
<TileMatrix>
|
||||
<ows:Identifier>0</ows:Identifier>
|
||||
|
@ -597,185 +401,7 @@
|
|||
<TileHeight>256</TileHeight>
|
||||
<MatrixWidth>524288</MatrixWidth>
|
||||
<MatrixHeight>262144</MatrixHeight>
|
||||
</TileMatrix>
|
||||
</TileMatrixSet>
|
||||
<TileMatrixSet>
|
||||
<ows:Title>WGS84_512</ows:Title>
|
||||
<ows:Abstract>WGS84_512 EPSG:4326</ows:Abstract>
|
||||
<ows:Identifier>WGS84_512</ows:Identifier>
|
||||
<ows:SupportedCRS>urn:ogc:def:crs:EPSG::4326</ows:SupportedCRS>
|
||||
<TileMatrix>
|
||||
<ows:Identifier>0</ows:Identifier>
|
||||
<ScaleDenominator>139770566.00718</ScaleDenominator>
|
||||
<TopLeftCorner>90 -180</TopLeftCorner>
|
||||
<TileWidth>512</TileWidth>
|
||||
<TileHeight>512</TileHeight>
|
||||
<MatrixWidth>2</MatrixWidth>
|
||||
<MatrixHeight>1</MatrixHeight>
|
||||
</TileMatrix>
|
||||
<TileMatrix>
|
||||
<ows:Identifier>1</ows:Identifier>
|
||||
<ScaleDenominator>69885283.00359</ScaleDenominator>
|
||||
<TopLeftCorner>90 -180</TopLeftCorner>
|
||||
<TileWidth>512</TileWidth>
|
||||
<TileHeight>512</TileHeight>
|
||||
<MatrixWidth>4</MatrixWidth>
|
||||
<MatrixHeight>2</MatrixHeight>
|
||||
</TileMatrix>
|
||||
<TileMatrix>
|
||||
<ows:Identifier>2</ows:Identifier>
|
||||
<ScaleDenominator>34942641.501795</ScaleDenominator>
|
||||
<TopLeftCorner>90 -180</TopLeftCorner>
|
||||
<TileWidth>512</TileWidth>
|
||||
<TileHeight>512</TileHeight>
|
||||
<MatrixWidth>8</MatrixWidth>
|
||||
<MatrixHeight>4</MatrixHeight>
|
||||
</TileMatrix>
|
||||
<TileMatrix>
|
||||
<ows:Identifier>3</ows:Identifier>
|
||||
<ScaleDenominator>17471320.750897</ScaleDenominator>
|
||||
<TopLeftCorner>90 -180</TopLeftCorner>
|
||||
<TileWidth>512</TileWidth>
|
||||
<TileHeight>512</TileHeight>
|
||||
<MatrixWidth>16</MatrixWidth>
|
||||
<MatrixHeight>8</MatrixHeight>
|
||||
</TileMatrix>
|
||||
<TileMatrix>
|
||||
<ows:Identifier>4</ows:Identifier>
|
||||
<ScaleDenominator>8735660.3754487</ScaleDenominator>
|
||||
<TopLeftCorner>90 -180</TopLeftCorner>
|
||||
<TileWidth>512</TileWidth>
|
||||
<TileHeight>512</TileHeight>
|
||||
<MatrixWidth>32</MatrixWidth>
|
||||
<MatrixHeight>16</MatrixHeight>
|
||||
</TileMatrix>
|
||||
<TileMatrix>
|
||||
<ows:Identifier>5</ows:Identifier>
|
||||
<ScaleDenominator>4367830.1877244</ScaleDenominator>
|
||||
<TopLeftCorner>90 -180</TopLeftCorner>
|
||||
<TileWidth>512</TileWidth>
|
||||
<TileHeight>512</TileHeight>
|
||||
<MatrixWidth>64</MatrixWidth>
|
||||
<MatrixHeight>32</MatrixHeight>
|
||||
</TileMatrix>
|
||||
<TileMatrix>
|
||||
<ows:Identifier>6</ows:Identifier>
|
||||
<ScaleDenominator>2183915.0938622</ScaleDenominator>
|
||||
<TopLeftCorner>90 -180</TopLeftCorner>
|
||||
<TileWidth>512</TileWidth>
|
||||
<TileHeight>512</TileHeight>
|
||||
<MatrixWidth>128</MatrixWidth>
|
||||
<MatrixHeight>64</MatrixHeight>
|
||||
</TileMatrix>
|
||||
<TileMatrix>
|
||||
<ows:Identifier>7</ows:Identifier>
|
||||
<ScaleDenominator>1091957.5469311</ScaleDenominator>
|
||||
<TopLeftCorner>90 -180</TopLeftCorner>
|
||||
<TileWidth>512</TileWidth>
|
||||
<TileHeight>512</TileHeight>
|
||||
<MatrixWidth>256</MatrixWidth>
|
||||
<MatrixHeight>128</MatrixHeight>
|
||||
</TileMatrix>
|
||||
<TileMatrix>
|
||||
<ows:Identifier>8</ows:Identifier>
|
||||
<ScaleDenominator>545978.77346554</ScaleDenominator>
|
||||
<TopLeftCorner>90 -180</TopLeftCorner>
|
||||
<TileWidth>512</TileWidth>
|
||||
<TileHeight>512</TileHeight>
|
||||
<MatrixWidth>512</MatrixWidth>
|
||||
<MatrixHeight>256</MatrixHeight>
|
||||
</TileMatrix>
|
||||
<TileMatrix>
|
||||
<ows:Identifier>9</ows:Identifier>
|
||||
<ScaleDenominator>272989.38673277</ScaleDenominator>
|
||||
<TopLeftCorner>90 -180</TopLeftCorner>
|
||||
<TileWidth>512</TileWidth>
|
||||
<TileHeight>512</TileHeight>
|
||||
<MatrixWidth>1024</MatrixWidth>
|
||||
<MatrixHeight>512</MatrixHeight>
|
||||
</TileMatrix>
|
||||
<TileMatrix>
|
||||
<ows:Identifier>10</ows:Identifier>
|
||||
<ScaleDenominator>136494.69336639</ScaleDenominator>
|
||||
<TopLeftCorner>90 -180</TopLeftCorner>
|
||||
<TileWidth>512</TileWidth>
|
||||
<TileHeight>512</TileHeight>
|
||||
<MatrixWidth>2048</MatrixWidth>
|
||||
<MatrixHeight>1024</MatrixHeight>
|
||||
</TileMatrix>
|
||||
<TileMatrix>
|
||||
<ows:Identifier>11</ows:Identifier>
|
||||
<ScaleDenominator>68247.346683193</ScaleDenominator>
|
||||
<TopLeftCorner>90 -180</TopLeftCorner>
|
||||
<TileWidth>512</TileWidth>
|
||||
<TileHeight>512</TileHeight>
|
||||
<MatrixWidth>4096</MatrixWidth>
|
||||
<MatrixHeight>2048</MatrixHeight>
|
||||
</TileMatrix>
|
||||
<TileMatrix>
|
||||
<ows:Identifier>12</ows:Identifier>
|
||||
<ScaleDenominator>34123.673341597</ScaleDenominator>
|
||||
<TopLeftCorner>90 -180</TopLeftCorner>
|
||||
<TileWidth>512</TileWidth>
|
||||
<TileHeight>512</TileHeight>
|
||||
<MatrixWidth>8192</MatrixWidth>
|
||||
<MatrixHeight>4096</MatrixHeight>
|
||||
</TileMatrix>
|
||||
<TileMatrix>
|
||||
<ows:Identifier>13</ows:Identifier>
|
||||
<ScaleDenominator>17061.836670798</ScaleDenominator>
|
||||
<TopLeftCorner>90 -180</TopLeftCorner>
|
||||
<TileWidth>512</TileWidth>
|
||||
<TileHeight>512</TileHeight>
|
||||
<MatrixWidth>16384</MatrixWidth>
|
||||
<MatrixHeight>8192</MatrixHeight>
|
||||
</TileMatrix>
|
||||
<TileMatrix>
|
||||
<ows:Identifier>14</ows:Identifier>
|
||||
<ScaleDenominator>8530.9183353991</ScaleDenominator>
|
||||
<TopLeftCorner>90 -180</TopLeftCorner>
|
||||
<TileWidth>512</TileWidth>
|
||||
<TileHeight>512</TileHeight>
|
||||
<MatrixWidth>32768</MatrixWidth>
|
||||
<MatrixHeight>16384</MatrixHeight>
|
||||
</TileMatrix>
|
||||
<TileMatrix>
|
||||
<ows:Identifier>15</ows:Identifier>
|
||||
<ScaleDenominator>4265.4591676996</ScaleDenominator>
|
||||
<TopLeftCorner>90 -180</TopLeftCorner>
|
||||
<TileWidth>512</TileWidth>
|
||||
<TileHeight>512</TileHeight>
|
||||
<MatrixWidth>65536</MatrixWidth>
|
||||
<MatrixHeight>32768</MatrixHeight>
|
||||
</TileMatrix>
|
||||
<TileMatrix>
|
||||
<ows:Identifier>16</ows:Identifier>
|
||||
<ScaleDenominator>2132.7295838498</ScaleDenominator>
|
||||
<TopLeftCorner>90 -180</TopLeftCorner>
|
||||
<TileWidth>512</TileWidth>
|
||||
<TileHeight>512</TileHeight>
|
||||
<MatrixWidth>131072</MatrixWidth>
|
||||
<MatrixHeight>65536</MatrixHeight>
|
||||
</TileMatrix>
|
||||
<TileMatrix>
|
||||
<ows:Identifier>17</ows:Identifier>
|
||||
<ScaleDenominator>1066.3647919249</ScaleDenominator>
|
||||
<TopLeftCorner>90 -180</TopLeftCorner>
|
||||
<TileWidth>512</TileWidth>
|
||||
<TileHeight>512</TileHeight>
|
||||
<MatrixWidth>262144</MatrixWidth>
|
||||
<MatrixHeight>131072</MatrixHeight>
|
||||
</TileMatrix>
|
||||
<TileMatrix>
|
||||
<ows:Identifier>18</ows:Identifier>
|
||||
<ScaleDenominator>533.182</ScaleDenominator>
|
||||
<TopLeftCorner>90 -180</TopLeftCorner>
|
||||
<TileWidth>512</TileWidth>
|
||||
<TileHeight>512</TileHeight>
|
||||
<MatrixWidth>524288</MatrixWidth>
|
||||
<MatrixHeight>262144</MatrixHeight>
|
||||
</TileMatrix>
|
||||
</TileMatrixSet>
|
||||
</TileMatrix></TileMatrixSet>
|
||||
</Contents>
|
||||
<ServiceMetadataURL xlink:href="{{baseUrl}}styles/{{id}}/wmts.xml"/>
|
||||
<ServiceMetadataURL xlink:href="{{baseUrl}}wmts/{{id}}/"/>
|
||||
</Capabilities>
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import * as http from 'http';
|
||||
const options = {
|
||||
var options = {
|
||||
timeout: 2000,
|
||||
};
|
||||
const url = 'http://localhost:8080/health';
|
||||
const request = http.request(url, options, (res) => {
|
||||
var url = 'http://localhost:8080/health';
|
||||
var request = http.request(url, options, (res) => {
|
||||
console.log(`STATUS: ${res.statusCode}`);
|
||||
if (res.statusCode == 200) {
|
||||
process.exit(0);
|
||||
|
|
212
src/main.js
212
src/main.js
|
@ -1,24 +1,14 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
'use strict';
|
||||
import os from 'os';
|
||||
|
||||
const envSize = parseInt(process.env.UV_THREADPOOL_SIZE, 10);
|
||||
process.env.UV_THREADPOOL_SIZE = Math.ceil(
|
||||
Math.max(4, isNaN(envSize) ? os.cpus().length * 1.5 : envSize),
|
||||
);
|
||||
|
||||
import fs from 'node:fs';
|
||||
import fsp from 'node:fs/promises';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import axios from 'axios';
|
||||
import request from 'request';
|
||||
import { server } from './server.js';
|
||||
import { isValidHttpUrl } from './utils.js';
|
||||
import { openPMtiles, getPMtilesInfo } from './pmtiles_adapter.js';
|
||||
import { program } from 'commander';
|
||||
import { existsP } from './promises.js';
|
||||
import { openMbTilesWrapper } from './mbtiles_wrapper.js';
|
||||
|
||||
import MBTiles from '@mapbox/mbtiles';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
@ -31,18 +21,13 @@ if (args.length >= 3 && args[2][0] !== '-') {
|
|||
args.splice(2, 0, '--mbtiles');
|
||||
}
|
||||
|
||||
import { program } from 'commander';
|
||||
program
|
||||
.description('tileserver-gl startup options')
|
||||
.usage('tileserver-gl [mbtiles] [options]')
|
||||
.option(
|
||||
'--file <file>',
|
||||
'MBTiles or PMTiles file\n' +
|
||||
'\t ignored if the configuration file is also specified',
|
||||
)
|
||||
.option(
|
||||
'--mbtiles <file>',
|
||||
'(DEPRECIATED) MBTiles file\n' +
|
||||
'\t ignored if file is also specified' +
|
||||
'MBTiles file (uses demo configuration);\n' +
|
||||
'\t ignored if the configuration file is also specified',
|
||||
)
|
||||
.option(
|
||||
|
@ -76,8 +61,8 @@ const startServer = (configPath, config) => {
|
|||
publicUrl += '/';
|
||||
}
|
||||
return server({
|
||||
configPath,
|
||||
config,
|
||||
configPath: configPath,
|
||||
config: config,
|
||||
bind: opts.bind,
|
||||
port: opts.port,
|
||||
cors: opts.cors,
|
||||
|
@ -85,31 +70,41 @@ const startServer = (configPath, config) => {
|
|||
silent: opts.silent,
|
||||
logFile: opts.log_file,
|
||||
logFormat: opts.log_format,
|
||||
publicUrl,
|
||||
publicUrl: publicUrl,
|
||||
});
|
||||
};
|
||||
|
||||
const startWithInputFile = async (inputFile) => {
|
||||
console.log(`[INFO] Automatically creating config file for ${inputFile}`);
|
||||
const startWithMBTiles = (mbtilesFile) => {
|
||||
console.log(`[INFO] Automatically creating config file for ${mbtilesFile}`);
|
||||
console.log(`[INFO] Only a basic preview style will be used.`);
|
||||
console.log(
|
||||
`[INFO] See documentation to learn how to create config.json file.`,
|
||||
);
|
||||
|
||||
let inputFilePath;
|
||||
if (isValidHttpUrl(inputFile)) {
|
||||
inputFilePath = process.cwd();
|
||||
} else {
|
||||
inputFile = path.resolve(process.cwd(), inputFile);
|
||||
inputFilePath = path.dirname(inputFile);
|
||||
mbtilesFile = path.resolve(process.cwd(), mbtilesFile);
|
||||
|
||||
const inputFileStats = await fsp.stat(inputFile);
|
||||
if (!inputFileStats.isFile() || inputFileStats.size === 0) {
|
||||
console.log(`ERROR: Not a valid input file: `);
|
||||
const mbtilesStats = fs.statSync(mbtilesFile);
|
||||
if (!mbtilesStats.isFile() || mbtilesStats.size === 0) {
|
||||
console.log(`ERROR: Not valid MBTiles file: ${mbtilesFile}`);
|
||||
process.exit(1);
|
||||
}
|
||||
const instance = new MBTiles(mbtilesFile + '?mode=ro', (err) => {
|
||||
if (err) {
|
||||
console.log('ERROR: Unable to open MBTiles.');
|
||||
console.log(`Make sure ${path.basename(mbtilesFile)} is valid MBTiles.`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
instance.getInfo((err, info) => {
|
||||
if (err || !info) {
|
||||
console.log('ERROR: Metadata missing in the MBTiles.');
|
||||
console.log(
|
||||
`Make sure ${path.basename(mbtilesFile)} is valid MBTiles.`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
const bounds = info.bounds;
|
||||
|
||||
const styleDir = path.resolve(
|
||||
__dirname,
|
||||
'../node_modules/tileserver-gl-styles/',
|
||||
|
@ -121,104 +116,30 @@ const startWithInputFile = async (inputFile) => {
|
|||
root: styleDir,
|
||||
fonts: 'fonts',
|
||||
styles: 'styles',
|
||||
mbtiles: inputFilePath,
|
||||
pmtiles: inputFilePath,
|
||||
mbtiles: path.dirname(mbtilesFile),
|
||||
},
|
||||
},
|
||||
styles: {},
|
||||
data: {},
|
||||
};
|
||||
|
||||
const extension = inputFile.split('.').pop().toLowerCase();
|
||||
if (extension === 'pmtiles') {
|
||||
const fileOpenInfo = openPMtiles(inputFile);
|
||||
const metadata = await getPMtilesInfo(fileOpenInfo);
|
||||
|
||||
if (
|
||||
metadata.format === 'pbf' &&
|
||||
metadata.name.toLowerCase().indexOf('openmaptiles') > -1
|
||||
) {
|
||||
if (isValidHttpUrl(inputFile)) {
|
||||
config['data'][`v3`] = {
|
||||
pmtiles: inputFile,
|
||||
};
|
||||
} else {
|
||||
config['data'][`v3`] = {
|
||||
pmtiles: path.basename(inputFile),
|
||||
};
|
||||
}
|
||||
|
||||
const styles = await fsp.readdir(path.resolve(styleDir, 'styles'));
|
||||
for (const styleName of styles) {
|
||||
const styleFileRel = styleName + '/style.json';
|
||||
const styleFile = path.resolve(styleDir, 'styles', styleFileRel);
|
||||
if (await existsP(styleFile)) {
|
||||
config['styles'][styleName] = {
|
||||
style: styleFileRel,
|
||||
tilejson: {
|
||||
bounds: metadata.bounds,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.log(
|
||||
`WARN: PMTiles not in "openmaptiles" format. Serving raw data only...`,
|
||||
);
|
||||
if (isValidHttpUrl(inputFile)) {
|
||||
config['data'][(metadata.id || 'pmtiles').replace(/[?/:]/g, '_')] = {
|
||||
pmtiles: inputFile,
|
||||
};
|
||||
} else {
|
||||
config['data'][(metadata.id || 'pmtiles').replace(/[?/:]/g, '_')] = {
|
||||
pmtiles: path.basename(inputFile),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (opts.verbose) {
|
||||
console.log(JSON.stringify(config, undefined, 2));
|
||||
} else {
|
||||
console.log('Run with --verbose to see the config file here.');
|
||||
}
|
||||
|
||||
return startServer(null, config);
|
||||
} else {
|
||||
if (isValidHttpUrl(inputFile)) {
|
||||
console.log(
|
||||
`ERROR: MBTiles does not support web based files. "${inputFile}" is not a valid data file.`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
let info;
|
||||
try {
|
||||
const mbw = await openMbTilesWrapper(inputFile);
|
||||
info = await mbw.getInfo();
|
||||
if (!info) throw new Error('Metadata missing in the MBTiles.');
|
||||
} catch (err) {
|
||||
console.log('ERROR: Unable to open MBTiles or read metadata:', err);
|
||||
console.log(`Make sure ${path.basename(inputFile)} is valid MBTiles.`);
|
||||
process.exit(1);
|
||||
}
|
||||
const bounds = info.bounds;
|
||||
|
||||
if (
|
||||
info.format === 'pbf' &&
|
||||
info.name.toLowerCase().indexOf('openmaptiles') > -1
|
||||
) {
|
||||
config['data'][`v3`] = {
|
||||
mbtiles: path.basename(inputFile),
|
||||
mbtiles: path.basename(mbtilesFile),
|
||||
};
|
||||
|
||||
const styles = await fsp.readdir(path.resolve(styleDir, 'styles'));
|
||||
const styles = fs.readdirSync(path.resolve(styleDir, 'styles'));
|
||||
for (const styleName of styles) {
|
||||
const styleFileRel = styleName + '/style.json';
|
||||
const styleFile = path.resolve(styleDir, 'styles', styleFileRel);
|
||||
if (await existsP(styleFile)) {
|
||||
if (fs.existsSync(styleFile)) {
|
||||
config['styles'][styleName] = {
|
||||
style: styleFileRel,
|
||||
tilejson: {
|
||||
bounds,
|
||||
bounds: bounds,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
@ -227,8 +148,13 @@ const startWithInputFile = async (inputFile) => {
|
|||
console.log(
|
||||
`WARN: MBTiles not in "openmaptiles" format. Serving raw data only...`,
|
||||
);
|
||||
config['data'][(info.id || 'mbtiles').replace(/[?/:]/g, '_')] = {
|
||||
mbtiles: path.basename(inputFile),
|
||||
config['data'][
|
||||
(info.id || 'mbtiles')
|
||||
.replace(/\//g, '_')
|
||||
.replace(/:/g, '_')
|
||||
.replace(/\?/g, '_')
|
||||
] = {
|
||||
mbtiles: path.basename(mbtilesFile),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -239,59 +165,41 @@ const startWithInputFile = async (inputFile) => {
|
|||
}
|
||||
|
||||
return startServer(null, config);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
fs.stat(path.resolve(opts.config), async (err, stats) => {
|
||||
fs.stat(path.resolve(opts.config), (err, stats) => {
|
||||
if (err || !stats.isFile() || stats.size === 0) {
|
||||
let inputFile;
|
||||
if (opts.file) {
|
||||
inputFile = opts.file;
|
||||
} else if (opts.mbtiles) {
|
||||
inputFile = opts.mbtiles;
|
||||
}
|
||||
|
||||
if (inputFile) {
|
||||
return startWithInputFile(inputFile);
|
||||
} else {
|
||||
let mbtiles = opts.mbtiles;
|
||||
if (!mbtiles) {
|
||||
// try to find in the cwd
|
||||
const files = await fsp.readdir(process.cwd());
|
||||
const files = fs.readdirSync(process.cwd());
|
||||
for (const filename of files) {
|
||||
if (filename.endsWith('.mbtiles') || filename.endsWith('.pmtiles')) {
|
||||
const inputFilesStats = await fsp.stat(filename);
|
||||
if (inputFilesStats.isFile() && inputFilesStats.size > 0) {
|
||||
inputFile = filename;
|
||||
if (filename.endsWith('.mbtiles')) {
|
||||
const mbTilesStats = fs.statSync(filename);
|
||||
if (mbTilesStats.isFile() && mbTilesStats.size > 0) {
|
||||
mbtiles = filename;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (inputFile) {
|
||||
console.log(`No input file specified, using ${inputFile}`);
|
||||
return startWithInputFile(inputFile);
|
||||
if (mbtiles) {
|
||||
console.log(`No MBTiles specified, using ${mbtiles}`);
|
||||
return startWithMBTiles(mbtiles);
|
||||
} else {
|
||||
const url =
|
||||
'https://github.com/maptiler/tileserver-gl/releases/download/v1.3.0/zurich_switzerland.mbtiles';
|
||||
const filename = 'zurich_switzerland.mbtiles';
|
||||
const writer = fs.createWriteStream(filename);
|
||||
console.log(`No input file found`);
|
||||
const stream = fs.createWriteStream(filename);
|
||||
console.log(`No MBTiles found`);
|
||||
console.log(`[DEMO] Downloading sample data (${filename}) from ${url}`);
|
||||
|
||||
try {
|
||||
const response = await axios({
|
||||
url,
|
||||
method: 'GET',
|
||||
responseType: 'stream',
|
||||
});
|
||||
|
||||
response.data.pipe(writer);
|
||||
writer.on('finish', () => startWithInputFile(filename));
|
||||
writer.on('error', (err) =>
|
||||
console.error(`Error writing file: ${err}`),
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(`Error downloading file: ${error}`);
|
||||
stream.on('finish', () => startWithMBTiles(filename));
|
||||
return request.get(url).pipe(stream);
|
||||
}
|
||||
}
|
||||
if (mbtiles) {
|
||||
return startWithMBTiles(mbtiles);
|
||||
}
|
||||
} else {
|
||||
console.log(`Using specified config file from ${opts.config}`);
|
||||
|
|
|
@ -1,46 +0,0 @@
|
|||
import MBTiles from '@mapbox/mbtiles';
|
||||
import util from 'node:util';
|
||||
|
||||
/**
|
||||
* Promise-ful wrapper around the MBTiles class.
|
||||
*/
|
||||
class MBTilesWrapper {
|
||||
constructor(mbtiles) {
|
||||
this._mbtiles = mbtiles;
|
||||
this._getInfoP = util.promisify(mbtiles.getInfo.bind(mbtiles));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the underlying MBTiles object.
|
||||
* @returns {MBTiles}
|
||||
*/
|
||||
getMbTiles() {
|
||||
return this._mbtiles;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the MBTiles metadata object.
|
||||
* @returns {Promise<object>}
|
||||
*/
|
||||
getInfo() {
|
||||
return this._getInfoP();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the given MBTiles file and return a promise that resolves with a
|
||||
* MBTilesWrapper instance.
|
||||
* @param inputFile Input file
|
||||
* @returns {Promise<MBTilesWrapper>}
|
||||
*/
|
||||
export function openMbTilesWrapper(inputFile) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const mbtiles = new MBTiles(inputFile + '?mode=ro', (err) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
resolve(new MBTilesWrapper(mbtiles));
|
||||
});
|
||||
});
|
||||
}
|
|
@ -1,151 +0,0 @@
|
|||
import fs from 'node:fs';
|
||||
import { PMTiles, FetchSource } from 'pmtiles';
|
||||
import { isValidHttpUrl } from './utils.js';
|
||||
|
||||
class PMTilesFileSource {
|
||||
constructor(fd) {
|
||||
this.fd = fd;
|
||||
}
|
||||
getKey() {
|
||||
return this.fd;
|
||||
}
|
||||
async getBytes(offset, length) {
|
||||
const buffer = Buffer.alloc(length);
|
||||
await readFileBytes(this.fd, buffer, offset);
|
||||
const ab = buffer.buffer.slice(
|
||||
buffer.byteOffset,
|
||||
buffer.byteOffset + buffer.byteLength,
|
||||
);
|
||||
return { data: ab };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param fd
|
||||
* @param buffer
|
||||
* @param offset
|
||||
*/
|
||||
async function readFileBytes(fd, buffer, offset) {
|
||||
return new Promise((resolve, reject) => {
|
||||
fs.read(fd, buffer, 0, buffer.length, offset, (err) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param FilePath
|
||||
*/
|
||||
export function openPMtiles(FilePath) {
|
||||
let pmtiles = undefined;
|
||||
|
||||
if (isValidHttpUrl(FilePath)) {
|
||||
const source = new FetchSource(FilePath);
|
||||
pmtiles = new PMTiles(source);
|
||||
} else {
|
||||
const fd = fs.openSync(FilePath, 'r');
|
||||
const source = new PMTilesFileSource(fd);
|
||||
pmtiles = new PMTiles(source);
|
||||
}
|
||||
return pmtiles;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param pmtiles
|
||||
*/
|
||||
export async function getPMtilesInfo(pmtiles) {
|
||||
const header = await pmtiles.getHeader();
|
||||
const metadata = await pmtiles.getMetadata();
|
||||
|
||||
//Add missing metadata from header
|
||||
metadata['format'] = getPmtilesTileType(header.tileType).type;
|
||||
metadata['minzoom'] = header.minZoom;
|
||||
metadata['maxzoom'] = header.maxZoom;
|
||||
|
||||
if (header.minLon && header.minLat && header.maxLon && header.maxLat) {
|
||||
metadata['bounds'] = [
|
||||
header.minLon,
|
||||
header.minLat,
|
||||
header.maxLon,
|
||||
header.maxLat,
|
||||
];
|
||||
} else {
|
||||
metadata['bounds'] = [-180, -85.05112877980659, 180, 85.0511287798066];
|
||||
}
|
||||
|
||||
if (header.centerZoom) {
|
||||
metadata['center'] = [
|
||||
header.centerLon,
|
||||
header.centerLat,
|
||||
header.centerZoom,
|
||||
];
|
||||
} else {
|
||||
metadata['center'] = [
|
||||
header.centerLon,
|
||||
header.centerLat,
|
||||
parseInt(metadata['maxzoom']) / 2,
|
||||
];
|
||||
}
|
||||
|
||||
return metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param pmtiles
|
||||
* @param z
|
||||
* @param x
|
||||
* @param y
|
||||
*/
|
||||
export async function getPMtilesTile(pmtiles, z, x, y) {
|
||||
const header = await pmtiles.getHeader();
|
||||
const tileType = getPmtilesTileType(header.tileType);
|
||||
let zxyTile = await pmtiles.getZxy(z, x, y);
|
||||
if (zxyTile && zxyTile.data) {
|
||||
zxyTile = Buffer.from(zxyTile.data);
|
||||
} else {
|
||||
zxyTile = undefined;
|
||||
}
|
||||
return { data: zxyTile, header: tileType.header };
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param typenum
|
||||
*/
|
||||
function getPmtilesTileType(typenum) {
|
||||
let head = {};
|
||||
let tileType;
|
||||
switch (typenum) {
|
||||
case 0:
|
||||
tileType = 'Unknown';
|
||||
break;
|
||||
case 1:
|
||||
tileType = 'pbf';
|
||||
head['Content-Type'] = 'application/x-protobuf';
|
||||
break;
|
||||
case 2:
|
||||
tileType = 'png';
|
||||
head['Content-Type'] = 'image/png';
|
||||
break;
|
||||
case 3:
|
||||
tileType = 'jpeg';
|
||||
head['Content-Type'] = 'image/jpeg';
|
||||
break;
|
||||
case 4:
|
||||
tileType = 'webp';
|
||||
head['Content-Type'] = 'image/webp';
|
||||
break;
|
||||
case 5:
|
||||
tileType = 'avif';
|
||||
head['Content-Type'] = 'image/avif';
|
||||
break;
|
||||
}
|
||||
return { type: tileType, header: head };
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
import util from 'node:util';
|
||||
import fsp from 'node:fs/promises';
|
||||
import zlib from 'zlib';
|
||||
|
||||
export const gzipP = util.promisify(zlib.gzip);
|
||||
export const gunzipP = util.promisify(zlib.gunzip);
|
||||
export const existsP = async (path) => {
|
||||
try {
|
||||
await fsp.access(path); // Defaults to F_OK: indicating that the file is visible to the calling process
|
||||
return true;
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
};
|
303
src/render.js
303
src/render.js
|
@ -1,303 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
import { createCanvas, Image } from 'canvas';
|
||||
|
||||
import SphericalMercator from '@mapbox/sphericalmercator';
|
||||
|
||||
const mercator = new SphericalMercator();
|
||||
|
||||
/**
|
||||
* Transforms coordinates to pixels.
|
||||
* @param {List[Number]} ll Longitude/Latitude coordinate pair.
|
||||
* @param {number} zoom Map zoom level.
|
||||
*/
|
||||
const precisePx = (ll, zoom) => {
|
||||
const px = mercator.px(ll, 20);
|
||||
const scale = Math.pow(2, zoom - 20);
|
||||
return [px[0] * scale, px[1] * scale];
|
||||
};
|
||||
|
||||
/**
|
||||
* Draws a marker in canvas context.
|
||||
* @param {object} ctx Canvas context object.
|
||||
* @param {object} marker Marker object parsed by extractMarkersFromQuery.
|
||||
* @param {number} z Map zoom level.
|
||||
*/
|
||||
const drawMarker = (ctx, marker, z) => {
|
||||
return new Promise((resolve) => {
|
||||
const img = new Image();
|
||||
const pixelCoords = precisePx(marker.location, z);
|
||||
|
||||
const getMarkerCoordinates = (imageWidth, imageHeight, scale) => {
|
||||
// Images are placed with their top-left corner at the provided location
|
||||
// within the canvas but we expect icons to be centered and above it.
|
||||
|
||||
// Substract half of the images width from the x-coordinate to center
|
||||
// the image in relation to the provided location
|
||||
let xCoordinate = pixelCoords[0] - imageWidth / 2;
|
||||
// Substract the images height from the y-coordinate to place it above
|
||||
// the provided location
|
||||
let yCoordinate = pixelCoords[1] - imageHeight;
|
||||
|
||||
// Since image placement is dependent on the size offsets have to be
|
||||
// scaled as well. Additionally offsets are provided as either positive or
|
||||
// negative values so we always add them
|
||||
if (marker.offsetX) {
|
||||
xCoordinate = xCoordinate + marker.offsetX * scale;
|
||||
}
|
||||
if (marker.offsetY) {
|
||||
yCoordinate = yCoordinate + marker.offsetY * scale;
|
||||
}
|
||||
|
||||
return {
|
||||
x: xCoordinate,
|
||||
y: yCoordinate,
|
||||
};
|
||||
};
|
||||
|
||||
const drawOnCanvas = () => {
|
||||
// Check if the images should be resized before beeing drawn
|
||||
const defaultScale = 1;
|
||||
const scale = marker.scale ? marker.scale : defaultScale;
|
||||
|
||||
// Calculate scaled image sizes
|
||||
const imageWidth = img.width * scale;
|
||||
const imageHeight = img.height * scale;
|
||||
|
||||
// Pass the desired sizes to get correlating coordinates
|
||||
const coords = getMarkerCoordinates(imageWidth, imageHeight, scale);
|
||||
|
||||
// Draw the image on canvas
|
||||
if (scale != defaultScale) {
|
||||
ctx.drawImage(img, coords.x, coords.y, imageWidth, imageHeight);
|
||||
} else {
|
||||
ctx.drawImage(img, coords.x, coords.y);
|
||||
}
|
||||
// Resolve the promise when image has been drawn
|
||||
resolve();
|
||||
};
|
||||
|
||||
img.onload = drawOnCanvas;
|
||||
img.onerror = (err) => {
|
||||
throw err;
|
||||
};
|
||||
img.src = marker.icon;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Draws a list of markers onto a canvas.
|
||||
* Wraps drawing of markers into list of promises and awaits them.
|
||||
* It's required because images are expected to load asynchronous in canvas js
|
||||
* even when provided from a local disk.
|
||||
* @param {object} ctx Canvas context object.
|
||||
* @param {List[Object]} markers Marker objects parsed by extractMarkersFromQuery.
|
||||
* @param {number} z Map zoom level.
|
||||
*/
|
||||
const drawMarkers = async (ctx, markers, z) => {
|
||||
const markerPromises = [];
|
||||
|
||||
for (const marker of markers) {
|
||||
// Begin drawing marker
|
||||
markerPromises.push(drawMarker(ctx, marker, z));
|
||||
}
|
||||
|
||||
// Await marker drawings before continuing
|
||||
await Promise.all(markerPromises);
|
||||
};
|
||||
|
||||
/**
|
||||
* Draws a list of coordinates onto a canvas and styles the resulting path.
|
||||
* @param {object} ctx Canvas context object.
|
||||
* @param {List[Number]} path List of coordinates.
|
||||
* @param {object} query Request query parameters.
|
||||
* @param {string} pathQuery Path query parameter.
|
||||
* @param {number} z Map zoom level.
|
||||
*/
|
||||
const drawPath = (ctx, path, query, pathQuery, z) => {
|
||||
const splitPaths = pathQuery.split('|');
|
||||
|
||||
if (!path || path.length < 2) {
|
||||
return null;
|
||||
}
|
||||
|
||||
ctx.beginPath();
|
||||
|
||||
// Transform coordinates to pixel on canvas and draw lines between points
|
||||
for (const pair of path) {
|
||||
const px = precisePx(pair, z);
|
||||
ctx.lineTo(px[0], px[1]);
|
||||
}
|
||||
|
||||
// Check if first coordinate matches last coordinate
|
||||
if (
|
||||
path[0][0] === path[path.length - 1][0] &&
|
||||
path[0][1] === path[path.length - 1][1]
|
||||
) {
|
||||
ctx.closePath();
|
||||
}
|
||||
|
||||
// Optionally fill drawn shape with a rgba color from query
|
||||
const pathHasFill = splitPaths.filter((x) => x.startsWith('fill')).length > 0;
|
||||
if (query.fill !== undefined || pathHasFill) {
|
||||
if ('fill' in query) {
|
||||
ctx.fillStyle = query.fill || 'rgba(255,255,255,0.4)';
|
||||
}
|
||||
if (pathHasFill) {
|
||||
ctx.fillStyle = splitPaths
|
||||
.find((x) => x.startsWith('fill:'))
|
||||
.replace('fill:', '');
|
||||
}
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
// Get line width from query and fall back to 1 if not provided
|
||||
const pathHasWidth =
|
||||
splitPaths.filter((x) => x.startsWith('width')).length > 0;
|
||||
if (query.width !== undefined || pathHasWidth) {
|
||||
let lineWidth = 1;
|
||||
// Get line width from query
|
||||
if ('width' in query) {
|
||||
lineWidth = Number(query.width);
|
||||
}
|
||||
// Get line width from path in query
|
||||
if (pathHasWidth) {
|
||||
lineWidth = Number(
|
||||
splitPaths.find((x) => x.startsWith('width:')).replace('width:', ''),
|
||||
);
|
||||
}
|
||||
// Get border width from query and fall back to 10% of line width
|
||||
const borderWidth =
|
||||
query.borderwidth !== undefined
|
||||
? parseFloat(query.borderwidth)
|
||||
: lineWidth * 0.1;
|
||||
|
||||
// Set rendering style for the start and end points of the path
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineCap
|
||||
ctx.lineCap = query.linecap || 'butt';
|
||||
|
||||
// Set rendering style for overlapping segments of the path with differing directions
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineJoin
|
||||
ctx.lineJoin = query.linejoin || 'miter';
|
||||
|
||||
// In order to simulate a border we draw the path two times with the first
|
||||
// beeing the wider border part.
|
||||
if (query.border !== undefined && borderWidth > 0) {
|
||||
// We need to double the desired border width and add it to the line width
|
||||
// in order to get the desired border on each side of the line.
|
||||
ctx.lineWidth = lineWidth + borderWidth * 2;
|
||||
// Set border style as rgba
|
||||
ctx.strokeStyle = query.border;
|
||||
ctx.stroke();
|
||||
}
|
||||
ctx.lineWidth = lineWidth;
|
||||
}
|
||||
|
||||
const pathHasStroke =
|
||||
splitPaths.filter((x) => x.startsWith('stroke')).length > 0;
|
||||
if (query.stroke !== undefined || pathHasStroke) {
|
||||
if ('stroke' in query) {
|
||||
ctx.strokeStyle = query.stroke;
|
||||
}
|
||||
// Path Stroke gets higher priority
|
||||
if (pathHasStroke) {
|
||||
ctx.strokeStyle = splitPaths
|
||||
.find((x) => x.startsWith('stroke:'))
|
||||
.replace('stroke:', '');
|
||||
}
|
||||
} else {
|
||||
ctx.strokeStyle = 'rgba(0,64,255,0.7)';
|
||||
}
|
||||
ctx.stroke();
|
||||
};
|
||||
|
||||
export const renderOverlay = async (
|
||||
z,
|
||||
x,
|
||||
y,
|
||||
bearing,
|
||||
pitch,
|
||||
w,
|
||||
h,
|
||||
scale,
|
||||
paths,
|
||||
markers,
|
||||
query,
|
||||
) => {
|
||||
if ((!paths || paths.length === 0) && (!markers || markers.length === 0)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const center = precisePx([x, y], z);
|
||||
|
||||
const mapHeight = 512 * (1 << z);
|
||||
const maxEdge = center[1] + h / 2;
|
||||
const minEdge = center[1] - h / 2;
|
||||
if (maxEdge > mapHeight) {
|
||||
center[1] -= maxEdge - mapHeight;
|
||||
} else if (minEdge < 0) {
|
||||
center[1] -= minEdge;
|
||||
}
|
||||
|
||||
const canvas = createCanvas(scale * w, scale * h);
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.scale(scale, scale);
|
||||
if (bearing) {
|
||||
ctx.translate(w / 2, h / 2);
|
||||
ctx.rotate((-bearing / 180) * Math.PI);
|
||||
ctx.translate(-center[0], -center[1]);
|
||||
} else {
|
||||
// optimized path
|
||||
ctx.translate(-center[0] + w / 2, -center[1] + h / 2);
|
||||
}
|
||||
|
||||
// Draw provided paths if any
|
||||
paths.forEach((path, i) => {
|
||||
const pathQuery = Array.isArray(query.path) ? query.path.at(i) : query.path;
|
||||
drawPath(ctx, path, query, pathQuery, z);
|
||||
});
|
||||
|
||||
// Await drawing of markers before rendering the canvas
|
||||
await drawMarkers(ctx, markers, z);
|
||||
|
||||
return canvas.toBuffer();
|
||||
};
|
||||
|
||||
export const renderWatermark = (width, height, scale, text) => {
|
||||
const canvas = createCanvas(scale * width, scale * height);
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.scale(scale, scale);
|
||||
|
||||
ctx.font = '10px sans-serif';
|
||||
ctx.strokeWidth = '1px';
|
||||
ctx.strokeStyle = 'rgba(255,255,255,.4)';
|
||||
ctx.strokeText(text, 5, height - 5);
|
||||
ctx.fillStyle = 'rgba(0,0,0,.4)';
|
||||
ctx.fillText(text, 5, height - 5);
|
||||
|
||||
return canvas;
|
||||
};
|
||||
|
||||
export const renderAttribution = (width, height, scale, text) => {
|
||||
const canvas = createCanvas(scale * width, scale * height);
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.scale(scale, scale);
|
||||
|
||||
ctx.font = '10px sans-serif';
|
||||
const textMetrics = ctx.measureText(text);
|
||||
const textWidth = textMetrics.width;
|
||||
const textHeight = 14;
|
||||
|
||||
const padding = 6;
|
||||
ctx.fillStyle = 'rgba(255, 255, 255, 0.8)';
|
||||
ctx.fillRect(
|
||||
width - textWidth - padding,
|
||||
height - textHeight - padding,
|
||||
textWidth + padding,
|
||||
textHeight + padding,
|
||||
);
|
||||
ctx.fillStyle = 'rgba(0,0,0,.8)';
|
||||
ctx.fillText(text, width - textWidth - padding / 2, height - textHeight + 8);
|
||||
|
||||
return canvas;
|
||||
};
|
|
@ -1,85 +1,32 @@
|
|||
'use strict';
|
||||
|
||||
import fsp from 'node:fs/promises';
|
||||
import fs from 'node:fs';
|
||||
import path from 'path';
|
||||
import zlib from 'zlib';
|
||||
|
||||
import clone from 'clone';
|
||||
import express from 'express';
|
||||
import MBTiles from '@mapbox/mbtiles';
|
||||
import Pbf from 'pbf';
|
||||
import { VectorTile } from '@mapbox/vector-tile';
|
||||
import SphericalMercator from '@mapbox/sphericalmercator';
|
||||
|
||||
import {
|
||||
fixTileJSONCenter,
|
||||
getTileUrls,
|
||||
isValidHttpUrl,
|
||||
fetchTileData,
|
||||
} from './utils.js';
|
||||
import { getPMtilesInfo, openPMtiles } from './pmtiles_adapter.js';
|
||||
import { gunzipP, gzipP } from './promises.js';
|
||||
import { openMbTilesWrapper } from './mbtiles_wrapper.js';
|
||||
|
||||
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;
|
||||
import { getTileUrls, fixTileJSONCenter } from './utils.js';
|
||||
|
||||
export const serve_data = {
|
||||
/**
|
||||
* Initializes the serve_data module.
|
||||
* @param {object} options Configuration options.
|
||||
* @param {object} repo Repository object.
|
||||
* @param {object} programOpts - An object containing the program options
|
||||
* @returns {express.Application} The initialized Express application.
|
||||
*/
|
||||
init: function (options, repo, programOpts) {
|
||||
const { verbose } = programOpts;
|
||||
init: (options, repo) => {
|
||||
const app = express().disable('x-powered-by');
|
||||
|
||||
/**
|
||||
* Handles requests for tile data, responding with the tile image.
|
||||
* @param {object} req - Express request object.
|
||||
* @param {object} res - Express response object.
|
||||
* @param {string} req.params.id - ID of the tile.
|
||||
* @param {string} req.params.z - Z coordinate of the tile.
|
||||
* @param {string} req.params.x - X coordinate of the tile.
|
||||
* @param {string} req.params.y - Y coordinate of the tile.
|
||||
* @param {string} req.params.format - Format of the tile.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
app.get('/:id/:z/:x/:y.:format', async (req, res) => {
|
||||
if (verbose) {
|
||||
console.log(
|
||||
`Handling tile request for: /data/%s/%s/%s/%s.%s`,
|
||||
String(req.params.id).replace(/\n|\r/g, ''),
|
||||
String(req.params.z).replace(/\n|\r/g, ''),
|
||||
String(req.params.x).replace(/\n|\r/g, ''),
|
||||
String(req.params.y).replace(/\n|\r/g, ''),
|
||||
String(req.params.format).replace(/\n|\r/g, ''),
|
||||
);
|
||||
}
|
||||
app.get(
|
||||
'/:id/:z(\\d+)/:x(\\d+)/:y(\\d+).:format([\\w.]+)',
|
||||
(req, res, next) => {
|
||||
const item = repo[req.params.id];
|
||||
if (!item) {
|
||||
return res.sendStatus(404);
|
||||
}
|
||||
const tileJSONFormat = item.tileJSON.format;
|
||||
const z = parseInt(req.params.z, 10);
|
||||
const x = parseInt(req.params.x, 10);
|
||||
const y = parseInt(req.params.y, 10);
|
||||
if (isNaN(z) || isNaN(x) || isNaN(y)) {
|
||||
return res.status(404).send('Invalid Tile');
|
||||
}
|
||||
|
||||
const z = req.params.z | 0;
|
||||
const x = req.params.x | 0;
|
||||
const y = req.params.y | 0;
|
||||
let format = req.params.format;
|
||||
if (format === options.pbfAlias) {
|
||||
format = 'pbf';
|
||||
|
@ -92,6 +39,7 @@ export const serve_data = {
|
|||
}
|
||||
if (
|
||||
z < item.tileJSON.minzoom ||
|
||||
0 ||
|
||||
x < 0 ||
|
||||
y < 0 ||
|
||||
z > item.tileJSON.maxzoom ||
|
||||
|
@ -100,42 +48,42 @@ export const serve_data = {
|
|||
) {
|
||||
return res.status(404).send('Out of bounds');
|
||||
}
|
||||
|
||||
const fetchTile = await fetchTileData(
|
||||
item.source,
|
||||
item.sourceType,
|
||||
z,
|
||||
x,
|
||||
y,
|
||||
);
|
||||
if (fetchTile == null) return res.status(204).send();
|
||||
|
||||
let data = fetchTile.data;
|
||||
let headers = fetchTile.headers;
|
||||
let isGzipped = data.slice(0, 2).indexOf(Buffer.from([0x1f, 0x8b])) === 0;
|
||||
|
||||
item.source.getTile(z, x, y, (err, data, headers) => {
|
||||
let isGzipped;
|
||||
if (err) {
|
||||
if (/does not exist/.test(err.message)) {
|
||||
return res.status(204).send();
|
||||
} else {
|
||||
return res
|
||||
.status(500)
|
||||
.header('Content-Type', 'text/plain')
|
||||
.send(err.message);
|
||||
}
|
||||
} else {
|
||||
if (data == null) {
|
||||
return res.status(404).send('Not found');
|
||||
} else {
|
||||
if (tileJSONFormat === 'pbf') {
|
||||
isGzipped =
|
||||
data.slice(0, 2).indexOf(Buffer.from([0x1f, 0x8b])) === 0;
|
||||
if (options.dataDecoratorFunc) {
|
||||
if (isGzipped) {
|
||||
data = await gunzipP(data);
|
||||
data = zlib.unzipSync(data);
|
||||
isGzipped = false;
|
||||
}
|
||||
|
||||
if (tileJSONFormat === 'pbf') {
|
||||
if (options.dataDecoratorFunc) {
|
||||
data = options.dataDecoratorFunc(
|
||||
req.params.id,
|
||||
'data',
|
||||
data,
|
||||
z,
|
||||
x,
|
||||
y,
|
||||
);
|
||||
data = options.dataDecoratorFunc(id, 'data', data, z, x, y);
|
||||
}
|
||||
}
|
||||
|
||||
if (format === 'pbf') {
|
||||
headers['Content-Type'] = 'application/x-protobuf';
|
||||
} else if (format === 'geojson') {
|
||||
headers['Content-Type'] = 'application/json';
|
||||
|
||||
if (isGzipped) {
|
||||
data = zlib.unzipSync(data);
|
||||
isGzipped = false;
|
||||
}
|
||||
|
||||
const tile = new VectorTile(new Pbf(data));
|
||||
const geojson = {
|
||||
type: 'FeatureCollection',
|
||||
|
@ -152,161 +100,32 @@ export const serve_data = {
|
|||
}
|
||||
data = JSON.stringify(geojson);
|
||||
}
|
||||
if (headers) {
|
||||
delete headers['ETag'];
|
||||
}
|
||||
delete headers['ETag']; // do not trust the tile ETag -- regenerate
|
||||
headers['Content-Encoding'] = 'gzip';
|
||||
res.set(headers);
|
||||
|
||||
if (!isGzipped) {
|
||||
data = await gzipP(data);
|
||||
data = zlib.gzipSync(data);
|
||||
isGzipped = true;
|
||||
}
|
||||
|
||||
return res.status(200).send(data);
|
||||
});
|
||||
|
||||
/**
|
||||
* Handles requests for elevation data.
|
||||
* @param {object} req - Express request object.
|
||||
* @param {object} res - Express response object.
|
||||
* @param {string} req.params.id - ID of the elevation data.
|
||||
* @param {string} req.params.z - Z coordinate of the tile.
|
||||
* @param {string} req.params.x - X coordinate of the tile (either integer or float).
|
||||
* @param {string} req.params.y - Y coordinate of the tile (either integer or float).
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
app.get('/:id/elevation/:z/:x/:y', async (req, res, next) => {
|
||||
try {
|
||||
if (verbose) {
|
||||
console.log(
|
||||
`Handling elevation request for: /data/%s/elevation/%s/%s/%s`,
|
||||
String(req.params.id).replace(/\n|\r/g, ''),
|
||||
String(req.params.z).replace(/\n|\r/g, ''),
|
||||
String(req.params.x).replace(/\n|\r/g, ''),
|
||||
String(req.params.y).replace(/\n|\r/g, ''),
|
||||
);
|
||||
}
|
||||
const item = repo?.[req.params.id];
|
||||
if (!item) return res.sendStatus(404);
|
||||
if (!item.source) return res.status(404).send('Missing source');
|
||||
if (!item.tileJSON) return res.status(404).send('Missing tileJSON');
|
||||
if (!item.sourceType) return res.status(404).send('Missing sourceType');
|
||||
const { source, tileJSON, sourceType } = item;
|
||||
if (sourceType !== 'pmtiles' && sourceType !== 'mbtiles') {
|
||||
return res
|
||||
.status(400)
|
||||
.send('Invalid sourceType. Must be pmtiles or mbtiles.');
|
||||
}
|
||||
const encoding = tileJSON?.encoding;
|
||||
if (encoding == null) {
|
||||
return res.status(400).send('Missing tileJSON.encoding');
|
||||
} else if (encoding !== 'terrarium' && encoding !== 'mapbox') {
|
||||
return res
|
||||
.status(400)
|
||||
.send('Invalid encoding. Must be terrarium or mapbox.');
|
||||
}
|
||||
const format = tileJSON?.format;
|
||||
if (format == null) {
|
||||
return res.status(400).send('Missing tileJSON.format');
|
||||
} else if (format !== 'webp' && format !== 'png') {
|
||||
return res.status(400).send('Invalid format. Must be webp or png.');
|
||||
}
|
||||
const z = parseInt(req.params.z, 10);
|
||||
const x = parseFloat(req.params.x);
|
||||
const y = parseFloat(req.params.y);
|
||||
if (tileJSON.minzoom == null || tileJSON.maxzoom == null) {
|
||||
return res.status(404).send(JSON.stringify(tileJSON));
|
||||
}
|
||||
const TILE_SIZE = tileJSON.tileSize || 512;
|
||||
let bbox;
|
||||
let xy;
|
||||
var zoom = z;
|
||||
|
||||
if (Number.isInteger(x) && Number.isInteger(y)) {
|
||||
const intX = parseInt(req.params.x, 10);
|
||||
const intY = parseInt(req.params.y, 10);
|
||||
if (
|
||||
zoom < tileJSON.minzoom ||
|
||||
zoom > tileJSON.maxzoom ||
|
||||
intX < 0 ||
|
||||
intY < 0 ||
|
||||
intX >= Math.pow(2, zoom) ||
|
||||
intY >= Math.pow(2, zoom)
|
||||
) {
|
||||
return res.status(404).send('Out of bounds');
|
||||
}
|
||||
xy = [intX, intY];
|
||||
bbox = new SphericalMercator().bbox(intX, intY, zoom);
|
||||
} else {
|
||||
//no zoom limit with coordinates
|
||||
if (zoom < tileJSON.minzoom) {
|
||||
zoom = tileJSON.minzoom;
|
||||
}
|
||||
if (zoom > tileJSON.maxzoom) {
|
||||
zoom = tileJSON.maxzoom;
|
||||
}
|
||||
bbox = [x, y, x + 0.1, y + 0.1];
|
||||
const { minX, minY } = new SphericalMercator().xyz(bbox, zoom);
|
||||
xy = [minX, minY];
|
||||
}
|
||||
|
||||
const fetchTile = await fetchTileData(
|
||||
source,
|
||||
sourceType,
|
||||
zoom,
|
||||
xy[0],
|
||||
xy[1],
|
||||
);
|
||||
if (fetchTile == null) return res.status(204).send();
|
||||
|
||||
let data = fetchTile.data;
|
||||
var param = {
|
||||
long: bbox[0].toFixed(7),
|
||||
lat: bbox[1].toFixed(7),
|
||||
encoding,
|
||||
format,
|
||||
tile_size: TILE_SIZE,
|
||||
z: zoom,
|
||||
x: xy[0],
|
||||
y: xy[1],
|
||||
};
|
||||
|
||||
res
|
||||
.status(200)
|
||||
.send(await serve_rendered.getTerrainElevation(data, param));
|
||||
} catch (err) {
|
||||
return res
|
||||
.status(500)
|
||||
.header('Content-Type', 'text/plain')
|
||||
.send(err.message);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Handles requests for tilejson for the data tiles.
|
||||
* @param {object} req - Express request object.
|
||||
* @param {object} res - Express response object.
|
||||
* @param {string} req.params.id - ID of the data source.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
app.get('/:id.json', (req, res) => {
|
||||
if (verbose) {
|
||||
console.log(
|
||||
`Handling tilejson request for: /data/%s.json`,
|
||||
String(req.params.id).replace(/\n|\r/g, ''),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
app.get('/:id.json', (req, res, next) => {
|
||||
const item = repo[req.params.id];
|
||||
if (!item) {
|
||||
return res.sendStatus(404);
|
||||
}
|
||||
const tileSize = undefined;
|
||||
const info = clone(item.tileJSON);
|
||||
info.tiles = getTileUrls(
|
||||
req,
|
||||
info.tiles,
|
||||
`data/${req.params.id}`,
|
||||
tileSize,
|
||||
info.format,
|
||||
item.publicUrl,
|
||||
{
|
||||
|
@ -318,83 +137,28 @@ export const serve_data = {
|
|||
|
||||
return app;
|
||||
},
|
||||
/**
|
||||
* Adds a new data source to the repository.
|
||||
* @param {object} options Configuration options.
|
||||
* @param {object} repo Repository object.
|
||||
* @param {object} params Parameters object.
|
||||
* @param {string} id ID of the data source.
|
||||
* @param {object} programOpts - An object containing the program options
|
||||
* @param {string} programOpts.publicUrl Public URL for the data.
|
||||
* @param {boolean} programOpts.verbose Whether verbose logging should be used.
|
||||
* @param {Function} dataResolver Function to resolve data.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
add: async function (options, repo, params, id, programOpts) {
|
||||
const { publicUrl } = programOpts;
|
||||
let inputFile;
|
||||
let inputType;
|
||||
if (params.pmtiles) {
|
||||
inputType = 'pmtiles';
|
||||
if (isValidHttpUrl(params.pmtiles)) {
|
||||
inputFile = params.pmtiles;
|
||||
} else {
|
||||
inputFile = path.resolve(options.paths.pmtiles, params.pmtiles);
|
||||
}
|
||||
} else if (params.mbtiles) {
|
||||
inputType = 'mbtiles';
|
||||
if (isValidHttpUrl(params.mbtiles)) {
|
||||
console.log(
|
||||
`ERROR: MBTiles does not support web based files. "${params.mbtiles}" is not a valid data file.`,
|
||||
);
|
||||
process.exit(1);
|
||||
} else {
|
||||
inputFile = path.resolve(options.paths.mbtiles, params.mbtiles);
|
||||
}
|
||||
}
|
||||
|
||||
add: (options, repo, params, id, publicUrl) => {
|
||||
const mbtilesFile = path.resolve(options.paths.mbtiles, params.mbtiles);
|
||||
let tileJSON = {
|
||||
tiles: params.domains || options.domains,
|
||||
};
|
||||
|
||||
if (!isValidHttpUrl(inputFile)) {
|
||||
const inputFileStats = await fsp.stat(inputFile);
|
||||
if (!inputFileStats.isFile() || inputFileStats.size === 0) {
|
||||
throw Error(`Not valid input file: "${inputFile}"`);
|
||||
const mbtilesFileStats = fs.statSync(mbtilesFile);
|
||||
if (!mbtilesFileStats.isFile() || mbtilesFileStats.size === 0) {
|
||||
throw Error(`Not valid MBTiles file: ${mbtilesFile}`);
|
||||
}
|
||||
}
|
||||
|
||||
let source;
|
||||
let sourceType;
|
||||
if (inputType === 'pmtiles') {
|
||||
source = openPMtiles(inputFile);
|
||||
sourceType = 'pmtiles';
|
||||
const metadata = await getPMtilesInfo(source);
|
||||
|
||||
tileJSON['encoding'] = params['encoding'];
|
||||
tileJSON['tileSize'] = params['tileSize'];
|
||||
tileJSON['name'] = id;
|
||||
tileJSON['format'] = 'pbf';
|
||||
Object.assign(tileJSON, metadata);
|
||||
|
||||
tileJSON['tilejson'] = '2.0.0';
|
||||
delete tileJSON['filesize'];
|
||||
delete tileJSON['mtime'];
|
||||
delete tileJSON['scheme'];
|
||||
|
||||
Object.assign(tileJSON, params.tilejson || {});
|
||||
fixTileJSONCenter(tileJSON);
|
||||
|
||||
if (options.dataDecoratorFunc) {
|
||||
tileJSON = options.dataDecoratorFunc(id, 'tilejson', tileJSON);
|
||||
const sourceInfoPromise = new Promise((resolve, reject) => {
|
||||
source = new MBTiles(mbtilesFile + '?mode=ro', (err) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
source.getInfo((err, info) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
} else if (inputType === 'mbtiles') {
|
||||
sourceType = 'mbtiles';
|
||||
const mbw = await openMbTilesWrapper(inputFile);
|
||||
const info = await mbw.getInfo();
|
||||
source = mbw.getMbTiles();
|
||||
tileJSON['encoding'] = params['encoding'];
|
||||
tileJSON['tileSize'] = params['tileSize'];
|
||||
tileJSON['name'] = id;
|
||||
tileJSON['format'] = 'pbf';
|
||||
|
||||
|
@ -411,13 +175,17 @@ export const serve_data = {
|
|||
if (options.dataDecoratorFunc) {
|
||||
tileJSON = options.dataDecoratorFunc(id, 'tilejson', tileJSON);
|
||||
}
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return sourceInfoPromise.then(() => {
|
||||
repo[id] = {
|
||||
tileJSON,
|
||||
publicUrl,
|
||||
source,
|
||||
sourceType,
|
||||
};
|
||||
});
|
||||
},
|
||||
};
|
||||
|
|
|
@ -1,18 +1,12 @@
|
|||
'use strict';
|
||||
|
||||
import express from 'express';
|
||||
import fs from 'node:fs';
|
||||
import path from 'path';
|
||||
|
||||
import { getFontsPbf, listFonts } from './utils.js';
|
||||
import { getFontsPbf } from './utils.js';
|
||||
|
||||
/**
|
||||
* Initializes and returns an Express app that serves font files.
|
||||
* @param {object} options - Configuration options for the server.
|
||||
* @param {object} allowedFonts - An object containing allowed fonts.
|
||||
* @param {object} programOpts - An object containing the program options.
|
||||
* @returns {Promise<express.Application>} - A promise that resolves to the Express app.
|
||||
*/
|
||||
export async function serve_font(options, allowedFonts, programOpts) {
|
||||
const { verbose } = programOpts;
|
||||
export const serve_font = (options, allowedFonts) => {
|
||||
const app = express().disable('x-powered-by');
|
||||
|
||||
const lastModified = new Date().toUTCString();
|
||||
|
@ -20,82 +14,56 @@ export async function serve_font(options, allowedFonts, programOpts) {
|
|||
const fontPath = options.paths.fonts;
|
||||
|
||||
const existingFonts = {};
|
||||
|
||||
/**
|
||||
* Handles requests for a font file.
|
||||
* @param {object} req - Express request object.
|
||||
* @param {object} res - Express response object.
|
||||
* @param {string} req.params.fontstack - Name of the font stack.
|
||||
* @param {string} req.params.range - The range of the font (e.g. 0-255).
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
app.get('/fonts/:fontstack/:range.pbf', async (req, res) => {
|
||||
const sRange = String(req.params.range).replace(/\n|\r/g, '');
|
||||
const sFontStack = String(decodeURI(req.params.fontstack)).replace(
|
||||
/\n|\r/g,
|
||||
'',
|
||||
);
|
||||
|
||||
if (verbose) {
|
||||
console.log(
|
||||
`Handling font request for: /fonts/%s/%s.pbf`,
|
||||
sFontStack,
|
||||
sRange,
|
||||
);
|
||||
const fontListingPromise = new Promise((resolve, reject) => {
|
||||
fs.readdir(options.paths.fonts, (err, files) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
for (const file of files) {
|
||||
fs.stat(path.join(fontPath, file), (err, stats) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
|
||||
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()
|
||||
stats.isDirectory() &&
|
||||
fs.existsSync(path.join(fontPath, file, '0-255.pbf'))
|
||||
) {
|
||||
return res.sendStatus(304);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const concatenated = await getFontsPbf(
|
||||
options.serveAllFonts ? null : allowedFonts,
|
||||
fontPath,
|
||||
sFontStack,
|
||||
sRange,
|
||||
existingFonts,
|
||||
);
|
||||
res.header('Content-type', 'application/x-protobuf');
|
||||
res.header('Last-Modified', lastModified);
|
||||
return res.send(concatenated);
|
||||
} catch (err) {
|
||||
console.error(
|
||||
`Error serving font: %s/%s.pbf, Error: %s`,
|
||||
sFontStack,
|
||||
sRange,
|
||||
String(err),
|
||||
);
|
||||
return res
|
||||
.status(400)
|
||||
.header('Content-Type', 'text/plain')
|
||||
.send('Error serving font');
|
||||
existingFonts[path.basename(file)] = true;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 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');
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
app.get('/fonts/:fontstack/:range([\\d]+-[\\d]+).pbf', (req, res, next) => {
|
||||
const fontstack = decodeURI(req.params.fontstack);
|
||||
const range = req.params.range;
|
||||
|
||||
getFontsPbf(
|
||||
options.serveAllFonts ? null : allowedFonts,
|
||||
fontPath,
|
||||
fontstack,
|
||||
range,
|
||||
existingFonts,
|
||||
).then(
|
||||
(concated) => {
|
||||
res.header('Content-type', 'application/x-protobuf');
|
||||
res.header('Last-Modified', lastModified);
|
||||
return res.send(concated);
|
||||
},
|
||||
(err) => res.status(400).header('Content-Type', 'text/plain').send(err),
|
||||
);
|
||||
});
|
||||
|
||||
app.get('/fonts.json', (req, res, next) => {
|
||||
res.header('Content-type', 'application/json');
|
||||
return res.send(
|
||||
Object.keys(options.serveAllFonts ? existingFonts : allowedFonts).sort(),
|
||||
);
|
||||
});
|
||||
|
||||
const fonts = await listFonts(options.paths.fonts);
|
||||
Object.assign(existingFonts, fonts);
|
||||
return app;
|
||||
}
|
||||
return fontListingPromise.then(() => app);
|
||||
};
|
||||
|
|
|
@ -3,12 +3,7 @@
|
|||
'use strict';
|
||||
|
||||
export const serve_rendered = {
|
||||
init: (options, repo, programOpts) => {},
|
||||
add: (options, repo, params, id, programOpts, dataResolver) => {},
|
||||
init: (options, repo) => {},
|
||||
add: (options, repo, params, id, publicUrl, dataResolver) => {},
|
||||
remove: (repo, id) => {},
|
||||
clear: (repo) => {},
|
||||
getTerrainElevation: (data, param) => {
|
||||
param['elevation'] = 'not supported in light';
|
||||
return param;
|
||||
},
|
||||
};
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -5,46 +5,33 @@ import fs from 'node:fs';
|
|||
|
||||
import clone from 'clone';
|
||||
import express from 'express';
|
||||
import { validateStyleMin } from '@maplibre/maplibre-gl-style-spec';
|
||||
import { validate } from '@maplibre/maplibre-gl-style-spec';
|
||||
|
||||
import {
|
||||
allowedSpriteScales,
|
||||
allowedSpriteFormats,
|
||||
fixUrl,
|
||||
readFile,
|
||||
} from './utils.js';
|
||||
import { getPublicUrl } from './utils.js';
|
||||
|
||||
const httpTester = /^https?:\/\//i;
|
||||
const httpTester = /^(http(s)?:)?\/\//;
|
||||
|
||||
const fixUrl = (req, url, publicUrl, opt_nokey) => {
|
||||
if (!url || typeof url !== 'string' || url.indexOf('local://') !== 0) {
|
||||
return url;
|
||||
}
|
||||
const queryParams = [];
|
||||
if (!opt_nokey && req.query.key) {
|
||||
queryParams.unshift(`key=${encodeURIComponent(req.query.key)}`);
|
||||
}
|
||||
let query = '';
|
||||
if (queryParams.length) {
|
||||
query = `?${queryParams.join('&')}`;
|
||||
}
|
||||
return url.replace('local://', getPublicUrl(publicUrl, req)) + query;
|
||||
};
|
||||
|
||||
export const serve_style = {
|
||||
/**
|
||||
* Initializes the serve_style module.
|
||||
* @param {object} options Configuration options.
|
||||
* @param {object} repo Repository object.
|
||||
* @param {object} programOpts - An object containing the program options.
|
||||
* @returns {express.Application} The initialized Express application.
|
||||
*/
|
||||
init: function (options, repo, programOpts) {
|
||||
const { verbose } = programOpts;
|
||||
init: (options, repo) => {
|
||||
const app = express().disable('x-powered-by');
|
||||
/**
|
||||
* Handles requests for style.json files.
|
||||
* @param {express.Request} req - Express request object.
|
||||
* @param {express.Response} res - Express response object.
|
||||
* @param {express.NextFunction} next - Express next function.
|
||||
* @param {string} req.params.id - ID of the style.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
|
||||
app.get('/:id/style.json', (req, res, next) => {
|
||||
const { id } = req.params;
|
||||
if (verbose) {
|
||||
console.log(
|
||||
'Handling style request for: /styles/%s/style.json',
|
||||
String(id).replace(/\n|\r/g, ''),
|
||||
);
|
||||
}
|
||||
try {
|
||||
const item = repo[id];
|
||||
const item = repo[req.params.id];
|
||||
if (!item) {
|
||||
return res.sendStatus(404);
|
||||
}
|
||||
|
@ -52,213 +39,96 @@ export const serve_style = {
|
|||
for (const name of Object.keys(styleJSON_.sources)) {
|
||||
const source = styleJSON_.sources[name];
|
||||
source.url = fixUrl(req, source.url, item.publicUrl);
|
||||
if (typeof source.data == 'string') {
|
||||
source.data = fixUrl(req, source.data, item.publicUrl);
|
||||
}
|
||||
}
|
||||
// mapbox-gl-js viewer cannot handle sprite urls with query
|
||||
if (styleJSON_.sprite) {
|
||||
if (Array.isArray(styleJSON_.sprite)) {
|
||||
styleJSON_.sprite.forEach((spriteItem) => {
|
||||
spriteItem.url = fixUrl(req, spriteItem.url, item.publicUrl);
|
||||
});
|
||||
} else {
|
||||
styleJSON_.sprite = fixUrl(req, styleJSON_.sprite, item.publicUrl);
|
||||
}
|
||||
styleJSON_.sprite = fixUrl(
|
||||
req,
|
||||
styleJSON_.sprite,
|
||||
item.publicUrl,
|
||||
false,
|
||||
);
|
||||
}
|
||||
if (styleJSON_.glyphs) {
|
||||
styleJSON_.glyphs = fixUrl(req, styleJSON_.glyphs, item.publicUrl);
|
||||
styleJSON_.glyphs = fixUrl(
|
||||
req,
|
||||
styleJSON_.glyphs,
|
||||
item.publicUrl,
|
||||
false,
|
||||
);
|
||||
}
|
||||
return res.send(styleJSON_);
|
||||
} catch (e) {
|
||||
next(e);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Handles GET requests for sprite images and JSON files.
|
||||
* @param {express.Request} req - Express request object.
|
||||
* @param {express.Response} res - Express response object.
|
||||
* @param {express.NextFunction} next - Express next function.
|
||||
* @param {string} req.params.id - ID of the sprite.
|
||||
* @param {string} [req.params.spriteID='default'] - ID of the specific sprite image, defaults to 'default'.
|
||||
* @param {string} [req.params.scale] - Scale of the sprite image, defaults to ''.
|
||||
* @param {string} req.params.format - Format of the sprite file, 'png' or 'json'.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
app.get(
|
||||
`/:id/sprite{/:spriteID}{@:scale}{.:format}`,
|
||||
async (req, res, next) => {
|
||||
const { spriteID = 'default', id, format, scale } = req.params;
|
||||
const sanitizedId = String(id).replace(/\n|\r/g, '');
|
||||
const sanitizedScale = scale ? String(scale).replace(/\n|\r/g, '') : '';
|
||||
const sanitizedSpriteID = String(spriteID).replace(/\n|\r/g, '');
|
||||
const sanitizedFormat = format
|
||||
? '.' + String(format).replace(/\n|\r/g, '')
|
||||
: '';
|
||||
if (verbose) {
|
||||
console.log(
|
||||
`Handling sprite request for: /styles/%s/sprite/%s%s%s`,
|
||||
sanitizedId,
|
||||
sanitizedSpriteID,
|
||||
sanitizedScale,
|
||||
sanitizedFormat,
|
||||
);
|
||||
}
|
||||
const item = repo[id];
|
||||
const validatedFormat = allowedSpriteFormats(format);
|
||||
if (!item || !validatedFormat) {
|
||||
if (verbose)
|
||||
console.error(
|
||||
`Sprite item or format not found for: /styles/%s/sprite/%s%s%s`,
|
||||
sanitizedId,
|
||||
sanitizedSpriteID,
|
||||
sanitizedScale,
|
||||
sanitizedFormat,
|
||||
);
|
||||
app.get('/:id/sprite:scale(@[23]x)?.:format([\\w]+)', (req, res, next) => {
|
||||
const item = repo[req.params.id];
|
||||
if (!item || !item.spritePath) {
|
||||
return res.sendStatus(404);
|
||||
}
|
||||
const sprite = item.spritePaths.find(
|
||||
(sprite) => sprite.id === spriteID,
|
||||
);
|
||||
const spriteScale = allowedSpriteScales(scale);
|
||||
if (!sprite || spriteScale === null) {
|
||||
if (verbose)
|
||||
console.error(
|
||||
`Bad Sprite ID or Scale for: /styles/%s/sprite/%s%s%s`,
|
||||
sanitizedId,
|
||||
sanitizedSpriteID,
|
||||
sanitizedScale,
|
||||
sanitizedFormat,
|
||||
);
|
||||
return res.status(400).send('Bad Sprite ID or Scale');
|
||||
}
|
||||
|
||||
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 });
|
||||
const scale = req.params.scale;
|
||||
const format = req.params.format;
|
||||
const filename = `${item.spritePath + (scale || '')}.${format}`;
|
||||
return fs.readFile(filename, (err, data) => {
|
||||
if (err) {
|
||||
console.log('Sprite load error:', filename);
|
||||
return res.sendStatus(404);
|
||||
} else {
|
||||
if (format === 'json') res.header('Content-type', 'application/json');
|
||||
if (format === 'png') res.header('Content-type', 'image/png');
|
||||
return res.send(data);
|
||||
} catch (err) {
|
||||
if (verbose) {
|
||||
console.error(
|
||||
'Sprite load error: %s, Error: %s',
|
||||
filename,
|
||||
String(err),
|
||||
);
|
||||
}
|
||||
return res.sendStatus(404);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
return app;
|
||||
},
|
||||
/**
|
||||
* Removes an item from the repository.
|
||||
* @param {object} repo Repository object.
|
||||
* @param {string} id ID of the item to remove.
|
||||
* @returns {void}
|
||||
*/
|
||||
remove: function (repo, id) {
|
||||
remove: (repo, id) => {
|
||||
delete repo[id];
|
||||
},
|
||||
/**
|
||||
* Adds a new style to the repository.
|
||||
* @param {object} options Configuration options.
|
||||
* @param {object} repo Repository object.
|
||||
* @param {object} params Parameters object containing style path
|
||||
* @param {string} id ID of the style.
|
||||
* @param {object} programOpts - An object containing the program options
|
||||
* @param {object} style pre-fetched/read StyleJSON object.
|
||||
* @param {Function} reportTiles Function for reporting tile sources.
|
||||
* @param {Function} reportFont Function for reporting font usage
|
||||
* @returns {boolean} true if add is successful
|
||||
*/
|
||||
add: function (
|
||||
options,
|
||||
repo,
|
||||
params,
|
||||
id,
|
||||
programOpts,
|
||||
style,
|
||||
reportTiles,
|
||||
reportFont,
|
||||
) {
|
||||
const { publicUrl } = programOpts;
|
||||
add: (options, repo, params, id, publicUrl, reportTiles, reportFont) => {
|
||||
const styleFile = path.resolve(options.paths.styles, params.style);
|
||||
const styleJSON = clone(style);
|
||||
|
||||
const validationErrors = validateStyleMin(styleJSON);
|
||||
let styleFileData;
|
||||
try {
|
||||
styleFileData = fs.readFileSync(styleFile);
|
||||
} catch (e) {
|
||||
console.log('Error reading style file');
|
||||
return false;
|
||||
}
|
||||
|
||||
const validationErrors = validate(styleFileData);
|
||||
if (validationErrors.length > 0) {
|
||||
console.log(`The file "${params.style}" is not a valid style file:`);
|
||||
console.log(
|
||||
`The file "${params.style}" is not valid a valid style file:`,
|
||||
);
|
||||
for (const err of validationErrors) {
|
||||
console.log(`${err.line}: ${err.message}`);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
const styleJSON = JSON.parse(styleFileData);
|
||||
|
||||
for (const name of Object.keys(styleJSON.sources)) {
|
||||
const source = styleJSON.sources[name];
|
||||
let url = source.url;
|
||||
if (
|
||||
url &&
|
||||
(url.startsWith('pmtiles://') || url.startsWith('mbtiles://'))
|
||||
) {
|
||||
const protocol = url.split(':')[0];
|
||||
const url = source.url;
|
||||
if (url && url.lastIndexOf('mbtiles:', 0) === 0) {
|
||||
let mbtilesFile = url.substring('mbtiles://'.length);
|
||||
const fromData =
|
||||
mbtilesFile[0] === '{' && mbtilesFile[mbtilesFile.length - 1] === '}';
|
||||
|
||||
let dataId = url.replace('pmtiles://', '').replace('mbtiles://', '');
|
||||
if (dataId.startsWith('{') && dataId.endsWith('}')) {
|
||||
dataId = dataId.slice(1, -1);
|
||||
}
|
||||
|
||||
const mapsTo = (params.mapping || {})[dataId];
|
||||
if (fromData) {
|
||||
mbtilesFile = mbtilesFile.substr(1, mbtilesFile.length - 2);
|
||||
const mapsTo = (params.mapping || {})[mbtilesFile];
|
||||
if (mapsTo) {
|
||||
dataId = mapsTo;
|
||||
mbtilesFile = mapsTo;
|
||||
}
|
||||
|
||||
const identifier = reportTiles(dataId, protocol);
|
||||
}
|
||||
const identifier = reportTiles(mbtilesFile, fromData);
|
||||
if (!identifier) {
|
||||
return false;
|
||||
}
|
||||
source.url = `local://data/${identifier}.json`;
|
||||
}
|
||||
|
||||
let data = source.data;
|
||||
if (data && typeof data == 'string' && data.startsWith('file://')) {
|
||||
source.data =
|
||||
'local://files' +
|
||||
path.resolve(
|
||||
'/',
|
||||
data.replace('file://', '').replace(options.paths.files, ''),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for (const obj of styleJSON.layers) {
|
||||
|
@ -273,11 +143,10 @@ export const serve_style = {
|
|||
}
|
||||
}
|
||||
|
||||
let spritePaths = [];
|
||||
if (styleJSON.sprite) {
|
||||
if (!Array.isArray(styleJSON.sprite)) {
|
||||
if (!httpTester.test(styleJSON.sprite)) {
|
||||
let spritePath = path.join(
|
||||
let spritePath;
|
||||
|
||||
if (styleJSON.sprite && !httpTester.test(styleJSON.sprite)) {
|
||||
spritePath = path.join(
|
||||
options.paths.sprites,
|
||||
styleJSON.sprite
|
||||
.replace('{style}', path.basename(styleFile, '.json'))
|
||||
|
@ -287,37 +156,16 @@ export const serve_style = {
|
|||
),
|
||||
);
|
||||
styleJSON.sprite = `local://styles/${id}/sprite`;
|
||||
spritePaths.push({ id: 'default', path: spritePath });
|
||||
}
|
||||
} else {
|
||||
for (let spriteItem of styleJSON.sprite) {
|
||||
if (!httpTester.test(spriteItem.url)) {
|
||||
let spritePath = path.join(
|
||||
options.paths.sprites,
|
||||
spriteItem.url
|
||||
.replace('{style}', path.basename(styleFile, '.json'))
|
||||
.replace(
|
||||
'{styleJsonFolder}',
|
||||
path.relative(options.paths.sprites, path.dirname(styleFile)),
|
||||
),
|
||||
);
|
||||
spriteItem.url = `local://styles/${id}/sprite/` + spriteItem.id;
|
||||
spritePaths.push({ id: spriteItem.id, path: spritePath });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (styleJSON.glyphs && !httpTester.test(styleJSON.glyphs)) {
|
||||
styleJSON.glyphs = 'local://fonts/{fontstack}/{range}.pbf';
|
||||
}
|
||||
|
||||
repo[id] = {
|
||||
styleJSON,
|
||||
spritePaths,
|
||||
spritePath,
|
||||
publicUrl,
|
||||
name: styleJSON.name,
|
||||
lastModified: new Date().toUTCString(),
|
||||
};
|
||||
|
||||
return true;
|
||||
|
|
508
src/server.js
508
src/server.js
|
@ -1,9 +1,12 @@
|
|||
#!/usr/bin/env node
|
||||
'use strict';
|
||||
|
||||
import os from 'os';
|
||||
process.env.UV_THREADPOOL_SIZE = Math.ceil(Math.max(4, os.cpus().length * 1.5));
|
||||
|
||||
import fs from 'node:fs';
|
||||
import path from 'path';
|
||||
import fnv1a from '@sindresorhus/fnv1a';
|
||||
|
||||
import chokidar from 'chokidar';
|
||||
import clone from 'clone';
|
||||
import cors from 'cors';
|
||||
|
@ -16,30 +19,25 @@ import morgan from 'morgan';
|
|||
import { serve_data } from './serve_data.js';
|
||||
import { serve_style } from './serve_style.js';
|
||||
import { serve_font } from './serve_font.js';
|
||||
import {
|
||||
allowedTileSizes,
|
||||
getTileUrls,
|
||||
getPublicUrl,
|
||||
isValidHttpUrl,
|
||||
} from './utils.js';
|
||||
import { getTileUrls, getPublicUrl } from './utils.js';
|
||||
|
||||
import { fileURLToPath } from 'url';
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const packageJson = JSON.parse(
|
||||
fs.readFileSync(__dirname + '/../package.json', 'utf8'),
|
||||
);
|
||||
const isLight = packageJson.name.slice(-6) === '-light';
|
||||
|
||||
const isLight = packageJson.name.slice(-6) === '-light';
|
||||
const serve_rendered = (
|
||||
await import(`${!isLight ? `./serve_rendered.js` : `./serve_light.js`}`)
|
||||
).serve_rendered;
|
||||
|
||||
/**
|
||||
* Starts the server.
|
||||
* @param {object} opts - Configuration options for the server.
|
||||
* @returns {Promise<object>} - A promise that resolves to the server object.
|
||||
*
|
||||
* @param opts
|
||||
*/
|
||||
async function start(opts) {
|
||||
function start(opts) {
|
||||
console.log('Starting server');
|
||||
|
||||
const app = express().disable('x-powered-by');
|
||||
|
@ -95,32 +93,31 @@ async function start(opts) {
|
|||
paths.fonts = path.resolve(paths.root, paths.fonts || '');
|
||||
paths.sprites = path.resolve(paths.root, paths.sprites || '');
|
||||
paths.mbtiles = path.resolve(paths.root, paths.mbtiles || '');
|
||||
paths.pmtiles = path.resolve(paths.root, paths.pmtiles || '');
|
||||
paths.icons = paths.icons
|
||||
? path.resolve(paths.root, paths.icons)
|
||||
: path.resolve(__dirname, '../public/resources/images');
|
||||
paths.files = paths.files
|
||||
? path.resolve(paths.root, paths.files)
|
||||
: path.resolve(__dirname, '../public/files');
|
||||
paths.icons = path.resolve(paths.root, paths.icons || '');
|
||||
|
||||
const startupPromises = [];
|
||||
|
||||
for (const type of Object.keys(paths)) {
|
||||
const checkPath = (type) => {
|
||||
if (!fs.existsSync(paths[type])) {
|
||||
console.error(
|
||||
`The specified path for "${type}" does not exist (${paths[type]}).`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
};
|
||||
checkPath('styles');
|
||||
checkPath('fonts');
|
||||
checkPath('sprites');
|
||||
checkPath('mbtiles');
|
||||
checkPath('icons');
|
||||
|
||||
/**
|
||||
* Recursively get all files within a directory.
|
||||
* Inspired by https://stackoverflow.com/a/45130990/10133863
|
||||
*
|
||||
* @param {string} directory Absolute path to a directory to get files from.
|
||||
* @returns {Promise<string[]>} - A promise that resolves to an array of file paths relative to the icon directory.
|
||||
*/
|
||||
async function getFiles(directory) {
|
||||
const getFiles = async (directory) => {
|
||||
// Fetch all entries of the directory and attach type information
|
||||
const dirEntries = await fs.promises.readdir(directory, {
|
||||
withFileTypes: true,
|
||||
|
@ -139,20 +136,24 @@ async function start(opts) {
|
|||
|
||||
// Flatten the list of files to a single array
|
||||
return files.flat();
|
||||
}
|
||||
};
|
||||
|
||||
// Load all available icons into a settings object
|
||||
startupPromises.push(
|
||||
new Promise((resolve) => {
|
||||
getFiles(paths.icons).then((files) => {
|
||||
paths.availableIcons = files;
|
||||
resolve();
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
if (options.dataDecorator) {
|
||||
try {
|
||||
options.dataDecoratorFunc = require(
|
||||
path.resolve(paths.root, options.dataDecorator),
|
||||
);
|
||||
options.dataDecoratorFunc = require(path.resolve(
|
||||
paths.root,
|
||||
options.dataDecorator,
|
||||
));
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
|
@ -162,90 +163,53 @@ async function start(opts) {
|
|||
app.use(cors());
|
||||
}
|
||||
|
||||
app.use('/data/', serve_data.init(options, serving.data, opts));
|
||||
app.use('/files/', express.static(paths.files));
|
||||
app.use('/styles/', serve_style.init(options, serving.styles, opts));
|
||||
app.use('/data/', serve_data.init(options, serving.data));
|
||||
app.use('/styles/', serve_style.init(options, serving.styles));
|
||||
if (!isLight) {
|
||||
startupPromises.push(
|
||||
serve_rendered.init(options, serving.rendered, opts).then((sub) => {
|
||||
serve_rendered.init(options, serving.rendered).then((sub) => {
|
||||
app.use('/styles/', sub);
|
||||
}),
|
||||
);
|
||||
}
|
||||
/**
|
||||
* Adds a style to the server.
|
||||
* @param {string} id - The ID of the style.
|
||||
* @param {object} item - The style configuration object.
|
||||
* @param {boolean} allowMoreData - Whether to allow adding more data sources.
|
||||
* @param {boolean} reportFonts - Whether to report fonts.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function addStyle(id, item, allowMoreData, reportFonts) {
|
||||
|
||||
const addStyle = (id, item, allowMoreData, reportFonts) => {
|
||||
let success = true;
|
||||
|
||||
let styleJSON;
|
||||
try {
|
||||
if (isValidHttpUrl(item.style)) {
|
||||
const res = await fetch(item.style);
|
||||
if (!res.ok) {
|
||||
throw new Error(`fetch error ${res.status}`);
|
||||
}
|
||||
styleJSON = await res.json();
|
||||
} else {
|
||||
const styleFile = path.resolve(options.paths.styles, item.style);
|
||||
const styleFileData = await fs.promises.readFile(styleFile);
|
||||
styleJSON = JSON.parse(styleFileData);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(`Error getting style file "${item.style}"`);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (item.serve_data !== false) {
|
||||
success = serve_style.add(
|
||||
options,
|
||||
serving.styles,
|
||||
item,
|
||||
id,
|
||||
opts,
|
||||
styleJSON,
|
||||
(styleSourceId, protocol) => {
|
||||
opts.publicUrl,
|
||||
(mbtiles, fromData) => {
|
||||
let dataItemId;
|
||||
for (const id of Object.keys(data)) {
|
||||
if (id === styleSourceId) {
|
||||
// Style id was found in data ids, return that id
|
||||
if (fromData) {
|
||||
if (id === mbtiles) {
|
||||
dataItemId = id;
|
||||
}
|
||||
} else {
|
||||
const fileType = Object.keys(data[id])[0];
|
||||
if (data[id][fileType] === styleSourceId) {
|
||||
// Style id was found in data filename, return the id that filename belong to
|
||||
if (data[id].mbtiles === mbtiles) {
|
||||
dataItemId = id;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (dataItemId) {
|
||||
// input files exists in the data config, return found id
|
||||
// mbtiles exist in the data config
|
||||
return dataItemId;
|
||||
} else {
|
||||
if (!allowMoreData) {
|
||||
if (fromData || !allowMoreData) {
|
||||
console.log(
|
||||
`ERROR: style "${item.style}" using unknown file "${styleSourceId}"! Skipping...`,
|
||||
`ERROR: style "${item.style}" using unknown mbtiles "${mbtiles}"! Skipping...`,
|
||||
);
|
||||
return undefined;
|
||||
} else {
|
||||
let id =
|
||||
styleSourceId.substr(0, styleSourceId.lastIndexOf('.')) ||
|
||||
styleSourceId;
|
||||
if (isValidHttpUrl(styleSourceId)) {
|
||||
id =
|
||||
fnv1a(styleSourceId) + '_' + id.replace(/^.*\/(.*)$/, '$1');
|
||||
}
|
||||
while (data[id]) id += '_'; //if the data source id already exists, add a "_" untill it doesn't
|
||||
//Add the new data source to the data array.
|
||||
let id = mbtiles.substr(0, mbtiles.lastIndexOf('.')) || mbtiles;
|
||||
while (data[id]) id += '_';
|
||||
data[id] = {
|
||||
[protocol]: styleSourceId,
|
||||
mbtiles: mbtiles,
|
||||
};
|
||||
|
||||
return id;
|
||||
}
|
||||
}
|
||||
|
@ -265,26 +229,15 @@ async function start(opts) {
|
|||
serving.rendered,
|
||||
item,
|
||||
id,
|
||||
opts,
|
||||
styleJSON,
|
||||
function dataResolver(styleSourceId) {
|
||||
let fileType;
|
||||
let inputFile;
|
||||
opts.publicUrl,
|
||||
(mbtiles) => {
|
||||
let mbtilesFile;
|
||||
for (const id of Object.keys(data)) {
|
||||
fileType = Object.keys(data[id])[0];
|
||||
if (styleSourceId == id) {
|
||||
inputFile = data[id][fileType];
|
||||
break;
|
||||
} else if (data[id][fileType] == styleSourceId) {
|
||||
inputFile = data[id][fileType];
|
||||
break;
|
||||
if (id === mbtiles) {
|
||||
mbtilesFile = data[id].mbtiles;
|
||||
}
|
||||
}
|
||||
if (!isValidHttpUrl(inputFile)) {
|
||||
inputFile = path.resolve(options.paths[fileType], inputFile);
|
||||
}
|
||||
|
||||
return { inputFile, fileType };
|
||||
return mbtilesFile;
|
||||
},
|
||||
),
|
||||
);
|
||||
|
@ -292,8 +245,7 @@ async function start(opts) {
|
|||
item.serve_rendered = false;
|
||||
}
|
||||
}
|
||||
return success;
|
||||
}
|
||||
};
|
||||
|
||||
for (const id of Object.keys(config.styles || {})) {
|
||||
const item = config.styles[id];
|
||||
|
@ -301,24 +253,28 @@ async function start(opts) {
|
|||
console.log(`Missing "style" property for ${id}`);
|
||||
continue;
|
||||
}
|
||||
startupPromises.push(addStyle(id, item, true, true));
|
||||
|
||||
addStyle(id, item, true, true);
|
||||
}
|
||||
|
||||
startupPromises.push(
|
||||
serve_font(options, serving.fonts, opts).then((sub) => {
|
||||
serve_font(options, serving.fonts).then((sub) => {
|
||||
app.use('/', sub);
|
||||
}),
|
||||
);
|
||||
|
||||
for (const id of Object.keys(data)) {
|
||||
const item = data[id];
|
||||
const fileType = Object.keys(data[id])[0];
|
||||
if (!fileType || !(fileType === 'pmtiles' || fileType === 'mbtiles')) {
|
||||
console.log(
|
||||
`Missing "pmtiles" or "mbtiles" property for ${id} data source`,
|
||||
);
|
||||
if (!item.mbtiles || item.mbtiles.length === 0) {
|
||||
console.log(`Missing "mbtiles" property for ${id}`);
|
||||
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) {
|
||||
fs.readdir(options.paths.styles, { withFileTypes: true }, (err, files) => {
|
||||
if (err) {
|
||||
|
@ -358,13 +314,7 @@ async function start(opts) {
|
|||
}
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Handles requests for a list of available styles.
|
||||
* @param {object} req - Express request object.
|
||||
* @param {object} res - Express response object.
|
||||
* @param {string} [req.query.key] - Optional API key.
|
||||
* @returns {void}
|
||||
*/
|
||||
|
||||
app.get('/styles.json', (req, res, next) => {
|
||||
const result = [];
|
||||
const query = req.query.key
|
||||
|
@ -375,7 +325,7 @@ async function start(opts) {
|
|||
result.push({
|
||||
version: styleJSON.version,
|
||||
name: styleJSON.name,
|
||||
id,
|
||||
id: id,
|
||||
url: `${getPublicUrl(
|
||||
opts.publicUrl,
|
||||
req,
|
||||
|
@ -385,15 +335,7 @@ async function start(opts) {
|
|||
res.send(result);
|
||||
});
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
const addTileJSONs = (arr, req, type) => {
|
||||
for (const id of Object.keys(serving[type])) {
|
||||
const info = clone(serving[type][id].tileJSON);
|
||||
let path = '';
|
||||
|
@ -406,7 +348,6 @@ async function start(opts) {
|
|||
req,
|
||||
info.tiles,
|
||||
path,
|
||||
tileSize,
|
||||
info.format,
|
||||
opts.publicUrl,
|
||||
{
|
||||
|
@ -416,47 +357,16 @@ async function start(opts) {
|
|||
arr.push(info);
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles requests for a rendered tilejson endpoint.
|
||||
* @param {object} req - Express request object.
|
||||
* @param {object} res - Express response object.
|
||||
* @param {string} req.params.tileSize - Optional tile size parameter.
|
||||
* @returns {void}
|
||||
*/
|
||||
app.get('{/:tileSize}/rendered.json', (req, res, next) => {
|
||||
const tileSize = allowedTileSizes(req.params['tileSize']);
|
||||
res.send(addTileJSONs([], req, 'rendered', parseInt(tileSize, 10)));
|
||||
app.get('/rendered.json', (req, res, next) => {
|
||||
res.send(addTileJSONs([], req, 'rendered'));
|
||||
});
|
||||
|
||||
/**
|
||||
* Handles requests for a data tilejson endpoint.
|
||||
* @param {object} req - Express request object.
|
||||
* @param {object} res - Express response object.
|
||||
* @returns {void}
|
||||
*/
|
||||
app.get('/data.json', (req, res) => {
|
||||
res.send(addTileJSONs([], req, 'data', undefined));
|
||||
app.get('/data.json', (req, res, next) => {
|
||||
res.send(addTileJSONs([], req, 'data'));
|
||||
});
|
||||
|
||||
/**
|
||||
* Handles requests for a combined rendered and data tilejson endpoint.
|
||||
* @param {object} req - Express request object.
|
||||
* @param {object} res - Express response object.
|
||||
* @param {string} req.params.tileSize - Optional tile size parameter.
|
||||
* @returns {void}
|
||||
*/
|
||||
app.get('{/:tileSize}/index.json', (req, res, next) => {
|
||||
const tileSize = allowedTileSizes(req.params['tileSize']);
|
||||
res.send(
|
||||
addTileJSONs(
|
||||
addTileJSONs([], req, 'rendered', parseInt(tileSize, 10)),
|
||||
req,
|
||||
'data',
|
||||
undefined,
|
||||
),
|
||||
);
|
||||
app.get('/index.json', (req, res, next) => {
|
||||
res.send(addTileJSONs(addTileJSONs([], req, 'rendered'), req, 'data'));
|
||||
});
|
||||
|
||||
// ------------------------------------
|
||||
|
@ -464,15 +374,7 @@ async function start(opts) {
|
|||
app.use('/', express.static(path.join(__dirname, '../public/resources')));
|
||||
|
||||
const templates = path.join(__dirname, '../public/templates');
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
const serveTemplate = (urlPath, template, dataGetter) => {
|
||||
let templateFile = `${templates}/${template}.tmpl`;
|
||||
if (template === 'index') {
|
||||
if (options.frontPage === false) {
|
||||
|
@ -484,19 +386,27 @@ async function start(opts) {
|
|||
templateFile = path.resolve(paths.root, options.frontPage);
|
||||
}
|
||||
}
|
||||
try {
|
||||
const content = fs.readFileSync(templateFile, 'utf-8');
|
||||
const compiled = handlebars.compile(content.toString());
|
||||
app.get(urlPath, (req, res, next) => {
|
||||
if (opts.verbose) {
|
||||
console.log(`Serving template at path: ${urlPath}`);
|
||||
startupPromises.push(
|
||||
new Promise((resolve, reject) => {
|
||||
fs.readFile(templateFile, (err, content) => {
|
||||
if (err) {
|
||||
err = new Error(`Template not found: ${err.message}`);
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
const compiled = handlebars.compile(content.toString());
|
||||
|
||||
app.use(urlPath, (req, res, next) => {
|
||||
let data = {};
|
||||
if (dataGetter) {
|
||||
data = dataGetter(req);
|
||||
if (data) {
|
||||
data['server_version'] =
|
||||
`${packageJson.name} v${packageJson.version}`;
|
||||
if (!data) {
|
||||
return res.status(404).send('Not found');
|
||||
}
|
||||
}
|
||||
data[
|
||||
'server_version'
|
||||
] = `${packageJson.name} v${packageJson.version}`;
|
||||
data['public_url'] = opts.publicUrl || '/';
|
||||
data['is_light'] = isLight;
|
||||
data['key_query_part'] = req.query.key
|
||||
|
@ -507,114 +417,75 @@ async function start(opts) {
|
|||
: '';
|
||||
if (template === 'wmts') res.set('Content-Type', 'text/xml');
|
||||
return res.status(200).send(compiled(data));
|
||||
} else {
|
||||
if (opts.verbose) {
|
||||
console.log(`Forwarding request for: ${urlPath} to next route`);
|
||||
}
|
||||
next('route');
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(`Error reading template file: ${templateFile}`, err);
|
||||
throw new Error(`Template not found: ${err.message}`); //throw an error so that the server doesnt start
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles requests for the index page, providing a list of available styles and data.
|
||||
* @param {object} req - Express request object.
|
||||
* @param {object} res - Express response object.
|
||||
* @returns {void}
|
||||
*/
|
||||
serveTemplate('/', 'index', (req) => {
|
||||
let styles = {};
|
||||
for (const id of Object.keys(serving.styles || {})) {
|
||||
let style = {
|
||||
...serving.styles[id],
|
||||
serving_data: serving.styles[id],
|
||||
serving_rendered: serving.rendered[id],
|
||||
resolve();
|
||||
});
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
serveTemplate('/$', 'index', (req) => {
|
||||
const styles = clone(serving.styles || {});
|
||||
for (const id of Object.keys(styles)) {
|
||||
const style = styles[id];
|
||||
style.name = (serving.styles[id] || serving.rendered[id] || {}).name;
|
||||
style.serving_data = serving.styles[id];
|
||||
style.serving_rendered = serving.rendered[id];
|
||||
if (style.serving_rendered) {
|
||||
const { center } = style.serving_rendered.tileJSON;
|
||||
const center = style.serving_rendered.tileJSON.center;
|
||||
if (center) {
|
||||
style.viewer_hash = `#${center[2]}/${center[1].toFixed(
|
||||
5,
|
||||
)}/${center[0].toFixed(5)}`;
|
||||
|
||||
const centerPx = mercator.px([center[0], center[1]], center[2]);
|
||||
// Set thumbnail default size to be 256px x 256px
|
||||
style.thumbnail = `${Math.floor(center[2])}/${Math.floor(
|
||||
style.thumbnail = `${center[2]}/${Math.floor(
|
||||
centerPx[0] / 256,
|
||||
)}/${Math.floor(centerPx[1] / 256)}.png`;
|
||||
}
|
||||
|
||||
const tileSize = 512;
|
||||
style.xyz_link = getTileUrls(
|
||||
req,
|
||||
style.serving_rendered.tileJSON.tiles,
|
||||
`styles/${id}`,
|
||||
tileSize,
|
||||
style.serving_rendered.tileJSON.format,
|
||||
opts.publicUrl,
|
||||
)[0];
|
||||
}
|
||||
|
||||
styles[id] = style;
|
||||
}
|
||||
let datas = {};
|
||||
for (const id of Object.keys(serving.data || {})) {
|
||||
let data = Object.assign({}, serving.data[id]);
|
||||
|
||||
const { tileJSON } = serving.data[id];
|
||||
const { center } = tileJSON;
|
||||
|
||||
const data = clone(serving.data || {});
|
||||
for (const id of Object.keys(data)) {
|
||||
const data_ = data[id];
|
||||
const tilejson = data[id].tileJSON;
|
||||
const center = tilejson.center;
|
||||
if (center) {
|
||||
data.viewer_hash = `#${center[2]}/${center[1].toFixed(
|
||||
data_.viewer_hash = `#${center[2]}/${center[1].toFixed(
|
||||
5,
|
||||
)}/${center[0].toFixed(5)}`;
|
||||
}
|
||||
data_.is_vector = tilejson.format === 'pbf';
|
||||
if (!data_.is_vector) {
|
||||
if (center) {
|
||||
const centerPx = mercator.px([center[0], center[1]], center[2]);
|
||||
data_.thumbnail = `${center[2]}/${Math.floor(
|
||||
centerPx[0] / 256,
|
||||
)}/${Math.floor(centerPx[1] / 256)}.${data_.tileJSON.format}`;
|
||||
}
|
||||
|
||||
const tileSize = undefined;
|
||||
data.xyz_link = getTileUrls(
|
||||
data_.xyz_link = getTileUrls(
|
||||
req,
|
||||
tileJSON.tiles,
|
||||
tilejson.tiles,
|
||||
`data/${id}`,
|
||||
tileSize,
|
||||
tileJSON.format,
|
||||
tilejson.format,
|
||||
opts.publicUrl,
|
||||
{
|
||||
pbf: options.pbfAlias,
|
||||
},
|
||||
)[0];
|
||||
|
||||
data.is_vector = tileJSON.format === 'pbf';
|
||||
if (!data.is_vector) {
|
||||
if (
|
||||
tileJSON.encoding === 'terrarium' ||
|
||||
tileJSON.encoding === 'mapbox'
|
||||
) {
|
||||
if (!isLight) {
|
||||
data.elevation_link = getTileUrls(
|
||||
req,
|
||||
tileJSON.tiles,
|
||||
`data/${id}/elevation`,
|
||||
)[0];
|
||||
}
|
||||
data.is_terrain = true;
|
||||
}
|
||||
if (center) {
|
||||
const centerPx = mercator.px([center[0], center[1]], center[2]);
|
||||
data.thumbnail = `${Math.floor(center[2])}/${Math.floor(
|
||||
centerPx[0] / 256,
|
||||
)}/${Math.floor(centerPx[1] / 256)}.${tileJSON.format}`;
|
||||
}
|
||||
}
|
||||
|
||||
if (data.filesize) {
|
||||
if (data_.filesize) {
|
||||
let suffix = 'kB';
|
||||
let size = parseInt(tileJSON.filesize, 10) / 1024;
|
||||
let size = parseInt(data_.filesize, 10) / 1024;
|
||||
if (size > 1024) {
|
||||
suffix = 'MB';
|
||||
size /= 1024;
|
||||
|
@ -623,106 +494,65 @@ async function start(opts) {
|
|||
suffix = 'GB';
|
||||
size /= 1024;
|
||||
}
|
||||
data.formatted_filesize = `${size.toFixed(2)} ${suffix}`;
|
||||
data_.formatted_filesize = `${size.toFixed(2)} ${suffix}`;
|
||||
}
|
||||
datas[id] = data;
|
||||
}
|
||||
return {
|
||||
styles: Object.keys(styles).length ? styles : null,
|
||||
data: Object.keys(datas).length ? datas : null,
|
||||
data: Object.keys(data).length ? data : null,
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* 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;
|
||||
serveTemplate('/styles/:id/$', 'viewer', (req) => {
|
||||
const id = req.params.id;
|
||||
const style = clone(((serving.styles || {})[id] || {}).styleJSON);
|
||||
|
||||
if (!style) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
...style,
|
||||
id,
|
||||
name: (serving.styles[id] || serving.rendered[id]).name,
|
||||
serving_data: serving.styles[id],
|
||||
serving_rendered: serving.rendered[id],
|
||||
};
|
||||
style.id = id;
|
||||
style.name = (serving.styles[id] || serving.rendered[id]).name;
|
||||
style.serving_data = serving.styles[id];
|
||||
style.serving_rendered = serving.rendered[id];
|
||||
return style;
|
||||
});
|
||||
|
||||
/**
|
||||
* Handles requests for a Web Map Tile Service (WMTS) XML template.
|
||||
* @param {object} req - Express request object.
|
||||
* @param {object} res - Express response object.
|
||||
* @param {string} req.params.id - ID of the style.
|
||||
* @returns {void}
|
||||
/*
|
||||
app.use('/rendered/:id/$', function(req, res, next) {
|
||||
return res.redirect(301, '/styles/' + req.params.id + '/');
|
||||
});
|
||||
*/
|
||||
serveTemplate('/styles/:id/wmts.xml', 'wmts', (req) => {
|
||||
const { id } = req.params;
|
||||
const id = req.params.id;
|
||||
const wmts = clone((serving.styles || {})[id]);
|
||||
|
||||
if (!wmts) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (wmts.hasOwnProperty('serve_rendered') && !wmts.serve_rendered) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let baseUrl;
|
||||
wmts.id = id;
|
||||
wmts.name = (serving.styles[id] || serving.rendered[id]).name;
|
||||
if (opts.publicUrl) {
|
||||
baseUrl = opts.publicUrl;
|
||||
wmts.baseUrl = opts.publicUrl;
|
||||
} else {
|
||||
baseUrl = `${
|
||||
wmts.baseUrl = `${
|
||||
req.get('X-Forwarded-Protocol')
|
||||
? req.get('X-Forwarded-Protocol')
|
||||
: req.protocol
|
||||
}://${req.get('host')}/`;
|
||||
}
|
||||
|
||||
return {
|
||||
...wmts,
|
||||
id,
|
||||
name: (serving.styles[id] || serving.rendered[id]).name,
|
||||
baseUrl,
|
||||
};
|
||||
return wmts;
|
||||
});
|
||||
|
||||
/**
|
||||
* Handles requests for a data view template for a specific data source.
|
||||
* @param {object} req - Express request object.
|
||||
* @param {object} res - Express response object.
|
||||
* @param {string} req.params.id - ID of the data source.
|
||||
* @param {string} [req.params.view] - Optional view type.
|
||||
* @returns {void}
|
||||
*/
|
||||
serveTemplate('/data{/:view}/:id/', 'data', (req) => {
|
||||
const { id, view } = req.params;
|
||||
const data = serving.data[id];
|
||||
|
||||
serveTemplate('/data/:id/$', 'data', (req) => {
|
||||
const id = req.params.id;
|
||||
const data = clone(serving.data[id]);
|
||||
if (!data) {
|
||||
return null;
|
||||
}
|
||||
const is_terrain =
|
||||
(data.tileJSON.encoding === 'terrarium' ||
|
||||
data.tileJSON.encoding === 'mapbox') &&
|
||||
view === 'preview';
|
||||
|
||||
return {
|
||||
...data,
|
||||
id,
|
||||
use_maplibre: data.tileJSON.format === 'pbf' || is_terrain,
|
||||
is_terrain: is_terrain,
|
||||
is_terrainrgb: data.tileJSON.encoding === 'mapbox',
|
||||
terrain_encoding: data.tileJSON.encoding,
|
||||
is_light: isLight,
|
||||
};
|
||||
data.id = id;
|
||||
data.is_vector = data.tileJSON.format === 'pbf';
|
||||
return data;
|
||||
});
|
||||
|
||||
let startupComplete = false;
|
||||
|
@ -730,14 +560,7 @@ async function start(opts) {
|
|||
console.log('Startup complete');
|
||||
startupComplete = true;
|
||||
});
|
||||
|
||||
/**
|
||||
* 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) => {
|
||||
app.get('/health', (req, res, next) => {
|
||||
if (startupComplete) {
|
||||
return res.status(200).send('OK');
|
||||
} else {
|
||||
|
@ -761,16 +584,16 @@ async function start(opts) {
|
|||
enableShutdown(server);
|
||||
|
||||
return {
|
||||
app,
|
||||
server,
|
||||
startupPromise,
|
||||
serving,
|
||||
app: app,
|
||||
server: server,
|
||||
startupPromise: startupPromise,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the server gracefully
|
||||
*
|
||||
* @param {string} signal Name of the received signal
|
||||
* @returns {void}
|
||||
*/
|
||||
function stopGracefully(signal) {
|
||||
console.log(`Caught signal ${signal}, stopping gracefully`);
|
||||
|
@ -778,12 +601,11 @@ function stopGracefully(signal) {
|
|||
}
|
||||
|
||||
/**
|
||||
* Starts and manages the server
|
||||
* @param {object} opts - Configuration options for the server.
|
||||
* @returns {Promise<object>} - A promise that resolves to the running server
|
||||
*
|
||||
* @param opts
|
||||
*/
|
||||
export async function server(opts) {
|
||||
const running = await start(opts);
|
||||
export function server(opts) {
|
||||
const running = start(opts);
|
||||
|
||||
running.startupPromise.catch((err) => {
|
||||
console.error(err.message);
|
||||
|
@ -797,16 +619,12 @@ export async function server(opts) {
|
|||
console.log(`Caught signal ${signal}, refreshing`);
|
||||
console.log('Stopping server and reloading config');
|
||||
|
||||
running.server.shutdown(async () => {
|
||||
const restarted = await start(opts);
|
||||
if (!isLight) {
|
||||
serve_rendered.clear(running.serving.rendered);
|
||||
}
|
||||
running.server.shutdown(() => {
|
||||
const restarted = start(opts);
|
||||
running.server = restarted.server;
|
||||
running.app = restarted.app;
|
||||
running.startupPromise = restarted.startupPromise;
|
||||
running.serving = restarted.serving;
|
||||
});
|
||||
});
|
||||
|
||||
return running;
|
||||
}
|
||||
|
|
344
src/utils.js
344
src/utils.js
|
@ -1,149 +1,33 @@
|
|||
'use strict';
|
||||
|
||||
import path from 'path';
|
||||
import fsPromises from 'fs/promises';
|
||||
import fs from 'node:fs';
|
||||
|
||||
import clone from 'clone';
|
||||
import { combine } from '@jsse/pbfont';
|
||||
import { existsP } from './promises.js';
|
||||
import { getPMtilesTile } from './pmtiles_adapter.js';
|
||||
|
||||
export const allowedSpriteFormats = allowedOptions(['png', 'json']);
|
||||
|
||||
export const allowedTileSizes = allowedOptions(['256', '512']);
|
||||
import glyphCompose from '@mapbox/glyph-pbf-composite';
|
||||
|
||||
/**
|
||||
* Restrict user input to an allowed set of options.
|
||||
* @param {string[]} opts - An array of allowed option strings.
|
||||
* @param {object} [config] - Optional configuration object.
|
||||
* @param {string} [config.defaultValue] - The default value to return if input doesn't match.
|
||||
* @returns {function(string): string} - A function that takes a value and returns it if valid or a default.
|
||||
* Generate new URL object
|
||||
*
|
||||
* @param req
|
||||
* @params {object} req - Express request
|
||||
* @returns {URL} object
|
||||
*/
|
||||
export function allowedOptions(opts, { defaultValue } = {}) {
|
||||
const values = Object.fromEntries(opts.map((key) => [key, key]));
|
||||
return (value) => values[value] || defaultValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a scale string to a number.
|
||||
* @param {string} scale The scale string (e.g., '2x', '4x').
|
||||
* @param {number} maxScale Maximum allowed scale digit.
|
||||
* @returns {number|null} The parsed scale as a number or null if invalid.
|
||||
*/
|
||||
export function allowedScales(scale, maxScale = 9) {
|
||||
if (scale === undefined) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line security/detect-non-literal-regexp
|
||||
const regex = new RegExp(`^[2-${maxScale}]x$`);
|
||||
if (!regex.test(scale)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return parseInt(scale.slice(0, -1), 10);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a string is a valid sprite scale and returns it if it is within the allowed range, and null if it does not conform.
|
||||
* @param {string} scale - The scale string to validate (e.g., '2x', '3x').
|
||||
* @param {number} [maxScale] - The maximum scale value. If no value is passed in, it defaults to a value of 3.
|
||||
* @returns {string|null} - The valid scale string or null if invalid.
|
||||
*/
|
||||
export function allowedSpriteScales(scale, maxScale = 3) {
|
||||
if (!scale) {
|
||||
return '';
|
||||
}
|
||||
const match = scale?.match(/^([2-9]\d*)x$/);
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
const parsedScale = parseInt(match[1], 10);
|
||||
if (parsedScale <= maxScale) {
|
||||
return `@${parsedScale}x`;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces local:// URLs with public http(s):// URLs.
|
||||
* @param {object} req - Express request object.
|
||||
* @param {string} url - The URL string to fix.
|
||||
* @param {string} publicUrl - The public URL prefix to use for replacements.
|
||||
* @returns {string} - The fixed URL string.
|
||||
*/
|
||||
export function fixUrl(req, url, publicUrl) {
|
||||
if (!url || typeof url !== 'string' || url.indexOf('local://') !== 0) {
|
||||
return url;
|
||||
}
|
||||
const queryParams = [];
|
||||
if (req.query.key) {
|
||||
queryParams.unshift(`key=${encodeURIComponent(req.query.key)}`);
|
||||
}
|
||||
let query = '';
|
||||
if (queryParams.length) {
|
||||
query = `?${queryParams.join('&')}`;
|
||||
}
|
||||
return url.replace('local://', getPublicUrl(publicUrl, req)) + query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a new URL object from the Express request.
|
||||
* @param {object} req - Express request object.
|
||||
* @returns {URL} - URL object with correct host and optionally path.
|
||||
*/
|
||||
function getUrlObject(req) {
|
||||
const getUrlObject = (req) => {
|
||||
const urlObject = new URL(`${req.protocol}://${req.headers.host}/`);
|
||||
// support overriding hostname by sending X-Forwarded-Host http header
|
||||
urlObject.hostname = req.hostname;
|
||||
|
||||
// support overriding port by sending X-Forwarded-Port http header
|
||||
const xForwardedPort = req.get('X-Forwarded-Port');
|
||||
if (xForwardedPort) {
|
||||
urlObject.port = xForwardedPort;
|
||||
}
|
||||
|
||||
// support add url prefix by sending X-Forwarded-Path http header
|
||||
const xForwardedPath = req.get('X-Forwarded-Path');
|
||||
if (xForwardedPath) {
|
||||
urlObject.pathname = path.posix.join(xForwardedPath, urlObject.pathname);
|
||||
}
|
||||
return urlObject;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the public URL, either from a provided publicUrl or generated from the request.
|
||||
* @param {string} publicUrl - The optional public URL to use.
|
||||
* @param {object} req - The Express request object.
|
||||
* @returns {string} - The final public URL string.
|
||||
*/
|
||||
export function getPublicUrl(publicUrl, req) {
|
||||
export const getPublicUrl = (publicUrl, req) => {
|
||||
if (publicUrl) {
|
||||
return publicUrl;
|
||||
}
|
||||
return getUrlObject(req).toString();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Generates an array of tile URLs based on given parameters.
|
||||
* @param {object} req - Express request object.
|
||||
* @param {string | string[]} domains - Domain(s) to use for tile URLs.
|
||||
* @param {string} path - The base path for the tiles.
|
||||
* @param {number} [tileSize] - The size of the tile (optional).
|
||||
* @param {string} format - The format of the tiles (e.g., 'png', 'jpg').
|
||||
* @param {string} publicUrl - The public URL to use (if not using domains).
|
||||
* @param {object} [aliases] - Aliases for format extensions.
|
||||
* @returns {string[]} An array of tile URL strings.
|
||||
*/
|
||||
export function getTileUrls(
|
||||
req,
|
||||
domains,
|
||||
path,
|
||||
tileSize,
|
||||
format,
|
||||
publicUrl,
|
||||
aliases,
|
||||
) {
|
||||
export const getTileUrls = (req, domains, path, format, publicUrl, aliases) => {
|
||||
const urlObject = getUrlObject(req);
|
||||
if (domains) {
|
||||
if (domains.constructor === String && domains.length > 0) {
|
||||
|
@ -184,38 +68,21 @@ export function getTileUrls(
|
|||
format = aliases[format];
|
||||
}
|
||||
|
||||
let tileParams = `{z}/{x}/{y}`;
|
||||
if (tileSize && ['png', 'jpg', 'jpeg', 'webp'].includes(format)) {
|
||||
tileParams = `${tileSize}/{z}/{x}/{y}`;
|
||||
}
|
||||
|
||||
if (format && format != '') {
|
||||
format = `.${format}`;
|
||||
} else {
|
||||
format = '';
|
||||
}
|
||||
|
||||
const uris = [];
|
||||
if (!publicUrl) {
|
||||
let xForwardedPath = `${req.get('X-Forwarded-Path') ? '/' + req.get('X-Forwarded-Path') : ''}`;
|
||||
for (const domain of domains) {
|
||||
uris.push(
|
||||
`${req.protocol}://${domain}${xForwardedPath}/${path}/${tileParams}${format}${query}`,
|
||||
`${req.protocol}://${domain}/${path}/{z}/{x}/{y}.${format}${query}`,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
uris.push(`${publicUrl}${path}/${tileParams}${format}${query}`);
|
||||
uris.push(`${publicUrl}${path}/{z}/{x}/{y}.${format}${query}`);
|
||||
}
|
||||
|
||||
return uris;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
export const fixTileJSONCenter = (tileJSON) => {
|
||||
if (tileJSON.bounds && !tileJSON.center) {
|
||||
const fitWidth = 1024;
|
||||
const tiles = fitWidth / 256;
|
||||
|
@ -228,77 +95,19 @@ export function fixTileJSONCenter(tileJSON) {
|
|||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
const getFontPbf = (allowedFonts, fontPath, name, range, fallbacks) =>
|
||||
new Promise((resolve, reject) => {
|
||||
if (!allowedFonts || (allowedFonts[name] && fallbacks)) {
|
||||
const fontMatch = name?.match(/^[\p{L}\p{N} \-\.~!*'()@&=+,#$\[\]]+$/u);
|
||||
const sanitizedName = fontMatch?.[0] || 'invalid';
|
||||
if (!name || typeof name !== 'string' || name.trim() === '' || !fontMatch) {
|
||||
console.error(
|
||||
'ERROR: Invalid font name: %s',
|
||||
sanitizedName.replace(/\n|\r/g, ''),
|
||||
);
|
||||
throw new Error('Invalid font name');
|
||||
}
|
||||
|
||||
const rangeMatch = range?.match(/^[\d-]+$/);
|
||||
const sanitizedRange = rangeMatch?.[0] || 'invalid';
|
||||
if (!/^\d+-\d+$/.test(range)) {
|
||||
console.error(
|
||||
'ERROR: Invalid range: %s',
|
||||
sanitizedRange.replace(/\n|\r/g, ''),
|
||||
);
|
||||
throw new Error('Invalid range');
|
||||
}
|
||||
const filename = path.join(
|
||||
fontPath,
|
||||
sanitizedName,
|
||||
`${sanitizedRange}.pbf`,
|
||||
);
|
||||
|
||||
const filename = path.join(fontPath, name, `${range}.pbf`);
|
||||
if (!fallbacks) {
|
||||
fallbacks = clone(allowedFonts || {});
|
||||
}
|
||||
delete fallbacks[name];
|
||||
|
||||
try {
|
||||
const data = await readFile(filename);
|
||||
return data;
|
||||
} catch (err) {
|
||||
console.error(
|
||||
'ERROR: Font not found: %s, Error: %s',
|
||||
filename.replace(/\n|\r/g, ''),
|
||||
String(err),
|
||||
);
|
||||
fs.readFile(filename, (err, data) => {
|
||||
if (err) {
|
||||
console.error(`ERROR: Font not found: ${name}`);
|
||||
if (fallbacks && Object.keys(fallbacks).length) {
|
||||
let fallbackName;
|
||||
|
||||
|
@ -313,37 +122,32 @@ async function getFontPbf(allowedFonts, fontPath, name, range, fallbacks) {
|
|||
fallbackName = Object.keys(fallbacks)[0];
|
||||
}
|
||||
}
|
||||
console.error(
|
||||
`ERROR: Trying to use %s as a fallback for: %s`,
|
||||
fallbackName,
|
||||
sanitizedName,
|
||||
);
|
||||
|
||||
console.error(`ERROR: Trying to use ${fallbackName} as a fallback`);
|
||||
delete fallbacks[fallbackName];
|
||||
return getFontPbf(null, fontPath, fallbackName, range, fallbacks);
|
||||
getFontPbf(null, fontPath, fallbackName, range, fallbacks).then(
|
||||
resolve,
|
||||
reject,
|
||||
);
|
||||
} else {
|
||||
throw new Error('Font load error');
|
||||
}
|
||||
reject(`Font load error: ${name}`);
|
||||
}
|
||||
} else {
|
||||
throw new Error('Font not allowed');
|
||||
resolve(data);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
reject(`Font not allowed: ${name}`);
|
||||
}
|
||||
/**
|
||||
* Combines multiple font pbf buffers into one.
|
||||
* @param {object} allowedFonts - An object of allowed fonts.
|
||||
* @param {string} fontPath - The path to the font directory.
|
||||
* @param {string} names - Comma-separated font names.
|
||||
* @param {string} range - The range of the font (e.g., '0-255').
|
||||
* @param {object} [fallbacks] - Fallback font list.
|
||||
* @returns {Promise<Buffer>} - A promise that resolves to the combined font data buffer.
|
||||
*/
|
||||
export async function getFontsPbf(
|
||||
});
|
||||
|
||||
export const getFontsPbf = (
|
||||
allowedFonts,
|
||||
fontPath,
|
||||
names,
|
||||
range,
|
||||
fallbacks,
|
||||
) {
|
||||
) => {
|
||||
const fonts = names.split(',');
|
||||
const queue = [];
|
||||
for (const font of fonts) {
|
||||
|
@ -358,73 +162,5 @@ export async function getFontsPbf(
|
|||
);
|
||||
}
|
||||
|
||||
const combined = combine(await Promise.all(queue), names);
|
||||
return Buffer.from(combined.buffer, 0, combined.buffer.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Lists available fonts in a given font directory.
|
||||
* @param {string} fontPath - The path to the font directory.
|
||||
* @returns {Promise<object>} - Promise that resolves with an object where keys are the font names.
|
||||
*/
|
||||
export async function listFonts(fontPath) {
|
||||
const existingFonts = {};
|
||||
|
||||
const files = await fsPromises.readdir(fontPath);
|
||||
for (const file of files) {
|
||||
const stats = await fsPromises.stat(path.join(fontPath, file));
|
||||
if (
|
||||
stats.isDirectory() &&
|
||||
(await existsP(path.join(fontPath, file, '0-255.pbf')))
|
||||
) {
|
||||
existingFonts[path.basename(file)] = true;
|
||||
}
|
||||
}
|
||||
|
||||
return existingFonts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a string is a valid HTTP or HTTPS URL.
|
||||
* @param {string} string - The string to validate.
|
||||
* @returns {boolean} True if the string is a valid HTTP/HTTPS URL, false otherwise.
|
||||
*/
|
||||
export function isValidHttpUrl(string) {
|
||||
let url;
|
||||
|
||||
try {
|
||||
url = new URL(string);
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return url.protocol === 'http:' || url.protocol === 'https:';
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches tile data from either PMTiles or MBTiles source.
|
||||
* @param {object} source - The source object, which may contain a mbtiles object, or pmtiles object.
|
||||
* @param {string} sourceType - The source type, which should be `pmtiles` or `mbtiles`
|
||||
* @param {number} z - The zoom level.
|
||||
* @param {number} x - The x coordinate of the tile.
|
||||
* @param {number} y - The y coordinate of the tile.
|
||||
* @returns {Promise<object | null>} - A promise that resolves to an object with data and headers or null if no data is found.
|
||||
*/
|
||||
export async function fetchTileData(source, sourceType, z, x, y) {
|
||||
if (sourceType === 'pmtiles') {
|
||||
return await new Promise(async (resolve) => {
|
||||
const tileinfo = await getPMtilesTile(source, z, x, y);
|
||||
if (!tileinfo?.data) return resolve(null);
|
||||
resolve({ data: tileinfo.data, headers: tileinfo.header });
|
||||
});
|
||||
} else if (sourceType === 'mbtiles') {
|
||||
return await new Promise((resolve) => {
|
||||
source.getTile(z, x, y, (err, tileData, tileHeader) => {
|
||||
if (err) {
|
||||
return resolve(null);
|
||||
}
|
||||
resolve({ data: tileData, headers: tileHeader });
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
return Promise.all(queue).then((values) => glyphCompose.combine(values));
|
||||
};
|
||||
|
|
|
@ -7,10 +7,10 @@ import { server } from '../src/server.js';
|
|||
global.expect = expect;
|
||||
global.supertest = supertest;
|
||||
|
||||
before(async function () {
|
||||
before(function () {
|
||||
console.log('global setup');
|
||||
process.chdir('test_data');
|
||||
const running = await server({
|
||||
const running = server({
|
||||
configPath: 'config.json',
|
||||
port: 8888,
|
||||
publicUrl: '/test/',
|
||||
|
@ -24,5 +24,6 @@ after(function () {
|
|||
console.log('global teardown');
|
||||
global.server.close(function () {
|
||||
console.log('Done');
|
||||
process.exit();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -78,7 +78,7 @@ describe('Static endpoints', function () {
|
|||
testStatic(prefix, '0,0,0/256x256', 'png', 404, 1);
|
||||
|
||||
testStatic(prefix, '0,0,-1/256x256', 'png', 404);
|
||||
testStatic(prefix, '0,0,0/256.5x256.5', 'png', 400);
|
||||
testStatic(prefix, '0,0,0/256.5x256.5', 'png', 404);
|
||||
|
||||
testStatic(prefix, '0,0,0,/256x256', 'png', 404);
|
||||
testStatic(prefix, '0,0,0,0,/256x256', 'png', 404);
|
||||
|
@ -135,7 +135,7 @@ describe('Static endpoints', function () {
|
|||
|
||||
testStatic(prefix, '0,0,1,1/1x1', 'gif', 400);
|
||||
|
||||
testStatic(prefix, '-180,-80,180,80/0.5x2.6', 'png', 400);
|
||||
testStatic(prefix, '-180,-80,180,80/0.5x2.6', 'png', 404);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -171,18 +171,6 @@ describe('Static endpoints', function () {
|
|||
'?path=-10,-10|-20,-20',
|
||||
);
|
||||
});
|
||||
|
||||
describe('encoded path', function () {
|
||||
testStatic(
|
||||
prefix,
|
||||
'auto/20x20',
|
||||
'png',
|
||||
200,
|
||||
2,
|
||||
/image\/png/,
|
||||
'?path=' + encodeURIComponent('enc:{{biGwvyGoUi@s_A|{@'),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('invalid requests return 4xx', function () {
|
||||
|
|
|
@ -41,16 +41,6 @@ describe('Styles', function () {
|
|||
testIs('/styles/' + prefix + '/sprite.png', /image\/png/);
|
||||
testIs('/styles/' + prefix + '/sprite@2x.png', /image\/png/);
|
||||
});
|
||||
|
||||
describe('/styles/' + prefix + '/sprite/default[@2x].{format}', function () {
|
||||
testIs('/styles/' + prefix + '/sprite/default.json', /application\/json/);
|
||||
testIs(
|
||||
'/styles/' + prefix + '/sprite/default@2x.json',
|
||||
/application\/json/,
|
||||
);
|
||||
testIs('/styles/' + prefix + '/sprite/default.png', /image\/png/);
|
||||
testIs('/styles/' + prefix + '/sprite/default@2x.png', /image\/png/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Fonts', function () {
|
||||
|
|
|
@ -1,30 +1,8 @@
|
|||
var testTile = function (
|
||||
prefix,
|
||||
tileSize = 256,
|
||||
z,
|
||||
x,
|
||||
y,
|
||||
format,
|
||||
status,
|
||||
scale,
|
||||
type,
|
||||
) {
|
||||
const testTile = function (prefix, z, x, y, format, status, scale, type) {
|
||||
if (scale) y += '@' + scale + 'x';
|
||||
var path =
|
||||
'/styles/' +
|
||||
prefix +
|
||||
'/' +
|
||||
tileSize +
|
||||
'/' +
|
||||
z +
|
||||
'/' +
|
||||
x +
|
||||
'/' +
|
||||
y +
|
||||
'.' +
|
||||
format;
|
||||
const path = '/styles/' + prefix + '/' + z + '/' + x + '/' + y + '.' + format;
|
||||
it(path + ' returns ' + status, function (done) {
|
||||
var test = supertest(app).get(path);
|
||||
const test = supertest(app).get(path);
|
||||
test.expect(status);
|
||||
if (type) test.expect('Content-Type', type);
|
||||
test.end(done);
|
||||
|
@ -36,40 +14,33 @@ const prefix = 'test-style';
|
|||
describe('Raster tiles', function () {
|
||||
describe('valid requests', function () {
|
||||
describe('various formats', function () {
|
||||
testTile(prefix, 256, 0, 0, 0, 'png', 200, undefined, /image\/png/);
|
||||
testTile(prefix, 512, 0, 0, 0, 'png', 200, undefined, /image\/png/);
|
||||
testTile(prefix, 256, 0, 0, 0, 'jpg', 200, undefined, /image\/jpeg/);
|
||||
testTile(prefix, 512, 0, 0, 0, 'jpg', 200, undefined, /image\/jpeg/);
|
||||
testTile(prefix, 256, 0, 0, 0, 'jpeg', 200, undefined, /image\/jpeg/);
|
||||
testTile(prefix, 512, 0, 0, 0, 'jpeg', 200, undefined, /image\/jpeg/);
|
||||
testTile(prefix, 256, 0, 0, 0, 'webp', 200, undefined, /image\/webp/);
|
||||
testTile(prefix, 512, 0, 0, 0, 'webp', 200, undefined, /image\/webp/);
|
||||
testTile(prefix, 0, 0, 0, 'png', 200, undefined, /image\/png/);
|
||||
testTile(prefix, 0, 0, 0, 'jpg', 200, undefined, /image\/jpeg/);
|
||||
testTile(prefix, 0, 0, 0, 'jpeg', 200, undefined, /image\/jpeg/);
|
||||
testTile(prefix, 0, 0, 0, 'webp', 200, undefined, /image\/webp/);
|
||||
});
|
||||
|
||||
describe('different coordinates and scales', function () {
|
||||
testTile(prefix, 256, 1, 0, 0, 'png', 200);
|
||||
testTile(prefix, 512, 1, 0, 0, 'png', 200);
|
||||
testTile(prefix, 256, 0, 0, 0, 'png', 200, 2);
|
||||
testTile(prefix, 512, 0, 0, 0, 'png', 200, 2);
|
||||
testTile(prefix, 256, 0, 0, 0, 'png', 200, 3);
|
||||
testTile(prefix, 512, 0, 0, 0, 'png', 200, 3);
|
||||
testTile(prefix, 256, 2, 1, 1, 'png', 200, 3);
|
||||
testTile(prefix, 512, 2, 1, 1, 'png', 200, 3);
|
||||
testTile(prefix, 1, 1, 1, 'png', 200);
|
||||
|
||||
testTile(prefix, 0, 0, 0, 'png', 200, 2);
|
||||
testTile(prefix, 0, 0, 0, 'png', 200, 3);
|
||||
testTile(prefix, 2, 1, 1, 'png', 200, 3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('invalid requests return 4xx', function () {
|
||||
testTile('non_existent', 256, 0, 0, 0, 'png', 404);
|
||||
testTile(prefix, 256, -1, 0, 0, 'png', 400);
|
||||
testTile(prefix, 256, 25, 0, 0, 'png', 400);
|
||||
testTile(prefix, 256, 0, 1, 0, 'png', 400);
|
||||
testTile(prefix, 256, 0, 0, 1, 'png', 400);
|
||||
testTile(prefix, 256, 0, 0, 0, 'gif', 400);
|
||||
testTile(prefix, 256, 0, 0, 0, 'pbf', 400);
|
||||
testTile('non_existent', 0, 0, 0, 'png', 404);
|
||||
testTile(prefix, -1, 0, 0, 'png', 404);
|
||||
testTile(prefix, 25, 0, 0, 'png', 404);
|
||||
testTile(prefix, 0, 1, 0, 'png', 404);
|
||||
testTile(prefix, 0, 0, 1, 'png', 404);
|
||||
testTile(prefix, 0, 0, 0, 'gif', 400);
|
||||
testTile(prefix, 0, 0, 0, 'pbf', 400);
|
||||
|
||||
testTile(prefix, 256, 0, 0, 0, 'png', 400, 1);
|
||||
testTile(prefix, 256, 0, 0, 0, 'png', 400, 5);
|
||||
testTile(prefix, 0, 0, 0, 'png', 404, 1);
|
||||
testTile(prefix, 0, 0, 0, 'png', 404, 5);
|
||||
|
||||
testTile(prefix, 300, 0, 0, 0, 'png', 400);
|
||||
// testTile('hybrid', 0, 0, 0, 'png', 404); //TODO: test this
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue