geovisio 2.4.0__py3-none-any.whl → 2.6.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- geovisio/__init__.py +3 -2
- geovisio/admin_cli/__init__.py +2 -2
- geovisio/admin_cli/db.py +11 -0
- geovisio/admin_cli/sequence_heading.py +2 -2
- geovisio/config_app.py +25 -0
- geovisio/templates/main.html +2 -2
- geovisio/utils/pictures.py +75 -30
- geovisio/utils/sequences.py +232 -34
- geovisio/web/auth.py +15 -2
- geovisio/web/collections.py +161 -111
- geovisio/web/docs.py +178 -4
- geovisio/web/items.py +169 -114
- geovisio/web/map.py +309 -93
- geovisio/web/params.py +82 -4
- geovisio/web/stac.py +14 -4
- geovisio/web/tokens.py +7 -3
- geovisio/web/users.py +4 -4
- geovisio/web/utils.py +10 -0
- geovisio/workers/runner_pictures.py +73 -70
- geovisio-2.6.0.dist-info/METADATA +92 -0
- geovisio-2.6.0.dist-info/RECORD +41 -0
- geovisio-2.4.0.dist-info/METADATA +0 -115
- geovisio-2.4.0.dist-info/RECORD +0 -41
- {geovisio-2.4.0.dist-info → geovisio-2.6.0.dist-info}/LICENSE +0 -0
- {geovisio-2.4.0.dist-info → geovisio-2.6.0.dist-info}/WHEEL +0 -0
geovisio/web/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
|
|
@@ -44,8 +129,8 @@ def checkTileValidity(z, x, y, format):
|
|
|
44
129
|
raise errors.InvalidAPIUsage("X or Y parameter is out of bounds", status_code=404)
|
|
45
130
|
if x < 0 or y < 0:
|
|
46
131
|
raise errors.InvalidAPIUsage("X or Y parameter is out of bounds", status_code=404)
|
|
47
|
-
if z < 0 or z >
|
|
48
|
-
raise errors.InvalidAPIUsage("Z parameter is out of bounds (should be 0-
|
|
132
|
+
if z < 0 or z > 15:
|
|
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):
|
|
@@ -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
|
|
177
|
+
Vector tiles contains different layers based on zoom level : sequences, pictures or grid.
|
|
72
178
|
|
|
73
179
|
Layer "sequences":
|
|
74
|
-
- Available on
|
|
75
|
-
- Available properties
|
|
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
|
-
|
|
143
|
-
|
|
144
|
-
#
|
|
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
|
-
#
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
-
|
|
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
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
)
|
|
196
|
-
if z >= 13:
|
|
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
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
) mvtsequences,
|
|
210
|
-
(
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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
|
-
|
|
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
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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
|
-
|
|
387
|
+
|
|
388
|
+
# Sequences + grid (z6-6.9)
|
|
389
|
+
elif z >= ZOOM_GRID_SEQUENCES:
|
|
249
390
|
query = sql.SQL(
|
|
250
391
|
"""
|
|
251
|
-
SELECT
|
|
252
|
-
FROM (
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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/params.py
CHANGED
|
@@ -6,12 +6,12 @@ from dateutil.parser import parse as dateparser
|
|
|
6
6
|
import datetime
|
|
7
7
|
import re
|
|
8
8
|
from werkzeug.datastructures import MultiDict
|
|
9
|
-
from
|
|
10
|
-
from typing import Optional, Tuple, Dict, List, Any
|
|
9
|
+
from typing import Optional, Tuple, Any, List
|
|
11
10
|
from pygeofilter.backends.sql import to_sql_where
|
|
12
11
|
from pygeofilter.parsers.ecql import parse as ecql_parser
|
|
13
12
|
from psycopg import sql
|
|
14
|
-
from geovisio.utils.sequences import
|
|
13
|
+
from geovisio.utils.sequences import STAC_FIELD_MAPPINGS, STAC_FIELD_TO_SQL_FILTER
|
|
14
|
+
from geovisio.utils.fields import SortBy, SQLDirection, SortByField
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
RGX_SORTBY = re.compile("[+-]?[A-Za-z_].*(,[+-]?[A-Za-z_].*)*")
|
|
@@ -171,6 +171,82 @@ def parse_bbox(value: Optional[Any], tryFallbacks=True):
|
|
|
171
171
|
return None
|
|
172
172
|
|
|
173
173
|
|
|
174
|
+
def parse_lonlat(values: Optional[Any], paramName: Optional[str] = None) -> Optional[List[float]]:
|
|
175
|
+
"""Reads lat,lon query parameter.
|
|
176
|
+
|
|
177
|
+
>>> parse_lonlat('0,0')
|
|
178
|
+
[0.0, 0.0]
|
|
179
|
+
>>> parse_lonlat('47.8,1.25')
|
|
180
|
+
[47.8, 1.25]
|
|
181
|
+
>>> parse_lonlat('-1.57,-12.5')
|
|
182
|
+
[-1.57, -12.5]
|
|
183
|
+
>>> parse_lonlat([42.7, -1.24])
|
|
184
|
+
[42.7, -1.24]
|
|
185
|
+
>>> parse_lonlat("[42.7, -1.24]")
|
|
186
|
+
[42.7, -1.24]
|
|
187
|
+
>>> parse_lonlat(None)
|
|
188
|
+
|
|
189
|
+
>>> parse_lonlat('aaa') # doctest: +IGNORE_EXCEPTION_DETAIL
|
|
190
|
+
Traceback (most recent call last):
|
|
191
|
+
geovisio.errors.InvalidAPIUsage: Parameter must be coordinates in lat,lon format
|
|
192
|
+
>>> parse_lonlat('182,0') # doctest: +IGNORE_EXCEPTION_DETAIL
|
|
193
|
+
Traceback (most recent call last):
|
|
194
|
+
geovisio.errors.InvalidAPIUsage: Longitude in parameter is not valid (should be between -180 and 180)
|
|
195
|
+
>>> parse_lonlat('0,-92') # doctest: +IGNORE_EXCEPTION_DETAIL
|
|
196
|
+
Traceback (most recent call last):
|
|
197
|
+
geovisio.errors.InvalidAPIUsage: Latitude in parameter is not valid (should be between -90 and 90)
|
|
198
|
+
"""
|
|
199
|
+
|
|
200
|
+
if values is None or values == []:
|
|
201
|
+
return None
|
|
202
|
+
|
|
203
|
+
entries = parse_list(values, paramName=paramName)
|
|
204
|
+
|
|
205
|
+
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)
|
|
207
|
+
|
|
208
|
+
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)"),
|
|
211
|
+
]
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def parse_distance_range(values: Optional[str], paramName: Optional[str] = None):
|
|
215
|
+
"""Reads distance range query parameter.
|
|
216
|
+
|
|
217
|
+
>>> parse_distance_range('3-12')
|
|
218
|
+
[3, 12]
|
|
219
|
+
>>> parse_distance_range(None)
|
|
220
|
+
|
|
221
|
+
>>> parse_distance_range('12-3') # doctest: +IGNORE_EXCEPTION_DETAIL
|
|
222
|
+
Traceback (most recent call last):
|
|
223
|
+
geovisio.errors.InvalidAPIUsage: Parameter has a min value greater than its max value
|
|
224
|
+
>>> parse_distance_range('12') # doctest: +IGNORE_EXCEPTION_DETAIL
|
|
225
|
+
Traceback (most recent call last):
|
|
226
|
+
geovisio.errors.InvalidAPIUsage: Parameter is invalid (should be a distance range in meters like "5-15")
|
|
227
|
+
"""
|
|
228
|
+
|
|
229
|
+
if values is not None:
|
|
230
|
+
dists = values.split("-")
|
|
231
|
+
if len(dists) != 2:
|
|
232
|
+
raise errors.InvalidAPIUsage(
|
|
233
|
+
f"Parameter {paramName or ''} is invalid (should be a distance range in meters like \"5-15\")", status_code=400
|
|
234
|
+
)
|
|
235
|
+
try:
|
|
236
|
+
dists = [int(d) for d in dists]
|
|
237
|
+
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)
|
|
239
|
+
else:
|
|
240
|
+
return dists
|
|
241
|
+
|
|
242
|
+
except ValueError:
|
|
243
|
+
raise errors.InvalidAPIUsage(
|
|
244
|
+
f"Parameter {paramName or ''} is invalid (should be a distance range in meters like \"5-15\")", status_code=400
|
|
245
|
+
)
|
|
246
|
+
else:
|
|
247
|
+
return None
|
|
248
|
+
|
|
249
|
+
|
|
174
250
|
def parse_list(value: Optional[Any], tryFallbacks: bool = True, paramName: Optional[str] = None):
|
|
175
251
|
"""Reads STAC query parameters that are structured like lists.
|
|
176
252
|
|
|
@@ -192,6 +268,8 @@ def parse_list(value: Optional[Any], tryFallbacks: bool = True, paramName: Optio
|
|
|
192
268
|
['a', 'b']
|
|
193
269
|
>>> parse_list("['a', 'b']")
|
|
194
270
|
['a', 'b']
|
|
271
|
+
>>> parse_list([42, 10])
|
|
272
|
+
[42, 10]
|
|
195
273
|
>>> parse_list([])
|
|
196
274
|
|
|
197
275
|
>>> parse_list(MultiDict([('collections', 'a'), ('collections', 'b')]))
|
|
@@ -235,7 +313,7 @@ def parse_list(value: Optional[Any], tryFallbacks: bool = True, paramName: Optio
|
|
|
235
313
|
if len(res) == 0:
|
|
236
314
|
return None
|
|
237
315
|
else:
|
|
238
|
-
return [n.strip('"').strip("'") for n in res]
|
|
316
|
+
return [n.strip('"').strip("'") if isinstance(n, str) else n for n in res]
|
|
239
317
|
|
|
240
318
|
else:
|
|
241
319
|
return None
|
geovisio/web/stac.py
CHANGED
|
@@ -4,7 +4,7 @@ from flask import Blueprint, current_app, request, url_for
|
|
|
4
4
|
from geovisio import errors
|
|
5
5
|
from geovisio.utils import auth
|
|
6
6
|
from psycopg.rows import dict_row
|
|
7
|
-
from geovisio.utils.fields import Bounds
|
|
7
|
+
from geovisio.utils.fields import SortBy, SQLDirection, Bounds, SortByField
|
|
8
8
|
from geovisio.web.utils import (
|
|
9
9
|
STAC_VERSION,
|
|
10
10
|
cleanNoneInList,
|
|
@@ -18,9 +18,6 @@ from geovisio.web.utils import (
|
|
|
18
18
|
from geovisio.utils.sequences import (
|
|
19
19
|
get_collections,
|
|
20
20
|
CollectionsRequest,
|
|
21
|
-
SortBy,
|
|
22
|
-
SortByField,
|
|
23
|
-
SQLDirection,
|
|
24
21
|
STAC_FIELD_MAPPINGS,
|
|
25
22
|
get_pagination_links,
|
|
26
23
|
)
|
|
@@ -122,6 +119,7 @@ def getLanding():
|
|
|
122
119
|
.replace("333", "{z}")
|
|
123
120
|
.replace("bob", "{userId}")
|
|
124
121
|
)
|
|
122
|
+
userStyleUrl = url_for("map.getUserStyle", userId="bob", _external=True).replace("bob", "{userId}")
|
|
125
123
|
|
|
126
124
|
if "stac_extensions" not in catalog:
|
|
127
125
|
catalog["stac_extensions"] = []
|
|
@@ -146,12 +144,24 @@ def getLanding():
|
|
|
146
144
|
"href": mapUrl,
|
|
147
145
|
"title": "Pictures and sequences vector tiles",
|
|
148
146
|
},
|
|
147
|
+
{
|
|
148
|
+
"rel": "xyz-style",
|
|
149
|
+
"type": "application/json",
|
|
150
|
+
"href": url_for("map.getStyle", _external=True),
|
|
151
|
+
"title": "MapLibre Style JSON",
|
|
152
|
+
},
|
|
149
153
|
{
|
|
150
154
|
"rel": "user-xyz",
|
|
151
155
|
"type": "application/vnd.mapbox-vector-tile",
|
|
152
156
|
"href": userMapUrl,
|
|
153
157
|
"title": "Pictures and sequences vector tiles for a given user",
|
|
154
158
|
},
|
|
159
|
+
{
|
|
160
|
+
"rel": "user-xyz-style",
|
|
161
|
+
"type": "application/json",
|
|
162
|
+
"href": userStyleUrl,
|
|
163
|
+
"title": "MapLibre Style JSON",
|
|
164
|
+
},
|
|
155
165
|
{
|
|
156
166
|
"rel": "collection-preview",
|
|
157
167
|
"type": "image/jpeg",
|