0001-opengis-add-support-for-geojson-shapes-57280.patch
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 |
- |