wrolpi/modules/map/api.py
Roland b8a532c8d9
Key changes:
- Add MapLibre GL JS viewer with light/dark themes, terrain layer,
  and embedded static map page for coordinate sharing
- Add map pin support with persistent map_pins.yaml config
- Add map region subscriptions and downloader with progress tracking,
  sourcing pre-extracted PMTiles from a cold CDN
- Add map export to PNG and GPS coordinate copy via context menu
- Add MapViewer React component alongside reworked Map page
- Add map catalog and region-building script (build_map_regions.py)
- Remove OSM import/render scripts, leaflet files, Apache configs,
  renderd service, and osm-map-dumper Docker container
- Update Caddyfile to serve PMTiles and map static assets
- Update build/install/repair scripts for Protomaps dependencies
- Drop map_file table, add/drop map_subscription table (alembic)
- Adding button to skip primary drive mounting during onboarding.
2026-04-04 13:48:19 -06:00

100 lines
3.4 KiB
Python

from http import HTTPStatus
from sanic import Request, response, Blueprint
from sanic_ext import validate
from sanic_ext.extensions.openapi import openapi
from modules.map import lib, schema
from modules.map.pins import get_map_pins_config
from wrolpi.api_utils import json_response
from wrolpi.common import wrol_mode_check
from wrolpi.events import Events
map_bp = Blueprint('Map', '/api/map')
@map_bp.get('/files')
@openapi.description('List available PMTiles map files')
def get_files(_: Request):
files = lib.get_pmtiles_files()
return json_response(dict(files=files), HTTPStatus.OK)
@map_bp.delete('/files/<filename:str>')
@openapi.description('Delete a PMTiles map file')
@wrol_mode_check
async def delete_file(_: Request, filename: str):
try:
deleted = lib.delete_pmtiles_file(filename)
except ValueError as e:
return response.json({'error': str(e)}, HTTPStatus.BAD_REQUEST)
if deleted:
return response.empty()
return response.json({'error': 'File not found'}, HTTPStatus.NOT_FOUND)
@map_bp.get('/subscribe')
@openapi.description('Get map catalog and current subscriptions')
def get_subscriptions(request: Request):
catalog = lib.get_map_catalog()
subscriptions = lib.get_map_subscriptions(request.ctx.session)
return json_response(dict(catalog=catalog, subscriptions=subscriptions), HTTPStatus.OK)
@map_bp.post('/subscribe')
@openapi.description('Subscribe to a map region for automatic downloads')
@validate(schema.MapSubscribeRequest)
@wrol_mode_check
async def post_subscribe(request: Request, body: schema.MapSubscribeRequest):
try:
await lib.subscribe(request.ctx.session, body.name, body.region)
except ValueError as e:
return response.json({'error': str(e)}, HTTPStatus.BAD_REQUEST)
Events.send_created(f'Map subscription created for {body.name}')
return response.empty(HTTPStatus.CREATED)
@map_bp.delete('/subscribe/<region:str>')
@openapi.description('Unsubscribe from a map region')
@wrol_mode_check
async def delete_subscription(request: Request, region: str):
try:
await lib.unsubscribe(request.ctx.session, region)
except ValueError as e:
return response.json({'error': str(e)}, HTTPStatus.NOT_FOUND)
return response.empty(HTTPStatus.NO_CONTENT)
@map_bp.get('/pins')
@openapi.description('Get all map pins')
def get_pins(_: Request):
pins = get_map_pins_config().pins
return json_response(dict(pins=pins), HTTPStatus.OK)
@map_bp.post('/pins')
@openapi.description('Add a map pin')
@validate(schema.MapPinRequest)
async def add_pin(_: Request, body: schema.MapPinRequest):
pin = get_map_pins_config().add_pin(body.lat, body.lon, body.label, body.color)
return json_response(dict(pin=pin), HTTPStatus.CREATED)
@map_bp.delete('/pins/<pin_id:int>')
@openapi.description('Delete a map pin by ID')
async def delete_pin(_: Request, pin_id: int):
if get_map_pins_config().delete_pin(pin_id):
return response.empty(HTTPStatus.NO_CONTENT)
return response.json({'error': 'Pin not found'}, HTTPStatus.NOT_FOUND)
@map_bp.put('/pins/<pin_id:int>')
@openapi.description('Update a map pin by ID')
@validate(schema.MapPinUpdateRequest)
async def update_pin(_: Request, pin_id: int, body: schema.MapPinUpdateRequest):
if get_map_pins_config().update_pin(pin_id, body.label, body.color):
return response.empty(HTTPStatus.NO_CONTENT)
return response.json({'error': 'Pin not found'}, HTTPStatus.NOT_FOUND)