geovisio 2.5.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/web/items.py CHANGED
@@ -23,13 +23,13 @@ from geovisio.web.params import (
23
23
  from geovisio.utils.fields import Bounds
24
24
 
25
25
  import psycopg
26
- from datetime import datetime
27
26
  from psycopg.rows import dict_row
28
27
  from psycopg.sql import SQL
29
28
  from geovisio.web.utils import (
30
29
  accountIdOrDefault,
31
30
  cleanNoneInList,
32
31
  dbTsToStac,
32
+ dbTsToStacTZ,
33
33
  get_license_link,
34
34
  get_root_link,
35
35
  removeNoneInDict,
@@ -50,7 +50,7 @@ def dbPictureToStacItem(seqId, dbPic):
50
50
  ----------
51
51
  seqId : uuid
52
52
  Associated sequence ID
53
- dbSeq : dict
53
+ dbPic : dict
54
54
  A row from pictures table in database (with id, geojson, ts, heading, cols, rows, width, height, prevpic, nextpic, prevpicgeojson, nextpicgeojson, exif fields)
55
55
 
56
56
  Returns
@@ -80,33 +80,38 @@ def dbPictureToStacItem(seqId, dbPic):
80
80
  ),
81
81
  ]
82
82
  ),
83
- "properties": {
84
- "datetime": dbTsToStac(dbPic["ts"]),
85
- "created": dbTsToStac(dbPic["inserted_at"]),
86
- # TODO : add "updated" TS for last edit time of metadata
87
- "license": current_app.config["API_PICTURES_LICENSE_SPDX_ID"],
88
- "view:azimuth": dbPic["heading"],
89
- "pers:interior_orientation": (
90
- removeNoneInDict(
91
- {
92
- "camera_manufacturer": dbPic["metadata"].get("make"),
93
- "camera_model": dbPic["metadata"].get("model"),
94
- "focal_length": dbPic["metadata"].get("focal_length"),
95
- "field_of_view": dbPic["metadata"].get("field_of_view"),
96
- }
97
- )
98
- if "metadata" in dbPic
99
- and any(True for f in dbPic["metadata"] if f in ["make", "model", "focal_length", "field_of_view"])
100
- else {}
101
- ),
102
- "geovisio:status": dbPic.get("status"),
103
- "geovisio:producer": dbPic["account_name"],
104
- "original_file:size": dbPic["metadata"].get("originalFileSize"),
105
- "original_file:name": dbPic["metadata"].get("originalFileName"),
106
- "geovisio:image": _getHDJpgPictureURL(dbPic["id"], dbPic.get("status")),
107
- "geovisio:thumbnail": _getThumbJpgPictureURL(dbPic["id"], dbPic.get("status")),
108
- "exif": removeNoneInDict(cleanupExif(dbPic["exif"])),
109
- },
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
+ ),
110
115
  "links": cleanNoneInList(
111
116
  [
112
117
  get_root_link(),
@@ -709,7 +714,6 @@ def searchItems():
709
714
  sqlWhere = [SQL("(p.status = 'ready' OR p.account_id = %(account)s)"), SQL("(is_sequence_visible_by_user(s, %(account)s))")]
710
715
  sqlParams: Dict[str, Any] = {"account": accountId}
711
716
  sqlSubQueryWhere = [SQL("(p.status = 'ready' OR p.account_id = %(account)s)")]
712
-
713
717
  order_by = SQL("")
714
718
 
715
719
  #
@@ -859,25 +863,24 @@ def searchItems():
859
863
  #
860
864
  # Database query
861
865
  #
862
-
863
866
  with psycopg.connect(current_app.config["DB_URL"], row_factory=dict_row, options="-c statement_timeout=30000") as conn:
864
867
  with conn.cursor() as cursor:
865
868
  query = SQL(
866
869
  """
867
- SELECT * FROM (
868
- SELECT
869
- p.id, p.ts, p.heading, p.metadata, p.inserted_at,
870
- ST_AsGeoJSON(p.geom)::json AS geojson,
871
- sp.seq_id, sp.rank AS rank,
872
- accounts.name AS account_name, p.exif
873
- FROM pictures p
874
- LEFT JOIN sequences_pictures sp ON p.id = sp.pic_id
875
- LEFT JOIN sequences s ON s.id = sp.seq_id
876
- LEFT JOIN accounts ON p.account_id = accounts.id
877
- WHERE {sqlWhere}
878
- {orderBy}
879
- LIMIT %(limit)s
880
- ) pic
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
881
884
  LEFT JOIN LATERAL (
882
885
  SELECT
883
886
  p.id AS prevpic, ST_AsGeoJSON(p.geom)::json AS prevpicgeojson
@@ -899,6 +902,7 @@ LEFT JOIN LATERAL (
899
902
  ;
900
903
  """
901
904
  ).format(sqlWhere=SQL(" AND ").join(sqlWhere), sqlSubQueryWhere=SQL(" AND ").join(sqlSubQueryWhere), orderBy=order_by)
905
+
902
906
  records = cursor.execute(query, sqlParams)
903
907
 
904
908
  items = [dbPictureToStacItem(str(dbPic["seq_id"]), dbPic) for dbPic in records]
geovisio/web/map.py CHANGED
@@ -3,9 +3,9 @@
3
3
 
4
4
  import psycopg
5
5
  import io
6
- from typing import Optional, Dict, Any, Tuple, List
6
+ from typing import Optional, Dict, Any, Tuple, List, Union
7
7
  from uuid import UUID
8
- from flask import Blueprint, current_app, send_file, request
8
+ from flask import Blueprint, current_app, send_file, request, jsonify, url_for
9
9
  from geovisio.utils import auth
10
10
  from geovisio.utils.auth import Account
11
11
  from geovisio.web import params
@@ -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="111", y="222", z="333", format="mvt", _external=True)
25
+ sourceId = "geovisio"
26
+ if forUser == "me":
27
+ tilesUrl = url_for("map.getMyTile", x="111", y="222", z="333", format="mvt", _external=True)
28
+ sourceId = "geovisio_me"
29
+ elif forUser is not None:
30
+ tilesUrl = url_for("map.getUserTile", userId=forUser, x="111", y="222", z="333", format="mvt", _external=True)
31
+ sourceId = f"geovisio_{str(forUser)}"
32
+ tilesUrl = tilesUrl.replace("111", "{x}").replace("222", "{y}").replace("333", "{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-hcl", ["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
@@ -63,18 +148,38 @@ def _getTile(z: int, x: int, y: int, format: str, onlyForUser: Optional[UUID] =
63
148
  return send_file(io.BytesIO(res), mimetype="application/vnd.mapbox-vector-tile")
64
149
 
65
150
 
151
+ @bp.route("/map/style.json")
152
+ @user_dependant_response(False)
153
+ def getStyle():
154
+ """Get vector tiles style.
155
+
156
+ This style file follows MapLibre Style Spec : https://maplibre.org/maplibre-style-spec/
157
+
158
+ ---
159
+ tags:
160
+ - Map
161
+ responses:
162
+ 200:
163
+ description: Vector tiles style JSON
164
+ content:
165
+ application/json:
166
+ schema:
167
+ $ref: '#/components/schemas/MapLibreStyleJSON'
168
+ """
169
+ return get_style_json()
170
+
171
+
66
172
  @bp.route("/map/<int:z>/<int:x>/<int:y>.<format>")
67
173
  @user_dependant_response(False)
68
174
  def getTile(z: int, x: int, y: int, format: str):
69
175
  """Get pictures and sequences as vector tiles
70
176
 
71
- Vector tiles contains possibly two layers : sequences and pictures.
177
+ Vector tiles contains different layers based on zoom level : sequences, pictures or grid.
72
178
 
73
179
  Layer "sequences":
74
- - Available on all zoom levels
75
- - Available properties (all levels)
180
+ - Available on zoom levels >= 6
181
+ - Available properties:
76
182
  - id (sequence ID)
77
- - Other properties (available on zoom levels >= 13)
78
183
  - account_id
79
184
  - model (camera make and model)
80
185
  - type (flat or equirectangular)
@@ -90,6 +195,14 @@ def getTile(z: int, x: int, y: int, format: str):
90
195
  - sequences (list of sequences ID this pictures belongs to)
91
196
  - type (flat or equirectangular)
92
197
  - model (camera make and model)
198
+
199
+ Layer "grid":
200
+ - Available on zoom levels 0 to 5 (included)
201
+ - Available properties:
202
+ - id
203
+ - nb_pictures
204
+ - coef (value from 0 to 1, relative quantity of available pictures)
205
+
93
206
  ---
94
207
  tags:
95
208
  - Map
@@ -135,27 +248,24 @@ def getTile(z: int, x: int, y: int, format: str):
135
248
  def _get_query(z: int, x: int, y: int, onlyForUser: Optional[UUID], additional_filter: Optional[sql.SQL]) -> Tuple[sql.Composed, Dict]:
136
249
  """Returns appropriate SQL query according to given zoom"""
137
250
 
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
251
  params: Dict[str, Any] = {"x": x, "y": y, "z": z}
141
252
 
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'"))
147
-
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
253
+ #############################################################
254
+ # SQL Filters
255
+ #
152
256
 
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'"))
257
+ # Basic filters
258
+ grid_filter: List[sql.Composable] = [sql.SQL("g.geom && ST_Transform(ST_TileEnvelope(%(z)s, %(x)s, %(y)s), 4326)")]
259
+ sequences_filter: List[sql.Composable] = [
260
+ sql.SQL("s.status != 'deleted'"), # we never want to display deleted sequences on the map
261
+ sql.SQL("s.geom && ST_Transform(ST_TileEnvelope(%(z)s, %(x)s, %(y)s), 4326)"),
262
+ ]
263
+ pictures_filter: List[sql.Composable] = [
264
+ sql.SQL("p.status != 'waiting-for-delete'"), # we never want to display deleted pictures on the map
265
+ sql.SQL("p.geom && ST_Transform(ST_TileEnvelope(%(z)s, %(x)s, %(y)s), 4326)"),
266
+ ]
158
267
 
268
+ # Supplementary filters
159
269
  if additional_filter:
160
270
  sequences_filter.append(additional_filter)
161
271
  filter_str = additional_filter.as_string(None)
@@ -166,16 +276,46 @@ def _get_query(z: int, x: int, y: int, onlyForUser: Optional[UUID], additional_f
166
276
  pic_additional_filter = sql.SQL(pic_additional_filter_str) # type: ignore
167
277
  pictures_filter.append(sql.SQL("(") + sql.SQL(" OR ").join([pic_additional_filter, additional_filter]) + sql.SQL(")"))
168
278
 
279
+ # Per-user filters
280
+ if onlyForUser:
281
+ sequences_filter.append(sql.SQL("s.account_id = %(account)s"))
282
+ pictures_filter.append(sql.SQL("p.account_id = %(account)s"))
283
+ params["account"] = onlyForUser
284
+
285
+ # Not logged-in requests -> only show "ready" pics/sequences
286
+ account = auth.get_current_account()
287
+ accountId = account.id if account is not None else None
288
+ if not onlyForUser or accountId != str(onlyForUser):
289
+ sequences_filter.append(sql.SQL("s.status = 'ready'"))
290
+ pictures_filter.append(sql.SQL("p.status = 'ready'"))
291
+ pictures_filter.append(sql.SQL("s.status = 'ready'"))
292
+
293
+ #############################################################
294
+ # SQL Result columns/fields
295
+ #
296
+
297
+ grid_fields = [
298
+ sql.SQL("ST_AsMVTGeom(ST_Transform(geom, 3857), ST_TileEnvelope(%(z)s, %(x)s, %(y)s)) AS geom"),
299
+ sql.SQL("id"),
300
+ sql.SQL("nb_pictures"),
301
+ sql.SQL(
302
+ """
303
+ ((CASE WHEN nb_pictures <= (select PERCENTILE_CONT(0.5) WITHIN GROUP(ORDER BY nb_pictures) from pictures_grid)
304
+ THEN nb_pictures::float / (select PERCENTILE_CONT(0.5) WITHIN GROUP(ORDER BY nb_pictures) from pictures_grid) * 0.5
305
+ ELSE 0.5 + nb_pictures::float / (SELECT MAX(nb_pictures) FROM pictures_grid) * 0.5
306
+ END) * 10)::int / 10::float AS coef
307
+ """
308
+ ),
309
+ ]
169
310
  sequences_fields = [
170
311
  sql.SQL("ST_AsMVTGeom(ST_Transform(geom, 3857), ST_TileEnvelope(%(z)s, %(x)s, %(y)s)) AS geom"),
171
312
  sql.SQL("id"),
172
313
  ]
173
314
  simplified_sequence_fields = [
174
- sql.SQL("ST_Simplify(geom, 0.01) AS geom"),
175
- sql.SQL("id"),
176
- sql.SQL("status"),
315
+ sql.SQL("ST_AsMVTGeom(ST_Transform(ST_Simplify(geom, 0.01), 3857), ST_TileEnvelope(%(z)s, %(x)s, %(y)s)) AS geom"),
177
316
  ]
178
- if z >= 7 or onlyForUser:
317
+
318
+ if z >= ZOOM_GRID_SEQUENCES or onlyForUser:
179
319
  sequences_fields.extend(
180
320
  [
181
321
  sql.SQL("account_id"),
@@ -185,91 +325,146 @@ def _get_query(z: int, x: int, y: int, onlyForUser: Optional[UUID], additional_f
185
325
  sql.SQL("computed_capture_date AS date"),
186
326
  ]
187
327
  )
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:
328
+
329
+ #############################################################
330
+ # SQL Full requests
331
+ #
332
+
333
+ # Full pictures + sequences (z15+)
334
+ if z >= ZOOM_PICTURES:
197
335
  query = sql.SQL(
198
336
  """
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
- """
337
+ SELECT mvtsequences.mvt || mvtpictures.mvt
338
+ FROM (
339
+ SELECT ST_AsMVT(mvtgeomseqs.*, 'sequences') AS mvt
340
+ FROM (
341
+ SELECT
342
+ {sequences_fields}
343
+ FROM sequences s
344
+ WHERE
345
+ {sequences_filter}
346
+ ) mvtgeomseqs
347
+ ) mvtsequences,
348
+ (
349
+ SELECT ST_AsMVT(mvtgeompics.*, 'pictures') AS mvt
350
+ FROM (
351
+ SELECT
352
+ ST_AsMVTGeom(ST_Transform(p.geom, 3857), ST_TileEnvelope(%(z)s, %(x)s, %(y)s)) AS geom,
353
+ p.id, p.ts, p.heading, p.account_id,
354
+ NULLIF(p.status != 'ready' OR s.status != 'ready', FALSE) AS hidden,
355
+ array_to_json(ARRAY_AGG(sp.seq_id)) AS sequences,
356
+ p.metadata->>'type' AS type,
357
+ TRIM(CONCAT(p.metadata->>'make', ' ', p.metadata->>'model')) AS model
358
+ FROM pictures p
359
+ LEFT JOIN sequences_pictures sp ON p.id = sp.pic_id
360
+ LEFT JOIN sequences s ON s.id = sp.seq_id
361
+ WHERE
362
+ {pictures_filter}
363
+ GROUP BY 1, 2, 3, 4, 5, 6
364
+ ) mvtgeompics
365
+ ) mvtpictures
366
+ """
229
367
  ).format(
230
368
  sequences_filter=sql.SQL(" AND ").join(sequences_filter),
231
369
  pictures_filter=sql.SQL(" AND ").join(pictures_filter),
232
370
  sequences_fields=sql.SQL(", ").join(sequences_fields),
233
371
  )
234
372
 
235
- elif z >= 7:
373
+ # Full sequences (z7-14.9 and z0-14.9 for specific users)
374
+ elif z >= ZOOM_GRID_SEQUENCES + 1 or onlyForUser:
236
375
  query = sql.SQL(
237
376
  """
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
- """
377
+ SELECT ST_AsMVT(mvtsequences.*, 'sequences') AS mvt
378
+ FROM (
379
+ SELECT
380
+ {sequences_fields}
381
+ FROM sequences s
382
+ WHERE
383
+ {sequences_filter}
384
+ ) mvtsequences
385
+ """
247
386
  ).format(sequences_filter=sql.SQL(" AND ").join(sequences_filter), sequences_fields=sql.SQL(", ").join(sequences_fields))
248
- else:
387
+
388
+ # Sequences + grid (z6-6.9)
389
+ elif z >= ZOOM_GRID_SEQUENCES:
249
390
  query = sql.SQL(
250
391
  """
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
- """
392
+ SELECT mvtsequences.mvt || mvtgrid.mvt
393
+ FROM (
394
+ SELECT ST_AsMVT(mvtgeomseqs.*, 'sequences') AS mvt
395
+ FROM (
396
+ SELECT
397
+ {simplified_sequence_fields}
398
+ FROM sequences s
399
+ WHERE
400
+ {sequences_filter}
401
+ ) mvtgeomseqs
402
+ ) mvtsequences,
403
+ (
404
+ SELECT ST_AsMVT(mvtgeomgrid.*, 'grid') AS mvt
405
+ FROM (
406
+ SELECT
407
+ {grid_fields}
408
+ FROM pictures_grid g
409
+ WHERE {grid_filter}
410
+ ) mvtgeomgrid
411
+ ) mvtgrid
412
+ """
264
413
  ).format(
265
414
  sequences_filter=sql.SQL(" AND ").join(sequences_filter),
266
- sequences_fields=sql.SQL(", ").join(sequences_fields),
267
415
  simplified_sequence_fields=sql.SQL(", ").join(simplified_sequence_fields),
416
+ grid_filter=sql.SQL(" AND ").join(grid_filter),
417
+ grid_fields=sql.SQL(", ").join(grid_fields),
418
+ )
419
+
420
+ # Grid overview (all users + z0-5.9)
421
+ else:
422
+ query = sql.SQL(
423
+ """
424
+ SELECT ST_AsMVT(mvtgrid.*, 'grid') AS mvt
425
+ FROM (
426
+ SELECT
427
+ {grid_fields}
428
+ FROM pictures_grid g
429
+ WHERE {grid_filter}
430
+ ) mvtgrid
431
+ """
432
+ ).format(
433
+ grid_filter=sql.SQL(" AND ").join(grid_filter),
434
+ grid_fields=sql.SQL(", ").join(grid_fields),
268
435
  )
269
436
 
270
437
  return query, params
271
438
 
272
439
 
440
+ @bp.route("/users/<uuid:userId>/map/style.json")
441
+ @user_dependant_response(False)
442
+ def getUserStyle(userId: UUID):
443
+ """Get vector tiles style for a single user.
444
+
445
+ This style file follows MapLibre Style Spec : https://maplibre.org/maplibre-style-spec/
446
+
447
+ ---
448
+ tags:
449
+ - Map
450
+ parameters:
451
+ - name: userId
452
+ in: path
453
+ description: User ID
454
+ required: true
455
+ schema:
456
+ type: string
457
+ responses:
458
+ 200:
459
+ description: Vector tiles style JSON
460
+ content:
461
+ application/json:
462
+ schema:
463
+ $ref: '#/components/schemas/MapLibreStyleJSON'
464
+ """
465
+ return get_style_json(forUser=userId)
466
+
467
+
273
468
  @bp.route("/users/<uuid:userId>/map/<int:z>/<int:x>/<int:y>.<format>")
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.
@@ -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):
geovisio/web/stac.py CHANGED
@@ -119,6 +119,7 @@ def getLanding():
119
119
  .replace("333", "{z}")
120
120
  .replace("bob", "{userId}")
121
121
  )
122
+ userStyleUrl = url_for("map.getUserStyle", userId="bob", _external=True).replace("bob", "{userId}")
122
123
 
123
124
  if "stac_extensions" not in catalog:
124
125
  catalog["stac_extensions"] = []
@@ -143,12 +144,24 @@ def getLanding():
143
144
  "href": mapUrl,
144
145
  "title": "Pictures and sequences vector tiles",
145
146
  },
147
+ {
148
+ "rel": "xyz-style",
149
+ "type": "application/json",
150
+ "href": url_for("map.getStyle", _external=True),
151
+ "title": "MapLibre Style JSON",
152
+ },
146
153
  {
147
154
  "rel": "user-xyz",
148
155
  "type": "application/vnd.mapbox-vector-tile",
149
156
  "href": userMapUrl,
150
157
  "title": "Pictures and sequences vector tiles for a given user",
151
158
  },
159
+ {
160
+ "rel": "user-xyz-style",
161
+ "type": "application/json",
162
+ "href": userStyleUrl,
163
+ "title": "MapLibre Style JSON",
164
+ },
152
165
  {
153
166
  "rel": "collection-preview",
154
167
  "type": "image/jpeg",