From 6d89332c8dac9f05693bc81d8b824b9230c94823 Mon Sep 17 00:00:00 2001 From: Petteri Pesonen Date: Tue, 17 Nov 2020 11:41:53 +0200 Subject: [PATCH] Add support for 512 sized raster tiles (#1) * Enable setting tilesize for raster tiles * Serve correct endpoint for raster tiles * Add 256 & 512 sized raster layers to wmts getCapabilities document * Update wmts getCapabilities tileMatrixSets * Add rendered tiles format for getTileUrls method * Update endpoints documentation --- docs/endpoints.rst | 3 +- public/templates/wmts.tmpl | 400 ++++++++- src/serve_rendered.js | 1611 +++++++++++++----------------------- src/server.js | 5 +- src/utils.js | 10 +- 5 files changed, 965 insertions(+), 1064 deletions(-) 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;