mirror of
https://github.com/zvx-echo6/central.git
synced 2026-06-10 20:04:43 +02:00
fix(2-B): normalize WFIGS field formats
WFIGS returns ISO 3166-2 state codes (US-MT) and 2-letter incident type codes (WF, RX). Normalize at parse boundary: - normalize_state: strips US- prefix (US-MT -> MT) - normalize_incident_type: maps codes to names (WF -> wildfire) Fixes: - category was fire.incident.wf, now fire.incident.wildfire - region was US-US-MT-GLACIER, now US-MT-GLACIER Both raw and normalized values stored in event.data. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
e0ffe686ec
commit
dfad7ef45d
4 changed files with 202 additions and 61 deletions
|
|
@ -17,6 +17,35 @@ WFIGS_PERIMETERS_URL = (
|
|||
# Fall-off sweep window: 14 days (matches WFIGS's longest fall-off: large fires)
|
||||
FALLOFF_WINDOW_DAYS = 14
|
||||
|
||||
# Incident type code mappings (WFIGS uses 2-letter codes)
|
||||
INCIDENT_TYPE_MAP = {
|
||||
"WF": "wildfire",
|
||||
"RX": "prescribed_fire",
|
||||
"CX": "complex",
|
||||
"FA": "false_alarm",
|
||||
}
|
||||
|
||||
|
||||
def normalize_state(state: str | None) -> str | None:
|
||||
"""Strip 'US-' prefix from POOState (ISO 3166-2 -> 2-letter)."""
|
||||
if not state:
|
||||
return None
|
||||
if state.startswith("US-") and len(state) == 5:
|
||||
return state[3:]
|
||||
if len(state) == 2:
|
||||
return state
|
||||
return state # unknown shape, pass through
|
||||
|
||||
|
||||
def normalize_incident_type(code: str | None) -> str:
|
||||
"""Map IncidentTypeCategory code to a readable name."""
|
||||
if not code:
|
||||
return "unknown"
|
||||
upper = code.upper()
|
||||
if upper in INCIDENT_TYPE_MAP:
|
||||
return INCIDENT_TYPE_MAP[upper]
|
||||
return code.lower()
|
||||
|
||||
|
||||
def severity_from_acres(acres: float | None) -> int:
|
||||
"""Map DailyAcres to severity level 0-4."""
|
||||
|
|
@ -42,6 +71,7 @@ def build_regions(state: str | None, county: str | None) -> tuple[list[str], str
|
|||
"""
|
||||
Build geo.regions list and primary_region from POOState and POOCounty.
|
||||
|
||||
Expects normalized 2-letter state codes (e.g., "MT" not "US-MT").
|
||||
Returns (regions, primary_region).
|
||||
"""
|
||||
if not state:
|
||||
|
|
@ -62,6 +92,7 @@ def subject_suffix(state: str | None, county: str | None) -> str:
|
|||
"""
|
||||
Build subject suffix from state and county.
|
||||
|
||||
Expects normalized 2-letter state codes.
|
||||
Returns lowercase state.county (county with spaces→underscores).
|
||||
Falls back to "unknown" if state is not available.
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -25,6 +25,8 @@ from central.adapters.wfigs_common import (
|
|||
extract_centroid,
|
||||
get_observed_guids,
|
||||
init_observed_table,
|
||||
normalize_incident_type,
|
||||
normalize_state,
|
||||
parse_wfigs_timestamp,
|
||||
point_in_bbox,
|
||||
severity_from_acres,
|
||||
|
|
@ -185,6 +187,7 @@ class WFIGSIncidentsAdapter(SourceAdapter):
|
|||
return f"central.fire.incident.removed.{state}"
|
||||
|
||||
# Regular incidents: central.fire.incident.<state>.<county>
|
||||
# POOState is already normalized (2-letter code)
|
||||
state = event.data.get("POOState")
|
||||
county = event.data.get("POOCounty")
|
||||
suffix = subject_suffix(state, county)
|
||||
|
|
@ -273,17 +276,22 @@ class WFIGSIncidentsAdapter(SourceAdapter):
|
|||
):
|
||||
continue
|
||||
|
||||
# Track this GUID as observed (for fall-off detection)
|
||||
state = props.get("POOState")
|
||||
# Normalize at parse boundary
|
||||
state_raw = props.get("POOState")
|
||||
state = normalize_state(state_raw)
|
||||
county = props.get("POOCounty")
|
||||
incident_type_raw = props.get("IncidentTypeCategory")
|
||||
incident_type = normalize_incident_type(incident_type_raw)
|
||||
|
||||
# Track this GUID as observed (for fall-off detection)
|
||||
# Store normalized state for consistency
|
||||
current_guids[irwin_id] = (state, county)
|
||||
|
||||
# Parse fields
|
||||
incident_type = props.get("IncidentTypeCategory", "unknown").lower()
|
||||
discovery_time = parse_wfigs_timestamp(props.get("FireDiscoveryDateTime"))
|
||||
daily_acres = props.get("DailyAcres")
|
||||
|
||||
# Build regions
|
||||
# Build regions (expects normalized 2-letter state code)
|
||||
regions, primary_region = build_regions(state, county)
|
||||
|
||||
# Build geo
|
||||
|
|
@ -297,7 +305,7 @@ class WFIGSIncidentsAdapter(SourceAdapter):
|
|||
else:
|
||||
geo = Geo(regions=regions, primary_region=primary_region)
|
||||
|
||||
# Build event
|
||||
# Build event with normalized values in data
|
||||
event = Event(
|
||||
id=irwin_id,
|
||||
adapter=self.name,
|
||||
|
|
@ -308,12 +316,14 @@ class WFIGSIncidentsAdapter(SourceAdapter):
|
|||
data={
|
||||
"IrwinID": irwin_id,
|
||||
"IncidentName": props.get("IncidentName"),
|
||||
"IncidentTypeCategory": props.get("IncidentTypeCategory"),
|
||||
"IncidentTypeCategory": incident_type,
|
||||
"IncidentTypeCategory_raw": incident_type_raw,
|
||||
"DailyAcres": daily_acres,
|
||||
"PercentContained": props.get("PercentContained"),
|
||||
"FireDiscoveryDateTime": props.get("FireDiscoveryDateTime"),
|
||||
"ModifiedOnDateTime": props.get("ModifiedOnDateTime"),
|
||||
"POOState": state,
|
||||
"POOState_raw": state_raw,
|
||||
"POOCounty": county,
|
||||
"raw": props,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -25,6 +25,8 @@ from central.adapters.wfigs_common import (
|
|||
extract_centroid,
|
||||
get_observed_guids,
|
||||
init_observed_table,
|
||||
normalize_incident_type,
|
||||
normalize_state,
|
||||
parse_wfigs_timestamp,
|
||||
polygon_intersects_bbox,
|
||||
severity_from_acres,
|
||||
|
|
@ -185,6 +187,7 @@ class WFIGSPerimetersAdapter(SourceAdapter):
|
|||
return f"central.fire.perimeter.removed.{state}"
|
||||
|
||||
# Regular perimeters: central.fire.perimeter.<state>.<county>
|
||||
# POOState is already normalized (2-letter code)
|
||||
state = event.data.get("POOState")
|
||||
county = event.data.get("POOCounty")
|
||||
suffix = subject_suffix(state, county)
|
||||
|
|
@ -271,18 +274,23 @@ class WFIGSPerimetersAdapter(SourceAdapter):
|
|||
):
|
||||
continue
|
||||
|
||||
# Track this GUID as observed (for fall-off detection)
|
||||
state = props.get("attr_POOState")
|
||||
# Normalize at parse boundary
|
||||
state_raw = props.get("attr_POOState")
|
||||
state = normalize_state(state_raw)
|
||||
county = props.get("attr_POOCounty")
|
||||
incident_type_raw = props.get("attr_IncidentTypeCategory")
|
||||
incident_type = normalize_incident_type(incident_type_raw)
|
||||
|
||||
# Track this GUID as observed (for fall-off detection)
|
||||
# Store normalized state for consistency
|
||||
current_guids[irwin_id] = (state, county)
|
||||
|
||||
# Parse fields using prefixed names
|
||||
incident_type = props.get("attr_IncidentTypeCategory", "unknown").lower()
|
||||
discovery_time = parse_wfigs_timestamp(props.get("attr_FireDiscoveryDateTime"))
|
||||
# Use poly_GISAcres or attr_IncidentSize for acreage
|
||||
daily_acres = props.get("attr_IncidentSize") or props.get("poly_GISAcres")
|
||||
|
||||
# Build regions
|
||||
# Build regions (expects normalized 2-letter state code)
|
||||
regions, primary_region = build_regions(state, county)
|
||||
|
||||
# Extract centroid for geo
|
||||
|
|
@ -320,13 +328,15 @@ class WFIGSPerimetersAdapter(SourceAdapter):
|
|||
data={
|
||||
"IrwinID": irwin_id,
|
||||
"IncidentName": props.get("attr_IncidentName") or props.get("poly_IncidentName"),
|
||||
"IncidentTypeCategory": props.get("attr_IncidentTypeCategory"),
|
||||
"IncidentTypeCategory": incident_type,
|
||||
"IncidentTypeCategory_raw": incident_type_raw,
|
||||
"DailyAcres": props.get("attr_IncidentSize"),
|
||||
"GISAcres": props.get("poly_GISAcres"),
|
||||
"PercentContained": props.get("attr_PercentContained"),
|
||||
"FireDiscoveryDateTime": props.get("attr_FireDiscoveryDateTime"),
|
||||
"ModifiedOnDateTime": props.get("attr_ModifiedOnDateTime_dt"),
|
||||
"POOState": state,
|
||||
"POOState_raw": state_raw,
|
||||
"POOCounty": county,
|
||||
"geometry": geometry, # Full GeoJSON polygon
|
||||
"raw": props,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue