class WayPoint: def __init__(self, **kwargs): super().__init__() self.location = MapPoint() self.__dict__.update(kwargs) self.pixmap = QGraphicsPixmapItem( QPixmap('HOME_DIR + /nparse/data/maps/waypoint.png')) self.pixmap.setOffset(-10, -20) self.line = QGraphicsLineItem(0.0, 0.0, self.location.x, self.location.y) self.line.setPen(QPen(Qt.green, 1, Qt.DashLine)) self.line.setVisible(False) self.pixmap.setZValue(5) self.line.setZValue(4) self.pixmap.setPos(self.location.x, self.location.y) def update_(self, scale, location=None): self.pixmap.setScale(scale) if location: line = self.line.line() line.setP1(QPointF(location.x, location.y)) self.line.setLine(line) pen = self.line.pen() pen.setWidth(1 / scale) self.line.setPen(pen) self.line.setVisible(True)
class ImageViewer(QGraphicsView, QObject): points_selection_sgn = pyqtSignal(list) key_press_sgn = pyqtSignal(QtGui.QKeyEvent) def __init__(self, parent=None): super(ImageViewer, self).__init__(parent) self.setDragMode(QGraphicsView.ScrollHandDrag) self.setRenderHints(QPainter.Antialiasing | QPainter.SmoothPixmapTransform) self.setTransformationAnchor(QGraphicsView.AnchorUnderMouse) self.setResizeAnchor(QGraphicsView.AnchorUnderMouse) self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded) self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded) self._scene = ImageViewerScene(self) self.setScene(self._scene) self._image = None self._image_original = None self._pixmap = None self._img_contrast = 1.0 self._img_brightness = 50.0 self._img_gamma = 1.0 self._create_grid() self._channels = [] self._current_tool = SELECTION_TOOL.POINTER self._dataset = None # create grid lines pen_color = QColor(255, 255, 255, 255) pen = QPen(pen_color) pen.setWidth(2) pen.setStyle(QtCore.Qt.DotLine) self.vline = QGraphicsLineItem() self.vline.setVisible(False) self.vline.setPen(pen) self.hline = QGraphicsLineItem() self.hline.setVisible(False) self.hline.setPen(pen) self._scene.addItem(self.vline) self._scene.addItem(self.hline) self._current_label = None # rectangle selection tool self._rectangle_tool_origin = QPoint() self._rectangle_tool_picker = QRubberBand(QRubberBand.Rectangle, self) # polygon selection tool app = QApplication.instance() color = app.palette().color(QPalette.Highlight) self._polygon_guide_line_pen = QPen(color) self._polygon_guide_line_pen.setWidth(3) self._polygon_guide_line_pen.setStyle(QtCore.Qt.DotLine) self._polygon_guide_line = QGraphicsLineItem() self._polygon_guide_line.setVisible(False) self._polygon_guide_line.setPen(self._polygon_guide_line_pen) self._scene.addItem(self._polygon_guide_line) self._current_polygon = None # circle self._current_ellipse = None # free selection tool self._current_free_path = None self._is_drawing = False self._last_point_drawn = QPoint() self._last_click_point = None self._free_Path_pen = QPen(color) self._free_Path_pen.setWidth(10) self._extreme_points = Queue(maxsize=4) @property def current_label(self): return self._current_label @current_label.setter def current_label(self, value): self._current_label = value if self._current_label: color = QColor(self._current_label.color) self._free_Path_pen.setColor(color) self._polygon_guide_line_pen.setColor(color) self._polygon_guide_line.setPen(self._polygon_guide_line_pen) @property def dataset(self): return self._dataset @dataset.setter def dataset(self, value): self._dataset = value @property def img_contrast(self): return self._img_contrast @img_contrast.setter def img_contrast(self, value): self._img_contrast = value @property def img_gamma(self): return self._img_gamma @img_gamma.setter def img_gamma(self, value): self._img_gamma = value @property def img_brightness(self): return self._img_brightness @img_brightness.setter def img_brightness(self, value): self._img_brightness = value @property def image(self): return self._image @image.setter def image(self, value): self._image = value self._image_original = value.copy() self.update_viewer() @property def pixmap(self) -> ImagePixmap: return self._pixmap @gui_exception def update_viewer(self, fit_image=True): rgb = cv2.cvtColor(self._image, cv2.COLOR_BGR2RGB) rgb = ImageUtilities.adjust_image(rgb, self._img_contrast, self._img_brightness) rgb = ImageUtilities.adjust_gamma(rgb, self._img_gamma) pil_image = Image.fromarray(rgb) qppixmap_image = pil_image.toqpixmap() x, y = -qppixmap_image.width() / 2, -qppixmap_image.height() / 2 if self._pixmap: self._pixmap.resetTransform() self._pixmap.setPixmap(qppixmap_image) self._pixmap.setOffset(x, y) else: self._pixmap = ImagePixmap() self._pixmap.setPixmap(qppixmap_image) self._pixmap.setOffset(x, y) self._scene.addItem(self._pixmap) self._pixmap.signals.hoverEnterEventSgn.connect( self.pixmap_hoverEnterEvent_slot) self._pixmap.signals.hoverLeaveEventSgn.connect( self.pixmap_hoverLeaveEvent_slot) self._pixmap.signals.hoverMoveEventSgn.connect( self.pixmap_hoverMoveEvent_slot) self._hide_guide_lines() if fit_image: self.fit_to_window() @gui_exception def reset_viewer(self): self._img_contrast = 1.0 self._img_brightness = 50.0 self._img_gamma = 1.0 self._image = self._image_original.copy() @gui_exception def equalize_histogram(self): self._image = ImageUtilities.histogram_equalization(self._image) @gui_exception def correct_lightness(self): self._image = ImageUtilities.correct_lightness(self._image) def clusterize(self, k): self._image = ImageUtilities.kmeans(self._image.copy(), k) @property def current_tool(self): return self._current_tool @current_tool.setter def current_tool(self, value): self._polygon_guide_line.hide() self._current_polygon = None self._current_free_path = None self._current_ellipse = None self._is_drawing = value == SELECTION_TOOL.FREE self._current_tool = value self.clear_extreme_points() if value == SELECTION_TOOL.POINTER: self.enable_items(True) else: self.enable_items(False) def fit_to_window(self): if not self._pixmap or not self._pixmap.pixmap(): return self.resetTransform() self.setTransform(QtGui.QTransform()) self.fitInView(self._pixmap, QtCore.Qt.KeepAspectRatio) def _create_grid(self, gridSize=15): app: QApplication = QApplication.instance() curr_theme = "dark" if app: curr_theme = app.property("theme") if curr_theme == "light": color1 = QtGui.QColor("white") color2 = QtGui.QColor(237, 237, 237) else: color1 = QtGui.QColor(20, 20, 20) color2 = QtGui.QColor(0, 0, 0) backgroundPixmap = QtGui.QPixmap(gridSize * 2, gridSize * 2) backgroundPixmap.fill(color1) painter = QtGui.QPainter(backgroundPixmap) painter.fillRect(0, 0, gridSize, gridSize, color2) painter.fillRect(gridSize, gridSize, gridSize, gridSize, color2) painter.end() self._scene.setBackgroundBrush(QtGui.QBrush(backgroundPixmap)) def wheelEvent(self, event: QWheelEvent): adj = (event.angleDelta().y() / 120) * 0.1 self.scale(1 + adj, 1 + adj) @gui_exception def keyPressEvent(self, event: QKeyEvent): if event.key() == QtCore.Qt.Key_Space: image_rect: QRectF = self._pixmap.sceneBoundingRect() if self.current_tool == SELECTION_TOOL.POLYGON and self._current_polygon: points = self._current_polygon.points self._polygon_guide_line.hide() self.setDragMode(QGraphicsView.ScrollHandDrag) if len(points) <= 2: self._current_polygon.delete_item() self.current_tool = SELECTION_TOOL.POINTER elif self.current_tool == SELECTION_TOOL.EXTREME_POINTS and \ self._extreme_points.full(): points = [] image_offset = QPointF(image_rect.width() / 2, image_rect.height() / 2) for pt in self._extreme_points.queue: pt: EditablePolygonPoint center = pt.sceneBoundingRect().center() x = math.floor(center.x() + image_offset.x()) y = math.floor(center.y() + image_offset.y()) points.append([x, y]) self.points_selection_sgn.emit(points) self.current_tool = SELECTION_TOOL.POINTER else: event.ignore() # guide lines events def _show_guide_lines(self): if self.hline and self.vline: self.hline.show() self.vline.show() def _hide_guide_lines(self): if self.hline and self.vline: self.hline.hide() self.vline.hide() def _update_guide_lines(self, x, y): bbox: QRect = self._pixmap.boundingRect() offset = QPointF(bbox.width() / 2, bbox.height() / 2) self.vline.setLine(x, -offset.y(), x, bbox.height() - offset.y()) self.vline.setZValue(1) self.hline.setLine(-offset.x(), y, bbox.width() - offset.x(), y) self.hline.setZValue(1) def pixmap_hoverMoveEvent_slot(self, evt: QGraphicsSceneHoverEvent, x, y): self._update_guide_lines(x, y) def pixmap_hoverEnterEvent_slot(self): self._show_guide_lines() def pixmap_hoverLeaveEvent_slot(self): self._hide_guide_lines() def delete_polygon_slot(self, polygon: EditablePolygon): self._current_polygon = None self.current_tool = SELECTION_TOOL.POINTER self._polygon_guide_line.hide() @gui_exception def mousePressEvent(self, evt: QtGui.QMouseEvent) -> None: image_rect: QRectF = self._pixmap.boundingRect() mouse_pos = self.mapToScene(evt.pos()) if evt.buttons() == QtCore.Qt.LeftButton: if self.current_tool == SELECTION_TOOL.BOX: # create rectangle self.setDragMode(QGraphicsView.NoDrag) self._rectangle_tool_origin = evt.pos() geometry = QRect(self._rectangle_tool_origin, QSize()) self._rectangle_tool_picker.setGeometry(geometry) self._rectangle_tool_picker.show() elif self.current_tool == SELECTION_TOOL.POLYGON: if image_rect.contains(mouse_pos): if self._current_polygon is None: self._current_polygon = EditablePolygon() self._current_polygon.label = self._current_label self._current_polygon.tag = self._dataset self._current_polygon.signals.deleted.connect( self.delete_polygon_slot) self._scene.addItem(self._current_polygon) self._current_polygon.addPoint(mouse_pos) else: self._current_polygon.addPoint(mouse_pos) elif self.current_tool == SELECTION_TOOL.ELLIPSE: if image_rect.contains(mouse_pos): self.setDragMode(QGraphicsView.NoDrag) ellipse_rec = QtCore.QRectF(mouse_pos.x(), mouse_pos.y(), 0, 0) self._current_ellipse = EditableEllipse() self._current_ellipse.tag = self.dataset self._current_ellipse.label = self._current_label self._current_ellipse.setRect(ellipse_rec) self._scene.addItem(self._current_ellipse) elif self.current_tool == SELECTION_TOOL.FREE: # consider only the points into the image if image_rect.contains(mouse_pos): self.setDragMode(QGraphicsView.NoDrag) self._last_point_drawn = mouse_pos self._current_free_path = QGraphicsPathItem() self._current_free_path.setOpacity(0.6) self._current_free_path.setPen(self._free_Path_pen) painter = QPainterPath() painter.moveTo(self._last_point_drawn) self._current_free_path.setPath(painter) self._scene.addItem(self._current_free_path) elif self.current_tool == SELECTION_TOOL.EXTREME_POINTS: if image_rect.contains(mouse_pos): if not self._extreme_points.full(): def delete_point(idx): del self._extreme_points.queue[idx] idx = self._extreme_points.qsize() editable_pt = EditablePolygonPoint(idx) editable_pt.signals.deleted.connect(delete_point) editable_pt.setPos(mouse_pos) self._scene.addItem(editable_pt) self._extreme_points.put(editable_pt) else: self.setDragMode(QGraphicsView.ScrollHandDrag) super(ImageViewer, self).mousePressEvent(evt) @gui_exception def mouseMoveEvent(self, evt: QtGui.QMouseEvent) -> None: mouse_pos = self.mapToScene(evt.pos()) image_rect: QRectF = self._pixmap.boundingRect() if self.current_tool == SELECTION_TOOL.BOX: if not self._rectangle_tool_origin.isNull(): geometry = QRect(self._rectangle_tool_origin, evt.pos()).normalized() self._rectangle_tool_picker.setGeometry(geometry) elif self.current_tool == SELECTION_TOOL.POLYGON: if self._current_polygon and image_rect.contains(mouse_pos): if self._current_polygon.count > 0: last_point: QPointF = self._current_polygon.last_point self._polygon_guide_line.setZValue(1) self._polygon_guide_line.show() mouse_pos = self.mapToScene(evt.pos()) self._polygon_guide_line.setLine(last_point.x(), last_point.y(), mouse_pos.x(), mouse_pos.y()) else: self._polygon_guide_line.hide() elif self.current_tool == SELECTION_TOOL.ELLIPSE: if self._current_ellipse and image_rect.contains(mouse_pos): ellipse_rect = self._current_ellipse.rect() ellipse_pos = QPointF(ellipse_rect.x(), ellipse_rect.y()) distance = math.hypot(mouse_pos.x() - ellipse_pos.x(), mouse_pos.y() - ellipse_pos.y()) ellipse_rect.setWidth(distance) ellipse_rect.setHeight(distance) self._current_ellipse.setRect(ellipse_rect) elif self.current_tool == SELECTION_TOOL.FREE and evt.buttons( ) and QtCore.Qt.LeftButton: if self._current_free_path and image_rect.contains(mouse_pos): painter: QPainterPath = self._current_free_path.path() self._last_point_drawn = self.mapToScene(evt.pos()) painter.lineTo(self._last_point_drawn) self._current_free_path.setPath(painter) super(ImageViewer, self).mouseMoveEvent(evt) @gui_exception def mouseReleaseEvent(self, evt: QtGui.QMouseEvent) -> None: image_rect: QRectF = self._pixmap.boundingRect() if self.current_tool == SELECTION_TOOL.BOX: roi: QRect = self._rectangle_tool_picker.geometry() roi: QRectF = self.mapToScene(roi).boundingRect() self._rectangle_tool_picker.hide() if image_rect == roi.united(image_rect): rect = EditableBox(roi) rect.label = self.current_label rect.tag = self._dataset self._scene.addItem(rect) self.current_tool = SELECTION_TOOL.POINTER self.setDragMode(QGraphicsView.ScrollHandDrag) elif self.current_tool == SELECTION_TOOL.ELLIPSE and self._current_ellipse: roi: QRect = self._current_ellipse.boundingRect() if image_rect == roi.united(image_rect): self.current_tool = SELECTION_TOOL.POINTER self.setDragMode(QGraphicsView.ScrollHandDrag) else: self._current_ellipse.delete_item() elif self.current_tool == SELECTION_TOOL.FREE and self._current_free_path: # create polygon self._current_free_path: QGraphicsPathItem path_rect = self._current_free_path.boundingRect() if image_rect == path_rect.united(image_rect): path = self._current_free_path.path() path_polygon = EditablePolygon() path_polygon.tag = self.dataset path_polygon.label = self.current_label self._scene.addItem(path_polygon) for i in range(0, path.elementCount(), 10): x, y = path.elementAt(i).x, path.elementAt(i).y path_polygon.addPoint(QPointF(x, y)) self._scene.removeItem(self._current_free_path) self.current_tool = SELECTION_TOOL.POINTER self.setDragMode(QGraphicsView.ScrollHandDrag) super(ImageViewer, self).mouseReleaseEvent(evt) def remove_annotations(self): for item in self._scene.items(): if isinstance(item, EditableItem): item.delete_item() def remove_annotations_by_label(self, label_name): for item in self._scene.items(): if isinstance(item, EditableItem): if item.label and item.label.name == label_name: item.delete_item() def enable_items(self, value): for item in self._scene.items(): if isinstance(item, EditableItem): item.setEnabled(value) def clear_extreme_points(self): if self._extreme_points.qsize() > 0: for pt in self._extreme_points.queue: self._scene.removeItem(pt) self._extreme_points.queue.clear()
class ImageViewer(QGraphicsView, QObject): def __init__(self, parent=None): super(ImageViewer, self).__init__(parent) self.setRenderHints(QPainter.Antialiasing | QPainter.SmoothPixmapTransform) #self.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignTop) self.setTransformationAnchor(QGraphicsView.AnchorUnderMouse) self.setDragMode(QGraphicsView.ScrollHandDrag) #self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded) #self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded) self._scene = ImageViewerScene(self) self.setScene(self._scene) self._create_grid() self._create_grid_lines() self._pixmap = None self._selection_mode = SELECTION_MODE.NONE # polygon selection _polygon_guide_line_pen = QPen(QtGui.QColor(235, 72, 40)) _polygon_guide_line_pen.setWidth(2) _polygon_guide_line_pen.setStyle(QtCore.Qt.DotLine) self._polygon_guide_line = QGraphicsLineItem() self._polygon_guide_line.setVisible(False) self._polygon_guide_line.setPen(_polygon_guide_line_pen) self._scene.addItem(self._polygon_guide_line) self._current_polygon = None # rectangle selection self._box_origin = QPoint() self._box_picker = QRubberBand(QRubberBand.Rectangle, self) # free selection self._current_free_path = None self._is_drawing = False self._last_point_drawn = QPoint() self._current_label = None @property def current_label(self): return self._current_label @current_label.setter def current_label(self, value): self._current_label = value @property def pixmap(self) -> ImagePixmap: return self._pixmap @pixmap.setter def pixmap(self, value: QPixmap): self.selection_mode = SELECTION_MODE.NONE self.resetTransform() if self.pixmap: self._scene.removeItem(self._pixmap) self.remove_annotations() self._pixmap = ImagePixmap() self._pixmap.setPixmap(value) self._pixmap.setOffset(-value.width() / 2, -value.height() / 2) self._pixmap.setTransformationMode(QtCore.Qt.SmoothTransformation) self._pixmap.signals.hoverEnterEventSgn.connect( self.pixmap_hoverEnterEvent_slot) self._pixmap.signals.hoverLeaveEventSgn.connect( self.pixmap_hoverLeaveEvent_slot) self._pixmap.signals.hoverMoveEventSgn.connect( self.pixmap_hoverMoveEvent_slot) self._scene.addItem(self._pixmap) # rect=self._scene.addRect(QtCore.QRectF(0,0,100,100), QtGui.QPen(QtGui.QColor("red"))) # rect.setZValue(1.0) self.fit_to_window() @property def selection_mode(self): return self._selection_mode @selection_mode.setter def selection_mode(self, value): self._polygon_guide_line.hide() self._current_polygon = None self._current_free_path = None self._is_drawing = value == SELECTION_MODE.FREE if value == SELECTION_MODE.NONE: self.enable_items(True) else: self.enable_items(False) self._selection_mode = value def remove_annotations(self): for item in self._scene.items(): if isinstance(item, EditableBox): self._scene.removeItem(item) elif isinstance(item, EditablePolygon): item.delete_polygon() def remove_annotations_by_label(self, label_name): for item in self._scene.items(): if isinstance(item, EditableBox): if item.label and item.label.name == label_name: self._scene.removeItem(item) elif isinstance(item, EditablePolygon): if item.label and item.label.name == label_name: item.delete_polygon() def enable_items(self, value): for item in self._scene.items(): if isinstance(item, EditablePolygon) or isinstance( item, EditableBox): item.setEnabled(value) def _create_grid(self): gridSize = 15 backgroundPixmap = QtGui.QPixmap(gridSize * 2, gridSize * 2) #backgroundPixmap.fill(QtGui.QColor("white")) backgroundPixmap.fill(QtGui.QColor(20, 20, 20)) #backgroundPixmap.fill(QtGui.QColor("powderblue")) painter = QtGui.QPainter(backgroundPixmap) #backgroundColor=QtGui.QColor("palegoldenrod") #backgroundColor=QtGui.QColor(237,237,237) backgroundColor = QtGui.QColor(0, 0, 0) painter.fillRect(0, 0, gridSize, gridSize, backgroundColor) painter.fillRect(gridSize, gridSize, gridSize, gridSize, backgroundColor) painter.end() self._scene.setBackgroundBrush(QtGui.QBrush(backgroundPixmap)) def _create_grid_lines(self): pen_color = QColor(255, 255, 255, 255) pen = QPen(pen_color) pen.setWidth(2) pen.setStyle(QtCore.Qt.DotLine) self.vline = QGraphicsLineItem() self.vline.setVisible(False) self.vline.setPen(pen) self.hline = QGraphicsLineItem() self.hline.setVisible(False) self.hline.setPen(pen) self._scene.addItem(self.vline) self._scene.addItem(self.hline) def wheelEvent(self, event: QWheelEvent): adj = (event.angleDelta().y() / 120) * 0.1 self.scale(1 + adj, 1 + adj) def fit_to_window(self): """Fit image within view.""" if not self.pixmap or not self._pixmap.pixmap(): return #self._pixmap.setTransformationMode(QtCore.Qt.SmoothTransformation) self.fitInView(self._pixmap, QtCore.Qt.KeepAspectRatio) def show_guide_lines(self): if self.hline and self.vline: self.hline.show() self.vline.show() def hide_guide_lines(self): if self.hline and self.vline: self.hline.hide() self.vline.hide() def pixmap_hoverEnterEvent_slot(self): self.show_guide_lines() def pixmap_hoverLeaveEvent_slot(self): self.hide_guide_lines() def pixmap_hoverMoveEvent_slot(self, evt: QGraphicsSceneHoverEvent, x, y): bbox: QRect = self._pixmap.boundingRect() offset = QPointF(bbox.width() / 2, bbox.height() / 2) self.vline.setLine(x, -offset.y(), x, bbox.height() - offset.y()) self.vline.setZValue(1) self.hline.setLine(-offset.x(), y, bbox.width() - offset.x(), y) self.hline.setZValue(1) def mouseMoveEvent(self, evt: QtGui.QMouseEvent) -> None: if self.selection_mode == SELECTION_MODE.BOX: if not self._box_origin.isNull(): self._box_picker.setGeometry( QRect(self._box_origin, evt.pos()).normalized()) elif self.selection_mode == SELECTION_MODE.POLYGON: if self._current_polygon: if self._current_polygon.count > 0: last_point: QPointF = self._current_polygon.last_point self._polygon_guide_line.setZValue(1) self._polygon_guide_line.show() mouse_pos = self.mapToScene(evt.pos()) self._polygon_guide_line.setLine(last_point.x(), last_point.y(), mouse_pos.x(), mouse_pos.y()) else: self._polygon_guide_line.hide() elif self.selection_mode == SELECTION_MODE.FREE and evt.buttons( ) and QtCore.Qt.LeftButton: if self._current_free_path: painter: QPainterPath = self._current_free_path.path() self._last_point_drawn = self.mapToScene(evt.pos()) painter.lineTo(self._last_point_drawn) self._current_free_path.setPath(painter) super(ImageViewer, self).mouseMoveEvent(evt) def mousePressEvent(self, evt: QtGui.QMouseEvent) -> None: if evt.buttons() == QtCore.Qt.LeftButton: if self.selection_mode == SELECTION_MODE.BOX: self.setDragMode(QGraphicsView.NoDrag) self._box_origin = evt.pos() self._box_picker.setGeometry(QRect(self._box_origin, QSize())) self._box_picker.show() elif self._selection_mode == SELECTION_MODE.POLYGON: pixmap_rect: QRectF = self._pixmap.boundingRect() new_point = self.mapToScene(evt.pos()) # consider only the points intothe image if pixmap_rect.contains(new_point): if self._current_polygon is None: self._current_polygon = EditablePolygon() self._current_polygon.signals.deleted.connect( self.delete_polygon_slot) self._scene.addItem(self._current_polygon) self._current_polygon.addPoint(new_point) else: self._current_polygon.addPoint(new_point) elif self._selection_mode == SELECTION_MODE.FREE: # start drawing new_point = self.mapToScene(evt.pos()) pixmap_rect: QRectF = self._pixmap.boundingRect() # consider only the points intothe image if pixmap_rect.contains(new_point): self.setDragMode(QGraphicsView.NoDrag) pen = QPen(QtGui.QColor(235, 72, 40)) pen.setWidth(10) self._last_point_drawn = new_point self._current_free_path = QGraphicsPathItem() self._current_free_path.setOpacity(0.6) self._current_free_path.setPen(pen) painter = QPainterPath() painter.moveTo(self._last_point_drawn) self._current_free_path.setPath(painter) self._scene.addItem(self._current_free_path) else: self.setDragMode(QGraphicsView.ScrollHandDrag) super(ImageViewer, self).mousePressEvent(evt) def mouseReleaseEvent(self, evt: QtGui.QMouseEvent) -> None: if evt.button() == QtCore.Qt.LeftButton: if self.selection_mode == SELECTION_MODE.BOX: roi: QRect = self._box_picker.geometry() roi: QRectF = self.mapToScene(roi).boundingRect() pixmap_rect = self._pixmap.boundingRect() self._box_picker.hide() if pixmap_rect == roi.united(pixmap_rect): rect = EditableBox(roi) rect.label = self.current_label self._scene.addItem(rect) self.selection_mode = SELECTION_MODE.NONE self.setDragMode(QGraphicsView.ScrollHandDrag) elif self.selection_mode == SELECTION_MODE.FREE and self._current_free_path: # create polygon self._current_free_path: QGraphicsPathItem path_rect = self._current_free_path.boundingRect() pixmap_rect = self._pixmap.boundingRect() if pixmap_rect == path_rect.united(pixmap_rect): path = self._current_free_path.path() path_polygon = EditablePolygon() path_polygon.label = self.current_label self._scene.addItem(path_polygon) for i in range(0, path.elementCount(), 10): x, y = path.elementAt(i).x, path.elementAt(i).y path_polygon.addPoint(QPointF(x, y)) self._scene.removeItem(self._current_free_path) self.selection_mode = SELECTION_MODE.NONE self.setDragMode(QGraphicsView.ScrollHandDrag) super(ImageViewer, self).mouseReleaseEvent(evt) def keyPressEvent(self, event: QtGui.QKeyEvent) -> None: if self._current_polygon and event.key() == QtCore.Qt.Key_Space: points = self._current_polygon.points self._current_polygon.label = self.current_label self._current_polygon = None self.selection_mode = SELECTION_MODE.NONE self._polygon_guide_line.hide() self.setDragMode(QGraphicsView.ScrollHandDrag) super(ImageViewer, self).keyPressEvent(event) def delete_polygon_slot(self, polygon: EditablePolygon): self._current_polygon = None self.selection_mode = SELECTION_MODE.NONE self._polygon_guide_line.hide()
class MonoScene(QGraphicsScene): def __init__(self): super().__init__() self.language = settings.LANG self.csv_separator = settings.CSV_SEPARATOR self.fmt_float = settings.FMT_FLOAT self.nodes = {} self.nb_nodes = 0 self.adj_list = {} self.current_line = None self.current_port = None self.project_path = '' self.setSceneRect( QRectF(0, 0, settings.SCENE_SIZE[0], settings.SCENE_SIZE[1])) self.transform = QTransform() self.selectionChanged.connect(self.selection_changed) self._init_with_default_node() def reinit(self): self.clear() self._init_with_default_node() self.update() def _init_without_node(self): self.nodes = {} self.nb_nodes = 0 self.adj_list = {} def _init_with_default_node(self): self._init_without_node() self.add_node(LoadSerafin2DNode(0), 50, 50) self._add_current_line() def _add_current_line(self): self.current_line = QGraphicsLineItem() self.addItem(self.current_line) self.current_line.setVisible(False) pen = QPen(QColor(0, 0, 0)) pen.setWidth(2) self.current_line.setPen(pen) self.current_port = None def mousePressEvent(self, event): super().mousePressEvent(event) for node_index, node in enumerate(self.nodes.values()): for port_index, port in enumerate(node.ports): if port.isSelected(): self.current_port = (node_index, port_index) def mouseMoveEvent(self, event): super().mouseMoveEvent(event) if self.current_port is not None: port = self.nodes[self.current_port[0]].ports[self.current_port[1]] self.current_line.setLine( QLineF(port.mapToScene(port.rect().center()), event.scenePos())) self.current_line.setVisible(True) def mouseReleaseEvent(self, event): if self.current_port is not None: target_item = self.itemAt(event.scenePos(), self.transform) if isinstance(target_item, Port): self._handle_add_link(target_item) self.current_line.setVisible(False) self.current_port = None super().mouseReleaseEvent(event) def mouseDoubleClickEvent(self, event): super().mouseDoubleClickEvent(event) target_item = self.itemAt(event.scenePos(), self.transform) if isinstance(target_item, Link): self._handle_remove_link(target_item) elif isinstance(target_item, Box): node = target_item.parentItem() node.configure() self.selection_changed() def keyPressEvent(self, event): if event.key() == Qt.Key_Delete: selected = self.selectedItems() if selected: link = selected[0] self._handle_remove_link(link) def selection_changed(self): view = self.views()[0] selected = self.selectedItems() if not selected: view.deselect_node() return selected = selected[0] if isinstance(selected, Node): view.select_node(selected) else: view.deselect_node() def add_node(self, node, x, y): logger.debug('Add node #%i %s' % (node.index(), node.label.replace('\n', ' '))) self.addItem(node) self.nodes[node.index()] = node self.adj_list[node.index()] = set() self.nb_nodes += 1 node.moveBy(x, y) def save(self): links = [] for item in self.items(): if isinstance(item, Link): links.append(item.save()) yield '.'.join([self.language, self.csv_separator]) yield '%d %d' % (self.nb_nodes, len(links)) for node in self.nodes.values(): yield node.save() for link in links: yield link def load(self, filename): logger.debug('Loading project in MONO: %s' % filename) self.project_path = filename self.clear() self._add_current_line() self._init_without_node() try: with open(filename, 'r') as f: self.language, self.csv_separator = f.readline().rstrip( ).split('.') nb_nodes, nb_links = map(int, f.readline().split()) for _ in range(nb_nodes): line = f.readline().rstrip().split('|') category, name, index, x, y = line[:5] node = NODES[category][name](int(index)) node.load(line[5:]) self.add_node(node, float(x), float(y)) for _ in range(nb_links): from_node_index, from_port_index, \ to_node_index, to_port_index = map(int, f.readline().rstrip().split('|')) from_node = self.nodes[from_node_index] to_node = self.nodes[to_node_index] from_port = from_node.ports[from_port_index] to_port = to_node.ports[to_port_index] _, _ = add_link(from_port, to_port) link = Link(from_port, to_port) from_node.add_link(link) to_node.add_link(link) self.addItem(link) link.setZValue(-1) self.adj_list[from_node_index].add(to_node_index) self.adj_list[to_node_index].add(from_node_index) self.update() return True except (IndexError, ValueError, KeyError) as e: logger.exception(e) logger.error("An exception occured while loading project in MONO.") self.reinit() return False def run_all(self): roots = self._to_sources() for root in roots: self.nodes[root].run_downward() def _to_sources(self): roots = [] visited = {node: False for node in self.nodes} def forward(node): for port in node.ports: if port.type == Port.OUTPUT: if port.has_children(): for child in port.children: child_node = child.parentItem() if not visited[child_node.index()]: visited[child_node.index()] = False forward(child_node) def backward(node_index): if not visited[node_index]: visited[node_index] = True node = self.nodes[node_index] if node.ports[0].type == Port.INPUT: backward(node.ports[0].parentItem().index()) if len(node.ports ) > 1 and node.ports[1].type == Port.INPUT: backward(node.ports[1].parentItem().index()) else: roots.append(node_index) forward(node) for node in self.nodes: backward(node) return roots def _handle_add_link(self, target_item): port_index = target_item.index() node_index = target_item.parentItem().index() if node_index == self.current_port[0]: return from_node, to_node, from_port, to_port = self._permute( self.current_port[0], self.current_port[1], node_index, port_index) if from_port is None: QMessageBox.critical( None, 'Error', 'Connections can only be established between Output and Input ports!', QMessageBox.Ok) return success, cause = add_link(from_port, to_port) if success: link = Link(from_port, to_port) from_node.add_link(link) to_node.add_link(link) self.addItem(link) link.setZValue(-1) self.update() self.adj_list[from_node.index()].add(to_node.index()) self.adj_list[to_node.index()].add(from_node.index()) else: if cause == 'already': QMessageBox.critical(None, 'Error', 'These two ports are already connected.', QMessageBox.Ok) elif cause == 'type': QMessageBox.critical( None, 'Error', 'These two ports cannot be connected: incompatible data types.', QMessageBox.Ok) else: QMessageBox.critical( None, 'Error', 'The input port is already connected to another port.', QMessageBox.Ok) def _handle_remove_link(self, target_item): msg = QMessageBox.warning(None, 'Confirm delete', 'Do you want to delete this link?', QMessageBox.Ok | QMessageBox.Cancel, QMessageBox.Ok) if msg == QMessageBox.Cancel: return from_index, to_index = target_item.from_port.parentItem().index( ), target_item.to_port.parentItem().index() self.adj_list[from_index].remove(to_index) self.adj_list[to_index].remove(from_index) target_item.remove() self.removeItem(target_item) def handle_remove_node(self, node): msg = QMessageBox.warning(None, 'Confirm delete', 'Do you want to delete this node?', QMessageBox.Ok | QMessageBox.Cancel, QMessageBox.Ok) if msg == QMessageBox.Cancel: return self.removeItem(node) for link in node.links.copy(): link.remove() self.removeItem(link) del self.nodes[node.index()] new_nodes = {} self.nb_nodes -= 1 for index, node in zip(range(self.nb_nodes), self.nodes.values()): new_nodes[index] = node node.set_index(index) self.nodes = new_nodes self.adj_list = {node: set() for node in self.nodes} for item in self.items(): if isinstance(item, Link): from_index, to_index = item.from_port.parentItem().index( ), item.to_port.parentItem().index() self.adj_list[from_index].add(to_index) self.adj_list[to_index].add(from_index) def _permute(self, first_node_index, first_port_index, second_node_index, second_port_index): first_node = self.nodes[first_node_index] second_node = self.nodes[second_node_index] p1 = first_node.ports[first_port_index] p2 = second_node.ports[second_port_index] if p1.type == Port.OUTPUT and p2.type == Port.INPUT: return first_node, second_node, p1, p2 elif p1.type == Port.INPUT and p2.type == Port.OUTPUT: return second_node, first_node, p2, p1 return None, None, None, None def suffix_pool(self): suffix = [] for node in self.nodes.values(): if hasattr(node, 'suffix'): suffix.append(node.suffix) return suffix def not_connected(self): nb_cc, _ = count_cc(list(self.adj_list.keys()), self.adj_list) return nb_cc > 1
class Editor(QWidget): """ This class is the central widget of the MainWindow. It contains the items library, diagram graphics scene and graphics view, and the inspector widget Function of Connections: Logically: A Connection is composed of a fromPort and a toPort, which gives the direction of the pipe. Ports are attached to Blocks. Visually: A diagram editor has a QGraphicsLineItem (connLineItem) which is set Visible only when a connection is being created Function of BlockItems: Items can be added to the library by adding them to the model of the library broswer view. Then they can be dragged and dropped into the diagram view. Function of trnsysExport: When exporting the trnsys file, exportData() is called. Function of save and load: A diagram can be saved to a json file by calling encodeDiagram and can then be loaded by calling decodeDiagram wiht appropiate filenames. Attributes ---------- projectFolder : str Path to the folder of the project diagramName : str Name used for saving the diagram saveAsPath : :obj:`Path` Default saving location is trnsysGUI/diagrams, path only set if "save as" used idGen : :obj:`IdGenerator` Is used to distribute ids (id, trnsysId(for trnsysExport), etc) alignMode : bool Enables mode in which a dragged block is aligned to y or x value of another one Toggled in the MainWindow class in toggleAlignMode() editorMode : int Mode 0: Pipes are PolySun-like Mode 1: Pipes have only 90deg angles, visio-like snapGrid : bool Enable/Disable align grid snapSize : int Size of align grid horizontalLayout : :obj:`QHBoxLayout` Contains the diagram editor and the layout containing the library browser view and the listview vertL : :obj:`QVBoxLayout` Cointains the library browser view and the listWidget moveDirectPorts: bool Enables/Disables moving direct ports of storagetank (doesn't work with HxPorts yet) diagramScene : :obj:`QGraphicsScene` Contains the "logical" part of the diagram diagramView : :obj:`QGraphicsView` Contains the visualization of the diagramScene _currentlyDraggedConnectionFromPort : :obj:`PortItem` connectionList : :obj:`List` of :obj:`Connection` trnsysObj : :obj:`List` of :obj:`BlockItem` and :obj:`Connection` graphicalObj : :obj:`List` of :obj:`GraphicalItem` connLine : :obj:`QLineF` connLineItem = :obj:`QGraphicsLineItem` """ def __init__(self, parent, projectFolder, jsonPath, loadValue, logger): super().__init__(parent) self.logger = logger self.logger.info("Initializing the diagram editor") self.projectFolder = projectFolder self.diagramName = os.path.split(self.projectFolder)[-1] + ".json" self.saveAsPath = _pl.Path() self.idGen = IdGenerator() self.testEnabled = False self.existReference = True self.controlExists = 0 self.controlDirectory = "" self.alignMode = False self.moveDirectPorts = False self.editorMode = 1 # Related to the grid blocks can snap to self.snapGrid = False self.snapSize = 20 self.trnsysPath = _pl.Path(r"C:\Trnsys17\Exe\TRNExe.exe") self.horizontalLayout = QHBoxLayout(self) self.libraryBrowserView = QListView(self) self.libraryModel = LibraryModel(self) self.libraryBrowserView.setGridSize(QSize(65, 65)) self.libraryBrowserView.setResizeMode(QListView.Adjust) self.libraryModel.setColumnCount(0) componentNamesWithIcon = [ ("Connector", _img.CONNECTOR_SVG.icon()), ("TeePiece", _img.TEE_PIECE_SVG.icon()), ("DPTee", _img.DP_TEE_PIECE_SVG.icon()), ("SPCnr", _img.SINGLE_DOUBLE_PIPE_CONNECTOR_SVG.icon()), ("DPCnr", _img.DOUBLE_DOUBLE_PIPE_CONNECTOR_SVG.icon()), ("TVentil", _img.T_VENTIL_SVG.icon()), ("WTap_main", _img.W_TAP_MAIN_SVG.icon()), ("WTap", _img.W_TAP_SVG.icon()), ("Pump", _img.PUMP_SVG.icon()), ("Collector", _img.COLLECTOR_SVG.icon()), ("GroundSourceHx", _img.GROUND_SOURCE_HX_SVG.icon()), ("PV", _img.PV_SVG.icon()), ("HP", _img.HP_SVG.icon()), ("HPTwoHx", _img.HP_TWO_HX_SVG.icon()), ("HPDoubleDual", _img.HP_DOUBLE_DUAL_SVG.icon()), ("HPDual", _img.HP_DUAL_SVG.icon()), ("AirSourceHP", _img.AIR_SOURCE_HP_SVG.icon()), ("StorageTank", _img.STORAGE_TANK_SVG.icon()), ("IceStorage", _img.ICE_STORAGE_SVG.icon()), ("PitStorage", _img.PIT_STORAGE_SVG.icon()), ("IceStorageTwoHx", _img.ICE_STORAGE_TWO_HX_SVG.icon()), ("ExternalHx", _img.EXTERNAL_HX_SVG.icon()), ("Radiator", _img.RADIATOR_SVG.icon()), ("Boiler", _img.BOILER_SVG.icon()), ("Sink", _img.SINK_SVG.icon()), ("Source", _img.SOURCE_SVG.icon()), ("SourceSink", _img.SOURCE_SINK_SVG.icon()), ("Geotherm", _img.GEOTHERM_SVG.icon()), ("Water", _img.WATER_SVG.icon()), ("Crystalizer", _img.CRYSTALIZER_SVG.icon()), ("GenericBlock", _img.GENERIC_BLOCK_PNG.icon()), ("GraphicalItem", _img.GENERIC_ITEM_PNG.icon()), ] libItems = [ QtGui.QStandardItem(icon, name) for name, icon in componentNamesWithIcon ] for i in libItems: self.libraryModel.appendRow(i) self.libraryBrowserView.setModel(self.libraryModel) self.libraryBrowserView.setViewMode(self.libraryBrowserView.IconMode) self.libraryBrowserView.setDragDropMode( self.libraryBrowserView.DragOnly) self.diagramScene = Scene(self) self.diagramView = View(self.diagramScene, self) # For list view self.vertL = QVBoxLayout() self.vertL.addWidget(self.libraryBrowserView) self.vertL.setStretchFactor(self.libraryBrowserView, 2) self.listV = QListWidget() self.vertL.addWidget(self.listV) self.vertL.setStretchFactor(self.listV, 1) # for file browser self.projectPath = "" self.fileList = [] if loadValue == "new" or loadValue == "json": self.createProjectFolder() self.fileBrowserLayout = QVBoxLayout() self.pathLayout = QHBoxLayout() self.projectPathLabel = QLabel("Project Path:") self.PPL = QLineEdit(self.projectFolder) self.PPL.setDisabled(True) self.pathLayout.addWidget(self.projectPathLabel) self.pathLayout.addWidget(self.PPL) self.scroll = QScrollArea() self.scroll.setWidgetResizable(True) self.splitter = QSplitter(Qt.Vertical, ) self.splitter.setChildrenCollapsible(False) self.scroll.setWidget(self.splitter) self.scroll.setFixedWidth(350) self.fileBrowserLayout.addLayout(self.pathLayout) self.fileBrowserLayout.addWidget(self.scroll) self.createDdckTree(self.projectFolder) if loadValue == "new" or loadValue == "json": self.createConfigBrowser(self.projectFolder) self.copyGenericFolder(self.projectFolder) self.createHydraulicDir(self.projectFolder) self.createWeatherAndControlDirs(self.projectFolder) self.horizontalLayout.addLayout(self.vertL) self.horizontalLayout.addWidget(self.diagramView) self.horizontalLayout.addLayout(self.fileBrowserLayout) self.horizontalLayout.setStretchFactor(self.diagramView, 5) self.horizontalLayout.setStretchFactor(self.libraryBrowserView, 1) self._currentlyDraggedConnectionFromPort = None self.connectionList = [] self.trnsysObj = [] self.graphicalObj = [] self.fluids = _hlm.Fluids([]) self.hydraulicLoops = _hlm.HydraulicLoops([]) self.copyGroupList = QGraphicsItemGroup() self.selectionGroupList = QGraphicsItemGroup() self.printerUnitnr = 0 # Different colors for connLineColor colorsc = "red" linePx = 4 if colorsc == "red": connLinecolor = QColor(Qt.red) elif colorsc == "blueish": connLinecolor = QColor(3, 124, 193) # Blue elif colorsc == "darkgray": connLinecolor = QColor(140, 140, 140) # Gray else: connLinecolor = QColor(196, 196, 196) # Gray # Only for displaying on-going creation of connection self.connLine = QLineF() self.connLineItem = QGraphicsLineItem(self.connLine) self.connLineItem.setPen(QtGui.QPen(connLinecolor, linePx)) self.connLineItem.setVisible(False) self.diagramScene.addItem(self.connLineItem) # For line that shows quickly up when using the align mode self.alignYLine = QLineF() self.alignYLineItem = QGraphicsLineItem(self.alignYLine) self.alignYLineItem.setPen(QtGui.QPen(QColor(196, 249, 252), 2)) self.alignYLineItem.setVisible(False) self.diagramScene.addItem(self.alignYLineItem) # For line that shows quickly up when using align mode self.alignXLine = QLineF() self.alignXLineItem = QGraphicsLineItem(self.alignXLine) self.alignXLineItem.setPen(QtGui.QPen(QColor(196, 249, 252), 2)) self.alignXLineItem.setVisible(False) self.diagramScene.addItem(self.alignXLineItem) if loadValue == "load" or loadValue == "copy": self._decodeDiagram(os.path.join(self.projectFolder, self.diagramName), loadValue=loadValue) elif loadValue == "json": self._decodeDiagram(jsonPath, loadValue=loadValue) # Debug function def dumpInformation(self): self.logger.debug("Diagram information:") self.logger.debug("Mode is " + str(self.editorMode)) self.logger.debug("Next ID is " + str(self.idGen.getID())) self.logger.debug("Next cID is " + str(self.idGen.getConnID())) self.logger.debug("TrnsysObjects are:") for t in self.trnsysObj: self.logger.debug(str(t)) self.logger.debug("") self.logger.debug("Scene items are:") sItems = self.diagramScene.items() for it in sItems: self.logger.info(str(it)) self.logger.debug("") for c in self.connectionList: c.printConn() self.logger.debug("") # Connections related methods def startConnection(self, port): self._currentlyDraggedConnectionFromPort = port def _createConnection(self, startPort, endPort) -> None: if startPort is not endPort: if (isinstance(startPort.parent, StorageTank) and isinstance(endPort.parent, StorageTank) and startPort.parent != endPort.parent): msgSTank = QMessageBox(self) msgSTank.setText( "Storage Tank to Storage Tank connection is not working atm!" ) msgSTank.exec_() isValidSinglePipeConnection = isinstance( startPort, SinglePipePortItem) and isinstance( endPort, SinglePipePortItem) if isValidSinglePipeConnection: command = CreateSinglePipeConnectionCommand( startPort, endPort, self) elif isinstance(startPort, DoublePipePortItem) and isinstance( endPort, DoublePipePortItem): command = CreateDoublePipeConnectionCommand( startPort, endPort, self) else: raise AssertionError( "Can only connect port items. Also, they have to be of the same type." ) self.parent().undoStack.push(command) def sceneMouseMoveEvent(self, event): """ This function is for dragging and connecting one port to another. When dragging, the fromPort will remain enlarged and black in color and when the toPort is hovered over, it will be enlarged and turn red. A port's details will also be displayed at the widget when they are hovered over. """ fromPort = self._currentlyDraggedConnectionFromPort if not fromPort: return fromX = fromPort.scenePos().x() fromY = fromPort.scenePos().y() toX = event.scenePos().x() toY = event.scenePos().y() self.connLine.setLine(fromX, fromY, toX, toY) self.connLineItem.setLine(self.connLine) self.connLineItem.setVisible(True) hitPortItem = self._getHitPortItemOrNone(event) if not hitPortItem: return mousePosition = event.scenePos() portItemX = hitPortItem.scenePos().x() portItemY = hitPortItem.scenePos().y() distance = _math.sqrt((mousePosition.x() - portItemX)**2 + (mousePosition.y() - portItemY)**2) if distance <= 3.5: hitPortItem.enlargePortSize() hitPortItem.innerCircle.setBrush(hitPortItem.ashColorR) self.listV.clear() hitPortItem.debugprint() else: hitPortItem.resetPortSize() hitPortItem.innerCircle.setBrush(hitPortItem.visibleColor) self.listV.clear() fromPort.debugprint() fromPort.enlargePortSize() fromPort.innerCircle.setBrush(hitPortItem.visibleColor) def _getHitPortItemOrNone(self, event: QEvent) -> _tp.Optional[PortItemBase]: fromPort = self._currentlyDraggedConnectionFromPort mousePosition = event.scenePos() relevantPortItems = self._getRelevantHitPortItems( mousePosition, fromPort) if not relevantPortItems: return None numberOfHitPortsItems = len(relevantPortItems) if numberOfHitPortsItems > 1: raise NotImplementedError( "Can't deal with overlapping port items.") hitPortItem = relevantPortItems[0] return hitPortItem def sceneMouseReleaseEvent(self, event): if not self._currentlyDraggedConnectionFromPort: return fromPort = self._currentlyDraggedConnectionFromPort self._currentlyDraggedConnectionFromPort = None self.connLineItem.setVisible(False) mousePosition = event.scenePos() relevantPortItems = self._getRelevantHitPortItems( mousePosition, fromPort) numberOfHitPortsItems = len(relevantPortItems) if numberOfHitPortsItems > 1: raise NotImplementedError( "Can't deal with overlapping port items.") if numberOfHitPortsItems == 1: toPort = relevantPortItems[0] if toPort != fromPort: self._createConnection(fromPort, toPort) def _getRelevantHitPortItems( self, mousePosition: QPointF, fromPort: PortItemBase) -> _tp.Sequence[PortItemBase]: hitItems = self.diagramScene.items(mousePosition) relevantPortItems = [ i for i in hitItems if isinstance(i, PortItemBase) and type(i) == type(fromPort) and not i.connectionList ] return relevantPortItems def cleanUpConnections(self): for c in self.connectionList: c.niceConn() def exportHydraulics(self, exportTo=_tp.Literal["ddck", "mfs"]): assert exportTo in ["ddck", "mfs"] if not self._isHydraulicConnected(): messageBox = QMessageBox() messageBox.setWindowTitle("Hydraulic not connected") messageBox.setText( "You need to connect all port items before you can export the hydraulics." ) messageBox.setStandardButtons(QMessageBox.Ok) messageBox.exec() return self.logger.info( "------------------------> START OF EXPORT <------------------------" ) self.sortTrnsysObj() fullExportText = "" ddckFolder = os.path.join(self.projectFolder, "ddck") if exportTo == "mfs": mfsFileName = self.diagramName.split(".")[0] + "_mfs.dck" exportPath = os.path.join(self.projectFolder, mfsFileName) elif exportTo == "ddck": exportPath = os.path.join(ddckFolder, "hydraulic\\hydraulic.ddck") if self._doesFileExistAndDontOverwrite(exportPath): return None self.logger.info("Printing the TRNSYS file...") if exportTo == "mfs": header = open(os.path.join(ddckFolder, "generic\\head.ddck"), "r") headerLines = header.readlines() for line in headerLines: if line[:4] == "STOP": fullExportText += "STOP = 1 \n" else: fullExportText += line header.close() elif exportTo == "ddck": fullExportText += "*************************************\n" fullExportText += "** BEGIN hydraulic.ddck\n" fullExportText += "*************************************\n\n" fullExportText += "*************************************\n" fullExportText += "** Outputs to energy balance in kWh\n" fullExportText += ( "** Following this naming standard : qSysIn_name, qSysOut_name, elSysIn_name, elSysOut_name\n" ) fullExportText += "*************************************\n" fullExportText += "EQUATIONS 1\n" fullExportText += "qSysOut_PipeLoss = PipeLossTot\n" simulationUnit = 450 simulationType = 935 descConnLength = 20 exporter = self._createExporter() blackBoxProblem, blackBoxText = exporter.exportBlackBox( exportTo=exportTo) if blackBoxProblem: return None fullExportText += blackBoxText if exportTo == "mfs": fullExportText += exporter.exportMassFlows() fullExportText += exporter.exportPumpOutlets() fullExportText += exporter.exportDivSetting(simulationUnit - 10) fullExportText += exporter.exportDoublePipeParameters( exportTo=exportTo) fullExportText += exporter.exportParametersFlowSolver( simulationUnit, simulationType, descConnLength) fullExportText += exporter.exportInputsFlowSolver() fullExportText += exporter.exportOutputsFlowSolver(simulationUnit) fullExportText += exporter.exportFluids() + "\n" fullExportText += exporter.exportHydraulicLoops() + "\n" fullExportText += exporter.exportPipeAndTeeTypesForTemp( simulationUnit + 1) # DC-ERROR fullExportText += exporter.exportPrintPipeLosses() fullExportText += exporter.exportMassFlowPrinter( self.printerUnitnr, 15) fullExportText += exporter.exportTempPrinter(self.printerUnitnr + 1, 15) if exportTo == "mfs": fullExportText += "CONSTANTS 1\nTRoomStore=1\n" fullExportText += "ENDS" self.logger.info( "------------------------> END OF EXPORT <------------------------" ) if exportTo == "mfs": f = open(exportPath, "w") f.truncate(0) f.write(fullExportText) f.close() elif exportTo == "ddck": if fullExportText[:1] == "\n": fullExportText = fullExportText[1:] hydraulicFolder = os.path.split(exportPath)[0] if not (os.path.isdir(hydraulicFolder)): os.makedirs(hydraulicFolder) f = open(exportPath, "w") f.truncate(0) f.write(fullExportText) f.close() try: lines = _du.loadDeck(exportPath, eraseBeginComment=True, eliminateComments=True) _du.checkEquationsAndConstants(lines, exportPath) except Exception as error: errorMessage = f"An error occurred while exporting the system hydraulics: {error}" _errs.showErrorMessageBox(errorMessage) return None return exportPath def _createExporter(self) -> Export: massFlowContributors = self._getMassFlowContributors() exporter = Export(massFlowContributors, self) return exporter def _getMassFlowContributors( self) -> _tp.Sequence[_mfs.MassFlowNetworkContributorMixin]: massFlowContributors = [ o for o in self.trnsysObj if isinstance(o, _mfs.MassFlowNetworkContributorMixin) ] return massFlowContributors def _isHydraulicConnected(self) -> bool: for obj in self.trnsysObj: if not isinstance(obj, _mfs.MassFlowNetworkContributorMixin): continue internalPiping = obj.getInternalPiping() for portItem in internalPiping.modelPortItemsToGraphicalPortItem.values( ): if not portItem.connectionList: return False return True def _doesFileExistAndDontOverwrite(self, folderPath): if not _pl.Path(folderPath).exists(): return False qmb = QMessageBox(self) qmb.setText( f"Warning: {folderPath} already exists. Do you want to overwrite it or cancel?" ) qmb.setStandardButtons(QMessageBox.Save | QMessageBox.Cancel) qmb.setDefaultButton(QMessageBox.Cancel) ret = qmb.exec() if ret == QMessageBox.Cancel: self.canceled = True self.logger.info("Canceling") return True self.canceled = False self.logger.info("Overwriting") return False def exportHydraulicControl(self): self.logger.info( "------------------------> START OF EXPORT <------------------------" ) self.sortTrnsysObj() fullExportText = "" ddckFolder = os.path.join(self.projectFolder, "ddck") hydCtrlPath = os.path.join(ddckFolder, "control\\hydraulic_control.ddck") if _pl.Path(hydCtrlPath).exists(): qmb = QMessageBox(self) qmb.setText( "Warning: " + "The file hydraulic_control.ddck already exists in the control folder. Do you want to overwrite it or cancel?" ) qmb.setStandardButtons(QMessageBox.Save | QMessageBox.Cancel) qmb.setDefaultButton(QMessageBox.Cancel) ret = qmb.exec() if ret == QMessageBox.Save: self.canceled = False self.logger.info("Overwriting") else: self.canceled = True self.logger.info("Canceling") return fullExportText += "*************************************\n" fullExportText += "**BEGIN hydraulic_control.ddck\n" fullExportText += "*************************************\n" simulationUnit = 450 exporter = self._createExporter() fullExportText += exporter.exportPumpOutlets() fullExportText += exporter.exportMassFlows() fullExportText += exporter.exportDivSetting(simulationUnit - 10) self.logger.info( "------------------------> END OF EXPORT <------------------------" ) if fullExportText[:1] == "\n": fullExportText = fullExportText[1:] controlFolder = os.path.split(hydCtrlPath)[0] if not (os.path.isdir(controlFolder)): os.makedirs(controlFolder) f = open(str(hydCtrlPath), "w") f.truncate(0) f.write(fullExportText) f.close() return hydCtrlPath def sortTrnsysObj(self): res = self.trnsysObj.sort(key=self.sortId) for s in self.trnsysObj: self.logger.debug("s has tr id " + str(s.trnsysId) + " has dname " + s.displayName) def sortId(self, l1): """ Sort function returning a sortable key Parameters ---------- l1 : Block/Connection Returns ------- """ return l1.trnsysId def setName(self, newName): self.diagramName = newName def delBlocks(self): """ Deletes the whole diagram Returns ------- """ self.hydraulicLoops.clear() while len(self.trnsysObj) > 0: self.logger.info("In deleting...") self.trnsysObj[0].deleteBlock() while len(self.graphicalObj) > 0: self.graphicalObj[0].deleteBlock() # Encoding / decoding def encodeDiagram(self, filename): """ Encodes the diagram to a json file. Parameters ---------- filename : str Returns ------- """ self.logger.info("filename is at encoder " + str(filename)) # if filename != "": with open(filename, "w") as jsonfile: json.dump(self, jsonfile, indent=4, sort_keys=True, cls=Encoder) def _decodeDiagram(self, filename, loadValue="load"): self.logger.info("Decoding " + filename) with open(filename, "r") as jsonfile: blocklist = json.load(jsonfile, cls=Decoder, editor=self) blockFolderNames = [] for j in blocklist["Blocks"]: for k in j: if isinstance(k, BlockItem): k.setParent(self.diagramView) k.changeSize() self.diagramScene.addItem(k) blockFolderNames.append(k.displayName) if isinstance(k, StorageTank): k.updateImage() if isinstance(k, GraphicalItem): k.setParent(self.diagramView) self.diagramScene.addItem(k) if isinstance(k, dict): if "__idDct__" in k: # here we don't set the ids because the copyGroup would need access to idGen self.logger.debug( "Found the id dict while loading, not setting the ids" ) self.idGen.setID(k["GlobalId"]) self.idGen.setTrnsysID(k["trnsysID"]) self.idGen.setConnID(k["globalConnID"]) if "__nameDct__" in k: self.logger.debug("Found the name dict while loading") if loadValue == "load": self.diagramName = k["DiagramName"] blockFolderNames.append("generic") blockFolderNames.append("hydraulic") blockFolderNames.append("weather") blockFolderNames.append("control") ddckFolder = os.path.join(self.projectFolder, "ddck") ddckFolders = os.listdir(ddckFolder) additionalFolders = [] for folder in ddckFolders: if folder not in blockFolderNames and "StorageTank" not in folder: additionalFolders.append(folder) if len(additionalFolders) > 0: warnBox = QMessageBox() warnBox.setWindowTitle("Additional ddck-folders") if len(additionalFolders) == 1: text = "The following ddck-folder does not have a corresponding component in the diagram:" else: text = "The following ddck-folders do not have a corresponding component in the diagram:" for folder in additionalFolders: text += "\n\t" + folder warnBox.setText(text) warnBox.setStandardButtons(QMessageBox.Ok) warnBox.setDefaultButton(QMessageBox.Ok) warnBox.exec() for t in self.trnsysObj: t.assignIDsToUninitializedValuesAfterJsonFormatMigration( self.idGen) self.logger.debug("Tr obj is" + str(t) + " " + str(t.trnsysId)) if hasattr(t, "isTempering"): self.logger.debug("tv has " + str(t.isTempering)) self._decodeHydraulicLoops(blocklist) def _decodeHydraulicLoops(self, blocklist): singlePipeConnections = [ c for c in self.connectionList if isinstance(c, SinglePipeConnection) ] if "hydraulicLoops" not in blocklist: hydraulicLoops = _hlmig.createLoops(singlePipeConnections, self.fluids.WATER) else: serializedHydraulicLoops = blocklist["hydraulicLoops"] hydraulicLoops = _hlm.HydraulicLoops.createFromJson( serializedHydraulicLoops, singlePipeConnections, self.fluids) self.hydraulicLoops = hydraulicLoops def exportSvg(self): """ For exporting a svg file (text is still too large) Returns ------- """ generator = QSvgGenerator() generator.setResolution(300) generator.setSize( QSize(self.diagramScene.width(), self.diagramScene.height())) # generator.setViewBox(QRect(0, 0, 800, 800)) generator.setViewBox(self.diagramScene.sceneRect()) generator.setFileName("VectorGraphicsExport.svg") painter = QPainter() painter.begin(generator) painter.setRenderHint(QPainter.Antialiasing) self.diagramScene.render(painter) painter.end() # Saving related def save(self, showWarning=True): """ If saveas has not been used, diagram will be saved in "/diagrams" If saveas has been used, diagram will be saved in self.saveAsPath Returns ------- """ self.diagramName = os.path.split(self.projectFolder)[-1] + ".json" diagramPath = os.path.join(self.projectFolder, self.diagramName) if os.path.isfile(diagramPath) and showWarning: qmb = QMessageBox(self) qmb.setText( "Warning: " + "This diagram name exists already. Do you want to overwrite or cancel?" ) qmb.setStandardButtons(QMessageBox.Save | QMessageBox.Cancel) qmb.setDefaultButton(QMessageBox.Cancel) ret = qmb.exec() if ret != QMessageBox.Save: self.logger.info("Canceling") return self.logger.info("Overwriting") self.encodeDiagram(diagramPath) self.encodeDiagram(diagramPath) msgb = QMessageBox(self) msgb.setText("Saved diagram at " + diagramPath) msgb.exec() def saveToProject(self): projectPath = self.projectPath def renameDiagram(self, newName): """ Parameters ---------- newName Returns ------- """ if self.saveAsPath.name != "": # print("Path name is " + self.saveAsPath.name) if newName + ".json" in self.saveAsPath.glob("*"): QMessageBox( self, "Warning", "This diagram name exists already in the directory." " Please rename this diagram") else: self.saveAsPath = _pl.Path( self.saveAsPath.stem[0:self.saveAsPath.name. index(self.diagramName)] + newName) self.diagramName = newName self.parent().currentFile = newName # fromPath = self.projectFolder # destPath = os.path.dirname(__file__) # destPath = os.path.join(destPath, 'default') # destPath = os.path.join(destPath, newName) # os.rename(fromPath, destPath) # print("Path is now: " + str(self.saveAsPath)) # print("Diagram name is: " + self.diagramName) def saveAtClose(self): self.logger.info("saveaspath is " + str(self.saveAsPath)) # closeDialog = closeDlg() # if closeDialog.closeBool: filepath = _pl.Path( _pl.Path(__file__).resolve().parent.joinpath("recent")) self.encodeDiagram(str(filepath.joinpath(self.diagramName + ".json"))) # Mode related def setAlignMode(self, b): self.alignMode = True def setEditorMode(self, b): self.editorMode = b def setMoveDirectPorts(self, b): """ Sets the bool moveDirectPorts. When mouse released in diagramScene, moveDirectPorts is set to False again Parameters ---------- b : bool Returns ------- """ self.moveDirectPorts = b def setSnapGrid(self, b): self.snapGrid = b def setSnapSize(self, s): self.snapSize = s def setConnLabelVis(self, isVisible: bool) -> None: for c in self.trnsysObj: if isinstance(c, ConnectionBase): c.setLabelVisible(isVisible) if isinstance(c, BlockItem): c.label.setVisible(isVisible) if isinstance(c, TVentil): c.posLabel.setVisible(isVisible) def updateConnGrads(self): for t in self.trnsysObj: if isinstance(t, ConnectionBase): t.updateSegGrads() # Dialog calls def showBlockDlg(self, bl): c = BlockDlg(bl, self) def showDoublePipeBlockDlg(self, bl): c = DoublePipeBlockDlg(bl, self) def showPumpDlg(self, bl): c = PumpDlg(bl, self) def showDiagramDlg(self): c = diagramDlg(self) def showGenericPortPairDlg(self, bl): c = GenericPortPairDlg(bl, self) def showHxDlg(self, hx): c = hxDlg(hx, self) def showSegmentDlg(self, seg): c = segmentDlg(seg, self) def showTVentilDlg(self, bl): c = TVentilDlg(bl, self) def showConfigStorageDlg(self, bl): c = ConfigureStorageDialog(bl, self) def getConnection(self, n): return self.connectionList[int(n)] # Unused def create_icon(self, map_icon): map_icon.fill() painter = QPainter(map_icon) painter.fillRect(10, 10, 40, 40, QColor(88, 233, 252)) # painter.setBrush(Qt.red) painter.setBrush(QColor(252, 136, 98)) painter.drawEllipse(36, 2, 15, 15) painter.setBrush(Qt.yellow) painter.drawEllipse(20, 20, 20, 20) painter.end() def setTrnsysIdBack(self): self.idGen.trnsysID = max(t.trnsysId for t in self.trnsysObj) def findStorageCorrespPorts1(self, portList): """ This function gets the ports on the other side of pipes connected to a port of the StorageTank. Unused Parameters ---------- portList : :obj:`List` of :obj:`PortItems` Returns ------- """ res = [] # print("Finding c ports") for p in portList: if len(p.connectionList) > 0: # check if not >1 needed # connectionList[0] is the hidden connection created when the portPair is i = 0 # while type(p.connectionList[i].fromPort.parent) is StorageTank and type(p.connectionList[i].toPort.parent) is StorageTank: while (p.connectionList[i].fromPort.parent) == ( p.connectionList[i].toPort.parent): i += 1 if len(p.connectionList) >= i + 1: if p.connectionList[i].fromPort is p: res.append(p.connectionList[i].toPort) elif p.connectionList[i].toPort is p: res.append(p.connectionList[i].fromPort) else: self.logger.debug("Port is not fromPort nor toPort") # [print(p.parent.displayName) for p in res] return res def printPDF(self): """ --------------------------------------------- Export diagram as pdf onto specified folder fn = user input directory --------------------------------------------- """ fn, _ = QFileDialog.getSaveFileName(self, "Export PDF", None, "PDF files (.pdf);;All Files()") if fn != "": if QFileInfo(fn).suffix() == "": fn += ".pdf" printer = QPrinter(QPrinter.HighResolution) printer.setOrientation(QPrinter.Landscape) printer.setOutputFormat(QPrinter.PdfFormat) printer.setOutputFileName(fn) painter = QPainter(printer) self.diagramScene.render(painter) painter.end() self.logger.info("File exported to %s" % fn) def openProject(self): self.projectPath = str( QFileDialog.getExistingDirectory(self, "Select Project Path")) if self.projectPath != "": test = self.parent() self.parent().newDia() self.PPL.setText(self.projectPath) loadPath = os.path.join(self.projectPath, "ddck") self.createConfigBrowser(self.projectPath) self.copyGenericFolder(self.projectPath) self.createHydraulicDir(self.projectPath) self.createWeatherAndControlDirs(self.projectPath) self.createDdckTree(loadPath) # todo : open diagram # todo : add files into list def createDdckTree(self, loadPath): treeToRemove = self.findChild(QTreeView, "ddck") try: # treeToRemove.hide() treeToRemove.deleteLater() except AttributeError: self.logger.debug("Widget doesnt exist!") else: self.logger.debug("Deleted widget") if self.projectPath == "": loadPath = os.path.join(loadPath, "ddck") if not os.path.exists(loadPath): os.makedirs(loadPath) self.model = MyQFileSystemModel() self.model.setRootPath(loadPath) self.model.setName("ddck") self.tree = MyQTreeView(self.model, self) self.tree.setModel(self.model) self.tree.setRootIndex(self.model.index(loadPath)) self.tree.setObjectName("ddck") self.tree.setMinimumHeight(600) self.tree.setSortingEnabled(True) self.splitter.insertWidget(0, self.tree) def createConfigBrowser(self, loadPath): self.layoutToRemove = self.findChild(QHBoxLayout, "Config_Layout") try: # treeToRemove.hide() self.layoutToRemove.deleteLater() except AttributeError: self.logger.debug("Widget doesnt exist!") else: self.logger.debug("Deleted widget") runConfigData = self._getPackageResourceData("templates/run.config") runConfigPath = _pl.Path(loadPath) / "run.config" runConfigPath.write_bytes(runConfigData) self.HBox = QHBoxLayout() self.refreshButton = QPushButton(self) self.refreshButton.setIcon(_img.ROTATE_TO_RIGHT_PNG.icon()) self.refreshButton.clicked.connect(self.refreshConfig) self.model = MyQFileSystemModel() self.model.setRootPath(loadPath) self.model.setName("Config File") self.model.setFilter(QDir.Files) self.tree = MyQTreeView(self.model, self) self.tree.setModel(self.model) self.tree.setRootIndex(self.model.index(loadPath)) self.tree.setObjectName("config") self.tree.setFixedHeight(60) self.tree.setSortingEnabled(False) self.HBox.addWidget(self.refreshButton) self.HBox.addWidget(self.tree) self.HBox.setObjectName("Config_Layout") self.fileBrowserLayout.addLayout(self.HBox) def createProjectFolder(self): if not os.path.exists(self.projectFolder): os.makedirs(self.projectFolder) def refreshConfig(self): # configPath = os.path.dirname(__file__) # configPath = os.path.join(configPath, 'project') # configPath = os.path.join(configPath, self.date_time) # emptyConfig = os.path.join(configPath, 'run.config') if self.projectPath == "": localPath = self.projectFolder else: localPath = self.projectPath self.configToEdit = os.path.join(localPath, "run.config") os.remove(self.configToEdit) shutil.copy(self.emptyConfig, localPath) self.configToEdit = os.path.join(localPath, "run.config") localDdckPath = os.path.join(localPath, "ddck") with open(self.configToEdit, "r") as file: lines = file.readlines() localPathStr = "string LOCAL$ %s" % str(localDdckPath) # localPathStr.replace('/', '\\') lines[21] = localPathStr + "\n" with open(self.configToEdit, "w") as file: file.writelines(lines) # print(localPathStr) self.userInputList() def userInputList(self): self.logger.debug(self.fileList) dia = FileOrderingDialog(self.fileList, self) def copyGenericFolder(self, loadPath): genericFolderPath = _pl.Path(loadPath) / "ddck" / "generic" if not genericFolderPath.exists(): self.logger.info("Creating %s", genericFolderPath) genericFolderPath.mkdir() headData = self._getPackageResourceData("templates/generic/head.ddck") self.logger.info("Copying head.ddck") (genericFolderPath / "head.ddck").write_bytes(headData) endData = self._getPackageResourceData("templates/generic/end.ddck") self.logger.info("Copying end.ddck") (genericFolderPath / "end.ddck").write_bytes(endData) @staticmethod def _getPackageResourceData(resourcePath): data = _pu.get_data(_tgui.__name__, resourcePath) assert data, f"{resourcePath} package resource not found" return data def createHydraulicDir(self, projectPath): self.hydraulicFolder = os.path.join(projectPath, "ddck") self.hydraulicFolder = os.path.join(self.hydraulicFolder, "hydraulic") if not os.path.exists(self.hydraulicFolder): self.logger.info("Creating " + self.hydraulicFolder) os.makedirs(self.hydraulicFolder) def createWeatherAndControlDirs(self, projectPath): ddckFolder = os.path.join(projectPath, "ddck") weatherFolder = os.path.join(ddckFolder, "weather") controlFolder = os.path.join(ddckFolder, "control") if not os.path.exists(weatherFolder): self.logger.info("Creating " + weatherFolder) os.makedirs(weatherFolder) if not os.path.exists(controlFolder): self.logger.info("Creating " + controlFolder) os.makedirs(controlFolder) def editHydraulicLoop(self, singlePipeConnection: SinglePipeConnection): assert isinstance(singlePipeConnection.fromPort, SinglePipePortItem) hydraulicLoop = self.hydraulicLoops.getLoopForExistingConnection( singlePipeConnection) _hledit.edit(hydraulicLoop, self.hydraulicLoops, self.fluids)