"""A GPS coverage survey over a fixed courtyard boundary.
A grid over the boundary is the pool of survey points. A pure recipe picks a
well-spread batch of points the spin videos so far leave uncovered, and a
coverage book leases each participant the batch point nearest to them; the
procedure guides them onto it and records a 360° spin video there.
"""
from datetime import UTC, datetime, timedelta
import capturegraph as cg
import capturegraph.scheduling as cgsh
from server import authoring as cgserver
BATCH_SIZE = 8
LOCATION_SIGMA_M = 10.0
DISTANCE = cgsh.distance.location(sigma_m=LOCATION_SIGMA_M)
BOUNDS = cg.Array[cg.Location](
[
cg.Location(longitude=-76.48039, latitude=42.44441),
cg.Location(longitude=-76.48040, latitude=42.44482),
cg.Location(longitude=-76.48121, latitude=42.44480),
cg.Location(longitude=-76.48125, latitude=42.44472),
cg.Location(longitude=-76.48140, latitude=42.44469),
cg.Location(longitude=-76.48136, latitude=42.44461),
cg.Location(longitude=-76.48133, latitude=42.44456),
cg.Location(longitude=-76.48133, latitude=42.44432),
cg.Location(longitude=-76.48132, latitude=42.44422),
cg.Location(longitude=-76.48129, latitude=42.44412),
cg.Location(longitude=-76.48044, latitude=42.44440),
cg.Location(longitude=-76.48039, latitude=42.44441),
]
)
class SurveySession(cg.Struct):
user_id: cg.UserID
location: cg.Location
target_location: cg.Location
spin_video: cg.Video
_metadata: cg.Metadata
class GPSCoverage(cg.Struct):
LocationSurvey: cg.Map[cg.Date, SurveySession]
_metadata: cg.Metadata
@cgserver.target
@cg.procedure(GPSCoverage)
def gps_coverage(root: cg.Procedure[cg.Path[GPSCoverage]]) -> None:
cg.do(cg.ShowInstructions(text="Walk to the guided location and record a 360° spin video."))
target_location = root._metadata._location.load()
cg.do(cg.ShowLocationGuide(target=target_location, threshold_meters=LOCATION_SIGMA_M))
session = root.LocationSurvey[cg.CaptureTime()]
session.user_id &= cg.GetUserID()
session.target_location &= target_location
session.spin_video &= cg.CaptureVideo(label="Record 360° Spin Video")
session.location &= cg.CaptureLocation()
@cg.pure_function("gps_coverage_batch/1")
def survey_batch(covered: cg.Array[cg.Location]) -> cg.Array[cg.Location]:
return cgsh.select_sessions(
potential_sessions=cgsh.forecast.locations_area(BOUNDS, resolution_meters=2.0),
previous_sessions=covered,
distance_fn=DISTANCE,
energy_fn=lambda d: -d,
energy_mode="max",
selections=BATCH_SIZE,
)
@cgserver.intercept(GPSCoverage, lambda s: s._metadata._location)
def next_spot(profile: cgserver.RequestProfile, target: GPSCoverage) -> cg.Location:
covered = cg.Array[cg.Location](
[
session.location
for session in target.LocationSurvey.values()
if not cg.is_missing(session.location)
]
)
chosen = cgsh.CoverageBook.open("gps-coverage").reserve(
profile.user_id,
at=profile.location,
fulfilled_by=list(covered),
distance=DISTANCE,
threshold=1.0,
refill=list(survey_batch(covered)),
)
if chosen is None:
raise LookupError("every survey point is covered")
return chosen
@cgserver.intercept(GPSCoverage, lambda s: s._metadata._next_notification)
def notification() -> cg.Time:
return cg.Time(datetime.now(UTC) + timedelta(minutes=1))