I couldn't quite accept that PostGIS forces me to use (lon,lat) order when EPSG:4326 says it should be (lat,lon), so I spent way too much time thinking of a clever solution to find a suitable abstraction and this is what I came up with:
- On the client side, I'm working with Python, using SQLAlchemy and GeoAlchemy.
- I specified a custom
GPS
type that is an extension of Geometry
which automatically swaps lat,lon to lon,lat on insert, and lon,lat back to lat,lon on select.
- Additionally, it can parse WKT data to Shapely shapes and limits the precision of coordinates with
ST_QuantizeCoordinates
to a reasonable amount.
Here's the GPS type definition
from geoalchemy2 import Geometry, functions as geo_func
from geoalchemy2.shape import to_shape
from sqlalchemy import TypeDecorator, func
class GPS(TypeDecorator):
"""
Extension of PostGIS type 'Geometry' with the following additions:
* Flips the order of latitude and longitude, so that from the outside,
we get lat,lon ordering, as it should be, but in the database,
it is lon,lat so that PostGIS is happy
* limits the precision of all stored coordinates to a reasonable amount
(no raw floats needed, reduces table size when TOAST compression
is available)
:param srid: a custom SRID (defaults to EPSG:4326 for GPS)
:param precision: number of significant decimal digits after
the decimal point
:param kwargs: other parameters for the Geometry type
"""
impl = Geometry
cache_ok = True
def __init__(self, srid=4326, precision=7, **kwargs):
super().__init__(srid=srid, **kwargs)
self.precision = precision
def column_expression(self, col):
# this function will be executed by the database on select
# flip coordinates from lon,lat back to lat,lon
return getattr(func, self.impl.as_binary)(
geo_func.ST_FlipCoordinates(col), type_=self)
def bind_expression(self, bindvalue):
# this function will be executed by the database on insert
# flip coordinates from lat,lon to lon,lat and limit precision
return geo_func.ST_QuantizeCoordinates(
geo_func.ST_FlipCoordinates(self.impl.bind_expression(bindvalue)),
self.precision, type_=self)
def process_result_value(self, value, dialect):
# this function will be executed locally on select
# parse the raw WKB value to a shapely geometry
return to_shape(value)
And this is how you can use
from sqlalchemy import Column, Integer
from sqlalchemy.orm import declarative_base
Base = declarative_base()
class GeoTest(Base):
__tablename__ = 'epsg_4326'
id = Column(Integer, primary=True)
position = Column(GPS(geometry_type='POINT', precision=5))
Let's try it
from shapely.geometry import Point
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
engine = create_engine('postgresql://user:secret@localhost:5432/test_db', echo=True)
Base.metadata.create_all(engine)
session = sessionmaker(bind=engine)
gps = GeoTest(id=0, position=Point(48.137, 11.575).wkt)
session.add(gps)
session.commit()
# stored as wkt "POINT(11.575 48.137)" in the database
gps2 = session.query(GeoTest).first()
# restored as shapely Point(48.137, 11.575) in python
# exact float values can be different, since we are rounding in binary
I'm sure there are some edge cases that I've missed, but I'm ok with this solution for now.
Incompatible Change: These functions now interpret latitude and longitude coordinates as in the order specified by the spatial reference system. The functions also accept an optional argument to override the default axis order.