Metadata-Version: 2.4
Name: xeibe-proj
Version: 0.2.0
Classifier: Development Status :: 2 - Pre-Alpha
Classifier: Programming Language :: Rust
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Scientific/Engineering :: GIS
Classifier: Typing :: Typed
Requires-Dist: xeibe-proj[numpy] ; extra == 'all'
Requires-Dist: numpy>=1.26 ; extra == 'numpy'
Requires-Dist: pytest>=8 ; extra == 'test'
Requires-Dist: pyproj>=3.7 ; extra == 'test'
Requires-Dist: numpy>=1.26 ; extra == 'test'
Requires-Dist: pyarrow>=26 ; extra == 'test'
Requires-Dist: arro3-core>=0.9 ; extra == 'test'
Provides-Extra: all
Provides-Extra: numpy
Provides-Extra: test
License-File: NOTICE.md
License-File: THIRD-PARTY-LICENSES.md
Summary: A pyproj-compatible API for EPSG coordinate conversion in pure Rust, without PROJ (experimental, AI-generated, unstable API)
License-Expression: CC0-1.0 AND MIT AND Apache-2.0 AND LicenseRef-EPSG
Requires-Python: >=3.11
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
Project-URL: Documentation, https://github.com/ttomasz/xeibe/blob/main/docs/pyproj-compat.md
Project-URL: Repository, https://github.com/ttomasz/xeibe

# xeibe-proj (Python)

Coordinate conversion between **EPSG** coordinate reference systems with a
**pyproj-compatible API**, in pure Rust: no PROJ, no `proj.db`, no C
dependencies, and no required Python dependencies. Part of
[xeibe](https://github.com/ttomasz/xeibe).

> **Warning: experimental, not for production use.** This package is
> AI-generated and has not been thoroughly tested yet. Its interfaces are not
> stable and will change, in breaking ways. Only part of pyproj's API is
> implemented; the plan, and what will never be supported, is in
> [`docs/pyproj-compat.md`](https://github.com/ttomasz/xeibe/blob/main/docs/pyproj-compat.md).

## Usage

```python
from xeibe_proj import CRS, Transformer

t = Transformer.from_crs("EPSG:4326", "EPSG:2180", always_xy=True)
x, y = t.transform(21.0, 52.0)
xs, ys = t.transform([21.0, 19.9], [52.0, 50.1])   # lists in, lists out
left, bottom, right, top = t.transform_bounds(14.1, 49.0, 24.2, 55.9)

crs = CRS("urn:ogc:def:crs:EPSG::2180")
crs.name, crs.axis_info, crs.area_of_use, crs.to_epsg()
crs == CRS(2180)                                    # True

from xeibe_proj.crs import Datum, Ellipsoid

Datum.from_name("ETRS89").name                      # an EPSG datum by name, alias or code
Ellipsoid.from_epsg(7019).semi_major_metre          # GRS 1980

from xeibe_proj.crs import GeographicCRS, ProjectedCRS
from xeibe_proj.crs.coordinate_operation import TransverseMercatorConversion

tm = TransverseMercatorConversion(longitude_natural_origin=19, scale_factor_natural_origin=0.9993,
                                  false_easting=500000, false_northing=-5300000)
ProjectedCRS(tm, geodetic_crs=GeographicCRS(datum="urn:ogc:def:ensemble:EPSG::6258"))  # pyproj's builders

prj = CRS(open("parcels.prj").read())               # WKT1 (GDAL's or Esri's) or WKT2
prj.to_epsg(), prj.list_authority()                 # identified by its definition
prj.to_wkt("WKT1_GDAL", pretty=True)                # or "WKT2_2019"

from xeibe_proj import Geod

g = Geod(ellps="WGS84")                             # or crs.get_geod()
az12, az21, dist = g.inv(-73.78, 40.64, 103.99, 1.36)
area, perimeter = g.polygon_area_perimeter([0, 1, 1], [0, 0, 1])

from xeibe_proj import Proj

p = Proj(2180)                                      # the CRS's projection, no datum shift
x, y = p(19.0, 52.0)                                # degrees from Greenwich in
lon, lat = p(x, y, inverse=True)
```

Coordinates come back in the type they went in: a number, a list, a tuple,
an `array.array` or a numpy array (pandas and xarray objects give numpy
arrays). numpy is not a dependency and is never imported: its arrays are
read through the buffer protocol. `pip install xeibe-proj[numpy]` only
declares it.

CRSs can be given as EPSG codes in any common spelling (`4326`,
`"EPSG:4326"`, `"EPSG:4326+5773"`, OGC URNs and URLs), as `"OGC:CRS84"`, as
EPSG names (`"WGS 84"`), as WKT (WKT1 as GDAL and Esri write it, with
`TOWGS84`, and WKT2), or as PROJJSON. Esri's old codes of CRSs EPSG has
since defined (`"ESRI:102100"`) give the EPSG CRS. PROJ strings
(`"+proj=utm +zone=33 +datum=WGS84"`), dicts and keyword arguments are read
as PROJ reads them, for the CRSs PROJ reads into a CRS method: EPSG's, and
PROJ's own (`+proj=robin`, `moll`, `geos`, …) and those PROJ names "PROJ
hammer", rotated poles (`ob_tran`) included, also on other celestial
bodies; pipelines and options PROJ keeps only as an extension (`+over`) are
an error.
`to_proj4()` writes PROJ's string. A CRS whose datum is unknown (a PROJ
string with `+ellps` but no `+datum`) converts only to CRSs on that datum:
there is no zero datum shift, as PROJ's ballpark would apply.

## GeoArrow

`xeibe_proj.arrow` converts GeoArrow arrays, which pyproj doesn't. It takes
anything with `__arrow_c_array__` or `__arrow_c_stream__` (pyarrow, arro3,
polars, nanoarrow) and imports none of them:

```python
import pyarrow as pa
from xeibe_proj import Transformer, arrow

t = Transformer.from_crs(2180, 4326)
out = pa.array(arrow.transform(t, column))  # pa.chunked_array() for chunked input
out = arrow.to_crs(column, 4326)            # source CRS from the GeoArrow metadata
```

GeoArrow's native encodings (interleaved or separated, XY to XYZM), WKB and
boxes are converted in place of their coordinates, in Rust without the GIL;
WKT is not. Coordinates are always easting or longitude first, as GeoArrow
has them. The result's GeoArrow metadata names the target CRS.

## Libraries that import pyproj

`install_as_pyproj()` makes `import pyproj` give this package, for
libraries such as GeoPandas that import pyproj themselves. Call it before
anything imports pyproj; it raises `RuntimeError` if something already has.
It is experimental: importing `xeibe_proj` directly stays the recommended way.

```python
import xeibe_proj.compat
xeibe_proj.compat.install_as_pyproj()

import geopandas  # gets xeibe_proj as pyproj
```

Each `pyproj.X` module is `xeibe_proj.X` itself, so `pyproj.CRS is
xeibe_proj.CRS`. `pyproj.__version__` is `"3.8.0"`, the pyproj release
whose API is followed. The rest of pyproj's modules raise
`ModuleNotFoundError` with the reason. `importlib.metadata.version("pyproj")`
still reads the installed distributions. Of GeoPandas 1.2.0's tests that
pass with pyproj, all but four pass this way: three use `IGNF:` CRSs, and
one expects equal CRSs to hash differently.

## Differences from pyproj

- **Failed coordinates raise** `ProjError`, which names the coordinate.
  There is no `inf` result: `errcheck` and `inplace` don't exist (on
  `Geod.fwd` and `inv` neither), and passing either raises
  `NotImplementedError`. `direction` and `return_back_azimuth` are
  keyword-only.
- **No ballpark transformations.** Without a usable EPSG operation,
  `Transformer.from_crs` raises `ProjError`.
- **Grids are fetched by default**, from `cdn.proj.org`, by byte range, and
  cached under the user cache directory. `xeibe_proj.network.set_network_enabled(False)`
  or `PROJ_NETWORK=OFF` turns this off, and then only operations without
  grids are used, as in pyproj's default install. Grid files in `PROJ_DATA`
  (or `xeibe_proj.datadir.set_data_dir`) are read instead of fetched.
- **A newer EPSG release** than pyproj's bundled `proj.db`:
  `xeibe_proj.EPSG_VERSION`. Names and operations can differ.
- **No PROJ version**: `proj_version_str` and `PROJ_VERSION` don't exist.
- **WKT1_ESRI isn't written**: `to_wkt("WKT1_ESRI")` raises
  `NotImplementedError`. The other versions are PROJ's output, but for ids
  PROJ looks up in its database.
- **`Proj` follows EPSG**, where pyproj's follows the CRS's PROJ string:
  full-precision parameters, EPSG's prime meridians and the CRS's own
  datum. Results differ by up to 0.35 mm (kilometres for EASE-Grid). `srs`
  is the CRS's, not a PROJ string.
- **PROJ's mistakes in a few projections are fixed**: ISEA on an
  ellipsoid (equal-area, 9 km from PROJ's on WGS 84) and with `lon_0`, the
  Bipolar Oblique Conic where its cones meet (240 km), GS48's, GS50's and
  Alaska's inverse, S2 on the far side of its face, and Mercator
  (Spherical) with a latitude of origin. In each PROJ's inverse doesn't
  take its own forward back; ours does.
- **Other authorities** than EPSG and OGC (`ESRI:`, `IGNF:`, …) are not
  known, apart from Esri's old codes of EPSG CRSs.

## Logging

Every use of a grid is logged to the `xeibe_proj.grids` logger: `INFO` when
a transformer first converts with grids and for each download, `DEBUG` for
the cache and data directories, `ERROR` for a failed download. The package
adds no handlers, so the `INFO` records are dropped unless the application
configures logging:

```python
import logging
logging.basicConfig(level=logging.INFO)
```

## Pyodide

There is a wheel for Pyodide 314 (`pyemscripten_2026_0_wasm32`), so the
package runs in the browser and in Node:

```python
import micropip
await micropip.install("xeibe-proj")

from xeibe_proj import Transformer
Transformer.from_crs(4326, 2180).transform(52.0, 21.0)
```

Grids are fetched with the browser's synchronous `XMLHttpRequest` and
cached in memory until the page is reloaded. A synchronous request blocks
the page while it runs on the main thread, so convert in a web worker when
the transformation may need grids. Node has no `XMLHttpRequest`: there,
put the grids in a data directory (`PROJ_DATA`, `set_data_dir`), or turn
the network off. See
[`docs/pyproj-phase3.md`](https://github.com/ttomasz/xeibe/blob/main/docs/pyproj-phase3.md#pyodide).

## Types

The package is fully typed (`py.typed`) and checked with mypy and pyright in
strict mode. `Transformer.transform` has overloads that follow the
container rules: `float` in gives `float` out, `list[float]` gives
`list[float]`, a numpy array gives a numpy array.

## Licence

The code is CC0-1.0. The embedded EPSG data is IOGP's, used under the EPSG
Dataset Terms of Use, some projection code is ported from PROJ (MIT), and
Esri's names of EPSG objects, for reading Esri's WKT, are Apache-2.0: see
`NOTICE.md`.

