Get Fire Perimeters from an OGC API

Explore data available through an OGC API, and how to filter data temporally, spatially, and by property.
Author

Tempest McCabe, Julia Signell, Nathan Zimmerman

Published

May 23, 2023

Run this notebook

You can launch this notbook using mybinder, by clicking the button below.

Binder

Approach

  1. Use OWSLib to determine what data is available and inspect the metadata
  2. Use OWSLib to filter and read the data
  3. Use geopandas and folium to analyze and plot the data

Note that the default examples environment is missing one requirement: oswlib. We can pip install that before we move on.

%pip install OWSLib==0.28.1 --quiet
Note: you may need to restart the kernel to use updated packages.
import datetime as dt

import geopandas as gpd
from owslib.ogcapi.features import Features
import requests

About the Data

The fire data shown is generated by the FEDs algorithm. The FEDs algorithm tracks fire movement and severity by ingesting observations from the VIIRS thermal sensors on the Suomi NPP and NOAA-20 satellites. This algorithm uses raw VIIRS observations to generate a polygon of the fire, locations of the active fire line, and estimates of fire mean Fire Radiative Power (FRP). The VIIRS sensors overpass at ~1:30 AM and PM local time, and provide estimates of fire evolution ~ every 12 hours. The data produced by this algorithm describe where fires are in space and how fires evolve through time. This CONUS-wide implementation of the FEDs algorithm is based on Chen et al 2020’s algorithm for California.

The data produced by this algorithm is considered experimental.

Look at the data that is availible through the OGC API

The datasets that are distributed throught the OGC API are organized into collections. We can display the collections with the command:

OGC_URL = "https://openveda.cloud/api/features"

w = Features(url=OGC_URL)
w.feature_collections()
['public.eis_fire_lf_perimeter_archive',
 'public.eis_fire_lf_newfirepix_archive',
 'public.eis_fire_lf_fireline_archive',
 'public.eis_fire_lf_fireline_nrt',
 'public.eis_fire_snapshot_fireline_nrt',
 'public.eis_fire_snapshot_newfirepix_nrt',
 'public.eis_fire_lf_newfirepix_nrt',
 'public.eis_fire_lf_perimeter_nrt',
 'public.eis_fire_snapshot_perimeter_nrt',
 'pg_temp.eis_fire_lf_perimeter_nrt_latest',
 'public.st_subdivide',
 'public.st_hexagongrid',
 'public.st_squaregrid']

We will focus on the public.eis_fire_snapshot_fireline_nrt collection, the public.eis_fire_snapshot_perimeter_nrt collection, and the public.eis_fire_lf_perimeter_archive collection here.

Inspect the metatdata for public.eis_fire_snapshot_perimeter_nrt collection

We can access information that describes the public.eis_fire_snapshot_perimeter_nrt table.

collection_id = "public.eis_fire_snapshot_perimeter_nrt"
perm = w.collection(collection_id)

We are interested in accessing the queryable fields. Each of these fields will represent a column in our dataframe.

perm_q = w.collection_queryables(collection_id)
perm_q["properties"]
{'geometry': {'$ref': 'https://geojson.org/schema/Geometry.json'},
 'duration': {'name': 'duration', 'type': 'number'},
 'farea': {'name': 'farea', 'type': 'number'},
 'fireid': {'name': 'fireid', 'type': 'number'},
 'flinelen': {'name': 'flinelen', 'type': 'number'},
 'fperim': {'name': 'fperim', 'type': 'number'},
 'geom_counts': {'name': 'geom_counts', 'type': 'string'},
 'isactive': {'name': 'isactive', 'type': 'number'},
 'low_confidence_grouping': {'name': 'low_confidence_grouping',
  'type': 'number'},
 'meanfrp': {'name': 'meanfrp', 'type': 'number'},
 'n_newpixels': {'name': 'n_newpixels', 'type': 'number'},
 'n_pixels': {'name': 'n_pixels', 'type': 'number'},
 'pixden': {'name': 'pixden', 'type': 'number'},
 'primarykey': {'name': 'primarykey', 'type': 'string'},
 'region': {'name': 'region', 'type': 'string'},
 't': {'name': 't', 'type': 'string'}}

We also want to know the most recent data from the dataset. OWSLib doesn’t enable all of the same fields as the API does, so to find the time of the most recent data, we will parse some json from the API.

This is a URL call to the API: https://openveda.cloud/api/features/collections/public.eis_fire_lf_fireline_nrt/items?f=geojson&sortby=-t"

  • ‘public.eis_fire_lf_fireline_nrt’ is our collection name (Large fires near real time)
  • ‘f=geojson’ is requesting our output in geojson format
  • ‘sortby=-t’ is requesting the output to be sorted by column ‘t’, and the ‘-t’ requests the output in reverse order, or newest entry first.
url_example = "https://openveda.cloud/api/features/collections/public.eis_fire_lf_fireline_nrt/items?f=geojson&sortby=-t" 
response = requests.get(url_example)
most_recent_time = response.json()['features'][0]['properties']['t'] # Extracting the most recent time from the json dictionary. 
most_recent_time
'2025-09-05T00:00:00'

Why Pagination Matters

Fire perimeter collections contain complex GeoJSON polygons with detailed boundaries that can be megabytes per feature. When requesting large numbers of these complex geometries:

  • Server capacity can be exhausted, causing timeouts
  • Response size limits may cause partial or failed responses
  • The default limit is 10 items; requesting limits of 1000-3000 often fails

The Solution: Use pagination! With offset and limit=100 (for ‘pages’ with 100 items each) we are able to fetch data in manageable chunks.

Here are some examples showing offset-based pagination:

# Page 1: offset=0, limit=100
print(f"{OGC_URL}/collections/{collection_id}/items?limit=100&offset=0")
# Page 2: offset=100, limit=100  
print(f"{OGC_URL}/collections/{collection_id}/items?limit=100&offset=100")
# Page 3: offset=200, limit=100
print(f"{OGC_URL}/collections/{collection_id}/items?limit=100&offset=200")
# Continue until all features are retrieved...
https://openveda.cloud/api/features/collections/public.eis_fire_snapshot_perimeter_nrt/items?limit=100&offset=0
https://openveda.cloud/api/features/collections/public.eis_fire_snapshot_perimeter_nrt/items?limit=100&offset=100
https://openveda.cloud/api/features/collections/public.eis_fire_snapshot_perimeter_nrt/items?limit=100&offset=200

Manual Pagination with OWSLib

With OWSLib, you can manually manage the offset to page through results:

# Manual pagination using OWSLib
all_features = []
offset = 0
limit = 100

for page in range(10):
    result = w.collection_items(
        "public.eis_fire_snapshot_perimeter_nrt",
        limit=limit,
        offset=offset
        # add other filters as needed: bbox=..., datetime=..., filter=...
    )
    features = result.get("features", [])
    if not features:
        break
    all_features.extend(features)
    offset += limit
    
print(f"Fetched {len(all_features)} features in pages of {limit}")
Fetched 1000 features in pages of 100

Helper Function for Pagination

But you don’t have to manage this yourself! Here’s a helper function we’ll use throughout this tutorial to keep things simple and clean:

import math

def iter_features_offset(w, collection_id, params=None, page_size=100, max_pages=None, progress=True):
    """
    Paginate through OGC API Features using offset parameter.
    - Uses w.collection_items() with offset increments
    - Default page_size=100
    - Shows progress as pages are fetched
    """
    params = dict(params or {})
    
    # Get total count with minimal data
    meta_params = dict(params)
    meta_params["limit"] = 1
    meta = w.collection_items(collection_id, **meta_params)
    total = meta.get("numberMatched", 0)
    
    if total == 0:
        if progress:
            print("No matching features")
        return []
    # Round up the division here for total number of pages
    pages = math.ceil(total / page_size)

    # Support a user-defined page limit
    if max_pages and max_pages < pages:
        pages = max_pages

    all_features = []
    
    for i in range(pages):
        offset = i * page_size
        page_params = dict(params)
        page_params["limit"] = page_size
        page_params["offset"] = offset
        
        page = w.collection_items(collection_id, **page_params)
        features = page.get("features", [])
        all_features.extend(features)
        
        if progress:
            print(f"Page {i+1}/{pages}: {len(all_features)}/{total} features")
        
        if len(features) < page_size:
            break
    
    return all_features

Filter the data

It is always a good idea to do any data filtering as early as possible. In this example we know that we want the data for particular spatial and temporal extents. We can apply those and other filters using the OWSLib package.

In the below example we are:

  • choosing the public.eis_fire_snapshot_perimeter_nrt collection
  • subsetting it by space using the bbox parameter
  • subsetting it by time using the datetime parameter
  • filtering for fires over 5km^2 and over 2 days long using the filter parameter. The filter parameter lets us filter by the columns in ‘public.eis_fire_snapshot_perimeter_nrt’ using SQL-style queries.

NOTE: The limit parameter desginates the maximum number of objects the query will return. The default limit is 10, so if we want to all of the fire perimeters within certain conditions, we need to make sure that the limit is large.

## Get 7 days before most recent fire perimeter
most_recent_time = most_recent_time + "+00:00"
now = dt.datetime.strptime(most_recent_time, "%Y-%m-%dT%H:%M:%S+00:00")
last_week = now - dt.timedelta(weeks=1)
last_week = dt.datetime.strftime(last_week, "%Y-%m-%dT%H:%M:%S+00:00")
print("Most Recent Time =", most_recent_time)
print("Last week =", last_week)
Most Recent Time = 2025-09-05T00:00:00+00:00
Last week = 2025-08-29T00:00:00+00:00
# Using pagination instead of a single large request
params = {
    "bbox": ["-106.8", "24.5", "-72.9", "37.3"],
    "datetime": [last_week + "/" + most_recent_time],
    "filter": "farea>5 AND duration>2",
}

# Fetch features with pagination
features = iter_features_offset(
    w,
    "public.eis_fire_snapshot_perimeter_nrt",
    params=params,
    page_size=100,
    progress=True,
)

# Create results dictionary compatible with existing code
perm_results = {
    "type": "FeatureCollection",
    "features": features,
    "numberMatched": len(features),
    "numberReturned": len(features)
}
Page 1/1: 9/9 features

The result is a dictionary containing all of the data and some summary fields. We can look at the keys to see what all is in there.

perm_results.keys()
dict_keys(['type', 'features', 'numberMatched', 'numberReturned'])

For instance you can check the total number of matched items and make sure that it is equal to the number of returned items. This is how you know that the limit you defined above is high enough.

perm_results["numberMatched"] == perm_results["numberReturned"]
True

You can also access the data directly in the browser or in an HTTP GET call using the constructed link.

perm_results["features"][0]["links"][1]["href"]
'https://openveda.cloud/api/features/collections/public.eis_fire_snapshot_perimeter_nrt/items/CONUS|10733|2025-09-04T00:00:00'

Visualize Most Recent Fire Perimeters with Firelines

If we wanted to combine collections to make more informative analyses, we can use some of the same principles.

First we’ll get the queryable fields, and the extents:

fline_q = w.collection_queryables("public.eis_fire_snapshot_fireline_nrt")
fline_collection = w.collection("public.eis_fire_snapshot_fireline_nrt")
fline_q["properties"]
{'geometry': {'$ref': 'https://geojson.org/schema/Geometry.json'},
 'fireid': {'name': 'fireid', 'type': 'number'},
 'mergeid': {'name': 'mergeid', 'type': 'number'},
 'primarykey': {'name': 'primarykey', 'type': 'string'},
 'region': {'name': 'region', 'type': 'string'},
 't': {'name': 't', 'type': 'string'}}

Read & Visualize

Then we’ll use those fields to get most recent fire perimeters and fire lines.

# Get most recent fire perimeters
params = {
    "datetime": most_recent_time,  # or specify a date like "2025-01-15T00:00:00"
}

# Get perimeters with pagination
features = iter_features_offset(
    w,
    "public.eis_fire_snapshot_perimeter_nrt",
    params=params,
    page_size=100,
    max_pages=1,  # Increase if you want more data
    progress=True,
)

perm_results = {
    "type": "FeatureCollection",
    "features": list(features)
}

perimeters = gpd.GeoDataFrame.from_features(perm_results["features"])
perimeters = perimeters.set_crs("epsg:4326")

# Print info about the data
print(f"Found {len(perimeters)} perimeter features")
print(f"Fire IDs: {perimeters.fireid.unique()[:10]}")
print(f"Bounds: {perimeters.total_bounds}")

# Create interactive map
center_lat = -15
center_lon = -100

m = perimeters.explore(
    zoom_start=2,
    location=(center_lat, center_lon),
    color='red',
    style_kwds={
        'fillOpacity': 0.3,
        'weight': 2
    },
    tooltip=['fireid', 't', 'farea'],
    popup=True,
    legend_name="Fire Perimeters"
)

m
Page 1/1: 100/511 features
Found 100 perimeter features
Fire IDs: [10259 10358 10887 10888 11437 11825 12040 12131 12158 12899]
Bounds: [-140.32123504   45.07248276  -66.45576306   66.2482122 ]
Make this Notebook Trusted to load map: File -> Trust Notebook
# Get fire lines (no filter to see what's available)
fline_params = {}  # Add filters as needed

# Get fire lines with pagination
fline_features = iter_features_offset(
    w,
    "public.eis_fire_snapshot_fireline_nrt",
    params=fline_params,
    page_size=500,
    max_pages=1,
    progress=True,
)

fline_results = {
    "type": "FeatureCollection",
    "features": list(fline_features)
}

fline = gpd.GeoDataFrame.from_features(fline_results["features"])
fline = fline.set_crs("epsg:4326")

# Print info about the data
print(f"Found {len(fline)} fire line features")
print(f"Fire IDs: {sorted(fline.fireid.unique())[:10]}")
print(f"Time periods: {fline['t'].unique()[:5]}")
print(f"Bounds: {fline.total_bounds}")

# Create interactive map
center_lat = 50
center_lon = -100

m = fline.explore(
    zoom_start=4,  # Zoom out more since this covers a larger area
    location=(center_lat, center_lon),
    color='orange',
    weight=3,
    tooltip=['fireid', 't', 'region'],  # Adjust columns as needed
    popup=True,
    legend_name="Fire Lines"
)

m
Page 1/1: 500/10633 features
Found 500 fire line features
Fire IDs: [np.int64(2), np.int64(5), np.int64(15), np.int64(16), np.int64(17), np.int64(18), np.int64(28), np.int64(29), np.int64(32), np.int64(34)]
Time periods: ['2025-02-23T00:00:00' '2025-01-08T00:00:00' '2025-02-25T00:00:00'
 '2025-03-01T00:00:00' '2025-03-25T00:00:00']
Bounds: [-148.66349246   44.02760469  -64.91996991   70.2839918 ]
Make this Notebook Trusted to load map: File -> Trust Notebook

Visualize the Growth of the Camp Fire

We may be interested in understanding how a fire evolved through time. To do this, we can work with the “Large fire” or “lf” perimeter collections. The public.eis_fire_lf_perimeter_nrt collection has the full spread history of fires from this year. public.eis_fire_lf_perimeter_archive has the full spread history of fires from 2018-2021 that were in the Western United States. The Camp Fire was in 2018, so we will work with the public.eis_fire_lf_perimeter_archive collection.

We can start by querying with information specific to the Camp Fire, like it’s genreal region (Northern California), and when it was active (November 2018). With that information, we can get the fireID associated with the Camp Fire.

# Get Camp Fire area perimeters with pagination
params = {
    "bbox": ["-124.52", "39.2", "-120", "42"],  # North California bounding box
    "datetime": ["2018-11-01T00:00:00+00:00/2018-11-30T12:00:00+00:00"],
}

features = iter_features_offset(
    w,
    "public.eis_fire_lf_perimeter_archive",
    params=params,
    page_size=100,
    progress=True,
)

perimeters_archive_results = {
    "type": "FeatureCollection",
    "features": features
}

perimeters = gpd.GeoDataFrame.from_features(perimeters_archive_results["features"])
perimeters = perimeters.sort_values(by="t", ascending=False)
perimeters = perimeters.set_crs("epsg:4326")

print(perimeters.fireid.unique())
m = perimeters.explore(
    style_kwds={"fillOpacity": 0}, zoom_start=9, location=(39.7, -121.4)
)
m
Page 1/1: 48/48 features
['F17028' 'F18493']
Make this Notebook Trusted to load map: File -> Trust Notebook

Based on the map, we know that the fireID for the Camp Fire is “F17028”. We can use that to directly query for that particular fire.

# Get Camp Fire specific perimeters with pagination
params = {
    "filter": "fireid = 'F17028'",
    "datetime": ["2018-01-01T00:00:00+00:00/2018-12-31T12:00:00+00:00"],
}

features = iter_features_offset(
    w,
    "public.eis_fire_lf_perimeter_archive",
    params=params,
    page_size=100,
    progress=True,
)

perimeters_archive_results = {
    "type": "FeatureCollection",
    "features": features
}

perimeters = gpd.GeoDataFrame.from_features(perimeters_archive_results["features"])
perimeters = perimeters.sort_values(by="t", ascending=False)
perimeters = perimeters.set_crs("epsg:4326")

m = perimeters.explore(
    style_kwds={"fillOpacity": 0}, zoom_start=12, location=(39.7, -121.4)
)
m
Page 1/1: 66/66 features
Make this Notebook Trusted to load map: File -> Trust Notebook

Download Data

Downloading pre-filtered data may be useful for working locally, or for working with the data in GIS software.

We can download the dataframe we made by writing it out to a GeoJSON file.

perimeters.to_file('perimeters.geojson', driver='GeoJSON')

Collection Information

The API hosts 9 different collections. There are four different types of data, and three different time-scales availible for querying through the API. “*snapshot*” collections are useful for visualizing the most recent data. It contains the most recent fires perimeters, active firelines, or VIIRS observations within the last 20 days. “*lf*” collections (short for Large Fire), show every fire perimeter, active fire line, or VIIRS observations for fires over 5 km^2. Collections that end in *archive are for year 2018 - 2021 across the Western United States. Collections with the *nrt ending are for CONUS from this most recent year. FireIDs are consistent only between layers with the same timescale (snapshot, lf_*nrt, and lf_archive*).

public.eis_fire_snapshot_perimeter_nrt

Perimeter of cumulative fire-area. Most recent perimeter from the last 20 days.

public.eis_fire_lf_perimeter_nrt

Perimeter of cumulative fire-area, from fires over 5 km^2. Every fire perimeter from current year to date.

public.eis_fire_lf_perimeter_archive

Perimeter of cumulative fire-area, from fires over 5 km^2 in the Western United States. Every fire perimeter from 2018-2021.

Column Description Unit
meanfrp Mean fire radiative power. The weighted sum of the fire radiative power detected at each new pixel, divided by the number of pixels. If no new pixels are detected, meanfrp is set to zero. MW/(pixel area)
t Time of VIIRS detection, corrected to noon and midnight. Datetime. yyyy-mm-ddThh:mm:ss. Local time.
fireid Fire ID. Unique for each fire. Matches fireid. Numeric ID
pixden Number of pixels divided by area of perimeter. pixels/Km^2
duration Number of days since first observation of fire. Fires with a single observation have a duration of zero. Days
flinelen Length of active fire line, based on new pixels. If no new pixels are detected, flinelen is set to zero. Km
fperim Length of fire perimeter. Km
farea Area within fire perimeter. Km^2
n_newpixels Number of pixels newly detected since last overpass. pixels
n_pixels Number of pixel-detections in history of fire. pixels
isactive Have new fire pixels been detected in the last 5 days? Boolean
ogc_fid The ID used by the OGC API to sort perimeters. Numeric ID
geometry The shape of the perimeter. Geometry

public.eis_fire_snapshot_fireline_nrt

Active fire line as estimated by new VIIRS detections. Most fire line from the last 20 days.

public.eis_fire_lf_fireline_nrt

Active fire line as estimated by new VIIRS detections, from fires over 5 km^2. Every fire line from current year to date.

public.eis_fire_lf_fireline_nrt

Active fire line as estimated by new VIIRS detections, from fires over 5 km^2 in the Western United States. Every fire line from 2018-2021.

Column Description Unit
fireid ID of fire pixel associated with. Numeric ID
t Time of VIIRS detection, corrected to noon and midnight. Datetime. yyyy-mm-ddThh:mm:ss. Local time.
mergeid ID used to connect pixels to perimeters. Matches fireid Numeric ID
ogc_fid The ID used by the OGC API to sort pixels. Numeric ID

public.eis_fire_snapshot_newfirepix_nrt

New pixel detections that inform the most recent time-step’s perimeter and fireline calculation from the last 20 days.

public.eis_fire_lf_newfirepix_nrt

New pixel detections that inform a given time-step’s perimeter and fireline calculation. Availible from start of current year to date.

public.eis_fire_lf_newfirepix_archive

New pixel detections that inform a given time-step’s perimeter and fireline calculation. Availible for Western United States from 2018-2021.

Column Description Unit
fireid ID of fire pixel associated with. Numeric ID
t Time of VIIRS detection, corrected to noon and midnight. Datetime. yyyy-mm-ddThh:mm:ss. Local time.
mergeid ID used to connect pixels to perimeters. Matches fireid Numeric ID
ogc_fid The ID used by the OGC API to sort pixels. Numeric ID