271 lines
9.3 KiB
Python
271 lines
9.3 KiB
Python
"""Map item service for business logic."""
|
|
from typing import List, Optional
|
|
from uuid import UUID
|
|
from sqlalchemy.orm import Session
|
|
from fastapi import HTTPException, status
|
|
from geoalchemy2.shape import from_shape, to_shape
|
|
from shapely.geometry import shape, Point, LineString
|
|
import json
|
|
import asyncio
|
|
|
|
from app.models.map_item import MapItem
|
|
from app.models.user import User
|
|
from app.schemas.map_item import MapItemCreate, MapItemUpdate
|
|
from app.services.map_service import get_map_by_id
|
|
from app.websocket.connection_manager import manager
|
|
|
|
|
|
def get_map_items(db: Session, map_id: UUID, user: Optional[User] = None) -> List[MapItem]:
|
|
"""Get all items for a map."""
|
|
# Verify user has access to the map
|
|
get_map_by_id(db, map_id, user)
|
|
|
|
items = db.query(MapItem).filter(MapItem.map_id == map_id).all()
|
|
return items
|
|
|
|
|
|
def get_map_item_by_id(db: Session, item_id: UUID, user: Optional[User] = None) -> MapItem:
|
|
"""Get a map item by ID."""
|
|
item = db.query(MapItem).filter(MapItem.id == item_id).first()
|
|
|
|
if not item:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="Map item not found"
|
|
)
|
|
|
|
# Verify user has access to the map
|
|
get_map_by_id(db, item.map_id, user)
|
|
|
|
return item
|
|
|
|
|
|
def geojson_to_geography(geojson: dict) -> str:
|
|
"""Convert GeoJSON geometry to PostGIS geography WKT."""
|
|
geom = shape(geojson)
|
|
|
|
# Ensure coordinates are in the correct format for PostGIS (lon, lat)
|
|
if isinstance(geom, Point):
|
|
return f'SRID=4326;POINT({geom.x} {geom.y})'
|
|
elif isinstance(geom, LineString):
|
|
coords = ', '.join([f'{x} {y}' for x, y in geom.coords])
|
|
return f'SRID=4326;LINESTRING({coords})'
|
|
else:
|
|
raise ValueError(f"Unsupported geometry type: {type(geom)}")
|
|
|
|
|
|
def geography_to_geojson(geography) -> dict:
|
|
"""Convert PostGIS geography to GeoJSON."""
|
|
geom = to_shape(geography)
|
|
return json.loads(json.dumps(geom.__geo_interface__))
|
|
|
|
|
|
def item_to_dict(item: MapItem) -> dict:
|
|
"""Convert MapItem to JSON-serializable dict for WebSocket broadcast."""
|
|
return {
|
|
"id": str(item.id),
|
|
"map_id": str(item.map_id),
|
|
"type": item.type,
|
|
"geometry": geography_to_geojson(item.geometry),
|
|
"properties": item.properties,
|
|
"created_at": item.created_at.isoformat(),
|
|
"updated_at": item.updated_at.isoformat()
|
|
}
|
|
|
|
|
|
def create_map_item(db: Session, map_id: UUID, item_data: MapItemCreate, user: User) -> MapItem:
|
|
"""Create a new map item."""
|
|
# Verify user has access to the map
|
|
get_map_by_id(db, map_id, user)
|
|
|
|
# Convert GeoJSON to PostGIS geography
|
|
geometry_wkt = geojson_to_geography(item_data.geometry)
|
|
|
|
item = MapItem(
|
|
map_id=map_id,
|
|
type=item_data.type,
|
|
geometry=geometry_wkt,
|
|
properties=item_data.properties,
|
|
created_by=user.id,
|
|
updated_by=user.id
|
|
)
|
|
|
|
db.add(item)
|
|
db.commit()
|
|
db.refresh(item)
|
|
|
|
# If this is a cable with device connections, update device port tracking
|
|
if item.type == 'cable':
|
|
start_device_id = item.properties.get('start_device_id')
|
|
end_device_id = item.properties.get('end_device_id')
|
|
|
|
print(f"Cable created: start_device_id={start_device_id}, end_device_id={end_device_id}")
|
|
|
|
if start_device_id:
|
|
print(f"Updating port connections for start device: {start_device_id}")
|
|
update_device_connections(db, UUID(start_device_id), item.id)
|
|
if end_device_id:
|
|
print(f"Updating port connections for end device: {end_device_id}")
|
|
update_device_connections(db, UUID(end_device_id), item.id)
|
|
|
|
# Broadcast item creation to WebSocket clients
|
|
try:
|
|
loop = asyncio.get_event_loop()
|
|
loop.create_task(manager.send_item_created(map_id, item_to_dict(item)))
|
|
except RuntimeError:
|
|
# No event loop running, skip WebSocket broadcast
|
|
pass
|
|
|
|
return item
|
|
|
|
|
|
def update_device_connections(db: Session, device_id: UUID, cable_id: UUID) -> None:
|
|
"""Add cable connection to device's connections array."""
|
|
print(f"update_device_connections called: device_id={device_id}, cable_id={cable_id}")
|
|
device = db.query(MapItem).filter(MapItem.id == device_id).first()
|
|
if not device:
|
|
print(f"Device not found: {device_id}")
|
|
return
|
|
|
|
# Create a mutable copy of properties
|
|
properties = dict(device.properties) if device.properties else {}
|
|
connections = properties.get('connections', [])
|
|
port_count = properties.get('port_count', 0)
|
|
|
|
print(f"Device {device_id}: port_count={port_count}, current_connections={len(connections)}")
|
|
|
|
# Find next available port
|
|
used_ports = {conn.get('port_number') for conn in connections if isinstance(conn, dict)}
|
|
next_port = 1
|
|
while next_port in used_ports and next_port <= port_count:
|
|
next_port += 1
|
|
|
|
# Only add if there's an available port
|
|
if next_port <= port_count:
|
|
connections.append({
|
|
'cable_id': str(cable_id),
|
|
'port_number': next_port
|
|
})
|
|
properties['connections'] = connections
|
|
|
|
# Mark the column as modified so SQLAlchemy detects the change
|
|
from sqlalchemy.orm.attributes import flag_modified
|
|
device.properties = properties
|
|
flag_modified(device, 'properties')
|
|
|
|
db.commit()
|
|
db.refresh(device)
|
|
print(f"Added connection to port {next_port}. Total connections now: {len(connections)}")
|
|
print(f"Device properties after update: {device.properties}")
|
|
else:
|
|
print(f"No available ports! Port count: {port_count}, used: {len(connections)}")
|
|
|
|
|
|
def update_map_item(db: Session, item_id: UUID, item_data: MapItemUpdate, user: User) -> MapItem:
|
|
"""Update a map item."""
|
|
item = get_map_item_by_id(db, item_id, user)
|
|
|
|
# Update fields if provided
|
|
if item_data.type is not None:
|
|
item.type = item_data.type
|
|
if item_data.geometry is not None:
|
|
item.geometry = geojson_to_geography(item_data.geometry)
|
|
if item_data.properties is not None:
|
|
item.properties = item_data.properties
|
|
|
|
item.updated_by = user.id
|
|
|
|
db.commit()
|
|
db.refresh(item)
|
|
|
|
# Broadcast item update to WebSocket clients
|
|
try:
|
|
loop = asyncio.get_event_loop()
|
|
loop.create_task(manager.send_item_updated(item.map_id, item_to_dict(item)))
|
|
except RuntimeError:
|
|
# No event loop running, skip WebSocket broadcast
|
|
pass
|
|
|
|
return item
|
|
|
|
|
|
def delete_map_item(db: Session, item_id: UUID, user: User) -> None:
|
|
"""Delete a map item."""
|
|
item = get_map_item_by_id(db, item_id, user)
|
|
|
|
# Capture map_id and item_id before deletion for WebSocket broadcast
|
|
map_id = item.map_id
|
|
deleted_item_id = str(item.id)
|
|
|
|
# If deleting a cable, remove it from device connections
|
|
if item.type == 'cable':
|
|
start_device_id = item.properties.get('start_device_id')
|
|
end_device_id = item.properties.get('end_device_id')
|
|
|
|
if start_device_id:
|
|
remove_device_connection(db, UUID(start_device_id), item.id)
|
|
if end_device_id:
|
|
remove_device_connection(db, UUID(end_device_id), item.id)
|
|
|
|
# If deleting an AP, delete all associated wireless mesh links
|
|
if item.type in ['indoor_ap', 'outdoor_ap']:
|
|
print(f"Deleting AP {item.id}, checking for wireless mesh links...")
|
|
# Find all wireless mesh links connected to this AP
|
|
wireless_meshes = db.query(MapItem).filter(
|
|
MapItem.map_id == item.map_id,
|
|
MapItem.type == 'wireless_mesh'
|
|
).all()
|
|
|
|
for mesh in wireless_meshes:
|
|
start_ap_id = mesh.properties.get('start_ap_id')
|
|
end_ap_id = mesh.properties.get('end_ap_id')
|
|
|
|
if start_ap_id == str(item.id) or end_ap_id == str(item.id):
|
|
print(f"Deleting wireless mesh {mesh.id} connected to AP {item.id}")
|
|
db.delete(mesh)
|
|
|
|
db.delete(item)
|
|
db.commit()
|
|
|
|
# Broadcast item deletion to WebSocket clients
|
|
try:
|
|
loop = asyncio.get_event_loop()
|
|
loop.create_task(manager.send_item_deleted(map_id, deleted_item_id))
|
|
except RuntimeError:
|
|
# No event loop running, skip WebSocket broadcast
|
|
pass
|
|
|
|
|
|
def remove_device_connection(db: Session, device_id: UUID, cable_id: UUID) -> None:
|
|
"""Remove cable connection from device's connections array."""
|
|
print(f"remove_device_connection called: device_id={device_id}, cable_id={cable_id}")
|
|
device = db.query(MapItem).filter(MapItem.id == device_id).first()
|
|
if not device:
|
|
print(f"Device not found: {device_id}")
|
|
return
|
|
|
|
# Create a mutable copy of properties
|
|
properties = dict(device.properties) if device.properties else {}
|
|
connections = properties.get('connections', [])
|
|
|
|
print(f"Before removal: {len(connections)} connections")
|
|
|
|
# Filter out the cable connection
|
|
connections = [
|
|
conn for conn in connections
|
|
if isinstance(conn, dict) and conn.get('cable_id') != str(cable_id)
|
|
]
|
|
|
|
print(f"After removal: {len(connections)} connections")
|
|
|
|
properties['connections'] = connections
|
|
|
|
# Mark the column as modified so SQLAlchemy detects the change
|
|
from sqlalchemy.orm.attributes import flag_modified
|
|
device.properties = properties
|
|
flag_modified(device, 'properties')
|
|
|
|
db.commit()
|
|
db.refresh(device)
|
|
print(f"Removed cable connection. Device now has {len(connections)} connections")
|