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)
|
||||
|
||||
|
||||
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]:
|
||||
"""Detect whether a layer has a colormap.
|
||||
|
||||
GIBS layers with colormaps typically have:
|
||||
- Metadata with xlink:role containing "colormap" (includes the URL)
|
||||
- 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
|
||||
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
|
||||
# Check metadata first — the href carries the actual colormap filename
|
||||
for meta in _findall(layer_el, "Metadata", _OWS_URI):
|
||||
role = meta.get(f"{{{_XLINK_URI}}}role", "")
|
||||
href = meta.get(f"{{{_XLINK_URI}}}href", "")
|
||||
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 False, None
|
||||
|
||||
@ -209,9 +209,13 @@ def parse_colormap(xml_text: str) -> ColorMapSet:
|
||||
title = cm_elem.get("title", "")
|
||||
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] = []
|
||||
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")
|
||||
entries.append(
|
||||
ColorMapEntry(
|
||||
|
||||
34
tests/fixtures/colormap_sample.xml
vendored
34
tests/fixtures/colormap_sample.xml
vendored
@ -1,20 +1,22 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ColorMaps xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
||||
<ColorMap title="Surface Air Temperature" units="K">
|
||||
<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="150,200,230" transparent="false" sourceValue="[2,3)" value="[210.0,220.0)" ref="3"/>
|
||||
<ColorMapEntry rgb="100,180,220" transparent="false" sourceValue="[3,4)" value="[220.0,230.0)" ref="4"/>
|
||||
<ColorMapEntry rgb="80,160,200" transparent="false" sourceValue="[4,5)" value="[230.0,240.0)" ref="5"/>
|
||||
<ColorMapEntry rgb="50,140,180" transparent="false" sourceValue="[5,6)" value="[240.0,250.0)" ref="6"/>
|
||||
<ColorMapEntry rgb="100,200,100" transparent="false" sourceValue="[6,7)" value="[250.0,260.0)" ref="7"/>
|
||||
<ColorMapEntry rgb="180,220,50" transparent="false" sourceValue="[7,8)" value="[260.0,270.0)" ref="8"/>
|
||||
<ColorMapEntry rgb="255,200,50" transparent="false" sourceValue="[8,9)" value="[270.0,280.0)" ref="9"/>
|
||||
<ColorMapEntry rgb="255,150,50" transparent="false" sourceValue="[9,10)" value="[280.0,290.0)" ref="10"/>
|
||||
<ColorMapEntry rgb="255,100,50" transparent="false" sourceValue="[10,11)" value="[290.0,300.0)" ref="11"/>
|
||||
<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="251,3,207" transparent="false" sourceValue="[13,14)" value="[320.0,+INF)" ref="14"/>
|
||||
<Entries>
|
||||
<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="150,200,230" transparent="false" sourceValue="[2,3)" value="[210.0,220.0)" ref="3"/>
|
||||
<ColorMapEntry rgb="100,180,220" transparent="false" sourceValue="[3,4)" value="[220.0,230.0)" ref="4"/>
|
||||
<ColorMapEntry rgb="80,160,200" transparent="false" sourceValue="[4,5)" value="[230.0,240.0)" ref="5"/>
|
||||
<ColorMapEntry rgb="50,140,180" transparent="false" sourceValue="[5,6)" value="[240.0,250.0)" ref="6"/>
|
||||
<ColorMapEntry rgb="100,200,100" transparent="false" sourceValue="[6,7)" value="[250.0,260.0)" ref="7"/>
|
||||
<ColorMapEntry rgb="180,220,50" transparent="false" sourceValue="[7,8)" value="[260.0,270.0)" ref="8"/>
|
||||
<ColorMapEntry rgb="255,200,50" transparent="false" sourceValue="[8,9)" value="[270.0,280.0)" ref="9"/>
|
||||
<ColorMapEntry rgb="255,150,50" transparent="false" sourceValue="[9,10)" value="[280.0,290.0)" ref="10"/>
|
||||
<ColorMapEntry rgb="255,100,50" transparent="false" sourceValue="[10,11)" value="[290.0,300.0)" ref="11"/>
|
||||
<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="251,3,207" transparent="false" sourceValue="[13,14)" value="[320.0,+INF)" ref="14"/>
|
||||
</Entries>
|
||||
<Legend type="continuous">
|
||||
<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"/>
|
||||
@ -24,7 +26,9 @@
|
||||
</Legend>
|
||||
</ColorMap>
|
||||
<ColorMap>
|
||||
<ColorMapEntry rgb="0,0,0" transparent="true" sourceValue="-9999" nodata="true" label="Missing Data"/>
|
||||
<Entries>
|
||||
<ColorMapEntry rgb="0,0,0" transparent="true" sourceValue="-9999" nodata="true" label="Missing Data"/>
|
||||
</Entries>
|
||||
<Legend type="classification">
|
||||
<LegendEntry rgb="0,0,0" tooltip="Missing Data" id="1"/>
|
||||
</Legend>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user