This commit is contained in:
160
www/map-geo.js
Normal file
160
www/map-geo.js
Normal file
@@ -0,0 +1,160 @@
|
||||
(() => {
|
||||
/**
|
||||
* 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,
|
||||
};
|
||||
})();
|
||||
Reference in New Issue
Block a user