geovisio 2.8.0__py3-none-any.whl → 2.9.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. geovisio/__init__.py +16 -3
  2. geovisio/config_app.py +11 -1
  3. geovisio/translations/br/LC_MESSAGES/messages.mo +0 -0
  4. geovisio/translations/br/LC_MESSAGES/messages.po +762 -0
  5. geovisio/translations/da/LC_MESSAGES/messages.mo +0 -0
  6. geovisio/translations/da/LC_MESSAGES/messages.po +10 -1
  7. geovisio/translations/de/LC_MESSAGES/messages.mo +0 -0
  8. geovisio/translations/de/LC_MESSAGES/messages.po +10 -1
  9. geovisio/translations/en/LC_MESSAGES/messages.mo +0 -0
  10. geovisio/translations/en/LC_MESSAGES/messages.po +9 -7
  11. geovisio/translations/eo/LC_MESSAGES/messages.mo +0 -0
  12. geovisio/translations/eo/LC_MESSAGES/messages.po +67 -1
  13. geovisio/translations/es/LC_MESSAGES/messages.mo +0 -0
  14. geovisio/translations/es/LC_MESSAGES/messages.po +4 -3
  15. geovisio/translations/fr/LC_MESSAGES/messages.mo +0 -0
  16. geovisio/translations/fr/LC_MESSAGES/messages.po +37 -4
  17. geovisio/translations/hu/LC_MESSAGES/messages.mo +0 -0
  18. geovisio/translations/hu/LC_MESSAGES/messages.po +4 -3
  19. geovisio/translations/it/LC_MESSAGES/messages.mo +0 -0
  20. geovisio/translations/it/LC_MESSAGES/messages.po +10 -1
  21. geovisio/translations/ja/LC_MESSAGES/messages.mo +0 -0
  22. geovisio/translations/ja/LC_MESSAGES/messages.po +242 -154
  23. geovisio/translations/nl/LC_MESSAGES/messages.mo +0 -0
  24. geovisio/translations/nl/LC_MESSAGES/messages.po +131 -25
  25. geovisio/translations/pl/LC_MESSAGES/messages.mo +0 -0
  26. geovisio/translations/pl/LC_MESSAGES/messages.po +4 -3
  27. geovisio/translations/sv/LC_MESSAGES/messages.mo +0 -0
  28. geovisio/translations/sv/LC_MESSAGES/messages.po +822 -0
  29. geovisio/utils/annotations.py +186 -0
  30. geovisio/utils/cql2.py +134 -0
  31. geovisio/utils/db.py +7 -0
  32. geovisio/utils/fields.py +24 -7
  33. geovisio/utils/loggers.py +14 -0
  34. geovisio/utils/model_query.py +2 -2
  35. geovisio/utils/params.py +7 -4
  36. geovisio/utils/pic_shape.py +63 -0
  37. geovisio/utils/pictures.py +54 -12
  38. geovisio/utils/reports.py +10 -17
  39. geovisio/utils/semantics.py +165 -55
  40. geovisio/utils/sentry.py +0 -1
  41. geovisio/utils/sequences.py +141 -60
  42. geovisio/utils/tags.py +31 -0
  43. geovisio/utils/upload_set.py +26 -21
  44. geovisio/utils/website.py +3 -0
  45. geovisio/web/annotations.py +205 -9
  46. geovisio/web/auth.py +3 -2
  47. geovisio/web/collections.py +49 -34
  48. geovisio/web/configuration.py +2 -1
  49. geovisio/web/docs.py +55 -16
  50. geovisio/web/items.py +55 -54
  51. geovisio/web/map.py +25 -13
  52. geovisio/web/params.py +11 -21
  53. geovisio/web/stac.py +19 -12
  54. geovisio/web/upload_set.py +92 -11
  55. geovisio/web/users.py +31 -4
  56. geovisio/workers/runner_pictures.py +71 -10
  57. {geovisio-2.8.0.dist-info → geovisio-2.9.0.dist-info}/METADATA +24 -22
  58. geovisio-2.9.0.dist-info/RECORD +98 -0
  59. {geovisio-2.8.0.dist-info → geovisio-2.9.0.dist-info}/WHEEL +1 -1
  60. geovisio-2.8.0.dist-info/RECORD +0 -89
  61. {geovisio-2.8.0.dist-info → geovisio-2.9.0.dist-info/licenses}/LICENSE +0 -0
geovisio/web/docs.py CHANGED
@@ -1,5 +1,10 @@
1
- from geovisio.web import annotations, collections, items, prepare, users, utils, upload_set, reports, excluded_areas, pages
2
- from geovisio.utils import upload_set as upload_set_utils, reports as reports_utils, excluded_areas as excluded_areas_utils
1
+ from geovisio.web import collections, items, prepare, users, utils, upload_set, reports, excluded_areas, pages
2
+ from geovisio.utils import (
3
+ upload_set as upload_set_utils,
4
+ reports as reports_utils,
5
+ excluded_areas as excluded_areas_utils,
6
+ annotations as annotations_utils,
7
+ )
3
8
  from importlib import metadata
4
9
  import re
5
10
 
@@ -258,6 +263,9 @@ Note that you may not rely only on these ID that could change through time.
258
263
  "GeoVisioUploadSetFiles": upload_set_utils.UploadSetFiles.model_json_schema(
259
264
  ref_template="#/components/schemas/GeoVisioUploadSetFiles/$defs/{model}", mode="serialization"
260
265
  ),
266
+ "UploadSetUpdateParameter": upload_set.UploadSetUpdateParameter.model_json_schema(
267
+ ref_template="#/components/schemas/UploadSetUpdateParameter/$defs/{model}", mode="serialization"
268
+ ),
261
269
  "GeoVisioCollectionOfCollection": {
262
270
  "allOf": [
263
271
  {"$ref": "#/components/schemas/STACCollection"},
@@ -462,6 +470,11 @@ The CSV headers will be:
462
470
  "geovisio:producer": {"type": "string"},
463
471
  "geovisio:image": {"type": "string", "format": "uri"},
464
472
  "geovisio:thumbnail": {"type": "string", "format": "uri"},
473
+ "geovisio:rank_in_collection": {
474
+ "type": "integer",
475
+ "minimum": 1,
476
+ "title": "Rank of the picture in its collection.",
477
+ },
465
478
  "original_file:size": {"type": "integer", "minimum": 0, "title": "Size of the original file, in bytes"},
466
479
  "original_file:name": {"type": "string", "title": "Original file name"},
467
480
  "panoramax:horizontal_pixel_density": {
@@ -647,20 +660,9 @@ Available properties are:
647
660
  "GeoVisioUserConfiguration": users.UserConfiguration.model_json_schema(
648
661
  ref_template="#/components/schemas/GeoVisioUserConfiguration/$defs/{model}", mode="serialization"
649
662
  ),
650
- "GeoVisioUser": {
651
- "type": "object",
652
- "properties": {
653
- "id": {"type": "string", "format": "uuid"},
654
- "name": {"type": "string"},
655
- "links": {
656
- "type": "array",
657
- "items": {
658
- "type": "object",
659
- "properties": {"href": {"type": "string"}, "ref": {"type": "string"}, "type": {"type": "string"}},
660
- },
661
- },
662
- },
663
- },
663
+ "GeoVisioUser": users.UserInfo.model_json_schema(
664
+ ref_template="#/components/schemas/GeoVisioUser/$defs/{model}", mode="serialization"
665
+ ),
664
666
  "GeoVisioUserAuth": {
665
667
  "type": "object",
666
668
  "properties": {
@@ -753,6 +755,10 @@ Available properties are:
753
755
  "properties": {
754
756
  "user_profile": {"type": "object", "properties": {"url": {"type": "string"}}},
755
757
  "enabled": {"type": "boolean"},
758
+ "registration_is_open": {
759
+ "type": "boolean",
760
+ "description": "If true, users can create their own account on the instance. Only used for reference in the federation for the moment",
761
+ },
756
762
  "enforce_tos_acceptance": {"type": "boolean"},
757
763
  },
758
764
  "required": ["enabled"],
@@ -832,6 +838,12 @@ Available properties are:
832
838
  "payload": {"type": "object", "description": "The error payload"},
833
839
  },
834
840
  },
841
+ "GeoVisioAnnotation": annotations_utils.Annotation.model_json_schema(
842
+ ref_template="#/components/schemas/GeoVisioAnnotation/$defs/{model}", mode="serialization"
843
+ ),
844
+ "GeoVisioPostAnnotation": annotations_utils.AnnotationCreationParameter.model_json_schema(
845
+ ref_template="#/components/schemas/GeoVisioPostAnnotation/$defs/{model}", mode="serialization"
846
+ ),
835
847
  },
836
848
  "parameters": {
837
849
  "STAC_bbox": {"$ref": f"https://api.stacspec.org/v{utils.STAC_VERSION}/item-search/openapi.yaml#/components/parameters/bbox"},
@@ -927,6 +939,33 @@ Note that this parameter is not taken in account for 360° pictures, as by defin
927
939
  "required": False,
928
940
  "schema": {"type": "integer", "minimum": 2, "maximum": 180, "default": 30},
929
941
  },
942
+ "searchCQL2_filter": {
943
+ "name": "filter",
944
+ "in": "query",
945
+ "description": """
946
+ A CQL2 filter expression for filtering search results.
947
+
948
+ Only works for semantic search for the moment.
949
+
950
+ The attributes must start with "semantics." and formated like "semantics.some_key"='some_value'.
951
+
952
+ Note: it's important for the attribute to be quoted (`"`) and the value to around simple quotes (`'`) to avoid issues with CQL2 parsing.
953
+
954
+ For the moment only equality (`=`) and list (`IN`) filters are supported. We do not support searching for multiple different tags at once with an `AND` operator (for example, `"semantics.traffic_sign"='yes' AND "semantics.colour"='red'` __will not work__). We suggest to filter data on your side, after retrieving by the main attribute depending on your interest.
955
+
956
+ To search for any values of a semantic tag, use `semantics.some_key IS NOT NULL` (case matter here).
957
+
958
+ Examples:
959
+
960
+ * "semantics.osm|traffic_sign"='yes'
961
+ * "semantics.osm|traffic_sign" IS NOT NULL'
962
+ * "semantics.osm|amenity" IN ('bench', 'whatever') OR "semantics.osm|traffic_sign"='yes'
963
+ """,
964
+ "required": False,
965
+ "schema": {
966
+ "type": "string",
967
+ },
968
+ },
930
969
  "GeoVisioReports_filter": {
931
970
  "name": "filter",
932
971
  "in": "query",
geovisio/web/items.py CHANGED
@@ -6,13 +6,16 @@ from typing import Dict, List, Optional, Any
6
6
  from urllib.parse import unquote
7
7
  from psycopg.types.json import Jsonb
8
8
  from pydantic import BaseModel, ConfigDict, ValidationError, field_validator, model_validator
9
+ from shapely import intersects
9
10
  from werkzeug.datastructures import MultiDict
10
11
  from uuid import UUID
11
12
  from geovisio import errors, utils
12
13
  from geovisio.utils import auth, db
14
+ from geovisio.utils.cql2 import parse_search_filter
13
15
  from geovisio.utils.params import validation_error
14
16
  from geovisio.utils.pictures import cleanupExif
15
- from geovisio.utils.semantics import SemanticTagUpdate, Entity, EntityType, update_tags
17
+ from geovisio.utils.semantics import Entity, EntityType, update_tags
18
+ from geovisio.utils.tags import SemanticTagUpdate
16
19
  from geovisio.web.params import (
17
20
  as_latitude,
18
21
  as_longitude,
@@ -138,6 +141,7 @@ def dbPictureToStacItem(seqId, dbPic):
138
141
  "pers:roll": dbPic["metadata"].get("roll"),
139
142
  "geovisio:status": dbPic.get("status"),
140
143
  "geovisio:producer": dbPic["account_name"],
144
+ "geovisio:rank_in_collection": dbPic["rank"],
141
145
  "original_file:size": dbPic["metadata"].get("originalFileSize"),
142
146
  "original_file:name": dbPic["metadata"].get("originalFileName"),
143
147
  "panoramax:horizontal_pixel_density": dbPic.get("h_pixel_density"),
@@ -145,7 +149,8 @@ def dbPictureToStacItem(seqId, dbPic):
145
149
  "geovisio:thumbnail": _getThumbJpgPictureURL(dbPic["id"], dbPic.get("status")),
146
150
  "exif": removeNoneInDict(cleanupExif(dbPic["exif"])),
147
151
  "quality:horizontal_accuracy": float("{:.1f}".format(dbPic["gps_accuracy_m"])) if dbPic.get("gps_accuracy_m") else None,
148
- "semantics": dbPic["semantics"] if "semantics" in dbPic else None,
152
+ "semantics": [s for s in dbPic.get("semantics") or [] if s],
153
+ "annotations": [a for a in dbPic.get("annotations") or [] if a],
149
154
  }
150
155
  ),
151
156
  "links": cleanNoneInList(
@@ -437,19 +442,12 @@ def getCollectionItems(collectionId):
437
442
  CASE WHEN LAG(p.status) OVER othpics = 'ready' THEN ST_AsGeoJSON(LAG(p.geom) OVER othpics)::json END AS prevpicgeojson,
438
443
  CASE WHEN LEAD(p.status) OVER othpics = 'ready' THEN LEAD(p.id) OVER othpics END AS nextpic,
439
444
  CASE WHEN LEAD(p.status) OVER othpics = 'ready' THEN ST_AsGeoJSON(LEAD(p.geom) OVER othpics)::json END AS nextpicgeojson,
440
- t.semantics
445
+ get_picture_semantics(p.id) as semantics,
446
+ get_picture_annotations(p.id) as annotations
441
447
  FROM sequences_pictures sp
442
448
  JOIN pictures p ON sp.pic_id = p.id
443
449
  JOIN accounts a ON a.id = p.account_id
444
450
  JOIN sequences s ON s.id = sp.seq_id
445
- LEFT JOIN (
446
- SELECT picture_id, json_agg(json_strip_nulls(json_build_object(
447
- 'key', key,
448
- 'value', value
449
- ))) AS semantics
450
- FROM pictures_semantics
451
- GROUP BY picture_id
452
- ) t ON t.picture_id = p.id
453
451
  WHERE
454
452
  {filter}
455
453
  WINDOW othpics AS (PARTITION BY sp.seq_id ORDER BY sp.rank)
@@ -460,15 +458,14 @@ def getCollectionItems(collectionId):
460
458
 
461
459
  records = cursor.execute(query, params)
462
460
 
463
- bounds: Optional[Bounds] = None
464
461
  items = []
462
+ first_rank, last_rank = None, None
465
463
  for dbPic in records:
466
- if not bounds:
467
- bounds = Bounds(min=dbPic["rank"], max=dbPic["rank"])
468
- else:
469
- bounds.update(dbPic["rank"])
470
-
464
+ if first_rank is None:
465
+ first_rank = dbPic["rank"]
466
+ last_rank = dbPic["rank"]
471
467
  items.append(dbPictureToStacItem(collectionId, dbPic))
468
+ bounds = Bounds(first=first_rank, last=last_rank) if records else None
472
469
 
473
470
  links = [
474
471
  get_root_link(),
@@ -491,8 +488,8 @@ def getCollectionItems(collectionId):
491
488
  ]
492
489
 
493
490
  if paginated and items and bounds:
494
- if bounds.min:
495
- has_item_before = bounds.min > seqMeta["min_rank"]
491
+ if bounds.first:
492
+ has_item_before = bounds.first > seqMeta["min_rank"]
496
493
  if has_item_before:
497
494
  links.append(
498
495
  {
@@ -504,7 +501,7 @@ def getCollectionItems(collectionId):
504
501
  # Previous page link
505
502
  # - If limit is set, rank is current - limit -1
506
503
  # - If no limit is set, rank is 0 (none)
507
- prevRank = bounds.min - limit - 1 if limit is not None else 0
504
+ prevRank = bounds.first - limit - 1 if limit is not None else 0
508
505
  if prevRank < 1:
509
506
  prevRank = None
510
507
  links.append(
@@ -521,7 +518,7 @@ def getCollectionItems(collectionId):
521
518
  }
522
519
  )
523
520
 
524
- has_item_after = bounds.max < seqMeta["max_rank"]
521
+ has_item_after = bounds.last < seqMeta["max_rank"]
525
522
  if has_item_after:
526
523
  links.append(
527
524
  {
@@ -532,7 +529,7 @@ def getCollectionItems(collectionId):
532
529
  _external=True,
533
530
  collectionId=collectionId,
534
531
  limit=limit,
535
- startAfterRank=bounds.max,
532
+ startAfterRank=bounds.last,
536
533
  ),
537
534
  }
538
535
  )
@@ -543,10 +540,10 @@ def getCollectionItems(collectionId):
543
540
 
544
541
  lastPageRank = startAfterRank
545
542
  if limit is not None:
546
- if seqMeta["max_rank"] > bounds.max:
543
+ if seqMeta["max_rank"] > bounds.last:
547
544
  lastPageRank = seqMeta["max_rank"] - limit
548
- if lastPageRank < bounds.max:
549
- lastPageRank = bounds.max
545
+ if lastPageRank < bounds.last:
546
+ lastPageRank = bounds.last
550
547
 
551
548
  links.append(
552
549
  {
@@ -608,19 +605,13 @@ def _getPictureItemById(collectionId, itemId):
608
605
  p.account_id AS account_id,
609
606
  spl.prevpic, spl.prevpicgeojson, spl.nextpic, spl.nextpicgeojson, p.exif,
610
607
  relp.related_pics, p.gps_accuracy_m, p.h_pixel_density,
611
- t.semantics
608
+
609
+ get_picture_semantics(p.id) as semantics,
610
+ get_picture_annotations(p.id) as annotations
612
611
  FROM pictures p
613
612
  JOIN sequences_pictures sp ON sp.pic_id = p.id
614
613
  JOIN accounts ON p.account_id = accounts.id
615
614
  JOIN sequences s ON sp.seq_id = s.id
616
- LEFT JOIN (
617
- SELECT picture_id, json_agg(json_strip_nulls(json_build_object(
618
- 'key', key,
619
- 'value', value
620
- ))) AS semantics
621
- FROM pictures_semantics
622
- GROUP BY picture_id
623
- ) t ON t.picture_id = p.id
624
615
  LEFT JOIN (
625
616
  SELECT
626
617
  p.id,
@@ -735,6 +726,18 @@ def getCollectionItem(collectionId, itemId):
735
726
  return stacItem, picStatusToHttpCode[stacItem["properties"]["geovisio:status"]], {"Content-Type": "application/geo+json"}
736
727
 
737
728
 
729
+ class SearchParams(BaseModel):
730
+ bbox: Optional[str] = None
731
+ limit: int = 10
732
+ datetime: Optional[str] = None
733
+ place_position: Optional[str] = None
734
+ place_distance: Optional[str] = None
735
+ place_fov_tolerance: Optional[int] = None
736
+ intersects: Optional[str] = None
737
+ ids: Optional[str] = None
738
+ collections: Optional[str] = None
739
+
740
+
738
741
  @bp.route("/search", methods=["GET", "POST"])
739
742
  def searchItems():
740
743
  """Search through all available items
@@ -755,6 +758,7 @@ def searchItems():
755
758
  - $ref: '#/components/parameters/GeoVisio_place_position'
756
759
  - $ref: '#/components/parameters/GeoVisio_place_distance'
757
760
  - $ref: '#/components/parameters/GeoVisio_place_fov_tolerance'
761
+ - $ref: '#/components/parameters/searchCQL2_filter'
758
762
  post:
759
763
  requestBody:
760
764
  required: true
@@ -788,14 +792,14 @@ def searchItems():
788
792
  args = request.args
789
793
 
790
794
  # Limit
791
- if args.get("limit") is not None:
792
- limit = args.get("limit", type=int)
793
- if limit is None or limit < 1 or limit > 10000:
794
- raise errors.InvalidAPIUsage(_("Parameter limit must be either empty or a number between 1 and 10000"), status_code=400)
795
- else:
796
- sqlParams["limit"] = limit
797
- else:
798
- sqlParams["limit"] = 10
795
+ limit = args.get("limit") or 10
796
+ try:
797
+ limit = int(limit)
798
+ if limit < 1 or limit > 10000:
799
+ raise ValueError()
800
+ except ValueError:
801
+ raise errors.InvalidAPIUsage(_("Parameter limit must be either empty or a number between 1 and 10000"), status_code=400)
802
+ sqlParams["limit"] = limit
799
803
 
800
804
  # Bounding box
801
805
  bboxarg = parse_bbox(args.getlist("bbox"))
@@ -900,7 +904,7 @@ def searchItems():
900
904
  raise errors.InvalidAPIUsage(_("Parameter collections should be a JSON array of strings"), status_code=400)
901
905
 
902
906
  # To speed up search, if it's a search by id and on only one id, we use the same code as /collections/:cid/items/:id
903
- if args.get("ids") is not None and args:
907
+ if args.get("ids") is not None:
904
908
  ids = parse_list(args.get("ids"), paramName="ids")
905
909
  if ids and len(ids) == 1:
906
910
  picture_id = ids[0]
@@ -917,7 +921,11 @@ def searchItems():
917
921
  200,
918
922
  {"Content-Type": "application/geo+json"},
919
923
  )
920
-
924
+ filter_param = args.get("filter")
925
+ if filter_param is not None:
926
+ cql_filter = parse_search_filter(filter_param)
927
+ if cql_filter is not None:
928
+ sqlWhere.append(cql_filter)
921
929
  #
922
930
  # Database query
923
931
  #
@@ -932,19 +940,12 @@ SELECT * FROM (
932
940
  accounts.name AS account_name,
933
941
  p.account_id AS account_id,
934
942
  p.exif, p.gps_accuracy_m, p.h_pixel_density,
935
- t.semantics
943
+ get_picture_semantics(p.id) as semantics,
944
+ get_picture_annotations(p.id) as annotations
936
945
  FROM pictures p
937
946
  LEFT JOIN sequences_pictures sp ON p.id = sp.pic_id
938
947
  LEFT JOIN sequences s ON s.id = sp.seq_id
939
948
  LEFT JOIN accounts ON p.account_id = accounts.id
940
- LEFT JOIN (
941
- SELECT picture_id, json_agg(json_strip_nulls(json_build_object(
942
- 'key', key,
943
- 'value', value
944
- ))) AS semantics
945
- FROM pictures_semantics
946
- GROUP BY picture_id
947
- ) t ON t.picture_id = p.id
948
949
  WHERE {sqlWhere}
949
950
  {orderBy}
950
951
  LIMIT %(limit)s
@@ -1247,7 +1248,7 @@ def patchCollectionItem(collectionId, itemId, account):
1247
1248
  ---
1248
1249
  tags:
1249
1250
  - Editing
1250
- - Tags
1251
+ - Semantics
1251
1252
  parameters:
1252
1253
  - name: collectionId
1253
1254
  in: path
geovisio/web/map.py CHANGED
@@ -352,23 +352,35 @@ def _get_query(z: int, x: int, y: int, onlyForUser: Optional[UUID], additional_f
352
352
  sql.SQL("nb_360_pictures"),
353
353
  sql.SQL("nb_pictures - nb_360_pictures AS nb_flat_pictures"),
354
354
  sql.SQL(
355
- """((CASE WHEN nb_pictures = 0 THEN 0 WHEN nb_pictures <= (select PERCENTILE_CONT(0.5) WITHIN GROUP(ORDER BY nb_pictures) from pictures_grid)
356
- THEN nb_pictures::float / (select PERCENTILE_CONT(0.5) WITHIN GROUP(ORDER BY nb_pictures) from pictures_grid) * 0.5
357
- ELSE 0.5 + nb_pictures::float / (SELECT MAX(nb_pictures) FROM pictures_grid) * 0.5
355
+ """((CASE WHEN nb_pictures = 0
356
+ THEN 0
357
+ WHEN nb_pictures <= (SELECT PERCENTILE_CONT(0.5) WITHIN GROUP(ORDER BY nb_pictures) FILTER (WHERE nb_pictures > 0) FROM pictures_grid)
358
+ THEN
359
+ nb_pictures::float / (SELECT PERCENTILE_CONT(0.5) WITHIN GROUP(ORDER BY nb_pictures) FILTER (WHERE nb_pictures > 0) FROM pictures_grid) * 0.5
360
+ ELSE
361
+ 0.5 + nb_pictures::float / (SELECT MAX(nb_pictures) FROM pictures_grid) * 0.5
358
362
  END) * 10)::int / 10::float AS coef"""
359
363
  ),
360
364
  sql.SQL(
361
- """((CASE WHEN nb_360_pictures = 0 THEN 0
362
- WHEN nb_360_pictures <= (select PERCENTILE_CONT(0.5) WITHIN GROUP(ORDER BY nb_360_pictures) from pictures_grid)
363
- THEN nb_360_pictures::float / (select PERCENTILE_CONT(0.5) WITHIN GROUP(ORDER BY nb_360_pictures) from pictures_grid) * 0.5
364
- ELSE 0.5 + nb_360_pictures::float / (SELECT MAX(nb_360_pictures) FROM pictures_grid) * 0.5
365
+ """((CASE WHEN nb_360_pictures = 0
366
+ THEN 0
367
+ WHEN (SELECT PERCENTILE_CONT(0.5) WITHIN GROUP(ORDER BY nb_360_pictures) FILTER (WHERE nb_360_pictures > 0) FROM pictures_grid) = 0
368
+ THEN 0
369
+ WHEN nb_360_pictures <= (SELECT PERCENTILE_CONT(0.5) WITHIN GROUP(ORDER BY nb_360_pictures) FILTER (WHERE nb_360_pictures > 0) FROM pictures_grid)
370
+ THEN nb_360_pictures::float / (SELECT PERCENTILE_CONT(0.5) WITHIN GROUP(ORDER BY nb_360_pictures) FILTER (WHERE nb_360_pictures > 0) FROM pictures_grid) * 0.5
371
+ ELSE
372
+ 0.5 + nb_360_pictures::float / (SELECT MAX(nb_360_pictures) FROM pictures_grid) * 0.5
365
373
  END) * 10)::int / 10::float AS coef_360_pictures"""
366
374
  ),
367
375
  sql.SQL(
368
- """((CASE WHEN (nb_pictures - nb_360_pictures) = 0 THEN 0
369
- WHEN (nb_pictures - nb_360_pictures) <= (select PERCENTILE_CONT(0.5) WITHIN GROUP(ORDER BY (nb_pictures - nb_360_pictures)) from pictures_grid)
370
- THEN (nb_pictures - nb_360_pictures)::float / (select PERCENTILE_CONT(0.5) WITHIN GROUP(ORDER BY (nb_pictures - nb_360_pictures)) from pictures_grid) * 0.5
371
- ELSE 0.5 + (nb_pictures - nb_360_pictures)::float / (SELECT MAX((nb_pictures - nb_360_pictures)) FROM pictures_grid) * 0.5
376
+ """((CASE WHEN (nb_pictures - nb_360_pictures) = 0
377
+ THEN 0
378
+ WHEN (SELECT PERCENTILE_CONT(0.5) WITHIN GROUP(ORDER BY (nb_pictures - nb_360_pictures)) FILTER (WHERE (nb_pictures - nb_360_pictures) > 0) FROM pictures_grid) = 0
379
+ THEN 0
380
+ WHEN (nb_pictures - nb_360_pictures) <= (SELECT PERCENTILE_CONT(0.5) WITHIN GROUP(ORDER BY (nb_pictures - nb_360_pictures)) FILTER (WHERE (nb_pictures - nb_360_pictures) > 0) FROM pictures_grid)
381
+ THEN (nb_pictures - nb_360_pictures)::float / (SELECT PERCENTILE_CONT(0.5) WITHIN GROUP(ORDER BY (nb_pictures - nb_360_pictures)) FILTER (WHERE (nb_pictures - nb_360_pictures) > 0) FROM pictures_grid) * 0.5
382
+ ELSE
383
+ 0.5 + (nb_pictures - nb_360_pictures)::float / (SELECT MAX((nb_pictures - nb_360_pictures)) FROM pictures_grid) * 0.5
372
384
  END) * 10)::int / 10::float AS coef_flat_pictures"""
373
385
  ),
374
386
  ]
@@ -616,7 +628,7 @@ def getUserTile(userId: UUID, z: int, x: int, y: int, format: str):
616
628
  format: binary
617
629
  """
618
630
 
619
- filter = params.parse_filter(request.args.get("filter"))
631
+ filter = params.parse_collection_filter(request.args.get("filter"))
620
632
  return _getTile(z, x, y, format, onlyForUser=userId, filter=filter)
621
633
 
622
634
 
@@ -688,5 +700,5 @@ def getMyTile(account: Account, z: int, x: int, y: int, format: str):
688
700
  type: string
689
701
  format: binary
690
702
  """
691
- filter = params.parse_filter(request.args.get("filter"))
703
+ filter = params.parse_collection_filter(request.args.get("filter"))
692
704
  return _getTile(z, x, y, format, onlyForUser=UUID(account.id), filter=filter)
geovisio/web/params.py CHANGED
@@ -8,8 +8,6 @@ import datetime
8
8
  import re
9
9
  from werkzeug.datastructures import MultiDict
10
10
  from typing import Optional, Tuple, Any, List
11
- from pygeofilter.backends.sql import to_sql_where
12
- from pygeofilter.parsers.ecql import parse as ecql_parser
13
11
  from pygeofilter import ast
14
12
  from pygeofilter.backends.evaluator import Evaluator, handle
15
13
  from psycopg import sql
@@ -17,6 +15,8 @@ from geovisio.utils.sequences import STAC_FIELD_MAPPINGS, STAC_FIELD_TO_SQL_FILT
17
15
  from geovisio.utils.fields import SortBy, SQLDirection, SortByField
18
16
  from flask_babel import gettext as _
19
17
 
18
+ from geovisio.utils.cql2 import parse_cql2_filter
19
+
20
20
 
21
21
  RGX_SORTBY = re.compile("[+-]?[A-Za-z_].*(,[+-]?[A-Za-z_].*)*")
22
22
  SEQUENCES_DEFAULT_FETCH = 100
@@ -325,37 +325,27 @@ def parse_list(value: Optional[Any], tryFallbacks: bool = True, paramName: Optio
325
325
  return None
326
326
 
327
327
 
328
- def parse_filter(value: Optional[str]) -> Optional[sql.SQL]:
328
+ def parse_collection_filter(value: Optional[str]) -> Optional[sql.SQL]:
329
329
  """Reads STAC filter parameter and sends SQL condition back.
330
330
 
331
- >>> parse_filter('')
331
+ >>> parse_collection_filter('')
332
332
 
333
- >>> parse_filter("updated >= '2023-12-31'")
333
+ >>> parse_collection_filter("updated >= '2023-12-31'")
334
334
  SQL("(s.updated_at >= '2023-12-31')")
335
- >>> parse_filter("updated >= '2023-12-31' AND created < '2023-10-31'")
335
+ >>> parse_collection_filter("updated >= '2023-12-31' AND created < '2023-10-31'")
336
336
  SQL("((s.updated_at >= '2023-12-31') AND (s.inserted_at < '2023-10-31'))")
337
- >>> parse_filter("status IN ('deleted','ready')") # when we ask for deleted, we should also have hidden collections
337
+ >>> parse_collection_filter("status IN ('deleted','ready')") # when we ask for deleted, we should also have hidden collections
338
338
  SQL("s.status IN ('deleted', 'ready', 'hidden')")
339
- >>> parse_filter("status = 'deleted' OR status = 'ready'")
339
+ >>> parse_collection_filter("status = 'deleted' OR status = 'ready'")
340
340
  SQL("(((s.status = 'deleted') OR (s.status = 'hidden')) OR (s.status = 'ready'))")
341
- >>> parse_filter('invalid = 10') # doctest: +IGNORE_EXCEPTION_DETAIL
341
+ >>> parse_collection_filter('invalid = 10') # doctest: +IGNORE_EXCEPTION_DETAIL
342
342
  Traceback (most recent call last):
343
343
  geovisio.errors.InvalidAPIUsage: Unsupported filter parameter
344
- >>> parse_filter('updated == 10') # doctest: +IGNORE_EXCEPTION_DETAIL
344
+ >>> parse_collection_filter('updated == 10') # doctest: +IGNORE_EXCEPTION_DETAIL
345
345
  Traceback (most recent call last):
346
346
  geovisio.errors.InvalidAPIUsage: Unsupported filter parameter
347
347
  """
348
- if value is not None and len(value) > 0:
349
- try:
350
- filterAst = ecql_parser(value)
351
- altered_ast = _alterFilterAst(filterAst) # type: ignore
352
-
353
- f = to_sql_where(altered_ast, STAC_FIELD_TO_SQL_FILTER).replace('"', "")
354
- return sql.SQL(f) # type: ignore
355
- except:
356
- raise errors.InvalidAPIUsage(_("Unsupported filter parameter"), status_code=400)
357
- else:
358
- return None
348
+ return parse_cql2_filter(value, STAC_FIELD_TO_SQL_FILTER, ast_updater=_alterFilterAst)
359
349
 
360
350
 
361
351
  def parse_picture_heading(heading: Optional[str]) -> Optional[int]:
geovisio/web/stac.py CHANGED
@@ -20,10 +20,11 @@ from geovisio.utils.sequences import (
20
20
  get_collections,
21
21
  CollectionsRequest,
22
22
  STAC_FIELD_MAPPINGS,
23
+ get_dataset_bounds,
23
24
  get_pagination_links,
24
25
  )
25
26
  from geovisio.web.params import (
26
- parse_filter,
27
+ parse_collection_filter,
27
28
  parse_collections_limit,
28
29
  )
29
30
 
@@ -334,12 +335,17 @@ def getUserCatalog(userId, userIdMatchesAccount=False):
334
335
  """
335
336
 
336
337
  collection_request = CollectionsRequest(
337
- sort_by=SortBy(fields=[SortByField(field=STAC_FIELD_MAPPINGS["created"], direction=SQLDirection.ASC)]),
338
+ sort_by=SortBy(
339
+ fields=[
340
+ SortByField(field=STAC_FIELD_MAPPINGS["created"], direction=SQLDirection.ASC),
341
+ SortByField(field=STAC_FIELD_MAPPINGS["id"], direction=SQLDirection.ASC),
342
+ ]
343
+ ),
338
344
  user_id=userId,
339
345
  userOwnsAllCollections=userIdMatchesAccount,
340
346
  )
341
347
  collection_request.limit = parse_collections_limit(request.args.get("limit"))
342
- collection_request.pagination_filter = parse_filter(request.args.get("page"))
348
+ collection_request.pagination_filter = parse_collection_filter(request.args.get("page"))
343
349
 
344
350
  userName = None
345
351
  meta_collection = None
@@ -350,12 +356,14 @@ def getUserCatalog(userId, userIdMatchesAccount=False):
350
356
  raise errors.InvalidAPIUsage(_("Impossible to find user %(u)s", u=userId))
351
357
  userName = userName["name"]
352
358
 
353
- meta_collection = cursor.execute(
354
- SQL("SELECT MIN(inserted_at) AS min_order, MAX(inserted_at) AS max_order FROM sequences s WHERE account_id = %(account)s"),
355
- params={"account": userId},
356
- ).fetchone()
359
+ datasetBounds = get_dataset_bounds(
360
+ cursor.connection,
361
+ collection_request.sort_by,
362
+ additional_filters=SQL("s.account_id = %(account)s"),
363
+ additional_filters_params={"account": userId},
364
+ )
357
365
 
358
- if not meta_collection or meta_collection["min_order"] is None:
366
+ if datasetBounds is None:
359
367
  # No data found, trying to give the most meaningfull error message
360
368
  raise errors.InvalidAPIUsage(_("No data loaded for user %(u)s", u=userId), 404)
361
369
 
@@ -388,10 +396,9 @@ def getUserCatalog(userId, userIdMatchesAccount=False):
388
396
  pagination_links = get_pagination_links(
389
397
  route="stac.getUserCatalog",
390
398
  routeArgs={"userId": str(userId), "limit": collection_request.limit},
391
- field=collection_request.sort_by.fields[0].field.stac,
392
- direction=collection_request.sort_by.fields[0].direction,
393
- datasetBounds=Bounds(min=meta_collection["min_order"], max=meta_collection["max_order"]),
394
- dataBounds=db_collections.query_first_order_bounds,
399
+ sortBy=collection_request.sort_by,
400
+ datasetBounds=datasetBounds,
401
+ dataBounds=db_collections.query_bounds,
395
402
  additional_filters=None,
396
403
  )
397
404