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:
parent
87565edc68
commit
7ad403fa21
2 changed files with 60 additions and 11 deletions
|
@ -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();
|
||||
};
|
||||
|
|
32
src/utils.js
32
src/utils.js
|
@ -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')}`;
|
||||
};
|
||||
|
|
Loading…
Reference in a new issue