LCOV - code coverage report
Current view: top level - src/src - MTBS.jl (source / functions) Coverage Total Hit
Test: on branch main Lines: 80.9 % 68 55
Test Date: 2026-09-13 00:06:12 Functions: - 0 0

            Line data    Source code
       1              : module MTBS
       2              : 
       3              : using ..WildfireData: WildfireData, AbstractDataset,
       4              :     _download, _download_file, _load_file, _count, _fields
       5              : using Downloads
       6              : 
       7              : 
       8              : #-----------------------------------------------------------------------------# Data Directory
       9            4 : dir() = WildfireData.dir("MTBS")
      10              : 
      11              : #-----------------------------------------------------------------------------# Constants
      12              : 
      13              : # ArcGIS MapServer (for queries)
      14              : const MAPSERVER_BASE = "https://apps.fs.usda.gov/arcx/rest/services/EDW/EDW_MTBS_01/MapServer"
      15              : 
      16              : # Direct file downloads from USDA Forest Service
      17              : const DOWNLOAD_BASE = "https://data.fs.usda.gov/geodata/edw"
      18              : 
      19              : # Layer IDs in the MapServer
      20              : const LAYER_FIRE_OCCURRENCE = 62  # Fire Occurrence Locations (All Years) - points
      21              : const LAYER_BURN_BOUNDARIES = 63  # Burned Area Boundaries (All Years) - polygons
      22              : 
      23              : # Time coverage
      24              : const YEAR_START = 1984
      25              : const YEAR_END = 2024
      26              : 
      27              : # Fire size thresholds (acres)
      28              : const SIZE_THRESHOLD_WEST = 1000  # Western US
      29              : const SIZE_THRESHOLD_EAST = 500   # Eastern US
      30              : 
      31              : # Available direct downloads
      32              : const DOWNLOADS = Dict(
      33              :     :burn_boundaries_shp => (
      34              :         url = "$DOWNLOAD_BASE/edw_resources/shp/S_USA.MTBS_BURN_AREA_BOUNDARY.zip",
      35              :         filename = "MTBS_Burn_Area_Boundary.zip",
      36              :         description = "Burn area boundaries shapefile (~374 MB)"
      37              :     ),
      38              :     :burn_boundaries_gdb => (
      39              :         url = "$DOWNLOAD_BASE/edw_resources/fc/S_USA.MTBS_BURN_AREA_BOUNDARY.gdb.zip",
      40              :         filename = "MTBS_Burn_Area_Boundary.gdb.zip",
      41              :         description = "Burn area boundaries geodatabase (~158 MB)"
      42              :     ),
      43              :     :fire_occurrence_shp => (
      44              :         url = "$DOWNLOAD_BASE/edw_resources/shp/S_USA.MTBS_FIRE_OCCURRENCE_PT.zip",
      45              :         filename = "MTBS_Fire_Occurrence.zip",
      46              :         description = "Fire occurrence points shapefile (~3 MB)"
      47              :     ),
      48              :     :fire_occurrence_gdb => (
      49              :         url = "$DOWNLOAD_BASE/edw_resources/fc/S_USA.MTBS_FIRE_OCCURRENCE_PT.gdb.zip",
      50              :         filename = "MTBS_Fire_Occurrence.gdb.zip",
      51              :         description = "Fire occurrence points geodatabase (~2 MB)"
      52              :     ),
      53              : )
      54              : 
      55              : #-----------------------------------------------------------------------------# Dataset Definitions
      56              : 
      57              : """
      58              :     MTBSDataset <: AbstractDataset
      59              : 
      60              : Metadata for an MTBS MapServer layer.
      61              : """
      62              : struct MTBSDataset <: AbstractDataset
      63              :     base_url::String
      64              :     layer::Int
      65              :     name::String
      66              :     description::String
      67              :     geometry_type::Symbol  # :point or :polygon
      68              : end
      69              : 
      70           19 : WildfireData.base_query_url(d::MTBSDataset) = "$(d.base_url)/$(d.layer)/query"
      71            2 : WildfireData.base_layer_url(d::MTBSDataset) = "$(d.base_url)/$(d.layer)"
      72              : 
      73              : const DATASETS = Dict{Symbol, MTBSDataset}(
      74              :     :fire_occurrence => MTBSDataset(
      75              :         MAPSERVER_BASE,
      76              :         LAYER_FIRE_OCCURRENCE,
      77              :         "Fire Occurrence Locations (All Years)",
      78              :         "Point locations of all inventoried MTBS fires from 1984 to present. Includes fire name, date, acres, and burn severity assessment data.",
      79              :         :point
      80              :     ),
      81              :     :burn_boundaries => MTBSDataset(
      82              :         MAPSERVER_BASE,
      83              :         LAYER_BURN_BOUNDARIES,
      84              :         "Burned Area Boundaries (All Years)",
      85              :         "Polygon boundaries of burned areas from 1984 to present. Includes fire perimeters with burn severity thresholds.",
      86              :         :polygon
      87              :     ),
      88              : )
      89              : 
      90              : #-----------------------------------------------------------------------------# URL Builder
      91              : 
      92              : """
      93              :     query_url(dataset::Symbol; kwargs...)
      94              : 
      95              : Build a MapServer query URL for the dataset.
      96              : """
      97           14 : function query_url(dataset::Symbol; kwargs...)
      98            8 :     haskey(DATASETS, dataset) || error("Unknown dataset: $dataset. Use `MTBS.datasets()` to list available datasets.")
      99            6 :     WildfireData.query_url(DATASETS[dataset]; kwargs...)
     100              : end
     101              : 
     102              : #-----------------------------------------------------------------------------# API Functions
     103              : 
     104              : """
     105              :     datasets()
     106              : 
     107              : List available MTBS datasets.
     108              : 
     109              : # Example
     110              : ```julia
     111              : MTBS.datasets()
     112              : ```
     113              : """
     114            2 : datasets() = DATASETS
     115              : 
     116              : """
     117              :     info(dataset::Symbol)
     118              : 
     119              : Print information about a specific dataset.
     120              : 
     121              : # Example
     122              : ```julia
     123              : MTBS.info(:fire_occurrence)
     124              : MTBS.info(:burn_boundaries)
     125              : ```
     126              : """
     127            3 : function info(dataset::Symbol)
     128            6 :     if !haskey(DATASETS, dataset)
     129            1 :         error("Unknown dataset: $dataset. Use `MTBS.datasets()` to list available datasets.")
     130              :     end
     131            2 :     d = DATASETS[dataset]
     132            2 :     println("Dataset: ", d.name)
     133            2 :     println("Geometry: ", d.geometry_type)
     134            2 :     println("Description: ", d.description)
     135            2 :     println("Layer ID: ", d.layer)
     136            2 :     println("Query URL: ", query_url(dataset))
     137            2 :     return nothing
     138              : end
     139              : 
     140              : """
     141              :     download(dataset::Symbol; where="1=1", fields="*", limit=nothing, bbox=nothing, verbose=true)
     142              : 
     143              : Download an MTBS dataset and return it as parsed GeoJSON.
     144              : 
     145              : !!! note "MapServer Record Limit"
     146              :     The MTBS MapServer enforces a maximum of 2000 records per request. If your query
     147              :     matches more than 2000 records, results will be silently truncated. Use `MTBS.count()`
     148              :     to check the total before downloading, and use `where` filters or `limit` to stay
     149              :     within bounds. For the full dataset, use `MTBS.download_shapefile()` instead.
     150              : 
     151              : # Arguments
     152              : - `dataset::Symbol`: The dataset key (`:fire_occurrence` or `:burn_boundaries`)
     153              : - `where::String`: SQL-like where clause (default: "1=1" for all records)
     154              : - `fields::String`: Comma-separated field names or "*" for all
     155              : - `limit::Int`: Maximum number of features to return (default: server max of 2000)
     156              : - `bbox`: Bounding box for spatial filtering, as `(west, south, east, north)` tuple or `"west,south,east,north"` string
     157              : - `verbose::Bool`: Print progress information
     158              : 
     159              : # Common Fields
     160              : - `:fire_occurrence`: `fire_id`, `fire_name`, `fire_type`, `ig_date`, `acres`, `latitude`, `longitude`
     161              : - `:burn_boundaries`: `fire_id`, `fire_name`, `fire_type`, `year`, `ig_date`, `acres`
     162              : - `fire_id` begins with the two-letter state code. `ig_date` is epoch milliseconds.
     163              : 
     164              : # Returns
     165              : A `GeoJSON.FeatureCollection`.
     166              : 
     167              : # Examples
     168              : ```julia
     169              : # Download fire occurrence points (limited to 100)
     170              : data = MTBS.download(:fire_occurrence, limit=100)
     171              : 
     172              : # Download fires in California
     173              : data = MTBS.download(:fire_occurrence, where="fire_id LIKE 'CA%'", limit=100)
     174              : 
     175              : # Download large fires (over 10,000 acres)
     176              : data = MTBS.download(:burn_boundaries, where="acres > 10000", limit=50)
     177              : 
     178              : # Download fires from a specific year
     179              : data = MTBS.download(:burn_boundaries, where="year = 2020", limit=100)
     180              : 
     181              : # Download fires within a bounding box (Colorado)
     182              : data = MTBS.download(:fire_occurrence, bbox=(-109, 37, -102, 41), limit=500)
     183              : ```
     184              : """
     185           20 : download(dataset::Symbol; kwargs...) = _download(DATASETS, dataset, "MTBS"; kwargs...)
     186              : 
     187              : """
     188              :     download_file(dataset::Symbol; filename=nothing, force=false, verbose=true, kwargs...)
     189              : 
     190              : Download an MTBS dataset and save it to the local data directory as GeoJSON.
     191              : 
     192              : # Arguments
     193              : - `dataset::Symbol`: The dataset key (`:fire_occurrence` or `:burn_boundaries`)
     194              : - `filename::String`: Custom filename (default: dataset key + .geojson)
     195              : - `force::Bool`: Overwrite existing file if it exists
     196              : - `verbose::Bool`: Print progress information
     197              : - `kwargs...`: Additional arguments passed to `download()`
     198              : 
     199              : # Returns
     200              : The path to the downloaded file.
     201              : 
     202              : # Example
     203              : ```julia
     204              : path = MTBS.download_file(:fire_occurrence, limit=1000)
     205              : ```
     206              : """
     207            2 : download_file(dataset::Symbol; kwargs...) = _download_file(DATASETS, dataset, "MTBS", dir(); kwargs...)
     208              : 
     209              : """
     210              :     load_file(dataset::Symbol; filename=nothing)
     211              : 
     212              : Load a previously downloaded dataset from the local data directory.
     213              : 
     214              : # Example
     215              : ```julia
     216              : MTBS.download_file(:fire_occurrence, limit=100)  # download first
     217              : data = MTBS.load_file(:fire_occurrence)
     218              : ```
     219              : """
     220            4 : load_file(dataset::Symbol; kwargs...) = _load_file(dir(), dataset; kwargs...)
     221              : 
     222              : """
     223              :     count(dataset::Symbol; where="1=1")
     224              : 
     225              : Get the count of features in a dataset matching the where clause.
     226              : 
     227              : # Example
     228              : ```julia
     229              : MTBS.count(:fire_occurrence)  # total count
     230              : MTBS.count(:burn_boundaries, where="year = 2020")  # 2020 fires
     231              : MTBS.count(:fire_occurrence, where="acres > 10000")  # large fires
     232              : ```
     233              : """
     234            8 : count(dataset::Symbol; kwargs...) = _count(DATASETS, dataset, "MTBS"; kwargs...)
     235              : 
     236              : """
     237              :     fields(dataset::Symbol)
     238              : 
     239              : Get the field names and types for a dataset.
     240              : 
     241              : # Example
     242              : ```julia
     243              : MTBS.fields(:fire_occurrence)
     244              : MTBS.fields(:burn_boundaries)
     245              : ```
     246              : """
     247            3 : fields(dataset::Symbol) = _fields(DATASETS, dataset, "MTBS")
     248              : 
     249              : #-----------------------------------------------------------------------------# Direct Download Functions
     250              : 
     251              : """
     252              :     available_downloads()
     253              : 
     254              : List available direct file downloads (shapefiles and geodatabases).
     255              : 
     256              : # Example
     257              : ```julia
     258              : MTBS.available_downloads()
     259              : ```
     260              : """
     261            1 : function available_downloads()
     262            1 :     println("Available MTBS Direct Downloads:")
     263            1 :     println("=" ^ 40)
     264            2 :     for (key, info) in DOWNLOADS
     265            4 :         println("\n:$key")
     266            4 :         println("  $(info.description)")
     267            7 :     end
     268            1 :     println("\nUse `MTBS.download_shapefile(:key)` to download.")
     269            1 :     return keys(DOWNLOADS)
     270              : end
     271              : 
     272              : """
     273              :     download_shapefile(key::Symbol; force=false, verbose=true)
     274              : 
     275              : Download a shapefile or geodatabase directly from USDA Forest Service.
     276              : 
     277              : # Arguments
     278              : - `key::Symbol`: Download key (see `MTBS.available_downloads()`)
     279              : - `force::Bool`: Re-download even if file exists
     280              : - `verbose::Bool`: Print progress information
     281              : 
     282              : # Available keys
     283              : - `:burn_boundaries_shp` - Burn area boundaries shapefile (~374 MB)
     284              : - `:burn_boundaries_gdb` - Burn area boundaries geodatabase (~158 MB)
     285              : - `:fire_occurrence_shp` - Fire occurrence points shapefile (~3 MB)
     286              : - `:fire_occurrence_gdb` - Fire occurrence points geodatabase (~2 MB)
     287              : 
     288              : # Returns
     289              : The path to the downloaded file.
     290              : 
     291              : # Example
     292              : ```julia
     293              : path = MTBS.download_shapefile(:fire_occurrence_shp)
     294              : ```
     295              : """
     296            2 : function download_shapefile(key::Symbol; force::Bool=false, verbose::Bool=true)
     297            2 :     if !haskey(DOWNLOADS, key)
     298            1 :         error("Unknown download key: $key. Use `MTBS.available_downloads()` to list options.")
     299              :     end
     300              : 
     301            0 :     info = DOWNLOADS[key]
     302            0 :     mkpath(dir())
     303            0 :     filepath = joinpath(dir(), info.filename)
     304              : 
     305            0 :     if isfile(filepath) && !force
     306            0 :         verbose && println("File already exists: $filepath")
     307            0 :         verbose && println("Use `force=true` to re-download.")
     308            0 :         return filepath
     309              :     end
     310              : 
     311            0 :     verbose && println("Downloading: $(info.description)")
     312            0 :     verbose && println("URL: $(info.url)")
     313              : 
     314            0 :     Downloads.download(info.url, filepath)
     315              : 
     316            0 :     verbose && println("Saved to: $filepath")
     317            0 :     return filepath
     318              : end
     319              : 
     320              : #-----------------------------------------------------------------------------# Convenience Functions
     321              : 
     322              : """
     323              :     fires(; year=nothing, min_acres=nothing, max_acres=nothing, fire_type=nothing, limit=1000)
     324              : 
     325              : Query fire occurrence points with optional filters.
     326              : 
     327              : Note: The MapServer enforces a maximum of 2000 records per request.
     328              : 
     329              : # Arguments
     330              : - `year::Int`: Filter by year (1984-2024)
     331              : - `min_acres::Real`: Minimum fire size in acres
     332              : - `max_acres::Real`: Maximum fire size in acres
     333              : - `fire_type::String`: Fire type (e.g., "Wildfire", "Prescribed Fire")
     334              : - `limit::Int`: Maximum number of records (default: 1000, server max: 2000)
     335              : 
     336              : # Returns
     337              : A `JSON3.Object` containing the GeoJSON FeatureCollection.
     338              : 
     339              : # Examples
     340              : ```julia
     341              : # Get fires from 2020
     342              : data = MTBS.fires(year=2020)
     343              : 
     344              : # Get large wildfires
     345              : data = MTBS.fires(min_acres=50000, fire_type="Wildfire", limit=100)
     346              : ```
     347              : """
     348            5 : function fires(; year::Union{Int,Nothing}=nothing,
     349              :                min_acres::Union{Real,Nothing}=nothing,
     350              :                max_acres::Union{Real,Nothing}=nothing,
     351              :                fire_type::Union{String,Nothing}=nothing,
     352              :                limit::Int=1000)
     353            3 :     conditions = String[]
     354              : 
     355              :     # The fire occurrence layer has no year field
     356            3 :     !isnothing(year) && push!(conditions, "EXTRACT(YEAR FROM ig_date) = $year")
     357            3 :     !isnothing(min_acres) && push!(conditions, "acres >= $min_acres")
     358            3 :     !isnothing(max_acres) && push!(conditions, "acres <= $max_acres")
     359            3 :     !isnothing(fire_type) && push!(conditions, "fire_type = '$(replace(fire_type, "'" => "''"))'")
     360              : 
     361              : 
     362            5 :     where_clause = isempty(conditions) ? "1=1" : join(conditions, " AND ")
     363              : 
     364            3 :     return download(:fire_occurrence; where=where_clause, limit=limit, verbose=false)
     365              : end
     366              : 
     367              : """
     368              :     boundaries(; year=nothing, min_acres=nothing, max_acres=nothing, limit=100)
     369              : 
     370              : Query burn area boundaries with optional filters.
     371              : 
     372              : Note: The MapServer enforces a maximum of 2000 records per request.
     373              : 
     374              : # Arguments
     375              : - `year::Int`: Filter by year (1984-2024)
     376              : - `min_acres::Real`: Minimum fire size in acres
     377              : - `max_acres::Real`: Maximum fire size in acres
     378              : - `limit::Int`: Maximum number of records (default: 100, server max: 2000)
     379              : 
     380              : # Returns
     381              : A `JSON3.Object` containing the GeoJSON FeatureCollection.
     382              : 
     383              : # Examples
     384              : ```julia
     385              : # Get burn boundaries from 2020
     386              : data = MTBS.boundaries(year=2020)
     387              : 
     388              : # Get large fire boundaries
     389              : data = MTBS.boundaries(min_acres=100000, limit=50)
     390              : ```
     391              : """
     392            3 : function boundaries(; year::Union{Int,Nothing}=nothing,
     393              :                     min_acres::Union{Real,Nothing}=nothing,
     394              :                     max_acres::Union{Real,Nothing}=nothing,
     395              :                     limit::Int=100)
     396            2 :     conditions = String[]
     397              : 
     398            2 :     !isnothing(year) && push!(conditions, "year = $year")
     399            2 :     !isnothing(min_acres) && push!(conditions, "acres >= $min_acres")
     400            2 :     !isnothing(max_acres) && push!(conditions, "acres <= $max_acres")
     401              : 
     402            3 :     where_clause = isempty(conditions) ? "1=1" : join(conditions, " AND ")
     403              : 
     404            2 :     return download(:burn_boundaries; where=where_clause, limit=limit, verbose=false)
     405              : end
     406              : 
     407              : """
     408              :     largest_fires(n::Int=100; year=nothing)
     409              : 
     410              : Get the n largest fires by acreage.
     411              : 
     412              : # Examples
     413              : ```julia
     414              : MTBS.largest_fires(10)  # top 10 largest fires ever
     415              : MTBS.largest_fires(10, year=2020)  # top 10 in 2020
     416              : ```
     417              : """
     418            2 : function largest_fires(n::Int=100; year::Union{Int,Nothing}=nothing)
     419            1 :     where_clause = isnothing(year) ? "1=1" : "EXTRACT(YEAR FROM ig_date) = $year"
     420              : 
     421              :     # Note: MapServer doesn't support ORDER BY in the same way, so we get more records
     422              :     # and sort client-side
     423            1 :     data = download(:fire_occurrence; where=where_clause, limit=min(n * 2, 2000), verbose=false)
     424              : 
     425            1 :     if length(data) > 0
     426              :         # Sort by acres descending and take top n
     427           45 :         sorted_features = sort(collect(data), by=f -> -something(f.acres, 0))
     428            1 :         return sorted_features[1:min(n, length(sorted_features))]
     429              :     end
     430              : 
     431            0 :     return collect(data)
     432              : end
     433              : 
     434              : end # module
        

Generated by: LCOV version 2.0-1