Fix colormap parsing against live GIBS API
Two bugs found via headless Claude CLI integration testing: 1. Colormap ID extraction: _detect_colormap returned the layer identifier as colormap_id, but GIBS layers often share colormaps under different filenames (e.g., AMSRU2_Sea_Ice_Concentration_12km -> colormap AMSR2_Sea_Ice_Concentration.xml). Now parses the actual filename from the metadata xlink:href. 2. Entries wrapper: GIBS v1.3 colormap XML nests ColorMapEntry elements inside an <Entries> wrapper. The parser only searched direct children of <ColorMap>, finding zero entries from live data. Now checks Entries/ColorMapEntry first, falling back to direct children. Updated test fixture to use the real <Entries> wrapper structure.
This commit is contained in:
parent
d163ade9a4
commit
31dff49351
@ -117,25 +117,44 @@ def _parse_bbox(layer_el: ET.Element) -> BBox | None:
|
|||||||
return BBox(west=west, south=south, east=east, north=north)
|
return BBox(west=west, south=south, east=east, north=north)
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_colormap_id_from_href(href: str) -> str | None:
|
||||||
|
"""Extract the colormap identifier from a GIBS colormap URL.
|
||||||
|
|
||||||
|
GIBS metadata hrefs look like:
|
||||||
|
https://gibs.earthdata.nasa.gov/colormaps/v1.3/AMSR2_Sea_Ice_Concentration.xml
|
||||||
|
|
||||||
|
Multiple resolution layers often share a single colormap file, so the
|
||||||
|
colormap filename may differ from the layer identifier.
|
||||||
|
"""
|
||||||
|
if not href:
|
||||||
|
return None
|
||||||
|
filename = href.rstrip("/").rsplit("/", 1)[-1]
|
||||||
|
if filename.endswith(".xml"):
|
||||||
|
return filename[:-4]
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _detect_colormap(layer_el: ET.Element, identifier: str) -> tuple[bool, str | None]:
|
def _detect_colormap(layer_el: ET.Element, identifier: str) -> tuple[bool, str | None]:
|
||||||
"""Detect whether a layer has a colormap.
|
"""Detect whether a layer has a colormap.
|
||||||
|
|
||||||
GIBS layers with colormaps typically have:
|
GIBS layers with colormaps typically have:
|
||||||
|
- Metadata with xlink:role containing "colormap" (includes the URL)
|
||||||
- A LegendURL element inside a Style
|
- A LegendURL element inside a Style
|
||||||
- Metadata with xlink:role containing "colormap"
|
|
||||||
|
|
||||||
Returns (has_colormap, colormap_id).
|
Returns (has_colormap, colormap_id). The colormap_id is extracted from
|
||||||
|
the metadata href when available, falling back to the layer identifier.
|
||||||
"""
|
"""
|
||||||
# Check for LegendURL in any Style
|
# Check metadata first — the href carries the actual colormap filename
|
||||||
for style in _findall(layer_el, "Style", _WMTS_URI):
|
|
||||||
if _find(style, "LegendURL", _WMTS_URI) is not None:
|
|
||||||
return True, identifier
|
|
||||||
|
|
||||||
# Check metadata for colormap hints
|
|
||||||
for meta in _findall(layer_el, "Metadata", _OWS_URI):
|
for meta in _findall(layer_el, "Metadata", _OWS_URI):
|
||||||
role = meta.get(f"{{{_XLINK_URI}}}role", "")
|
role = meta.get(f"{{{_XLINK_URI}}}role", "")
|
||||||
href = meta.get(f"{{{_XLINK_URI}}}href", "")
|
href = meta.get(f"{{{_XLINK_URI}}}href", "")
|
||||||
if "colormap" in role.lower() or "colormap" in href.lower():
|
if "colormap" in role.lower() or "colormap" in href.lower():
|
||||||
|
colormap_id = _extract_colormap_id_from_href(href) or identifier
|
||||||
|
return True, colormap_id
|
||||||
|
|
||||||
|
# Fallback: check for LegendURL in any Style
|
||||||
|
for style in _findall(layer_el, "Style", _WMTS_URI):
|
||||||
|
if _find(style, "LegendURL", _WMTS_URI) is not None:
|
||||||
return True, identifier
|
return True, identifier
|
||||||
|
|
||||||
return False, None
|
return False, None
|
||||||
|
|||||||
@ -209,9 +209,13 @@ def parse_colormap(xml_text: str) -> ColorMapSet:
|
|||||||
title = cm_elem.get("title", "")
|
title = cm_elem.get("title", "")
|
||||||
units = cm_elem.get("units", "")
|
units = cm_elem.get("units", "")
|
||||||
|
|
||||||
# Parse entries
|
# Parse entries — GIBS v1.3 wraps them in <Entries>, but some
|
||||||
|
# documents have <ColorMapEntry> as direct children of <ColorMap>.
|
||||||
entries: list[ColorMapEntry] = []
|
entries: list[ColorMapEntry] = []
|
||||||
for entry_elem in cm_elem.findall("ColorMapEntry"):
|
entry_elements = cm_elem.findall("Entries/ColorMapEntry")
|
||||||
|
if not entry_elements:
|
||||||
|
entry_elements = cm_elem.findall("ColorMapEntry")
|
||||||
|
for entry_elem in entry_elements:
|
||||||
rgb_raw = entry_elem.get("rgb", "0,0,0")
|
rgb_raw = entry_elem.get("rgb", "0,0,0")
|
||||||
entries.append(
|
entries.append(
|
||||||
ColorMapEntry(
|
ColorMapEntry(
|
||||||
|
|||||||
4
tests/fixtures/colormap_sample.xml
vendored
4
tests/fixtures/colormap_sample.xml
vendored
@ -1,6 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<ColorMaps xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
<ColorMaps xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
||||||
<ColorMap title="Surface Air Temperature" units="K">
|
<ColorMap title="Surface Air Temperature" units="K">
|
||||||
|
<Entries>
|
||||||
<ColorMapEntry rgb="227,245,255" transparent="false" sourceValue="[0,1)" value="[-INF,200.0)" ref="1"/>
|
<ColorMapEntry rgb="227,245,255" transparent="false" sourceValue="[0,1)" value="[-INF,200.0)" ref="1"/>
|
||||||
<ColorMapEntry rgb="204,234,252" transparent="false" sourceValue="[1,2)" value="[200.0,210.0)" ref="2"/>
|
<ColorMapEntry rgb="204,234,252" transparent="false" sourceValue="[1,2)" value="[200.0,210.0)" ref="2"/>
|
||||||
<ColorMapEntry rgb="150,200,230" transparent="false" sourceValue="[2,3)" value="[210.0,220.0)" ref="3"/>
|
<ColorMapEntry rgb="150,200,230" transparent="false" sourceValue="[2,3)" value="[210.0,220.0)" ref="3"/>
|
||||||
@ -15,6 +16,7 @@
|
|||||||
<ColorMapEntry rgb="220,50,30" transparent="false" sourceValue="[11,12)" value="[300.0,310.0)" ref="12"/>
|
<ColorMapEntry rgb="220,50,30" transparent="false" sourceValue="[11,12)" value="[300.0,310.0)" ref="12"/>
|
||||||
<ColorMapEntry rgb="180,20,50" transparent="false" sourceValue="[12,13)" value="[310.0,320.0)" ref="13"/>
|
<ColorMapEntry rgb="180,20,50" transparent="false" sourceValue="[12,13)" value="[310.0,320.0)" ref="13"/>
|
||||||
<ColorMapEntry rgb="251,3,207" transparent="false" sourceValue="[13,14)" value="[320.0,+INF)" ref="14"/>
|
<ColorMapEntry rgb="251,3,207" transparent="false" sourceValue="[13,14)" value="[320.0,+INF)" ref="14"/>
|
||||||
|
</Entries>
|
||||||
<Legend type="continuous">
|
<Legend type="continuous">
|
||||||
<LegendEntry rgb="227,245,255" tooltip="< 200 K" id="1"/>
|
<LegendEntry rgb="227,245,255" tooltip="< 200 K" id="1"/>
|
||||||
<LegendEntry rgb="204,234,252" tooltip="200 - 210 K" id="2" showTick="true" showLabel="true"/>
|
<LegendEntry rgb="204,234,252" tooltip="200 - 210 K" id="2" showTick="true" showLabel="true"/>
|
||||||
@ -24,7 +26,9 @@
|
|||||||
</Legend>
|
</Legend>
|
||||||
</ColorMap>
|
</ColorMap>
|
||||||
<ColorMap>
|
<ColorMap>
|
||||||
|
<Entries>
|
||||||
<ColorMapEntry rgb="0,0,0" transparent="true" sourceValue="-9999" nodata="true" label="Missing Data"/>
|
<ColorMapEntry rgb="0,0,0" transparent="true" sourceValue="-9999" nodata="true" label="Missing Data"/>
|
||||||
|
</Entries>
|
||||||
<Legend type="classification">
|
<Legend type="classification">
|
||||||
<LegendEntry rgb="0,0,0" tooltip="Missing Data" id="1"/>
|
<LegendEntry rgb="0,0,0" tooltip="Missing Data" id="1"/>
|
||||||
</Legend>
|
</Legend>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user