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/collections.py
CHANGED
|
@@ -1,23 +1,24 @@
|
|
|
1
1
|
from enum import Enum
|
|
2
|
-
from attr import dataclass
|
|
3
2
|
from geovisio import errors, utils, db
|
|
4
3
|
from geovisio.utils import auth, sequences
|
|
5
4
|
from geovisio.utils.params import validation_error
|
|
6
|
-
from geovisio.utils.semantics import
|
|
5
|
+
from geovisio.utils.semantics import Entity, EntityType, update_tags
|
|
6
|
+
from geovisio.utils.tags import SemanticTagUpdate
|
|
7
7
|
from geovisio.web.params import (
|
|
8
8
|
parse_datetime,
|
|
9
9
|
parse_datetime_interval,
|
|
10
10
|
parse_bbox,
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
parse_collection_filter,
|
|
12
|
+
parse_collection_sortby,
|
|
13
13
|
parse_collections_limit,
|
|
14
14
|
)
|
|
15
15
|
from geovisio.utils.sequences import (
|
|
16
16
|
STAC_FIELD_MAPPINGS,
|
|
17
17
|
CollectionsRequest,
|
|
18
18
|
get_collections,
|
|
19
|
+
get_dataset_bounds,
|
|
19
20
|
)
|
|
20
|
-
from geovisio.utils.fields import SortBy, SortByField, SQLDirection,
|
|
21
|
+
from geovisio.utils.fields import SortBy, SortByField, SQLDirection, BBox, parse_relative_heading
|
|
21
22
|
from geovisio.web.rss import dbSequencesToGeoRSS
|
|
22
23
|
from psycopg.rows import dict_row
|
|
23
24
|
from psycopg.sql import SQL
|
|
@@ -94,7 +95,7 @@ def dbSequenceToStacCollection(dbSeq, description="A sequence of geolocated pict
|
|
|
94
95
|
"title": str(dbSeq["name"]),
|
|
95
96
|
"description": description,
|
|
96
97
|
"keywords": ["pictures", str(dbSeq["name"])],
|
|
97
|
-
"semantics": dbSeq
|
|
98
|
+
"semantics": dbSeq.get("semantics", []),
|
|
98
99
|
"license": current_app.config["API_PICTURES_LICENSE_SPDX_ID"],
|
|
99
100
|
"created": dbTsToStac(dbSeq["created"]),
|
|
100
101
|
"updated": dbTsToStac(dbSeq.get("updated")),
|
|
@@ -157,6 +158,16 @@ def dbSequenceToStacCollection(dbSeq, description="A sequence of geolocated pict
|
|
|
157
158
|
"href": url_for("stac_collections.getCollection", _external=True, collectionId=dbSeq["id"]),
|
|
158
159
|
},
|
|
159
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
|
+
),
|
|
160
171
|
]
|
|
161
172
|
),
|
|
162
173
|
}
|
|
@@ -244,16 +255,22 @@ def getAllCollections():
|
|
|
244
255
|
format = "rss"
|
|
245
256
|
|
|
246
257
|
# Sort-by parameter
|
|
247
|
-
sortBy =
|
|
258
|
+
sortBy = parse_collection_sortby(request.args.get("sortby"))
|
|
248
259
|
if not sortBy:
|
|
249
260
|
direction = SQLDirection.DESC if format == "rss" else SQLDirection.ASC
|
|
250
261
|
sortBy = SortBy(fields=[SortByField(field=STAC_FIELD_MAPPINGS["created"], direction=direction)])
|
|
262
|
+
# 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
|
|
263
|
+
# we'll also get
|
|
264
|
+
if not any(s.field == STAC_FIELD_MAPPINGS["created"] for s in sortBy.fields):
|
|
265
|
+
sortBy.fields.append(SortByField(field=STAC_FIELD_MAPPINGS["created"], direction=SQLDirection.ASC))
|
|
266
|
+
if not any(s.field == STAC_FIELD_MAPPINGS["id"] for s in sortBy.fields):
|
|
267
|
+
sortBy.fields.append(SortByField(field=STAC_FIELD_MAPPINGS["id"], direction=SQLDirection.ASC))
|
|
251
268
|
|
|
252
269
|
collection_request = CollectionsRequest(sort_by=sortBy)
|
|
253
270
|
|
|
254
271
|
# Filter parameter
|
|
255
|
-
collection_request.user_filter =
|
|
256
|
-
collection_request.pagination_filter =
|
|
272
|
+
collection_request.user_filter = parse_collection_filter(request.args.get("filter"))
|
|
273
|
+
collection_request.pagination_filter = parse_collection_filter(request.args.get("page"))
|
|
257
274
|
|
|
258
275
|
# Limit parameter
|
|
259
276
|
collection_request.limit = parse_collections_limit(request.args.get("limit"))
|
|
@@ -298,14 +315,14 @@ def getAllCollections():
|
|
|
298
315
|
},
|
|
299
316
|
]
|
|
300
317
|
|
|
301
|
-
with db.
|
|
302
|
-
|
|
303
|
-
if
|
|
318
|
+
with db.conn(current_app) as conn:
|
|
319
|
+
datasetBounds = get_dataset_bounds(conn, collection_request.sort_by, additional_filters=collection_request.user_filter)
|
|
320
|
+
if datasetBounds is None:
|
|
304
321
|
return ({"collections": [], "links": links}, 200, {"Content-Type": "application/json"})
|
|
305
|
-
|
|
306
|
-
if collection_request.created_after and collection_request.created_after > datasetBounds.
|
|
322
|
+
creation_date_index = collection_request.sort_by.get_field_index("created")
|
|
323
|
+
if collection_request.created_after and collection_request.created_after > datasetBounds.last[creation_date_index]:
|
|
307
324
|
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.
|
|
325
|
+
if collection_request.created_before and collection_request.created_before < datasetBounds.first[creation_date_index]:
|
|
309
326
|
raise errors.InvalidAPIUsage(_("There is no collection created before %(d)s", d=collection_request.created_before))
|
|
310
327
|
|
|
311
328
|
db_collections = get_collections(collection_request)
|
|
@@ -322,10 +339,9 @@ def getAllCollections():
|
|
|
322
339
|
pagination_links = sequences.get_pagination_links(
|
|
323
340
|
route="stac_collections.getAllCollections",
|
|
324
341
|
routeArgs={"limit": collection_request.limit},
|
|
325
|
-
|
|
326
|
-
direction=sortBy.fields[0].direction,
|
|
342
|
+
sortBy=sortBy,
|
|
327
343
|
datasetBounds=datasetBounds,
|
|
328
|
-
dataBounds=db_collections.
|
|
344
|
+
dataBounds=db_collections.query_bounds,
|
|
329
345
|
additional_filters=additional_filters,
|
|
330
346
|
)
|
|
331
347
|
|
|
@@ -374,8 +390,7 @@ def getCollection(collectionId):
|
|
|
374
390
|
|
|
375
391
|
record = db.fetchone(
|
|
376
392
|
current_app,
|
|
377
|
-
"""
|
|
378
|
-
SELECT
|
|
393
|
+
"""SELECT
|
|
379
394
|
s.id,
|
|
380
395
|
s.metadata->>'title' AS name,
|
|
381
396
|
ST_XMin(s.bbox) AS minx,
|
|
@@ -385,6 +400,7 @@ def getCollection(collectionId):
|
|
|
385
400
|
s.status AS status,
|
|
386
401
|
accounts.name AS account_name,
|
|
387
402
|
s.account_id AS account_id,
|
|
403
|
+
s.upload_set_id,
|
|
388
404
|
s.inserted_at AS created,
|
|
389
405
|
s.updated_at AS updated,
|
|
390
406
|
s.current_sort AS current_sort,
|
|
@@ -396,16 +412,16 @@ def getCollection(collectionId):
|
|
|
396
412
|
ROUND(ST_Length(s.geom::geography)) / 1000 as length_km,
|
|
397
413
|
s.computed_h_pixel_density,
|
|
398
414
|
s.computed_gps_accuracy,
|
|
399
|
-
|
|
415
|
+
COALESCE(seq_sem.semantics, '[]'::json) AS semantics
|
|
400
416
|
FROM sequences s
|
|
401
417
|
LEFT JOIN (
|
|
402
418
|
SELECT sequence_id, json_agg(json_strip_nulls(json_build_object(
|
|
403
419
|
'key', key,
|
|
404
420
|
'value', value
|
|
405
|
-
))) AS semantics
|
|
421
|
+
)) ORDER BY key, value) AS semantics
|
|
406
422
|
FROM sequences_semantics
|
|
407
423
|
GROUP BY sequence_id
|
|
408
|
-
)
|
|
424
|
+
) seq_sem ON seq_sem.sequence_id = s.id
|
|
409
425
|
JOIN accounts ON s.account_id = accounts.id, (
|
|
410
426
|
SELECT
|
|
411
427
|
array_agg(DISTINCT jsonb_build_object(
|
|
@@ -494,6 +510,10 @@ def getCollectionThumbnail(collectionId):
|
|
|
494
510
|
@auth.login_required_by_setting("API_FORCE_AUTH_ON_UPLOAD")
|
|
495
511
|
def postCollection(account=None):
|
|
496
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, ...
|
|
497
517
|
---
|
|
498
518
|
tags:
|
|
499
519
|
- Upload
|
|
@@ -503,7 +523,7 @@ def postCollection(account=None):
|
|
|
503
523
|
required: false
|
|
504
524
|
schema:
|
|
505
525
|
type: string
|
|
506
|
-
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"
|
|
507
527
|
requestBody:
|
|
508
528
|
content:
|
|
509
529
|
application/json:
|
|
@@ -557,7 +577,7 @@ class PatchCollectionParameter(BaseModel):
|
|
|
557
577
|
"""Parameters used to add an item to an UploadSet"""
|
|
558
578
|
|
|
559
579
|
relative_heading: Optional[int] = None
|
|
560
|
-
"""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."""
|
|
561
581
|
visible: Optional[bool] = None
|
|
562
582
|
"""Should the sequence be publicly visible ?"""
|
|
563
583
|
title: Optional[str] = Field(max_length=250, default=None)
|
|
@@ -602,15 +622,7 @@ If unset, sort order is unchanged."""
|
|
|
602
622
|
@field_validator("relative_heading", mode="before")
|
|
603
623
|
@classmethod
|
|
604
624
|
def parse_relative_heading(cls, value):
|
|
605
|
-
|
|
606
|
-
relHeading = int(value)
|
|
607
|
-
if relHeading < -180 or relHeading > 180:
|
|
608
|
-
raise ValueError()
|
|
609
|
-
return relHeading
|
|
610
|
-
except ValueError:
|
|
611
|
-
raise errors.InvalidAPIUsage(
|
|
612
|
-
_("Relative heading is not valid, should be an integer in degrees from -180 to 180"), status_code=400
|
|
613
|
-
)
|
|
625
|
+
return parse_relative_heading(value)
|
|
614
626
|
|
|
615
627
|
def has_only_semantics_updates(self):
|
|
616
628
|
return self.model_fields_set == {"semantics"}
|
|
@@ -629,7 +641,7 @@ def patchCollection(collectionId, account):
|
|
|
629
641
|
---
|
|
630
642
|
tags:
|
|
631
643
|
- Editing
|
|
632
|
-
-
|
|
644
|
+
- Semantics
|
|
633
645
|
parameters:
|
|
634
646
|
- name: collectionId
|
|
635
647
|
in: path
|
|
@@ -749,13 +761,7 @@ def patchCollection(collectionId, account):
|
|
|
749
761
|
sqlUpdates.append(SQL("last_account_to_edit = %(account)s"))
|
|
750
762
|
|
|
751
763
|
cursor.execute(
|
|
752
|
-
SQL(
|
|
753
|
-
"""
|
|
754
|
-
UPDATE sequences
|
|
755
|
-
SET {updates}
|
|
756
|
-
WHERE id = %(id)s
|
|
757
|
-
"""
|
|
758
|
-
).format(updates=SQL(", ").join(sqlUpdates)),
|
|
764
|
+
SQL("UPDATE sequences SET {updates} WHERE id = %(id)s").format(updates=SQL(", ").join(sqlUpdates)),
|
|
759
765
|
sqlParams,
|
|
760
766
|
)
|
|
761
767
|
|
|
@@ -924,7 +930,12 @@ def send_collections_as_csv(collection_request: CollectionsRequest):
|
|
|
924
930
|
raise errors.InvalidAPIUsage(_("CSV export does not support pagination"), status_code=400)
|
|
925
931
|
if collection_request.filters():
|
|
926
932
|
raise errors.InvalidAPIUsage(_("CSV export does not support filters"), status_code=400)
|
|
927
|
-
if collection_request.sort_by != SortBy(
|
|
933
|
+
if collection_request.sort_by != SortBy(
|
|
934
|
+
fields=[
|
|
935
|
+
SortByField(field=STAC_FIELD_MAPPINGS["created"], direction=SQLDirection.DESC),
|
|
936
|
+
SortByField(field=STAC_FIELD_MAPPINGS["id"], direction=SQLDirection.ASC),
|
|
937
|
+
]
|
|
938
|
+
):
|
|
928
939
|
raise errors.InvalidAPIUsage(_("CSV export does not support sorting by anything but creation date"), status_code=400)
|
|
929
940
|
|
|
930
941
|
def generate_csv():
|
|
@@ -957,7 +968,7 @@ SELECT
|
|
|
957
968
|
s.computed_gps_accuracy AS computed_gps_accuracy
|
|
958
969
|
FROM sequences s
|
|
959
970
|
WHERE {filter}
|
|
960
|
-
ORDER BY s.inserted_at DESC
|
|
971
|
+
ORDER BY s.inserted_at DESC, id ASC
|
|
961
972
|
) TO STDOUT CSV HEADER"""
|
|
962
973
|
).format(filter=SQL(" AND ").join(filters)),
|
|
963
974
|
params,
|
|
@@ -966,7 +977,7 @@ ORDER BY s.inserted_at DESC
|
|
|
966
977
|
for a in copy:
|
|
967
978
|
yield bytes(a)
|
|
968
979
|
|
|
969
|
-
return stream_with_context(generate_csv()), {"Content-Disposition": "attachment"}
|
|
980
|
+
return stream_with_context(generate_csv()), {"Content-Type": "text/csv", "Content-Disposition": "attachment"}
|
|
970
981
|
|
|
971
982
|
|
|
972
983
|
@bp.route("/users/<uuid:userId>/collection")
|
|
@@ -1029,17 +1040,21 @@ def getUserCollection(userId, userIdMatchesAccount=False):
|
|
|
1029
1040
|
format = "csv"
|
|
1030
1041
|
|
|
1031
1042
|
# Sort-by parameter
|
|
1032
|
-
sortBy =
|
|
1043
|
+
sortBy = parse_collection_sortby(request.args.get("sortby"))
|
|
1033
1044
|
if not sortBy:
|
|
1034
1045
|
sortBy = SortBy(fields=[SortByField(field=STAC_FIELD_MAPPINGS["created"], direction=SQLDirection.DESC)])
|
|
1035
1046
|
|
|
1047
|
+
if not any(s.field == STAC_FIELD_MAPPINGS["created"] for s in sortBy.fields):
|
|
1048
|
+
sortBy.fields.append(SortByField(field=STAC_FIELD_MAPPINGS["created"], direction=SQLDirection.ASC))
|
|
1049
|
+
if not any(s.field == STAC_FIELD_MAPPINGS["id"] for s in sortBy.fields):
|
|
1050
|
+
sortBy.fields.append(SortByField(field=STAC_FIELD_MAPPINGS["id"], direction=SQLDirection.ASC))
|
|
1036
1051
|
collection_request = CollectionsRequest(sort_by=sortBy, userOwnsAllCollections=userIdMatchesAccount)
|
|
1037
1052
|
|
|
1038
1053
|
# Filter parameter
|
|
1039
|
-
collection_request.user_filter =
|
|
1054
|
+
collection_request.user_filter = parse_collection_filter(request.args.get("filter"))
|
|
1040
1055
|
|
|
1041
1056
|
# Filters added by the pagination
|
|
1042
|
-
collection_request.pagination_filter =
|
|
1057
|
+
collection_request.pagination_filter = parse_collection_filter(request.args.get("page"))
|
|
1043
1058
|
|
|
1044
1059
|
# Limit parameter
|
|
1045
1060
|
# if not specified, the default with CSV it 1000. if there are more, the paginated API should be used
|
|
@@ -1095,8 +1110,6 @@ def getUserCollection(userId, userIdMatchesAccount=False):
|
|
|
1095
1110
|
MAX(LEAST(90, ST_YMax(s.bbox))) AS maxy,
|
|
1096
1111
|
MIN(s.inserted_at) AS created,
|
|
1097
1112
|
MAX(s.updated_at) AS updated,
|
|
1098
|
-
MIN({order_column}) AS min_order,
|
|
1099
|
-
MAX({order_column}) AS max_order,
|
|
1100
1113
|
ROUND(SUM(ST_Length(s.geom::geography))) / 1000 AS length_km
|
|
1101
1114
|
FROM sequences s
|
|
1102
1115
|
WHERE {filter}
|
|
@@ -1115,6 +1128,13 @@ def getUserCollection(userId, userIdMatchesAccount=False):
|
|
|
1115
1128
|
else:
|
|
1116
1129
|
raise errors.InvalidAPIUsage(_("No matching sequences found"), 404)
|
|
1117
1130
|
|
|
1131
|
+
datasetBounds = get_dataset_bounds(
|
|
1132
|
+
cursor.connection,
|
|
1133
|
+
collection_request.sort_by,
|
|
1134
|
+
additional_filters=SQL(" AND ").join(meta_filter),
|
|
1135
|
+
additional_filters_params={"account": userId},
|
|
1136
|
+
)
|
|
1137
|
+
|
|
1118
1138
|
collections = get_collections(collection_request)
|
|
1119
1139
|
|
|
1120
1140
|
sequences_links = [
|
|
@@ -1164,10 +1184,9 @@ def getUserCollection(userId, userIdMatchesAccount=False):
|
|
|
1164
1184
|
pagination_links = sequences.get_pagination_links(
|
|
1165
1185
|
route="stac_collections.getUserCollection",
|
|
1166
1186
|
routeArgs={"userId": str(userId), "limit": collection_request.limit},
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
dataBounds=collections.query_first_order_bounds,
|
|
1187
|
+
sortBy=sortBy,
|
|
1188
|
+
datasetBounds=datasetBounds,
|
|
1189
|
+
dataBounds=collections.query_bounds,
|
|
1171
1190
|
additional_filters=additional_filters,
|
|
1172
1191
|
)
|
|
1173
1192
|
|
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
|
|
@@ -29,13 +34,14 @@ def configuration():
|
|
|
29
34
|
"name": _get_translated(apiSum.name, userLang),
|
|
30
35
|
"description": _get_translated(apiSum.description, userLang),
|
|
31
36
|
"geo_coverage": _get_translated(apiSum.geo_coverage, userLang),
|
|
32
|
-
"logo": apiSum.logo,
|
|
37
|
+
"logo": str(apiSum.logo),
|
|
33
38
|
"color": str(apiSum.color),
|
|
34
39
|
"email": apiSum.email,
|
|
35
40
|
"auth": _auth_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
|
|
|
@@ -53,6 +59,7 @@ def _auth_configuration():
|
|
|
53
59
|
return {
|
|
54
60
|
"enabled": True,
|
|
55
61
|
"user_profile": {"url": auth.oauth_provider.user_profile_page_url()},
|
|
62
|
+
"registration_is_open": flask.current_app.config["API_REGISTRATION_IS_OPEN"],
|
|
56
63
|
"enforce_tos_acceptance": flask.current_app.config["API_ENFORCE_TOS_ACCEPTANCE"],
|
|
57
64
|
}
|
|
58
65
|
|
|
@@ -66,9 +73,23 @@ def _license_configuration():
|
|
|
66
73
|
|
|
67
74
|
|
|
68
75
|
def _get_pages():
|
|
69
|
-
from geovisio.utils import db
|
|
70
|
-
from flask import current_app
|
|
71
76
|
|
|
72
77
|
pages = db.fetchall(current_app, "SELECT distinct(name) FROM pages")
|
|
73
78
|
|
|
74
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()
|
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, annotations
|
|
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
|
|
|
@@ -240,6 +245,12 @@ Note that you may not rely only on these ID that could change through time.
|
|
|
240
245
|
"PreparationParameter": prepare.PreparationParameter.model_json_schema(
|
|
241
246
|
ref_template="#/components/schemas/PreparationParameter/$defs/{model}", mode="serialization"
|
|
242
247
|
),
|
|
248
|
+
"GeovisioPostToken": {
|
|
249
|
+
"type": "object",
|
|
250
|
+
"properties": {
|
|
251
|
+
"description": {"type": "string", "description": "optional description of the token"},
|
|
252
|
+
},
|
|
253
|
+
},
|
|
243
254
|
"GeoVisioPostUploadSet": upload_set.UploadSetCreationParameter.model_json_schema(
|
|
244
255
|
ref_template="#/components/schemas/GeoVisioPostUploadSet/$defs/{model}", mode="serialization"
|
|
245
256
|
),
|
|
@@ -258,6 +269,9 @@ Note that you may not rely only on these ID that could change through time.
|
|
|
258
269
|
"GeoVisioUploadSetFiles": upload_set_utils.UploadSetFiles.model_json_schema(
|
|
259
270
|
ref_template="#/components/schemas/GeoVisioUploadSetFiles/$defs/{model}", mode="serialization"
|
|
260
271
|
),
|
|
272
|
+
"UploadSetUpdateParameter": upload_set.UploadSetUpdateParameter.model_json_schema(
|
|
273
|
+
ref_template="#/components/schemas/UploadSetUpdateParameter/$defs/{model}", mode="serialization"
|
|
274
|
+
),
|
|
261
275
|
"GeoVisioCollectionOfCollection": {
|
|
262
276
|
"allOf": [
|
|
263
277
|
{"$ref": "#/components/schemas/STACCollection"},
|
|
@@ -295,7 +309,7 @@ Note that you may not rely only on these ID that could change through time.
|
|
|
295
309
|
},
|
|
296
310
|
"GeoVisioCSVCollections": {
|
|
297
311
|
"type": "string",
|
|
298
|
-
"
|
|
312
|
+
"description": f"""CSV file containing the collections.
|
|
299
313
|
|
|
300
314
|
The CSV headers will be:
|
|
301
315
|
* id: ID of the collection
|
|
@@ -456,12 +470,32 @@ The CSV headers will be:
|
|
|
456
470
|
"datetimetz": {
|
|
457
471
|
"type": "string",
|
|
458
472
|
"format": "date-time",
|
|
459
|
-
"title": "Date & time
|
|
473
|
+
"title": "Date & time of the picture (when it was captured).",
|
|
474
|
+
},
|
|
475
|
+
"datetimetz": {
|
|
476
|
+
"type": "string",
|
|
477
|
+
"format": "date-time",
|
|
478
|
+
"title": "Date & time of the picture (when it was captured) with original timezone information",
|
|
479
|
+
},
|
|
480
|
+
"created": {
|
|
481
|
+
"type": "string",
|
|
482
|
+
"format": "date-time",
|
|
483
|
+
"title": "Date & time of picture upload",
|
|
484
|
+
},
|
|
485
|
+
"updated": {
|
|
486
|
+
"type": "string",
|
|
487
|
+
"format": "date-time",
|
|
488
|
+
"title": "Date & time of picture's metadata update",
|
|
460
489
|
},
|
|
461
490
|
"geovisio:status": {"$ref": "#/components/schemas/GeoVisioItemStatus"},
|
|
462
491
|
"geovisio:producer": {"type": "string"},
|
|
463
492
|
"geovisio:image": {"type": "string", "format": "uri"},
|
|
464
493
|
"geovisio:thumbnail": {"type": "string", "format": "uri"},
|
|
494
|
+
"geovisio:rank_in_collection": {
|
|
495
|
+
"type": "integer",
|
|
496
|
+
"minimum": 1,
|
|
497
|
+
"title": "Rank of the picture in its collection.",
|
|
498
|
+
},
|
|
465
499
|
"original_file:size": {"type": "integer", "minimum": 0, "title": "Size of the original file, in bytes"},
|
|
466
500
|
"original_file:name": {"type": "string", "title": "Original file name"},
|
|
467
501
|
"panoramax:horizontal_pixel_density": {
|
|
@@ -525,7 +559,7 @@ The CSV headers will be:
|
|
|
525
559
|
"override_capture_time": {
|
|
526
560
|
"type": "string",
|
|
527
561
|
"format": "date-time",
|
|
528
|
-
"description": "datetime when the picture was taken. It will change the picture's metadata with this datetime. It should be an iso 3339
|
|
562
|
+
"description": "datetime when the picture was taken. It will change the picture's metadata with this datetime. It should be an iso 3339 formatted datetime (like '2017-07-21T17:32:28Z')",
|
|
529
563
|
},
|
|
530
564
|
"override_latitude": {
|
|
531
565
|
"type": "number",
|
|
@@ -578,6 +612,20 @@ Example values are:
|
|
|
578
612
|
Note that this parameter is not taken in account for 360° pictures, as by definition a nearby place would be theorically always visible in it.
|
|
579
613
|
""",
|
|
580
614
|
},
|
|
615
|
+
"sortby": {
|
|
616
|
+
"description": """Define the sort order of the results of a search.
|
|
617
|
+
Sort order is defined based on preceding '+' (asc) or '-' (desc).
|
|
618
|
+
|
|
619
|
+
By default we sort to get the last updated pictures first.
|
|
620
|
+
|
|
621
|
+
Available properties are:
|
|
622
|
+
* `ts`: capture datetime of the picture
|
|
623
|
+
* `updated`: sort by updated datetime of the picture
|
|
624
|
+
* `id`: us the picture ID for sort
|
|
625
|
+
""",
|
|
626
|
+
"default": "-updated",
|
|
627
|
+
"type": "string",
|
|
628
|
+
},
|
|
581
629
|
},
|
|
582
630
|
},
|
|
583
631
|
],
|
|
@@ -742,6 +790,10 @@ Available properties are:
|
|
|
742
790
|
"properties": {
|
|
743
791
|
"user_profile": {"type": "object", "properties": {"url": {"type": "string"}}},
|
|
744
792
|
"enabled": {"type": "boolean"},
|
|
793
|
+
"registration_is_open": {
|
|
794
|
+
"type": "boolean",
|
|
795
|
+
"description": "If true, users can create their own account on the instance. Only used for reference in the federation for the moment",
|
|
796
|
+
},
|
|
745
797
|
"enforce_tos_acceptance": {"type": "boolean"},
|
|
746
798
|
},
|
|
747
799
|
"required": ["enabled"],
|
|
@@ -759,6 +811,32 @@ Available properties are:
|
|
|
759
811
|
"description": "The GeoVisio API version number",
|
|
760
812
|
"example": "2.6.0-12-ab12cd34",
|
|
761
813
|
},
|
|
814
|
+
"defaults": {
|
|
815
|
+
"type": "object",
|
|
816
|
+
"properties": {
|
|
817
|
+
"collaborative_metadata": {
|
|
818
|
+
"type": "integer",
|
|
819
|
+
"description": "If `true`, the pictures's metadata will be, by default, editable by all users.",
|
|
820
|
+
},
|
|
821
|
+
"split_distance": {
|
|
822
|
+
"type": "integer",
|
|
823
|
+
"description": "Maximum distance between two pictures to be considered in the same sequence (in meters). If both split_distance and split_time are None, no split will occur by default.",
|
|
824
|
+
},
|
|
825
|
+
"split_time": {
|
|
826
|
+
"type": "integer",
|
|
827
|
+
"description": "Maximum time interval between two pictures to be considered in the same sequence. If both split_distance and split_time are None, no split will occur by default.",
|
|
828
|
+
},
|
|
829
|
+
"duplicate_distance": {
|
|
830
|
+
"type": "integer",
|
|
831
|
+
"description": "Maximum distance between two pictures to be considered as duplicates (in meters). If both duplicate_distance andduplicate_rotation are None, no deduplication will occur by default.",
|
|
832
|
+
},
|
|
833
|
+
"duplicate_rotation": {
|
|
834
|
+
"type": "integer",
|
|
835
|
+
"description": "Maximum angle of rotation for two too-close-pictures to be considered as duplicates (in degrees).",
|
|
836
|
+
},
|
|
837
|
+
},
|
|
838
|
+
"required": ["collaborative_metadata", "duplicate_distance", "duplicate_rotation", "split_distance", "split_time"],
|
|
839
|
+
},
|
|
762
840
|
},
|
|
763
841
|
"required": ["auth"],
|
|
764
842
|
},
|
|
@@ -821,6 +899,15 @@ Available properties are:
|
|
|
821
899
|
"payload": {"type": "object", "description": "The error payload"},
|
|
822
900
|
},
|
|
823
901
|
},
|
|
902
|
+
"GeoVisioAnnotation": annotations_utils.Annotation.model_json_schema(
|
|
903
|
+
ref_template="#/components/schemas/GeoVisioAnnotation/$defs/{model}", mode="serialization"
|
|
904
|
+
),
|
|
905
|
+
"GeoVisioPostAnnotation": annotations.AnnotationPostParameter.model_json_schema(
|
|
906
|
+
ref_template="#/components/schemas/GeoVisioPostAnnotation/$defs/{model}", mode="serialization"
|
|
907
|
+
),
|
|
908
|
+
"GeoVisioPatchAnnotation": annotations.AnnotationPatchParameter.model_json_schema(
|
|
909
|
+
ref_template="#/components/schemas/GeoVisioPatchAnnotation/$defs/{model}", mode="serialization"
|
|
910
|
+
),
|
|
824
911
|
},
|
|
825
912
|
"parameters": {
|
|
826
913
|
"STAC_bbox": {"$ref": f"https://api.stacspec.org/v{utils.STAC_VERSION}/item-search/openapi.yaml#/components/parameters/bbox"},
|
|
@@ -848,7 +935,7 @@ Available properties are:
|
|
|
848
935
|
"description": """
|
|
849
936
|
A CQL2 filter expression for filtering sequences.
|
|
850
937
|
|
|
851
|
-
Allowed properties are:
|
|
938
|
+
Allowed properties are:
|
|
852
939
|
* "created": upload date
|
|
853
940
|
* "updated": last edit date
|
|
854
941
|
* "status": status of the sequence. Can either be "ready" (for collections ready to be served) or "deleted" for deleted collection. By default, only the "ready" collections will be shown.
|
|
@@ -872,7 +959,7 @@ Examples:
|
|
|
872
959
|
"description": """
|
|
873
960
|
A CQL2 filter expression for filtering tiles.
|
|
874
961
|
|
|
875
|
-
Allowed properties are:
|
|
962
|
+
Allowed properties are:
|
|
876
963
|
* "status": status of the sequence. Can either be "ready" (for collections ready to be served) or "hidden" for hidden collections. By default, only the "ready" collections will be shown.
|
|
877
964
|
|
|
878
965
|
Usage doc can be found here: https://docs.geoserver.org/2.23.x/en/user/tutorials/cql/cql_tutorial.html
|
|
@@ -916,13 +1003,58 @@ Note that this parameter is not taken in account for 360° pictures, as by defin
|
|
|
916
1003
|
"required": False,
|
|
917
1004
|
"schema": {"type": "integer", "minimum": 2, "maximum": 180, "default": 30},
|
|
918
1005
|
},
|
|
1006
|
+
"GeoVisioSearchSortedBy": {
|
|
1007
|
+
"name": "sortby",
|
|
1008
|
+
"in": "query",
|
|
1009
|
+
"description": """Define the sort order of the results of a search.
|
|
1010
|
+
Sort order is defined based on preceding '+' (asc) or '-' (desc).
|
|
1011
|
+
|
|
1012
|
+
By default we sort to get the last updated pictures firstn (-updated).
|
|
1013
|
+
|
|
1014
|
+
Available properties are:
|
|
1015
|
+
* `ts`: capture datetime of the picture
|
|
1016
|
+
* `updated`: sort by updated datetime of the picture
|
|
1017
|
+
* `id`: us the picture ID for sort
|
|
1018
|
+
""",
|
|
1019
|
+
"required": False,
|
|
1020
|
+
"schema": {
|
|
1021
|
+
"type": "string",
|
|
1022
|
+
},
|
|
1023
|
+
},
|
|
1024
|
+
"searchCQL2_filter": {
|
|
1025
|
+
"name": "filter",
|
|
1026
|
+
"in": "query",
|
|
1027
|
+
"description": """
|
|
1028
|
+
A CQL2 filter expression for filtering search results.
|
|
1029
|
+
|
|
1030
|
+
Only works for semantic search for the moment.
|
|
1031
|
+
|
|
1032
|
+
The attributes must start with "semantics." and formated like "semantics.some_key"='some_value'.
|
|
1033
|
+
|
|
1034
|
+
Note: it's important for the attribute to be quoted (`"`) and the value to around simple quotes (`'`) to avoid issues with CQL2 parsing.
|
|
1035
|
+
|
|
1036
|
+
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.
|
|
1037
|
+
|
|
1038
|
+
To search for any values of a semantic tag, use `semantics.some_key IS NOT NULL` (case matter here).
|
|
1039
|
+
|
|
1040
|
+
Examples:
|
|
1041
|
+
|
|
1042
|
+
* "semantics.osm|traffic_sign"='yes'
|
|
1043
|
+
* "semantics.osm|traffic_sign" IS NOT NULL'
|
|
1044
|
+
* "semantics.osm|amenity" IN ('bench', 'whatever') OR "semantics.osm|traffic_sign"='yes'
|
|
1045
|
+
""",
|
|
1046
|
+
"required": False,
|
|
1047
|
+
"schema": {
|
|
1048
|
+
"type": "string",
|
|
1049
|
+
},
|
|
1050
|
+
},
|
|
919
1051
|
"GeoVisioReports_filter": {
|
|
920
1052
|
"name": "filter",
|
|
921
1053
|
"in": "query",
|
|
922
1054
|
"description": """
|
|
923
1055
|
A CQL2 filter expression for filtering reports.
|
|
924
1056
|
|
|
925
|
-
Allowed properties are:
|
|
1057
|
+
Allowed properties are:
|
|
926
1058
|
* status: 'open', 'open_autofix', 'waiting', 'closed_solved', 'closed_ignored'
|
|
927
1059
|
* reporter: 'me', user account ID or unset
|
|
928
1060
|
* owner: 'me', user account ID or unset
|
|
@@ -947,7 +1079,7 @@ By default, we only show open or waiting reports, sorted by descending creation
|
|
|
947
1079
|
"description": """
|
|
948
1080
|
A CQL2 filter expression for filtering reports.
|
|
949
1081
|
|
|
950
|
-
Allowed properties are:
|
|
1082
|
+
Allowed properties are:
|
|
951
1083
|
* status: 'open', 'open_autofix', 'waiting', 'closed_solved', 'closed_ignored'
|
|
952
1084
|
* reporter: 'me' or unset
|
|
953
1085
|
* owner: 'me' or unset
|
|
@@ -972,7 +1104,7 @@ By default, we only show open or waiting reports concerning you, sorted by desce
|
|
|
972
1104
|
"description": """
|
|
973
1105
|
A CQL2 filter expression for filtering upload sets.
|
|
974
1106
|
|
|
975
|
-
Allowed properties are:
|
|
1107
|
+
Allowed properties are:
|
|
976
1108
|
* completed: TRUE or FALSE
|
|
977
1109
|
* dispatched: TRUE or FALSE
|
|
978
1110
|
|
|
@@ -1042,7 +1174,7 @@ def getApiInfo():
|
|
|
1042
1174
|
"""Return API metadata parsed from pyproject.toml"""
|
|
1043
1175
|
apiMeta = metadata.metadata("geovisio")
|
|
1044
1176
|
|
|
1045
|
-
# url is
|
|
1177
|
+
# url is formatted like 'Home, <url>
|
|
1046
1178
|
url = apiMeta["Project-URL"].split(",")[1].rstrip()
|
|
1047
1179
|
# there can be several authors, but we only display the first one in docs
|
|
1048
1180
|
author = apiMeta["Author-email"].split(",")[0]
|