175 lines
6.1 KiB
Python
175 lines
6.1 KiB
Python
from __future__ import annotations
|
|
|
|
import os
|
|
from dataclasses import dataclass
|
|
|
|
import pymysql
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class DbConfig:
|
|
host: str = os.getenv("NAVSEA_DB_HOST", "localhost")
|
|
port: int = int(os.getenv("NAVSEA_DB_PORT", "3306"))
|
|
user: str = os.getenv("NAVSEA_DB_USER", "root")
|
|
password: str = os.getenv("NAVSEA_DB_PASSWORD", "2chi9ks2")
|
|
database: str = os.getenv("NAVSEA_DB_NAME", "pbf_analysis")
|
|
unix_socket: str | None = os.getenv("NAVSEA_DB_SOCKET", "/tmp/mysql.sock")
|
|
source_table: str = os.getenv("NAVSEA_GEOMETRY_SOURCE_TABLE", "feature_geometry_stage")
|
|
|
|
|
|
class GeometryEnablement:
|
|
def __init__(self, config: DbConfig) -> None:
|
|
self.config = config
|
|
|
|
def connect(self):
|
|
kwargs = {
|
|
"host": self.config.host,
|
|
"port": self.config.port,
|
|
"user": self.config.user,
|
|
"password": self.config.password,
|
|
"database": self.config.database,
|
|
"charset": "utf8mb4",
|
|
"autocommit": False,
|
|
}
|
|
if self.config.unix_socket and self.config.host in {"localhost", "127.0.0.1"}:
|
|
kwargs["unix_socket"] = self.config.unix_socket
|
|
return pymysql.connect(**kwargs)
|
|
|
|
def run(self) -> None:
|
|
with self.connect() as conn:
|
|
with conn.cursor() as cur:
|
|
source_mode = self.detect_source_mode(cur)
|
|
self.create_geometry_table(cur, source_mode)
|
|
self.create_spatial_view(cur)
|
|
conn.commit()
|
|
print("Built navsea_feature_geometry and navsea_detection_objects_spatial")
|
|
|
|
def detect_source_mode(self, cur) -> str:
|
|
cur.execute("SHOW TABLES LIKE %s", (self.config.source_table,))
|
|
if cur.fetchone() is None:
|
|
raise RuntimeError(
|
|
"geometry staging table missing: "
|
|
f"{self.config.source_table}. "
|
|
"Expected one of: "
|
|
"(feature_id, geometry_wkt), "
|
|
"(feature_id, geometry_wkb), "
|
|
"(feature_id, lon, lat), "
|
|
"or (feature_id, geometry GEOMETRY)."
|
|
)
|
|
|
|
cur.execute(
|
|
"""
|
|
SELECT COLUMN_NAME, DATA_TYPE
|
|
FROM information_schema.COLUMNS
|
|
WHERE TABLE_SCHEMA=%s AND TABLE_NAME=%s
|
|
""",
|
|
(self.config.database, self.config.source_table),
|
|
)
|
|
columns = {name: data_type for name, data_type in cur.fetchall()}
|
|
|
|
if "feature_id" not in columns:
|
|
raise RuntimeError(f"{self.config.source_table} missing required column: feature_id")
|
|
|
|
if "geometry" in columns and columns["geometry"] == "geometry":
|
|
return "geometry"
|
|
if "geometry_wkt" in columns:
|
|
return "geometry_wkt"
|
|
if "geometry_wkb" in columns:
|
|
return "geometry_wkb"
|
|
if {"lon", "lat"}.issubset(columns):
|
|
return "lonlat"
|
|
|
|
raise RuntimeError(
|
|
f"{self.config.source_table} does not expose a supported geometry source. "
|
|
"Supported layouts: geometry GEOMETRY, geometry_wkt, geometry_wkb, or lon/lat."
|
|
)
|
|
|
|
def create_geometry_table(self, cur, source_mode: str) -> None:
|
|
cur.execute("DROP TABLE IF EXISTS navsea_feature_geometry")
|
|
cur.execute(
|
|
"""
|
|
CREATE TABLE navsea_feature_geometry (
|
|
feature_id BIGINT NOT NULL,
|
|
geometry GEOMETRY NOT NULL,
|
|
geometry_source VARCHAR(32) NOT NULL,
|
|
PRIMARY KEY (feature_id),
|
|
SPATIAL KEY idx_geometry (geometry)
|
|
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4
|
|
"""
|
|
)
|
|
|
|
source_table = self.config.source_table
|
|
if source_mode == "geometry":
|
|
cur.execute(
|
|
f"""
|
|
INSERT INTO navsea_feature_geometry (feature_id, geometry, geometry_source)
|
|
SELECT feature_id, geometry, 'geometry'
|
|
FROM {source_table}
|
|
"""
|
|
)
|
|
elif source_mode == "geometry_wkt":
|
|
cur.execute(
|
|
f"""
|
|
INSERT INTO navsea_feature_geometry (feature_id, geometry, geometry_source)
|
|
SELECT feature_id, GeomFromText(geometry_wkt), 'geometry_wkt'
|
|
FROM {source_table}
|
|
WHERE geometry_wkt IS NOT NULL AND geometry_wkt <> ''
|
|
"""
|
|
)
|
|
elif source_mode == "geometry_wkb":
|
|
cur.execute(
|
|
f"""
|
|
INSERT INTO navsea_feature_geometry (feature_id, geometry, geometry_source)
|
|
SELECT feature_id, GeomFromWKB(geometry_wkb), 'geometry_wkb'
|
|
FROM {source_table}
|
|
WHERE geometry_wkb IS NOT NULL
|
|
"""
|
|
)
|
|
elif source_mode == "lonlat":
|
|
cur.execute(
|
|
f"""
|
|
INSERT INTO navsea_feature_geometry (feature_id, geometry, geometry_source)
|
|
SELECT feature_id, Point(lon, lat), 'lonlat'
|
|
FROM {source_table}
|
|
WHERE lon IS NOT NULL AND lat IS NOT NULL
|
|
"""
|
|
)
|
|
else:
|
|
raise RuntimeError(f"unsupported source mode: {source_mode}")
|
|
|
|
@staticmethod
|
|
def create_spatial_view(cur) -> None:
|
|
cur.execute("DROP VIEW IF EXISTS navsea_detection_objects_spatial")
|
|
cur.execute(
|
|
"""
|
|
CREATE VIEW navsea_detection_objects_spatial AS
|
|
SELECT
|
|
d.feature_id,
|
|
g.geometry,
|
|
d.z,
|
|
d.x,
|
|
d.y,
|
|
d.geom_type,
|
|
d.source_layer,
|
|
d.class_name,
|
|
d.shape_name,
|
|
d.layer_name,
|
|
d.canonical_object_type,
|
|
d.object_family,
|
|
d.detection_class,
|
|
d.capability,
|
|
d.detection_key
|
|
FROM navsea_detection_objects d
|
|
JOIN navsea_feature_geometry g
|
|
ON d.feature_id = g.feature_id
|
|
"""
|
|
)
|
|
|
|
|
|
def main() -> None:
|
|
GeometryEnablement(DbConfig()).run()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|