build backend in bin. choose several maps for action.

This commit is contained in:
parent ed272c5973
commit f8ae8d7d54
22 changed files with 1750 additions and 329 deletions

46
Docs/nuitka.md Normal file
View file

@ -0,0 +1,46 @@
## Nuitka build (backend)
### Важно
- `gdal2tiles.py` и GDAL — это системные бинарники/библиотеки, Nuitka их не “впаяет” в один файл. На целевой машине GDAL всё равно должен быть установлен, если ты используешь `/tiles/slice` (нарезку).
- Для onefile-режима бинарник распаковывается во временную папку, поэтому проектные папки внутри пакета не должны использоваться для записи. В коде это решено через `GNSS_DATA_DIR`.
### Где лежат данные
По умолчанию:
- в обычном режиме (запуск из исходников) данные лежат рядом с пакетом (`gnss/`).
- в “замороженном” режиме (`nuitka --onefile`) данные идут в `~/.local/share/gnss`.
Можно переопределить:
- `GNSS_DATA_DIR=/path/to/data`
Структура:
- `$GNSS_DATA_DIR/tilesets/<tileset>/...`
- `$GNSS_DATA_DIR/uploads/...`
- `$GNSS_DATA_DIR/tiles/...` (опционально)
### Запуск без Nuitka (для проверки)
`poetry run gnss-backend --host 0.0.0.0 --port 8000`
### Установка Nuitka (в Poetry)
Рекомендовано держать Nuitka как dev-зависимость:
`poetry add --group dev nuitka`
### Сборка (standalone + onefile)
Пример:
`poetry run python -m nuitka --onefile --standalone --follow-imports --assume-yes-for-downloads --output-dir=dist --output-filename=gnss-backend gnss/__main__.py`
После сборки:
- бинарник будет в `dist/` (например `dist/gnss-backend.bin`)
### Запуск бинарника
`GNSS_DATA_DIR=./data ./dist/gnss-backend.bin --host 0.0.0.0 --port 8000`
Положи tileset’ы сюда:
- `./data/tilesets/<name>/...`

View file

@ -218,6 +218,14 @@ body, #root {
color: var(--muted); color: var(--muted);
} }
.empty code {
color: #e2e8f0;
background: rgba(255, 255, 255, 0.06);
padding: 2px 6px;
border-radius: 8px;
word-break: break-word;
}
.leaflet-control-container { .leaflet-control-container {
filter: none; filter: none;
} }

File diff suppressed because it is too large Load diff

34
gnss/__main__.py Normal file
View file

@ -0,0 +1,34 @@
import argparse
import os
from typing import Optional
import uvicorn
from gnss.main import app
def _env(name: str, default: str) -> str:
v = os.getenv(name)
return v if v is not None and v != "" else default
def main(argv: Optional[list[str]] = None) -> int:
parser = argparse.ArgumentParser(prog="gnss", description="GNSS Backend (FastAPI)")
parser.add_argument("--host", default=_env("GNSS_HOST", "0.0.0.0"))
parser.add_argument("--port", type=int, default=int(_env("GNSS_PORT", "8000")))
parser.add_argument("--log-level", default=_env("GNSS_LOG_LEVEL", "info"))
args = parser.parse_args(argv)
uvicorn.run(
app,
host=args.host,
port=args.port,
log_level=args.log_level,
access_log=True,
workers=1,
)
return 0
if __name__ == "__main__":
raise SystemExit(main())

Binary file not shown.

View file

@ -1,3 +1,15 @@
from .state import get_gnss_service, get_log_service, get_tile_service, get_vector_mbtiles_service from .state import (
get_gnss_service,
get_log_service,
get_raster_mbtiles_service,
get_tile_service,
get_vector_mbtiles_service,
)
__all__ = ["get_gnss_service", "get_tile_service", "get_log_service", "get_vector_mbtiles_service"] __all__ = [
"get_gnss_service",
"get_tile_service",
"get_log_service",
"get_raster_mbtiles_service",
"get_vector_mbtiles_service",
]

View file

@ -12,6 +12,8 @@ async def get_config() -> dict:
"tiles_url_tms": "/tiles/tms/{z}/{x}/{y}.png", "tiles_url_tms": "/tiles/tms/{z}/{x}/{y}.png",
"vector_tiles_url": "/tiles/vector/{z}/{x}/{y}.pbf", "vector_tiles_url": "/tiles/vector/{z}/{x}/{y}.pbf",
"vector_metadata_url": "/tiles/vector/metadata", "vector_metadata_url": "/tiles/vector/metadata",
"vector_tileset_url": "/tilesets/active/vector/{z}/{x}/{y}.pbf",
"vector_tileset_metadata_url": "/tilesets/active/vector/metadata",
"tilesets_url": "/tilesets", "tilesets_url": "/tilesets",
"tilesets_activate_url": "/tilesets/activate", "tilesets_activate_url": "/tilesets/activate",
"ws_url": "/ws/track", "ws_url": "/ws/track",

61
gnss/api/routes/maps.py Normal file
View file

@ -0,0 +1,61 @@
from pathlib import Path
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel, Field
from ..dependencies import get_raster_mbtiles_service, get_tile_service, get_vector_mbtiles_service
from ...services.raster_mbtiles import RasterMBTilesError, RasterMBTilesService
from ...services.tiles import TileCutError, TileService
from ...services.vector_mbtiles import VectorMBTilesError, VectorMBTilesService
router = APIRouter(tags=["maps"])
class MapIngestRequest(BaseModel):
path: str
tileset: str | None = None
min_zoom: int = Field(default=10, ge=0)
max_zoom: int = Field(default=16, ge=0)
@router.post("/maps/ingest")
async def ingest_map(
payload: MapIngestRequest,
tile_service: TileService = Depends(get_tile_service),
raster_mbtiles: RasterMBTilesService = Depends(get_raster_mbtiles_service),
vector_mbtiles: VectorMBTilesService = Depends(get_vector_mbtiles_service),
) -> dict:
p = Path(payload.path).expanduser().resolve()
if not p.exists():
raise HTTPException(status_code=400, detail=f"path not found: {p}")
if p.suffix.lower() == ".mbtiles":
try:
info = vector_mbtiles.activate(p)
return {"status": "ok", "kind": "vector", "driver": "mbtiles", "path": str(info.path), "format": info.metadata.get("format")}
except VectorMBTilesError:
pass
try:
info = raster_mbtiles.activate(p)
except RasterMBTilesError as exc:
raise HTTPException(status_code=400, detail=str(exc))
return {"status": "ok", "kind": "raster", "driver": "mbtiles", "path": str(info.path), "format": info.metadata.get("format")}
if p.suffix.lower() in {".tif", ".tiff", ".vrt"}:
if payload.max_zoom < payload.min_zoom:
raise HTTPException(status_code=400, detail="max_zoom must be >= min_zoom")
try:
tile_service.slice_to_tiles(
source=p,
destination=tile_service.tiles_dir,
min_zoom=payload.min_zoom,
max_zoom=payload.max_zoom,
scheme="xyz",
)
except TileCutError as exc:
raise HTTPException(status_code=400, detail=str(exc))
return {"status": "ok", "kind": "raster", "driver": "gdal2tiles", "path": str(p), "tiles_dir": str(tile_service.tiles_dir)}
raise HTTPException(status_code=400, detail="unsupported input; expected .mbtiles or GeoTIFF/VRT")

View file

@ -0,0 +1,51 @@
from pathlib import Path
from fastapi import APIRouter, Depends, HTTPException, Response
from pydantic import BaseModel
from ..dependencies import get_raster_mbtiles_service
from ...services.raster_mbtiles import RasterMBTilesError, RasterMBTilesService
router = APIRouter(tags=["raster-mbtiles"])
class ActivateRasterMBTiles(BaseModel):
path: str
@router.post("/tiles/raster/activate")
async def activate_raster_mbtiles(
payload: ActivateRasterMBTiles,
service: RasterMBTilesService = Depends(get_raster_mbtiles_service),
):
try:
info = service.activate(Path(payload.path))
except RasterMBTilesError as exc:
raise HTTPException(status_code=400, detail=str(exc))
return {"status": "ok", "path": str(info.path), "format": info.metadata.get("format")}
@router.get("/tiles/raster/metadata")
async def raster_metadata(service: RasterMBTilesService = Depends(get_raster_mbtiles_service)):
try:
return service.get_metadata()
except RasterMBTilesError as exc:
raise HTTPException(status_code=400, detail=str(exc))
@router.get("/tiles/raster/{z}/{x}/{y}")
async def raster_tile(
z: int,
x: int,
y: int,
service: RasterMBTilesService = Depends(get_raster_mbtiles_service),
):
try:
data, media_type = service.get_tile(z=z, x=x, y_xyz=y)
except KeyError:
raise HTTPException(status_code=404, detail="tile not found")
except RasterMBTilesError as exc:
raise HTTPException(status_code=400, detail=str(exc))
return Response(content=data, media_type=media_type)

View file

@ -3,23 +3,53 @@ from pathlib import Path
from fastapi import APIRouter, Depends, HTTPException, UploadFile from fastapi import APIRouter, Depends, HTTPException, UploadFile
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
from ...api.dependencies import get_tile_service import json
from ...api.dependencies import get_raster_mbtiles_service, get_tile_service
from ...models.tiles import TileSliceRequest from ...models.tiles import TileSliceRequest
from ...services.tiles import TileCutError, TileService from ...services.tiles import TileCutError, TileService
from ... import config from ... import config
from ...services.tilesets import TilesetService, TilesetError from ...services.tilesets import TilesetService, TilesetError
from ...services.raster_mbtiles import RasterMBTilesService
router = APIRouter(tags=["tiles"]) router = APIRouter(tags=["tiles"])
@router.get("/tiles/{zoom}/{x}/{y}.png") @router.get("/tiles/{zoom}/{x}/{y}.png")
async def serve_tile(zoom: int, x: int, y: int, tile_service: TileService = Depends(get_tile_service)): async def serve_tile(
zoom: int,
x: int,
y: int,
tile_service: TileService = Depends(get_tile_service),
raster_mbtiles: RasterMBTilesService = Depends(get_raster_mbtiles_service),
):
hit = tile_service._tiles_dir_tile_path(zoom, x, y)
if hit:
return tile_service.get_tile_response(zoom, x, y) return tile_service.get_tile_response(zoom, x, y)
try:
data, media_type = raster_mbtiles.get_tile(z=zoom, x=x, y_xyz=y)
except Exception:
return tile_service.get_tile_response(zoom, x, y)
return tile_service.bytes_response(data=data, media_type=media_type)
@router.get("/tiles/tms/{zoom}/{x}/{y}.png") @router.get("/tiles/tms/{zoom}/{x}/{y}.png")
async def serve_tile_tms(zoom: int, x: int, y: int, tile_service: TileService = Depends(get_tile_service)): async def serve_tile_tms(
zoom: int,
x: int,
y: int,
tile_service: TileService = Depends(get_tile_service),
raster_mbtiles: RasterMBTilesService = Depends(get_raster_mbtiles_service),
):
y_converted = (2**zoom - 1) - y
hit = tile_service._tiles_dir_tile_path(zoom, x, y_converted)
if hit:
return tile_service.get_tile_response(zoom, x, y_converted)
try:
data, media_type = raster_mbtiles.get_tile(z=zoom, x=x, y_xyz=y_converted)
except Exception:
return tile_service.get_tile_response_tms(zoom, x, y) return tile_service.get_tile_response_tms(zoom, x, y)
return tile_service.bytes_response(data=data, media_type=media_type)
@router.post("/tiles/slice") @router.post("/tiles/slice")
@ -53,12 +83,35 @@ async def slice_tiles(payload: TileSliceRequest, tile_service: TileService = Dep
) )
except TileCutError as exc: except TileCutError as exc:
raise HTTPException(status_code=400, detail=str(exc)) raise HTTPException(status_code=400, detail=str(exc))
active_tileset = tileset_name or tileset_svc.get_active()
try:
tileset_root = tileset_svc.resolve(active_tileset)
(tileset_root / "gnss_tileset.json").write_text(
json.dumps(
{
"tileset": active_tileset,
"scheme": payload.scheme,
"source_format": payload.source_format,
"tile_format": payload.tile_format,
"min_zoom": payload.min_zoom,
"max_zoom": payload.max_zoom,
"source": str(source),
"destination": str(destination),
},
ensure_ascii=False,
indent=2,
),
encoding="utf-8",
)
except Exception:
pass
return { return {
"status": "ok", "status": "ok",
"tiles_dir": str(destination), "tiles_dir": str(destination),
"source": str(source), "source": str(source),
"scheme": payload.scheme, "scheme": payload.scheme,
"tileset": tileset_name or tileset_svc.get_active(), "tileset": active_tileset,
} }

View file

@ -1,4 +1,8 @@
from fastapi import APIRouter, Depends, HTTPException import json
import math
from pathlib import Path
from fastapi import APIRouter, Depends, HTTPException, Response
from pydantic import BaseModel from pydantic import BaseModel
from ..dependencies import get_tile_service from ..dependencies import get_tile_service
@ -22,10 +26,185 @@ class ActivateTileset(BaseModel):
name: str name: str
def _is_gzipped(data: bytes) -> bool:
return len(data) >= 2 and data[0] == 0x1F and data[1] == 0x8B
def _detect_kind(path: Path) -> str:
meta = path / "metadata.json"
if meta.is_file():
try:
parsed = json.loads(meta.read_text(encoding="utf-8"))
fmt = str(parsed.get("format") or "").lower()
if fmt in {"pbf", "mvt"}:
return "vector-dir"
except Exception:
pass
if any(path.rglob("*.png")) or any(path.rglob("*.jpg")) or any(path.rglob("*.jpeg")) or any(path.rglob("*.webp")):
return "raster"
if any(path.rglob("*.pbf")):
return "vector-dir"
return "unknown"
def _tile_y_to_lat(y: float, z: int) -> float:
n = math.pi - (2.0 * math.pi * y) / (2**z)
return math.degrees(math.atan(math.sinh(n)))
def _tile_x_to_lon(x: float, z: int) -> float:
return (x / (2**z)) * 360.0 - 180.0
def _scan_xyz_tile_matrix_extent(path: Path, z_value: int) -> tuple[int, int, int, int] | None:
z_dir = path / str(z_value)
if not z_dir.is_dir():
return None
min_x = None
max_x = None
min_y = None
max_y = None
for xdir in z_dir.iterdir():
if not xdir.is_dir():
continue
try:
x = int(xdir.name)
except Exception:
continue
min_x = x if min_x is None else min(min_x, x)
max_x = x if max_x is None else max(max_x, x)
for f in xdir.iterdir():
if not f.is_file():
continue
if f.suffix.lower() not in {".png", ".jpg", ".jpeg", ".webp"}:
continue
try:
y = int(f.stem)
except Exception:
continue
min_y = y if min_y is None else min(min_y, y)
max_y = y if max_y is None else max(max_y, y)
if min_x is None or max_x is None or min_y is None or max_y is None:
return None
return min_x, min_y, max_x, max_y
def _tile_matrix_to_bounds(x_min: int, y_min: int, x_max: int, y_max: int, z: int) -> tuple[float, float, float, float]:
west = _tile_x_to_lon(x_min, z)
east = _tile_x_to_lon(x_max + 1, z)
north = _tile_y_to_lat(y_min, z)
south = _tile_y_to_lat(y_max + 1, z)
return west, south, east, north
def _parse_mapml_extent(path: Path) -> dict | None:
mapml = path / "mapml.mapml"
if not mapml.is_file():
return None
text = mapml.read_text(encoding="utf-8", errors="ignore")
import re
z_tag = re.search(r'<input[^>]*name="z"[^>]*>', text)
x_tag = re.search(r'<input[^>]*name="x"[^>]*>', text)
y_tag = re.search(r'<input[^>]*name="y"[^>]*>', text)
if not z_tag or not x_tag or not y_tag:
return None
def _attrs(tag: str) -> dict[str, str]:
return {m.group(1): m.group(2) for m in re.finditer(r'(\w+)="([^"]+)"', tag)}
z_attrs = _attrs(z_tag.group(0))
x_attrs = _attrs(x_tag.group(0))
y_attrs = _attrs(y_tag.group(0))
try:
z_value = int(z_attrs.get("value", z_attrs.get("max", "0")))
z_min = int(z_attrs.get("min", "0"))
z_max = int(z_attrs.get("max", str(z_value)))
x_min = int(x_attrs["min"])
x_max = int(x_attrs["max"])
y_min = int(y_attrs["min"])
y_max = int(y_attrs["max"])
except Exception:
return None
scanned = _scan_xyz_tile_matrix_extent(path, z_value)
if scanned:
x_min, y_min, x_max, y_max = scanned
west, south, east, north = _tile_matrix_to_bounds(x_min, y_min, x_max, y_max, z_value)
return {
"scheme": "xyz",
"z_value": z_value,
"minzoom": z_min,
"maxzoom": z_max,
"bounds": [west, south, east, north],
"center": [(west + east) / 2.0, (south + north) / 2.0],
"tile_matrix": {"z": z_value, "x_min": x_min, "x_max": x_max, "y_min": y_min, "y_max": y_max},
}
def _parse_leaflet_bounds(path: Path) -> dict | None:
leaflet = path / "leaflet.html"
if not leaflet.is_file():
return None
text = leaflet.read_text(encoding="utf-8", errors="ignore")
import re
m = re.search(
r"map\\.fitBounds\\(\\[\\[([0-9.+-]+),\\s*([0-9.+-]+)\\],\\s*\\[([0-9.+-]+),\\s*([0-9.+-]+)\\]\\]\\)",
text,
)
if not m:
return None
lat1, lon1, lat2, lon2 = map(float, m.groups())
south = min(lat1, lat2)
north = max(lat1, lat2)
west = min(lon1, lon2)
east = max(lon1, lon2)
mzoom = re.search(r"minZoom:\\s*(\\d+)", text)
mxzoom = re.search(r"maxZoom:\\s*(\\d+)", text)
mcenter = re.search(r"center:\\s*\\[([0-9.+-]+),\\s*([0-9.+-]+)\\]", text)
minzoom = int(mzoom.group(1)) if mzoom else None
maxzoom = int(mxzoom.group(1)) if mxzoom else None
center = (
[float(mcenter.group(2)), float(mcenter.group(1))]
if mcenter
else [(west + east) / 2.0, (south + north) / 2.0]
)
return {
"scheme": "xyz",
"bounds": [west, south, east, north],
"center": center,
"minzoom": minzoom,
"maxzoom": maxzoom,
}
@router.get("/tilesets") @router.get("/tilesets")
async def list_tilesets() -> dict: async def list_tilesets() -> dict:
svc = _tileset_service() svc = _tileset_service()
return {"tilesets": svc.list_tilesets(), "active": svc.get_active()} tilesets = svc.list_tilesets()
kinds = {name: _detect_kind(svc.resolve(name)) for name in tilesets}
extents = {}
for name in tilesets:
p = svc.resolve(name)
e = _parse_mapml_extent(p) or _parse_leaflet_bounds(p)
if e:
extents[name] = e
return {"tilesets": tilesets, "active": svc.get_active(), "kinds": kinds, "extents": extents}
@router.get("/tilesets/active/extent")
async def active_tileset_extent() -> dict:
svc = _tileset_service()
tileset_dir = svc.resolve(None)
extent = _parse_mapml_extent(tileset_dir) or _parse_leaflet_bounds(tileset_dir)
if not extent:
raise HTTPException(status_code=404, detail="no extent info for active tileset")
extent["tileset"] = svc.get_active()
return extent
@router.post("/tilesets") @router.post("/tilesets")
@ -47,3 +226,33 @@ async def activate_tileset(payload: ActivateTileset, tile_service: TileService =
raise HTTPException(status_code=400, detail=str(exc)) raise HTTPException(status_code=400, detail=str(exc))
tile_service.set_tiles_dir(svc.resolve(active)) tile_service.set_tiles_dir(svc.resolve(active))
return {"status": "ok", "active": active} return {"status": "ok", "active": active}
@router.get("/tilesets/active/vector/metadata")
async def active_vector_metadata() -> dict:
svc = _tileset_service()
tileset_dir = svc.resolve(None)
meta_path = tileset_dir / "metadata.json"
if not meta_path.is_file():
raise HTTPException(status_code=400, detail="active tileset has no metadata.json (not a MapTiler vector folder)")
try:
parsed = json.loads(meta_path.read_text(encoding="utf-8"))
except Exception as exc:
raise HTTPException(status_code=400, detail=f"failed to parse metadata.json: {exc}")
parsed["tileset"] = svc.get_active()
return parsed
@router.get("/tilesets/active/vector/{z}/{x}/{y}.pbf")
async def active_vector_tile(z: int, x: int, y: int) -> Response:
svc = _tileset_service()
tileset_dir = svc.resolve(None)
tile_path = tileset_dir / str(z) / str(x) / f"{y}.pbf"
if not tile_path.is_file():
raise HTTPException(status_code=404, detail="tile not found")
data = tile_path.read_bytes()
headers = {}
if _is_gzipped(data):
headers["Content-Encoding"] = "gzip"
return Response(content=data, media_type="application/x-protobuf", headers=headers)

View file

@ -3,21 +3,28 @@ from typing import Optional
from ..services.gnss import GNSSService from ..services.gnss import GNSSService
from ..services.logs import LogService from ..services.logs import LogService
from ..services.tiles import TileService from ..services.tiles import TileService
from ..services.raster_mbtiles import RasterMBTilesService
from ..services.vector_mbtiles import VectorMBTilesService from ..services.vector_mbtiles import VectorMBTilesService
_gnss_service: Optional[GNSSService] = None _gnss_service: Optional[GNSSService] = None
_tile_service: Optional[TileService] = None _tile_service: Optional[TileService] = None
_log_service: Optional[LogService] = None _log_service: Optional[LogService] = None
_raster_mbtiles_service: Optional[RasterMBTilesService] = None
_vector_mbtiles_service: Optional[VectorMBTilesService] = None _vector_mbtiles_service: Optional[VectorMBTilesService] = None
def set_services( def set_services(
gnss: GNSSService, tiles: TileService, logger: LogService, vector_mbtiles: VectorMBTilesService gnss: GNSSService,
tiles: TileService,
logger: LogService,
raster_mbtiles: RasterMBTilesService,
vector_mbtiles: VectorMBTilesService,
) -> None: ) -> None:
global _gnss_service, _tile_service, _log_service, _vector_mbtiles_service global _gnss_service, _tile_service, _log_service, _raster_mbtiles_service, _vector_mbtiles_service
_gnss_service = gnss _gnss_service = gnss
_tile_service = tiles _tile_service = tiles
_log_service = logger _log_service = logger
_raster_mbtiles_service = raster_mbtiles
_vector_mbtiles_service = vector_mbtiles _vector_mbtiles_service = vector_mbtiles
@ -43,3 +50,9 @@ def get_vector_mbtiles_service() -> VectorMBTilesService:
if _vector_mbtiles_service is None: if _vector_mbtiles_service is None:
raise RuntimeError("Vector MBTiles service is not initialized") raise RuntimeError("Vector MBTiles service is not initialized")
return _vector_mbtiles_service return _vector_mbtiles_service
def get_raster_mbtiles_service() -> RasterMBTilesService:
if _raster_mbtiles_service is None:
raise RuntimeError("Raster MBTiles service is not initialized")
return _raster_mbtiles_service

View file

@ -1,16 +1,28 @@
import os
import sys
from pathlib import Path from pathlib import Path
BASE_DIR = Path(__file__).parent BASE_DIR = Path(__file__).parent
TILES_DIR = BASE_DIR / "tiles"
TILESSETS_DIR = BASE_DIR / "tilesets"
def _default_data_dir() -> Path:
if getattr(sys, "frozen", False):
return Path.home() / ".local" / "share" / "gnss"
return BASE_DIR
DATA_DIR = Path(os.getenv("GNSS_DATA_DIR", str(_default_data_dir()))).expanduser()
TILES_DIR = DATA_DIR / "tiles"
TILESSETS_DIR = DATA_DIR / "tilesets"
DEFAULT_TILESET = "default" DEFAULT_TILESET = "default"
TILE_PLACEHOLDER = BASE_DIR / "tiles" / "placeholder.png" TILE_PLACEHOLDER = TILES_DIR / "placeholder.png"
UPLOAD_DIR = BASE_DIR / "uploads" UPLOAD_DIR = DATA_DIR / "uploads"
GNSS_SOURCE = "simulated" GNSS_SOURCE = "simulated"
GNSS_PORT = "/dev/ttyAMA0" GNSS_PORT = "/dev/ttyAMA0"
GNSS_BAUD = 9600 GNSS_BAUD = 9600
GNSS_TIMEOUT = 1.0 GNSS_TIMEOUT = 1.0
SIM_START_POINT = (55.300, 61.350) SIM_START_POINT = (55.1644, 61.4368)
SIM_PATH = "circle" # circle | rectangle SIM_PATH = "circle" # circle | rectangle
# Для симуляции движения по прямоугольнику раскомментируй строку ниже: # Для симуляции движения по прямоугольнику раскомментируй строку ниже:
# SIM_PATH = "rectangle" # SIM_PATH = "rectangle"

View file

@ -6,10 +6,11 @@ from fastapi.middleware.cors import CORSMiddleware
from . import config from . import config
from .api import state from .api import state
from .api.routes import config_route, health, logs, tiles, tilesets, vector_tiles, ws from .api.routes import config_route, health, logs, maps, raster_mbtiles, tiles, tilesets, vector_tiles, ws
from .services.gnss import GNSSService, RectangularPathSource, SerialNMEASource, SimulatedPathSource from .services.gnss import GNSSService, RectangularPathSource, SerialNMEASource, SimulatedPathSource
from .services.logs import LogService from .services.logs import LogService
from .services.tiles import TileService from .services.tiles import TileService
from .services.raster_mbtiles import RasterMBTilesService
from .services.vector_mbtiles import VectorMBTilesService from .services.vector_mbtiles import VectorMBTilesService
from .services.tilesets import TilesetService from .services.tilesets import TilesetService
@ -52,9 +53,16 @@ tiles_dir = tileset_service.resolve(None)
placeholder_tile = Path(config.TILE_PLACEHOLDER) placeholder_tile = Path(config.TILE_PLACEHOLDER)
tile_service = TileService(tiles_dir=tiles_dir, placeholder=placeholder_tile, upload_dir=Path(config.UPLOAD_DIR)) tile_service = TileService(tiles_dir=tiles_dir, placeholder=placeholder_tile, upload_dir=Path(config.UPLOAD_DIR))
log_service = LogService(base_dir=Path("logs")) log_service = LogService(base_dir=Path("logs"))
raster_mbtiles_service = RasterMBTilesService()
vector_mbtiles_service = VectorMBTilesService() vector_mbtiles_service = VectorMBTilesService()
gnss_service = create_gnss_service() gnss_service = create_gnss_service()
state.set_services(gnss=gnss_service, tiles=tile_service, logger=log_service, vector_mbtiles=vector_mbtiles_service) state.set_services(
gnss=gnss_service,
tiles=tile_service,
logger=log_service,
raster_mbtiles=raster_mbtiles_service,
vector_mbtiles=vector_mbtiles_service,
)
app = FastAPI(title="GNSS Backend", version="0.2.0") app = FastAPI(title="GNSS Backend", version="0.2.0")
@ -76,6 +84,8 @@ app.include_router(config_route.router)
app.include_router(tiles.router) app.include_router(tiles.router)
app.include_router(ws.router) app.include_router(ws.router)
app.include_router(logs.router) app.include_router(logs.router)
app.include_router(maps.router)
app.include_router(raster_mbtiles.router)
app.include_router(vector_tiles.router) app.include_router(vector_tiles.router)
app.include_router(tilesets.router) app.include_router(tilesets.router)

View file

@ -0,0 +1,121 @@
import sqlite3
import threading
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Optional
class RasterMBTilesError(RuntimeError):
pass
@dataclass(frozen=True)
class RasterMBTilesInfo:
path: Path
metadata: dict[str, Any]
def _read_metadata(conn: sqlite3.Connection) -> dict[str, str]:
cur = conn.execute("SELECT name, value FROM metadata")
return {name: value for name, value in cur.fetchall()}
def _has_table(conn: sqlite3.Connection, table: str) -> bool:
cur = conn.execute(
"SELECT 1 FROM sqlite_master WHERE type='table' AND name=? LIMIT 1",
(table,),
)
return cur.fetchone() is not None
def _format_to_media_type(fmt: str) -> str:
f = (fmt or "").lower()
if f in {"jpg", "jpeg"}:
return "image/jpeg"
if f == "png":
return "image/png"
if f == "webp":
return "image/webp"
return "application/octet-stream"
class RasterMBTilesService:
def __init__(self):
self._lock = threading.Lock()
self._active: Optional[RasterMBTilesInfo] = None
def activate(self, path: Path) -> RasterMBTilesInfo:
p = Path(path).expanduser().resolve()
if not p.exists():
raise RasterMBTilesError(f"mbtiles not found: {p}")
if p.suffix.lower() != ".mbtiles":
raise RasterMBTilesError("expected .mbtiles file")
conn = sqlite3.connect(f"file:{p}?mode=ro", uri=True)
try:
metadata = _read_metadata(conn)
finally:
conn.close()
fmt = (metadata.get("format") or "").lower()
if fmt in {"pbf", "mvt"}:
raise RasterMBTilesError("this is a vector mbtiles; use /tiles/vector/activate")
if fmt not in {"png", "jpg", "jpeg", "webp"}:
raise RasterMBTilesError(f"mbtiles format is '{fmt or 'unknown'}'; expected raster png/jpg/webp")
info = RasterMBTilesInfo(path=p, metadata=metadata)
with self._lock:
self._active = info
return info
def get_active(self) -> RasterMBTilesInfo:
with self._lock:
if self._active is None:
raise RasterMBTilesError("no active raster mbtiles; upload/activate first")
return self._active
def get_metadata(self) -> dict[str, Any]:
info = self.get_active()
meta = dict(info.metadata)
meta["path"] = str(info.path)
meta["media_type"] = _format_to_media_type(str(info.metadata.get("format") or ""))
return meta
def get_tile(self, z: int, x: int, y_xyz: int) -> tuple[bytes, str]:
info = self.get_active()
if z < 0:
raise RasterMBTilesError("invalid z")
max_index = (2**z) - 1
if x < 0 or x > max_index or y_xyz < 0 or y_xyz > max_index:
raise RasterMBTilesError("invalid x/y for z")
y_tms = max_index - y_xyz
conn = sqlite3.connect(f"file:{info.path}?mode=ro", uri=True)
try:
if _has_table(conn, "tiles"):
cur = conn.execute(
"SELECT tile_data FROM tiles WHERE zoom_level=? AND tile_column=? AND tile_row=? LIMIT 1",
(z, x, y_tms),
)
row = cur.fetchone()
if row is None:
raise KeyError("tile not found")
data = row[0]
elif _has_table(conn, "map") and _has_table(conn, "images"):
cur = conn.execute(
"SELECT images.tile_data FROM map JOIN images ON map.tile_id = images.tile_id "
"WHERE map.zoom_level=? AND map.tile_column=? AND map.tile_row=? LIMIT 1",
(z, x, y_tms),
)
row = cur.fetchone()
if row is None:
raise KeyError("tile not found")
data = row[0]
else:
raise RasterMBTilesError("unsupported mbtiles schema (no tiles table and no map/images tables)")
finally:
conn.close()
fmt = str(info.metadata.get("format") or "").lower()
return data, _format_to_media_type(fmt)

View file

@ -79,14 +79,31 @@ class TileService:
raise TileCutError(f"gdal2tiles failed: {result.stderr or result.stdout}") raise TileCutError(f"gdal2tiles failed: {result.stderr or result.stdout}")
return destination return destination
def _tiles_dir_tile_path(self, zoom: int, x: int, y: int) -> tuple[Path, str] | None:
base = self.tiles_dir / str(zoom) / str(x) / f"{y}"
for ext, media_type in (
(".png", "image/png"),
(".jpg", "image/jpeg"),
(".jpeg", "image/jpeg"),
(".webp", "image/webp"),
):
candidate = base.with_suffix(ext)
if candidate.is_file():
return candidate, media_type
return None
def get_tile_response(self, zoom: int, x: int, y: int): def get_tile_response(self, zoom: int, x: int, y: int):
tile_path = self.tiles_dir / str(zoom) / str(x) / f"{y}.png" hit = self._tiles_dir_tile_path(zoom, x, y)
if tile_path.is_file(): if hit:
return FileResponse(tile_path, media_type="image/png") tile_path, media_type = hit
return FileResponse(tile_path, media_type=media_type)
if self.placeholder.is_file(): if self.placeholder.is_file():
return FileResponse(self.placeholder, media_type="image/png") return FileResponse(self.placeholder, media_type="image/png")
return Response(content=DEFAULT_PLACEHOLDER_PNG, media_type="image/png") return Response(content=DEFAULT_PLACEHOLDER_PNG, media_type="image/png")
def bytes_response(self, data: bytes, media_type: str):
return Response(content=data, media_type=media_type)
def get_tile_response_tms(self, zoom: int, x: int, y: int): def get_tile_response_tms(self, zoom: int, x: int, y: int):
y_converted = (2**zoom - 1) - y y_converted = (2**zoom - 1) - y
return self.get_tile_response(zoom, x, y_converted) return self.get_tile_response(zoom, x, y_converted)

149
poetry.lock generated
View file

@ -194,6 +194,43 @@ files = [
[package.extras] [package.extras]
all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"]
[[package]]
name = "nuitka"
version = "2.8.9"
description = "Python compiler with full language support and CPython compatibility"
optional = false
python-versions = "*"
groups = ["dev"]
files = [
{file = "nuitka-2.8.9.tar.gz", hash = "sha256:b178cd437f2110c46943b368db51d20d57d586a13f8f6323ab1be4e51e2fabf8"},
]
[package.dependencies]
ordered-set = ">=4.1.0"
zstandard = ">=0.15"
[package.extras]
all = ["imageio", "setuptools (>=42)", "toml", "zstandard (>=0.15)"]
app = ["zstandard (>=0.15)"]
build-wheel = ["setuptools (>=42)", "toml", "wheel"]
icon-conversion = ["imageio"]
onefile = ["zstandard (>=0.15)"]
[[package]]
name = "ordered-set"
version = "4.1.0"
description = "An OrderedSet is a custom MutableSet that remembers its order, so that every"
optional = false
python-versions = ">=3.7"
groups = ["dev"]
files = [
{file = "ordered-set-4.1.0.tar.gz", hash = "sha256:694a8e44c87657c59292ede72891eb91d34131f6531463aab3009191c77364a8"},
{file = "ordered_set-4.1.0-py3-none-any.whl", hash = "sha256:046e1132c71fcf3330438a539928932caf51ddbc582496833e23de611de14562"},
]
[package.extras]
dev = ["black", "mypy", "pytest"]
[[package]] [[package]]
name = "pydantic" name = "pydantic"
version = "2.12.5" version = "2.12.5"
@ -852,6 +889,118 @@ files = [
{file = "websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee"}, {file = "websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee"},
] ]
[[package]]
name = "zstandard"
version = "0.25.0"
description = "Zstandard bindings for Python"
optional = false
python-versions = ">=3.9"
groups = ["dev"]
files = [
{file = "zstandard-0.25.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e59fdc271772f6686e01e1b3b74537259800f57e24280be3f29c8a0deb1904dd"},
{file = "zstandard-0.25.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4d441506e9b372386a5271c64125f72d5df6d2a8e8a2a45a0ae09b03cb781ef7"},
{file = "zstandard-0.25.0-cp310-cp310-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:ab85470ab54c2cb96e176f40342d9ed41e58ca5733be6a893b730e7af9c40550"},
{file = "zstandard-0.25.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e05ab82ea7753354bb054b92e2f288afb750e6b439ff6ca78af52939ebbc476d"},
{file = "zstandard-0.25.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:78228d8a6a1c177a96b94f7e2e8d012c55f9c760761980da16ae7546a15a8e9b"},
{file = "zstandard-0.25.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:2b6bd67528ee8b5c5f10255735abc21aa106931f0dbaf297c7be0c886353c3d0"},
{file = "zstandard-0.25.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4b6d83057e713ff235a12e73916b6d356e3084fd3d14ced499d84240f3eecee0"},
{file = "zstandard-0.25.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9174f4ed06f790a6869b41cba05b43eeb9a35f8993c4422ab853b705e8112bbd"},
{file = "zstandard-0.25.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:25f8f3cd45087d089aef5ba3848cd9efe3ad41163d3400862fb42f81a3a46701"},
{file = "zstandard-0.25.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3756b3e9da9b83da1796f8809dd57cb024f838b9eeafde28f3cb472012797ac1"},
{file = "zstandard-0.25.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:81dad8d145d8fd981b2962b686b2241d3a1ea07733e76a2f15435dfb7fb60150"},
{file = "zstandard-0.25.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:a5a419712cf88862a45a23def0ae063686db3d324cec7edbe40509d1a79a0aab"},
{file = "zstandard-0.25.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:e7360eae90809efd19b886e59a09dad07da4ca9ba096752e61a2e03c8aca188e"},
{file = "zstandard-0.25.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:75ffc32a569fb049499e63ce68c743155477610532da1eb38e7f24bf7cd29e74"},
{file = "zstandard-0.25.0-cp310-cp310-win32.whl", hash = "sha256:106281ae350e494f4ac8a80470e66d1fe27e497052c8d9c3b95dc4cf1ade81aa"},
{file = "zstandard-0.25.0-cp310-cp310-win_amd64.whl", hash = "sha256:ea9d54cc3d8064260114a0bbf3479fc4a98b21dffc89b3459edd506b69262f6e"},
{file = "zstandard-0.25.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:933b65d7680ea337180733cf9e87293cc5500cc0eb3fc8769f4d3c88d724ec5c"},
{file = "zstandard-0.25.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3f79487c687b1fc69f19e487cd949bf3aae653d181dfb5fde3bf6d18894706f"},
{file = "zstandard-0.25.0-cp311-cp311-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:0bbc9a0c65ce0eea3c34a691e3c4b6889f5f3909ba4822ab385fab9057099431"},
{file = "zstandard-0.25.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:01582723b3ccd6939ab7b3a78622c573799d5d8737b534b86d0e06ac18dbde4a"},
{file = "zstandard-0.25.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5f1ad7bf88535edcf30038f6919abe087f606f62c00a87d7e33e7fc57cb69fcc"},
{file = "zstandard-0.25.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:06acb75eebeedb77b69048031282737717a63e71e4ae3f77cc0c3b9508320df6"},
{file = "zstandard-0.25.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9300d02ea7c6506f00e627e287e0492a5eb0371ec1670ae852fefffa6164b072"},
{file = "zstandard-0.25.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:bfd06b1c5584b657a2892a6014c2f4c20e0db0208c159148fa78c65f7e0b0277"},
{file = "zstandard-0.25.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f373da2c1757bb7f1acaf09369cdc1d51d84131e50d5fa9863982fd626466313"},
{file = "zstandard-0.25.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6c0e5a65158a7946e7a7affa6418878ef97ab66636f13353b8502d7ea03c8097"},
{file = "zstandard-0.25.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c8e167d5adf59476fa3e37bee730890e389410c354771a62e3c076c86f9f7778"},
{file = "zstandard-0.25.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:98750a309eb2f020da61e727de7d7ba3c57c97cf6213f6f6277bb7fb42a8e065"},
{file = "zstandard-0.25.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:22a086cff1b6ceca18a8dd6096ec631e430e93a8e70a9ca5efa7561a00f826fa"},
{file = "zstandard-0.25.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:72d35d7aa0bba323965da807a462b0966c91608ef3a48ba761678cb20ce5d8b7"},
{file = "zstandard-0.25.0-cp311-cp311-win32.whl", hash = "sha256:f5aeea11ded7320a84dcdd62a3d95b5186834224a9e55b92ccae35d21a8b63d4"},
{file = "zstandard-0.25.0-cp311-cp311-win_amd64.whl", hash = "sha256:daab68faadb847063d0c56f361a289c4f268706b598afbf9ad113cbe5c38b6b2"},
{file = "zstandard-0.25.0-cp311-cp311-win_arm64.whl", hash = "sha256:22a06c5df3751bb7dc67406f5374734ccee8ed37fc5981bf1ad7041831fa1137"},
{file = "zstandard-0.25.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7b3c3a3ab9daa3eed242d6ecceead93aebbb8f5f84318d82cee643e019c4b73b"},
{file = "zstandard-0.25.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:913cbd31a400febff93b564a23e17c3ed2d56c064006f54efec210d586171c00"},
{file = "zstandard-0.25.0-cp312-cp312-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:011d388c76b11a0c165374ce660ce2c8efa8e5d87f34996aa80f9c0816698b64"},
{file = "zstandard-0.25.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6dffecc361d079bb48d7caef5d673c88c8988d3d33fb74ab95b7ee6da42652ea"},
{file = "zstandard-0.25.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:7149623bba7fdf7e7f24312953bcf73cae103db8cae49f8154dd1eadc8a29ecb"},
{file = "zstandard-0.25.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:6a573a35693e03cf1d67799fd01b50ff578515a8aeadd4595d2a7fa9f3ec002a"},
{file = "zstandard-0.25.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5a56ba0db2d244117ed744dfa8f6f5b366e14148e00de44723413b2f3938a902"},
{file = "zstandard-0.25.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:10ef2a79ab8e2974e2075fb984e5b9806c64134810fac21576f0668e7ea19f8f"},
{file = "zstandard-0.25.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:aaf21ba8fb76d102b696781bddaa0954b782536446083ae3fdaa6f16b25a1c4b"},
{file = "zstandard-0.25.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1869da9571d5e94a85a5e8d57e4e8807b175c9e4a6294e3b66fa4efb074d90f6"},
{file = "zstandard-0.25.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:809c5bcb2c67cd0ed81e9229d227d4ca28f82d0f778fc5fea624a9def3963f91"},
{file = "zstandard-0.25.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f27662e4f7dbf9f9c12391cb37b4c4c3cb90ffbd3b1fb9284dadbbb8935fa708"},
{file = "zstandard-0.25.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:99c0c846e6e61718715a3c9437ccc625de26593fea60189567f0118dc9db7512"},
{file = "zstandard-0.25.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:474d2596a2dbc241a556e965fb76002c1ce655445e4e3bf38e5477d413165ffa"},
{file = "zstandard-0.25.0-cp312-cp312-win32.whl", hash = "sha256:23ebc8f17a03133b4426bcc04aabd68f8236eb78c3760f12783385171b0fd8bd"},
{file = "zstandard-0.25.0-cp312-cp312-win_amd64.whl", hash = "sha256:ffef5a74088f1e09947aecf91011136665152e0b4b359c42be3373897fb39b01"},
{file = "zstandard-0.25.0-cp312-cp312-win_arm64.whl", hash = "sha256:181eb40e0b6a29b3cd2849f825e0fa34397f649170673d385f3598ae17cca2e9"},
{file = "zstandard-0.25.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ec996f12524f88e151c339688c3897194821d7f03081ab35d31d1e12ec975e94"},
{file = "zstandard-0.25.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a1a4ae2dec3993a32247995bdfe367fc3266da832d82f8438c8570f989753de1"},
{file = "zstandard-0.25.0-cp313-cp313-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:e96594a5537722fdfb79951672a2a63aec5ebfb823e7560586f7484819f2a08f"},
{file = "zstandard-0.25.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bfc4e20784722098822e3eee42b8e576b379ed72cca4a7cb856ae733e62192ea"},
{file = "zstandard-0.25.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:457ed498fc58cdc12fc48f7950e02740d4f7ae9493dd4ab2168a47c93c31298e"},
{file = "zstandard-0.25.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:fd7a5004eb1980d3cefe26b2685bcb0b17989901a70a1040d1ac86f1d898c551"},
{file = "zstandard-0.25.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8e735494da3db08694d26480f1493ad2cf86e99bdd53e8e9771b2752a5c0246a"},
{file = "zstandard-0.25.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3a39c94ad7866160a4a46d772e43311a743c316942037671beb264e395bdd611"},
{file = "zstandard-0.25.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:172de1f06947577d3a3005416977cce6168f2261284c02080e7ad0185faeced3"},
{file = "zstandard-0.25.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3c83b0188c852a47cd13ef3bf9209fb0a77fa5374958b8c53aaa699398c6bd7b"},
{file = "zstandard-0.25.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1673b7199bbe763365b81a4f3252b8e80f44c9e323fc42940dc8843bfeaf9851"},
{file = "zstandard-0.25.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:0be7622c37c183406f3dbf0cba104118eb16a4ea7359eeb5752f0794882fc250"},
{file = "zstandard-0.25.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:5f5e4c2a23ca271c218ac025bd7d635597048b366d6f31f420aaeb715239fc98"},
{file = "zstandard-0.25.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f187a0bb61b35119d1926aee039524d1f93aaf38a9916b8c4b78ac8514a0aaf"},
{file = "zstandard-0.25.0-cp313-cp313-win32.whl", hash = "sha256:7030defa83eef3e51ff26f0b7bfb229f0204b66fe18e04359ce3474ac33cbc09"},
{file = "zstandard-0.25.0-cp313-cp313-win_amd64.whl", hash = "sha256:1f830a0dac88719af0ae43b8b2d6aef487d437036468ef3c2ea59c51f9d55fd5"},
{file = "zstandard-0.25.0-cp313-cp313-win_arm64.whl", hash = "sha256:85304a43f4d513f5464ceb938aa02c1e78c2943b29f44a750b48b25ac999a049"},
{file = "zstandard-0.25.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e29f0cf06974c899b2c188ef7f783607dbef36da4c242eb6c82dcd8b512855e3"},
{file = "zstandard-0.25.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:05df5136bc5a011f33cd25bc9f506e7426c0c9b3f9954f056831ce68f3b6689f"},
{file = "zstandard-0.25.0-cp314-cp314-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:f604efd28f239cc21b3adb53eb061e2a205dc164be408e553b41ba2ffe0ca15c"},
{file = "zstandard-0.25.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223415140608d0f0da010499eaa8ccdb9af210a543fac54bce15babbcfc78439"},
{file = "zstandard-0.25.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2e54296a283f3ab5a26fc9b8b5d4978ea0532f37b231644f367aa588930aa043"},
{file = "zstandard-0.25.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ca54090275939dc8ec5dea2d2afb400e0f83444b2fc24e07df7fdef677110859"},
{file = "zstandard-0.25.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e09bb6252b6476d8d56100e8147b803befa9a12cea144bbe629dd508800d1ad0"},
{file = "zstandard-0.25.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a9ec8c642d1ec73287ae3e726792dd86c96f5681eb8df274a757bf62b750eae7"},
{file = "zstandard-0.25.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a4089a10e598eae6393756b036e0f419e8c1d60f44a831520f9af41c14216cf2"},
{file = "zstandard-0.25.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f67e8f1a324a900e75b5e28ffb152bcac9fbed1cc7b43f99cd90f395c4375344"},
{file = "zstandard-0.25.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:9654dbc012d8b06fc3d19cc825af3f7bf8ae242226df5f83936cb39f5fdc846c"},
{file = "zstandard-0.25.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4203ce3b31aec23012d3a4cf4a2ed64d12fea5269c49aed5e4c3611b938e4088"},
{file = "zstandard-0.25.0-cp314-cp314-win32.whl", hash = "sha256:da469dc041701583e34de852d8634703550348d5822e66a0c827d39b05365b12"},
{file = "zstandard-0.25.0-cp314-cp314-win_amd64.whl", hash = "sha256:c19bcdd826e95671065f8692b5a4aa95c52dc7a02a4c5a0cac46deb879a017a2"},
{file = "zstandard-0.25.0-cp314-cp314-win_arm64.whl", hash = "sha256:d7541afd73985c630bafcd6338d2518ae96060075f9463d7dc14cfb33514383d"},
{file = "zstandard-0.25.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b9af1fe743828123e12b41dd8091eca1074d0c1569cc42e6e1eee98027f2bbd0"},
{file = "zstandard-0.25.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4b14abacf83dfb5c25eb4e4a79520de9e7e205f72c9ee7702f91233ae57d33a2"},
{file = "zstandard-0.25.0-cp39-cp39-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:a51ff14f8017338e2f2e5dab738ce1ec3b5a851f23b18c1ae1359b1eecbee6df"},
{file = "zstandard-0.25.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3b870ce5a02d4b22286cf4944c628e0f0881b11b3f14667c1d62185a99e04f53"},
{file = "zstandard-0.25.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:05353cef599a7b0b98baca9b068dd36810c3ef0f42bf282583f438caf6ddcee3"},
{file = "zstandard-0.25.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:19796b39075201d51d5f5f790bf849221e58b48a39a5fc74837675d8bafc7362"},
{file = "zstandard-0.25.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:53e08b2445a6bc241261fea89d065536f00a581f02535f8122eba42db9375530"},
{file = "zstandard-0.25.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:1f3689581a72eaba9131b1d9bdbfe520ccd169999219b41000ede2fca5c1bfdb"},
{file = "zstandard-0.25.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d8c56bb4e6c795fc77d74d8e8b80846e1fb8292fc0b5060cd8131d522974b751"},
{file = "zstandard-0.25.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:53f94448fe5b10ee75d246497168e5825135d54325458c4bfffbaafabcc0a577"},
{file = "zstandard-0.25.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:c2ba942c94e0691467ab901fc51b6f2085ff48f2eea77b1a48240f011e8247c7"},
{file = "zstandard-0.25.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:07b527a69c1e1c8b5ab1ab14e2afe0675614a09182213f21a0717b62027b5936"},
{file = "zstandard-0.25.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:51526324f1b23229001eb3735bc8c94f9c578b1bd9e867a0a646a3b17109f388"},
{file = "zstandard-0.25.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:89c4b48479a43f820b749df49cd7ba2dbc2b1b78560ecb5ab52985574fd40b27"},
{file = "zstandard-0.25.0-cp39-cp39-win32.whl", hash = "sha256:1cd5da4d8e8ee0e88be976c294db744773459d51bb32f707a0f166e5ad5c8649"},
{file = "zstandard-0.25.0-cp39-cp39-win_amd64.whl", hash = "sha256:37daddd452c0ffb65da00620afb8e17abd4adaae6ce6310702841760c2c26860"},
{file = "zstandard-0.25.0.tar.gz", hash = "sha256:7713e1179d162cf5c7906da876ec2ccb9c3a9dcbdffef0cc7f70c3667a205f0b"},
]
[package.extras]
cffi = ["cffi (>=1.17,<2.0) ; platform_python_implementation != \"PyPy\" and python_version < \"3.14\"", "cffi (>=2.0.0b) ; platform_python_implementation != \"PyPy\" and python_version >= \"3.14\""]
[metadata] [metadata]
lock-version = "2.1" lock-version = "2.1"
python-versions = ">=3.10,<4.0" python-versions = ">=3.10,<4.0"

View file

@ -16,6 +16,14 @@ uvicorn = {extras = ["standard"], version = ">=0.30.0,<0.31.0"}
python-multipart = ">=0.0.21,<0.0.22" python-multipart = ">=0.0.21,<0.0.22"
gdal2tiles = "^0.1.9" gdal2tiles = "^0.1.9"
[tool.poetry.scripts]
gnss-backend = "gnss.__main__:main"
[build-system] [build-system]
requires = ["poetry-core>=2.0.0,<3.0.0"] requires = ["poetry-core>=2.0.0,<3.0.0"]
build-backend = "poetry.core.masonry.api" build-backend = "poetry.core.masonry.api"
[dependency-groups]
dev = [
"nuitka (>=2.8.9,<3.0.0)"
]