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 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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ coordshift = coordshift.cli:cli
@@ -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