feat: CoordConverter 坐标转换套件 v1.0.0
TXT / Excel(CSV) / SHP 三种格式任意互转
支持 WGS84 / CGCS2000 / Xian80 / Beijing54 / Web Mercator 坐标系转换
由 Mapo 🗺️ 自动生成
This commit is contained in:
@@ -0,0 +1 @@
|
||||
# core/__init__.py
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,68 @@
|
||||
"""
|
||||
Coordinate transformation engine — wraps pyproj for all CRS conversions.
|
||||
"""
|
||||
import pyproj
|
||||
from typing import Tuple
|
||||
|
||||
# Well-known EPSG codes
|
||||
WELL_KNOWN = {
|
||||
"WGS84": "EPSG:4326",
|
||||
"CGCS2000": "EPSG:4490",
|
||||
"Xian80": "EPSG:4610",
|
||||
"Beijing54": "EPSG:4214",
|
||||
"WebMercator": "EPSG:3857",
|
||||
"PseudoMercator": "EPSG:3857",
|
||||
}
|
||||
|
||||
|
||||
def resolve_epsg(name: str) -> str:
|
||||
"""Resolve a well-known name or return as-is if already EPSG:xxxx."""
|
||||
name = name.strip()
|
||||
if name.upper() in WELL_KNOWN:
|
||||
return WELL_KNOWN[name.upper()]
|
||||
if name.upper().startswith("EPSG:"):
|
||||
return name.upper()
|
||||
return f"EPSG:{name}"
|
||||
|
||||
|
||||
class CoordTransformer:
|
||||
"""Convert coordinates between any two CRS using pyproj."""
|
||||
|
||||
def __init__(self, from_crs: str, to_crs: str):
|
||||
self.from_crs = resolve_epsg(from_crs)
|
||||
self.to_crs = resolve_epsg(to_crs)
|
||||
if self.from_crs == self.to_crs:
|
||||
self._is_identity = True
|
||||
self._transformer = None
|
||||
else:
|
||||
self._is_identity = False
|
||||
self._transformer = pyproj.Transformer.from_crs(
|
||||
self.from_crs, self.to_crs, always_xy=True
|
||||
)
|
||||
|
||||
def transform(self, x: float, y: float) -> Tuple[float, float]:
|
||||
if self._is_identity:
|
||||
return x, y
|
||||
return self._transformer.transform(x, y)
|
||||
|
||||
def transform_batch(self, xs: list[float], ys: list[float]) -> Tuple[list[float], list[float]]:
|
||||
if self._is_identity:
|
||||
return xs, ys
|
||||
results = self._transformer.transform(xs, ys)
|
||||
return results[0], results[1]
|
||||
|
||||
@property
|
||||
def description(self) -> str:
|
||||
if self._is_identity:
|
||||
return f"{self.from_crs} (无转换)"
|
||||
return f"{self.from_crs} → {self.to_crs}"
|
||||
|
||||
@staticmethod
|
||||
def list_supported() -> list[dict]:
|
||||
return [
|
||||
{"name": "WGS84", "epsg": "EPSG:4326", "desc": "GPS / Google Earth"},
|
||||
{"name": "CGCS2000", "epsg": "EPSG:4490", "desc": "2000国家大地坐标系"},
|
||||
{"name": "Xian80", "epsg": "EPSG:4610", "desc": "西安80坐标系"},
|
||||
{"name": "Beijing54", "epsg": "EPSG:4214", "desc": "北京54坐标系"},
|
||||
{"name": "Web Mercator","epsg": "EPSG:3857", "desc": "互联网地图投影"},
|
||||
]
|
||||
@@ -0,0 +1,57 @@
|
||||
"""
|
||||
Data model — unified internal representation for all coordinate data.
|
||||
"""
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
|
||||
|
||||
@dataclass
|
||||
class CoordPoint:
|
||||
x: float
|
||||
y: float
|
||||
z: float | None = None
|
||||
attrs: dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
|
||||
@dataclass
|
||||
class CoordData:
|
||||
"""Unified container for all coordinate data regardless of source format."""
|
||||
points: list[CoordPoint] = field(default_factory=list)
|
||||
crs: str | None = None # e.g. "EPSG:4326"
|
||||
columns: list[str] = field(default_factory=list) # all column names
|
||||
geometry_type: str = "Point" # Point / LineString / Polygon
|
||||
extra_meta: dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
@property
|
||||
def count(self) -> int:
|
||||
return len(self.points)
|
||||
|
||||
def to_records(self) -> list[dict[str, Any]]:
|
||||
"""Convert to list of flat dicts for DataFrame/Excel export."""
|
||||
records = []
|
||||
for pt in self.points:
|
||||
row = dict(pt.attrs)
|
||||
# Always put X, Y (and Z) at front
|
||||
row["X"] = pt.x
|
||||
row["Y"] = pt.y
|
||||
if pt.z is not None:
|
||||
row["Z"] = pt.z
|
||||
records.append(row)
|
||||
return records
|
||||
|
||||
@classmethod
|
||||
def from_records(cls, records: list[dict], crs: str | None = None,
|
||||
x_col: str = "X", y_col: str = "Y", z_col: str | None = "Z",
|
||||
geometry_type: str = "Point") -> "CoordData":
|
||||
points = []
|
||||
for rec in records:
|
||||
pt = CoordPoint(
|
||||
x=float(rec[x_col]),
|
||||
y=float(rec[y_col]),
|
||||
z=float(rec[z_col]) if z_col and z_col in rec else None,
|
||||
attrs={k: v for k, v in rec.items()
|
||||
if k not in (x_col, y_col, z_col)},
|
||||
)
|
||||
points.append(pt)
|
||||
cols = list(records[0].keys()) if records else []
|
||||
return cls(points=points, crs=crs, columns=cols, geometry_type=geometry_type)
|
||||
Reference in New Issue
Block a user