// Map2DPlotter Component - Konva.js canvas with zoom/pan for site visualization import React, { useMemo, useState, useRef, useCallback } from 'react'; import { Stage, Layer, Line, Rect, Text, Group } from 'react-konva'; import { ZoomIn, ZoomOut, Move, Maximize2 } from 'lucide-react'; import type { LayoutOption, SiteMetadata } from '../types'; import type Konva from 'konva'; interface Map2DPlotterProps { boundaryCoords: number[][] | null; metadata: SiteMetadata | null; selectedOption: LayoutOption | null; width?: number; height?: number; } export const Map2DPlotter: React.FC = ({ boundaryCoords, metadata, selectedOption, width = 800, height = 600, }) => { // Zoom and pan state const [scale, setScale] = useState(1); const [position, setPosition] = useState({ x: 0, y: 0 }); const stageRef = useRef(null); // Calculate base transform to fit boundary in canvas const baseTransform = useMemo(() => { if (!boundaryCoords || boundaryCoords.length === 0) { return { scale: 1, offsetX: 0, offsetY: 0, minX: 0, minY: 0 }; } const xs = boundaryCoords.map(c => c[0]); const ys = boundaryCoords.map(c => c[1]); const minX = Math.min(...xs); const maxX = Math.max(...xs); const minY = Math.min(...ys); const maxY = Math.max(...ys); const dataWidth = maxX - minX; const dataHeight = maxY - minY; const padding = 60; const scaleX = (width - padding * 2) / dataWidth; const scaleY = (height - padding * 2) / dataHeight; const baseScale = Math.min(scaleX, scaleY) * 0.85; const offsetX = padding + (width - padding * 2 - dataWidth * baseScale) / 2 - minX * baseScale; const offsetY = padding + (height - padding * 2 - dataHeight * baseScale) / 2 + maxY * baseScale; return { scale: baseScale, offsetX, offsetY, minX, minY, maxY }; }, [boundaryCoords, width, height]); // Transform coordinates (flip Y for screen coords) const transformPoint = useCallback((x: number, y: number): [number, number] => { return [ x * baseTransform.scale + baseTransform.offsetX, baseTransform.offsetY - y * baseTransform.scale, ]; }, [baseTransform]); // Flatten boundary coords for Konva Line const boundaryPoints = useMemo(() => { if (!boundaryCoords) return []; return boundaryCoords.flatMap(([x, y]) => transformPoint(x, y)); }, [boundaryCoords, transformPoint]); // Calculate setback boundary (50m inside) const setbackPoints = useMemo(() => { if (!boundaryCoords || boundaryCoords.length < 3) return []; const xs = boundaryCoords.map(c => c[0]); const ys = boundaryCoords.map(c => c[1]); const centerX = xs.reduce((a, b) => a + b, 0) / xs.length; const centerY = ys.reduce((a, b) => a + b, 0) / ys.length; const shrinkFactor = 0.82; const shrunkCoords = boundaryCoords.map(([x, y]) => [ centerX + (x - centerX) * shrinkFactor, centerY + (y - centerY) * shrinkFactor, ]); return shrunkCoords.flatMap(([x, y]) => transformPoint(x, y)); }, [boundaryCoords, transformPoint]); // Handle zoom const handleZoom = (delta: number) => { const newScale = Math.min(Math.max(scale + delta, 0.5), 3); setScale(newScale); }; // Handle wheel zoom const handleWheel = (e: Konva.KonvaEventObject) => { e.evt.preventDefault(); const delta = e.evt.deltaY > 0 ? -0.1 : 0.1; handleZoom(delta); }; // Reset view const resetView = () => { setScale(1); setPosition({ x: 0, y: 0 }); }; // Render plots from selected option const renderPlots = () => { if (!selectedOption?.plots) return null; return selectedOption.plots.map((plot, index) => { // Transform plot position const [x, y] = transformPoint(plot.x, plot.y + plot.height); const w = plot.width * baseTransform.scale; const h = plot.height * baseTransform.scale; // Color based on index const colors = ['#3B82F6', '#10B981', '#F59E0B', '#EF4444', '#8B5CF6']; const color = colors[index % colors.length]; return ( ); }); }; return (
{/* Zoom controls */}
{Math.round(scale * 100)}%
setPosition({ x: e.target.x(), y: e.target.y() })} > {/* Background grid */} {[...Array(25)].map((_, i) => ( ))} {[...Array(25)].map((_, i) => ( ))} {/* Site boundary */} {boundaryPoints.length > 0 && ( )} {/* Setback zone */} {setbackPoints.length > 0 && ( )} {/* Plots */} {renderPlots()} {/* Legend */} {/* Metadata */} {metadata && ( )} {/* Empty state */} {!boundaryCoords && ( )}
{/* Drag hint */}
Drag to pan • Scroll to zoom
); }; export default Map2DPlotter;