feat: enable Google Polyline in Static Images endpoint 🕺 (#648)

* docs: update `encodedpath` documentation

Co-Authored-By: Niklas Hösl <nik.hoesl@hotmail.com>
Signed-off-by: Vinayak Kulkarni <19776877+vinayakkulkarni@users.noreply.github.com>

* feat: enable `encodedpath` in query to load encoded polyline

Signed-off-by: Vinayak Kulkarni <19776877+vinayakkulkarni@users.noreply.github.com>

* refactor!: update static map endpoint `path`

Signed-off-by: Vinayak Kulkarni <19776877+vinayakkulkarni@users.noreply.github.com>

* chore: remove `console.log()`

Signed-off-by: Vinayak Kulkarni <19776877+vinayakkulkarni@users.noreply.github.com>

* docs: remove `encodedpath` arg

Signed-off-by: Vinayak Kulkarni <19776877+vinayakkulkarni@users.noreply.github.com>

* revert: docs update

Signed-off-by: Vinayak Kulkarni <19776877+vinayakkulkarni@users.noreply.github.com>

* chore(deps): add `@mapbox/polyline` dep

Signed-off-by: Vinayak Kulkarni <19776877+vinayakkulkarni@users.noreply.github.com>

* chore(deps): add `@mapbox/polyline` dep

Signed-off-by: Vinayak Kulkarni <19776877+vinayakkulkarni@users.noreply.github.com>

* fix: enable default `stroke`

Signed-off-by: Vinayak Kulkarni <19776877+vinayakkulkarni@users.noreply.github.com>

* docs: update documentation

Signed-off-by: Vinayak Kulkarni <19776877+vinayakkulkarni@users.noreply.github.com>

* test: decode URI component

Signed-off-by: Vinayak Kulkarni <19776877+vinayakkulkarni@users.noreply.github.com>

* fix: drawPath method

Signed-off-by: Vinayak Kulkarni <19776877+vinayakkulkarni@users.noreply.github.com>

* fix: safely decode URI Component

Signed-off-by: Vinayak Kulkarni <19776877+vinayakkulkarni@users.noreply.github.com>

* fix: enable multiple paths while extracting them from query

Signed-off-by: Vinayak Kulkarni <19776877+vinayakkulkarni@users.noreply.github.com>

* fix: lockfile conflict

Signed-off-by: Vinayak Kulkarni <19776877+vinayakkulkarni@users.noreply.github.com>

Signed-off-by: Vinayak Kulkarni <19776877+vinayakkulkarni@users.noreply.github.com>
Co-authored-by: Niklas Hösl <nik.hoesl@hotmail.com>
This commit is contained in:
Vinayak Kulkarni 2023-01-13 02:18:43 +05:30 committed by GitHub
parent 646f67e987
commit bb0cd60e64
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 1553 additions and 8103 deletions

View file

@ -39,6 +39,8 @@ Static images
* e.g. ``5.9,45.8|5.9,47.8|10.5,47.8|10.5,45.8|5.9,45.8`` * e.g. ``5.9,45.8|5.9,47.8|10.5,47.8|10.5,45.8|5.9,45.8``
* can be provided multiple times * can be provided multiple times
* or pass the path as per [Maptiler Cloud API](https://docs.maptiler.com/cloud/api/static-maps/)
* Match pattern: ((fill|stroke|width)\:[^\|]+\|)*((enc:.+)|((-?\d+\.?\d*,-?\d+\.?\d*\|)+(-?\d+\.?\d*,-?\d+\.?\d*)))
* ``latlng`` - indicates coordinates are in ``lat,lng`` order rather than the usual ``lng,lat`` * ``latlng`` - indicates coordinates are in ``lat,lng`` order rather than the usual ``lng,lat``
* ``fill`` - color to use as the fill (e.g. ``red``, ``rgba(255,255,255,0.5)``, ``#0000ff``) * ``fill`` - color to use as the fill (e.g. ``red``, ``rgba(255,255,255,0.5)``, ``#0000ff``)

9398
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -20,6 +20,7 @@
"dependencies": { "dependencies": {
"@mapbox/glyph-pbf-composite": "0.0.3", "@mapbox/glyph-pbf-composite": "0.0.3",
"@mapbox/mbtiles": "0.12.1", "@mapbox/mbtiles": "0.12.1",
"@mapbox/polyline": "^1.1.1",
"@mapbox/sphericalmercator": "1.2.0", "@mapbox/sphericalmercator": "1.2.0",
"@mapbox/vector-tile": "1.3.1", "@mapbox/vector-tile": "1.3.1",
"@maplibre/maplibre-gl-native": "5.1.1", "@maplibre/maplibre-gl-native": "5.1.1",
@ -38,9 +39,9 @@
"pbf": "3.2.1", "pbf": "3.2.1",
"proj4": "2.8.0", "proj4": "2.8.0",
"request": "2.88.2", "request": "2.88.2",
"sanitize-filename": "1.6.3",
"sharp": "0.31.3", "sharp": "0.31.3",
"tileserver-gl-styles": "2.0.0", "tileserver-gl-styles": "2.0.0"
"sanitize-filename": "1.6.3"
}, },
"devDependencies": { "devDependencies": {
"@commitlint/cli": "^17.4.1", "@commitlint/cli": "^17.4.1",

View file

@ -15,11 +15,14 @@ import sanitize from 'sanitize-filename';
import SphericalMercator from '@mapbox/sphericalmercator'; import SphericalMercator from '@mapbox/sphericalmercator';
import mlgl from '@maplibre/maplibre-gl-native'; import mlgl from '@maplibre/maplibre-gl-native';
import MBTiles from '@mapbox/mbtiles'; import MBTiles from '@mapbox/mbtiles';
import polyline from '@mapbox/polyline';
import proj4 from 'proj4'; import proj4 from 'proj4';
import request from 'request'; import request from 'request';
import { getFontsPbf, getTileUrls, fixTileJSONCenter } from './utils.js'; import { getFontsPbf, getTileUrls, fixTileJSONCenter } from './utils.js';
const FLOAT_PATTERN = '[+-]?(?:\\d+|\\d+.?\\d+)'; const FLOAT_PATTERN = '[+-]?(?:\\d+|\\d+.?\\d+)';
const PATH_PATTERN =
/^((fill|stroke|width)\:[^\|]+\|)*((enc:.+)|((-?\d+\.?\d*,-?\d+\.?\d*\|)+(-?\d+\.?\d*,-?\d+\.?\d*)))/;
const httpTester = /^(http(s)?:)?\/\//; const httpTester = /^(http(s)?:)?\/\//;
const mercator = new SphericalMercator(); const mercator = new SphericalMercator();
@ -147,47 +150,67 @@ const parseCoordinates = (coordinatePair, query, transformer) => {
* @param {Function} transformer Optional transform function. * @param {Function} transformer Optional transform function.
*/ */
const extractPathsFromQuery = (query, transformer) => { const extractPathsFromQuery = (query, transformer) => {
// Return an empty list if no paths have been provided // Initiate paths array
if (!query.path) {
return [];
}
const paths = []; const paths = [];
// Return an empty list if no paths have been provided
// Check if multiple paths have been provided and mimic a list if it's a if ('path' in query && !query.path) {
// single path. return paths;
const providedPaths = Array.isArray(query.path) ? query.path : [query.path]; }
// Parse paths provided via path query argument
// Iterate through paths, parse and validate them if ('path' in query) {
for (const provided_path of providedPaths) { const providedPaths = Array.isArray(query.path) ? query.path : [query.path];
const currentPath = []; // Iterate through paths, parse and validate them
for (const providedPath of providedPaths) {
// Extract coordinate-list from path // Logic for pushing coords to path when path includes google polyline
const pathParts = (provided_path || '').split('|'); if (
providedPath.includes('enc:') &&
// Iterate through coordinate-list, parse the coordinates and validate them PATH_PATTERN.test(decodeURIComponent(providedPath))
for (const pair of pathParts) { ) {
// Extract coordinates from coordinate pair const encodedPaths = providedPath.split(',');
const pairParts = pair.split(','); for (const path of encodedPaths) {
const line = path
// Ensure we have two coordinates .split('|')
if (pairParts.length === 2) { .filter(
const pair = parseCoordinates(pairParts, query, transformer); (x) =>
!x.startsWith('fill') &&
// Ensure coordinates could be parsed and skip them if not !x.startsWith('stroke') &&
if (pair === null) { !x.startsWith('width'),
continue; )
.join('')
.replace('enc:', '');
const coords = polyline.decode(line).map(([lat, lng]) => [lng, lat]);
paths.push(coords);
} }
} else {
// Iterate through paths, parse and validate them
const currentPath = [];
// Add the coordinate-pair to the current path if they are valid // Extract coordinate-list from path
currentPath.push(pair); 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);
}
} }
} }
// Extend list of paths with current path if it contains coordinates
if (currentPath.length) {
paths.push(currentPath);
}
} }
return paths; return paths;
}; };
@ -422,65 +445,109 @@ const drawMarkers = async (ctx, markers, z) => {
* @param {number} z Map zoom level. * @param {number} z Map zoom level.
*/ */
const drawPath = (ctx, path, query, z) => { const drawPath = (ctx, path, query, z) => {
if (!path || path.length < 2) { const renderPath = (splitPaths) => {
return null; if (!path || path.length < 2) {
} return null;
ctx.beginPath();
// Transform coordinates to pixel on canvas and draw lines between points
for (const pair of path) {
const px = precisePx(pair, z);
ctx.lineTo(px[0], px[1]);
}
// Check if first coordinate matches last coordinate
if (
path[0][0] === path[path.length - 1][0] &&
path[0][1] === path[path.length - 1][1]
) {
ctx.closePath();
}
// Optionally fill drawn shape with a rgba color from query
if (query.fill !== undefined) {
ctx.fillStyle = query.fill;
ctx.fill();
}
// Get line width from query and fall back to 1 if not provided
const lineWidth = query.width !== undefined ? parseFloat(query.width) : 1;
// Ensure line width is valid
if (lineWidth > 0) {
// Get border width from query and fall back to 10% of line width
const borderWidth =
query.borderwidth !== undefined
? parseFloat(query.borderwidth)
: lineWidth * 0.1;
// Set rendering style for the start and end points of the path
// https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineCap
ctx.lineCap = query.linecap || 'butt';
// Set rendering style for overlapping segments of the path with differing directions
// https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineJoin
ctx.lineJoin = query.linejoin || 'miter';
// In order to simulate a border we draw the path two times with the first
// beeing the wider border part.
if (query.border !== undefined && borderWidth > 0) {
// We need to double the desired border width and add it to the line width
// in order to get the desired border on each side of the line.
ctx.lineWidth = lineWidth + borderWidth * 2;
// Set border style as rgba
ctx.strokeStyle = query.border;
ctx.stroke();
} }
ctx.lineWidth = lineWidth; ctx.beginPath();
ctx.strokeStyle = query.stroke || 'rgba(0,64,255,0.7)';
// Transform coordinates to pixel on canvas and draw lines between points
for (const pair of path) {
const px = precisePx(pair, z);
ctx.lineTo(px[0], px[1]);
}
// Check if first coordinate matches last coordinate
if (
path[0][0] === path[path.length - 1][0] &&
path[0][1] === path[path.length - 1][1]
) {
ctx.closePath();
}
// Optionally fill drawn shape with a rgba color from query
const pathHasFill =
splitPaths.filter((x) => x.startsWith('fill')).length > 0;
if (query.fill !== undefined || pathHasFill) {
if ('fill' in query) {
ctx.fillStyle = query.fill || 'rgba(255,255,255,0.4)';
}
if (pathHasFill) {
ctx.fillStyle = splitPaths
.find((x) => x.startsWith('fill:'))
.replace('fill:', '');
}
ctx.fill();
}
// Get line width from query and fall back to 1 if not provided
const pathHasWidth =
splitPaths.filter((x) => x.startsWith('width')).length > 0;
if (query.width !== undefined || pathHasWidth) {
let lineWidth = 1;
// Get line width from query
if ('width' in query) {
lineWidth = Number(query.width);
}
// Get line width from path in query
if (pathHasWidth) {
lineWidth = Number(
splitPaths.find((x) => x.startsWith('width:')).replace('width:', ''),
);
}
// Get border width from query and fall back to 10% of line width
const borderWidth =
query.borderwidth !== undefined
? parseFloat(query.borderwidth)
: lineWidth * 0.1;
// Set rendering style for the start and end points of the path
// https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineCap
ctx.lineCap = query.linecap || 'butt';
// Set rendering style for overlapping segments of the path with differing directions
// https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineJoin
ctx.lineJoin = query.linejoin || 'miter';
// In order to simulate a border we draw the path two times with the first
// beeing the wider border part.
if (query.border !== undefined && borderWidth > 0) {
// We need to double the desired border width and add it to the line width
// in order to get the desired border on each side of the line.
ctx.lineWidth = lineWidth + borderWidth * 2;
// Set border style as rgba
ctx.strokeStyle = query.border;
ctx.stroke();
}
ctx.lineWidth = lineWidth;
}
const pathHasStroke =
splitPaths.filter((x) => x.startsWith('stroke')).length > 0;
if (query.stroke !== undefined || pathHasStroke) {
if ('stroke' in query) {
ctx.strokeStyle = query.stroke;
}
// Path Width gets higher priority
if (pathHasWidth) {
ctx.strokeStyle = splitPaths
.find((x) => x.startsWith('stroke:'))
.replace('stroke:', '');
}
} else {
ctx.strokeStyle = 'rgba(0,64,255,0.7)';
}
ctx.stroke(); ctx.stroke();
};
// Check if path in query is valid
if (Array.isArray(query.path)) {
for (let i = 0; i < query.path.length; i += 1) {
renderPath(decodeURIComponent(query.path.at(i)).split('|'));
}
} else {
renderPath(decodeURIComponent(query.path).split('|'));
} }
}; };