From fb440f29a2613ae9337ffd713c48848957e87171 Mon Sep 17 00:00:00 2001 From: Sarah Hoffmann Date: Mon, 31 Mar 2025 09:39:01 +0200 Subject: [PATCH] implement BDD osm2pgsql tests with pytest-bdd --- test/bdd/conftest.py | 65 ++- test/bdd/features/api/reverse/v1_json.feature | 4 +- test/bdd/features/api/reverse/v1_xml.feature | 4 +- .../features/osm2pgsql/import/broken.feature | 35 ++ .../osm2pgsql/import/custom_style.feature | 318 +++++++++++ .../osm2pgsql/import/relation.feature | 10 + .../features/osm2pgsql/import/simple.feature | 42 ++ .../features/osm2pgsql/import/tags.feature | 289 ++++++++++ .../osm2pgsql/update/interpolations.feature | 135 +++++ .../osm2pgsql/update/postcodes.feature | 167 ++++++ .../osm2pgsql/update/relation.feature | 140 +++++ .../features/osm2pgsql/update/simple.feature | 48 ++ .../features/osm2pgsql/update/tags.feature | 512 ++++++++++++++++++ test/bdd/test_osm2pgsql.py | 152 ++++++ test/bdd/utils/checks.py | 146 ++++- test/bdd/utils/db.py | 57 ++ test/bdd/utils/geometry_alias.py | 262 +++++++++ test/bdd/utils/grid.py | 34 ++ 18 files changed, 2406 insertions(+), 14 deletions(-) create mode 100644 test/bdd/features/osm2pgsql/import/broken.feature create mode 100644 test/bdd/features/osm2pgsql/import/custom_style.feature create mode 100644 test/bdd/features/osm2pgsql/import/relation.feature create mode 100644 test/bdd/features/osm2pgsql/import/simple.feature create mode 100644 test/bdd/features/osm2pgsql/import/tags.feature create mode 100644 test/bdd/features/osm2pgsql/update/interpolations.feature create mode 100644 test/bdd/features/osm2pgsql/update/postcodes.feature create mode 100644 test/bdd/features/osm2pgsql/update/relation.feature create mode 100644 test/bdd/features/osm2pgsql/update/simple.feature create mode 100644 test/bdd/features/osm2pgsql/update/tags.feature create mode 100644 test/bdd/test_osm2pgsql.py create mode 100644 test/bdd/utils/geometry_alias.py create mode 100644 test/bdd/utils/grid.py diff --git a/test/bdd/conftest.py b/test/bdd/conftest.py index 97d09dee..06af37d4 100644 --- a/test/bdd/conftest.py +++ b/test/bdd/conftest.py @@ -11,18 +11,24 @@ import sys import json from pathlib import Path +# always test against the source +SRC_DIR = (Path(__file__) / '..' / '..' / '..').resolve() +sys.path.insert(0, str(SRC_DIR / 'src')) + import pytest from pytest_bdd.parsers import re as step_parse -from pytest_bdd import when, then +from pytest_bdd import given, when, then + +pytest.register_assert_rewrite('utils') from utils.api_runner import APIRunner from utils.api_result import APIResult from utils.checks import ResultAttr, COMPARATOR_TERMS +from utils.geometry_alias import ALIASES +from utils.grid import Grid +from utils.db import DBManager -# always test against the source -SRC_DIR = (Path(__file__) / '..' / '..' / '..').resolve() -sys.path.insert(0, str(SRC_DIR / 'src')) - +from nominatim_db.config import Configuration def _strlist(inp): return [s.strip() for s in inp.split(',')] @@ -60,6 +66,35 @@ def datatable(): return None +@pytest.fixture +def node_grid(): + """ Default fixture for node grids. Nothing set. + """ + return Grid([[]], None, None) + + +@pytest.fixture(scope='session') +def template_db(pytestconfig): + """ Create a template database containing the extensions and base data + needed by Nominatim. Using the template instead of doing the full + setup can speed up the tests. + + The template database will only be created if it does not exist yet + or a purge has been explicitly requested. + """ + dbm = DBManager(purge=pytestconfig.option.NOMINATIM_PURGE) + + template_db = pytestconfig.getini('nominatim_template_db') + + template_config = Configuration( + None, environ={'NOMINATIM_DATABASE_DSN': f"pgsql:dbname={template_db}"}) + + dbm.setup_template_db(template_config) + + return template_db + + + @when(step_parse(r'reverse geocoding (?P[\d.-]*),(?P[\d.-]*)'), target_fixture='nominatim_result') def reverse_geocode_via_api(test_config_env, pytestconfig, datatable, lat, lon): @@ -223,3 +258,23 @@ def check_specific_result_for_fields(nominatim_result, datatable, num, field): for k, v in pairs: assert ResultAttr(nominatim_result.result[num], prefix + k) == v + + +@given(step_parse(r'the (?P[0-9.]+ )?grid(?: with origin (?P.*))?'), + target_fixture='node_grid') +def set_node_grid(datatable, step, origin): + if step is not None: + step = float(step) + + if origin: + if ',' in origin: + coords = origin.split(',') + if len(coords) != 2: + raise RuntimeError('Grid origin expects origin with x,y coordinates.') + origin = list(map(float, coords)) + elif origin in ALIASES: + origin = ALIASES[origin] + else: + raise RuntimeError('Grid origin must be either coordinate or alias.') + + return Grid(datatable, step, origin) diff --git a/test/bdd/features/api/reverse/v1_json.feature b/test/bdd/features/api/reverse/v1_json.feature index ca361033..829adc5a 100644 --- a/test/bdd/features/api/reverse/v1_json.feature +++ b/test/bdd/features/api/reverse/v1_json.feature @@ -136,8 +136,8 @@ Feature: Json output for Reverse API Then a HTTP 200 is returned And the result is valid json And the result contains - | geotext!fm | - | LINESTRING\(9.5039353 47.0657546, ?9.5040437 47.0657781, ?9.5040808 47.065787, ?9.5054298 47.0661407\) | + | geotext!wkt | + | 9.5039353 47.0657546, 9.5040437 47.0657781, 9.5040808 47.065787, 9.5054298 47.0661407 | Examples: | format | diff --git a/test/bdd/features/api/reverse/v1_xml.feature b/test/bdd/features/api/reverse/v1_xml.feature index e4a25ff3..55cf0196 100644 --- a/test/bdd/features/api/reverse/v1_xml.feature +++ b/test/bdd/features/api/reverse/v1_xml.feature @@ -92,8 +92,8 @@ Feature: XML output for Reverse API Then a HTTP 200 is returned And the result is valid xml And the result contains - | geotext!fm | - | LINESTRING\(9.5039353 47.0657546, ?9.5040437 47.0657781, ?9.5040808 47.065787, ?9.5054298 47.0661407\) | + | geotext!wkt | + | 9.5039353 47.0657546, 9.5040437 47.0657781, 9.5040808 47.065787, 9.5054298 47.0661407 | Scenario: Reverse XML - Output of SVG When sending v1/reverse with format xml diff --git a/test/bdd/features/osm2pgsql/import/broken.feature b/test/bdd/features/osm2pgsql/import/broken.feature new file mode 100644 index 00000000..6f04a30f --- /dev/null +++ b/test/bdd/features/osm2pgsql/import/broken.feature @@ -0,0 +1,35 @@ +Feature: Import of objects with broken geometries by osm2pgsql + + Scenario: Import way with double nodes + When loading osm data + """ + n100 x0 y0 + n101 x0 y0.1 + n102 x0.1 y0.2 + w1 Thighway=primary Nn100,n101,n101,n102 + """ + Then place contains + | object | class | type | geometry!wkt | + | W1 | highway | primary | 0 0, 0 0.1, 0.1 0.2 | + + Scenario: Import of ballon areas + Given the grid + | 2 | | 3 | + | 1 | | 4 | + | 5 | | | + When loading osm data + """ + n1 + n2 + n3 + n4 + n5 + w1 Thighway=unclassified Nn1,n2,n3,n4,n1,n5 + w2 Thighway=unclassified Nn1,n2,n3,n4,n1 + w3 Thighway=unclassified Nn1,n2,n3,n4,n3 + """ + Then place contains + | object | geometry!wkt | + | W1 | 1,2,3,4,1,5 | + | W2 | (1,2,3,4,1) | + | W3 | 1,2,3,4 | diff --git a/test/bdd/features/osm2pgsql/import/custom_style.feature b/test/bdd/features/osm2pgsql/import/custom_style.feature new file mode 100644 index 00000000..05ab73aa --- /dev/null +++ b/test/bdd/features/osm2pgsql/import/custom_style.feature @@ -0,0 +1,318 @@ +Feature: Import with custom styles by osm2pgsql + Tests for the example customizations given in the documentation. + + Scenario: Custom main tags (set new ones) + Given the lua style file + """ + local flex = require('import-full') + + flex.set_main_tags{ + boundary = {administrative = 'named'}, + highway = {'always', street_lamp = 'named'}, + landuse = 'fallback' + } + """ + When loading osm data + """ + n10 Tboundary=administrative x0 y0 + n11 Tboundary=administrative,name=Foo x0 y0 + n12 Tboundary=electoral x0 y0 + n13 Thighway=primary x0 y0 + n14 Thighway=street_lamp x0 y0 + n15 Thighway=primary,landuse=street x0 y0 + """ + Then place contains exactly + | object | class | type | + | N11 | boundary | administrative | + | N13 | highway | primary | + | N15 | highway | primary | + + Scenario: Custom main tags (modify existing) + Given the lua style file + """ + local flex = require('import-full') + + flex.modify_main_tags{ + amenity = {prison = 'delete'}, + highway = {stop = 'named'}, + aeroway = 'named' + } + """ + When loading osm data + """ + n10 Tamenity=hotel x0 y0 + n11 Tamenity=prison x0 y0 + n12 Thighway=stop x0 y0 + n13 Thighway=stop,name=BigStop x0 y0 + n14 Thighway=give_way x0 y0 + n15 Thighway=bus_stop x0 y0 + n16 Taeroway=no,name=foo x0 y0 + n17 Taeroway=taxiway,name=D15 x0 y0 + """ + Then place contains exactly + | object | class | type | + | N10 | amenity | hotel | + | N13 | highway | stop | + | N15 | highway | bus_stop | + | N17 | aeroway | taxiway | + + Scenario: Prefiltering tags + Given the lua style file + """ + local flex = require('import-full') + + flex.set_prefilters{ + delete_keys = {'source', 'source:*'}, + extra_tags = {amenity = {'yes', 'no'}} + } + flex.set_main_tags{ + amenity = 'always', + tourism = 'always' + } + """ + When loading osm data + """ + n1 Tamenity=yes x0 y6 + n2 Tamenity=hospital,source=survey x3 y6 + n3 Ttourism=hotel,amenity=yes x0 y0 + n4 Ttourism=hotel,amenity=telephone x0 y0 + """ + Then place contains exactly + | object | class | extratags!dict | + | N2 | amenity | - | + | N3 | tourism | 'amenity': 'yes' | + | N4 | tourism | - | + | N4 | amenity | - | + + Scenario: Ignore some tags + Given the lua style file + """ + local flex = require('import-extratags') + + flex.ignore_keys{'ref:*', 'surface'} + """ + When loading osm data + """ + n100 Thighway=residential,ref=34,ref:bodo=34,surface=gray,extra=1 x0 y0 + """ + Then place contains exactly + | object | name!dict | extratags!dict | + | N100 | 'ref' : '34' | 'extra': '1' | + + + Scenario: Add for extratags + Given the lua style file + """ + local flex = require('import-full') + + flex.add_for_extratags{'ref:*', 'surface'} + """ + When loading osm data + """ + n100 Thighway=residential,ref=34,ref:bodo=34,surface=gray,extra=1 x0 y0 + """ + Then place contains exactly + | object | name!dict | extratags!dict | + | N100 | 'ref' : '34' | 'ref:bodo': '34', 'surface': 'gray' | + + + Scenario: Name tags + Given the lua style file + """ + local flex = require('flex-base') + + flex.set_main_tags{highway = {traffic_light = 'named'}} + flex.set_name_tags{main = {'name', 'name:*'}, + extra = {'ref'} + } + """ + When loading osm data + """ + n1 Thighway=stop,name=Something x0 y0 + n2 Thighway=traffic_light,ref=453-4 x0 y0 + n3 Thighway=traffic_light,name=Greens x0 y0 + n4 Thighway=traffic_light,name=Red,ref=45 x0 y0 + """ + Then place contains exactly + | object | class | name!dict | + | N3 | highway | 'name': 'Greens' | + | N4 | highway | 'name': 'Red', 'ref': '45' | + + Scenario: Modify name tags + Given the lua style file + """ + local flex = require('import-full') + + flex.modify_name_tags{house = {}, extra = {'o'}} + """ + When loading osm data + """ + n1 Ttourism=hotel,ref=45,o=good + n2 Taddr:housename=Old,addr:street=Away + """ + Then place contains exactly + | object | class | name!dict | + | N1 | tourism | 'o': 'good' | + + Scenario: Address tags + Given the lua style file + """ + local flex = require('import-full') + + flex.set_address_tags{ + main = {'addr:housenumber'}, + extra = {'addr:*'}, + postcode = {'postal_code', 'postcode', 'addr:postcode'}, + country = {'country-code', 'ISO3166-1'} + } + """ + When loading osm data + """ + n1 Ttourism=hotel,addr:street=Foo x0 y0 + n2 Taddr:housenumber=23,addr:street=Budd,postal_code=5567 x0 y0 + n3 Taddr:street=None,addr:city=Where x0 y0 + """ + Then place contains exactly + | object | class | type | address!dict | + | N1 | tourism | hotel | 'street': 'Foo' | + | N2 | place | house | 'housenumber': '23', 'street': 'Budd', 'postcode': '5567' | + + Scenario: Modify address tags + Given the lua style file + """ + local flex = require('import-full') + + flex.set_address_tags{ + extra = {'addr:*'}, + } + """ + When loading osm data + """ + n2 Taddr:housenumber=23,addr:street=Budd,is_in:city=Faraway,postal_code=5567 x0 y0 + """ + Then place contains exactly + | object | class | type | address!dict | + | N2 | place | house | 'housenumber': '23', 'street': 'Budd', 'postcode': '5567' | + + Scenario: Unused handling (delete) + Given the lua style file + """ + local flex = require('import-full') + + flex.set_address_tags{ + main = {'addr:housenumber'}, + extra = {'addr:*', 'tiger:county'} + } + flex.set_unused_handling{delete_keys = {'tiger:*'}} + """ + When loading osm data + """ + n1 Ttourism=hotel,tiger:county=Fargo x0 y0 + n2 Ttourism=hotel,tiger:xxd=56,else=other x0 y0 + """ + Then place contains exactly + | object | class | type | address!dict | extratags!dict | + | N1 | tourism | hotel | 'tiger:county': 'Fargo' | - | + | N2 | tourism | hotel | - | 'else': 'other' | + + Scenario: Unused handling (extra) + Given the lua style file + """ + local flex = require('flex-base') + flex.set_main_tags{highway = 'always', + wikipedia = 'extra'} + flex.add_for_extratags{'wikipedia:*', 'wikidata'} + flex.set_unused_handling{extra_keys = {'surface'}} + """ + When loading osm data + """ + n100 Thighway=path,foo=bar,wikipedia=en:Path x0 y0 + n234 Thighway=path,surface=rough x0 y0 + n445 Thighway=path,name=something x0 y0 + n446 Thighway=path,wikipedia:en=Path,wikidata=Q23 x0 y0 + n567 Thighway=path,surface=dirt,wikipedia:en=Path x0 y0 + """ + Then place contains exactly + | object | class | type | extratags!dict | + | N100 | highway | path | 'wikipedia': 'en:Path' | + | N234 | highway | path | 'surface': 'rough' | + | N445 | highway | path | - | + | N446 | highway | path | 'wikipedia:en': 'Path', 'wikidata': 'Q23' | + | N567 | highway | path | 'surface': 'dirt', 'wikipedia:en': 'Path' | + + Scenario: Additional relation types + Given the lua style file + """ + local flex = require('import-full') + + flex.RELATION_TYPES['site'] = flex.relation_as_multipolygon + """ + And the grid + | 1 | 2 | + | 4 | 3 | + When loading osm data + """ + n1 + n2 + n3 + n4 + w1 Nn1,n2,n3,n4,n1 + r1 Ttype=multipolygon,amenity=school Mw1@ + r2 Ttype=site,amenity=school Mw1@ + """ + Then place contains exactly + | object | class | type | + | R1 | amenity | school | + | R2 | amenity | school | + + Scenario: Exclude country relations + Given the lua style file + """ + local flex = require('import-full') + + function osm2pgsql.process_relation(object) + if object.tags.boundary ~= 'administrative' or object.tags.admin_level ~= '2' then + flex.process_relation(object) + end + end + """ + And the grid + | 1 | 2 | + | 4 | 3 | + When loading osm data + """ + n1 + n2 + n3 + n4 + w1 Nn1,n2,n3,n4,n1 + r1 Ttype=multipolygon,boundary=administrative,admin_level=4,name=Small Mw1@ + r2 Ttype=multipolygon,boundary=administrative,admin_level=2,name=Big Mw1@ + """ + Then place contains exactly + | object | class | type | + | R1 | boundary | administrative | + + Scenario: Customize processing functions + Given the lua style file + """ + local flex = require('import-full') + + local original_process_tags = flex.process_tags + + function flex.process_tags(o) + if o.object.tags.highway ~= nil and o.object.tags.access == 'no' then + return + end + + original_process_tags(o) + end + """ + When loading osm data + """ + n1 Thighway=residential x0 y0 + n2 Thighway=residential,access=no x0 y0 + """ + Then place contains exactly + | object | class | type | + | N1 | highway | residential | diff --git a/test/bdd/features/osm2pgsql/import/relation.feature b/test/bdd/features/osm2pgsql/import/relation.feature new file mode 100644 index 00000000..13d4278e --- /dev/null +++ b/test/bdd/features/osm2pgsql/import/relation.feature @@ -0,0 +1,10 @@ +Feature: Import of relations by osm2pgsql + Testing specific relation problems related to members. + + Scenario: Don't import empty waterways + When loading osm data + """ + n1 Tamenity=prison,name=foo + r1 Ttype=waterway,waterway=river,name=XZ Mn1@ + """ + Then place has no entry for R1 diff --git a/test/bdd/features/osm2pgsql/import/simple.feature b/test/bdd/features/osm2pgsql/import/simple.feature new file mode 100644 index 00000000..217c2b7c --- /dev/null +++ b/test/bdd/features/osm2pgsql/import/simple.feature @@ -0,0 +1,42 @@ +Feature: Import of simple objects by osm2pgsql + Testing basic tagging in osm2pgsql imports. + + Scenario: Import simple objects + When loading osm data + """ + n1 Tamenity=prison,name=foo x34.3 y-23 + n100 x0 y0 + n101 x0 y0.1 + n102 x0.1 y0.2 + n200 x0 y0 + n201 x0 y1 + n202 x1 y1 + n203 x1 y0 + w1 Tshop=toys,name=tata Nn100,n101,n102 + w2 Tref=45 Nn200,n201,n202,n203,n200 + r1 Ttype=multipolygon,tourism=hotel,name=XZ Mn1@,w2@ + """ + Then place contains exactly + | object | class | type | name!dict | geometry!wkt | + | N1 | amenity | prison | 'name' : 'foo' | 34.3 -23 | + | W1 | shop | toys | 'name' : 'tata' | 0 0, 0 0.1, 0.1 0.2 | + | R1 | tourism | hotel | 'name' : 'XZ' | (0 0, 0 1, 1 1, 1 0, 0 0) | + + Scenario: Import object with two main tags + When loading osm data + """ + n1 Ttourism=hotel,amenity=restaurant,name=foo + """ + Then place contains exactly + | object | class | type | name!dict | + | N1 | tourism | hotel | 'name' : 'foo' | + | N1 | amenity | restaurant | 'name' : 'foo' | + + Scenario: Import stand-alone house number with postcode + When loading osm data + """ + n1 Taddr:housenumber=4,addr:postcode=3345 + """ + Then place contains exactly + | object | class | type | + | N1 | place | house | diff --git a/test/bdd/features/osm2pgsql/import/tags.feature b/test/bdd/features/osm2pgsql/import/tags.feature new file mode 100644 index 00000000..0671a43f --- /dev/null +++ b/test/bdd/features/osm2pgsql/import/tags.feature @@ -0,0 +1,289 @@ +Feature: Tag evaluation + Tests if tags are correctly imported into the place table + + Scenario: Main tags as fallback + When loading osm data + """ + n100 Tjunction=yes,highway=bus_stop + n101 Tjunction=yes,name=Bar + n200 Tbuilding=yes,amenity=cafe + n201 Tbuilding=yes,name=Intersting + n202 Tbuilding=yes + """ + Then place contains exactly + | object | class | type | + | N100 | highway | bus_stop | + | N101 | junction | yes | + | N200 | amenity | cafe | + | N201 | building | yes | + + + Scenario: Name and reg tags + When loading osm data + """ + n2001 Thighway=road,name=Foo,alt_name:de=Bar,ref=45 + n2002 Thighway=road,name:prefix=Pre,name:suffix=Post,ref:de=55 + n2003 Thighway=yes,name:%20%de=Foo,name=real1 + n2004 Thighway=yes,name:%a%de=Foo,name=real2 + n2005 Thighway=yes,name:%9%de=Foo,name:\\=real3 + n2006 Thighway=yes,name:%9%de=Foo,name=rea\l3 + """ + Then place contains exactly + | object | class | type | name!dict | + | N2001 | highway | road | 'name': 'Foo', 'alt_name:de': 'Bar', 'ref': '45' | + | N2002 | highway | road | - | + | N2003 | highway | yes | 'name: de': 'Foo', 'name': 'real1' | + | N2004 | highway | yes | 'name:\\nde': 'Foo', 'name': 'real2' | + | N2005 | highway | yes | 'name:\tde': 'Foo', r'name:\\\\': 'real3' | + | N2006 | highway | yes | 'name:\tde': 'Foo', 'name': r'rea\l3' | + + And place contains + | object | extratags!dict | + | N2002 | 'name:prefix': 'Pre', 'name:suffix': 'Post', 'ref:de': '55' | + + + Scenario: Name when using with_name flag + When loading osm data + """ + n3001 Tbridge=yes,bridge:name=GoldenGate + n3002 Tbridge=yes,bridge:name:en=Rainbow + """ + Then place contains exactly + | object | class | type | name!dict | + | N3001 | bridge | yes | 'name': 'GoldenGate' | + | N3002 | bridge | yes | 'name:en': 'Rainbow' | + + + Scenario: Address tags + When loading osm data + """ + n4001 Taddr:housenumber=34,addr:city=Esmarald,addr:county=Land + n4002 Taddr:streetnumber=10,is_in:city=Rootoo,is_in=Gold + """ + Then place contains exactly + | object | class | address!dict | + | N4001 | place | 'housenumber': '34', 'city': 'Esmarald', 'county': 'Land' | + | N4002 | place | 'streetnumber': '10', 'city': 'Rootoo' | + + + Scenario: Country codes + When loading osm data + """ + n5001 Tshop=yes,country_code=DE + n5002 Tshop=yes,country_code=toolong + n5003 Tshop=yes,country_code=x + n5004 Tshop=yes,addr:country=us + n5005 Tshop=yes,country=be + n5006 Tshop=yes,addr:country=France + """ + Then place contains exactly + | object | class | address!dict | + | N5001 | shop | 'country': 'DE' | + | N5002 | shop | - | + | N5003 | shop | - | + | N5004 | shop | 'country': 'us' | + | N5005 | shop | - | + | N5006 | shop | - | + + + Scenario: Postcodes + When loading osm data + """ + n6001 Tshop=bank,addr:postcode=12345 + n6002 Tshop=bank,tiger:zip_left=34343 + n6003 Tshop=bank,is_in:postcode=9009 + """ + Then place contains exactly + | object | class | address!dict | + | N6001 | shop | 'postcode': '12345' | + | N6002 | shop | 'postcode': '34343' | + | N6003 | shop | - | + + + Scenario: Postcode areas + When loading osm data + """ + n1 x12.36853 y51.50618 + n2 x12.36853 y51.42362 + n3 x12.63666 y51.42362 + n4 x12.63666 y51.50618 + w1 Tboundary=postal_code,ref=3456 Nn1,n2,n3,n4,n1 + """ + Then place contains exactly + | object | class | type | name!dict | + | W1 | boundary | postal_code | 'ref': '3456' | + + Scenario: Main with extra + When loading osm data + """ + n7001 Thighway=primary,bridge=yes,name=1 + n7002 Thighway=primary,bridge=yes,bridge:name=1 + """ + Then place contains exactly + | object | class | type | name!dict | extratags!dict | + | N7001 | highway | primary | 'name': '1' | 'bridge': 'yes' | + | N7002 | highway | primary | - | 'bridge': 'yes', 'bridge:name': '1' | + | N7002 | bridge | yes | 'name': '1' | 'highway': 'primary', 'bridge:name': '1' | + + + Scenario: Global fallback and skipping + When loading osm data + """ + n8001 Tshop=shoes,note:de=Nein,xx=yy + n8002 Tshop=shoes,natural=no,ele=234 + n8003 Tshop=shoes,name:source=survey + """ + Then place contains exactly + | object | class | name!dict | extratags!dict | + | N8001 | shop | - | 'xx': 'yy' | + | N8002 | shop | - | 'ele': '234' | + | N8003 | shop | - | - | + + + Scenario: Admin levels + When loading osm data + """ + n9001 Tplace=city + n9002 Tplace=city,admin_level=16 + n9003 Tplace=city,admin_level=x + n9004 Tplace=city,admin_level=1 + n9005 Tplace=city,admin_level=0 + n9006 Tplace=city,admin_level=2.5 + """ + Then place contains exactly + | object | class | admin_level | + | N9001 | place | 15 | + | N9002 | place | 15 | + | N9003 | place | 15 | + | N9004 | place | 1 | + | N9005 | place | 15 | + | N9006 | place | 15 | + + + Scenario: Administrative boundaries with place tags + When loading osm data + """ + n10001 Tboundary=administrative,place=city,name=A + n10002 Tboundary=natural,place=city,name=B + n10003 Tboundary=administrative,place=island,name=C + """ + Then place contains + | object | class | type | extratags!dict | + | N10001 | boundary | administrative | 'place': 'city' | + And place contains + | object | class | type | + | N10002 | boundary | natural | + | N10002 | place | city | + | N10003 | boundary | administrative | + | N10003 | place | island | + + + Scenario: Building fallbacks + When loading osm data + """ + n12001 Ttourism=hotel,building=yes + n12002 Tbuilding=house + n12003 Tbuilding=shed,addr:housenumber=1 + n12004 Tbuilding=yes,name=Das-Haus + n12005 Tbuilding=yes,addr:postcode=12345 + """ + Then place contains exactly + | object | class | type | + | N12001 | tourism | hotel | + | N12003 | building | shed | + | N12004 | building | yes | + | N12005 | place | postcode | + + + Scenario: Address interpolations + When loading osm data + """ + n13001 Taddr:interpolation=odd + n13002 Taddr:interpolation=even,place=city + """ + Then place contains exactly + | object | class | type | address!dict | + | N13001 | place | houses | 'interpolation': 'odd' | + | N13002 | place | houses | 'interpolation': 'even' | + + + Scenario: Footways + When loading osm data + """ + n1 x0.0 y0.0 + n2 x0 y0.0001 + w1 Thighway=footway Nn1,n2 + w2 Thighway=footway,name=Road Nn1,n2 + w3 Thighway=footway,name=Road,footway=sidewalk Nn1,n2 + w4 Thighway=footway,name=Road,footway=crossing Nn1,n2 + w5 Thighway=footway,name=Road,footway=residential Nn1,n2 + """ + Then place contains exactly + | object | name+name | + | W2 | Road | + | W5 | Road | + + + Scenario: Tourism information + When loading osm data + """ + n100 Ttourism=information + n101 Ttourism=information,name=Generic + n102 Ttourism=information,information=guidepost + n103 Thighway=information,information=house + n104 Ttourism=information,information=yes,name=Something + n105 Ttourism=information,information=route_marker,name=3 + """ + Then place contains exactly + | object | class | type | + | N100 | tourism | information | + | N101 | tourism | information | + | N102 | information | guidepost | + | N103 | highway | information | + | N104 | tourism | information | + + + Scenario: Water features + When loading osm data + """ + n20 Tnatural=water + n21 Tnatural=water,name=SomePond + n22 Tnatural=water,water=pond + n23 Tnatural=water,water=pond,name=Pond + n24 Tnatural=water,water=river,name=BigRiver + n25 Tnatural=water,water=yes + n26 Tnatural=water,water=yes,name=Random + """ + Then place contains exactly + | object | class | type | + | N21 | natural | water | + | N23 | water | pond | + | N26 | natural | water | + + Scenario: Drop name for address fallback + When loading osm data + """ + n1 Taddr:housenumber=23,name=Foo + n2 Taddr:housenumber=23,addr:housename=Foo + n3 Taddr:housenumber=23 + """ + Then place contains exactly + | object | class | type | address!dict | name!dict | + | N1 | place | house | 'housenumber': '23' | - | + | N2 | place | house | 'housenumber': '23' | 'addr:housename': 'Foo' | + | N3 | place | house | 'housenumber': '23' | - | + + + Scenario: Waterway locks + When loading osm data + """ + n1 Twaterway=river,lock=yes + n2 Twaterway=river,lock=yes,lock_name=LeLock + n3 Twaterway=river,lock=yes,name=LeWater + n4 Tamenity=parking,lock=yes,lock_name=Gold + """ + Then place contains exactly + | object | class | type | name!dict | + | N2 | lock | yes | 'name': 'LeLock' | + | N3 | waterway | river | 'name': 'LeWater' | + | N4 | amenity | parking | - | diff --git a/test/bdd/features/osm2pgsql/update/interpolations.feature b/test/bdd/features/osm2pgsql/update/interpolations.feature new file mode 100644 index 00000000..ca87ed12 --- /dev/null +++ b/test/bdd/features/osm2pgsql/update/interpolations.feature @@ -0,0 +1,135 @@ +Feature: Updates of address interpolation objects + Test that changes to address interpolation objects are correctly + propagated. + + Background: + Given the grid + | 1 | 2 | + + + Scenario: Adding a new interpolation + When loading osm data + """ + n1 Taddr:housenumber=3 + n2 Taddr:housenumber=17 + w33 Thighway=residential,name=Tao Nn1,n2 + """ + Then place contains + | object | class | type | + | N1 | place | house | + | N2 | place | house | + + When updating osm data + """ + w99 Taddr:interpolation=odd Nn1,n2 + """ + Then place contains + | object | class | type | + | N1 | place | house | + | N2 | place | house | + | W99 | place | houses | + When indexing + Then placex contains exactly + | object | class | type | + | N1 | place | house | + | N2 | place | house | + | W33 | highway | residential | + Then location_property_osmline contains exactly + | osm_id | startnumber | + | 99 | 5 | + + + Scenario: Delete an existing interpolation + When loading osm data + """ + n1 Taddr:housenumber=2 + n2 Taddr:housenumber=7 + w99 Taddr:interpolation=odd Nn1,n2 + """ + Then place contains + | object | class | type | + | N1 | place | house | + | N2 | place | house | + | W99 | place | houses | + + When updating osm data + """ + w99 v2 dD + """ + Then place contains + | object | class | type | + | N1 | place | house | + | N2 | place | house | + When indexing + Then placex contains exactly + | object | class | type | + | N1 | place | house | + | N2 | place | house | + Then location_property_osmline contains exactly + | osm_id | + + + Scenario: Changing an object to an interpolation + When loading osm data + """ + n1 Taddr:housenumber=3 + n2 Taddr:housenumber=17 + w33 Thighway=residential Nn1,n2 + w99 Thighway=residential Nn1,n2 + """ + Then place contains + | object | class | type | + | N1 | place | house | + | N2 | place | house | + | W99 | highway | residential | + + When updating osm data + """ + w99 Taddr:interpolation=odd Nn1,n2 + """ + Then place contains + | object | class | type | + | N1 | place | house | + | N2 | place | house | + | W99 | place | houses | + When indexing + Then placex contains exactly + | object | class | type | + | N1 | place | house | + | N2 | place | house | + | W33 | highway | residential | + And location_property_osmline contains exactly + | osm_id | startnumber | + | 99 | 5 | + + + Scenario: Changing an interpolation to something else + When loading osm data + """ + n1 Taddr:housenumber=3 + n2 Taddr:housenumber=17 + w99 Taddr:interpolation=odd Nn1,n2 + """ + Then place contains + | object | class | type | + | N1 | place | house | + | N2 | place | house | + | W99 | place | houses | + + When updating osm data + """ + w99 Thighway=residential Nn1,n2 + """ + Then place contains + | object | class | type | + | N1 | place | house | + | N2 | place | house | + | W99 | highway | residential | + When indexing + Then placex contains exactly + | object | class | type | + | N1 | place | house | + | N2 | place | house | + | W99 | highway | residential | + And location_property_osmline contains exactly + | osm_id | diff --git a/test/bdd/features/osm2pgsql/update/postcodes.feature b/test/bdd/features/osm2pgsql/update/postcodes.feature new file mode 100644 index 00000000..607eeccb --- /dev/null +++ b/test/bdd/features/osm2pgsql/update/postcodes.feature @@ -0,0 +1,167 @@ +Feature: Update of postcode only objects + Tests that changes to objects containing only a postcode are + propagated correctly. + + + Scenario: Adding a postcode-only node + When loading osm data + """ + n1 + """ + Then place contains exactly + | object | + + When updating osm data + """ + n34 Tpostcode=4456 + """ + Then place contains exactly + | object | class | type | + | N34 | place | postcode | + When indexing + Then placex contains exactly + | object | + + + Scenario: Deleting a postcode-only node + When loading osm data + """ + n34 Tpostcode=4456 + """ + Then place contains exactly + | object | class | type | + | N34 | place | postcode | + + When updating osm data + """ + n34 v2 dD + """ + Then place contains exactly + | object | + When indexing + Then placex contains exactly + | object | + + + Scenario Outline: Converting a regular object into a postcode-only node + When loading osm data + """ + n34 T= + """ + Then place contains exactly + | object | class | type | + | N34 | | | + + When updating osm data + """ + n34 Tpostcode=4456 + """ + Then place contains exactly + | object | class | type | + | N34 | place | postcode | + When indexing + Then placex contains exactly + | object | + + Examples: + | class | type | + | amenity | restaurant | + | place | hamlet | + + + Scenario Outline: Converting a postcode-only node into a regular object + When loading osm data + """ + n34 Tpostcode=4456 + """ + Then place contains exactly + | object | class | type | + | N34 | place | postcode | + + When updating osm data + """ + n34 T= + """ + Then place contains exactly + | object | class | type | + | N34 | | | + When indexing + Then placex contains exactly + | object | class | type | + | N34 | | | + + Examples: + | class | type | + | amenity | restaurant | + | place | hamlet | + + + Scenario: Converting na interpolation into a postcode-only node + Given the grid + | 1 | 2 | + When loading osm data + """ + n1 Taddr:housenumber=3 + n2 Taddr:housenumber=17 + w34 Taddr:interpolation=odd Nn1,n2 + """ + Then place contains exactly + | object | class | type | + | N1 | place | house | + | N2 | place | house | + | W34 | place | houses | + + When updating osm data + """ + w34 Tpostcode=4456 Nn1,n2 + """ + Then place contains exactly + | object | class | type | + | N1 | place | house | + | N2 | place | house | + | W34 | place | postcode | + When indexing + Then location_property_osmline contains exactly + | osm_id | + And placex contains exactly + | object | class | type | + | N1 | place | house | + | N2 | place | house | + + + Scenario: Converting a postcode-only node into an interpolation + Given the grid + | 1 | 2 | + When loading osm data + """ + n1 Taddr:housenumber=3 + n2 Taddr:housenumber=17 + w33 Thighway=residential Nn1,n2 + w34 Tpostcode=4456 Nn1,n2 + """ + Then place contains exactly + | object | class | type | + | N1 | place | house | + | N2 | place | house | + | W33 | highway | residential | + | W34 | place | postcode | + + When updating osm data + """ + w34 Taddr:interpolation=odd Nn1,n2 + """ + Then place contains exactly + | object | class | type | + | N1 | place | house | + | N2 | place | house | + | W33 | highway | residential | + | W34 | place | houses | + When indexing + Then location_property_osmline contains exactly + | osm_id | startnumber | endnumber | + | 34 | 5 | 15 | + And placex contains exactly + | object | class | type | + | N1 | place | house | + | N2 | place | house | + | W33 | highway | residential | diff --git a/test/bdd/features/osm2pgsql/update/relation.feature b/test/bdd/features/osm2pgsql/update/relation.feature new file mode 100644 index 00000000..302231b4 --- /dev/null +++ b/test/bdd/features/osm2pgsql/update/relation.feature @@ -0,0 +1,140 @@ +Feature: Update of relations by osm2pgsql + Testing relation update by osm2pgsql. + + Scenario: Remove all members of a relation + When loading osm data + """ + n1 Tamenity=prison,name=foo + n200 x0 y0 + n201 x0 y0.0001 + n202 x0.0001 y0.0001 + n203 x0.0001 y0 + w2 Tref=45' Nn200,n201,n202,n203,n200 + r1 Ttype=multipolygon,tourism=hotel,name=XZ Mw2@ + """ + Then place contains + | object | class | type | name!dict | + | R1 | tourism | hotel | 'name' : 'XZ' | + When updating osm data + """ + r1 Ttype=multipolygon,tourism=hotel,name=XZ Mn1@ + """ + Then place has no entry for R1 + + + Scenario: Change type of a relation + When loading osm data + """ + n200 x0 y0 + n201 x0 y0.0001 + n202 x0.0001 y0.0001 + n203 x0.0001 y0 + w2 Tref=45 Nn200,n201,n202,n203,n200 + r1 Ttype=multipolygon,tourism=hotel,name=XZ Mw2@ + """ + Then place contains + | object | class | type | name!dict | + | R1 | tourism | hotel | 'name' : 'XZ' | + When updating osm data + """ + r1 Ttype=multipolygon,amenity=prison,name=XZ Mw2@ + """ + Then place has no entry for R1:tourism + And place contains + | object | class | type | name!dict | + | R1 | amenity | prison | 'name' : 'XZ' | + + Scenario: Change name of a relation + When loading osm data + """ + n200 x0 y0 + n201 x0 y0.0001 + n202 x0.0001 y0.0001 + n203 x0.0001 y0 + w2 Tref=45 Nn200,n201,n202,n203,n200 + r1 Ttype=multipolygon,tourism=hotel,name=AB Mw2@ + """ + Then place contains + | object | class | type | name!dict | + | R1 | tourism | hotel | 'name' : 'AB' | + When updating osm data + """ + r1 Ttype=multipolygon,tourism=hotel,name=XY Mw2@ + """ + Then place contains + | object | class | type | name!dict | + | R1 | tourism | hotel | 'name' : 'XY' | + + Scenario: Change type of a relation into something unknown + When loading osm data + """ + n200 x0 y0 + n201 x0 y0.0001 + n202 x0.0001 y0.0001 + n203 x0.0001 y0 + w2 Tref=45 Nn200,n201,n202,n203,n200 + r1 Ttype=multipolygon,tourism=hotel,name=XY Mw2@ + """ + Then place contains + | object | class | type | name!dict | + | R1 | tourism | hotel | 'name' : 'XY' | + When updating osm data + """ + r1 Ttype=multipolygon,amenities=prison,name=XY Mw2@ + """ + Then place has no entry for R1 + + Scenario: Type tag is removed + When loading osm data + """ + n200 x0 y0 + n201 x0 y0.0001 + n202 x0.0001 y0.0001 + n203 x0.0001 y0 + w2 Tref=45 Nn200,n201,n202,n203,n200 + r1 Ttype=multipolygon,tourism=hotel,name=XY Mw2@ + """ + Then place contains + | object | class | type | name!dict | + | R1 | tourism | hotel | 'name' : 'XY' | + When updating osm data + """ + r1 Ttourism=hotel,name=XY Mw2@ + """ + Then place has no entry for R1 + + Scenario: Type tag is renamed to something unknown + When loading osm data + """ + n200 x0 y0 + n201 x0 y0.0001 + n202 x0.0001 y0.0001 + n203 x0.0001 y0 + w2 Tref=45 Nn200,n201,n202,n203,n200 + r1 Ttype=multipolygon,tourism=hotel,name=XY Mw2@ + """ + Then place contains + | object | class | type | name!dict | + | R1 | tourism | hotel | 'name' : 'XY' | + When updating osm data + """ + r1 Ttype=multipolygonn,tourism=hotel,name=XY Mw2@ + """ + Then place has no entry for R1 + + Scenario: Country boundary names are left untouched when country_code unknown + When loading osm data + """ + n200 Tamenity=prison x0 y0 + n201 x0 y0.0001 + n202 x0.0001 y0.0001 + n203 x0.0001 y0 + """ + And updating osm data + """ + w1 Nn200,n201,n202,n203,n200 + r1 Ttype=boundary,boundary=administrative,name=Foo,country_code=XX,admin_level=2 Mw1@ + """ + Then place contains + | object | address+country | name!dict | + | R1 | XX | 'name' : 'Foo' | diff --git a/test/bdd/features/osm2pgsql/update/simple.feature b/test/bdd/features/osm2pgsql/update/simple.feature new file mode 100644 index 00000000..cc26f8bd --- /dev/null +++ b/test/bdd/features/osm2pgsql/update/simple.feature @@ -0,0 +1,48 @@ +Feature: Update of simple objects by osm2pgsql + Testing basic update functions of osm2pgsql. + + Scenario: Adding a new object + When loading osm data + """ + n1 Tplace=town,name=Middletown + """ + Then place contains exactly + | object | class | type | name+name | + | N1 | place | town | Middletown | + + When updating osm data + """ + n2 Tamenity=hotel,name=Posthotel + """ + Then place contains exactly + | object | class | type | name+name | + | N1 | place | town | Middletown | + | N2 | amenity | hotel | Posthotel | + And placex contains exactly + | object | class | type | name+name | indexed_status | + | N1 | place | town | Middletown | 0 | + | N2 | amenity | hotel | Posthotel | 1 | + + + Scenario: Deleting an existing object + When loading osm data + """ + n1 Tplace=town,name=Middletown + n2 Tamenity=hotel,name=Posthotel + """ + Then place contains exactly + | object | class | type | name+name | + | N1 | place | town | Middletown | + | N2 | amenity | hotel | Posthotel | + + When updating osm data + """ + n2 dD + """ + Then place contains exactly + | object | class | type | name+name | + | N1 | place | town | Middletown | + And placex contains exactly + | object | class | type | name+name | indexed_status | + | N1 | place | town | Middletown | 0 | + | N2 | amenity | hotel | Posthotel | 100 | diff --git a/test/bdd/features/osm2pgsql/update/tags.feature b/test/bdd/features/osm2pgsql/update/tags.feature new file mode 100644 index 00000000..371a5089 --- /dev/null +++ b/test/bdd/features/osm2pgsql/update/tags.feature @@ -0,0 +1,512 @@ +Feature: Tag evaluation + Tests if tags are correctly updated in the place table + + Background: + Given the grid + | 1 | 2 | 3 | + | 10 | 11 | | + | 45 | 46 | | + + Scenario: Main tag deleted + When loading osm data + """ + n1 Tamenity=restaurant + n2 Thighway=bus_stop,railway=stop,name=X + n3 Tamenity=prison + """ + Then place contains exactly + | object | class | type | + | N1 | amenity | restaurant | + | N2 | highway | bus_stop | + | N2 | railway | stop | + | N3 | amenity | prison | + + When updating osm data + """ + n1 Tnot_a=restaurant + n2 Thighway=bus_stop,name=X + """ + Then place contains exactly + | object | class | type | + | N2 | highway | bus_stop | + | N3 | amenity | prison | + And placex contains + | object | class | indexed_status | + | N3 | amenity | 0 | + When indexing + Then placex contains exactly + | object | class | type | name!dict | + | N2 | highway | bus_stop | 'name': 'X' | + | N3 | amenity | prison | - | + + + Scenario: Main tag added + When loading osm data + """ + n1 Tatity=restaurant + n2 Thighway=bus_stop,name=X + """ + Then place contains exactly + | object | class | type | + | N2 | highway | bus_stop | + + When updating osm data + """ + n1 Tamenity=restaurant + n2 Thighway=bus_stop,railway=stop,name=X + """ + Then place contains exactly + | object | class | type | + | N1 | amenity | restaurant | + | N2 | highway | bus_stop | + | N2 | railway | stop | + When indexing + Then placex contains exactly + | object | class | type | name!dict | + | N1 | amenity | restaurant | - | + | N2 | highway | bus_stop | 'name': 'X' | + | N2 | railway | stop | 'name': 'X' | + + + Scenario: Main tag modified + When loading osm data + """ + n10 Thighway=footway,name=X + n11 Tamenity=atm + """ + Then place contains exactly + | object | class | type | + | N10 | highway | footway | + | N11 | amenity | atm | + + When updating osm data + """ + n10 Thighway=path,name=X + n11 Thighway=primary + """ + Then place contains exactly + | object | class | type | + | N10 | highway | path | + | N11 | highway | primary | + When indexing + Then placex contains exactly + | object | class | type | name!dict | + | N10 | highway | path | 'name': 'X' | + | N11 | highway | primary | - | + + + Scenario: Main tags with name, name added + When loading osm data + """ + n45 Tlanduse=cemetry + n46 Tbuilding=yes + """ + Then place contains exactly + | object | class | type | + + When updating osm data + """ + n45 Tlanduse=cemetry,name=TODO + n46 Tbuilding=yes,addr:housenumber=1 + """ + Then place contains exactly + | object | class | type | + | N45 | landuse | cemetry | + | N46 | building| yes | + When indexing + Then placex contains exactly + | object | class | type | name!dict | address!dict | + | N45 | landuse | cemetry | 'name': 'TODO' | - | + | N46 | building| yes | - | 'housenumber': '1' | + + + Scenario: Main tags with name, name removed + When loading osm data + """ + n45 Tlanduse=cemetry,name=TODO + n46 Tbuilding=yes,addr:housenumber=1 + """ + Then place contains exactly + | object | class | type | + | N45 | landuse | cemetry | + | N46 | building| yes | + + When updating osm data + """ + n45 Tlanduse=cemetry + n46 Tbuilding=yes + """ + Then place contains exactly + | object | class | type | + When indexing + Then placex contains exactly + | object | + + Scenario: Main tags with name, name modified + When loading osm data + """ + n45 Tlanduse=cemetry,name=TODO + n46 Tbuilding=yes,addr:housenumber=1 + """ + Then place contains exactly + | object | class | type | name!dict | address!dict | + | N45 | landuse | cemetry | 'name' : 'TODO' | - | + | N46 | building| yes | - | 'housenumber': '1'| + + When updating osm data + """ + n45 Tlanduse=cemetry,name=DONE + n46 Tbuilding=yes,addr:housenumber=10 + """ + Then place contains exactly + | object | class | type | name!dict | address!dict | + | N45 | landuse | cemetry | 'name' : 'DONE' | - | + | N46 | building| yes | - | 'housenumber': '10'| + When indexing + Then placex contains exactly + | object | class | type | name!dict | address!dict | + | N45 | landuse | cemetry | 'name' : 'DONE' | - | + | N46 | building| yes | - | 'housenumber': '10'| + + + Scenario: Main tag added to address only node + When loading osm data + """ + n1 Taddr:housenumber=345 + """ + Then place contains exactly + | object | class | type | address!dict | + | N1 | place | house | 'housenumber': '345'| + + When updating osm data + """ + n1 Taddr:housenumber=345,building=yes + """ + Then place contains exactly + | object | class | type | address!dict | + | N1 | building | yes | 'housenumber': '345'| + When indexing + Then placex contains exactly + | object | class | type | address!dict | + | N1 | building | yes | 'housenumber': '345'| + + + Scenario: Main tag removed from address only node + When loading osm data + """ + n1 Taddr:housenumber=345,building=yes + """ + Then place contains exactly + | object | class | type | address!dict | + | N1 | building | yes | 'housenumber': '345'| + + When updating osm data + """ + n1 Taddr:housenumber=345 + """ + Then place contains exactly + | object | class | type | address!dict | + | N1 | place | house | 'housenumber': '345'| + When indexing + Then placex contains exactly + | object | class | type | address!dict | + | N1 | place | house | 'housenumber': '345'| + + + Scenario: Main tags with name key, adding key name + When loading osm data + """ + n2 Tbridge=yes + """ + Then place contains exactly + | object | class | type | + + When updating osm data + """ + n2 Tbridge=yes,bridge:name=high + """ + Then place contains exactly + | object | class | type | name!dict | + | N2 | bridge | yes | 'name': 'high' | + When indexing + Then placex contains exactly + | object | class | type | name!dict | + | N2 | bridge | yes | 'name': 'high' | + + + Scenario: Main tags with name key, deleting key name + When loading osm data + """ + n2 Tbridge=yes,bridge:name=high + """ + Then place contains exactly + | object | class | type | name!dict | + | N2 | bridge | yes | 'name': 'high' | + + When updating osm data + """ + n2 Tbridge=yes + """ + Then place contains exactly + | object | + When indexing + Then placex contains exactly + | object | + + + Scenario: Main tags with name key, changing key name + When loading osm data + """ + n2 Tbridge=yes,bridge:name=high + """ + Then place contains exactly + | object | class | type | name!dict | + | N2 | bridge | yes | 'name': 'high' | + + When updating osm data + """ + n2 Tbridge=yes,bridge:name:en=high + """ + Then place contains exactly + | object | class | type | name!dict | + | N2 | bridge | yes | 'name:en': 'high' | + When indexing + Then placex contains exactly + | object | class | type | name!dict | + | N2 | bridge | yes | 'name:en': 'high' | + + + Scenario: Downgrading a highway to one that is dropped without name + When loading osm data + """ + n100 x0 y0 + n101 x0.0001 y0.0001 + w1 Thighway=residential Nn100,n101 + """ + Then place contains exactly + | object | class | + | W1 | highway | + + When updating osm data + """ + w1 Thighway=service Nn100,n101 + """ + Then place contains exactly + | object | + When indexing + Then placex contains exactly + | object | + + + Scenario: Upgrading a highway to one that is not dropped without name + When loading osm data + """ + n100 x0 y0 + n101 x0.0001 y0.0001 + w1 Thighway=service Nn100,n101 + """ + Then place contains exactly + | object | + + When updating osm data + """ + w1 Thighway=unclassified Nn100,n101 + """ + Then place contains exactly + | object | class | + | W1 | highway | + When indexing + Then placex contains exactly + | object | class | + | W1 | highway | + + + Scenario: Downgrading a highway when a second tag is present + When loading osm data + """ + n100 x0 y0 + n101 x0.0001 y0.0001 + w1 Thighway=residential,tourism=hotel Nn100,n101 + """ + Then place contains exactly + | object | class | type | + | W1 | highway | residential | + | W1 | tourism | hotel | + + When updating osm data + """ + w1 Thighway=service,tourism=hotel Nn100,n101 + """ + Then place contains exactly + | object | class | type | + | W1 | tourism | hotel | + When indexing + Then placex contains exactly + | object | class | type | + | W1 | tourism | hotel | + + + Scenario: Upgrading a highway when a second tag is present + When loading osm data + """ + n100 x0 y0 + n101 x0.0001 y0.0001 + w1 Thighway=service,tourism=hotel Nn100,n101 + """ + Then place contains exactly + | object | class | type | + | W1 | tourism | hotel | + + When updating osm data + """ + w1 Thighway=residential,tourism=hotel Nn100,n101 + """ + Then place contains exactly + | object | class | type | + | W1 | highway | residential | + | W1 | tourism | hotel | + When indexing + Then placex contains exactly + | object | class | type | + | W1 | highway | residential | + | W1 | tourism | hotel | + + + Scenario: Replay on administrative boundary + When loading osm data + """ + n10 x34.0 y-4.23 + n11 x34.1 y-4.23 + n12 x34.2 y-4.13 + w10 Tboundary=administrative,waterway=river,name=Border,admin_level=2 Nn12,n11,n10 + """ + Then place contains exactly + | object | class | type | admin_level | name!dict | + | W10 | waterway | river | 2 | 'name': 'Border' | + | W10 | boundary | administrative | 2 | 'name': 'Border' | + + When updating osm data + """ + w10 Tboundary=administrative,waterway=river,name=Border,admin_level=2 Nn12,n11,n10 + """ + Then place contains exactly + | object | class | type | admin_level | name!dict | + | W10 | waterway | river | 2 | 'name': 'Border' | + | W10 | boundary | administrative | 2 | 'name': 'Border' | + When indexing + Then placex contains exactly + | object | class | type | admin_level | name!dict | + | W10 | waterway | river | 2 | 'name': 'Border' | + + + Scenario: Change admin_level on administrative boundary + Given the grid + | 10 | 11 | + | 13 | 12 | + When loading osm data + """ + n10 + n11 + n12 + n13 + w10 Nn10,n11,n12,n13,n10 + r10 Ttype=multipolygon,boundary=administrative,name=Border,admin_level=2 Mw10@ + """ + Then place contains exactly + | object | class | admin_level | + | R10 | boundary | 2 | + + When updating osm data + """ + r10 Ttype=multipolygon,boundary=administrative,name=Border,admin_level=4 Mw10@ + """ + Then place contains exactly + | object | class | type | admin_level | + | R10 | boundary | administrative | 4 | + When indexing + Then placex contains exactly + | object | class | type | admin_level | + | R10 | boundary | administrative | 4 | + + + Scenario: Change boundary to administrative + Given the grid + | 10 | 11 | + | 13 | 12 | + When loading osm data + """ + n10 + n11 + n12 + n13 + w10 Nn10,n11,n12,n13,n10 + r10 Ttype=multipolygon,boundary=informal,name=Border,admin_level=4 Mw10@ + """ + Then place contains exactly + | object | class | type | admin_level | + | R10 | boundary | informal | 4 | + + When updating osm data + """ + r10 Ttype=multipolygon,boundary=administrative,name=Border,admin_level=4 Mw10@ + """ + Then place contains exactly + | object | class | type | admin_level | + | R10 | boundary | administrative | 4 | + When indexing + Then placex contains exactly + | object | class | type | admin_level | + | R10 | boundary | administrative | 4 | + + + Scenario: Change boundary away from administrative + Given the grid + | 10 | 11 | + | 13 | 12 | + When loading osm data + """ + n10 + n11 + n12 + n13 + w10 Nn10,n11,n12,n13,n10 + r10 Ttype=multipolygon,boundary=administrative,name=Border,admin_level=4 Mw10@ + """ + Then place contains exactly + | object | class | type | admin_level | + | R10 | boundary | administrative | 4 | + + When updating osm data + """ + r10 Ttype=multipolygon,boundary=informal,name=Border,admin_level=4 Mw10@ + """ + Then place contains exactly + | object | class | type | admin_level | + | R10 | boundary | informal | 4 | + When indexing + Then placex contains exactly + | object | class | type | admin_level | + | R10 | boundary | informal | 4 | + + + Scenario: Main tag and geometry is changed + When loading osm data + """ + n1 x40 y40 + n2 x40.0001 y40 + n3 x40.0001 y40.0001 + n4 x40 y40.0001 + w5 Tbuilding=house,name=Foo Nn1,n2,n3,n4,n1 + """ + Then place contains exactly + | object | class | type | + | W5 | building | house | + + When updating osm data + """ + n1 x39.999 y40 + w5 Tbuilding=terrace,name=Bar Nn1,n2,n3,n4,n1 + """ + Then place contains exactly + | object | class | type | + | W5 | building | terrace | diff --git a/test/bdd/test_osm2pgsql.py b/test/bdd/test_osm2pgsql.py new file mode 100644 index 00000000..0575ce92 --- /dev/null +++ b/test/bdd/test_osm2pgsql.py @@ -0,0 +1,152 @@ +# SPDX-License-Identifier: GPL-3.0-or-later +# +# This file is part of Nominatim. (https://nominatim.org) +# +# Copyright (C) 2025 by the Nominatim developer community. +# For a full list of authors see the git log. +""" +Collector for BDD osm2pgsql import style tests. +""" +import asyncio +import random + +import psycopg + +import pytest +from pytest_bdd.parsers import re as step_parse +from pytest_bdd import scenarios, when, given, then + +from nominatim_db import cli +from nominatim_db.config import Configuration +from nominatim_db.tools.exec_utils import run_osm2pgsql +from nominatim_db.tools.database_import import load_data, create_table_triggers +from nominatim_db.tools.replication import run_osm2pgsql_updates + +from utils.db import DBManager +from utils.checks import check_table_content, check_table_has_lines + + +@pytest.fixture +def def_config(pytestconfig): + dbname = pytestconfig.getini('nominatim_test_db') + + return Configuration(None, + environ={'NOMINATIM_DATABASE_DSN': f"pgsql:dbname={dbname}"}) + + +@pytest.fixture +def db(template_db, pytestconfig): + """ Set up an empty database for use with osm2pgsql. + """ + dbm = DBManager(purge=pytestconfig.option.NOMINATIM_PURGE) + + dbname = pytestconfig.getini('nominatim_test_db') + + dbm.create_db_from_template(dbname, template_db) + + yield dbname + + if not pytestconfig.option.NOMINATIM_KEEP_DB: + dbm.drop_db(dbname) + + +@pytest.fixture +def db_conn(def_config): + with psycopg.connect(def_config.get_libpq_dsn()) as conn: + info = psycopg.types.TypeInfo.fetch(conn, "hstore") + psycopg.types.hstore.register_hstore(info, conn) + yield conn + + +@pytest.fixture +def osm2pgsql_options(def_config): + return dict(osm2pgsql='osm2pgsql', + osm2pgsql_cache=50, + osm2pgsql_style=str(def_config.get_import_style_file()), + osm2pgsql_style_path=def_config.lib_dir.lua, + threads=1, + dsn=def_config.get_libpq_dsn(), + flatnode_file='', + tablespaces=dict(slim_data='', slim_index='', + main_data='', main_index=''), + append=False) + + +@pytest.fixture +def opl_writer(tmp_path, node_grid): + nr = [0] + + def _write(data): + fname = tmp_path / f"test_osm_{nr[0]}.opl" + nr[0] += 1 + with fname.open('wt') as fd: + for line in data.split('\n'): + if line.startswith('n') and ' x' not in line: + coord = node_grid.get(line[1:].split(' ')[0]) \ + or (random.uniform(-180, 180), random.uniform(-90, 90)) + line = f"{line} x{coord[0]:.7f} y{coord[1]:.7f}" + fd.write(line) + fd.write('\n') + return fname + + return _write + + +@given('the lua style file', target_fixture='osm2pgsql_options') +def set_lua_style_file(osm2pgsql_options, docstring, tmp_path): + style = tmp_path / 'custom.lua' + style.write_text(docstring) + osm2pgsql_options['osm2pgsql_style'] = str(style) + + return osm2pgsql_options + + +@when('loading osm data') +def load_from_osm_file(db, osm2pgsql_options, opl_writer, docstring): + """ Load the given data into a freshly created test database using osm2pgsql. + No further indexing is done. + + The data is expected as attached text in OPL format. + """ + osm2pgsql_options['import_file'] = opl_writer(docstring.replace(r'//', r'/')) + osm2pgsql_options['append'] = False + run_osm2pgsql(osm2pgsql_options) + + +@when('updating osm data') +def update_from_osm_file(db_conn, def_config, osm2pgsql_options, opl_writer, docstring): + """ Update a database previously populated with 'loading osm data'. + Needs to run indexing on the existing data first to yield the correct + result. + + The data is expected as attached text in OPL format. + """ + create_table_triggers(db_conn, def_config) + asyncio.run(load_data(def_config.get_libpq_dsn(), 1)) + cli.nominatim(['index'], def_config.environ) + cli.nominatim(['refresh', '--functions'], def_config.environ) + + osm2pgsql_options['import_file'] = opl_writer(docstring.replace(r'//', r'/')) + run_osm2pgsql_updates(db_conn, osm2pgsql_options) + + +@when('indexing') +def do_index(def_config): + """ Run Nominatim's indexing step. + """ + cli.nominatim(['index'], def_config.environ) + + +@then(step_parse(r'(?P\w+) contains(?P exactly)?')) +def check_place_content(db_conn, datatable, node_grid, table, exact): + check_table_content(db_conn, table, datatable, grid=node_grid, exact=bool(exact)) + + +@then(step_parse('(?P
placex?) has no entry for ' + r'(?P[NRW])(?P\d+)(?::(?P\S+))?'), + converters={'osm_id': int}) +def check_place_missing_lines(db_conn, table, osm_type, osm_id, osm_class): + check_table_has_lines(db_conn, table, osm_type, osm_id, osm_class) + + +scenarios('features/osm2pgsql') diff --git a/test/bdd/utils/checks.py b/test/bdd/utils/checks.py index 22c538f9..30bb245d 100644 --- a/test/bdd/utils/checks.py +++ b/test/bdd/utils/checks.py @@ -9,6 +9,12 @@ Helper functions to compare expected values. """ import json import re +import math +import itertools + +from psycopg import sql as pysql +from psycopg.rows import dict_row, tuple_row +from .geometry_alias import ALIASES COMPARATOR_TERMS = { 'exactly': lambda exp, act: exp == act, @@ -43,10 +49,12 @@ COMPARISON_FUNCS = { None: lambda val, exp: str(val) == exp, 'i': lambda val, exp: str(val).lower() == exp.lower(), 'fm': lambda val, exp: re.fullmatch(exp, val) is not None, + 'dict': lambda val, exp: val is None if exp == '-' else (val == eval('{' + exp + '}')), 'in_box': within_box } -OSM_TYPE = {'node': 'n', 'way': 'w', 'relation': 'r'} +OSM_TYPE = {'node': 'n', 'way': 'w', 'relation': 'r', + 'N': 'n', 'W': 'w', 'R': 'r'} class ResultAttr: @@ -60,12 +68,15 @@ class ResultAttr: Available formatters: - !:... - use a formatting expression according to Python Mini Format Spec - !i - make case-insensitive comparison - !fm - consider comparison string a regular expression and match full value + !:... - use a formatting expression according to Python Mini Format Spec + !i - make case-insensitive comparison + !fm - consider comparison string a regular expression and match full value + !wkt - convert the expected value to a WKT string before comparing + !in_box - the expected value is a comma-separated bbox description """ - def __init__(self, obj, key): + def __init__(self, obj, key, grid=None): + self.grid = grid self.obj = obj if '!' in key: self.key, self.fmt = key.rsplit('!', 1) @@ -100,6 +111,9 @@ class ResultAttr: if self.fmt.startswith(':'): return other == f"{{{self.fmt}}}".format(self.subobj) + if self.fmt == 'wkt': + return self.compare_wkt(self.subobj, other) + raise RuntimeError(f"Unknown format string '{self.fmt}'.") def __repr__(self): @@ -107,3 +121,125 @@ class ResultAttr: if self.fmt: k += '!' + self.fmt return f"result[{k}]({self.subobj})" + + def compare_wkt(self, value, expected): + """ Compare a WKT value against a compact geometry format. + The function understands the following formats: + + country: + Point geometry guaranteed to be in the given country +

+ Point geometry +

,...,

+ Line geometry + (

,...,

) + Polygon geometry + +

may either be a coordinate of the form ' ' or a single + number. In the latter case it must refer to a point in + a previously defined grid. + """ + m = re.fullmatch(r'(POINT)\(([0-9. -]*)\)', value) \ + or re.fullmatch(r'(LINESTRING)\(([0-9,. -]*)\)', value) \ + or re.fullmatch(r'(POLYGON)\(\(([0-9,. -]*)\)\)', value) + if not m: + return False + + converted = [list(map(float, pt.split(' ', 1))) + for pt in map(str.strip, m[2].split(','))] + + if expected.startswith('country:'): + ccode = geom[8:].upper() + assert ccode in ALIASES, f"Geometry error: unknown country {ccode}" + return m[1] == 'POINT' and \ + all(math.isclose(p1, p2) for p1, p2 in + zip(converted[0], ALIASES[ccode])) + + if ',' not in expected: + return m[1] == 'POINT' and \ + all(math.isclose(p1, p2) for p1, p2 in + zip(converted[0], self.get_point(expected))) + + if '(' not in expected: + return m[1] == 'LINESTRING' and \ + all(math.isclose(p1[0], p2[0]) and math.isclose(p1[1], p2[1]) for p1, p2 in + zip(converted, (self.get_point(p) for p in expected.split(',')))) + + if m[1] != 'POLYGON': + return False + + # Polygon comparison is tricky because the polygons don't necessarily + # end at the same point or have the same winding order. + # Brute force all possible variants of the expected polygon + exp_coords = [self.get_point(p) for p in expected[1:-1].split(',')] + if exp_coords[0] != exp_coords[-1]: + raise RuntimeError(f"Invalid polygon {expected}. " + "First and last point need to be the same") + for line in (exp_coords[:-1], exp_coords[-1:0:-1]): + for i in range(len(line)): + if all(math.isclose(p1[0], p2[0]) and math.isclose(p1[1], p2[1]) for p1, p2 in + zip(converted, line[i:] + line[:i])): + return True + + return False + + def get_point(self, pt): + pt = pt.strip() + if ' ' in pt: + return list(map(float, pt.split(' ', 1))) + + assert self.grid + + return self.grid.get(pt) + + +def check_table_content(conn, tablename, data, grid=None, exact=False): + lines = set(range(1, len(data))) + + cols = [] + for col in data[0]: + if col == 'object': + cols.extend(('osm_id', 'osm_type')) + elif '!' in col: + name, fmt = col.rsplit('!', 1) + if fmt == 'wkt': + cols.append(f"ST_AsText({name}) as {name}") + else: + cols.append(name.split('+')[0]) + else: + cols.append(col.split('+')[0]) + + with conn.cursor(row_factory=dict_row) as cur: + cur.execute(pysql.SQL(f"SELECT {','.join(cols)} FROM") + + pysql.Identifier(tablename)) + + table_content = '' + for row in cur: + table_content += '\n' + str(row) + for i in lines: + for col, value in zip(data[0], data[i]): + if ResultAttr(row, col, grid=grid) != value: + break + else: + lines.remove(i) + break + else: + assert not exact, f"Unexpected row in table {tablename}: {row}" + + assert not lines, \ + "Rows not found:\n" \ + + '\n'.join(str(data[i]) for i in lines) \ + + "\nTable content:\n" \ + + table_content + + +def check_table_has_lines(conn, tablename, osm_type, osm_id, osm_class): + sql = pysql.SQL("""SELECT count(*) FROM {} + WHERE osm_type = %s and osm_id = %s""").format(pysql.Identifier(tablename)) + params = [osm_type, int(osm_id)] + if osm_class: + sql += pysql.SQL(' AND class = %s') + params.append(osm_class) + + with conn.cursor(row_factory=tuple_row) as cur: + assert cur.execute(sql, params).fetchone()[0] == 0 diff --git a/test/bdd/utils/db.py b/test/bdd/utils/db.py index 661112ee..fd13dfb2 100644 --- a/test/bdd/utils/db.py +++ b/test/bdd/utils/db.py @@ -7,9 +7,16 @@ """ Helper functions for managing test databases. """ +import asyncio import psycopg from psycopg import sql as pysql +from nominatim_db.tools.database_import import setup_database_skeleton, create_tables, \ + create_partition_tables, create_search_indices +from nominatim_db.data.country_info import setup_country_tables +from nominatim_db.tools.refresh import create_functions, load_address_levels_from_config +from nominatim_db.tools.exec_utils import run_osm2pgsql +from nominatim_db.tokenizer import factory as tokenizer_factory class DBManager: @@ -42,3 +49,53 @@ class DBManager: cur = conn.execute('select count(*) from pg_database where datname = %s', (dbname,)) return cur.fetchone()[0] == 1 + + def create_db_from_template(self, dbname, template): + """ Create a new database from the given template database. + Any existing database with the same name will be dropped. + """ + with psycopg.connect(dbname='postgres') as conn: + conn.autocommit = True + conn.execute(pysql.SQL('DROP DATABASE IF EXISTS') + + pysql.Identifier(dbname)) + conn.execute(pysql.SQL('CREATE DATABASE {} WITH TEMPLATE {}') + .format(pysql.Identifier(dbname), + pysql.Identifier(template))) + + def setup_template_db(self, config): + """ Create a template DB which contains the necessary extensions + and basic static tables. + + The template will only be created if the database does not yet + exist or 'purge' is set. + """ + dsn = config.get_libpq_dsn() + + if self.check_for_db(config.get_database_params()['dbname']): + return + + setup_database_skeleton(dsn) + + run_osm2pgsql(dict(osm2pgsql='osm2pgsql', + osm2pgsql_cache=1, + osm2pgsql_style=str(config.get_import_style_file()), + osm2pgsql_style_path=config.lib_dir.lua, + threads=1, + dsn=dsn, + flatnode_file='', + tablespaces=dict(slim_data='', slim_index='', + main_data='', main_index=''), + append=False, + import_data=b'')) + + setup_country_tables(dsn, config.lib_dir.data) + + with psycopg.connect(dsn) as conn: + create_tables(conn, config) + load_address_levels_from_config(conn, config) + create_partition_tables(conn, config) + create_functions(conn, config, enable_diff_updates=False) + asyncio.run(create_search_indices(conn, config)) + + tokenizer_factory.create_tokenizer(config) + diff --git a/test/bdd/utils/geometry_alias.py b/test/bdd/utils/geometry_alias.py new file mode 100644 index 00000000..dbec5201 --- /dev/null +++ b/test/bdd/utils/geometry_alias.py @@ -0,0 +1,262 @@ +# SPDX-License-Identifier: GPL-2.0-only +# +# This file is part of Nominatim. (https://nominatim.org) +# +# Copyright (C) 2025 by the Nominatim developer community. +# For a full list of authors see the git log. +""" +Collection of aliases for various world coordinates. +""" + +ALIASES = { + # Country aliases + 'AD': (1.58972, 42.54241), + 'AE': (54.61589, 24.82431), + 'AF': (65.90264, 34.84708), + 'AG': (-61.72430, 17.069), + 'AI': (-63.10571, 18.25461), + 'AL': (19.84941, 40.21232), + 'AM': (44.64229, 40.37821), + 'AO': (16.21924, -12.77014), + 'AQ': (44.99999, -75.65695), + 'AR': (-61.10759, -34.37615), + 'AS': (-170.68470, -14.29307), + 'AT': (14.25747, 47.36542), + 'AU': (138.23155, -23.72068), + 'AW': (-69.98255, 12.555), + 'AX': (19.91839, 59.81682), + 'AZ': (48.38555, 40.61639), + 'BA': (17.18514, 44.25582), + 'BB': (-59.53342, 13.19), + 'BD': (89.75989, 24.34205), + 'BE': (4.90078, 50.34682), + 'BF': (-0.56743, 11.90471), + 'BG': (24.80616, 43.09859), + 'BH': (50.52032, 25.94685), + 'BI': (29.54561, -2.99057), + 'BJ': (2.70062, 10.02792), + 'BL': (-62.79349, 17.907), + 'BM': (-64.77406, 32.30199), + 'BN': (114.52196, 4.28638), + 'BO': (-62.02473, -17.77723), + 'BQ': (-63.14322, 17.566), + 'BR': (-45.77065, -9.58685), + 'BS': (-77.60916, 23.8745), + 'BT': (90.01350, 27.28137), + 'BV': (3.35744, -54.4215), + 'BW': (23.51505, -23.48391), + 'BY': (26.77259, 53.15885), + 'BZ': (-88.63489, 16.33951), + 'CA': (-107.74817, 67.12612), + 'CC': (96.84420, -12.01734), + 'CD': (24.09544, -1.67713), + 'CF': (22.58701, 5.98438), + 'CG': (15.78875, 0.40388), + 'CH': (7.65705, 46.57446), + 'CI': (-6.31190, 6.62783), + 'CK': (-159.77835, -21.23349), + 'CL': (-70.41790, -53.77189), + 'CM': (13.26022, 5.94519), + 'CN': (96.44285, 38.04260), + 'CO': (-72.52951, 2.45174), + 'CR': (-83.83314, 9.93514), + 'CU': (-80.81673, 21.88852), + 'CV': (-24.50810, 14.929), + 'CW': (-68.96409, 12.1845), + 'CX': (105.62411, -10.48417), + 'CY': (32.95922, 35.37010), + 'CZ': (16.32098, 49.50692), + 'DE': (9.30716, 50.21289), + 'DJ': (42.96904, 11.41542), + 'DK': (9.18490, 55.98916), + 'DM': (-61.00358, 15.65470), + 'DO': (-69.62855, 18.58841), + 'DZ': (4.24749, 25.79721), + 'EC': (-77.45831, -0.98284), + 'EE': (23.94288, 58.43952), + 'EG': (28.95293, 28.17718), + 'EH': (-13.69031, 25.01241), + 'ER': (39.01223, 14.96033), + 'ES': (-2.59110, 38.79354), + 'ET': (38.61697, 7.71399), + 'FI': (26.89798, 63.56194), + 'FJ': (177.91853, -17.74237), + 'FK': (-58.99044, -51.34509), + 'FM': (151.95358, 8.5045), + 'FO': (-6.60483, 62.10000), + 'FR': (0.28410, 47.51045), + 'GA': (10.81070, -0.07429), + 'GB': (-0.92823, 52.01618), + 'GD': (-61.64524, 12.191), + 'GE': (44.16664, 42.00385), + 'GF': (-53.46524, 3.56188), + 'GG': (-2.50580, 49.58543), + 'GH': (-0.46348, 7.16051), + 'GI': (-5.32053, 36.11066), + 'GL': (-33.85511, 74.66355), + 'GM': (-16.40960, 13.25), + 'GN': (-13.83940, 10.96291), + 'GP': (-61.68712, 16.23049), + 'GQ': (10.23973, 1.43119), + 'GR': (23.17850, 39.06206), + 'GS': (-36.49430, -54.43067), + 'GT': (-90.74368, 15.20428), + 'GU': (144.73362, 13.44413), + 'GW': (-14.83525, 11.92486), + 'GY': (-58.45167, 5.73698), + 'HK': (114.18577, 22.34923), + 'HM': (73.68230, -53.22105), + 'HN': (-86.95414, 15.23820), + 'HR': (17.49966, 45.52689), + 'HT': (-73.51925, 18.32492), + 'HU': (20.35362, 47.51721), + 'ID': (123.34505, -0.83791), + 'IE': (-9.00520, 52.87725), + 'IL': (35.46314, 32.86165), + 'IM': (-4.86740, 54.023), + 'IN': (88.67620, 27.86155), + 'IO': (71.42743, -6.14349), + 'IQ': (42.58109, 34.26103), + 'IR': (56.09355, 30.46751), + 'IS': (-17.51785, 64.71687), + 'IT': (10.42639, 44.87904), + 'JE': (-2.19261, 49.12458), + 'JM': (-76.84020, 18.3935), + 'JO': (36.55552, 30.75741), + 'JP': (138.72531, 35.92099), + 'KE': (36.90602, 1.08512), + 'KG': (76.15571, 41.66497), + 'KH': (104.31901, 12.95555), + 'KI': (173.63353, 0.139), + 'KM': (44.31474, -12.241), + 'KN': (-62.69379, 17.2555), + 'KP': (126.65575, 39.64575), + 'KR': (127.27740, 36.41388), + 'KW': (47.30684, 29.69180), + 'KY': (-81.07455, 19.29949), + 'KZ': (72.00811, 49.88855), + 'LA': (102.44391, 19.81609), + 'LB': (35.48464, 33.41766), + 'LC': (-60.97894, 13.891), + 'LI': (9.54693, 47.15934), + 'LK': (80.38520, 8.41649), + 'LR': (-11.16960, 4.04122), + 'LS': (28.66984, -29.94538), + 'LT': (24.51735, 55.49293), + 'LU': (6.08649, 49.81533), + 'LV': (23.51033, 56.67144), + 'LY': (15.36841, 28.12177), + 'MA': (-4.03061, 33.21696), + 'MC': (7.47743, 43.62917), + 'MD': (29.61725, 46.66517), + 'ME': (19.72291, 43.02441), + 'MF': (-63.06666, 18.08102), + 'MG': (45.86378, -20.50245), + 'MH': (171.94982, 5.983), + 'MK': (21.42108, 41.08980), + 'ML': (-1.93310, 16.46993), + 'MM': (95.54624, 21.09620), + 'MN': (99.81138, 48.18615), + 'MO': (113.56441, 22.16209), + 'MP': (145.21345, 14.14902), + 'MQ': (-60.81128, 14.43706), + 'MR': (-9.42324, 22.59251), + 'MS': (-62.19455, 16.745), + 'MT': (14.38363, 35.94467), + 'MU': (57.55121, -20.41), + 'MV': (73.39292, 4.19375), + 'MW': (33.95722, -12.28218), + 'MX': (-105.89221, 25.86826), + 'MY': (112.71154, 2.10098), + 'MZ': (37.58689, -13.72682), + 'NA': (16.68569, -21.46572), + 'NC': (164.95322, -20.38889), + 'NE': (10.06041, 19.08273), + 'NF': (167.95718, -29.0645), + 'NG': (10.17781, 10.17804), + 'NI': (-85.87974, 13.21715), + 'NL': (-68.57062, 12.041), + 'NO': (23.11556, 70.09934), + 'NP': (83.36259, 28.13107), + 'NR': (166.93479, -0.5275), + 'NU': (-169.84873, -19.05305), + 'NZ': (167.97209, -45.13056), + 'OM': (56.86055, 20.47413), + 'PA': (-79.40160, 8.80656), + 'PE': (-78.66540, -7.54711), + 'PF': (-145.05719, -16.70862), + 'PG': (146.64600, -7.37427), + 'PH': (121.48359, 15.09965), + 'PK': (72.11347, 31.14629), + 'PL': (17.88136, 52.77182), + 'PM': (-56.19515, 46.78324), + 'PN': (-130.10642, -25.06955), + 'PR': (-65.88755, 18.37169), + 'PS': (35.39801, 32.24773), + 'PT': (-8.45743, 40.11154), + 'PW': (134.49645, 7.3245), + 'PY': (-59.51787, -22.41281), + 'QA': (51.49903, 24.99816), + 'RE': (55.77345, -21.36388), + 'RO': (26.37632, 45.36120), + 'RS': (20.40371, 44.56413), + 'RU': (116.44060, 59.06780), + 'RW': (29.57882, -1.62404), + 'SA': (47.73169, 22.43790), + 'SB': (164.63894, -10.23606), + 'SC': (46.36566, -9.454), + 'SD': (28.14720, 14.56423), + 'SE': (15.68667, 60.35568), + 'SG': (103.84187, 1.304), + 'SH': (-12.28155, -37.11546), + 'SI': (14.04738, 46.39085), + 'SJ': (15.27552, 79.23365), + 'SK': (20.41603, 48.86970), + 'SL': (-11.47773, 8.78156), + 'SM': (12.46062, 43.94279), + 'SN': (-15.37111, 14.99477), + 'SO': (46.93383, 9.34094), + 'SR': (-55.42864, 4.56985), + 'SS': (28.13573, 8.50933), + 'ST': (6.61025, 0.2215), + 'SV': (-89.36665, 13.43072), + 'SX': (-63.15393, 17.9345), + 'SY': (38.15513, 35.34221), + 'SZ': (31.78263, -26.14244), + 'TC': (-71.32554, 21.35), + 'TD': (17.42092, 13.46223), + 'TF': (137.5, -67.5), + 'TG': (1.06983, 7.87677), + 'TH': (102.00877, 16.42310), + 'TJ': (71.91349, 39.01527), + 'TK': (-171.82603, -9.20990), + 'TL': (126.22520, -8.72636), + 'TM': (57.71603, 39.92534), + 'TN': (9.04958, 34.84199), + 'TO': (-176.99320, -23.11104), + 'TR': (32.82002, 39.86350), + 'TT': (-60.70793, 11.1385), + 'TV': (178.77499, -9.41685), + 'TW': (120.30074, 23.17002), + 'TZ': (33.53892, -5.01840), + 'UA': (33.44335, 49.30619), + 'UG': (32.96523, 2.08584), + 'UM': (-169.50993, 16.74605), + 'US': (-116.39535, 40.71379), + 'UY': (-56.46505, -33.62658), + 'UZ': (61.35529, 42.96107), + 'VA': (12.33197, 42.04931), + 'VC': (-61.09905, 13.316), + 'VE': (-64.88323, 7.69849), + 'VG': (-64.62479, 18.419), + 'VI': (-64.88950, 18.32263), + 'VN': (104.20179, 10.27644), + 'VU': (167.31919, -15.88687), + 'WF': (-176.20781, -13.28535), + 'WS': (-172.10966, -13.85093), + 'YE': (45.94562, 16.16338), + 'YT': (44.93774, -12.60882), + 'ZA': (23.19488, -30.43276), + 'ZM': (26.38618, -14.39966), + 'ZW': (30.12419, -19.86907) + } diff --git a/test/bdd/utils/grid.py b/test/bdd/utils/grid.py new file mode 100644 index 00000000..bc1db386 --- /dev/null +++ b/test/bdd/utils/grid.py @@ -0,0 +1,34 @@ +# SPDX-License-Identifier: GPL-3.0-or-later +# +# This file is part of Nominatim. (https://nominatim.org) +# +# Copyright (C) 2025 by the Nominatim developer community. +# For a full list of authors see the git log. +""" +A grid describing node placement in an area. +Useful for visually describing geometries. +""" + + +class Grid: + + def __init__(self, table, step, origin): + if step is None: + step = 0.00001 + if origin is None: + origin = (0.0, 0.0) + self.grid = {} + + y = origin[1] + for line in table: + x = origin[0] + for pt_id in line: + if pt_id: + self.grid[pt_id] = (x, y) + x += step + y += step + + def get(self, nodeid): + """ Get the coordinates for the given grid node. + """ + return self.grid.get(nodeid) -- 2.39.5