← back to Homesonspec

apps/web/src/app/map/MapCanvas.tsx

75 lines

"use client";

import { useEffect, useRef } from "react";
import { MapContainer, TileLayer, useMap } from "react-leaflet";
import "leaflet/dist/leaflet.css";
import L from "leaflet";

export interface CanvasMarker {
  i: string;
  la: number;
  lo: number;
  color: string;
  title: string;
  sub: string;
}

/**
 * Imperative canvas layer — draws thousands of circle markers in a single
 * canvas pass instead of mounting one React element per point. Rebuilds when
 * the filtered marker set changes; fits bounds only on the first non-empty set.
 */
function CircleLayer({ markers }: { markers: CanvasMarker[] }) {
  const map = useMap();
  const rendererRef = useRef<L.Canvas | null>(null);
  const fittedRef = useRef(false);

  useEffect(() => {
    if (!rendererRef.current) rendererRef.current = L.canvas({ padding: 0.5 });
    const renderer = rendererRef.current;
    const group = L.layerGroup().addTo(map);

    for (const m of markers) {
      L.circleMarker([m.la, m.lo], {
        renderer,
        radius: 5,
        weight: 1,
        color: "#ffffff",
        fillColor: m.color,
        fillOpacity: 0.85,
      })
        .bindPopup(
          `<div style="min-width:150px"><strong>${m.title}</strong><br>${m.sub}<br>` +
            `<a href="/homes/${m.i}" style="color:#0f766e;font-weight:600">View home →</a></div>`,
        )
        .addTo(group);
    }

    if (!fittedRef.current && markers.length > 0) {
      map.fitBounds(
        L.latLngBounds(markers.map((m) => [m.la, m.lo] as [number, number])).pad(0.1),
        { maxZoom: 11 },
      );
      fittedRef.current = true;
    }

    return () => {
      map.removeLayer(group);
    };
  }, [map, markers]);

  return null;
}

export default function MapCanvas({ markers }: { markers: CanvasMarker[] }) {
  return (
    <MapContainer center={[39.5, -98.35]} zoom={4} preferCanvas style={{ width: "100%", height: "100%" }}>
      <TileLayer
        attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
        url="https://tile.openstreetmap.org/{z}/{x}/{y}.png"
      />
      <CircleLayer markers={markers} />
    </MapContainer>
  );
}