coordshift 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- coordshift/__init__.py +13 -0
- coordshift/cli.py +145 -0
- coordshift/core.py +126 -0
- coordshift/crs.py +95 -0
- coordshift/io.py +65 -0
- coordshift/presets.py +69 -0
- coordshift-0.1.0.dist-info/METADATA +242 -0
- coordshift-0.1.0.dist-info/RECORD +12 -0
- coordshift-0.1.0.dist-info/WHEEL +5 -0
- coordshift-0.1.0.dist-info/entry_points.txt +2 -0
- coordshift-0.1.0.dist-info/licenses/LICENSE +21 -0
- coordshift-0.1.0.dist-info/top_level.txt +1 -0
coordshift/__init__.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""
|
|
2
|
+
coordshift — Universal coordinate system conversion for CSV and tabular data.
|
|
3
|
+
|
|
4
|
+
Basic usage:
|
|
5
|
+
from coordshift import convert
|
|
6
|
+
df = convert("input.csv", from_crs="EPSG:4326", to_crs="EPSG:2965", x="lon", y="lat")
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from coordshift.core import convert
|
|
10
|
+
from coordshift.crs import resolve_crs, search_crs
|
|
11
|
+
|
|
12
|
+
__version__ = "0.1.0"
|
|
13
|
+
__all__ = ["convert", "resolve_crs", "search_crs"]
|
coordshift/cli.py
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""
|
|
2
|
+
cli.py — Command line interface for coordshift.
|
|
3
|
+
|
|
4
|
+
Built with Click. This file is intentionally thin — it parses arguments
|
|
5
|
+
and calls functions from core.py and io.py. No conversion logic lives here.
|
|
6
|
+
|
|
7
|
+
Entry point is defined in pyproject.toml:
|
|
8
|
+
[project.scripts]
|
|
9
|
+
coordshift = "coordshift.cli:cli"
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import sys
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
import click
|
|
18
|
+
from pandas.errors import EmptyDataError, ParserError
|
|
19
|
+
|
|
20
|
+
from coordshift import __version__
|
|
21
|
+
from coordshift.core import convert as convert_file
|
|
22
|
+
from coordshift.crs import CRSError, search_crs
|
|
23
|
+
from coordshift.presets import PRESETS
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _configure_utf8_stdio() -> None:
|
|
27
|
+
"""Use UTF-8 for stdout/stderr when possible (Windows consoles often default to cp1252)."""
|
|
28
|
+
for stream in (getattr(sys, "stdout", None), getattr(sys, "stderr", None)):
|
|
29
|
+
if stream is None or not hasattr(stream, "reconfigure"):
|
|
30
|
+
continue
|
|
31
|
+
try:
|
|
32
|
+
stream.reconfigure(encoding="utf-8")
|
|
33
|
+
except (AttributeError, OSError, ValueError):
|
|
34
|
+
continue
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
_configure_utf8_stdio()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _default_output_path(input_path: str) -> str:
|
|
41
|
+
"""Return input path with ``_converted`` inserted before the file extension."""
|
|
42
|
+
p = Path(input_path)
|
|
43
|
+
if p.suffix:
|
|
44
|
+
return str(p.with_name(f"{p.stem}_converted{p.suffix}"))
|
|
45
|
+
return str(p.with_name(f"{p.name}_converted"))
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _format_preset_line(name: str, epsg: str, description: str) -> str:
|
|
49
|
+
"""Format one preset for terminal output."""
|
|
50
|
+
return f"{name} → {epsg} — {description}"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@click.group()
|
|
54
|
+
@click.version_option(version=__version__)
|
|
55
|
+
def cli():
|
|
56
|
+
"""coordshift — Universal coordinate system conversion for CSV data."""
|
|
57
|
+
pass
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@cli.command()
|
|
61
|
+
@click.argument("filepath", type=click.Path(exists=True))
|
|
62
|
+
@click.option("--from", "from_crs", required=True, help="Source CRS (e.g. EPSG:4326, wgs84)")
|
|
63
|
+
@click.option("--to", "to_crs", required=True, help="Target CRS (e.g. EPSG:2965, indiana-east)")
|
|
64
|
+
@click.option("--x", default=None, help="X/longitude/easting column name (auto-detected if omitted)")
|
|
65
|
+
@click.option("--y", default=None, help="Y/latitude/northing column name (auto-detected if omitted)")
|
|
66
|
+
@click.option(
|
|
67
|
+
"--suffix",
|
|
68
|
+
default="_converted",
|
|
69
|
+
show_default=True,
|
|
70
|
+
help="Suffix for the output column names (e.g. '_proj' produces lon_proj, lat_proj). "
|
|
71
|
+
"Original X/Y columns are always kept; converted values go into new columns "
|
|
72
|
+
"placed right after the originals.",
|
|
73
|
+
)
|
|
74
|
+
@click.option("--out", default=None, help="Output file path (default: input_converted.csv)")
|
|
75
|
+
def convert(filepath, from_crs, to_crs, x, y, suffix, out):
|
|
76
|
+
"""Convert coordinate columns in a CSV file from one CRS to another.
|
|
77
|
+
|
|
78
|
+
Original X/Y columns are preserved. Converted values are written to new
|
|
79
|
+
columns (e.g. lon_converted, lat_converted) placed immediately after the
|
|
80
|
+
originals. Use --suffix to customise the column name suffix.
|
|
81
|
+
"""
|
|
82
|
+
output_path = out if out else _default_output_path(filepath)
|
|
83
|
+
x_col = x if x else None
|
|
84
|
+
y_col = y if y else None
|
|
85
|
+
|
|
86
|
+
try:
|
|
87
|
+
df = convert_file(
|
|
88
|
+
filepath,
|
|
89
|
+
from_crs,
|
|
90
|
+
to_crs,
|
|
91
|
+
x=x_col,
|
|
92
|
+
y=y_col,
|
|
93
|
+
output=output_path,
|
|
94
|
+
suffix=suffix,
|
|
95
|
+
)
|
|
96
|
+
except (
|
|
97
|
+
CRSError,
|
|
98
|
+
ValueError,
|
|
99
|
+
KeyError,
|
|
100
|
+
OSError,
|
|
101
|
+
TypeError,
|
|
102
|
+
EmptyDataError,
|
|
103
|
+
ParserError,
|
|
104
|
+
FileNotFoundError,
|
|
105
|
+
) as e:
|
|
106
|
+
click.secho(str(e), fg="red")
|
|
107
|
+
raise SystemExit(1) from e
|
|
108
|
+
|
|
109
|
+
n = len(df)
|
|
110
|
+
click.echo(
|
|
111
|
+
f"Converted {n} row(s) from {from_crs!r} to {to_crs!r}. "
|
|
112
|
+
f"Output: {output_path}"
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@cli.command()
|
|
117
|
+
@click.argument("query")
|
|
118
|
+
def search(query):
|
|
119
|
+
"""Search for a CRS by name or keyword. Example: coordshift search 'indiana'"""
|
|
120
|
+
results = search_crs(query)
|
|
121
|
+
if not results:
|
|
122
|
+
click.echo(f"No presets found matching: {query}")
|
|
123
|
+
return
|
|
124
|
+
for row in sorted(results, key=lambda r: r["name"]):
|
|
125
|
+
click.echo(
|
|
126
|
+
_format_preset_line(
|
|
127
|
+
row["name"],
|
|
128
|
+
str(row["epsg"]),
|
|
129
|
+
str(row["description"]),
|
|
130
|
+
)
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@cli.command()
|
|
135
|
+
def list_crs():
|
|
136
|
+
"""List all built-in CRS presets."""
|
|
137
|
+
for name in sorted(PRESETS.keys()):
|
|
138
|
+
data = PRESETS[name]
|
|
139
|
+
click.echo(
|
|
140
|
+
_format_preset_line(
|
|
141
|
+
name,
|
|
142
|
+
str(data["epsg"]),
|
|
143
|
+
str(data.get("description", "")),
|
|
144
|
+
)
|
|
145
|
+
)
|
coordshift/core.py
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""
|
|
2
|
+
core.py — The main conversion logic.
|
|
3
|
+
|
|
4
|
+
This is the heart of coordshift. Everything else (CLI, file I/O) calls
|
|
5
|
+
functions from here. Keeping conversion logic here means it can be used
|
|
6
|
+
both from the command line and from Python scripts.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import pandas as pd
|
|
10
|
+
from pyproj import Transformer
|
|
11
|
+
from pyproj.exceptions import CRSError as PyprojCRSError
|
|
12
|
+
|
|
13
|
+
from coordshift.crs import CRSError, resolve_crs
|
|
14
|
+
|
|
15
|
+
from . import io
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def transform_points(
|
|
19
|
+
xs: list[float],
|
|
20
|
+
ys: list[float],
|
|
21
|
+
from_crs: str,
|
|
22
|
+
to_crs: str,
|
|
23
|
+
) -> tuple[list[float], list[float]]:
|
|
24
|
+
"""
|
|
25
|
+
Transform a list of X/Y points from one CRS to another.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
xs: List of X values (longitude or easting).
|
|
29
|
+
ys: List of Y values (latitude or northing).
|
|
30
|
+
from_crs: Source CRS string (e.g. from ``resolve_crs`` or an EPSG code).
|
|
31
|
+
to_crs: Target CRS string in the same forms as ``from_crs``.
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
Tuple of (transformed_xs, transformed_ys).
|
|
35
|
+
"""
|
|
36
|
+
try:
|
|
37
|
+
transformer = Transformer.from_crs(from_crs, to_crs, always_xy=True)
|
|
38
|
+
except PyprojCRSError as e:
|
|
39
|
+
raise CRSError(
|
|
40
|
+
f"Could not build coordinate transform from {from_crs!r} to {to_crs!r}: {e}. "
|
|
41
|
+
"Use resolvable CRS strings or preset names. "
|
|
42
|
+
"Run `coordshift search` to list presets."
|
|
43
|
+
) from e
|
|
44
|
+
tx, ty = transformer.transform(xs, ys)
|
|
45
|
+
return [float(v) for v in tx], [float(v) for v in ty]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def convert(
|
|
49
|
+
filepath: str,
|
|
50
|
+
from_crs: str,
|
|
51
|
+
to_crs: str,
|
|
52
|
+
x: str | None = None,
|
|
53
|
+
y: str | None = None,
|
|
54
|
+
output: str | None = None,
|
|
55
|
+
suffix: str = "_converted",
|
|
56
|
+
) -> pd.DataFrame:
|
|
57
|
+
"""
|
|
58
|
+
Convert coordinate columns in a CSV file from one CRS to another.
|
|
59
|
+
|
|
60
|
+
The original X/Y columns are always preserved. Converted values are written
|
|
61
|
+
to new columns named ``{x}{suffix}`` and ``{y}{suffix}``, inserted immediately
|
|
62
|
+
after their respective originals.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
filepath: Path to the input CSV file.
|
|
66
|
+
from_crs: Source CRS — EPSG code (e.g. "EPSG:4326"), PROJ string, or preset name.
|
|
67
|
+
to_crs: Target CRS — same formats accepted.
|
|
68
|
+
x: Name of the X/longitude/easting column. Auto-detected if not provided.
|
|
69
|
+
y: Name of the Y/latitude/northing column. Auto-detected if not provided.
|
|
70
|
+
output: Path to save the output CSV. If None, returns DataFrame only.
|
|
71
|
+
suffix: Suffix appended to X/Y column names to form the output column names
|
|
72
|
+
(default ``"_converted"``). For example, with ``x="lon"`` and
|
|
73
|
+
``suffix="_proj"``, the output column is ``"lon_proj"``.
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
pandas DataFrame with original coordinate columns preserved and new converted
|
|
77
|
+
columns inserted immediately after them. All other columns are unchanged.
|
|
78
|
+
"""
|
|
79
|
+
from_resolved = resolve_crs(from_crs)
|
|
80
|
+
to_resolved = resolve_crs(to_crs)
|
|
81
|
+
|
|
82
|
+
df_in = io.read_csv(filepath)
|
|
83
|
+
x_col = x
|
|
84
|
+
y_col = y
|
|
85
|
+
if x_col is None or y_col is None:
|
|
86
|
+
auto_x, auto_y = io.detect_columns(df_in)
|
|
87
|
+
if x_col is None:
|
|
88
|
+
x_col = auto_x
|
|
89
|
+
if y_col is None:
|
|
90
|
+
y_col = auto_y
|
|
91
|
+
|
|
92
|
+
if x_col is None or y_col is None:
|
|
93
|
+
raise ValueError(
|
|
94
|
+
"Could not determine X/Y columns. "
|
|
95
|
+
"Pass x= and y= with column names, or use recognizable names such as "
|
|
96
|
+
f"lon/lat (hints: {io.X_COLUMN_HINTS}, {io.Y_COLUMN_HINTS})."
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
x_out = f"{x_col}{suffix}"
|
|
100
|
+
y_out = f"{y_col}{suffix}"
|
|
101
|
+
for name in (x_out, y_out):
|
|
102
|
+
if name in df_in.columns:
|
|
103
|
+
raise ValueError(
|
|
104
|
+
f"Column {name!r} already exists. Choose a different --suffix or rename the input column."
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
out = df_in.copy()
|
|
108
|
+
xs = out[x_col].astype(float).tolist()
|
|
109
|
+
ys = out[y_col].astype(float).tolist()
|
|
110
|
+
new_xs, new_ys = transform_points(xs, ys, from_resolved, to_resolved)
|
|
111
|
+
|
|
112
|
+
out[x_out] = new_xs
|
|
113
|
+
out[y_out] = new_ys
|
|
114
|
+
|
|
115
|
+
# Insert the new columns immediately after their originals
|
|
116
|
+
cols = [c for c in out.columns if c not in (x_out, y_out)]
|
|
117
|
+
x_idx = cols.index(x_col)
|
|
118
|
+
cols.insert(x_idx + 1, x_out)
|
|
119
|
+
y_idx = cols.index(y_col)
|
|
120
|
+
cols.insert(y_idx + 1, y_out)
|
|
121
|
+
out = out[cols]
|
|
122
|
+
|
|
123
|
+
if output is not None:
|
|
124
|
+
io.write_csv(out, output)
|
|
125
|
+
|
|
126
|
+
return out
|
coordshift/crs.py
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""
|
|
2
|
+
crs.py — CRS resolution and search.
|
|
3
|
+
|
|
4
|
+
Responsible for turning whatever the user types (EPSG code, PROJ string,
|
|
5
|
+
friendly name like "indiana-east") into something pyproj can use.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from pyproj import CRS
|
|
9
|
+
from pyproj.exceptions import CRSError as PyprojCRSError
|
|
10
|
+
|
|
11
|
+
from coordshift.presets import PRESETS
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class CRSError(ValueError):
|
|
15
|
+
"""Raised when a CRS string cannot be resolved."""
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _normalize_preset_key(user_input: str) -> str:
|
|
20
|
+
"""Normalize user text for lookup in PRESETS (lowercase, hyphenated)."""
|
|
21
|
+
return user_input.strip().lower().replace(" ", "-")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def resolve_crs(crs_input: str) -> str:
|
|
25
|
+
"""
|
|
26
|
+
Resolve a CRS input string to a canonical form pyproj can use.
|
|
27
|
+
|
|
28
|
+
Accepts:
|
|
29
|
+
- EPSG codes as string: "EPSG:4326" or "4326"
|
|
30
|
+
- PROJ strings: "+proj=longlat +datum=WGS84"
|
|
31
|
+
- Preset names: "wgs84", "indiana-east", "nad83"
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
crs_input: The CRS string to resolve.
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
A CRS string ready for pyproj.
|
|
38
|
+
|
|
39
|
+
Raises:
|
|
40
|
+
CRSError: If the CRS cannot be resolved.
|
|
41
|
+
"""
|
|
42
|
+
raw = crs_input.strip()
|
|
43
|
+
if not raw:
|
|
44
|
+
raise CRSError(
|
|
45
|
+
"Empty CRS input. Provide an EPSG code, PROJ string, or preset name "
|
|
46
|
+
"(try `coordshift search`)."
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
preset_key = _normalize_preset_key(raw)
|
|
50
|
+
if preset_key in PRESETS:
|
|
51
|
+
return str(PRESETS[preset_key]["epsg"])
|
|
52
|
+
|
|
53
|
+
try:
|
|
54
|
+
crs = CRS(raw)
|
|
55
|
+
except PyprojCRSError as e:
|
|
56
|
+
raise CRSError(
|
|
57
|
+
f"Could not resolve CRS {raw!r}: {e}. "
|
|
58
|
+
"Try an EPSG code, a PROJ string, or a preset name. "
|
|
59
|
+
"Run `coordshift search` to list presets."
|
|
60
|
+
) from e
|
|
61
|
+
|
|
62
|
+
epsg = crs.to_epsg()
|
|
63
|
+
if epsg is not None:
|
|
64
|
+
return f"EPSG:{epsg}"
|
|
65
|
+
return crs.to_wkt()
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def search_crs(query: str) -> list[dict]:
|
|
69
|
+
"""
|
|
70
|
+
Search for CRS presets matching a query string.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
query: Search term (e.g. "indiana", "state plane", "utm zone 16").
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
List of matching preset dicts with keys: name, epsg, description.
|
|
77
|
+
"""
|
|
78
|
+
q = query.strip().lower()
|
|
79
|
+
if not q:
|
|
80
|
+
return []
|
|
81
|
+
|
|
82
|
+
matches: list[dict] = []
|
|
83
|
+
for name, data in PRESETS.items():
|
|
84
|
+
description = str(data.get("description", ""))
|
|
85
|
+
haystack_name = name.lower()
|
|
86
|
+
haystack_desc = description.lower()
|
|
87
|
+
if q in haystack_name or q in haystack_desc:
|
|
88
|
+
matches.append(
|
|
89
|
+
{
|
|
90
|
+
"name": name,
|
|
91
|
+
"epsg": data["epsg"],
|
|
92
|
+
"description": description,
|
|
93
|
+
}
|
|
94
|
+
)
|
|
95
|
+
return matches
|
coordshift/io.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""
|
|
2
|
+
io.py — File reading, column detection, and output writing.
|
|
3
|
+
|
|
4
|
+
Handles everything related to getting data in and out of coordshift.
|
|
5
|
+
Kept separate from core.py so conversion logic stays clean and testable
|
|
6
|
+
without needing actual files.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import pandas as pd
|
|
10
|
+
|
|
11
|
+
# Common column name patterns we'll try to auto-detect
|
|
12
|
+
X_COLUMN_HINTS = ["lon", "longitude", "long", "x", "easting", "x_coord", "lng"]
|
|
13
|
+
Y_COLUMN_HINTS = ["lat", "latitude", "y", "northing", "y_coord"]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _first_column_matching_hints(df: pd.DataFrame, hints: list[str]) -> str | None:
|
|
17
|
+
"""Return the first DataFrame column whose normalized name matches a hint."""
|
|
18
|
+
hint_set = {h.lower() for h in hints}
|
|
19
|
+
for col in df.columns:
|
|
20
|
+
normalized = str(col).strip().lower()
|
|
21
|
+
if normalized in hint_set:
|
|
22
|
+
return str(col)
|
|
23
|
+
return None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def read_csv(filepath: str) -> pd.DataFrame:
|
|
27
|
+
"""
|
|
28
|
+
Read a CSV file into a pandas DataFrame.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
filepath: Path to the CSV file.
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
pandas DataFrame.
|
|
35
|
+
"""
|
|
36
|
+
return pd.read_csv(filepath)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def detect_columns(df: pd.DataFrame) -> tuple[str | None, str | None]:
|
|
40
|
+
"""
|
|
41
|
+
Try to auto-detect which columns contain X and Y coordinates.
|
|
42
|
+
|
|
43
|
+
Checks column names against known patterns (lon, lat, easting, etc.)
|
|
44
|
+
Case-insensitive. Returns None for either if it can't find a match.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
df: The input DataFrame.
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
Tuple of (x_column_name, y_column_name). Either may be None.
|
|
51
|
+
"""
|
|
52
|
+
x_col = _first_column_matching_hints(df, X_COLUMN_HINTS)
|
|
53
|
+
y_col = _first_column_matching_hints(df, Y_COLUMN_HINTS)
|
|
54
|
+
return x_col, y_col
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def write_csv(df: pd.DataFrame, filepath: str) -> None:
|
|
58
|
+
"""
|
|
59
|
+
Write a DataFrame to a CSV file.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
df: The DataFrame to write.
|
|
63
|
+
filepath: Output file path.
|
|
64
|
+
"""
|
|
65
|
+
df.to_csv(filepath, index=False)
|
coordshift/presets.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""
|
|
2
|
+
presets.py — Friendly name to EPSG mappings.
|
|
3
|
+
|
|
4
|
+
Lets users type "indiana-east" instead of "EPSG:2965". Add more as needed.
|
|
5
|
+
Organized by category. EPSG realizations vary by entry: most State Plane zones
|
|
6
|
+
use classic NAD83; Iowa uses NAD83(2011). Check each entry's description.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
PRESETS: dict[str, dict] = {
|
|
10
|
+
# --- Geographic (lat/lon) ---
|
|
11
|
+
"wgs84": {
|
|
12
|
+
"epsg": "EPSG:4326",
|
|
13
|
+
"description": "WGS84 Geographic (lat/lon) — GPS default",
|
|
14
|
+
},
|
|
15
|
+
"nad83": {
|
|
16
|
+
"epsg": "EPSG:4269",
|
|
17
|
+
"description": "NAD83 Geographic (lat/lon) — North America",
|
|
18
|
+
},
|
|
19
|
+
|
|
20
|
+
# --- Indiana State Plane ---
|
|
21
|
+
"indiana-east": {
|
|
22
|
+
"epsg": "EPSG:2965",
|
|
23
|
+
"description": "Indiana State Plane East (NAD83, meters)",
|
|
24
|
+
},
|
|
25
|
+
"indiana-west": {
|
|
26
|
+
"epsg": "EPSG:2966",
|
|
27
|
+
"description": "Indiana State Plane West (NAD83, meters)",
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
# --- Iowa State Plane (NAD83(2011), ftUS) — proj4.js needs explicit defs in web app ---
|
|
31
|
+
"iowa-north-ftus": {
|
|
32
|
+
"epsg": "EPSG:6463",
|
|
33
|
+
"description": "Iowa State Plane North (NAD83(2011), US survey feet)",
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
# --- Arizona State Plane ---
|
|
37
|
+
"arizona-east": {
|
|
38
|
+
"epsg": "EPSG:2223",
|
|
39
|
+
"description": "Arizona State Plane East (NAD83, feet)",
|
|
40
|
+
},
|
|
41
|
+
"arizona-central": {
|
|
42
|
+
"epsg": "EPSG:2224",
|
|
43
|
+
"description": "Arizona State Plane Central (NAD83, feet)",
|
|
44
|
+
},
|
|
45
|
+
"arizona-west": {
|
|
46
|
+
"epsg": "EPSG:2225",
|
|
47
|
+
"description": "Arizona State Plane West (NAD83, feet)",
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
# --- UTM (NAD83) ---
|
|
51
|
+
"utm-15n": {
|
|
52
|
+
"epsg": "EPSG:26915",
|
|
53
|
+
"description": "UTM Zone 15N (NAD83) — Central US",
|
|
54
|
+
},
|
|
55
|
+
"utm-16n": {
|
|
56
|
+
"epsg": "EPSG:26916",
|
|
57
|
+
"description": "UTM Zone 16N (NAD83) — Indiana/Ohio area",
|
|
58
|
+
},
|
|
59
|
+
"utm-17n": {
|
|
60
|
+
"epsg": "EPSG:26917",
|
|
61
|
+
"description": "UTM Zone 17N (NAD83) — Eastern US",
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
# --- Web / Mapping ---
|
|
65
|
+
"web-mercator": {
|
|
66
|
+
"epsg": "EPSG:3857",
|
|
67
|
+
"description": "Web Mercator — Google Maps, OpenStreetMap",
|
|
68
|
+
},
|
|
69
|
+
}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: coordshift
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Universal coordinate system conversion for CSV and tabular data
|
|
5
|
+
Author: Nick Fulton
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Keywords: gis,coordinates,crs,projection,epsg,pyproj,survey,gnss
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Intended Audience :: Science/Research
|
|
10
|
+
Classifier: Topic :: Scientific/Engineering :: GIS
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Requires-Python: >=3.10
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Requires-Dist: pyproj>=3.5
|
|
18
|
+
Requires-Dist: pandas>=2.0
|
|
19
|
+
Requires-Dist: click>=8.0
|
|
20
|
+
Provides-Extra: dev
|
|
21
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
22
|
+
Requires-Dist: ruff>=0.4; extra == "dev"
|
|
23
|
+
Requires-Dist: build>=1.0; extra == "dev"
|
|
24
|
+
Requires-Dist: twine>=4.0; extra == "dev"
|
|
25
|
+
Dynamic: license-file
|
|
26
|
+
|
|
27
|
+
# coordshift
|
|
28
|
+
|
|
29
|
+
> **⚠ Alpha software — use with caution**
|
|
30
|
+
>
|
|
31
|
+
> coordshift is in early development (pre-v1.0). APIs and output formats may change without notice.
|
|
32
|
+
> **Always verify converted coordinates against a trusted independent source before using them in any survey, legal, safety-critical, or production workflow.**
|
|
33
|
+
> The authors provide no warranty and accept no liability for errors in coordinate conversion results. Use at your own risk.
|
|
34
|
+
|
|
35
|
+
Universal coordinate system conversion for CSV and tabular data — built for GIS analysts, surveyors, and drone mapping workflows.
|
|
36
|
+
|
|
37
|
+
## What it does
|
|
38
|
+
|
|
39
|
+
`coordshift` reprojects coordinate columns in CSV files from one CRS to another, using any system supported by PROJ. It ships as both a command-line tool and a Python library, plus a standalone browser-based converter (`coordshift.html`) that works entirely offline.
|
|
40
|
+
|
|
41
|
+
**CLI**
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
coordshift convert input.csv --from EPSG:4326 --to EPSG:2965 --x lon --y lat
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
**Python**
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
from coordshift import convert
|
|
51
|
+
|
|
52
|
+
df = convert("input.csv", from_crs="EPSG:4326", to_crs="EPSG:2965", x="lon", y="lat")
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
**Browser**
|
|
56
|
+
|
|
57
|
+
Open `coordshift.html` in any modern browser — no install required.
|
|
58
|
+
|
|
59
|
+
## Why this exists
|
|
60
|
+
|
|
61
|
+
Tools like cs2cs, ogr2ogr, and raw pyproj are powerful but assume you already speak PROJ. `coordshift` targets practitioners who need fast, repeatable conversions without wrestling with syntax.
|
|
62
|
+
|
|
63
|
+
## What's included
|
|
64
|
+
|
|
65
|
+
- Convert any EPSG/PROJ CRS to any other
|
|
66
|
+
- Auto-detect common column names (lat, lon, x, y, easting, northing, etc.)
|
|
67
|
+
- Source and target CRS via EPSG code, PROJ string, or friendly preset name
|
|
68
|
+
- Preserve all columns in output — original X/Y columns are always kept
|
|
69
|
+
- Converted coordinates written to new columns placed immediately after the originals
|
|
70
|
+
- Customisable output column suffix (default `_converted`, e.g. `lon_converted`, `lat_converted`)
|
|
71
|
+
- CLI for one-off conversions
|
|
72
|
+
- Python API for scripting and pipelines
|
|
73
|
+
- Browser-based converter (`coordshift.html`) with map preview — works offline
|
|
74
|
+
|
|
75
|
+
## Planned
|
|
76
|
+
|
|
77
|
+
- Vertical coordinate support (ellipsoidal ↔ orthometric, GEOID)
|
|
78
|
+
- Batch conversion across multiple files
|
|
79
|
+
- Output to GeoJSON or Shapefile
|
|
80
|
+
|
|
81
|
+
## Installation
|
|
82
|
+
|
|
83
|
+
Install from source until the first PyPI release:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
git clone https://github.com/FultonGeo/coordshift.git
|
|
87
|
+
cd coordshift
|
|
88
|
+
pip install -e .
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Or, once published:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
pip install coordshift
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Usage
|
|
98
|
+
|
|
99
|
+
### CLI
|
|
100
|
+
|
|
101
|
+
Basic conversion (original `lon`/`lat` columns are preserved; converted values go into `lon_converted`/`lat_converted` placed right after them):
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
coordshift convert input.csv --from EPSG:4326 --to EPSG:2965 --x lon --y lat
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
With output path:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
coordshift convert input.csv --from EPSG:4326 --to EPSG:2965 --x lon --y lat --out output.csv
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Custom column name suffix (produces `lon_proj`, `lat_proj` instead of the default `lon_converted`, `lat_converted`):
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
coordshift convert input.csv --from EPSG:4326 --to EPSG:2965 --suffix _proj
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
PROJ strings (use a single line on Windows, or wrap as your shell allows):
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
coordshift convert input.csv \
|
|
123
|
+
--from "+proj=longlat +datum=WGS84" \
|
|
124
|
+
--to "+proj=tmerc +lat_0=37.5 +lon_0=-85.66666667 +k=0.999966667 +x_0=100000 +y_0=250000 +ellps=GRS80" \
|
|
125
|
+
--x longitude --y latitude
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
List presets and search CRS:
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
coordshift list-crs
|
|
132
|
+
coordshift search "indiana east"
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### Python API
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
from coordshift import convert, search_crs
|
|
139
|
+
|
|
140
|
+
df = convert(
|
|
141
|
+
"field_points.csv",
|
|
142
|
+
from_crs="EPSG:4326",
|
|
143
|
+
to_crs="EPSG:2965",
|
|
144
|
+
x="lon",
|
|
145
|
+
y="lat",
|
|
146
|
+
)
|
|
147
|
+
# df now has lon, lon_converted, lat, lat_converted (plus any other original columns)
|
|
148
|
+
df.to_csv("field_points_converted.csv", index=False)
|
|
149
|
+
|
|
150
|
+
results = search_crs("indiana east")
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
See [docs/examples.md](docs/examples.md) for more detailed examples.
|
|
154
|
+
|
|
155
|
+
### Browser app
|
|
156
|
+
|
|
157
|
+
Open `coordshift.html` in any modern browser (Chrome, Firefox, Edge, Safari). No server or internet connection required after the page loads. Features include:
|
|
158
|
+
|
|
159
|
+
- Upload a CSV and pick coordinate columns
|
|
160
|
+
- Search and select source/target CRS from a built-in catalog of State Plane zones, UTM, and common geographic systems
|
|
161
|
+
- Preview converted points on an interactive map
|
|
162
|
+
- Download the converted CSV
|
|
163
|
+
|
|
164
|
+
## Project structure
|
|
165
|
+
|
|
166
|
+
```text
|
|
167
|
+
coordshift/
|
|
168
|
+
├── coordshift/ # Python package
|
|
169
|
+
│ ├── __init__.py # Public API
|
|
170
|
+
│ ├── core.py # Conversion (pyproj)
|
|
171
|
+
│ ├── cli.py # CLI (Click)
|
|
172
|
+
│ ├── io.py # CSV I/O, column detection
|
|
173
|
+
│ ├── crs.py # CRS resolution
|
|
174
|
+
│ └── presets.py # Friendly name → EPSG
|
|
175
|
+
├── tests/
|
|
176
|
+
│ ├── test_core.py
|
|
177
|
+
│ ├── test_cli.py
|
|
178
|
+
│ ├── test_io.py
|
|
179
|
+
│ └── fixtures/
|
|
180
|
+
├── docs/
|
|
181
|
+
│ └── examples.md
|
|
182
|
+
├── scripts/ # Developer tooling (regenerate HTML catalog)
|
|
183
|
+
├── coordshift.html # Standalone browser-based converter
|
|
184
|
+
├── pyproject.toml
|
|
185
|
+
├── README.md
|
|
186
|
+
└── LICENSE
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
## Development setup
|
|
190
|
+
|
|
191
|
+
```bash
|
|
192
|
+
git clone https://github.com/FultonGeo/coordshift.git
|
|
193
|
+
cd coordshift
|
|
194
|
+
python -m venv venv
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Activate the virtual environment:
|
|
198
|
+
|
|
199
|
+
- **Windows (PowerShell):** `.\venv\Scripts\Activate.ps1`
|
|
200
|
+
- **macOS / Linux:** `source venv/bin/activate`
|
|
201
|
+
|
|
202
|
+
Then:
|
|
203
|
+
|
|
204
|
+
```bash
|
|
205
|
+
pip install -e ".[dev]"
|
|
206
|
+
pytest
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Run the linter:
|
|
210
|
+
|
|
211
|
+
```bash
|
|
212
|
+
ruff check coordshift/
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
## Dependencies
|
|
216
|
+
|
|
217
|
+
- [pyproj](https://pyproj4.github.io/pyproj/) — PROJ bindings
|
|
218
|
+
- [pandas](https://pandas.pydata.org/) — tabular data
|
|
219
|
+
- [click](https://click.palletsprojects.com/) — CLI
|
|
220
|
+
|
|
221
|
+
## Contributing
|
|
222
|
+
|
|
223
|
+
Contributions welcome. Open an issue before large changes so direction stays aligned.
|
|
224
|
+
|
|
225
|
+
1. Fork the repo
|
|
226
|
+
2. Create a branch: `git checkout -b feature/my-feature`
|
|
227
|
+
3. Commit and push
|
|
228
|
+
4. Open a pull request
|
|
229
|
+
|
|
230
|
+
## Disclaimer
|
|
231
|
+
|
|
232
|
+
coordshift is alpha software provided **as-is**, without warranty of any kind, express or implied. Coordinate conversion results depend on the accuracy of the underlying PROJ library, the correctness of the CRS definitions used, and the quality of the input data. Always double-check converted coordinates against an authoritative source before using them in any survey, engineering, legal, or safety-critical context. The authors and contributors are not responsible for errors, losses, or damages arising from the use of this software.
|
|
233
|
+
|
|
234
|
+
## License
|
|
235
|
+
|
|
236
|
+
MIT. See [LICENSE](LICENSE).
|
|
237
|
+
|
|
238
|
+
## Author
|
|
239
|
+
|
|
240
|
+
Nick Fulton — geospatial work, FAA Part 107, drone and survey experience.
|
|
241
|
+
|
|
242
|
+
Repository: [FultonGeo/coordshift](https://github.com/FultonGeo/coordshift).
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
coordshift/__init__.py,sha256=D28qTXzsDi2Uv85wN7WXlhncFLRuDSQpXpilYPHqjuw,389
|
|
2
|
+
coordshift/cli.py,sha256=Ls7gHHJr22CMIqSocgHj9hXkCgnugD5nPYkBugzkW1E,4510
|
|
3
|
+
coordshift/core.py,sha256=1fegbqyCNudl2A3-YWeXO7Up3V3B5lqS-NI0YMTpJmg,4264
|
|
4
|
+
coordshift/crs.py,sha256=c1lBfN8yX9ntq3m-4pwT2ocD4-q52yfIPlHYRTFGmx0,2597
|
|
5
|
+
coordshift/io.py,sha256=_KeVd6yTfQOjPZbP0ZSiO8mgiagSjIuDKuLIGS84E04,1863
|
|
6
|
+
coordshift/presets.py,sha256=JmNxp-0eF6LMQmy9GrIUVQ2AhmO7aK_641rP34WpL1A,2063
|
|
7
|
+
coordshift-0.1.0.dist-info/licenses/LICENSE,sha256=eHl6bQTjsn-zTuTG4p1ZdJ4iKi3_E2h5F2UV2M2jiBo,1068
|
|
8
|
+
coordshift-0.1.0.dist-info/METADATA,sha256=PKJ51XyFaLYkVPRVC5MRKFRN4g9E7ZUhU2Rrhw5fWj8,7475
|
|
9
|
+
coordshift-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
10
|
+
coordshift-0.1.0.dist-info/entry_points.txt,sha256=RFWkxqwyiGQjQ7HjNbruaGJ-VyJrLZag5KqbaJ3eTMo,50
|
|
11
|
+
coordshift-0.1.0.dist-info/top_level.txt,sha256=9XCGmRJi3oJXAKYePggBuLMjDX6FkBbzvQ8lOH2FXVg,11
|
|
12
|
+
coordshift-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Nick Fulton
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
coordshift
|