Feat: rnd file viewer built
This commit is contained in:
@@ -644,6 +644,8 @@ async def ftp_download_to_server(
|
||||
'.flac': 'audio',
|
||||
'.m4a': 'audio',
|
||||
'.aac': 'audio',
|
||||
# Sound level meter measurement files
|
||||
'.rnd': 'measurement',
|
||||
# Data files
|
||||
'.csv': 'data',
|
||||
'.txt': 'data',
|
||||
@@ -1023,6 +1025,176 @@ async def download_project_file(
|
||||
)
|
||||
|
||||
|
||||
@router.get("/{project_id}/files/{file_id}/view-rnd", response_class=HTMLResponse)
|
||||
async def view_rnd_file(
|
||||
request: Request,
|
||||
project_id: str,
|
||||
file_id: str,
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
View an RND (sound level meter measurement) file.
|
||||
Returns a dedicated page with data table and charts.
|
||||
"""
|
||||
from backend.models import DataFile
|
||||
from pathlib import Path
|
||||
|
||||
# Get the file record
|
||||
file_record = db.query(DataFile).filter_by(id=file_id).first()
|
||||
if not file_record:
|
||||
raise HTTPException(status_code=404, detail="File not found")
|
||||
|
||||
# Verify file belongs to this project
|
||||
session = db.query(RecordingSession).filter_by(id=file_record.session_id).first()
|
||||
if not session or session.project_id != project_id:
|
||||
raise HTTPException(status_code=403, detail="File does not belong to this project")
|
||||
|
||||
# Build full file path
|
||||
file_path = Path("data") / file_record.file_path
|
||||
|
||||
if not file_path.exists():
|
||||
raise HTTPException(status_code=404, detail="File not found on disk")
|
||||
|
||||
# Get project info
|
||||
project = db.query(Project).filter_by(id=project_id).first()
|
||||
|
||||
# Get location info if available
|
||||
location = None
|
||||
if session.location_id:
|
||||
location = db.query(MonitoringLocation).filter_by(id=session.location_id).first()
|
||||
|
||||
# Get unit info if available
|
||||
unit = None
|
||||
if session.unit_id:
|
||||
unit = db.query(RosterUnit).filter_by(id=session.unit_id).first()
|
||||
|
||||
# Parse file metadata
|
||||
metadata = {}
|
||||
if file_record.file_metadata:
|
||||
try:
|
||||
metadata = json.loads(file_record.file_metadata)
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
|
||||
return templates.TemplateResponse("rnd_viewer.html", {
|
||||
"request": request,
|
||||
"project": project,
|
||||
"project_id": project_id,
|
||||
"file": file_record,
|
||||
"file_id": file_id,
|
||||
"session": session,
|
||||
"location": location,
|
||||
"unit": unit,
|
||||
"metadata": metadata,
|
||||
"filename": file_path.name,
|
||||
})
|
||||
|
||||
|
||||
@router.get("/{project_id}/files/{file_id}/rnd-data")
|
||||
async def get_rnd_data(
|
||||
project_id: str,
|
||||
file_id: str,
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Get parsed RND file data as JSON.
|
||||
Returns the measurement data for charts and tables.
|
||||
"""
|
||||
from backend.models import DataFile
|
||||
from pathlib import Path
|
||||
import csv
|
||||
import io
|
||||
|
||||
# Get the file record
|
||||
file_record = db.query(DataFile).filter_by(id=file_id).first()
|
||||
if not file_record:
|
||||
raise HTTPException(status_code=404, detail="File not found")
|
||||
|
||||
# Verify file belongs to this project
|
||||
session = db.query(RecordingSession).filter_by(id=file_record.session_id).first()
|
||||
if not session or session.project_id != project_id:
|
||||
raise HTTPException(status_code=403, detail="File does not belong to this project")
|
||||
|
||||
# Build full file path
|
||||
file_path = Path("data") / file_record.file_path
|
||||
|
||||
if not file_path.exists():
|
||||
raise HTTPException(status_code=404, detail="File not found on disk")
|
||||
|
||||
# Read and parse the RND file
|
||||
try:
|
||||
with open(file_path, 'r', encoding='utf-8', errors='replace') as f:
|
||||
content = f.read()
|
||||
|
||||
# Parse as CSV
|
||||
reader = csv.DictReader(io.StringIO(content))
|
||||
rows = []
|
||||
headers = []
|
||||
|
||||
for row in reader:
|
||||
if not headers:
|
||||
headers = list(row.keys())
|
||||
# Clean up values - strip whitespace and handle special values
|
||||
cleaned_row = {}
|
||||
for key, value in row.items():
|
||||
if key: # Skip empty keys
|
||||
cleaned_key = key.strip()
|
||||
cleaned_value = value.strip() if value else ''
|
||||
# Convert numeric values
|
||||
if cleaned_value and cleaned_value not in ['-.-', '-', '']:
|
||||
try:
|
||||
cleaned_value = float(cleaned_value)
|
||||
except ValueError:
|
||||
pass
|
||||
elif cleaned_value in ['-.-', '-']:
|
||||
cleaned_value = None
|
||||
cleaned_row[cleaned_key] = cleaned_value
|
||||
rows.append(cleaned_row)
|
||||
|
||||
# Detect file type (Leq vs Lp) based on columns
|
||||
file_type = 'unknown'
|
||||
if headers:
|
||||
header_str = ','.join(headers).lower()
|
||||
if 'leq' in header_str:
|
||||
file_type = 'leq' # Time-averaged data
|
||||
elif 'lp(main)' in header_str or 'lp (main)' in header_str:
|
||||
file_type = 'lp' # Instantaneous data
|
||||
|
||||
# Get summary statistics
|
||||
summary = {
|
||||
"total_rows": len(rows),
|
||||
"file_type": file_type,
|
||||
"headers": [h.strip() for h in headers if h.strip()],
|
||||
}
|
||||
|
||||
# Calculate min/max/avg for key metrics if available
|
||||
metrics_to_summarize = ['Leq(Main)', 'Lmax(Main)', 'Lmin(Main)', 'Lpeak(Main)', 'Lp(Main)']
|
||||
for metric in metrics_to_summarize:
|
||||
values = [row.get(metric) for row in rows if isinstance(row.get(metric), (int, float))]
|
||||
if values:
|
||||
summary[f"{metric}_min"] = min(values)
|
||||
summary[f"{metric}_max"] = max(values)
|
||||
summary[f"{metric}_avg"] = sum(values) / len(values)
|
||||
|
||||
# Get time range
|
||||
if rows:
|
||||
first_time = rows[0].get('Start Time', '')
|
||||
last_time = rows[-1].get('Start Time', '')
|
||||
summary['time_start'] = first_time
|
||||
summary['time_end'] = last_time
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"summary": summary,
|
||||
"headers": summary["headers"],
|
||||
"data": rows,
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error parsing RND file: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"Error parsing file: {str(e)}")
|
||||
|
||||
|
||||
@router.get("/types/list", response_class=HTMLResponse)
|
||||
async def get_project_types(request: Request, db: Session = Depends(get_db)):
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user