Spaces:
Sleeping
Sleeping
Download frontend/src/features/map/MapView.tsx from gui-sparim/mp_tpu_TESTES: direct link, hf CLI and curl.
- Browser
- Download file 9.3 kB
-
https://huggingface.co/spaces/gui-sparim/mp_tpu_TESTES/resolve/main/frontend/src/features/map/MapView.tsx
- Command line
-
hf download hf://spaces/gui-sparim/mp_tpu_TESTES/frontend/src/features/map/MapView.tsx
-
curl -L -o MapView.tsx https://huggingface.co/spaces/gui-sparim/mp_tpu_TESTES/resolve/main/frontend/src/features/map/MapView.tsx
9.3 kB
| 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<string, Position>) => void; | |
| onMoveComplete: (positions: Record<string, Position>) => void; | |
| }; | |
| type DragState = { | |
| start: Position; | |
| origins: Record<string, Position>; | |
| positions: Record<string, Position>; | |
| 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<SVGSVGElement>(null); | |
| const [drag, setDrag] = useState<DragState | null>(null); | |
| const [hovered, setHovered] = useState<HoveredItem | null>(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<string, Tpu>(); | |
| 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<string>(); | |
| 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<SVGGElement>, 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<SVGSVGElement>) { | |
| 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 ( | |
| <div className="map-scroll"> | |
| <div className="map-shell" style={{ width: `${zoom * 100}%` }}> | |
| <svg | |
| ref={svgRef} | |
| className="market-map" | |
| viewBox={`0 0 ${floor.width} ${floor.height}`} | |
| onPointerMove={moveDrag} | |
| onPointerUp={endDrag} | |
| onPointerCancel={endDrag} | |
| onClick={() => { if (!movingSpaceId) props.onClearInspection(); }} | |
| aria-label={`Planta do ${floor.name}`} | |
| > | |
| <image href={floor.image} width={floor.width} height={floor.height} preserveAspectRatio="none" /> | |
| {showTpus && tpus.map(tpu => { | |
| const geometry = geometryForTpu(tpu, floor, spacesById); | |
| if (!geometry.frames.length) return null; | |
| return ( | |
| <g key={tpu.id} className={`tpu-visual ${focusedTpuId === tpu.id ? "highlighted" : ""} ${hasMapFocus && focusedTpuId !== tpu.id ? "dimmed" : ""}`} style={{ "--tpu-color": tpu.color } as CSSProperties}> | |
| {geometry.links.map((link, index) => ( | |
| <g | |
| key={`link-${index}`} | |
| className="tpu-link-hit" | |
| onPointerEnter={event => { 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}`} | |
| > | |
| <title>Ver detalhes de {tpu.name}</title> | |
| <line className="tpu-link-target" x1={link.x1} y1={link.y1} x2={link.x2} y2={link.y2} /> | |
| <line className={`tpu-link ${link.kind}`} x1={link.x1} y1={link.y1} x2={link.x2} y2={link.y2} /> | |
| </g> | |
| ))} | |
| {geometry.frames.map((frame, index) => ( | |
| <g | |
| key={`frame-${index}`} | |
| onPointerEnter={event => { if (!movingSpaceId) hoverTpu(tpu, event.clientX, event.clientY); }} | |
| onPointerLeave={() => setHovered(null)} | |
| onClick={event => { event.stopPropagation(); if (!movingSpaceId) props.onInspectTpu(tpu); }} | |
| className="tpu-frame-hit" | |
| > | |
| <rect className="tpu-frame" x={frame.x} y={frame.y} width={frame.width} height={frame.height} rx="10" /> | |
| {frame.label && <text className="tpu-label" x={frame.x + 6} y={frame.y - 6}>{frame.label}</text>} | |
| </g> | |
| ))} | |
| </g> | |
| ); | |
| })} | |
| {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 ( | |
| <g | |
| key={space.id} | |
| className={`space-marker ${movingSpaceId === space.id ? "movable" : ""} ${highlighted ? "highlighted" : ""} ${hasMapFocus && !highlighted ? "dimmed" : ""}`} | |
| transform={`translate(${x} ${y})`} | |
| onPointerEnter={event => { 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}` : ""}`} | |
| > | |
| <rect x="-22" y="-11" width="44" height="22" rx="6" fill={type?.color || "#fff"} stroke={tpu?.color || "#425466"} /> | |
| <text textAnchor="middle" dominantBaseline="central">{space.name.length > 12 ? `${space.name.slice(0, 11)}…` : space.name}</text> | |
| </g> | |
| ); | |
| })} | |
| </svg> | |
| {hovered && !drag && ( | |
| <div | |
| className="map-hover-card" | |
| style={{ | |
| "--hover-x": `${hovered.x}px`, | |
| "--hover-y": `${hovered.y}px`, | |
| "--hover-color": hovered.color, | |
| } as CSSProperties} | |
| aria-hidden="true" | |
| > | |
| <span>{hovered.kind === "space" ? "ESPAÇO" : "TPU"}</span> | |
| <strong>{hovered.name}</strong> | |
| <small>{hovered.detail}</small> | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| ); | |
| } | |