1
- import math
2
1
from typing import Any , Dict , List , Optional
3
2
4
- import mapbox_vector_tile # type: ignore
5
3
from asyncpg import Connection
6
4
from fastapi import APIRouter , Depends , HTTPException , Request , Response
7
- from shapely .geometry import Point , Polygon # type: ignore
8
5
9
6
from modules import query , tiles
10
7
from modules .dependencies import commons_params , database
17
14
class MVTResponse (Response ):
18
15
media_type = "application/vnd.mapbox-vector-tile"
19
16
20
- def render (self , content : Any ) -> bytes :
21
- return mapbox_vector_tile .encode (
22
- content ["content" ],
23
- extents = content .get ("extents" , 2048 ),
24
- quantize_bounds = content .get ("quantize_bounds" ),
25
- )
26
-
27
-
28
- def mvtResponse (content ) -> Response :
29
- if not content or not content ["content" ]:
30
- return Response (status_code = 204 )
31
- else :
32
- return MVTResponse (content , media_type = "application/vnd.mapbox-vector-tile" )
33
-
34
-
35
- def _errors_mvt (
36
- results : List [Dict [str , Any ]],
37
- z : int ,
38
- min_lon : float ,
39
- min_lat : float ,
40
- max_lon : float ,
41
- max_lat : float ,
42
- limit : int ,
43
- ) -> Optional [Dict [str , Any ]]:
44
- if not results or len (results ) == 0 :
45
- return None
46
- else :
47
- limit_feature = []
48
- if len (results ) == limit and z < 18 :
49
- limit_feature = [
50
- {
51
- "name" : "limit" ,
52
- "features" : [
53
- {
54
- "geometry" : Point (
55
- (min_lon + max_lon ) / 2 , (min_lat + max_lat ) / 2
56
- )
57
- }
58
- ],
59
- }
60
- ]
61
-
62
- issues_features = []
63
- for res in sorted (results , key = lambda res : - res ["lat" ]):
64
- issues_features .append (
65
- {
66
- "id" : res ["id" ],
67
- "geometry" : Point (res ["lon" ], res ["lat" ]),
68
- "properties" : {
69
- "uuid" : str (res ["uuid" ]),
70
- "item" : res ["item" ] or 0 ,
71
- "class" : res ["class" ] or 0 ,
72
- },
73
- }
74
- )
75
-
76
- return {
77
- "content" : [{"name" : "issues" , "features" : issues_features }]
78
- + limit_feature ,
79
- "quantize_bounds" : (min_lon , min_lat , max_lon , max_lat ),
80
- }
81
-
82
17
83
18
def _errors_geojson (
84
19
results : List [Dict [str , Any ]],
@@ -179,15 +114,15 @@ async def heat(
179
114
sql = (
180
115
f"""
181
116
SELECT
182
- COUNT(*),
117
+ COUNT(*) AS count ,
183
118
(
184
119
(lon-${ len (sql_params )- 4 } ) * ${ len (sql_params )} /
185
- (${ len (sql_params )- 2 } -${ len (sql_params )- 4 } ) + 0.5
186
- )::int AS latn ,
120
+ (${ len (sql_params )- 2 } -${ len (sql_params )- 4 } ) - 0.5
121
+ )::int AS x ,
187
122
(
188
123
(lat-${ len (sql_params )- 3 } ) * ${ len (sql_params )} /
189
124
(${ len (sql_params )- 1 } -${ len (sql_params )- 3 } ) + 0.5
190
- )::int AS lonn ,
125
+ )::int AS y ,
191
126
mode() WITHIN GROUP (ORDER BY items.marker_color) AS color
192
127
FROM
193
128
"""
@@ -198,39 +133,45 @@ async def heat(
198
133
+ where
199
134
+ """
200
135
GROUP BY
201
- latn,
202
- lonn
136
+ x, y
203
137
"""
204
138
)
205
139
206
- features = []
207
- for row in await db . fetch ( sql , * sql_params ):
208
- count , x , y , color = row
209
- count = max (
210
- int (
211
- math . log ( count )
212
- / math . log ( limit / (( z - 4 + 1 + math . sqrt ( COUNT )) ** 2 ))
213
- * 255
214
- ),
215
- 1 if count > 0 else 0 ,
216
- )
217
- if count > 0 :
218
- count = 255 if count > 255 else count
219
- features . append (
220
- {
221
- "geometry" : Polygon (
222
- [( x , y ), ( x - 1 , y ), ( x - 1 , y - 1 ), ( x , y - 1 )]
223
- ),
224
- "properties" : { "color" : int ( color [ 1 :], 16 ), "count" : count },
225
- }
226
- )
227
-
228
- return mvtResponse (
229
- {
230
- "content" : [{ "name" : "issues" , "features" : features }],
231
- "extents" : COUNT ,
232
- }
140
+ sql_params + = [params . limit , params . zoom ]
141
+ sql = f"""
142
+ WITH
143
+ grid AS ( { sql } ),
144
+ grid_count AS (
145
+ SELECT
146
+ greatest(
147
+ (
148
+ log(count)
149
+ / log($ { len ( sql_params ) - 1 } / (($ { len ( sql_params ) } - 4 + 1 + sqrt($ { len ( sql_params ) - 2 } )) ^ 2))
150
+ * 255
151
+ )::int,
152
+ CASE WHEN count > 0 THEN 1 ELSE 0 END
153
+ ) AS count,
154
+ x AS x, $ { len ( sql_params ) - 2 } - y AS y, color
155
+ FROM
156
+ grid
157
+ ),
158
+ a AS (
159
+ SELECT
160
+ least(count, 255) AS count,
161
+ ('0x' || substring(color, 2))::int AS color,
162
+ ST_MakeEnvelope(x, y, x+1, y+1) AS geom
163
+ FROM
164
+ grid_count
165
+ WHERE
166
+ count > 0
233
167
)
168
+ SELECT ST_AsMVT(a, 'issues', ${ len (sql_params )- 2 } ::int, 'geom') FROM a
169
+ """
170
+ results = await db .fetchval (sql , * sql_params )
171
+ if results is None or len (results ) == 0 :
172
+ return Response (status_code = 204 )
173
+ else :
174
+ return MVTResponse (results )
234
175
235
176
236
177
def _issues_params (
@@ -277,10 +218,11 @@ async def issues_mvt(
277
218
if params .zoom > 18 or params .zoom < 7 :
278
219
return Response (status_code = 204 )
279
220
280
- results = await query ._gets (db , params )
281
- lon1 , lat2 = tiles .tile2lonlat (x , y , z )
282
- lon2 , lat1 = tiles .tile2lonlat (x + 1 , y + 1 , z )
283
- return mvtResponse (_errors_mvt (results , z , lon1 , lat1 , lon2 , lat2 , params .limit ))
221
+ results = await query ._gets (db , params , mvt = True )
222
+ if results is None or len (results ) == 0 :
223
+ return Response (status_code = 204 )
224
+ else :
225
+ return MVTResponse (results )
284
226
285
227
286
228
@router .get (
0 commit comments