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/__init__.py +3 -2
- geovisio/admin_cli/__init__.py +2 -2
- geovisio/admin_cli/db.py +11 -0
- geovisio/config_app.py +25 -0
- geovisio/templates/main.html +2 -2
- geovisio/utils/pictures.py +4 -3
- geovisio/utils/sequences.py +56 -32
- geovisio/web/auth.py +15 -2
- geovisio/web/collections.py +38 -36
- geovisio/web/docs.py +51 -2
- geovisio/web/items.py +49 -45
- geovisio/web/map.py +307 -91
- geovisio/web/stac.py +13 -0
- geovisio/web/tokens.py +7 -3
- geovisio/web/utils.py +10 -0
- geovisio/workers/runner_pictures.py +47 -0
- geovisio-2.6.0.dist-info/METADATA +92 -0
- {geovisio-2.5.0.dist-info → geovisio-2.6.0.dist-info}/RECORD +20 -20
- geovisio-2.5.0.dist-info/METADATA +0 -115
- {geovisio-2.5.0.dist-info → geovisio-2.6.0.dist-info}/LICENSE +0 -0
- {geovisio-2.5.0.dist-info → geovisio-2.6.0.dist-info}/WHEEL +0 -0
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
|
-
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
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
|
|
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 >= 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
|
-
|
|
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/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",
|