You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(
originalImg,
minLon = 10.0,
maxLon = 11.0,
minLat = 52.0,
maxLat = 53.0,
intervalDeg = 0.25,
frameWidth = 2,
frameColor = 'black',
tickLength = 10,
tickColor = 'black',
labelFont = '14px Arial',
labelColor = 'black',
labelPadding = 5
) {
/**
* Helper function to format decimal degrees into a DMS-like string.
* @param {number} decimal The decimal degree value.
* @param {'lat'|'lon'} type The type of coordinate ('lat' or 'lon').
* @returns {string} The formatted coordinate string (e.g., "52°30'N").
*/
function formatCoord(decimal, type) {
const isLat = type === 'lat';
let suffix = '';
if (isLat) {
suffix = decimal >= 0 ? 'N' : 'S';
} else {
suffix = decimal >= 0 ? 'E' : 'W';
}
const absDecimal = Math.abs(decimal);
let degrees = Math.floor(absDecimal);
let minutes = Math.round((absDecimal - degrees) * 60);
if (minutes === 60) {
minutes = 0;
degrees += 1;
}
const minutesStr = String(minutes).padStart(2, '0');
return `${degrees}°${minutesStr}'${suffix}`;
}
// Attempt to dynamically load Google Fonts if not a standard web-safe font.
try {
const fontNameMatch = labelFont.match(/(?:'|")([^'"]+)(?:'|")|([\w\s-]+)$/);
if (fontNameMatch) {
const fontName = (fontNameMatch[1] || fontNameMatch[2] || '').trim();
if (fontName && !document.fonts.check(`1em "${fontName}"`)) {
const fontUrl = `https://fonts.googleapis.com/css2?family=${fontName.replace(/ /g, '+')}&display=swap`;
// Avoid adding duplicate link tags
if (!document.querySelector(`link[href="${fontUrl}"]`)) {
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = fontUrl;
document.head.appendChild(link);
}
await document.fonts.load(`1em "${fontName}"`);
}
}
} catch (e) {
console.error("Font loading failed, continuing with browser default.", e);
}
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
ctx.font = labelFont;
// Estimate the size of the labels to calculate necessary padding around the image.
const testLabel = formatCoord(-179.999, 'lon'); // A long label like "180°00'W"
const metrics = ctx.measureText(testLabel);
const maxLabelWidth = metrics.width;
const estimatedFontHeight = parseInt(labelFont.match(/(\d+)px/i)?.[1] || '14', 10);
const fontHeight = (metrics.actualBoundingBoxAscent && metrics.actualBoundingBoxDescent) ?
(metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent) :
(estimatedFontHeight * 1.2); // Fallback with a multiplier
const horizontalSpace = frameWidth / 2 + labelPadding + maxLabelWidth;
const verticalSpace = frameWidth / 2 + labelPadding + fontHeight;
canvas.width = originalImg.width + 2 * horizontalSpace;
canvas.height = originalImg.height + 2 * verticalSpace;
// Fill background (optional, but good practice)
ctx.fillStyle = 'white';
ctx.fillRect(0, 0, canvas.width, canvas.height);
const imgArea = {
x: horizontalSpace,
y: verticalSpace,
width: originalImg.width,
height: originalImg.height,
right: horizontalSpace + originalImg.width,
bottom: verticalSpace + originalImg.height
};
ctx.drawImage(originalImg, imgArea.x, imgArea.y);
// Draw the main map frame
ctx.strokeStyle = frameColor;
ctx.lineWidth = frameWidth;
ctx.strokeRect(imgArea.x, imgArea.y, imgArea.width, imgArea.height);
// Prepare for drawing ticks and labels
ctx.fillStyle = labelColor;
ctx.strokeStyle = tickColor;
ctx.lineWidth = 1; // Ticks are typically thin
const lonSpan = maxLon - minLon;
const latSpan = maxLat - minLat;
// Draw longitude ticks and labels (Top/Bottom)
if (lonSpan > 0 && intervalDeg > 0) {
const lonSteps = Math.round(lonSpan / intervalDeg);
for (let i = 0; i <= lonSteps; i++) {
const lon = minLon + i * intervalDeg;
const xPos = imgArea.x + (i / lonSteps) * imgArea.width;
const label = formatCoord(lon, 'lon');
// Top tick (points inward/down)
ctx.beginPath();
ctx.moveTo(xPos, imgArea.y);
ctx.lineTo(xPos, imgArea.y + tickLength);
ctx.stroke();
// Bottom tick (points inward/up)
ctx.beginPath();
ctx.moveTo(xPos, imgArea.bottom);
ctx.lineTo(xPos, imgArea.bottom - tickLength);
ctx.stroke();
// Top label
ctx.textAlign = 'center';
ctx.textBaseline = 'bottom';
ctx.fillText(label, xPos, imgArea.y - frameWidth / 2 - labelPadding);
// Bottom label
ctx.textBaseline = 'top';
ctx.fillText(label, xPos, imgArea.bottom + frameWidth / 2 + labelPadding);
}
}
// Draw latitude ticks and labels (Left/Right)
if (latSpan > 0 && intervalDeg > 0) {
const latSteps = Math.round(latSpan / intervalDeg);
for (let i = 0; i <= latSteps; i++) {
const lat = minLat + i * intervalDeg;
// Canvas Y is inverted, so proportion calculation is different
const yPos = imgArea.bottom - (i / latSteps) * imgArea.height;
const label = formatCoord(lat, 'lat');
// Left tick (points inward/right)
ctx.beginPath();
ctx.moveTo(imgArea.x, yPos);
ctx.lineTo(imgArea.x + tickLength, yPos);
ctx.stroke();
// Right tick (points inward/left)
ctx.beginPath();
ctx.moveTo(imgArea.right, yPos);
ctx.lineTo(imgArea.right - tickLength, yPos);
ctx.stroke();
// Left label
ctx.textAlign = 'right';
ctx.textBaseline = 'middle';
ctx.fillText(label, imgArea.x - frameWidth / 2 - labelPadding, yPos);
// Right label
ctx.textAlign = 'left';
ctx.fillText(label, imgArea.right + frameWidth / 2 + labelPadding, yPos);
}
}
return canvas;
}
Apply Changes