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
46
Docs/nuitka.md
Normal 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>/...`
|
||||
|
|
@ -218,6 +218,14 @@ body, #root {
|
|||
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 {
|
||||
filter: none;
|
||||
}
|
||||
|
|
|
|||
1133
frontend/src/App.tsx
1133
frontend/src/App.tsx
File diff suppressed because it is too large
Load diff
34
gnss/__main__.py
Normal file
34
gnss/__main__.py
Normal 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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -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",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ async def get_config() -> dict:
|
|||
"tiles_url_tms": "/tiles/tms/{z}/{x}/{y}.png",
|
||||
"vector_tiles_url": "/tiles/vector/{z}/{x}/{y}.pbf",
|
||||
"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_activate_url": "/tilesets/activate",
|
||||
"ws_url": "/ws/track",
|
||||
|
|
|
|||
61
gnss/api/routes/maps.py
Normal file
61
gnss/api/routes/maps.py
Normal 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")
|
||||
|
||||
51
gnss/api/routes/raster_mbtiles.py
Normal file
51
gnss/api/routes/raster_mbtiles.py
Normal 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)
|
||||
|
||||
|
|
@ -3,23 +3,53 @@ from pathlib import Path
|
|||
from fastapi import APIRouter, Depends, HTTPException, UploadFile
|
||||
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 ...services.tiles import TileCutError, TileService
|
||||
from ... import config
|
||||
from ...services.tilesets import TilesetService, TilesetError
|
||||
from ...services.raster_mbtiles import RasterMBTilesService
|
||||
|
||||
router = APIRouter(tags=["tiles"])
|
||||
|
||||
|
||||
@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)
|
||||
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")
|
||||
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.bytes_response(data=data, media_type=media_type)
|
||||
|
||||
|
||||
@router.post("/tiles/slice")
|
||||
|
|
@ -53,12 +83,35 @@ async def slice_tiles(payload: TileSliceRequest, tile_service: TileService = Dep
|
|||
)
|
||||
except TileCutError as 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 {
|
||||
"status": "ok",
|
||||
"tiles_dir": str(destination),
|
||||
"source": str(source),
|
||||
"scheme": payload.scheme,
|
||||
"tileset": tileset_name or tileset_svc.get_active(),
|
||||
"tileset": active_tileset,
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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 ..dependencies import get_tile_service
|
||||
|
|
@ -22,10 +26,185 @@ class ActivateTileset(BaseModel):
|
|||
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")
|
||||
async def list_tilesets() -> dict:
|
||||
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")
|
||||
|
|
@ -47,3 +226,33 @@ async def activate_tileset(payload: ActivateTileset, tile_service: TileService =
|
|||
raise HTTPException(status_code=400, detail=str(exc))
|
||||
tile_service.set_tiles_dir(svc.resolve(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)
|
||||
|
||||
|
|
|
|||
|
|
@ -3,21 +3,28 @@ from typing import Optional
|
|||
from ..services.gnss import GNSSService
|
||||
from ..services.logs import LogService
|
||||
from ..services.tiles import TileService
|
||||
from ..services.raster_mbtiles import RasterMBTilesService
|
||||
from ..services.vector_mbtiles import VectorMBTilesService
|
||||
|
||||
_gnss_service: Optional[GNSSService] = None
|
||||
_tile_service: Optional[TileService] = None
|
||||
_log_service: Optional[LogService] = None
|
||||
_raster_mbtiles_service: Optional[RasterMBTilesService] = None
|
||||
_vector_mbtiles_service: Optional[VectorMBTilesService] = None
|
||||
|
||||
|
||||
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:
|
||||
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
|
||||
_tile_service = tiles
|
||||
_log_service = logger
|
||||
_raster_mbtiles_service = raster_mbtiles
|
||||
_vector_mbtiles_service = vector_mbtiles
|
||||
|
||||
|
||||
|
|
@ -43,3 +50,9 @@ def get_vector_mbtiles_service() -> VectorMBTilesService:
|
|||
if _vector_mbtiles_service is None:
|
||||
raise RuntimeError("Vector MBTiles service is not initialized")
|
||||
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
|
||||
|
|
|
|||
|
|
@ -1,16 +1,28 @@
|
|||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
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"
|
||||
TILE_PLACEHOLDER = BASE_DIR / "tiles" / "placeholder.png"
|
||||
UPLOAD_DIR = BASE_DIR / "uploads"
|
||||
TILE_PLACEHOLDER = TILES_DIR / "placeholder.png"
|
||||
UPLOAD_DIR = DATA_DIR / "uploads"
|
||||
GNSS_SOURCE = "simulated"
|
||||
GNSS_PORT = "/dev/ttyAMA0"
|
||||
GNSS_BAUD = 9600
|
||||
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 = "rectangle"
|
||||
|
|
|
|||
14
gnss/main.py
14
gnss/main.py
|
|
@ -6,10 +6,11 @@ from fastapi.middleware.cors import CORSMiddleware
|
|||
|
||||
from . import config
|
||||
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.logs import LogService
|
||||
from .services.tiles import TileService
|
||||
from .services.raster_mbtiles import RasterMBTilesService
|
||||
from .services.vector_mbtiles import VectorMBTilesService
|
||||
from .services.tilesets import TilesetService
|
||||
|
||||
|
|
@ -52,9 +53,16 @@ tiles_dir = tileset_service.resolve(None)
|
|||
placeholder_tile = Path(config.TILE_PLACEHOLDER)
|
||||
tile_service = TileService(tiles_dir=tiles_dir, placeholder=placeholder_tile, upload_dir=Path(config.UPLOAD_DIR))
|
||||
log_service = LogService(base_dir=Path("logs"))
|
||||
raster_mbtiles_service = RasterMBTilesService()
|
||||
vector_mbtiles_service = VectorMBTilesService()
|
||||
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")
|
||||
|
||||
|
|
@ -76,6 +84,8 @@ app.include_router(config_route.router)
|
|||
app.include_router(tiles.router)
|
||||
app.include_router(ws.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(tilesets.router)
|
||||
|
||||
|
|
|
|||
121
gnss/services/raster_mbtiles.py
Normal file
121
gnss/services/raster_mbtiles.py
Normal 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)
|
||||
|
||||
|
|
@ -79,14 +79,31 @@ class TileService:
|
|||
raise TileCutError(f"gdal2tiles failed: {result.stderr or result.stdout}")
|
||||
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):
|
||||
tile_path = self.tiles_dir / str(zoom) / str(x) / f"{y}.png"
|
||||
if tile_path.is_file():
|
||||
return FileResponse(tile_path, media_type="image/png")
|
||||
hit = self._tiles_dir_tile_path(zoom, x, y)
|
||||
if hit:
|
||||
tile_path, media_type = hit
|
||||
return FileResponse(tile_path, media_type=media_type)
|
||||
if self.placeholder.is_file():
|
||||
return FileResponse(self.placeholder, 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):
|
||||
y_converted = (2**zoom - 1) - y
|
||||
return self.get_tile_response(zoom, x, y_converted)
|
||||
|
|
|
|||
149
poetry.lock
generated
149
poetry.lock
generated
|
|
@ -194,6 +194,43 @@ files = [
|
|||
[package.extras]
|
||||
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]]
|
||||
name = "pydantic"
|
||||
version = "2.12.5"
|
||||
|
|
@ -852,6 +889,118 @@ files = [
|
|||
{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]
|
||||
lock-version = "2.1"
|
||||
python-versions = ">=3.10,<4.0"
|
||||
|
|
|
|||
|
|
@ -16,6 +16,14 @@ uvicorn = {extras = ["standard"], version = ">=0.30.0,<0.31.0"}
|
|||
python-multipart = ">=0.0.21,<0.0.22"
|
||||
gdal2tiles = "^0.1.9"
|
||||
|
||||
[tool.poetry.scripts]
|
||||
gnss-backend = "gnss.__main__:main"
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core>=2.0.0,<3.0.0"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
"nuitka (>=2.8.9,<3.0.0)"
|
||||
]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue