Files
App/www/map-geo.js
HiepLM 819323f8c8
Some checks failed
Test / test (push) Has been cancelled
add function map viewer
2026-06-20 10:53:49 +07:00

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,
};
})();