Projet

Général

Profil

0001-opengis-add-reverse-geocoding-based-on-WFS-21558.patch

Voir les différences:

Subject: [PATCH] opengis: add reverse geocoding based on WFS (#21558)

 .../opengis/migrations/0005_auto_20180227_1531.py  |  20 ++++
 passerelle/apps/opengis/models.py                  |  64 ++++++++++++
 tests/test_opengis.py                              | 109 +++++++++++++++++++++
 3 files changed, 193 insertions(+)
 create mode 100644 passerelle/apps/opengis/migrations/0005_auto_20180227_1531.py
passerelle/apps/opengis/migrations/0005_auto_20180227_1531.py
1
# -*- coding: utf-8 -*-
2
from __future__ import unicode_literals
3

  
4
from django.db import migrations, models
5
import jsonfield.fields
6

  
7

  
8
class Migration(migrations.Migration):
9

  
10
    dependencies = [
11
        ('opengis', '0004_auto_20180219_1613'),
12
    ]
13

  
14
    operations = [
15
        migrations.AddField(
16
            model_name='opengis',
17
            name='search_radius',
18
            field=models.IntegerField(default=5, verbose_name='Radius for point search'),
19
        ),
20
    ]
passerelle/apps/opengis/models.py
58 58
    query_layer = models.CharField(_('Query Layer'), max_length=256)
59 59
    projection = models.CharField(_('GIS projection'), choices=PROJECTIONS,
60 60
                                  default='EPSG:3857', max_length=16)
61
    search_radius = models.IntegerField(_('Radius for point search'), default=5)
62
    attributes_mapping = (('road', ('road', 'road_name', 'street', 'street_name', 'voie', 'nom_voie', 'rue')),
63
                          ('city', ('city', 'city_name', 'town', 'town_name', 'commune', 'nom_commune', 'ville', 'nom_ville')),
64
                          ('house_number', ('house_number', 'number', 'numero', 'numero_voie', 'numero_rue')),
65
                          ('postcode', ('postcode', 'postalCode', 'zipcode', 'codepostal', 'cp', 'code_postal', 'code_post')),
66
                          ('country', ('country', 'country_name', 'pays', 'nom_pays'))
67
                          )
61 68

  
62 69
    class Meta:
63 70
        verbose_name = _('OpenGIS')
......
152 159
        raise APIError(u'OpenGIS Error: %s' % exception_code or 'unknown code',
153 160
                       data={'text': content})
154 161

  
162
    def convert_coordinates(self, lon, lat, reverse=False):
163
        lon, lat = float(lon), float(lat)
164
        if self.projection != 'EPSG:4326':
165
            wgs84 = pyproj.Proj(init='EPSG:4326')
166
            target_projection = pyproj.Proj(init=self.projection)
167
            if reverse:
168
                lon, lat = pyproj.transform(target_projection, wgs84, lon, lat)
169
            else:
170
                lon, lat = pyproj.transform(wgs84, target_projection, lon, lat)
171
        return lon, lat
172

  
155 173
    @endpoint(perm='can_access',
156 174
              description=_('Get feature info'),
157 175
              parameters={
......
237 255
        return HttpResponse(
238 256
                self.requests.get(self.wms_service_url, params=params, cache_duration=300).content,
239 257
                content_type='image/png')
258

  
259
    @endpoint(perm='can_access', description=_('Get feature info'))
260
    def reverse(self, request, lat, lon, **kwargs):
261
        result = None
262

  
263
        lon, lat = self.convert_coordinates(lon, lat)
264

  
265
        cql_filter = 'DWITHIN(the_geom,Point(%s %s),%s,meters)' % (lon, lat, self.search_radius)
266
        params = {
267
            'VERSION': self.get_wfs_service_version(),
268
            'SERVICE': 'WFS',
269
            'REQUEST': 'GetFeature',
270
            'TYPENAMES': self.query_layer,
271
            'OUTPUTFORMAT': 'json',
272
            'CQL_FILTER': cql_filter
273
        }
274
        response = self.requests.get(self.wfs_service_url, params=params)
275
        closest_feature = {}
276
        min_delta = None
277
        for feature in response.json().get('features'):
278
            if not feature['geometry']['type'] == 'Point':
279
                continue  # skip unknown
280
            lon_diff = abs(float(lon) - float(feature['geometry']['coordinates'][0]))
281
            lat_diff = abs(float(lat) - float(feature['geometry']['coordinates'][1]))
282
            delta = math.sqrt(lon_diff * lon_diff + lat_diff * lat_diff)
283
            if min_delta is None:
284
                min_delta = delta
285
            if delta <= min_delta:
286
                closest_feature = feature
287

  
288
        if closest_feature:
289
            result = {}
290
            point_lon = closest_feature['geometry']['coordinates'][0]
291
            point_lat = closest_feature['geometry']['coordinates'][1]
292
            point_lon, point_lat = self.convert_coordinates(point_lon, point_lat, reverse=True)
293
            result['lon'] = str(point_lon)
294
            result['lat'] = str(point_lat)
295
            result['address'] = {}
296

  
297
            for attribute, properties in self.attributes_mapping:
298
                for field in properties:
299
                    if closest_feature['properties'].get(field):
300
                        result['address'][attribute] = unicode(closest_feature['properties'][field])
301
                        break
302

  
303
        return result
tests/test_opengis.py
101 101

  
102 102
FAKE_ERROR = u'<ows:ExceptionReport xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:ows="http://www.opengis.net/ows/1.1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.0.0" xsi:schemaLocation="http://www.opengis.net/ows/1.1 https://sigmetropole.lametro.fr/geoserver/schemas/ows/1.1.0/owsAll.xsd">\n  <ows:Exception exceptionCode="NoApplicableCode">\n    <ows:ExceptionText>Could not parse CQL filter list.\nEncountered &amp;quot;BIS&amp;quot; at line 1, column 129.\nWas expecting one of:\n    &amp;lt;EOF&amp;gt; \n    &amp;quot;and&amp;quot; ...\n    &amp;quot;or&amp;quot; ...\n    &amp;quot;;&amp;quot; ...\n    &amp;quot;/&amp;quot; ...\n    &amp;quot;*&amp;quot; ...\n    &amp;quot;+&amp;quot; ...\n    &amp;quot;-&amp;quot; ...\n     Parsing : strEqualsIgnoreCase(nom_commune, &amp;apos;Grenoble&amp;apos;) = true AND strEqualsIgnoreCase(nom_voie, &amp;apos;rue albert recoura&amp;apos;) = true AND numero=8 BIS.</ows:ExceptionText>\n  </ows:Exception>\n</ows:ExceptionReport>\n'
103 103

  
104
FAKE_GEOLOCATED_FEATURE = '''{
105
    "crs": {
106
        "properties": {
107
            "name": "urn:ogc:def:crs:EPSG::3945"
108
        },
109
        "type": "name"
110
    },
111
    "features": [
112
        {
113
            "geometry": {
114
                "coordinates": [
115
                    1914059.51,
116
                    4224699.2
117
                ],
118
                "type": "Point"
119
            },
120
            "geometry_name": "the_geom",
121
            "properties": {
122
                "code_insee": 38185,
123
                "code_post": 38000,
124
                "nom_afnor": "BOULEVARD EDOUARD REY",
125
                "nom_commune": "Grenoble",
126
                "nom_voie": "boulevard \u00e9douard rey",
127
                "numero": 17
128
            },
129
            "type": "Feature"
130
        },
131
        {
132
            "geometry": {
133
                "coordinates": [
134
                    1914042.47,
135
                    4224665.2
136
                ],
137
                "type": "Point"
138
            },
139
            "geometry_name": "the_geom",
140
            "properties": {
141
                "code_insee": 38185,
142
                "code_post": 38000,
143
                "nom_commune": "Grenoble",
144
                "nom_voie": "place victor hugo",
145
                "numero": 2
146
            },
147
            "type": "Feature"
148
        },
149
        {
150
            "geometry": {
151
                "coordinates": [
152
                    1914035.7,
153
                    4224700.42
154
                ],
155
                "type": "Point"
156
            },
157
            "geometry_name": "the_geom",
158
            "properties": {
159
                "code_insee": 38185,
160
                "code_post": 38000,
161
                "nom_commune": "Grenoble",
162
                "nom_voie": "boulevard \u00e9douard rey",
163
                "numero": 28
164
            },
165
            "type": "Feature"
166
        },
167
        {
168
            "geometry": {
169
                "coordinates": [
170
                    1914018.64,
171
                    4224644.61
172
                ],
173
                "type": "Point"
174
            },
175
            "geometry_name": "the_geom",
176
            "properties": {
177
                "code_insee": 38185,
178
                "code_post": 38000,
179
                "nom_commune": "Grenoble",
180
                "nom_voie": "place victor hugo",
181
                "numero": 4
182
            },
183
            "type": "Feature"
184
        }
185
    ],
186
    "totalFeatures": 4,
187
    "type": "FeatureCollection"
188
}'''
189

  
104 190

  
105 191
@pytest.fixture
106 192
def connector(db):
......
250 336
    assert result['err'] == 1
251 337
    assert result['err_desc'] == 'OpenGIS Error: unparsable error'
252 338
    assert '<ows:' in result['data']['content']
339

  
340
@mock.patch('passerelle.utils.Request.get')
341
def test_reverse_geocoding(mocked_get, app, connector):
342
    connector.search_radius = 45
343
    connector.projection = 'EPSG:3945'
344
    connector.save()
345
    endpoint = utils.generic_endpoint_url('opengis', 'reverse', slug=connector.slug)
346
    assert endpoint == '/opengis/test/reverse'
347
    mocked_get.return_value = utils.FakedResponse(content=FAKE_GEOLOCATED_FEATURE, status_code=200)
348
    resp = app.get(endpoint, params={'lat':'45.1893469606986', 'lon': '5.72462060798'})
349
    assert mocked_get.call_args[1]['params']['CQL_FILTER'] == 'DWITHIN(the_geom,Point(1914061.48604 4224640.45779),45,meters)'
350
    assert resp.json['lon'] == '5.72407744145'
351
    assert resp.json['lat'] == '45.1893972656'
352
    assert resp.json['address']['house_number'] == '4'
353
    assert resp.json['address']['road'] == 'place victor hugo'
354
    assert resp.json['address']['postcode'] == '38000'
355
    assert resp.json['address']['city'] == 'Grenoble'
356

  
357
    connector.projection = 'EPSG:4326'
358
    connector.search_radius = 10
359
    connector.save()
360
    resp = app.get(endpoint, params={'lat':'45.183784', 'lon': '5.714885'})
361
    assert mocked_get.call_args[1]['params']['CQL_FILTER'] == 'DWITHIN(the_geom,Point(5.714885 45.183784),10,meters)'
253
-