Skip to content

capturegraph.scheduling.forecast.location #

Location Forecast - Geographic Coverage#

Forecasts the probability of capturing a location based on geographic coverage.

locations_area(bounds, resolution_meters=1.0) #

Generate a grid of locations within the given polygon bounds.

Creates a regular grid of points at the specified resolution, returning only those points that fall inside the polygon defined by bounds.

Handles antimeridian (International Date Line) crossings by normalizing longitude coordinates before grid generation, then converting back to standard [-180, 180] range for output.

Parameters:

Name Type Description Default
bounds List[Location]

List of locations defining the polygon vertices. Must have at least 3 vertices. Vertices should be in order (clockwise or counter-clockwise) to define a valid polygon.

required
resolution_meters float

Spacing between grid points in meters. Smaller values produce more points but take longer to compute.

1.0

Returns:

Type Description
List[Location]

List of locations inside the polygon. Each location has longitude

List[Location]

in [-180, 180] and latitude in [-90, 90]. Altitude is not set.

List[Location]

May return empty list if the polygon is too small relative to

List[Location]

the resolution.

Note

Grid generation uses a simple lat/lon mesh, which introduces distortion at high latitudes where longitude degrees are shorter. The resolution is respected at the center latitude of the bounds.

Source code in capturegraph-lib/capturegraph/scheduling/forecast/location.py
def locations_area(
    bounds: cg.List[cg.Location],
    resolution_meters: float = 1.0,
) -> cg.List[cg.Location]:
    """Generate a grid of locations within the given polygon bounds.

    Creates a regular grid of points at the specified resolution, returning
    only those points that fall inside the polygon defined by bounds.

    Handles antimeridian (International Date Line) crossings by normalizing
    longitude coordinates before grid generation, then converting back to
    standard [-180, 180] range for output.

    Args:
        bounds: List of locations defining the polygon vertices. Must have
            at least 3 vertices. Vertices should be in order (clockwise or
            counter-clockwise) to define a valid polygon.
        resolution_meters: Spacing between grid points in meters. Smaller
            values produce more points but take longer to compute.

    Returns:
        List of locations inside the polygon. Each location has longitude
        in [-180, 180] and latitude in [-90, 90]. Altitude is not set.
        May return empty list if the polygon is too small relative to
        the resolution.

    Note:
        Grid generation uses a simple lat/lon mesh, which introduces
        distortion at high latitudes where longitude degrees are shorter.
        The resolution is respected at the center latitude of the bounds.
    """
    lons = np.array(bounds.longitude)
    lats = np.array(bounds.latitude)

    # Normalize longitudes to handle antimeridian crossing
    lons = _loop_number(lons, np.min(lons), 180.0)

    polygon = Polygon(zip(lons, lats))

    # Recalculate bounds after normalization
    max_lat, max_lon = np.max(lats), np.max(lons)
    min_lat, min_lon = np.min(lats), np.min(lons)

    center_lat = (max_lat + min_lat) / 2.0
    lon_dist_m = (
        (max_lon - min_lon)
        * (np.pi * EARTH_RADIUS_METERS / 180.0)
        * np.cos(np.radians(center_lat))
    )
    lat_dist_m = (
        (max_lat - min_lat)  # Latitudes always produce same distance
        * (np.pi * EARTH_RADIUS_METERS / 180.0)
    )

    lon_steps = int(np.round(lon_dist_m / resolution_meters))
    lat_steps = int(np.round(lat_dist_m / resolution_meters))

    lons = np.linspace(min_lon, max_lon, lon_steps)
    lats = np.linspace(min_lat, max_lat, lat_steps)

    grid = np.meshgrid(lons, lats)

    locations = cg.List()
    for lon, lat in zip(grid[0].flatten(), grid[1].flatten()):
        point = Point(lon, lat)
        if polygon.contains(point):
            # Convert back to standard -180 to 180 range
            lon = _loop_number(lon, 0.0, 180.0)
            locations.append(cg.Location(lon, lat))

    return locations

locations_perimeter(bounds, resolution_meters=1.0) #

Generate evenly-spaced locations along the perimeter of a polygon.

Interpolates points along each edge of the polygon at the specified resolution. The polygon is automatically closed (last vertex connects back to the first).

Handles antimeridian (International Date Line) crossings by normalizing longitude coordinates during interpolation, then converting back to standard [-180, 180] range for output.

Parameters:

Name Type Description Default
bounds List[Location]

List of locations defining the polygon vertices. Must have at least 3 vertices. Vertices should be in order (clockwise or counter-clockwise).

required
resolution_meters float

Target spacing between points in meters. Actual spacing may vary slightly to ensure each vertex is included.

1.0

Returns:

Type Description
List[Location]

List of locations along the polygon edges, in order starting from

List[Location]

the first vertex. Each edge includes its starting vertex but not

List[Location]

its ending vertex (to avoid duplicates). Longitudes are normalized

List[Location]

to [-180, 180]. Altitude is not set.

Note

Uses linear interpolation in lat/lon space, which is a reasonable approximation for short edges but may deviate from true geodesics over long distances.

Source code in capturegraph-lib/capturegraph/scheduling/forecast/location.py
def locations_perimeter(
    bounds: cg.List[cg.Location],
    resolution_meters: float = 1.0,
) -> cg.List[cg.Location]:
    """Generate evenly-spaced locations along the perimeter of a polygon.

    Interpolates points along each edge of the polygon at the specified
    resolution. The polygon is automatically closed (last vertex connects
    back to the first).

    Handles antimeridian (International Date Line) crossings by normalizing
    longitude coordinates during interpolation, then converting back to
    standard [-180, 180] range for output.

    Args:
        bounds: List of locations defining the polygon vertices. Must have
            at least 3 vertices. Vertices should be in order (clockwise or
            counter-clockwise).
        resolution_meters: Target spacing between points in meters. Actual
            spacing may vary slightly to ensure each vertex is included.

    Returns:
        List of locations along the polygon edges, in order starting from
        the first vertex. Each edge includes its starting vertex but not
        its ending vertex (to avoid duplicates). Longitudes are normalized
        to [-180, 180]. Altitude is not set.

    Note:
        Uses linear interpolation in lat/lon space, which is a reasonable
        approximation for short edges but may deviate from true geodesics
        over long distances.
    """

    lons = np.array(bounds.longitude)
    lats = np.array(bounds.latitude)

    lons = _loop_number(lons, np.min(lons), 180.0)

    bounds = cg.List([cg.Location(lon, lat) for lon, lat in zip(lons, lats)])

    point_pairs = list(zip(bounds, (bounds[1:] + [bounds[0]])))
    distances_m = [
        1000 * location_distance_km(loc_a, loc_b) for loc_a, loc_b in point_pairs
    ]
    steps = [max(2, int(np.round(d / resolution_meters))) for d in distances_m]

    points = []

    for (loc_a, loc_b), step in zip(point_pairs, steps):
        lon_a, lat_a = loc_a.longitude, loc_a.latitude
        lon_b, lat_b = loc_b.longitude, loc_b.latitude

        for lon, lat in zip(
            np.linspace(lon_a, lon_b, step, endpoint=False),
            np.linspace(lat_a, lat_b, step, endpoint=False),
        ):
            lon = _loop_number(lon, 0.0, 180.0)
            points.append(cg.Location(lon, lat))

    return cg.List(points)