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

            Line data    Source code
       1              : module WildfireData
       2              : 
       3              : using Scratch
       4              : using HTTP
       5              : using JSON3
       6              : using GeoJSON
       7              : 
       8              : export WFIGS, IRWIN, FPA_FOD, MTBS, FIRMS, LANDFIRE, FEDS, CWFIS, HMS, GWIS, EGP, RxCADRE
       9              : 
      10              : #-----------------------------------------------------------------------------# Data Directory
      11           56 : dir(x...) = joinpath(Scratch.@get_scratch!("data"), x...)
      12              : 
      13              : #-----------------------------------------------------------------------------# Abstract Dataset Interface
      14              : 
      15              : """
      16              :     AbstractDataset
      17              : 
      18              : Abstract type for dataset metadata. Subtypes must have a `name::String` field
      19              : and implement `base_query_url(d)` and `base_layer_url(d)`.
      20              : """
      21              : abstract type AbstractDataset end
      22              : 
      23              : """
      24              :     base_query_url(d::AbstractDataset) -> String
      25              : 
      26              : Return the base URL for query requests (without parameters).
      27              : """
      28              : function base_query_url end
      29              : 
      30              : """
      31              :     base_layer_url(d::AbstractDataset) -> String
      32              : 
      33              : Return the base URL for layer metadata requests.
      34              : """
      35              : function base_layer_url end
      36              : 
      37              : #-----------------------------------------------------------------------------# ArcGIS Dataset
      38              : 
      39              : """
      40              :     ArcGISDataset
      41              : 
      42              : Metadata for an ArcGIS FeatureServer dataset.
      43              : 
      44              : # Fields
      45              : - `base_url::String`: Base URL for the ArcGIS service
      46              : - `service::String`: ArcGIS service name
      47              : - `layer::Int`: Layer index (usually 0)
      48              : - `name::String`: Human-readable name
      49              : - `description::String`: Dataset description
      50              : - `category::Symbol`: Category (e.g., :perimeters, :locations, :incidents, :history)
      51              : """
      52              : struct ArcGISDataset <: AbstractDataset
      53              :     base_url::String
      54              :     service::String
      55              :     layer::Int
      56              :     name::String
      57              :     description::String
      58              :     category::Symbol
      59              : end
      60              : 
      61           55 : base_query_url(d::ArcGISDataset) = "$(d.base_url)/$(d.service)/FeatureServer/$(d.layer)/query"
      62            3 : base_layer_url(d::ArcGISDataset) = "$(d.base_url)/$(d.service)/FeatureServer/$(d.layer)"
      63              : 
      64              : #-----------------------------------------------------------------------------# Common URL Builder
      65              : 
      66              : """
      67              :     query_url(d::AbstractDataset; where="1=1", outfields="*", limit=nothing, format="geojson", bbox=nothing)
      68              : 
      69              : Build a query URL for the dataset.
      70              : 
      71              : # Arguments
      72              : - `bbox`: Bounding box as `(west, south, east, north)` tuple or "west,south,east,north" string for spatial filtering.
      73              : """
      74           70 : function query_url(d::AbstractDataset; where::String="1=1", outfields::String="*",
      75              :                    limit::Union{Int,Nothing}=nothing, format::String="geojson",
      76              :                    bbox::Union{NTuple{4,Real},String,Nothing}=nothing)
      77           35 :     url = base_query_url(d)
      78           35 :     params = [
      79              :         "where" => HTTP.escapeuri(where),
      80              :         "outFields" => outfields,
      81              :         "f" => format,
      82              :         "outSR" => "4326",
      83              :     ]
      84           35 :     if !isnothing(limit)
      85           44 :         push!(params, "resultRecordCount" => string(limit))
      86              :     end
      87           35 :     if !isnothing(bbox)
      88            0 :         envelope = bbox isa String ? bbox : join(bbox, ",")
      89            0 :         push!(params, "geometry" => envelope)
      90            0 :         push!(params, "geometryType" => "esriGeometryEnvelope")
      91            0 :         push!(params, "inSR" => "4326")
      92            0 :         push!(params, "spatialRel" => "esriSpatialRelIntersects")
      93              :     end
      94           35 :     return url * "?" * join(["$k=$v" for (k, v) in params], "&")
      95              : end
      96              : 
      97              : #-----------------------------------------------------------------------------# Common API Functions
      98              : 
      99              : """
     100              :     _datasets(all_datasets::Dict{Symbol,<:AbstractDataset}; category=nothing)
     101              : 
     102              : Filter datasets by category. Used internally by submodules.
     103              : """
     104           14 : function _datasets(all_datasets::Dict{Symbol,<:AbstractDataset}; category::Union{Symbol,Nothing}=nothing)
     105           14 :     if isnothing(category)
     106            7 :         return all_datasets
     107              :     else
     108            7 :         return Dict(k => v for (k, v) in all_datasets if v.category == category)
     109              :     end
     110              : end
     111              : 
     112              : """
     113              :     _info(all_datasets::Dict{Symbol,<:AbstractDataset}, dataset::Symbol, module_name::String)
     114              : 
     115              : Print information about a specific dataset. Used internally by submodules.
     116              : """
     117            6 : function _info(all_datasets::Dict{Symbol,<:AbstractDataset}, dataset::Symbol, module_name::String)
     118           12 :     if !haskey(all_datasets, dataset)
     119            3 :         error("Unknown dataset: $dataset. Use `$module_name.datasets()` to list available datasets.")
     120              :     end
     121            3 :     d = all_datasets[dataset]
     122            3 :     println("Dataset: ", d.name)
     123            3 :     println("Category: ", d.category)
     124            3 :     println("Description: ", d.description)
     125            3 :     println("Service: ", d.service)
     126            3 :     println("Query URL: ", query_url(d))
     127            3 :     return nothing
     128              : end
     129              : 
     130              : """
     131              :     _download(all_datasets::Dict{Symbol,<:AbstractDataset}, dataset::Symbol, module_name::String; kwargs...)
     132              : 
     133              : Download a dataset and return it as a GeoJSON FeatureCollection. Used internally by submodules.
     134              : """
     135           44 : function _download(all_datasets::Dict{Symbol,<:AbstractDataset}, dataset::Symbol, module_name::String;
     136              :                    where::String="1=1", fields::String="*", limit::Union{Int,Nothing}=nothing,
     137              :                    bbox::Union{NTuple{4,Real},String,Nothing}=nothing, verbose::Bool=true)
     138           44 :     if !haskey(all_datasets, dataset)
     139            4 :         error("Unknown dataset: $dataset. Use `$module_name.datasets()` to list available datasets.")
     140              :     end
     141              : 
     142           18 :     d = all_datasets[dataset]
     143           18 :     url = query_url(d; where=where, outfields=fields, limit=limit, bbox=bbox)
     144              : 
     145           18 :     verbose && println("Downloading: $(d.name)")
     146           18 :     verbose && println("URL: $url")
     147              : 
     148           18 :     response = HTTP.get(url; status_exception=false, connect_timeout=60, readtimeout=60)
     149              : 
     150           18 :     if response.status != 200
     151            0 :         error("Failed to download dataset. HTTP status: $(response.status)\nResponse: $(String(response.body))")
     152              :     end
     153              : 
     154           18 :     verbose && println("Parsing GeoJSON...")
     155           18 :     body = String(response.body)
     156              : 
     157              :     # Check for ArcGIS error response (avoid full double-parse)
     158           36 :     if contains(body, "\"error\"")
     159            0 :         json_data = JSON3.read(body)
     160            0 :         if haskey(json_data, :error)
     161            0 :             error("ArcGIS API error: $(json_data.error)")
     162              :         end
     163              :     end
     164              : 
     165              :     # Parse as GeoJSON
     166           18 :     data = GeoJSON.read(body)
     167              : 
     168           18 :     n = length(data)
     169           18 :     verbose && println("Downloaded $n features")
     170           36 :     if contains(body, "exceededTransferLimit") && contains(body, "true")
     171           18 :         verbose && println("⚠ Warning: Transfer limit exceeded. Use `limit` parameter or refine `where` clause to get all data.")
     172              :     end
     173              : 
     174           18 :     return data
     175              : end
     176              : 
     177              : """
     178              :     _download_file(all_datasets, dataset, module_name, data_dir; kwargs...)
     179              : 
     180              : Download a dataset and save it to the local data directory. Used internally by submodules.
     181              : """
     182            8 : function _download_file(all_datasets::Dict{Symbol,<:AbstractDataset}, dataset::Symbol, module_name::String, data_dir::String;
     183              :                         filename::Union{String,Nothing}=nothing, force::Bool=false, verbose::Bool=true, kwargs...)
     184            8 :     if !haskey(all_datasets, dataset)
     185            0 :         error("Unknown dataset: $dataset. Use `$module_name.datasets()` to list available datasets.")
     186              :     end
     187              : 
     188            4 :     mkpath(data_dir)
     189              : 
     190            4 :     if isnothing(filename)
     191            4 :         filename = string(dataset) * ".geojson"
     192              :     end
     193            4 :     filepath = joinpath(data_dir, filename)
     194              : 
     195            4 :     if isfile(filepath) && !force
     196            0 :         verbose && println("File already exists: $filepath")
     197            0 :         verbose && println("Use `force=true` to overwrite.")
     198            0 :         return filepath
     199              :     end
     200              : 
     201            4 :     data = _download(all_datasets, dataset, module_name; verbose=verbose, kwargs...)
     202              : 
     203            8 :     open(filepath, "w") do io
     204            4 :         JSON3.write(io, data)
     205              :     end
     206            4 :     verbose && println("Saved to: $filepath")
     207              : 
     208            4 :     return filepath
     209              : end
     210              : 
     211              : """
     212              :     _load_file(data_dir, dataset; filename=nothing)
     213              : 
     214              : Load a previously downloaded dataset from the local data directory. Used internally by submodules.
     215              : """
     216           16 : function _load_file(data_dir::String, dataset::Symbol; filename::Union{String,Nothing}=nothing)
     217            8 :     if isnothing(filename)
     218            8 :         filename = string(dataset) * ".geojson"
     219              :     end
     220            8 :     filepath = joinpath(data_dir, filename)
     221              : 
     222            8 :     if !isfile(filepath)
     223            4 :         error("File not found: $filepath. Download the dataset first.")
     224              :     end
     225              : 
     226            4 :     return GeoJSON.read(read(filepath, String))
     227              : end
     228              : 
     229              : """
     230              :     _count(all_datasets::Dict{Symbol,<:AbstractDataset}, dataset::Symbol, module_name::String; where="1=1")
     231              : 
     232              : Get the count of features in a dataset. Used internally by submodules.
     233              : """
     234           24 : function _count(all_datasets::Dict{Symbol,<:AbstractDataset}, dataset::Symbol, module_name::String; where::String="1=1")
     235           24 :     if !haskey(all_datasets, dataset)
     236            4 :         error("Unknown dataset: $dataset. Use `$module_name.datasets()` to list available datasets.")
     237              :     end
     238              : 
     239            8 :     d = all_datasets[dataset]
     240            8 :     params = [
     241              :         "where" => HTTP.escapeuri(where),
     242              :         "returnCountOnly" => "true",
     243              :         "f" => "json",
     244              :     ]
     245           16 :     full_url = base_query_url(d) * "?" * join(["$k=$v" for (k, v) in params], "&")
     246              : 
     247            8 :     response = HTTP.get(full_url; status_exception=false, connect_timeout=60, readtimeout=60)
     248            8 :     if response.status != 200
     249            0 :         error("Failed to get count. HTTP status: $(response.status)")
     250              :     end
     251              : 
     252            8 :     data = JSON3.read(response.body)
     253            8 :     return data.count
     254              : end
     255              : 
     256              : """
     257              :     _fields(all_datasets::Dict{Symbol,<:AbstractDataset}, dataset::Symbol, module_name::String)
     258              : 
     259              : Get the field names and types for a dataset. Used internally by submodules.
     260              : """
     261            9 : function _fields(all_datasets::Dict{Symbol,<:AbstractDataset}, dataset::Symbol, module_name::String)
     262           18 :     if !haskey(all_datasets, dataset)
     263            4 :         error("Unknown dataset: $dataset. Use `$module_name.datasets()` to list available datasets.")
     264              :     end
     265              : 
     266            5 :     d = all_datasets[dataset]
     267            5 :     url = base_layer_url(d) * "?f=json"
     268              : 
     269            5 :     response = HTTP.get(url; status_exception=false, connect_timeout=60, readtimeout=60)
     270            5 :     if response.status != 200
     271            0 :         error("Failed to get field info. HTTP status: $(response.status)")
     272              :     end
     273              : 
     274            5 :     data = JSON3.read(response.body)
     275              : 
     276            5 :     if haskey(data, :fields)
     277            5 :         return [(name=f.name, type=f.type, alias=get(f, :alias, f.name)) for f in data.fields]
     278              :     else
     279            0 :         error("Could not retrieve field information for this dataset.")
     280              :     end
     281              : end
     282              : 
     283              : #-----------------------------------------------------------------------------# Submodules
     284              : include("WFIGS.jl")
     285              : include("IRWIN.jl")
     286              : include("FPA_FOD.jl")
     287              : include("MTBS.jl")
     288              : include("FIRMS.jl")
     289              : include("LANDFIRE.jl")
     290              : include("FEDS.jl")
     291              : include("CWFIS.jl")
     292              : include("HMS.jl")
     293              : include("GWIS.jl")
     294              : include("EGP.jl")
     295              : include("RxCADRE.jl")
     296              : 
     297              : end
        

Generated by: LCOV version 2.0-1