diff --git a/game/theater/conflicttheater.py b/game/theater/conflicttheater.py index 694b5415..d38e62a6 100644 --- a/game/theater/conflicttheater.py +++ b/game/theater/conflicttheater.py @@ -375,10 +375,16 @@ class MizCampaignLoader: self.theater.set_frontline_data(self.front_lines) +@dataclass +class ReferencePoint: + world_coordinates: Point + image_coordinates: Point + + class ConflictTheater: terrain: Terrain - reference_points: Dict[Tuple[float, float], Tuple[float, float]] + reference_points: Tuple[ReferencePoint, ReferencePoint] overview_image: str landmap: Optional[Landmap] """ @@ -593,8 +599,10 @@ class ConflictTheater: class CaucasusTheater(ConflictTheater): terrain = caucasus.Caucasus() overview_image = "caumap.gif" - reference_points = {(-317948.32727306, 635639.37385346): (278.5 * 4, 319 * 4), - (-355692.3067714, 617269.96285781): (263 * 4, 352 * 4), } + reference_points = ( + ReferencePoint(caucasus.Gelendzhik.position, Point(176, 298)), + ReferencePoint(caucasus.Batumi.position, Point(1307, 1205)), + ) landmap = load_landmap("resources\\caulandmap.p") daytime_map = { @@ -608,10 +616,11 @@ class CaucasusTheater(ConflictTheater): class PersianGulfTheater(ConflictTheater): terrain = persiangulf.PersianGulf() overview_image = "persiangulf.gif" - reference_points = { - (persiangulf.Shiraz_International_Airport.position.x, persiangulf.Shiraz_International_Airport.position.y): ( - 772, -1970), - (persiangulf.Liwa_Airbase.position.x, persiangulf.Liwa_Airbase.position.y): (1188, 78), } + reference_points = ( + ReferencePoint(persiangulf.Jiroft_Airport.position, + Point(1692, 1343)), + ReferencePoint(persiangulf.Liwa_Airbase.position, Point(358, 3238)), + ) landmap = load_landmap("resources\\gulflandmap.p") daytime_map = { "dawn": (6, 8), @@ -620,11 +629,14 @@ class PersianGulfTheater(ConflictTheater): "night": (0, 5), } + class NevadaTheater(ConflictTheater): terrain = nevada.Nevada() overview_image = "nevada.gif" - reference_points = {(nevada.Mina_Airport_3Q0.position.x, nevada.Mina_Airport_3Q0.position.y): (45 * 2, -360 * 2), - (nevada.Laughlin_Airport.position.x, nevada.Laughlin_Airport.position.y): (440 * 2, 80 * 2), } + reference_points = ( + ReferencePoint(nevada.Mina_Airport_3Q0.position, Point(252, 295)), + ReferencePoint(nevada.Laughlin_Airport.position, Point(844, 909)), + ) landmap = load_landmap("resources\\nevlandmap.p") daytime_map = { "dawn": (4, 6), @@ -633,11 +645,14 @@ class NevadaTheater(ConflictTheater): "night": (0, 5), } + class NormandyTheater(ConflictTheater): terrain = normandy.Normandy() overview_image = "normandy.gif" - reference_points = {(normandy.Needs_Oar_Point.position.x, normandy.Needs_Oar_Point.position.y): (330, -970), - (normandy.Evreux.position.x, normandy.Evreux.position.y): (1780, 520)} + reference_points = ( + ReferencePoint(normandy.Needs_Oar_Point.position, Point(515, 329)), + ReferencePoint(normandy.Evreux.position, Point(2029, 1709)), + ) landmap = load_landmap("resources\\normandylandmap.p") daytime_map = { "dawn": (6, 8), @@ -646,11 +661,14 @@ class NormandyTheater(ConflictTheater): "night": (0, 5), } + class TheChannelTheater(ConflictTheater): terrain = thechannel.TheChannel() overview_image = "thechannel.gif" - reference_points = {(thechannel.Abbeville_Drucat.position.x, thechannel.Abbeville_Drucat.position.y): (2400, 4100), - (thechannel.Detling.position.x, thechannel.Detling.position.y): (1100, 2000)} + reference_points = ( + ReferencePoint(thechannel.Abbeville_Drucat.position, Point(2005, 2390)), + ReferencePoint(thechannel.Detling.position, Point(706, 382)) + ) landmap = load_landmap("resources\\channellandmap.p") daytime_map = { "dawn": (6, 8), @@ -659,11 +677,14 @@ class TheChannelTheater(ConflictTheater): "night": (0, 5), } + class SyriaTheater(ConflictTheater): terrain = syria.Syria() overview_image = "syria.gif" - reference_points = {(syria.Eyn_Shemer.position.x, syria.Eyn_Shemer.position.y): (1300, 1380), - (syria.Tabqa.position.x, syria.Tabqa.position.y): (2060, 570)} + reference_points = ( + ReferencePoint(syria.Eyn_Shemer.position, Point(564, 1289)), + ReferencePoint(syria.Tabqa.position, Point(1329, 491)), + ) landmap = load_landmap("resources\\syrialandmap.p") daytime_map = { "dawn": (6, 8), @@ -672,6 +693,7 @@ class SyriaTheater(ConflictTheater): "night": (0, 5), } + @dataclass class ComplexFrontLine: """ diff --git a/qt_ui/widgets/map/QLiberationMap.py b/qt_ui/widgets/map/QLiberationMap.py index e34fc3b6..58c7ad19 100644 --- a/qt_ui/widgets/map/QLiberationMap.py +++ b/qt_ui/widgets/map/QLiberationMap.py @@ -28,7 +28,7 @@ from dcs.mapping import point_from_heading import qt_ui.uiconstants as CONST from game import Game, db from game.theater import ControlPoint, Enum -from game.theater.conflicttheater import FrontLine +from game.theater.conflicttheater import FrontLine, ReferencePoint from game.theater.theatergroundobject import ( TheaterGroundObject, ) @@ -190,37 +190,55 @@ class QLiberationMap(QGraphicsView): self.reference_point_setup_mode = False self.reload_scene() else: - numpad_mod = int(event.modifiers()) & QtCore.Qt.KeypadModifier - i = 0 - for k,v in self.game.theater.reference_points.items(): - if i == 0: - point_0 = k - else: - point_1 = k - i = i + 1 + 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.game.theater.reference_points[point_0] = self.game.theater.reference_points[point_0][0] + 10, self.game.theater.reference_points[point_0][1] + self.update_reference_point( + self.game.theater.reference_points[0], + Point(0, distance)) if event.key() == QtCore.Qt.Key_Up: - self.game.theater.reference_points[point_0] = self.game.theater.reference_points[point_0][0] - 10, self.game.theater.reference_points[point_0][1] + self.update_reference_point( + self.game.theater.reference_points[0], + Point(0, -distance)) if event.key() == QtCore.Qt.Key_Left: - self.game.theater.reference_points[point_0] = self.game.theater.reference_points[point_0][0], self.game.theater.reference_points[point_0][1] + 10 + self.update_reference_point( + self.game.theater.reference_points[0], + Point(-distance, 0)) if event.key() == QtCore.Qt.Key_Right: - self.game.theater.reference_points[point_0] = self.game.theater.reference_points[point_0][0], self.game.theater.reference_points[point_0][1] - 10 + 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)) - if event.key() == QtCore.Qt.Key_2 and numpad_mod: - self.game.theater.reference_points[point_1] = self.game.theater.reference_points[point_1][0] + 10, self.game.theater.reference_points[point_1][1] - if event.key() == QtCore.Qt.Key_8 and numpad_mod: - self.game.theater.reference_points[point_1] = self.game.theater.reference_points[point_1][0] - 10, self.game.theater.reference_points[point_1][1] - if event.key() == QtCore.Qt.Key_4 and numpad_mod: - self.game.theater.reference_points[point_1] = self.game.theater.reference_points[point_1][0], self.game.theater.reference_points[point_1][1] + 10 - if event.key() == QtCore.Qt.Key_6 and numpad_mod: - self.game.theater.reference_points[point_1] = self.game.theater.reference_points[point_1][0], self.game.theater.reference_points[point_1][1] - 10 - - print(self.game.theater.reference_points) + logging.debug( + f"Reference points: {self.game.theater.reference_points}") self.reload_scene() + @staticmethod + def update_reference_point(point: ReferencePoint, change: Point) -> None: + point.image_coordinates += change + @staticmethod def aa_ranges(ground_object: TheaterGroundObject) -> Tuple[int, int]: detection_range = 0 @@ -590,51 +608,60 @@ class QLiberationMap(QGraphicsView): else: self._zoom = -4 - def _transform_point(self, p: Point) -> Tuple[int, int]: - point_a = list(self.game.theater.reference_points.keys())[0] - point_a_img = self.game.theater.reference_points[point_a] + @staticmethod + def _transpose_point(p: Point) -> Point: + return Point(p.y, p.x) - point_b = list(self.game.theater.reference_points.keys())[1] - point_b_img = self.game.theater.reference_points[point_b] + def _scaling_factor(self) -> Point: + point_a = self.game.theater.reference_points[0] + point_b = self.game.theater.reference_points[1] - Y_dist = point_a_img[0] - point_b_img[0] - lon_dist = point_a[1] - point_b[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_dist = point_a_img[1] - point_b_img[1] - lat_dist = point_b[0] - point_a[0] + x_scale = image_distance.x / world_distance.x + y_scale = image_distance.y / world_distance.y + return Point(x_scale, y_scale) - Y_scale = float(Y_dist) / float(lon_dist) - X_scale = float(X_dist) / float(lat_dist) + # TODO: Move this and its inverse into ConflictTheater. + def _transform_point(self, world_point: Point) -> Tuple[float, float]: + """Transforms world coordinates to image coordinates. - # --- - Y_offset = p.x - point_a[0] - X_offset = p.y - point_a[1] + World coordinates are transposed. X increases toward the North, Y + increases toward the East. The origin point depends on the map. - X = point_b_img[1] + X_offset * X_scale - Y = point_a_img[0] - Y_offset * Y_scale + Image coordinates originate from the top left. X increases to the right, + Y increases toward the bottom. - return X, Y + 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. - def _scene_to_dcs_coords(self, p: Point): - pa = list(self.game.theater.reference_points.keys())[0] - pa2 = self.game.theater.reference_points[pa] + 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. - pb = list(self.game.theater.reference_points.keys())[1] - pb2 = self.game.theater.reference_points[pb] + X is latitude, increasing northward. + Y is longitude, increasing eastward. + """ + point_a = self.game.theater.reference_points[0] + scale = self._scaling_factor() - dy2 = pa2[0] - pb2[0] - dy = pa[1] - pb[1] + 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 - dx2 = pa2[1] - pb2[1] - dx = pb[0] - pa[0] + def _scene_to_dcs_coords(self, scene_point: Point) -> Point: + point_a = self.game.theater.reference_points[0] + scale = self._scaling_factor() - ys = float(dy2) / float(dy) - xs = float(dx2) / float(dx) - - X = ((float(p.x - pb2[1])) / float(xs)) + pa[1] - Y = ((float(pa2[0] - p.y)) / float(ys)) + pa[0] - - return Y, X + 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 km_to_pixel(self, km): p1 = Point(0, 0) @@ -739,38 +766,47 @@ class QLiberationMap(QGraphicsView): scene.addPolygon(poly, CONST.COLORS["grey"], CONST.COLORS["dark_dark_grey"]) # Uncomment to display plan projection test - #self.projection_test(scene) + # self.projection_test() self.draw_scale() if self.reference_point_setup_mode: - for i, r in enumerate(self.game.theater.reference_points.values()): - self.scene().addRect(QRectF(r[0], r[1], 25, 25), pen=CONST.COLORS["red"], brush=CONST.COLORS["red"]) - text = self.scene().addText("P{0} = {1}, {2}".format(i, r[0], r[1]), - font=QFont("Trebuchet MS", 14, weight=8, italic=False)) + 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(r[0]+26, r[1]) + text.setPos(point.image_coordinates.x + 26, + point.image_coordinates.y) - def projection_test(self, scene): + # 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 projection_test(self): for i in range(100): for j in range(100): - x = i * 100 - y = j * 100 - self.scene().addRect(QRectF(x, y, 4, 4), CONST.COLORS["green"]) - proj = self._scene_to_dcs_coords(Point(x, y)) - unproj = self._transform_point(Point(proj[0], proj[1])) - text = scene.addText(str(i) + ", " + str(j) + "\n" + str(unproj[0]) + ", " + str(unproj[1]), - font=QFont("Trebuchet MS", 6, weight=5, italic=False)) - text.setPos(unproj[0] + 2, unproj[1] + 2) - text.setDefaultTextColor(Qt.red) - text2 = scene.addText(str(i) + ", " + str(j) + "\n" + str(x) + ", " + str(y), - font=QFont("Trebuchet MS", 6, weight=5, italic=False)) - text2.setPos(x + 2, y + 10) - text2.setDefaultTextColor(Qt.green) - self.scene().addRect(QRectF(unproj[0] + 1, unproj[1] + 1, 4, 4), CONST.COLORS["red"]) - if i % 2 == 0: - self.scene().addLine(QLineF(x + 1, y + 1, unproj[0], unproj[1]), CONST.COLORS["yellow"]) - else: - self.scene().addLine(QLineF(x + 1, y + 1, unproj[0], unproj[1]), CONST.COLORS["purple"]) + 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 setSelectedUnit(self, selected_cp: QMapControlPoint): self.state = QLiberationMapState.MOVING_UNIT @@ -779,23 +815,27 @@ class QLiberationMap(QGraphicsView): self.movement_line = QtWidgets.QGraphicsLineItem(QLineF(QPointF(*position), QPointF(*position))) self.scene().addItem(self.movement_line) + def is_valid_ship_pos(self, scene_position: Point) -> bool: + world_destination = self._scene_to_dcs_coords(scene_position) + distance = self.selected_cp.control_point.position.distance_to_point( + world_destination + ) + if meter_to_nm(distance) > MAX_SHIP_DISTANCE: + return False + return self.game.theater.is_in_sea(world_destination) + def sceneMouseMovedEvent(self, event: QGraphicsSceneMouseEvent): if self.state == QLiberationMapState.MOVING_UNIT: self.setCursor(Qt.PointingHandCursor) - pos = event.scenePos() - p1 = self.movement_line.line().p1() + self.movement_line.setLine( + QLineF(self.movement_line.line().p1(), event.scenePos())) - distance = Point(p1.x(), p1.y()).distance_to_point(Point(pos.x(), pos.y())) - - self.movement_line.setLine(QLineF(p1, pos)) - - if distance / self.nm_to_pixel_ratio < MAX_SHIP_DISTANCE: + 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"]) - - def sceneMousePressEvent(self, event: QGraphicsSceneMouseEvent): if self.state == QLiberationMapState.MOVING_UNIT: if event.buttons() == Qt.RightButton: @@ -805,13 +845,9 @@ class QLiberationMap(QGraphicsView): # Set movement position for the cp pos = event.scenePos() point = Point(int(pos.x()), int(pos.y())) - proj = Point(*self._scene_to_dcs_coords(point)) + proj = self._scene_to_dcs_coords(point) - # Check distance (max = 80 nm) - distance = meter_to_nm(proj.distance_to_point(self.selected_cp.control_point.position)) - - # Check if point is in sea - if self.game.theater.is_in_sea(proj) and distance < MAX_SHIP_DISTANCE: + if self.is_valid_ship_pos(point): self.selected_cp.control_point.target_position = proj else: self.selected_cp.control_point.target_position = None