{"version":3,"file":"TrajectoryMap.cjs","names":[],"sources":["../../src/geo/TrajectoryMap.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines, props-count, function-lines — the SVG plot (points,\n * current, height, padding, showGrid, showScale, strokeColor, label), the optional\n * leaflet tile layer (tileUrl, tileAttribution) and the overlay (markers,\n * onMarkerClick). The body projects the path, fits the bounds and hands the same\n * bounds to the tile layer when there is one.\n */\nimport { lazy, Suspense, useEffect, useMemo, useRef, useState, type HTMLAttributes } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport { boundingBox, expandBounds } from \"./bounds\";\nimport { haversineKm } from \"./distance\";\nimport { fitProjection, type PixelPoint } from \"./projection\";\nimport type { Coordinate, GeoMarker } from \"./types\";\nimport styles from \"./TrajectoryMap.module.css\";\n\n// Leaflet layer is loaded on demand — only when a `tileUrl` is supplied, so\n// apps that stick to the tile-free SVG never pull leaflet into their bundle.\nconst LeafletTrajectory = lazy(() =>\n    import(\"./leaflet-map\").then((m) => ({ default: m.LeafletTrajectory })),\n);\n\nexport interface TrajectoryMapProps extends Omit<HTMLAttributes<HTMLDivElement>, \"children\"> {\n    /** Ordered coordinates forming the trajectory. */\n    points: readonly Coordinate[];\n    /** Optional \"current position\" marker, drawn on top of the path. */\n    current?: Coordinate | null;\n    /** Viewport height in pixels. Default: `320`. */\n    height?: number;\n    /** Inner padding in pixels kept clear on every edge. Default: `24`. */\n    padding?: number;\n    /** Draw a light reference grid behind the path (SVG mode). Default: `true`. */\n    showGrid?: boolean;\n    /** Draw a distance scale bar (SVG mode). Default: `true`. */\n    showScale?: boolean;\n    /** Path stroke color. Default: the primary token. */\n    strokeColor?: string;\n    /**\n     * Tile URL template (`{z}/{x}/{y}`) of a map server **you host**. When set,\n     * the map upgrades to a real Leaflet tile layer (requires `leaflet` to be\n     * installed). Omit for the zero-dependency, zero-network SVG plot.\n     */\n    tileUrl?: string;\n    /** Attribution text shown over the tile layer. */\n    tileAttribution?: string;\n    /** Accessible label for the map region. Default: `\"Trajetória\"`. */\n    label?: string;\n    /** Point markers to overlay (e.g. stops, POIs). Included in the auto-fit. */\n    markers?: readonly GeoMarker[];\n    /** Fired when a marker is clicked. */\n    onMarkerClick?: (marker: GeoMarker, index: number) => void;\n}\n\nconst NICE_KM_STEPS = [0.05, 0.1, 0.25, 0.5, 1, 2, 5, 10, 25, 50, 100, 250, 500, 1000] as const;\n\n/** Pick the largest \"nice\" distance whose bar fits within `maxPixels`. */\nfunction pickScaleKm(kmPerPixel: number, maxPixels: number): { km: number; pixels: number } {\n    const maxKm = kmPerPixel * maxPixels;\n    let chosen: number = NICE_KM_STEPS[0];\n    for (const step of NICE_KM_STEPS) {\n        if (step <= maxKm) chosen = step;\n    }\n    return { km: chosen, pixels: chosen / kmPerPixel };\n}\n\n/** Format a distance in km for the scale bar (meters below 1 km). */\nfunction formatScale(km: number): string {\n    return km < 1 ? `${Math.round(km * 1000)} m` : `${km % 1 === 0 ? km : km.toFixed(1)} km`;\n}\n\n/**\n * Plot a GPS trajectory. By default it renders a **tile-free SVG** map (Web\n * Mercator projection, auto-fit to the points, optional grid + scale bar) —\n * 100% self-contained, no external tiles or paid API. Pass `tileUrl` pointing\n * at a map server **you host** to upgrade to a real Leaflet tile layer.\n *\n * @example\n * // Zero-dependency SVG plot\n * <TrajectoryMap points={points} current={lastPoint} height={360} />\n *\n * @example\n * // Real tiles from your own server (requires `leaflet` installed)\n * <TrajectoryMap points={points} tileUrl=\"https://tiles.internal/{z}/{x}/{y}.png\" />\n */\nexport function TrajectoryMap({\n    points,\n    current,\n    height = 320,\n    padding = 24,\n    showGrid = true,\n    showScale = true,\n    strokeColor,\n    tileUrl,\n    tileAttribution,\n    label = \"Trajetória\",\n    markers,\n    onMarkerClick,\n    className,\n    style,\n    ...rest\n}: TrajectoryMapProps) {\n    const containerRef = useRef<HTMLDivElement>(null);\n    const [width, setWidth] = useState<number>(600);\n\n    useEffect(() => {\n        const node = containerRef.current;\n        if (!node || typeof ResizeObserver === \"undefined\") return;\n        const observer = new ResizeObserver((entries) => {\n            const measured = entries[0]?.contentRect.width;\n            if (measured && measured > 0) setWidth(measured);\n        });\n        observer.observe(node);\n        return () => observer.disconnect();\n    }, []);\n\n    const allPoints = useMemo<Coordinate[]>(\n        () => [...points, ...(current ? [current] : []), ...(markers ?? [])],\n        [points, current, markers],\n    );\n\n    const svg = useMemo(() => {\n        const box = boundingBox(allPoints);\n        if (!box) return null;\n\n        const projection = fitProjection(expandBounds(box, 0.12), width, height, { padding });\n        const pixels: PixelPoint[] = points.map((p) => projection.project(p));\n        const polyline = pixels.map((p) => `${p.x.toFixed(1)},${p.y.toFixed(1)}`).join(\" \");\n\n        const startPixel = pixels[0] ?? null;\n        const endPixel = pixels.length > 0 ? pixels[pixels.length - 1] : null;\n        const currentPixel = current ? projection.project(current) : null;\n\n        // Distance-per-pixel at the map center, for the scale bar: measure the\n        // center → east-edge span in both km (haversine) and pixels (projected).\n        const centerCoord = {\n            latitude: (box.minLatitude + box.maxLatitude) / 2,\n            longitude: (box.minLongitude + box.maxLongitude) / 2,\n        };\n        const center = projection.project(centerCoord);\n        const eastRef = projection.project({ ...centerCoord, longitude: box.maxLongitude });\n        const edgeKm = haversineKm(centerCoord, { ...centerCoord, longitude: box.maxLongitude });\n        const edgePixels = Math.abs(eastRef.x - center.x) || 1;\n        const kmPerPixel = edgeKm / edgePixels || 0;\n\n        const markerPixels = (markers ?? []).map((m) => ({ marker: m, p: projection.project(m) }));\n\n        return { polyline, startPixel, endPixel, currentPixel, kmPerPixel, markerPixels };\n    }, [allPoints, points, current, markers, width, height, padding]);\n\n    // ── Leaflet tile mode ────────────────────────────────────────────────────\n    if (tileUrl) {\n        return (\n            <div\n                ref={containerRef}\n                className={cn(styles.map, className)}\n                style={{ height, ...style }}\n                role=\"img\"\n                aria-label={label}\n                {...rest}\n            >\n                <Suspense fallback={<div className={styles.loading}>Carregando mapa…</div>}>\n                    <LeafletTrajectory\n                        points={points}\n                        current={current}\n                        tileUrl={tileUrl}\n                        tileAttribution={tileAttribution}\n                        strokeColor={strokeColor}\n                    />\n                </Suspense>\n            </div>\n        );\n    }\n\n    // ── Tile-free SVG mode ─────────────────────────────────────────────────────\n    const scale =\n        showScale && svg && svg.kmPerPixel > 0\n            ? pickScaleKm(svg.kmPerPixel, Math.min(120, width * 0.35))\n            : null;\n\n    return (\n        <div\n            ref={containerRef}\n            className={cn(styles.map, className)}\n            style={{ height, ...style }}\n            role=\"img\"\n            aria-label={label}\n            {...rest}\n        >\n            {!svg ? (\n                <div className={styles.empty}>Sem pontos de trajetória ainda.</div>\n            ) : (\n                <svg\n                    className={styles.svg}\n                    width=\"100%\"\n                    height={height}\n                    viewBox={`0 0 ${width} ${height}`}\n                    preserveAspectRatio=\"xMidYMid meet\"\n                >\n                    {showGrid && (\n                        <g className={styles.grid}>\n                            {[0.25, 0.5, 0.75].map((f) => (\n                                <line\n                                    key={`h${f}`}\n                                    x1={0}\n                                    y1={height * f}\n                                    x2={width}\n                                    y2={height * f}\n                                />\n                            ))}\n                            {[0.25, 0.5, 0.75].map((f) => (\n                                <line\n                                    key={`v${f}`}\n                                    x1={width * f}\n                                    y1={0}\n                                    x2={width * f}\n                                    y2={height}\n                                />\n                            ))}\n                        </g>\n                    )}\n\n                    {svg.polyline && (\n                        <polyline\n                            className={styles.path}\n                            points={svg.polyline}\n                            fill=\"none\"\n                            stroke={strokeColor}\n                            data-testid=\"trajectory-path\"\n                        />\n                    )}\n\n                    {svg.startPixel && (\n                        <circle\n                            className={styles.start}\n                            cx={svg.startPixel.x}\n                            cy={svg.startPixel.y}\n                            r={5}\n                        />\n                    )}\n                    {svg.endPixel && !current && (\n                        <circle\n                            className={styles.end}\n                            cx={svg.endPixel.x}\n                            cy={svg.endPixel.y}\n                            r={5}\n                        />\n                    )}\n                    {svg.currentPixel && (\n                        <>\n                            <circle\n                                className={styles.pulse}\n                                cx={svg.currentPixel.x}\n                                cy={svg.currentPixel.y}\n                                r={10}\n                            />\n                            <circle\n                                className={styles.current}\n                                cx={svg.currentPixel.x}\n                                cy={svg.currentPixel.y}\n                                r={5}\n                            />\n                        </>\n                    )}\n\n                    {svg.markerPixels.map(({ marker, p }, i) => {\n                        const interactive = Boolean(onMarkerClick);\n                        return (\n                            <circle\n                                key={marker.id ?? i}\n                                className={styles.marker}\n                                cx={p.x}\n                                cy={p.y}\n                                r={marker.radius ?? 6}\n                                style={marker.color ? { fill: marker.color } : undefined}\n                                data-marker-id={marker.id ?? i}\n                                tabIndex={interactive ? 0 : undefined}\n                                role={interactive ? \"button\" : undefined}\n                                aria-label={marker.label}\n                                onClick={interactive ? () => onMarkerClick?.(marker, i) : undefined}\n                                onKeyDown={\n                                    interactive\n                                        ? (e) => {\n                                              if (e.key === \"Enter\" || e.key === \" \") {\n                                                  e.preventDefault();\n                                                  onMarkerClick?.(marker, i);\n                                              }\n                                          }\n                                        : undefined\n                                }\n                            >\n                                {marker.label ? <title>{marker.label}</title> : null}\n                            </circle>\n                        );\n                    })}\n\n                    {scale && (\n                        <g\n                            className={styles.scale}\n                            transform={`translate(${padding}, ${height - padding})`}\n                        >\n                            <line x1={0} y1={0} x2={scale.pixels} y2={0} />\n                            <line x1={0} y1={-4} x2={0} y2={4} />\n                            <line x1={scale.pixels} y1={-4} x2={scale.pixels} y2={4} />\n                            <text x={scale.pixels / 2} y={-8} textAnchor=\"middle\">\n                                {formatScale(scale.km)}\n                            </text>\n                        </g>\n                    )}\n                </svg>\n            )}\n        </div>\n    );\n}\n"],"mappings":"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"}