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

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
from owslib.ogcapi.features import Features
import geopandas as gpd
import datetime as dt
from datetime import datetime, timedelta

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://firenrt.delta-backend.com"

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

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.

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

We are particularly interested in the spatial and temporal extents of the data.

perm["extent"]
{'spatial': {'bbox': [[-124.63103485107422,
    24.069578170776367,
    -62.97413635253906,
    49.40156555175781]],
  'crs': 'http://www.opengis.net/def/crs/OGC/1.3/CRS84'},
 'temporal': {'interval': [['2023-06-07T00:00:00+00:00',
    '2023-06-27T00:00:00+00:00']],
  'trs': 'http://www.opengis.net/def/uom/ISO-8601/0/Gregorian'}}

In addition to getting metadata about the data we can access the queryable fields. Each of these fields will represent a column in our dataframe.

perm_q = w.collection_queryables("public.eis_fire_snapshot_perimeter_nrt")
perm_q["properties"]
{'wkb_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'},
 'isactive': {'name': 'isactive', 'type': 'number'},
 'meanfrp': {'name': 'meanfrp', 'type': 'number'},
 'n_newpixels': {'name': 'n_newpixels', 'type': 'number'},
 'n_pixels': {'name': 'n_pixels', 'type': 'number'},
 'ogc_fid': {'name': 'ogc_fid', 'type': 'number'},
 'pixden': {'name': 'pixden', 'type': 'number'},
 't': {'name': 't', 'type': 'string'}}

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 the most recent fire perimeters, and 7 days before most recent fire perimeter
most_recent_time = max(*perm["extent"]["temporal"]["interval"])
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 = 2023-06-27T00:00:00+00:00
Last week = 2023-06-20T00:00:00+00:00
perm_results = w.collection_items(
    "public.eis_fire_snapshot_perimeter_nrt",  # name of the dataset we want
    bbox=["-106.8", "24.5", "-72.9", "37.3"],  # coodrinates of bounding box,
    datetime=[last_week + "/" + most_recent_time],  # date range
    limit=1000,  # max number of items returned
    filter="farea>5 AND duration>2",  # additional filters based on queryable fields
)

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', 'id', 'title', 'description', 'numberMatched', 'numberReturned', 'links', 'features'])

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["links"][1]["href"]
'https://firenrt.delta-backend.com/collections/public.eis_fire_snapshot_perimeter_nrt/items?bbox=-106.8%2C24.5%2C-72.9%2C37.3&datetime=2023-06-20T00%3A00%3A00%2B00%3A00%2F2023-06-27T00%3A00%3A00%2B00%3A00&limit=1000&filter=farea%3E5+AND+duration%3E2'

Read data

In addition to all the summary fields, the perm_results dict contains all the data. We can pass the data into geopandas to make it easier to interact with.

df = gpd.GeoDataFrame.from_features(perm_results["features"])
df
geometry duration farea fireid flinelen fperim isactive meanfrp n_newpixels n_pixels ogc_fid pixden t
0 POLYGON ((-95.88101 30.22697, -95.88102 30.227... 45.0 9.811672 63986 0.0 12.124017 1 0.0 0 104 430 10.599620 2023-06-26T12:00:00
1 POLYGON ((-92.83274 29.81987, -92.83264 29.820... 4.0 8.958987 80714 0.0 12.583987 0 0.0 0 63 538 7.032045 2023-06-20T12:00:00
2 POLYGON ((-89.29801 36.86458, -89.29801 36.864... 5.0 7.138301 80749 0.0 13.474343 0 0.0 0 25 1902 3.502234 2023-06-21T12:00:00
3 POLYGON ((-106.33318 36.38887, -106.32771 36.3... 5.5 11.325657 82393 0.0 18.949202 1 0.0 0 157 5271 13.862330 2023-06-26T00:00:00
4 POLYGON ((-106.00861 36.60666, -106.00859 36.6... 11.0 12.858160 79918 0.0 20.962730 1 0.0 0 85 5273 6.610588 2023-06-25T12:00:00
5 MULTIPOLYGON (((-101.28638 32.47797, -101.2863... 14.0 9.415797 78723 0.0 36.674171 1 0.0 0 114 5647 12.107313 2023-06-26T00:00:00
6 MULTIPOLYGON (((-102.69888 31.72183, -102.6987... 27.0 10.484613 71936 0.0 17.485984 1 0.0 0 123 5736 11.731477 2023-06-25T12:00:00
7 POLYGON ((-103.44609 31.79658, -103.44608 31.7... 17.0 23.515158 76919 0.0 22.167102 1 0.0 0 10 5774 0.425258 2023-06-25T00:00:00
8 POLYGON ((-103.89600 32.08170, -103.89603 32.0... 7.0 11.917955 79765 0.0 21.145696 0 0.0 0 7 5806 0.587349 2023-06-21T00:00:00
9 POLYGON ((-106.63614 30.07301, -106.63617 30.0... 3.5 76.317924 83254 0.0 51.647117 1 0.0 0 341 5881 4.468151 2023-06-26T00:00:00
10 POLYGON ((-106.78326 30.05115, -106.78330 30.0... 2.5 10.102581 83084 0.0 13.674702 1 0.0 0 72 5901 7.126891 2023-06-24T12:00:00
11 POLYGON ((-102.85583 27.81858, -102.85601 27.8... 8.0 33.159692 81431 0.0 25.731754 1 0.0 0 340 5969 10.253412 2023-06-26T00:00:00
12 POLYGON ((-97.43908 28.43692, -97.43900 28.436... 19.0 5.312484 73340 0.0 10.166578 0 0.0 0 40 6187 7.529434 2023-06-20T12:00:00
13 POLYGON ((-100.71656 25.53519, -100.71659 25.5... 6.5 5.131956 79873 0.0 8.995199 0 0.0 0 98 6423 19.096034 2023-06-21T00:00:00
14 POLYGON ((-102.25288 26.97686, -102.25289 26.9... 6.0 10.666481 82098 0.0 13.479402 1 0.0 0 73 6470 6.843869 2023-06-26T00:00:00
15 POLYGON ((-106.64036 25.02528, -106.64036 25.0... 14.5 9.843044 78547 0.0 18.376243 1 0.0 0 62 7118 6.298864 2023-06-26T00:00:00
16 POLYGON ((-106.52000 24.91752, -106.52000 24.9... 12.5 16.358760 79217 0.0 19.469750 1 0.0 0 138 7129 8.435847 2023-06-25T12:00:00
17 POLYGON ((-106.47703 24.93926, -106.47705 24.9... 7.0 5.051376 82013 0.0 10.648427 1 0.0 0 47 7143 9.304395 2023-06-26T12:00:00

Explore data

We can quickly explore the data by setting the coordinate reference system (crs) and using .explore()

df = df.set_crs("EPSG:4326")
df.explore()
Make this Notebook Trusted to load map: File -> Trust Notebook

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"]
{'wkb_geometry': {'$ref': 'https://geojson.org/schema/Geometry.json'},
 'fireid': {'name': 'fireid', 'type': 'number'},
 'mergeid': {'name': 'mergeid', 'type': 'number'},
 'ogc_fid': {'name': 'ogc_fid', 'type': 'number'},
 't': {'name': 't', 'type': 'string'}}

Read

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

perm_results = w.collection_items(
    "public.eis_fire_snapshot_perimeter_nrt",
    datetime=most_recent_time,
    limit=1000,
)
perimeters = gpd.GeoDataFrame.from_features(perm_results["features"])

## Get the most recent fire lines
perimeter_ids = perimeters.fireid.unique()
perimeter_ids = ",".join(map(str, perimeter_ids))

fline_results = w.collection_items(
    "public.eis_fire_snapshot_fireline_nrt",
    limit=1000,
    filter="fireid IN ("
    + perimeter_ids
    + ")",  # only the fires from the fire perimeter query above
)
fline = gpd.GeoDataFrame.from_features(fline_results["features"])

Visualize

perimeters = perimeters.set_crs("epsg:4326")
fline = fline.set_crs("epsg:4326")

m = perimeters.explore()
m = fline.explore(m=m, color="orange")
m
Make this Notebook Trusted to load map: File -> Trust Notebook