From 42e92db2b06a5685cbde9a7ce7b2f8d66ea6d5e2 Mon Sep 17 00:00:00 2001 From: Serghei Mihai Date: Wed, 31 Jan 2018 17:35:02 +0100 Subject: [PATCH] opengis: add reverse geocoding based on WFS (#21558) --- .../opengis/migrations/0005_auto_20180227_1531.py | 20 ++++ passerelle/apps/opengis/models.py | 66 ++++++++++++ tests/test_opengis.py | 117 +++++++++++++++++++++ 3 files changed, 203 insertions(+) create mode 100644 passerelle/apps/opengis/migrations/0005_auto_20180227_1531.py diff --git a/passerelle/apps/opengis/migrations/0005_auto_20180227_1531.py b/passerelle/apps/opengis/migrations/0005_auto_20180227_1531.py new file mode 100644 index 00000000..e866d62e --- /dev/null +++ b/passerelle/apps/opengis/migrations/0005_auto_20180227_1531.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models +import jsonfield.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('opengis', '0004_auto_20180219_1613'), + ] + + operations = [ + migrations.AddField( + model_name='opengis', + name='search_radius', + field=models.IntegerField(default=5, verbose_name='Radius for point search'), + ), + ] diff --git a/passerelle/apps/opengis/models.py b/passerelle/apps/opengis/models.py index dead4ca6..078578a5 100644 --- a/passerelle/apps/opengis/models.py +++ b/passerelle/apps/opengis/models.py @@ -58,6 +58,13 @@ class OpenGIS(BaseResource): query_layer = models.CharField(_('Query Layer'), max_length=256) projection = models.CharField(_('GIS projection'), choices=PROJECTIONS, default='EPSG:3857', max_length=16) + search_radius = models.IntegerField(_('Radius for point search'), default=5) + attributes_mapping = (('road', ('road', 'road_name', 'street', 'street_name', 'voie', 'nom_voie', 'rue')), + ('city', ('city', 'city_name', 'town', 'town_name', 'commune', 'nom_commune', 'ville', 'nom_ville')), + ('house_number', ('house_number', 'number', 'numero', 'numero_voie', 'numero_rue')), + ('postcode', ('postcode', 'postalCode', 'zipcode', 'codepostal', 'cp', 'code_postal', 'code_post')), + ('country', ('country', 'country_name', 'pays', 'nom_pays')) + ) class Meta: verbose_name = _('OpenGIS') @@ -152,6 +159,17 @@ class OpenGIS(BaseResource): raise APIError(u'OpenGIS Error: %s' % exception_code or 'unknown code', data={'text': content}) + def convert_coordinates(self, lon, lat, reverse=False): + lon, lat = float(lon), float(lat) + if self.projection != 'EPSG:4326': + wgs84 = pyproj.Proj(init='EPSG:4326') + target_projection = pyproj.Proj(init=self.projection) + if reverse: + lon, lat = pyproj.transform(target_projection, wgs84, lon, lat) + else: + lon, lat = pyproj.transform(wgs84, target_projection, lon, lat) + return lon, lat + @endpoint(perm='can_access', description=_('Get feature info'), parameters={ @@ -237,3 +255,51 @@ class OpenGIS(BaseResource): return HttpResponse( self.requests.get(self.wms_service_url, params=params, cache_duration=300).content, content_type='image/png') + + @endpoint(perm='can_access', description=_('Get feature info')) + def reverse(self, request, lat, lon, **kwargs): + lon, lat = self.convert_coordinates(lon, lat) + + cql_filter = 'DWITHIN(the_geom,Point(%s %s),%s,meters)' % (lon, lat, self.search_radius) + params = { + 'VERSION': self.get_wfs_service_version(), + 'SERVICE': 'WFS', + 'REQUEST': 'GetFeature', + 'TYPENAMES': self.query_layer, + 'OUTPUTFORMAT': 'json', + 'CQL_FILTER': cql_filter + } + response = self.requests.get(self.wfs_service_url, params=params) + if not response.ok: + raise APIError('Webservice returned status code %s' % response.status_code) + closest_feature = {} + min_delta = None + for feature in response.json().get('features'): + if not feature['geometry']['type'] == 'Point': + continue # skip unknown + lon_diff = abs(float(lon) - float(feature['geometry']['coordinates'][0])) + lat_diff = abs(float(lat) - float(feature['geometry']['coordinates'][1])) + # compute hypotenuse til the point + delta = math.sqrt(lon_diff * lon_diff + lat_diff * lat_diff) + if min_delta is None: + min_delta = delta + # choose the shortest + if delta <= min_delta: + closest_feature = feature + + if closest_feature: + result = {} + point_lon = closest_feature['geometry']['coordinates'][0] + point_lat = closest_feature['geometry']['coordinates'][1] + point_lon, point_lat = self.convert_coordinates(point_lon, point_lat, reverse=True) + result['lon'] = str(point_lon) + result['lat'] = str(point_lat) + result['address'] = {} + + for attribute, properties in self.attributes_mapping: + for field in properties: + if closest_feature['properties'].get(field): + result['address'][attribute] = unicode(closest_feature['properties'][field]) + break + return result + raise APIError('Unable to geocode') diff --git a/tests/test_opengis.py b/tests/test_opengis.py index 263550d0..13fb80d7 100644 --- a/tests/test_opengis.py +++ b/tests/test_opengis.py @@ -101,6 +101,92 @@ FAKE_FEATURES_JSON = ''' FAKE_ERROR = u'\n \n Could not parse CQL filter list.\nEncountered &quot;BIS&quot; at line 1, column 129.\nWas expecting one of:\n &lt;EOF&gt; \n &quot;and&quot; ...\n &quot;or&quot; ...\n &quot;;&quot; ...\n &quot;/&quot; ...\n &quot;*&quot; ...\n &quot;+&quot; ...\n &quot;-&quot; ...\n Parsing : strEqualsIgnoreCase(nom_commune, &apos;Grenoble&apos;) = true AND strEqualsIgnoreCase(nom_voie, &apos;rue albert recoura&apos;) = true AND numero=8 BIS.\n \n\n' +FAKE_GEOLOCATED_FEATURE = '''{ + "crs": { + "properties": { + "name": "urn:ogc:def:crs:EPSG::3945" + }, + "type": "name" + }, + "features": [ + { + "geometry": { + "coordinates": [ + 1914059.51, + 4224699.2 + ], + "type": "Point" + }, + "geometry_name": "the_geom", + "properties": { + "code_insee": 38185, + "code_post": 38000, + "nom_afnor": "BOULEVARD EDOUARD REY", + "nom_commune": "Grenoble", + "nom_voie": "boulevard \u00e9douard rey", + "numero": 17 + }, + "type": "Feature" + }, + { + "geometry": { + "coordinates": [ + 1914042.47, + 4224665.2 + ], + "type": "Point" + }, + "geometry_name": "the_geom", + "properties": { + "code_insee": 38185, + "code_post": 38000, + "nom_commune": "Grenoble", + "nom_voie": "place victor hugo", + "numero": 2 + }, + "type": "Feature" + }, + { + "geometry": { + "coordinates": [ + 1914035.7, + 4224700.42 + ], + "type": "Point" + }, + "geometry_name": "the_geom", + "properties": { + "code_insee": 38185, + "code_post": 38000, + "nom_commune": "Grenoble", + "nom_voie": "boulevard \u00e9douard rey", + "numero": 28 + }, + "type": "Feature" + }, + { + "geometry": { + "coordinates": [ + 1914018.64, + 4224644.61 + ], + "type": "Point" + }, + "geometry_name": "the_geom", + "properties": { + "code_insee": 38185, + "code_post": 38000, + "nom_commune": "Grenoble", + "nom_voie": "place victor hugo", + "numero": 4 + }, + "type": "Feature" + } + ], + "totalFeatures": 4, + "type": "FeatureCollection" +}''' + @pytest.fixture def connector(db): @@ -250,3 +336,34 @@ def test_get_feature_error(mocked_get, app, connector): assert result['err'] == 1 assert result['err_desc'] == 'OpenGIS Error: unparsable error' assert '