feat: CoordConverter 坐标转换套件 v1.0.0
TXT / Excel(CSV) / SHP 三种格式任意互转
支持 WGS84 / CGCS2000 / Xian80 / Beijing54 / Web Mercator 坐标系转换
由 Mapo 🗺️ 自动生成
This commit is contained in:
@@ -0,0 +1,131 @@
|
||||
"""
|
||||
TXT file reader/writer.
|
||||
Auto-detects delimiters (space, tab, comma) and column names.
|
||||
Handles optional header line and comment lines (#, //).
|
||||
"""
|
||||
import csv
|
||||
import io
|
||||
from pathlib import Path
|
||||
from core.data_model import CoordData, CoordPoint
|
||||
|
||||
|
||||
class TxtHandler:
|
||||
EXTENSIONS = {".txt", ".csv"}
|
||||
|
||||
@classmethod
|
||||
def read(cls, path: str) -> CoordData:
|
||||
path = Path(path)
|
||||
raw = path.read_text(encoding="utf-8-sig")
|
||||
|
||||
# Strip comment lines
|
||||
lines = [l for l in raw.splitlines()
|
||||
if l.strip() and not l.strip().startswith(("#", "//"))]
|
||||
|
||||
if not lines:
|
||||
raise ValueError(f"文件为空或全为注释: {path}")
|
||||
|
||||
# Detect delimiter
|
||||
delim = cls._detect_delimiter(lines)
|
||||
reader = csv.reader(io.StringIO("\n".join(lines)), delimiter=delim)
|
||||
|
||||
all_rows = list(reader)
|
||||
if not all_rows:
|
||||
raise ValueError(f"无法解析文件: {path}")
|
||||
|
||||
# Check if first row looks like a header (contains non-numeric first field
|
||||
# or known column names)
|
||||
first_row = all_rows[0]
|
||||
has_header = cls._looks_like_header(first_row)
|
||||
|
||||
if has_header:
|
||||
col_names = first_row
|
||||
data_rows = all_rows[1:]
|
||||
else:
|
||||
# No header — auto-generate column names
|
||||
num_cols = len(first_row)
|
||||
col_names = [f"COL{i+1}" for i in range(num_cols)]
|
||||
data_rows = all_rows
|
||||
|
||||
# Detect X, Y, Z columns
|
||||
x_col, y_col, z_col = cls._find_coord_cols(col_names)
|
||||
|
||||
records = []
|
||||
for row in data_rows:
|
||||
record = {}
|
||||
for i, name in enumerate(col_names):
|
||||
val = row[i].strip() if i < len(row) else None
|
||||
record[name] = val
|
||||
records.append(record)
|
||||
|
||||
return CoordData.from_records(records, x_col=x_col, y_col=y_col, z_col=z_col)
|
||||
|
||||
@classmethod
|
||||
def write(cls, data: CoordData, path: str):
|
||||
records = data.to_records()
|
||||
if not records:
|
||||
raise ValueError("无数据可写入")
|
||||
|
||||
delim = cls._detect_delimiter_from_ext(path)
|
||||
fieldnames = list(records[0].keys())
|
||||
|
||||
with open(path, "w", newline="", encoding="utf-8-sig") as f:
|
||||
writer = csv.DictWriter(f, fieldnames=fieldnames, delimiter=delim)
|
||||
writer.writeheader()
|
||||
writer.writerows(records)
|
||||
|
||||
@staticmethod
|
||||
def _looks_like_header(row: list[str]) -> bool:
|
||||
"""Heuristic: header if first few entries are non-numeric strings."""
|
||||
if not row:
|
||||
return False
|
||||
# If any column name matches known coord patterns, it's a header
|
||||
lower = {c.lower().strip() for c in row[:5]}
|
||||
coord_keywords = {"x", "y", "lon", "lat", "经度", "纬度", "点号", "id"}
|
||||
if lower & coord_keywords:
|
||||
return True
|
||||
# If the first entry is not a valid number, it's probably a header
|
||||
try:
|
||||
float(row[0].strip())
|
||||
return False
|
||||
except (ValueError, AttributeError):
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def _detect_delimiter(lines: list[str]) -> str:
|
||||
"""Auto-detect: tab > comma > space."""
|
||||
sample = "\n".join(lines[:20])
|
||||
tab_count = sample.count("\t")
|
||||
comma_count = sample.count(",")
|
||||
# Check if commas are between spaces (likely CSV) vs within text
|
||||
if comma_count > 3 or comma_count > tab_count:
|
||||
return ","
|
||||
if tab_count > 0:
|
||||
return "\t"
|
||||
return " " # default: space-separated
|
||||
|
||||
@staticmethod
|
||||
def _detect_delimiter_from_ext(path: str) -> str:
|
||||
return "," if path.lower().endswith(".csv") else "\t"
|
||||
|
||||
@staticmethod
|
||||
def _find_coord_cols(cols: list[str]) -> tuple[str, str, str | None]:
|
||||
"""Find X, Y, Z columns."""
|
||||
x_patterns = ["x", "lon", "经度", "lng", "easting"]
|
||||
y_patterns = ["y", "lat", "纬度", "northing"]
|
||||
z_patterns = ["z", "h", "高程", "高度", "alt", "elevation"]
|
||||
|
||||
x_col = y_col = z_col = None
|
||||
for c in cols:
|
||||
lc = c.lower().replace(" ", "").replace("_", "").replace("-", "")
|
||||
if any(p == lc or lc.startswith(p) or lc.endswith(p) for p in x_patterns):
|
||||
x_col = c
|
||||
elif any(p == lc or lc.startswith(p) or lc.endswith(p) for p in y_patterns):
|
||||
y_col = c
|
||||
elif any(p == lc or lc.startswith(p) or lc.endswith(p) for p in z_patterns):
|
||||
z_col = c
|
||||
|
||||
# Fallback: first two numeric-ish columns if no match
|
||||
if not x_col and len(cols) >= 2:
|
||||
x_col, y_col = cols[0], cols[1]
|
||||
|
||||
return x_col, y_col, z_col
|
||||
Reference in New Issue
Block a user