diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ecbe6b9f9..9098f4746e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ Copy and pasting the git commit messages is __NOT__ enough. ### Added - Added single vehicle `Trip` into type. - Added new video record ultility using moviepy. +- Added `ConfigurableZone` for `Zone` object to types which enable users to build bubble by providing coordinates of the polygon. ### Deprecated ### Changed ### Removed diff --git a/envision/web/src/helpers/state_unpacker.js b/envision/web/src/helpers/state_unpacker.js index 1ec234a2c0..ff067f7399 100644 --- a/envision/web/src/helpers/state_unpacker.js +++ b/envision/web/src/helpers/state_unpacker.js @@ -33,9 +33,10 @@ const WorldState = Object.freeze({ SCENARIO_ID: 1, SCENARIO_NAME: 2, TRAFFIC: 3, - BUBBLES: 4, - SCORES: 5, - EGO_AGENT_IDS: 6, + TRAFFIC_SIGNALS: 4, + BUBBLES: 5, + SCORES: 6, + EGO_AGENT_IDS: 7, }); const Traffic = Object.freeze({ diff --git a/smarts/sstudio/types.py b/smarts/sstudio/types.py index 58cd1f202a..98bf63e9c3 100644 --- a/smarts/sstudio/types.py +++ b/smarts/sstudio/types.py @@ -27,7 +27,7 @@ from dataclasses import dataclass, field from enum import IntEnum from sys import maxsize -from typing import Any, Callable, Dict, NewType, Optional, Sequence, Tuple, Union +from typing import Any, Callable, Dict, NewType, Optional, Sequence, Tuple, Union, List import numpy as np from shapely.affinity import rotate as shapely_rotate @@ -38,6 +38,7 @@ MultiPolygon, Point, Polygon, + box, ) from shapely.ops import split, unary_union @@ -691,7 +692,7 @@ class GroupedLapMission: class Zone: """The base for a descriptor that defines a capture area.""" - def to_geometry(self, road_map: RoadMap) -> Polygon: + def to_geometry(self, road_map: Optional[RoadMap] = None) -> Polygon: """Generates the geometry from this zone.""" raise NotImplementedError @@ -852,6 +853,55 @@ def to_geometry(self, road_map: Optional[RoadMap] = None) -> Polygon: return shapely_translate(poly, xoff=x, yoff=y) +@dataclass(frozen=True) +class ConfigurableZone(Zone): + """A descriptor for a zone with user-defined geometry.""" + + ext_coordinates: List[Tuple[float, float]] + """external coordinates of the polygon + < 2 points provided: error + = 2 points provided: generates a box using these two points as diagonal + > 2 points provided: generates a polygon according to the coordinates""" + rotation: Optional[float] = None + """The heading direction of the bubble(radians, clock-wise rotation)""" + + def __post_init__(self): + if ( + not self.ext_coordinates + or len(self.ext_coordinates) < 2 + or not isinstance(self.ext_coordinates[0], tuple) + ): + raise ValueError( + "Two points or more are needed to create a polygon. (less than two points are provided)" + ) + + x_set = set(point[0] for point in self.ext_coordinates) + y_set = set(point[1] for point in self.ext_coordinates) + if len(x_set) == 1 or len(y_set) == 1: + raise ValueError( + "Parallel line cannot form a polygon. (points provided form a parallel line)" + ) + + def to_geometry(self, road_map: Optional[RoadMap] = None) -> Polygon: + """Generate a polygon according to given coordinates""" + poly = None + if ( + len(self.ext_coordinates) == 2 + ): # if user only specified two points, create a box + x_min = min(self.ext_coordinates[0][0], self.ext_coordinates[1][0]) + x_max = max(self.ext_coordinates[0][0], self.ext_coordinates[1][0]) + y_min = min(self.ext_coordinates[0][1], self.ext_coordinates[1][1]) + y_max = max(self.ext_coordinates[0][1], self.ext_coordinates[1][1]) + poly = box(x_min, y_min, x_max, y_max) + + else: # else create a polygon according to the coordinates + poly = Polygon(self.ext_coordinates) + + if self.rotation is not None: + poly = shapely_rotate(poly, self.rotation, use_radians=True) + return poly + + @dataclass(frozen=True) class BubbleLimits: """Defines the capture limits of a bubble.""" @@ -928,6 +978,20 @@ def __post_init__(self): "Only boids can have keep_alive enabled (for persistent boids)" ) + if not isinstance(self.zone, MapZone): + poly = self.zone.to_geometry(road_map=None) + if not poly.is_valid: + follow_id = ( + self.follow_actor_id + if self.follow_actor_id + else self.follow_vehicle_id + ) + raise ValueError( + f"The zone polygon of {type(self.zone).__name__} of moving {self.id} which following {follow_id} is not a valid closed loop" + if follow_id + else f"The zone polygon of {type(self.zone).__name__} of fixed position {self.id} is not a valid closed loop" + ) + @staticmethod def to_actor_id(actor, mission_group): """Mashes the actor id and mission group to create what needs to be a unique id."""