geovisio 2.4.0__py3-none-any.whl → 2.6.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 +3 -2
- geovisio/admin_cli/__init__.py +2 -2
- geovisio/admin_cli/db.py +11 -0
- geovisio/admin_cli/sequence_heading.py +2 -2
- geovisio/config_app.py +25 -0
- geovisio/templates/main.html +2 -2
- geovisio/utils/pictures.py +75 -30
- geovisio/utils/sequences.py +232 -34
- geovisio/web/auth.py +15 -2
- geovisio/web/collections.py +161 -111
- geovisio/web/docs.py +178 -4
- geovisio/web/items.py +169 -114
- geovisio/web/map.py +309 -93
- geovisio/web/params.py +82 -4
- geovisio/web/stac.py +14 -4
- geovisio/web/tokens.py +7 -3
- geovisio/web/users.py +4 -4
- geovisio/web/utils.py +10 -0
- geovisio/workers/runner_pictures.py +73 -70
- geovisio-2.6.0.dist-info/METADATA +92 -0
- geovisio-2.6.0.dist-info/RECORD +41 -0
- geovisio-2.4.0.dist-info/METADATA +0 -115
- geovisio-2.4.0.dist-info/RECORD +0 -41
- {geovisio-2.4.0.dist-info → geovisio-2.6.0.dist-info}/LICENSE +0 -0
- {geovisio-2.4.0.dist-info → geovisio-2.6.0.dist-info}/WHEEL +0 -0
geovisio/web/docs.py
CHANGED
|
@@ -120,6 +120,50 @@ API_CONFIG = {
|
|
|
120
120
|
"STACItemSearchBody": {
|
|
121
121
|
"$ref": f"https://api.stacspec.org/v{utils.STAC_VERSION}/item-search/openapi.yaml#/components/schemas/searchBody"
|
|
122
122
|
},
|
|
123
|
+
"MapLibreStyleJSON": {
|
|
124
|
+
"type": "object",
|
|
125
|
+
"description": """
|
|
126
|
+
MapLibre Style JSON, see https://maplibre.org/maplibre-style-spec/ for reference.
|
|
127
|
+
|
|
128
|
+
Source ID is either \"geovisio\" or \"geovisio_\{userId\}\".
|
|
129
|
+
|
|
130
|
+
Layers ID are \"geovisio_grid\", \"geovisio_sequences\" and \"geovisio_pictures\", or with user UUID included (\"geovisio_\{userId\}_sequences\" and \"geovisio_\{userId\}_pictures\").
|
|
131
|
+
|
|
132
|
+
Note that you may not rely only on these ID that could change through time.
|
|
133
|
+
""",
|
|
134
|
+
"properties": {
|
|
135
|
+
"version": {"type": "integer", "example": 8},
|
|
136
|
+
"name": {"type": "string", "example": "GeoVisio Vector Tiles"},
|
|
137
|
+
"sources": {
|
|
138
|
+
"type": "object",
|
|
139
|
+
"properties": {
|
|
140
|
+
"geovisio": {
|
|
141
|
+
"type": "object",
|
|
142
|
+
"properties": {
|
|
143
|
+
"type": {"type": "string", "example": "vector"},
|
|
144
|
+
"minzoom": {"type": "integer", "example": "0"},
|
|
145
|
+
"maxzoom": {"type": "integer", "example": "15"},
|
|
146
|
+
"tiles": {"type": "array", "items": {"type": "string"}},
|
|
147
|
+
},
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
"layers": {
|
|
152
|
+
"type": "array",
|
|
153
|
+
"items": {
|
|
154
|
+
"type": "object",
|
|
155
|
+
"properties": {
|
|
156
|
+
"id": {"type": "string"},
|
|
157
|
+
"source": {"type": "string"},
|
|
158
|
+
"source-layer": {"type": "string"},
|
|
159
|
+
"type": {"type": "string"},
|
|
160
|
+
"paint": {"type": "object"},
|
|
161
|
+
"layout": {"type": "object"},
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
},
|
|
123
167
|
"GeoVisioLanding": {
|
|
124
168
|
"allOf": [
|
|
125
169
|
{"$ref": "#/components/schemas/STACLanding"},
|
|
@@ -223,7 +267,14 @@ API_CONFIG = {
|
|
|
223
267
|
"GeoVisioCollection": {
|
|
224
268
|
"allOf": [
|
|
225
269
|
{"$ref": "#/components/schemas/STACCollection"},
|
|
226
|
-
{
|
|
270
|
+
{
|
|
271
|
+
"type": "object",
|
|
272
|
+
"properties": {
|
|
273
|
+
"stats:items": {"$ref": "#/components/schemas/STACStatsForItems"},
|
|
274
|
+
"geovisio:status": {"$ref": "#/components/schemas/GeoVisioCollectionStatus"},
|
|
275
|
+
"geovisio:sorted-by": {"$ref": "#/components/schemas/GeoVisioCollectionSortedBy"},
|
|
276
|
+
},
|
|
277
|
+
},
|
|
227
278
|
]
|
|
228
279
|
},
|
|
229
280
|
"GeoVisioCollectionImportStatus": {
|
|
@@ -264,6 +315,26 @@ API_CONFIG = {
|
|
|
264
315
|
"type": "string",
|
|
265
316
|
"description": "The sequence title (publicly displayed)",
|
|
266
317
|
},
|
|
318
|
+
"relative_heading": {
|
|
319
|
+
"type": "number",
|
|
320
|
+
"minimum": -180,
|
|
321
|
+
"maximum": 180,
|
|
322
|
+
"description": "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.",
|
|
323
|
+
},
|
|
324
|
+
"sortby": {
|
|
325
|
+
"description": """
|
|
326
|
+
Define the pictures sort order based on given property. Sort order is defined based on preceding '+' (asc) or '-' (desc).
|
|
327
|
+
|
|
328
|
+
Available properties are:
|
|
329
|
+
* `gpsdate`: sort by GPS datetime
|
|
330
|
+
* `filedate`: sort by the camera-generated capture date. This is based on EXIF tags `Exif.Image.DateTimeOriginal`, `Exif.Photo.DateTimeOriginal`, `Exif.Image.DateTime` or `Xmp.GPano.SourceImageCreateTime` (in this order).
|
|
331
|
+
* `filename`: sort by the original picture file name
|
|
332
|
+
|
|
333
|
+
If unset, sort order is unchanged.
|
|
334
|
+
""",
|
|
335
|
+
"type": "string",
|
|
336
|
+
"enum": ["+gpsdate", "-gpsdate", "+filedate", "-filedate", "+filename", "-filename"],
|
|
337
|
+
},
|
|
267
338
|
},
|
|
268
339
|
},
|
|
269
340
|
"GeoVisioCollectionItems": {
|
|
@@ -285,6 +356,11 @@ API_CONFIG = {
|
|
|
285
356
|
"properties": {
|
|
286
357
|
"type": "object",
|
|
287
358
|
"properties": {
|
|
359
|
+
"datetimetz": {
|
|
360
|
+
"type": "string",
|
|
361
|
+
"format": "date-time",
|
|
362
|
+
"title": "Date & time with original timezone information",
|
|
363
|
+
},
|
|
288
364
|
"geovisio:status": {"$ref": "#/components/schemas/GeoVisioItemStatus"},
|
|
289
365
|
"geovisio:producer": {"type": "string"},
|
|
290
366
|
"geovisio:image": {"type": "string", "format": "uri"},
|
|
@@ -357,6 +433,49 @@ API_CONFIG = {
|
|
|
357
433
|
},
|
|
358
434
|
},
|
|
359
435
|
},
|
|
436
|
+
"GeoVisioItemSearchBody": {
|
|
437
|
+
"description": "The search criteria",
|
|
438
|
+
"type": "object",
|
|
439
|
+
"allOf": [
|
|
440
|
+
{"$ref": "#/components/schemas/STACItemSearchBody"},
|
|
441
|
+
{
|
|
442
|
+
"type": "object",
|
|
443
|
+
"properties": {
|
|
444
|
+
"place_position": {
|
|
445
|
+
"description": "Geographical coordinates (lon,lat) of a place you'd like to have pictures of. Returned pictures are either 360° or looking in direction of wanted place.",
|
|
446
|
+
"type": "string",
|
|
447
|
+
"pattern": "-?\d+\.\d+,-?\d+\.\d+",
|
|
448
|
+
},
|
|
449
|
+
"place_distance": {
|
|
450
|
+
"description": "Distance range (in meters) to search pictures for a particular place (place_position). Default range is 3-15. Only used if place_position parameter is defined.",
|
|
451
|
+
"type": "string",
|
|
452
|
+
"pattern": "\d+-\d+",
|
|
453
|
+
},
|
|
454
|
+
"place_fov_tolerance": {
|
|
455
|
+
"type": "integer",
|
|
456
|
+
"minimum": 2,
|
|
457
|
+
"maximum": 180,
|
|
458
|
+
"description": """
|
|
459
|
+
Tolerance on how much the place should be centered in nearby pictures:
|
|
460
|
+
|
|
461
|
+
* A lower value means place have to be at the very center of picture
|
|
462
|
+
* A higher value means place could be more in picture sides
|
|
463
|
+
|
|
464
|
+
Value is expressed in degrees (from 2 to 180, defaults to 30°), and represents the acceptable field of view relative to picture heading. Only used if place_position parameter is defined.
|
|
465
|
+
|
|
466
|
+
Example values are:
|
|
467
|
+
|
|
468
|
+
* <= 30° for place to be in the very center of picture
|
|
469
|
+
* 60° for place to be in recognizable human field of view
|
|
470
|
+
* 180° for place to be anywhere in a wide-angle picture
|
|
471
|
+
|
|
472
|
+
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.
|
|
473
|
+
""",
|
|
474
|
+
},
|
|
475
|
+
},
|
|
476
|
+
},
|
|
477
|
+
],
|
|
478
|
+
},
|
|
360
479
|
"GeoVisioPatchItem": {
|
|
361
480
|
"type": "object",
|
|
362
481
|
"properties": {
|
|
@@ -365,10 +484,29 @@ API_CONFIG = {
|
|
|
365
484
|
"description": "Should the picture be publicly visible ?",
|
|
366
485
|
"enum": ["true", "false", "null"],
|
|
367
486
|
"default": "null",
|
|
368
|
-
}
|
|
487
|
+
},
|
|
488
|
+
"heading": {
|
|
489
|
+
"type": "number",
|
|
490
|
+
"minimum": 0,
|
|
491
|
+
"maximum": 360,
|
|
492
|
+
"description": "The picture heading (in degrees). North is 0°, East = 90°, South = 180° and West = 270°.",
|
|
493
|
+
},
|
|
369
494
|
},
|
|
370
495
|
},
|
|
371
496
|
"GeoVisioCollectionStatus": {"type": "string", "enum": ["ready", "broken", "preparing", "waiting-for-process"]},
|
|
497
|
+
"GeoVisioCollectionSortedBy": {
|
|
498
|
+
"description": """
|
|
499
|
+
Define the pictures sort order of the sequence. Null by default, and can be set via the collection PATCH.
|
|
500
|
+
Sort order is defined based on preceding '+' (asc) or '-' (desc).
|
|
501
|
+
|
|
502
|
+
Available properties are:
|
|
503
|
+
* `gpsdate`: sort by GPS datetime
|
|
504
|
+
* `filedate`: sort by the camera-generated capture date. This is based on EXIF tags `Exif.Image.DateTimeOriginal`, `Exif.Photo.DateTimeOriginal`, `Exif.Image.DateTime` or `Xmp.GPano.SourceImageCreateTime` (in this order).
|
|
505
|
+
* `filename`: sort by the original picture file name
|
|
506
|
+
""",
|
|
507
|
+
"type": "string",
|
|
508
|
+
"enum": ["+gpsdate", "-gpsdate", "+filedate", "-filedate", "+filename", "-filename"],
|
|
509
|
+
},
|
|
372
510
|
"GeoVisioItemStatus": {
|
|
373
511
|
"type": "string",
|
|
374
512
|
"enum": ["ready", "broken", "waiting-for-process"],
|
|
@@ -469,7 +607,7 @@ API_CONFIG = {
|
|
|
469
607
|
},
|
|
470
608
|
},
|
|
471
609
|
},
|
|
472
|
-
"
|
|
610
|
+
"GeoVisioEncodedToken": {
|
|
473
611
|
"type": "object",
|
|
474
612
|
"properties": {
|
|
475
613
|
"id": {"type": "string"},
|
|
@@ -483,7 +621,7 @@ API_CONFIG = {
|
|
|
483
621
|
},
|
|
484
622
|
"JWTokenClaimable": {
|
|
485
623
|
"allOf": [
|
|
486
|
-
{"$ref": "#/components/schemas/
|
|
624
|
+
{"$ref": "#/components/schemas/GeoVisioEncodedToken"},
|
|
487
625
|
{
|
|
488
626
|
"type": "object",
|
|
489
627
|
"properties": {
|
|
@@ -561,6 +699,42 @@ Usage doc can be found here: https://docs.geoserver.org/2.23.x/en/user/tutorials
|
|
|
561
699
|
"required": False,
|
|
562
700
|
"schema": {"type": "string"},
|
|
563
701
|
},
|
|
702
|
+
"GeoVisio_place_position": {
|
|
703
|
+
"name": "place_position",
|
|
704
|
+
"in": "query",
|
|
705
|
+
"required": False,
|
|
706
|
+
"description": "Geographical coordinates (lon,lat) of a place you'd like to have pictures of. Returned pictures are either 360° or looking in direction of wanted place.",
|
|
707
|
+
"schema": {"type": "string", "pattern": "-?\d+\.\d+,-?\d+\.\d+"},
|
|
708
|
+
},
|
|
709
|
+
"GeoVisio_place_distance": {
|
|
710
|
+
"name": "place_distance",
|
|
711
|
+
"in": "query",
|
|
712
|
+
"required": False,
|
|
713
|
+
"description": "Distance range (in meters) to search pictures for a particular place (place_position). Default range is 3-15. Only used if place_position parameter is defined.",
|
|
714
|
+
"schema": {"type": "string", "pattern": "\d+-\d+", "default": "3-15"},
|
|
715
|
+
},
|
|
716
|
+
"GeoVisio_place_fov_tolerance": {
|
|
717
|
+
"name": "place_fov_tolerance",
|
|
718
|
+
"in": "query",
|
|
719
|
+
"description": """
|
|
720
|
+
Tolerance on how much the place should be centered in nearby pictures:
|
|
721
|
+
|
|
722
|
+
* A lower value means place have to be at the very center of picture
|
|
723
|
+
* A higher value means place could be more in picture sides
|
|
724
|
+
|
|
725
|
+
Value is expressed in degrees (from 2 to 180, defaults to 30°), and represents the acceptable field of view relative to picture heading. Only used if place_position parameter is defined.
|
|
726
|
+
|
|
727
|
+
Example values are:
|
|
728
|
+
|
|
729
|
+
* <= 30° for place to be in the very center of picture
|
|
730
|
+
* 60° for place to be in recognizable human field of view
|
|
731
|
+
* 180° for place to be anywhere in a wide-angle picture
|
|
732
|
+
|
|
733
|
+
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.
|
|
734
|
+
""",
|
|
735
|
+
"required": False,
|
|
736
|
+
"schema": {"type": "integer", "minimum": 2, "maximum": 180, "default": 30},
|
|
737
|
+
},
|
|
564
738
|
"OGC_sortby": {
|
|
565
739
|
"name": "sortby",
|
|
566
740
|
"in": "query",
|
geovisio/web/items.py
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
import io
|
|
2
1
|
import json
|
|
3
2
|
import logging
|
|
4
3
|
import os
|
|
5
|
-
import re
|
|
6
4
|
from typing import Dict, Optional, Any
|
|
7
5
|
from urllib.parse import unquote
|
|
8
6
|
from psycopg.types.json import Jsonb
|
|
@@ -10,6 +8,7 @@ from werkzeug.datastructures import MultiDict
|
|
|
10
8
|
from uuid import UUID
|
|
11
9
|
from geovisio import errors, utils
|
|
12
10
|
from geovisio.utils import auth
|
|
11
|
+
from geovisio.utils.pictures import cleanupExif
|
|
13
12
|
from geovisio.web.params import (
|
|
14
13
|
as_latitude,
|
|
15
14
|
as_longitude,
|
|
@@ -18,17 +17,19 @@ from geovisio.web.params import (
|
|
|
18
17
|
parse_datetime_interval,
|
|
19
18
|
parse_bbox,
|
|
20
19
|
parse_list,
|
|
20
|
+
parse_lonlat,
|
|
21
|
+
parse_distance_range,
|
|
21
22
|
)
|
|
22
23
|
from geovisio.utils.fields import Bounds
|
|
23
24
|
|
|
24
25
|
import psycopg
|
|
25
|
-
from datetime import datetime
|
|
26
26
|
from psycopg.rows import dict_row
|
|
27
27
|
from psycopg.sql import SQL
|
|
28
28
|
from geovisio.web.utils import (
|
|
29
29
|
accountIdOrDefault,
|
|
30
30
|
cleanNoneInList,
|
|
31
31
|
dbTsToStac,
|
|
32
|
+
dbTsToStacTZ,
|
|
32
33
|
get_license_link,
|
|
33
34
|
get_root_link,
|
|
34
35
|
removeNoneInDict,
|
|
@@ -41,9 +42,6 @@ from geovisio.workers import runner_pictures
|
|
|
41
42
|
|
|
42
43
|
bp = Blueprint("stac_items", __name__, url_prefix="/api")
|
|
43
44
|
|
|
44
|
-
RGX_BINARY_KEY = re.compile(".+\..+\.0x[0-f]+")
|
|
45
|
-
RGX_BINARY_VAL = re.compile("(\d{1,3} ){20,}\d{1,3}") # At least 21 blocks of 1-3 digits values
|
|
46
|
-
|
|
47
45
|
|
|
48
46
|
def dbPictureToStacItem(seqId, dbPic):
|
|
49
47
|
"""Transforms a picture extracted from database into a STAC Item
|
|
@@ -52,7 +50,7 @@ def dbPictureToStacItem(seqId, dbPic):
|
|
|
52
50
|
----------
|
|
53
51
|
seqId : uuid
|
|
54
52
|
Associated sequence ID
|
|
55
|
-
|
|
53
|
+
dbPic : dict
|
|
56
54
|
A row from pictures table in database (with id, geojson, ts, heading, cols, rows, width, height, prevpic, nextpic, prevpicgeojson, nextpicgeojson, exif fields)
|
|
57
55
|
|
|
58
56
|
Returns
|
|
@@ -82,33 +80,38 @@ def dbPictureToStacItem(seqId, dbPic):
|
|
|
82
80
|
),
|
|
83
81
|
]
|
|
84
82
|
),
|
|
85
|
-
"properties":
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
83
|
+
"properties": removeNoneInDict(
|
|
84
|
+
{
|
|
85
|
+
"datetime": dbTsToStac(dbPic["ts"]),
|
|
86
|
+
"datetimetz": dbTsToStacTZ(dbPic["ts"], dbPic["metadata"].get("tz")),
|
|
87
|
+
"created": dbTsToStac(dbPic["inserted_at"]),
|
|
88
|
+
# TODO : add "updated" TS for last edit time of metadata
|
|
89
|
+
"license": current_app.config["API_PICTURES_LICENSE_SPDX_ID"],
|
|
90
|
+
"view:azimuth": dbPic["heading"],
|
|
91
|
+
"pers:interior_orientation": (
|
|
92
|
+
removeNoneInDict(
|
|
93
|
+
{
|
|
94
|
+
"camera_manufacturer": dbPic["metadata"].get("make"),
|
|
95
|
+
"camera_model": dbPic["metadata"].get("model"),
|
|
96
|
+
"focal_length": dbPic["metadata"].get("focal_length"),
|
|
97
|
+
"field_of_view": dbPic["metadata"].get("field_of_view"),
|
|
98
|
+
}
|
|
99
|
+
)
|
|
100
|
+
if "metadata" in dbPic
|
|
101
|
+
and any(True for f in dbPic["metadata"] if f in ["make", "model", "focal_length", "field_of_view"])
|
|
102
|
+
else {}
|
|
103
|
+
),
|
|
104
|
+
"pers:pitch": dbPic["metadata"].get("pitch"),
|
|
105
|
+
"pers:roll": dbPic["metadata"].get("roll"),
|
|
106
|
+
"geovisio:status": dbPic.get("status"),
|
|
107
|
+
"geovisio:producer": dbPic["account_name"],
|
|
108
|
+
"original_file:size": dbPic["metadata"].get("originalFileSize"),
|
|
109
|
+
"original_file:name": dbPic["metadata"].get("originalFileName"),
|
|
110
|
+
"geovisio:image": _getHDJpgPictureURL(dbPic["id"], dbPic.get("status")),
|
|
111
|
+
"geovisio:thumbnail": _getThumbJpgPictureURL(dbPic["id"], dbPic.get("status")),
|
|
112
|
+
"exif": removeNoneInDict(cleanupExif(dbPic["exif"])),
|
|
113
|
+
}
|
|
114
|
+
),
|
|
112
115
|
"links": cleanNoneInList(
|
|
113
116
|
[
|
|
114
117
|
get_root_link(),
|
|
@@ -235,30 +238,6 @@ def dbPictureToStacItem(seqId, dbPic):
|
|
|
235
238
|
return item
|
|
236
239
|
|
|
237
240
|
|
|
238
|
-
def cleanupExif(exif: Optional[Dict[str, str]]) -> Optional[Dict[str, str]]:
|
|
239
|
-
"""Removes things from EXIF dictionnary that should not land in STAC responses
|
|
240
|
-
>>> cleanupExif({'A': 'B', 'Exif.Sony.0x0102': 'Blablabla'})
|
|
241
|
-
{'A': 'B'}
|
|
242
|
-
>>> cleanupExif({'A': 'B', 'Exif.Photo.MakerNote': 'Blablabla'})
|
|
243
|
-
{'A': 'B'}
|
|
244
|
-
>>> cleanupExif({'A': 'B', 'Exif.Sony.Whatever': '1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21'})
|
|
245
|
-
{'A': 'B'}
|
|
246
|
-
>>> cleanupExif({'A': 'B', 'Exif.Sony.Whatever': '1 2 3 4 5'})
|
|
247
|
-
{'A': 'B', 'Exif.Sony.Whatever': '1 2 3 4 5'}
|
|
248
|
-
"""
|
|
249
|
-
|
|
250
|
-
if exif is None:
|
|
251
|
-
return None
|
|
252
|
-
|
|
253
|
-
cleanExif = {}
|
|
254
|
-
|
|
255
|
-
for k, v in exif.items():
|
|
256
|
-
if not (k in ["Exif.Photo.MakerNote"] or RGX_BINARY_KEY.match(k) or RGX_BINARY_VAL.match(v)):
|
|
257
|
-
cleanExif[k] = v
|
|
258
|
-
|
|
259
|
-
return cleanExif
|
|
260
|
-
|
|
261
|
-
|
|
262
241
|
def get_first_rank_of_page(rankToHave: int, limit: Optional[int]) -> int:
|
|
263
242
|
"""if there is a limit, we try to emulate a page, so we'll return the nth page that should contain this picture
|
|
264
243
|
Note: the ranks starts from 1
|
|
@@ -715,13 +694,16 @@ def searchItems():
|
|
|
715
694
|
- $ref: '#/components/parameters/STAC_limit'
|
|
716
695
|
- $ref: '#/components/parameters/STAC_ids'
|
|
717
696
|
- $ref: '#/components/parameters/STAC_collectionsArray'
|
|
697
|
+
- $ref: '#/components/parameters/GeoVisio_place_position'
|
|
698
|
+
- $ref: '#/components/parameters/GeoVisio_place_distance'
|
|
699
|
+
- $ref: '#/components/parameters/GeoVisio_place_fov_tolerance'
|
|
718
700
|
post:
|
|
719
701
|
requestBody:
|
|
720
702
|
required: true
|
|
721
703
|
content:
|
|
722
704
|
application/json:
|
|
723
705
|
schema:
|
|
724
|
-
$ref: '#/components/schemas/
|
|
706
|
+
$ref: '#/components/schemas/GeoVisioItemSearchBody'
|
|
725
707
|
responses:
|
|
726
708
|
200:
|
|
727
709
|
$ref: '#/components/responses/STAC_search'
|
|
@@ -732,7 +714,6 @@ def searchItems():
|
|
|
732
714
|
sqlWhere = [SQL("(p.status = 'ready' OR p.account_id = %(account)s)"), SQL("(is_sequence_visible_by_user(s, %(account)s))")]
|
|
733
715
|
sqlParams: Dict[str, Any] = {"account": accountId}
|
|
734
716
|
sqlSubQueryWhere = [SQL("(p.status = 'ready' OR p.account_id = %(account)s)")]
|
|
735
|
-
|
|
736
717
|
order_by = SQL("")
|
|
737
718
|
|
|
738
719
|
#
|
|
@@ -779,6 +760,52 @@ def searchItems():
|
|
|
779
760
|
sqlWhere.append(SQL("p.ts <= %(maxts)s::timestamp with time zone"))
|
|
780
761
|
sqlParams["maxts"] = max_dt
|
|
781
762
|
|
|
763
|
+
# Place position & distance
|
|
764
|
+
place_pos = parse_lonlat(args.getlist("place_position"), "place_position")
|
|
765
|
+
if place_pos is not None:
|
|
766
|
+
sqlParams["placex"] = place_pos[0]
|
|
767
|
+
sqlParams["placey"] = place_pos[1]
|
|
768
|
+
|
|
769
|
+
# Filter to keep pictures in acceptable distance range to POI
|
|
770
|
+
place_dist = parse_distance_range(args.get("place_distance"), "place_distance") or [3, 15]
|
|
771
|
+
sqlParams["placedmin"] = place_dist[0]
|
|
772
|
+
sqlParams["placedmax"] = place_dist[1]
|
|
773
|
+
|
|
774
|
+
sqlWhere.append(
|
|
775
|
+
SQL(
|
|
776
|
+
"""
|
|
777
|
+
ST_Intersects(
|
|
778
|
+
p.geom,
|
|
779
|
+
ST_Difference(
|
|
780
|
+
ST_Buffer(ST_Point(%(placex)s, %(placey)s)::geography, %(placedmax)s)::geometry,
|
|
781
|
+
ST_Buffer(ST_Point(%(placex)s, %(placey)s)::geography, %(placedmin)s)::geometry
|
|
782
|
+
)
|
|
783
|
+
)
|
|
784
|
+
"""
|
|
785
|
+
)
|
|
786
|
+
)
|
|
787
|
+
|
|
788
|
+
# Compute acceptable field of view
|
|
789
|
+
place_fov_tolerance = args.get("place_fov_tolerance", type=int, default=30)
|
|
790
|
+
if place_fov_tolerance < 2 or place_fov_tolerance > 180:
|
|
791
|
+
raise errors.InvalidAPIUsage(
|
|
792
|
+
"Parameter place_fov_tolerance must be either empty or a number between 2 and 180", status_code=400
|
|
793
|
+
)
|
|
794
|
+
else:
|
|
795
|
+
sqlParams["placefov"] = place_fov_tolerance / 2
|
|
796
|
+
|
|
797
|
+
sqlWhere.append(
|
|
798
|
+
SQL(
|
|
799
|
+
"""(
|
|
800
|
+
p.metadata->>'type' = 'equirectangular'
|
|
801
|
+
OR ST_Azimuth(p.geom, ST_Point(%(placex)s, %(placey)s, 4326)) BETWEEN radians(p.heading - %(placefov)s) AND radians(p.heading + %(placefov)s)
|
|
802
|
+
)"""
|
|
803
|
+
)
|
|
804
|
+
)
|
|
805
|
+
|
|
806
|
+
# Sort pictures by nearest to POI
|
|
807
|
+
order_by = SQL("ORDER BY p.geom <-> ST_Point(%(placex)s, %(placey)s, 4326)")
|
|
808
|
+
|
|
782
809
|
# Intersects
|
|
783
810
|
if args.get("intersects") is not None:
|
|
784
811
|
try:
|
|
@@ -836,25 +863,24 @@ def searchItems():
|
|
|
836
863
|
#
|
|
837
864
|
# Database query
|
|
838
865
|
#
|
|
839
|
-
|
|
840
866
|
with psycopg.connect(current_app.config["DB_URL"], row_factory=dict_row, options="-c statement_timeout=30000") as conn:
|
|
841
867
|
with conn.cursor() as cursor:
|
|
842
868
|
query = SQL(
|
|
843
869
|
"""
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
870
|
+
SELECT * FROM (
|
|
871
|
+
SELECT
|
|
872
|
+
p.id, p.ts, p.heading, p.metadata, p.inserted_at,
|
|
873
|
+
ST_AsGeoJSON(p.geom)::json AS geojson,
|
|
874
|
+
sp.seq_id, sp.rank AS rank,
|
|
875
|
+
accounts.name AS account_name, p.exif
|
|
876
|
+
FROM pictures p
|
|
877
|
+
LEFT JOIN sequences_pictures sp ON p.id = sp.pic_id
|
|
878
|
+
LEFT JOIN sequences s ON s.id = sp.seq_id
|
|
879
|
+
LEFT JOIN accounts ON p.account_id = accounts.id
|
|
880
|
+
WHERE {sqlWhere}
|
|
881
|
+
{orderBy}
|
|
882
|
+
LIMIT %(limit)s
|
|
883
|
+
) pic
|
|
858
884
|
LEFT JOIN LATERAL (
|
|
859
885
|
SELECT
|
|
860
886
|
p.id AS prevpic, ST_AsGeoJSON(p.geom)::json AS prevpicgeojson
|
|
@@ -876,6 +902,7 @@ LEFT JOIN LATERAL (
|
|
|
876
902
|
;
|
|
877
903
|
"""
|
|
878
904
|
).format(sqlWhere=SQL(" AND ").join(sqlWhere), sqlSubQueryWhere=SQL(" AND ").join(sqlSubQueryWhere), orderBy=order_by)
|
|
905
|
+
|
|
879
906
|
records = cursor.execute(query, sqlParams)
|
|
880
907
|
|
|
881
908
|
items = [dbPictureToStacItem(str(dbPic["seq_id"]), dbPic) for dbPic in records]
|
|
@@ -1085,25 +1112,37 @@ def patchCollectionItem(collectionId, itemId, account):
|
|
|
1085
1112
|
# Parse received parameters
|
|
1086
1113
|
metadata = {}
|
|
1087
1114
|
content_type = (request.headers.get("Content-Type") or "").split(";")[0]
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1115
|
+
for param in ["visible", "heading"]:
|
|
1116
|
+
if request.is_json and request.json:
|
|
1117
|
+
metadata[param] = request.json.get(param)
|
|
1118
|
+
elif content_type in ["multipart/form-data", "application/x-www-form-urlencoded"]:
|
|
1119
|
+
metadata[param] = request.form.get(param)
|
|
1120
|
+
|
|
1121
|
+
visible = metadata.get("visible")
|
|
1122
|
+
if visible is not None:
|
|
1123
|
+
if visible not in ["true", "false"]:
|
|
1124
|
+
raise errors.InvalidAPIUsage("Picture visibility parameter (visible) should be either unset, true or false", status_code=400)
|
|
1125
|
+
visible = visible == "true"
|
|
1126
|
+
|
|
1127
|
+
# Check if heading is valid
|
|
1128
|
+
heading = metadata.get("heading")
|
|
1129
|
+
if heading is not None:
|
|
1130
|
+
try:
|
|
1131
|
+
heading = int(heading)
|
|
1132
|
+
if heading < 0 or heading > 360:
|
|
1133
|
+
raise ValueError()
|
|
1134
|
+
except ValueError:
|
|
1135
|
+
raise errors.InvalidAPIUsage(
|
|
1136
|
+
"Heading is not valid, should be an integer in degrees from 0° to 360°. North is 0°, East = 90°, South = 180° and West = 270°.",
|
|
1137
|
+
status_code=400,
|
|
1138
|
+
)
|
|
1099
1139
|
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
raise errors.InvalidAPIUsage("Picture visibility parameter (visible) should be either unset, true or false", status_code=400)
|
|
1140
|
+
# If no parameter is set
|
|
1141
|
+
if {visible, heading} == {None}:
|
|
1142
|
+
return getCollectionItem(collectionId, itemId)
|
|
1104
1143
|
|
|
1105
1144
|
# Check if picture exists and if given account is authorized to edit
|
|
1106
|
-
with psycopg.connect(current_app.config["DB_URL"]) as conn:
|
|
1145
|
+
with psycopg.connect(current_app.config["DB_URL"], row_factory=dict_row) as conn:
|
|
1107
1146
|
with conn.cursor() as cursor:
|
|
1108
1147
|
pic = cursor.execute("SELECT status, account_id FROM pictures WHERE id = %s", [itemId]).fetchone()
|
|
1109
1148
|
|
|
@@ -1112,34 +1151,50 @@ def patchCollectionItem(collectionId, itemId, account):
|
|
|
1112
1151
|
raise errors.InvalidAPIUsage(f"Picture {itemId} wasn't found in database", status_code=404)
|
|
1113
1152
|
|
|
1114
1153
|
# Account associated to picture doesn't match current user
|
|
1115
|
-
if account is not None and account.id != str(pic[
|
|
1154
|
+
if account is not None and account.id != str(pic["account_id"]):
|
|
1116
1155
|
raise errors.InvalidAPIUsage("You're not authorized to edit this picture", status_code=403)
|
|
1117
1156
|
|
|
1157
|
+
sqlUpdates = []
|
|
1158
|
+
sqlParams = {"id": itemId, "account": account.id}
|
|
1159
|
+
|
|
1118
1160
|
# Let's edit this picture
|
|
1119
|
-
oldStatus = pic[
|
|
1120
|
-
|
|
1161
|
+
oldStatus = pic["status"]
|
|
1162
|
+
if oldStatus not in ["ready", "hidden"]:
|
|
1163
|
+
# Picture is in a preparing/broken/... state so no edit possible
|
|
1164
|
+
raise errors.InvalidAPIUsage(
|
|
1165
|
+
f"Picture {itemId} is in {oldStatus} state, its visibility can't be changed for now", status_code=400
|
|
1166
|
+
)
|
|
1121
1167
|
|
|
1168
|
+
newStatus = None
|
|
1122
1169
|
if visible is not None:
|
|
1123
|
-
if visible is True
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1170
|
+
newStatus = "ready" if visible is True else "hidden"
|
|
1171
|
+
if newStatus != oldStatus:
|
|
1172
|
+
sqlUpdates.append(SQL("status = %(status)s"))
|
|
1173
|
+
sqlParams["status"] = newStatus
|
|
1174
|
+
|
|
1175
|
+
if heading is not None:
|
|
1176
|
+
sqlUpdates.extend([SQL("heading = %(heading)s"), SQL("heading_computed = false")])
|
|
1177
|
+
sqlParams["heading"] = heading
|
|
1178
|
+
|
|
1179
|
+
if not sqlUpdates:
|
|
1180
|
+
# Nothing to change, we can return the item
|
|
1181
|
+
return getCollectionItem(collectionId, itemId)
|
|
1182
|
+
|
|
1183
|
+
# Note: we set the field `last_account_to_edit` to track who changed the collection last (later we'll make it possible for everybody to edit some collection fields)
|
|
1184
|
+
# setting this field will trigger the history tracking of the collection (using postgres trigger)
|
|
1185
|
+
sqlUpdates.append(SQL("last_account_to_edit = %(account)s"))
|
|
1186
|
+
|
|
1187
|
+
cursor.execute(
|
|
1188
|
+
SQL(
|
|
1189
|
+
"""
|
|
1190
|
+
UPDATE pictures
|
|
1191
|
+
SET {updates}
|
|
1192
|
+
WHERE id = %(id)s
|
|
1193
|
+
"""
|
|
1194
|
+
).format(updates=SQL(", ").join(sqlUpdates)),
|
|
1195
|
+
sqlParams,
|
|
1196
|
+
)
|
|
1197
|
+
conn.commit()
|
|
1143
1198
|
|
|
1144
1199
|
# Redirect response to a classic GET
|
|
1145
1200
|
return getCollectionItem(collectionId, itemId)
|