diff --git a/docs/endpoints.rst b/docs/endpoints.rst
index d025e46..bc42804 100644
--- a/docs/endpoints.rst
+++ b/docs/endpoints.rst
@@ -13,10 +13,11 @@ Styles
Rendered tiles
==============
-* Rendered tiles are served at ``/styles/{id}/{z}/{x}/{y}[@2x].{format}``
+* Rendered tiles are served at ``/styles/{id}/{tileSize}/{z}/{x}/{y}[@2x].{format}``
* The optional ``@2x`` (or ``@3x``, ``@4x``) part can be used to render HiDPI (retina) tiles
* Available formats: ``png``, ``jpg`` (``jpeg``), ``webp``
+ * Tile sizes: ``256``, ``512``
* TileJSON at ``/styles/{id}.json``
* The rendered tiles are not available in the ``tileserver-gl-light`` version.
diff --git a/public/templates/wmts.tmpl b/public/templates/wmts.tmpl
index 0db7349..7a2a5dd 100644
--- a/public/templates/wmts.tmpl
+++ b/public/templates/wmts.tmpl
@@ -36,8 +36,8 @@
-
- {{name}}
+
+ {{name}}-256
{{id}}
-180 -85.051128779807
@@ -48,13 +48,30 @@
image/png
- GoogleMapsCompatible
+ GoogleMapsCompatible_256
-
-
- GoogleMapsCompatible
- GoogleMapsCompatible EPSG:3857
- GoogleMapsCompatible
+
+
+
+ {{name}}-512
+ {{id}}
+
+ -180 -85.051128779807
+ 180 85.051128779807
+
+
+ image/png
+
+ GoogleMapsCompatible_512
+
+
+
+
+ GoogleMapsCompatible_256
+ GoogleMapsCompatible_256 EPSG:3857
+ GoogleMapsCompatible_256
urn:ogc:def:crs:EPSG::3857
0
@@ -226,10 +243,189 @@
256
262144
262144
-
- WGS84
- WGS84 EPSG:4326
- WGS84
+
+
+
+ GoogleMapsCompatible_512
+ GoogleMapsCompatible_512 EPSG:3857
+ GoogleMapsCompatible_512
+ urn:ogc:def:crs:EPSG::3857
+
+ 0
+ 279541132.0143589
+ -20037508.34 20037508.34
+ 512
+ 512
+ 1
+ 1
+
+
+ 1
+ 139770566.0071794
+ -20037508.34 20037508.34
+ 512
+ 512
+ 2
+ 2
+
+
+ 2
+ 69885283.00358972
+ -20037508.34 20037508.34
+ 512
+ 512
+ 4
+ 4
+
+
+ 3
+ 34942641.501795
+ -20037508.34 20037508.34
+ 512
+ 512
+ 8
+ 8
+
+
+ 4
+ 17471320.750897
+ -20037508.34 20037508.34
+ 512
+ 512
+ 16
+ 16
+
+
+ 5
+ 8735660.3754487
+ -20037508.34 20037508.34
+ 512
+ 512
+ 32
+ 32
+
+
+ 6
+ 4367830.1877244
+ -20037508.34 20037508.34
+ 512
+ 512
+ 64
+ 64
+
+
+ 7
+ 2183915.0938622
+ -20037508.34 20037508.34
+ 512
+ 512
+ 128
+ 128
+
+
+ 8
+ 1091957.5469311
+ -20037508.34 20037508.34
+ 512
+ 512
+ 256
+ 256
+
+
+ 9
+ 545978.77346554
+ -20037508.34 20037508.34
+ 512
+ 512
+ 512
+ 512
+
+
+ 10
+ 272989.38673277
+ -20037508.34 20037508.34
+ 512
+ 512
+ 1024
+ 1024
+
+
+ 11
+ 136494.69336639
+ -20037508.34 20037508.34
+ 512
+ 512
+ 2048
+ 2048
+
+
+ 12
+ 68247.346683193
+ -20037508.34 20037508.34
+ 512
+ 512
+ 4096
+ 4096
+
+
+ 13
+ 34123.673341597
+ -20037508.34 20037508.34
+ 512
+ 512
+ 8192
+ 8192
+
+
+ 14
+ 17061.836670798
+ -20037508.34 20037508.34
+ 512
+ 512
+ 16384
+ 16384
+
+
+ 15
+ 8530.9183353991
+ -20037508.34 20037508.34
+ 512
+ 512
+ 32768
+ 32768
+
+
+ 16
+ 4265.4591676996
+ -20037508.34 20037508.34
+ 512
+ 512
+ 65536
+ 65536
+
+
+ 17
+ 2132.7295838498
+ -20037508.34 20037508.34
+ 512
+ 512
+ 131072
+ 131072
+
+
+ 18
+ 1066.364791924892
+ -20037508.34 20037508.34
+ 512
+ 512
+ 262144
+ 262144
+
+
+
+ WGS84_256
+ WGS84_256 EPSG:4326
+ WGS84_256
urn:ogc:def:crs:EPSG::4326
0
@@ -401,7 +597,185 @@
256
524288
262144
-
+
+
+
+ WGS84_512
+ WGS84_512 EPSG:4326
+ WGS84_512
+ urn:ogc:def:crs:EPSG::4326
+
+ 0
+ 139770566.00718
+ 90 -180
+ 512
+ 512
+ 2
+ 1
+
+
+ 1
+ 69885283.00359
+ 90 -180
+ 512
+ 512
+ 4
+ 2
+
+
+ 2
+ 34942641.501795
+ 90 -180
+ 512
+ 512
+ 8
+ 4
+
+
+ 3
+ 17471320.750897
+ 90 -180
+ 512
+ 512
+ 16
+ 8
+
+
+ 4
+ 8735660.3754487
+ 90 -180
+ 512
+ 512
+ 32
+ 16
+
+
+ 5
+ 4367830.1877244
+ 90 -180
+ 512
+ 512
+ 64
+ 32
+
+
+ 6
+ 2183915.0938622
+ 90 -180
+ 512
+ 512
+ 128
+ 64
+
+
+ 7
+ 1091957.5469311
+ 90 -180
+ 512
+ 512
+ 256
+ 128
+
+
+ 8
+ 545978.77346554
+ 90 -180
+ 512
+ 512
+ 512
+ 256
+
+
+ 9
+ 272989.38673277
+ 90 -180
+ 512
+ 512
+ 1024
+ 512
+
+
+ 10
+ 136494.69336639
+ 90 -180
+ 512
+ 512
+ 2048
+ 1024
+
+
+ 11
+ 68247.346683193
+ 90 -180
+ 512
+ 512
+ 4096
+ 2048
+
+
+ 12
+ 34123.673341597
+ 90 -180
+ 512
+ 512
+ 8192
+ 4096
+
+
+ 13
+ 17061.836670798
+ 90 -180
+ 512
+ 512
+ 16384
+ 8192
+
+
+ 14
+ 8530.9183353991
+ 90 -180
+ 512
+ 512
+ 32768
+ 16384
+
+
+ 15
+ 4265.4591676996
+ 90 -180
+ 512
+ 512
+ 65536
+ 32768
+
+
+ 16
+ 2132.7295838498
+ 90 -180
+ 512
+ 512
+ 131072
+ 65536
+
+
+ 17
+ 1066.3647919249
+ 90 -180
+ 512
+ 512
+ 262144
+ 131072
+
+
+ 18
+ 533.182
+ 90 -180
+ 512
+ 512
+ 524288
+ 262144
+
+
diff --git a/src/serve_rendered.js b/src/serve_rendered.js
index 88f2012..fdc015c 100644
--- a/src/serve_rendered.js
+++ b/src/serve_rendered.js
@@ -1,59 +1,37 @@
'use strict';
-// SECTION START
-//
-// The order of the two imports below is important.
-// For an unknown reason, if the order is reversed, rendering can crash.
-// This happens on ARM:
-// > terminate called after throwing an instance of 'std::runtime_error'
-// > what(): Cannot read GLX extensions.
-import 'canvas';
-import '@maplibre/maplibre-gl-native';
-//
-// SECTION END
+const advancedPool = require('advanced-pool');
+const fs = require('fs');
+const path = require('path');
+const url = require('url');
+const util = require('util');
+const zlib = require('zlib');
-import advancedPool from 'advanced-pool';
-import fs from 'node:fs';
-import path from 'path';
-import url from 'url';
-import util from 'util';
-import zlib from 'zlib';
-import sharp from 'sharp';
-import clone from 'clone';
-import Color from 'color';
-import express from 'express';
-import sanitize from 'sanitize-filename';
-import SphericalMercator from '@mapbox/sphericalmercator';
-import mlgl from '@maplibre/maplibre-gl-native';
-import MBTiles from '@mapbox/mbtiles';
-import polyline from '@mapbox/polyline';
-import proj4 from 'proj4';
-import axios from 'axios';
-import {
- getFontsPbf,
- listFonts,
- getTileUrls,
- isValidHttpUrl,
- fixTileJSONCenter,
-} from './utils.js';
-import {
- openPMtiles,
- getPMtilesInfo,
- getPMtilesTile,
-} from './pmtiles_adapter.js';
-import { renderOverlay, renderWatermark, renderAttribution } from './render.js';
+// sharp has to be required before node-canvas
+// see https://github.com/lovell/sharp/issues/371
+const sharp = require('sharp');
-const FLOAT_PATTERN = '[+-]?(?:\\d+|\\d+.?\\d+)';
-const PATH_PATTERN =
- /^((fill|stroke|width)\:[^\|]+\|)*(enc:.+|-?\d+(\.\d*)?,-?\d+(\.\d*)?(\|-?\d+(\.\d*)?,-?\d+(\.\d*)?)+)/;
+const { createCanvas } = require('canvas');
+
+const clone = require('clone');
+const Color = require('color');
+const express = require('express');
+const mercator = new (require('@mapbox/sphericalmercator'))();
+const mbgl = require('@mapbox/mapbox-gl-native');
+const MBTiles = require('@mapbox/mbtiles');
+const proj4 = require('proj4');
+const request = require('request');
+
+const utils = require('./utils');
+
+const FLOAT_PATTERN = '[+-]?(?:\\d+|\\d+\.?\\d+)';
const httpTester = /^(http(s)?:)?\/\//;
-const mercator = new SphericalMercator();
-const getScale = (scale) => (scale || '@1x').slice(1, 2) | 0;
+const getScale = scale => (scale || '@1x').slice(1, 2) | 0;
-mlgl.on('message', (e) => {
+mbgl.on('message', e => {
if (e.severity === 'WARNING' || e.severity === 'ERROR') {
- console.log('mlgl:', e);
+ console.log('mbgl:', e);
}
});
@@ -64,7 +42,7 @@ const extensionToFormat = {
'.jpg': 'jpeg',
'.jpeg': 'jpeg',
'.png': 'png',
- '.webp': 'webp',
+ '.webp': 'webp'
};
/**
@@ -72,14 +50,14 @@ const extensionToFormat = {
* string is for unknown or unsupported formats.
*/
const cachedEmptyResponses = {
- '': Buffer.alloc(0),
+ '': Buffer.alloc(0)
};
/**
- * Create an appropriate mlgl response for http errors.
+ * Create an appropriate mbgl response for http errors.
* @param {string} format The format (a sharp format or 'pbf').
* @param {string} color The background color (or empty string for transparent).
- * @param {Function} callback The mlgl callback.
+ * @param {Function} callback The mbgl callback.
*/
function createEmptyResponse(format, color, callback) {
if (!format || format === 'pbf') {
@@ -109,419 +87,139 @@ function createEmptyResponse(format, color, callback) {
raw: {
width: 1,
height: 1,
- channels,
- },
- })
- .toFormat(format)
- .toBuffer((err, buffer, info) => {
- if (!err) {
- cachedEmptyResponses[cacheKey] = buffer;
- }
- callback(null, { data: buffer });
- });
+ channels: channels
+ }
+ }).toFormat(format).toBuffer((err, buffer, info) => {
+ if (!err) {
+ cachedEmptyResponses[cacheKey] = buffer;
+ }
+ callback(null, { data: buffer });
+ });
}
-/**
- * Parses coordinate pair provided to pair of floats and ensures the resulting
- * pair is a longitude/latitude combination depending on lnglat query parameter.
- * @param {List} coordinatePair Coordinate pair.
- * @param coordinates
- * @param {object} query Request query parameters.
- */
-const parseCoordinatePair = (coordinates, query) => {
- const firstCoordinate = parseFloat(coordinates[0]);
- const secondCoordinate = parseFloat(coordinates[1]);
+const extractPathFromQuery = (query, transformer) => {
+ const pathParts = (query.path || '').split('|');
+ const path = [];
+ for (const pair of pathParts) {
+ const pairParts = pair.split(',');
+ if (pairParts.length === 2) {
+ let pair;
+ if (query.latlng === '1' || query.latlng === 'true') {
+ pair = [+(pairParts[1]), +(pairParts[0])];
+ } else {
+ pair = [+(pairParts[0]), +(pairParts[1])];
+ }
+ if (transformer) {
+ pair = transformer(pair);
+ }
+ path.push(pair);
+ }
+ }
+ return path;
+};
- // Ensure provided coordinates could be parsed and abort if not
- if (isNaN(firstCoordinate) || isNaN(secondCoordinate)) {
+const renderOverlay = (z, x, y, bearing, pitch, w, h, scale,
+ path, query) => {
+ if (!path || path.length < 2) {
return null;
}
+ const precisePx = (ll, zoom) => {
+ const px = mercator.px(ll, 20);
+ const scale = Math.pow(2, zoom - 20);
+ return [px[0] * scale, px[1] * scale];
+ };
- // Check if coordinates have been provided as lat/lng pair instead of the
- // ususal lng/lat pair and ensure resulting pair is lng/lat
- if (query.latlng === '1' || query.latlng === 'true') {
- return [secondCoordinate, firstCoordinate];
+ 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;
}
- return [firstCoordinate, secondCoordinate];
-};
-
-/**
- * Parses a coordinate pair from query arguments and optionally transforms it.
- * @param {List} coordinatePair Coordinate pair.
- * @param {object} query Request query parameters.
- * @param {Function} transformer Optional transform function.
- */
-const parseCoordinates = (coordinatePair, query, transformer) => {
- const parsedCoordinates = parseCoordinatePair(coordinatePair, query);
-
- // Transform coordinates
- if (transformer) {
- return transformer(parsedCoordinates);
+ 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);
+ }
+ const lineWidth = query.width !== undefined ?
+ parseFloat(query.width) : 1;
+ ctx.lineWidth = lineWidth;
+ ctx.strokeStyle = query.stroke || 'rgba(0,64,255,0.7)';
+ ctx.fillStyle = query.fill || 'rgba(255,255,255,0.4)';
+ ctx.beginPath();
+ for (const pair of path) {
+ const px = precisePx(pair, z);
+ ctx.lineTo(px[0], px[1]);
+ }
+ if (path[0][0] === path[path.length - 1][0] &&
+ path[0][1] === path[path.length - 1][1]) {
+ ctx.closePath();
+ }
+ ctx.fill();
+ if (lineWidth > 0) {
+ ctx.stroke();
}
- return parsedCoordinates;
-};
-
-/**
- * Parses paths provided via query into a list of path objects.
- * @param {object} query Request query parameters.
- * @param {Function} transformer Optional transform function.
- */
-const extractPathsFromQuery = (query, transformer) => {
- // Initiate paths array
- const paths = [];
- // Return an empty list if no paths have been provided
- if ('path' in query && !query.path) {
- return paths;
- }
- // Parse paths provided via path query argument
- if ('path' in query) {
- const providedPaths = Array.isArray(query.path) ? query.path : [query.path];
- // Iterate through paths, parse and validate them
- for (const providedPath of providedPaths) {
- // Logic for pushing coords to path when path includes google polyline
- if (providedPath.includes('enc:') && PATH_PATTERN.test(providedPath)) {
- // +4 because 'enc:' is 4 characters, everything after 'enc:' is considered to be part of the polyline
- const encIndex = providedPath.indexOf('enc:') + 4;
- const coords = polyline
- .decode(providedPath.substring(encIndex))
- .map(([lat, lng]) => [lng, lat]);
- paths.push(coords);
- } else {
- // Iterate through paths, parse and validate them
- const currentPath = [];
-
- // Extract coordinate-list from path
- const pathParts = (providedPath || '').split('|');
-
- // Iterate through coordinate-list, parse the coordinates and validate them
- for (const pair of pathParts) {
- // Extract coordinates from coordinate pair
- const pairParts = pair.split(',');
- // Ensure we have two coordinates
- if (pairParts.length === 2) {
- const pair = parseCoordinates(pairParts, query, transformer);
-
- // Ensure coordinates could be parsed and skip them if not
- if (pair === null) {
- continue;
- }
-
- // Add the coordinate-pair to the current path if they are valid
- currentPath.push(pair);
- }
- }
- // Extend list of paths with current path if it contains coordinates
- if (currentPath.length) {
- paths.push(currentPath);
- }
- }
- }
- }
- return paths;
-};
-
-/**
- * Parses marker options provided via query and sets corresponding attributes
- * on marker object.
- * Options adhere to the following format
- * [optionName]:[optionValue]
- * @param {List[String]} optionsList List of option strings.
- * @param {object} marker Marker object to configure.
- */
-const parseMarkerOptions = (optionsList, marker) => {
- for (const options of optionsList) {
- const optionParts = options.split(':');
- // Ensure we got an option name and value
- if (optionParts.length < 2) {
- continue;
- }
-
- switch (optionParts[0]) {
- // Scale factor to up- or downscale icon
- case 'scale':
- // Scale factors must not be negative
- marker.scale = Math.abs(parseFloat(optionParts[1]));
- break;
- // Icon offset as positive or negative pixel value in the following
- // format [offsetX],[offsetY] where [offsetY] is optional
- case 'offset':
- const providedOffset = optionParts[1].split(',');
- // Set X-axis offset
- marker.offsetX = parseFloat(providedOffset[0]);
- // Check if an offset has been provided for Y-axis
- if (providedOffset.length > 1) {
- marker.offsetY = parseFloat(providedOffset[1]);
- }
- break;
- }
- }
-};
-
-/**
- * Parses markers provided via query into a list of marker objects.
- * @param {object} query Request query parameters.
- * @param {object} options Configuration options.
- * @param {Function} transformer Optional transform function.
- */
-const extractMarkersFromQuery = (query, options, transformer) => {
- // Return an empty list if no markers have been provided
- if (!query.marker) {
- return [];
- }
-
- const markers = [];
-
- // Check if multiple markers have been provided and mimic a list if it's a
- // single maker.
- const providedMarkers = Array.isArray(query.marker)
- ? query.marker
- : [query.marker];
-
- // Iterate through provided markers which can have one of the following
- // formats
- // [location]|[pathToFileTelativeToConfiguredIconPath]
- // [location]|[pathToFile...]|[option]|[option]|...
- for (const providedMarker of providedMarkers) {
- const markerParts = providedMarker.split('|');
- // Ensure we got at least a location and an icon uri
- if (markerParts.length < 2) {
- continue;
- }
-
- const locationParts = markerParts[0].split(',');
- // Ensure the locationParts contains two items
- if (locationParts.length !== 2) {
- continue;
- }
-
- let iconURI = markerParts[1];
- // Check if icon is served via http otherwise marker icons are expected to
- // be provided as filepaths relative to configured icon path
- const isRemoteURL =
- iconURI.startsWith('http://') || iconURI.startsWith('https://');
- const isDataURL = iconURI.startsWith('data:');
- if (!(isRemoteURL || isDataURL)) {
- // Sanitize URI with sanitize-filename
- // https://www.npmjs.com/package/sanitize-filename#details
- iconURI = sanitize(iconURI);
-
- // If the selected icon is not part of available icons skip it
- if (!options.paths.availableIcons.includes(iconURI)) {
- continue;
- }
-
- iconURI = path.resolve(options.paths.icons, iconURI);
-
- // When we encounter a remote icon check if the configuration explicitly allows them.
- } else if (isRemoteURL && options.allowRemoteMarkerIcons !== true) {
- continue;
- } else if (isDataURL && options.allowInlineMarkerImages !== true) {
- continue;
- }
-
- // Ensure marker location could be parsed
- const location = parseCoordinates(locationParts, query, transformer);
- if (location === null) {
- continue;
- }
-
- const marker = {};
-
- marker.location = location;
- marker.icon = iconURI;
-
- // Check if options have been provided
- if (markerParts.length > 2) {
- parseMarkerOptions(markerParts.slice(2), marker);
- }
-
- // Add marker to list
- markers.push(marker);
- }
- return markers;
+ return canvas.toBuffer();
};
const calcZForBBox = (bbox, w, h, query) => {
let z = 25;
- const padding = query.padding !== undefined ? parseFloat(query.padding) : 0.1;
+ const padding = query.padding !== undefined ?
+ parseFloat(query.padding) : 0.1;
- const minCorner = mercator.px([bbox[0], bbox[3]], z);
- const maxCorner = mercator.px([bbox[2], bbox[1]], z);
+ const minCorner = mercator.px([bbox[0], bbox[3]], z),
+ maxCorner = mercator.px([bbox[2], bbox[1]], z);
const w_ = w / (1 + 2 * padding);
const h_ = h / (1 + 2 * padding);
- z -=
- Math.max(
- Math.log((maxCorner[0] - minCorner[0]) / w_),
- Math.log((maxCorner[1] - minCorner[1]) / h_),
- ) / Math.LN2;
+ z -= Math.max(
+ Math.log((maxCorner[0] - minCorner[0]) / w_),
+ Math.log((maxCorner[1] - minCorner[1]) / h_)
+ ) / Math.LN2;
z = Math.max(Math.log(Math.max(w, h) / 256) / Math.LN2, Math.min(25, z));
return z;
};
-const respondImage = (
- options,
- item,
- z,
- lon,
- lat,
- bearing,
- pitch,
- width,
- height,
- scale,
- format,
- res,
- overlay = null,
- mode = 'tile',
-) => {
- if (
- Math.abs(lon) > 180 ||
- Math.abs(lat) > 85.06 ||
- lon !== lon ||
- lat !== lat
- ) {
- return res.status(400).send('Invalid center');
- }
-
- if (
- Math.min(width, height) <= 0 ||
- Math.max(width, height) * scale > (options.maxSize || 2048) ||
- width !== width ||
- height !== height
- ) {
- return res.status(400).send('Invalid size');
- }
-
- if (format === 'png' || format === 'webp') {
- } else if (format === 'jpg' || format === 'jpeg') {
- format = 'jpeg';
- } else {
- return res.status(400).send('Invalid format');
- }
-
- const tileMargin = Math.max(options.tileMargin || 0, 0);
- let pool;
- if (mode === 'tile' && tileMargin === 0 && z !==0) {
- pool = item.map.renderers[scale];
- } else {
- pool = item.map.renderersStatic[scale];
- }
- pool.acquire((err, renderer) => {
- const mlglZ = Math.max(0, z - 1);
- const params = {
- zoom: mlglZ,
- center: [lon, lat],
- bearing,
- pitch,
- width,
- height,
- };
-
- if (z === 0) {
- params.width *= 2;
- params.height *= 2;
- }
-
- if (z > 2 && tileMargin > 0) {
- params.width += tileMargin * 2;
- params.height += tileMargin * 2;
- }
-
- renderer.render(params, (err, data) => {
- pool.release(renderer);
- if (err) {
- console.error(err);
- return res.status(500).header('Content-Type', 'text/plain').send(err);
- }
-
- const image = sharp(data, {
- raw: {
- premultiplied: true,
- width: params.width * scale,
- height: params.height * scale,
- channels: 4,
- },
- });
-
- if (z > 2 && tileMargin > 0) {
- const [_, y] = mercator.px(params.center, z);
- let yoffset = Math.max(
- Math.min(0, y - 128 - tileMargin),
- y + 128 + tileMargin - Math.pow(2, z + 8),
- );
- image.extract({
- left: tileMargin * scale,
- top: (tileMargin + yoffset) * scale,
- width: width * scale,
- height: height * scale,
- });
- }
-
- if (z === 0) {
- // HACK: when serving zoom 0, resize the 0 tile from 512 to 256
- image.resize(width * scale, height * scale);
- }
-
- const composites = [];
- if (overlay) {
- composites.push({ input: overlay });
- }
- if (item.watermark) {
- const canvas = renderWatermark(width, height, scale, item.watermark);
-
- composites.push({ input: canvas.toBuffer() });
- }
-
- if (mode === 'static' && item.staticAttributionText) {
- const canvas = renderAttribution(
- width,
- height,
- scale,
- item.staticAttributionText,
- );
-
- composites.push({ input: canvas.toBuffer() });
- }
-
- if (composites.length > 0) {
- image.composite(composites);
- }
-
- const formatQuality = (options.formatQuality || {})[format];
-
- if (format === 'png') {
- image.png({ adaptiveFiltering: false });
- } else if (format === 'jpeg') {
- image.jpeg({ quality: formatQuality || 80 });
- } else if (format === 'webp') {
- image.webp({ quality: formatQuality || 90 });
- }
- image.toBuffer((err, buffer, info) => {
- if (!buffer) {
- return res.status(404).send('Not found');
- }
-
- res.set({
- 'Last-Modified': item.lastModified,
- 'Content-Type': `image/${format}`,
- });
- return res.status(200).send(buffer);
- });
- });
- });
-};
-
const existingFonts = {};
let maxScaleFactor = 2;
-export const serve_rendered = {
- init: async (options, repo) => {
+module.exports = {
+ init: (options, repo) => {
+ 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(options.paths.fonts, file), (err, stats) => {
+ if (err) {
+ reject(err);
+ return;
+ }
+ if (stats.isDirectory()) {
+ existingFonts[path.basename(file)] = true;
+ }
+ });
+ }
+ resolve();
+ });
+ });
+
maxScaleFactor = Math.min(Math.floor(options.maxScaleFactor || 3), 9);
let scalePattern = '';
for (let i = 2; i <= maxScaleFactor; i++) {
@@ -531,241 +229,262 @@ export const serve_rendered = {
const app = express().disable('x-powered-by');
- app.get(
- `/:id/:z(\\d+)/:x(\\d+)/:y(\\d+):scale(${scalePattern})?.:format([\\w]+)`,
- (req, res, next) => {
- const item = repo[req.params.id];
- if (!item) {
- return res.sendStatus(404);
+ const respondImage = (item, z, lon, lat, bearing, pitch,
+ width, height, scale, format, res, next,
+ opt_overlay) => {
+ if (Math.abs(lon) > 180 || Math.abs(lat) > 85.06 ||
+ lon !== lon || lat !== lat) {
+ return res.status(400).send('Invalid center');
+ }
+ if (Math.min(width, height) <= 0 ||
+ Math.max(width, height) * scale > (options.maxSize || 2048) ||
+ width !== width || height !== height) {
+ return res.status(400).send('Invalid size');
+ }
+ if (format === 'png' || format === 'webp') {
+ } else if (format === 'jpg' || format === 'jpeg') {
+ format = 'jpeg';
+ } else {
+ return res.status(400).send('Invalid format');
+ }
+
+ const pool = item.map.renderers[scale];
+ pool.acquire((err, renderer) => {
+ const mbglZ = Math.max(0, z - 1);
+ const params = {
+ zoom: mbglZ,
+ center: [lon, lat],
+ bearing: bearing,
+ pitch: pitch,
+ width: width,
+ height: height
+ };
+ if (z === 0) {
+ params.width *= 2;
+ params.height *= 2;
}
- 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) <= new Date(modifiedSince)) {
- return res.sendStatus(304);
+ const tileMargin = Math.max(options.tileMargin || 0, 0);
+ if (z > 2 && tileMargin > 0) {
+ params.width += tileMargin * 2;
+ params.height += tileMargin * 2;
+ }
+
+ renderer.render(params, (err, data) => {
+ pool.release(renderer);
+ if (err) {
+ console.error(err);
+ return res.status(500).send(err);
}
- }
- const z = req.params.z | 0;
- const x = req.params.x | 0;
- const y = req.params.y | 0;
- const scale = getScale(req.params.scale);
- const format = req.params.format;
- const tileSize = 256;
- if (
- z < 0 ||
- x < 0 ||
- y < 0 ||
- z > 22 ||
- x >= Math.pow(2, z) ||
- y >= Math.pow(2, z)
- ) {
- return res.status(404).send('Out of bounds');
- }
- const tileCenter = mercator.ll(
- [
- ((x + 0.5) / (1 << z)) * (tileSize << z),
- ((y + 0.5) / (1 << z)) * (tileSize << z),
- ],
- z,
- );
-
- // prettier-ignore
- return respondImage(
- options, item, z, tileCenter[0], tileCenter[1], 0, 0, tileSize, tileSize, scale, format, res,
- );
- },
- );
-
- app.get(
- `/:id/:tileSize(256|512)/:z(\\d+)/:x(\\d+)/:y(\\d+):scale(${scalePattern})?.:format([\\w]+)`,
- (req, res, next) => {
- const item = repo[req.params.id];
- if (!item) {
- return res.sendStatus(404);
- }
-
- 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) <= new Date(modifiedSince)) {
- return res.sendStatus(304);
- }
- }
-
- const z = req.params.z | 0;
- const x = req.params.x | 0;
- const y = req.params.y | 0;
- const scale = getScale(req.params.scale);
- const format = req.params.format;
- const tileSize = parseInt(req.params.tileSize, 10) || 256;
- console.log(tileSize);
- if (
- z < 0 ||
- x < 0 ||
- y < 0 ||
- z > 22 ||
- x >= Math.pow(2, z) ||
- y >= Math.pow(2, z)
- ) {
- return res.status(404).send('Out of bounds');
- }
- const tileCenter = mercator.ll(
- [
- ((x + 0.5) / (1 << z)) * (tileSize << z),
- ((y + 0.5) / (1 << z)) * (tileSize << z),
- ],
- z,
- );
-
- // prettier-ignore
- return respondImage(
- options, item, z, tileCenter[0], tileCenter[1], 0, 0, tileSize, tileSize, scale, format, res,
- );
- },
- );
-
- if (options.serveStaticMaps !== false) {
- const staticPattern = `/:id/static/:raw(raw)?/%s/:width(\\d+)x:height(\\d+):scale(${scalePattern})?.:format([\\w]+)`;
-
- const centerPattern = util.format(
- ':x(%s),:y(%s),:z(%s)(@:bearing(%s)(,:pitch(%s))?)?',
- FLOAT_PATTERN,
- FLOAT_PATTERN,
- FLOAT_PATTERN,
- FLOAT_PATTERN,
- FLOAT_PATTERN,
- );
-
- app.get(
- util.format(staticPattern, centerPattern),
- async (req, res, next) => {
- try {
- const item = repo[req.params.id];
- if (!item) {
- return res.sendStatus(404);
+ // Fix semi-transparent outlines on raw, premultiplied input
+ // https://github.com/maptiler/tileserver-gl/issues/350#issuecomment-477857040
+ for (var i = 0; i < data.length; i += 4) {
+ var alpha = data[i + 3];
+ var norm = alpha / 255;
+ if (alpha === 0) {
+ data[i] = 0;
+ data[i + 1] = 0;
+ data[i + 2] = 0;
+ } else {
+ data[i] = data[i] / norm;
+ data[i + 1] = data[i + 1] / norm;
+ data[i + 2] = data[i + 2] / norm;
}
- const raw = req.params.raw;
- const z = +req.params.z;
- let x = +req.params.x;
- let y = +req.params.y;
- const bearing = +(req.params.bearing || '0');
- const pitch = +(req.params.pitch || '0');
- const w = req.params.width | 0;
- const h = req.params.height | 0;
- const scale = getScale(req.params.scale);
- const format = req.params.format;
+ }
- if (z < 0) {
- return res.status(404).send('Invalid zoom');
+ const image = sharp(data, {
+ raw: {
+ width: params.width * scale,
+ height: params.height * scale,
+ channels: 4
+ }
+ });
+
+ if (z > 2 && tileMargin > 0) {
+ image.extract({
+ left: tileMargin * scale,
+ top: tileMargin * scale,
+ width: width * scale,
+ height: height * scale
+ });
+ }
+
+ if (z === 0) {
+ // HACK: when serving zoom 0, resize the 0 tile from 512 to 256
+ image.resize(width * scale, height * scale);
+ }
+
+ if (opt_overlay) {
+ image.composite([{ input: opt_overlay }]);
+ }
+ if (item.watermark) {
+ 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(item.watermark, 5, height - 5);
+ ctx.fillStyle = 'rgba(0,0,0,.4)';
+ ctx.fillText(item.watermark, 5, height - 5);
+
+ image.composite([{ input: canvas.toBuffer() }]);
+ }
+
+ const formatQuality = (options.formatQuality || {})[format];
+
+ if (format === 'png') {
+ image.png({ adaptiveFiltering: false });
+ } else if (format === 'jpeg') {
+ image.jpeg({ quality: formatQuality || 80 });
+ } else if (format === 'webp') {
+ image.webp({ quality: formatQuality || 90 });
+ }
+ image.toBuffer((err, buffer, info) => {
+ if (!buffer) {
+ return res.status(404).send('Not found');
}
- const transformer = raw
- ? mercator.inverse.bind(mercator)
- : item.dataProjWGStoInternalWGS;
+ res.set({
+ 'Last-Modified': item.lastModified,
+ 'Content-Type': `image/${format}`
+ });
+ return res.status(200).send(buffer);
+ });
+ });
+ });
+ };
- if (transformer) {
- const ll = transformer([x, y]);
- x = ll[0];
- y = ll[1];
- }
+ app.get(`/:id/:tileSize(256|512)/:z(\\d+)/:x(\\d+)/:y(\\d+):scale(${scalePattern})?.:format([\\w]+)`, (req, res, next) => {
+ const item = repo[req.params.id];
+ if (!item) {
+ return res.sendStatus(404);
+ }
- const paths = extractPathsFromQuery(req.query, transformer);
- const markers = extractMarkersFromQuery(
- req.query,
- options,
- transformer,
- );
-
- // prettier-ignore
- const overlay = await renderOverlay(
- z, x, y, bearing, pitch, w, h, scale, paths, markers, req.query,
- );
-
- // prettier-ignore
- return respondImage(
- options, item, z, x, y, bearing, pitch, w, h, scale, format, res, overlay, 'static',
- );
- } catch (e) {
- next(e);
- }
- },
- );
-
- const serveBounds = async (req, res, next) => {
- try {
- const item = repo[req.params.id];
- if (!item) {
- return res.sendStatus(404);
- }
- const raw = req.params.raw;
- const bbox = [
- +req.params.minx,
- +req.params.miny,
- +req.params.maxx,
- +req.params.maxy,
- ];
- let center = [(bbox[0] + bbox[2]) / 2, (bbox[1] + bbox[3]) / 2];
-
- const transformer = raw
- ? mercator.inverse.bind(mercator)
- : item.dataProjWGStoInternalWGS;
-
- if (transformer) {
- const minCorner = transformer(bbox.slice(0, 2));
- const maxCorner = transformer(bbox.slice(2));
- bbox[0] = minCorner[0];
- bbox[1] = minCorner[1];
- bbox[2] = maxCorner[0];
- bbox[3] = maxCorner[1];
- center = transformer(center);
- }
-
- const w = req.params.width | 0;
- const h = req.params.height | 0;
- const scale = getScale(req.params.scale);
- const format = req.params.format;
-
- const z = calcZForBBox(bbox, w, h, req.query);
- const x = center[0];
- const y = center[1];
- const bearing = 0;
- const pitch = 0;
-
- const paths = extractPathsFromQuery(req.query, transformer);
- const markers = extractMarkersFromQuery(
- req.query,
- options,
- transformer,
- );
-
- // prettier-ignore
- const overlay = await renderOverlay(
- z, x, y, bearing, pitch, w, h, scale, paths, markers, req.query,
- );
-
- // prettier-ignore
- return respondImage(
- options, item, z, x, y, bearing, pitch, w, h, scale, format, res, overlay, 'static',
- );
- } catch (e) {
- next(e);
+ const modifiedSince = req.get('if-modified-since'), cc = req.get('cache-control');
+ if (modifiedSince && (!cc || cc.indexOf('no-cache') === -1)) {
+ if (new Date(item.lastModified) <= new Date(modifiedSince)) {
+ return res.sendStatus(304);
}
+ }
+
+ const z = req.params.z | 0,
+ x = req.params.x | 0,
+ y = req.params.y | 0,
+ scale = getScale(req.params.scale),
+ format = req.params.format,
+ tileSize = parseInt(req.params.tileSize, 10) || 256;
+
+ if (z < 0 || x < 0 || y < 0 ||
+ z > 22 || x >= Math.pow(2, z) || y >= Math.pow(2, z)) {
+ return res.status(404).send('Out of bounds');
};
- const boundsPattern = util.format(
- ':minx(%s),:miny(%s),:maxx(%s),:maxy(%s)',
- FLOAT_PATTERN,
- FLOAT_PATTERN,
- FLOAT_PATTERN,
- FLOAT_PATTERN,
- );
+ const tileCenter = mercator.ll([
+ ((x + 0.5) / (1 << z)) * (tileSize << z),
+ ((y + 0.5) / (1 << z)) * (tileSize << z)
+ ], z);
+
+ return respondImage(item, z, tileCenter[0], tileCenter[1], 0, 0,
+ tileSize, tileSize, scale, format, res, next);
+ });
+
+ if (options.serveStaticMaps !== false) {
+ const staticPattern =
+ `/:id/static/:raw(raw)?/%s/:width(\\d+)x:height(\\d+):scale(${scalePattern})?.:format([\\w]+)`;
+
+ const centerPattern =
+ util.format(':x(%s),:y(%s),:z(%s)(@:bearing(%s)(,:pitch(%s))?)?',
+ FLOAT_PATTERN, FLOAT_PATTERN, FLOAT_PATTERN,
+ FLOAT_PATTERN, FLOAT_PATTERN);
+
+ app.get(util.format(staticPattern, centerPattern), (req, res, next) => {
+ const item = repo[req.params.id];
+ if (!item) {
+ return res.sendStatus(404);
+ }
+ const raw = req.params.raw;
+ let z = +req.params.z,
+ x = +req.params.x,
+ y = +req.params.y,
+ bearing = +(req.params.bearing || '0'),
+ pitch = +(req.params.pitch || '0'),
+ w = req.params.width | 0,
+ h = req.params.height | 0,
+ scale = getScale(req.params.scale),
+ format = req.params.format;
+
+ if (z < 0) {
+ return res.status(404).send('Invalid zoom');
+ }
+
+ const transformer = raw ?
+ mercator.inverse.bind(mercator) : item.dataProjWGStoInternalWGS;
+
+ if (transformer) {
+ const ll = transformer([x, y]);
+ x = ll[0];
+ y = ll[1];
+ }
+
+ const path = extractPathFromQuery(req.query, transformer);
+ const overlay = renderOverlay(z, x, y, bearing, pitch, w, h, scale,
+ path, req.query);
+
+ return respondImage(item, z, x, y, bearing, pitch, w, h, scale, format,
+ res, next, overlay);
+ });
+
+ const serveBounds = (req, res, next) => {
+ const item = repo[req.params.id];
+ if (!item) {
+ return res.sendStatus(404);
+ }
+ const raw = req.params.raw;
+ const bbox = [+req.params.minx, +req.params.miny,
+ +req.params.maxx, +req.params.maxy];
+ let center = [(bbox[0] + bbox[2]) / 2, (bbox[1] + bbox[3]) / 2];
+
+ const transformer = raw ?
+ mercator.inverse.bind(mercator) : item.dataProjWGStoInternalWGS;
+
+ if (transformer) {
+ const minCorner = transformer(bbox.slice(0, 2));
+ const maxCorner = transformer(bbox.slice(2));
+ bbox[0] = minCorner[0];
+ bbox[1] = minCorner[1];
+ bbox[2] = maxCorner[0];
+ bbox[3] = maxCorner[1];
+ center = transformer(center);
+ }
+
+ const w = req.params.width | 0,
+ h = req.params.height | 0,
+ scale = getScale(req.params.scale),
+ format = req.params.format;
+
+ const z = calcZForBBox(bbox, w, h, req.query),
+ x = center[0],
+ y = center[1],
+ bearing = 0,
+ pitch = 0;
+
+ const path = extractPathFromQuery(req.query, transformer);
+ const overlay = renderOverlay(z, x, y, bearing, pitch, w, h, scale,
+ path, req.query);
+ return respondImage(item, z, x, y, bearing, pitch, w, h, scale, format,
+ res, next, overlay);
+ };
+
+ const boundsPattern =
+ util.format(':minx(%s),:miny(%s),:maxx(%s),:maxy(%s)',
+ FLOAT_PATTERN, FLOAT_PATTERN, FLOAT_PATTERN, FLOAT_PATTERN);
app.get(util.format(staticPattern, boundsPattern), serveBounds);
app.get('/:id/static/', (req, res, next) => {
- for (const key in req.query) {
+ for (let key in req.query) {
req.query[key.toLowerCase()] = req.query[key];
}
req.params.raw = true;
@@ -788,126 +507,80 @@ export const serve_rendered = {
const autoPattern = 'auto';
- app.get(
- util.format(staticPattern, autoPattern),
- async (req, res, next) => {
- try {
- const item = repo[req.params.id];
- if (!item) {
- return res.sendStatus(404);
- }
- const raw = req.params.raw;
- const w = req.params.width | 0;
- const h = req.params.height | 0;
- const bearing = 0;
- const pitch = 0;
- const scale = getScale(req.params.scale);
- const format = req.params.format;
+ app.get(util.format(staticPattern, autoPattern), (req, res, next) => {
+ const item = repo[req.params.id];
+ if (!item) {
+ return res.sendStatus(404);
+ }
+ const raw = req.params.raw;
+ const w = req.params.width | 0,
+ h = req.params.height | 0,
+ bearing = 0,
+ pitch = 0,
+ scale = getScale(req.params.scale),
+ format = req.params.format;
- const transformer = raw
- ? mercator.inverse.bind(mercator)
- : item.dataProjWGStoInternalWGS;
+ const transformer = raw ?
+ mercator.inverse.bind(mercator) : item.dataProjWGStoInternalWGS;
- const paths = extractPathsFromQuery(req.query, transformer);
- const markers = extractMarkersFromQuery(
- req.query,
- options,
- transformer,
- );
+ const path = extractPathFromQuery(req.query, transformer);
+ if (path.length < 2) {
+ return res.status(400).send('Invalid path');
+ }
- // Extract coordinates from markers
- const markerCoordinates = [];
- for (const marker of markers) {
- markerCoordinates.push(marker.location);
- }
+ const bbox = [Infinity, Infinity, -Infinity, -Infinity];
+ for (const pair of path) {
+ bbox[0] = Math.min(bbox[0], pair[0]);
+ bbox[1] = Math.min(bbox[1], pair[1]);
+ bbox[2] = Math.max(bbox[2], pair[0]);
+ bbox[3] = Math.max(bbox[3], pair[1]);
+ }
- // Create array with coordinates from markers and path
- const coords = [].concat(paths.flat()).concat(markerCoordinates);
+ const bbox_ = mercator.convert(bbox, '900913');
+ const center = mercator.inverse(
+ [(bbox_[0] + bbox_[2]) / 2, (bbox_[1] + bbox_[3]) / 2]
+ );
- // Check if we have at least one coordinate to calculate a bounding box
- if (coords.length < 1) {
- return res.status(400).send('No coordinates provided');
- }
+ const z = calcZForBBox(bbox, w, h, req.query),
+ x = center[0],
+ y = center[1];
- const bbox = [Infinity, Infinity, -Infinity, -Infinity];
- for (const pair of coords) {
- bbox[0] = Math.min(bbox[0], pair[0]);
- bbox[1] = Math.min(bbox[1], pair[1]);
- bbox[2] = Math.max(bbox[2], pair[0]);
- bbox[3] = Math.max(bbox[3], pair[1]);
- }
+ const overlay = renderOverlay(z, x, y, bearing, pitch, w, h, scale,
+ path, req.query);
- const bbox_ = mercator.convert(bbox, '900913');
- const center = mercator.inverse([
- (bbox_[0] + bbox_[2]) / 2,
- (bbox_[1] + bbox_[3]) / 2,
- ]);
-
- // Calculate zoom level
- const maxZoom = parseFloat(req.query.maxzoom);
- let z = calcZForBBox(bbox, w, h, req.query);
- if (maxZoom > 0) {
- z = Math.min(z, maxZoom);
- }
-
- const x = center[0];
- const y = center[1];
-
- // prettier-ignore
- const overlay = await renderOverlay(
- z, x, y, bearing, pitch, w, h, scale, paths, markers, req.query,
- );
-
- // prettier-ignore
- return respondImage(
- options, item, z, x, y, bearing, pitch, w, h, scale, format, res, overlay, 'static',
- );
- } catch (e) {
- next(e);
- }
- },
- );
+ return respondImage(item, z, x, y, bearing, pitch, w, h, scale, format,
+ res, next, overlay);
+ });
}
app.get('/:id.json', (req, res, next) => {
- const tileSize = 512;
const item = repo[req.params.id];
if (!item) {
return res.sendStatus(404);
}
const info = clone(item.tileJSON);
- info.tiles = getTileUrls(
- req,
- info.tiles,
- `styles/${req.params.id}`,
- tileSize,
- info.format,
- item.publicUrl,
- );
+ info.tiles = utils.getTileUrls(req, info.tiles,
+ `styles/${req.params.id}`, info.format, item.publicUrl);
return res.send(info);
});
- const fonts = await listFonts(options.paths.fonts);
- Object.assign(existingFonts, fonts);
- return app;
+ return Promise.all([fontListingPromise]).then(() => app);
},
- add: async (options, repo, params, id, publicUrl, dataResolver) => {
+ add: (options, repo, params, id, publicUrl, dataResolver) => {
const map = {
renderers: [],
- renderersStatic: [],
- sources: {},
- sourceTypes: {},
+ sources: {}
};
let styleJSON;
- const createPool = (ratio, mode, min, max) => {
+ const createPool = (ratio, min, max) => {
const createRenderer = (ratio, createCallback) => {
- const renderer = new mlgl.Map({
- mode,
- ratio,
- request: async (req, callback) => {
+ const renderer = new mbgl.Map({
+ mode: "tile",
+ ratio: ratio,
+ request: (req, callback) => {
const protocol = req.url.split(':')[0];
- // console.log('Handling request:', req);
+ //console.log('Handling request:', req);
if (protocol === 'sprites') {
const dir = options.paths[protocol];
const file = unescape(req.url).substring(protocol.length + 3);
@@ -918,157 +591,92 @@ export const serve_rendered = {
const parts = req.url.split('/');
const fontstack = unescape(parts[2]);
const range = parts[3].split('.')[0];
-
- try {
- const concatenated = await getFontsPbf(
- null,
- options.paths[protocol],
- fontstack,
- range,
- existingFonts,
- );
- callback(null, { data: concatenated });
- } catch (err) {
+ utils.getFontsPbf(
+ null, options.paths[protocol], fontstack, range, existingFonts
+ ).then(concated => {
+ callback(null, { data: concated });
+ }, err => {
callback(err, { data: null });
- }
- } else if (protocol === 'mbtiles' || protocol === 'pmtiles') {
+ });
+ } else if (protocol === 'mbtiles') {
const parts = req.url.split('/');
const sourceId = parts[2];
const source = map.sources[sourceId];
- const sourceType = map.sourceTypes[sourceId];
const sourceInfo = styleJSON.sources[sourceId];
-
- const z = parts[3] | 0;
- const x = parts[4] | 0;
- const y = parts[5].split('.')[0] | 0;
- const format = parts[5].split('.')[1];
-
- if (sourceType === 'pmtiles') {
- let tileinfo = await getPMtilesTile(source, z, x, y);
- let data = tileinfo.data;
- let headers = tileinfo.header;
- if (data == undefined) {
- if (options.verbose)
- console.log('MBTiles error, serving empty', err);
- createEmptyResponse(
- sourceInfo.format,
- sourceInfo.color,
- callback,
- );
+ const z = parts[3] | 0,
+ x = parts[4] | 0,
+ y = parts[5].split('.')[0] | 0,
+ format = parts[5].split('.')[1];
+ source.getTile(z, x, y, (err, data, headers) => {
+ if (err) {
+ if (options.verbose) console.log('MBTiles error, serving empty', err);
+ createEmptyResponse(sourceInfo.format, sourceInfo.color, callback);
return;
+ }
+
+ const response = {};
+ if (headers['Last-Modified']) {
+ response.modified = new Date(headers['Last-Modified']);
+ }
+
+ if (format === 'pbf') {
+ try {
+ response.data = zlib.unzipSync(data);
+ } catch (err) {
+ console.log("Skipping incorrect header for tile mbtiles://%s/%s/%s/%s.pbf", id, z, x, y);
+ }
+ if (options.dataDecoratorFunc) {
+ response.data = options.dataDecoratorFunc(
+ sourceId, 'data', response.data, z, x, y);
+ }
} else {
- const response = {};
response.data = data;
- if (headers['Last-Modified']) {
- response.modified = new Date(headers['Last-Modified']);
- }
-
- if (format === 'pbf') {
- if (options.dataDecoratorFunc) {
- response.data = options.dataDecoratorFunc(
- sourceId,
- 'data',
- response.data,
- z,
- x,
- y,
- );
- }
- }
-
- callback(null, response);
}
- } else if (sourceType === 'mbtiles') {
- source.getTile(z, x, y, (err, data, headers) => {
- if (err) {
- if (options.verbose)
- console.log('MBTiles error, serving empty', err);
- createEmptyResponse(
- sourceInfo.format,
- sourceInfo.color,
- callback,
- );
- return;
- }
- const response = {};
- if (headers['Last-Modified']) {
- response.modified = new Date(headers['Last-Modified']);
- }
-
- if (format === 'pbf') {
- try {
- response.data = zlib.unzipSync(data);
- } catch (err) {
- console.log(
- 'Skipping incorrect header for tile mbtiles://%s/%s/%s/%s.pbf',
- id,
- z,
- x,
- y,
- );
- }
- if (options.dataDecoratorFunc) {
- response.data = options.dataDecoratorFunc(
- sourceId,
- 'data',
- response.data,
- z,
- x,
- y,
- );
- }
- } else {
- response.data = data;
- }
-
- callback(null, response);
- });
- }
+ callback(null, response);
+ });
} else if (protocol === 'http' || protocol === 'https') {
- try {
- const response = await axios.get(req.url, {
- responseType: 'arraybuffer', // Get the response as raw buffer
- // Axios handles gzip by default, so no need for a gzip flag
- });
-
- const responseHeaders = response.headers;
- const responseData = response.data;
-
- const parsedResponse = {};
- if (responseHeaders['last-modified']) {
- parsedResponse.modified = new Date(
- responseHeaders['last-modified'],
- );
- }
- if (responseHeaders.expires) {
- parsedResponse.expires = new Date(responseHeaders.expires);
- }
- if (responseHeaders.etag) {
- parsedResponse.etag = responseHeaders.etag;
- }
-
- parsedResponse.data = responseData;
- callback(null, parsedResponse);
- } catch (error) {
+ request({
+ url: req.url,
+ encoding: null,
+ gzip: true
+ }, (err, res, body) => {
const parts = url.parse(req.url);
const extension = path.extname(parts.pathname).toLowerCase();
const format = extensionToFormat[extension] || '';
- createEmptyResponse(format, '', callback);
- }
+ if (err || res.statusCode < 200 || res.statusCode >= 300) {
+ // console.log('HTTP error', err || res.statusCode);
+ createEmptyResponse(format, '', callback);
+ return;
+ }
+
+ const response = {};
+ if (res.headers.modified) {
+ response.modified = new Date(res.headers.modified);
+ }
+ if (res.headers.expires) {
+ response.expires = new Date(res.headers.expires);
+ }
+ if (res.headers.etag) {
+ response.etag = res.headers.etag;
+ }
+
+ response.data = body;
+ callback(null, response);
+ });
}
- },
+ }
});
renderer.load(styleJSON);
createCallback(null, renderer);
};
return new advancedPool.Pool({
- min,
- max,
+ min: min,
+ max: max,
create: createRenderer.bind(null, ratio),
- destroy: (renderer) => {
+ destroy: renderer => {
renderer.release();
- },
+ }
});
};
@@ -1082,20 +690,16 @@ export const serve_rendered = {
}
if (styleJSON.sprite && !httpTester.test(styleJSON.sprite)) {
- styleJSON.sprite =
- 'sprites://' +
+ styleJSON.sprite = 'sprites://' +
styleJSON.sprite
.replace('{style}', path.basename(styleFile, '.json'))
- .replace(
- '{styleJsonFolder}',
- path.relative(options.paths.sprites, path.dirname(styleJSONPath)),
- );
+ .replace('{styleJsonFolder}', path.relative(options.paths.sprites, path.dirname(styleJSONPath)));
}
if (styleJSON.glyphs && !httpTester.test(styleJSON.glyphs)) {
styleJSON.glyphs = `fonts://${styleJSON.glyphs}`;
}
- for (const layer of styleJSON.layers || []) {
+ for (const layer of (styleJSON.layers || [])) {
if (layer && layer.paint) {
// Remove (flatten) 3D buildings
if (layer.paint['fill-extrusion-height']) {
@@ -1108,196 +712,121 @@ export const serve_rendered = {
}
const tileJSON = {
- tilejson: '2.0.0',
- name: styleJSON.name,
- attribution: '',
- minzoom: 0,
- maxzoom: 20,
- bounds: [-180, -85.0511, 180, 85.0511],
- format: 'png',
- type: 'baselayer',
+ 'tilejson': '2.0.0',
+ 'name': styleJSON.name,
+ 'attribution': '',
+ 'minzoom': 0,
+ 'maxzoom': 20,
+ 'bounds': [-180, -85.0511, 180, 85.0511],
+ 'format': 'png',
+ 'type': 'baselayer'
};
const attributionOverride = params.tilejson && params.tilejson.attribution;
- if (styleJSON.center && styleJSON.zoom) {
- tileJSON.center = styleJSON.center.concat(Math.round(styleJSON.zoom));
- }
Object.assign(tileJSON, params.tilejson || {});
tileJSON.tiles = params.domains || options.domains;
- fixTileJSONCenter(tileJSON);
+ utils.fixTileJSONCenter(tileJSON);
- const repoobj = {
+ repo[id] = {
tileJSON,
publicUrl,
map,
dataProjWGStoInternalWGS: null,
lastModified: new Date().toUTCString(),
- watermark: params.watermark || options.watermark,
- staticAttributionText:
- params.staticAttributionText || options.staticAttributionText,
+ watermark: params.watermark || options.watermark
};
- repo[id] = repoobj;
const queue = [];
for (const name of Object.keys(styleJSON.sources)) {
- let sourceType;
let source = styleJSON.sources[name];
- let url = source.url;
- if (
- url &&
- (url.startsWith('pmtiles://') || url.startsWith('mbtiles://'))
- ) {
- // found pmtiles or mbtiles source, replace with info from local file
+ const url = source.url;
+
+ if (url && url.lastIndexOf('mbtiles:', 0) === 0) {
+ // found mbtiles source, replace with info from local file
delete source.url;
- let dataId = url.replace('pmtiles://', '').replace('mbtiles://', '');
- if (dataId.startsWith('{') && dataId.endsWith('}')) {
- dataId = dataId.slice(1, -1);
- }
+ let mbtilesFile = url.substring('mbtiles://'.length);
+ const fromData = mbtilesFile[0] === '{' &&
+ mbtilesFile[mbtilesFile.length - 1] === '}';
- const mapsTo = (params.mapping || {})[dataId];
- if (mapsTo) {
- dataId = mapsTo;
- }
-
- let inputFile;
- const dataInfo = dataResolver(dataId);
- if (dataInfo.inputFile) {
- inputFile = dataInfo.inputFile;
- sourceType = dataInfo.fileType;
- } else {
- console.error(`ERROR: data "${inputFile}" not found!`);
- process.exit(1);
- }
-
- if (!isValidHttpUrl(inputFile)) {
- const inputFileStats = fs.statSync(inputFile);
- if (!inputFileStats.isFile() || inputFileStats.size === 0) {
- throw Error(`Not valid PMTiles file: "${inputFile}"`);
+ if (fromData) {
+ mbtilesFile = mbtilesFile.substr(1, mbtilesFile.length - 2);
+ const mapsTo = (params.mapping || {})[mbtilesFile];
+ if (mapsTo) {
+ mbtilesFile = mapsTo;
+ }
+ mbtilesFile = dataResolver(mbtilesFile);
+ if (!mbtilesFile) {
+ console.error(`ERROR: data "${mbtilesFile}" not found!`);
+ process.exit(1);
}
}
- if (sourceType === 'pmtiles') {
- map.sources[name] = openPMtiles(inputFile);
- map.sourceTypes[name] = 'pmtiles';
- const metadata = await getPMtilesInfo(map.sources[name]);
-
- if (!repoobj.dataProjWGStoInternalWGS && metadata.proj4) {
- // how to do this for multiple sources with different proj4 defs?
- const to3857 = proj4('EPSG:3857');
- const toDataProj = proj4(metadata.proj4);
- repoobj.dataProjWGStoInternalWGS = (xy) =>
- to3857.inverse(toDataProj.forward(xy));
+ queue.push(new Promise((resolve, reject) => {
+ mbtilesFile = path.resolve(options.paths.mbtiles, mbtilesFile);
+ const mbtilesFileStats = fs.statSync(mbtilesFile);
+ if (!mbtilesFileStats.isFile() || mbtilesFileStats.size === 0) {
+ throw Error(`Not valid MBTiles file: ${mbtilesFile}`);
}
-
- const type = source.type;
- Object.assign(source, metadata);
- source.type = type;
- source.tiles = [
- // meta url which will be detected when requested
- `pmtiles://${name}/{z}/{x}/{y}.${metadata.format || 'pbf'}`,
- ];
- delete source.scheme;
-
- if (
- !attributionOverride &&
- source.attribution &&
- source.attribution.length > 0
- ) {
- if (!tileJSON.attribution.includes(source.attribution)) {
- if (tileJSON.attribution.length > 0) {
- tileJSON.attribution += ' | ';
+ map.sources[name] = new MBTiles(mbtilesFile, err => {
+ map.sources[name].getInfo((err, info) => {
+ if (err) {
+ console.error(err);
+ return;
}
- tileJSON.attribution += source.attribution;
- }
- }
- } else {
- queue.push(
- new Promise((resolve, reject) => {
- inputFile = path.resolve(options.paths.mbtiles, inputFile);
- const inputFileStats = fs.statSync(inputFile);
- if (!inputFileStats.isFile() || inputFileStats.size === 0) {
- throw Error(`Not valid MBTiles file: "${inputFile}"`);
+
+ if (!repo[id].dataProjWGStoInternalWGS && info.proj4) {
+ // how to do this for multiple sources with different proj4 defs?
+ const to3857 = proj4('EPSG:3857');
+ const toDataProj = proj4(info.proj4);
+ repo[id].dataProjWGStoInternalWGS = xy => to3857.inverse(toDataProj.forward(xy));
}
- map.sources[name] = new MBTiles(inputFile + '?mode=ro', (err) => {
- map.sources[name].getInfo((err, info) => {
- if (err) {
- console.error(err);
- return;
- }
- map.sourceTypes[name] = 'mbtiles';
- if (!repoobj.dataProjWGStoInternalWGS && info.proj4) {
- // how to do this for multiple sources with different proj4 defs?
- const to3857 = proj4('EPSG:3857');
- const toDataProj = proj4(info.proj4);
- repoobj.dataProjWGStoInternalWGS = (xy) =>
- to3857.inverse(toDataProj.forward(xy));
- }
+ const type = source.type;
+ Object.assign(source, info);
+ source.type = type;
+ source.tiles = [
+ // meta url which will be detected when requested
+ `mbtiles://${name}/{z}/{x}/{y}.${info.format || 'pbf'}`
+ ];
+ delete source.scheme;
- const type = source.type;
- Object.assign(source, info);
- source.type = type;
- source.tiles = [
- // meta url which will be detected when requested
- `mbtiles://${name}/{z}/{x}/{y}.${info.format || 'pbf'}`,
- ];
- delete source.scheme;
+ if (options.dataDecoratorFunc) {
+ source = options.dataDecoratorFunc(name, 'tilejson', source);
+ }
- if (options.dataDecoratorFunc) {
- source = options.dataDecoratorFunc(
- name,
- 'tilejson',
- source,
- );
- }
-
- if (
- !attributionOverride &&
- source.attribution &&
- source.attribution.length > 0
- ) {
- if (!tileJSON.attribution.includes(source.attribution)) {
- if (tileJSON.attribution.length > 0) {
- tileJSON.attribution += ' | ';
- }
- tileJSON.attribution += source.attribution;
- }
- }
- resolve();
- });
- });
- }),
- );
- }
+ if (!attributionOverride &&
+ source.attribution && source.attribution.length > 0) {
+ if (tileJSON.attribution.length > 0) {
+ tileJSON.attribution += '; ';
+ }
+ tileJSON.attribution += source.attribution;
+ }
+ resolve();
+ });
+ });
+ }));
}
}
- await Promise.all(queue);
+ const renderersReadyPromise = Promise.all(queue).then(() => {
+ // standard and @2x tiles are much more usual -> default to larger pools
+ const minPoolSizes = options.minRendererPoolSizes || [8, 4, 2];
+ const maxPoolSizes = options.maxRendererPoolSizes || [16, 8, 4];
+ for (let s = 1; s <= maxScaleFactor; s++) {
+ const i = Math.min(minPoolSizes.length - 1, s - 1);
+ const j = Math.min(maxPoolSizes.length - 1, s - 1);
+ const minPoolSize = minPoolSizes[i];
+ const maxPoolSize = Math.max(minPoolSize, maxPoolSizes[j]);
+ map.renderers[s] = createPool(s, minPoolSize, maxPoolSize);
+ }
+ });
- // standard and @2x tiles are much more usual -> default to larger pools
- const minPoolSizes = options.minRendererPoolSizes || [8, 4, 2];
- const maxPoolSizes = options.maxRendererPoolSizes || [16, 8, 4];
- for (let s = 1; s <= maxScaleFactor; s++) {
- const i = Math.min(minPoolSizes.length - 1, s - 1);
- const j = Math.min(maxPoolSizes.length - 1, s - 1);
- const minPoolSize = minPoolSizes[i];
- const maxPoolSize = Math.max(minPoolSize, maxPoolSizes[j]);
- map.renderers[s] = createPool(s, 'tile', minPoolSize, maxPoolSize);
- map.renderersStatic[s] = createPool(
- s,
- 'static',
- minPoolSize,
- maxPoolSize,
- );
- }
+ return Promise.all([renderersReadyPromise]);
},
remove: (repo, id) => {
- const item = repo[id];
+ let item = repo[id];
if (item) {
- item.map.renderers.forEach((pool) => {
- pool.close();
- });
- item.map.renderersStatic.forEach((pool) => {
+ item.map.renderers.forEach(pool => {
pool.close();
});
}
diff --git a/src/server.js b/src/server.js
index 465b0e5..bedcd2f 100644
--- a/src/server.js
+++ b/src/server.js
@@ -462,9 +462,8 @@ function start(opts) {
)}/${center[0].toFixed(5)}`;
const centerPx = mercator.px([center[0], center[1]], center[2]);
- style.thumbnail = `${center[2]}/${Math.floor(
- centerPx[0] / 256,
- )}/${Math.floor(centerPx[1] / 256)}.png`;
+ // Set thumbnail default size to be 256px x 256px
+ style.thumbnail = `256/${center[2]}/${Math.floor(centerPx[0] / 256)}/${Math.floor(centerPx[1] / 256)}.png`;
}
style.xyz_link = getTileUrls(
diff --git a/src/utils.js b/src/utils.js
index c3b23b4..9e7b9d0 100644
--- a/src/utils.js
+++ b/src/utils.js
@@ -68,19 +68,17 @@ export const getTileUrls = (req, domains, path, tileSize, format, publicUrl, ali
}
let tileParams = '{z}/{x}/{y}';
- if (['png', 'jpg', 'jpeg', 'webp'].includes(format) && tileSize) {
- tileParams = `${tileSize}/{z}/{x}/{y}`;
+ if (['png', 'jpg', 'jpeg', 'webp'].includes(format)) {
+ tileParams = '256/{z}/{x}/{y}';
}
const uris = [];
if (!publicUrl) {
for (const domain of domains) {
- uris.push(
- `${req.protocol}://${domain}/${path}/${tileParams}.${format}${query}`,
- );
+ uris.push(`${req.protocol}://${domain}/${path}/${tileParams}.${format}${query}`);
}
} else {
- uris.push(`${publicUrl}${path}/${tileParams}.${format}${query}`);
+ uris.push(`${publicUrl}${path}/${tileParams}.${format}${query}`)
}
return uris;