Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/beeutil/__init__.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
from .image_cache import image_cache_status, enable_image_collection, disable_image_collection, enable_stereo_collection, disable_stereo_collection, purge_data, list_contents, upload_to_s3
from . import secrets
from .secrets import SecretsError, DecryptionError, SecretsNetworkError, SecretsNotFoundError
from . import geo
from . import video

__all__ = [
'image_cache_status', 'enable_image_collection', 'disable_image_collection',
'enable_stereo_collection', 'disable_stereo_collection', 'purge_data',
'list_contents', 'upload_to_s3',
'secrets',
'SecretsError', 'DecryptionError', 'SecretsNetworkError', 'SecretsNotFoundError',
'geo', 'video',
]

91 changes: 91 additions & 0 deletions src/beeutil/geo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
def parse_location_from_handle(handle):
"""Extract lat/lon from image cache handle string.

Handle format: {timestamp}_{lat}_{lon}
Returns (lat, lon) tuple or None if parsing fails.
"""
parts = handle.split('_')
if len(parts) < 3:
return None
try:
lat = float(parts[1])
lon = float(parts[2])
return (lat, lon)
except (ValueError, IndexError):
return None


def point_in_polygon(lat, lon, polygon_coords):
"""Ray-casting point-in-polygon test.

Args:
lat: Latitude of point
lon: Longitude of point
polygon_coords: GeoJSON polygon coordinates (list of rings).
Each ring is a list of [lon, lat] pairs.
First ring is exterior, rest are holes.

Returns True if point is inside the polygon.
"""
if not polygon_coords or not polygon_coords[0]:
return False

# Check exterior ring
ring = polygon_coords[0]
if not _point_in_ring(lat, lon, ring):
return False

# Check holes (interior rings) — point must NOT be in any hole
for hole in polygon_coords[1:]:
if _point_in_ring(lat, lon, hole):
return False

return True


def _point_in_ring(lat, lon, ring):
"""Ray-casting algorithm for a single ring.

Ring is a list of [lon, lat] pairs (GeoJSON order).
"""
n = len(ring)
inside = False

j = n - 1
for i in range(n):
xi, yi = ring[i][0], ring[i][1] # lon, lat
xj, yj = ring[j][0], ring[j][1]

if ((yi > lat) != (yj > lat)) and \
(lon < (xj - xi) * (lat - yi) / (yj - yi) + xi):
inside = not inside
j = i

return inside


def point_in_any_burst(lat, lon, bursts):
"""Check if a point is inside any active burst geometry.

Args:
lat: Latitude of point
lon: Longitude of point
bursts: List of burst dicts with 'geojson' field containing
GeoJSON Polygon or MultiPolygon geometry.

Returns the first matching burst dict, or None.
"""
for burst in bursts:
geojson = burst.get('geojson', {})
geo_type = geojson.get('type', '')
coords = geojson.get('coordinates', [])

if geo_type == 'Polygon':
if point_in_polygon(lat, lon, coords):
return burst
elif geo_type == 'MultiPolygon':
for polygon_coords in coords:
if point_in_polygon(lat, lon, polygon_coords):
return burst

return None
61 changes: 61 additions & 0 deletions src/beeutil/video.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import os
import time

VIDEO_DIR = '/data/video/'


def list_video_files(since_ts=None):
"""List video files from the device video directory.

Args:
since_ts: Optional unix timestamp (seconds). Only return files
modified after this time.

Returns list of absolute file paths sorted by modification time.
"""
if not os.path.isdir(VIDEO_DIR):
return []

files = []
for name in os.listdir(VIDEO_DIR):
if not name.endswith('.mp4'):
continue
path = os.path.join(VIDEO_DIR, name)
mtime = os.path.getmtime(path)
if since_ts is not None and mtime < since_ts:
continue
files.append((mtime, path))

files.sort()
return [path for _, path in files]


def upload_video_file(filepath, prefix, aws_bucket, aws_region, aws_secret, aws_key):
"""Upload a video file to S3 via the ODC API proxy.

Uses the same cache upload endpoint pattern as image uploads,
passing the video file path for the device-side upload handler.

Args:
filepath: Absolute path to the video file on device
prefix: S3 key prefix (e.g. session ID or burst ID)
aws_bucket: S3 bucket name
aws_region: AWS region
aws_secret: AWS secret access key
aws_key: AWS access key ID
"""
import requests

filename = os.path.basename(filepath)
host_url = 'http://127.0.0.1:5000'
url = f'{host_url}/cache/uploadS3/{filename}?prefix={prefix}&key={aws_key}&bucket={aws_bucket}&region={aws_region}&source=video'

headers = {
'authorization': aws_secret,
}

res = requests.post(url, headers=headers)
if res.status_code != 200:
raise Exception(f'Video upload failed for {filename}: {res.text}')

return res.json()
Loading