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
/**
* 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
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 fromoverlay/123123/files/image.png)
Step 1: Create overlays directory and copy test image
mkdir -p public/overlays
cp overlay/123123/files/image.png public/overlays/test-123123.pngStep 2: Commit
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:
// Map overlay controls
showOverlay: boolean;
overlayOpacity: number;Step 2: Add default values in constants.ts
Add these fields to DEFAULT_CONTROLS after canvasColorScheme:
// Map overlay controls
showOverlay: false,
overlayOpacity: 0.75,Step 3: Verify TypeScript compiles
pnpm build:checkExpected: Build succeeds (may have warnings, but no errors related to our changes)
Step 4: Commit
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:
import {
getOverlayForMineSite,
type MapOverlay,
} from "@/features/heatmap/overlayConfig";Step 2: Add overlay ref after canvasOverlayRef (around line 86)
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):
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):
// 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):
// 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
pnpm build:checkStep 7: Commit
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):
{/* 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
pnpm build:checkStep 3: Commit
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
pnpm devStep 2: Test the overlay functionality
- Navigate to the Heatmap page
- Select a mine site that has an overlay configured
- Verify the "Map Overlay" section appears in the control panel
- Toggle "Show Overlay" on
- Verify the overlay image appears on the map
- Adjust the opacity slider and verify the overlay transparency changes
- Toggle off and verify the overlay disappears
Step 3: Test edge cases
- Select a mine site without an overlay - verify the Map Overlay section doesn't appear
- Switch between mine sites - verify overlay updates correctly
- Zoom in/out - verify overlay stays in correct position
Task 7: Final Commit and Cleanup
Step 1: Run linting
pnpm lint:fixStep 2: Run type check
pnpm build:checkStep 3: Final commit if any lint fixes
git add -A
git commit -m "chore(heatmap): lint fixes for overlay feature"Summary
After completing all tasks, you will have:
src/features/heatmap/overlayConfig.ts- Configuration file for map overlayspublic/overlays/test-123123.png- Test overlay image- Updated
types.tswithshowOverlayandoverlayOpacityfields - Updated
constants.tswith default values - Updated
heatmap/index.tsxwith rendering logic and UI controls
To add new overlays in the future:
- Pre-process the image (remove rotation)
- Place in
public/overlays/ - Add configuration to
MAP_OVERLAYSarray inoverlayConfig.ts