chore: initialize insar management system v2
This commit is contained in:
@@ -0,0 +1,582 @@
|
||||
"""
|
||||
GF3 (GaoFen-3) L1A -> L2 processing service.
|
||||
|
||||
Pipeline: Extract archive -> Parse XML metadata -> Radiometric calibration -> RPC geometric correction.
|
||||
Pure Python implementation using GDAL/numpy, no ENVI/SARscape dependency.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import math
|
||||
import os
|
||||
import shutil
|
||||
import tarfile
|
||||
import zipfile
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
import numpy as np
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Archive extraction
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _extract_archive(path: str, dest: str) -> str:
|
||||
"""Extract .tar.gz or .zip archive to *dest*, return extracted directory path."""
|
||||
os.makedirs(dest, exist_ok=True)
|
||||
if tarfile.is_tarfile(path):
|
||||
with tarfile.open(path, "r:*") as tf:
|
||||
# Security: prevent path traversal
|
||||
for member in tf.getmembers():
|
||||
if member.name.startswith("/") or ".." in member.name:
|
||||
raise ValueError(f"Unsafe path in archive: {member.name}")
|
||||
tf.extractall(dest)
|
||||
elif zipfile.is_zipfile(path):
|
||||
with zipfile.ZipFile(path, "r") as zf:
|
||||
for info in zf.infolist():
|
||||
if info.filename.startswith("/") or ".." in info.filename:
|
||||
raise ValueError(f"Unsafe path in archive: {info.filename}")
|
||||
zf.extractall(dest)
|
||||
else:
|
||||
raise ValueError(f"Unsupported archive format: {path}")
|
||||
return dest
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# GF3 XML metadata parser
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _parse_gf3_meta(xml_path: str) -> Dict[str, Any]:
|
||||
"""Parse GF3 product XML metadata.
|
||||
|
||||
Returns dict with keys per polarization:
|
||||
polarizations: list of str (e.g. ["HH", "VV"])
|
||||
calibration: {pol: {"QualifyValue": float, "CalibrationConst": float}}
|
||||
"""
|
||||
import defusedxml.ElementTree as ET
|
||||
|
||||
tree = ET.parse(xml_path)
|
||||
root = tree.getroot()
|
||||
|
||||
result: Dict[str, Any] = {"polarizations": [], "calibration": {}}
|
||||
|
||||
# Find all imageinfo or channel elements
|
||||
# GF3 XML structure varies; search by tag name for robustness
|
||||
def _find_all_recursive(element, tag):
|
||||
found = []
|
||||
for child in element.iter():
|
||||
if tag.lower() in child.tag.lower():
|
||||
found.append(child)
|
||||
return found
|
||||
|
||||
# Try to find QualifyValue and CalibrationConst
|
||||
# Typical GF3 XML has <imageinfo> -> <QualifyValue> and <CalibrationConst>
|
||||
# per polarization channel
|
||||
qualify_values = {}
|
||||
cal_consts = {}
|
||||
|
||||
# Search for elements containing polarization-specific calibration info
|
||||
for elem in root.iter():
|
||||
tag = elem.tag.split("}")[-1] if "}" in elem.tag else elem.tag # strip namespace
|
||||
|
||||
if tag == "QualifyValue" and elem.text:
|
||||
# Parent should indicate which polarization this belongs to
|
||||
parent = _find_parent(root, elem)
|
||||
pol = _extract_polarization(parent, elem)
|
||||
if pol:
|
||||
try:
|
||||
qualify_values[pol] = float(elem.text.strip())
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
if tag == "CalibrationConst" and elem.text:
|
||||
parent = _find_parent(root, elem)
|
||||
pol = _extract_polarization(parent, elem)
|
||||
if pol:
|
||||
try:
|
||||
cal_consts[pol] = float(elem.text.strip())
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# If per-polarization search didn't work, try flat extraction
|
||||
if not qualify_values:
|
||||
# Fallback: find all QualifyValue elements in order
|
||||
qv_elems = [e for e in root.iter() if e.tag.split("}")[-1] == "QualifyValue" and e.text]
|
||||
cc_elems = [e for e in root.iter() if e.tag.split("}")[-1] == "CalibrationConst" and e.text]
|
||||
# Find polarization list
|
||||
pol_elems = [e for e in root.iter() if e.tag.split("}")[-1] == "Polarisation" and e.text]
|
||||
if not pol_elems:
|
||||
pol_elems = [e for e in root.iter() if e.tag.split("}")[-1] == "polarization" and e.text]
|
||||
|
||||
pols = [e.text.strip().upper() for e in pol_elems]
|
||||
|
||||
for i, pol in enumerate(pols):
|
||||
if i < len(qv_elems):
|
||||
try:
|
||||
qualify_values[pol] = float(qv_elems[i].text.strip())
|
||||
except ValueError:
|
||||
pass
|
||||
if i < len(cc_elems):
|
||||
try:
|
||||
cal_consts[pol] = float(cc_elems[i].text.strip())
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
polarizations = sorted(set(list(qualify_values.keys()) + list(cal_consts.keys())))
|
||||
if not polarizations:
|
||||
# Last resort: guess from TIFF filenames in same directory
|
||||
xml_dir = os.path.dirname(xml_path)
|
||||
for f in os.listdir(xml_dir):
|
||||
fl = f.upper()
|
||||
for pol in ("HH", "HV", "VH", "VV"):
|
||||
if pol in fl and f.lower().endswith((".tif", ".tiff")):
|
||||
if pol not in polarizations:
|
||||
polarizations.append(pol)
|
||||
polarizations.sort()
|
||||
|
||||
calibration = {}
|
||||
for pol in polarizations:
|
||||
calibration[pol] = {
|
||||
"QualifyValue": qualify_values.get(pol, 1.0),
|
||||
"CalibrationConst": cal_consts.get(pol, 0.0),
|
||||
}
|
||||
|
||||
result["polarizations"] = polarizations
|
||||
result["calibration"] = calibration
|
||||
|
||||
logger.info("[GF3] Parsed metadata: polarizations=%s, calibration=%s", polarizations, calibration)
|
||||
return result
|
||||
|
||||
|
||||
def _find_parent(root, target):
|
||||
"""Find the parent element of *target* in the tree."""
|
||||
for parent in root.iter():
|
||||
for child in parent:
|
||||
if child is target:
|
||||
return parent
|
||||
return None
|
||||
|
||||
|
||||
def _extract_polarization(parent, elem) -> Optional[str]:
|
||||
"""Try to extract polarization from context around an XML element."""
|
||||
if parent is None:
|
||||
return None
|
||||
# Check parent tag or sibling elements
|
||||
tag = parent.tag.split("}")[-1] if "}" in parent.tag else parent.tag
|
||||
for pol in ("HH", "HV", "VH", "VV"):
|
||||
if pol in tag.upper():
|
||||
return pol
|
||||
# Check sibling text
|
||||
for child in parent:
|
||||
child_tag = child.tag.split("}")[-1] if "}" in child.tag else child.tag
|
||||
if "polaris" in child_tag.lower() and child.text:
|
||||
return child.text.strip().upper()
|
||||
return None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# RPC file parser
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _read_rpb(rpb_path: str) -> Dict[str, Any]:
|
||||
"""Parse a .rpb (Rational Polynomial Coefficients) file into GDAL metadata dict."""
|
||||
rpc = {}
|
||||
current_key = None
|
||||
values = []
|
||||
|
||||
with open(rpb_path, "r") as f:
|
||||
for line in f:
|
||||
line = line.strip().rstrip(";")
|
||||
if "=" in line:
|
||||
if current_key and values:
|
||||
rpc[current_key] = values
|
||||
values = []
|
||||
key, _, val = line.partition("=")
|
||||
current_key = key.strip()
|
||||
val = val.strip().strip("(").strip(")")
|
||||
if val:
|
||||
for v in val.replace(",", " ").split():
|
||||
try:
|
||||
values.append(float(v))
|
||||
except ValueError:
|
||||
values.append(v)
|
||||
else:
|
||||
# continuation of values
|
||||
val = line.strip("()").strip()
|
||||
if val:
|
||||
for v in val.replace(",", " ").split():
|
||||
try:
|
||||
values.append(float(v))
|
||||
except ValueError:
|
||||
values.append(v)
|
||||
|
||||
if current_key and values:
|
||||
rpc[current_key] = values
|
||||
|
||||
# Map to GDAL RPC metadata keys
|
||||
gdal_rpc = {}
|
||||
key_map = {
|
||||
"lineOffset": "LINE_OFF",
|
||||
"sampOffset": "SAMP_OFF",
|
||||
"latOffset": "LAT_OFF",
|
||||
"longOffset": "LONG_OFF",
|
||||
"heightOffset": "HEIGHT_OFF",
|
||||
"lineScale": "LINE_SCALE",
|
||||
"sampScale": "SAMP_SCALE",
|
||||
"latScale": "LAT_SCALE",
|
||||
"longScale": "LONG_SCALE",
|
||||
"heightScale": "HEIGHT_SCALE",
|
||||
"lineNumCoef": "LINE_NUM_COEFF",
|
||||
"lineDenCoef": "LINE_DEN_COEFF",
|
||||
"sampNumCoef": "SAMP_NUM_COEFF",
|
||||
"sampDenCoef": "SAMP_DEN_COEFF",
|
||||
}
|
||||
|
||||
for rpb_key, gdal_key in key_map.items():
|
||||
if rpb_key in rpc:
|
||||
val = rpc[rpb_key]
|
||||
if isinstance(val, list):
|
||||
if len(val) == 1:
|
||||
gdal_rpc[gdal_key] = str(val[0])
|
||||
else:
|
||||
gdal_rpc[gdal_key] = " ".join(str(v) for v in val)
|
||||
else:
|
||||
gdal_rpc[gdal_key] = str(val)
|
||||
|
||||
return gdal_rpc
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Radiometric calibration
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _radiometric_calibration(tiff_path: str, qv: float, cal: float, output_path: str) -> str:
|
||||
"""L1A -> L1B single-polarization radiometric calibration.
|
||||
|
||||
Formula: A = sqrt(I^2 + Q^2), dB = 20*log10(A * QV / 65535) - Cal
|
||||
For amplitude-only TIFF: dB = 20*log10(A * QV / 65535) - Cal
|
||||
"""
|
||||
from osgeo import gdal
|
||||
|
||||
ds = gdal.Open(tiff_path, gdal.GA_ReadOnly)
|
||||
if ds is None:
|
||||
raise FileNotFoundError(f"Cannot open TIFF: {tiff_path}")
|
||||
|
||||
n_bands = ds.RasterCount
|
||||
width = ds.RasterXSize
|
||||
height = ds.RasterYSize
|
||||
|
||||
if n_bands >= 2:
|
||||
# Complex I/Q data
|
||||
band_i = ds.GetRasterBand(1).ReadAsArray().astype(np.float64)
|
||||
band_q = ds.GetRasterBand(2).ReadAsArray().astype(np.float64)
|
||||
amplitude = np.sqrt(band_i ** 2 + band_q ** 2)
|
||||
else:
|
||||
# Amplitude only
|
||||
amplitude = ds.GetRasterBand(1).ReadAsArray().astype(np.float64)
|
||||
|
||||
# Avoid log of zero
|
||||
amplitude = np.where(amplitude > 0, amplitude, np.nan)
|
||||
db_values = 20.0 * np.log10(amplitude * qv / 65535.0) - cal
|
||||
db_values = np.where(np.isfinite(db_values), db_values, 0).astype(np.float32)
|
||||
|
||||
# Write calibrated result preserving georeference
|
||||
driver = gdal.GetDriverByName("GTiff")
|
||||
out_ds = driver.Create(output_path, width, height, 1, gdal.GDT_Float32,
|
||||
options=["COMPRESS=DEFLATE"])
|
||||
out_ds.SetGeoTransform(ds.GetGeoTransform())
|
||||
out_ds.SetProjection(ds.GetProjection())
|
||||
|
||||
# Copy RPC metadata if present
|
||||
rpc_md = ds.GetMetadata("RPC")
|
||||
if rpc_md:
|
||||
out_ds.SetMetadata(rpc_md, "RPC")
|
||||
|
||||
out_ds.GetRasterBand(1).WriteArray(db_values)
|
||||
out_ds.GetRasterBand(1).SetNoDataValue(0)
|
||||
out_ds.FlushCache()
|
||||
out_ds = None
|
||||
ds = None
|
||||
|
||||
logger.info("[GF3] Calibration done: %s -> %s (QV=%.2f, Cal=%.2f)", tiff_path, output_path, qv, cal)
|
||||
return output_path
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Geometric correction
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _geometric_correction(
|
||||
l1b_path: str,
|
||||
rpb_path: Optional[str],
|
||||
output_path: str,
|
||||
resolution: float,
|
||||
dem_path: str,
|
||||
) -> str:
|
||||
"""L1B -> L2 RPC geometric correction using GDAL Warp."""
|
||||
from osgeo import gdal
|
||||
|
||||
ds = gdal.Open(l1b_path, gdal.GA_ReadOnly)
|
||||
if ds is None:
|
||||
raise FileNotFoundError(f"Cannot open L1B: {l1b_path}")
|
||||
|
||||
# If RPC not already in dataset, load from .rpb
|
||||
rpc_md = ds.GetMetadata("RPC")
|
||||
if not rpc_md and rpb_path and os.path.isfile(rpb_path):
|
||||
rpc_md = _read_rpb(rpb_path)
|
||||
ds.SetMetadata(rpc_md, "RPC")
|
||||
logger.info("[GF3] Loaded RPC from %s", rpb_path)
|
||||
|
||||
warp_options = gdal.WarpOptions(
|
||||
dstSRS="EPSG:4326",
|
||||
format="GTiff",
|
||||
xRes=resolution,
|
||||
yRes=resolution,
|
||||
rpc=True,
|
||||
creationOptions=["COMPRESS=DEFLATE"],
|
||||
)
|
||||
|
||||
# Use DEM if available
|
||||
if dem_path and os.path.isfile(dem_path):
|
||||
warp_options = gdal.WarpOptions(
|
||||
dstSRS="EPSG:4326",
|
||||
format="GTiff",
|
||||
xRes=resolution,
|
||||
yRes=resolution,
|
||||
rpc=True,
|
||||
transformerOptions=[f"RPC_DEM={dem_path}"],
|
||||
creationOptions=["COMPRESS=DEFLATE"],
|
||||
)
|
||||
|
||||
result = gdal.Warp(output_path, ds, options=warp_options)
|
||||
ds = None
|
||||
|
||||
if result is None:
|
||||
raise RuntimeError(f"GDAL Warp failed for {l1b_path}")
|
||||
result = None
|
||||
|
||||
logger.info("[GF3] Geometric correction done: %s -> %s", l1b_path, output_path)
|
||||
return output_path
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Main pipeline
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def run_gf3_l1a_to_l2(
|
||||
input_dir: str,
|
||||
output_dir: str,
|
||||
resolution: float = 0.0002,
|
||||
job_id: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Run GF3 L1A -> L2 pipeline: extract -> calibrate -> geometric correction.
|
||||
|
||||
Args:
|
||||
input_dir: Path to GF3 L1A product directory (or archive file)
|
||||
output_dir: Output directory for L2 products
|
||||
resolution: Output resolution in degrees (default 0.0002 ~ 20m)
|
||||
job_id: Optional job ID for progress tracking
|
||||
|
||||
Returns:
|
||||
dict with keys: ok, l2_paths, polarizations, output_dir
|
||||
"""
|
||||
from ..config import settings
|
||||
|
||||
dem_path = settings.GF3_GEO_DEM_PATH
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
|
||||
# Step 1: Extract if archive
|
||||
work_dir = input_dir
|
||||
if os.path.isfile(input_dir):
|
||||
logger.info("[GF3] Extracting archive: %s", input_dir)
|
||||
extract_dir = os.path.join(output_dir, "_extracted")
|
||||
_extract_archive(input_dir, extract_dir)
|
||||
# Find actual data directory (may be nested)
|
||||
subdirs = [d for d in os.listdir(extract_dir)
|
||||
if os.path.isdir(os.path.join(extract_dir, d))]
|
||||
work_dir = os.path.join(extract_dir, subdirs[0]) if subdirs else extract_dir
|
||||
|
||||
# Step 2: Scan for XML + TIFF + RPB files
|
||||
xml_path = None
|
||||
tiff_files: Dict[str, str] = {} # pol -> tiff path
|
||||
rpb_files: Dict[str, str] = {} # pol -> rpb path
|
||||
|
||||
for f in os.listdir(work_dir):
|
||||
fl = f.lower()
|
||||
fp = os.path.join(work_dir, f)
|
||||
if fl.endswith(".meta.xml") or (fl.endswith(".xml") and "meta" in fl):
|
||||
xml_path = fp
|
||||
elif fl.endswith(".xml") and xml_path is None:
|
||||
xml_path = fp
|
||||
elif fl.endswith((".tif", ".tiff")):
|
||||
for pol in ("HH", "HV", "VH", "VV"):
|
||||
if pol in f.upper():
|
||||
tiff_files[pol] = fp
|
||||
break
|
||||
elif fl.endswith(".rpb"):
|
||||
for pol in ("HH", "HV", "VH", "VV"):
|
||||
if pol in f.upper():
|
||||
rpb_files[pol] = fp
|
||||
break
|
||||
|
||||
if not xml_path:
|
||||
# Try subdirectories
|
||||
for sub in os.listdir(work_dir):
|
||||
sub_path = os.path.join(work_dir, sub)
|
||||
if os.path.isdir(sub_path):
|
||||
for f in os.listdir(sub_path):
|
||||
fl = f.lower()
|
||||
fp = os.path.join(sub_path, f)
|
||||
if fl.endswith(".xml") and not xml_path:
|
||||
xml_path = fp
|
||||
elif fl.endswith((".tif", ".tiff")):
|
||||
for pol in ("HH", "HV", "VH", "VV"):
|
||||
if pol in f.upper():
|
||||
tiff_files[pol] = fp
|
||||
break
|
||||
elif fl.endswith(".rpb"):
|
||||
for pol in ("HH", "HV", "VH", "VV"):
|
||||
if pol in f.upper():
|
||||
rpb_files[pol] = fp
|
||||
break
|
||||
|
||||
if not tiff_files:
|
||||
return {"ok": False, "error": f"No TIFF files found in {work_dir}"}
|
||||
|
||||
# Step 3: Parse XML metadata
|
||||
meta = {"polarizations": list(tiff_files.keys()), "calibration": {}}
|
||||
if xml_path:
|
||||
try:
|
||||
meta = _parse_gf3_meta(xml_path)
|
||||
except Exception as e:
|
||||
logger.warning("[GF3] Failed to parse XML %s: %s, using defaults", xml_path, e)
|
||||
|
||||
polarizations = meta.get("polarizations", list(tiff_files.keys()))
|
||||
calibration = meta.get("calibration", {})
|
||||
|
||||
# Step 4-5: Process each polarization
|
||||
l2_paths = []
|
||||
for pol in polarizations:
|
||||
if pol not in tiff_files:
|
||||
logger.warning("[GF3] No TIFF found for polarization %s, skipping", pol)
|
||||
continue
|
||||
|
||||
tiff_path = tiff_files[pol]
|
||||
cal_info = calibration.get(pol, {"QualifyValue": 1.0, "CalibrationConst": 0.0})
|
||||
qv = cal_info["QualifyValue"]
|
||||
cal = cal_info["CalibrationConst"]
|
||||
|
||||
# L1A -> L1B (calibration)
|
||||
l1b_path = os.path.join(output_dir, f"{pol}_L1B.tif")
|
||||
logger.info("[GF3] Calibrating %s (QV=%.2f, Cal=%.2f)", pol, qv, cal)
|
||||
_radiometric_calibration(tiff_path, qv, cal, l1b_path)
|
||||
|
||||
# L1B -> L2 (geometric correction)
|
||||
l2_path = os.path.join(output_dir, f"{pol}_L2.tif")
|
||||
rpb_path = rpb_files.get(pol)
|
||||
logger.info("[GF3] Geometric correction %s (resolution=%.6f)", pol, resolution)
|
||||
_geometric_correction(l1b_path, rpb_path, l2_path, resolution, dem_path)
|
||||
|
||||
l2_paths.append(l2_path)
|
||||
|
||||
# Clean up intermediate L1B
|
||||
try:
|
||||
os.remove(l1b_path)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
if not l2_paths:
|
||||
return {"ok": False, "error": "No polarization channels processed successfully"}
|
||||
|
||||
logger.info("[GF3] Pipeline complete: %d L2 products", len(l2_paths))
|
||||
return {
|
||||
"ok": True,
|
||||
"l2_paths": l2_paths,
|
||||
"polarizations": polarizations,
|
||||
"output_dir": output_dir,
|
||||
"input_dir_name": os.path.basename(input_dir),
|
||||
}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Auto-register L2 result into radar_data table
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
async def register_l2_to_radar_data(l2_dir: str, input_dir_name: str, polarizations: List[str], db) -> Optional[int]:
|
||||
"""Register a GF3 L2 output directory as a radar_data record.
|
||||
|
||||
Args:
|
||||
l2_dir: Path to the L2 output directory
|
||||
input_dir_name: Original L1A input directory name (for metadata extraction)
|
||||
polarizations: List of polarization channels processed
|
||||
db: AsyncSession
|
||||
|
||||
Returns:
|
||||
radar_data.id if successfully registered, else None
|
||||
"""
|
||||
from ..utils import parse_gf3_l2_dirname
|
||||
from ..models import RadarDataORM
|
||||
from .data_service import extract_geotiff_bounds
|
||||
from geoalchemy2.shape import from_shape
|
||||
from shapely.geometry import Polygon as ShapelyPolygon
|
||||
from sqlalchemy.future import select
|
||||
|
||||
# Check if already registered (by file_path)
|
||||
result = await db.execute(
|
||||
select(RadarDataORM).where(RadarDataORM.file_path == l2_dir).limit(1)
|
||||
)
|
||||
if result.scalar_one_or_none():
|
||||
logger.info("[GF3] L2 dir already registered: %s", l2_dir)
|
||||
return None
|
||||
|
||||
# Parse metadata from the input directory name
|
||||
meta = parse_gf3_l2_dirname(input_dir_name)
|
||||
if not meta:
|
||||
# Fallback: minimal metadata
|
||||
meta = {
|
||||
"satellite": "GF3",
|
||||
"imaging_date": None,
|
||||
"polarization": ",".join(polarizations) if polarizations else None,
|
||||
}
|
||||
|
||||
# Try to extract polygon from the first L2 GeoTIFF
|
||||
polygon = None
|
||||
try:
|
||||
for f in os.listdir(l2_dir):
|
||||
if f.lower().endswith((".tif", ".tiff")) and "L2" in f:
|
||||
tiff_path = os.path.join(l2_dir, f)
|
||||
polygon = extract_geotiff_bounds(tiff_path)
|
||||
if polygon:
|
||||
break
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
coverage_geom = None
|
||||
if polygon and len(polygon) >= 4:
|
||||
try:
|
||||
shp = ShapelyPolygon(polygon)
|
||||
if shp.is_valid:
|
||||
coverage_geom = from_shape(shp, srid=4326)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
radar = RadarDataORM(
|
||||
file_path=l2_dir,
|
||||
satellite=meta.get("satellite", "GF3"),
|
||||
imaging_date=meta.get("imaging_date"),
|
||||
imaging_mode=meta.get("imaging_mode"),
|
||||
polarization=meta.get("polarization") or (",".join(polarizations) if polarizations else None),
|
||||
scene_center_lon=meta.get("scene_center_lon"),
|
||||
scene_center_lat=meta.get("scene_center_lat"),
|
||||
coverage_polygon=coverage_geom,
|
||||
)
|
||||
db.add(radar)
|
||||
await db.flush()
|
||||
radar_id = radar.id
|
||||
await db.commit()
|
||||
|
||||
logger.info("[GF3] Registered L2 in radar_data: id=%s, path=%s", radar_id, l2_dir)
|
||||
return radar_id
|
||||
Reference in New Issue
Block a user