auto-sync: 2026-05-03 00:20:01

This commit is contained in:
Stream
2026-05-03 00:20:01 +03:00
parent cdfb9f966d
commit 14844be1e6

View File

@@ -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",
},
)