geovisio 2.8.1__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 +16 -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 +101 -6
- 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 +421 -86
- 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 +823 -0
- 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 +183 -0
- geovisio/utils/auth.py +14 -13
- geovisio/utils/cql2.py +134 -0
- geovisio/utils/db.py +7 -0
- geovisio/utils/fields.py +38 -9
- geovisio/utils/items.py +44 -0
- geovisio/utils/model_query.py +4 -4
- geovisio/utils/pic_shape.py +63 -0
- geovisio/utils/pictures.py +164 -29
- geovisio/utils/reports.py +10 -17
- geovisio/utils/semantics.py +196 -57
- geovisio/utils/sentry.py +1 -2
- geovisio/utils/sequences.py +191 -93
- geovisio/utils/tags.py +31 -0
- geovisio/utils/upload_set.py +287 -209
- geovisio/utils/website.py +1 -1
- geovisio/web/annotations.py +346 -9
- geovisio/web/auth.py +1 -1
- geovisio/web/collections.py +73 -54
- geovisio/web/configuration.py +26 -5
- geovisio/web/docs.py +143 -11
- geovisio/web/items.py +232 -155
- geovisio/web/map.py +25 -13
- geovisio/web/params.py +55 -52
- geovisio/web/pictures.py +34 -0
- geovisio/web/stac.py +19 -12
- geovisio/web/tokens.py +49 -1
- geovisio/web/upload_set.py +148 -37
- geovisio/web/users.py +4 -4
- geovisio/web/utils.py +2 -2
- geovisio/workers/runner_pictures.py +190 -24
- {geovisio-2.8.1.dist-info → geovisio-2.10.0.dist-info}/METADATA +27 -26
- geovisio-2.10.0.dist-info/RECORD +105 -0
- {geovisio-2.8.1.dist-info → geovisio-2.10.0.dist-info}/WHEEL +1 -1
- geovisio-2.8.1.dist-info/RECORD +0 -92
- {geovisio-2.8.1.dist-info → geovisio-2.10.0.dist-info}/licenses/LICENSE +0 -0
geovisio/utils/sequences.py
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
from operator import ne
|
|
2
|
+
from click import Option
|
|
3
|
+
from numpy import sort
|
|
1
4
|
import psycopg
|
|
2
|
-
from flask import current_app, url_for
|
|
5
|
+
from flask import current_app, g, url_for
|
|
3
6
|
from flask_babel import gettext as _
|
|
4
7
|
from psycopg.types.json import Jsonb
|
|
5
8
|
from psycopg.sql import SQL, Composable
|
|
@@ -11,7 +14,7 @@ from uuid import UUID
|
|
|
11
14
|
from enum import Enum
|
|
12
15
|
from geovisio.utils import db
|
|
13
16
|
from geovisio.utils.auth import Account
|
|
14
|
-
from geovisio.utils.fields import FieldMapping, SortBy, SQLDirection, BBox, Bounds
|
|
17
|
+
from geovisio.utils.fields import FieldMapping, SortBy, SQLDirection, BBox, Bounds, SortByField
|
|
15
18
|
from geopic_tag_reader import reader
|
|
16
19
|
from pathlib import PurePath
|
|
17
20
|
from geovisio import errors, utils
|
|
@@ -39,6 +42,7 @@ STAC_FIELD_MAPPINGS = {
|
|
|
39
42
|
FieldMapping(sql_column=SQL("updated_at"), stac="updated"),
|
|
40
43
|
FieldMapping(sql_column=SQL("computed_capture_date"), stac="datetime"),
|
|
41
44
|
FieldMapping(sql_column=SQL("status"), stac="status"),
|
|
45
|
+
FieldMapping(sql_column=SQL("id"), stac="id"),
|
|
42
46
|
]
|
|
43
47
|
}
|
|
44
48
|
STAC_FIELD_TO_SQL_FILTER = {p.stac: p.sql_filter.as_string(None) for p in STAC_FIELD_MAPPINGS.values()}
|
|
@@ -51,8 +55,8 @@ class Collections:
|
|
|
51
55
|
"""
|
|
52
56
|
|
|
53
57
|
collections: List[Dict[Any, Any]] = field(default_factory=lambda: [])
|
|
54
|
-
# Bounds of the field used by the first field of the `ORDER BY` (
|
|
55
|
-
|
|
58
|
+
# Bounds of the field used by the first field of the `ORDER BY` (useful especially for pagination)
|
|
59
|
+
query_bounds: Optional[Bounds] = None
|
|
56
60
|
|
|
57
61
|
|
|
58
62
|
@dataclass
|
|
@@ -153,23 +157,24 @@ def get_collections(request: CollectionsRequest) -> Collections:
|
|
|
153
157
|
s.min_picture_ts AS mints,
|
|
154
158
|
s.max_picture_ts AS maxts,
|
|
155
159
|
s.nb_pictures AS nbpic,
|
|
160
|
+
s.upload_set_id,
|
|
156
161
|
{status},
|
|
157
162
|
s.computed_capture_date AS datetime,
|
|
158
163
|
s.user_agent,
|
|
159
164
|
ROUND(ST_Length(s.geom::geography)) / 1000 AS length_km,
|
|
160
165
|
s.computed_h_pixel_density,
|
|
161
166
|
s.computed_gps_accuracy,
|
|
162
|
-
|
|
167
|
+
COALESCE(seq_sem.semantics, '[]'::json) AS semantics
|
|
163
168
|
FROM sequences s
|
|
164
169
|
LEFT JOIN accounts on s.account_id = accounts.id
|
|
165
170
|
LEFT JOIN (
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
171
|
+
SELECT sequence_id, json_agg(json_strip_nulls(json_build_object(
|
|
172
|
+
'key', key,
|
|
173
|
+
'value', value
|
|
174
|
+
)) ORDER BY key, value) AS semantics
|
|
175
|
+
FROM sequences_semantics
|
|
176
|
+
GROUP BY sequence_id
|
|
177
|
+
) seq_sem ON seq_sem.sequence_id = s.id
|
|
173
178
|
WHERE {filter}
|
|
174
179
|
ORDER BY {order1}
|
|
175
180
|
LIMIT {limit}
|
|
@@ -184,71 +189,150 @@ def get_collections(request: CollectionsRequest) -> Collections:
|
|
|
184
189
|
|
|
185
190
|
# Different request if we want the last n sequences
|
|
186
191
|
# Useful for paginating from last page to first
|
|
187
|
-
if request.pagination_filter
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
)
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
192
|
+
if request.pagination_filter:
|
|
193
|
+
# note: we don't want to compare the leading parenthesis
|
|
194
|
+
pagination = request.pagination_filter.as_string(None).strip("(")
|
|
195
|
+
first_sort = request.sort_by.fields[0]
|
|
196
|
+
if (first_sort.direction == SQLDirection.ASC and pagination.startswith(f"{first_sort.field.sql_filter.as_string(None)} <")) or (
|
|
197
|
+
first_sort.direction == SQLDirection.DESC and pagination.startswith(f"{first_sort.field.sql_filter.as_string(None)} >")
|
|
198
|
+
):
|
|
199
|
+
base_query = sqlSequencesRaw.format(
|
|
200
|
+
filter=SQL(" AND ").join(seq_filter),
|
|
201
|
+
order1=request.sort_by.revert(),
|
|
202
|
+
limit=request.limit,
|
|
203
|
+
status=status_field,
|
|
204
|
+
)
|
|
205
|
+
sqlSequences = SQL(
|
|
206
|
+
"""
|
|
207
|
+
SELECT *
|
|
208
|
+
FROM ({base_query}) s
|
|
209
|
+
ORDER BY {order2}
|
|
204
210
|
"""
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
).format(
|
|
210
|
-
order2=request.sort_by.as_sql(),
|
|
211
|
-
base_query=base_query,
|
|
212
|
-
)
|
|
211
|
+
).format(
|
|
212
|
+
order2=request.sort_by.as_sql(),
|
|
213
|
+
base_query=base_query,
|
|
214
|
+
)
|
|
213
215
|
|
|
214
216
|
records = cursor.execute(sqlSequences, seq_params).fetchall()
|
|
215
217
|
|
|
216
218
|
query_bounds = None
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
if query_bounds is None:
|
|
222
|
-
query_bounds = Bounds(first_order_val, first_order_val)
|
|
223
|
-
else:
|
|
224
|
-
query_bounds.update(first_order_val)
|
|
219
|
+
if records:
|
|
220
|
+
first = [records[0].get(f.field.stac) for f in request.sort_by.fields]
|
|
221
|
+
last = [records[-1].get(f.field.stac) for f in request.sort_by.fields]
|
|
222
|
+
query_bounds = Bounds(first, last)
|
|
225
223
|
|
|
226
224
|
return Collections(
|
|
227
225
|
collections=records,
|
|
228
|
-
|
|
226
|
+
query_bounds=query_bounds,
|
|
229
227
|
)
|
|
230
228
|
|
|
231
229
|
|
|
230
|
+
def get_pagination_stac_filter(sortBy: SortBy, dataBounds: Optional[Bounds[List[Any]]], next: bool) -> str:
|
|
231
|
+
"""Create a pagination API filters, using the sorts and the bounds of the current query"""
|
|
232
|
+
filters = []
|
|
233
|
+
bounds = dataBounds.last if next else dataBounds.first
|
|
234
|
+
for i, f in enumerate(sortBy.fields):
|
|
235
|
+
direction = f.direction
|
|
236
|
+
# bounds is a list of values, for all sorty_by fields
|
|
237
|
+
if (next and direction == SQLDirection.ASC) or (not next and direction == SQLDirection.DESC):
|
|
238
|
+
cmp = ">"
|
|
239
|
+
else:
|
|
240
|
+
cmp = "<"
|
|
241
|
+
field_pagination = f"{f.field.stac} {cmp} '{bounds[i]}'"
|
|
242
|
+
|
|
243
|
+
previous_filters = sortBy.fields[:i]
|
|
244
|
+
if previous_filters:
|
|
245
|
+
prev_fields = " AND ".join([f"{f.field.stac} = '{bounds[prev_i]}'" for prev_i, f in enumerate(previous_filters)]) + " AND "
|
|
246
|
+
filters.append(f"({prev_fields}{field_pagination})")
|
|
247
|
+
else:
|
|
248
|
+
filters.append(field_pagination)
|
|
249
|
+
return " OR ".join(filters)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def get_dataset_bounds(
|
|
253
|
+
conn: psycopg.Connection,
|
|
254
|
+
sortBy: SortBy,
|
|
255
|
+
additional_filters: Optional[SQL] = None,
|
|
256
|
+
additional_filters_params: Optional[Dict[str, Any]] = None,
|
|
257
|
+
) -> Optional[Bounds]:
|
|
258
|
+
"""Computes the dataset bounds from the sortBy field (using lexicographic order)
|
|
259
|
+
|
|
260
|
+
if there are several sort-by fields like (inserted_at, updated_at), this will return a bound with minimum (resp maximum)
|
|
261
|
+
inserted_at value, and for this value, the minimum (resp maximum) updated_at value.
|
|
262
|
+
"""
|
|
263
|
+
with conn.cursor() as cursor:
|
|
264
|
+
|
|
265
|
+
sql_bounds = cursor.execute(
|
|
266
|
+
SQL(
|
|
267
|
+
"""WITH min_bounds AS (
|
|
268
|
+
SELECT {fields} from sequences s WHERE {filters} ORDER BY {ordered_fields} LIMIT 1
|
|
269
|
+
),
|
|
270
|
+
max_bounds AS (
|
|
271
|
+
SELECT {fields} from sequences s WHERE {filters} ORDER BY {reverse_fields} LIMIT 1
|
|
272
|
+
)
|
|
273
|
+
SELECT * FROM min_bounds, max_bounds;
|
|
274
|
+
"""
|
|
275
|
+
).format(
|
|
276
|
+
fields=SQL(", ").join([f.field.sql_column for f in sortBy.fields]),
|
|
277
|
+
ordered_fields=sortBy.as_non_aliased_sql(),
|
|
278
|
+
reverse_fields=sortBy.revert_non_aliased_sql(),
|
|
279
|
+
filters=additional_filters or SQL("TRUE"),
|
|
280
|
+
),
|
|
281
|
+
params=additional_filters_params or {},
|
|
282
|
+
).fetchone()
|
|
283
|
+
if not sql_bounds:
|
|
284
|
+
return None
|
|
285
|
+
min = [sql_bounds[i] for i, f in enumerate(sortBy.fields)]
|
|
286
|
+
max = [sql_bounds[i + len(sortBy.fields)] for i, f in enumerate(sortBy.fields)]
|
|
287
|
+
return Bounds(first=min, last=max)
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def has_previous_results(sortBy: SortBy, datasetBounds: Bounds, dataBounds: Bounds) -> bool:
|
|
291
|
+
"""Check if there are results in the database before the one returned by the queries
|
|
292
|
+
To do this, we do a lexicographic comparison of the bounds, using the fields direction
|
|
293
|
+
|
|
294
|
+
Note: the bounds are reversed for the DESC direction, so the bounds.min >= bounds.max for the DESC direction
|
|
295
|
+
"""
|
|
296
|
+
for i, f in enumerate(sortBy.fields):
|
|
297
|
+
if dataBounds.first[i] is None or datasetBounds.first[i] is None:
|
|
298
|
+
continue
|
|
299
|
+
if (f.direction == SQLDirection.ASC and dataBounds.first[i] > datasetBounds.first[i]) or (
|
|
300
|
+
f.direction == SQLDirection.DESC and datasetBounds.first[i] > dataBounds.first[i]
|
|
301
|
+
):
|
|
302
|
+
return True
|
|
303
|
+
return False
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def has_next_results(sortBy: SortBy, datasetBounds: Bounds, dataBounds: Bounds) -> bool:
|
|
307
|
+
"""Check if there are results in the database after the one returned by the queries
|
|
308
|
+
To do this, we do a lexicographic comparison of the bounds, using the fields direction"""
|
|
309
|
+
for i, f in enumerate(sortBy.fields):
|
|
310
|
+
if dataBounds.last[i] is None or datasetBounds.last[i] is None:
|
|
311
|
+
continue
|
|
312
|
+
if (f.direction == SQLDirection.ASC and dataBounds.last[i] < datasetBounds.last[i]) or (
|
|
313
|
+
f.direction == SQLDirection.DESC and datasetBounds.last[i] < dataBounds.last[i]
|
|
314
|
+
):
|
|
315
|
+
return True
|
|
316
|
+
return False
|
|
317
|
+
|
|
318
|
+
|
|
232
319
|
def get_pagination_links(
|
|
233
320
|
route: str,
|
|
234
321
|
routeArgs: dict,
|
|
235
|
-
|
|
236
|
-
direction: SQLDirection,
|
|
322
|
+
sortBy: SortBy,
|
|
237
323
|
datasetBounds: Bounds,
|
|
238
324
|
dataBounds: Optional[Bounds],
|
|
239
325
|
additional_filters: Optional[str],
|
|
240
326
|
) -> List:
|
|
241
327
|
"""Computes STAC links to handle pagination"""
|
|
242
328
|
|
|
243
|
-
sortby =
|
|
329
|
+
sortby = sortBy.as_stac()
|
|
244
330
|
links = []
|
|
245
|
-
if dataBounds is None:
|
|
331
|
+
if dataBounds is None or datasetBounds is None:
|
|
246
332
|
return links
|
|
247
333
|
|
|
248
334
|
# Check if first/prev links are necessary
|
|
249
|
-
if (
|
|
250
|
-
direction == SQLDirection.DESC and dataBounds.max < datasetBounds.max
|
|
251
|
-
):
|
|
335
|
+
if has_previous_results(sortBy, datasetBounds=datasetBounds, dataBounds=dataBounds):
|
|
252
336
|
links.append(
|
|
253
337
|
{
|
|
254
338
|
"rel": "first",
|
|
@@ -256,7 +340,9 @@ def get_pagination_links(
|
|
|
256
340
|
"href": url_for(route, _external=True, **routeArgs, filter=additional_filters, sortby=sortby),
|
|
257
341
|
}
|
|
258
342
|
)
|
|
259
|
-
|
|
343
|
+
|
|
344
|
+
page_filter = get_pagination_stac_filter(sortBy, dataBounds, next=False)
|
|
345
|
+
|
|
260
346
|
links.append(
|
|
261
347
|
{
|
|
262
348
|
"rel": "prev",
|
|
@@ -273,11 +359,8 @@ def get_pagination_links(
|
|
|
273
359
|
)
|
|
274
360
|
|
|
275
361
|
# Check if next/last links are required
|
|
276
|
-
if (
|
|
277
|
-
|
|
278
|
-
):
|
|
279
|
-
next_filter = f"{field} {'>' if direction == SQLDirection.ASC else '<'} '{dataBounds.max if direction == SQLDirection.ASC else dataBounds.min}'"
|
|
280
|
-
last_filter = f"{field} {'<=' if direction == SQLDirection.ASC else '>='} '{datasetBounds.max if direction == SQLDirection.ASC else datasetBounds.min}'"
|
|
362
|
+
if has_next_results(sortBy, datasetBounds=datasetBounds, dataBounds=dataBounds):
|
|
363
|
+
next_filter = get_pagination_stac_filter(sortBy, dataBounds, next=True)
|
|
281
364
|
links.append(
|
|
282
365
|
{
|
|
283
366
|
"rel": "next",
|
|
@@ -292,6 +375,10 @@ def get_pagination_links(
|
|
|
292
375
|
),
|
|
293
376
|
}
|
|
294
377
|
)
|
|
378
|
+
# for last, we only consider the first field used for sorting, the rest are useless
|
|
379
|
+
# Note: we compare to the datasetBounds last since it depends on the sort direction (so, for DESC, it last<first)
|
|
380
|
+
f = sortBy.fields[0]
|
|
381
|
+
last_filter = f"{f.field.stac} {'<=' if f.direction == SQLDirection.ASC else '>='} '{datasetBounds.last[0]}'"
|
|
295
382
|
links.append(
|
|
296
383
|
{
|
|
297
384
|
"rel": "last",
|
|
@@ -341,7 +428,7 @@ def sort_collection(db, collectionId: UUID, sortby: CollectionSort):
|
|
|
341
428
|
"""
|
|
342
429
|
Sort a collection by a given parameter
|
|
343
430
|
|
|
344
|
-
Note: the transaction is not
|
|
431
|
+
Note: the transaction is not committed at the end, you need to commit it or use an autocommit connection
|
|
345
432
|
"""
|
|
346
433
|
|
|
347
434
|
# Remove existing order, and keep list of pictures IDs
|
|
@@ -423,23 +510,18 @@ def sort_collection(db, collectionId: UUID, sortby: CollectionSort):
|
|
|
423
510
|
picForDb = [(collectionId, i + 1, p["id"]) for i, p in enumerate(picMetas)]
|
|
424
511
|
|
|
425
512
|
# Inject back pictures in sequence
|
|
426
|
-
db.executemany(
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
VALUES (%s, %s, %s)
|
|
431
|
-
"""
|
|
432
|
-
),
|
|
433
|
-
picForDb,
|
|
434
|
-
)
|
|
513
|
+
db.executemany(SQL("INSERT INTO sequences_pictures(seq_id, rank, pic_id) VALUES (%s, %s, %s)"), picForDb)
|
|
514
|
+
|
|
515
|
+
# we update the geometry of the sequence after this (the other computed fields have no need for an update)
|
|
516
|
+
db.execute(SQL("UPDATE sequences SET geom = compute_sequence_geom(id) WHERE id = %s"), [collectionId])
|
|
435
517
|
|
|
436
518
|
|
|
437
519
|
def update_headings(
|
|
438
520
|
db,
|
|
439
521
|
sequenceId: UUID,
|
|
440
522
|
editingAccount: Optional[UUID] = None,
|
|
441
|
-
relativeHeading: int =
|
|
442
|
-
updateOnlyMissing: bool =
|
|
523
|
+
relativeHeading: Optional[int] = None,
|
|
524
|
+
updateOnlyMissing: Optional[bool] = None,
|
|
443
525
|
):
|
|
444
526
|
"""Defines pictures heading according to sequence path.
|
|
445
527
|
Database is not committed in this function, to make entry definitively stored
|
|
@@ -451,27 +533,42 @@ def update_headings(
|
|
|
451
533
|
Database connection
|
|
452
534
|
sequenceId : uuid
|
|
453
535
|
The sequence's uuid, as stored in the database
|
|
454
|
-
relativeHeading : int
|
|
536
|
+
relativeHeading : Optional[int]
|
|
455
537
|
Camera relative orientation compared to path, in degrees clockwise.
|
|
456
538
|
Example: 0° = looking forward, 90° = looking to right, 180° = looking backward, -90° = looking left.
|
|
457
|
-
|
|
539
|
+
If not provided, will first use the relative_heading stored in the sequence's metadata, then the relative_heading of its upload_set (if if none is set, default to 0).
|
|
540
|
+
updateOnlyMissing : Optional[bool]
|
|
458
541
|
If true, doesn't change existing heading values in database
|
|
542
|
+
if not provided, we check if some relative heading has been set (either in the sequence or in its upload_set), and if so, we recompute all
|
|
459
543
|
"""
|
|
460
|
-
|
|
461
544
|
db.execute(
|
|
462
545
|
SQL(
|
|
463
|
-
"""
|
|
464
|
-
|
|
546
|
+
"""WITH
|
|
547
|
+
relative_heading AS (
|
|
548
|
+
SELECT COALESCE(
|
|
549
|
+
%(relativeHeading)s,
|
|
550
|
+
(SELECT (metadata->>'relative_heading')::int FROM sequences WHERE id = %(seq)s),
|
|
551
|
+
(SELECT upload_sets.relative_heading FROM sequences JOIN upload_sets ON sequences.upload_set_id = upload_sets.id WHERE sequences.id = %(seq)s),
|
|
552
|
+
0
|
|
553
|
+
) AS heading,
|
|
554
|
+
COALESCE(
|
|
555
|
+
%(update_only_missing)s,
|
|
556
|
+
(SELECT metadata->'relative_heading' IS NULL FROM sequences WHERE id = %(seq)s and metadata ? 'relative_heading'),
|
|
557
|
+
(SELECT upload_sets.relative_heading IS NULL FROM sequences JOIN upload_sets ON sequences.upload_set_id = upload_sets.id WHERE sequences.id = %(seq)s)
|
|
558
|
+
) AS update_only_missing
|
|
559
|
+
)
|
|
560
|
+
, h AS (
|
|
465
561
|
SELECT
|
|
466
562
|
p.id,
|
|
467
563
|
p.heading AS old_heading,
|
|
468
564
|
CASE
|
|
469
565
|
WHEN LEAD(sp.rank) OVER othpics IS NULL AND LAG(sp.rank) OVER othpics IS NULL
|
|
470
|
-
|
|
566
|
+
-- if there is a single picture, we take the relative heading directly
|
|
567
|
+
THEN (SELECT heading FROM relative_heading)
|
|
471
568
|
WHEN LEAD(sp.rank) OVER othpics IS NULL
|
|
472
|
-
THEN (360 + FLOOR(DEGREES(ST_Azimuth(LAG(p.geom) OVER othpics, p.geom)))::int + (
|
|
569
|
+
THEN (360 + FLOOR(DEGREES(ST_Azimuth(LAG(p.geom) OVER othpics, p.geom)))::int + ((SELECT heading FROM relative_heading) %% 360)) %% 360
|
|
473
570
|
ELSE
|
|
474
|
-
(360 + FLOOR(DEGREES(ST_Azimuth(p.geom, LEAD(p.geom) OVER othpics)))::int + (
|
|
571
|
+
(360 + FLOOR(DEGREES(ST_Azimuth(p.geom, LEAD(p.geom) OVER othpics)))::int + ((SELECT heading FROM relative_heading) %% 360)) %% 360
|
|
475
572
|
END AS heading
|
|
476
573
|
FROM pictures p
|
|
477
574
|
JOIN sequences_pictures sp ON sp.pic_id = p.id AND sp.seq_id = %(seq)s
|
|
@@ -480,13 +577,15 @@ def update_headings(
|
|
|
480
577
|
UPDATE pictures p
|
|
481
578
|
SET heading = h.heading, heading_computed = true {editing_account}
|
|
482
579
|
FROM h
|
|
483
|
-
WHERE h.id = p.id
|
|
580
|
+
WHERE h.id = p.id AND (
|
|
581
|
+
(SELECT NOT update_only_missing FROM relative_heading)
|
|
582
|
+
OR (p.heading IS NULL OR p.heading = 0 OR p.heading_computed) -- # lots of camera have heading set to 0 for unset heading, so we recompute the heading when it's 0 too, even if this could be a valid value
|
|
583
|
+
)
|
|
484
584
|
"""
|
|
485
585
|
).format(
|
|
486
|
-
update_missing=SQL(" AND (p.heading IS NULL OR p.heading = 0 OR p.heading_computed)") if updateOnlyMissing else SQL(""),
|
|
487
586
|
editing_account=SQL(", last_account_to_edit = %(account)s") if editingAccount is not None else SQL(""),
|
|
488
|
-
),
|
|
489
|
-
{"seq": sequenceId, "
|
|
587
|
+
),
|
|
588
|
+
{"seq": sequenceId, "relativeHeading": relativeHeading, "account": editingAccount, "update_only_missing": updateOnlyMissing},
|
|
490
589
|
)
|
|
491
590
|
|
|
492
591
|
|
|
@@ -512,14 +611,13 @@ def finalize(cursor, seqId: UUID, logger: logging.Logger = logging.getLogger()):
|
|
|
512
611
|
span.set_data("sequence_id", seqId)
|
|
513
612
|
logger.debug(f"Finalizing sequence {seqId}")
|
|
514
613
|
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
update_headings(cursor, seqId)
|
|
614
|
+
# Complete missing headings in pictures
|
|
615
|
+
update_headings(cursor, seqId)
|
|
518
616
|
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
617
|
+
# Change sequence database status in DB
|
|
618
|
+
# Also generates data in computed columns
|
|
619
|
+
cursor.execute(
|
|
620
|
+
"""WITH
|
|
523
621
|
aggregated_pictures AS (
|
|
524
622
|
SELECT
|
|
525
623
|
sp.seq_id,
|
|
@@ -548,10 +646,10 @@ computed_gps_accuracy = gpsacc
|
|
|
548
646
|
FROM aggregated_pictures
|
|
549
647
|
WHERE id = %(seq)s
|
|
550
648
|
""",
|
|
551
|
-
|
|
552
|
-
|
|
649
|
+
{"seq": seqId},
|
|
650
|
+
)
|
|
553
651
|
|
|
554
|
-
|
|
652
|
+
logger.info(f"Sequence {seqId} is ready")
|
|
555
653
|
|
|
556
654
|
|
|
557
655
|
def update_pictures_grid() -> Optional[datetime.datetime]:
|
geovisio/utils/tags.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class TagAction(str, Enum):
|
|
7
|
+
"""Actions to perform on a tag list"""
|
|
8
|
+
|
|
9
|
+
add = "add"
|
|
10
|
+
delete = "delete"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class SemanticTagUpdate(BaseModel):
|
|
14
|
+
"""Parameters used to update a tag list"""
|
|
15
|
+
|
|
16
|
+
action: TagAction = Field(default=TagAction.add)
|
|
17
|
+
"""Action to perform on the tag list. The default action is `add` which will add the given tag to the list.
|
|
18
|
+
The action can also be to `delete` the key/value"""
|
|
19
|
+
key: str = Field(max_length=256)
|
|
20
|
+
"""Key of the tag to update limited to 256 characters"""
|
|
21
|
+
value: str = Field(max_length=2048)
|
|
22
|
+
"""Value of the tag to update limited ot 2048 characters"""
|
|
23
|
+
|
|
24
|
+
model_config = ConfigDict(use_attribute_docstrings=True)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class SemanticTag(BaseModel):
|
|
28
|
+
key: str
|
|
29
|
+
"""Key of the tag"""
|
|
30
|
+
value: str
|
|
31
|
+
"""Value of the tag"""
|