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.
Files changed (62) hide show
  1. geovisio/__init__.py +36 -7
  2. geovisio/admin_cli/cleanup.py +2 -2
  3. geovisio/admin_cli/db.py +1 -4
  4. geovisio/config_app.py +40 -1
  5. geovisio/db_migrations.py +24 -3
  6. geovisio/templates/main.html +13 -13
  7. geovisio/templates/viewer.html +3 -3
  8. geovisio/translations/de/LC_MESSAGES/messages.mo +0 -0
  9. geovisio/translations/de/LC_MESSAGES/messages.po +804 -0
  10. geovisio/translations/el/LC_MESSAGES/messages.mo +0 -0
  11. geovisio/translations/el/LC_MESSAGES/messages.po +685 -0
  12. geovisio/translations/en/LC_MESSAGES/messages.mo +0 -0
  13. geovisio/translations/en/LC_MESSAGES/messages.po +738 -0
  14. geovisio/translations/es/LC_MESSAGES/messages.mo +0 -0
  15. geovisio/translations/es/LC_MESSAGES/messages.po +778 -0
  16. geovisio/translations/fi/LC_MESSAGES/messages.mo +0 -0
  17. geovisio/translations/fi/LC_MESSAGES/messages.po +589 -0
  18. geovisio/translations/fr/LC_MESSAGES/messages.mo +0 -0
  19. geovisio/translations/fr/LC_MESSAGES/messages.po +814 -0
  20. geovisio/translations/hu/LC_MESSAGES/messages.mo +0 -0
  21. geovisio/translations/hu/LC_MESSAGES/messages.po +773 -0
  22. geovisio/translations/ko/LC_MESSAGES/messages.mo +0 -0
  23. geovisio/translations/ko/LC_MESSAGES/messages.po +685 -0
  24. geovisio/translations/messages.pot +694 -0
  25. geovisio/translations/nl/LC_MESSAGES/messages.mo +0 -0
  26. geovisio/translations/nl/LC_MESSAGES/messages.po +602 -0
  27. geovisio/utils/__init__.py +1 -1
  28. geovisio/utils/auth.py +50 -11
  29. geovisio/utils/db.py +65 -0
  30. geovisio/utils/excluded_areas.py +83 -0
  31. geovisio/utils/extent.py +30 -0
  32. geovisio/utils/fields.py +1 -1
  33. geovisio/utils/filesystems.py +0 -1
  34. geovisio/utils/link.py +14 -0
  35. geovisio/utils/params.py +20 -0
  36. geovisio/utils/pictures.py +110 -88
  37. geovisio/utils/reports.py +171 -0
  38. geovisio/utils/sequences.py +262 -126
  39. geovisio/utils/tokens.py +37 -42
  40. geovisio/utils/upload_set.py +642 -0
  41. geovisio/web/auth.py +37 -37
  42. geovisio/web/collections.py +304 -304
  43. geovisio/web/configuration.py +14 -0
  44. geovisio/web/docs.py +276 -15
  45. geovisio/web/excluded_areas.py +377 -0
  46. geovisio/web/items.py +169 -112
  47. geovisio/web/map.py +104 -36
  48. geovisio/web/params.py +69 -26
  49. geovisio/web/pictures.py +14 -31
  50. geovisio/web/reports.py +399 -0
  51. geovisio/web/rss.py +13 -7
  52. geovisio/web/stac.py +129 -134
  53. geovisio/web/tokens.py +98 -109
  54. geovisio/web/upload_set.py +771 -0
  55. geovisio/web/users.py +100 -73
  56. geovisio/web/utils.py +28 -9
  57. geovisio/workers/runner_pictures.py +241 -207
  58. {geovisio-2.6.0.dist-info → geovisio-2.7.1.dist-info}/METADATA +17 -14
  59. geovisio-2.7.1.dist-info/RECORD +70 -0
  60. {geovisio-2.6.0.dist-info → geovisio-2.7.1.dist-info}/WHEEL +1 -1
  61. geovisio-2.6.0.dist-info/RECORD +0 -41
  62. {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 geovisio.utils import auth
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="111", y="222", z="333", format="mvt", _external=True)
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="111", y="222", z="333", format="mvt", _external=True)
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="111", y="222", z="333", format="mvt", _external=True)
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("111", "{x}").replace("222", "{y}").replace("333", "{z}")
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 = ["interpolate", ["linear"], ["zoom"], ZOOM_GRID_SEQUENCES, 0, ZOOM_GRID_SEQUENCES + 1, 1] if forUser is None else 1
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": "fill",
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
- "fill-color": ["interpolate-hcl", ["linear"], ["get", "coef"], 0, "#FFCC80", 0.5, "#E65100", 1, "#BF360C"],
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
- 1,
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.8,
87
- ZOOM_GRID_SEQUENCES + 0.5,
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": "GeoVisio Vector Tiles",
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
- with psycopg.connect(current_app.config["DB_URL"], options="-c statement_timeout=10000") as conn:
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
- if not res:
145
- raise errors.InternalError("Impossible to get tile")
183
+ res = db.fetchone(current_app, query, params, timeout=10000)
146
184
 
147
- res = res[0]
148
- return send_file(io.BytesIO(res), mimetype="application/vnd.mapbox-vector-tile")
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 as e:
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=f"Invalid `datetime` argument", fallback_as_UTC=True)
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=f"Invalid start date in `datetime` argument", fallback_as_UTC=True)
91
- maxd = None if maxd == ".." else parse_datetime(maxd, error=f"Invalid end date in `datetime` argument", fallback_as_UTC=True)
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(f"Parameter {paramName or ''} must be coordinates in lat,lon format", status_code=400)
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], f"Longitude in parameter {paramName or ''} is not valid (should be between -180 and 180)"),
210
- as_latitude(entries[1], f"Latitude in parameter {paramName or ''} is not valid (should be between -90 and 90)"),
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
- f"Parameter {paramName or ''} is invalid (should be a distance range in meters like \"5-15\")", status_code=400
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(f"Parameter {paramName or ''} has a min value greater than its max value", status_code=400)
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
- f"Parameter {paramName or ''} is invalid (should be a distance range in meters like \"5-15\")", status_code=400
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(f"Parameter {paramName or ''} must be a valid list", status_code=400)
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
- f = to_sql_where(filterAst, STAC_FIELD_TO_SQL_FILTER).replace('"', "")
346
- return sql.SQL(f)
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(f"Unsupported filter parameter", status_code=400)
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(f"Unsupported sortby parameter: invalid column name", status_code=400)
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(f"Unsupported sortby parameter: syntax isn't correct", status_code=400)
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(f"limit parameter should be a valid, positive integer (between 1 and {SEQUENCES_MAX_FETCH})")
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(f"limit parameter should be an integer between 1 and {SEQUENCES_MAX_FETCH}")
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 os
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 (either jpg or webp)
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 (either jpg or webp)
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 (either jpg or webp)
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 (either jpg or webp)
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)