whatcrs 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.
- whatcrs/__init__.py +41 -0
- whatcrs/__main__.py +3 -0
- whatcrs/cli.py +192 -0
- whatcrs/core.py +686 -0
- whatcrs/crs_db.py +289 -0
- whatcrs/data/NOTICE.md +10 -0
- whatcrs/data/countries.json.gz +0 -0
- whatcrs/data/crs_builtin.json.gz +0 -0
- whatcrs/projections.py +1056 -0
- whatcrs/py.typed +0 -0
- whatcrs/readers.py +297 -0
- whatcrs/regions.py +435 -0
- whatcrs-0.1.0.dist-info/METADATA +261 -0
- whatcrs-0.1.0.dist-info/RECORD +18 -0
- whatcrs-0.1.0.dist-info/WHEEL +5 -0
- whatcrs-0.1.0.dist-info/entry_points.txt +2 -0
- whatcrs-0.1.0.dist-info/licenses/LICENSE +21 -0
- whatcrs-0.1.0.dist-info/top_level.txt +1 -0
whatcrs/__init__.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""whatcrs - "What CRS is this?"
|
|
2
|
+
|
|
3
|
+
Guess the coordinate reference system of mystery coordinates and rank
|
|
4
|
+
candidate EPSG codes with explanations::
|
|
5
|
+
|
|
6
|
+
>>> import whatcrs
|
|
7
|
+
>>> best = whatcrs.guess_crs([(440000, 4474000)], hint="Spain")[0]
|
|
8
|
+
>>> best.epsg
|
|
9
|
+
25830
|
|
10
|
+
|
|
11
|
+
Works offline with a bundled set of ~900 widely used CRSs and pure-Python
|
|
12
|
+
inverse projections; installs with ``pyproj`` widen the search to every EPSG
|
|
13
|
+
projected CRS.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from .core import Candidate, describe_coordinates, fix_swapped_axes, guess_crs, to_lonlat
|
|
19
|
+
from .crs_db import CRSRecord, builtin_records, pyproj_available
|
|
20
|
+
from .readers import PointSource, read_points, read_text
|
|
21
|
+
from .regions import Region, country_at, parse_hint
|
|
22
|
+
|
|
23
|
+
__version__ = "0.1.0"
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
"Candidate",
|
|
27
|
+
"CRSRecord",
|
|
28
|
+
"PointSource",
|
|
29
|
+
"Region",
|
|
30
|
+
"__version__",
|
|
31
|
+
"builtin_records",
|
|
32
|
+
"country_at",
|
|
33
|
+
"describe_coordinates",
|
|
34
|
+
"fix_swapped_axes",
|
|
35
|
+
"guess_crs",
|
|
36
|
+
"parse_hint",
|
|
37
|
+
"pyproj_available",
|
|
38
|
+
"read_points",
|
|
39
|
+
"read_text",
|
|
40
|
+
"to_lonlat",
|
|
41
|
+
]
|
whatcrs/__main__.py
ADDED
whatcrs/cli.py
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"""Command line interface: ``whatcrs``.
|
|
2
|
+
|
|
3
|
+
Examples::
|
|
4
|
+
|
|
5
|
+
whatcrs 512345 4182345
|
|
6
|
+
whatcrs 512345 4182345 --hint Spain
|
|
7
|
+
whatcrs data.csv --x lon --y lat --hint "Spain"
|
|
8
|
+
whatcrs shapes.geojson --json
|
|
9
|
+
whatcrs "POINT (2600000 1200000)"
|
|
10
|
+
cat points.csv | whatcrs -
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import argparse
|
|
16
|
+
import json
|
|
17
|
+
import re
|
|
18
|
+
import sys
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import List, Optional, Sequence, TextIO
|
|
21
|
+
|
|
22
|
+
from . import __version__
|
|
23
|
+
from .core import Candidate, describe_coordinates, guess_crs
|
|
24
|
+
from .crs_db import pyproj_available
|
|
25
|
+
from .readers import PointSource, read_points, read_text
|
|
26
|
+
|
|
27
|
+
_NUMBER = re.compile(r"^[-+]?(?:\d+\.?\d*|\.\d+)(?:[eE][-+]?\d+)?$")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
31
|
+
p = argparse.ArgumentParser(
|
|
32
|
+
prog="whatcrs",
|
|
33
|
+
description=(
|
|
34
|
+
"What CRS is this? Guess the coordinate reference system of mystery coordinates "
|
|
35
|
+
"and rank candidate EPSG codes with explanations."
|
|
36
|
+
),
|
|
37
|
+
epilog=(
|
|
38
|
+
"INPUT is either numbers (x y [x y ...], also 'x,y'), a CSV/GeoJSON/WKT file, "
|
|
39
|
+
"a WKT string, or '-' for standard input."
|
|
40
|
+
),
|
|
41
|
+
)
|
|
42
|
+
p.add_argument("input", nargs="+", metavar="INPUT", help="coordinates, a file, WKT or '-'")
|
|
43
|
+
p.add_argument("--hint", help='where the data should be, e.g. "Spain", "Texas", "Europe"')
|
|
44
|
+
p.add_argument(
|
|
45
|
+
"--bbox",
|
|
46
|
+
help="expected area as west,south,east,north in degrees (use --bbox=-10,35,5,44 "
|
|
47
|
+
"when the first value is negative)",
|
|
48
|
+
)
|
|
49
|
+
p.add_argument("--units", help="known linear unit: m, ft or deg")
|
|
50
|
+
p.add_argument(
|
|
51
|
+
"--x", dest="xcol", help="CSV column with the first coordinate (name or 0-based index)"
|
|
52
|
+
)
|
|
53
|
+
p.add_argument("--y", dest="ycol", help="CSV column with the second coordinate")
|
|
54
|
+
p.add_argument("--delimiter", help="CSV delimiter (default: sniffed)")
|
|
55
|
+
p.add_argument(
|
|
56
|
+
"--format", dest="fmt", choices=["csv", "tsv", "geojson", "wkt"], help="input format"
|
|
57
|
+
)
|
|
58
|
+
p.add_argument("--top", type=int, default=5, help="number of candidates to show (default 5)")
|
|
59
|
+
p.add_argument("--json", action="store_true", help="print JSON")
|
|
60
|
+
p.add_argument(
|
|
61
|
+
"--no-pyproj",
|
|
62
|
+
action="store_true",
|
|
63
|
+
help="use only the bundled CRS subset even if pyproj is installed",
|
|
64
|
+
)
|
|
65
|
+
p.add_argument("--no-swap", action="store_true", help="do not consider swapped axes")
|
|
66
|
+
p.add_argument("--version", action="version", version=f"whatcrs {__version__}")
|
|
67
|
+
return p
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _parse_numbers(tokens: Sequence[str]) -> Optional[List[float]]:
|
|
71
|
+
values: List[float] = []
|
|
72
|
+
for tok in tokens:
|
|
73
|
+
for part in re.split(r"[,;\s]+", tok.strip()):
|
|
74
|
+
if not part:
|
|
75
|
+
continue
|
|
76
|
+
if not _NUMBER.match(part):
|
|
77
|
+
return None
|
|
78
|
+
values.append(float(part))
|
|
79
|
+
return values
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _load(args: argparse.Namespace) -> PointSource:
|
|
83
|
+
tokens: List[str] = args.input
|
|
84
|
+
numbers = _parse_numbers(tokens)
|
|
85
|
+
if numbers is not None:
|
|
86
|
+
if len(numbers) < 2 or len(numbers) % 2:
|
|
87
|
+
raise ValueError("give coordinates as pairs: x y [x y ...]")
|
|
88
|
+
pts = [(numbers[i], numbers[i + 1]) for i in range(0, len(numbers), 2)]
|
|
89
|
+
return PointSource(pts, "arguments")
|
|
90
|
+
if len(tokens) != 1:
|
|
91
|
+
raise ValueError("give either numbers, or a single file / WKT string / '-'")
|
|
92
|
+
tok = tokens[0]
|
|
93
|
+
if tok == "-":
|
|
94
|
+
return read_text(
|
|
95
|
+
sys.stdin.read(), args.fmt, x=args.xcol, y=args.ycol, delimiter=args.delimiter
|
|
96
|
+
)
|
|
97
|
+
path = Path(tok)
|
|
98
|
+
if path.is_file():
|
|
99
|
+
return read_points(path, x=args.xcol, y=args.ycol, delimiter=args.delimiter, fmt=args.fmt)
|
|
100
|
+
if re.match(r"^\s*(SRID\s*=\s*\d+\s*;\s*)?[A-Za-z]+\s*(Z|M|ZM)?\s*\(", tok):
|
|
101
|
+
return read_text(tok, "wkt")
|
|
102
|
+
raise ValueError(f"not a number list, an existing file or WKT: {tok!r}")
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _hemi(lon: float, lat: float) -> str:
|
|
106
|
+
return f"{abs(lat):.5f}°{'N' if lat >= 0 else 'S'} {abs(lon):.5f}°{'E' if lon >= 0 else 'W'}"
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _print_text(src: PointSource, cands: List[Candidate], out: TextIO, hint: Optional[str]) -> None:
|
|
110
|
+
n = len(src.points)
|
|
111
|
+
x0, y0 = src.points[0]
|
|
112
|
+
fields = f" (x={src.x_field}, y={src.y_field})" if src.x_field else ""
|
|
113
|
+
out.write(
|
|
114
|
+
f"whatcrs {__version__}: {n} point{'s' if n != 1 else ''} from {src.format}{fields}\n"
|
|
115
|
+
)
|
|
116
|
+
out.write(f"first point: {x0:.10g}, {y0:.10g}\n")
|
|
117
|
+
for note in src.notes:
|
|
118
|
+
out.write(f"note: {note}\n")
|
|
119
|
+
for note in describe_coordinates(src.points):
|
|
120
|
+
out.write(f"pattern: {note}\n")
|
|
121
|
+
if src.declared_crs:
|
|
122
|
+
out.write(f"declared in file: {src.declared_crs}\n")
|
|
123
|
+
if hint:
|
|
124
|
+
out.write(f"hint: {hint}\n")
|
|
125
|
+
if not cands:
|
|
126
|
+
out.write("\nNo candidate CRS fits these coordinates.\n")
|
|
127
|
+
return
|
|
128
|
+
out.write("\n")
|
|
129
|
+
for i, c in enumerate(cands, 1):
|
|
130
|
+
swap = " [axes swapped]" if c.swapped else ""
|
|
131
|
+
out.write(f"{i}. {c.code} {c.name} - confidence {c.confidence:.0%}{swap}\n")
|
|
132
|
+
where = f" ({c.country})" if c.country else " (at sea)"
|
|
133
|
+
out.write(f" centre: {_hemi(*c.centroid)}{where}\n")
|
|
134
|
+
label = "WGS 84" if c.wgs84 else "lon/lat"
|
|
135
|
+
sample = "; ".join(f"{lon:.5f}, {lat:.5f}" for lon, lat in c.sample_lonlat[:3])
|
|
136
|
+
out.write(f" {label}: {sample}\n")
|
|
137
|
+
for r in c.reasons:
|
|
138
|
+
out.write(f" - {r}\n")
|
|
139
|
+
if src.declared_crs and src.declared_crs.startswith("EPSG:"):
|
|
140
|
+
best = cands[0].code
|
|
141
|
+
verdict = "agrees with" if best == src.declared_crs else "differs from"
|
|
142
|
+
out.write(f"\nBest guess {best} {verdict} the declared {src.declared_crs}.\n")
|
|
143
|
+
if not pyproj_available():
|
|
144
|
+
out.write("\n(bundled CRS subset; `pip install whatcrs[pyproj]` searches all EPSG codes)\n")
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def main(argv: Optional[Sequence[str]] = None) -> int:
|
|
148
|
+
for stream in (sys.stdout, sys.stderr):
|
|
149
|
+
reconfigure = getattr(stream, "reconfigure", None)
|
|
150
|
+
if reconfigure is not None:
|
|
151
|
+
try:
|
|
152
|
+
reconfigure(errors="replace")
|
|
153
|
+
except (ValueError, OSError): # pragma: no cover - exotic streams
|
|
154
|
+
pass
|
|
155
|
+
parser = _build_parser()
|
|
156
|
+
args = parser.parse_args(argv)
|
|
157
|
+
try:
|
|
158
|
+
src = _load(args)
|
|
159
|
+
bbox = None
|
|
160
|
+
if args.bbox:
|
|
161
|
+
bbox = [float(v) for v in re.split(r"[,\s]+", args.bbox.strip()) if v]
|
|
162
|
+
cands = guess_crs(
|
|
163
|
+
src.points,
|
|
164
|
+
args.hint,
|
|
165
|
+
bbox=bbox,
|
|
166
|
+
units=args.units,
|
|
167
|
+
top=max(1, args.top),
|
|
168
|
+
use_pyproj=False if args.no_pyproj else None,
|
|
169
|
+
allow_swapped=not args.no_swap,
|
|
170
|
+
)
|
|
171
|
+
except (ValueError, KeyError, OSError) as exc:
|
|
172
|
+
parser.exit(2, f"whatcrs: error: {exc}\n")
|
|
173
|
+
if args.json:
|
|
174
|
+
payload = {
|
|
175
|
+
"version": __version__,
|
|
176
|
+
"n_points": len(src.points),
|
|
177
|
+
"format": src.format,
|
|
178
|
+
"x_field": src.x_field,
|
|
179
|
+
"y_field": src.y_field,
|
|
180
|
+
"declared_crs": src.declared_crs,
|
|
181
|
+
"hint": args.hint,
|
|
182
|
+
"pattern": describe_coordinates(src.points),
|
|
183
|
+
"candidates": [c.to_dict() for c in cands],
|
|
184
|
+
}
|
|
185
|
+
sys.stdout.write(json.dumps(payload, indent=2, ensure_ascii=False) + "\n")
|
|
186
|
+
else:
|
|
187
|
+
_print_text(src, cands, sys.stdout, args.hint)
|
|
188
|
+
return 0 if cands else 1
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
if __name__ == "__main__": # pragma: no cover
|
|
192
|
+
sys.exit(main())
|