Xong phần map viewer
Some checks failed
Test / test (push) Has been cancelled

This commit is contained in:
2026-06-20 09:18:19 +02:00
parent 819323f8c8
commit 90e8e9d252
13 changed files with 431 additions and 32 deletions

View File

@@ -2,6 +2,7 @@
const el = (id) => document.getElementById(id);
const t = (key, vars) => window.I18n?.t(key, vars) ?? key;
const Geo = () => window.MapGeo;
const Occ = () => window.MapOccupancyCanvas;
const state = {
mapId: null,
@@ -14,6 +15,9 @@
view: Geo()?.createView(1, 0, 0) || { scale: 1, panX: 0, panY: 0 },
panning: null,
tipVisible: true,
showOrigin: true,
/** ROS yaml thresholds (occupied_thresh, free_thresh, negate) from map.yaml. */
yamlMeta: null,
/** Pending ROS metadata from upload dialog (set before PNG picker). */
uploadMeta: null,
};
@@ -27,7 +31,10 @@
const sheetEl = el("mapEditorSheet");
const gridEl = el("mapEditorSheetGrid");
const imageEl = el("mapEditorImage");
const occupancyCanvasEl = el("mapEditorOccupancyCanvas");
const originEl = el("mapEditorOrigin");
const originHitEl = el("mapEditorOriginHit");
const originLabelEl = el("mapEditorOriginLabel");
const emptyEl = el("mapEditorEmpty");
const tipEl = el("mapEditorCanvasTip");
const statusViewEl = el("mapEditorStatusView");
@@ -94,7 +101,58 @@
}
function hasFloorPlan() {
return !!(state.map?.image_file && imageEl && !imageEl.hidden && imageEl.naturalWidth);
return !!(
state.map?.image_file &&
imageEl &&
!imageEl.hidden &&
imageEl.naturalWidth &&
occupancyCanvasEl &&
!occupancyCanvasEl.hidden &&
occupancyCanvasEl.width
);
}
/** ROS yaml thresholds for occupancy coloring. */
function mapRenderMeta() {
const base = mapMetaForOriginDisplay() || state.map || {};
const yaml = state.yamlMeta || {};
return {
occupied_thresh:
base.occupied_thresh != null ? base.occupied_thresh : yaml.occupied_thresh,
free_thresh: base.free_thresh != null ? base.free_thresh : yaml.free_thresh,
negate: base.negate != null ? base.negate : yaml.negate,
};
}
async function loadYamlMeta() {
state.yamlMeta = await fetchExistingYamlMeta();
}
function setOccupancyCanvasVisible(visible) {
if (!occupancyCanvasEl) return;
occupancyCanvasEl.hidden = !visible;
occupancyCanvasEl.setAttribute("aria-hidden", visible ? "false" : "true");
}
/** Paint RViz-style occupancy colors from loaded PNG (hidden loader img). */
function paintOccupancyFromImage() {
const occ = Occ();
if (!occ || !occupancyCanvasEl || !imageEl?.naturalWidth) return false;
const ok = occ.renderFromImage(occupancyCanvasEl, imageEl, mapRenderMeta());
setOccupancyCanvasVisible(ok);
return ok;
}
/**
* Paint live occupancy grid (record/stream — roadmap step 2+).
* @param {{ width: number, height: number, data: number[]|Int8Array|string }} grid
*/
function paintOccupancyGrid(grid) {
const occ = Occ();
if (!occ || !occupancyCanvasEl || !grid) return false;
const ok = occ.renderGrid(occupancyCanvasEl, grid);
if (ok) setOccupancyCanvasVisible(true);
return ok;
}
function setDirty(flag) {
@@ -204,7 +262,6 @@
gridEl.hidden = true;
}
if (originEl) originEl.hidden = !has;
updateOriginMarker();
updateStatusBar();
}
@@ -224,11 +281,25 @@
return base;
}
function setShowOrigin(show) {
state.showOrigin = !!show;
const btn = el("mapEditorOriginBtn");
btn?.classList.toggle("is-active", state.showOrigin);
btn?.setAttribute("aria-pressed", state.showOrigin ? "true" : "false");
updateOriginMarker();
}
function setOriginLabelVisible(visible) {
if (!originEl) return;
originEl.classList.toggle("mapEditorOrigin--showLabel", !!visible);
if (originLabelEl) originLabelEl.setAttribute("aria-hidden", visible ? "false" : "true");
}
function updateOriginMarker() {
if (!originEl) return;
const geo = Geo();
const { width, height } = floorPlanSize();
if (!geo || !hasFloorPlan() || !width || !height) {
if (!state.showOrigin || !geo || !hasFloorPlan() || !width || !height) {
originEl.hidden = true;
originEl.setAttribute("aria-hidden", "true");
return;
@@ -249,18 +320,23 @@
originEl.style.top = `${pt.y}px`;
originEl.style.transform = `rotate(${yawDeg}deg)`;
const labelEl = el("mapEditorOriginLabel");
if (labelEl) {
labelEl.textContent = t("maps.editor.originLabelShort", {
if (originLabelEl) {
originLabelEl.textContent = t("maps.editor.originLabelShort", {
x: ox.toFixed(2),
y: oy.toFixed(2),
});
originLabelEl.setAttribute("aria-hidden", "true");
}
originEl.title = t("maps.editor.originTooltip", {
const tooltip = t("maps.editor.originTooltip", {
x: ox.toFixed(3),
y: oy.toFixed(3),
yaw: ((oyaw * 180) / Math.PI).toFixed(1),
});
if (originHitEl) {
originHitEl.title = tooltip;
originHitEl.setAttribute("aria-label", tooltip);
}
setOriginLabelVisible(false);
}
function updateStatusBar(pointerClient) {
@@ -311,11 +387,13 @@
imageEl.src = url;
imageEl.hidden = false;
if (emptyEl) emptyEl.hidden = true;
setOccupancyCanvasVisible(false);
} else {
if (imageEl) {
imageEl.hidden = true;
imageEl.removeAttribute("src");
}
setOccupancyCanvasVisible(false);
if (emptyEl) emptyEl.hidden = false;
}
updateMenuActionsUi();
@@ -323,6 +401,7 @@
imageEl?.addEventListener(
"load",
() => {
paintOccupancyFromImage();
updateImageLayer();
fitToView();
},
@@ -381,6 +460,7 @@
async function reloadMap() {
if (!state.mapId) return;
state.map = await api(`/api/maps/${encodeURIComponent(state.mapId)}`);
await loadYamlMeta();
updateHeader();
renderMapImage();
fillSettingsForm();
@@ -392,6 +472,7 @@
state.readOnly = !!callbacks.readOnly || !callbacks.canWrite;
state.dirty = false;
state.tipVisible = true;
state.showOrigin = true;
state.activeTool = "pan";
state.view = Geo()?.createView(1, 0, 0) || { scale: 1, panX: 0, panY: 0 };
if (tipEl) {
@@ -399,6 +480,7 @@
tipEl.textContent = t("maps.editor.canvasTip");
}
setActiveTool("pan");
setShowOrigin(true);
setDirty(false);
applyReadOnlyUi();
reloadMap().catch((e) => alert(e.message));
@@ -409,6 +491,7 @@
state.map = null;
state.callbacks = {};
state.uploadMeta = null;
state.yamlMeta = null;
menuDialogEl?.close();
settingsDialogEl?.close();
activateDialogEl?.close();
@@ -584,6 +667,7 @@
state.callbacks.onMapUpdated?.(updated);
setDirty(false);
fillSettingsForm();
await loadYamlMeta();
renderMapImage();
menuDialogEl?.close();
uploadMetaDialogEl?.close();
@@ -689,6 +773,11 @@
}
function bindEvents() {
originHitEl?.addEventListener("mouseenter", () => setOriginLabelVisible(true));
originHitEl?.addEventListener("mouseleave", () => setOriginLabelVisible(false));
originHitEl?.addEventListener("focus", () => setOriginLabelVisible(true));
originHitEl?.addEventListener("blur", () => setOriginLabelVisible(false));
el("mapEditorBackBtn")?.addEventListener("click", () => {
if (state.dirty && !confirm(t("maps.editor.unsavedLeave"))) return;
state.callbacks.onClose?.();
@@ -714,6 +803,7 @@
saveMap().catch((e) => alert(e.message));
});
el("mapEditorPanBtn")?.addEventListener("click", () => setActiveTool("pan"));
el("mapEditorOriginBtn")?.addEventListener("click", () => setShowOrigin(!state.showOrigin));
el("mapEditorFitBtn")?.addEventListener("click", fitToView);
el("mapEditorCenterBtn")?.addEventListener("click", () => {
dismissCanvasTip();
@@ -818,6 +908,9 @@
Object.values(uploadMetaFields).forEach((node) => {
node?.addEventListener("input", () => {
updateOriginMarker();
if (uploadMetaDialogEl?.open && imageEl?.naturalWidth) {
paintOccupancyFromImage();
}
if (node === uploadMetaFields.resolution && uploadMetaDialogEl?.open) {
updateImageLayer();
}
@@ -835,5 +928,11 @@
bindCanvasPanZoom();
bindEvents();
window.MapEditorApp = { open, close, reloadMap };
window.MapEditorApp = {
open,
close,
reloadMap,
paintOccupancyGrid,
paintOccupancyFromImage,
};
})();