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.
- geovisio/__init__.py +16 -3
- geovisio/config_app.py +11 -1
- geovisio/translations/br/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/br/LC_MESSAGES/messages.po +762 -0
- geovisio/translations/da/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/da/LC_MESSAGES/messages.po +10 -1
- geovisio/translations/de/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/de/LC_MESSAGES/messages.po +10 -1
- geovisio/translations/en/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/en/LC_MESSAGES/messages.po +9 -7
- geovisio/translations/eo/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/eo/LC_MESSAGES/messages.po +67 -1
- geovisio/translations/es/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/es/LC_MESSAGES/messages.po +4 -3
- geovisio/translations/fr/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/fr/LC_MESSAGES/messages.po +37 -4
- geovisio/translations/hu/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/hu/LC_MESSAGES/messages.po +4 -3
- geovisio/translations/it/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/it/LC_MESSAGES/messages.po +10 -1
- geovisio/translations/ja/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/ja/LC_MESSAGES/messages.po +242 -154
- geovisio/translations/nl/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/nl/LC_MESSAGES/messages.po +131 -25
- geovisio/translations/pl/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/pl/LC_MESSAGES/messages.po +4 -3
- geovisio/translations/sv/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/sv/LC_MESSAGES/messages.po +822 -0
- geovisio/utils/annotations.py +186 -0
- geovisio/utils/cql2.py +134 -0
- geovisio/utils/db.py +7 -0
- geovisio/utils/fields.py +24 -7
- geovisio/utils/loggers.py +14 -0
- geovisio/utils/model_query.py +2 -2
- geovisio/utils/params.py +7 -4
- geovisio/utils/pic_shape.py +63 -0
- geovisio/utils/pictures.py +54 -12
- geovisio/utils/reports.py +10 -17
- geovisio/utils/semantics.py +165 -55
- geovisio/utils/sentry.py +0 -1
- geovisio/utils/sequences.py +141 -60
- geovisio/utils/tags.py +31 -0
- geovisio/utils/upload_set.py +26 -21
- geovisio/utils/website.py +3 -0
- geovisio/web/annotations.py +205 -9
- geovisio/web/auth.py +3 -2
- geovisio/web/collections.py +49 -34
- geovisio/web/configuration.py +2 -1
- geovisio/web/docs.py +55 -16
- geovisio/web/items.py +55 -54
- geovisio/web/map.py +25 -13
- geovisio/web/params.py +11 -21
- geovisio/web/stac.py +19 -12
- geovisio/web/upload_set.py +92 -11
- geovisio/web/users.py +31 -4
- geovisio/workers/runner_pictures.py +71 -10
- {geovisio-2.8.0.dist-info → geovisio-2.9.0.dist-info}/METADATA +24 -22
- geovisio-2.9.0.dist-info/RECORD +98 -0
- {geovisio-2.8.0.dist-info → geovisio-2.9.0.dist-info}/WHEEL +1 -1
- geovisio-2.8.0.dist-info/RECORD +0 -89
- {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
|
|
2
|
-
from geovisio.utils import
|
|
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
|
-
"
|
|
652
|
-
|
|
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
|
|
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":
|
|
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
|
-
|
|
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
|
|
467
|
-
|
|
468
|
-
|
|
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.
|
|
495
|
-
has_item_before = bounds.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
543
|
+
if seqMeta["max_rank"] > bounds.last:
|
|
547
544
|
lastPageRank = seqMeta["max_rank"] - limit
|
|
548
|
-
if lastPageRank < bounds.
|
|
549
|
-
lastPageRank = bounds.
|
|
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
|
-
|
|
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
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
-
|
|
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
|
|
356
|
-
|
|
357
|
-
|
|
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
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
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
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
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.
|
|
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.
|
|
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
|
|
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
|
-
>>>
|
|
331
|
+
>>> parse_collection_filter('')
|
|
332
332
|
|
|
333
|
-
>>>
|
|
333
|
+
>>> parse_collection_filter("updated >= '2023-12-31'")
|
|
334
334
|
SQL("(s.updated_at >= '2023-12-31')")
|
|
335
|
-
>>>
|
|
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
|
-
>>>
|
|
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
|
-
>>>
|
|
339
|
+
>>> parse_collection_filter("status = 'deleted' OR status = 'ready'")
|
|
340
340
|
SQL("(((s.status = 'deleted') OR (s.status = 'hidden')) OR (s.status = 'ready'))")
|
|
341
|
-
>>>
|
|
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
|
-
>>>
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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 =
|
|
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
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
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
|
|
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
|
-
|
|
392
|
-
|
|
393
|
-
|
|
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
|
|