/** * MapLocalization: canvas drawing + view/events (gộp từ mapCanvas.js và mapLocalization.js). * Dùng ElementReference từ Blazor, không dùng element ID. */ // ==================== Canvas: occupancy grid ==================== function drawCell(pixels, canvasWidth, canvasHeight, x, y, r, g, b, a) { if (x >= 0 && x < canvasWidth && y >= 0 && y < canvasHeight) { const pixelIndex = (y * canvasWidth + x) * 4; pixels[pixelIndex] = r; pixels[pixelIndex + 1] = g; pixels[pixelIndex + 2] = b; pixels[pixelIndex + 3] = a; } } /** * @param {HTMLCanvasElement} canvas * @param {number} width * @param {number} height * @param {Uint8Array|number[]|ArrayBuffer} knownCells - sparse: 5 bytes per cell [index_byte0..3, value] */ export function drawOccupancyGrid(canvas, width, height, knownCells) { if (!canvas || !knownCells || knownCells.length === 0) return; if (typeof canvas.getContext !== 'function') return; let cellsArray; if (knownCells instanceof ArrayBuffer) { cellsArray = new Uint8Array(knownCells); } else if (knownCells instanceof Uint8Array) { cellsArray = knownCells; } else if (Array.isArray(knownCells)) { cellsArray = new Uint8Array(knownCells); } else if (typeof knownCells === 'string') { try { const binaryString = atob(knownCells); cellsArray = new Uint8Array(binaryString.length); for (let i = 0; i < binaryString.length; i++) cellsArray[i] = binaryString.charCodeAt(i); } catch (e) { return; } } else { return; } const canvasWidth = width; const canvasHeight = height; const sizeChanged = canvas.width !== canvasWidth || canvas.height !== canvasHeight; const previousKnownCells = canvas._previousKnownCells || new Set(); const previousWidth = canvas._previousWidth || width; const previousHeight = canvas._previousHeight || height; if (sizeChanged || previousWidth !== width || previousHeight !== height) { canvas.width = canvasWidth; canvas.height = canvasHeight; previousKnownCells.clear(); } const ctx = canvas.getContext('2d'); if (!ctx) return; if (sizeChanged || previousWidth !== width || previousHeight !== height) { ctx.clearRect(0, 0, canvas.width, canvas.height); } const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); const pixels = imageData.data; const unknownA = 0; const currentKnownCellsSet = new Set(); for (let i = 0; i < cellsArray.length; i += 5) { const cellIndex = (cellsArray[i] | (cellsArray[i + 1] << 8) | (cellsArray[i + 2] << 16) | (cellsArray[i + 3] << 24)) >>> 0; currentKnownCellsSet.add(cellIndex); } if (!sizeChanged && previousWidth === width && previousHeight === height) { previousKnownCells.forEach(cellIndex => { if (!currentKnownCellsSet.has(cellIndex)) { const gridX = cellIndex % width; const gridY = Math.floor(cellIndex / width); const canvasX = gridX; const canvasY = gridY; // No Y-flip: ROS convention (gridY=0 at bottom) → CSS scale(1,-1) handles display flip if (canvasX >= 0 && canvasX < width && canvasY >= 0 && canvasY < height) { drawCell(pixels, canvas.width, canvas.height, canvasX, canvasY, 0, 0, 0, unknownA); } } }); } for (let i = 0; i < cellsArray.length; i += 5) { if (i + 4 >= cellsArray.length) break; const cellIndex = (cellsArray[i] | (cellsArray[i + 1] << 8) | (cellsArray[i + 2] << 16) | (cellsArray[i + 3] << 24)) >>> 0; const cellValue = cellsArray[i + 4]; if (cellValue < 0 || cellValue > 100) continue; const gridX = cellIndex % width; const gridY = Math.floor(cellIndex / width); const canvasX = gridX; const canvasY = gridY; // No Y-flip: ROS convention (gridY=0 at bottom) → CSS scale(1,-1) handles display flip if (canvasX < 0 || canvasX >= width || canvasY < 0 || canvasY >= height) continue; let intensity; if (cellValue === 0) { intensity = 254; } else if (cellValue >= 1 && cellValue <= 100) { const v = 254.0 - (cellValue * 254.0 / 100.0); intensity = Math.max(0, Math.min(254, Math.round(v))); } else { continue; } drawCell(pixels, canvas.width, canvas.height, canvasX, canvasY, intensity, intensity, intensity, 255); } ctx.putImageData(imageData, 0, 0); canvas._previousKnownCells = currentKnownCellsSet; canvas._previousWidth = width; canvas._previousHeight = height; } /** * @param {HTMLCanvasElement} canvas */ export function clearCanvas(canvas) { if (!canvas) return; const ctx = canvas.getContext('2d'); if (!ctx) return; ctx.clearRect(0, 0, canvas.width, canvas.height); canvas._previousKnownCells = new Set(); canvas._previousWidth = 0; canvas._previousHeight = 0; } /** * @param {HTMLCanvasElement} canvas * @param {number} width * @param {number} height */ export function setCanvasSize(canvas, width, height) { if (!canvas) return; canvas.width = width; canvas.height = height; } /** * @param {HTMLElement} element * @returns {number[]} [width, height] */ export function getElementSize(element) { if (!element) return [800, 600]; try { if (typeof element.getBoundingClientRect === 'function') { const rect = element.getBoundingClientRect(); return [rect.width || 0, rect.height || 0]; } } catch (e) {} return [800, 600]; } /** * @param {HTMLCanvasElement} canvas * @param {number[][]} points - [[x,y], ...] * @param {string} color * @param {number} radius */ export function drawPointsOnCanvas(canvas, points, color, radius) { if (!canvas) return; const ctx = canvas.getContext('2d'); if (!ctx) return; /*ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.fillStyle = color; for (const point of points) { if (point.length >= 2) { ctx.beginPath(); ctx.arc(point[0], point[1], radius, 0, 2 * Math.PI); ctx.fill(); } }*/ ctx.clearRect(0, 0, canvas.width, canvas.height); const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); const pixels = imageData.data; for (const point of points) { if (point[0] < 0 || point[0] >= canvas.width || point[1] < 0 || point[1] >= canvas.height) continue; const index = (Math.floor(point[1]) * canvas.width + Math.floor(point[0])) * 4; pixels[index] = 255; pixels[index + 1] = 0; pixels[index + 2] = 0; pixels[index + 3] = 255; } ctx.putImageData(imageData, 0, 0); } // ==================== View / SVG / Rect ==================== /** * @param {SVGElement} svg * @param {number} width * @param {number} height * @param {number} originX * @param {number} originY */ export function setSvgConfig(svg, width, height, originX, originY) { if (!svg) return; svg.setAttribute('width', width.toString()); svg.setAttribute('height', height.toString()); svg.setAttribute('viewBox', `${originX} ${originY} ${width} ${height}`); } /** * @param {SVGElement} svg * @param {number} width * @param {number} height */ export function setSvgRect(svg, width, height) { if (!svg) return; svg.setAttribute('width', width.toString()); svg.setAttribute('height', height.toString()); } /** * @param {HTMLCanvasElement} canvas * @param {number} width * @param {number} height */ export function setCanvasRect(canvas, width, height) { if (!canvas) return; canvas.style.width = width + 'px'; canvas.style.height = height + 'px'; } /** * @param {HTMLElement} element * @param {number} top * @param {number} left * @param {number} [width] * @param {number} [height] */ export function setMapMovement(element, top, left, width = null, height = null) { if (!element) return; element.style.top = top + 'px'; element.style.left = left + 'px'; if (width !== null && height !== null) { element.style.width = width + 'px'; element.style.height = height + 'px'; } } /** * @param {SVGPolylineElement} polylineElement * @param {string} points - "x1,y1 x2,y2 ..." */ export function setPolylinePointsOnly(polylineElement, points) { if (!polylineElement) return; polylineElement.setAttribute('points', points || ''); } // ==================== Events (container, wheel, mouse) ==================== /** * @param {any} dotnetRef * @param {HTMLElement} element * @param {string} funcName */ export async function updateContainerRect(dotnetRef, element, funcName) { if (!element || !dotnetRef) return; const rect = element.getBoundingClientRect(); await dotnetRef.invokeMethodAsync(funcName, rect.x, rect.y, rect.width, rect.height, rect.top, rect.right, rect.bottom, rect.left); } /** * @param {any} dotnetRef * @param {HTMLElement} element * @param {string} funcName */ export function registerResizeObserver(dotnetRef, element, funcName) { if (!element || !dotnetRef) return; const resizeObserver = new ResizeObserver(async () => { await updateContainerRect(dotnetRef, element, funcName); }); resizeObserver.observe(element); } /** * @param {any} dotnetRef * @param {HTMLElement} element * @param {string} funcName */ export function addMouseWheelEventListener(dotnetRef, element, funcName) { if (!element || !dotnetRef) return; element.addEventListener('wheel', async (ev) => { ev.preventDefault(); ev.stopPropagation(); await dotnetRef.invokeMethodAsync(funcName, ev.deltaY, ev.clientX, ev.clientY); }, { passive: false }); } /** * @param {any} dotnetRef * @param {HTMLElement} element * @param {string} funcName */ export function addMouseMoveEventListener(dotnetRef, element, funcName) { if (!element || !dotnetRef) return; element.addEventListener('mousemove', async (ev) => { ev.preventDefault(); ev.stopPropagation(); await dotnetRef.invokeMethodAsync(funcName, ev.clientX, ev.clientY, ev.buttons, ev.ctrlKey, ev.movementX, ev.movementY); }); } /** * @param {any} dotnetRef * @param {HTMLElement} element * @param {string} funcName */ export function addMouseDownEventListener(dotnetRef, element, funcName) { if (!element || !dotnetRef) return; element.addEventListener('mousedown', async (ev) => { ev.preventDefault(); ev.stopPropagation(); await dotnetRef.invokeMethodAsync(funcName, ev.button, ev.altKey, ev.ctrlKey, ev.shiftKey); }); } /** * @param {any} dotnetRef * @param {HTMLElement} element * @param {string} funcName */ export function addMouseUpEventListener(dotnetRef, element, funcName) { if (!element || !dotnetRef) return; element.addEventListener('mouseup', async (ev) => { ev.preventDefault(); ev.stopPropagation(); await dotnetRef.invokeMethodAsync(funcName, ev.button, ev.altKey, ev.ctrlKey, ev.shiftKey); }); }