Spaces:
Running
Running
File size: 7,840 Bytes
a3b21d2 0c02b78 a3b21d2 0c02b78 a3b21d2 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 | 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>
)
}
|