Skip to content

Commit d8cf4af

Browse files
authored
Merge pull request #23 from govlt/allow-setting-geometry-output
Allow setting geometry output
2 parents 9eef2bc + 770b8c2 commit d8cf4af

File tree

5 files changed

+129
-35
lines changed

5 files changed

+129
-35
lines changed

api/constants.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
from fastapi import Query
44
from fastapi.openapi.models import Example
55

6+
import schemas
7+
68
query_srid: Query = Query(
79
3346,
810
openapi_examples={
@@ -20,6 +22,23 @@
2022
description="A spatial reference identifier (SRID) for geometry output."
2123
)
2224

25+
query_geometry_output_type: schemas.GeometryOutputFormat = Query(
26+
schemas.GeometryOutputFormat.ewkt,
27+
openapi_examples={
28+
"example_ewkt": {
29+
"summary": "EWKT",
30+
"description": "Extended Well-Known Text (EWKT) format for representing geometric data.",
31+
"value": schemas.GeometryOutputFormat.ewkt
32+
},
33+
"example_ewkb": {
34+
"summary": "EWKB",
35+
"description": "Extended Well-Known Binary (EWKB) format for representing geometric data.",
36+
"value": schemas.GeometryOutputFormat.ewkb
37+
},
38+
},
39+
description="Specify the format for geometry output."
40+
)
41+
2342
openapi_examples_geometry_filtering: Dict[str, Example] = {
2443
"example_ewkb": {
2544
"summary": "Filter using EWKB and 'intersects'",

api/database.py

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
from typing import Optional
2+
13
import geoalchemy2
24
import sqlean
3-
from geoalchemy2 import load_spatialite
5+
from geoalchemy2 import load_spatialite, Geometry, WKTElement
46
from geoalchemy2.functions import GenericFunction
57
from sqlalchemy import create_engine
68
from sqlalchemy.orm import declarative_base, sessionmaker, Session
@@ -39,14 +41,18 @@ class GeomFromGeoJSON(GenericFunction):
3941
inherit_cache = True
4042

4143

42-
class GeomFromEWKB(GenericFunction):
43-
"""
44-
Returns geometric object given its EWKB Representation
45-
46-
see https://www.gaia-gis.it/gaia-sins/spatialite-sql-5.1.0.html
44+
class EWKTGeometry(Geometry):
45+
# We need to override constructor only to set extended to True
46+
def __init__(self, geometry_type: Optional[str] = "GEOMETRY", srid=-1, dimension=2, spatial_index=True,
47+
use_N_D_index=False, use_typmod: Optional[bool] = None, from_text: Optional[str] = None,
48+
name: Optional[str] = None, nullable=True, _spatial_index_reflected=None) -> None:
49+
super().__init__(geometry_type, srid, dimension, spatial_index, use_N_D_index, use_typmod, from_text, name,
50+
nullable, _spatial_index_reflected)
51+
self.extended = True
4752

48-
Return type: :class:`geoalchemy2.types.Geometry`.
49-
"""
53+
name = "geometry"
54+
from_text = 'ST_GeomFromEWKT'
55+
as_binary = 'AsEWKT'
56+
ElementType = WKTElement
5057

51-
type = geoalchemy2.types.Geometry()
52-
inherit_cache = True
58+
cache_ok = False

api/router.py

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,8 @@ def boundaries_search(
5656
sort_order=sort_order,
5757
request=request,
5858
boundaries_filter=boundaries_filter,
59-
srid=None
59+
srid=None,
60+
geometry_output_format=None,
6061
)
6162

6263
@router.get(
@@ -105,9 +106,10 @@ def get_with_geometry(
105106
),
106107
db: Session = Depends(database.get_db),
107108
srid: int = constants.query_srid,
109+
geometry_output_format: schemas.GeometryOutputFormat = constants.query_geometry_output_type,
108110
service: services.BaseBoundariesService = Depends(service_class),
109111
):
110-
if row := service.get_by_code(db=db, code=code, srid=srid):
112+
if row := service.get_by_code(db=db, code=code, srid=srid, geometry_output_format=geometry_output_format):
111113
return row
112114
else:
113115
raise HTTPException(
@@ -183,6 +185,7 @@ def addresses_search(
183185
],
184186
sort_by: schemas.SearchSortBy = Query(default=schemas.SearchSortBy.code),
185187
sort_order: schemas.SearchSortOrder = Query(default=schemas.SearchSortOrder.asc),
188+
geometry_output_format: schemas.GeometryOutputFormat = constants.query_geometry_output_type,
186189
srid: int = constants.query_srid,
187190
db: Session = Depends(database.get_db),
188191
addresses_filter: filters.AddressesFilter = Depends(filters.AddressesFilter),
@@ -195,6 +198,7 @@ def addresses_search(
195198
request=request,
196199
srid=srid,
197200
boundaries_filter=addresses_filter,
201+
geometry_output_format=geometry_output_format
198202
)
199203

200204

@@ -212,15 +216,19 @@ def addresses_search(
212216
def get(
213217
code: int = Path(
214218
description="The code of the address to retrieve",
215-
examples=[
216-
155218235
217-
]
219+
openapi_examples={
220+
"example_address_code": {
221+
"summary": "Example address code",
222+
"value": 155218235
223+
},
224+
},
218225
),
219226
srid: int = constants.query_srid,
227+
geometry_output_format: schemas.GeometryOutputFormat = constants.query_geometry_output_type,
220228
db: Session = Depends(database.get_db),
221229
service: services.AddressesService = Depends(services.AddressesService),
222230
):
223-
if item := service.get_by_code(db=db, code=code, srid=srid):
231+
if item := service.get_by_code(db=db, code=code, srid=srid, geometry_output_format=geometry_output_format):
224232
return item
225233
else:
226234
raise HTTPException(
@@ -257,6 +265,7 @@ def rooms_search(
257265
sort_by: schemas.SearchSortBy = Query(default=schemas.SearchSortBy.code),
258266
sort_order: schemas.SearchSortOrder = Query(default=schemas.SearchSortOrder.asc),
259267
srid: int = constants.query_srid,
268+
geometry_output_format: schemas.GeometryOutputFormat = Query(default=schemas.GeometryOutputFormat.ewkt),
260269
db: Session = Depends(database.get_db),
261270
rooms_filter: filters.RoomsFilter = Depends(filters.RoomsFilter),
262271
service: services.RoomsService = Depends(services.RoomsService),
@@ -268,6 +277,7 @@ def rooms_search(
268277
request=request,
269278
srid=srid,
270279
boundaries_filter=rooms_filter,
280+
geometry_output_format=geometry_output_format
271281
)
272282

273283

@@ -285,15 +295,19 @@ def rooms_search(
285295
def get(
286296
code: int = Path(
287297
description="The code of the room to retrieve",
288-
examples=[
289-
194858325
290-
]
298+
openapi_examples={
299+
"example_room_code": {
300+
"summary": "Example room code",
301+
"value": 194858325
302+
},
303+
},
291304
),
292305
srid: int = constants.query_srid,
306+
geometry_output_format: schemas.GeometryOutputFormat = constants.query_geometry_output_type,
293307
db: Session = Depends(database.get_db),
294308
service: services.RoomsService = Depends(services.RoomsService),
295309
):
296-
if item := service.get_by_code(db=db, code=code, srid=srid):
310+
if item := service.get_by_code(db=db, code=code, srid=srid, geometry_output_format=geometry_output_format):
297311
return item
298312
else:
299313
raise HTTPException(

api/schemas.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@ class SearchSortOrder(str, enum.Enum):
1717
desc = 'desc'
1818

1919

20+
class GeometryOutputFormat(str, enum.Enum):
21+
ewkt = 'ewkt'
22+
ewkb = 'ewkb'
23+
24+
2025
class Geometry(BaseModel):
2126
srid: int = Field(description="Spatial Reference Identifier (SRID) for the geometry")
2227
data: str = Field(description="Geometry data in WKB (Well-Known Binary) format, represented as a hex string")

api/services.py

Lines changed: 65 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,14 @@
33

44
from fastapi_pagination import Page
55
from fastapi_pagination.ext.sqlalchemy import paginate
6+
from geoalchemy2 import Geometry
67
from geoalchemy2.functions import ST_Transform
78
from sqlalchemy import select, Select, func, text, Row, Label
89
from sqlalchemy.dialects.postgresql import JSONB
910
from sqlalchemy.orm import Session, InstrumentedAttribute
1011
from sqlalchemy.sql import operators
1112

13+
import database
1214
import filters
1315
import models
1416
import schemas
@@ -70,16 +72,33 @@ class BaseBoundariesService(abc.ABC):
7072
model_class: Type[models.BaseBoundaries]
7173

7274
@abc.abstractmethod
73-
def _get_select_query(self, srid: Optional[int]) -> Select:
75+
def _get_select_query(self, srid: Optional[int],
76+
geometry_output_format: Optional[schemas.GeometryOutputFormat]) -> Select:
7477
pass
7578

7679
@abc.abstractmethod
7780
def _filter_by_code(self, query: Select, code: int) -> Select:
7881
pass
7982

8083
@staticmethod
81-
def _get_geometry_field(field: InstrumentedAttribute, srid: int) -> Label:
82-
return ST_Transform(field, srid).label("geometry")
84+
def _get_geometry_output_type(geometry_output_format: schemas.GeometryOutputFormat):
85+
match geometry_output_format:
86+
case schemas.GeometryOutputFormat.ewkt:
87+
return database.EWKTGeometry
88+
case schemas.GeometryOutputFormat.ewkb:
89+
return Geometry()
90+
case _:
91+
raise ValueError(f"Unable to map geometry output format {geometry_output_format}")
92+
93+
@staticmethod
94+
def _get_geometry_field(
95+
field: InstrumentedAttribute,
96+
srid: int,
97+
geometry_output_format: schemas.GeometryOutputFormat
98+
) -> Label:
99+
geometry_output_type = BaseBoundariesService._get_geometry_output_type(geometry_output_format)
100+
101+
return ST_Transform(field, srid, type_=geometry_output_type).label("geometry")
83102

84103
def search(
85104
self,
@@ -89,8 +108,9 @@ def search(
89108
request: schemas.BaseSearchRequest,
90109
boundaries_filter: filters.BaseFilter,
91110
srid: Optional[int],
111+
geometry_output_format: Optional[schemas.GeometryOutputFormat]
92112
) -> Page:
93-
query = self._get_select_query(srid=srid)
113+
query = self._get_select_query(srid=srid, geometry_output_format=geometry_output_format)
94114

95115
query = boundaries_filter.apply(request, db, query)
96116

@@ -106,9 +126,10 @@ def get_by_code(
106126
self,
107127
db: Session,
108128
code: int,
129+
geometry_output_format: Optional[schemas.GeometryOutputFormat] = None,
109130
srid: Optional[int] = None,
110131
) -> Row | None:
111-
query = self._get_select_query(srid=srid)
132+
query = self._get_select_query(srid=srid, geometry_output_format=geometry_output_format)
112133
query = self._filter_by_code(code=code, query=query)
113134

114135
return db.execute(query).first()
@@ -117,15 +138,20 @@ def get_by_code(
117138
class CountiesService(BaseBoundariesService):
118139
model_class = models.Counties
119140

120-
def _get_select_query(self, srid: Optional[int]) -> Select:
141+
def _get_select_query(
142+
self,
143+
srid: Optional[int],
144+
geometry_output_format: Optional[schemas.GeometryOutputFormat],
145+
) -> Select:
121146
columns = [
122147
models.Counties.code,
123148
models.Counties.feature_id,
124149
models.Counties.name,
125150
models.Counties.area_ha,
126151
models.Counties.area_ha,
127152
models.Counties.created_at,
128-
] + ([self._get_geometry_field(models.Counties.geom, srid)] if srid else [])
153+
] + ([self._get_geometry_field(models.Counties.geom, srid,
154+
geometry_output_format)] if srid and geometry_output_format else [])
129155

130156
return select(*columns).select_from(models.Counties)
131157

@@ -136,7 +162,11 @@ def _filter_by_code(self, query: Select, code: int) -> Select:
136162
class MunicipalitiesService(BaseBoundariesService):
137163
model_class = models.Municipalities
138164

139-
def _get_select_query(self, srid: Optional[int]) -> Select:
165+
def _get_select_query(
166+
self,
167+
srid: Optional[int],
168+
geometry_output_format: Optional[schemas.GeometryOutputFormat],
169+
) -> Select:
140170
columns = [
141171
models.Municipalities.code,
142172
models.Municipalities.feature_id,
@@ -157,7 +187,11 @@ def _filter_by_code(self, query: Select, code: int) -> Select:
157187
class EldershipsService(BaseBoundariesService):
158188
model_class = models.Elderships
159189

160-
def _get_select_query(self, srid: Optional[int]) -> Select:
190+
def _get_select_query(
191+
self,
192+
srid: Optional[int],
193+
geometry_output_format: Optional[schemas.GeometryOutputFormat],
194+
) -> Select:
161195
columns = [
162196
models.Elderships.code,
163197
models.Elderships.feature_id,
@@ -178,7 +212,10 @@ def _filter_by_code(self, query: Select, code: int) -> Select:
178212
class ResidentialAreasService(BaseBoundariesService):
179213
model_class = models.ResidentialAreas
180214

181-
def _get_select_query(self, srid: Optional[int]) -> Select:
215+
def _get_select_query(
216+
self, srid: Optional[int],
217+
geometry_output_format: Optional[schemas.GeometryOutputFormat],
218+
) -> Select:
182219
columns = [
183220
models.ResidentialAreas.code,
184221
models.ResidentialAreas.feature_id,
@@ -199,7 +236,11 @@ def _filter_by_code(self, query: Select, code: int) -> Select:
199236
class StreetsService(BaseBoundariesService):
200237
model_class = models.Streets
201238

202-
def _get_select_query(self, srid: Optional[int]) -> Select:
239+
def _get_select_query(
240+
self,
241+
srid: Optional[int],
242+
geometry_output_format: Optional[schemas.GeometryOutputFormat],
243+
) -> Select:
203244
columns = [
204245
models.Streets.code,
205246
models.Streets.feature_id,
@@ -221,7 +262,11 @@ def _filter_by_code(self, query: Select, code: int) -> Select:
221262
class AddressesService(BaseBoundariesService):
222263
model_class = models.Addresses
223264

224-
def _get_select_query(self, srid: Optional[int]) -> Select:
265+
def _get_select_query(
266+
self,
267+
srid: Optional[int],
268+
geometry_output_format: Optional[schemas.GeometryOutputFormat],
269+
) -> Select:
225270
columns = [
226271
models.Addresses.feature_id,
227272
models.Addresses.code,
@@ -232,7 +277,7 @@ def _get_select_query(self, srid: Optional[int]) -> Select:
232277
_flat_residential_area_object,
233278
_municipality_object,
234279
_flat_street_object,
235-
self._get_geometry_field(models.Addresses.geom, srid)
280+
self._get_geometry_field(models.Addresses.geom, srid, geometry_output_format)
236281
]
237282

238283
return select(*columns).select_from(models.Addresses) \
@@ -248,13 +293,18 @@ def _filter_by_code(self, query: Select, code: int) -> Select:
248293
class RoomsService(BaseBoundariesService):
249294
model_class = models.Rooms
250295

251-
def _get_select_query(self, srid: Optional[int]) -> Select:
296+
def _get_select_query(
297+
self,
298+
srid: Optional[int],
299+
geometry_output_format: Optional[schemas.GeometryOutputFormat],
300+
) -> Select:
252301
columns = [
253302
models.Rooms.code,
254303
models.Rooms.room_number,
255304
models.Rooms.created_at,
256305
_address_short_object,
257-
] + ([self._get_geometry_field(models.Addresses.geom, srid)] if srid else [])
306+
] + ([self._get_geometry_field(models.Addresses.geom, srid,
307+
geometry_output_format)] if srid and geometry_output_format else [])
258308

259309
return select(*columns).select_from(models.Rooms) \
260310
.outerjoin(models.Rooms.address) \

0 commit comments

Comments
 (0)