Spaces:
Sleeping
Sleeping
Download frontend/src/components/Map/ClimateMap.jsx from SeriousSam07/Ritu-AI: direct link, hf CLI and curl.
- Browser
- Download file 7.84 kB
-
https://huggingface.co/spaces/SeriousSam07/Ritu-AI/resolve/main/frontend/src/components/Map/ClimateMap.jsx
- Command line
-
hf download hf://spaces/SeriousSam07/Ritu-AI/frontend/src/components/Map/ClimateMap.jsx
-
curl -L -o ClimateMap.jsx https://huggingface.co/spaces/SeriousSam07/Ritu-AI/resolve/main/frontend/src/components/Map/ClimateMap.jsx
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> | |
| ) | |
| } | |