161 lines
4.7 KiB
JavaScript
161 lines
4.7 KiB
JavaScript
(() => {
|
|
/**
|
|
* MiR-style map coordinate model (3 layers):
|
|
*
|
|
* 1. View space — screen pixels in the viewport; pan + zoom (UI only).
|
|
* 2. Image space — floor plan pixels (1 px image = 1 px on sheet; 20 px/m at res 0.05).
|
|
* 3. World space — map coordinates in metres + yaw (robot, positions).
|
|
*
|
|
* view --scale+translate--> image --resolution+origin--> world
|
|
*/
|
|
|
|
function meta(map) {
|
|
return {
|
|
resolution: Number(map?.resolution) || 0.05,
|
|
originX: Number(map?.origin_x) || 0,
|
|
originY: Number(map?.origin_y) || 0,
|
|
originYaw: Number(map?.origin_yaw) || 0,
|
|
width: Number(map?.width) || 0,
|
|
height: Number(map?.height) || 0,
|
|
};
|
|
}
|
|
|
|
function pixelsPerMeter(map) {
|
|
const res = meta(map).resolution;
|
|
return res > 0 ? 1 / res : 20;
|
|
}
|
|
|
|
function imageSize(map, imageEl) {
|
|
const w = imageEl?.naturalWidth || meta(map).width || 0;
|
|
const h = imageEl?.naturalHeight || meta(map).height || 0;
|
|
return { width: w, height: h };
|
|
}
|
|
|
|
/** --- View space (layer 1) --- */
|
|
|
|
function createView(scale = 1, panX = 0, panY = 0) {
|
|
return { scale, panX, panY };
|
|
}
|
|
|
|
function applyViewTransform(el, view) {
|
|
if (!el || !view) return;
|
|
el.style.transform = `translate(${view.panX}px, ${view.panY}px) scale(${view.scale})`;
|
|
}
|
|
|
|
function fitViewToImage(viewportW, viewportH, imageW, imageH, pad = 48) {
|
|
if (!imageW || !imageH) {
|
|
return createView(1, Math.max(40, pad), Math.max(40, pad));
|
|
}
|
|
const scale = Math.min((viewportW - pad) / imageW, (viewportH - pad) / imageH, 4);
|
|
const s = Math.max(0.1, scale);
|
|
return {
|
|
scale: s,
|
|
panX: (viewportW - imageW * s) / 2,
|
|
panY: (viewportH - imageH * s) / 2,
|
|
};
|
|
}
|
|
|
|
function centerViewOnImage(viewportW, viewportH, imageW, imageH, view) {
|
|
const s = view?.scale || 1;
|
|
return {
|
|
scale: s,
|
|
panX: Math.max(40, (viewportW - imageW * s) / 2),
|
|
panY: Math.max(40, (viewportH - imageH * s) / 2),
|
|
};
|
|
}
|
|
|
|
/** Viewport-local px → image px (inverse of translate+scale). */
|
|
function viewportToImage(view, vx, vy) {
|
|
const s = view.scale || 1;
|
|
return {
|
|
x: (vx - view.panX) / s,
|
|
y: (vy - view.panY) / s,
|
|
};
|
|
}
|
|
|
|
/** Image px → viewport-local px. */
|
|
function imageToViewport(view, ix, iy) {
|
|
return {
|
|
x: view.panX + ix * view.scale,
|
|
y: view.panY + iy * view.scale,
|
|
};
|
|
}
|
|
|
|
/** Zoom toward a viewport anchor; keeps image point under cursor fixed. */
|
|
function zoomViewAt(view, anchorVx, anchorVy, factor, minScale = 0.1, maxScale = 8) {
|
|
const img = viewportToImage(view, anchorVx, anchorVy);
|
|
const nextScale = Math.min(maxScale, Math.max(minScale, view.scale * factor));
|
|
return {
|
|
scale: nextScale,
|
|
panX: anchorVx - img.x * nextScale,
|
|
panY: anchorVy - img.y * nextScale,
|
|
};
|
|
}
|
|
|
|
/** --- Image space (layer 2) --- */
|
|
|
|
function gridSteps(map) {
|
|
const ppm = pixelsPerMeter(map);
|
|
return {
|
|
minor: Math.max(1, Math.round(ppm)),
|
|
major: Math.max(1, Math.round(ppm * 5)),
|
|
};
|
|
}
|
|
|
|
function clampImagePoint(px, py, imageW, imageH) {
|
|
if (px < 0 || py < 0 || px > imageW || py > imageH) return null;
|
|
return { x: px, y: py };
|
|
}
|
|
|
|
/** Screen/client coords → image px using the transformed sheet rect. */
|
|
function clientToImage(clientX, clientY, sheetRect, imageW, imageH) {
|
|
if (!sheetRect?.width || !sheetRect?.height || !imageW || !imageH) return null;
|
|
const px = ((clientX - sheetRect.left) / sheetRect.width) * imageW;
|
|
const py = ((clientY - sheetRect.top) / sheetRect.height) * imageH;
|
|
return clampImagePoint(px, py, imageW, imageH);
|
|
}
|
|
|
|
/** --- World space (layer 3) — ROS map_server convention --- */
|
|
|
|
function worldToPixel(map, imgW, imgH, wx, wy) {
|
|
const { resolution, originX, originY } = meta(map);
|
|
return {
|
|
x: (wx - originX) / resolution,
|
|
y: imgH - (wy - originY) / resolution,
|
|
};
|
|
}
|
|
|
|
function pixelToWorld(map, imgW, imgH, px, py) {
|
|
const { resolution, originX, originY } = meta(map);
|
|
return {
|
|
x: originX + px * resolution,
|
|
y: originY + (imgH - py) * resolution,
|
|
};
|
|
}
|
|
|
|
/** Viewport pointer → world (chains view→image→world). */
|
|
function clientToWorld(map, clientX, clientY, sheetRect, imageW, imageH) {
|
|
const img = clientToImage(clientX, clientY, sheetRect, imageW, imageH);
|
|
if (!img) return null;
|
|
return pixelToWorld(map, imageW, imageH, img.x, img.y);
|
|
}
|
|
|
|
window.MapGeo = {
|
|
meta,
|
|
pixelsPerMeter,
|
|
imageSize,
|
|
createView,
|
|
applyViewTransform,
|
|
fitViewToImage,
|
|
centerViewOnImage,
|
|
viewportToImage,
|
|
imageToViewport,
|
|
zoomViewAt,
|
|
gridSteps,
|
|
clientToImage,
|
|
clientToWorld,
|
|
worldToPixel,
|
|
pixelToWorld,
|
|
};
|
|
})();
|