add a WKB decoder for the Point class

This allows to return point geometries from the database and makes
the SQL a bit simpler.
This commit is contained in:
Sarah Hoffmann
2023-02-16 15:35:54 +01:00
parent b320f1c7e3
commit ee0c5e24bb
5 changed files with 39 additions and 22 deletions

View File

@@ -46,8 +46,7 @@ async def find_in_placex(conn: SearchConnection, place: ntyp.PlaceRef,
t.c.importance, t.c.wikipedia, t.c.indexed_date, t.c.importance, t.c.wikipedia, t.c.indexed_date,
t.c.parent_place_id, t.c.rank_address, t.c.rank_search, t.c.parent_place_id, t.c.rank_address, t.c.rank_search,
t.c.linked_place_id, t.c.linked_place_id,
sa.func.ST_X(t.c.centroid).label('x'), t.c.centroid,
sa.func.ST_Y(t.c.centroid).label('y'),
_select_column_geometry(t.c.geometry, details.geometry_output)) _select_column_geometry(t.c.geometry, details.geometry_output))
if isinstance(place, ntyp.PlaceID): if isinstance(place, ntyp.PlaceID):
@@ -76,8 +75,7 @@ async def find_in_osmline(conn: SearchConnection, place: ntyp.PlaceRef,
sql = sa.select(t.c.place_id, t.c.osm_id, t.c.parent_place_id, sql = sa.select(t.c.place_id, t.c.osm_id, t.c.parent_place_id,
t.c.indexed_date, t.c.startnumber, t.c.endnumber, t.c.indexed_date, t.c.startnumber, t.c.endnumber,
t.c.step, t.c.address, t.c.postcode, t.c.country_code, t.c.step, t.c.address, t.c.postcode, t.c.country_code,
sa.func.ST_X(sa.func.ST_Centroid(t.c.linegeo)).label('x'), t.c.linegeo.ST_Centroid().label('centroid'),
sa.func.ST_Y(sa.func.ST_Centroid(t.c.linegeo)).label('y'),
_select_column_geometry(t.c.linegeo, details.geometry_output)) _select_column_geometry(t.c.linegeo, details.geometry_output))
if isinstance(place, ntyp.PlaceID): if isinstance(place, ntyp.PlaceID):
@@ -106,8 +104,7 @@ async def find_in_tiger(conn: SearchConnection, place: ntyp.PlaceRef,
sql = sa.select(t.c.place_id, t.c.parent_place_id, sql = sa.select(t.c.place_id, t.c.parent_place_id,
t.c.startnumber, t.c.endnumber, t.c.step, t.c.startnumber, t.c.endnumber, t.c.step,
t.c.postcode, t.c.postcode,
sa.func.ST_X(sa.func.ST_Centroid(t.c.linegeo)).label('x'), t.c.linegeo.ST_Centroid().label('centroid'),
sa.func.ST_Y(sa.func.ST_Centroid(t.c.linegeo)).label('y'),
_select_column_geometry(t.c.linegeo, details.geometry_output)) _select_column_geometry(t.c.linegeo, details.geometry_output))
if isinstance(place, ntyp.PlaceID): if isinstance(place, ntyp.PlaceID):
@@ -128,8 +125,7 @@ async def find_in_postcode(conn: SearchConnection, place: ntyp.PlaceRef,
sql = sa.select(t.c.place_id, t.c.parent_place_id, sql = sa.select(t.c.place_id, t.c.parent_place_id,
t.c.rank_search, t.c.rank_address, t.c.rank_search, t.c.rank_address,
t.c.indexed_date, t.c.postcode, t.c.country_code, t.c.indexed_date, t.c.postcode, t.c.country_code,
sa.func.ST_X(t.c.geometry).label('x'), t.c.geometry.label('centroid'),
sa.func.ST_Y(t.c.geometry).label('y'),
_select_column_geometry(t.c.geometry, details.geometry_output)) _select_column_geometry(t.c.geometry, details.geometry_output))
if isinstance(place, ntyp.PlaceID): if isinstance(place, ntyp.PlaceID):

View File

@@ -132,13 +132,6 @@ class SearchResult:
return self.importance or (0.7500001 - (self.rank_search/40.0)) return self.importance or (0.7500001 - (self.rank_search/40.0))
# pylint: disable=consider-using-f-string
def centroid_as_geojson(self) -> str:
""" Get the centroid in GeoJSON format.
"""
return '{"type": "Point","coordinates": [%f, %f]}' % self.centroid
def _filter_geometries(row: SaRow) -> Dict[str, str]: def _filter_geometries(row: SaRow) -> Dict[str, str]:
return {k[9:]: v for k, v in row._mapping.items() # pylint: disable=W0212 return {k[9:]: v for k, v in row._mapping.items() # pylint: disable=W0212
if k.startswith('geometry_')} if k.startswith('geometry_')}
@@ -166,7 +159,7 @@ def create_from_placex_row(row: SaRow) -> SearchResult:
importance=row.importance, importance=row.importance,
country_code=row.country_code, country_code=row.country_code,
indexed_date=getattr(row, 'indexed_date'), indexed_date=getattr(row, 'indexed_date'),
centroid=Point(row.x, row.y), centroid=Point.from_wkb(row.centroid.data),
geometry=_filter_geometries(row)) geometry=_filter_geometries(row))
@@ -186,7 +179,7 @@ def create_from_osmline_row(row: SaRow) -> SearchResult:
'step': str(row.step)}, 'step': str(row.step)},
country_code=row.country_code, country_code=row.country_code,
indexed_date=getattr(row, 'indexed_date'), indexed_date=getattr(row, 'indexed_date'),
centroid=Point(row.x, row.y), centroid=Point.from_wkb(row.centroid.data),
geometry=_filter_geometries(row)) geometry=_filter_geometries(row))
@@ -203,7 +196,7 @@ def create_from_tiger_row(row: SaRow) -> SearchResult:
'endnumber': str(row.endnumber), 'endnumber': str(row.endnumber),
'step': str(row.step)}, 'step': str(row.step)},
country_code='us', country_code='us',
centroid=Point(row.x, row.y), centroid=Point.from_wkb(row.centroid.data),
geometry=_filter_geometries(row)) geometry=_filter_geometries(row))
@@ -219,7 +212,7 @@ def create_from_postcode_row(row: SaRow) -> SearchResult:
rank_search=row.rank_search, rank_search=row.rank_search,
rank_address=row.rank_address, rank_address=row.rank_address,
country_code=row.country_code, country_code=row.country_code,
centroid=Point(row.x, row.y), centroid=Point.from_wkb(row.centroid.data),
indexed_date=row.indexed_date, indexed_date=row.indexed_date,
geometry=_filter_geometries(row)) geometry=_filter_geometries(row))

View File

@@ -10,6 +10,7 @@ Complex datatypes used by the Nominatim API.
from typing import Optional, Union, NamedTuple from typing import Optional, Union, NamedTuple
import dataclasses import dataclasses
import enum import enum
from struct import unpack
@dataclasses.dataclass @dataclasses.dataclass
class PlaceID: class PlaceID:
@@ -55,6 +56,33 @@ class Point(NamedTuple):
return self.x return self.x
def to_geojson(self) -> str:
""" Return the point in GeoJSON format.
"""
return f'{{"type": "Point","coordinates": [{self.x}, {self.y}]}}'
@staticmethod
def from_wkb(wkb: bytes) -> 'Point':
""" Create a point from EWKB as returned from the database.
"""
if len(wkb) != 25:
raise ValueError("Point wkb has unexpected length")
if wkb[0] == 0:
gtype, srid, x, y = unpack('>iidd', wkb[1:])
elif wkb[0] == 1:
gtype, srid, x, y = unpack('<iidd', wkb[1:])
else:
raise ValueError("WKB has unknown endian value.")
if gtype != 0x20000001:
raise ValueError("WKB must be a point geometry.")
if srid != 4326:
raise ValueError("Only WGS84 WKB supported.")
return Point(x, y)
class GeometryFormat(enum.Flag): class GeometryFormat(enum.Flag):
""" Geometry output formats supported by Nominatim. """ Geometry output formats supported by Nominatim.
""" """

View File

@@ -96,7 +96,7 @@ def _add_parent_rows_grouped(writer: JsonWriter, rows: napi.AddressLines,
def _format_search_json(result: napi.SearchResult, options: Mapping[str, Any]) -> str: def _format_search_json(result: napi.SearchResult, options: Mapping[str, Any]) -> str:
locales = options.get('locales', napi.Locales()) locales = options.get('locales', napi.Locales())
geom = result.geometry.get('geojson') geom = result.geometry.get('geojson')
centroid = result.centroid_as_geojson() centroid = result.centroid.to_geojson()
out = JsonWriter() out = JsonWriter()
out.start_object()\ out.start_object()\

View File

@@ -80,7 +80,7 @@ class TestCliDetailsCall:
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def setup_status_mock(self, monkeypatch): def setup_status_mock(self, monkeypatch):
result = napi.SearchResult(napi.SourceTable.PLACEX, ('place', 'thing'), result = napi.SearchResult(napi.SourceTable.PLACEX, ('place', 'thing'),
(1.0, -3.0)) napi.Point(1.0, -3.0))
monkeypatch.setattr(napi.NominatimAPI, 'lookup', monkeypatch.setattr(napi.NominatimAPI, 'lookup',
lambda *args: result) lambda *args: result)
@@ -90,7 +90,7 @@ class TestCliDetailsCall:
('--relation', '1'), ('--relation', '1'),
('--place_id', '10001')]) ('--place_id', '10001')])
def test_status_json_format(self, cli_call, tmp_path, capsys, params): def test_details_json_format(self, cli_call, tmp_path, capsys, params):
result = cli_call('details', '--project-dir', str(tmp_path), *params) result = cli_call('details', '--project-dir', str(tmp_path), *params)
assert result == 0 assert result == 0