geovisio 2.9.0__py3-none-any.whl → 2.10.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 +6 -1
- geovisio/config_app.py +5 -5
- geovisio/translations/ar/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/ar/LC_MESSAGES/messages.po +818 -0
- geovisio/translations/br/LC_MESSAGES/messages.po +1 -1
- geovisio/translations/da/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/da/LC_MESSAGES/messages.po +4 -3
- geovisio/translations/de/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/de/LC_MESSAGES/messages.po +55 -2
- geovisio/translations/el/LC_MESSAGES/messages.po +1 -1
- geovisio/translations/en/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/en/LC_MESSAGES/messages.po +193 -139
- geovisio/translations/eo/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/eo/LC_MESSAGES/messages.po +53 -4
- geovisio/translations/es/LC_MESSAGES/messages.po +1 -1
- geovisio/translations/fi/LC_MESSAGES/messages.po +1 -1
- geovisio/translations/fr/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/fr/LC_MESSAGES/messages.po +91 -3
- geovisio/translations/hu/LC_MESSAGES/messages.po +1 -1
- geovisio/translations/it/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/it/LC_MESSAGES/messages.po +63 -3
- geovisio/translations/ja/LC_MESSAGES/messages.po +1 -1
- geovisio/translations/ko/LC_MESSAGES/messages.po +1 -1
- geovisio/translations/messages.pot +185 -129
- geovisio/translations/nl/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/nl/LC_MESSAGES/messages.po +292 -63
- geovisio/translations/oc/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/oc/LC_MESSAGES/messages.po +818 -0
- geovisio/translations/pl/LC_MESSAGES/messages.po +1 -1
- geovisio/translations/sv/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/sv/LC_MESSAGES/messages.po +4 -3
- geovisio/translations/ti/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/ti/LC_MESSAGES/messages.po +762 -0
- geovisio/translations/zh_Hant/LC_MESSAGES/messages.po +1 -1
- geovisio/utils/annotations.py +14 -17
- geovisio/utils/auth.py +14 -13
- geovisio/utils/cql2.py +2 -2
- geovisio/utils/fields.py +14 -2
- geovisio/utils/items.py +44 -0
- geovisio/utils/model_query.py +2 -2
- geovisio/utils/pic_shape.py +1 -1
- geovisio/utils/pictures.py +111 -18
- geovisio/utils/semantics.py +32 -3
- geovisio/utils/sentry.py +1 -1
- geovisio/utils/sequences.py +51 -34
- geovisio/utils/upload_set.py +285 -198
- geovisio/utils/website.py +1 -1
- geovisio/web/annotations.py +209 -68
- geovisio/web/auth.py +1 -1
- geovisio/web/collections.py +26 -22
- geovisio/web/configuration.py +24 -4
- geovisio/web/docs.py +93 -11
- geovisio/web/items.py +197 -121
- geovisio/web/params.py +44 -31
- geovisio/web/pictures.py +34 -0
- geovisio/web/tokens.py +49 -1
- geovisio/web/upload_set.py +150 -32
- geovisio/web/users.py +4 -4
- geovisio/web/utils.py +2 -2
- geovisio/workers/runner_pictures.py +128 -23
- {geovisio-2.9.0.dist-info → geovisio-2.10.0.dist-info}/METADATA +13 -13
- geovisio-2.10.0.dist-info/RECORD +105 -0
- geovisio-2.9.0.dist-info/RECORD +0 -98
- {geovisio-2.9.0.dist-info → geovisio-2.10.0.dist-info}/WHEEL +0 -0
- {geovisio-2.9.0.dist-info → geovisio-2.10.0.dist-info}/licenses/LICENSE +0 -0
geovisio/web/annotations.py
CHANGED
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
from typing import List, Optional
|
|
2
2
|
from geovisio.utils import auth, db
|
|
3
|
-
from geovisio.utils.annotations import
|
|
3
|
+
from geovisio.utils.annotations import (
|
|
4
|
+
AnnotationCreationParameter,
|
|
5
|
+
creation_annotation,
|
|
6
|
+
get_annotation,
|
|
7
|
+
update_annotation,
|
|
8
|
+
InputAnnotationShape,
|
|
9
|
+
)
|
|
4
10
|
from geovisio.utils.tags import SemanticTagUpdate
|
|
5
11
|
from geovisio.web.utils import accountIdOrDefault
|
|
6
12
|
from geovisio.utils.params import validation_error
|
|
7
13
|
from geovisio import errors
|
|
8
|
-
from pydantic import BaseModel, ValidationError
|
|
14
|
+
from pydantic import BaseModel, ValidationError, Field
|
|
9
15
|
from uuid import UUID
|
|
10
16
|
from flask import Blueprint, current_app, request, url_for
|
|
11
17
|
from flask_babel import gettext as _
|
|
@@ -14,86 +20,145 @@ from flask_babel import gettext as _
|
|
|
14
20
|
bp = Blueprint("annotations", __name__, url_prefix="/api")
|
|
15
21
|
|
|
16
22
|
|
|
17
|
-
|
|
23
|
+
class AnnotationPostParameter(BaseModel):
|
|
24
|
+
shape: InputAnnotationShape
|
|
25
|
+
"""Shape defining the annotation.
|
|
26
|
+
The annotation shape is either a full geojson geometry or only a bounding box (4 floats).
|
|
27
|
+
|
|
28
|
+
The coordinates should be given in pixel, starting from the bottom left of the picture.
|
|
29
|
+
|
|
30
|
+
Note that the API will always output geometry as geojson geometry (thus will transform the bbox into a polygon).
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
semantics: List[SemanticTagUpdate] = Field(default_factory=list)
|
|
34
|
+
"""Semantic tags associated to the annotation"""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@bp.route("/pictures/<uuid:itemId>/annotations", methods=["POST"])
|
|
18
38
|
@auth.login_required()
|
|
19
|
-
def
|
|
39
|
+
def postAnnotationNonStacAlias(itemId, account):
|
|
20
40
|
"""Create an annotation on a picture.
|
|
21
41
|
|
|
22
42
|
The geometry can be provided as a bounding box (a list of 4 integers, minx, miny, maxx, maxy) or as a geojson geometry.
|
|
23
43
|
All coordinates must be in pixel, starting from the top left of the picture.
|
|
24
44
|
|
|
25
45
|
If an annotation already exists on the picture with the same shape, it will be used.
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
46
|
+
|
|
47
|
+
The is an alias to the `/api/collections/<collectionId>/items/<itemId>/annotations` endpoint (but you don't need to know the collection ID here).
|
|
48
|
+
---
|
|
49
|
+
tags:
|
|
50
|
+
- Editing
|
|
51
|
+
- Semantics
|
|
52
|
+
parameters:
|
|
53
|
+
- name: itemId
|
|
54
|
+
in: path
|
|
55
|
+
description: ID of item to retrieve
|
|
56
|
+
required: true
|
|
57
|
+
schema:
|
|
58
|
+
type: string
|
|
59
|
+
requestBody:
|
|
60
|
+
content:
|
|
61
|
+
application/json:
|
|
62
|
+
schema:
|
|
63
|
+
$ref: '#/components/schemas/GeoVisioPostAnnotation'
|
|
64
|
+
security:
|
|
65
|
+
- bearerToken: []
|
|
66
|
+
- cookieAuth: []
|
|
67
|
+
responses:
|
|
68
|
+
200:
|
|
69
|
+
description: the annotation metadata
|
|
44
70
|
content:
|
|
45
71
|
application/json:
|
|
46
72
|
schema:
|
|
47
|
-
$ref: '#/components/schemas/
|
|
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'
|
|
73
|
+
$ref: '#/components/schemas/GeoVisioAnnotation'
|
|
58
74
|
"""
|
|
59
75
|
|
|
60
76
|
account_id = UUID(accountIdOrDefault(account))
|
|
61
77
|
|
|
62
78
|
pic = db.fetchone(
|
|
63
79
|
current_app,
|
|
64
|
-
"SELECT 1 FROM
|
|
65
|
-
{"
|
|
80
|
+
"SELECT 1 FROM pictures WHERE id = %(pic)s",
|
|
81
|
+
{"pic": itemId},
|
|
66
82
|
)
|
|
67
83
|
if not pic:
|
|
68
84
|
raise errors.InvalidAPIUsage(_("Picture %(p)s wasn't found in database", p=itemId), status_code=404)
|
|
69
85
|
|
|
70
86
|
if request.is_json and request.json is not None:
|
|
71
87
|
try:
|
|
72
|
-
|
|
88
|
+
post_params = AnnotationPostParameter(**request.json, account_id=account_id, picture_id=itemId)
|
|
73
89
|
except ValidationError as ve:
|
|
74
90
|
raise errors.InvalidAPIUsage(_("Impossible to create an annotation"), payload=validation_error(ve))
|
|
75
91
|
else:
|
|
76
92
|
raise errors.InvalidAPIUsage(_("Parameter for creating an annotation should be a valid JSON"), status_code=415)
|
|
77
93
|
|
|
78
|
-
|
|
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
|
-
},
|
|
94
|
+
creation_params = AnnotationCreationParameter(
|
|
95
|
+
account_id=account_id, picture_id=itemId, shape=post_params.shape, semantics=post_params.semantics
|
|
90
96
|
)
|
|
91
97
|
|
|
98
|
+
with db.conn(current_app) as conn:
|
|
99
|
+
annotation = creation_annotation(creation_params, conn)
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
annotation.model_dump_json(exclude_none=True),
|
|
103
|
+
200,
|
|
104
|
+
{
|
|
105
|
+
"Content-Type": "application/json",
|
|
106
|
+
"Access-Control-Expose-Headers": "Location", # Needed for allowing web browsers access Location header
|
|
107
|
+
"Location": url_for("annotations.getAnnotationById", _external=True, annotationId=annotation.id),
|
|
108
|
+
},
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@bp.route("/collections/<uuid:collectionId>/items/<uuid:itemId>/annotations", methods=["POST"])
|
|
113
|
+
@auth.login_required()
|
|
114
|
+
def postAnnotation(collectionId, itemId, account):
|
|
115
|
+
"""Create an annotation on a picture.
|
|
116
|
+
|
|
117
|
+
The geometry can be provided as a bounding box (a list of 4 integers, minx, miny, maxx, maxy) or as a geojson geometry.
|
|
118
|
+
All coordinates must be in pixel, starting from the top left of the picture.
|
|
119
|
+
|
|
120
|
+
If an annotation already exists on the picture with the same shape, it will be used.
|
|
121
|
+
---
|
|
122
|
+
tags:
|
|
123
|
+
- Editing
|
|
124
|
+
- Semantics
|
|
125
|
+
parameters:
|
|
126
|
+
- name: collectionId
|
|
127
|
+
in: path
|
|
128
|
+
description: ID of collection to retrieve
|
|
129
|
+
required: true
|
|
130
|
+
schema:
|
|
131
|
+
type: string
|
|
132
|
+
- name: itemId
|
|
133
|
+
in: path
|
|
134
|
+
description: ID of item to retrieve
|
|
135
|
+
required: true
|
|
136
|
+
schema:
|
|
137
|
+
type: string
|
|
138
|
+
requestBody:
|
|
139
|
+
content:
|
|
140
|
+
application/json:
|
|
141
|
+
schema:
|
|
142
|
+
$ref: '#/components/schemas/GeoVisioPostAnnotation'
|
|
143
|
+
security:
|
|
144
|
+
- bearerToken: []
|
|
145
|
+
- cookieAuth: []
|
|
146
|
+
responses:
|
|
147
|
+
200:
|
|
148
|
+
description: the annotation metadata
|
|
149
|
+
content:
|
|
150
|
+
application/json:
|
|
151
|
+
schema:
|
|
152
|
+
$ref: '#/components/schemas/GeoVisioAnnotation'
|
|
153
|
+
"""
|
|
154
|
+
return postAnnotationNonStacAlias(itemId=itemId, account=account)
|
|
155
|
+
|
|
92
156
|
|
|
93
157
|
@bp.route("/collections/<uuid:collectionId>/items/<uuid:itemId>/annotations/<uuid:annotationId>", methods=["GET"])
|
|
94
158
|
def getAnnotation(collectionId, itemId, annotationId):
|
|
95
159
|
"""Get an annotation
|
|
96
160
|
|
|
161
|
+
Note that this is the same route as `/api/annotations/<uuid:annotationId>` but you need to know the picture's and collection's IDs.
|
|
97
162
|
---
|
|
98
163
|
tags:
|
|
99
164
|
- Semantics
|
|
@@ -103,19 +168,19 @@ def getAnnotation(collectionId, itemId, annotationId):
|
|
|
103
168
|
description: ID of collection
|
|
104
169
|
required: true
|
|
105
170
|
schema:
|
|
106
|
-
|
|
171
|
+
type: string
|
|
107
172
|
- name: itemId
|
|
108
173
|
in: path
|
|
109
174
|
description: ID of item
|
|
110
175
|
required: true
|
|
111
176
|
schema:
|
|
112
|
-
|
|
177
|
+
type: string
|
|
113
178
|
- name: annotationId
|
|
114
179
|
in: path
|
|
115
180
|
description: ID of annotation
|
|
116
181
|
required: true
|
|
117
182
|
schema:
|
|
118
|
-
|
|
183
|
+
type: string
|
|
119
184
|
security:
|
|
120
185
|
- bearerToken: []
|
|
121
186
|
- cookieAuth: []
|
|
@@ -136,6 +201,41 @@ def getAnnotation(collectionId, itemId, annotationId):
|
|
|
136
201
|
return annotation.model_dump_json(exclude_none=True), 200, {"Content-Type": "application/json"}
|
|
137
202
|
|
|
138
203
|
|
|
204
|
+
@bp.route("/annotations/<uuid:annotationId>", methods=["GET"])
|
|
205
|
+
def getAnnotationById(annotationId):
|
|
206
|
+
"""Get an annotation.
|
|
207
|
+
|
|
208
|
+
This is the same route as `/api/collections/<uuid:collectionId>/items/<uuid:itemId>/annotations/<uuid:annotationId>` but you don't need to know the picture's and collection's IDs.
|
|
209
|
+
|
|
210
|
+
---
|
|
211
|
+
tags:
|
|
212
|
+
- Semantics
|
|
213
|
+
parameters:
|
|
214
|
+
- name: annotationId
|
|
215
|
+
in: path
|
|
216
|
+
description: ID of annotation
|
|
217
|
+
required: true
|
|
218
|
+
schema:
|
|
219
|
+
type: string
|
|
220
|
+
security:
|
|
221
|
+
- bearerToken: []
|
|
222
|
+
- cookieAuth: []
|
|
223
|
+
responses:
|
|
224
|
+
200:
|
|
225
|
+
description: the annotation metadata
|
|
226
|
+
content:
|
|
227
|
+
application/json:
|
|
228
|
+
schema:
|
|
229
|
+
$ref: '#/components/schemas/GeoVisioAnnotation'
|
|
230
|
+
"""
|
|
231
|
+
with db.conn(current_app) as conn:
|
|
232
|
+
annotation = get_annotation(conn, annotationId)
|
|
233
|
+
if not annotation:
|
|
234
|
+
raise errors.InvalidAPIUsage(_("Annotation %(p)s not found", p=annotationId), status_code=404)
|
|
235
|
+
|
|
236
|
+
return annotation.model_dump_json(exclude_none=True), 200, {"Content-Type": "application/json"}
|
|
237
|
+
|
|
238
|
+
|
|
139
239
|
class AnnotationPatchParameter(BaseModel):
|
|
140
240
|
"""Parameters used to update an annotation"""
|
|
141
241
|
|
|
@@ -152,34 +252,29 @@ class AnnotationPatchParameter(BaseModel):
|
|
|
152
252
|
"""
|
|
153
253
|
|
|
154
254
|
|
|
155
|
-
@bp.route("/
|
|
255
|
+
@bp.route("/annotations/<uuid:annotationId>", methods=["PATCH"])
|
|
156
256
|
@auth.login_required()
|
|
157
|
-
def
|
|
257
|
+
def patchAnnotationNonStacAlias(annotationId, account):
|
|
158
258
|
"""Patch an annotation
|
|
159
259
|
|
|
160
260
|
Note that if the annotation has no associated tags anymore, it will be deleted.
|
|
261
|
+
|
|
262
|
+
Note that is an alias to the `/api/collections/<collectionId>/items/<itemId>/annotations/<annotationId>` endpoint (but you don't need to know the collection/item ID here).
|
|
161
263
|
---
|
|
162
264
|
tags:
|
|
163
265
|
- Semantics
|
|
164
266
|
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
267
|
- name: annotationId
|
|
178
268
|
in: path
|
|
179
269
|
description: ID of annotation
|
|
180
270
|
required: true
|
|
181
271
|
schema:
|
|
182
|
-
|
|
272
|
+
type: string
|
|
273
|
+
requestBody:
|
|
274
|
+
content:
|
|
275
|
+
application/json:
|
|
276
|
+
schema:
|
|
277
|
+
$ref: '#/components/schemas/GeoVisioPatchAnnotation'
|
|
183
278
|
security:
|
|
184
279
|
- bearerToken: []
|
|
185
280
|
- cookieAuth: []
|
|
@@ -204,10 +299,56 @@ def patchAnnotation(collectionId, itemId, annotationId, account):
|
|
|
204
299
|
with db.conn(current_app) as conn:
|
|
205
300
|
|
|
206
301
|
annotation = get_annotation(conn, annotationId)
|
|
207
|
-
if not annotation
|
|
208
|
-
raise errors.InvalidAPIUsage(_("Annotation %(p)s not found", p=
|
|
302
|
+
if not annotation:
|
|
303
|
+
raise errors.InvalidAPIUsage(_("Annotation %(p)s not found", p=annotationId), status_code=404)
|
|
209
304
|
|
|
210
305
|
a = update_annotation(annotation, params.semantics, account.id)
|
|
211
306
|
if a is None:
|
|
212
307
|
return "", 204
|
|
213
308
|
return a.model_dump_json(exclude_none=True), 200, {"Content-Type": "application/json"}
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
@bp.route("/collections/<uuid:collectionId>/items/<uuid:itemId>/annotations/<uuid:annotationId>", methods=["PATCH"])
|
|
312
|
+
@auth.login_required()
|
|
313
|
+
def patchAnnotation(collectionId, itemId, annotationId, account):
|
|
314
|
+
"""Patch an annotation
|
|
315
|
+
|
|
316
|
+
Note that if the annotation has no associated tags anymore, it will be deleted.
|
|
317
|
+
|
|
318
|
+
Note that is the an alias to the `/api/annotations/<annotationId>` endpoint (but you need to know the collection/item ID here).
|
|
319
|
+
---
|
|
320
|
+
tags:
|
|
321
|
+
- Semantics
|
|
322
|
+
parameters:
|
|
323
|
+
- name: collectionId
|
|
324
|
+
in: path
|
|
325
|
+
description: ID of collection
|
|
326
|
+
required: true
|
|
327
|
+
schema:
|
|
328
|
+
type: string
|
|
329
|
+
- name: itemId
|
|
330
|
+
in: path
|
|
331
|
+
description: ID of item
|
|
332
|
+
required: true
|
|
333
|
+
schema:
|
|
334
|
+
type: string
|
|
335
|
+
- name: annotationId
|
|
336
|
+
in: path
|
|
337
|
+
description: ID of annotation
|
|
338
|
+
required: true
|
|
339
|
+
schema:
|
|
340
|
+
type: string
|
|
341
|
+
security:
|
|
342
|
+
- bearerToken: []
|
|
343
|
+
- cookieAuth: []
|
|
344
|
+
responses:
|
|
345
|
+
200:
|
|
346
|
+
description: the annotation metadata
|
|
347
|
+
content:
|
|
348
|
+
application/json:
|
|
349
|
+
schema:
|
|
350
|
+
$ref: '#/components/schemas/GeoVisioAnnotation'
|
|
351
|
+
204:
|
|
352
|
+
description: The annotation was empty, it has been correctly deleted
|
|
353
|
+
"""
|
|
354
|
+
return patchAnnotationNonStacAlias(annotationId=annotationId, account=account)
|
geovisio/web/auth.py
CHANGED
|
@@ -112,7 +112,7 @@ def auth():
|
|
|
112
112
|
def logout():
|
|
113
113
|
"""Log out from geovisio
|
|
114
114
|
* If the OAuth Provider is keycloak, this will redirect to a keycloak confirmation page,
|
|
115
|
-
and
|
|
115
|
+
and upon confirmation keycloak will call post_logout_redirect that will invalidate the session
|
|
116
116
|
* If the OAuth Provider is not keycloak, this will invalidate the session
|
|
117
117
|
---
|
|
118
118
|
tags:
|
geovisio/web/collections.py
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
from copy import deepcopy
|
|
2
1
|
from enum import Enum
|
|
3
|
-
from attr import dataclass
|
|
4
2
|
from geovisio import errors, utils, db
|
|
5
3
|
from geovisio.utils import auth, sequences
|
|
6
4
|
from geovisio.utils.params import validation_error
|
|
@@ -11,7 +9,7 @@ from geovisio.web.params import (
|
|
|
11
9
|
parse_datetime_interval,
|
|
12
10
|
parse_bbox,
|
|
13
11
|
parse_collection_filter,
|
|
14
|
-
|
|
12
|
+
parse_collection_sortby,
|
|
15
13
|
parse_collections_limit,
|
|
16
14
|
)
|
|
17
15
|
from geovisio.utils.sequences import (
|
|
@@ -20,7 +18,7 @@ from geovisio.utils.sequences import (
|
|
|
20
18
|
get_collections,
|
|
21
19
|
get_dataset_bounds,
|
|
22
20
|
)
|
|
23
|
-
from geovisio.utils.fields import SortBy, SortByField, SQLDirection,
|
|
21
|
+
from geovisio.utils.fields import SortBy, SortByField, SQLDirection, BBox, parse_relative_heading
|
|
24
22
|
from geovisio.web.rss import dbSequencesToGeoRSS
|
|
25
23
|
from psycopg.rows import dict_row
|
|
26
24
|
from psycopg.sql import SQL
|
|
@@ -97,7 +95,7 @@ def dbSequenceToStacCollection(dbSeq, description="A sequence of geolocated pict
|
|
|
97
95
|
"title": str(dbSeq["name"]),
|
|
98
96
|
"description": description,
|
|
99
97
|
"keywords": ["pictures", str(dbSeq["name"])],
|
|
100
|
-
"semantics": dbSeq
|
|
98
|
+
"semantics": dbSeq.get("semantics", []),
|
|
101
99
|
"license": current_app.config["API_PICTURES_LICENSE_SPDX_ID"],
|
|
102
100
|
"created": dbTsToStac(dbSeq["created"]),
|
|
103
101
|
"updated": dbTsToStac(dbSeq.get("updated")),
|
|
@@ -160,6 +158,16 @@ def dbSequenceToStacCollection(dbSeq, description="A sequence of geolocated pict
|
|
|
160
158
|
"href": url_for("stac_collections.getCollection", _external=True, collectionId=dbSeq["id"]),
|
|
161
159
|
},
|
|
162
160
|
get_license_link(),
|
|
161
|
+
(
|
|
162
|
+
{
|
|
163
|
+
"rel": "upload_set",
|
|
164
|
+
"type": "application/json",
|
|
165
|
+
"title": "Link to the upload set",
|
|
166
|
+
"href": url_for("upload_set.getUploadSet", _external=True, upload_set_id=dbSeq["upload_set_id"]),
|
|
167
|
+
}
|
|
168
|
+
if dbSeq.get("upload_set_id")
|
|
169
|
+
else None
|
|
170
|
+
),
|
|
163
171
|
]
|
|
164
172
|
),
|
|
165
173
|
}
|
|
@@ -247,7 +255,7 @@ def getAllCollections():
|
|
|
247
255
|
format = "rss"
|
|
248
256
|
|
|
249
257
|
# Sort-by parameter
|
|
250
|
-
sortBy =
|
|
258
|
+
sortBy = parse_collection_sortby(request.args.get("sortby"))
|
|
251
259
|
if not sortBy:
|
|
252
260
|
direction = SQLDirection.DESC if format == "rss" else SQLDirection.ASC
|
|
253
261
|
sortBy = SortBy(fields=[SortByField(field=STAC_FIELD_MAPPINGS["created"], direction=direction)])
|
|
@@ -382,8 +390,7 @@ def getCollection(collectionId):
|
|
|
382
390
|
|
|
383
391
|
record = db.fetchone(
|
|
384
392
|
current_app,
|
|
385
|
-
"""
|
|
386
|
-
SELECT
|
|
393
|
+
"""SELECT
|
|
387
394
|
s.id,
|
|
388
395
|
s.metadata->>'title' AS name,
|
|
389
396
|
ST_XMin(s.bbox) AS minx,
|
|
@@ -393,6 +400,7 @@ def getCollection(collectionId):
|
|
|
393
400
|
s.status AS status,
|
|
394
401
|
accounts.name AS account_name,
|
|
395
402
|
s.account_id AS account_id,
|
|
403
|
+
s.upload_set_id,
|
|
396
404
|
s.inserted_at AS created,
|
|
397
405
|
s.updated_at AS updated,
|
|
398
406
|
s.current_sort AS current_sort,
|
|
@@ -404,7 +412,7 @@ def getCollection(collectionId):
|
|
|
404
412
|
ROUND(ST_Length(s.geom::geography)) / 1000 as length_km,
|
|
405
413
|
s.computed_h_pixel_density,
|
|
406
414
|
s.computed_gps_accuracy,
|
|
407
|
-
|
|
415
|
+
COALESCE(seq_sem.semantics, '[]'::json) AS semantics
|
|
408
416
|
FROM sequences s
|
|
409
417
|
LEFT JOIN (
|
|
410
418
|
SELECT sequence_id, json_agg(json_strip_nulls(json_build_object(
|
|
@@ -413,7 +421,7 @@ def getCollection(collectionId):
|
|
|
413
421
|
)) ORDER BY key, value) AS semantics
|
|
414
422
|
FROM sequences_semantics
|
|
415
423
|
GROUP BY sequence_id
|
|
416
|
-
)
|
|
424
|
+
) seq_sem ON seq_sem.sequence_id = s.id
|
|
417
425
|
JOIN accounts ON s.account_id = accounts.id, (
|
|
418
426
|
SELECT
|
|
419
427
|
array_agg(DISTINCT jsonb_build_object(
|
|
@@ -502,6 +510,10 @@ def getCollectionThumbnail(collectionId):
|
|
|
502
510
|
@auth.login_required_by_setting("API_FORCE_AUTH_ON_UPLOAD")
|
|
503
511
|
def postCollection(account=None):
|
|
504
512
|
"""Create a new sequence
|
|
513
|
+
|
|
514
|
+
Note that this is the legacy API, upload should be done using the [UploadSet](#UploadSet) endpoints if possible.
|
|
515
|
+
|
|
516
|
+
Using an upload set makes it possible to handle more use cases like dispatching pictures into several collections, removing capture duplicates, parralele upload, ...
|
|
505
517
|
---
|
|
506
518
|
tags:
|
|
507
519
|
- Upload
|
|
@@ -511,7 +523,7 @@ def postCollection(account=None):
|
|
|
511
523
|
required: false
|
|
512
524
|
schema:
|
|
513
525
|
type: string
|
|
514
|
-
description: An explicit User-Agent value is
|
|
526
|
+
description: An explicit User-Agent value is preferred if you create a production-ready tool, formatted like "PanoramaxCLI/1.0"
|
|
515
527
|
requestBody:
|
|
516
528
|
content:
|
|
517
529
|
application/json:
|
|
@@ -565,7 +577,7 @@ class PatchCollectionParameter(BaseModel):
|
|
|
565
577
|
"""Parameters used to add an item to an UploadSet"""
|
|
566
578
|
|
|
567
579
|
relative_heading: Optional[int] = None
|
|
568
|
-
"""The relative heading (in degrees), offset based on movement path (0° = looking forward, -90° = looking left, 90° = looking right). Headings are unchanged if this parameter is not set."""
|
|
580
|
+
"""The relative heading (in degrees), offset based on movement path (0° = looking forward, -90° = looking left, 90° = looking right). For single picture collections, 0° is heading north). Headings are unchanged if this parameter is not set."""
|
|
569
581
|
visible: Optional[bool] = None
|
|
570
582
|
"""Should the sequence be publicly visible ?"""
|
|
571
583
|
title: Optional[str] = Field(max_length=250, default=None)
|
|
@@ -610,15 +622,7 @@ If unset, sort order is unchanged."""
|
|
|
610
622
|
@field_validator("relative_heading", mode="before")
|
|
611
623
|
@classmethod
|
|
612
624
|
def parse_relative_heading(cls, value):
|
|
613
|
-
|
|
614
|
-
relHeading = int(value)
|
|
615
|
-
if relHeading < -180 or relHeading > 180:
|
|
616
|
-
raise ValueError()
|
|
617
|
-
return relHeading
|
|
618
|
-
except (ValueError, TypeError):
|
|
619
|
-
raise errors.InvalidAPIUsage(
|
|
620
|
-
_("Relative heading is not valid, should be an integer in degrees from -180 to 180"), status_code=400
|
|
621
|
-
)
|
|
625
|
+
return parse_relative_heading(value)
|
|
622
626
|
|
|
623
627
|
def has_only_semantics_updates(self):
|
|
624
628
|
return self.model_fields_set == {"semantics"}
|
|
@@ -1036,7 +1040,7 @@ def getUserCollection(userId, userIdMatchesAccount=False):
|
|
|
1036
1040
|
format = "csv"
|
|
1037
1041
|
|
|
1038
1042
|
# Sort-by parameter
|
|
1039
|
-
sortBy =
|
|
1043
|
+
sortBy = parse_collection_sortby(request.args.get("sortby"))
|
|
1040
1044
|
if not sortBy:
|
|
1041
1045
|
sortBy = SortBy(fields=[SortByField(field=STAC_FIELD_MAPPINGS["created"], direction=SQLDirection.DESC)])
|
|
1042
1046
|
|
geovisio/web/configuration.py
CHANGED
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
import flask
|
|
2
2
|
from typing import Dict, Any
|
|
3
|
-
from flask import jsonify
|
|
3
|
+
from flask import jsonify, current_app
|
|
4
4
|
from flask_babel import get_locale
|
|
5
5
|
from geovisio.web.utils import get_api_version
|
|
6
|
+
from geovisio.utils import db
|
|
7
|
+
from psycopg.rows import class_row
|
|
8
|
+
from typing import Optional
|
|
9
|
+
from pydantic import BaseModel, Field, ConfigDict, field_serializer
|
|
10
|
+
import datetime
|
|
6
11
|
|
|
7
12
|
bp = flask.Blueprint("configuration", __name__, url_prefix="/api")
|
|
8
13
|
|
|
9
14
|
|
|
10
15
|
@bp.route("/configuration")
|
|
11
16
|
def configuration():
|
|
12
|
-
"""Return instance configuration
|
|
17
|
+
"""Return instance configuration information
|
|
13
18
|
---
|
|
14
19
|
tags:
|
|
15
20
|
- Metadata
|
|
@@ -36,6 +41,7 @@ def configuration():
|
|
|
36
41
|
"license": _license_configuration(),
|
|
37
42
|
"version": get_api_version(),
|
|
38
43
|
"pages": _get_pages(),
|
|
44
|
+
"defaults": _get_default_values(),
|
|
39
45
|
}
|
|
40
46
|
)
|
|
41
47
|
|
|
@@ -67,9 +73,23 @@ def _license_configuration():
|
|
|
67
73
|
|
|
68
74
|
|
|
69
75
|
def _get_pages():
|
|
70
|
-
from geovisio.utils import db
|
|
71
|
-
from flask import current_app
|
|
72
76
|
|
|
73
77
|
pages = db.fetchall(current_app, "SELECT distinct(name) FROM pages")
|
|
74
78
|
|
|
75
79
|
return [p[0] for p in pages]
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class Config(BaseModel):
|
|
83
|
+
collaborative_metadata: Optional[bool]
|
|
84
|
+
split_distance: Optional[int] = Field(validation_alias="default_split_distance")
|
|
85
|
+
split_time: Optional[datetime.timedelta] = Field(validation_alias="default_split_time")
|
|
86
|
+
duplicate_distance: Optional[float] = Field(validation_alias="default_duplicate_distance")
|
|
87
|
+
duplicate_rotation: Optional[int] = Field(validation_alias="default_duplicate_rotation")
|
|
88
|
+
|
|
89
|
+
@field_serializer("split_time")
|
|
90
|
+
def split_time_to_s(self, s: datetime.timedelta, _):
|
|
91
|
+
return s.total_seconds()
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _get_default_values():
|
|
95
|
+
return db.fetchone(current_app, "SELECT * FROM configurations", row_factory=class_row(Config)).model_dump()
|