auto-sync: 2026-05-03 00:20:01
This commit is contained in:
@@ -12,12 +12,31 @@ import struct
|
||||
import sqlite3
|
||||
import json
|
||||
from pathlib import Path
|
||||
from shapely.geometry import LineString
|
||||
|
||||
from functools import lru_cache
|
||||
from fastapi import FastAPI, HTTPException, Response
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
import uvicorn
|
||||
|
||||
# ─── Tile cache ──────────────────────────────────────────────────────────────
|
||||
|
||||
_tile_cache: dict = {}
|
||||
_TILE_CACHE_MAX = 512
|
||||
|
||||
|
||||
def get_cached_tile(z, x, y):
|
||||
return _tile_cache.get((z, x, y))
|
||||
|
||||
|
||||
def set_cached_tile(z, x, y, data: bytes):
|
||||
if len(_tile_cache) >= _TILE_CACHE_MAX:
|
||||
# FIFO вытеснение — удаляем самый старый ключ
|
||||
_tile_cache.pop(next(iter(_tile_cache)))
|
||||
_tile_cache[(z, x, y)] = data
|
||||
|
||||
|
||||
# ─── Конфиг ───────────────────────────────────────────────────────────────────
|
||||
|
||||
DATA_PATH = os.environ.get(
|
||||
@@ -106,6 +125,26 @@ def wkb_point_coords(blob: bytes):
|
||||
|
||||
# ─── MVT builder ──────────────────────────────────────────────────────────────
|
||||
|
||||
def simplify_coords(coords, z):
|
||||
"""Упрощает геометрию трека по зуму через Douglas-Peucker."""
|
||||
if z >= 12:
|
||||
return coords # без упрощения на детальных зумах
|
||||
elif z >= 10:
|
||||
tolerance = 0.0005 # ~50м
|
||||
elif z >= 8:
|
||||
tolerance = 0.002 # ~200м
|
||||
else:
|
||||
tolerance = 0.008 # ~800м на z7 и ниже
|
||||
|
||||
if len(coords) < 3:
|
||||
return coords
|
||||
|
||||
line = LineString(coords)
|
||||
simplified = line.simplify(tolerance, preserve_topology=False)
|
||||
result = list(simplified.coords)
|
||||
return result if len(result) >= 2 else coords
|
||||
|
||||
|
||||
def build_mvt(trails_rows, poi_rows, z, x, y) -> bytes:
|
||||
"""Собирает MVT тайл. Передаёт полные геометрии без серверного клиппинга.
|
||||
MapLibre GL сам клипит на клиенте. quantize_bounds расширяем на 10% чтобы
|
||||
@@ -119,6 +158,7 @@ def build_mvt(trails_rows, poi_rows, z, x, y) -> bytes:
|
||||
coords = wkb_to_coords(row["geom"])
|
||||
if not coords:
|
||||
continue
|
||||
coords = simplify_coords(coords, z)
|
||||
try:
|
||||
props = {
|
||||
"highway": row["highway_type"] or "",
|
||||
@@ -175,6 +215,12 @@ def build_mvt(trails_rows, poi_rows, z, x, y) -> bytes:
|
||||
|
||||
# ─── API endpoints ────────────────────────────────────────────────────────────
|
||||
|
||||
@app.get("/api/cache/clear")
|
||||
async def clear_cache():
|
||||
_tile_cache.clear()
|
||||
return {"status": "ok", "cleared": True}
|
||||
|
||||
|
||||
@app.get("/api/tiles/{z}/{x}/{y}.mvt")
|
||||
async def get_tile(z: int, x: int, y: int):
|
||||
if z < 0 or z > 22:
|
||||
@@ -183,6 +229,15 @@ async def get_tile(z: int, x: int, y: int):
|
||||
if x < 0 or x >= max_coord or y < 0 or y >= max_coord:
|
||||
raise HTTPException(400, "Invalid x/y for zoom level")
|
||||
|
||||
# Проверяем кэш до обращения к БД
|
||||
cached = get_cached_tile(z, x, y)
|
||||
if cached is not None:
|
||||
return Response(
|
||||
content=cached,
|
||||
media_type="application/x-protobuf",
|
||||
headers={"Content-Encoding": "identity", "Access-Control-Allow-Origin": "*", "X-Cache": "HIT"},
|
||||
)
|
||||
|
||||
if not os.path.exists(DATA_PATH):
|
||||
raise HTTPException(503, f"База данных не найдена: {DATA_PATH}")
|
||||
|
||||
@@ -196,19 +251,14 @@ async def get_tile(z: int, x: int, y: int):
|
||||
q_south = south - buf_y
|
||||
q_north = north + buf_y
|
||||
|
||||
# Минимальная длина трека по зуму — короткие треки не показываем на обзорных зумах.
|
||||
# Это даёт консистентную картину: трек либо виден на всех зумах >= порога, либо нет.
|
||||
# Значения подобраны эмпирически для ЦФО (~300км на z9, ~75км на z11).
|
||||
if z <= 7:
|
||||
min_length = 5000 # только треки длиннее 5 км
|
||||
limit = 3000
|
||||
elif z <= 9:
|
||||
min_length = 2000 # длиннее 2 км
|
||||
limit = 8000
|
||||
elif z <= 11:
|
||||
min_length = 500 # длиннее 500 м
|
||||
limit = 15000
|
||||
else:
|
||||
min_length = 0 # все треки
|
||||
|
||||
limit = 20000 # единый высокий лимит — фильтрация по длине важнее
|
||||
limit = 25000
|
||||
|
||||
try:
|
||||
conn = get_db()
|
||||
@@ -219,10 +269,8 @@ async def get_tile(z: int, x: int, y: int):
|
||||
FROM trails
|
||||
WHERE min_lon <= ? AND max_lon >= ?
|
||||
AND min_lat <= ? AND max_lat >= ?
|
||||
AND (length_m IS NULL OR length_m >= ?)
|
||||
ORDER BY length_m DESC
|
||||
LIMIT ?
|
||||
""", (q_east, q_west, q_north, q_south, min_length, limit))
|
||||
""", (q_east, q_west, q_north, q_south, limit))
|
||||
trails_rows = cur.fetchall()
|
||||
|
||||
cur.execute("""
|
||||
@@ -239,12 +287,17 @@ async def get_tile(z: int, x: int, y: int):
|
||||
|
||||
mvt = build_mvt(trails_rows, poi_rows, z, x, y)
|
||||
|
||||
# Кэшируем только непустые тайлы
|
||||
if mvt:
|
||||
set_cached_tile(z, x, y, mvt)
|
||||
|
||||
return Response(
|
||||
content=mvt,
|
||||
media_type="application/x-protobuf",
|
||||
headers={
|
||||
"Content-Encoding": "identity",
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
"X-Cache": "MISS",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user