From b82997a091dc82d7d302e31852b3be125f9d3d04 Mon Sep 17 00:00:00 2001 From: Gabriel RZ Date: Wed, 26 Nov 2025 14:24:44 -0600 Subject: [PATCH] interactive venue map --- src/components/venue-map/BfsShortestPath.ts | 29 +++ src/components/venue-map/InteractiveMap.tsx | 216 +++++++++++++++++++ src/components/venue-map/MapElements.ts | 68 ++++++ src/components/venue-map/VenueMap.tsx | 55 +++++ src/components/venue-map/VenueMapSection.tsx | 21 ++ src/pages/Index.tsx | 3 + 6 files changed, 392 insertions(+) create mode 100644 src/components/venue-map/BfsShortestPath.ts create mode 100644 src/components/venue-map/InteractiveMap.tsx create mode 100644 src/components/venue-map/MapElements.ts create mode 100644 src/components/venue-map/VenueMap.tsx create mode 100644 src/components/venue-map/VenueMapSection.tsx diff --git a/src/components/venue-map/BfsShortestPath.ts b/src/components/venue-map/BfsShortestPath.ts new file mode 100644 index 0000000..104255e --- /dev/null +++ b/src/components/venue-map/BfsShortestPath.ts @@ -0,0 +1,29 @@ +export function bfsShortestPath(graph, start, end) { + const queue = [start]; + const visited = new Set([start]); + const parent = {}; // para reconstruir el path + + while (queue.length > 0) { + const node = queue.shift(); + if (node === end) { + // reconstruir camino + const path = []; + let cur = end; + while (cur) { + path.push(cur); + cur = parent[cur]; + } + return path.reverse(); + } + + for (const neighbor of graph[node] || []) { + if (!visited.has(neighbor)) { + visited.add(neighbor); + parent[neighbor] = node; + queue.push(neighbor); + } + } + } + + return null; // sin camino +} \ No newline at end of file diff --git a/src/components/venue-map/InteractiveMap.tsx b/src/components/venue-map/InteractiveMap.tsx new file mode 100644 index 0000000..6a4b9cb --- /dev/null +++ b/src/components/venue-map/InteractiveMap.tsx @@ -0,0 +1,216 @@ +import React, {useRef, useEffect, useState} from "react"; +import {graph, shapeMap, shapes} from "@/components/venue-map/MapElements.ts"; +import VenueMap from "@/components/venue-map/VenueMap.tsx"; +import {bfsShortestPath} from "@/components/venue-map/BfsShortestPath.ts"; + + +function getShapeCenter(shape, width, height) { + if (shape.radius) { + return { + x: shape.x * width, + y: shape.y * height + }; + } + + return { + x: shape.x * width + (shape.width * width) / 2, + y: shape.y * height + (shape.height * height) / 2 + }; +} + +function drawPath(ctx, path, width, height) { + if (!path || path.length < 2) return; + + ctx.strokeStyle = "yellow"; + ctx.lineWidth = width * 0.01; + ctx.beginPath(); + + let first = shapeMap[path[0]]; + let c = getShapeCenter(first, width, height); + ctx.moveTo(c.x, c.y); + + for (let i = 1; i < path.length; i++) { + const shape = shapeMap[path[i]]; + const pos = getShapeCenter(shape, width, height); + ctx.lineTo(pos.x, pos.y); + } + + ctx.stroke(); +} + + +function drawShapes(ctx, shapes, width, height) { + shapes.forEach(shape => { + ctx.fillStyle = shape.color; + ctx.strokeStyle = shape.color; + + switch (shape.type) { + case "assistant": + ctx.beginPath(); + ctx.arc( + shape.x * width, + shape.y * height, + shape.radius * width, + 0, + Math.PI * 2 + ); + ctx.fill(); + break; + + case "path": + ctx.lineWidth = shape.thickness * width; + ctx.beginPath(); + ctx.moveTo(shape.x1 * width, shape.y1 * height); + ctx.lineTo(shape.x2 * width, shape.y2 * height); + ctx.stroke(); + break; + + default: + ctx.fillRect( + shape.x * width, + shape.y * height, + shape.width * width, + shape.height * height + ); + ctx.fillStyle = "white"; + const textX = shape.x * width + (shape.width * width)/2 - ctx.measureText(shape.label).width/2; + const textY = shape.y * height + (shape.height * height)/2 - (width * 0.01); + ctx.fillText(shape.label, textX, textY); + break; + } + }); +} + +const InteractiveMap = () => { + const containerRef = useRef(null); + const canvasRef = useRef(null); + + const [startNode, setStartNode] = useState(null); + const [endNode, setEndNode] = useState(null); + + const [hovered, setHovered] = useState(null); + + /** Detectar shape bajo el mouse usando coordenadas escaladas */ + function getShapeAt(x, y, width, height) { + return shapes.find(shape => { + const sx = shape.x * width; + const sy = shape.y * height; + const sw = shape.width * width; + const sh = shape.height * height; + + return x >= sx && x <= sx + sw && y >= sy && y <= sy + sh; + }); + } + + function handleMouseMove(e) { + const container = containerRef.current; + const rect = container.getBoundingClientRect(); + + const x = e.clientX - rect.left; + const y = e.clientY - rect.top; + + const width = rect.width; + const height = rect.height; + + const shape = getShapeAt(x, y, width, height); + + if (shape?.type !== hovered) { + setHovered(shape?.type || null); + } + } + + function handleClick(e) { + const container = containerRef.current; + const rect = container.getBoundingClientRect(); + + const x = e.clientX - rect.left; + const y = e.clientY - rect.top; + + const width = rect.width; + const height = rect.height; + + const shape = getShapeAt(x, y, width, height); + if (!shape) return; + + if (!startNode) { + setStartNode(shape.type); + } else if (!endNode) { + setEndNode(shape.type); + } else { + setStartNode(shape.type); + setEndNode(null); + } + } + + useEffect(() => { + const container = containerRef.current; + const canvas = canvasRef.current; + const ctx = canvas.getContext("2d"); + + function draw() { + const rect = container.getBoundingClientRect(); + const w = rect.width; + const h = rect.height; + + canvas.width = w; + canvas.height = h; + + ctx.clearRect(0, 0, w, h); + + drawShapes(ctx, shapes, w, h); + + // Resaltar hover + if (hovered) { + const s = shapes.find(s => s.type === hovered); + if (s) { + ctx.save(); + ctx.strokeStyle = "rgba(0, 150, 255, 0.9)"; + ctx.lineWidth = w * 0.005; + + ctx.strokeRect( + s.x * w, + s.y * h, + s.width * w, + s.height * h + ); + + ctx.restore(); + } + } + + if (startNode && endNode) { + const path = bfsShortestPath(graph, startNode, endNode); + drawPath(ctx, path, w, h); + } + } + + draw(); + window.addEventListener("resize", draw); + return () => window.removeEventListener("resize", draw); + }, [hovered, startNode, endNode]); // 👈 hovered agregado aquí + + // Cambiar cursor según hover + useEffect(() => { + const container = containerRef.current; + container.style.cursor = hovered ? "pointer" : "default"; + }, [hovered]); + + + return ( +
+ + +
+ ); +}; + + +export default InteractiveMap; diff --git a/src/components/venue-map/MapElements.ts b/src/components/venue-map/MapElements.ts new file mode 100644 index 0000000..2e7a781 --- /dev/null +++ b/src/components/venue-map/MapElements.ts @@ -0,0 +1,68 @@ +export const shapes = [ + { type: "entrance", label: "Entrada", x: 0.845, y: 0.75, width: 0.13, height: 0.11, color: "blue" }, + { type: "free_space", label: "", x: 0.45, y: 0.79, width: 0.1, height: 0.1, color: "white" }, + { type: "main_stage", label: "Sala Principal", x: 0.02, y: 0.21, width: 0.445, height: 0.5, color: "blue" }, + { type: "sala_1", label: "Sala 1", x: 0.52, y: 0.19, width: 0.135, height: 0.245, color: "orange" }, + { type: "sala_2", label: "Sala 2", x: 0.52, y: 0.46, width: 0.135, height: 0.245, color: "orange" }, + { type: "sala_3", label: "Sala 3", x: 0.67, y: 0.19, width: 0.135, height: 0.245, color: "orange" }, + { type: "sala_speakers", label: "Speakers", x: 0.02, y: 0.84, width: 0.085, height: 0.14, color: "green" }, + { type: "coffe_break", label: "Coffe Break", x: 0.19, y: 0.19, width: 0.11, height: 0.05, color: "pink" }, + { type: "tamales", label: "Tamales", x: 0.31, y: 0.19, width: 0.11, height: 0.05, color: "pink" }, + { type: "comedor_1", label: "Comedor", x: 0.67, y: 0.46, width: 0.154, height: 0.25, color: "green" }, + { type: "comedor_2", label: "Comedor", x: 0.824, y: 0.22, width: 0.135, height: 0.490, color: "green" }, + { type: "wc_1", label: "Baños", x: 0.05, y: 0.02, width: 0.228, height: 0.12, color: "red" }, + { type: "wc_2", label: "Baños", x: 0.705, y: 0.02, width: 0.228, height: 0.12, color: "red" }, + + { type: "p1", label: "Puerta", x: 0.1, y: 0.69, width: 0.08, height: 0.05, color: "black" }, + { type: "p2", label: "Puerta", x: 0.3, y: 0.69, width: 0.08, height: 0.05, color: "black" }, + { type: "p3", label: "Puerta", x: 0.462, y: 0.26, width: 0.06, height: 0.12, color: "black" }, + { type: "p4", label: "Puerta", x: 0.462, y: 0.53, width: 0.06, height: 0.12, color: "black" }, + { type: "p5", label: "Puerta", x: 0.548, y: 0.69, width: 0.08, height: 0.05, color: "black" }, + { type: "p6", label: "Puerta", x: 0.63, y: 0.26, width: 0.06, height: 0.12, color: "black" }, + { type: "p7", label: "Puerta", x: 0.63, y: 0.53, width: 0.06, height: 0.12, color: "black" }, + { type: "p8", label: "Puerta", x: 0.785, y: 0.26, width: 0.06, height: 0.12, color: "black" }, + { type: "p9", label: "Puerta", x: 0.78, y: 0.69, width: 0.08, height: 0.05, color: "black" }, + { type: "p_wc_1", label: "Puerta", x: 0.117, y: 0.17, width: 0.055, height: 0.06, color: "black" }, + { type: "p_wc_2", label: "Puerta", x: 0.8, y: 0.17, width: 0.05, height: 0.06, color: "black" }, + { type: "assistant", label: "Tu", x: 0.86, y: 0.8, radius: 0.01, color: "red" }, + + /**{ + type: "path", + label: "Camino", + x1: 0.1, y1: 0.1, + x2: 0.8, y2: 0.9, + thickness: 0.003, + color: "red" + }**/ +]; + +export const shapeMap = Object.fromEntries( + shapes.map(s => [s.type, s]) +); + +export const graph = { + entrance: ["p5", "p9", "free_space"], + free_space: ["p2", "p1", "p5", "p9", "sala_speakers", "entrance"], + main_stage: ["coffe_break", "tamales", "p2", "p1", "p3", "p4", "p_wc_1"], + sala_1: ["p3", "p6"], + sala_2: ["p4","p7"], + sala_3: ["p6", "p8"], + sala_speakers: ["p1","p2","free_space"], + coffe_break: ["main_stage"], + tamales: ["main_stage"], + comedor_1: ["p7", "p9", "comedor_2"], + comedor_2: ["p8", "p9", "p_wc_2", "comedor_1"], + p1: ["main_stage", "sala_speakers", "free_space"], + p2: ["main_stage", "sala_speakers", "free_space"], + p3: ["main_stage", "sala_1"], + p4: ["main_stage", "sala_2"], + p5: ["sala_speakers", "sala_2", "entrance"], + p6: ["sala_1", "sala_3"], + p7: ["sala_2", "comedor_1"], + p8: ["sala_3", "comedor_2"], + p9: ["comedor_1", "comedor_2", "free_space", "entrance"], + p_wc_1: ["main_stage", "wc_1"], + p_wc_2: ["comedor_2", "wc_2", "p8"], + wc_1: ["p_wc_1"], + wc_2: ["p_wc_2"], +} diff --git a/src/components/venue-map/VenueMap.tsx b/src/components/venue-map/VenueMap.tsx new file mode 100644 index 0000000..ffc8021 --- /dev/null +++ b/src/components/venue-map/VenueMap.tsx @@ -0,0 +1,55 @@ +import React from "react"; + +export const VenueMap: React.FC> = (props) => { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ) +} +export default VenueMap diff --git a/src/components/venue-map/VenueMapSection.tsx b/src/components/venue-map/VenueMapSection.tsx new file mode 100644 index 0000000..ac5c213 --- /dev/null +++ b/src/components/venue-map/VenueMapSection.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import InteractiveMap from "@/components/venue-map/InteractiveMap.tsx"; + +const VenueMapSection = () => { + + return ( +
+

Mapa del Evento

+
+ + +
+ ); +}; + +export default VenueMapSection; diff --git a/src/pages/Index.tsx b/src/pages/Index.tsx index 2f02387..28ee1c7 100644 --- a/src/pages/Index.tsx +++ b/src/pages/Index.tsx @@ -7,6 +7,8 @@ import Organizers from "@/components/Organizers.tsx"; import Speakers from "@/components/Speakers.tsx"; import CommunitiesAllies from "@/components/communities/CommunitiesAllies.tsx"; import Sponsors from "@/components/Sponsors.tsx"; +import VenueMap from "@/components/venue-map/VenueMap.tsx"; +import VenueMapSection from "@/components/venue-map/VenueMapSection.tsx"; const Index = () => { const location = useLocation(); @@ -32,6 +34,7 @@ const Index = () => { <> +