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                  |  63 ++++++++++++
 tests/test_opengis.py                              | 113 +++++++++++++++++++++
 3 files changed, 196 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')
......
231 238
        return HttpResponse(
232 239
                self.requests.get(self.wms_service_url, params=params, cache_duration=300).content,
233 240
                content_type='image/png')
241

  
242
    @endpoint(perm='can_access', description=_('Get feature info'))
243
    def reverse(self, request, lat, lon):
244
        result = None
245

  
246
        if self.projection != 'epsg:4326':
247
            wgs84 = pyproj.Proj(init='epsg:4326')
248
            target_projection = pyproj.Proj(init=self.projection)
249
            lon, lat = pyproj.transform(wgs84, target_projection, lon, lat)
250

  
251
        cql_filter = 'DWITHIN(the_geom,Point(%s %s),%s,meters)' % (lon, lat, self.search_radius)
252
        params = {
253
            'VERSION': self.get_wfs_service_version(),
254
            'SERVICE': 'WFS',
255
            'REQUEST': 'GetFeature',
256
            'TYPENAMES': self.query_layer,
257
            'OUTPUTFORMAT': 'json',
258
            'CQL_FILTER': cql_filter
259
        }
260
        response = self.requests.get(self.wfs_service_url, params=params)
261
        min_lon_diff = None
262
        min_lat_diff = None
263
        closest_feature = {}
264
        for feature in response.json().get('features'):
265
            if not feature['geometry']['type'] == 'Point':
266
                continue  # skip unknown
267
            lon_diff = abs(float(lon) - float(feature['geometry']['coordinates'][0]))
268
            lat_diff = abs(float(lat) - float(feature['geometry']['coordinates'][1]))
269
            if min_lon_diff is None and min_lat_diff is None:
270
                min_lon_diff = lon_diff
271
                min_lat_diff = lat_diff
272
            if lon_diff <= min_lon_diff and lat_diff <= min_lat_diff:
273
                closest_feature = feature
274

  
275
        if closest_feature:
276
            result = {}
277
            point_lon = closest_feature['geometry']['coordinates'][0]
278
            point_lat = closest_feature['geometry']['coordinates'][1]
279
            if self.projection != 'epsg:4326':
280
                point_lon, point_lat = pyproj.transform(target_projection, wgs84,
281
                                                        point_lon, point_lat)
282
            result['lon'] = str(point_lon)
283
            result['lat'] = str(point_lat)
284
            result['address'] = {}
285

  
286
            for attribute, properties in self.attributes_mapping:
287
                if request.GET.get(attribute):
288
                    result['address'][attribute] = request.GET[attribute]
289
                    continue
290
                for field in properties:
291
                    if closest_feature['properties'].get(field):
292
                        result['address'][attribute] = unicode(closest_feature['properties'][field])
293
                        break
294

  
295

  
296
        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):
......
244 330
    assert result['err'] == 1
245 331
    assert result['err_desc'] == 'OpenGIS Error: unparsable error'
246 332
    assert '<ows:' in result['data']['content']
333

  
334
@mock.patch('passerelle.utils.Request.get')
335
def test_reverse_geocoding(mocked_get, app, connector):
336
    connector.search_radius = 45
337
    connector.projection = 'EPSG:3945'
338
    connector.save()
339
    endpoint = utils.generic_endpoint_url('opengis', 'reverse', slug=connector.slug)
340
    assert endpoint == '/opengis/test/reverse'
341
    mocked_get.return_value = utils.FakedResponse(content=FAKE_GEOLOCATED_FEATURE, status_code=200)
342
    resp = app.get(endpoint, params={'lat':'45.1895752129513', 'lon': '5.72438938399621'})
343
    assert mocked_get.call_args[1]['params']['CQL_FILTER'] == 'DWITHIN(the_geom,Point(1914042.47393 4224665.19751),45,meters)'
344
    assert resp.json['lon'] == '5.72438933506'
345
    assert resp.json['lat'] == '45.1895752366'
346
    assert resp.json['address']['house_number'] == '2'
347
    assert resp.json['address']['road'] == 'place victor hugo'
348
    assert resp.json['address']['postcode'] == '38000'
349
    assert resp.json['address']['city'] == 'Grenoble'
350
    assert 'country' not in resp.json['address']
351

  
352
    resp = app.get(endpoint, params={'lat':'45.1895752129513', 'lon': '5.72438938399621', 'country': 'France'})
353
    assert resp.json['address']['country'] == 'France'
354

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