예제 #1
0
 def display_culling(self, scene: QGraphicsScene) -> None:
     """Draws the culling distance rings on the map"""
     culling_points = self.game_model.game.get_culling_points()
     culling_zones = self.game_model.game.get_culling_zones()
     culling_distance = self.game_model.game.settings.perf_culling_distance
     for point in culling_points:
         culling_distance_point = Point(point.x + 2500, point.y + 2500)
         distance_point = self._transform_point(culling_distance_point)
         transformed = self._transform_point(point)
         radius = distance_point[0] - transformed[0]
         scene.addEllipse(
             transformed[0] - radius,
             transformed[1] - radius,
             2 * radius,
             2 * radius,
             CONST.COLORS["transparent"],
             CONST.COLORS["light_green_transparent"],
         )
     for zone in culling_zones:
         culling_distance_zone = Point(
             zone.x + culling_distance * 1000, zone.y + culling_distance * 1000
         )
         distance_zone = self._transform_point(culling_distance_zone)
         transformed = self._transform_point(zone)
         radius = distance_zone[0] - transformed[0]
         scene.addEllipse(
             transformed[0] - radius,
             transformed[1] - radius,
             2 * radius,
             2 * radius,
             CONST.COLORS["transparent"],
             CONST.COLORS["light_green_transparent"],
         )
예제 #2
0
 def projection_test(self):
     for i in range(100):
         for j in range(100):
             x = i * 100.0
             y = j * 100.0
             original = Point(x, y)
             proj = self._scene_to_dcs_coords(original)
             unproj = self._transform_point(proj)
             converted = Point(*unproj)
             assert math.isclose(original.x, converted.x, abs_tol=0.00000001)
             assert math.isclose(original.y, converted.y, abs_tol=0.00000001)
    def draw_navmesh_neighbor_line(self, scene: QGraphicsScene, poly: Polygon,
                                   begin: ShapelyPoint) -> None:
        vertex = Point(begin.x, begin.y)
        centroid = poly.centroid
        direction = Point(centroid.x, centroid.y)
        end = vertex.point_from_heading(
            vertex.heading_between_point(direction),
            nautical_miles(2).meters)

        scene.addLine(
            QLineF(QPointF(*self._transform_point(vertex)),
                   QPointF(*self._transform_point(end))),
            CONST.COLORS["yellow"])
    def keyPressEvent(self, event):
        modifiers = QtWidgets.QApplication.keyboardModifiers()
        if not self.reference_point_setup_mode:
            if modifiers == QtCore.Qt.ShiftModifier and event.key(
            ) == QtCore.Qt.Key_R:
                self.reference_point_setup_mode = True
                self.reload_scene()
            else:
                super(QLiberationMap, self).keyPressEvent(event)
        else:
            if modifiers == QtCore.Qt.ShiftModifier and event.key(
            ) == QtCore.Qt.Key_R:
                self.reference_point_setup_mode = False
                self.reload_scene()
            else:
                distance = 1
                modifiers = int(event.modifiers())
                if modifiers & QtCore.Qt.ShiftModifier:
                    distance *= 10
                elif modifiers & QtCore.Qt.ControlModifier:
                    distance *= 100

                if event.key() == QtCore.Qt.Key_Down:
                    self.update_reference_point(
                        self.game.theater.reference_points[0],
                        Point(0, distance))
                if event.key() == QtCore.Qt.Key_Up:
                    self.update_reference_point(
                        self.game.theater.reference_points[0],
                        Point(0, -distance))
                if event.key() == QtCore.Qt.Key_Left:
                    self.update_reference_point(
                        self.game.theater.reference_points[0],
                        Point(-distance, 0))
                if event.key() == QtCore.Qt.Key_Right:
                    self.update_reference_point(
                        self.game.theater.reference_points[0],
                        Point(distance, 0))

                if event.key() == QtCore.Qt.Key_S:
                    self.update_reference_point(
                        self.game.theater.reference_points[1],
                        Point(0, distance))
                if event.key() == QtCore.Qt.Key_W:
                    self.update_reference_point(
                        self.game.theater.reference_points[1],
                        Point(0, -distance))
                if event.key() == QtCore.Qt.Key_A:
                    self.update_reference_point(
                        self.game.theater.reference_points[1],
                        Point(-distance, 0))
                if event.key() == QtCore.Qt.Key_D:
                    self.update_reference_point(
                        self.game.theater.reference_points[1],
                        Point(distance, 0))

                logging.debug(
                    f"Reference points: {self.game.theater.reference_points}")
                self.reload_scene()
예제 #5
0
def set_destination(
        cp_id: UUID,
        destination: LeafletPoint = Body(..., title="destination"),
        game: Game = Depends(GameContext.require),
) -> None:
    cp = game.theater.find_control_point_by_id(cp_id)
    if cp is None:
        raise HTTPException(
            status.HTTP_404_NOT_FOUND,
            detail=f"Game has no control point with ID {cp_id}",
        )
    if not cp.moveable:
        raise HTTPException(status.HTTP_403_FORBIDDEN,
                            detail=f"{cp} is not mobile")
    if not cp.captured:
        raise HTTPException(status.HTTP_403_FORBIDDEN,
                            detail=f"{cp} is not owned by the player")

    point = Point.from_latlng(LatLng(destination.lat, destination.lng),
                              game.theater.terrain)
    if not cp.destination_in_range(point):
        raise HTTPException(
            status.HTTP_400_BAD_REQUEST,
            detail=f"Cannot move {cp} more than "
            f"{cp.max_move_distance.nautical_miles}nm.",
        )
    cp.target_position = point
    EventStream.put_nowait(GameUpdateEvents().update_control_point(cp))
예제 #6
0
    def generate_destroyed_units(self) -> None:
        """Add destroyed units to the Mission"""
        if not self.game.settings.perf_destroyed_units:
            return

        for d in self.game.get_destroyed_units():
            try:
                type_name = d["type"]
                if not isinstance(type_name, str):
                    raise TypeError(
                        "Expected the type of the destroyed static to be a string"
                    )
                utype = unit_type_from_name(type_name)
            except KeyError:
                logging.warning(f"Destroyed unit has no type: {d}")
                continue

            pos = Point(cast(float, d["x"]), cast(float, d["z"]),
                        self.mission.terrain)
            if utype is not None and not self.game.position_culled(pos):
                self.mission.static_group(
                    country=self.mission.country(self.game.blue.country_name),
                    name="",
                    _type=utype,
                    hidden=True,
                    position=pos,
                    heading=d["orientation"],
                    dead=True,
                )
    def _transform_point(self, world_point: Point) -> Tuple[float, float]:
        """Transforms world coordinates to image coordinates.

        World coordinates are transposed. X increases toward the North, Y
        increases toward the East. The origin point depends on the map.

        Image coordinates originate from the top left. X increases to the right,
        Y increases toward the bottom.

        The two points should be as distant as possible in both latitude and
        logitude, and tuning the reference points will be simpler if they are in
        geographically recognizable locations. For example, the Caucasus map is
        aligned using the first point on Gelendzhik and the second on Batumi.

        The distances between each point are computed and a scaling factor is
        determined from that. The given point is then offset from the first
        point using the scaling factor.

        X is latitude, increasing northward.
        Y is longitude, increasing eastward.
        """
        point_a = self.game.theater.reference_points[0]
        scale = self._scaling_factor()

        offset = self._transpose_point(point_a.world_coordinates - world_point)
        scaled = Point(offset.x * scale.x, offset.y * scale.y)
        transformed = point_a.image_coordinates - scaled
        return transformed.x, transformed.y
예제 #8
0
    def _scene_to_dcs_coords(self, scene_point: Point) -> Point:
        point_a = self.game.theater.reference_points[0]
        scale = self._scaling_factor()

        offset = point_a.image_coordinates - scene_point
        scaled = self._transpose_point(Point(offset.x / scale.x, offset.y / scale.y))
        return point_a.world_coordinates - scaled
    def sceneMousePressEvent(self, event: QGraphicsSceneMouseEvent):
        if self.state == QLiberationMapState.MOVING_UNIT:
            if event.buttons() == Qt.RightButton:
                pass
            elif event.buttons() == Qt.LeftButton:
                if self.selected_cp is not None:
                    # Set movement position for the cp
                    pos = event.scenePos()
                    point = Point(int(pos.x()), int(pos.y()))
                    proj = self._scene_to_dcs_coords(point)

                    if self.is_valid_ship_pos(point):
                        self.selected_cp.control_point.target_position = proj
                    else:
                        self.selected_cp.control_point.target_position = None

                    GameUpdateSignal.get_instance().updateGame(
                        self.game_model.game)
            else:
                return
            self.state = QLiberationMapState.NORMAL
            try:
                self.scene().removeItem(self.movement_line)
            except:
                pass
            self.selected_cp = None
예제 #10
0
    def generate_routes(self) -> None:
        """
        Generate routes drawing between cps
        """
        seen = set()
        for cp in self.game.theater.controlpoints:
            seen.add(cp)
            for destination, convoy_route in cp.convoy_routes.items():
                if destination in seen:
                    continue
                else:

                    # Determine path color
                    if cp.captured and destination.captured:
                        color = BLUE_PATH_COLOR
                    elif not cp.captured and not destination.captured:
                        color = RED_PATH_COLOR
                    else:
                        color = ACTIVE_PATH_COLOR

                    # Add shape to layer
                    shape = self.player_layer.add_line_segments(
                        cp.position,
                        [Point(0, 0, self.game.theater.terrain)] +
                        [p - cp.position for p in convoy_route] +
                        [destination.position - cp.position],
                        line_thickness=6,
                        color=color,
                        line_style=LineStyle.Solid,
                    )
                    shape.name = "path from " + cp.name + " to " + destination.name
    def draw_flight_plan(self, scene: QGraphicsScene, flight: Flight,
                         selected: bool) -> None:
        is_player = flight.from_cp.captured
        pos = self._transform_point(flight.from_cp.position)

        self.draw_waypoint(scene, pos, is_player, selected)
        prev_pos = tuple(pos)
        drew_target = False
        target_types = (
            FlightWaypointType.TARGET_GROUP_LOC,
            FlightWaypointType.TARGET_POINT,
            FlightWaypointType.TARGET_SHIP,
        )
        for idx, point in enumerate(flight.flight_plan.waypoints[1:]):
            if point.waypoint_type == FlightWaypointType.DIVERT:
                # Don't clutter the map showing divert points.
                continue

            new_pos = self._transform_point(Point(point.x, point.y))
            self.draw_flight_path(scene, prev_pos, new_pos, is_player,
                                  selected)
            self.draw_waypoint(scene, new_pos, is_player, selected)
            if selected and DisplayOptions.waypoint_info:
                if point.waypoint_type in target_types:
                    if drew_target:
                        # Don't draw dozens of targets over each other.
                        continue
                    drew_target = True
                self.draw_waypoint_info(scene, idx + 1, point, new_pos,
                                        flight.flight_plan)
            prev_pos = tuple(new_pos)

        if selected and DisplayOptions.barcap_commit_range:
            self.draw_barcap_commit_range(scene, flight)
예제 #12
0
    def repair_unit(self, unit, price):
        if self.game.blue.budget > price:
            self.game.blue.budget -= price
            unit.alive = True
            GameUpdateSignal.get_instance().updateGame(self.game)

            # Remove destroyed units in the vicinity
            destroyed_units = self.game.get_destroyed_units()
            for d in destroyed_units:
                p = Point(d["x"], d["z"], self.game.theater.terrain)
                if p.distance_to_point(unit.position) < 15:
                    destroyed_units.remove(d)
                    logging.info("Removed destroyed units " + str(d))
            logging.info(f"Repaired unit: {unit.unit_name}")

        self.update_game()
예제 #13
0
    def repair_unit(self, group, unit, price):
        if self.game.budget > price:
            self.game.budget -= price
            group.units_losts = [u for u in group.units_losts if u.id != unit.id]
            group.units.append(unit)
            GameUpdateSignal.get_instance().updateGame(self.game)

            # Remove destroyed units in the vicinity
            destroyed_units = self.game.get_destroyed_units()
            for d in destroyed_units:
                p = Point(d["x"], d["z"])
                if p.distance_to_point(unit.position) < 15:
                    destroyed_units.remove(d)
                    logging.info("Removed destroyed units " + str(d))
            logging.info("Repaired unit : " + str(unit.id) + " " + str(unit.type))

        self.do_refresh_layout()
예제 #14
0
 def coalition(self, obj):
     bullseye = obj.bullseye if not None else {'x': 0, 'y': 0}
     bullseye = Point(bullseye['x'], bullseye['y'])
     return {
         'name': obj.name,
         'bullseye': self.point(bullseye),
         'countries': self.default(obj.countries)
     }
예제 #15
0
 def poly_to_leaflet(cls, poly: Polygon,
                     theater: ConflictTheater) -> LeafletPoly:
     if poly.is_empty:
         return []
     return [
         cls.latlng_to_leaflet(Point(x, y, theater.terrain).latlng())
         for x, y in poly.exterior.coords
     ]
 def draw_shapely_poly(self, scene: QGraphicsScene, poly: Polygon,
                       pen: QPen, brush: QBrush) -> Optional[QPolygonF]:
     if poly.is_empty:
         return None
     points = []
     for x, y in poly.exterior.coords:
         x, y = self._transform_point(Point(x, y))
         points.append(QPointF(x, y))
     return scene.addPolygon(QPolygonF(points), pen, brush)
예제 #17
0
    def sceneMouseMovedEvent(self, event: QGraphicsSceneMouseEvent):
        if self.game is None:
            return

        mouse_position = Point(event.scenePos().x(), event.scenePos().y())
        if self.state == QLiberationMapState.MOVING_UNIT:
            self.setCursor(Qt.PointingHandCursor)
            self.movement_line.setLine(
                QLineF(self.movement_line.line().p1(), event.scenePos())
            )

            if self.is_valid_ship_pos(mouse_position):
                self.movement_line.setPen(CONST.COLORS["green"])
            else:
                self.movement_line.setPen(CONST.COLORS["red"])

        mouse_world_pos = self._scene_to_dcs_coords(mouse_position)
        if DisplayOptions.navmeshes.blue_navmesh:
            self.highlight_mouse_navmesh(
                self.scene(),
                self.game.blue_navmesh,
                self._scene_to_dcs_coords(mouse_position),
            )
            if DisplayOptions.path_debug.shortest_path:
                self.draw_shortest_path(
                    self.scene(), self.game.blue_navmesh, mouse_world_pos, player=True
                )

        if DisplayOptions.navmeshes.red_navmesh:
            self.highlight_mouse_navmesh(
                self.scene(), self.game.red_navmesh, mouse_world_pos
            )

        debug_blue = DisplayOptions.path_debug_faction.blue
        if DisplayOptions.path_debug.shortest_path:
            self.draw_shortest_path(
                self.scene(),
                self.game.navmesh_for(player=debug_blue),
                mouse_world_pos,
                player=False,
            )
        elif not DisplayOptions.path_debug.hide:
            if DisplayOptions.path_debug.barcap:
                task = FlightType.BARCAP
            elif DisplayOptions.path_debug.cas:
                task = FlightType.CAS
            elif DisplayOptions.path_debug.sweep:
                task = FlightType.SWEEP
            elif DisplayOptions.path_debug.strike:
                task = FlightType.STRIKE
            elif DisplayOptions.path_debug.tarcap:
                task = FlightType.TARCAP
            else:
                raise ValueError("Unexpected value for DisplayOptions.path_debug")
            self.draw_test_flight_plan(
                self.scene(), task, mouse_world_pos, player=debug_blue
            )
    def _scaling_factor(self) -> Point:
        point_a = self.game.theater.reference_points[0]
        point_b = self.game.theater.reference_points[1]

        world_distance = self._transpose_point(point_b.world_coordinates -
                                               point_a.world_coordinates)
        image_distance = point_b.image_coordinates - point_a.image_coordinates

        x_scale = image_distance.x / world_distance.x
        y_scale = image_distance.y / world_distance.y
        return Point(x_scale, y_scale)
예제 #19
0
    def on_select_wpt_changed(self):
        super(QCASMissionGenerator, self).on_select_wpt_changed()
        wpts = self.wpt_selection_box.get_selected_waypoints()

        if len(wpts) > 0:
            self.distanceToTargetLabel.setText("~" + str(
                meter_to_nm(
                    self.flight.from_cp.position.distance_to_point(
                        Point(wpts[0].x, wpts[0].y)))) + " nm")
        else:
            self.distanceToTargetLabel.setText("??? nm")
예제 #20
0
    def sceneMouseMovedEvent(self, event: QGraphicsSceneMouseEvent):
        if self.state == QLiberationMapState.MOVING_UNIT:
            self.setCursor(Qt.PointingHandCursor)
            self.movement_line.setLine(
                QLineF(self.movement_line.line().p1(), event.scenePos()))

            pos = Point(event.scenePos().x(), event.scenePos().y())
            if self.is_valid_ship_pos(pos):
                self.movement_line.setPen(CONST.COLORS["green"])
            else:
                self.movement_line.setPen(CONST.COLORS["red"])
예제 #21
0
    def addBackground(self):
        scene = self.scene()

        if not DisplayOptions.map_poly:
            bg = QPixmap("./resources/" + self.game.theater.overview_image)
            scene.addPixmap(bg)

            # Apply graphical effects to simulate current daytime
            if self.game.current_turn_time_of_day == TimeOfDay.Day:
                pass
            elif self.game.current_turn_time_of_day == TimeOfDay.Night:
                ov = QPixmap(bg.width(), bg.height())
                ov.fill(CONST.COLORS["night_overlay"])
                overlay = scene.addPixmap(ov)
                effect = QGraphicsOpacityEffect()
                effect.setOpacity(0.7)
                overlay.setGraphicsEffect(effect)
            else:
                ov = QPixmap(bg.width(), bg.height())
                ov.fill(CONST.COLORS["dawn_dust_overlay"])
                overlay = scene.addPixmap(ov)
                effect = QGraphicsOpacityEffect()
                effect.setOpacity(0.3)
                overlay.setGraphicsEffect(effect)

        else:
            # Polygon display mode
            if self.game.theater.landmap is not None:

                for sea_zone in self.game.theater.landmap[2]:
                    print(sea_zone)
                    poly = QPolygonF([QPointF(*self._transform_point(Point(point[0], point[1]))) for point in sea_zone])
                    scene.addPolygon(poly, CONST.COLORS["sea_blue"], CONST.COLORS["sea_blue"])

                for inclusion_zone in self.game.theater.landmap[0]:
                    poly = QPolygonF([QPointF(*self._transform_point(Point(point[0], point[1]))) for point in inclusion_zone])
                    scene.addPolygon(poly, CONST.COLORS["grey"], CONST.COLORS["dark_grey"])

                for exclusion_zone in self.game.theater.landmap[1]:
                    poly = QPolygonF([QPointF(*self._transform_point(Point(point[0], point[1]))) for point in exclusion_zone])
                    scene.addPolygon(poly, CONST.COLORS["grey"], CONST.COLORS["dark_dark_grey"])
    def draw_threat_range(self, scene: QGraphicsScene, group: Group,
                          ground_object: TheaterGroundObject,
                          cp: ControlPoint) -> None:
        go_pos = self._transform_point(ground_object.position)
        detection_range = ground_object.detection_range(group)
        threat_range = ground_object.threat_range(group)
        if threat_range:
            threat_pos = self._transform_point(
                ground_object.position +
                Point(threat_range.meters, threat_range.meters))
            threat_radius = Point(*go_pos).distance_to_point(
                Point(*threat_pos))

            # Add threat range circle
            scene.addEllipse(go_pos[0] - threat_radius / 2 + 7,
                             go_pos[1] - threat_radius / 2 + 6, threat_radius,
                             threat_radius, self.threat_pen(cp.captured))

        if detection_range and DisplayOptions.detection_range:
            # Add detection range circle
            detection_pos = self._transform_point(
                ground_object.position +
                Point(detection_range.meters, detection_range.meters))
            detection_radius = Point(*go_pos).distance_to_point(
                Point(*detection_pos))
            scene.addEllipse(go_pos[0] - detection_radius / 2 + 7,
                             go_pos[1] - detection_radius / 2 + 6,
                             detection_radius, detection_radius,
                             self.detection_pen(cp.captured))
예제 #23
0
def destination_in_range(
    cp_id: UUID,
    lat: float,
    lng: float,
    game: Game = Depends(GameContext.require)) -> bool:
    cp = game.theater.find_control_point_by_id(cp_id)
    if cp is None:
        raise HTTPException(
            status.HTTP_404_NOT_FOUND,
            detail=f"Game has no control point with ID {cp_id}",
        )

    point = Point.from_latlng(LatLng(lat, lng), game.theater.terrain)
    return cp.destination_in_range(point)
    def display_navmesh(self, scene: QGraphicsScene, player: bool) -> None:
        for navpoly in self.game.navmesh_for(player).polys:
            self.draw_shapely_poly(scene, navpoly.poly, CONST.COLORS["black"],
                                   CONST.COLORS["transparent"])

            position = self._transform_point(
                Point(navpoly.poly.centroid.x, navpoly.poly.centroid.y))
            text = scene.addSimpleText(f"Navmesh {navpoly.ident}",
                                       self.waypoint_info_font)
            text.setBrush(QColor(255, 255, 255))
            text.setPen(QColor(255, 255, 255))
            text.moveBy(position[0] + 8, position[1])
            text.setZValue(2)

            for border in navpoly.neighbors.values():
                self.draw_navmesh_border(border, scene, navpoly.poly)
예제 #25
0
    def generate(self) -> None:

        if self.cp.captured:
            country_name = self.game.player_country
        else:
            country_name = self.game.enemy_country
        country = self.m.country(country_name)

        for i, helipad in enumerate(self.cp.helipads):
            name = self.cp.name + "_helipad_" + str(i)
            logging.info("Generating helipad : " + name)
            pad = SingleHeliPad(name=(name + "_unit"))
            pad.position = Point(helipad.x, helipad.y)
            pad.heading = helipad.heading
            # pad.heliport_frequency = self.radio_registry.alloc_uhf() TODO : alloc radio & callsign
            sg = unitgroup.StaticGroup(self.m.next_group_id(), name)
            sg.add_unit(pad)
            sp = StaticPoint()
            sp.position = pad.position
            sg.add_point(sp)
            country.add_static_group(sg)
예제 #26
0
    def setup_mission_coalitions(self) -> None:
        self.mission.coalition["blue"] = Coalition(
            "blue", bullseye=self.game.blue.bullseye.to_pydcs())
        self.mission.coalition["red"] = Coalition(
            "red", bullseye=self.game.red.bullseye.to_pydcs())
        self.mission.coalition["neutrals"] = Coalition(
            "neutrals",
            bullseye=Bullseye(Point(0, 0, self.mission.terrain)).to_pydcs())

        p_country = self.game.blue.country_name
        e_country = self.game.red.country_name
        self.mission.coalition["blue"].add_country(
            country_dict[country_id_from_name(p_country)]())
        self.mission.coalition["red"].add_country(
            country_dict[country_id_from_name(e_country)]())

        belligerents = [
            country_id_from_name(p_country),
            country_id_from_name(e_country),
        ]
        for country in country_dict.keys():
            if country not in belligerents:
                self.mission.coalition["neutrals"].add_country(
                    country_dict[country]())
예제 #27
0
    def __init__(
        self,
        target: Point,
        home: Point,
        coalition: Coalition,
    ) -> None:
        self._target = target
        self.threat_zone = coalition.opponent.threat_zone.all
        self.home = ShapelyPoint(home.x, home.y)

        max_ip_distance = coalition.doctrine.max_ingress_distance
        min_ip_distance = coalition.doctrine.min_ingress_distance

        # The minimum distance between the home location and the IP.
        min_distance_from_home = nautical_miles(5)

        # The distance that is expected to be needed between the beginning of the attack
        # and weapon release. This buffers the threat zone to give a 5nm window between
        # the edge of the "safe" zone and the actual threat so that "safe" IPs are less
        # likely to end up with the attacker entering a threatened area.
        attack_distance_buffer = nautical_miles(5)

        home_threatened = coalition.opponent.threat_zone.threatened(home)

        shapely_target = ShapelyPoint(target.x, target.y)
        home_to_target_distance = meters(home.distance_to_point(target))

        self.home_bubble = self.home.buffer(home_to_target_distance.meters).difference(
            self.home.buffer(min_distance_from_home.meters)
        )

        # If the home zone is not threatened and home is within LAR, constrain the max
        # range to the home-to-target distance to prevent excessive backtracking.
        #
        # If the home zone *is* threatened, we need to back out of the zone to
        # rendezvous anyway.
        if not home_threatened and (
            min_ip_distance < home_to_target_distance < max_ip_distance
        ):
            max_ip_distance = home_to_target_distance
        max_ip_bubble = shapely_target.buffer(max_ip_distance.meters)
        min_ip_bubble = shapely_target.buffer(min_ip_distance.meters)
        self.ip_bubble = max_ip_bubble.difference(min_ip_bubble)

        # The intersection of the home bubble and IP bubble will be all the points that
        # are within the valid IP range that are not farther from home than the target
        # is. However, if the origin airfield is threatened but there are safe
        # placements for the IP, we should not constrain to the home zone. In this case
        # we'll either end up with a safe zone outside the home zone and pick the
        # closest point in to to home (minimizing backtracking), or we'll have no safe
        # IP anywhere within range of the target, and we'll later pick the IP nearest
        # the edge of the threat zone.
        if home_threatened:
            self.permissible_zone = self.ip_bubble
        else:
            self.permissible_zone = self.ip_bubble.intersection(self.home_bubble)

        if self.permissible_zone.is_empty:
            # If home is closer to the target than the min range, there will not be an
            # IP solution that's close enough to home, in which case we need to ignore
            # the home bubble.
            self.permissible_zone = self.ip_bubble

        safe_zones = self.permissible_zone.difference(
            self.threat_zone.buffer(attack_distance_buffer.meters)
        )

        if not isinstance(safe_zones, MultiPolygon):
            safe_zones = MultiPolygon([safe_zones])
        self.safe_zones = safe_zones
    def addBackground(self):
        scene = self.scene()

        if not DisplayOptions.map_poly:
            bg = QPixmap("./resources/" + self.game.theater.overview_image)
            scene.addPixmap(bg)

            # Apply graphical effects to simulate current daytime
            if self.game.current_turn_time_of_day == TimeOfDay.Day:
                pass
            elif self.game.current_turn_time_of_day == TimeOfDay.Night:
                ov = QPixmap(bg.width(), bg.height())
                ov.fill(CONST.COLORS["night_overlay"])
                overlay = scene.addPixmap(ov)
                effect = QGraphicsOpacityEffect()
                effect.setOpacity(0.7)
                overlay.setGraphicsEffect(effect)
            else:
                ov = QPixmap(bg.width(), bg.height())
                ov.fill(CONST.COLORS["dawn_dust_overlay"])
                overlay = scene.addPixmap(ov)
                effect = QGraphicsOpacityEffect()
                effect.setOpacity(0.3)
                overlay.setGraphicsEffect(effect)

        if DisplayOptions.map_poly or self.reference_point_setup_mode:
            # Polygon display mode
            if self.game.theater.landmap is not None:

                for sea_zone in self.game.theater.landmap.sea_zones:
                    print(sea_zone)
                    poly = QPolygonF([
                        QPointF(
                            *self._transform_point(Point(point[0], point[1])))
                        for point in sea_zone.exterior.coords
                    ])
                    if self.reference_point_setup_mode:
                        color = "sea_blue_transparent"
                    else:
                        color = "sea_blue"
                    scene.addPolygon(poly, CONST.COLORS[color],
                                     CONST.COLORS[color])

                for inclusion_zone in self.game.theater.landmap.inclusion_zones:
                    poly = QPolygonF([
                        QPointF(
                            *self._transform_point(Point(point[0], point[1])))
                        for point in inclusion_zone.exterior.coords
                    ])
                    if self.reference_point_setup_mode:
                        scene.addPolygon(poly,
                                         CONST.COLORS["grey_transparent"],
                                         CONST.COLORS["dark_grey_transparent"])
                    else:
                        scene.addPolygon(poly, CONST.COLORS["grey"],
                                         CONST.COLORS["dark_grey"])

                for exclusion_zone in self.game.theater.landmap.exclusion_zones:
                    poly = QPolygonF([
                        QPointF(
                            *self._transform_point(Point(point[0], point[1])))
                        for point in exclusion_zone.exterior.coords
                    ])
                    if self.reference_point_setup_mode:
                        scene.addPolygon(
                            poly, CONST.COLORS["grey_transparent"],
                            CONST.COLORS["dark_dark_grey_transparent"])
                    else:
                        scene.addPolygon(poly, CONST.COLORS["grey"],
                                         CONST.COLORS["dark_dark_grey"])

        # Uncomment to display plan projection test
        # self.projection_test()
        self.draw_scale()

        if self.reference_point_setup_mode:
            for i, point in enumerate(self.game.theater.reference_points):
                self.scene().addRect(QRectF(point.image_coordinates.x,
                                            point.image_coordinates.y, 25, 25),
                                     pen=CONST.COLORS["red"],
                                     brush=CONST.COLORS["red"])
                text = self.scene().addText(
                    f"P{i} = {point.image_coordinates}",
                    font=QFont("Trebuchet MS", 14, weight=8, italic=False))
                text.setDefaultTextColor(CONST.COLORS["red"])
                text.setPos(point.image_coordinates.x + 26,
                            point.image_coordinates.y)

                # Set to True to visually debug _transform_point.
                draw_transformed = False
                if draw_transformed:
                    x, y = self._transform_point(point.world_coordinates)
                    self.scene().addRect(QRectF(x, y, 25, 25),
                                         pen=CONST.COLORS["red"],
                                         brush=CONST.COLORS["red"])
                    text = self.scene().addText(f"P{i}' = {x}, {y}",
                                                font=QFont("Trebuchet MS",
                                                           14,
                                                           weight=8,
                                                           italic=False))
                    text.setDefaultTextColor(CONST.COLORS["red"])
                    text.setPos(x + 26, y)
 def distance_to_pixels(self, distance: Distance) -> int:
     p1 = Point(0, 0)
     p2 = Point(0, distance.meters)
     p1a = Point(*self._transform_point(p1))
     p2a = Point(*self._transform_point(p2))
     return int(p1a.distance_to_point(p2a))
 def _transpose_point(p: Point) -> Point:
     return Point(p.y, p.x)