point.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. from fastapi import APIRouter, Request, Depends, Query, HTTPException, status,Path
  4. from common.security import valid_access_token
  5. from fastapi.responses import JSONResponse,Response
  6. from fastapi.responses import StreamingResponse
  7. from sqlalchemy.orm import Session
  8. from sqlalchemy import and_, or_,text,literal
  9. from sqlalchemy.sql import func
  10. from sqlalchemy.future import select
  11. from common.auth_user import *
  12. from pydantic import BaseModel
  13. from typing import Any, Dict
  14. # import contextily as ctx
  15. # import geopandas as gpd
  16. # from matplotlib import pyplot as plt
  17. import io
  18. from database import get_db
  19. from typing import List
  20. from models import *
  21. from utils import *
  22. from utils.ry_system_util import *
  23. from utils.video_util import *
  24. from collections import defaultdict
  25. import traceback
  26. from concurrent.futures import ThreadPoolExecutor, as_completed
  27. from multiprocessing import Pool, cpu_count
  28. import json
  29. import time
  30. import math
  31. from shapely import wkb
  32. from shapely.geometry import Point
  33. router = APIRouter()
  34. def get_geom(db,iszjcj,pac):
  35. if iszjcj=='zj':
  36. pac = pac[:6]
  37. zjcjtable = 'tp_geojson_data_qx'
  38. elif iszjcj=='cj':
  39. pac = pac[:9]
  40. zjcjtable = 'tp_geojson_data_zj'
  41. sql = f"""select name, ST_AsBinary(geometry)as geometry from {zjcjtable} where pac like '{pac}%'"""
  42. result = db.execute(sql)
  43. infos = result.fetchall()
  44. return infos
  45. @router.post("/get_info")
  46. @router.get("/get_info")
  47. async def get_infos(
  48. body = Depends(remove_xss_json),
  49. # zoom_level: float = Query(..., description="Zoom level for clustering"),
  50. # latitude_min: float = Query(..., description="Minimum latitude"),
  51. # latitude_max: float = Query(..., description="Maximum latitude"),
  52. # longitude_min: float = Query(..., description="Minimum longitude"),
  53. # longitude_max: float = Query(..., description="Maximum longitude"),
  54. # dict_value: str = Query(None),
  55. # option:str = Query(None),
  56. db: Session = Depends(get_db)
  57. ):
  58. try:
  59. # 根据缩放级别动态调整分组粒度
  60. zoom_level = float(body['zoom_level'])
  61. zoom_levels = {
  62. 3: 10000, # 全国范围
  63. 4: 5000,
  64. 5: 2500,
  65. 6: 1250,
  66. 7: 825,
  67. 8: 412.5,
  68. 9: 256.25,
  69. 10: 178.125,
  70. 11: 69.0625,
  71. 12: 29.53125,
  72. 13: 13.765625,
  73. 14: 5.8828125,
  74. 15: 2.44140625,
  75. 16: 1.220703125,
  76. 17: 0.6103515625,
  77. 18: 0.30517578125
  78. }
  79. distance_threshold=zoom_levels[int(zoom_level-1)]
  80. # distance_threshold = 100000 / (2.2 ** zoom_level) # 例如:每缩放一级,距离阈值减半
  81. dict_value= body['dict_value'].split(',')
  82. latitude_min = float(body['latitude_min'])
  83. latitude_max = float(body['latitude_max'])
  84. longitude_min = float(body['longitude_min'])
  85. longitude_max = float(body['longitude_max'])
  86. option = body['option'].split(',')
  87. print("1",time.time())
  88. iszjcj = ''
  89. pac = ''
  90. if 'iszjcj'in body:
  91. iszjcj = body['iszjcj']
  92. pac = body['pac']
  93. videos = get_videos(db,dict_value,latitude_min,latitude_max,longitude_min,longitude_max,iszjcj)
  94. infos = get_points(db,option,latitude_min,latitude_max,longitude_min,longitude_max,iszjcj,pac)
  95. # 动态分组逻辑
  96. if 'iszjcj' in body:
  97. iszjcj = body['iszjcj']
  98. pac = body['pac']
  99. groups = group_points(videos + infos, distance_threshold,db,iszjcj,pac)
  100. else:
  101. groups = group_points(videos+infos, distance_threshold)
  102. print("4",time.time())
  103. return {"code": 200,
  104. "msg": "操作成功",
  105. "data": groups}
  106. except Exception as e:
  107. # 处理异常
  108. traceback.print_exc()
  109. raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e))
  110. @router.post("/get_details")
  111. @router.get("/get_details")
  112. async def get_details(
  113. body = Depends(remove_xss_json),
  114. # center_latitude: float = Query(..., description="网格中心点的纬度"),
  115. # center_longitude: float = Query(..., description="网格中心点的经度"),
  116. # zoom_level: float = Query(..., description="缩放级别"),
  117. db: Session = Depends(get_db)
  118. ):
  119. try:
  120. # 计算网格大小
  121. zoom_level = float(body['zoom_level'])
  122. zoom_levels = {
  123. 3: 10000, # 全国范围
  124. 4: 5000,
  125. 5: 2500,
  126. 6: 1250,
  127. 7: 825,
  128. 8: 412.5,
  129. 9: 256.25,
  130. 10: 178.125,
  131. 11: 69.0625,
  132. 12: 29.53125,
  133. 13: 13.765625,
  134. 14: 5.8828125,
  135. 15: 2.44140625,
  136. 16: 1.220703125,
  137. 17: 0.6103515625,
  138. 18: 0.30517578125
  139. }
  140. distance_threshold=zoom_levels[int(zoom_level-1)]
  141. # distance_threshold = 1000 / (1.5 ** zoom_level) # 例如:每缩放一级,距离阈值减半
  142. grid_size = calculate_grid_size(distance_threshold) # 地球半径为6371公里
  143. center_latitude = float(body['latitude'])
  144. center_longitude = float(body['longitude'])
  145. dict_value = body['dict_value'].split(',')
  146. option = body['option'].split(',')
  147. # 计算网格的经纬度范围
  148. latitude_min, latitude_max, longitude_min, longitude_max = get_grid_bounds_from_center(center_latitude, center_longitude, grid_size)
  149. videos = get_videos(db,dict_value,latitude_min,latitude_max,longitude_min,longitude_max)
  150. infos = get_points(db,option,latitude_min,latitude_max,longitude_min,longitude_max)
  151. return {"code": 200,
  152. "msg": "操作成功",
  153. "data": videos+infos }#{"videos":videos,"points":infos}}
  154. except Exception as e:
  155. # 处理异常
  156. traceback.print_exc()
  157. raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e))
  158. def calculate_grid_size(distance_threshold):
  159. # 假设地球半径为6371公里,将距离阈值转换为经纬度的差值
  160. # 这里假设纬度变化对距离的影响较小,仅根据经度计算网格大小
  161. earth_radius = 6371 # 地球半径,单位为公里
  162. grid_size = distance_threshold / earth_radius
  163. return grid_size
  164. def get_grid_key(latitude, longitude, grid_size):
  165. # 根据经纬度和网格大小计算网格键
  166. return (math.floor(latitude / grid_size), math.floor(longitude / grid_size))
  167. def get_grid_bounds_from_center(center_latitude, center_longitude, grid_size):
  168. half_grid_size = grid_size / 2
  169. min_latitude = center_latitude - half_grid_size
  170. max_latitude = center_latitude + half_grid_size
  171. min_longitude = center_longitude - half_grid_size
  172. max_longitude = center_longitude + half_grid_size
  173. return min_latitude, max_latitude, min_longitude, max_longitude
  174. def calculate_distance(point1, point2):
  175. # 使用 Haversine 公式计算两点之间的距离
  176. from math import radians, sin, cos, sqrt, atan2
  177. R = 6371 # 地球半径(公里)
  178. lat1, lon1 = radians(point1.latitude), radians(point1.longitude)
  179. lat2, lon2 = radians(point2.latitude), radians(point2.longitude)
  180. dlat = lat2 - lat1
  181. dlon = lon2 - lon1
  182. a = sin(dlat / 2) ** 2 + cos(lat1) * cos(lat2) * sin(dlon / 2) ** 2
  183. c = 2 * atan2(sqrt(a), sqrt(1 - a))
  184. return R * c
  185. def group_points(points, distance_threshold,db=get_db(),iszjcj='',pac=''):
  186. grid_size = calculate_grid_size(distance_threshold)
  187. grid = defaultdict(lambda:{"count":0}) #,"list":[]
  188. groups = []
  189. tmp = defaultdict(list)
  190. for point in points:
  191. grid_key = get_grid_key(float(point.latitude), float(point.longitude), grid_size)
  192. lovalue = str(point.latitude)+str(point.longitude)
  193. if lovalue not in tmp['%s-%s'%grid_key]:
  194. tmp['%s-%s'%grid_key].append(lovalue)
  195. grid['%s-%s'%grid_key]['count']+=1
  196. if grid['%s-%s'%grid_key]['count']>1 and len(tmp['%s-%s'%grid_key])<3:
  197. grid['%s-%s'%grid_key]['dataType'] = ''
  198. grid['%s-%s'%grid_key]['id'] = ""
  199. if len(tmp['%s-%s'%grid_key])<2:
  200. grid['%s-%s'%grid_key]['name'] = '多数据点位'
  201. grid['%s-%s' % grid_key]['type'] ='1'
  202. else:
  203. grid['%s-%s' % grid_key]['name'] = '聚合点位'
  204. grid['%s-%s' % grid_key]['type'] = '3'
  205. # grid['%s-%s'%grid_key]['latitude'] = float(point.latitude) #(grid_key[0] + 0.5) * grid_size
  206. # grid['%s-%s'%grid_key]['longitude'] = float(point.longitude) #(grid_key[1] + 0.5) * grid_size
  207. elif grid['%s-%s'%grid_key]['count']==1:
  208. if point.dataType=='video':
  209. grid['%s-%s' % grid_key]['id'] = point.gbIndexCode
  210. else:
  211. grid['%s-%s' % grid_key]['id'] = point.id
  212. grid['%s-%s'%grid_key]['dataType'] = point.dataType
  213. grid['%s-%s'%grid_key]['infoType'] = point.infoType
  214. grid['%s-%s'%grid_key]['name'] = point.name
  215. grid['%s-%s'%grid_key]['type'] ='2'
  216. grid['%s-%s'%grid_key]['latitude'] = float(point.latitude)
  217. grid['%s-%s'%grid_key]['longitude'] = float(point.longitude)
  218. groups = list(grid.values())
  219. if iszjcj!='':
  220. geom = get_geom(db, iszjcj, pac)
  221. for group in groups:
  222. for name, wkb_bytes in geom:
  223. lon, lat = group['longitude'],group['latitude']
  224. if wkb.loads(wkb_bytes).contains(Point(lon, lat)):
  225. break
  226. else:
  227. groups.remove(group)
  228. return groups
  229. def get_videos(db:Session,dict_value,latitude_min,latitude_max,longitude_min,longitude_max,iszjcj=''):
  230. que = True
  231. if len(dict_value)>0:
  232. videolist = []
  233. for value in dict_value:
  234. tag_info = get_dict_data_info(db, 'video_type', value)
  235. if tag_info:
  236. if tag_info.dict_label == '全量视频':
  237. break
  238. else:
  239. videolist += [i.video_code for i in tag_get_video_tag_list(db, value)]
  240. else:
  241. que = TPVideoInfo.gbIndexCode.in_(videolist)
  242. if iszjcj!='':
  243. pass
  244. # 查询分组
  245. query = (
  246. select(
  247. TPVideoInfo.gbIndexCode,
  248. TPVideoInfo.latitude,
  249. TPVideoInfo.longitude,
  250. TPVideoInfo.name,
  251. TPVideoInfo.status,
  252. literal('video').label("dataType"),
  253. literal('video').label("infoType")
  254. )
  255. .select_from(TPVideoInfo).where(
  256. and_(
  257. TPVideoInfo.latitude >= latitude_min,
  258. TPVideoInfo.latitude <= latitude_max,
  259. TPVideoInfo.longitude >= longitude_min,
  260. TPVideoInfo.longitude <= longitude_max,
  261. TPVideoInfo.longitude > 0,
  262. TPVideoInfo.latitude > 0, que
  263. )
  264. )
  265. .order_by(TPVideoInfo.status.asc())
  266. )
  267. result = db.execute(query)
  268. videos = result.fetchall()
  269. return videos
  270. def get_points(db:Session,option,latitude_min,latitude_max,longitude_min,longitude_max,iszjcj='',pac=''):
  271. # 使用参数化查询避免 SQL 注入
  272. if isinstance(option, list):
  273. option = tuple(option)
  274. # if iszjcj=='zj':
  275. # zd = ',T2.name as pacname,T2.pac,T2.parent_pac'
  276. # pac = pac[:6]
  277. # zjcjtable = f"""select * from tp_geojson_data_qx where pac like '{pac}%'"""
  278. # que = f' JOIN ({zjcjtable}) T2 on ST_Intersects(T2.geometry, ST_SRID(POINT(A.longitude, A.latitude), 4326))'
  279. # elif iszjcj=='cj':
  280. # pac = pac[:9]
  281. # zjcjtable = f"""select * from tp_geojson_data_zj where pac like '{pac}%'"""
  282. # que = f'LEFT JOIN ({zjcjtable}) T2 on ST_Intersects(T2.geometry, ST_SRID(POINT(A.longitude, A.latitude), 4326))'
  283. # else:
  284. que=''
  285. query = text(f"""
  286. SELECT
  287. A.`name`,A.`id`,A.dataType,A.longitude,A.latitude,A.infoType
  288. FROM (
  289. SELECT
  290. *,
  291. ROW_NUMBER() OVER (PARTITION BY longitude, latitude, `name`
  292. ORDER BY longitude, latitude, `name`) AS rn
  293. FROM
  294. `point_data`
  295. WHERE
  296. longitude > 0
  297. AND latitude BETWEEN :latitude_min AND :latitude_max
  298. AND longitude BETWEEN :longitude_min AND :longitude_max
  299. AND dataType IN :option
  300. ) AS A {que}
  301. WHERE rn = 1
  302. """)
  303. # 执行查询并传递参数
  304. result = db.execute(query, {
  305. 'latitude_min': latitude_min,
  306. 'latitude_max': latitude_max,
  307. 'longitude_min': longitude_min,
  308. 'longitude_max': longitude_max,
  309. 'option': option
  310. })
  311. infos = result.fetchall()
  312. return infos
  313. @router.post("/get_geojson")
  314. async def get_geojson(
  315. body = Depends(remove_xss_json),
  316. db: Session = Depends(get_db)
  317. ):
  318. try:
  319. # 根据缩放级别动态调整分组粒度
  320. latitude_min = float(body['latitude_min'])
  321. latitude_max = float(body['latitude_max'])
  322. longitude_min = float(body['longitude_min'])
  323. longitude_max = float(body['longitude_max'])
  324. if latitude_min<-90 or latitude_max>90 :
  325. return JSONResponse(status_code=500, content={"code": 500, "msg": "Latitude must be within [-90.000000, 90.000000]"})
  326. if longitude_min<-180 or longitude_max>180 :
  327. return JSONResponse(status_code=500, content={"code": 500, "msg": "Longitude must be within [-180.000000, 180.000000]"})
  328. table_name = 'tp_geojson_data_zj'
  329. option = body['option']
  330. if 'cj' == option:
  331. table_name = 'tp_geojson_data_cj_sq'
  332. sql = f"""SELECT id,
  333. name,
  334. pac,
  335. ST_AsGeoJSON(geometry) AS geojson,
  336. properties
  337. FROM {table_name}
  338. WHERE ST_Intersects(
  339. geometry,
  340. ST_GeomFromText(
  341. 'POLYGON(({latitude_min} {longitude_min},
  342. {latitude_max} {longitude_min},
  343. {latitude_max} {longitude_max},
  344. {latitude_min} {longitude_max},
  345. {latitude_min} {longitude_min}))',
  346. 4326
  347. )
  348. );"""
  349. result = db.execute(sql)
  350. features = result.fetchall()
  351. return {"code": 200,
  352. "msg": "操作成功","type":"FeatureCollection",
  353. "features": features}
  354. except Exception as e:
  355. # 处理异常
  356. traceback.print_exc()
  357. raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e))
  358. @router.post("/get_geojson_new")
  359. async def get_geojson(
  360. body = Depends(remove_xss_json),
  361. db: Session = Depends(get_db)
  362. ):
  363. try:
  364. # 根据缩放级别动态调整分组粒度
  365. pac = body['area_code']
  366. table_name = 'tp_geojson_data_zj'
  367. option = body['option']
  368. if 'cj' == option:
  369. table_name = 'tp_geojson_data_cj_sq'
  370. pac = pac[:9]
  371. else:
  372. pac = pac[:6]
  373. sql = f"""SELECT
  374. ST_AsGeoJSON(geometry) AS geometry,
  375. properties
  376. FROM {table_name}
  377. WHERE parent_pac = '{pac}';"""
  378. def gen():
  379. # 1. 写头
  380. yield '{"type":"FeatureCollection","features":['
  381. first = True
  382. # 2. 逐行流式
  383. for geom, prop_json in db.execute(sql): # 迭代器,不 fetchall
  384. if not first:
  385. yield ","
  386. feature = {
  387. "geometry": json.loads(geom),
  388. "properties": {
  389. "PAC": json.loads(prop_json)['PAC'],
  390. "NAME": json.loads(prop_json)['NAME']
  391. }
  392. }
  393. yield json.dumps(feature, ensure_ascii=False)
  394. first = False
  395. # 3. 写尾
  396. yield "]}"
  397. return StreamingResponse(gen(), media_type="application/json",
  398. headers={"Content-Disposition": "attachment; filename=data.geojson"})
  399. except Exception as e:
  400. # 处理异常
  401. traceback.print_exc()
  402. raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e))
  403. # @router.post("/get_map_img")
  404. # async def get_map_img(
  405. # body: Dict[str, Any] = Depends(remove_xss_json),
  406. # db: Session = Depends(get_db),
  407. # ):
  408. # """
  409. # 输入:
  410. # {
  411. # "latitude_min": 27.0,
  412. # "latitude_max": 30.0,
  413. # "longitude_min": 118.0,
  414. # "longitude_max": 121.0,
  415. # "option": "zj" // "zj" 或 "cj"
  416. # }
  417. # 返回:PNG 图片
  418. # """
  419. # try:
  420. # # 1. 参数提取与合法性校验
  421. # lat_min = float(body["latitude_min"])
  422. # lat_max = float(body["latitude_max"])
  423. # lon_min = float(body["longitude_min"])
  424. # lon_max = float(body["longitude_max"])
  425. # option = body.get("option", "zj")
  426. #
  427. # if not (-90 <= lat_min <= 90 and -90 <= lat_max <= 90):
  428. # raise ValueError("Latitude must be within [-90, 90]")
  429. # if not (-180 <= lon_min <= 180 and -180 <= lon_max <= 180):
  430. # raise ValueError("Longitude must be within [-180, 180]")
  431. #
  432. # table_name = "tp_geojson_data_zj" if option != "cj" else "tp_geojson_data_cj_sq"
  433. #
  434. # # 2. 构造 SQL
  435. # sql = text(
  436. # f"""
  437. # SELECT id, name, pac, ST_AsGeoJSON(geometry) AS geojson, properties
  438. # FROM {table_name}
  439. # WHERE ST_Intersects(
  440. # geometry,
  441. # ST_GeomFromText(
  442. # 'POLYGON(({lon_min} {lat_min},
  443. # {lon_max} {lat_min},
  444. # {lon_max} {lat_max},
  445. # {lon_min} {lat_max},
  446. # {lon_min} {lat_min}))',
  447. # 4326
  448. # )
  449. # );
  450. # """
  451. # )
  452. #
  453. # rows = db.execute(sql).fetchall()
  454. # if not rows:
  455. # raise HTTPException(
  456. # status_code=status.HTTP_404_NOT_FOUND,
  457. # detail="No data within the given bbox."
  458. # )
  459. #
  460. # # 3. 组装 GeoDataFrame
  461. # features = [
  462. # {**json.loads(r.geojson), "properties": json.loads(r.properties)}
  463. # for r in rows
  464. # ]
  465. # gdf = gpd.GeoDataFrame.from_features(features, crs="EPSG:4326")
  466. #
  467. # # 4. 绘图
  468. # fig, ax = plt.subplots(figsize=(6, 6), dpi=150)
  469. # gdf.to_crs(epsg=3857).plot(ax=ax, alpha=0.5, edgecolor="black")
  470. # ctx.add_basemap(ax, source=ctx.providers.Stamen.TonerLite, crs=gdf.to_crs(epsg=3857).crs)
  471. # ax.set_axis_off()
  472. # plt.tight_layout(pad=0)
  473. #
  474. # # 5. 保存成字节流
  475. # buf = io.BytesIO()
  476. # fig.savefig(buf, format="png")
  477. # buf.seek(0)
  478. # plt.close(fig)
  479. #
  480. # # 6. 返回图片
  481. # return StreamingResponse(buf, media_type="image/png")
  482. #
  483. # except Exception as e:
  484. # raise HTTPException(
  485. # status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
  486. # detail=str(e)
  487. # )
  488. # TILE_EXTENT = 4096 # 标准 MVT 精度
  489. # # ---------- 纯 Python 坐标转换 ----------
  490. # def lonlat2xy(lon: float, lat: float) -> tuple[float, float]:
  491. # """4326 -> 3857"""
  492. # x = lon * 20037508.34 / 180
  493. # y = math.log(math.tan((90 + lat) * math.pi / 360)) / (math.pi / 180)
  494. # y = y * 20037508.34 / 180
  495. # return x, y
  496. #
  497. # import mercantile
  498. # from mapbox_vector_tile import encode
  499. # from shapely.geometry import shape
  500. # @router.get("/tile/{option}/{z}/{x}/{y}.pbf")
  501. # async def get_tile(
  502. # option: str = Path(..., regex="^(zj|cj)$"),
  503. # z: int = Path(..., ge=0, le=22),
  504. # x: int = Path(..., ge=0),
  505. # y: int = Path(..., ge=0),
  506. # db: Session = Depends(get_db),
  507. # ):
  508. # """
  509. # 根据 Slippy Map 标准 XYZ 返回 MVT 二进制。
  510. # 前端 layer.url = '/tile/zj/{z}/{x}/{y}.pbf'
  511. # """
  512. # table = "tp_geojson_data_zj" if option == "zj" else "tp_geojson_data_cj_sq"
  513. #
  514. # # 1. 计算瓦片 bbox (4326)
  515. # tile_bounds = mercantile.bounds(mercantile.Tile(x, y, z))
  516. # xmin, ymin, xmax, ymax = tile_bounds.west, tile_bounds.south, tile_bounds.east, tile_bounds.north
  517. # print(xmin, ymin, xmax, ymax)
  518. # # 2. 查相交要素
  519. # sql = text(
  520. # f"""
  521. # SELECT id, name, pac, properties,
  522. # ST_AsGeoJSON(geometry) AS geojson
  523. # FROM {table}
  524. # WHERE ST_Intersects(
  525. # geometry,
  526. # ST_GeomFromText(
  527. # 'POLYGON((
  528. # {ymin} {xmin},
  529. # {ymin} {xmax},
  530. # {ymax} {xmax},
  531. # {ymax} {xmin},
  532. # {ymin} {xmin}))',
  533. # 4326
  534. # )
  535. # );
  536. # """
  537. # )
  538. # rows = db.execute(sql).fetchall()
  539. # if not rows:
  540. # raise HTTPException(status_code=204)
  541. #
  542. # # 3. 构造 MVT features
  543. # features: List[Dict[str, Any]] = []
  544. # bounds_3857 = mercantile.xy_bounds(mercantile.Tile(x, y, z))
  545. # bx, by, bw, bh = bounds_3857.left, bounds_3857.bottom, \
  546. # bounds_3857.right - bounds_3857.left, \
  547. # bounds_3857.top - bounds_3857.bottom
  548. #
  549. # for r in rows:
  550. # # 直接用原始 GeoJSON,不做任何坐标变换
  551. # geo = json.loads(r.geojson)
  552. # features.append({
  553. # "geometry": geo, # 必须是 4326 坐标
  554. # "properties": json.loads(r.properties),
  555. # })
  556. #
  557. # # 4. 生成 MVT
  558. # mvt_bytes = encode([
  559. # {
  560. # "name": "layer",
  561. # "features": features,
  562. # "extent": TILE_EXTENT,
  563. # }
  564. # ])
  565. #
  566. # return Response(content=mvt_bytes, media_type="application/x-protobuf")