📍 GPS Coverage Map

36 server-assigned survey points, each a 360° spin video stitched into a panorama. Click a marker to see it.

📄 Capture Graph Code

GPSCoverage.py

"""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))

🎬 Capture Video