Skip to content

Conversation

@giswqs
Copy link
Member

@giswqs giswqs commented Jan 27, 2026

Fix #1206

Copilot AI review requested due to automatic review settings January 27, 2026 05:58
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request adds support for visualizing NASA fire datasets from the Fire Event Data Suite (FEDs) algorithm via the OpenVEDA OGC API Features endpoint. The implementation addresses issue #1206 by providing a new fire module with functions to search and retrieve fire perimeter data, along with Map methods to visualize this data.

Changes:

  • Added new leafmap/fire.py module with functions to query fire data from OpenVEDA API
  • Added three new methods to Map classes (add_fire_data, add_fire_perimeters, add_fire_timeseries) for visualizing fire data
  • Added documentation and example notebook demonstrating the new functionality

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 13 comments.

Show a summary per file
File Description
leafmap/fire.py New module providing functions to search and retrieve NASA fire data from OpenVEDA API
leafmap/leafmap.py Added three methods to visualize fire data on ipyleaflet-based maps
leafmap/foliumap.py Added three methods to visualize fire data on folium-based maps
leafmap/__init__.py Exported fire module functions and constants at package level
mkdocs.yml Added fire module documentation and example notebook to navigation
docs/fire.md Created documentation page for fire module
docs/notebooks/114_nasa_fire.ipynb Added comprehensive example notebook demonstrating fire data visualization

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +4299 to +4420
def add_fire_perimeters(
self,
bbox: Optional[List[float]] = None,
place: Optional[str] = None,
collection: str = "snapshot_perimeter_nrt",
datetime: Optional[str] = None,
farea_min: Optional[float] = None,
color_column: Optional[str] = "farea",
cmap: str = "YlOrRd",
layer_name: str = "Fire Perimeters",
style: Optional[Dict] = None,
info_mode: str = "on_hover",
zoom_to_layer: bool = True,
add_legend: bool = True,
legend_title: str = "Fire Area (km²)",
**kwargs,
) -> None:
"""Add fire perimeters with color scaling based on fire attributes.

Args:
bbox: Bounding box [west, south, east, north] in EPSG:4326.
place: Place name to geocode (e.g., "California").
collection: Fire collection ID. Defaults to "snapshot_perimeter_nrt".
datetime: ISO 8601 date/time or interval.
farea_min: Minimum fire area in km² to filter results.
color_column: Column to use for color scaling. Defaults to "farea".
cmap: Colormap name for color scaling. Defaults to "YlOrRd".
layer_name: Name for the layer.
style: Base style dictionary (color will be overridden by colormap).
info_mode: Display attributes "on_hover" or "on_click".
zoom_to_layer: Whether to zoom to the layer bounds.
add_legend: Whether to add a legend. Defaults to True.
legend_title: Title for the legend.
**kwargs: Additional keyword arguments for add_gdf().

Example:
>>> m = leafmap.Map()
>>> m.add_fire_perimeters(
... place="California",
... datetime="2024-07-01/2024-07-31",
... color_column="farea"
... )
>>> m
"""
from . import fire as fire_module

gdf = fire_module.search_fires(
bbox=bbox,
place=place,
collection=collection,
datetime=datetime,
farea_min=farea_min,
)

if gdf.empty:
print("No fire data found for the specified parameters.")
return

# Create color-scaled styling based on the specified column
if color_column and color_column in gdf.columns:
try:
import matplotlib.pyplot as plt
import matplotlib.colors as mcolors

colormap = plt.get_cmap(cmap)
values = gdf[color_column].fillna(0)
vmin, vmax = values.min(), values.max()

if vmax > vmin:
norm = mcolors.Normalize(vmin=vmin, vmax=vmax)

def style_callback(feature):
value = feature["properties"].get(color_column, 0)
if value is None:
value = 0
rgba = colormap(norm(value))
hex_color = mcolors.rgb2hex(rgba)
base_style = style or {}
return {
"color": hex_color,
"weight": base_style.get("weight", 2),
"fillColor": hex_color,
"fillOpacity": base_style.get("fillOpacity", 0.5),
}

self.add_gdf(
gdf,
layer_name=layer_name,
style_callback=style_callback,
info_mode=info_mode,
zoom_to_layer=zoom_to_layer,
**kwargs,
)

if add_legend:
self.add_colormap(
cmap=cmap,
vmin=vmin,
vmax=vmax,
label=legend_title,
)
return
except ImportError:
pass

# Fallback to default styling
if style is None:
style = {
"color": "#FF4500",
"weight": 2,
"fillColor": "#FF6347",
"fillOpacity": 0.5,
}

self.add_gdf(
gdf,
layer_name=layer_name,
style=style,
info_mode=info_mode,
zoom_to_layer=zoom_to_layer,
**kwargs,
)
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method has nearly identical implementation to the same method in leafmap.py (lines 6970-7091). Consider extracting the common logic into a shared helper function in the fire module to reduce code duplication and improve maintainability. This would make it easier to fix bugs or add features in one place.

Copilot uses AI. Check for mistakes.
Comment on lines +76 to +77
response = requests.get(url, timeout=30)
response.raise_for_status()
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The API request with raise_for_status() will throw an exception if the HTTP request fails, but there's no try-except block to provide a more user-friendly error message. Consider wrapping this in a try-except block to catch HTTPError and provide a more informative error message to users about what went wrong with the API call.

Suggested change
response = requests.get(url, timeout=30)
response.raise_for_status()
try:
response = requests.get(url, timeout=30)
response.raise_for_status()
except requests.exceptions.HTTPError as http_err:
raise RuntimeError(
f"Failed to retrieve fire collections from {url}: {http_err}"
) from http_err
except requests.exceptions.RequestException as req_err:
raise RuntimeError(
f"Error while requesting fire collections from {url}: {req_err}"
) from req_err

Copilot uses AI. Check for mistakes.
Comment on lines +4209 to +4297
def add_fire_data(
self,
data: Optional[Union[str, "gpd.GeoDataFrame"]] = None,
bbox: Optional[List[float]] = None,
place: Optional[str] = None,
collection: str = "snapshot_perimeter_nrt",
datetime: Optional[str] = None,
farea_min: Optional[float] = None,
layer_name: str = "Fire Perimeters",
style: Optional[Dict] = None,
style_callback: Optional[Callable] = None,
info_mode: str = "on_hover",
zoom_to_layer: bool = True,
**kwargs,
) -> None:
"""Add NASA fire perimeter data to the map.

This method fetches and displays fire perimeter data from the NASA Fire
Event Data Suite (FEDs) via the OpenVEDA OGC API Features.

Args:
data: Pre-loaded fire data as a file path or GeoDataFrame. If provided,
bbox and place are ignored.
bbox: Bounding box [west, south, east, north] in EPSG:4326.
place: Place name to geocode (e.g., "California").
collection: Fire collection ID. Options:
- "snapshot_perimeter_nrt": 20-day recent fire perimeters (default)
- "lf_perimeter_nrt": Current year large fires (>5 km²)
- "lf_perimeter_archive": 2018-2021 Western US archived fires
- "lf_fireline_nrt": Active fire lines
- "lf_newfirepix_nrt": New fire pixels
datetime: ISO 8601 date/time or interval (e.g., "2024-07-01/2024-07-31").
farea_min: Minimum fire area in km² to filter results.
layer_name: Name for the layer. Defaults to "Fire Perimeters".
style: Style dictionary for the fire perimeters. Defaults to orange/red.
style_callback: Styling function called for each feature.
info_mode: Display attributes "on_hover" or "on_click".
zoom_to_layer: Whether to zoom to the layer bounds.
**kwargs: Additional keyword arguments for add_gdf().

Example:
>>> m = leafmap.Map()
>>> m.add_fire_data(place="California", datetime="2024-07-01/2024-07-31")
>>> m
"""
from . import fire as fire_module

if data is not None:
# Use provided data
if isinstance(data, str):
import geopandas as gpd

gdf = gpd.read_file(data)
else:
gdf = data
elif bbox is not None or place is not None:
# Fetch fire data
gdf = fire_module.search_fires(
bbox=bbox,
place=place,
collection=collection,
datetime=datetime,
farea_min=farea_min,
)
else:
raise ValueError("Either data, bbox, or place must be provided")

if gdf.empty:
print("No fire data found for the specified parameters.")
return

# Default fire styling
if style is None:
style = {
"color": "#FF4500",
"weight": 2,
"fillColor": "#FF6347",
"fillOpacity": 0.5,
}

self.add_gdf(
gdf,
layer_name=layer_name,
style=style,
style_callback=style_callback,
info_mode=info_mode,
zoom_to_layer=zoom_to_layer,
**kwargs,
)
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method has nearly identical implementation to the same method in leafmap.py (lines 6874-6968). The only difference is that foliumap.py doesn't include the hover_style parameter. Consider extracting the common logic into a shared helper function in the fire module to reduce code duplication and improve maintainability. This would make it easier to fix bugs or add features in one place.

Copilot uses AI. Check for mistakes.
Comment on lines +346 to +427
def fire_gdf_from_place(
place: str,
collection: str = "snapshot_perimeter_nrt",
datetime: Optional[str] = None,
farea_min: Optional[float] = None,
farea_max: Optional[float] = None,
duration_min: Optional[float] = None,
meanfrp_min: Optional[float] = None,
limit: int = 1000,
buffer_dist: Optional[float] = None,
) -> "gpd.GeoDataFrame":
"""Get fire perimeter data for a place by name.

Uses OpenStreetMap Nominatim for geocoding the place name.

Args:
place: Place name to geocode (e.g., "California", "Los Angeles County").
collection: Fire collection ID. Defaults to "snapshot_perimeter_nrt".
datetime: ISO 8601 date/time or interval (e.g., "2024-07-01/2024-07-31").
farea_min: Minimum fire area in km².
farea_max: Maximum fire area in km².
duration_min: Minimum fire duration in days.
meanfrp_min: Minimum mean Fire Radiative Power.
limit: Maximum number of features to return.
buffer_dist: Distance to buffer around the place geometry, in meters.

Returns:
A GeoDataFrame containing fire perimeter features.

Raises:
ImportError: If required packages are not installed.
ValueError: If place cannot be geocoded.

Example:
>>> gdf = fire_gdf_from_place("California", datetime="2024-07-01/2024-07-31")
>>> print(f"Found {len(gdf)} fires")
"""
try:
import geopandas as gpd
from shapely.geometry import box
except ImportError:
raise ImportError(
"geopandas and shapely are required. "
"Install with: pip install geopandas shapely"
)

try:
import osmnx as ox
except ImportError:
raise ImportError(
"osmnx is required for geocoding. " "Install with: pip install osmnx"
)

# Geocode the place name
place_gdf = ox.geocode_to_gdf(place)
if place_gdf.empty:
raise ValueError(f"Could not geocode place: {place}")

# Apply buffer if specified
if buffer_dist is not None and buffer_dist > 0:
# Buffer in meters requires projecting to a metric CRS
place_gdf_proj = place_gdf.to_crs(epsg=3857)
place_gdf_proj["geometry"] = place_gdf_proj.buffer(buffer_dist)
place_gdf = place_gdf_proj.to_crs(epsg=4326)

# Get bounding box from place geometry
bounds = place_gdf.total_bounds # [minx, miny, maxx, maxy]
bbox = [bounds[0], bounds[1], bounds[2], bounds[3]]

# Fetch fire data for the bounding box
gdf = fire_gdf_from_bbox(
bbox=bbox,
collection=collection,
datetime=datetime,
farea_min=farea_min,
farea_max=farea_max,
duration_min=duration_min,
meanfrp_min=meanfrp_min,
limit=limit,
)

return gdf
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fire_gdf_from_place function doesn't include a duration_max parameter, but fire_gdf_from_bbox does support it. This creates an API inconsistency where users calling fire_gdf_from_place cannot filter by maximum duration, while users calling fire_gdf_from_bbox can. Consider adding duration_max as a parameter to maintain consistency across the API.

Copilot uses AI. Check for mistakes.
Comment on lines +211 to +225
mask = [True] * len(gdf)

if farea_min is not None and "farea" in gdf.columns:
mask = mask & (gdf["farea"] >= farea_min)
if farea_max is not None and "farea" in gdf.columns:
mask = mask & (gdf["farea"] <= farea_max)
if duration_min is not None and "duration" in gdf.columns:
mask = mask & (gdf["duration"] >= duration_min)
if duration_max is not None and "duration" in gdf.columns:
mask = mask & (gdf["duration"] <= duration_max)
if meanfrp_min is not None and "meanfrp" in gdf.columns:
mask = mask & (gdf["meanfrp"] >= meanfrp_min)
if fire_id is not None and "fireid" in gdf.columns:
# Support both string and numeric fire IDs
mask = mask & (gdf["fireid"].astype(str) == str(fire_id))
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The mask variable is initialized as a list of booleans, but then bitwise operations (&) are applied with pandas Series. This will fail at runtime because you cannot use bitwise AND (&) between a Python list and a pandas Series. The mask should be initialized as a pandas Series of True values or each condition should use logical AND to build up the mask correctly.

Copilot uses AI. Check for mistakes.
Comment on lines +262 to +266
A GeoDataFrame containing fire perimeter features.

Raises:
ImportError: If geopandas is not installed.
ValueError: If no features are found.
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The docstring mentions "Raises: ValueError: If no features are found" but the code actually returns an empty GeoDataFrame instead of raising a ValueError when no features are found. The docstring should be updated to accurately reflect the actual behavior, which is to return an empty GeoDataFrame.

Suggested change
A GeoDataFrame containing fire perimeter features.
Raises:
ImportError: If geopandas is not installed.
ValueError: If no features are found.
A GeoDataFrame containing fire perimeter features. If no features
match the query, an empty GeoDataFrame is returned.
Raises:
ImportError: If geopandas is not installed.

Copilot uses AI. Check for mistakes.
Comment on lines +179 to +180
response = requests.get(url, params=params, timeout=60)
response.raise_for_status()
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The API request with raise_for_status() will throw an exception if the HTTP request fails, but there's no try-except block to provide a more user-friendly error message. Consider wrapping this in a try-except block to catch HTTPError and provide a more informative error message to users about what went wrong with the API call (e.g., network issues, invalid collection ID, API temporarily unavailable).

Suggested change
response = requests.get(url, params=params, timeout=60)
response.raise_for_status()
try:
response = requests.get(url, params=params, timeout=60)
response.raise_for_status()
except requests.exceptions.HTTPError as http_err:
raise RuntimeError(
f"OpenVEDA API request failed with status {response.status_code} "
f"for collection '{resolved_collection}': {http_err}"
) from http_err
except requests.exceptions.RequestException as req_err:
raise RuntimeError(
f"Error connecting to OpenVEDA API for collection '{resolved_collection}': {req_err}"
) from req_err

Copilot uses AI. Check for mistakes.
label=legend_title,
)
return
except ImportError:
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'except' clause does nothing but pass and there is no explanatory comment.

Suggested change
except ImportError:
except ImportError:
# Matplotlib is an optional dependency; if it's not available,
# silently fall back to the default styling defined below.

Copilot uses AI. Check for mistakes.
label=legend_title,
)
return
except ImportError:
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'except' clause does nothing but pass and there is no explanatory comment.

Suggested change
except ImportError:
except ImportError:
# matplotlib is an optional dependency; if it's not available,
# skip color-scaled styling and fall back to the default styling below.

Copilot uses AI. Check for mistakes.
@github-actions
Copy link

github-actions bot commented Jan 27, 2026

@github-actions github-actions bot temporarily deployed to pull request January 27, 2026 06:05 Inactive
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
@giswqs giswqs merged commit a130182 into master Jan 27, 2026
13 of 14 checks passed
@giswqs giswqs deleted the fire branch January 27, 2026 06:06
@github-actions github-actions bot temporarily deployed to pull request January 27, 2026 06:12 Inactive
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add support for NASA Fire Progression Dataset

1 participant