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

            Line data    Source code
       1              : module FIRMS
       2              : 
       3              : using ..WildfireData
       4              : using HTTP
       5              : using CSV
       6              : using DataFrames
       7              : using Dates
       8              : 
       9              : 
      10              : #-----------------------------------------------------------------------------# Data Directory
      11            5 : dir() = WildfireData.dir("FIRMS")
      12              : 
      13              : #-----------------------------------------------------------------------------# Constants
      14              : 
      15              : const API_BASE = "https://firms.modaps.eosdis.nasa.gov/api"
      16              : 
      17              : # Available data sources
      18              : const SOURCES = Dict{Symbol, NamedTuple{(:name, :description, :start_date, :type), Tuple{String, String, String, Symbol}}}(
      19              :     :MODIS_NRT => (
      20              :         name = "MODIS_NRT",
      21              :         description = "MODIS Collection 6.1 Near Real-Time (Aqua/Terra satellites)",
      22              :         start_date = "2000-11-01",
      23              :         type = :NRT
      24              :     ),
      25              :     :MODIS_SP => (
      26              :         name = "MODIS_SP",
      27              :         description = "MODIS Collection 6.1 Standard Processing (science quality)",
      28              :         start_date = "2000-11-01",
      29              :         type = :SP
      30              :     ),
      31              :     :VIIRS_SNPP_NRT => (
      32              :         name = "VIIRS_SNPP_NRT",
      33              :         description = "VIIRS 375m S-NPP Near Real-Time",
      34              :         start_date = "2012-01-20",
      35              :         type = :NRT
      36              :     ),
      37              :     :VIIRS_SNPP_SP => (
      38              :         name = "VIIRS_SNPP_SP",
      39              :         description = "VIIRS 375m S-NPP Standard Processing",
      40              :         start_date = "2012-01-20",
      41              :         type = :SP
      42              :     ),
      43              :     :VIIRS_NOAA20_NRT => (
      44              :         name = "VIIRS_NOAA20_NRT",
      45              :         description = "VIIRS 375m NOAA-20 Near Real-Time",
      46              :         start_date = "2018-04-01",
      47              :         type = :NRT
      48              :     ),
      49              :     :VIIRS_NOAA20_SP => (
      50              :         name = "VIIRS_NOAA20_SP",
      51              :         description = "VIIRS 375m NOAA-20 Standard Processing",
      52              :         start_date = "2018-04-01",
      53              :         type = :SP
      54              :     ),
      55              :     :VIIRS_NOAA21_NRT => (
      56              :         name = "VIIRS_NOAA21_NRT",
      57              :         description = "VIIRS 375m NOAA-21 Near Real-Time",
      58              :         start_date = "2024-01-17",
      59              :         type = :NRT
      60              :     ),
      61              :     :LANDSAT_NRT => (
      62              :         name = "LANDSAT_NRT",
      63              :         description = "Landsat 8/9 30m Near Real-Time (US/Canada only)",
      64              :         start_date = "2022-06-20",
      65              :         type = :NRT
      66              :     ),
      67              : )
      68              : 
      69              : # Common bounding boxes for convenience
      70              : const REGIONS = Dict{Symbol, String}(
      71              :     :world => "world",
      72              :     :conus => "-125,24,-66,50",           # Continental US
      73              :     :alaska => "-180,51,-129,72",          # Alaska
      74              :     :california => "-125,32,-114,42",      # California
      75              :     :western_us => "-125,31,-102,49",      # Western US
      76              :     :eastern_us => "-102,24,-66,50",       # Eastern US
      77              :     :canada => "-141,41,-52,84",           # Canada
      78              :     :australia => "112,-44,154,-10",       # Australia
      79              :     :europe => "-25,35,40,72",             # Europe
      80              :     :amazon => "-82,-20,-34,13",           # Amazon basin
      81              :     :africa => "-18,-35,52,38",            # Africa
      82              : )
      83              : 
      84              : #-----------------------------------------------------------------------------# MAP_KEY Management
      85              : 
      86              : """
      87              :     get_map_key()
      88              : 
      89              : Get the current FIRMS MAP_KEY from the `FIRMS_MAP_KEY` environment variable.
      90              : 
      91              : Returns `nothing` if not configured.
      92              : """
      93           10 : get_map_key() = get(ENV, "FIRMS_MAP_KEY", nothing)
      94              : 
      95              : """
      96              :     set_map_key!(key::String)
      97              : 
      98              : Set the FIRMS MAP_KEY by setting the `FIRMS_MAP_KEY` environment variable.
      99              : 
     100              : You can obtain a free MAP_KEY by registering at:
     101              : https://firms.modaps.eosdis.nasa.gov/api/map_key/
     102              : 
     103              : # Example
     104              : ```julia
     105              : FIRMS.set_map_key!("your-32-character-map-key-here")
     106              : ```
     107              : """
     108            1 : function set_map_key!(key::String)
     109           50 :     if length(key) != 32 || !all(c -> isletter(c) || isdigit(c), key)
     110            0 :         @warn "MAP_KEY should be a 32-character alphanumeric string"
     111              :     end
     112            1 :     ENV["FIRMS_MAP_KEY"] = key
     113            1 :     return nothing
     114              : end
     115              : 
     116              : function require_map_key()
     117            7 :     key = get_map_key()
     118           14 :     if isnothing(key)
     119            0 :         error("""
     120              :             FIRMS MAP_KEY not configured.
     121              : 
     122              :             To use the FIRMS API, you need a free MAP_KEY:
     123              :             1. Register at: https://firms.modaps.eosdis.nasa.gov/api/map_key/
     124              :             2. Set it via: FIRMS.set_map_key!("your-key")
     125              :                Or set the FIRMS_MAP_KEY environment variable
     126              :             """)
     127              :     end
     128            7 :     return key
     129              : end
     130              : 
     131              : #-----------------------------------------------------------------------------# Info Functions
     132              : 
     133              : """
     134              :     info()
     135              : 
     136              : Print information about the FIRMS API and available data sources.
     137              : """
     138            1 : function info()
     139            1 :     println("NASA FIRMS: Fire Information for Resource Management System")
     140            1 :     println("=" ^ 60)
     141            1 :     println("API: $API_BASE")
     142            1 :     println()
     143            1 :     println("Satellite fire detection data from MODIS, VIIRS, and Landsat.")
     144            1 :     println("Data available within 3 hours of satellite observation globally,")
     145            1 :     println("real-time for US/Canada.")
     146            1 :     println()
     147            1 :     println("Rate Limit: 5000 transactions per 10-minute interval")
     148            1 :     println()
     149              : 
     150            1 :     key = get_map_key()
     151            2 :     if isnothing(key)
     152            0 :         println("Status: MAP_KEY not configured")
     153            0 :         println("Register for free at: https://firms.modaps.eosdis.nasa.gov/api/map_key/")
     154              :     else
     155            1 :         println("Status: MAP_KEY configured")
     156              :     end
     157              : 
     158            1 :     return nothing
     159              : end
     160              : 
     161              : """
     162              :     sources(; type=nothing)
     163              : 
     164              : List available FIRMS data sources.
     165              : 
     166              : # Arguments
     167              : - `type::Symbol`: Filter by type (`:NRT` for Near Real-Time, `:SP` for Standard Processing)
     168              : 
     169              : # Example
     170              : ```julia
     171              : FIRMS.sources()  # all sources
     172              : FIRMS.sources(type=:NRT)  # only NRT sources
     173              : ```
     174              : """
     175            3 : function sources(; type::Union{Symbol, Nothing}=nothing)
     176            3 :     if isnothing(type)
     177            1 :         return SOURCES
     178              :     else
     179            2 :         return Dict(k => v for (k, v) in SOURCES if v.type == type)
     180              :     end
     181              : end
     182              : 
     183              : """
     184              :     regions()
     185              : 
     186              : List predefined geographic regions for convenience queries.
     187              : 
     188              : # Example
     189              : ```julia
     190              : FIRMS.regions()
     191              : ```
     192              : """
     193            1 : regions() = REGIONS
     194              : 
     195              : #-----------------------------------------------------------------------------# API Functions
     196              : 
     197              : """
     198              :     query_url(source::Symbol, area::String, days::Int; date=nothing)
     199              : 
     200              : Build a FIRMS API query URL.
     201              : 
     202              : # Arguments
     203              : - `source::Symbol`: Data source (see `FIRMS.sources()`)
     204              : - `area::String`: Bounding box as "west,south,east,north" or "world"
     205              : - `days::Int`: Number of days (1-10)
     206              : - `date`: Optional date (Date or "YYYY-MM-DD" string) for historical queries
     207              : """
     208           18 : function query_url(source::Symbol, area::String, days::Int; date::Union{Date, String, Nothing}=nothing)
     209           11 :     haskey(SOURCES, source) || error("Unknown source: $source. Use `FIRMS.sources()` to list available sources.")
     210            8 :     1 <= days <= 10 || error("days must be between 1 and 10")
     211              : 
     212           12 :     key = require_map_key()
     213            6 :     source_name = SOURCES[source].name
     214              : 
     215            6 :     url = "$API_BASE/area/csv/$key/$source_name/$area/$days"
     216              : 
     217            6 :     if !isnothing(date)
     218            1 :         date_str = date isa Date ? Dates.format(date, "yyyy-mm-dd") : date
     219            1 :         url *= "/$date_str"
     220              :     end
     221              : 
     222            6 :     return url
     223              : end
     224              : 
     225              : """
     226              :     download(source::Symbol; area="world", region=nothing, days=1, date=nothing, verbose=true)
     227              : 
     228              : Download active fire data from FIRMS.
     229              : 
     230              : # Arguments
     231              : - `source::Symbol`: Data source (e.g., `:VIIRS_NOAA20_NRT`, `:MODIS_NRT`)
     232              : - `area::String`: Bounding box as "west,south,east,north" (default: "world")
     233              : - `region::Symbol`: Use a predefined region instead of area (see `FIRMS.regions()`)
     234              : - `days::Int`: Number of days to query (1-10, default: 1)
     235              : - `date`: Specific date for historical data (Date or "YYYY-MM-DD" string)
     236              : - `verbose::Bool`: Print progress information
     237              : 
     238              : # Returns
     239              : A `DataFrame` containing the fire detection data.
     240              : 
     241              : # Examples
     242              : ```julia
     243              : # Download last day of VIIRS NOAA-20 NRT data worldwide
     244              : df = FIRMS.download(:VIIRS_NOAA20_NRT)
     245              : 
     246              : # Download 3 days of data for California
     247              : df = FIRMS.download(:VIIRS_NOAA20_NRT, region=:california, days=3)
     248              : 
     249              : # Download data for a specific bounding box
     250              : df = FIRMS.download(:MODIS_NRT, area="-120,35,-115,40", days=2)
     251              : 
     252              : # Download historical data for a specific date
     253              : df = FIRMS.download(:VIIRS_SNPP_SP, region=:western_us, days=1, date="2023-08-15")
     254              : ```
     255              : """
     256           10 : function download(source::Symbol; area::String="world", region::Union{Symbol, Nothing}=nothing,
     257              :                   days::Int=1, date::Union{Date, String, Nothing}=nothing, verbose::Bool=true)
     258              : 
     259              :     # Handle region shortcut
     260            5 :     if !isnothing(region)
     261            3 :         haskey(REGIONS, region) || error("Unknown region: $region. Use `FIRMS.regions()` to list available regions.")
     262            3 :         area = REGIONS[region]
     263              :     end
     264              : 
     265            5 :     url = query_url(source, area, days; date=date)
     266              : 
     267            4 :     verbose && println("Downloading: $(SOURCES[source].description)")
     268            4 :     verbose && println("Area: $area")
     269            4 :     verbose && println("Days: $days")
     270            4 :     !isnothing(date) && verbose && println("Date: $date")
     271            4 :     verbose && println("URL: $(replace(url, get_map_key() => "[MAP_KEY]"))")
     272              : 
     273            4 :     response = HTTP.get(url; status_exception=false, connect_timeout=60, readtimeout=60)
     274              : 
     275            4 :     if response.status != 200
     276            0 :         error("Failed to download data. HTTP status: $(response.status)\nResponse: $(String(response.body))")
     277              :     end
     278              : 
     279            4 :     body = String(response.body)
     280              : 
     281              :     # Check for error messages
     282           12 :     if startswith(body, "Invalid") || startswith(body, "Error") || contains(body, "exceeded")
     283            0 :         error("FIRMS API error: $body")
     284              :     end
     285              : 
     286              :     # Parse CSV
     287            4 :     verbose && println("Parsing CSV...")
     288            4 :     df = CSV.read(IOBuffer(body), DataFrame)
     289              : 
     290            4 :     verbose && println("Downloaded $(nrow(df)) fire detections")
     291              : 
     292            4 :     return df
     293              : end
     294              : 
     295              : """
     296              :     download_file(source::Symbol; filename=nothing, force=false, verbose=true, kwargs...)
     297              : 
     298              : Download FIRMS data and save it to the local data directory.
     299              : 
     300              : # Arguments
     301              : - `source::Symbol`: Data source
     302              : - `filename::String`: Custom filename (default: auto-generated)
     303              : - `force::Bool`: Overwrite existing file
     304              : - `verbose::Bool`: Print progress information
     305              : - `kwargs...`: Additional arguments passed to `download()`
     306              : 
     307              : # Returns
     308              : The path to the downloaded CSV file.
     309              : 
     310              : # Example
     311              : ```julia
     312              : path = FIRMS.download_file(:VIIRS_NOAA20_NRT, region=:california, days=3)
     313              : ```
     314              : """
     315            2 : function download_file(source::Symbol; filename::Union{String, Nothing}=nothing,
     316              :                        force::Bool=false, verbose::Bool=true, kwargs...)
     317            1 :     mkpath(dir())
     318              : 
     319            1 :     if isnothing(filename)
     320            0 :         date_str = Dates.format(today(), "yyyymmdd")
     321            0 :         filename = "$(SOURCES[source].name)_$date_str.csv"
     322              :     end
     323            1 :     filepath = joinpath(dir(), filename)
     324              : 
     325            1 :     if isfile(filepath) && !force
     326            0 :         verbose && println("File already exists: $filepath")
     327            0 :         verbose && println("Use `force=true` to overwrite.")
     328            0 :         return filepath
     329              :     end
     330              : 
     331            1 :     df = download(source; verbose=verbose, kwargs...)
     332              : 
     333            1 :     CSV.write(filepath, df)
     334            1 :     verbose && println("Saved to: $filepath")
     335              : 
     336            1 :     return filepath
     337              : end
     338              : 
     339              : """
     340              :     load_file(filename::String)
     341              : 
     342              : Load a previously downloaded FIRMS CSV file.
     343              : 
     344              : # Example
     345              : ```julia
     346              : df = FIRMS.load_file("VIIRS_NOAA20_NRT_20240115.csv")
     347              : ```
     348              : """
     349            2 : function load_file(filename::String)
     350            2 :     filepath = joinpath(dir(), filename)
     351            2 :     if !isfile(filepath)
     352            1 :         error("File not found: $filepath")
     353              :     end
     354            1 :     return CSV.read(filepath, DataFrame)
     355              : end
     356              : 
     357              : #-----------------------------------------------------------------------------# Data Availability
     358              : 
     359              : """
     360              :     data_availability(source::Symbol=:VIIRS_NOAA20_NRT)
     361              : 
     362              : Check data availability for a specific source.
     363              : 
     364              : Returns a DataFrame showing available dates and their status.
     365              : 
     366              : # Example
     367              : ```julia
     368              : FIRMS.data_availability(:VIIRS_NOAA20_NRT)
     369              : ```
     370              : """
     371            2 : function data_availability(source::Symbol=:VIIRS_NOAA20_NRT)
     372            3 :     haskey(SOURCES, source) || error("Unknown source: $source")
     373              : 
     374            2 :     key = require_map_key()
     375            1 :     source_name = SOURCES[source].name
     376            1 :     url = "$API_BASE/data_availability/csv/$key/$source_name"
     377              : 
     378            1 :     response = HTTP.get(url; status_exception=false, connect_timeout=60, readtimeout=60)
     379            1 :     if response.status != 200
     380            0 :         error("Failed to get data availability. HTTP status: $(response.status)")
     381              :     end
     382              : 
     383            1 :     return CSV.read(IOBuffer(String(response.body)), DataFrame)
     384              : end
     385              : 
     386              : #-----------------------------------------------------------------------------# Convenience Functions
     387              : 
     388              : """
     389              :     recent_fires(; source=:VIIRS_NOAA20_NRT, region=:conus, days=1, min_confidence=nothing)
     390              : 
     391              : Get recent fire detections with optional confidence filtering.
     392              : 
     393              : # Arguments
     394              : - `source::Symbol`: Data source (default: `:VIIRS_NOAA20_NRT`)
     395              : - `region::Symbol`: Geographic region (default: `:conus`)
     396              : - `days::Int`: Number of days (default: 1)
     397              : - `min_confidence::Real`: Minimum confidence value to include (source-dependent)
     398              : 
     399              : # Example
     400              : ```julia
     401              : df = FIRMS.recent_fires(region=:california, days=2)
     402              : ```
     403              : """
     404            1 : function recent_fires(; source::Symbol=:VIIRS_NOAA20_NRT, region::Symbol=:conus,
     405              :                       days::Int=1, min_confidence::Union{Real, Nothing}=nothing)
     406            1 :     df = download(source; region=region, days=days, verbose=false)
     407              : 
     408            1 :     if !isnothing(min_confidence) && "confidence" in names(df)
     409              :         # VIIRS confidence can be "l", "n", "h" (low, nominal, high) or numeric
     410              :         # MODIS confidence is 0-100
     411            0 :         if eltype(df.confidence) <: Number
     412            0 :             df = filter(row -> row.confidence >= min_confidence, df)
     413              :         end
     414              :     end
     415              : 
     416            1 :     return df
     417              : end
     418              : 
     419              : """
     420              :     hotspots_by_date(; source=:VIIRS_NOAA20_NRT, region=:conus, days=7)
     421              : 
     422              : Get fire detections grouped by date.
     423              : 
     424              : # Returns
     425              : A dictionary mapping dates to DataFrames of fire detections.
     426              : 
     427              : # Example
     428              : ```julia
     429              : by_date = FIRMS.hotspots_by_date(region=:western_us, days=5)
     430              : ```
     431              : """
     432            0 : function hotspots_by_date(; source::Symbol=:VIIRS_NOAA20_NRT, region::Symbol=:conus, days::Int=7)
     433            0 :     df = download(source; region=region, days=days, verbose=false)
     434              : 
     435            0 :     if "acq_date" in names(df)
     436            0 :         return Dict(date => filter(row -> row.acq_date == date, df) for date in unique(df.acq_date))
     437              :     else
     438            0 :         return Dict("all" => df)
     439              :     end
     440              : end
     441              : 
     442              : end # module
        

Generated by: LCOV version 2.0-1