Files
mapmaker/app/services/item_service.py

228 lines
7.9 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
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
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 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)
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)
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)
# 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()
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")