From 14844be1e655609a2edfd6fb58b0de6b3d3bae5f Mon Sep 17 00:00:00 2001 From: Stream Date: Sun, 3 May 2026 00:20:01 +0300 Subject: [PATCH] auto-sync: 2026-05-03 00:20:01 --- tasks/enduro-trails/prototype/app.py | 77 +++++++++++++++++++++++----- 1 file changed, 65 insertions(+), 12 deletions(-) diff --git a/tasks/enduro-trails/prototype/app.py b/tasks/enduro-trails/prototype/app.py index 7917344..12978c3 100644 --- a/tasks/enduro-trails/prototype/app.py +++ b/tasks/enduro-trails/prototype/app.py @@ -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", }, )