Projet

Général

Profil

0001-opengis-add-support-for-geojson-shapes-57280.patch

Frédéric Péters, 27 septembre 2021 16:42

Télécharger (13,6 ko)

Voir les différences:

Subject: [PATCH] opengis: add support for geojson shapes (#57280)

 .../migrations/0014_auto_20210927_1006.py     | 23 +++++
 passerelle/apps/opengis/models.py             | 83 +++++++++++++++---
 tests/test_opengis.py                         | 85 ++++++++++++++++---
 3 files changed, 169 insertions(+), 22 deletions(-)
 create mode 100644 passerelle/apps/opengis/migrations/0014_auto_20210927_1006.py
passerelle/apps/opengis/migrations/0014_auto_20210927_1006.py
1
# Generated by Django 2.2.21 on 2021-09-27 08:06
2

  
3
from django.db import migrations, models
4

  
5

  
6
class Migration(migrations.Migration):
7

  
8
    dependencies = [
9
        ('opengis', '0013_remove_query_index_properties'),
10
    ]
11

  
12
    operations = [
13
        migrations.AddField(
14
            model_name='featurecache',
15
            name='bbox_lat2',
16
            field=models.FloatField(blank=True, null=True),
17
        ),
18
        migrations.AddField(
19
            model_name='featurecache',
20
            name='bbox_lon2',
21
            field=models.FloatField(blank=True, null=True),
22
        ),
23
    ]
passerelle/apps/opengis/models.py
23 23
from django.contrib.postgres.fields import JSONField
24 24
from django.core.cache import cache
25 25
from django.db import models, transaction
26
from django.db.models import Q
26 27
from django.http import HttpResponse
27 28
from django.shortcuts import get_object_or_404
28 29
from django.template import Context, Template
......
522 523
                filters[lookup] = value
523 524
        features = features.filter(**filters)
524 525

  
526
        lonmin, latmin, lonmax, latmax = None, None, None, None
525 527
        if bbox:
526 528
            try:
527 529
                lonmin, latmin, lonmax, latmax = (float(x) for x in bbox.split(','))
......
529 531
                raise APIError(
530 532
                    'Invalid bbox parameter, it must be a comma separated list of ' 'floating point numbers.'
531 533
                )
532
            features = features.filter(lon__gte=lonmin, lon__lte=lonmax, lat__gte=latmin, lat__lte=latmax)
533 534

  
534 535
        if circle:
535 536
            try:
......
541 542
                )
542 543
            coords = self.get_bbox_containing_circle(center_lon, center_lat, radius)
543 544
            lonmin, latmin, lonmax, latmax = coords
544
            features = features.filter(lon__gte=lonmin, lon__lte=lonmax, lat__gte=latmin, lat__lte=latmax)
545

  
546
        if lonmin is not None:
547
            # adjust lonmin, latmin, lonmax, latmax to make sure min are min and max are max
548
            lonmin, lonmax = min(lonmin, lonmax), max(lonmin, lonmax)
549
            latmin, latmax = min(latmin, latmax), max(latmin, latmax)
550

  
551
            features = features.filter(
552
                Q(bbox_lat2__isnull=True, lon__gte=lonmin, lon__lte=lonmax, lat__gte=latmin, lat__lte=latmax)
553
                | Q(
554
                    bbox_lat2__isnull=False,  # geometry != Point
555
                    lon__lte=lonmax,
556
                    bbox_lon2__gte=lonmin,
557
                    lat__lte=latmax,
558
                    bbox_lat2__gte=latmin,
559
                )
560
            )
545 561

  
546 562
        if q:
547 563
            features = features.filter(text__search=simplify(q))
......
550 566
        if circle:
551 567
            results = []
552 568
            for feature in features:
553
                distance = self.get_coords_distance(feature.lat, feature.lon, center_lat, center_lon)
554
                if distance < radius:
569
                if feature.bbox_lat2:  # not a point
555 570
                    results.append(feature.data)
571
                else:
572
                    distance = self.get_coords_distance(feature.lat, feature.lon, center_lat, center_lon)
573
                    if distance < radius:
574
                        results.append(feature.data)
575

  
556 576
            data['features'] = results
557 577
        else:
558 578
            data['features'] = list(features.values_list('data', flat=True))
......
598 618
            template = Template(self.indexing_template)
599 619
        for feature in data['data']:
600 620
            geometry = feature.get('geometry') or {}
601
            if geometry.get('type') != 'Point':
602
                continue
603
            try:
604
                lon, lat = geometry['coordinates']
605
            except (KeyError, TypeError):
606
                self.resource.logger.warning('invalid coordinates in geometry: %s', geometry)
621
            if not geometry:
607 622
                continue
623
            if geometry.get('type') == 'Point':
624
                try:
625
                    lon, lat = geometry['coordinates']
626
                except (KeyError, TypeError):
627
                    self.resource.logger.warning('invalid coordinates in geometry: %s', geometry)
628
                    continue
629
                lon2, lat2 = None, None
630
            else:
631
                # define bbox, lat/lon as min values, bbox_lat2/bbox_lon2 as max values
632
                min_lat, min_lon, max_lat, max_lon = None, None, None, None
633

  
634
                def add_coordinates(coordinates):
635
                    nonlocal min_lat, min_lon, max_lat, max_lon
636
                    if not coordinates:
637
                        return
638
                    if not isinstance(coordinates[0], (float, int)):
639
                        for child in coordinates:
640
                            add_coordinates(child)
641
                        return
642

  
643
                    # position
644
                    lon, lat = coordinates
645
                    if min_lat is None or lat < min_lat:
646
                        min_lat = lat
647
                    if max_lat is None or lat > max_lat:
648
                        max_lat = lat
649
                    if min_lon is None or lon < min_lon:
650
                        min_lon = lon
651
                    if max_lon is None or lon > max_lon:
652
                        max_lon = lon
653

  
654
                add_coordinates(geometry['coordinates'])
655
                lat, lon, lat2, lon2 = min_lat, min_lon, max_lat, max_lon
656

  
608 657
            text = ''
609 658
            if self.indexing_template:
610 659
                context = Context(feature.get('properties', {}))
611 660
                text = simplify(template.render(context))
612
            features.append(FeatureCache(query=self, lat=lat, lon=lon, text=text, data=feature))
661
            features.append(
662
                FeatureCache(
663
                    query=self,
664
                    lat=lat,
665
                    lon=lon,
666
                    bbox_lat2=lat2,
667
                    bbox_lon2=lon2,
668
                    text=text,
669
                    data=feature,
670
                )
671
            )
613 672
        with transaction.atomic():
614 673
            self.features.all().delete()
615 674
            FeatureCache.objects.bulk_create(features)
......
625 684
    )
626 685
    lat = models.FloatField()
627 686
    lon = models.FloatField()
687
    bbox_lat2 = models.FloatField(blank=True, null=True)
688
    bbox_lon2 = models.FloatField(blank=True, null=True)
628 689
    text = models.CharField(max_length=2048)
629 690
    data = JSONField()
tests/test_opengis.py
246 246
                "numero": 4
247 247
            },
248 248
            "type": "Feature"
249
        },
250
        {
251
            "geometry": {
252
                "coordinates": [
253
                    [
254
                        [1914018, 4224644],
255
                        [1914018, 4224844],
256
                        [1914318, 4224944],
257
                        [1914318, 4224544]
258
                    ]
259
                ],
260
                "type": "Polygon"
261
            },
262
            "geometry_name": "the_geom",
263
            "properties": {
264
                "code_insee": 38185,
265
                "code_post": 38000,
266
                "nom_commune": "Grenoble",
267
                "nom_square": "place trapeze"
268
            },
269
            "type": "Feature"
270
        },
271
        {
272
            "geometry": {
273
                "coordinates": [
274
                    [
275
                        [1914059, 4224699],
276
                        [1914059, 4224899],
277
                        [1914259, 4224699]
278
                    ]
279
                ],
280
                "type": "Polygon"
281
            },
282
            "geometry_name": "the_geom",
283
            "properties": {
284
                "code_insee": 38185,
285
                "code_post": 38000,
286
                "nom_commune": "Grenoble",
287
                "nom_square": "place triangle"
288
            },
289
            "type": "Feature"
249 290
        }
250 291
    ],
251
    "totalFeatures": 4,
292
    "totalFeatures": 6,
252 293
    "type": "FeatureCollection"
253 294
}'''
254 295

  
......
288 329
            },
289 330
            'geometry': {'type': 'Point', 'coordinates': [2.304, 48.8086]},
290 331
        },
332
        {
333
            'type': 'Feature',
334
            'properties': {
335
                'in-circle': True,
336
                'in-bbox': True,
337
            },
338
            'geometry': {
339
                'type': 'Polygon',
340
                'coordinates': [[[2.304, 48.8086], [2.304, 49.8086], [1.304, 48.8086]]],
341
            },
342
        },
343
        {
344
            'type': 'Feature',
345
            'properties': {
346
                'in-circle': False,
347
                'in-bbox': False,
348
            },
349
            'geometry': {
350
                'type': 'Polygon',
351
                'coordinates': [[[-2.304, 48.8086], [-2.304, 49.8086], [-1.304, 48.8086]]],
352
            },
353
        },
291 354
    ],
292 355
}
293 356

  
......
599 662

  
600 663
    assert mocked_get.call_args[1]['params']['filter'] == query.filter_expression
601 664
    assert mocked_get.call_args[1]['params']['typenames'] == query.typename
602
    assert FeatureCache.objects.count() == 4
665
    assert FeatureCache.objects.count() == 6
603 666

  
604 667
    feature = FeatureCache.objects.get(lon=1914059.51, lat=4224699.2)
605 668
    assert feature.data['properties']['code_post'] == 38000
......
622 685
    assert feature_data == feature.data
623 686

  
624 687
    resp = app.get(endpoint + '?bbox=1914041,4224660,1914060,4224670')
625
    assert len(resp.json['features']) == 1
688
    assert len(resp.json['features']) == 2
626 689

  
627 690
    resp = app.get(endpoint + '?bbox=wrong')
628 691
    assert resp.json['err'] == 1
......
632 695
def test_opengis_query_cache_update_change(mocked_get, app, connector, query):
633 696
    mocked_get.side_effect = geoserver_geolocated_responses
634 697
    query.update_cache()
635
    assert FeatureCache.objects.count() == 4
698
    assert FeatureCache.objects.count() == 6
636 699

  
637 700
    def new_response(url, **kwargs):
638 701
        if kwargs['params'].get('request') == 'GetCapabilities':
......
669 732
    assert not FeatureCache.objects.exists()
670 733

  
671 734
    connector.jobs()
672
    assert FeatureCache.objects.count() == 4
735
    assert FeatureCache.objects.count() == 6
673 736
    job.refresh_from_db()
674 737
    assert job.status == 'completed'
675 738

  
......
704 767
    mocked_get.side_effect = geoserver_geolocated_responses
705 768
    assert not FeatureCache.objects.exists()
706 769
    call_command('cron', 'daily')
707
    assert FeatureCache.objects.count() == 4
770
    assert FeatureCache.objects.count() == 6
708 771

  
709 772

  
710 773
@mock.patch('passerelle.utils.Request.get')
......
729 792
    query.update_cache()
730 793

  
731 794
    resp = app.get(endpoint + '?q=grenoble')
732
    assert len(resp.json['features']) == 4
795
    assert len(resp.json['features']) == 6
733 796

  
734 797
    resp = app.get(endpoint + '?q=victor')
735 798
    assert len(resp.json['features']) == 2
......
746 809
    query.update_cache()
747 810

  
748 811
    resp = app.get(endpoint + '?q=plop')
749
    assert len(resp.json['features']) == 4
812
    assert len(resp.json['features']) == 6
750 813

  
751 814

  
752 815
@mock.patch('passerelle.utils.Request.get')
......
762 825
    bbox = Query.get_bbox_containing_circle(center_lon, center_lat, float(radius))
763 826
    resp = app.get(endpoint + '?bbox=' + ','.join((str(x) for x in bbox)))
764 827
    features = resp.json['features']
765
    assert len(features) == 3
828
    assert len(features) == 4
766 829
    assert all(feature['properties']['in-circle'] or feature['properties']['in-bbox'] for feature in features)
767 830

  
768 831
    resp = app.get(endpoint + '?circle=%s,%s,%s' % (center_lon, center_lat, radius))
769 832
    features = resp.json['features']
770
    assert len(features) == 2
833
    assert len(features) == 3
771 834
    assert all(feature['properties']['in-circle'] for feature in features)
772 835

  
773 836

  
......
778 841
    query.update_cache()
779 842

  
780 843
    resp = app.get(endpoint + '?property:code_insee=38185')
781
    assert len(resp.json['features']) == 4
844
    assert len(resp.json['features']) == 6
782 845

  
783 846
    resp = app.get(endpoint + '?property:nom_voie=place victor hugo')
784 847
    assert len(resp.json['features']) == 2
785
-