This project is experimental and was developed for exploratory purposes only.
https://burn-watch.netlify.app
This project is an interactive experiment built with React and Leaflet for visualizing fire hotspots detected by MODIS and VIIRS (S-NPP, NOAA-20, NOAA-21) sensors, using data from the FIRMS (NASA) platform accessed via API.
The goal was to explore the integration between React, Leaflet, and public satellite monitoring APIs.
- Displays the most recent fire hotspots (last few days)
- Filters hotspots within a specific geographic area (ROI) defined by a GeoJSON file
- Custom icons for each sensor type (MODIS, VIIRS)
- Automatic zoom adjustment based on the ROI area
- Explanatory legend with the sensors used
API Request Fetches CSV data from NASA FIRMS API for each sensor: MODIS, VIIRS S-NPP, VIIRS NOAA-20, and VIIRS NOAA-21.
CSV Parsing & Normalization Parses rows, assigns sensor label, and converts coordinates to numeric format. Each line becomes a JavaScript object with hotspot metadata.
ROI Filtering
Uses Turf.js to check which points fall within the defined GeoJSON region of interest (ROI) - @turf/boolean-point-in-polygon
Mapping & Visualization Displays filtered fire points on a Leaflet map, using a custom icon per sensor.
<Marker>component is rendered withicon={sensorIcons[point.sensor]}- A
<Popup>shows additional data (brightness, date, satellite, etc.) - The
FitBoundsToROIfunction centers the map based on the GeoJSON boundaries.
Deployment Built with React and deployed via Netlify, using .env variables to protect the API key.
The frontend uses a flat file structure (no components/hooks/utils subfolders) to keep the codebase lightweight and maintainable. Each file has a single responsibility.
| File | Type | Responsibility | Inputs | Outputs |
|---|---|---|---|---|
App.js |
Container | Orchestrates the app. Holds timeFilter state and renders the map, controls, and legend. |
useFireData() return |
JSX layout |
useFireData.js |
Custom Hook | Fetches FIRMS data for all 4 sensors in parallel, caches results in sessionStorage, parses CSV, and filters by ROI. |
REACT_APP_FIRMS_KEY env var |
{ fireData, loading, error } |
geoUtils.js |
Pure Utils | Geospatial and time functions: isInsideROI, getBBox, filterByTime. |
GeoJSON / raw data | Boolean / string / filtered array |
mapConfig.js |
Config | Leaflet icon definitions and sensor constant arrays. | Static image imports | sensorIcons, SENSOR_TYPES, FIRMS_SENSORS |
SensorMarkers.js |
Component | Renders Marker + Popup for a specific sensor, applying timeFilter via filterByTime. |
data, sensorName, timeFilter |
<LayerGroup> of markers |
TimeFilter.js |
Component | Radio button group for selecting all, 24h, 48h, or 72h. |
timeFilter, setTimeFilter |
Styled radio inputs |
Legend.js |
Component | Floating legend showing active sensor icons and current filter status. | timeFilter |
Positioned <div> with icons |
FitBoundsToROI.js |
Component | Leaflet effect that auto-zooms the map to the ROI GeoJSON bounds. | geojson |
null (side effect) |
How it works: The App component calls useFireData() once on mount. The hook fetches data from NASA FIRMS in parallel, caches it, and returns fireData. The App then passes this array down to SensorMarkers (one instance per sensor), which filters it by timeFilter before rendering. TimeFilter and Legend are purely presentational and receive only the state they need. This separation makes unit testing (geoUtils.js) and future features (animations, intensity filters) straightforward.
Possible Future Extensions
- Add filtering by date or intensity
- Comparison between sensors
- Temporal analysis (animation of hotspots)
- Integration with historical FIRMS data
BurnWatch uses NRT (Near Real Time) data from NASA FIRMS. This choice was deliberate for the project's purpose: testing a working integration between React and a public satellite API.
NRT vs SP:
- NRT → available in 3-4 hours after satellite overpass. Minimal processing: automated detection, quick geolocation, no human validation. This is the "draft" of the data.
- SP (Standard Processing) → available in days/weeks. Goes through orbital refinement, false positive elimination, and scientific validation. This is the "final product".
Why NRT is less accurate:
- No orbital refinement: position may vary by ~hundreds of meters
- More false positives: sun glint on metal surfaces, hot sand
- Algorithm uses approximate thresholds (adjusted in SP with ground data)
Why we chose NRT: BurnWatch is an architecture experiment — not a scientific analysis or emergency response tool. NRT delivers speed, REST API accessibility, and manageable volume for a frontend. It is the right fuel for validating that React ↔ Leaflet ↔ FIRMS API work together.
In practice: the user sees fire hotspots detected within the last 48 hours, with a few hours of delay. Some points may be artifacts. For scientific use, use SP. For rapid prototyping, [NRT] (https://www.earthdata.nasa.gov/data/tools/firms/faq) is the right choice.
git clone https://github.com/jesseburlamaque/burnwatch
cd burnwatchnpm installCreate a .env file at the root of the project with the following content. You can obtain your free API key at: https://firms.modaps.eosdis.nasa.gov/api/
REACT_APP_FIRMS_KEY=your_api_key_herenpm startThis app can be easily hosted on Netlify.
Make sure to add the REACT_APP_FIRMS_KEY variable in the Site Settings > Environment Variables panel.
- FIRMS archive data - https://firms.modaps.eosdis.nasa.gov/download/
- What is FIRMS - https://www.earthdata.nasa.gov/faq/firms-faq
| Status | Categoria | Item |
|---|---|---|
| 🟩 Feito | Funcionalidade | Mapa Leaflet com React |
| 🟩 Feito | Funcionalidade | Integracao API FIRMS (MODIS, VIIRS S-NPP, NOAA-20, NOAA-21) |
| 🟩 Feito | Funcionalidade | Parse de CSV e normalizacao dos dados |
| 🟩 Feito | Funcionalidade | Filtro por ROI via Turf.js |
| 🟩 Feito | Funcionalidade | Icones personalizados por tipo de sensor |
| 🟩 Feito | Funcionalidade | Filtro temporal (24h/48h/72h/todos) |
| 🟩 Feito | Funcionalidade | Camada base (OSM, OpenTopoMap, Stadia) |
| 🟩 Feito | Funcionalidade | Popup Clicável com metadados do foco |
| 🟩 Feito | Funcionalidade | Legenda dos sensores |
| 🟩 Feito | Funcionalidade | Deploy no Netlify |
| 🟩 Feito | Correcao | URL do Stadia Satellite incorreta (&api_key= em vez de ?api_key=) |
| 🟩 Feito | Correcao | filterByTime trata horario como UTC — FIRMS usa hora local |
| 🟩 Feito | Correcao | Console logs de debug esquecidos no codigo de producao |
| 🟩 Feito | Correcao | Camada "Regiao de Interesse" vazia no LayersControl |
| 🟩 Feito | Correcao | Falha de um sensor derruba todos os dados |
| 🟥 Pendente | Seguranca | Rotacionar chaves API (FIRMS e Stadia expostas no git) |
| 🟩 Feito | Seguranca | Impedir versionamento do .env |
| 🟥 Pendente | Teste | Testes unitarios: isInsideROI, filterByTime, parse CSV |
| 🟥 Pendente | Teste | Testes de integracao: fluxo fetch → parse → filtro → mapa |
| 🟥 Pendente | Teste | Smoke tests: App renderiza sem crash, loading e error states |
| 🟩 Feito | Refatoracao | Componentizar App.js (387 linhas → 7 arquivos separados) |
| 🟥 Pendente | Refatoracao | Adicionar Error Boundary |
| 🟥 Pendente | Refatoracao | Adicionar PropTypes ou TypeScript |
| 🟥 Pendente | Refatoracao | Acessibilidade (ARIA labels, navegacao por teclado) |
| 🟥 Pendente | Melhoria | Filtro por data ou intensidade (brightness) |
| 🟥 Pendente | Melhoria | Comparacao entre sensores |
| 🟥 Pendente | Melhoria | Animacao temporal dos focos |
| 🟥 Pendente | Melhoria | Dados historicos FIRMS |
| 🟩 Feito | Melhoria | Cache local para evitar requisicoes repetidas a API FIRMS |
| 🟩 Feito | Melhoria | Buscar apenas coordenadas da ROI ao inves de 'world' inteiro na API FIRMS |
| 🟥 Pendente | Melhoria | Responsividade (media queries) |