geovisio 2.6.0__py3-none-any.whl → 2.7.1__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 +36 -7
- geovisio/admin_cli/cleanup.py +2 -2
- geovisio/admin_cli/db.py +1 -4
- geovisio/config_app.py +40 -1
- geovisio/db_migrations.py +24 -3
- geovisio/templates/main.html +13 -13
- geovisio/templates/viewer.html +3 -3
- geovisio/translations/de/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/de/LC_MESSAGES/messages.po +804 -0
- geovisio/translations/el/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/el/LC_MESSAGES/messages.po +685 -0
- geovisio/translations/en/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/en/LC_MESSAGES/messages.po +738 -0
- geovisio/translations/es/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/es/LC_MESSAGES/messages.po +778 -0
- geovisio/translations/fi/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/fi/LC_MESSAGES/messages.po +589 -0
- geovisio/translations/fr/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/fr/LC_MESSAGES/messages.po +814 -0
- geovisio/translations/hu/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/hu/LC_MESSAGES/messages.po +773 -0
- geovisio/translations/ko/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/ko/LC_MESSAGES/messages.po +685 -0
- geovisio/translations/messages.pot +694 -0
- geovisio/translations/nl/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/nl/LC_MESSAGES/messages.po +602 -0
- geovisio/utils/__init__.py +1 -1
- geovisio/utils/auth.py +50 -11
- geovisio/utils/db.py +65 -0
- geovisio/utils/excluded_areas.py +83 -0
- geovisio/utils/extent.py +30 -0
- geovisio/utils/fields.py +1 -1
- geovisio/utils/filesystems.py +0 -1
- geovisio/utils/link.py +14 -0
- geovisio/utils/params.py +20 -0
- geovisio/utils/pictures.py +110 -88
- geovisio/utils/reports.py +171 -0
- geovisio/utils/sequences.py +262 -126
- geovisio/utils/tokens.py +37 -42
- geovisio/utils/upload_set.py +642 -0
- geovisio/web/auth.py +37 -37
- geovisio/web/collections.py +304 -304
- geovisio/web/configuration.py +14 -0
- geovisio/web/docs.py +276 -15
- geovisio/web/excluded_areas.py +377 -0
- geovisio/web/items.py +169 -112
- geovisio/web/map.py +104 -36
- geovisio/web/params.py +69 -26
- geovisio/web/pictures.py +14 -31
- geovisio/web/reports.py +399 -0
- geovisio/web/rss.py +13 -7
- geovisio/web/stac.py +129 -134
- geovisio/web/tokens.py +98 -109
- geovisio/web/upload_set.py +771 -0
- geovisio/web/users.py +100 -73
- geovisio/web/utils.py +28 -9
- geovisio/workers/runner_pictures.py +241 -207
- {geovisio-2.6.0.dist-info → geovisio-2.7.1.dist-info}/METADATA +17 -14
- geovisio-2.7.1.dist-info/RECORD +70 -0
- {geovisio-2.6.0.dist-info → geovisio-2.7.1.dist-info}/WHEEL +1 -1
- geovisio-2.6.0.dist-info/RECORD +0 -41
- {geovisio-2.6.0.dist-info → geovisio-2.7.1.dist-info}/LICENSE +0 -0
geovisio/web/map.py
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
# Some parts of code here are heavily inspired from Paul Ramsey's work
|
|
2
2
|
# See for reference : https://github.com/pramsey/minimal-mvt
|
|
3
3
|
|
|
4
|
-
import psycopg
|
|
5
4
|
import io
|
|
6
5
|
from typing import Optional, Dict, Any, Tuple, List, Union
|
|
7
6
|
from uuid import UUID
|
|
8
7
|
from flask import Blueprint, current_app, send_file, request, jsonify, url_for
|
|
9
|
-
from
|
|
8
|
+
from flask_babel import gettext as _, get_locale
|
|
9
|
+
from geovisio.utils import auth, db
|
|
10
10
|
from geovisio.utils.auth import Account
|
|
11
11
|
from geovisio.web import params
|
|
12
12
|
from geovisio.web.utils import user_dependant_response
|
|
13
|
+
from geovisio.web.configuration import _get_translated
|
|
13
14
|
from geovisio import errors
|
|
14
15
|
from psycopg import sql
|
|
15
16
|
|
|
@@ -21,18 +22,20 @@ ZOOM_PICTURES = 15
|
|
|
21
22
|
|
|
22
23
|
def get_style_json(forUser: Optional[Union[UUID, str]] = None):
|
|
23
24
|
# Get correct vector tiles URL
|
|
24
|
-
tilesUrl = url_for("map.getTile", x="
|
|
25
|
+
tilesUrl = url_for("map.getTile", x="11111111", y="22222222", z="33333333", format="mvt", _external=True)
|
|
25
26
|
sourceId = "geovisio"
|
|
26
27
|
if forUser == "me":
|
|
27
|
-
tilesUrl = url_for("map.getMyTile", x="
|
|
28
|
+
tilesUrl = url_for("map.getMyTile", x="11111111", y="22222222", z="33333333", format="mvt", _external=True)
|
|
28
29
|
sourceId = "geovisio_me"
|
|
29
30
|
elif forUser is not None:
|
|
30
|
-
tilesUrl = url_for("map.getUserTile", userId=forUser, x="
|
|
31
|
+
tilesUrl = url_for("map.getUserTile", userId=forUser, x="11111111", y="22222222", z="33333333", format="mvt", _external=True)
|
|
31
32
|
sourceId = f"geovisio_{str(forUser)}"
|
|
32
|
-
tilesUrl = tilesUrl.replace("
|
|
33
|
+
tilesUrl = tilesUrl.replace("11111111", "{x}").replace("22222222", "{y}").replace("33333333", "{z}")
|
|
33
34
|
|
|
34
35
|
# Display sequence on all zooms if user tiles, after grid on general tiles
|
|
35
|
-
sequenceOpacity =
|
|
36
|
+
sequenceOpacity = (
|
|
37
|
+
["interpolate", ["linear"], ["zoom"], ZOOM_GRID_SEQUENCES + 0.25, 0, ZOOM_GRID_SEQUENCES + 1, 1] if forUser is None else 1
|
|
38
|
+
)
|
|
36
39
|
|
|
37
40
|
layers = [
|
|
38
41
|
{
|
|
@@ -69,35 +72,74 @@ def get_style_json(forUser: Optional[Union[UUID, str]] = None):
|
|
|
69
72
|
layers.append(
|
|
70
73
|
{
|
|
71
74
|
"id": f"{sourceId}_grid",
|
|
72
|
-
"type": "
|
|
75
|
+
"type": "circle",
|
|
73
76
|
"source": sourceId,
|
|
74
77
|
"source-layer": "grid",
|
|
78
|
+
"layout": {
|
|
79
|
+
"circle-sort-key": ["get", "coef"],
|
|
80
|
+
},
|
|
75
81
|
"paint": {
|
|
76
|
-
"
|
|
77
|
-
"fill-opacity": [
|
|
82
|
+
"circle-radius": [
|
|
78
83
|
"interpolate",
|
|
79
84
|
["linear"],
|
|
80
85
|
["zoom"],
|
|
81
|
-
0,
|
|
82
86
|
1,
|
|
87
|
+
# The match get coef rule allows to hide circle if coef is set to 0
|
|
88
|
+
["match", ["get", "coef"], 0, 0, 1],
|
|
83
89
|
ZOOM_GRID_SEQUENCES - 2,
|
|
84
|
-
|
|
90
|
+
["match", ["get", "coef"], 0, 0, 6],
|
|
91
|
+
ZOOM_GRID_SEQUENCES - 1,
|
|
92
|
+
["match", ["get", "coef"], 0, 0, 2.5],
|
|
85
93
|
ZOOM_GRID_SEQUENCES,
|
|
86
|
-
0
|
|
87
|
-
ZOOM_GRID_SEQUENCES +
|
|
94
|
+
["match", ["get", "coef"], 0, 0, 4],
|
|
95
|
+
ZOOM_GRID_SEQUENCES + 1,
|
|
96
|
+
["match", ["get", "coef"], 0, 0, 7],
|
|
97
|
+
],
|
|
98
|
+
"circle-color": ["interpolate", ["linear"], ["get", "coef"], 0, "#FFA726", 0.5, "#E65100", 1, "#3E2723"],
|
|
99
|
+
"circle-opacity": [
|
|
100
|
+
"interpolate",
|
|
101
|
+
["linear"],
|
|
102
|
+
["zoom"],
|
|
103
|
+
ZOOM_GRID_SEQUENCES - 2,
|
|
104
|
+
0.5,
|
|
105
|
+
ZOOM_GRID_SEQUENCES - 1,
|
|
106
|
+
1,
|
|
107
|
+
ZOOM_GRID_SEQUENCES + 0.75,
|
|
108
|
+
1,
|
|
109
|
+
ZOOM_GRID_SEQUENCES + 1,
|
|
88
110
|
0,
|
|
89
111
|
],
|
|
90
112
|
},
|
|
91
113
|
}
|
|
92
114
|
)
|
|
93
115
|
|
|
116
|
+
apiSum = current_app.config["API_SUMMARY"]
|
|
117
|
+
userLang = get_locale().language
|
|
118
|
+
|
|
94
119
|
style = {
|
|
95
120
|
"version": 8,
|
|
96
|
-
"name":
|
|
121
|
+
"name": _get_translated(apiSum.name, userLang)["label"],
|
|
122
|
+
"metadata": {
|
|
123
|
+
"panoramax:fields": {
|
|
124
|
+
"sequences": ["id", "account_id", "model", "type", "date", "gps_accuracy", "h_pixel_density"],
|
|
125
|
+
"pictures": ["id", "account_id", "ts", "heading", "sequences", "type", "model", "gps_accuracy", "h_pixel_density"],
|
|
126
|
+
}
|
|
127
|
+
},
|
|
97
128
|
"sources": {sourceId: {"type": "vector", "tiles": [tilesUrl], "minzoom": 0, "maxzoom": ZOOM_PICTURES}},
|
|
98
129
|
"layers": layers,
|
|
99
130
|
}
|
|
100
131
|
|
|
132
|
+
if forUser is None:
|
|
133
|
+
style["metadata"]["panoramax:fields"]["grid"] = [
|
|
134
|
+
"id",
|
|
135
|
+
"nb_pictures",
|
|
136
|
+
"nb_360_pictures",
|
|
137
|
+
"nb_flat_pictures",
|
|
138
|
+
"coef",
|
|
139
|
+
"coef_360_pictures",
|
|
140
|
+
"coef_flat_pictures",
|
|
141
|
+
]
|
|
142
|
+
|
|
101
143
|
return jsonify(style)
|
|
102
144
|
|
|
103
145
|
|
|
@@ -120,32 +162,31 @@ def checkTileValidity(z, x, y, format):
|
|
|
120
162
|
raises InvalidAPIUsage exceptions if parameters are not OK
|
|
121
163
|
"""
|
|
122
164
|
if z is None or x is None or y is None or format is None:
|
|
123
|
-
raise errors.InvalidAPIUsage("One of required parameter is empty", status_code=404)
|
|
165
|
+
raise errors.InvalidAPIUsage(_("One of required parameter is empty"), status_code=404)
|
|
124
166
|
if format not in ["pbf", "mvt"]:
|
|
125
|
-
raise errors.InvalidAPIUsage("Tile format is invalid, should be either pbf or mvt", status_code=400)
|
|
167
|
+
raise errors.InvalidAPIUsage(_("Tile format is invalid, should be either pbf or mvt"), status_code=400)
|
|
126
168
|
|
|
127
169
|
size = 2**z
|
|
128
170
|
if x >= size or y >= size:
|
|
129
|
-
raise errors.InvalidAPIUsage("X or Y parameter is out of bounds", status_code=404)
|
|
171
|
+
raise errors.InvalidAPIUsage(_("X or Y parameter is out of bounds"), status_code=404)
|
|
130
172
|
if x < 0 or y < 0:
|
|
131
|
-
raise errors.InvalidAPIUsage("X or Y parameter is out of bounds", status_code=404)
|
|
173
|
+
raise errors.InvalidAPIUsage(_("X or Y parameter is out of bounds"), status_code=404)
|
|
132
174
|
if z < 0 or z > 15:
|
|
133
|
-
raise errors.InvalidAPIUsage("Z parameter is out of bounds (should be 0-15)", status_code=404)
|
|
175
|
+
raise errors.InvalidAPIUsage(_("Z parameter is out of bounds (should be 0-15)"), status_code=404)
|
|
134
176
|
|
|
135
177
|
|
|
136
178
|
def _getTile(z: int, x: int, y: int, format: str, onlyForUser: Optional[UUID] = None, filter: Optional[sql.SQL] = None):
|
|
137
179
|
checkTileValidity(z, x, y, format)
|
|
138
180
|
|
|
139
|
-
|
|
140
|
-
with conn.cursor() as cursor:
|
|
141
|
-
query, params = _get_query(z, x, y, onlyForUser, additional_filter=filter)
|
|
142
|
-
res = cursor.execute(query, params).fetchone()
|
|
181
|
+
query, params = _get_query(z, x, y, onlyForUser, additional_filter=filter)
|
|
143
182
|
|
|
144
|
-
|
|
145
|
-
raise errors.InternalError("Impossible to get tile")
|
|
183
|
+
res = db.fetchone(current_app, query, params, timeout=10000)
|
|
146
184
|
|
|
147
|
-
|
|
148
|
-
|
|
185
|
+
if not res:
|
|
186
|
+
raise errors.InternalError(_("Impossible to get tile"))
|
|
187
|
+
|
|
188
|
+
res = res[0]
|
|
189
|
+
return send_file(io.BytesIO(res), mimetype="application/vnd.mapbox-vector-tile")
|
|
149
190
|
|
|
150
191
|
|
|
151
192
|
@bp.route("/map/style.json")
|
|
@@ -184,6 +225,8 @@ def getTile(z: int, x: int, y: int, format: str):
|
|
|
184
225
|
- model (camera make and model)
|
|
185
226
|
- type (flat or equirectangular)
|
|
186
227
|
- date (capture date, as YYYY-MM-DD)
|
|
228
|
+
- gps_accuracy (95% confidence interval of GPS position precision, in meters)
|
|
229
|
+
- h_pixel_density (number of pixels on horizon per field of view degree)
|
|
187
230
|
|
|
188
231
|
Layer "pictures":
|
|
189
232
|
- Available on zoom levels >= 13
|
|
@@ -195,13 +238,19 @@ def getTile(z: int, x: int, y: int, format: str):
|
|
|
195
238
|
- sequences (list of sequences ID this pictures belongs to)
|
|
196
239
|
- type (flat or equirectangular)
|
|
197
240
|
- model (camera make and model)
|
|
241
|
+
- gps_accuracy (95% confidence interval of GPS position precision, in meters)
|
|
242
|
+
- h_pixel_density (number of pixels on horizon per field of view degree)
|
|
198
243
|
|
|
199
244
|
Layer "grid":
|
|
200
245
|
- Available on zoom levels 0 to 5 (included)
|
|
201
246
|
- Available properties:
|
|
202
247
|
- id
|
|
203
248
|
- nb_pictures
|
|
249
|
+
- nb_360_pictures (number of 360° pictures)
|
|
250
|
+
- nb_flat_pictures (number of flat pictures)
|
|
204
251
|
- coef (value from 0 to 1, relative quantity of available pictures)
|
|
252
|
+
- coef_360_pictures (value from 0 to 1, relative quantity of available 360° pictures)
|
|
253
|
+
- coef_flat_pictures (value from 0 to 1, relative quantity of available flat pictures)
|
|
205
254
|
|
|
206
255
|
---
|
|
207
256
|
tags:
|
|
@@ -295,16 +344,30 @@ def _get_query(z: int, x: int, y: int, onlyForUser: Optional[UUID], additional_f
|
|
|
295
344
|
#
|
|
296
345
|
|
|
297
346
|
grid_fields = [
|
|
298
|
-
sql.SQL("ST_AsMVTGeom(ST_Transform(geom, 3857), ST_TileEnvelope(%(z)s, %(x)s, %(y)s)) AS geom"),
|
|
347
|
+
sql.SQL("ST_AsMVTGeom(ST_Transform(ST_Centroid(geom), 3857), ST_TileEnvelope(%(z)s, %(x)s, %(y)s)) AS geom"),
|
|
299
348
|
sql.SQL("id"),
|
|
300
349
|
sql.SQL("nb_pictures"),
|
|
350
|
+
sql.SQL("nb_360_pictures"),
|
|
351
|
+
sql.SQL("nb_pictures - nb_360_pictures AS nb_flat_pictures"),
|
|
301
352
|
sql.SQL(
|
|
302
|
-
"""
|
|
303
|
-
((CASE WHEN nb_pictures <= (select PERCENTILE_CONT(0.5) WITHIN GROUP(ORDER BY nb_pictures) from pictures_grid)
|
|
353
|
+
"""((CASE WHEN nb_pictures = 0 THEN 0 WHEN nb_pictures <= (select PERCENTILE_CONT(0.5) WITHIN GROUP(ORDER BY nb_pictures) from pictures_grid)
|
|
304
354
|
THEN nb_pictures::float / (select PERCENTILE_CONT(0.5) WITHIN GROUP(ORDER BY nb_pictures) from pictures_grid) * 0.5
|
|
305
355
|
ELSE 0.5 + nb_pictures::float / (SELECT MAX(nb_pictures) FROM pictures_grid) * 0.5
|
|
306
|
-
END) * 10)::int / 10::float AS coef
|
|
307
|
-
|
|
356
|
+
END) * 10)::int / 10::float AS coef"""
|
|
357
|
+
),
|
|
358
|
+
sql.SQL(
|
|
359
|
+
"""((CASE WHEN nb_360_pictures = 0 THEN 0
|
|
360
|
+
WHEN nb_360_pictures <= (select PERCENTILE_CONT(0.5) WITHIN GROUP(ORDER BY nb_360_pictures) from pictures_grid)
|
|
361
|
+
THEN nb_360_pictures::float / (select PERCENTILE_CONT(0.5) WITHIN GROUP(ORDER BY nb_360_pictures) from pictures_grid) * 0.5
|
|
362
|
+
ELSE 0.5 + nb_360_pictures::float / (SELECT MAX(nb_360_pictures) FROM pictures_grid) * 0.5
|
|
363
|
+
END) * 10)::int / 10::float AS coef_360_pictures"""
|
|
364
|
+
),
|
|
365
|
+
sql.SQL(
|
|
366
|
+
"""((CASE WHEN (nb_pictures - nb_360_pictures) = 0 THEN 0
|
|
367
|
+
WHEN (nb_pictures - nb_360_pictures) <= (select PERCENTILE_CONT(0.5) WITHIN GROUP(ORDER BY (nb_pictures - nb_360_pictures)) from pictures_grid)
|
|
368
|
+
THEN (nb_pictures - nb_360_pictures)::float / (select PERCENTILE_CONT(0.5) WITHIN GROUP(ORDER BY (nb_pictures - nb_360_pictures)) from pictures_grid) * 0.5
|
|
369
|
+
ELSE 0.5 + (nb_pictures - nb_360_pictures)::float / (SELECT MAX((nb_pictures - nb_360_pictures)) FROM pictures_grid) * 0.5
|
|
370
|
+
END) * 10)::int / 10::float AS coef_flat_pictures"""
|
|
308
371
|
),
|
|
309
372
|
]
|
|
310
373
|
sequences_fields = [
|
|
@@ -323,6 +386,8 @@ def _get_query(z: int, x: int, y: int, onlyForUser: Optional[UUID], additional_f
|
|
|
323
386
|
sql.SQL("computed_model AS model"),
|
|
324
387
|
sql.SQL("computed_type AS type"),
|
|
325
388
|
sql.SQL("computed_capture_date AS date"),
|
|
389
|
+
sql.SQL("computed_gps_accuracy AS gps_accuracy"),
|
|
390
|
+
sql.SQL("computed_h_pixel_density AS h_pixel_density"),
|
|
326
391
|
]
|
|
327
392
|
)
|
|
328
393
|
|
|
@@ -352,15 +417,17 @@ def _get_query(z: int, x: int, y: int, onlyForUser: Optional[UUID], additional_f
|
|
|
352
417
|
ST_AsMVTGeom(ST_Transform(p.geom, 3857), ST_TileEnvelope(%(z)s, %(x)s, %(y)s)) AS geom,
|
|
353
418
|
p.id, p.ts, p.heading, p.account_id,
|
|
354
419
|
NULLIF(p.status != 'ready' OR s.status != 'ready', FALSE) AS hidden,
|
|
355
|
-
array_to_json(ARRAY_AGG(sp.seq_id)) AS sequences,
|
|
356
420
|
p.metadata->>'type' AS type,
|
|
357
|
-
TRIM(CONCAT(p.metadata->>'make', ' ', p.metadata->>'model')) AS model
|
|
421
|
+
TRIM(CONCAT(p.metadata->>'make', ' ', p.metadata->>'model')) AS model,
|
|
422
|
+
gps_accuracy_m AS gps_accuracy,
|
|
423
|
+
h_pixel_density,
|
|
424
|
+
array_to_json(ARRAY_AGG(sp.seq_id)) AS sequences
|
|
358
425
|
FROM pictures p
|
|
359
426
|
LEFT JOIN sequences_pictures sp ON p.id = sp.pic_id
|
|
360
427
|
LEFT JOIN sequences s ON s.id = sp.seq_id
|
|
361
428
|
WHERE
|
|
362
429
|
{pictures_filter}
|
|
363
|
-
GROUP BY 1, 2, 3, 4, 5, 6
|
|
430
|
+
GROUP BY 1, 2, 3, 4, 5, 6, 7, 8, 9, 10
|
|
364
431
|
) mvtgeompics
|
|
365
432
|
) mvtpictures
|
|
366
433
|
"""
|
|
@@ -466,6 +533,7 @@ def getUserStyle(userId: UUID):
|
|
|
466
533
|
|
|
467
534
|
|
|
468
535
|
@bp.route("/users/<uuid:userId>/map/<int:z>/<int:x>/<int:y>.<format>")
|
|
536
|
+
@user_dependant_response(True)
|
|
469
537
|
def getUserTile(userId: UUID, z: int, x: int, y: int, format: str):
|
|
470
538
|
"""Get pictures and sequences as vector tiles for a specific user.
|
|
471
539
|
This tile will contain the same layers as the generic tiles (from `/map/z/x/y.format` route), but with sequences properties on all levels
|
geovisio/web/params.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from uuid import UUID
|
|
2
|
+
|
|
2
3
|
from geovisio import errors
|
|
3
4
|
import dateutil.parser
|
|
4
5
|
from dateutil import tz
|
|
@@ -9,9 +10,12 @@ from werkzeug.datastructures import MultiDict
|
|
|
9
10
|
from typing import Optional, Tuple, Any, List
|
|
10
11
|
from pygeofilter.backends.sql import to_sql_where
|
|
11
12
|
from pygeofilter.parsers.ecql import parse as ecql_parser
|
|
13
|
+
from pygeofilter import ast
|
|
14
|
+
from pygeofilter.backends.evaluator import Evaluator, handle
|
|
12
15
|
from psycopg import sql
|
|
13
16
|
from geovisio.utils.sequences import STAC_FIELD_MAPPINGS, STAC_FIELD_TO_SQL_FILTER
|
|
14
17
|
from geovisio.utils.fields import SortBy, SQLDirection, SortByField
|
|
18
|
+
from flask_babel import gettext as _
|
|
15
19
|
|
|
16
20
|
|
|
17
21
|
RGX_SORTBY = re.compile("[+-]?[A-Za-z_].*(,[+-]?[A-Za-z_].*)*")
|
|
@@ -47,7 +51,7 @@ def parse_datetime(value, error, fallback_as_UTC=False):
|
|
|
47
51
|
d = None
|
|
48
52
|
try:
|
|
49
53
|
d = datetime.datetime.fromisoformat(value)
|
|
50
|
-
except ValueError
|
|
54
|
+
except ValueError:
|
|
51
55
|
pass
|
|
52
56
|
if not d:
|
|
53
57
|
try:
|
|
@@ -81,17 +85,17 @@ def parse_datetime_interval(value: Optional[str]) -> Tuple[Optional[datetime.dat
|
|
|
81
85
|
dates = value.split("/")
|
|
82
86
|
|
|
83
87
|
if len(dates) == 1:
|
|
84
|
-
d = parse_datetime(dates[0], error=
|
|
88
|
+
d = parse_datetime(dates[0], error="Invalid `datetime` argument", fallback_as_UTC=True)
|
|
85
89
|
return (d, d)
|
|
86
90
|
|
|
87
91
|
elif len(dates) == 2:
|
|
88
92
|
# Check if interval is closed or open-ended
|
|
89
93
|
mind, maxd = dates
|
|
90
|
-
mind = None if mind == ".." else parse_datetime(mind, error=
|
|
91
|
-
maxd = None if maxd == ".." else parse_datetime(maxd, error=
|
|
94
|
+
mind = None if mind == ".." else parse_datetime(mind, error="Invalid start date in `datetime` argument", fallback_as_UTC=True)
|
|
95
|
+
maxd = None if maxd == ".." else parse_datetime(maxd, error="Invalid end date in `datetime` argument", fallback_as_UTC=True)
|
|
92
96
|
return (mind, maxd)
|
|
93
97
|
else:
|
|
94
|
-
raise errors.InvalidAPIUsage("Parameter datetime should contain one or two dates", status_code=400)
|
|
98
|
+
raise errors.InvalidAPIUsage(_("Parameter datetime should contain one or two dates"), status_code=400)
|
|
95
99
|
|
|
96
100
|
|
|
97
101
|
def parse_bbox(value: Optional[Any], tryFallbacks=True):
|
|
@@ -161,12 +165,12 @@ def parse_bbox(value: Optional[Any], tryFallbacks=True):
|
|
|
161
165
|
or bbox[3] > 90
|
|
162
166
|
):
|
|
163
167
|
raise errors.InvalidAPIUsage(
|
|
164
|
-
"Parameter bbox must contain valid longitude (-180 to 180) and latitude (-90 to 90) values", status_code=400
|
|
168
|
+
_("Parameter bbox must contain valid longitude (-180 to 180) and latitude (-90 to 90) values"), status_code=400
|
|
165
169
|
)
|
|
166
170
|
else:
|
|
167
171
|
return bbox
|
|
168
172
|
except ValueError:
|
|
169
|
-
raise errors.InvalidAPIUsage("Parameter bbox must be in format [minX, minY, maxX, maxY]", status_code=400)
|
|
173
|
+
raise errors.InvalidAPIUsage(_("Parameter bbox must be in format [minX, minY, maxX, maxY]"), status_code=400)
|
|
170
174
|
else:
|
|
171
175
|
return None
|
|
172
176
|
|
|
@@ -203,11 +207,11 @@ def parse_lonlat(values: Optional[Any], paramName: Optional[str] = None) -> Opti
|
|
|
203
207
|
entries = parse_list(values, paramName=paramName)
|
|
204
208
|
|
|
205
209
|
if entries is None or len(entries) != 2:
|
|
206
|
-
raise errors.InvalidAPIUsage(
|
|
210
|
+
raise errors.InvalidAPIUsage(_("Parameter %(p)s must be coordinates in lat,lon format", p=paramName or ""), status_code=400)
|
|
207
211
|
|
|
208
212
|
return [
|
|
209
|
-
as_longitude(entries[0],
|
|
210
|
-
as_latitude(entries[1],
|
|
213
|
+
as_longitude(entries[0], _("Longitude in parameter %(p)s is not valid (should be between -180 and 180)", p=paramName or "")),
|
|
214
|
+
as_latitude(entries[1], _("Latitude in parameter %(p)s is not valid (should be between -90 and 90)", p=paramName or "")),
|
|
211
215
|
]
|
|
212
216
|
|
|
213
217
|
|
|
@@ -230,18 +234,20 @@ def parse_distance_range(values: Optional[str], paramName: Optional[str] = None)
|
|
|
230
234
|
dists = values.split("-")
|
|
231
235
|
if len(dists) != 2:
|
|
232
236
|
raise errors.InvalidAPIUsage(
|
|
233
|
-
|
|
237
|
+
_('Parameter %(p)s is invalid (should be a distance range in meters like "5-15")', p={paramName or ""}), status_code=400
|
|
234
238
|
)
|
|
235
239
|
try:
|
|
236
240
|
dists = [int(d) for d in dists]
|
|
237
241
|
if dists[0] > dists[1]:
|
|
238
|
-
raise errors.InvalidAPIUsage(
|
|
242
|
+
raise errors.InvalidAPIUsage(
|
|
243
|
+
_("Parameter %(p)s has a min value greater than its max value", p=paramName or ""), status_code=400
|
|
244
|
+
)
|
|
239
245
|
else:
|
|
240
246
|
return dists
|
|
241
247
|
|
|
242
248
|
except ValueError:
|
|
243
249
|
raise errors.InvalidAPIUsage(
|
|
244
|
-
|
|
250
|
+
_('Parameter %(p)s is invalid (should be a distance range in meters like "5-15")', p={paramName or ""}), status_code=400
|
|
245
251
|
)
|
|
246
252
|
else:
|
|
247
253
|
return None
|
|
@@ -308,7 +314,7 @@ def parse_list(value: Optional[Any], tryFallbacks: bool = True, paramName: Optio
|
|
|
308
314
|
value = value.replace("[", "").replace("]", "")
|
|
309
315
|
res = [n.strip() for n in value.split(",")]
|
|
310
316
|
else:
|
|
311
|
-
raise errors.InvalidAPIUsage(
|
|
317
|
+
raise errors.InvalidAPIUsage(_("Parameter %(p)s must be a valid list", p=paramName or ""), status_code=400)
|
|
312
318
|
|
|
313
319
|
if len(res) == 0:
|
|
314
320
|
return None
|
|
@@ -328,10 +334,10 @@ def parse_filter(value: Optional[str]) -> Optional[sql.SQL]:
|
|
|
328
334
|
SQL("(s.updated_at >= '2023-12-31')")
|
|
329
335
|
>>> parse_filter("updated >= '2023-12-31' AND created < '2023-10-31'")
|
|
330
336
|
SQL("((s.updated_at >= '2023-12-31') AND (s.inserted_at < '2023-10-31'))")
|
|
331
|
-
>>> parse_filter("status IN ('deleted','ready')")
|
|
332
|
-
SQL("s.status IN ('deleted', 'ready')")
|
|
337
|
+
>>> parse_filter("status IN ('deleted','ready')") # when we ask for deleted, we should also have hidden collections
|
|
338
|
+
SQL("s.status IN ('deleted', 'ready', 'hidden')")
|
|
333
339
|
>>> parse_filter("status = 'deleted' OR status = 'ready'")
|
|
334
|
-
SQL("((s.status = 'deleted') OR (s.status = 'ready'))")
|
|
340
|
+
SQL("(((s.status = 'deleted') OR (s.status = 'hidden')) OR (s.status = 'ready'))")
|
|
335
341
|
>>> parse_filter('invalid = 10') # doctest: +IGNORE_EXCEPTION_DETAIL
|
|
336
342
|
Traceback (most recent call last):
|
|
337
343
|
geovisio.errors.InvalidAPIUsage: Unsupported filter parameter
|
|
@@ -342,14 +348,51 @@ def parse_filter(value: Optional[str]) -> Optional[sql.SQL]:
|
|
|
342
348
|
if value is not None and len(value) > 0:
|
|
343
349
|
try:
|
|
344
350
|
filterAst = ecql_parser(value)
|
|
345
|
-
|
|
346
|
-
|
|
351
|
+
altered_ast = _alterFilterAst(filterAst) # type: ignore
|
|
352
|
+
|
|
353
|
+
f = to_sql_where(altered_ast, STAC_FIELD_TO_SQL_FILTER).replace('"', "")
|
|
354
|
+
return sql.SQL(f) # type: ignore
|
|
347
355
|
except:
|
|
348
|
-
raise errors.InvalidAPIUsage(
|
|
356
|
+
raise errors.InvalidAPIUsage(_("Unsupported filter parameter"), status_code=400)
|
|
349
357
|
else:
|
|
350
358
|
return None
|
|
351
359
|
|
|
352
360
|
|
|
361
|
+
class _FilterAstUpdated(Evaluator):
|
|
362
|
+
"""
|
|
363
|
+
We alter the parsed AST in order to always query for 'hidden' pictures when we query for 'deleted' ones
|
|
364
|
+
|
|
365
|
+
The rational here is that for non-owned pictures/sequences, when a pictures/sequence is 'hidden' it should be advertised as 'deleted'.
|
|
366
|
+
|
|
367
|
+
This is especially important for crawler like the meta-catalog, since they should also delete the sequence/picture when it is hidden
|
|
368
|
+
"""
|
|
369
|
+
|
|
370
|
+
@handle(ast.Equal)
|
|
371
|
+
def eq(self, node, lhs, rhs):
|
|
372
|
+
if lhs == ast.Attribute("status") and rhs == "deleted":
|
|
373
|
+
return ast.Or(node, ast.Equal(ast.Attribute("status"), "hidden")) # type: ignore
|
|
374
|
+
return node
|
|
375
|
+
|
|
376
|
+
@handle(ast.Or)
|
|
377
|
+
def or_(self, node, lhs, rhs):
|
|
378
|
+
return ast.Or(lhs, rhs)
|
|
379
|
+
|
|
380
|
+
@handle(ast.In)
|
|
381
|
+
def in_(self, node, lhs, *options):
|
|
382
|
+
if "deleted" in node.sub_nodes:
|
|
383
|
+
node.sub_nodes.append("hidden")
|
|
384
|
+
return node
|
|
385
|
+
|
|
386
|
+
def adopt(self, node, *sub_args):
|
|
387
|
+
return node
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def _alterFilterAst(ast: ast.Node):
|
|
391
|
+
filtered = _FilterAstUpdated().evaluate(ast)
|
|
392
|
+
|
|
393
|
+
return filtered
|
|
394
|
+
|
|
395
|
+
|
|
353
396
|
def parse_sortby(value: Optional[str]) -> Optional[SortBy]:
|
|
354
397
|
"""Reads STAC/OGC sortby parameter, and sends a SQL ORDER BY string.
|
|
355
398
|
|
|
@@ -395,7 +438,7 @@ def parse_sortby(value: Optional[str]) -> Optional[SortBy]:
|
|
|
395
438
|
|
|
396
439
|
# Check if in value mapping
|
|
397
440
|
if vOnly not in STAC_FIELD_MAPPINGS:
|
|
398
|
-
raise errors.InvalidAPIUsage(
|
|
441
|
+
raise errors.InvalidAPIUsage(_("Unsupported sortby parameter: invalid column name"), status_code=400)
|
|
399
442
|
field_mapping = STAC_FIELD_MAPPINGS[vOnly]
|
|
400
443
|
|
|
401
444
|
orders.append(SortByField(field=field_mapping, direction=direction))
|
|
@@ -403,7 +446,7 @@ def parse_sortby(value: Optional[str]) -> Optional[SortBy]:
|
|
|
403
446
|
# Create definitive ORDER string
|
|
404
447
|
return SortBy(fields=orders)
|
|
405
448
|
else:
|
|
406
|
-
raise errors.InvalidAPIUsage(
|
|
449
|
+
raise errors.InvalidAPIUsage(_("Unsupported sortby parameter: syntax isn't correct"), status_code=400)
|
|
407
450
|
else:
|
|
408
451
|
return None
|
|
409
452
|
|
|
@@ -429,10 +472,10 @@ def parse_collections_limit(limit: Optional[str]) -> int:
|
|
|
429
472
|
try:
|
|
430
473
|
int_limit = int(limit)
|
|
431
474
|
except ValueError:
|
|
432
|
-
raise errors.InvalidAPIUsage(
|
|
475
|
+
raise errors.InvalidAPIUsage(_("limit parameter should be a valid, positive integer (between 1 and %(v)s)", v=SEQUENCES_MAX_FETCH))
|
|
433
476
|
|
|
434
477
|
if int_limit < 1 or int_limit > SEQUENCES_MAX_FETCH:
|
|
435
|
-
raise errors.InvalidAPIUsage(
|
|
478
|
+
raise errors.InvalidAPIUsage(_("limit parameter should be an integer between 1 and %(v)s", v=SEQUENCES_MAX_FETCH))
|
|
436
479
|
else:
|
|
437
480
|
return int_limit
|
|
438
481
|
|
|
@@ -443,7 +486,7 @@ def as_longitude(value: str, error):
|
|
|
443
486
|
except ValueError as e:
|
|
444
487
|
raise errors.InvalidAPIUsage(message=error, payload={"details": {"error": str(e)}})
|
|
445
488
|
if l < -180 or l > 180:
|
|
446
|
-
raise errors.InvalidAPIUsage(message=error, payload={"details": {"error": "longitude needs to be between -180 and 180"}})
|
|
489
|
+
raise errors.InvalidAPIUsage(message=error, payload={"details": {"error": _("longitude needs to be between -180 and 180")}})
|
|
447
490
|
return l
|
|
448
491
|
|
|
449
492
|
|
|
@@ -453,7 +496,7 @@ def as_latitude(value: str, error):
|
|
|
453
496
|
except ValueError as e:
|
|
454
497
|
raise errors.InvalidAPIUsage(message=error, payload={"details": {"error": str(e)}})
|
|
455
498
|
if l < -90 or l > 90:
|
|
456
|
-
raise errors.InvalidAPIUsage(message=error, payload={"details": {"error": "latitude needs to be between -90 and 90"}})
|
|
499
|
+
raise errors.InvalidAPIUsage(message=error, payload={"details": {"error": _("latitude needs to be between -90 and 90")}})
|
|
457
500
|
return l
|
|
458
501
|
|
|
459
502
|
|
geovisio/web/pictures.py
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import
|
|
2
|
-
from itertools import repeat
|
|
3
|
-
from flask import Blueprint, current_app, request
|
|
1
|
+
from flask import Blueprint, current_app
|
|
4
2
|
from geovisio import utils, errors
|
|
5
3
|
from flask import redirect
|
|
4
|
+
from flask_babel import gettext as _
|
|
6
5
|
import logging
|
|
7
6
|
|
|
8
7
|
bp = Blueprint("pictures", __name__, url_prefix="/api/pictures")
|
|
@@ -25,7 +24,7 @@ def getPictureHD(pictureId, format):
|
|
|
25
24
|
type: string
|
|
26
25
|
- name: format
|
|
27
26
|
in: path
|
|
28
|
-
description: Wanted format for output image (
|
|
27
|
+
description: Wanted format for output image (for the moment only jpg)
|
|
29
28
|
required: true
|
|
30
29
|
schema:
|
|
31
30
|
type: string
|
|
@@ -37,10 +36,6 @@ def getPictureHD(pictureId, format):
|
|
|
37
36
|
schema:
|
|
38
37
|
type: string
|
|
39
38
|
format: binary
|
|
40
|
-
image/webp:
|
|
41
|
-
schema:
|
|
42
|
-
type: string
|
|
43
|
-
format: binary
|
|
44
39
|
"""
|
|
45
40
|
|
|
46
41
|
utils.pictures.checkFormatParam(format)
|
|
@@ -55,7 +50,7 @@ def getPictureHD(pictureId, format):
|
|
|
55
50
|
try:
|
|
56
51
|
picture = fses.permanent.openbin(utils.pictures.getHDPicturePath(pictureId))
|
|
57
52
|
except:
|
|
58
|
-
raise errors.InvalidAPIUsage("Unable to read picture on filesystem", status_code=500)
|
|
53
|
+
raise errors.InvalidAPIUsage(_("Unable to read picture on filesystem"), status_code=500)
|
|
59
54
|
|
|
60
55
|
return utils.pictures.sendInFormat(picture, "jpeg", format)
|
|
61
56
|
|
|
@@ -75,7 +70,7 @@ def getPictureSD(pictureId, format):
|
|
|
75
70
|
type: string
|
|
76
71
|
- name: format
|
|
77
72
|
in: path
|
|
78
|
-
description: Wanted format for output image (
|
|
73
|
+
description: Wanted format for output image (for the moment only jpg)
|
|
79
74
|
required: true
|
|
80
75
|
schema:
|
|
81
76
|
type: string
|
|
@@ -87,10 +82,6 @@ def getPictureSD(pictureId, format):
|
|
|
87
82
|
schema:
|
|
88
83
|
type: string
|
|
89
84
|
format: binary
|
|
90
|
-
image/webp:
|
|
91
|
-
schema:
|
|
92
|
-
type: string
|
|
93
|
-
format: binary
|
|
94
85
|
"""
|
|
95
86
|
utils.pictures.checkFormatParam(format)
|
|
96
87
|
|
|
@@ -104,7 +95,7 @@ def getPictureSD(pictureId, format):
|
|
|
104
95
|
try:
|
|
105
96
|
picture = fses.derivates.openbin(utils.pictures.getPictureFolderPath(pictureId) + "/sd.jpg")
|
|
106
97
|
except:
|
|
107
|
-
raise errors.InvalidAPIUsage("Unable to read picture on filesystem", status_code=500)
|
|
98
|
+
raise errors.InvalidAPIUsage(_("Unable to read picture on filesystem"), status_code=500)
|
|
108
99
|
|
|
109
100
|
return utils.pictures.sendInFormat(picture, "jpeg", format)
|
|
110
101
|
|
|
@@ -124,7 +115,7 @@ def getPictureThumb(pictureId, format):
|
|
|
124
115
|
type: string
|
|
125
116
|
- name: format
|
|
126
117
|
in: path
|
|
127
|
-
description: Wanted format for output image (
|
|
118
|
+
description: Wanted format for output image (for the moment only jpg)
|
|
128
119
|
required: true
|
|
129
120
|
schema:
|
|
130
121
|
type: string
|
|
@@ -136,10 +127,6 @@ def getPictureThumb(pictureId, format):
|
|
|
136
127
|
schema:
|
|
137
128
|
type: string
|
|
138
129
|
format: binary
|
|
139
|
-
image/webp:
|
|
140
|
-
schema:
|
|
141
|
-
type: string
|
|
142
|
-
format: binary
|
|
143
130
|
"""
|
|
144
131
|
return utils.pictures.sendThumbnail(pictureId, format)
|
|
145
132
|
|
|
@@ -171,7 +158,7 @@ def getPictureTile(pictureId, col, row, format):
|
|
|
171
158
|
type: number
|
|
172
159
|
- name: format
|
|
173
160
|
in: path
|
|
174
|
-
description: Wanted format for output image (
|
|
161
|
+
description: Wanted format for output image (for the moment only jpg)
|
|
175
162
|
required: true
|
|
176
163
|
schema:
|
|
177
164
|
type: string
|
|
@@ -183,10 +170,6 @@ def getPictureTile(pictureId, col, row, format):
|
|
|
183
170
|
schema:
|
|
184
171
|
type: string
|
|
185
172
|
format: binary
|
|
186
|
-
image/webp:
|
|
187
|
-
schema:
|
|
188
|
-
type: string
|
|
189
|
-
format: binary
|
|
190
173
|
"""
|
|
191
174
|
|
|
192
175
|
utils.pictures.checkFormatParam(format)
|
|
@@ -201,27 +184,27 @@ def getPictureTile(pictureId, col, row, format):
|
|
|
201
184
|
picPath = f"{utils.pictures.getPictureFolderPath(pictureId)}/tiles/{col}_{row}.jpg"
|
|
202
185
|
|
|
203
186
|
if metadata["type"] == "flat":
|
|
204
|
-
raise errors.InvalidAPIUsage("Tiles are not available for flat pictures", status_code=404)
|
|
187
|
+
raise errors.InvalidAPIUsage(_("Tiles are not available for flat pictures"), status_code=404)
|
|
205
188
|
|
|
206
189
|
try:
|
|
207
190
|
col = int(col)
|
|
208
191
|
except:
|
|
209
|
-
raise errors.InvalidAPIUsage("Column parameter is invalid, should be an integer", status_code=404)
|
|
192
|
+
raise errors.InvalidAPIUsage(_("Column parameter is invalid, should be an integer"), status_code=404)
|
|
210
193
|
|
|
211
194
|
if col < 0 or col >= metadata["cols"]:
|
|
212
|
-
raise errors.InvalidAPIUsage("Column parameter is invalid", status_code=404)
|
|
195
|
+
raise errors.InvalidAPIUsage(_("Column parameter is invalid"), status_code=404)
|
|
213
196
|
|
|
214
197
|
try:
|
|
215
198
|
row = int(row)
|
|
216
199
|
except:
|
|
217
|
-
raise errors.InvalidAPIUsage("Row parameter is invalid, should be an integer", status_code=404)
|
|
200
|
+
raise errors.InvalidAPIUsage(_("Row parameter is invalid, should be an integer"), status_code=404)
|
|
218
201
|
|
|
219
202
|
if row < 0 or row >= metadata["rows"]:
|
|
220
|
-
raise errors.InvalidAPIUsage("Row parameter is invalid", status_code=404)
|
|
203
|
+
raise errors.InvalidAPIUsage(_("Row parameter is invalid"), status_code=404)
|
|
221
204
|
|
|
222
205
|
try:
|
|
223
206
|
picture = fses.derivates.openbin(picPath)
|
|
224
207
|
except:
|
|
225
|
-
raise errors.InvalidAPIUsage("Unable to read picture on filesystem", status_code=500)
|
|
208
|
+
raise errors.InvalidAPIUsage(_("Unable to read picture on filesystem"), status_code=500)
|
|
226
209
|
|
|
227
210
|
return utils.pictures.sendInFormat(picture, "jpeg", format)
|