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/web/items.py
CHANGED
|
@@ -6,13 +6,18 @@ 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.items import SortableItemField, SortBy, ItemSortByField
|
|
19
|
+
from geovisio.utils.tags import SemanticTagUpdate
|
|
20
|
+
from geovisio.utils.auth import Account
|
|
16
21
|
from geovisio.web.params import (
|
|
17
22
|
as_latitude,
|
|
18
23
|
as_longitude,
|
|
@@ -20,12 +25,13 @@ from geovisio.web.params import (
|
|
|
20
25
|
parse_datetime,
|
|
21
26
|
parse_datetime_interval,
|
|
22
27
|
parse_bbox,
|
|
28
|
+
parse_item_sortby,
|
|
23
29
|
parse_list,
|
|
24
30
|
parse_lonlat,
|
|
25
31
|
parse_distance_range,
|
|
26
32
|
parse_picture_heading,
|
|
27
33
|
)
|
|
28
|
-
from geovisio.utils.fields import Bounds
|
|
34
|
+
from geovisio.utils.fields import Bounds, SQLDirection
|
|
29
35
|
import hashlib
|
|
30
36
|
from psycopg.rows import dict_row
|
|
31
37
|
from psycopg.sql import SQL
|
|
@@ -49,7 +55,7 @@ import math
|
|
|
49
55
|
bp = Blueprint("stac_items", __name__, url_prefix="/api")
|
|
50
56
|
|
|
51
57
|
|
|
52
|
-
def dbPictureToStacItem(
|
|
58
|
+
def dbPictureToStacItem(dbPic):
|
|
53
59
|
"""Transforms a picture extracted from database into a STAC Item
|
|
54
60
|
|
|
55
61
|
Parameters
|
|
@@ -67,6 +73,7 @@ def dbPictureToStacItem(seqId, dbPic):
|
|
|
67
73
|
|
|
68
74
|
sensorDim = None
|
|
69
75
|
visibleArea = None
|
|
76
|
+
seqId = str(dbPic["seq_id"])
|
|
70
77
|
if dbPic["metadata"].get("crop") is not None:
|
|
71
78
|
sensorDim = [dbPic["metadata"]["crop"].get("fullWidth"), dbPic["metadata"]["crop"].get("fullHeight")]
|
|
72
79
|
visibleArea = [
|
|
@@ -112,7 +119,7 @@ def dbPictureToStacItem(seqId, dbPic):
|
|
|
112
119
|
"datetime": dbTsToStac(dbPic["ts"]),
|
|
113
120
|
"datetimetz": dbTsToStacTZ(dbPic["ts"], dbPic["metadata"].get("tz")),
|
|
114
121
|
"created": dbTsToStac(dbPic["inserted_at"]),
|
|
115
|
-
|
|
122
|
+
"updated": dbTsToStac(dbPic["updated_at"]),
|
|
116
123
|
"license": current_app.config["API_PICTURES_LICENSE_SPDX_ID"],
|
|
117
124
|
"view:azimuth": dbPic["heading"],
|
|
118
125
|
"pers:interior_orientation": (
|
|
@@ -138,6 +145,7 @@ def dbPictureToStacItem(seqId, dbPic):
|
|
|
138
145
|
"pers:roll": dbPic["metadata"].get("roll"),
|
|
139
146
|
"geovisio:status": dbPic.get("status"),
|
|
140
147
|
"geovisio:producer": dbPic["account_name"],
|
|
148
|
+
"geovisio:rank_in_collection": dbPic["rank"],
|
|
141
149
|
"original_file:size": dbPic["metadata"].get("originalFileSize"),
|
|
142
150
|
"original_file:name": dbPic["metadata"].get("originalFileName"),
|
|
143
151
|
"panoramax:horizontal_pixel_density": dbPic.get("h_pixel_density"),
|
|
@@ -145,7 +153,9 @@ def dbPictureToStacItem(seqId, dbPic):
|
|
|
145
153
|
"geovisio:thumbnail": _getThumbJpgPictureURL(dbPic["id"], dbPic.get("status")),
|
|
146
154
|
"exif": removeNoneInDict(cleanupExif(dbPic["exif"])),
|
|
147
155
|
"quality:horizontal_accuracy": float("{:.1f}".format(dbPic["gps_accuracy_m"])) if dbPic.get("gps_accuracy_m") else None,
|
|
148
|
-
"semantics":
|
|
156
|
+
"semantics": [s for s in dbPic.get("semantics") or [] if s],
|
|
157
|
+
"annotations": [a for a in dbPic.get("annotations") or [] if a],
|
|
158
|
+
"collection": {"semantics": dbPic["sequence_semantics"]} if "sequence_semantics" in dbPic else None,
|
|
149
159
|
}
|
|
150
160
|
),
|
|
151
161
|
"links": cleanNoneInList(
|
|
@@ -419,7 +429,7 @@ def getCollectionItems(collectionId):
|
|
|
419
429
|
params={"id": withPicture, "seq": collectionId},
|
|
420
430
|
).fetchone()
|
|
421
431
|
if not pic:
|
|
422
|
-
raise errors.InvalidAPIUsage(_("Picture with id %(p)s does not
|
|
432
|
+
raise errors.InvalidAPIUsage(_("Picture with id %(p)s does not exist", p=withPicture))
|
|
423
433
|
rank = get_first_rank_of_page(pic["rank"], limit)
|
|
424
434
|
|
|
425
435
|
filters.append(SQL("rank >= %(start_after_rank)s"))
|
|
@@ -428,28 +438,21 @@ def getCollectionItems(collectionId):
|
|
|
428
438
|
query = SQL(
|
|
429
439
|
"""
|
|
430
440
|
SELECT
|
|
431
|
-
p.id, p.ts, p.heading, p.metadata, p.inserted_at, p.status,
|
|
441
|
+
p.id, p.ts, p.heading, p.metadata, p.inserted_at, p.updated_at, p.status,
|
|
432
442
|
ST_AsGeoJSON(p.geom)::json AS geojson,
|
|
433
443
|
a.name AS account_name,
|
|
434
444
|
p.account_id AS account_id,
|
|
435
|
-
sp.rank, p.exif, p.gps_accuracy_m, p.h_pixel_density,
|
|
445
|
+
sp.seq_id, sp.rank, p.exif, p.gps_accuracy_m, p.h_pixel_density,
|
|
436
446
|
CASE WHEN LAG(p.status) OVER othpics = 'ready' THEN LAG(p.id) OVER othpics END AS prevpic,
|
|
437
447
|
CASE WHEN LAG(p.status) OVER othpics = 'ready' THEN ST_AsGeoJSON(LAG(p.geom) OVER othpics)::json END AS prevpicgeojson,
|
|
438
448
|
CASE WHEN LEAD(p.status) OVER othpics = 'ready' THEN LEAD(p.id) OVER othpics END AS nextpic,
|
|
439
449
|
CASE WHEN LEAD(p.status) OVER othpics = 'ready' THEN ST_AsGeoJSON(LEAD(p.geom) OVER othpics)::json END AS nextpicgeojson,
|
|
440
|
-
|
|
450
|
+
get_picture_semantics(p.id) as semantics,
|
|
451
|
+
get_picture_annotations(p.id) as annotations
|
|
441
452
|
FROM sequences_pictures sp
|
|
442
453
|
JOIN pictures p ON sp.pic_id = p.id
|
|
443
454
|
JOIN accounts a ON a.id = p.account_id
|
|
444
455
|
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
456
|
WHERE
|
|
454
457
|
{filter}
|
|
455
458
|
WINDOW othpics AS (PARTITION BY sp.seq_id ORDER BY sp.rank)
|
|
@@ -460,15 +463,14 @@ def getCollectionItems(collectionId):
|
|
|
460
463
|
|
|
461
464
|
records = cursor.execute(query, params)
|
|
462
465
|
|
|
463
|
-
bounds: Optional[Bounds] = None
|
|
464
466
|
items = []
|
|
467
|
+
first_rank, last_rank = None, None
|
|
465
468
|
for dbPic in records:
|
|
466
|
-
if
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
items.append(dbPictureToStacItem(collectionId, dbPic))
|
|
469
|
+
if first_rank is None:
|
|
470
|
+
first_rank = dbPic["rank"]
|
|
471
|
+
last_rank = dbPic["rank"]
|
|
472
|
+
items.append(dbPictureToStacItem(dbPic))
|
|
473
|
+
bounds = Bounds(first=first_rank, last=last_rank) if records else None
|
|
472
474
|
|
|
473
475
|
links = [
|
|
474
476
|
get_root_link(),
|
|
@@ -491,8 +493,8 @@ def getCollectionItems(collectionId):
|
|
|
491
493
|
]
|
|
492
494
|
|
|
493
495
|
if paginated and items and bounds:
|
|
494
|
-
if bounds.
|
|
495
|
-
has_item_before = bounds.
|
|
496
|
+
if bounds.first:
|
|
497
|
+
has_item_before = bounds.first > seqMeta["min_rank"]
|
|
496
498
|
if has_item_before:
|
|
497
499
|
links.append(
|
|
498
500
|
{
|
|
@@ -504,7 +506,7 @@ def getCollectionItems(collectionId):
|
|
|
504
506
|
# Previous page link
|
|
505
507
|
# - If limit is set, rank is current - limit -1
|
|
506
508
|
# - If no limit is set, rank is 0 (none)
|
|
507
|
-
prevRank = bounds.
|
|
509
|
+
prevRank = bounds.first - limit - 1 if limit is not None else 0
|
|
508
510
|
if prevRank < 1:
|
|
509
511
|
prevRank = None
|
|
510
512
|
links.append(
|
|
@@ -521,7 +523,7 @@ def getCollectionItems(collectionId):
|
|
|
521
523
|
}
|
|
522
524
|
)
|
|
523
525
|
|
|
524
|
-
has_item_after = bounds.
|
|
526
|
+
has_item_after = bounds.last < seqMeta["max_rank"]
|
|
525
527
|
if has_item_after:
|
|
526
528
|
links.append(
|
|
527
529
|
{
|
|
@@ -532,7 +534,7 @@ def getCollectionItems(collectionId):
|
|
|
532
534
|
_external=True,
|
|
533
535
|
collectionId=collectionId,
|
|
534
536
|
limit=limit,
|
|
535
|
-
startAfterRank=bounds.
|
|
537
|
+
startAfterRank=bounds.last,
|
|
536
538
|
),
|
|
537
539
|
}
|
|
538
540
|
)
|
|
@@ -543,10 +545,10 @@ def getCollectionItems(collectionId):
|
|
|
543
545
|
|
|
544
546
|
lastPageRank = startAfterRank
|
|
545
547
|
if limit is not None:
|
|
546
|
-
if seqMeta["max_rank"] > bounds.
|
|
548
|
+
if seqMeta["max_rank"] > bounds.last:
|
|
547
549
|
lastPageRank = seqMeta["max_rank"] - limit
|
|
548
|
-
if lastPageRank < bounds.
|
|
549
|
-
lastPageRank = bounds.
|
|
550
|
+
if lastPageRank < bounds.last:
|
|
551
|
+
lastPageRank = bounds.last
|
|
550
552
|
|
|
551
553
|
links.append(
|
|
552
554
|
{
|
|
@@ -573,26 +575,8 @@ def getCollectionItems(collectionId):
|
|
|
573
575
|
)
|
|
574
576
|
|
|
575
577
|
|
|
576
|
-
def _getPictureItemById(
|
|
577
|
-
"""Get a picture metadata by its ID
|
|
578
|
-
|
|
579
|
-
---
|
|
580
|
-
tags:
|
|
581
|
-
- Pictures
|
|
582
|
-
parameters:
|
|
583
|
-
- name: collectionId
|
|
584
|
-
in: path
|
|
585
|
-
description: ID of collection to retrieve
|
|
586
|
-
required: true
|
|
587
|
-
schema:
|
|
588
|
-
type: string
|
|
589
|
-
- name: itemId
|
|
590
|
-
in: path
|
|
591
|
-
description: ID of item to retrieve
|
|
592
|
-
required: true
|
|
593
|
-
schema:
|
|
594
|
-
type: string
|
|
595
|
-
"""
|
|
578
|
+
def _getPictureItemById(itemId: UUID, account: Optional[Account]):
|
|
579
|
+
"""Get a picture metadata by its ID"""
|
|
596
580
|
with current_app.pool.connection() as conn:
|
|
597
581
|
with conn.cursor(row_factory=dict_row) as cursor:
|
|
598
582
|
# Check if there is a logged user
|
|
@@ -601,26 +585,22 @@ def _getPictureItemById(collectionId, itemId):
|
|
|
601
585
|
|
|
602
586
|
# Get rank + position of wanted picture
|
|
603
587
|
record = cursor.execute(
|
|
604
|
-
"""
|
|
588
|
+
"""WITH seq AS (
|
|
589
|
+
SELECT seq_id FROM sequences_pictures WHERE pic_id = %(pic)s LIMIT 1
|
|
590
|
+
)
|
|
605
591
|
SELECT
|
|
606
|
-
p.id, sp.rank, ST_AsGeoJSON(p.geom)::json AS geojson, p.heading, p.ts, p.metadata,
|
|
607
|
-
p.inserted_at, p.status, accounts.name AS account_name,
|
|
592
|
+
p.id, sp.seq_id, sp.rank, ST_AsGeoJSON(p.geom)::json AS geojson, p.heading, p.ts, p.metadata,
|
|
593
|
+
p.inserted_at, p.updated_at, p.status, accounts.name AS account_name,
|
|
608
594
|
p.account_id AS account_id,
|
|
609
595
|
spl.prevpic, spl.prevpicgeojson, spl.nextpic, spl.nextpicgeojson, p.exif,
|
|
610
596
|
relp.related_pics, p.gps_accuracy_m, p.h_pixel_density,
|
|
611
|
-
|
|
597
|
+
get_picture_semantics(p.id) as semantics,
|
|
598
|
+
get_picture_annotations(p.id) as annotations,
|
|
599
|
+
COALESCE(seq_sem.semantics, '[]'::json) AS sequence_semantics
|
|
612
600
|
FROM pictures p
|
|
613
601
|
JOIN sequences_pictures sp ON sp.pic_id = p.id
|
|
614
602
|
JOIN accounts ON p.account_id = accounts.id
|
|
615
603
|
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
604
|
LEFT JOIN (
|
|
625
605
|
SELECT
|
|
626
606
|
p.id,
|
|
@@ -631,7 +611,7 @@ def _getPictureItemById(collectionId, itemId):
|
|
|
631
611
|
FROM pictures p
|
|
632
612
|
JOIN sequences_pictures sp ON p.id = sp.pic_id
|
|
633
613
|
WHERE
|
|
634
|
-
sp.seq_id
|
|
614
|
+
sp.seq_id IN (SELECT seq_id FROM seq)
|
|
635
615
|
AND (p.account_id = %(acc)s OR p.status != 'hidden')
|
|
636
616
|
WINDOW othpics AS (PARTITION BY sp.seq_id ORDER BY sp.rank)
|
|
637
617
|
) spl ON p.id = spl.id
|
|
@@ -659,7 +639,7 @@ def _getPictureItemById(collectionId, itemId):
|
|
|
659
639
|
AND relp.status != 'waiting-for-delete'
|
|
660
640
|
AND relp.id != p.id
|
|
661
641
|
AND relsp.pic_id = relp.id
|
|
662
|
-
AND relsp.seq_id
|
|
642
|
+
AND relsp.seq_id NOT IN (SELECT seq_id FROM seq)
|
|
663
643
|
AND (
|
|
664
644
|
p.metadata->>'type' = 'equirectangular'
|
|
665
645
|
OR (relp.heading IS NULL OR p.heading IS NULL)
|
|
@@ -672,19 +652,27 @@ def _getPictureItemById(collectionId, itemId):
|
|
|
672
652
|
ORDER BY relsp.seq_id, p.geom <-> relp.geom
|
|
673
653
|
) a
|
|
674
654
|
) relp ON TRUE
|
|
675
|
-
|
|
655
|
+
LEFT JOIN (
|
|
656
|
+
SELECT sequence_id, json_agg(json_strip_nulls(json_build_object(
|
|
657
|
+
'key', key,
|
|
658
|
+
'value', value
|
|
659
|
+
)) ORDER BY key, value) AS semantics
|
|
660
|
+
FROM sequences_semantics
|
|
661
|
+
GROUP BY sequence_id
|
|
662
|
+
) seq_sem ON seq_sem.sequence_id = s.id
|
|
663
|
+
WHERE sp.seq_id IN (SELECT seq_id FROM seq)
|
|
676
664
|
AND p.id = %(pic)s
|
|
677
665
|
AND (p.account_id = %(acc)s OR p.status != 'hidden')
|
|
678
666
|
AND (s.status != 'hidden' OR s.account_id = %(acc)s)
|
|
679
667
|
AND s.status != 'deleted'
|
|
680
668
|
""",
|
|
681
|
-
{"
|
|
669
|
+
{"pic": itemId, "acc": accountId},
|
|
682
670
|
).fetchone()
|
|
683
671
|
|
|
684
672
|
if record is None:
|
|
685
673
|
return None
|
|
686
674
|
|
|
687
|
-
return dbPictureToStacItem(
|
|
675
|
+
return dbPictureToStacItem(record)
|
|
688
676
|
|
|
689
677
|
|
|
690
678
|
@bp.route("/collections/<uuid:collectionId>/items/<uuid:itemId>")
|
|
@@ -720,12 +708,12 @@ def getCollectionItem(collectionId, itemId):
|
|
|
720
708
|
schema:
|
|
721
709
|
$ref: '#/components/schemas/GeoVisioItem'
|
|
722
710
|
"""
|
|
711
|
+
account = auth.get_current_account()
|
|
723
712
|
|
|
724
|
-
stacItem = _getPictureItemById(
|
|
713
|
+
stacItem = _getPictureItemById(itemId, account)
|
|
725
714
|
if stacItem is None:
|
|
726
715
|
raise errors.InvalidAPIUsage(_("Item doesn't exist"), status_code=404)
|
|
727
716
|
|
|
728
|
-
account = auth.get_current_account()
|
|
729
717
|
picStatusToHttpCode = {
|
|
730
718
|
"waiting-for-process": 102,
|
|
731
719
|
"ready": 200,
|
|
@@ -755,6 +743,8 @@ def searchItems():
|
|
|
755
743
|
- $ref: '#/components/parameters/GeoVisio_place_position'
|
|
756
744
|
- $ref: '#/components/parameters/GeoVisio_place_distance'
|
|
757
745
|
- $ref: '#/components/parameters/GeoVisio_place_fov_tolerance'
|
|
746
|
+
- $ref: '#/components/parameters/searchCQL2_filter'
|
|
747
|
+
- $ref: '#/components/parameters/GeoVisioSearchSortedBy'
|
|
758
748
|
post:
|
|
759
749
|
requestBody:
|
|
760
750
|
required: true
|
|
@@ -764,7 +754,11 @@ def searchItems():
|
|
|
764
754
|
$ref: '#/components/schemas/GeoVisioItemSearchBody'
|
|
765
755
|
responses:
|
|
766
756
|
200:
|
|
767
|
-
|
|
757
|
+
description: The search results
|
|
758
|
+
content:
|
|
759
|
+
application/geo+json:
|
|
760
|
+
schema:
|
|
761
|
+
$ref: '#/components/schemas/GeoVisioCollectionItems'
|
|
768
762
|
"""
|
|
769
763
|
|
|
770
764
|
account = auth.get_current_account()
|
|
@@ -772,7 +766,6 @@ def searchItems():
|
|
|
772
766
|
sqlWhere = [SQL("(p.status = 'ready' OR p.account_id = %(account)s)"), SQL("(is_sequence_visible_by_user(s, %(account)s))")]
|
|
773
767
|
sqlParams: Dict[str, Any] = {"account": accountId}
|
|
774
768
|
sqlSubQueryWhere = [SQL("(p.status = 'ready' OR p.account_id = %(account)s)")]
|
|
775
|
-
order_by = SQL("")
|
|
776
769
|
|
|
777
770
|
#
|
|
778
771
|
# Parameters parsing and verification
|
|
@@ -788,14 +781,16 @@ def searchItems():
|
|
|
788
781
|
args = request.args
|
|
789
782
|
|
|
790
783
|
# Limit
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
784
|
+
limit = args.get("limit") or 10
|
|
785
|
+
try:
|
|
786
|
+
limit = int(limit)
|
|
787
|
+
if limit < 1 or limit > 10000:
|
|
788
|
+
raise ValueError()
|
|
789
|
+
except ValueError:
|
|
790
|
+
raise errors.InvalidAPIUsage(_("Parameter limit must be either empty or a number between 1 and 10000"), status_code=400)
|
|
791
|
+
sqlParams["limit"] = limit
|
|
792
|
+
|
|
793
|
+
sort_by = parse_item_sortby(args.get("sortby"))
|
|
799
794
|
|
|
800
795
|
# Bounding box
|
|
801
796
|
bboxarg = parse_bbox(args.getlist("bbox"))
|
|
@@ -806,7 +801,16 @@ def searchItems():
|
|
|
806
801
|
sqlParams["maxx"] = bboxarg[2]
|
|
807
802
|
sqlParams["maxy"] = bboxarg[3]
|
|
808
803
|
# if we search by bbox, we'll give first the items near the center of the bounding box
|
|
809
|
-
|
|
804
|
+
if not sort_by:
|
|
805
|
+
sort_by = SortBy(
|
|
806
|
+
fields=[
|
|
807
|
+
ItemSortByField(
|
|
808
|
+
field=SortableItemField.distance_to,
|
|
809
|
+
direction=SQLDirection.ASC,
|
|
810
|
+
obj_to_compare=SQL("ST_Centroid(ST_MakeEnvelope(%(minx)s, %(miny)s, %(maxx)s, %(maxy)s, 4326))"),
|
|
811
|
+
),
|
|
812
|
+
]
|
|
813
|
+
)
|
|
810
814
|
|
|
811
815
|
# Datetime
|
|
812
816
|
min_dt, max_dt = parse_datetime_interval(args.get("datetime"))
|
|
@@ -862,7 +866,16 @@ def searchItems():
|
|
|
862
866
|
)
|
|
863
867
|
|
|
864
868
|
# Sort pictures by nearest to POI
|
|
865
|
-
|
|
869
|
+
if not sort_by:
|
|
870
|
+
sort_by = SortBy(
|
|
871
|
+
fields=[
|
|
872
|
+
ItemSortByField(
|
|
873
|
+
field=SortableItemField.distance_to,
|
|
874
|
+
direction=SQLDirection.ASC,
|
|
875
|
+
obj_to_compare=SQL("ST_Point(%(placex)s, %(placey)s, 4326)"),
|
|
876
|
+
),
|
|
877
|
+
]
|
|
878
|
+
)
|
|
866
879
|
|
|
867
880
|
# Intersects
|
|
868
881
|
if args.get("intersects") is not None:
|
|
@@ -877,7 +890,16 @@ def searchItems():
|
|
|
877
890
|
sqlWhere.append(SQL("ST_Intersects(p.geom, ST_GeomFromGeoJSON(%(geom)s))"))
|
|
878
891
|
sqlParams["geom"] = Jsonb(intersects)
|
|
879
892
|
# if we search by bbox, we'll give first the items near the center of the bounding box
|
|
880
|
-
|
|
893
|
+
if not sort_by:
|
|
894
|
+
sort_by = SortBy(
|
|
895
|
+
fields=[
|
|
896
|
+
ItemSortByField(
|
|
897
|
+
field=SortableItemField.distance_to,
|
|
898
|
+
direction=SQLDirection.ASC,
|
|
899
|
+
obj_to_compare=SQL("ST_Centroid(ST_GeomFromGeoJSON(%(geom)s))"),
|
|
900
|
+
),
|
|
901
|
+
]
|
|
902
|
+
)
|
|
881
903
|
|
|
882
904
|
# Ids
|
|
883
905
|
if args.get("ids") is not None:
|
|
@@ -900,23 +922,35 @@ def searchItems():
|
|
|
900
922
|
raise errors.InvalidAPIUsage(_("Parameter collections should be a JSON array of strings"), status_code=400)
|
|
901
923
|
|
|
902
924
|
# 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
|
|
925
|
+
if args.get("ids") is not None:
|
|
904
926
|
ids = parse_list(args.get("ids"), paramName="ids")
|
|
905
927
|
if ids and len(ids) == 1:
|
|
906
928
|
picture_id = ids[0]
|
|
907
929
|
|
|
908
930
|
with current_app.pool.connection() as conn, conn.cursor() as cursor:
|
|
909
|
-
|
|
910
|
-
if not seq:
|
|
911
|
-
raise errors.InvalidAPIUsage(_("Picture doesn't exist"), status_code=404)
|
|
912
|
-
|
|
913
|
-
item = _getPictureItemById(seq[0], UUID(picture_id))
|
|
931
|
+
item = _getPictureItemById(UUID(picture_id), account)
|
|
914
932
|
features = [item] if item else []
|
|
915
933
|
return (
|
|
916
934
|
{"type": "FeatureCollection", "features": features, "links": [get_root_link()]},
|
|
917
935
|
200,
|
|
918
936
|
{"Content-Type": "application/geo+json"},
|
|
919
937
|
)
|
|
938
|
+
filter_param = args.get("filter")
|
|
939
|
+
if filter_param is not None:
|
|
940
|
+
cql_filter = parse_search_filter(filter_param)
|
|
941
|
+
if cql_filter is not None:
|
|
942
|
+
sqlWhere.append(cql_filter)
|
|
943
|
+
|
|
944
|
+
if not sort_by:
|
|
945
|
+
# by default we sort by last updated (and id in case of equalities)
|
|
946
|
+
sort_by = SortBy(
|
|
947
|
+
fields=[
|
|
948
|
+
ItemSortByField(field=SortableItemField.updated, direction=SQLDirection.DESC),
|
|
949
|
+
ItemSortByField(field=SortableItemField.id, direction=SQLDirection.ASC),
|
|
950
|
+
]
|
|
951
|
+
)
|
|
952
|
+
|
|
953
|
+
order_by = sort_by.to_sql()
|
|
920
954
|
|
|
921
955
|
#
|
|
922
956
|
# Database query
|
|
@@ -926,25 +960,27 @@ def searchItems():
|
|
|
926
960
|
"""
|
|
927
961
|
SELECT * FROM (
|
|
928
962
|
SELECT
|
|
929
|
-
p.id, p.ts, p.heading, p.metadata, p.inserted_at,
|
|
963
|
+
p.id, p.ts, p.heading, p.metadata, p.inserted_at, p.updated_at,
|
|
930
964
|
ST_AsGeoJSON(p.geom)::json AS geojson,
|
|
931
965
|
sp.seq_id, sp.rank AS rank,
|
|
932
966
|
accounts.name AS account_name,
|
|
933
967
|
p.account_id AS account_id,
|
|
934
968
|
p.exif, p.gps_accuracy_m, p.h_pixel_density,
|
|
935
|
-
|
|
969
|
+
get_picture_semantics(p.id) as semantics,
|
|
970
|
+
get_picture_annotations(p.id) as annotations,
|
|
971
|
+
COALESCE(seq_sem.semantics, '[]'::json) AS sequence_semantics
|
|
936
972
|
FROM pictures p
|
|
937
973
|
LEFT JOIN sequences_pictures sp ON p.id = sp.pic_id
|
|
938
974
|
LEFT JOIN sequences s ON s.id = sp.seq_id
|
|
939
975
|
LEFT JOIN accounts ON p.account_id = accounts.id
|
|
940
976
|
LEFT JOIN (
|
|
941
|
-
SELECT
|
|
977
|
+
SELECT sequence_id, json_agg(json_strip_nulls(json_build_object(
|
|
942
978
|
'key', key,
|
|
943
979
|
'value', value
|
|
944
|
-
))) AS semantics
|
|
945
|
-
FROM
|
|
946
|
-
GROUP BY
|
|
947
|
-
)
|
|
980
|
+
)) ORDER BY key, value) AS semantics
|
|
981
|
+
FROM sequences_semantics
|
|
982
|
+
GROUP BY sequence_id
|
|
983
|
+
) seq_sem ON seq_sem.sequence_id = s.id
|
|
948
984
|
WHERE {sqlWhere}
|
|
949
985
|
{orderBy}
|
|
950
986
|
LIMIT %(limit)s
|
|
@@ -967,13 +1003,14 @@ LEFT JOIN LATERAL (
|
|
|
967
1003
|
ORDER BY sp.rank ASC
|
|
968
1004
|
LIMIT 1
|
|
969
1005
|
) next on true
|
|
1006
|
+
|
|
970
1007
|
;
|
|
971
1008
|
"""
|
|
972
1009
|
).format(sqlWhere=SQL(" AND ").join(sqlWhere), sqlSubQueryWhere=SQL(" AND ").join(sqlSubQueryWhere), orderBy=order_by)
|
|
973
1010
|
|
|
974
1011
|
records = cursor.execute(query, sqlParams)
|
|
975
1012
|
|
|
976
|
-
items = [dbPictureToStacItem(
|
|
1013
|
+
items = [dbPictureToStacItem(dbPic) for dbPic in records]
|
|
977
1014
|
|
|
978
1015
|
return (
|
|
979
1016
|
{
|
|
@@ -991,7 +1028,9 @@ LEFT JOIN LATERAL (
|
|
|
991
1028
|
@bp.route("/collections/<uuid:collectionId>/items", methods=["POST"])
|
|
992
1029
|
@auth.login_required_by_setting("API_FORCE_AUTH_ON_UPLOAD")
|
|
993
1030
|
def postCollectionItem(collectionId, account=None):
|
|
994
|
-
"""Add a new picture in a given sequence
|
|
1031
|
+
"""Add a new picture in a given sequence.
|
|
1032
|
+
|
|
1033
|
+
Note that this is the legacy API, upload should be done using the [UploadSet](#UploadSet) endpoints if possible.
|
|
995
1034
|
---
|
|
996
1035
|
tags:
|
|
997
1036
|
- Upload
|
|
@@ -1017,6 +1056,42 @@ def postCollectionItem(collectionId, account=None):
|
|
|
1017
1056
|
application/geo+json:
|
|
1018
1057
|
schema:
|
|
1019
1058
|
$ref: '#/components/schemas/GeoVisioItem'
|
|
1059
|
+
400:
|
|
1060
|
+
description: Error if the request is malformed
|
|
1061
|
+
content:
|
|
1062
|
+
application/json:
|
|
1063
|
+
schema:
|
|
1064
|
+
$ref: '#/components/schemas/GeoVisioError'
|
|
1065
|
+
401:
|
|
1066
|
+
description: Error if you're not logged in
|
|
1067
|
+
content:
|
|
1068
|
+
application/json:
|
|
1069
|
+
schema:
|
|
1070
|
+
$ref: '#/components/schemas/GeoVisioError'
|
|
1071
|
+
403:
|
|
1072
|
+
description: Error if you're not authorized to add picture to this collection
|
|
1073
|
+
content:
|
|
1074
|
+
application/json:
|
|
1075
|
+
schema:
|
|
1076
|
+
$ref: '#/components/schemas/GeoVisioError'
|
|
1077
|
+
404:
|
|
1078
|
+
description: Error if the collection doesn't exist
|
|
1079
|
+
content:
|
|
1080
|
+
application/json:
|
|
1081
|
+
schema:
|
|
1082
|
+
$ref: '#/components/schemas/GeoVisioError'
|
|
1083
|
+
409:
|
|
1084
|
+
description: Error if a picture (named `item` in the API) has already been added in the same index (named `position` in the API) in this collection
|
|
1085
|
+
content:
|
|
1086
|
+
application/json:
|
|
1087
|
+
schema:
|
|
1088
|
+
$ref: '#/components/schemas/GeoVisioError'
|
|
1089
|
+
415:
|
|
1090
|
+
description: Error if the content type is not multipart/form-data
|
|
1091
|
+
content:
|
|
1092
|
+
application/json:
|
|
1093
|
+
schema:
|
|
1094
|
+
$ref: '#/components/schemas/GeoVisioError'
|
|
1020
1095
|
"""
|
|
1021
1096
|
|
|
1022
1097
|
if not request.headers.get("Content-Type", "").startswith("multipart/form-data"):
|
|
@@ -1124,7 +1199,7 @@ def postCollectionItem(collectionId, account=None):
|
|
|
1124
1199
|
conn, collectionId, position, updated_picture, accountId, additionalMetadata, lang=get_locale().language
|
|
1125
1200
|
)
|
|
1126
1201
|
except utils.pictures.PicturePositionConflict:
|
|
1127
|
-
raise errors.InvalidAPIUsage(_("
|
|
1202
|
+
raise errors.InvalidAPIUsage(_("There is already a picture with the same index in the sequence"), status_code=409)
|
|
1128
1203
|
except utils.pictures.MetadataReadingError as e:
|
|
1129
1204
|
raise errors.InvalidAPIUsage(_("Impossible to parse picture metadata"), payload={"details": {"error": e.details}})
|
|
1130
1205
|
except utils.pictures.InvalidMetadataValue as e:
|
|
@@ -1232,60 +1307,8 @@ class PatchItemParameter(BaseModel):
|
|
|
1232
1307
|
return self.model_fields_set == {"semantics"}
|
|
1233
1308
|
|
|
1234
1309
|
|
|
1235
|
-
|
|
1236
|
-
@auth.login_required()
|
|
1237
|
-
def patchCollectionItem(collectionId, itemId, account):
|
|
1238
|
-
"""Edits properties of an existing picture
|
|
1239
|
-
|
|
1240
|
-
Note that tags cannot be added as form-data for the moment, only as JSON.
|
|
1241
|
-
|
|
1242
|
-
Note that there are rules on the editing of a picture's metadata:
|
|
1243
|
-
|
|
1244
|
-
- Only the owner of a picture can change its visibility
|
|
1245
|
-
- For core metadata (heading, capture_time, position, longitude, latitude), the owner can restrict their change by other accounts (see `collaborative_metadata` field in `/api/users/me`) and if not explicitly defined by the user, the instance's default value is used.
|
|
1246
|
-
- Everyone can add/edit/delete semantics tags.
|
|
1247
|
-
---
|
|
1248
|
-
tags:
|
|
1249
|
-
- Editing
|
|
1250
|
-
- Tags
|
|
1251
|
-
parameters:
|
|
1252
|
-
- name: collectionId
|
|
1253
|
-
in: path
|
|
1254
|
-
description: ID of sequence the picture belongs to
|
|
1255
|
-
required: true
|
|
1256
|
-
schema:
|
|
1257
|
-
type: string
|
|
1258
|
-
- name: itemId
|
|
1259
|
-
in: path
|
|
1260
|
-
description: ID of picture to edit
|
|
1261
|
-
required: true
|
|
1262
|
-
schema:
|
|
1263
|
-
type: string
|
|
1264
|
-
requestBody:
|
|
1265
|
-
content:
|
|
1266
|
-
application/json:
|
|
1267
|
-
schema:
|
|
1268
|
-
$ref: '#/components/schemas/GeoVisioPatchItem'
|
|
1269
|
-
application/x-www-form-urlencoded:
|
|
1270
|
-
schema:
|
|
1271
|
-
$ref: '#/components/schemas/GeoVisioPatchItem'
|
|
1272
|
-
multipart/form-data:
|
|
1273
|
-
schema:
|
|
1274
|
-
$ref: '#/components/schemas/GeoVisioPatchItem'
|
|
1275
|
-
security:
|
|
1276
|
-
- bearerToken: []
|
|
1277
|
-
- cookieAuth: []
|
|
1278
|
-
responses:
|
|
1279
|
-
200:
|
|
1280
|
-
description: the wanted item
|
|
1281
|
-
content:
|
|
1282
|
-
application/geo+json:
|
|
1283
|
-
schema:
|
|
1284
|
-
$ref: '#/components/schemas/GeoVisioItem'
|
|
1285
|
-
"""
|
|
1286
|
-
|
|
1310
|
+
def update_picture(itemId: UUID, account: Optional[Account]):
|
|
1287
1311
|
# Parse received parameters
|
|
1288
|
-
|
|
1289
1312
|
metadata = None
|
|
1290
1313
|
content_type = (request.headers.get("Content-Type") or "").split(";")[0]
|
|
1291
1314
|
|
|
@@ -1299,7 +1322,7 @@ def patchCollectionItem(collectionId, itemId, account):
|
|
|
1299
1322
|
|
|
1300
1323
|
# If no parameter is set
|
|
1301
1324
|
if metadata is None or not metadata.has_override():
|
|
1302
|
-
return
|
|
1325
|
+
return (_getPictureItemById(itemId, account), 304)
|
|
1303
1326
|
|
|
1304
1327
|
# Check if picture exists and if given account is authorized to edit
|
|
1305
1328
|
with db.conn(current_app) as conn:
|
|
@@ -1380,7 +1403,61 @@ WHERE id = %(id)s"""
|
|
|
1380
1403
|
)
|
|
1381
1404
|
|
|
1382
1405
|
# Redirect response to a classic GET
|
|
1383
|
-
return
|
|
1406
|
+
return (_getPictureItemById(itemId, account), 200)
|
|
1407
|
+
|
|
1408
|
+
|
|
1409
|
+
@bp.route("/collections/<uuid:collectionId>/items/<uuid:itemId>", methods=["PATCH"])
|
|
1410
|
+
@auth.login_required()
|
|
1411
|
+
def patchCollectionItem(collectionId, itemId, account):
|
|
1412
|
+
"""Edits properties of an existing picture
|
|
1413
|
+
|
|
1414
|
+
Note that tags cannot be added as form-data for the moment, only as JSON.
|
|
1415
|
+
|
|
1416
|
+
Note that there are rules on the editing of a picture's metadata:
|
|
1417
|
+
|
|
1418
|
+
- Only the owner of a picture can change its visibility
|
|
1419
|
+
- For core metadata (heading, capture_time, position, longitude, latitude), the owner can restrict their change by other accounts (see `collaborative_metadata` field in `/api/users/me`) and if not explicitly defined by the user, the instance's default value is used.
|
|
1420
|
+
- Everyone can add/edit/delete semantics tags.
|
|
1421
|
+
---
|
|
1422
|
+
tags:
|
|
1423
|
+
- Editing
|
|
1424
|
+
- Semantics
|
|
1425
|
+
parameters:
|
|
1426
|
+
- name: collectionId
|
|
1427
|
+
in: path
|
|
1428
|
+
description: ID of sequence the picture belongs to
|
|
1429
|
+
required: true
|
|
1430
|
+
schema:
|
|
1431
|
+
type: string
|
|
1432
|
+
- name: itemId
|
|
1433
|
+
in: path
|
|
1434
|
+
description: ID of picture to edit
|
|
1435
|
+
required: true
|
|
1436
|
+
schema:
|
|
1437
|
+
type: string
|
|
1438
|
+
requestBody:
|
|
1439
|
+
content:
|
|
1440
|
+
application/json:
|
|
1441
|
+
schema:
|
|
1442
|
+
$ref: '#/components/schemas/GeoVisioPatchItem'
|
|
1443
|
+
application/x-www-form-urlencoded:
|
|
1444
|
+
schema:
|
|
1445
|
+
$ref: '#/components/schemas/GeoVisioPatchItem'
|
|
1446
|
+
multipart/form-data:
|
|
1447
|
+
schema:
|
|
1448
|
+
$ref: '#/components/schemas/GeoVisioPatchItem'
|
|
1449
|
+
security:
|
|
1450
|
+
- bearerToken: []
|
|
1451
|
+
- cookieAuth: []
|
|
1452
|
+
responses:
|
|
1453
|
+
200:
|
|
1454
|
+
description: the wanted item
|
|
1455
|
+
content:
|
|
1456
|
+
application/geo+json:
|
|
1457
|
+
schema:
|
|
1458
|
+
$ref: '#/components/schemas/GeoVisioItem'
|
|
1459
|
+
"""
|
|
1460
|
+
return update_picture(itemId, account)
|
|
1384
1461
|
|
|
1385
1462
|
|
|
1386
1463
|
@bp.route("/collections/<uuid:collectionId>/items/<uuid:itemId>", methods=["DELETE"])
|