Skip to content

Instantly share code, notes, and snippets.

@michaelaye
Last active December 18, 2025 00:44
Show Gist options
  • Select an option

  • Save michaelaye/5ddb550bf50727ee7a0d0f7f91aebd57 to your computer and use it in GitHub Desktop.

Select an option

Save michaelaye/5ddb550bf50727ee7a0d0f7f91aebd57 to your computer and use it in GitHub Desktop.
#!/usr/bin/env python3
"""
Extract polygon footprint from ISIS cube file and convert to GML or GeoPackage format.
Requires: pvl, geopandas, shapely
"""
import argparse
import sys
import geopandas as gpd
import pvl
from shapely import wkt
from shapely.geometry import MultiPolygon, Polygon
def find_footprint_location(cub_file):
"""Find the footprint polygon location from the PVL label.
Returns tuple (start_byte, num_bytes) or None if not found.
"""
label = pvl.load(cub_file)
# Search for Polygon object with Name="Footprint"
if isinstance(label, dict):
# PVL might return a dict or a list of objects
objects = label.get("Object", [])
if not isinstance(objects, list):
objects = [objects]
else:
# If label is a list or other structure
objects = label if isinstance(label, list) else [label]
# First try direct search through objects
for obj in objects:
if isinstance(obj, dict) and obj.get("Name") == "Footprint":
start_byte = obj.get("StartByte")
num_bytes = obj.get("Bytes")
if start_byte is not None and num_bytes is not None:
return int(start_byte), int(num_bytes)
# If not found in direct search, try recursive search through entire label
def find_footprint_recursive(data):
"""Recursively search for Footprint object in PVL structure."""
if isinstance(data, dict):
# Check if this dict has Name="Footprint" and required fields
if (
data.get("Name") == "Footprint"
and "StartByte" in data
and "Bytes" in data
):
return data
# Recursively search all values
for value in data.values():
result = find_footprint_recursive(value)
if result:
return result
elif isinstance(data, list):
# Search through list items
for item in data:
result = find_footprint_recursive(item)
if result:
return result
return None
footprint_obj = find_footprint_recursive(label)
if footprint_obj:
start_byte = footprint_obj.get("StartByte")
num_bytes = footprint_obj.get("Bytes")
if start_byte is not None and num_bytes is not None:
return int(start_byte), int(num_bytes)
return None
def extract_polygon_wkt(cub_file, start_byte, num_bytes):
"""Extract WKT polygon text from cube file.
Note: PVL StartByte uses 1-based indexing (first byte is 1), while Python's
f.seek() uses 0-based indexing (first byte is 0). Therefore, we subtract 1
from the PVL StartByte value before using f.seek().
Args:
cub_file: Path to the ISIS cube file
start_byte: Start byte position from PVL (1-based)
num_bytes: Number of bytes to read
"""
with open(cub_file, "rb") as f:
# Convert PVL 1-based indexing to Python 0-based indexing
# PVL StartByte = 1 means first byte, f.seek(0) means first byte
seek_position = start_byte - 1
f.seek(seek_position)
data = f.read(num_bytes)
wkt_text = data.decode("ascii", errors="replace").strip()
return wkt_text
def parse_wkt_polygon(wkt_text):
"""Parse WKT polygon string and extract coordinates.
Supports both POLYGON and MULTIPOLYGON formats.
MULTIPOLYGON format: MULTIPOLYGON (((lon lat, ...)), ((lon lat, ...)), ...)
POLYGON format: POLYGON ((lon lat, lon lat, ...))
Returns a list of polygons, where each polygon is a list of (lon, lat) tuples.
For POLYGON, returns a list with one polygon.
For MULTIPOLYGON, returns a list with all polygons.
"""
wkt_text = wkt_text.strip()
# Parse using Shapely's WKT parser
geom = wkt.loads(wkt_text)
polygons = []
if geom.geom_type == "Polygon":
# Single polygon
coords = list(geom.exterior.coords)
polygons.append(coords)
elif geom.geom_type == "MultiPolygon":
# Multiple polygons
for poly in geom.geoms:
coords = list(poly.exterior.coords)
polygons.append(coords)
else:
raise ValueError(f"Unsupported geometry type: {geom.geom_type}")
return polygons
def create_geodataframe(polygons, crs="EPSG:4326"):
"""Create a GeoDataFrame from polygons.
Args:
polygons: List of polygons, where each polygon is a list of (lon, lat) tuples
crs: Coordinate reference system
Returns:
GeoDataFrame
"""
# Create Shapely geometry from polygons
if len(polygons) == 1:
# Single polygon
# Note: coordinates are already closed (first == last)
geometry = Polygon(polygons[0])
else:
# Multiple polygons - create MultiPolygon
shapely_polygons = [Polygon(coords) for coords in polygons]
geometry = MultiPolygon(shapely_polygons)
# Create GeoDataFrame
gdf = gpd.GeoDataFrame(
[{"id": 1, "name": "footprint", "geometry": geometry}], crs=crs
)
return gdf
def create_geopackage(polygons, output_file, layer_name="footprint", crs="EPSG:4326"):
"""Create a GeoPackage file from polygons using geopandas exporter.
Args:
polygons: List of polygons, where each polygon is a list of (lon, lat) tuples
output_file: Output file path
layer_name: Layer name for GeoPackage
crs: Coordinate reference system
Returns:
GeoDataFrame
"""
gdf = create_geodataframe(polygons, crs=crs)
# Update layer name if needed
if layer_name != "footprint":
gdf["name"] = layer_name
# Write to GeoPackage using geopandas exporter
gdf.to_file(output_file, driver="GPKG", layer=layer_name)
return gdf
def create_gml(polygons, output_file, crs="EPSG:4326"):
"""Create a GML file from polygons using geopandas exporter.
Args:
polygons: List of polygons, where each polygon is a list of (lon, lat) tuples
output_file: Output file path
crs: Coordinate reference system
Returns:
GeoDataFrame
"""
gdf = create_geodataframe(polygons, crs=crs)
# Write to GML using geopandas exporter
gdf.to_file(output_file, driver="GML")
return gdf
if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="Extract polygon footprint from ISIS cube file and convert to GML or GeoPackage format."
)
parser.add_argument(
"--format",
choices=["gml", "gpkg", "both"],
default="gml",
help="Output format: gml, gpkg, or both (default: gml). Both formats use geopandas exporters.",
)
parser.add_argument(
"--output", type=str, help="Output file path (default: based on format)"
)
parser.add_argument(
"--cub-file",
type=str,
default="hirise40.cal.map.cub",
help="Input ISIS cube file (default: hirise40.cal.map.cub)",
)
parser.add_argument(
"--start-byte",
type=int,
default=None,
help="Start byte offset (overrides PVL label). Note: If WKT starts with 'ULTIPOLYGON', the script will automatically include the preceding 'M'.",
)
parser.add_argument(
"--num-bytes",
type=int,
default=None,
help="Number of bytes to read (overrides PVL label)",
)
parser.add_argument(
"--layer-name",
type=str,
default="footprint",
help="Layer name for GeoPackage (default: footprint)",
)
parser.add_argument(
"--crs",
type=str,
default="EPSG:4326",
help="CRS for GeoPackage (default: EPSG:4326)",
)
args = parser.parse_args()
cub_file = args.cub_file
# Try to parse footprint location from PVL label if not provided
if args.start_byte is None or args.num_bytes is None:
print("Parsing PVL label to find footprint location...")
footprint_location = find_footprint_location(cub_file)
if footprint_location:
pvl_start_byte, pvl_num_bytes = footprint_location
print("Found footprint in PVL label:")
print(f" StartByte: {pvl_start_byte}")
print(f" Bytes: {pvl_num_bytes}")
start_byte = (
args.start_byte if args.start_byte is not None else pvl_start_byte
)
num_bytes = args.num_bytes if args.num_bytes is not None else pvl_num_bytes
else:
if args.start_byte is None or args.num_bytes is None:
print("Error: Could not find footprint location in PVL label.")
print("Please provide --start-byte and --num-bytes arguments.")
sys.exit(1)
start_byte = args.start_byte
num_bytes = args.num_bytes
else:
start_byte = args.start_byte
num_bytes = args.num_bytes
print()
print("Extracting polygon from ISIS cube file...")
print(f"File: {cub_file}")
print(f"Start byte: {start_byte}")
print(f"Number of bytes: {num_bytes}")
print()
# Extract WKT text
wkt_text = extract_polygon_wkt(cub_file, start_byte, num_bytes)
print(f"WKT text (first 200 chars): {wkt_text[:200]}...")
print(f"WKT text (last 200 chars): ...{wkt_text[-200:]}")
print()
# Parse coordinates
print("Parsing coordinates...")
polygons = parse_wkt_polygon(wkt_text)
print(f"Found {len(polygons)} polygon(s) in MULTIPOLYGON")
for i, poly_coords in enumerate(polygons):
print(f" Polygon {i + 1}: {len(poly_coords)} coordinate pairs")
print(f" First coordinate: {poly_coords[0]}")
print(f" Last coordinate: {poly_coords[-1]}")
print(f" Closed: {poly_coords[0] == poly_coords[-1]}")
print()
# Determine output format(s)
formats_to_create = []
if args.format == "both":
formats_to_create = ["gml", "gpkg"]
else:
formats_to_create = [args.format]
# Create GML if requested
if "gml" in formats_to_create:
output_file = args.output or "hirise40_footprint.gml"
print("Creating GML file...")
gdf = create_gml(polygons, output_file, crs=args.crs)
print(f"Successfully created {output_file}")
print(f" Geometry type: {gdf.geometry.iloc[0].geom_type}")
if len(polygons) > 1:
print(f" Contains {len(polygons)} polygons")
# Create GeoPackage if requested
if "gpkg" in formats_to_create:
output_file = args.output or "hirise40_footprint.gpkg"
print("Creating GeoPackage...")
gdf = create_geopackage(
polygons, output_file, layer_name=args.layer_name, crs=args.crs
)
print(f"Successfully created {output_file}")
print(f" Layer: {args.layer_name}")
print(f" CRS: {args.crs}")
print(f" Geometry type: {gdf.geometry.iloc[0].geom_type}")
if len(polygons) > 1:
print(f" Contains {len(polygons)} polygons")
total_coords = sum(len(poly) for poly in polygons)
print(f"\nTotal coordinates across all polygons: {total_coords}")
Display the source blob
Display the rendered blob
Raw
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Extracting Polygon Footprint from ISIS Cube File\n",
"\n",
"This notebook demonstrates the step-by-step process of extracting a polygon footprint from an ISIS cube file and converting it to GML or GeoPackage format.\n",
"\n",
"**Note**: This notebook assumes a full geospatial Python stack is available (shapely, geopandas, matplotlib, etc.).\n",
"\n",
"## Overview\n",
"\n",
"The polygon footprint location is stored in the PVL (Planetary Data System Label) header of the .cub file. We'll parse the PVL label to find:\n",
"- **Object Name**: Polygon\n",
"- **Name**: Footprint\n",
"- **StartByte**: Location in the file (parsed from PVL)\n",
"- **Bytes**: Size of the footprint data (parsed from PVL)\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Step 1: Parse PVL Label and Extract Footprint Location\n",
"\n",
"First, we'll parse the PVL label from the beginning of the cube file to find the footprint location.\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Parsing PVL label to find footprint location...\n",
"File: hirise40.cal.map.cub\n",
"\n",
"✓ Found footprint in PVL label (via recursive search):\n",
" StartByte: 225143647\n",
" Bytes: 16318\n",
"\n",
"⚠️ Important: The StartByte points to after the 'M' in MULTIPOLYGON\n",
" We need to check one byte before to get the complete WKT string.\n",
"\n"
]
},
{
"data": {
"text/plain": [
"225143646"
]
},
"execution_count": 8,
"metadata": {},
"output_type": "execute_result"
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Byte at position 225143646: 4d = 'M'\n",
"✓ Found 'M' - starting from one byte earlier to include it\n"
]
},
{
"data": {
"text/plain": [
"225143646"
]
},
"execution_count": 8,
"metadata": {},
"output_type": "execute_result"
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"\n",
"Extracted 16318 bytes of data\n",
"First 100 bytes (hex): 4d554c5449504f4c59474f4e2028282833302e313037353938343335383836363038202d34302e33353639363137373731323030392c2033302e313038363235383735373635303433202d34302e3335363836383136373135353939342c2033302e3130\n"
]
}
],
"source": [
"import pvl\n",
"\n",
"cub_file = \"hirise40.cal.map.cub\"\n",
"\n",
"# Parse PVL label to find footprint location\n",
"print(\"Parsing PVL label to find footprint location...\")\n",
"print(f\"File: {cub_file}\\n\")\n",
"\n",
"# Load PVL label using pvl library\n",
"label = pvl.load(cub_file)\n",
"\n",
"# Search for Polygon object with Name=\"Footprint\"\n",
"# PVL structure may vary, so we need to handle different formats\n",
"footprint_found = False\n",
"\n",
"# Handle different PVL structures (dict or list)\n",
"if isinstance(label, dict):\n",
" # PVL might return a dict with \"Object\" key containing a list or single object\n",
" objects = label.get(\"Object\", [])\n",
" if not isinstance(objects, list):\n",
" objects = [objects]\n",
"else:\n",
" # If label is a list or other structure\n",
" objects = label if isinstance(label, list) else [label]\n",
"\n",
"# Search through objects for Footprint\n",
"# The pvl library structures PVL objects as dicts where the object type\n",
"# might be stored as \"Object\" = \"Polygon\" and attributes like \"Name\" = \"Footprint\"\n",
"for obj in objects:\n",
" if isinstance(obj, dict):\n",
" # Check if this is a Polygon object with Name=\"Footprint\"\n",
" # The pvl library may store \"Object\" = \"Polygon\" as a key-value pair\n",
" if obj.get(\"Name\") == \"Footprint\":\n",
" start_byte = obj.get(\"StartByte\")\n",
" num_bytes = obj.get(\"Bytes\")\n",
" if start_byte is not None and num_bytes is not None:\n",
" start_byte = int(start_byte)\n",
" num_bytes = int(num_bytes)\n",
" print(\"✓ Found footprint in PVL label:\")\n",
" print(f\" StartByte: {start_byte}\")\n",
" print(f\" Bytes: {num_bytes}\")\n",
" footprint_found = True\n",
" break\n",
"\n",
"# If not found in the direct search, try recursive search through the entire label\n",
"if not footprint_found:\n",
"\n",
" def find_footprint_recursive(data, path=\"\"):\n",
" \"\"\"Recursively search for Footprint object in PVL structure.\"\"\"\n",
" if isinstance(data, dict):\n",
" # Check if this dict has Name=\"Footprint\" and required fields\n",
" if (\n",
" data.get(\"Name\") == \"Footprint\"\n",
" and \"StartByte\" in data\n",
" and \"Bytes\" in data\n",
" ):\n",
" return data\n",
" # Recursively search all values\n",
" for key, value in data.items():\n",
" result = find_footprint_recursive(\n",
" value, f\"{path}.{key}\" if path else key\n",
" )\n",
" if result:\n",
" return result\n",
" elif isinstance(data, list):\n",
" # Search through list items\n",
" for i, item in enumerate(data):\n",
" result = find_footprint_recursive(\n",
" item, f\"{path}[{i}]\" if path else f\"[{i}]\"\n",
" )\n",
" if result:\n",
" return result\n",
" return None\n",
"\n",
" footprint_obj = find_footprint_recursive(label)\n",
" if footprint_obj:\n",
" start_byte = int(footprint_obj.get(\"StartByte\"))\n",
" num_bytes = int(footprint_obj.get(\"Bytes\"))\n",
" print(\"✓ Found footprint in PVL label (via recursive search):\")\n",
" print(f\" StartByte: {start_byte}\")\n",
" print(f\" Bytes: {num_bytes}\")\n",
" footprint_found = True\n",
"\n",
"if not footprint_found:\n",
" raise ValueError(\n",
" \"Could not find footprint location in PVL label (Object = Polygon, Name = Footprint)\"\n",
" )\n",
"\n",
"print(\n",
" \"\\nℹ️ Important: PVL StartByte uses 1-based indexing, while Python's f.seek() uses 0-based indexing.\"\n",
")\n",
"print(\n",
" \" We convert PVL StartByte to 0-based by subtracting 1 before using f.seek().\\n\"\n",
")\n",
"\n",
"# Convert PVL 1-based indexing to Python 0-based indexing\n",
"# PVL StartByte = 1 means first byte, f.seek(0) means first byte\n",
"seek_position = start_byte - 1\n",
"\n",
"# Read the data\n",
"with open(cub_file, \"rb\") as f:\n",
" f.seek(seek_position)\n",
" data = f.read(num_bytes)\n",
"\n",
"print(f\"\\nExtracted {len(data)} bytes of data\")\n",
"print(f\"First 100 bytes (hex): {data[:100].hex()}\")\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Step 2: Discover the Data Format\n",
"\n",
"Let's examine the first bytes to understand the data format. We'll try interpreting it as binary data first.\n"
]
},
{
"cell_type": "code",
"execution_count": 3,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Attempting to interpret as binary data...\n",
"\n",
"First 8 bytes as double (big-endian):\n",
" Value: 3.50462211659709e+64\n",
"(This doesn't look like a coordinate!)\n",
"\n",
"First 100 bytes as ASCII text:\n",
" MULTIPOLYGON (((30.107598435886608 -40.35696177712009, 30.108625875765043 -40.356868167155994, 30.10\n",
"\n",
"✓ The data appears to be ASCII text, not binary!\n"
]
}
],
"source": [
"import struct\n",
"\n",
"print(\"Attempting to interpret as binary data...\")\n",
"print(\"\\nFirst 8 bytes as double (big-endian):\")\n",
"if len(data) >= 8:\n",
" val = struct.unpack(\">d\", data[:8])[0]\n",
" print(f\" Value: {val}\")\n",
" print(\"(This doesn't look like a coordinate!)\")\n",
"\n",
"print(\"\\nFirst 100 bytes as ASCII text:\")\n",
"try:\n",
" text_preview = data[:100].decode(\"ascii\", errors=\"replace\")\n",
" print(f\" {text_preview}\")\n",
" print(\"\\n✓ The data appears to be ASCII text, not binary!\")\n",
"except:\n",
" print(\" Could not decode as ASCII\")\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Step 3: Decode as ASCII Text (WKT Format)\n",
"\n",
"The data is stored as ASCII text in WKT (Well-Known Text) format. Let's decode it completely.\n"
]
},
{
"cell_type": "code",
"execution_count": 4,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Full WKT text length: 16318 characters\n",
"\n",
"First 300 characters:\n",
"MULTIPOLYGON (((30.107598435886608 -40.35696177712009, 30.108625875765043 -40.356868167155994, 30.109720318260305 -40.3567745571919, 30.110803590153807 -40.3566809472278, 30.111898026586665 -40.356587337263704, 30.112992459980585 -40.35649372729961, 30.113439048963176 -40.3572936669928, 30.113561778\n",
"\n",
"...\n",
"\n",
"Last 300 characters:\n",
"053835 -40.36203373517487, 30.10822289921157 -40.361199755494724, 30.10810023278538 -40.360357265817846, 30.107977565653698 -40.35953179613444, 30.107854901522746 -40.35870632645101, 30.107732240392313 -40.35788085676761, 30.10760958226222 -40.35705538708419, 30.107598435886608 -40.35696177712009)))\n",
"\n",
"✓ Format identified: WKT (Well-Known Text) polygon format\n",
" ✓ Correctly starts with 'MULTIPOLYGON'\n",
" ℹ️ MULTIPOLYGON is a valid WKT geometry type (OGC standard)\n",
" This footprint is stored as MULTIPOLYGON containing a single polygon\n",
" Structure: MULTIPOLYGON (((coordinates))) - three levels of parentheses\n"
]
}
],
"source": [
"wkt_text = data.decode(\"ascii\", errors=\"replace\").strip()\n",
"\n",
"print(f\"Full WKT text length: {len(wkt_text)} characters\")\n",
"print(f\"\\nFirst 300 characters:\")\n",
"print(wkt_text[:300])\n",
"print(f\"\\n...\")\n",
"print(f\"\\nLast 300 characters:\")\n",
"print(wkt_text[-300:])\n",
"\n",
"print(\"\\n✓ Format identified: WKT (Well-Known Text) polygon format\")\n",
"if wkt_text.startswith(\"MULTIPOLYGON\"):\n",
" print(\" ✓ Correctly starts with 'MULTIPOLYGON'\")\n",
" print(\" ℹ️ MULTIPOLYGON is a valid WKT geometry type (OGC standard)\")\n",
" print(\" This footprint is stored as MULTIPOLYGON containing a single polygon\")\n",
" print(\n",
" \" Structure: MULTIPOLYGON (((coordinates))) - three levels of parentheses\"\n",
" )\n",
"elif wkt_text.startswith(\"ULTIPOLYGON\"):\n",
" print(\" ⚠️ Starts with 'ULTIPOLYGON' - missing the 'M'!\")\n",
" print(\" This means we need to read from one byte earlier (start_byte - 1)\")\n",
"else:\n",
" print(f\" Starts with: {wkt_text[:20]}\")\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Step 4: Parse WKT to Extract Coordinates\n",
"\n",
"Now we'll parse the WKT text using Shapely's WKT parser, which handles MULTIPOLYGON correctly and is more robust than manual parsing.\n"
]
},
{
"cell_type": "code",
"execution_count": 5,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"✓ Parsed MultiPolygon with 1 polygon(s)\n",
" Polygon 1: 420 coordinate pairs\n",
"\n",
" First coordinate: (30.107598435886608, -40.35696177712009)\n",
" Last coordinate: (30.107598435886608, -40.35696177712009)\n",
" Polygon is closed: True\n"
]
}
],
"source": [
"from shapely import wkt\n",
"\n",
"# Parse WKT using Shapely's robust parser\n",
"geom = wkt.loads(wkt_text)\n",
"\n",
"if geom.geom_type == \"Polygon\":\n",
" coords = list(geom.exterior.coords)\n",
" print(f\"✓ Parsed Polygon with {len(coords)} coordinate pairs\")\n",
"elif geom.geom_type == \"MultiPolygon\":\n",
" print(f\"✓ Parsed MultiPolygon with {len(geom.geoms)} polygon(s)\")\n",
" # For demonstration, we'll work with all polygons\n",
" polygons = []\n",
" for i, poly in enumerate(geom.geoms):\n",
" poly_coords = list(poly.exterior.coords)\n",
" polygons.append(poly_coords)\n",
" print(f\" Polygon {i + 1}: {len(poly_coords)} coordinate pairs\")\n",
" # Use first polygon for subsequent steps\n",
" coords = polygons[0]\n",
"else:\n",
" raise ValueError(f\"Unsupported geometry type: {geom.geom_type}\")\n",
"\n",
"print(f\"\\n First coordinate: {coords[0]}\")\n",
"print(f\" Last coordinate: {coords[-1]}\")\n",
"print(f\" Polygon is closed: {coords[0] == coords[-1]}\")\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Step 5: Analyze Coordinate Range\n",
"\n",
"Let's examine the spatial extent of the polygon.\n"
]
},
{
"cell_type": "code",
"execution_count": 6,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Coordinate Statistics:\n",
" Longitude range: 30.1075984359 to 30.1380679991 degrees\n",
" Latitude range: -40.5260899229 to -40.3564937273 degrees\n",
" Longitude span: 0.0304695632 degrees\n",
" Latitude span: 0.1695961956 degrees\n",
"\n",
" This appears to be a small region on Mars\n",
" (Longitude ~30°E, Latitude ~40°S)\n"
]
}
],
"source": [
"lons = [c[0] for c in coords]\n",
"lats = [c[1] for c in coords]\n",
"\n",
"print(\"Coordinate Statistics:\")\n",
"print(f\" Longitude range: {min(lons):.10f} to {max(lons):.10f} degrees\")\n",
"print(f\" Latitude range: {min(lats):.10f} to {max(lats):.10f} degrees\")\n",
"print(f\" Longitude span: {max(lons) - min(lons):.10f} degrees\")\n",
"print(f\" Latitude span: {max(lats) - min(lats):.10f} degrees\")\n",
"print(f\"\\n This appears to be a small region on Mars\")\n",
"print(f\" (Longitude ~30°E, Latitude ~40°S)\")\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Step 6: Visualize the Polygon\n",
"\n",
"Visualize the polygon using geopandas' built-in plotting capabilities.\n"
]
},
{
"cell_type": "code",
"execution_count": 7,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"Text(0.5, 58.7222222222222, 'Longitude (degrees)')"
]
},
"execution_count": 7,
"metadata": {},
"output_type": "execute_result"
},
{
"data": {
"text/plain": [
"Text(394.7332674541019, 0.5, 'Latitude (degrees)')"
]
},
"execution_count": 7,
"metadata": {},
"output_type": "execute_result"
},
{
"data": {
"text/plain": [
"Text(0.5, 1.0, 'HiRISE Footprint Polygon')"
]
},
"execution_count": 7,
"metadata": {},
"output_type": "execute_result"
},
{
"data": {
"image/png": "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",
"text/plain": [
"<Figure size 1000x800 with 1 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"✓ Polygon visualized\n",
" Area: 0.0009798887 square degrees\n",
" Perimeter: 0.3524344117 degrees\n",
" Contains 1 polygons\n"
]
}
],
"source": [
"import geopandas as gpd\n",
"import matplotlib.pyplot as plt\n",
"\n",
"# Use the geometry we already parsed from WKT\n",
"# If it's a MultiPolygon, use it directly; otherwise create Polygon\n",
"if geom.geom_type == \"MultiPolygon\":\n",
" geometry = geom\n",
"else:\n",
" from shapely.geometry import Polygon\n",
"\n",
" geometry = Polygon(coords)\n",
"\n",
"# Create GeoDataFrame for plotting\n",
"gdf_plot = gpd.GeoDataFrame([{\"geometry\": geometry}], crs=\"EPSG:4326\")\n",
"\n",
"# Plot using geopandas' built-in plotter\n",
"ax = gdf_plot.plot(\n",
" figsize=(10, 8), color=\"blue\", alpha=0.3, edgecolor=\"blue\", linewidth=2\n",
")\n",
"ax.set_xlabel(\"Longitude (degrees)\")\n",
"ax.set_ylabel(\"Latitude (degrees)\")\n",
"ax.set_title(\"HiRISE Footprint Polygon\")\n",
"ax.grid(True, alpha=0.3)\n",
"plt.tight_layout()\n",
"plt.show()\n",
"\n",
"print(f\"✓ Polygon visualized\")\n",
"print(f\" Area: {geometry.area:.10f} square degrees\")\n",
"print(f\" Perimeter: {geometry.length:.10f} degrees\")\n",
"if geom.geom_type == \"MultiPolygon\":\n",
" print(f\" Contains {len(geom.geoms)} polygons\")\n"
]
},
{
"cell_type": "code",
"execution_count": 7,
"metadata": {},
"outputs": [
{
"data": {
"text/html": [
"<script type=\"esms-options\">{\"shimMode\": true}</script><style>*[data-root-id],\n",
"*[data-root-id] > * {\n",
" box-sizing: border-box;\n",
" font-family: var(--jp-ui-font-family);\n",
" font-size: var(--jp-ui-font-size1);\n",
" color: var(--vscode-editor-foreground, var(--jp-ui-font-color1));\n",
"}\n",
"\n",
"/* Override VSCode background color */\n",
".cell-output-ipywidget-background:has(\n",
" > .cell-output-ipywidget-background > .lm-Widget > *[data-root-id]\n",
"),\n",
".cell-output-ipywidget-background:has(> .lm-Widget > *[data-root-id]) {\n",
" background-color: transparent !important;\n",
"}\n",
"</style>"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"application/javascript": "(function(root) {\n function now() {\n return new Date();\n }\n\n const force = true;\n const version = '3.8.1'.replace('rc', '-rc.').replace('.dev', '-dev.');\n const reloading = false;\n const Bokeh = root.Bokeh;\n const BK_RE = /^https:\\/\\/cdn\\.bokeh\\.org\\/bokeh\\/(release|dev)\\/bokeh-/;\n const PN_RE = /^https:\\/\\/cdn\\.holoviz\\.org\\/panel\\/[^/]+\\/dist\\/panel/i;\n\n // Set a timeout for this load but only if we are not already initializing\n if (typeof (root._bokeh_timeout) === \"undefined\" || (force || !root._bokeh_is_initializing)) {\n root._bokeh_timeout = Date.now() + 5000;\n root._bokeh_failed_load = false;\n }\n\n function run_callbacks() {\n try {\n root._bokeh_onload_callbacks.forEach(function(callback) {\n if (callback != null)\n callback();\n });\n } finally {\n delete root._bokeh_onload_callbacks;\n }\n console.debug(\"Bokeh: all callbacks have finished\");\n }\n\n function load_libs(css_urls, js_urls, js_modules, js_exports, Bokeh, callback) {\n if (css_urls == null) css_urls = [];\n if (js_urls == null) js_urls = [];\n if (js_modules == null) js_modules = [];\n if (js_exports == null) js_exports = {};\n\n root._bokeh_onload_callbacks.push(callback);\n\n if (root._bokeh_is_loading > 0) {\n // Don't load bokeh if it is still initializing\n console.debug(\"Bokeh: BokehJS is being loaded, scheduling callback at\", now());\n return null;\n } else if (js_urls.length === 0 && js_modules.length === 0 && Object.keys(js_exports).length === 0) {\n // There is nothing to load\n run_callbacks();\n return null;\n }\n\n function on_load() {\n root._bokeh_is_loading--;\n if (root._bokeh_is_loading === 0) {\n console.debug(\"Bokeh: all BokehJS libraries/stylesheets loaded\");\n run_callbacks()\n }\n }\n window._bokeh_on_load = on_load\n\n function on_error(e) {\n const src_el = e.srcElement\n console.error(\"failed to load \" + (src_el.href || src_el.src));\n }\n\n const skip = [];\n if (window.requirejs) {\n window.requirejs.config({'packages': {}, 'paths': {}, 'shim': {}});\n root._bokeh_is_loading = css_urls.length + 0;\n } else {\n root._bokeh_is_loading = css_urls.length + js_urls.length + js_modules.length + Object.keys(js_exports).length;\n }\n\n const existing_stylesheets = []\n const links = document.getElementsByTagName('link')\n for (let i = 0; i < links.length; i++) {\n const link = links[i]\n if (link.href != null) {\n existing_stylesheets.push(link.href)\n }\n }\n for (let i = 0; i < css_urls.length; i++) {\n const url = css_urls[i];\n const escaped = encodeURI(url)\n if (existing_stylesheets.indexOf(escaped) !== -1) {\n on_load()\n continue;\n }\n const element = document.createElement(\"link\");\n element.onload = on_load;\n element.onerror = on_error;\n element.rel = \"stylesheet\";\n element.type = \"text/css\";\n element.href = url;\n console.debug(\"Bokeh: injecting link tag for BokehJS stylesheet: \", url);\n document.body.appendChild(element);\n } var existing_scripts = []\n const scripts = document.getElementsByTagName('script')\n for (let i = 0; i < scripts.length; i++) {\n var script = scripts[i]\n if (script.src != null) {\n existing_scripts.push(script.src)\n }\n }\n for (let i = 0; i < js_urls.length; i++) {\n const url = js_urls[i];\n const escaped = encodeURI(url)\n const shouldSkip = skip.includes(escaped) || existing_scripts.includes(escaped)\n const isBokehOrPanel = BK_RE.test(escaped) || PN_RE.test(escaped)\n const missingOrBroken = Bokeh == null || Bokeh.Panel == null || (Bokeh.version != version && !Bokeh.versions?.has(version)) || Bokeh.versions?.get(version).Panel == null;\n if (shouldSkip && !(isBokehOrPanel && missingOrBroken)) {\n if (!window.requirejs) {\n on_load();\n }\n continue;\n }\n const element = document.createElement('script');\n element.onload = on_load;\n element.onerror = on_error;\n element.async = false;\n element.src = url;\n console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n document.head.appendChild(element);\n }\n for (let i = 0; i < js_modules.length; i++) {\n const url = js_modules[i];\n const escaped = encodeURI(url)\n if (skip.indexOf(escaped) !== -1 || existing_scripts.indexOf(escaped) !== -1) {\n if (!window.requirejs) {\n on_load();\n }\n continue;\n }\n var element = document.createElement('script');\n element.onload = on_load;\n element.onerror = on_error;\n element.async = false;\n element.src = url;\n element.type = \"module\";\n console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n document.head.appendChild(element);\n }\n for (const name in js_exports) {\n const url = js_exports[name];\n const escaped = encodeURI(url)\n if (skip.indexOf(escaped) >= 0 || root[name] != null) {\n if (!window.requirejs) {\n on_load();\n }\n continue;\n }\n var element = document.createElement('script');\n element.onerror = on_error;\n element.async = false;\n element.type = \"module\";\n console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n element.textContent = `\n import ${name} from \"${url}\"\n window.${name} = ${name}\n window._bokeh_on_load()\n `\n document.head.appendChild(element);\n }\n if (!js_urls.length && !js_modules.length) {\n on_load()\n }\n };\n\n function inject_raw_css(css) {\n const element = document.createElement(\"style\");\n element.appendChild(document.createTextNode(css));\n document.body.appendChild(element);\n }\n\n const js_urls = [\"https://cdn.holoviz.org/panel/1.8.4/dist/bundled/reactiveesm/es-module-shims@^1.10.0/dist/es-module-shims.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-3.8.1.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-gl-3.8.1.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-widgets-3.8.1.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-tables-3.8.1.min.js\", \"https://cdn.holoviz.org/panel/1.8.4/dist/panel.min.js\"];\n const js_modules = [];\n const js_exports = {};\n const css_urls = [];\n const inline_js = [ function(Bokeh) {\n Bokeh.set_log_level(\"info\");\n },\nfunction(Bokeh) {} // ensure no trailing comma for IE\n ];\n\n function run_inline_js() {\n if ((root.Bokeh !== undefined) || (force === true)) {\n for (let i = 0; i < inline_js.length; i++) {\n try {\n inline_js[i].call(root, root.Bokeh);\n } catch(e) {\n if (!reloading) {\n throw e;\n }\n }\n }\n } else if (Date.now() < root._bokeh_timeout) {\n setTimeout(run_inline_js, 100);\n } else if (!root._bokeh_failed_load) {\n console.log(\"Bokeh: BokehJS failed to load within specified timeout.\");\n root._bokeh_failed_load = true;\n }\n root._bokeh_is_initializing = false;\n }\n\n function load_or_wait() {\n // Implement a backoff loop that tries to ensure we do not load multiple\n // versions of Bokeh and its dependencies at the same time.\n // In recent versions we use the root._bokeh_is_initializing flag\n // to determine whether there is an ongoing attempt to initialize\n // bokeh, however for backward compatibility we also try to ensure\n // that we do not start loading a newer (Panel>=1.0 and Bokeh>3) version\n // before older versions are fully initialized.\n if (root._bokeh_is_initializing && Date.now() > root._bokeh_timeout) {\n // If the timeout and bokeh was not successfully loaded we reset\n // everything and try loading again\n root._bokeh_timeout = Date.now() + 5000;\n root._bokeh_is_initializing = false;\n root._bokeh_onload_callbacks = undefined;\n root._bokeh_is_loading = 0;\n console.log(\"Bokeh: BokehJS was loaded multiple times but one version failed to initialize.\");\n load_or_wait();\n } else if (root._bokeh_is_initializing || (typeof root._bokeh_is_initializing === \"undefined\" && root._bokeh_onload_callbacks !== undefined)) {\n setTimeout(load_or_wait, 100);\n } else {\n root._bokeh_is_initializing = true;\n root._bokeh_onload_callbacks = [];\n const bokeh_loaded = Bokeh != null && ((Bokeh.version === version && Bokeh.Panel) || (Bokeh.versions?.has(version) && Bokeh.versions.get(version).Panel));\n if (!reloading && !bokeh_loaded) {\n if (root.Bokeh) {\n root.Bokeh = undefined;\n }\n console.debug(\"Bokeh: BokehJS not loaded, scheduling load and callback at\", now());\n }\n load_libs(css_urls, js_urls, js_modules, js_exports, Bokeh, function() {\n console.debug(\"Bokeh: BokehJS plotting callback run at\", now());\n run_inline_js();\n if (Bokeh != undefined && !reloading) {\n const NewBokeh = root.Bokeh;\n if (Bokeh.versions === undefined) {\n Bokeh.versions = new Map();\n }\n if (NewBokeh.version !== Bokeh.version) {\n Bokeh[NewBokeh.version] = NewBokeh;\n Bokeh.versions.set(NewBokeh.version, NewBokeh);\n }\n root.Bokeh = Bokeh;\n }\n });\n }\n }\n // Give older versions of the autoload script a head-start to ensure\n // they initialize before we start loading newer version.\n setTimeout(load_or_wait, 100)\n}(window));",
"application/vnd.holoviews_load.v0+json": ""
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"application/javascript": "\nif ((window.PyViz === undefined) || (window.PyViz instanceof HTMLElement)) {\n window.PyViz = {comms: {}, comm_status:{}, kernels:{}, receivers: {}, plot_index: []}\n}\n\n\n function JupyterCommManager() {\n }\n\n JupyterCommManager.prototype.register_target = function(plot_id, comm_id, msg_handler) {\n if (window.comm_manager || ((window.Jupyter !== undefined) && (Jupyter.notebook.kernel != null))) {\n var comm_manager = window.comm_manager || Jupyter.notebook.kernel.comm_manager;\n comm_manager.register_target(comm_id, function(comm) {\n comm.on_msg(msg_handler);\n });\n } else if ((plot_id in window.PyViz.kernels) && (window.PyViz.kernels[plot_id])) {\n window.PyViz.kernels[plot_id].registerCommTarget(comm_id, function(comm) {\n comm.onMsg = msg_handler;\n });\n } else if (typeof google != 'undefined' && google.colab.kernel != null) {\n google.colab.kernel.comms.registerTarget(comm_id, (comm) => {\n var messages = comm.messages[Symbol.asyncIterator]();\n function processIteratorResult(result) {\n var message = result.value;\n var content = {data: message.data, comm_id};\n var buffers = []\n for (var buffer of message.buffers || []) {\n buffers.push(new DataView(buffer))\n }\n var metadata = message.metadata || {};\n var msg = {content, buffers, metadata}\n msg_handler(msg);\n return messages.next().then(processIteratorResult);\n }\n return messages.next().then(processIteratorResult);\n })\n }\n }\n\n JupyterCommManager.prototype.get_client_comm = function(plot_id, comm_id, msg_handler) {\n if (comm_id in window.PyViz.comms) {\n return window.PyViz.comms[comm_id];\n } else if (window.comm_manager || ((window.Jupyter !== undefined) && (Jupyter.notebook.kernel != null))) {\n var comm_manager = window.comm_manager || Jupyter.notebook.kernel.comm_manager;\n var comm = comm_manager.new_comm(comm_id, {}, {}, {}, comm_id);\n if (msg_handler) {\n comm.on_msg(msg_handler);\n }\n } else if ((plot_id in window.PyViz.kernels) && (window.PyViz.kernels[plot_id])) {\n var comm = window.PyViz.kernels[plot_id].connectToComm(comm_id);\n let retries = 0;\n const open = () => {\n if (comm.active) {\n comm.open();\n } else if (retries > 3) {\n console.warn('Comm target never activated')\n } else {\n retries += 1\n setTimeout(open, 500)\n }\n }\n if (comm.active) {\n comm.open();\n } else {\n setTimeout(open, 500)\n }\n if (msg_handler) {\n comm.onMsg = msg_handler;\n }\n } else if (typeof google != 'undefined' && google.colab.kernel != null) {\n var comm_promise = google.colab.kernel.comms.open(comm_id)\n comm_promise.then((comm) => {\n window.PyViz.comms[comm_id] = comm;\n if (msg_handler) {\n var messages = comm.messages[Symbol.asyncIterator]();\n function processIteratorResult(result) {\n var message = result.value;\n var content = {data: message.data};\n var metadata = message.metadata || {comm_id};\n var msg = {content, metadata}\n msg_handler(msg);\n return messages.next().then(processIteratorResult);\n }\n return messages.next().then(processIteratorResult);\n }\n })\n var sendClosure = (data, metadata, buffers, disposeOnDone) => {\n return comm_promise.then((comm) => {\n comm.send(data, metadata, buffers, disposeOnDone);\n });\n };\n var comm = {\n send: sendClosure\n };\n }\n window.PyViz.comms[comm_id] = comm;\n return comm;\n }\n window.PyViz.comm_manager = new JupyterCommManager();\n \n\n\nvar JS_MIME_TYPE = 'application/javascript';\nvar HTML_MIME_TYPE = 'text/html';\nvar EXEC_MIME_TYPE = 'application/vnd.holoviews_exec.v0+json';\nvar CLASS_NAME = 'output';\n\n/**\n * Render data to the DOM node\n */\nfunction render(props, node) {\n var div = document.createElement(\"div\");\n var script = document.createElement(\"script\");\n node.appendChild(div);\n node.appendChild(script);\n}\n\n/**\n * Handle when a new output is added\n */\nfunction handle_add_output(event, handle) {\n var output_area = handle.output_area;\n var output = handle.output;\n if ((output.data == undefined) || (!output.data.hasOwnProperty(EXEC_MIME_TYPE))) {\n return\n }\n var id = output.metadata[EXEC_MIME_TYPE][\"id\"];\n var toinsert = output_area.element.find(\".\" + CLASS_NAME.split(' ')[0]);\n if (id !== undefined) {\n var nchildren = toinsert.length;\n var html_node = toinsert[nchildren-1].children[0];\n html_node.innerHTML = output.data[HTML_MIME_TYPE];\n var scripts = [];\n var nodelist = html_node.querySelectorAll(\"script\");\n for (var i in nodelist) {\n if (nodelist.hasOwnProperty(i)) {\n scripts.push(nodelist[i])\n }\n }\n\n scripts.forEach( function (oldScript) {\n var newScript = document.createElement(\"script\");\n var attrs = [];\n var nodemap = oldScript.attributes;\n for (var j in nodemap) {\n if (nodemap.hasOwnProperty(j)) {\n attrs.push(nodemap[j])\n }\n }\n attrs.forEach(function(attr) { newScript.setAttribute(attr.name, attr.value) });\n newScript.appendChild(document.createTextNode(oldScript.innerHTML));\n oldScript.parentNode.replaceChild(newScript, oldScript);\n });\n if (JS_MIME_TYPE in output.data) {\n toinsert[nchildren-1].children[1].textContent = output.data[JS_MIME_TYPE];\n }\n output_area._hv_plot_id = id;\n if ((window.Bokeh !== undefined) && (id in Bokeh.index)) {\n window.PyViz.plot_index[id] = Bokeh.index[id];\n } else {\n window.PyViz.plot_index[id] = null;\n }\n } else if (output.metadata[EXEC_MIME_TYPE][\"server_id\"] !== undefined) {\n var bk_div = document.createElement(\"div\");\n bk_div.innerHTML = output.data[HTML_MIME_TYPE];\n var script_attrs = bk_div.children[0].attributes;\n for (var i = 0; i < script_attrs.length; i++) {\n toinsert[toinsert.length - 1].childNodes[1].setAttribute(script_attrs[i].name, script_attrs[i].value);\n }\n // store reference to server id on output_area\n output_area._bokeh_server_id = output.metadata[EXEC_MIME_TYPE][\"server_id\"];\n }\n}\n\n/**\n * Handle when an output is cleared or removed\n */\nfunction handle_clear_output(event, handle) {\n var id = handle.cell.output_area._hv_plot_id;\n var server_id = handle.cell.output_area._bokeh_server_id;\n if (((id === undefined) || !(id in PyViz.plot_index)) && (server_id !== undefined)) { return; }\n var comm = window.PyViz.comm_manager.get_client_comm(\"hv-extension-comm\", \"hv-extension-comm\", function () {});\n if (server_id !== null) {\n comm.send({event_type: 'server_delete', 'id': server_id});\n return;\n } else if (comm !== null) {\n comm.send({event_type: 'delete', 'id': id});\n }\n delete PyViz.plot_index[id];\n if ((window.Bokeh !== undefined) & (id in window.Bokeh.index)) {\n var doc = window.Bokeh.index[id].model.document\n doc.clear();\n const i = window.Bokeh.documents.indexOf(doc);\n if (i > -1) {\n window.Bokeh.documents.splice(i, 1);\n }\n }\n}\n\n/**\n * Handle kernel restart event\n */\nfunction handle_kernel_cleanup(event, handle) {\n delete PyViz.comms[\"hv-extension-comm\"];\n window.PyViz.plot_index = {}\n}\n\n/**\n * Handle update_display_data messages\n */\nfunction handle_update_output(event, handle) {\n handle_clear_output(event, {cell: {output_area: handle.output_area}})\n handle_add_output(event, handle)\n}\n\nfunction register_renderer(events, OutputArea) {\n function append_mime(data, metadata, element) {\n // create a DOM node to render to\n var toinsert = this.create_output_subarea(\n metadata,\n CLASS_NAME,\n EXEC_MIME_TYPE\n );\n this.keyboard_manager.register_events(toinsert);\n // Render to node\n var props = {data: data, metadata: metadata[EXEC_MIME_TYPE]};\n render(props, toinsert[0]);\n element.append(toinsert);\n return toinsert\n }\n\n events.on('output_added.OutputArea', handle_add_output);\n events.on('output_updated.OutputArea', handle_update_output);\n events.on('clear_output.CodeCell', handle_clear_output);\n events.on('delete.Cell', handle_clear_output);\n events.on('kernel_ready.Kernel', handle_kernel_cleanup);\n\n OutputArea.prototype.register_mime_type(EXEC_MIME_TYPE, append_mime, {\n safe: true,\n index: 0\n });\n}\n\nif (window.Jupyter !== undefined) {\n try {\n var events = require('base/js/events');\n var OutputArea = require('notebook/js/outputarea').OutputArea;\n if (OutputArea.prototype.mime_types().indexOf(EXEC_MIME_TYPE) == -1) {\n register_renderer(events, OutputArea);\n }\n } catch(err) {\n }\n}\n",
"application/vnd.holoviews_load.v0+json": ""
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"application/vnd.holoviews_exec.v0+json": "",
"text/html": [
"<div id='7131f068-4797-4dcf-a567-e060d2aa4932'>\n",
" <div id=\"e4680793-c9cf-43ce-9771-1bd353b5d516\" data-root-id=\"7131f068-4797-4dcf-a567-e060d2aa4932\" style=\"display: contents;\"></div>\n",
"</div>\n",
"<script type=\"application/javascript\">(function(root) {\n",
" var docs_json = {\"cda10539-c797-4b1e-ba09-87e162ac175a\":{\"version\":\"3.8.1\",\"title\":\"Bokeh Application\",\"config\":{\"type\":\"object\",\"name\":\"DocumentConfig\",\"id\":\"10715739-82ce-4c07-8fd7-d1884c300c19\",\"attributes\":{\"notifications\":{\"type\":\"object\",\"name\":\"Notifications\",\"id\":\"0ccf28bc-53f8-448d-8204-8fc201b58e38\"}}},\"roots\":[{\"type\":\"object\",\"name\":\"panel.models.browser.BrowserInfo\",\"id\":\"7131f068-4797-4dcf-a567-e060d2aa4932\"},{\"type\":\"object\",\"name\":\"panel.models.comm_manager.CommManager\",\"id\":\"e256e8ca-ab2f-480c-ab90-9bebce1c171c\",\"attributes\":{\"plot_id\":\"7131f068-4797-4dcf-a567-e060d2aa4932\",\"comm_id\":\"53d1d4170b134a08b97ffe6f1b9cc595\",\"client_comm_id\":\"4beb2cc2c2a64dd4bf982b6b4e411f52\"}}],\"defs\":[{\"type\":\"model\",\"name\":\"ReactiveHTML1\"},{\"type\":\"model\",\"name\":\"FlexBox1\",\"properties\":[{\"name\":\"align_content\",\"kind\":\"Any\",\"default\":\"flex-start\"},{\"name\":\"align_items\",\"kind\":\"Any\",\"default\":\"flex-start\"},{\"name\":\"flex_direction\",\"kind\":\"Any\",\"default\":\"row\"},{\"name\":\"flex_wrap\",\"kind\":\"Any\",\"default\":\"wrap\"},{\"name\":\"gap\",\"kind\":\"Any\",\"default\":\"\"},{\"name\":\"justify_content\",\"kind\":\"Any\",\"default\":\"flex-start\"}]},{\"type\":\"model\",\"name\":\"FloatPanel1\",\"properties\":[{\"name\":\"config\",\"kind\":\"Any\",\"default\":{\"type\":\"map\"}},{\"name\":\"contained\",\"kind\":\"Any\",\"default\":true},{\"name\":\"position\",\"kind\":\"Any\",\"default\":\"right-top\"},{\"name\":\"offsetx\",\"kind\":\"Any\",\"default\":null},{\"name\":\"offsety\",\"kind\":\"Any\",\"default\":null},{\"name\":\"theme\",\"kind\":\"Any\",\"default\":\"primary\"},{\"name\":\"status\",\"kind\":\"Any\",\"default\":\"normalized\"}]},{\"type\":\"model\",\"name\":\"GridStack1\",\"properties\":[{\"name\":\"ncols\",\"kind\":\"Any\",\"default\":null},{\"name\":\"nrows\",\"kind\":\"Any\",\"default\":null},{\"name\":\"allow_resize\",\"kind\":\"Any\",\"default\":true},{\"name\":\"allow_drag\",\"kind\":\"Any\",\"default\":true},{\"name\":\"state\",\"kind\":\"Any\",\"default\":[]}]},{\"type\":\"model\",\"name\":\"drag1\",\"properties\":[{\"name\":\"slider_width\",\"kind\":\"Any\",\"default\":5},{\"name\":\"slider_color\",\"kind\":\"Any\",\"default\":\"black\"},{\"name\":\"start\",\"kind\":\"Any\",\"default\":0},{\"name\":\"end\",\"kind\":\"Any\",\"default\":100},{\"name\":\"value\",\"kind\":\"Any\",\"default\":50}]},{\"type\":\"model\",\"name\":\"click1\",\"properties\":[{\"name\":\"terminal_output\",\"kind\":\"Any\",\"default\":\"\"},{\"name\":\"debug_name\",\"kind\":\"Any\",\"default\":\"\"},{\"name\":\"clears\",\"kind\":\"Any\",\"default\":0}]},{\"type\":\"model\",\"name\":\"ReactiveESM1\",\"properties\":[{\"name\":\"esm_constants\",\"kind\":\"Any\",\"default\":{\"type\":\"map\"}}]},{\"type\":\"model\",\"name\":\"JSComponent1\",\"properties\":[{\"name\":\"esm_constants\",\"kind\":\"Any\",\"default\":{\"type\":\"map\"}}]},{\"type\":\"model\",\"name\":\"ReactComponent1\",\"properties\":[{\"name\":\"use_shadow_dom\",\"kind\":\"Any\",\"default\":true},{\"name\":\"esm_constants\",\"kind\":\"Any\",\"default\":{\"type\":\"map\"}}]},{\"type\":\"model\",\"name\":\"AnyWidgetComponent1\",\"properties\":[{\"name\":\"use_shadow_dom\",\"kind\":\"Any\",\"default\":true},{\"name\":\"esm_constants\",\"kind\":\"Any\",\"default\":{\"type\":\"map\"}}]},{\"type\":\"model\",\"name\":\"FastWrapper1\",\"properties\":[{\"name\":\"object\",\"kind\":\"Any\",\"default\":null},{\"name\":\"style\",\"kind\":\"Any\",\"default\":null}]},{\"type\":\"model\",\"name\":\"NotificationArea1\",\"properties\":[{\"name\":\"js_events\",\"kind\":\"Any\",\"default\":{\"type\":\"map\"}},{\"name\":\"max_notifications\",\"kind\":\"Any\",\"default\":5},{\"name\":\"notifications\",\"kind\":\"Any\",\"default\":[]},{\"name\":\"position\",\"kind\":\"Any\",\"default\":\"bottom-right\"},{\"name\":\"_clear\",\"kind\":\"Any\",\"default\":0},{\"name\":\"types\",\"kind\":\"Any\",\"default\":[{\"type\":\"map\",\"entries\":[[\"type\",\"warning\"],[\"background\",\"#ffc107\"],[\"icon\",{\"type\":\"map\",\"entries\":[[\"className\",\"fas fa-exclamation-triangle\"],[\"tagName\",\"i\"],[\"color\",\"white\"]]}]]},{\"type\":\"map\",\"entries\":[[\"type\",\"info\"],[\"background\",\"#007bff\"],[\"icon\",{\"type\":\"map\",\"entries\":[[\"className\",\"fas fa-info-circle\"],[\"tagName\",\"i\"],[\"color\",\"white\"]]}]]}]}]},{\"type\":\"model\",\"name\":\"Notification\",\"properties\":[{\"name\":\"background\",\"kind\":\"Any\",\"default\":null},{\"name\":\"duration\",\"kind\":\"Any\",\"default\":3000},{\"name\":\"icon\",\"kind\":\"Any\",\"default\":null},{\"name\":\"message\",\"kind\":\"Any\",\"default\":\"\"},{\"name\":\"notification_type\",\"kind\":\"Any\",\"default\":null},{\"name\":\"_rendered\",\"kind\":\"Any\",\"default\":false},{\"name\":\"_destroyed\",\"kind\":\"Any\",\"default\":false}]},{\"type\":\"model\",\"name\":\"TemplateActions1\",\"properties\":[{\"name\":\"open_modal\",\"kind\":\"Any\",\"default\":0},{\"name\":\"close_modal\",\"kind\":\"Any\",\"default\":0}]},{\"type\":\"model\",\"name\":\"BootstrapTemplateActions1\",\"properties\":[{\"name\":\"open_modal\",\"kind\":\"Any\",\"default\":0},{\"name\":\"close_modal\",\"kind\":\"Any\",\"default\":0}]},{\"type\":\"model\",\"name\":\"TemplateEditor1\",\"properties\":[{\"name\":\"layout\",\"kind\":\"Any\",\"default\":[]}]},{\"type\":\"model\",\"name\":\"MaterialTemplateActions1\",\"properties\":[{\"name\":\"open_modal\",\"kind\":\"Any\",\"default\":0},{\"name\":\"close_modal\",\"kind\":\"Any\",\"default\":0}]},{\"type\":\"model\",\"name\":\"request_value1\",\"properties\":[{\"name\":\"fill\",\"kind\":\"Any\",\"default\":\"none\"},{\"name\":\"_synced\",\"kind\":\"Any\",\"default\":null},{\"name\":\"_request_sync\",\"kind\":\"Any\",\"default\":0}]},{\"type\":\"model\",\"name\":\"holoviews.plotting.bokeh.raster.HoverModel\",\"properties\":[{\"name\":\"xy\",\"kind\":\"Any\",\"default\":null},{\"name\":\"data\",\"kind\":\"Any\",\"default\":null}]}]}};\n",
" var render_items = [{\"docid\":\"cda10539-c797-4b1e-ba09-87e162ac175a\",\"roots\":{\"7131f068-4797-4dcf-a567-e060d2aa4932\":\"e4680793-c9cf-43ce-9771-1bd353b5d516\"},\"root_ids\":[\"7131f068-4797-4dcf-a567-e060d2aa4932\"]}];\n",
" var docs = Object.values(docs_json)\n",
" if (!docs) {\n",
" return\n",
" }\n",
" const version = docs[0].version.replace('rc', '-rc.').replace('.dev', '-dev.')\n",
" async function embed_document(root) {\n",
" var Bokeh = get_bokeh(root)\n",
" await Bokeh.embed.embed_items_notebook(docs_json, render_items);\n",
" for (const render_item of render_items) {\n",
" for (const root_id of render_item.root_ids) {\n",
"\tconst id_el = document.getElementById(root_id)\n",
"\tif (id_el.children.length && id_el.children[0].hasAttribute('data-root-id')) {\n",
"\t const root_el = id_el.children[0]\n",
"\t root_el.id = root_el.id + '-rendered'\n",
"\t for (const child of root_el.children) {\n",
" // Ensure JupyterLab does not capture keyboard shortcuts\n",
" // see: https://jupyterlab.readthedocs.io/en/4.1.x/extension/notebook.html#keyboard-interaction-model\n",
"\t child.setAttribute('data-lm-suppress-shortcuts', 'true')\n",
"\t }\n",
"\t}\n",
" }\n",
" }\n",
" }\n",
" function get_bokeh(root) {\n",
" if (root.Bokeh === undefined) {\n",
" return null\n",
" } else if (root.Bokeh.version !== version) {\n",
" if (root.Bokeh.versions === undefined || !root.Bokeh.versions.has(version)) {\n",
"\treturn null\n",
" }\n",
" return root.Bokeh.versions.get(version);\n",
" } else if (root.Bokeh.version === version) {\n",
" return root.Bokeh\n",
" }\n",
" return null\n",
" }\n",
" function is_loaded(root) {\n",
" var Bokeh = get_bokeh(root)\n",
" return (Bokeh != null && Bokeh.Panel !== undefined)\n",
" }\n",
" if (is_loaded(root)) {\n",
" embed_document(root);\n",
" } else {\n",
" var attempts = 0;\n",
" var timer = setInterval(function(root) {\n",
" if (is_loaded(root)) {\n",
" clearInterval(timer);\n",
" embed_document(root);\n",
" } else if (document.readyState == \"complete\") {\n",
" attempts++;\n",
" if (attempts > 200) {\n",
" clearInterval(timer);\n",
"\t var Bokeh = get_bokeh(root)\n",
"\t if (Bokeh == null || Bokeh.Panel == null) {\n",
" console.warn(\"Panel: ERROR: Unable to run Panel code because Bokeh or Panel library is missing\");\n",
"\t } else {\n",
"\t console.warn(\"Panel: WARNING: Attempting to render but not all required libraries could be resolved.\")\n",
"\t embed_document(root)\n",
"\t }\n",
" }\n",
" }\n",
" }, 25, root)\n",
" }\n",
"})(window);</script>"
]
},
"metadata": {
"application/vnd.holoviews_exec.v0+json": {
"id": "7131f068-4797-4dcf-a567-e060d2aa4932"
}
},
"output_type": "display_data"
}
],
"source": [
"import hvplot.pandas"
]
},
{
"cell_type": "code",
"execution_count": 15,
"metadata": {},
"outputs": [
{
"data": {},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"application/vnd.holoviews_exec.v0+json": "",
"text/html": [
"<div id='48f34b9c-8595-427a-a149-0deb12f5318c'>\n",
" <div id=\"aeffe791-c23c-4ad5-9be8-0f6f86fc10c7\" data-root-id=\"48f34b9c-8595-427a-a149-0deb12f5318c\" style=\"display: contents;\"></div>\n",
"</div>\n",
"<script type=\"application/javascript\">(function(root) {\n",
" var docs_json = {\"b5bf2de9-b75d-4556-9356-b32868916f06\":{\"version\":\"3.8.1\",\"title\":\"Bokeh Application\",\"config\":{\"type\":\"object\",\"name\":\"DocumentConfig\",\"id\":\"27584c74-e307-460c-8a8b-69e54930d4ce\",\"attributes\":{\"notifications\":{\"type\":\"object\",\"name\":\"Notifications\",\"id\":\"c33226b2-5a0e-4156-a913-2f5da6152918\"}}},\"roots\":[{\"type\":\"object\",\"name\":\"Row\",\"id\":\"48f34b9c-8595-427a-a149-0deb12f5318c\",\"attributes\":{\"name\":\"Row00681\",\"tags\":[\"embedded\"],\"stylesheets\":[\"\\n:host(.pn-loading):before, .pn-loading:before {\\n background-color: #c3c3c3;\\n mask-size: auto calc(min(50%, 300px));\\n -webkit-mask-size: auto calc(min(50%, 300px));\\n}\",{\"type\":\"object\",\"name\":\"ImportedStyleSheet\",\"id\":\"5d278abd-ffb0-4950-9c04-af4b48c85d17\",\"attributes\":{\"url\":\"https://cdn.holoviz.org/panel/1.8.4/dist/css/loading.css\"}},{\"type\":\"object\",\"name\":\"ImportedStyleSheet\",\"id\":\"d4002de6-9543-46fd-afe6-74ff503c73a0\",\"attributes\":{\"url\":\"https://cdn.holoviz.org/panel/1.8.4/dist/css/listpanel.css\"}},{\"type\":\"object\",\"name\":\"ImportedStyleSheet\",\"id\":\"ce0d0f22-d8c7-4927-be56-06a6be3798f0\",\"attributes\":{\"url\":\"https://cdn.holoviz.org/panel/1.8.4/dist/bundled/theme/default.css\"}},{\"type\":\"object\",\"name\":\"ImportedStyleSheet\",\"id\":\"c6edb713-0c24-4693-831b-a534f5f66265\",\"attributes\":{\"url\":\"https://cdn.holoviz.org/panel/1.8.4/dist/bundled/theme/native.css\"}}],\"min_width\":0,\"margin\":0,\"sizing_mode\":\"stretch_width\",\"align\":\"start\",\"children\":[{\"type\":\"object\",\"name\":\"Spacer\",\"id\":\"97e25f5a-5382-426f-a6c2-cba72316a7c5\",\"attributes\":{\"name\":\"HSpacer00685\",\"stylesheets\":[\"\\n:host(.pn-loading):before, .pn-loading:before {\\n background-color: #c3c3c3;\\n mask-size: auto calc(min(50%, 300px));\\n -webkit-mask-size: auto calc(min(50%, 300px));\\n}\",{\"id\":\"5d278abd-ffb0-4950-9c04-af4b48c85d17\"},{\"id\":\"ce0d0f22-d8c7-4927-be56-06a6be3798f0\"},{\"id\":\"c6edb713-0c24-4693-831b-a534f5f66265\"}],\"min_width\":0,\"margin\":0,\"sizing_mode\":\"stretch_width\",\"align\":\"start\"}},{\"type\":\"object\",\"name\":\"Figure\",\"id\":\"bf1aa184-ac50-4f3c-a852-421ed5391f73\",\"attributes\":{\"width\":null,\"height\":null,\"margin\":[5,10],\"sizing_mode\":\"fixed\",\"align\":\"start\",\"x_range\":{\"type\":\"object\",\"name\":\"Range1d\",\"id\":\"52c04568-5555-422b-a2ab-648381dc82e1\",\"attributes\":{\"name\":\"x\",\"tags\":[[[\"x\",null]],[]],\"start\":30.107051021318842,\"end\":30.138615413643485,\"reset_start\":30.107051021318842,\"reset_end\":30.138615413643485}},\"y_range\":{\"type\":\"object\",\"name\":\"Range1d\",\"id\":\"650a92ab-8245-4b1c-a5e4-54118b85ace6\",\"attributes\":{\"name\":\"y\",\"tags\":[[[\"y\",null]],{\"type\":\"map\",\"entries\":[[\"invert_yaxis\",false],[\"autorange\",false]]}],\"start\":-40.54304954242811,\"end\":-40.33953410774247,\"reset_start\":-40.54304954242811,\"reset_end\":-40.33953410774247}},\"x_scale\":{\"type\":\"object\",\"name\":\"LinearScale\",\"id\":\"02c7f421-999f-45cd-b9dc-9cc7a75fa10b\"},\"y_scale\":{\"type\":\"object\",\"name\":\"LinearScale\",\"id\":\"d6df2514-fcd0-4b83-a6d3-6e16e0145a22\"},\"title\":{\"type\":\"object\",\"name\":\"Title\",\"id\":\"1f09d5a3-4147-481f-851b-4a985a22da9d\",\"attributes\":{\"text_color\":\"black\",\"text_font_size\":\"12pt\"}},\"renderers\":[{\"type\":\"object\",\"name\":\"GlyphRenderer\",\"id\":\"d045e878-248a-48f4-b3f0-8e7f56569463\",\"attributes\":{\"data_source\":{\"type\":\"object\",\"name\":\"ColumnDataSource\",\"id\":\"a8a5befc-1b07-4dcf-b3d0-8989eefd1f5f\",\"attributes\":{\"selected\":{\"type\":\"object\",\"name\":\"Selection\",\"id\":\"4ed87b5c-5b09-42df-954f-49701584d3c5\",\"attributes\":{\"indices\":[],\"line_indices\":[]}},\"selection_policy\":{\"type\":\"object\",\"name\":\"UnionRenderers\",\"id\":\"8fdcfc67-8101-4205-bb43-29bae730a543\"},\"data\":{\"type\":\"map\",\"entries\":[[\"xs\",[[[{\"type\":\"ndarray\",\"array\":{\"type\":\"bytes\",\"data\":\"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\"},\"shape\":[420],\"dtype\":\"float64\",\"order\":\"little\"}]]]],[\"ys\",[[[{\"type\":\"ndarray\",\"array\":{\"type\":\"bytes\",\"data\":\"H4sIAAEAAAAC/03XaTSVax8GcHNllllERHbGjR2iQ2yZpwplSBJ1SBkTTqFMDYaOTFGScMxj5rSPgzIVDXZsU5HpqBBJqPfaa73vWu+n37dnPc/9/IfrTjriPV+hRKYkBRFopXTTpjuL6Dbk1RfQpbkXPKK7uSs1B/7lea2nCVpwcg8OwOXK9I+LMM1BZolTmUzJkA2Ml4c+Y27vDeHBDGuSKzQ886AvFB4iLXqnQmNmgy1VdPv/fNgLTbMnDsxCCx+NQRYVMsVKJzpQCtpsG+DRhbZUuWIHeDQv+FAA3OPqcTcB9rIs5BfCnJaRH20w6FK31Tg0UWt4uA7nqYblQqpkyp+XXzQR4T6Z488tIO35xJszMPL8+fdX4eqvlPx7MCtPqroe6pkXP30N+Vkz+L/AhiIZKXYimeJiU6YoC1XZsyL04UC5XLwTDLOrzLgIrXiyP92GKzWEHyUww7GG7Tk8wKDHPwHf53VK/oSx5kcVRdXIFMXFUS0N2J/6u5E1vKi7bOsF53t3CEfDpIB8mQdwnyhRtQnSWpp0B2DZpcKkRRipljbJqU6m2M1HaclDQr7/LUO47npy/ATsE7XSCIWPXuvEpcBL8YThCmhqLKzaAyUYWaOm4ULjEpVJg0xpDxxX2AnTlF+Ea8Mu3qun7OCPJZKRH9z7dnZPPKxnlI7Lh7MTb71aoWjHdctROB8enLMGk2SZygRIZAqp+1ajCixxrgk3g9KfaWQPmBHOzB4BdWUThDIh31iYVy2cTP+9pR9OeTYe/wRnNThWtu6DTM5Ju+GOjzwB+tDi2T92ztAyRWAxGFq7e9xKhtISR8bLoOy/uWFdcE/DstAUJMQaVTFqkilOv6/dkYA3fLw998HpaP1wayhyo8rmLDQ756keBdlKGWn34J1BbZ06KMXmn9kHy9SK1mfhqMCJUVYt1PnB1N8k4Q+fF/e1oGNMbp0tnKmiCXvD5nOd3jFQ7eeCag4cyA070AwtSLlMVHin6kzEIhxSUfzJoU2myJQuhMlBXaHDVfrQLrx6xhFemBGQDIICCdkKSdCJ43RhEXyVdfN8OxQTs5kfg4nFwZU/INuB7IuC+3E+je7LKrDd/GaDGdw/UnXFA15zDN4IhwQGpm2ZcL3hi3MtfBEwUtEPHyp1s3yCQdP1x7bqoG4eb1uWgeYXVo31oAThY6YjlFolaV6Eo5r9o4lwjFU6tBh+eO0v+AxO5PxT8QH6vVjn3YCKPGn1grroo0WzTCIUUZsyMIdmAndlz0BWO1JvBNTUnC3Pgl+DoxxqobSj1v5XsDLG9+6/0OcZhwHrATLlXuHT3bug45xjz36o+MhEyh4q+2c0+UEV/Tn7eKjGrbP0FyQN34xvo1s0LD8Oz9xK4tiAaefJn4V/I1Oe23zvU4draiXV1lBB8GSqN3Ra5Q+JhTcHnznlwpPJj8tboJsbm/kQdFdxmFqB+2bJ+/n08P51KW8VoU70lK8J5NZ70+YOeblkT4ZDvS/vDDKhb/8t2ToYkD1CeQUfRd7O/QxfuxvFsOuTKcyH1s7KwS0CZiwGUPPb5pQLPPuusjMEpjV6lKTAu3G1xpUwy37LRA/s1Wpqn4Ev2TjdWA5ivrxx3pCErx+WpulAlmi3UAe45UiVYADUJHl+T4DazHXJRVC3f4tKB/SubuL7AM9FcpZuwvvulERRAzIlm8inQIJ9/O1ctvDVe8HCcwb0eehJjoPUy3VjuXDIfGvYU8iu3OxCg5wbnGvfoO4ohbbdkExpldHtUoaN72ILzGD6odo6S/gkPaPDBrp45QwfgaQW6Tl7WNNOXNWFlavjTrtghKBSCCu0UQ9JncVzLT3k13uhRRrVtQqadsa0pUKTdRIhDBorfYx3hQLmxGJDyH95nEce8pUnBnJCnve/DS7gO7n5Px94C2mq+50a4OCp2af34Ls76buvQmqH8XVP+GgyOd+MrpAhhwrMMVm6wA+zQ3PerOL875XYaA/T/8/or3sU6LVh3vQIaoulv4uD7FqTK+fgNq9kR1tIjR0LJkFTpwJhcSgS8lWDAd6waHozhbr4KXktsBf6fzUTqIETHdtr7kK5mGDOSChTL+fnAZff+ieYwsUt0l+V4D+y53m3wxYHiaBvqNMn13sG6XUbXyCySq/jE+2nBeh1rTBRQaTX+SbDppX3//XB//qi42oBSQS21djfpfdN6xTrL3ofLSiKFLdD8YC9DkXQtEGXOREG/bIqD4CBxJ/8x6Cfe+klXagfuZYiBXmzjapZ4Fjz7b4Z9HXp0MinHnjlO4GjEloKXZRPgRIarUYhkLn7JI8r/Nft1zsyfDi532kvdDxzcYQH8s1Vuq5gzqxF2BBo9Hkk/GWJAjvK4pvzYbmRYswtuO1BTogffPfkU6odLKBp12jDi2vR/RLwkPCrz4zQyDTyyBTmoEGYan0X1CsdEy+H574uPk+GmdosQZfg4Wa9VRfIoRcWYghb/65dl4eFx+zaueEpWpLxMuay6ImezkEYWH/wSQv0m1vYlQt9xR/ExEL5bNpWH1jFVWN4GHolfWnXhMs/nzFIwJJI5mtMkLRAezKNvfHZX1i/F+avHG6tgsTTCptpcHbKM/wyVMzvprpDXw9VVTNYszslThXKfneWF4ZedX9HbGKfDd+4LjUJrU/YUrpgskStSCUcHBbzS4M7ssI7L8N89mMs7lApRFXPBDZMbw1Vgr5e5I3tkPAp/Mp37FveqJ0mo/C72BO+NsjR0SJdBL86u8wnwi1bXTeDoET1SW76Hmcx+0U9CEVXGGb3QNUHTD+4oJE5C8cycsDxb6ziQ/D+Y69ACnwf+LInH+7W0JCNhyO1Fwr8IW1mJ6cDLGLzJujAv7TEEiThJXvPYmbYLNjNPId88orSI9AHBSOIPrWQoKB+JQseHNBIvAqjUjKmz0Lb6o8sljBibbWCCEv02F2E4GC0+LZ15Ca2HuXHY1B9+0G3Nuhy7AhXIXS+weQfD6/3Cez0h7VCe7rsYY3JgLIOHE+YGZKEXG9/xLBArR1c6rPIc3eCrsq9gAsvV0WrofleH650WBD1geEyZBxzWHaDTtq904dgfbIBTQFODh6N5YV6Gl2hK8iX3qHDckOw7Y2t2FO4LHa6I5eeT4MGG+JgwUurUh/IvLftwWFI9B2w0IQ3u8z1xeHU7r/VGaFB+L49U8i/9weLxbrpOVl9F3cFPTc/XDNJgSMb53VDobbDpIorTK48LkOG4lJqJwjQxJPMzg2DSuxrl5DHc5fOnqLCPq0w7mZISF5gzIH2g0xl0fR8z1G13QtG2ra1WMEr1wJk1OEfj6XjhGHhuobGBu4JeUqT6ePwVcA3cjvdvLzCQviSepQ7Aa5LnGn0h2vWQpIOUC5VvlsHyj6nqkjBSGlLGVY49Eer0BzuL/uomuwv6fccYulmNd3/3ov+A056VTYgDQAA\"},\"shape\":[420],\"dtype\":\"float64\",\"order\":\"little\"}]]]]]}}},\"view\":{\"type\":\"object\",\"name\":\"CDSView\",\"id\":\"9b6f3d83-52f2-41d0-9ffc-4647342a19fe\",\"attributes\":{\"filter\":{\"type\":\"object\",\"name\":\"AllIndices\",\"id\":\"6d62a592-7c5a-4bbc-b762-250403877c32\"}}},\"glyph\":{\"type\":\"object\",\"name\":\"MultiPolygons\",\"id\":\"c75ba1e0-14d2-484b-a9c2-16fe8a151958\",\"attributes\":{\"xs\":{\"type\":\"field\",\"field\":\"xs\"},\"ys\":{\"type\":\"field\",\"field\":\"ys\"},\"fill_color\":{\"type\":\"value\",\"value\":\"#30a2da\"},\"hatch_color\":{\"type\":\"value\",\"value\":\"#30a2da\"}}},\"selection_glyph\":{\"type\":\"object\",\"name\":\"MultiPolygons\",\"id\":\"2d283e12-0ecd-403c-afba-332c52fe3a7f\",\"attributes\":{\"xs\":{\"type\":\"field\",\"field\":\"xs\"},\"ys\":{\"type\":\"field\",\"field\":\"ys\"},\"line_color\":{\"type\":\"value\",\"value\":\"black\"},\"line_alpha\":{\"type\":\"value\",\"value\":1.0},\"line_width\":{\"type\":\"value\",\"value\":1},\"line_join\":{\"type\":\"value\",\"value\":\"bevel\"},\"line_cap\":{\"type\":\"value\",\"value\":\"butt\"},\"line_dash\":{\"type\":\"value\",\"value\":[]},\"line_dash_offset\":{\"type\":\"value\",\"value\":0},\"fill_color\":{\"type\":\"value\",\"value\":\"#30a2da\"},\"fill_alpha\":{\"type\":\"value\",\"value\":1.0},\"hatch_color\":{\"type\":\"value\",\"value\":\"#30a2da\"},\"hatch_alpha\":{\"type\":\"value\",\"value\":1.0},\"hatch_scale\":{\"type\":\"value\",\"value\":12.0},\"hatch_pattern\":{\"type\":\"value\",\"value\":null},\"hatch_weight\":{\"type\":\"value\",\"value\":1.0}}},\"nonselection_glyph\":{\"type\":\"object\",\"name\":\"MultiPolygons\",\"id\":\"3ce2471b-c644-4dac-9e8f-bf7b750966c0\",\"attributes\":{\"xs\":{\"type\":\"field\",\"field\":\"xs\"},\"ys\":{\"type\":\"field\",\"field\":\"ys\"},\"line_alpha\":{\"type\":\"value\",\"value\":0.1},\"fill_color\":{\"type\":\"value\",\"value\":\"#30a2da\"},\"fill_alpha\":{\"type\":\"value\",\"value\":0.1},\"hatch_color\":{\"type\":\"value\",\"value\":\"#30a2da\"},\"hatch_alpha\":{\"type\":\"value\",\"value\":0.1}}},\"muted_glyph\":{\"type\":\"object\",\"name\":\"MultiPolygons\",\"id\":\"d7f5188e-4c5b-4c84-8a42-833b7fcef164\",\"attributes\":{\"xs\":{\"type\":\"field\",\"field\":\"xs\"},\"ys\":{\"type\":\"field\",\"field\":\"ys\"},\"line_alpha\":{\"type\":\"value\",\"value\":0.2},\"fill_color\":{\"type\":\"value\",\"value\":\"#30a2da\"},\"fill_alpha\":{\"type\":\"value\",\"value\":0.2},\"hatch_color\":{\"type\":\"value\",\"value\":\"#30a2da\"},\"hatch_alpha\":{\"type\":\"value\",\"value\":0.2}}}}}],\"toolbar\":{\"type\":\"object\",\"name\":\"Toolbar\",\"id\":\"2f0a86f4-627c-46bb-83e8-8cc403ca9f4a\",\"attributes\":{\"tools\":[{\"type\":\"object\",\"name\":\"WheelZoomTool\",\"id\":\"a4a553cc-6b90-47b5-8d75-ca08d6100c9a\",\"attributes\":{\"tags\":[\"hv_created\"],\"renderers\":\"auto\",\"zoom_on_axis\":false,\"zoom_together\":\"none\"}},{\"type\":\"object\",\"name\":\"SaveTool\",\"id\":\"3c2ef998-3d91-43a2-a944-951ffef18dd8\"},{\"type\":\"object\",\"name\":\"PanTool\",\"id\":\"317a0bbf-757d-4a09-a1fe-1d33fb9e797e\"},{\"type\":\"object\",\"name\":\"BoxZoomTool\",\"id\":\"a44f522b-a0c5-4e54-830c-56ea92208edb\",\"attributes\":{\"overlay\":{\"type\":\"object\",\"name\":\"BoxAnnotation\",\"id\":\"350001a6-772c-46cc-a666-da3108266b99\",\"attributes\":{\"syncable\":false,\"line_color\":\"black\",\"line_alpha\":1.0,\"line_width\":2,\"line_dash\":[4,4],\"fill_color\":\"lightgrey\",\"fill_alpha\":0.5,\"level\":\"overlay\",\"visible\":false,\"left\":{\"type\":\"number\",\"value\":\"nan\"},\"right\":{\"type\":\"number\",\"value\":\"nan\"},\"top\":{\"type\":\"number\",\"value\":\"nan\"},\"bottom\":{\"type\":\"number\",\"value\":\"nan\"},\"left_units\":\"canvas\",\"right_units\":\"canvas\",\"top_units\":\"canvas\",\"bottom_units\":\"canvas\",\"handles\":{\"type\":\"object\",\"name\":\"BoxInteractionHandles\",\"id\":\"c12e9ef3-ec86-447a-98e3-9c2f8c324580\",\"attributes\":{\"all\":{\"type\":\"object\",\"name\":\"AreaVisuals\",\"id\":\"03d223b8-8c26-4495-be56-7d8586766ce9\",\"attributes\":{\"fill_color\":\"white\",\"hover_fill_color\":\"lightgray\"}}}}}},\"match_aspect\":true}},{\"type\":\"object\",\"name\":\"ResetTool\",\"id\":\"4ca5e9ee-d4ff-4d9c-82a4-82e706d16766\"}],\"active_drag\":{\"id\":\"317a0bbf-757d-4a09-a1fe-1d33fb9e797e\"},\"active_scroll\":{\"id\":\"a4a553cc-6b90-47b5-8d75-ca08d6100c9a\"}}},\"left\":[{\"type\":\"object\",\"name\":\"LinearAxis\",\"id\":\"70652fbf-6540-4887-8fb9-f3b440e8445c\",\"attributes\":{\"ticker\":{\"type\":\"object\",\"name\":\"BasicTicker\",\"id\":\"9daa9dd4-ca4a-424f-a01e-6817a8dcf427\",\"attributes\":{\"mantissas\":[1,2,5]}},\"formatter\":{\"type\":\"object\",\"name\":\"BasicTickFormatter\",\"id\":\"2d34f922-bd1b-4436-985c-f0d96c84f851\"},\"axis_label\":\"y\",\"major_label_policy\":{\"type\":\"object\",\"name\":\"AllLabels\",\"id\":\"f514c8b0-3db5-453a-bcbb-c127a34b058c\"}}}],\"below\":[{\"type\":\"object\",\"name\":\"LinearAxis\",\"id\":\"64573aec-0c33-4002-b800-a9c45cd0863a\",\"attributes\":{\"ticker\":{\"type\":\"object\",\"name\":\"BasicTicker\",\"id\":\"92e52b81-eff1-4aee-9448-23c0627d95b2\",\"attributes\":{\"mantissas\":[1,2,5]}},\"formatter\":{\"type\":\"object\",\"name\":\"BasicTickFormatter\",\"id\":\"6c7a2513-0503-42c3-8b1a-dec7752573f5\"},\"axis_label\":\"x\",\"major_label_policy\":{\"type\":\"object\",\"name\":\"AllLabels\",\"id\":\"c96d9c12-5fee-49ea-82a3-b60bf8e65a68\"}}}],\"center\":[{\"type\":\"object\",\"name\":\"Grid\",\"id\":\"a36de2df-912a-4422-ac24-9048cc341b48\",\"attributes\":{\"axis\":{\"id\":\"64573aec-0c33-4002-b800-a9c45cd0863a\"},\"grid_line_color\":null}},{\"type\":\"object\",\"name\":\"Grid\",\"id\":\"c4ff6b59-5c4a-440c-9d0b-deaad496a7e3\",\"attributes\":{\"dimension\":1,\"axis\":{\"id\":\"70652fbf-6540-4887-8fb9-f3b440e8445c\"},\"grid_line_color\":null}}],\"frame_width\":100,\"frame_height\":644,\"min_border_top\":10,\"min_border_bottom\":10,\"min_border_left\":10,\"min_border_right\":10,\"output_backend\":\"webgl\",\"match_aspect\":true}},{\"type\":\"object\",\"name\":\"Spacer\",\"id\":\"a6147c41-9c7f-437f-a333-1d91268af532\",\"attributes\":{\"name\":\"HSpacer00686\",\"stylesheets\":[\"\\n:host(.pn-loading):before, .pn-loading:before {\\n background-color: #c3c3c3;\\n mask-size: auto calc(min(50%, 300px));\\n -webkit-mask-size: auto calc(min(50%, 300px));\\n}\",{\"id\":\"5d278abd-ffb0-4950-9c04-af4b48c85d17\"},{\"id\":\"ce0d0f22-d8c7-4927-be56-06a6be3798f0\"},{\"id\":\"c6edb713-0c24-4693-831b-a534f5f66265\"}],\"min_width\":0,\"margin\":0,\"sizing_mode\":\"stretch_width\",\"align\":\"start\"}}]}}],\"defs\":[{\"type\":\"model\",\"name\":\"ReactiveHTML1\"},{\"type\":\"model\",\"name\":\"FlexBox1\",\"properties\":[{\"name\":\"align_content\",\"kind\":\"Any\",\"default\":\"flex-start\"},{\"name\":\"align_items\",\"kind\":\"Any\",\"default\":\"flex-start\"},{\"name\":\"flex_direction\",\"kind\":\"Any\",\"default\":\"row\"},{\"name\":\"flex_wrap\",\"kind\":\"Any\",\"default\":\"wrap\"},{\"name\":\"gap\",\"kind\":\"Any\",\"default\":\"\"},{\"name\":\"justify_content\",\"kind\":\"Any\",\"default\":\"flex-start\"}]},{\"type\":\"model\",\"name\":\"FloatPanel1\",\"properties\":[{\"name\":\"config\",\"kind\":\"Any\",\"default\":{\"type\":\"map\"}},{\"name\":\"contained\",\"kind\":\"Any\",\"default\":true},{\"name\":\"position\",\"kind\":\"Any\",\"default\":\"right-top\"},{\"name\":\"offsetx\",\"kind\":\"Any\",\"default\":null},{\"name\":\"offsety\",\"kind\":\"Any\",\"default\":null},{\"name\":\"theme\",\"kind\":\"Any\",\"default\":\"primary\"},{\"name\":\"status\",\"kind\":\"Any\",\"default\":\"normalized\"}]},{\"type\":\"model\",\"name\":\"GridStack1\",\"properties\":[{\"name\":\"ncols\",\"kind\":\"Any\",\"default\":null},{\"name\":\"nrows\",\"kind\":\"Any\",\"default\":null},{\"name\":\"allow_resize\",\"kind\":\"Any\",\"default\":true},{\"name\":\"allow_drag\",\"kind\":\"Any\",\"default\":true},{\"name\":\"state\",\"kind\":\"Any\",\"default\":[]}]},{\"type\":\"model\",\"name\":\"drag1\",\"properties\":[{\"name\":\"slider_width\",\"kind\":\"Any\",\"default\":5},{\"name\":\"slider_color\",\"kind\":\"Any\",\"default\":\"black\"},{\"name\":\"start\",\"kind\":\"Any\",\"default\":0},{\"name\":\"end\",\"kind\":\"Any\",\"default\":100},{\"name\":\"value\",\"kind\":\"Any\",\"default\":50}]},{\"type\":\"model\",\"name\":\"click1\",\"properties\":[{\"name\":\"terminal_output\",\"kind\":\"Any\",\"default\":\"\"},{\"name\":\"debug_name\",\"kind\":\"Any\",\"default\":\"\"},{\"name\":\"clears\",\"kind\":\"Any\",\"default\":0}]},{\"type\":\"model\",\"name\":\"ReactiveESM1\",\"properties\":[{\"name\":\"esm_constants\",\"kind\":\"Any\",\"default\":{\"type\":\"map\"}}]},{\"type\":\"model\",\"name\":\"JSComponent1\",\"properties\":[{\"name\":\"esm_constants\",\"kind\":\"Any\",\"default\":{\"type\":\"map\"}}]},{\"type\":\"model\",\"name\":\"ReactComponent1\",\"properties\":[{\"name\":\"use_shadow_dom\",\"kind\":\"Any\",\"default\":true},{\"name\":\"esm_constants\",\"kind\":\"Any\",\"default\":{\"type\":\"map\"}}]},{\"type\":\"model\",\"name\":\"AnyWidgetComponent1\",\"properties\":[{\"name\":\"use_shadow_dom\",\"kind\":\"Any\",\"default\":true},{\"name\":\"esm_constants\",\"kind\":\"Any\",\"default\":{\"type\":\"map\"}}]},{\"type\":\"model\",\"name\":\"FastWrapper1\",\"properties\":[{\"name\":\"object\",\"kind\":\"Any\",\"default\":null},{\"name\":\"style\",\"kind\":\"Any\",\"default\":null}]},{\"type\":\"model\",\"name\":\"NotificationArea1\",\"properties\":[{\"name\":\"js_events\",\"kind\":\"Any\",\"default\":{\"type\":\"map\"}},{\"name\":\"max_notifications\",\"kind\":\"Any\",\"default\":5},{\"name\":\"notifications\",\"kind\":\"Any\",\"default\":[]},{\"name\":\"position\",\"kind\":\"Any\",\"default\":\"bottom-right\"},{\"name\":\"_clear\",\"kind\":\"Any\",\"default\":0},{\"name\":\"types\",\"kind\":\"Any\",\"default\":[{\"type\":\"map\",\"entries\":[[\"type\",\"warning\"],[\"background\",\"#ffc107\"],[\"icon\",{\"type\":\"map\",\"entries\":[[\"className\",\"fas fa-exclamation-triangle\"],[\"tagName\",\"i\"],[\"color\",\"white\"]]}]]},{\"type\":\"map\",\"entries\":[[\"type\",\"info\"],[\"background\",\"#007bff\"],[\"icon\",{\"type\":\"map\",\"entries\":[[\"className\",\"fas fa-info-circle\"],[\"tagName\",\"i\"],[\"color\",\"white\"]]}]]}]}]},{\"type\":\"model\",\"name\":\"Notification\",\"properties\":[{\"name\":\"background\",\"kind\":\"Any\",\"default\":null},{\"name\":\"duration\",\"kind\":\"Any\",\"default\":3000},{\"name\":\"icon\",\"kind\":\"Any\",\"default\":null},{\"name\":\"message\",\"kind\":\"Any\",\"default\":\"\"},{\"name\":\"notification_type\",\"kind\":\"Any\",\"default\":null},{\"name\":\"_rendered\",\"kind\":\"Any\",\"default\":false},{\"name\":\"_destroyed\",\"kind\":\"Any\",\"default\":false}]},{\"type\":\"model\",\"name\":\"TemplateActions1\",\"properties\":[{\"name\":\"open_modal\",\"kind\":\"Any\",\"default\":0},{\"name\":\"close_modal\",\"kind\":\"Any\",\"default\":0}]},{\"type\":\"model\",\"name\":\"BootstrapTemplateActions1\",\"properties\":[{\"name\":\"open_modal\",\"kind\":\"Any\",\"default\":0},{\"name\":\"close_modal\",\"kind\":\"Any\",\"default\":0}]},{\"type\":\"model\",\"name\":\"TemplateEditor1\",\"properties\":[{\"name\":\"layout\",\"kind\":\"Any\",\"default\":[]}]},{\"type\":\"model\",\"name\":\"MaterialTemplateActions1\",\"properties\":[{\"name\":\"open_modal\",\"kind\":\"Any\",\"default\":0},{\"name\":\"close_modal\",\"kind\":\"Any\",\"default\":0}]},{\"type\":\"model\",\"name\":\"request_value1\",\"properties\":[{\"name\":\"fill\",\"kind\":\"Any\",\"default\":\"none\"},{\"name\":\"_synced\",\"kind\":\"Any\",\"default\":null},{\"name\":\"_request_sync\",\"kind\":\"Any\",\"default\":0}]},{\"type\":\"model\",\"name\":\"holoviews.plotting.bokeh.raster.HoverModel\",\"properties\":[{\"name\":\"xy\",\"kind\":\"Any\",\"default\":null},{\"name\":\"data\",\"kind\":\"Any\",\"default\":null}]}]}};\n",
" var render_items = [{\"docid\":\"b5bf2de9-b75d-4556-9356-b32868916f06\",\"roots\":{\"48f34b9c-8595-427a-a149-0deb12f5318c\":\"aeffe791-c23c-4ad5-9be8-0f6f86fc10c7\"},\"root_ids\":[\"48f34b9c-8595-427a-a149-0deb12f5318c\"]}];\n",
" var docs = Object.values(docs_json)\n",
" if (!docs) {\n",
" return\n",
" }\n",
" const version = docs[0].version.replace('rc', '-rc.').replace('.dev', '-dev.')\n",
" async function embed_document(root) {\n",
" var Bokeh = get_bokeh(root)\n",
" await Bokeh.embed.embed_items_notebook(docs_json, render_items);\n",
" for (const render_item of render_items) {\n",
" for (const root_id of render_item.root_ids) {\n",
"\tconst id_el = document.getElementById(root_id)\n",
"\tif (id_el.children.length && id_el.children[0].hasAttribute('data-root-id')) {\n",
"\t const root_el = id_el.children[0]\n",
"\t root_el.id = root_el.id + '-rendered'\n",
"\t for (const child of root_el.children) {\n",
" // Ensure JupyterLab does not capture keyboard shortcuts\n",
" // see: https://jupyterlab.readthedocs.io/en/4.1.x/extension/notebook.html#keyboard-interaction-model\n",
"\t child.setAttribute('data-lm-suppress-shortcuts', 'true')\n",
"\t }\n",
"\t}\n",
" }\n",
" }\n",
" }\n",
" function get_bokeh(root) {\n",
" if (root.Bokeh === undefined) {\n",
" return null\n",
" } else if (root.Bokeh.version !== version) {\n",
" if (root.Bokeh.versions === undefined || !root.Bokeh.versions.has(version)) {\n",
"\treturn null\n",
" }\n",
" return root.Bokeh.versions.get(version);\n",
" } else if (root.Bokeh.version === version) {\n",
" return root.Bokeh\n",
" }\n",
" return null\n",
" }\n",
" function is_loaded(root) {\n",
" var Bokeh = get_bokeh(root)\n",
" return (Bokeh != null && Bokeh.Panel !== undefined)\n",
" }\n",
" if (is_loaded(root)) {\n",
" embed_document(root);\n",
" } else {\n",
" var attempts = 0;\n",
" var timer = setInterval(function(root) {\n",
" if (is_loaded(root)) {\n",
" clearInterval(timer);\n",
" embed_document(root);\n",
" } else if (document.readyState == \"complete\") {\n",
" attempts++;\n",
" if (attempts > 200) {\n",
" clearInterval(timer);\n",
"\t var Bokeh = get_bokeh(root)\n",
"\t if (Bokeh == null || Bokeh.Panel == null) {\n",
" console.warn(\"Panel: ERROR: Unable to run Panel code because Bokeh or Panel library is missing\");\n",
"\t } else {\n",
"\t console.warn(\"Panel: WARNING: Attempting to render but not all required libraries could be resolved.\")\n",
"\t embed_document(root)\n",
"\t }\n",
" }\n",
" }\n",
" }, 25, root)\n",
" }\n",
"})(window);</script>"
],
"text/plain": [
":Polygons [x,y]"
]
},
"execution_count": 15,
"metadata": {
"application/vnd.holoviews_exec.v0+json": {
"id": "48f34b9c-8595-427a-a149-0deb12f5318c"
}
},
"output_type": "execute_result"
}
],
"source": [
"gdf_plot.hvplot(aspect=\"equal\", frame_width=100)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Step 7: Convert to GML Format\n",
"\n",
"Convert the polygon to GML (Geography Markup Language) format using geopandas exporter.\n"
]
},
{
"cell_type": "code",
"execution_count": 16,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"✓ GML file created: hirise40_footprint_demo.gml\n",
" Geometry type: MultiPolygon\n",
" Contains 1 polygons\n"
]
}
],
"source": [
"# Create GeoDataFrame from the geometry we parsed\n",
"gdf = gpd.GeoDataFrame(\n",
" [{\"id\": 1, \"name\": \"footprint\", \"geometry\": geom}], crs=\"EPSG:4326\"\n",
")\n",
"\n",
"# Write to GML using geopandas exporter\n",
"output_gml = \"hirise40_footprint_demo.gml\"\n",
"gdf.to_file(output_gml, driver=\"GML\")\n",
"\n",
"print(f\"✓ GML file created: {output_gml}\")\n",
"print(f\" Geometry type: {gdf.geometry.iloc[0].geom_type}\")\n",
"if geom.geom_type == \"MultiPolygon\":\n",
" print(f\" Contains {len(geom.geoms)} polygons\")\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Step 8: Convert to GeoPackage Format\n",
"\n",
"Convert the polygon to GeoPackage format using geopandas. We'll reuse the geometry we already created.\n"
]
},
{
"cell_type": "code",
"execution_count": 17,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"✓ GeoPackage file created: hirise40_footprint_demo.gpkg\n",
" Layer: footprint\n",
" CRS: EPSG:4326\n",
" Geometry type: MultiPolygon\n",
" Contains 1 polygons\n",
"\n",
" You can open this file in QGIS, ArcGIS, or other GIS software\n"
]
}
],
"source": [
"# Use the geometry we already parsed from WKT\n",
"# If it's a MultiPolygon, use it directly; otherwise create Polygon\n",
"if geom.geom_type == \"MultiPolygon\":\n",
" geometry = geom\n",
"else:\n",
" from shapely.geometry import Polygon\n",
"\n",
" geometry = Polygon(coords)\n",
"\n",
"# Create GeoDataFrame\n",
"gdf = gpd.GeoDataFrame(\n",
" [{\"id\": 1, \"name\": \"footprint\", \"geometry\": geometry}], crs=\"EPSG:4326\"\n",
")\n",
"\n",
"# Write to GeoPackage\n",
"output_gpkg = \"hirise40_footprint_demo.gpkg\"\n",
"gdf.to_file(output_gpkg, driver=\"GPKG\", layer=\"footprint\")\n",
"\n",
"print(f\"✓ GeoPackage file created: {output_gpkg}\")\n",
"print(f\" Layer: footprint\")\n",
"print(f\" CRS: EPSG:4326\")\n",
"print(f\" Geometry type: {gdf.geometry.iloc[0].geom_type}\")\n",
"if geom.geom_type == \"MultiPolygon\":\n",
" print(f\" Contains {len(geom.geoms)} polygons\")\n",
"print(f\"\\n You can open this file in QGIS, ArcGIS, or other GIS software\")\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Summary\n",
"\n",
"### Key Findings:\n",
"\n",
"1. **Storage Format**: The polygon is stored as **ASCII text** in WKT format, not binary data\n",
"2. **Location**: Parsed automatically from PVL label (Object = Polygon, Name = Footprint)\n",
"3. **Format**: WKT polygon with longitude-latitude coordinate pairs\n",
"4. **Coordinates**: 420 coordinate pairs forming a closed polygon\n",
"5. **Output Formats**: Successfully converted to both GML and GeoPackage formats using geopandas exporters\n",
"\n",
"### Files Created:\n",
"- `hirise40_footprint_demo.gml` - GML format\n",
"- `hirise40_footprint_demo.gpkg` - GeoPackage format\n",
"\n",
"### Usage:\n",
"\n",
"For automated extraction, use the `extract_isis_footprint.py` script:\n",
"\n",
"```bash\n",
"python3 extract_isis_footprint.py --format gml\n",
"python3 extract_isis_footprint.py --format gpkg\n",
"python3 extract_isis_footprint.py --format both\n",
"```\n",
"\n",
"The script automatically parses the PVL label to find the footprint location - no manual byte offsets needed!\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": []
},
{
"cell_type": "markdown",
"metadata": {},
"source": []
}
],
"metadata": {
"kernelspec": {
"display_name": "py313",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.13.9"
}
},
"nbformat": 4,
"nbformat_minor": 2
}

Polygon Storage Format in ISIS Cube Files

Summary

The polygon footprint data in the ISIS cube file hirise40.cal.map.cub is stored as ASCII text in WKT (Well-Known Text) format, not as binary data.

Input Data

The example cube file hirise40.cal.map.cub was created from the following HiRISE data processing pipeline:

  1. Source Data: ESP_039983_1390_RED4_0.IMG (raw HiRISE image data)
  2. Import to ISIS: Used hi2isis to convert the IMG file to ISIS cube format
  3. Calibration: Applied radiometric calibration using hical
  4. Map Projection: Applied default map projection using cam2map
  5. Footprint Generation: Generated the spatial footprint using footprintinit

The resulting cube file (hirise40.cal.map.cub) contains the calibrated, map-projected image data along with the polygon footprint stored in the PVL label and as attached binary data.

Location Information

The polygon footprint location is stored in the PVL (Planetary Data System Label) header of the .cub file. The extraction script automatically parses the PVL label to find:

  • Object Name: Polygon
  • Name: Footprint
  • StartByte: Location in the file (parsed from PVL)
  • Bytes: Size of the footprint data (parsed from PVL)

Indexing Note: PVL uses 1-based indexing (first byte is position 1), while Python's f.seek() uses 0-based indexing (first byte is position 0). The script automatically converts PVL StartByte values to 0-based by subtracting 1 before using f.seek().

Storage Format

The polygon data is stored as a plain ASCII text string in WKT (Well-Known Text) format:

MULTIPOLYGON (((30.107598435886608 -40.35696177712009, 30.108625875765043 -40.356868167155994, ...)))

WKT Format Notes:

  • MULTIPOLYGON is a valid WKT geometry type (OGC standard) used to represent collections of polygons
  • This particular footprint is stored as a MULTIPOLYGON containing a single polygon, which is valid WKT
  • The structure has three levels of parentheses: outer (MULTIPOLYGON), middle (polygon), inner (coordinate ring)

Indexing Conversion: When PVL reports StartByte = 225143647, this means the 225143647th byte (1-based). To read from Python using f.seek(), we convert to 0-based indexing: f.seek(225143647 - 1), which correctly positions us at the start of the MULTIPOLYGON string.

Coordinate Format

  • Coordinates are stored as longitude latitude pairs (not latitude longitude)
  • Each coordinate pair is separated by commas
  • Coordinates are floating-point numbers with high precision (~15 decimal places)
  • The polygon is closed (first and last coordinates are identical)

Extracted Data

  • Total coordinate pairs: 420
  • Coordinate range:
    • Longitude: ~30.107° to ~30.138° (East)
    • Latitude: ~-40.357° to ~-40.526° (South)
  • Projection: Sinusoidal (based on cube file metadata)

Conversion to GML or GeoPackage

The polygon can be extracted and converted to either GML (Geography Markup Language) or GeoPackage format using the extract_isis_footprint.py script. The script uses geopandas exporters for both formats, ensuring compatibility with GIS software.

GML Format

The GML format is created using geopandas.GeoDataFrame.to_file() with driver="GML":

  • GML 3.2 namespace
  • Proper geometry encoding for Polygon/MultiPolygon
  • Coordinates stored as space-separated "lon lat" pairs
  • Compatible with OGC GML standards

GeoPackage Format

The GeoPackage format is created using geopandas.GeoDataFrame.to_file() with driver="GPKG":

  • SQLite-based format (OGC GeoPackage standard)
  • Single polygon feature in a named layer (default: "footprint")
  • Configurable CRS (default: EPSG:4326)
  • Compatible with QGIS, ArcGIS, and other GIS software

Usage

The script automatically parses the PVL label to find the footprint location - no manual byte offsets needed:

# Extract to GML (default) - automatically finds footprint location
python3 extract_isis_footprint.py --format gml

# Extract to GeoPackage - automatically finds footprint location
python3 extract_isis_footprint.py --format gpkg

# Extract to both formats
python3 extract_isis_footprint.py --format both

# Custom options
python3 extract_isis_footprint.py --format gpkg --output my_footprint.gpkg \
    --layer-name "hirise_footprint" --crs "EPSG:4326"

# Override PVL parsing with manual byte offsets (if needed)
python3 extract_isis_footprint.py --format gml --start-byte 225143647 --num-bytes 16318

Required Libraries: The script requires pvl, geopandas, and shapely. Install with:

pip install pvl geopandas shapely

Extraction Method

The extraction process works as follows:

  1. Parse PVL Label: The script reads the PVL label from the beginning of the .cub file using the pvl library
  2. Find Footprint Location: Searches for an object with Object = Polygon and Name = Footprint to extract StartByte and Bytes values
    • Uses direct search through label.get("Object", []) first
    • Falls back to recursive search through the entire PVL structure if needed
  3. Convert Indexing: Converts PVL 1-based StartByte to Python 0-based indexing by subtracting 1
  4. Read WKT Data: Seeks to the calculated position and reads the specified number of bytes
  5. Parse WKT: Uses shapely.wkt.loads() to parse the WKT text into geometric objects
    • Handles both POLYGON and MULTIPOLYGON formats
    • Extracts all polygons if MULTIPOLYGON contains multiple polygons
  6. Export: Uses geopandas.GeoDataFrame.to_file() to export to GML or GeoPackage format
    • GML: gdf.to_file(output_file, driver="GML")
    • GeoPackage: gdf.to_file(output_file, driver="GPKG", layer=layer_name)

Key Implementation Details:

  • PVL parsing handles different label structures (dict, list, nested objects)
  • Recursive search ensures footprint is found even in complex PVL hierarchies
  • Indexing conversion is automatic and transparent to the user
  • WKT parsing uses Shapely for robustness and standards compliance
  • Export uses geopandas exporters for compatibility and maintainability
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment