import { useMemo, useRef, useState, type CSSProperties, type PointerEvent } from "react"; import type { Floor, Position, Space, SpaceType, Tpu } from "../../types"; import { geometryForTpu } from "./geometry"; type Props = { floor: Floor; spaces: Space[]; geometrySpaces: Space[]; spaceTypes: SpaceType[]; tpus: Tpu[]; showTpus: boolean; zoom: number; movingSpaceId: string | null; inspectedItem: FocusedItem | null; onInspectSpace: (space: Space) => void; onInspectTpu: (tpu: Tpu) => void; onClearInspection: () => void; onEditSpace: (space: Space) => void; onPreviewPositions: (positions: Record) => void; onMoveComplete: (positions: Record) => void; }; type DragState = { start: Position; origins: Record; positions: Record; moved: boolean; }; type FocusedItem = { kind: "space" | "tpu"; id: string; }; type HoveredItem = FocusedItem & { name: string; detail: string; x: number; y: number; color: string; }; export function MapView(props: Props) { const { floor, spaces, geometrySpaces, spaceTypes, tpus, showTpus, zoom, movingSpaceId } = props; const svgRef = useRef(null); const [drag, setDrag] = useState(null); const [hovered, setHovered] = useState(null); const spacesById = useMemo(() => new Map(geometrySpaces.map(space => [space.id, space])), [geometrySpaces]); const typeByCode = useMemo(() => new Map(spaceTypes.map(type => [type.code, type])), [spaceTypes]); const membership = useMemo(() => { const result = new Map(); tpus.forEach(tpu => tpu.members.forEach(member => result.set(member.space_id, tpu))); return result; }, [tpus]); const focusedItem = movingSpaceId ? { kind: "space" as const, id: movingSpaceId } : hovered || props.inspectedItem; const focusedTpuId = useMemo(() => { if (!focusedItem) return undefined; return focusedItem.kind === "tpu" ? focusedItem.id : membership.get(focusedItem.id)?.id; }, [focusedItem, membership]); const highlightedSpaceIds = useMemo(() => { if (!focusedItem) return new Set(); if (!focusedTpuId) return new Set([focusedItem.id]); return new Set(tpus.find(tpu => tpu.id === focusedTpuId)?.members.map(member => member.space_id) || []); }, [focusedItem, focusedTpuId, tpus]); const hasMapFocus = Boolean(focusedItem); function pointerPosition(event: PointerEvent): Position { const bounds = svgRef.current!.getBoundingClientRect(); return { x: Math.max(0, Math.min(1, (event.clientX - bounds.left) / bounds.width)), y: Math.max(0, Math.min(1, (event.clientY - bounds.top) / bounds.height)), }; } function startDrag(event: PointerEvent, space: Space) { if (movingSpaceId !== space.id) return; event.stopPropagation(); props.onInspectSpace(space); const origins = { [space.id]: { ...space.position } }; event.currentTarget.setPointerCapture(event.pointerId); setDrag({ start: pointerPosition(event), origins, positions: origins, moved: false }); } function moveDrag(event: PointerEvent) { if (!drag) return; const current = pointerPosition(event); const dx = current.x - drag.start.x; const dy = current.y - drag.start.y; const positions = Object.fromEntries(Object.entries(drag.origins).map(([id, origin]) => [id, { x: Math.max(0, Math.min(1, origin.x + dx)), y: Math.max(0, Math.min(1, origin.y + dy)), }])); props.onPreviewPositions(positions); setDrag({ ...drag, positions, moved: drag.moved || Math.abs(dx) + Math.abs(dy) > .001 }); } function endDrag() { if (!drag) return; if (drag.moved) props.onMoveComplete(drag.positions); setDrag(null); } function hoverTpu(tpu: Tpu, x: number, y: number) { setHovered({ kind: "tpu", id: tpu.id, name: tpu.name, detail: `${tpu.members.length} espaço${tpu.members.length === 1 ? "" : "s"} · clique para ver os detalhes`, x, y, color: tpu.color, }); } return (
{ if (!movingSpaceId) props.onClearInspection(); }} aria-label={`Planta do ${floor.name}`} > {showTpus && tpus.map(tpu => { const geometry = geometryForTpu(tpu, floor, spacesById); if (!geometry.frames.length) return null; return ( {geometry.links.map((link, index) => ( { if (!movingSpaceId) hoverTpu(tpu, event.clientX, event.clientY); }} onPointerLeave={() => setHovered(null)} onClick={event => { event.stopPropagation(); if (!movingSpaceId) props.onInspectTpu(tpu); }} role="button" aria-label={`Ver detalhes de ${tpu.name}`} > Ver detalhes de {tpu.name} ))} {geometry.frames.map((frame, index) => ( { if (!movingSpaceId) hoverTpu(tpu, event.clientX, event.clientY); }} onPointerLeave={() => setHovered(null)} onClick={event => { event.stopPropagation(); if (!movingSpaceId) props.onInspectTpu(tpu); }} className="tpu-frame-hit" > {frame.label && {frame.label}} ))} ); })} {spaces.map(space => { const x = space.position.x * floor.width; const y = space.position.y * floor.height; const type = typeByCode.get(space.type); const tpu = membership.get(space.id); const highlighted = highlightedSpaceIds.has(space.id); return ( { if (!movingSpaceId) setHovered({ kind: "space", id: space.id, name: space.name, detail: `${type?.label || "Espaço"} · clique para ver os detalhes`, x: event.clientX, y: event.clientY, color: type?.color || "#425466", }); }} onPointerLeave={() => setHovered(null)} onPointerDown={event => startDrag(event, space)} onClick={event => { event.stopPropagation(); if (!movingSpaceId || movingSpaceId === space.id) props.onInspectSpace(space); }} onDoubleClick={event => { event.stopPropagation(); if (!movingSpaceId || movingSpaceId === space.id) props.onEditSpace(space); }} role="button" aria-label={`${space.name}, ${space.base_area_m2.toFixed(2)} metros quadrados${tpu ? `, ${tpu.name}` : ""}`} > {space.name.length > 12 ? `${space.name.slice(0, 11)}…` : space.name} ); })} {hovered && !drag && ( )}
); }