geovisio 2.8.1__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 +1 -1
- geovisio/config_app.py +11 -0
- geovisio/translations/fr/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/fr/LC_MESSAGES/messages.po +12 -5
- geovisio/translations/nl/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/nl/LC_MESSAGES/messages.po +131 -25
- 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/model_query.py +2 -2
- 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 +2 -11
- geovisio/web/annotations.py +205 -9
- geovisio/web/collections.py +49 -34
- geovisio/web/configuration.py +2 -1
- geovisio/web/docs.py +52 -2
- 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 +2 -9
- geovisio/workers/runner_pictures.py +71 -10
- {geovisio-2.8.1.dist-info → geovisio-2.9.0.dist-info}/METADATA +21 -20
- {geovisio-2.8.1.dist-info → geovisio-2.9.0.dist-info}/RECORD +35 -29
- {geovisio-2.8.1.dist-info → geovisio-2.9.0.dist-info}/WHEEL +1 -1
- {geovisio-2.8.1.dist-info → geovisio-2.9.0.dist-info}/licenses/LICENSE +0 -0
geovisio/web/annotations.py
CHANGED
|
@@ -1,17 +1,213 @@
|
|
|
1
|
-
from
|
|
2
|
-
from
|
|
3
|
-
from
|
|
4
|
-
from geovisio.utils.
|
|
1
|
+
from typing import List, Optional
|
|
2
|
+
from geovisio.utils import auth, db
|
|
3
|
+
from geovisio.utils.annotations import AnnotationCreationParameter, creation_annotation, get_annotation, update_annotation
|
|
4
|
+
from geovisio.utils.tags import SemanticTagUpdate
|
|
5
5
|
from geovisio.web.utils import accountIdOrDefault
|
|
6
|
-
from psycopg.types.json import Jsonb
|
|
7
|
-
from geovisio.utils import db
|
|
8
6
|
from geovisio.utils.params import validation_error
|
|
9
7
|
from geovisio import errors
|
|
10
|
-
from pydantic import BaseModel,
|
|
8
|
+
from pydantic import BaseModel, ValidationError
|
|
11
9
|
from uuid import UUID
|
|
12
|
-
from
|
|
13
|
-
from flask import Blueprint, request, current_app
|
|
10
|
+
from flask import Blueprint, current_app, request, url_for
|
|
14
11
|
from flask_babel import gettext as _
|
|
15
12
|
|
|
16
13
|
|
|
17
14
|
bp = Blueprint("annotations", __name__, url_prefix="/api")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@bp.route("/collections/<uuid:collectionId>/items/<uuid:itemId>/annotations", methods=["POST"])
|
|
18
|
+
@auth.login_required()
|
|
19
|
+
def postAnnotation(collectionId, itemId, account):
|
|
20
|
+
"""Create an annotation on a picture.
|
|
21
|
+
|
|
22
|
+
The geometry can be provided as a bounding box (a list of 4 integers, minx, miny, maxx, maxy) or as a geojson geometry.
|
|
23
|
+
All coordinates must be in pixel, starting from the top left of the picture.
|
|
24
|
+
|
|
25
|
+
If an annotation already exists on the picture with the same shape, it will be used.
|
|
26
|
+
---
|
|
27
|
+
tags:
|
|
28
|
+
- Editing
|
|
29
|
+
- Semantics
|
|
30
|
+
parameters:
|
|
31
|
+
- name: collectionId
|
|
32
|
+
in: path
|
|
33
|
+
description: ID of collection to retrieve
|
|
34
|
+
required: true
|
|
35
|
+
schema:
|
|
36
|
+
type: string
|
|
37
|
+
- name: itemId
|
|
38
|
+
in: path
|
|
39
|
+
description: ID of item to retrieve
|
|
40
|
+
required: true
|
|
41
|
+
schema:
|
|
42
|
+
type: string
|
|
43
|
+
requestBody:
|
|
44
|
+
content:
|
|
45
|
+
application/json:
|
|
46
|
+
schema:
|
|
47
|
+
$ref: '#/components/schemas/GeoVisioPostAnnotation'
|
|
48
|
+
security:
|
|
49
|
+
- bearerToken: []
|
|
50
|
+
- cookieAuth: []
|
|
51
|
+
responses:
|
|
52
|
+
200:
|
|
53
|
+
description: the annotation metadata
|
|
54
|
+
content:
|
|
55
|
+
application/json:
|
|
56
|
+
schema:
|
|
57
|
+
$ref: '#/components/schemas/GeoVisioAnnotation'
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
account_id = UUID(accountIdOrDefault(account))
|
|
61
|
+
|
|
62
|
+
pic = db.fetchone(
|
|
63
|
+
current_app,
|
|
64
|
+
"SELECT 1 FROM sequences_pictures WHERE seq_id = %(seq)s AND pic_id = %(pic)s",
|
|
65
|
+
{"seq": collectionId, "pic": itemId},
|
|
66
|
+
)
|
|
67
|
+
if not pic:
|
|
68
|
+
raise errors.InvalidAPIUsage(_("Picture %(p)s wasn't found in database", p=itemId), status_code=404)
|
|
69
|
+
|
|
70
|
+
if request.is_json and request.json is not None:
|
|
71
|
+
try:
|
|
72
|
+
params = AnnotationCreationParameter(**request.json, account_id=account_id, picture_id=itemId)
|
|
73
|
+
except ValidationError as ve:
|
|
74
|
+
raise errors.InvalidAPIUsage(_("Impossible to create an annotation"), payload=validation_error(ve))
|
|
75
|
+
else:
|
|
76
|
+
raise errors.InvalidAPIUsage(_("Parameter for creating an annotation should be a valid JSON"), status_code=415)
|
|
77
|
+
|
|
78
|
+
annotation = creation_annotation(params)
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
annotation.model_dump_json(exclude_none=True),
|
|
82
|
+
200,
|
|
83
|
+
{
|
|
84
|
+
"Content-Type": "application/json",
|
|
85
|
+
"Access-Control-Expose-Headers": "Location", # Needed for allowing web browsers access Location header
|
|
86
|
+
"Location": url_for(
|
|
87
|
+
"annotations.getAnnotation", _external=True, annotationId=annotation.id, collectionId=collectionId, itemId=itemId
|
|
88
|
+
),
|
|
89
|
+
},
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@bp.route("/collections/<uuid:collectionId>/items/<uuid:itemId>/annotations/<uuid:annotationId>", methods=["GET"])
|
|
94
|
+
def getAnnotation(collectionId, itemId, annotationId):
|
|
95
|
+
"""Get an annotation
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
tags:
|
|
99
|
+
- Semantics
|
|
100
|
+
parameters:
|
|
101
|
+
- name: collectionId
|
|
102
|
+
in: path
|
|
103
|
+
description: ID of collection
|
|
104
|
+
required: true
|
|
105
|
+
schema:
|
|
106
|
+
type: string
|
|
107
|
+
- name: itemId
|
|
108
|
+
in: path
|
|
109
|
+
description: ID of item
|
|
110
|
+
required: true
|
|
111
|
+
schema:
|
|
112
|
+
type: string
|
|
113
|
+
- name: annotationId
|
|
114
|
+
in: path
|
|
115
|
+
description: ID of annotation
|
|
116
|
+
required: true
|
|
117
|
+
schema:
|
|
118
|
+
type: string
|
|
119
|
+
security:
|
|
120
|
+
- bearerToken: []
|
|
121
|
+
- cookieAuth: []
|
|
122
|
+
responses:
|
|
123
|
+
200:
|
|
124
|
+
description: the annotation metadata
|
|
125
|
+
content:
|
|
126
|
+
application/json:
|
|
127
|
+
schema:
|
|
128
|
+
$ref: '#/components/schemas/GeoVisioAnnotation'
|
|
129
|
+
"""
|
|
130
|
+
with db.conn(current_app) as conn:
|
|
131
|
+
|
|
132
|
+
annotation = get_annotation(conn, annotationId)
|
|
133
|
+
if not annotation or annotation.picture_id != itemId:
|
|
134
|
+
raise errors.InvalidAPIUsage(_("Annotation %(p)s not found", p=itemId), status_code=404)
|
|
135
|
+
|
|
136
|
+
return annotation.model_dump_json(exclude_none=True), 200, {"Content-Type": "application/json"}
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class AnnotationPatchParameter(BaseModel):
|
|
140
|
+
"""Parameters used to update an annotation"""
|
|
141
|
+
|
|
142
|
+
semantics: Optional[List[SemanticTagUpdate]] = None
|
|
143
|
+
"""Tags to update on the annotation. By default each tag will be added to the annotation's tags, but you can change this behavior by setting the `action` parameter to `delete`.
|
|
144
|
+
|
|
145
|
+
If you want to replace a tag, you need to first delete it, then add it again.
|
|
146
|
+
|
|
147
|
+
Like:
|
|
148
|
+
[
|
|
149
|
+
{"key": "some_key", "value": "some_value", "action": "delete"},
|
|
150
|
+
{"key": "some_key", "value": "some_new_value"}
|
|
151
|
+
]
|
|
152
|
+
"""
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
@bp.route("/collections/<uuid:collectionId>/items/<uuid:itemId>/annotations/<uuid:annotationId>", methods=["PATCH"])
|
|
156
|
+
@auth.login_required()
|
|
157
|
+
def patchAnnotation(collectionId, itemId, annotationId, account):
|
|
158
|
+
"""Patch an annotation
|
|
159
|
+
|
|
160
|
+
Note that if the annotation has no associated tags anymore, it will be deleted.
|
|
161
|
+
---
|
|
162
|
+
tags:
|
|
163
|
+
- Semantics
|
|
164
|
+
parameters:
|
|
165
|
+
- name: collectionId
|
|
166
|
+
in: path
|
|
167
|
+
description: ID of collection
|
|
168
|
+
required: true
|
|
169
|
+
schema:
|
|
170
|
+
type: string
|
|
171
|
+
- name: itemId
|
|
172
|
+
in: path
|
|
173
|
+
description: ID of item
|
|
174
|
+
required: true
|
|
175
|
+
schema:
|
|
176
|
+
type: string
|
|
177
|
+
- name: annotationId
|
|
178
|
+
in: path
|
|
179
|
+
description: ID of annotation
|
|
180
|
+
required: true
|
|
181
|
+
schema:
|
|
182
|
+
type: string
|
|
183
|
+
security:
|
|
184
|
+
- bearerToken: []
|
|
185
|
+
- cookieAuth: []
|
|
186
|
+
responses:
|
|
187
|
+
200:
|
|
188
|
+
description: the annotation metadata
|
|
189
|
+
content:
|
|
190
|
+
application/json:
|
|
191
|
+
schema:
|
|
192
|
+
$ref: '#/components/schemas/GeoVisioAnnotation'
|
|
193
|
+
204:
|
|
194
|
+
description: The annotation was empty, it has been correctly deleted
|
|
195
|
+
"""
|
|
196
|
+
if request.is_json and request.json is not None:
|
|
197
|
+
try:
|
|
198
|
+
params = AnnotationPatchParameter(**request.json)
|
|
199
|
+
except ValidationError as ve:
|
|
200
|
+
raise errors.InvalidAPIUsage(_("Impossible to patch annotation, invalid parameters"), payload=validation_error(ve))
|
|
201
|
+
else:
|
|
202
|
+
raise errors.InvalidAPIUsage(_("Parameter for updating an annotation should be a valid JSON"), status_code=415)
|
|
203
|
+
|
|
204
|
+
with db.conn(current_app) as conn:
|
|
205
|
+
|
|
206
|
+
annotation = get_annotation(conn, annotationId)
|
|
207
|
+
if not annotation or annotation.picture_id != itemId:
|
|
208
|
+
raise errors.InvalidAPIUsage(_("Annotation %(p)s not found", p=itemId), status_code=404)
|
|
209
|
+
|
|
210
|
+
a = update_annotation(annotation, params.semantics, account.id)
|
|
211
|
+
if a is None:
|
|
212
|
+
return "", 204
|
|
213
|
+
return a.model_dump_json(exclude_none=True), 200, {"Content-Type": "application/json"}
|
geovisio/web/collections.py
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
|
+
from copy import deepcopy
|
|
1
2
|
from enum import Enum
|
|
2
3
|
from attr import dataclass
|
|
3
4
|
from geovisio import errors, utils, db
|
|
4
5
|
from geovisio.utils import auth, sequences
|
|
5
6
|
from geovisio.utils.params import validation_error
|
|
6
|
-
from geovisio.utils.semantics import
|
|
7
|
+
from geovisio.utils.semantics import Entity, EntityType, update_tags
|
|
8
|
+
from geovisio.utils.tags import SemanticTagUpdate
|
|
7
9
|
from geovisio.web.params import (
|
|
8
10
|
parse_datetime,
|
|
9
11
|
parse_datetime_interval,
|
|
10
12
|
parse_bbox,
|
|
11
|
-
|
|
13
|
+
parse_collection_filter,
|
|
12
14
|
parse_sortby,
|
|
13
15
|
parse_collections_limit,
|
|
14
16
|
)
|
|
@@ -16,6 +18,7 @@ from geovisio.utils.sequences import (
|
|
|
16
18
|
STAC_FIELD_MAPPINGS,
|
|
17
19
|
CollectionsRequest,
|
|
18
20
|
get_collections,
|
|
21
|
+
get_dataset_bounds,
|
|
19
22
|
)
|
|
20
23
|
from geovisio.utils.fields import SortBy, SortByField, SQLDirection, Bounds, BBox
|
|
21
24
|
from geovisio.web.rss import dbSequencesToGeoRSS
|
|
@@ -248,12 +251,18 @@ def getAllCollections():
|
|
|
248
251
|
if not sortBy:
|
|
249
252
|
direction = SQLDirection.DESC if format == "rss" else SQLDirection.ASC
|
|
250
253
|
sortBy = SortBy(fields=[SortByField(field=STAC_FIELD_MAPPINGS["created"], direction=direction)])
|
|
254
|
+
# we always add the creation date fields in the sort list (after the selected ones), this will we'll get the `created` bounds of the dataset
|
|
255
|
+
# we'll also get
|
|
256
|
+
if not any(s.field == STAC_FIELD_MAPPINGS["created"] for s in sortBy.fields):
|
|
257
|
+
sortBy.fields.append(SortByField(field=STAC_FIELD_MAPPINGS["created"], direction=SQLDirection.ASC))
|
|
258
|
+
if not any(s.field == STAC_FIELD_MAPPINGS["id"] for s in sortBy.fields):
|
|
259
|
+
sortBy.fields.append(SortByField(field=STAC_FIELD_MAPPINGS["id"], direction=SQLDirection.ASC))
|
|
251
260
|
|
|
252
261
|
collection_request = CollectionsRequest(sort_by=sortBy)
|
|
253
262
|
|
|
254
263
|
# Filter parameter
|
|
255
|
-
collection_request.user_filter =
|
|
256
|
-
collection_request.pagination_filter =
|
|
264
|
+
collection_request.user_filter = parse_collection_filter(request.args.get("filter"))
|
|
265
|
+
collection_request.pagination_filter = parse_collection_filter(request.args.get("page"))
|
|
257
266
|
|
|
258
267
|
# Limit parameter
|
|
259
268
|
collection_request.limit = parse_collections_limit(request.args.get("limit"))
|
|
@@ -298,14 +307,14 @@ def getAllCollections():
|
|
|
298
307
|
},
|
|
299
308
|
]
|
|
300
309
|
|
|
301
|
-
with db.
|
|
302
|
-
|
|
303
|
-
if
|
|
310
|
+
with db.conn(current_app) as conn:
|
|
311
|
+
datasetBounds = get_dataset_bounds(conn, collection_request.sort_by, additional_filters=collection_request.user_filter)
|
|
312
|
+
if datasetBounds is None:
|
|
304
313
|
return ({"collections": [], "links": links}, 200, {"Content-Type": "application/json"})
|
|
305
|
-
|
|
306
|
-
if collection_request.created_after and collection_request.created_after > datasetBounds.
|
|
314
|
+
creation_date_index = collection_request.sort_by.get_field_index("created")
|
|
315
|
+
if collection_request.created_after and collection_request.created_after > datasetBounds.last[creation_date_index]:
|
|
307
316
|
raise errors.InvalidAPIUsage(_("There is no collection created after %(d)s", d=collection_request.created_after))
|
|
308
|
-
if collection_request.created_before and collection_request.created_before < datasetBounds.
|
|
317
|
+
if collection_request.created_before and collection_request.created_before < datasetBounds.first[creation_date_index]:
|
|
309
318
|
raise errors.InvalidAPIUsage(_("There is no collection created before %(d)s", d=collection_request.created_before))
|
|
310
319
|
|
|
311
320
|
db_collections = get_collections(collection_request)
|
|
@@ -322,10 +331,9 @@ def getAllCollections():
|
|
|
322
331
|
pagination_links = sequences.get_pagination_links(
|
|
323
332
|
route="stac_collections.getAllCollections",
|
|
324
333
|
routeArgs={"limit": collection_request.limit},
|
|
325
|
-
|
|
326
|
-
direction=sortBy.fields[0].direction,
|
|
334
|
+
sortBy=sortBy,
|
|
327
335
|
datasetBounds=datasetBounds,
|
|
328
|
-
dataBounds=db_collections.
|
|
336
|
+
dataBounds=db_collections.query_bounds,
|
|
329
337
|
additional_filters=additional_filters,
|
|
330
338
|
)
|
|
331
339
|
|
|
@@ -402,7 +410,7 @@ def getCollection(collectionId):
|
|
|
402
410
|
SELECT sequence_id, json_agg(json_strip_nulls(json_build_object(
|
|
403
411
|
'key', key,
|
|
404
412
|
'value', value
|
|
405
|
-
))) AS semantics
|
|
413
|
+
)) ORDER BY key, value) AS semantics
|
|
406
414
|
FROM sequences_semantics
|
|
407
415
|
GROUP BY sequence_id
|
|
408
416
|
) t ON t.sequence_id = s.id
|
|
@@ -607,7 +615,7 @@ If unset, sort order is unchanged."""
|
|
|
607
615
|
if relHeading < -180 or relHeading > 180:
|
|
608
616
|
raise ValueError()
|
|
609
617
|
return relHeading
|
|
610
|
-
except ValueError:
|
|
618
|
+
except (ValueError, TypeError):
|
|
611
619
|
raise errors.InvalidAPIUsage(
|
|
612
620
|
_("Relative heading is not valid, should be an integer in degrees from -180 to 180"), status_code=400
|
|
613
621
|
)
|
|
@@ -629,7 +637,7 @@ def patchCollection(collectionId, account):
|
|
|
629
637
|
---
|
|
630
638
|
tags:
|
|
631
639
|
- Editing
|
|
632
|
-
-
|
|
640
|
+
- Semantics
|
|
633
641
|
parameters:
|
|
634
642
|
- name: collectionId
|
|
635
643
|
in: path
|
|
@@ -749,13 +757,7 @@ def patchCollection(collectionId, account):
|
|
|
749
757
|
sqlUpdates.append(SQL("last_account_to_edit = %(account)s"))
|
|
750
758
|
|
|
751
759
|
cursor.execute(
|
|
752
|
-
SQL(
|
|
753
|
-
"""
|
|
754
|
-
UPDATE sequences
|
|
755
|
-
SET {updates}
|
|
756
|
-
WHERE id = %(id)s
|
|
757
|
-
"""
|
|
758
|
-
).format(updates=SQL(", ").join(sqlUpdates)),
|
|
760
|
+
SQL("UPDATE sequences SET {updates} WHERE id = %(id)s").format(updates=SQL(", ").join(sqlUpdates)),
|
|
759
761
|
sqlParams,
|
|
760
762
|
)
|
|
761
763
|
|
|
@@ -924,7 +926,12 @@ def send_collections_as_csv(collection_request: CollectionsRequest):
|
|
|
924
926
|
raise errors.InvalidAPIUsage(_("CSV export does not support pagination"), status_code=400)
|
|
925
927
|
if collection_request.filters():
|
|
926
928
|
raise errors.InvalidAPIUsage(_("CSV export does not support filters"), status_code=400)
|
|
927
|
-
if collection_request.sort_by != SortBy(
|
|
929
|
+
if collection_request.sort_by != SortBy(
|
|
930
|
+
fields=[
|
|
931
|
+
SortByField(field=STAC_FIELD_MAPPINGS["created"], direction=SQLDirection.DESC),
|
|
932
|
+
SortByField(field=STAC_FIELD_MAPPINGS["id"], direction=SQLDirection.ASC),
|
|
933
|
+
]
|
|
934
|
+
):
|
|
928
935
|
raise errors.InvalidAPIUsage(_("CSV export does not support sorting by anything but creation date"), status_code=400)
|
|
929
936
|
|
|
930
937
|
def generate_csv():
|
|
@@ -957,7 +964,7 @@ SELECT
|
|
|
957
964
|
s.computed_gps_accuracy AS computed_gps_accuracy
|
|
958
965
|
FROM sequences s
|
|
959
966
|
WHERE {filter}
|
|
960
|
-
ORDER BY s.inserted_at DESC
|
|
967
|
+
ORDER BY s.inserted_at DESC, id ASC
|
|
961
968
|
) TO STDOUT CSV HEADER"""
|
|
962
969
|
).format(filter=SQL(" AND ").join(filters)),
|
|
963
970
|
params,
|
|
@@ -966,7 +973,7 @@ ORDER BY s.inserted_at DESC
|
|
|
966
973
|
for a in copy:
|
|
967
974
|
yield bytes(a)
|
|
968
975
|
|
|
969
|
-
return stream_with_context(generate_csv()), {"Content-Disposition": "attachment"}
|
|
976
|
+
return stream_with_context(generate_csv()), {"Content-Type": "text/csv", "Content-Disposition": "attachment"}
|
|
970
977
|
|
|
971
978
|
|
|
972
979
|
@bp.route("/users/<uuid:userId>/collection")
|
|
@@ -1033,13 +1040,17 @@ def getUserCollection(userId, userIdMatchesAccount=False):
|
|
|
1033
1040
|
if not sortBy:
|
|
1034
1041
|
sortBy = SortBy(fields=[SortByField(field=STAC_FIELD_MAPPINGS["created"], direction=SQLDirection.DESC)])
|
|
1035
1042
|
|
|
1043
|
+
if not any(s.field == STAC_FIELD_MAPPINGS["created"] for s in sortBy.fields):
|
|
1044
|
+
sortBy.fields.append(SortByField(field=STAC_FIELD_MAPPINGS["created"], direction=SQLDirection.ASC))
|
|
1045
|
+
if not any(s.field == STAC_FIELD_MAPPINGS["id"] for s in sortBy.fields):
|
|
1046
|
+
sortBy.fields.append(SortByField(field=STAC_FIELD_MAPPINGS["id"], direction=SQLDirection.ASC))
|
|
1036
1047
|
collection_request = CollectionsRequest(sort_by=sortBy, userOwnsAllCollections=userIdMatchesAccount)
|
|
1037
1048
|
|
|
1038
1049
|
# Filter parameter
|
|
1039
|
-
collection_request.user_filter =
|
|
1050
|
+
collection_request.user_filter = parse_collection_filter(request.args.get("filter"))
|
|
1040
1051
|
|
|
1041
1052
|
# Filters added by the pagination
|
|
1042
|
-
collection_request.pagination_filter =
|
|
1053
|
+
collection_request.pagination_filter = parse_collection_filter(request.args.get("page"))
|
|
1043
1054
|
|
|
1044
1055
|
# Limit parameter
|
|
1045
1056
|
# if not specified, the default with CSV it 1000. if there are more, the paginated API should be used
|
|
@@ -1095,8 +1106,6 @@ def getUserCollection(userId, userIdMatchesAccount=False):
|
|
|
1095
1106
|
MAX(LEAST(90, ST_YMax(s.bbox))) AS maxy,
|
|
1096
1107
|
MIN(s.inserted_at) AS created,
|
|
1097
1108
|
MAX(s.updated_at) AS updated,
|
|
1098
|
-
MIN({order_column}) AS min_order,
|
|
1099
|
-
MAX({order_column}) AS max_order,
|
|
1100
1109
|
ROUND(SUM(ST_Length(s.geom::geography))) / 1000 AS length_km
|
|
1101
1110
|
FROM sequences s
|
|
1102
1111
|
WHERE {filter}
|
|
@@ -1115,6 +1124,13 @@ def getUserCollection(userId, userIdMatchesAccount=False):
|
|
|
1115
1124
|
else:
|
|
1116
1125
|
raise errors.InvalidAPIUsage(_("No matching sequences found"), 404)
|
|
1117
1126
|
|
|
1127
|
+
datasetBounds = get_dataset_bounds(
|
|
1128
|
+
cursor.connection,
|
|
1129
|
+
collection_request.sort_by,
|
|
1130
|
+
additional_filters=SQL(" AND ").join(meta_filter),
|
|
1131
|
+
additional_filters_params={"account": userId},
|
|
1132
|
+
)
|
|
1133
|
+
|
|
1118
1134
|
collections = get_collections(collection_request)
|
|
1119
1135
|
|
|
1120
1136
|
sequences_links = [
|
|
@@ -1164,10 +1180,9 @@ def getUserCollection(userId, userIdMatchesAccount=False):
|
|
|
1164
1180
|
pagination_links = sequences.get_pagination_links(
|
|
1165
1181
|
route="stac_collections.getUserCollection",
|
|
1166
1182
|
routeArgs={"userId": str(userId), "limit": collection_request.limit},
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
dataBounds=collections.query_first_order_bounds,
|
|
1183
|
+
sortBy=sortBy,
|
|
1184
|
+
datasetBounds=datasetBounds,
|
|
1185
|
+
dataBounds=collections.query_bounds,
|
|
1171
1186
|
additional_filters=additional_filters,
|
|
1172
1187
|
)
|
|
1173
1188
|
|
geovisio/web/configuration.py
CHANGED
|
@@ -29,7 +29,7 @@ def configuration():
|
|
|
29
29
|
"name": _get_translated(apiSum.name, userLang),
|
|
30
30
|
"description": _get_translated(apiSum.description, userLang),
|
|
31
31
|
"geo_coverage": _get_translated(apiSum.geo_coverage, userLang),
|
|
32
|
-
"logo": apiSum.logo,
|
|
32
|
+
"logo": str(apiSum.logo),
|
|
33
33
|
"color": str(apiSum.color),
|
|
34
34
|
"email": apiSum.email,
|
|
35
35
|
"auth": _auth_configuration(),
|
|
@@ -53,6 +53,7 @@ def _auth_configuration():
|
|
|
53
53
|
return {
|
|
54
54
|
"enabled": True,
|
|
55
55
|
"user_profile": {"url": auth.oauth_provider.user_profile_page_url()},
|
|
56
|
+
"registration_is_open": flask.current_app.config["API_REGISTRATION_IS_OPEN"],
|
|
56
57
|
"enforce_tos_acceptance": flask.current_app.config["API_ENFORCE_TOS_ACCEPTANCE"],
|
|
57
58
|
}
|
|
58
59
|
|
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": {
|
|
@@ -742,6 +755,10 @@ Available properties are:
|
|
|
742
755
|
"properties": {
|
|
743
756
|
"user_profile": {"type": "object", "properties": {"url": {"type": "string"}}},
|
|
744
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
|
+
},
|
|
745
762
|
"enforce_tos_acceptance": {"type": "boolean"},
|
|
746
763
|
},
|
|
747
764
|
"required": ["enabled"],
|
|
@@ -821,6 +838,12 @@ Available properties are:
|
|
|
821
838
|
"payload": {"type": "object", "description": "The error payload"},
|
|
822
839
|
},
|
|
823
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
|
+
),
|
|
824
847
|
},
|
|
825
848
|
"parameters": {
|
|
826
849
|
"STAC_bbox": {"$ref": f"https://api.stacspec.org/v{utils.STAC_VERSION}/item-search/openapi.yaml#/components/parameters/bbox"},
|
|
@@ -916,6 +939,33 @@ Note that this parameter is not taken in account for 360° pictures, as by defin
|
|
|
916
939
|
"required": False,
|
|
917
940
|
"schema": {"type": "integer", "minimum": 2, "maximum": 180, "default": 30},
|
|
918
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
|
+
},
|
|
919
969
|
"GeoVisioReports_filter": {
|
|
920
970
|
"name": "filter",
|
|
921
971
|
"in": "query",
|