Ritu-AI / frontend /src /components /Map /ClimateMap.jsx
SeriousSam07's picture
features- Enhanced Map and what if simlulation and heat pockets.
0c02b78
Raw History Blame Contribute Delete
7.84 kB
import React, { useState, useEffect, useMemo, useCallback } from 'react'
import Map from 'react-map-gl/maplibre'
import DeckGL from '@deck.gl/react'
import { TileLayer } from '@deck.gl/geo-layers'
import { BitmapLayer, GeoJsonLayer } from '@deck.gl/layers'
import { useClimateStore } from '../../store/climateStore'
// the region Bounding Box (Strict Lock)
const MAHARASHTRA_BOUNDS = [
[72.5, 15.5], // Southwest coordinates (Lng, Lat)
[81.0, 22.1] // Northeast coordinates (Lng, Lat)
]
const INITIAL_VIEW_STATE = {
longitude: 76.5,
latitude: 19.3,
zoom: 6.0,
minZoom: 5.5,
maxZoom: 12,
pitch: 0,
bearing: 0
}
export default function ClimateMap() {
const { selectedVariable } = useClimateStore()
const [radarTime, setRadarTime] = useState(null)
const [clickedCoord, setClickedCoord] = useState(null)
const [viewState, setViewState] = useState(INITIAL_VIEW_STATE)
const [searchQuery, setSearchQuery] = useState('')
const [isSearching, setIsSearching] = useState(false)
const handleSearch = async (e) => {
e.preventDefault()
if (!searchQuery.trim()) return
setIsSearching(true)
try {
const res = await fetch(`https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(searchQuery + ', India')}`)
const data = await res.json()
if (data && data.length > 0) {
setViewState({
...viewState,
longitude: parseFloat(data[0].lon),
latitude: parseFloat(data[0].lat),
zoom: 10,
transitionDuration: 1500
})
}
} catch (err) {
console.error('Search failed', err)
} finally {
setIsSearching(false)
}
}
const handleZoom = (delta) => {
setViewState(prev => ({
...prev,
zoom: Math.min(Math.max(prev.zoom + delta, prev.minZoom), prev.maxZoom),
transitionDuration: 300
}))
}
const resetMap = () => {
setViewState({
...INITIAL_VIEW_STATE,
transitionDuration: 1000
})
}
// Fetch latest RainViewer timestamp for live radar
useEffect(() => {
fetch('https://api.rainviewer.com/public/weather-maps.json')
.then(res => res.json())
.then(data => {
if (data && data.radar && data.radar.past && data.radar.past.length > 0) {
const latest = data.radar.past[data.radar.past.length - 1].time
setRadarTime(latest)
}
})
.catch(console.error)
}, [])
const layers = useMemo(() => {
const list = []
// Inverted Mask GeoJSON Layer to hide neighboring states
list.push(
new GeoJsonLayer({
id: 'maharashtra-mask',
data: '/data/maharashtra_mask.json',
pickable: false,
stroked: false,
filled: true,
getFillColor: [248, 250, 252, 255], // matches slate-50 background perfectly
})
)
// Add RainViewer layer if rain is selected and we have a timestamp
if (selectedVariable === 'rain' && radarTime) {
list.push(
new TileLayer({
id: 'rainviewer-radar',
data: `https://tilecache.rainviewer.com/v2/radar/${radarTime}/256/{z}/{x}/{y}/2/1_1.png`,
minZoom: 0,
maxZoom: 7,
tileSize: 256,
renderSubLayers: props => {
const { bbox: { west, south, east, north } } = props.tile
return new BitmapLayer(props, {
data: null,
image: props.data,
bounds: [west, south, east, north],
opacity: 0.85,
transparentColor: [0, 0, 0, 0]
})
}
})
)
}
return list
}, [selectedVariable, radarTime])
return (
<div className="relative w-full h-full rounded-2xl overflow-hidden border border-slate-200 shadow-sm bg-slate-50">
<DeckGL
viewState={viewState}
onViewStateChange={({ viewState }) => setViewState(viewState)}
controller={{ scrollZoom: { smooth: true }, dragPan: true, doubleClickZoom: true, touchZoom: true }}
layers={layers}
getTooltip={({object}) => object && (object.properties.NAME_2 || object.properties.district || object.properties.NAME_1 || 'Region')}
getCursor={({ isDragging }) => (isDragging ? 'grabbing' : 'crosshair')}
onClick={(info) => {
if (info.coordinate) {
setClickedCoord({
lon: info.coordinate[0].toFixed(2),
lat: info.coordinate[1].toFixed(2)
})
}
}}
>
<Map
mapStyle="https://basemaps.cartocdn.com/gl/voyager-gl-style/style.json"
attributionControl={false}
maxBounds={MAHARASHTRA_BOUNDS}
/>
</DeckGL>
{/* ── Search Bar ── */}
<form onSubmit={handleSearch} className="absolute top-4 left-1/2 -translate-x-1/2 z-20 flex bg-white rounded-full shadow-md border border-slate-200 overflow-hidden w-64 transition-all">
<input
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Search city in MH..."
className="flex-1 px-4 py-2 text-sm outline-none bg-transparent text-slate-800"
/>
<button type="submit" disabled={isSearching} className="px-3 text-slate-500 hover:text-slate-800 hover:bg-slate-50 transition-colors disabled:opacity-50">
πŸ”
</button>
</form>
{/* ── Map Controls ── */}
<div className="absolute top-4 right-4 z-20 flex flex-col gap-2">
<button onClick={() => handleZoom(1)} className="w-8 h-8 flex items-center justify-center bg-white rounded-lg shadow border border-slate-200 text-slate-600 hover:bg-slate-50 transition-colors text-lg font-medium">
+
</button>
<button onClick={() => handleZoom(-1)} className="w-8 h-8 flex items-center justify-center bg-white rounded-lg shadow border border-slate-200 text-slate-600 hover:bg-slate-50 transition-colors text-lg font-medium">
βˆ’
</button>
<button onClick={resetMap} className="w-8 h-8 flex items-center justify-center bg-white rounded-lg shadow border border-slate-200 text-slate-600 hover:bg-slate-50 transition-colors text-sm" title="Reset View">
⟲
</button>
</div>
{/* Click Popup */}
{clickedCoord && (
<div className="absolute bottom-6 left-1/2 -translate-x-1/2 z-20 animate-fade-in-up">
<div className="glass-card px-4 py-2 flex items-center gap-3 text-sm border-slate-200 text-slate-700 bg-white/90">
<span className="text-blue-600">πŸ“</span>
<span className="text-slate-500">Lat:</span>
<span className="font-semibold text-slate-800">{clickedCoord.lat}Β°</span>
<span className="text-slate-500">Lon:</span>
<span className="font-semibold text-slate-800">{clickedCoord.lon}Β°</span>
<button
onClick={() => setClickedCoord(null)}
className="ml-2 text-slate-400 hover:text-slate-800 transition-colors"
>
βœ•
</button>
</div>
</div>
)}
{/* Loading overlay for Radar */}
{selectedVariable === 'rain' && !radarTime && (
<div className="absolute top-4 left-4 bg-white/90 backdrop-blur px-3 py-1.5 rounded-lg border border-slate-200 text-xs text-slate-600 animate-pulse flex items-center gap-2 shadow-sm">
<div className="w-2 h-2 rounded-full bg-blue-500 animate-ping" />
Connecting to RADAR...
</div>
)}
{/* Attribution */}
<div className="absolute bottom-2 right-2 text-[10px] text-slate-500 bg-white/80 px-2 py-1 rounded backdrop-blur border border-slate-200">
Data: IMD Β· MOSDAC/ISRO Β· RainViewer Β· CartoDB
</div>
</div>
)
}