TXT / Excel(CSV) / SHP 三种格式任意互转
支持 WGS84 / CGCS2000 / Xian80 / Beijing54 / Web Mercator 坐标系转换
由 Mapo 🗺️ 自动生成
132 lines
4.6 KiB
Python
132 lines
4.6 KiB
Python
"""
|
|
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
|