This commit is contained in:
Andrew Calcutt 2025-03-12 07:01:31 +00:00 committed by GitHub
commit 736e53a1a2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 197 additions and 63 deletions

View file

@ -1,9 +1,26 @@
import * as http from 'http'; import * as http from 'http';
/**
* Options for the HTTP request.
* @type {object}
* @property {number} timeout - Timeout for the request in milliseconds.
*/
const options = { const options = {
timeout: 2000, timeout: 2000,
}; };
/**
* The URL to make the HTTP request to.
* @type {string}
*/
const url = 'http://localhost:8080/health'; const url = 'http://localhost:8080/health';
const request = http.request(url, options, (res) => {
/**
* Makes an HTTP request to the health endpoint and checks the response.
* Exits the process with a 0 status code if the health check is successful (status 200),
* or with a 1 status code otherwise.
*/
const request = http.request(url, options, function (res) {
console.log(`STATUS: ${res.statusCode}`); console.log(`STATUS: ${res.statusCode}`);
if (res.statusCode == 200) { if (res.statusCode == 200) {
process.exit(0); process.exit(0);
@ -11,8 +28,15 @@ const request = http.request(url, options, (res) => {
process.exit(1); process.exit(1);
} }
}); });
/**
* Handles errors that occur during the HTTP request.
* Logs an error message and exits the process with a 1 status code.
* @param {Error} err - The error object.
*/
request.on('error', function (err) { request.on('error', function (err) {
console.log('ERROR'); console.log('ERROR');
process.exit(1); process.exit(1);
}); });
request.end(); request.end();

View file

@ -70,6 +70,12 @@ const opts = program.opts();
console.log(`Starting ${packageJson.name} v${packageJson.version}`); console.log(`Starting ${packageJson.name} v${packageJson.version}`);
/**
* Starts the tile server with the given configuration.
* @param {string|null} configPath - The path to the configuration file, or null if not using a config file.
* @param {object|null} config - The configuration object, or null if reading from a file.
* @returns {Promise<void>} - A Promise that resolves when the server starts.
*/
const startServer = (configPath, config) => { const startServer = (configPath, config) => {
let publicUrl = opts.public_url; let publicUrl = opts.public_url;
if (publicUrl && publicUrl.lastIndexOf('/') !== publicUrl.length - 1) { if (publicUrl && publicUrl.lastIndexOf('/') !== publicUrl.length - 1) {
@ -89,6 +95,12 @@ const startServer = (configPath, config) => {
}); });
}; };
/**
* Starts the server with a given input file (MBTiles or PMTiles).
* Automatically creates a basic config file based on the input file.
* @param {string} inputFile - The path to the input MBTiles or PMTiles file.
* @returns {Promise<void>} - A Promise that resolves when the server starts.
*/
const startWithInputFile = async (inputFile) => { const startWithInputFile = async (inputFile) => {
console.log(`[INFO] Automatically creating config file for ${inputFile}`); console.log(`[INFO] Automatically creating config file for ${inputFile}`);
console.log(`[INFO] Only a basic preview style will be used.`); console.log(`[INFO] Only a basic preview style will be used.`);
@ -242,6 +254,14 @@ const startWithInputFile = async (inputFile) => {
} }
}; };
/**
* Main function to start the server. Checks for a config file or input file,
* and starts the server based on the available inputs.
* If no config or input file are provided, downloads a demo file.
* @async
* @returns {Promise<void>} - A Promise that resolves when the server starts or finishes the download.
*/
// eslint-disable-next-line security/detect-non-literal-fs-filename
fs.stat(path.resolve(opts.config), async (err, stats) => { fs.stat(path.resolve(opts.config), async (err, stats) => {
if (err || !stats.isFile() || stats.size === 0) { if (err || !stats.isFile() || stats.size === 0) {
let inputFile; let inputFile;

View file

@ -2,7 +2,8 @@ import MBTiles from '@mapbox/mbtiles';
import util from 'node:util'; import util from 'node:util';
/** /**
* Promise-ful wrapper around the MBTiles class. * A promise-based wrapper around the `@mapbox/mbtiles` class,
* providing asynchronous access to MBTiles database functionality.
*/ */
class MBTilesWrapper { class MBTilesWrapper {
constructor(mbtiles) { constructor(mbtiles) {
@ -11,27 +12,30 @@ class MBTilesWrapper {
} }
/** /**
* Get the underlying MBTiles object. * Gets the underlying MBTiles object.
* @returns {MBTiles} * @returns {MBTiles} The underlying MBTiles object.
*/ */
getMbTiles() { getMbTiles() {
return this._mbtiles; return this._mbtiles;
} }
/** /**
* Get the MBTiles metadata object. * Gets the MBTiles metadata object.
* @returns {Promise<object>} * @async
* @returns {Promise<object>} A promise that resolves with the MBTiles metadata.
*/ */
getInfo() { async getInfo() {
return this._getInfoP(); return this._getInfoP();
} }
} }
/** /**
* Open the given MBTiles file and return a promise that resolves with a * Opens an MBTiles file and returns a promise that resolves with an MBTilesWrapper instance.
* MBTilesWrapper instance. *
* @param inputFile Input file * The MBTiles database is opened in read-only mode.
* @returns {Promise<MBTilesWrapper>} * @param {string} inputFile - The path to the MBTiles file.
* @returns {Promise<MBTilesWrapper>} A promise that resolves with a new MBTilesWrapper instance.
* @throws {Error} If there is an error opening the MBTiles file.
*/ */
export function openMbTilesWrapper(inputFile) { export function openMbTilesWrapper(inputFile) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {

View file

@ -2,13 +2,37 @@ import fs from 'node:fs';
import { PMTiles, FetchSource } from 'pmtiles'; import { PMTiles, FetchSource } from 'pmtiles';
import { isValidHttpUrl } from './utils.js'; import { isValidHttpUrl } from './utils.js';
/**
* A PMTiles source that reads data from a file descriptor.
*/
class PMTilesFileSource { class PMTilesFileSource {
/**
* Creates a new PMTilesFileSource instance.
* @param {number} fd - The file descriptor of the PMTiles file.
*/
constructor(fd) { constructor(fd) {
/**
* @type {number} The file descriptor of the PMTiles file
* @private
*/
this.fd = fd; this.fd = fd;
} }
/**
* Returns the file descriptor.
* @returns {number} The file descriptor.
*/
getKey() { getKey() {
return this.fd; return this.fd;
} }
/**
* Asynchronously retrieves a chunk of bytes from the PMTiles file.
* @async
* @param {number} offset - The byte offset to start reading from.
* @param {number} length - The number of bytes to read.
* @returns {Promise<{data: ArrayBuffer}>} A promise that resolves with an object containing the read bytes as an ArrayBuffer.
*/
async getBytes(offset, length) { async getBytes(offset, length) {
const buffer = Buffer.alloc(length); const buffer = Buffer.alloc(length);
await readFileBytes(this.fd, buffer, offset); await readFileBytes(this.fd, buffer, offset);
@ -21,10 +45,13 @@ class PMTilesFileSource {
} }
/** /**
* * Asynchronously reads a specified number of bytes from a file descriptor into a buffer.
* @param fd * @async
* @param buffer * @param {number} fd - The file descriptor to read from.
* @param offset * @param {Buffer} buffer - The buffer to write the read bytes into.
* @param {number} offset - The byte offset in the file to start reading from.
* @returns {Promise<void>} A promise that resolves when the read operation completes.
* @throws {Error} If there is an error during the read operation.
*/ */
async function readFileBytes(fd, buffer, offset) { async function readFileBytes(fd, buffer, offset) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@ -38,8 +65,9 @@ async function readFileBytes(fd, buffer, offset) {
} }
/** /**
* * Opens a PMTiles file (either local or remote) and returns a PMTiles instance.
* @param FilePath * @param {string} FilePath - The path to the PMTiles file or a URL.
* @returns {PMTiles} A PMTiles instance.
*/ */
export function openPMtiles(FilePath) { export function openPMtiles(FilePath) {
let pmtiles = undefined; let pmtiles = undefined;
@ -56,8 +84,10 @@ export function openPMtiles(FilePath) {
} }
/** /**
* * Asynchronously retrieves metadata and header information from a PMTiles file.
* @param pmtiles * @async
* @param {PMTiles} pmtiles - The PMTiles instance.
* @returns {Promise<object>} A promise that resolves with the metadata object.
*/ */
export async function getPMtilesInfo(pmtiles) { export async function getPMtilesInfo(pmtiles) {
const header = await pmtiles.getHeader(); const header = await pmtiles.getHeader();
@ -97,11 +127,13 @@ export async function getPMtilesInfo(pmtiles) {
} }
/** /**
* * Asynchronously retrieves a tile from a PMTiles file.
* @param pmtiles * @async
* @param z * @param {PMTiles} pmtiles - The PMTiles instance.
* @param x * @param {number} z - The zoom level of the tile.
* @param y * @param {number} x - The x coordinate of the tile.
* @param {number} y - The y coordinate of the tile.
* @returns {Promise<{data: Buffer|undefined, header: object}>} A promise that resolves with an object containing the tile data (as a Buffer, or undefined if not found) and the appropriate headers.
*/ */
export async function getPMtilesTile(pmtiles, z, x, y) { export async function getPMtilesTile(pmtiles, z, x, y) {
const header = await pmtiles.getHeader(); const header = await pmtiles.getHeader();
@ -116,8 +148,9 @@ export async function getPMtilesTile(pmtiles, z, x, y) {
} }
/** /**
* * Determines the tile type and corresponding headers based on the PMTiles tile type number.
* @param typenum * @param {number} typenum - The tile type number from the PMTiles header.
* @returns {{type: string, header: object}} An object containing the tile type and associated headers.
*/ */
function getPmtilesTileType(typenum) { function getPmtilesTileType(typenum) {
let head = {}; let head = {};

View file

@ -1,34 +1,47 @@
'use strict'; 'use strict';
import { createCanvas, Image } from 'canvas'; import { createCanvas, Image } from 'canvas';
import SphericalMercator from '@mapbox/sphericalmercator'; import SphericalMercator from '@mapbox/sphericalmercator';
const mercator = new SphericalMercator(); const mercator = new SphericalMercator();
/** /**
* Transforms coordinates to pixels. * Transforms geographical coordinates (longitude/latitude) to pixel coordinates at a given zoom level.
* @param {List[Number]} ll Longitude/Latitude coordinate pair. * Uses spherical mercator projection and calculates pixel coordinates relative to zoom level 20 then scales it.
* @param {number} zoom Map zoom level. * @param {number[]} ll - Longitude/Latitude coordinate pair [longitude, latitude].
* @param {number} zoom - Map zoom level.
* @returns {number[]} Pixel coordinates [x, y].
*/ */
const precisePx = (ll, zoom) => { function precisePx(ll, zoom) {
const px = mercator.px(ll, 20); const px = mercator.px(ll, 20);
const scale = Math.pow(2, zoom - 20); const scale = Math.pow(2, zoom - 20);
return [px[0] * scale, px[1] * scale]; return [px[0] * scale, px[1] * scale];
}; }
/** /**
* Draws a marker in canvas context. * Draws a marker on a canvas context.
* @param {object} ctx Canvas context object. * The marker image is loaded asynchronously.
* @param {object} marker Marker object parsed by extractMarkersFromQuery. * @async
* @param {number} z Map zoom level. * @param {CanvasRenderingContext2D} ctx - Canvas context object.
* @param {object} marker - Marker object, with properties like `icon`, `location`, `offsetX`, `offsetY`, `scale`.
* @param {number} z - Map zoom level.
* @returns {Promise<void>} A promise that resolves when the marker image is loaded and drawn.
* @throws {Error} If there is an error loading the marker image.
*/ */
const drawMarker = (ctx, marker, z) => { function drawMarker(ctx, marker, z) {
return new Promise((resolve) => { return new Promise((resolve) => {
const img = new Image(); const img = new Image();
const pixelCoords = precisePx(marker.location, z); const pixelCoords = precisePx(marker.location, z);
const getMarkerCoordinates = (imageWidth, imageHeight, scale) => { /**
* Calculates the pixel coordinates for placing the marker image on the canvas.
* Takes into account the image dimensions, scaling, and any offsets.
* @param {number} imageWidth - The width of the marker image.
* @param {number} imageHeight - The height of the marker image.
* @param {number} scale - The scaling factor.
* @returns {{x: number, y: number}} An object containing the x and y pixel coordinates.
*/
function getMarkerCoordinates(imageWidth, imageHeight, scale) {
// Images are placed with their top-left corner at the provided location // Images are placed with their top-left corner at the provided location
// within the canvas but we expect icons to be centered and above it. // within the canvas but we expect icons to be centered and above it.
@ -53,9 +66,13 @@ const drawMarker = (ctx, marker, z) => {
x: xCoordinate, x: xCoordinate,
y: yCoordinate, y: yCoordinate,
}; };
}; }
const drawOnCanvas = () => { /**
* Draws the marker image on the canvas, handling scaling if necessary.
* Resolves the parent promise once the image is drawn.
*/
function drawOnCanvas() {
// Check if the images should be resized before beeing drawn // Check if the images should be resized before beeing drawn
const defaultScale = 1; const defaultScale = 1;
const scale = marker.scale ? marker.scale : defaultScale; const scale = marker.scale ? marker.scale : defaultScale;
@ -75,7 +92,7 @@ const drawMarker = (ctx, marker, z) => {
} }
// Resolve the promise when image has been drawn // Resolve the promise when image has been drawn
resolve(); resolve();
}; }
img.onload = drawOnCanvas; img.onload = drawOnCanvas;
img.onerror = (err) => { img.onerror = (err) => {
@ -83,18 +100,20 @@ const drawMarker = (ctx, marker, z) => {
}; };
img.src = marker.icon; img.src = marker.icon;
}); });
}; }
/** /**
* Draws a list of markers onto a canvas. * Draws a list of markers onto a canvas.
* Wraps drawing of markers into list of promises and awaits them. * Wraps drawing of markers into list of promises and awaits them.
* It's required because images are expected to load asynchronous in canvas js * It's required because images are expected to load asynchronously in canvas js
* even when provided from a local disk. * even when provided from a local disk.
* @param {object} ctx Canvas context object. * @async
* @param {List[Object]} markers Marker objects parsed by extractMarkersFromQuery. * @param {CanvasRenderingContext2D} ctx - Canvas context object.
* @param {number} z Map zoom level. * @param {object[]} markers - Array of marker objects, see drawMarker for individual marker properties.
* @param {number} z - Map zoom level.
* @returns {Promise<void>} A promise that resolves when all marker images are loaded and drawn.
*/ */
const drawMarkers = async (ctx, markers, z) => { async function drawMarkers(ctx, markers, z) {
const markerPromises = []; const markerPromises = [];
for (const marker of markers) { for (const marker of markers) {
@ -104,17 +123,18 @@ const drawMarkers = async (ctx, markers, z) => {
// Await marker drawings before continuing // Await marker drawings before continuing
await Promise.all(markerPromises); await Promise.all(markerPromises);
}; }
/** /**
* Draws a list of coordinates onto a canvas and styles the resulting path. * Draws a path (polyline or polygon) on a canvas with specified styles.
* @param {object} ctx Canvas context object. * @param {CanvasRenderingContext2D} ctx - Canvas context object.
* @param {List[Number]} path List of coordinates. * @param {number[][]} path - List of coordinates [longitude, latitude] representing the path.
* @param {object} query Request query parameters. * @param {object} query - Request query parameters, which can include `fill`, `width`, `borderwidth`, `linecap`, `linejoin`, `border`, and `stroke` styles.
* @param {string} pathQuery Path query parameter. * @param {string} pathQuery - Path specific query parameters, which can include `fill:`, `width:`, `stroke:` styles.
* @param {number} z Map zoom level. * @param {number} z - Map zoom level.
* @returns {void | null}
*/ */
const drawPath = (ctx, path, query, pathQuery, z) => { function drawPath(ctx, path, query, pathQuery, z) {
const splitPaths = pathQuery.split('|'); const splitPaths = pathQuery.split('|');
if (!path || path.length < 2) { if (!path || path.length < 2) {
@ -209,9 +229,26 @@ const drawPath = (ctx, path, query, pathQuery, z) => {
ctx.strokeStyle = 'rgba(0,64,255,0.7)'; ctx.strokeStyle = 'rgba(0,64,255,0.7)';
} }
ctx.stroke(); ctx.stroke();
}; return;
}
export const renderOverlay = async ( /**
* Renders an overlay on a canvas, including paths and markers.
* @async
* @param {number} z - Map zoom level.
* @param {number} x - X tile coordinate.
* @param {number} y - Y tile coordinate.
* @param {number} bearing - Map bearing in degrees.
* @param {number} pitch - Map pitch in degrees.
* @param {number} w - Width of the canvas.
* @param {number} h - Height of the canvas.
* @param {number} scale - Scaling factor.
* @param {number[][][]} paths - Array of paths, each path is an array of coordinate pairs [longitude, latitude].
* @param {object[]} markers - Array of marker objects, see drawMarker for individual marker properties.
* @param {object} query - Request query parameters.
* @returns {Promise<Buffer | null>} A promise that resolves with the canvas as a Buffer or null if nothing to draw.
*/
export async function renderOverlay(
z, z,
x, x,
y, y,
@ -223,7 +260,7 @@ export const renderOverlay = async (
paths, paths,
markers, markers,
query, query,
) => { ) {
if ((!paths || paths.length === 0) && (!markers || markers.length === 0)) { if ((!paths || paths.length === 0) && (!markers || markers.length === 0)) {
return null; return null;
} }
@ -261,9 +298,17 @@ export const renderOverlay = async (
await drawMarkers(ctx, markers, z); await drawMarkers(ctx, markers, z);
return canvas.toBuffer(); return canvas.toBuffer();
}; }
export const renderWatermark = (width, height, scale, text) => { /**
* Renders a watermark text on a canvas.
* @param {number} width - Width of the canvas.
* @param {number} height - Height of the canvas.
* @param {number} scale - Scaling factor.
* @param {string} text - The watermark text to render.
* @returns {HTMLCanvasElement} A canvas element with the rendered watermark text.
*/
export function renderWatermark(width, height, scale, text) {
const canvas = createCanvas(scale * width, scale * height); const canvas = createCanvas(scale * width, scale * height);
const ctx = canvas.getContext('2d'); const ctx = canvas.getContext('2d');
ctx.scale(scale, scale); ctx.scale(scale, scale);
@ -276,9 +321,17 @@ export const renderWatermark = (width, height, scale, text) => {
ctx.fillText(text, 5, height - 5); ctx.fillText(text, 5, height - 5);
return canvas; return canvas;
}; }
export const renderAttribution = (width, height, scale, text) => { /**
* Renders an attribution text on a canvas with a background.
* @param {number} width - Width of the canvas.
* @param {number} height - Height of the canvas.
* @param {number} scale - Scaling factor.
* @param {string} text - The attribution text to render.
* @returns {HTMLCanvasElement} A canvas element with the rendered attribution text.
*/
export function renderAttribution(width, height, scale, text) {
const canvas = createCanvas(scale * width, scale * height); const canvas = createCanvas(scale * width, scale * height);
const ctx = canvas.getContext('2d'); const ctx = canvas.getContext('2d');
ctx.scale(scale, scale); ctx.scale(scale, scale);
@ -300,4 +353,4 @@ export const renderAttribution = (width, height, scale, text) => {
ctx.fillText(text, width - textWidth - padding / 2, height - textHeight + 8); ctx.fillText(text, width - textWidth - padding / 2, height - textHeight + 8);
return canvas; return canvas;
}; }