geovisio 2.5.0__py3-none-any.whl → 2.7.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.
Files changed (59) hide show
  1. geovisio/__init__.py +38 -8
  2. geovisio/admin_cli/__init__.py +2 -2
  3. geovisio/admin_cli/db.py +8 -0
  4. geovisio/config_app.py +64 -0
  5. geovisio/db_migrations.py +24 -3
  6. geovisio/templates/main.html +14 -14
  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 +667 -0
  10. geovisio/translations/en/LC_MESSAGES/messages.mo +0 -0
  11. geovisio/translations/en/LC_MESSAGES/messages.po +730 -0
  12. geovisio/translations/es/LC_MESSAGES/messages.mo +0 -0
  13. geovisio/translations/es/LC_MESSAGES/messages.po +778 -0
  14. geovisio/translations/fi/LC_MESSAGES/messages.mo +0 -0
  15. geovisio/translations/fi/LC_MESSAGES/messages.po +589 -0
  16. geovisio/translations/fr/LC_MESSAGES/messages.mo +0 -0
  17. geovisio/translations/fr/LC_MESSAGES/messages.po +814 -0
  18. geovisio/translations/ko/LC_MESSAGES/messages.mo +0 -0
  19. geovisio/translations/ko/LC_MESSAGES/messages.po +685 -0
  20. geovisio/translations/messages.pot +686 -0
  21. geovisio/translations/nl/LC_MESSAGES/messages.mo +0 -0
  22. geovisio/translations/nl/LC_MESSAGES/messages.po +594 -0
  23. geovisio/utils/__init__.py +1 -1
  24. geovisio/utils/auth.py +50 -11
  25. geovisio/utils/db.py +65 -0
  26. geovisio/utils/excluded_areas.py +83 -0
  27. geovisio/utils/extent.py +30 -0
  28. geovisio/utils/fields.py +1 -1
  29. geovisio/utils/filesystems.py +0 -1
  30. geovisio/utils/link.py +14 -0
  31. geovisio/utils/params.py +20 -0
  32. geovisio/utils/pictures.py +94 -69
  33. geovisio/utils/reports.py +171 -0
  34. geovisio/utils/sequences.py +288 -126
  35. geovisio/utils/tokens.py +37 -42
  36. geovisio/utils/upload_set.py +654 -0
  37. geovisio/web/auth.py +50 -37
  38. geovisio/web/collections.py +305 -319
  39. geovisio/web/configuration.py +14 -0
  40. geovisio/web/docs.py +288 -12
  41. geovisio/web/excluded_areas.py +377 -0
  42. geovisio/web/items.py +203 -151
  43. geovisio/web/map.py +322 -106
  44. geovisio/web/params.py +69 -26
  45. geovisio/web/pictures.py +14 -31
  46. geovisio/web/reports.py +399 -0
  47. geovisio/web/rss.py +13 -7
  48. geovisio/web/stac.py +129 -121
  49. geovisio/web/tokens.py +105 -112
  50. geovisio/web/upload_set.py +768 -0
  51. geovisio/web/users.py +100 -73
  52. geovisio/web/utils.py +38 -9
  53. geovisio/workers/runner_pictures.py +278 -183
  54. geovisio-2.7.0.dist-info/METADATA +95 -0
  55. geovisio-2.7.0.dist-info/RECORD +66 -0
  56. geovisio-2.5.0.dist-info/METADATA +0 -115
  57. geovisio-2.5.0.dist-info/RECORD +0 -41
  58. {geovisio-2.5.0.dist-info → geovisio-2.7.0.dist-info}/LICENSE +0 -0
  59. {geovisio-2.5.0.dist-info → geovisio-2.7.0.dist-info}/WHEEL +0 -0
geovisio/web/map.py CHANGED
@@ -1,12 +1,12 @@
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
- from typing import Optional, Dict, Any, Tuple, List
5
+ from typing import Optional, Dict, Any, Tuple, List, Union
7
6
  from uuid import UUID
8
- from flask import Blueprint, current_app, send_file, request
9
- from geovisio.utils import auth
7
+ from flask import Blueprint, current_app, send_file, request, jsonify, url_for
8
+ from flask_babel import gettext as _
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
@@ -15,6 +15,91 @@ from psycopg import sql
15
15
 
16
16
  bp = Blueprint("map", __name__, url_prefix="/api")
17
17
 
18
+ ZOOM_GRID_SEQUENCES = 6
19
+ ZOOM_PICTURES = 15
20
+
21
+
22
+ def get_style_json(forUser: Optional[Union[UUID, str]] = None):
23
+ # Get correct vector tiles URL
24
+ tilesUrl = url_for("map.getTile", x="11111111", y="22222222", z="33333333", format="mvt", _external=True)
25
+ sourceId = "geovisio"
26
+ if forUser == "me":
27
+ tilesUrl = url_for("map.getMyTile", x="11111111", y="22222222", z="33333333", format="mvt", _external=True)
28
+ sourceId = "geovisio_me"
29
+ elif forUser is not None:
30
+ tilesUrl = url_for("map.getUserTile", userId=forUser, x="11111111", y="22222222", z="33333333", format="mvt", _external=True)
31
+ sourceId = f"geovisio_{str(forUser)}"
32
+ tilesUrl = tilesUrl.replace("11111111", "{x}").replace("22222222", "{y}").replace("33333333", "{z}")
33
+
34
+ # 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
+
37
+ layers = [
38
+ {
39
+ "id": f"{sourceId}_sequences",
40
+ "type": "line",
41
+ "source": sourceId,
42
+ "source-layer": "sequences",
43
+ "paint": {
44
+ "line-color": "#FF6F00",
45
+ "line-width": ["interpolate", ["linear"], ["zoom"], 0, 0.5, 10, 2, 14, 4, 16, 5, 22, 3],
46
+ "line-opacity": sequenceOpacity,
47
+ },
48
+ "layout": {
49
+ "line-cap": "square",
50
+ },
51
+ },
52
+ {
53
+ "id": f"{sourceId}_pictures",
54
+ "type": "circle",
55
+ "source": sourceId,
56
+ "source-layer": "pictures",
57
+ "paint": {
58
+ "circle-color": "#FF6F00",
59
+ "circle-radius": ["interpolate", ["linear"], ["zoom"], ZOOM_PICTURES, 4.5, 17, 8, 22, 12],
60
+ "circle-opacity": ["interpolate", ["linear"], ["zoom"], ZOOM_PICTURES, 0, ZOOM_PICTURES + 1, 1],
61
+ "circle-stroke-color": "#ffffff",
62
+ "circle-stroke-width": ["interpolate", ["linear"], ["zoom"], 17, 0, 20, 2],
63
+ },
64
+ },
65
+ ]
66
+
67
+ # Grid layer of general tiles
68
+ if forUser is None:
69
+ layers.append(
70
+ {
71
+ "id": f"{sourceId}_grid",
72
+ "type": "fill",
73
+ "source": sourceId,
74
+ "source-layer": "grid",
75
+ "paint": {
76
+ "fill-color": ["interpolate", ["linear"], ["get", "coef"], 0, "#FFCC80", 0.5, "#E65100", 1, "#BF360C"],
77
+ "fill-opacity": [
78
+ "interpolate",
79
+ ["linear"],
80
+ ["zoom"],
81
+ 0,
82
+ 1,
83
+ ZOOM_GRID_SEQUENCES - 2,
84
+ 1,
85
+ ZOOM_GRID_SEQUENCES,
86
+ 0.8,
87
+ ZOOM_GRID_SEQUENCES + 0.5,
88
+ 0,
89
+ ],
90
+ },
91
+ }
92
+ )
93
+
94
+ style = {
95
+ "version": 8,
96
+ "name": "GeoVisio Vector Tiles",
97
+ "sources": {sourceId: {"type": "vector", "tiles": [tilesUrl], "minzoom": 0, "maxzoom": ZOOM_PICTURES}},
98
+ "layers": layers,
99
+ }
100
+
101
+ return jsonify(style)
102
+
18
103
 
19
104
  def checkTileValidity(z, x, y, format):
20
105
  """Check if tile parameters are valid
@@ -35,32 +120,52 @@ def checkTileValidity(z, x, y, format):
35
120
  raises InvalidAPIUsage exceptions if parameters are not OK
36
121
  """
37
122
  if z is None or x is None or y is None or format is None:
38
- raise errors.InvalidAPIUsage("One of required parameter is empty", status_code=404)
123
+ raise errors.InvalidAPIUsage(_("One of required parameter is empty"), status_code=404)
39
124
  if format not in ["pbf", "mvt"]:
40
- raise errors.InvalidAPIUsage("Tile format is invalid, should be either pbf or mvt", status_code=400)
125
+ raise errors.InvalidAPIUsage(_("Tile format is invalid, should be either pbf or mvt"), status_code=400)
41
126
 
42
127
  size = 2**z
43
128
  if x >= size or y >= size:
44
- raise errors.InvalidAPIUsage("X or Y parameter is out of bounds", status_code=404)
129
+ raise errors.InvalidAPIUsage(_("X or Y parameter is out of bounds"), status_code=404)
45
130
  if x < 0 or y < 0:
46
- raise errors.InvalidAPIUsage("X or Y parameter is out of bounds", status_code=404)
131
+ raise errors.InvalidAPIUsage(_("X or Y parameter is out of bounds"), status_code=404)
47
132
  if z < 0 or z > 15:
48
- raise errors.InvalidAPIUsage("Z parameter is out of bounds (should be 0-15)", status_code=404)
133
+ raise errors.InvalidAPIUsage(_("Z parameter is out of bounds (should be 0-15)"), status_code=404)
49
134
 
50
135
 
51
136
  def _getTile(z: int, x: int, y: int, format: str, onlyForUser: Optional[UUID] = None, filter: Optional[sql.SQL] = None):
52
137
  checkTileValidity(z, x, y, format)
53
138
 
54
- with psycopg.connect(current_app.config["DB_URL"], options="-c statement_timeout=10000") as conn:
55
- with conn.cursor() as cursor:
56
- query, params = _get_query(z, x, y, onlyForUser, additional_filter=filter)
57
- res = cursor.execute(query, params).fetchone()
139
+ query, params = _get_query(z, x, y, onlyForUser, additional_filter=filter)
140
+
141
+ res = db.fetchone(current_app, query, params, timeout=10000)
58
142
 
59
- if not res:
60
- raise errors.InternalError("Impossible to get tile")
143
+ if not res:
144
+ raise errors.InternalError(_("Impossible to get tile"))
61
145
 
62
- res = res[0]
63
- return send_file(io.BytesIO(res), mimetype="application/vnd.mapbox-vector-tile")
146
+ res = res[0]
147
+ return send_file(io.BytesIO(res), mimetype="application/vnd.mapbox-vector-tile")
148
+
149
+
150
+ @bp.route("/map/style.json")
151
+ @user_dependant_response(False)
152
+ def getStyle():
153
+ """Get vector tiles style.
154
+
155
+ This style file follows MapLibre Style Spec : https://maplibre.org/maplibre-style-spec/
156
+
157
+ ---
158
+ tags:
159
+ - Map
160
+ responses:
161
+ 200:
162
+ description: Vector tiles style JSON
163
+ content:
164
+ application/json:
165
+ schema:
166
+ $ref: '#/components/schemas/MapLibreStyleJSON'
167
+ """
168
+ return get_style_json()
64
169
 
65
170
 
66
171
  @bp.route("/map/<int:z>/<int:x>/<int:y>.<format>")
@@ -68,13 +173,12 @@ def _getTile(z: int, x: int, y: int, format: str, onlyForUser: Optional[UUID] =
68
173
  def getTile(z: int, x: int, y: int, format: str):
69
174
  """Get pictures and sequences as vector tiles
70
175
 
71
- Vector tiles contains possibly two layers : sequences and pictures.
176
+ Vector tiles contains different layers based on zoom level : sequences, pictures or grid.
72
177
 
73
178
  Layer "sequences":
74
- - Available on all zoom levels
75
- - Available properties (all levels)
179
+ - Available on zoom levels >= 6
180
+ - Available properties:
76
181
  - id (sequence ID)
77
- - Other properties (available on zoom levels >= 13)
78
182
  - account_id
79
183
  - model (camera make and model)
80
184
  - type (flat or equirectangular)
@@ -90,6 +194,14 @@ def getTile(z: int, x: int, y: int, format: str):
90
194
  - sequences (list of sequences ID this pictures belongs to)
91
195
  - type (flat or equirectangular)
92
196
  - model (camera make and model)
197
+
198
+ Layer "grid":
199
+ - Available on zoom levels 0 to 5 (included)
200
+ - Available properties:
201
+ - id
202
+ - nb_pictures
203
+ - coef (value from 0 to 1, relative quantity of available pictures)
204
+
93
205
  ---
94
206
  tags:
95
207
  - Map
@@ -135,27 +247,24 @@ def getTile(z: int, x: int, y: int, format: str):
135
247
  def _get_query(z: int, x: int, y: int, onlyForUser: Optional[UUID], additional_filter: Optional[sql.SQL]) -> Tuple[sql.Composed, Dict]:
136
248
  """Returns appropriate SQL query according to given zoom"""
137
249
 
138
- sequences_filter: List[sql.Composable] = [sql.SQL("s.geom && ST_Transform(ST_TileEnvelope(%(z)s, %(x)s, %(y)s), 4326)")]
139
- pictures_filter: List[sql.Composable] = [sql.SQL("p.geom && ST_Transform(ST_TileEnvelope(%(z)s, %(x)s, %(y)s), 4326)")]
140
250
  params: Dict[str, Any] = {"x": x, "y": y, "z": z}
141
251
 
142
- account = auth.get_current_account()
143
- accountId = account.id if account is not None else None
144
- # we never want to display deleted sequences on the map
145
- sequences_filter.append(sql.SQL("s.status != 'deleted'"))
146
- pictures_filter.append(sql.SQL("p.status != 'waiting-for-delete'"))
252
+ #############################################################
253
+ # SQL Filters
254
+ #
147
255
 
148
- if onlyForUser:
149
- sequences_filter.append(sql.SQL("s.account_id = %(account)s"))
150
- pictures_filter.append(sql.SQL("p.account_id = %(account)s"))
151
- params["account"] = onlyForUser
152
-
153
- # we want to show only 'ready' collection to the general users
154
- if not onlyForUser or accountId != str(onlyForUser):
155
- sequences_filter.append(sql.SQL("s.status = 'ready'"))
156
- pictures_filter.append(sql.SQL("p.status = 'ready'"))
157
- pictures_filter.append(sql.SQL("s.status = 'ready'"))
256
+ # Basic filters
257
+ grid_filter: List[sql.Composable] = [sql.SQL("g.geom && ST_Transform(ST_TileEnvelope(%(z)s, %(x)s, %(y)s), 4326)")]
258
+ sequences_filter: List[sql.Composable] = [
259
+ sql.SQL("s.status != 'deleted'"), # we never want to display deleted sequences on the map
260
+ sql.SQL("s.geom && ST_Transform(ST_TileEnvelope(%(z)s, %(x)s, %(y)s), 4326)"),
261
+ ]
262
+ pictures_filter: List[sql.Composable] = [
263
+ sql.SQL("p.status != 'waiting-for-delete'"), # we never want to display deleted pictures on the map
264
+ sql.SQL("p.geom && ST_Transform(ST_TileEnvelope(%(z)s, %(x)s, %(y)s), 4326)"),
265
+ ]
158
266
 
267
+ # Supplementary filters
159
268
  if additional_filter:
160
269
  sequences_filter.append(additional_filter)
161
270
  filter_str = additional_filter.as_string(None)
@@ -166,16 +275,46 @@ def _get_query(z: int, x: int, y: int, onlyForUser: Optional[UUID], additional_f
166
275
  pic_additional_filter = sql.SQL(pic_additional_filter_str) # type: ignore
167
276
  pictures_filter.append(sql.SQL("(") + sql.SQL(" OR ").join([pic_additional_filter, additional_filter]) + sql.SQL(")"))
168
277
 
278
+ # Per-user filters
279
+ if onlyForUser:
280
+ sequences_filter.append(sql.SQL("s.account_id = %(account)s"))
281
+ pictures_filter.append(sql.SQL("p.account_id = %(account)s"))
282
+ params["account"] = onlyForUser
283
+
284
+ # Not logged-in requests -> only show "ready" pics/sequences
285
+ account = auth.get_current_account()
286
+ accountId = account.id if account is not None else None
287
+ if not onlyForUser or accountId != str(onlyForUser):
288
+ sequences_filter.append(sql.SQL("s.status = 'ready'"))
289
+ pictures_filter.append(sql.SQL("p.status = 'ready'"))
290
+ pictures_filter.append(sql.SQL("s.status = 'ready'"))
291
+
292
+ #############################################################
293
+ # SQL Result columns/fields
294
+ #
295
+
296
+ grid_fields = [
297
+ sql.SQL("ST_AsMVTGeom(ST_Transform(geom, 3857), ST_TileEnvelope(%(z)s, %(x)s, %(y)s)) AS geom"),
298
+ sql.SQL("id"),
299
+ sql.SQL("nb_pictures"),
300
+ sql.SQL(
301
+ """
302
+ ((CASE WHEN nb_pictures <= (select PERCENTILE_CONT(0.5) WITHIN GROUP(ORDER BY nb_pictures) from pictures_grid)
303
+ THEN nb_pictures::float / (select PERCENTILE_CONT(0.5) WITHIN GROUP(ORDER BY nb_pictures) from pictures_grid) * 0.5
304
+ ELSE 0.5 + nb_pictures::float / (SELECT MAX(nb_pictures) FROM pictures_grid) * 0.5
305
+ END) * 10)::int / 10::float AS coef
306
+ """
307
+ ),
308
+ ]
169
309
  sequences_fields = [
170
310
  sql.SQL("ST_AsMVTGeom(ST_Transform(geom, 3857), ST_TileEnvelope(%(z)s, %(x)s, %(y)s)) AS geom"),
171
311
  sql.SQL("id"),
172
312
  ]
173
313
  simplified_sequence_fields = [
174
- sql.SQL("ST_Simplify(geom, 0.01) AS geom"),
175
- sql.SQL("id"),
176
- sql.SQL("status"),
314
+ sql.SQL("ST_AsMVTGeom(ST_Transform(ST_Simplify(geom, 0.01), 3857), ST_TileEnvelope(%(z)s, %(x)s, %(y)s)) AS geom"),
177
315
  ]
178
- if z >= 7 or onlyForUser:
316
+
317
+ if z >= ZOOM_GRID_SEQUENCES or onlyForUser:
179
318
  sequences_fields.extend(
180
319
  [
181
320
  sql.SQL("account_id"),
@@ -185,92 +324,148 @@ def _get_query(z: int, x: int, y: int, onlyForUser: Optional[UUID], additional_f
185
324
  sql.SQL("computed_capture_date AS date"),
186
325
  ]
187
326
  )
188
- simplified_sequence_fields.extend(
189
- [
190
- sql.SQL("account_id"),
191
- sql.SQL("computed_model"),
192
- sql.SQL("computed_type"),
193
- sql.SQL("computed_capture_date"),
194
- ]
195
- )
196
- if z >= 15:
327
+
328
+ #############################################################
329
+ # SQL Full requests
330
+ #
331
+
332
+ # Full pictures + sequences (z15+)
333
+ if z >= ZOOM_PICTURES:
197
334
  query = sql.SQL(
198
335
  """
199
- SELECT mvtsequences.mvt || mvtpictures.mvt
200
- FROM (
201
- SELECT ST_AsMVT(mvtgeomseqs.*, 'sequences') AS mvt
202
- FROM (
203
- SELECT
204
- {sequences_fields}
205
- FROM sequences s
206
- WHERE
207
- {sequences_filter}
208
- ) mvtgeomseqs
209
- ) mvtsequences,
210
- (
211
- SELECT ST_AsMVT(mvtgeompics.*, 'pictures') AS mvt
212
- FROM (
213
- SELECT
214
- ST_AsMVTGeom(ST_Transform(p.geom, 3857), ST_TileEnvelope(%(z)s, %(x)s, %(y)s)) AS geom,
215
- p.id, p.ts, p.heading, p.account_id,
216
- NULLIF(p.status != 'ready' OR s.status != 'ready', FALSE) AS hidden,
217
- array_to_json(ARRAY_AGG(sp.seq_id)) AS sequences,
218
- p.metadata->>'type' AS type,
219
- TRIM(CONCAT(p.metadata->>'make', ' ', p.metadata->>'model')) AS model
220
- FROM pictures p
221
- LEFT JOIN sequences_pictures sp ON p.id = sp.pic_id
222
- LEFT JOIN sequences s ON s.id = sp.seq_id
223
- WHERE
224
- {pictures_filter}
225
- GROUP BY 1, 2, 3, 4, 5, 6
226
- ) mvtgeompics
227
- ) mvtpictures
228
- """
336
+ SELECT mvtsequences.mvt || mvtpictures.mvt
337
+ FROM (
338
+ SELECT ST_AsMVT(mvtgeomseqs.*, 'sequences') AS mvt
339
+ FROM (
340
+ SELECT
341
+ {sequences_fields}
342
+ FROM sequences s
343
+ WHERE
344
+ {sequences_filter}
345
+ ) mvtgeomseqs
346
+ ) mvtsequences,
347
+ (
348
+ SELECT ST_AsMVT(mvtgeompics.*, 'pictures') AS mvt
349
+ FROM (
350
+ SELECT
351
+ ST_AsMVTGeom(ST_Transform(p.geom, 3857), ST_TileEnvelope(%(z)s, %(x)s, %(y)s)) AS geom,
352
+ p.id, p.ts, p.heading, p.account_id,
353
+ NULLIF(p.status != 'ready' OR s.status != 'ready', FALSE) AS hidden,
354
+ array_to_json(ARRAY_AGG(sp.seq_id)) AS sequences,
355
+ p.metadata->>'type' AS type,
356
+ TRIM(CONCAT(p.metadata->>'make', ' ', p.metadata->>'model')) AS model
357
+ FROM pictures p
358
+ LEFT JOIN sequences_pictures sp ON p.id = sp.pic_id
359
+ LEFT JOIN sequences s ON s.id = sp.seq_id
360
+ WHERE
361
+ {pictures_filter}
362
+ GROUP BY 1, 2, 3, 4, 5, 6
363
+ ) mvtgeompics
364
+ ) mvtpictures
365
+ """
229
366
  ).format(
230
367
  sequences_filter=sql.SQL(" AND ").join(sequences_filter),
231
368
  pictures_filter=sql.SQL(" AND ").join(pictures_filter),
232
369
  sequences_fields=sql.SQL(", ").join(sequences_fields),
233
370
  )
234
371
 
235
- elif z >= 7:
372
+ # Full sequences (z7-14.9 and z0-14.9 for specific users)
373
+ elif z >= ZOOM_GRID_SEQUENCES + 1 or onlyForUser:
236
374
  query = sql.SQL(
237
375
  """
238
- SELECT ST_AsMVT(mvtsequences.*, 'sequences') AS mvt
239
- FROM (
240
- SELECT
241
- {sequences_fields}
242
- FROM sequences s
243
- WHERE
244
- {sequences_filter}
245
- ) mvtsequences
246
- """
376
+ SELECT ST_AsMVT(mvtsequences.*, 'sequences') AS mvt
377
+ FROM (
378
+ SELECT
379
+ {sequences_fields}
380
+ FROM sequences s
381
+ WHERE
382
+ {sequences_filter}
383
+ ) mvtsequences
384
+ """
247
385
  ).format(sequences_filter=sql.SQL(" AND ").join(sequences_filter), sequences_fields=sql.SQL(", ").join(sequences_fields))
248
- else:
386
+
387
+ # Sequences + grid (z6-6.9)
388
+ elif z >= ZOOM_GRID_SEQUENCES:
249
389
  query = sql.SQL(
250
390
  """
251
- SELECT ST_AsMVT(mvtsequences.*, 'sequences') AS mvt
252
- FROM (
253
- SELECT
254
- {sequences_fields}
255
- FROM (
256
- SELECT {simplified_sequence_fields}
257
- FROM sequences s
258
- WHERE
259
- {sequences_filter}
260
- ) s
261
- WHERE geom IS NOT NULL
262
- ) mvtsequences
263
- """
391
+ SELECT mvtsequences.mvt || mvtgrid.mvt
392
+ FROM (
393
+ SELECT ST_AsMVT(mvtgeomseqs.*, 'sequences') AS mvt
394
+ FROM (
395
+ SELECT
396
+ {simplified_sequence_fields}
397
+ FROM sequences s
398
+ WHERE
399
+ {sequences_filter}
400
+ ) mvtgeomseqs
401
+ ) mvtsequences,
402
+ (
403
+ SELECT ST_AsMVT(mvtgeomgrid.*, 'grid') AS mvt
404
+ FROM (
405
+ SELECT
406
+ {grid_fields}
407
+ FROM pictures_grid g
408
+ WHERE {grid_filter}
409
+ ) mvtgeomgrid
410
+ ) mvtgrid
411
+ """
264
412
  ).format(
265
413
  sequences_filter=sql.SQL(" AND ").join(sequences_filter),
266
- sequences_fields=sql.SQL(", ").join(sequences_fields),
267
414
  simplified_sequence_fields=sql.SQL(", ").join(simplified_sequence_fields),
415
+ grid_filter=sql.SQL(" AND ").join(grid_filter),
416
+ grid_fields=sql.SQL(", ").join(grid_fields),
417
+ )
418
+
419
+ # Grid overview (all users + z0-5.9)
420
+ else:
421
+ query = sql.SQL(
422
+ """
423
+ SELECT ST_AsMVT(mvtgrid.*, 'grid') AS mvt
424
+ FROM (
425
+ SELECT
426
+ {grid_fields}
427
+ FROM pictures_grid g
428
+ WHERE {grid_filter}
429
+ ) mvtgrid
430
+ """
431
+ ).format(
432
+ grid_filter=sql.SQL(" AND ").join(grid_filter),
433
+ grid_fields=sql.SQL(", ").join(grid_fields),
268
434
  )
269
435
 
270
436
  return query, params
271
437
 
272
438
 
439
+ @bp.route("/users/<uuid:userId>/map/style.json")
440
+ @user_dependant_response(False)
441
+ def getUserStyle(userId: UUID):
442
+ """Get vector tiles style for a single user.
443
+
444
+ This style file follows MapLibre Style Spec : https://maplibre.org/maplibre-style-spec/
445
+
446
+ ---
447
+ tags:
448
+ - Map
449
+ parameters:
450
+ - name: userId
451
+ in: path
452
+ description: User ID
453
+ required: true
454
+ schema:
455
+ type: string
456
+ responses:
457
+ 200:
458
+ description: Vector tiles style JSON
459
+ content:
460
+ application/json:
461
+ schema:
462
+ $ref: '#/components/schemas/MapLibreStyleJSON'
463
+ """
464
+ return get_style_json(forUser=userId)
465
+
466
+
273
467
  @bp.route("/users/<uuid:userId>/map/<int:z>/<int:x>/<int:y>.<format>")
468
+ @user_dependant_response(True)
274
469
  def getUserTile(userId: UUID, z: int, x: int, y: int, format: str):
275
470
  """Get pictures and sequences as vector tiles for a specific user.
276
471
  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
@@ -327,6 +522,27 @@ def getUserTile(userId: UUID, z: int, x: int, y: int, format: str):
327
522
  return _getTile(z, x, y, format, onlyForUser=userId, filter=filter)
328
523
 
329
524
 
525
+ @bp.route("/users/me/map/style.json")
526
+ @auth.login_required_with_redirect()
527
+ def getMyStyle(account: Account):
528
+ """Get vector tiles style.
529
+
530
+ This style file follows MapLibre Style Spec : https://maplibre.org/maplibre-style-spec/
531
+
532
+ ---
533
+ tags:
534
+ - Map
535
+ responses:
536
+ 200:
537
+ description: Vector tiles style JSON
538
+ content:
539
+ application/json:
540
+ schema:
541
+ $ref: '#/components/schemas/MapLibreStyleJSON'
542
+ """
543
+ return get_style_json(forUser="me")
544
+
545
+
330
546
  @bp.route("/users/me/map/<int:z>/<int:x>/<int:y>.<format>")
331
547
  @auth.login_required_with_redirect()
332
548
  def getMyTile(account: Account, z: int, x: int, y: int, format: str):