feat: use built-in svg marker (#2)

* feat: use built-in svg marker

Signed-off-by: Craig Kochis <cjkochis@gmail.com>

* Scaling based on screen resolution

---------

Signed-off-by: Craig Kochis <cjkochis@gmail.com>
Co-authored-by: Boon Boonsiri <bboonsir@uwaterloo.ca>
This commit is contained in:
Craig Kochis 2023-10-25 10:26:56 -04:00 committed by GitHub
parent 87565edc68
commit 7ad403fa21
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 60 additions and 11 deletions

View file

@ -23,6 +23,7 @@ import {
getTileUrls,
isValidHttpUrl,
fixTileJSONCenter,
generateMarker,
} from './utils.js';
import {
PMtilesOpen,
@ -242,6 +243,10 @@ const parseMarkerOptions = (optionsList, marker) => {
marker.offsetY = parseFloat(providedOffset[1]);
}
break;
// Custom color when using default marker
case 'color':
marker.color = String(optionParts[1]);
break;
}
}
};
@ -284,12 +289,16 @@ const extractMarkersFromQuery = (query, options, transformer) => {
}
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 isDefaultMarker = iconURI === 'default';
const isRemoteURL = iconURI.startsWith('http://') || iconURI.startsWith('https://');
const isDataURL = iconURI.startsWith('data:');
if (!(isRemoteURL || isDataURL)) {
if (isDefaultMarker) {
iconURI = 'default'; // use built-in marker
} else if (!(isRemoteURL || isDataURL)) {
// Sanitize URI with sanitize-filename
// https://www.npmjs.com/package/sanitize-filename#details
iconURI = sanitize(iconURI);
@ -303,9 +312,9 @@ const extractMarkersFromQuery = (query, options, transformer) => {
// When we encounter a remote icon check if the configuration explicitly allows them.
} else if (isRemoteURL && options.allowRemoteMarkerIcons !== true) {
continue;
continue; // skip
} else if (isDataURL && options.allowInlineMarkerImages !== true) {
continue;
continue; // skip
}
// Ensure marker location could be parsed
@ -347,11 +356,19 @@ const precisePx = (ll, zoom) => {
* @param {object} marker Marker object parsed by extractMarkersFromQuery.
* @param {number} z Map zoom level.
*/
const drawMarker = (ctx, marker, z) => {
return new Promise((resolve) => {
const drawMarker = (ctx, marker, z, scale) => {
return new Promise(async (resolve) => {
const img = new Image();
const pixelCoords = precisePx(marker.location, z);
let imgSrc = marker.icon;
if (marker.icon === 'default') {
// use default built-in marker (svg -> dataURL)
const markerDataURL = await generateMarker(scale, marker.color);
marker.scale = marker.scale ? marker.scale * (1 / scale) : 1 / scale;
imgSrc = markerDataURL;
}
const getMarkerCoordinates = (imageWidth, imageHeight, scale) => {
// Images are placed with their top-left corner at the provided location
// within the canvas but we expect icons to be centered and above it.
@ -402,10 +419,10 @@ const drawMarker = (ctx, marker, z) => {
};
img.onload = drawOnCanvas;
img.src = imgSrc;
img.onerror = (err) => {
throw err;
};
img.src = marker.icon;
});
};
@ -418,12 +435,12 @@ const drawMarker = (ctx, marker, z) => {
* @param {List[Object]} markers Marker objects parsed by extractMarkersFromQuery.
* @param {number} z Map zoom level.
*/
const drawMarkers = async (ctx, markers, z) => {
const drawMarkers = async (ctx, markers, z, scale) => {
const markerPromises = [];
for (const marker of markers) {
// Begin drawing marker
markerPromises.push(drawMarker(ctx, marker, z));
markerPromises.push(drawMarker(ctx, marker, z, scale));
}
// Await marker drawings before continuing
@ -582,7 +599,7 @@ const renderOverlay = async (
});
// Await drawing of markers before rendering the canvas
await drawMarkers(ctx, markers, z);
await drawMarkers(ctx, markers, z, scale);
return canvas.toBuffer();
};

View file

@ -3,6 +3,7 @@
import path from 'path';
import fs from 'node:fs';
import clone from 'clone';
import sharp from 'sharp';
import glyphCompose from '@mapbox/glyph-pbf-composite';
/**
@ -174,3 +175,34 @@ export const isValidHttpUrl = (string) => {
return url.protocol === 'http:' || url.protocol === 'https:';
};
// generate base64 data url for default marker
export const generateMarker = async (scale = 1, color = '#000257') => {
const markerSVG = `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="30" height="45" fill="none">
<defs>
<path id="reuse-0" fill="#000" d="M15 44.4c-6.44 0-11.67-2.6-11.67-5.82 0-3.21 5.23-5.82 11.67-5.82 6.44 0 11.67 2.6 11.67 5.82S21.44 44.4 15 44.4Z" opacity=".04"/>
</defs>
<use xlink:href="#reuse-0" opacity=".04"/>
<use xlink:href="#reuse-0" opacity=".04"/>
<path fill="#000" d="M15 43.87c-5.83 0-10.56-2.37-10.56-5.29S9.17 33.3 15 33.3s10.56 2.37 10.56 5.3c0 2.91-4.73 5.28-10.56 5.28Z" opacity=".04"/>
<path fill="#000" d="M15 43.34c-5.22 0-9.44-2.13-9.44-4.76s4.22-4.76 9.44-4.76 9.44 2.13 9.44 4.76-4.22 4.76-9.44 4.76Z" opacity=".04"/>
<path fill="#000" d="M15 42.81c-4.6 0-8.33-1.9-8.33-4.23 0-2.34 3.73-4.23 8.33-4.23s8.33 1.9 8.33 4.23c0 2.34-3.73 4.23-8.33 4.23Z" opacity=".04"/>
<path fill="#000" d="M15 42.29c-3.99 0-7.22-1.66-7.22-3.7 0-2.05 3.23-3.71 7.22-3.71 3.99 0 7.22 1.66 7.22 3.7 0 2.05-3.23 3.7-7.22 3.7Z" opacity=".04"/>
<path fill="#000" d="M15 41.76c-3.38 0-6.11-1.43-6.11-3.18 0-1.75 2.73-3.17 6.11-3.17 3.38 0 6.11 1.42 6.11 3.17s-2.73 3.18-6.11 3.18Z" opacity=".04"/>
<path fill="#000" d="M15 41.23c-2.76 0-5-1.19-5-2.65s2.24-2.64 5-2.64 5 1.18 5 2.64c0 1.46-2.24 2.65-5 2.65Z" opacity=".04"/>
<path fill="${color}" d="M0 14.97c0 6.18 7.5 14.96 13.61 23.28.82 1.1 1.96 1.1 2.78 0C22.5 29.93 30 21.3 30 14.97 30 6.7 23.28 0 15 0 6.72 0 0 6.7 0 14.97Z"/>
<path fill="#000" d="M15 0c8.28 0 15 6.7 15 14.97 0 6.34-7.5 14.96-13.61 23.28-.83 1.13-1.96 1.1-2.78 0C7.5 29.93 0 21.15 0 14.97 0 6.7 6.72 0 15 0Zm0 1.1C7.32 1.1 1.11 7.3 1.11 14.98c0 2.66 1.67 6.3 4.2 10.24 2.53 3.94 6.13 8.2 9.2 12.38.22.3.36.46.49.6.13-.14.27-.3.5-.6 3.07-4.19 6.26-8.42 8.93-12.34 2.66-3.93 4.46-7.56 4.46-10.28C28.89 7.3 22.69 1.1 15 1.1Z" opacity=".25"/>
<path fill="#000" d="M15 21.06a6.1 6.1 0 1 1-.01-12.2 6.1 6.1 0 0 1 .01 12.2Z" opacity=".25"/>
<path fill="#fff" d="M15 21.06a6.1 6.1 0 1 1-.01-12.2 6.1 6.1 0 0 1 .01 12.2Z"/>
</svg>`;
let markerBuffer = Buffer.from(markerSVG);
if (scale > 1) {
markerBuffer = await sharp(markerBuffer)
.resize(30 * scale, 45 * scale)
.toBuffer();
}
const pngBuffer = await sharp(markerBuffer).png().toBuffer();
return `data:image/png;base64,${pngBuffer.toString('base64')}`;
};