{{ theme.skipToContentLabel || 'Skip to content' }}

Heatmap Map Overlay Implementation Plan

For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

Goal: Add map overlay functionality to display updated mine site images on Google Maps, with toggle and opacity controls.

Architecture: Create overlay configuration file, extend existing HeatmapControlsState with overlay settings, use Google Maps GroundOverlay API to render images below the heatmap canvas layer.

Tech Stack: React, TypeScript, Google Maps JavaScript API (GroundOverlay)


Task 1: Create Overlay Configuration

Files:

  • Create: src/features/heatmap/overlayConfig.ts

Step 1: Create the overlay configuration file

typescript
/**
 * Map overlay configuration for mine sites
 * Images should be pre-processed (no rotation) and placed in public/overlays/
 */

export type MapOverlayBounds = {
  north: number;
  south: number;
  east: number;
  west: number;
};

export type MapOverlay = {
  id: string;
  name: string;
  mineSiteId: string;
  imagePath: string;
  bounds: MapOverlayBounds;
};

export const MAP_OVERLAYS: MapOverlay[] = [
  // Test overlay - coordinates from overlay/123123/doc.kml
  {
    id: "test-overlay-123123",
    name: "Test Overlay 2024",
    mineSiteId: "", // Will be updated with actual mine site ID
    imagePath: "/overlays/test-123123.png",
    bounds: {
      north: -22.51509160607861,
      south: -22.55023212272908,
      east: 119.0573010775616,
      west: 118.9966508470948,
    },
  },
];

/**
 * Get all overlays for a specific mine site
 */
export function getOverlaysForMineSite(mineSiteId: string): MapOverlay[] {
  return MAP_OVERLAYS.filter((overlay) => overlay.mineSiteId === mineSiteId);
}

/**
 * Get the first overlay for a mine site (for simple single-overlay use case)
 */
export function getOverlayForMineSite(
  mineSiteId: string
): MapOverlay | undefined {
  return MAP_OVERLAYS.find((overlay) => overlay.mineSiteId === mineSiteId);
}

Step 2: Commit

bash
git add src/features/heatmap/overlayConfig.ts
git commit -m "feat(heatmap): add overlay configuration file"

Task 2: Set Up Test Image

Files:

  • Create: public/overlays/test-123123.png (copy from overlay/123123/files/image.png)

Step 1: Create overlays directory and copy test image

bash
mkdir -p public/overlays
cp overlay/123123/files/image.png public/overlays/test-123123.png

Step 2: Commit

bash
git add public/overlays/test-123123.png
git commit -m "feat(heatmap): add test overlay image"

Task 3: Extend Types and Constants

Files:

  • Modify: src/features/heatmap/types.ts
  • Modify: src/features/heatmap/constants.ts

Step 1: Add overlay fields to HeatmapControlsState in types.ts

Add these fields to the HeatmapControlsState type after canvasColorScheme:

typescript
  // Map overlay controls
  showOverlay: boolean;
  overlayOpacity: number;

Step 2: Add default values in constants.ts

Add these fields to DEFAULT_CONTROLS after canvasColorScheme:

typescript
  // Map overlay controls
  showOverlay: false,
  overlayOpacity: 0.75,

Step 3: Verify TypeScript compiles

bash
pnpm build:check

Expected: Build succeeds (may have warnings, but no errors related to our changes)

Step 4: Commit

bash
git add src/features/heatmap/types.ts src/features/heatmap/constants.ts
git commit -m "feat(heatmap): add overlay state to types and constants"

Task 4: Add Overlay Rendering Logic

Files:

  • Modify: src/app/(admin)/(pages)/heatmap/index.tsx

Step 1: Add imports at the top of the file

Add after the existing heatmap imports:

typescript
import {
  getOverlayForMineSite,
  type MapOverlay,
} from "@/features/heatmap/overlayConfig";

Step 2: Add overlay ref after canvasOverlayRef (around line 86)

typescript
const groundOverlayRef = useRef<google.maps.GroundOverlay | null>(null);

Step 3: Add overlay state for current mine site

Add after the selectedMineSite state (around line 121):

typescript
const [currentOverlay, setCurrentOverlay] = useState<MapOverlay | null>(null);

Step 4: Add useEffect to update currentOverlay when mine site changes

Add after the existing mine site effects (around line 180):

typescript
// Update current overlay when mine site changes
useEffect(() => {
  if (selectedMineSite) {
    const overlay = getOverlayForMineSite(selectedMineSite);
    setCurrentOverlay(overlay ?? null);
  } else {
    setCurrentOverlay(null);
  }
}, [selectedMineSite]);

Step 5: Add useEffect for GroundOverlay rendering

Add after the canvas overlay effects (around line 455):

typescript
// Ground overlay rendering
useEffect(() => {
  if (
    mapsStatus !== "ready" ||
    !map ||
    !window.google?.maps ||
    !currentOverlay
  ) {
    // Remove existing overlay if conditions not met
    if (groundOverlayRef.current) {
      groundOverlayRef.current.setMap(null);
      groundOverlayRef.current = null;
    }
    return;
  }

  // Don't show if toggle is off
  if (!controls.showOverlay) {
    if (groundOverlayRef.current) {
      groundOverlayRef.current.setMap(null);
      groundOverlayRef.current = null;
    }
    return;
  }

  const googleMaps = window.google.maps;

  // Create or update ground overlay
  if (!groundOverlayRef.current) {
    const bounds = new googleMaps.LatLngBounds(
      { lat: currentOverlay.bounds.south, lng: currentOverlay.bounds.west },
      { lat: currentOverlay.bounds.north, lng: currentOverlay.bounds.east }
    );

    groundOverlayRef.current = new googleMaps.GroundOverlay(
      currentOverlay.imagePath,
      bounds,
      { opacity: controls.overlayOpacity }
    );
    groundOverlayRef.current.setMap(map);
  } else {
    // Update opacity if overlay already exists
    groundOverlayRef.current.setOpacity(controls.overlayOpacity);
  }

  return () => {
    if (groundOverlayRef.current) {
      groundOverlayRef.current.setMap(null);
      groundOverlayRef.current = null;
    }
  };
}, [mapsStatus, map, currentOverlay, controls.showOverlay, controls.overlayOpacity]);

Step 6: Verify TypeScript compiles

bash
pnpm build:check

Step 7: Commit

bash
git add src/app/\(admin\)/\(pages\)/heatmap/index.tsx
git commit -m "feat(heatmap): add ground overlay rendering logic"

Task 5: Add UI Controls

Files:

  • Modify: src/app/(admin)/(pages)/heatmap/index.tsx

Step 1: Add Map Overlay controls section

Find the "Appearance" section in the JSX (around line 1129). Add the following before the Appearance section (around line 1127):

tsx
{/* Map Overlay Controls */}
{currentOverlay && (
  <div className="space-y-4 rounded-lg border border-slate-200 dark:border-slate-700 bg-slate-50 dark:bg-slate-800/50 p-4">
    <h3 className="text-sm font-medium text-slate-700 dark:text-slate-300 flex items-center gap-2">
      <Icon className="w-4 h-4" icon="solar:layers-bold-duotone" />
      Map Overlay
    </h3>

    {/* Toggle */}
    <div className="flex items-center justify-between">
      <div className="flex items-center gap-2">
        <label className="text-sm text-slate-700 dark:text-slate-300">
          Show Overlay
        </label>
        <button
          className="group relative"
          title="Map overlay explanation"
          type="button"
        >
          <Icon
            className="w-3.5 h-3.5 text-slate-400 hover:text-blue-500 transition-colors"
            icon="solar:question-circle-bold"
          />
          <div className="absolute left-1/2 -translate-x-1/2 bottom-full mb-2 w-64 p-3 bg-slate-900 text-white text-xs rounded-lg shadow-xl border border-slate-700 opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all z-[60] pointer-events-none">
            Display an updated map image overlay on top of the satellite imagery.
            <div className="absolute left-1/2 -translate-x-1/2 top-full w-0 h-0 border-l-4 border-r-4 border-t-4 border-l-transparent border-r-transparent border-t-slate-900"></div>
          </div>
        </button>
      </div>
      <label className="relative inline-flex items-center cursor-pointer">
        <input
          type="checkbox"
          className="sr-only peer"
          checked={controls.showOverlay}
          onChange={(e) => {
            updateControls({ showOverlay: e.target.checked });
          }}
        />
        <div className="w-11 h-6 bg-slate-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 dark:peer-focus:ring-blue-800 rounded-full peer dark:bg-slate-700 peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-slate-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-slate-600 peer-checked:bg-blue-600"></div>
      </label>
    </div>

    {/* Opacity Slider - only show when overlay is enabled */}
    {controls.showOverlay && (
      <div className="space-y-2">
        <div className="flex items-center justify-between">
          <label className="text-sm text-slate-700 dark:text-slate-300">
            Overlay Opacity
          </label>
          <span className="font-medium text-blue-600 dark:text-blue-400 text-sm">
            {Math.round(controls.overlayOpacity * 100)}%
          </span>
        </div>
        <input
          className="w-full h-2 bg-slate-200 dark:bg-slate-700 rounded-lg appearance-none cursor-pointer"
          max={1}
          min={0.1}
          step={0.05}
          type="range"
          value={controls.overlayOpacity}
          onChange={(event) => {
            updateControls({
              overlayOpacity: Number(event.target.value),
            });
          }}
        />
      </div>
    )}

    {/* Overlay name */}
    <div className="text-xs text-slate-500 dark:text-slate-400">
      {currentOverlay.name}
    </div>
  </div>
)}

Step 2: Verify TypeScript compiles

bash
pnpm build:check

Step 3: Commit

bash
git add src/app/\(admin\)/\(pages\)/heatmap/index.tsx
git commit -m "feat(heatmap): add overlay UI controls"

Task 6: Manual Testing

Step 1: Start development server

bash
pnpm dev

Step 2: Test the overlay functionality

  1. Navigate to the Heatmap page
  2. Select a mine site that has an overlay configured
  3. Verify the "Map Overlay" section appears in the control panel
  4. Toggle "Show Overlay" on
  5. Verify the overlay image appears on the map
  6. Adjust the opacity slider and verify the overlay transparency changes
  7. Toggle off and verify the overlay disappears

Step 3: Test edge cases

  1. Select a mine site without an overlay - verify the Map Overlay section doesn't appear
  2. Switch between mine sites - verify overlay updates correctly
  3. Zoom in/out - verify overlay stays in correct position

Task 7: Final Commit and Cleanup

Step 1: Run linting

bash
pnpm lint:fix

Step 2: Run type check

bash
pnpm build:check

Step 3: Final commit if any lint fixes

bash
git add -A
git commit -m "chore(heatmap): lint fixes for overlay feature"

Summary

After completing all tasks, you will have:

  1. src/features/heatmap/overlayConfig.ts - Configuration file for map overlays
  2. public/overlays/test-123123.png - Test overlay image
  3. Updated types.ts with showOverlay and overlayOpacity fields
  4. Updated constants.ts with default values
  5. Updated heatmap/index.tsx with rendering logic and UI controls

To add new overlays in the future:

  1. Pre-process the image (remove rotation)
  2. Place in public/overlays/
  3. Add configuration to MAP_OVERLAYS array in overlayConfig.ts