######################################################################################################################
# Copyright (C) 2017-2020 Spine project consortium
# This file is part of Spine Toolbox.
# Spine Toolbox is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General
# Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option)
# any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General
# Public License for more details. You should have received a copy of the GNU Lesser General Public License along with
# this program. If not, see <http://www.gnu.org/licenses/>.
######################################################################################################################
"""
Contains the GraphViewMixin class.
:author: M. Marin (KTH)
:date: 26.11.2018
"""
import itertools
from PySide2.QtCore import Signal, Slot, QRectF
from PySide2.QtWidgets import QGraphicsTextItem, QActionGroup
from PySide2.QtPrintSupport import QPrinter
from PySide2.QtGui import QPainter
from spinedb_api import to_database, from_database
from ...widgets.custom_qwidgets import ZoomWidgetAction, RotateWidgetAction
from ...widgets.custom_qgraphicsscene import CustomGraphicsScene
from ..graphics_items import (
EntityItem,
ObjectItem,
RelationshipItem,
ArcItem,
CrossHairsItem,
CrossHairsRelationshipItem,
CrossHairsArcItem,
make_figure_graphics_item,
)
from .graph_layout_generator import GraphLayoutGenerator, make_heat_map
from ...helpers import get_save_file_name_in_last_dir
from .add_items_dialogs import AddReadyRelationshipsDialog
[docs]class GraphViewMixin:
"""Provides the graph view for the DS form."""
[docs] _POS_PARAM_NAME = "entity_graph_position"
[docs] _ARC_WIDTH = 0.15 * _VERTEX_EXTENT
[docs] _ARC_LENGTH_HINT = 1.5 * _VERTEX_EXTENT
[docs] graph_selection_changed = Signal(object)
[docs] graph_build_finished = Signal()
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.graph_db_map = self.first_db_map
self._show_cascading_relationships = (
self.qsettings.value("appSettings/showCascadingRelationships", defaultValue="false") == "true"
)
self.ui.actionShow_cascading_relationships.setChecked(self._show_cascading_relationships)
self._persistent = False
self.scene = None
self.object_items = list()
self.relationship_items = list()
self.arc_items = list()
self.selected_tree_inds = {}
self.object_ids = list()
self.relationship_ids = list()
self.src_inds = list()
self.dst_inds = list()
self.hidden_items = list()
self.prunned_entity_ids = dict()
self.removed_items = list()
self.selected_items = list()
self._relationships_being_added = False
self.added_relationship_ids = set()
self.wip_relationship_class = None
self._blank_item = None
self._point_value_tuples_per_parameter_name = dict() # Used in the menu add heat map
self.heat_map_items = []
self.zoom_widget_action = None
self.rotate_widget_action = None
self.graph_db_action_group = QActionGroup(self)
self.layout_gens = list()
self.ui.graphicsView.connect_data_store_form(self)
self.setup_widget_actions()
self.populate_graph_db_action_group()
[docs] def populate_graph_db_action_group(self):
menu = self._make_db_menu()
if menu is None:
return
before = self.ui.menuGraph.actions()[0]
self.ui.menuGraph.insertMenu(before, menu)
actions = menu.actions()
for action in actions:
self.graph_db_action_group.addAction(action)
actions[0].setChecked(True)
@Slot("QAction")
[docs] def _update_graph_db_map(self, action):
self.graph_db_map = self.db_maps_by_codename[action.text()]
self.build_graph()
[docs] def init_models(self):
super().init_models()
self.scene = CustomGraphicsScene(self)
self.ui.graphicsView.setScene(self.scene)
[docs] def connect_signals(self):
"""Connects signals."""
super().connect_signals()
self.ui.treeView_object.tree_selection_changed.connect(self.rebuild_graph)
self.ui.treeView_relationship.tree_selection_changed.connect(self.rebuild_graph)
self.ui.dockWidget_entity_graph.visibilityChanged.connect(self._handle_entity_graph_visibility_changed)
self.ui.actionHide_selected.triggered.connect(self.hide_selected_items)
self.ui.actionShow_hidden.triggered.connect(self.show_hidden_items)
self.ui.actionPrune_selected_entities.triggered.connect(self.prune_selected_entities)
self.ui.actionPrune_selected_classes.triggered.connect(self.prune_selected_classes)
self.ui.actionRestore_all_pruned.triggered.connect(self.restore_all_pruned_items)
self.ui.actionSave_positions.triggered.connect(self.save_positions)
self.ui.actionClear_positions.triggered.connect(self.clear_saved_positions)
self.ui.actionExport_graph_as_pdf.triggered.connect(self.export_as_pdf)
self.ui.actionRebuild_graph.triggered.connect(self.build_graph)
self.ui.actionShow_cascading_relationships.toggled.connect(self.set_show_cascading_relationships)
self.ui.menuAdd_parameter_heat_map.triggered.connect(self.add_heat_map)
# Dock Widgets menu action
self.ui.menuGraph.aboutToShow.connect(self._handle_menu_graph_about_to_show)
self.zoom_widget_action.minus_pressed.connect(self.ui.graphicsView.zoom_out)
self.zoom_widget_action.plus_pressed.connect(self.ui.graphicsView.zoom_in)
self.zoom_widget_action.reset_pressed.connect(self.ui.graphicsView.reset_zoom)
self.rotate_widget_action.clockwise_pressed.connect(self.ui.graphicsView.rotate_clockwise)
self.rotate_widget_action.anticlockwise_pressed.connect(self.ui.graphicsView.rotate_anticlockwise)
self.scene.selectionChanged.connect(self._handle_scene_selection_changed)
self.graph_db_action_group.triggered.connect(self._update_graph_db_map)
@Slot()
[docs] def _handle_scene_selection_changed(self):
"""Filters parameters by selected objects in the graph."""
if self.scene is None:
return
selected_items = self.scene.selectedItems()
selected_objs = [x for x in selected_items if isinstance(x, ObjectItem)]
selected_rels = [x for x in selected_items if isinstance(x, RelationshipItem)]
self.selected_items = selected_objs + selected_rels
self.graph_selection_changed.emit({"object": selected_objs, "relationship": selected_rels})
@Slot(bool)
[docs] def set_show_cascading_relationships(self, checked):
self.ui.actionShow_cascading_relationships.setChecked(checked)
self._show_cascading_relationships = checked
self.build_graph()
[docs] def receive_objects_added(self, db_map_data):
"""Runs when objects are added to the db.
Adds the new objects to the graph if needed.
Args:
db_map_data (dict): list of dictionary-items keyed by DiffDatabaseMapping instance.
"""
super().receive_objects_added(db_map_data)
objects = db_map_data.get(self.graph_db_map, [])
added_ids = {x["id"] for x in objects}
self.restore_removed_entities(added_ids)
[docs] def receive_relationships_added(self, db_map_data):
"""Runs when relationships are added to the db.
Adds the new relationships to the graph if needed.
Args:
db_map_data (dict): list of dictionary-items keyed by DiffDatabaseMapping instance.
"""
super().receive_relationships_added(db_map_data)
relationships = db_map_data.get(self.graph_db_map, [])
added_ids = {x["id"] for x in relationships}
restored_ids = self.restore_removed_entities(added_ids)
added_ids -= restored_ids
if added_ids and self._relationships_being_added:
self.added_relationship_ids.update(added_ids)
self.build_graph(persistent=True)
self._end_add_relationships()
[docs] def receive_object_classes_updated(self, db_map_data):
super().receive_object_classes_updated(db_map_data)
self.refresh_icons(db_map_data)
[docs] def receive_relationship_classes_updated(self, db_map_data):
super().receive_relationship_classes_updated(db_map_data)
self.refresh_icons(db_map_data)
[docs] def receive_objects_updated(self, db_map_data):
"""Runs when objects are updated in the db. Refreshes names of objects in graph.
Args:
db_map_data (dict): list of dictionary-items keyed by DiffDatabaseMapping instance.
"""
super().receive_objects_updated(db_map_data)
updated_ids = {x["id"]: (x["name"], x["description"]) for x in db_map_data.get(self.graph_db_map, [])}
for item in self.ui.graphicsView.items():
if isinstance(item, ObjectItem) and item.entity_id in updated_ids:
name, description = updated_ids[item.entity_id]
item.update_name(name)
item.update_description(description)
[docs] def receive_objects_removed(self, db_map_data):
"""Runs when objects are removed from the db. Rebuilds graph if needed.
Args:
db_map_data (dict): list of dictionary-items keyed by DiffDatabaseMapping instance.
"""
super().receive_objects_removed(db_map_data)
self.hide_removed_entities(db_map_data)
[docs] def receive_relationships_removed(self, db_map_data):
"""Runs when relationships are removed from the db. Rebuilds graph if needed.
Args:
db_map_data (dict): list of dictionary-items keyed by DiffDatabaseMapping instance.
"""
super().receive_relationships_removed(db_map_data)
self.hide_removed_entities(db_map_data)
[docs] def restore_removed_entities(self, added_ids):
"""Restores any entities that have been previously removed and returns their ids.
This happens in the context of undo/redo.
Args:
added_ids (set(int)): Set of newly added ids.
Returns:
set(int)
"""
restored_items = [item for item in self.removed_items if item.entity_id in added_ids]
for item in restored_items:
self.removed_items.remove(item)
item.set_all_visible(True)
return {item.entity_id for item in restored_items}
[docs] def hide_removed_entities(self, db_map_data):
"""Hides removed entities while saving them into a list attribute.
This allows entities to be restored in case the user undoes the operation."""
removed_ids = {x["id"] for x in db_map_data.get(self.graph_db_map, [])}
self.added_relationship_ids -= removed_ids
removed_items = [
item
for item in self.ui.graphicsView.items()
if isinstance(item, EntityItem) and item.entity_id in removed_ids
]
if not removed_items:
return
self.removed_items.extend(removed_items)
self.scene.selectionChanged.disconnect(self._handle_scene_selection_changed)
for item in removed_items:
item.set_all_visible(False)
self.scene.selectionChanged.connect(self._handle_scene_selection_changed)
[docs] def refresh_icons(self, db_map_data):
"""Runs when entity classes are updated in the db. Refreshes icons of entities in graph.
Args:
db_map_data (dict): list of dictionary-items keyed by DiffDatabaseMapping instance.
"""
updated_ids = {x["id"] for x in db_map_data.get(self.graph_db_map, [])}
for item in self.ui.graphicsView.items():
if isinstance(item, EntityItem) and item.entity_class_id in updated_ids:
item.refresh_icon()
@Slot()
[docs] def _handle_menu_graph_about_to_show(self):
"""Enables or disables actions according to current selection in the graph."""
visible = self.ui.dockWidget_entity_graph.isVisible()
self.ui.actionSave_positions.setEnabled(visible and bool(self.selected_items))
self.ui.actionClear_positions.setEnabled(visible and bool(self.selected_items))
self.ui.actionHide_selected.setEnabled(visible and bool(self.selected_items))
self.ui.actionShow_hidden.setEnabled(visible and bool(self.hidden_items))
self.ui.actionPrune_selected_entities.setEnabled(visible and bool(self.selected_items))
self.ui.actionPrune_selected_classes.setEnabled(visible and bool(self.selected_items))
self.ui.menuRestore_pruned.setEnabled(visible and any(self.prunned_entity_ids.values()))
self.ui.actionRestore_all_pruned.setEnabled(visible and any(self.prunned_entity_ids.values()))
self.zoom_widget_action.setEnabled(visible)
self.ui.actionPrune_selected_entities.setText(f"Prune {self._get_selected_entity_names()}")
self.ui.actionPrune_selected_classes.setText(f"Prune {self._get_selected_class_names()}")
self.ui.actionRebuild_graph.setEnabled(visible)
self.zoom_widget_action.setEnabled(visible)
self.rotate_widget_action.setEnabled(visible)
self.ui.menuAdd_parameter_heat_map.setEnabled(visible)
if visible:
self._populate_menu_add_parameter_heat_map()
@Slot(bool)
[docs] def _handle_entity_graph_visibility_changed(self, visible):
self.build_graph()
@Slot(dict)
[docs] def rebuild_graph(self, selected):
"""Stores the given selection of entity tree indexes and builds graph."""
self.selected_tree_inds = selected
self.added_relationship_ids.clear()
self.build_graph()
[docs] def build_graph(self, persistent=False):
"""Builds the graph.
Args:
persistent (bool, optional): If True, builds the graph on top of the current one.
"""
if not self.ui.dockWidget_entity_graph.isVisible():
return
self.ui.graphicsView.clear_cross_hairs_items() # Needed
self._persistent = persistent
for layout_gen in self.layout_gens:
layout_gen.stop()
self._update_graph_data()
layout_gen = self._make_layout_generator()
self.layout_gens.append(layout_gen)
layout_gen.show_progress_widget(self.ui.graphicsView)
layout_gen.finished.connect(self._complete_graph)
layout_gen.done.connect(lambda layout_gen=layout_gen: self.layout_gens.remove(layout_gen))
layout_gen.start()
@Slot(object, object)
[docs] def _complete_graph(self, x, y):
"""
Args:
x (list): Horizontal coordinates
y (list): Vertical coordinates
"""
if self.layout_gens:
return
self.hidden_items.clear()
self.removed_items.clear()
self.selected_items.clear()
self.heat_map_items.clear()
self.scene.clear()
if not self._make_new_items(x, y):
self._blank_item = QGraphicsTextItem("Nothing to show.")
self.scene.addItem(self._blank_item)
self.ui.actionExport_graph_as_pdf.setEnabled(False)
else:
self._add_new_items() # pylint: disable=no-value-for-parameter
self.ui.actionExport_graph_as_pdf.setEnabled(True)
if not self._persistent:
self.ui.graphicsView.reset_zoom()
else:
self.ui.graphicsView.apply_zoom()
self.graph_build_finished.emit()
[docs] def _get_selected_entity_ids(self):
"""Returns a set of ids corresponding to selected entities in the trees.
Returns:
set: selected object ids
set: selected relationship ids
"""
if "root" in self.selected_tree_inds:
return set(x["id"] for x in self.db_mngr.get_items(self.graph_db_map, "object")), set()
selected_object_ids = set()
selected_relationship_ids = set()
for index in self.selected_tree_inds.get("object", {}):
item = index.model().item_from_index(index)
object_id = item.db_map_id(self.graph_db_map)
selected_object_ids.add(object_id)
for index in self.selected_tree_inds.get("relationship", {}):
item = index.model().item_from_index(index)
relationship_id = item.db_map_id(self.graph_db_map)
selected_relationship_ids.add(relationship_id)
for index in self.selected_tree_inds.get("object_class", {}):
item = index.model().item_from_index(index)
object_ids = set(item._get_children_ids(self.graph_db_map))
selected_object_ids.update(object_ids)
for index in self.selected_tree_inds.get("relationship_class", {}):
item = index.model().item_from_index(index)
relationship_ids = set(item._get_children_ids(self.graph_db_map))
selected_relationship_ids.update(relationship_ids)
return selected_object_ids, selected_relationship_ids
[docs] def _get_all_relationships_for_graph(self, object_ids, relationship_ids):
cond = any if self._show_cascading_relationships else all
return [
x
for x in self.db_mngr.get_items(self.graph_db_map, "relationship")
if cond([int(id_) in object_ids for id_ in x["object_id_list"].split(",")])
] + [self.db_mngr.get_item(self.graph_db_map, "relationship", id_) for id_ in relationship_ids]
[docs] def _update_graph_data(self):
"""Updates data for graph according to selection in trees."""
object_ids, relationship_ids = self._get_selected_entity_ids()
relationship_ids.update(self.added_relationship_ids)
prunned_entity_ids = {id_ for ids in self.prunned_entity_ids.values() for id_ in ids}
object_ids -= prunned_entity_ids
relationship_ids -= prunned_entity_ids
relationships = self._get_all_relationships_for_graph(object_ids, relationship_ids)
object_id_lists = dict()
for relationship in relationships:
if relationship["id"] in prunned_entity_ids:
continue
object_id_list = [
id_
for id_ in (int(x) for x in relationship["object_id_list"].split(","))
if id_ not in prunned_entity_ids
]
if len(object_id_list) < 2:
continue
object_ids.update(object_id_list)
object_id_lists[relationship["id"]] = object_id_list
self.object_ids = list(object_ids)
self.relationship_ids = list(object_id_lists)
self._update_src_dst_inds(object_id_lists)
[docs] def _update_src_dst_inds(self, object_id_lists):
self.src_inds = list()
self.dst_inds = list()
object_ind_lookup = {id_: k for k, id_ in enumerate(self.object_ids)}
relationship_ind_lookup = {id_: len(self.object_ids) + k for k, id_ in enumerate(self.relationship_ids)}
for relationship_id, object_id_list in object_id_lists.items():
object_inds = [object_ind_lookup[object_id] for object_id in object_id_list]
relationship_ind = relationship_ind_lookup[relationship_id]
for object_ind in object_inds:
self.src_inds.append(relationship_ind)
self.dst_inds.append(object_ind)
[docs] def _make_layout_generator(self):
"""Returns a layout generator for the current graph.
Returns:
GraphLayoutGenerator
"""
parameter_positions = {
p["entity_id"]: dict(from_database(p["value"]).value_to_database_data())
for p in self.db_mngr.get_items_by_field(
self.graph_db_map, "parameter_value", "parameter_name", self._POS_PARAM_NAME
)
}
if self._persistent:
persisted_positions = {
item.entity_id: {"x": item.pos().x(), "y": item.pos().y()}
for item in self.ui.graphicsView.items()
if isinstance(item, EntityItem)
}
else:
persisted_positions = {}
persisted_positions.update(parameter_positions)
entity_ids = self.object_ids + self.relationship_ids
heavy_positions = {
ind: persisted_positions[id_] for ind, id_ in enumerate(entity_ids) if id_ in persisted_positions
}
return GraphLayoutGenerator(
len(entity_ids), self.src_inds, self.dst_inds, self._ARC_LENGTH_HINT, heavy_positions=heavy_positions
)
[docs] def _make_new_items(self, x, y):
"""Returns new items for the graph.
Args:
x (list)
y (list)
"""
self.object_items = list()
self.relationship_items = list()
self.arc_items = list()
for i, object_id in enumerate(self.object_ids):
object_item = ObjectItem(self, x[i], y[i], self._VERTEX_EXTENT, object_id)
self.object_items.append(object_item)
offset = len(self.object_items)
for i, relationship_id in enumerate(self.relationship_ids):
relationship_item = RelationshipItem(
self, x[offset + i], y[offset + i], 0.5 * self._VERTEX_EXTENT, relationship_id
)
self.relationship_items.append(relationship_item)
for rel_ind, obj_ind in zip(self.src_inds, self.dst_inds):
arc_item = ArcItem(self.relationship_items[rel_ind - offset], self.object_items[obj_ind], self._ARC_WIDTH)
self.arc_items.append(arc_item)
return any(self.object_items)
[docs] def _add_new_items(self):
for item in self.object_items + self.relationship_items + self.arc_items:
self.scene.addItem(item)
@Slot(bool)
[docs] def hide_selected_items(self, checked=False):
"""Hides selected items."""
self.hidden_items.extend(self.selected_items)
for item in self.selected_items:
item.set_all_visible(False)
@Slot(bool)
[docs] def show_hidden_items(self, checked=False):
"""Shows hidden items."""
if not self.scene:
return
for item in self.hidden_items:
item.set_all_visible(True)
self.hidden_items.clear()
[docs] def _get_selected_entity_names(self):
if len(self.selected_items) == 1:
return "'" + self.selected_items[0].entity_name + "'"
return "selected entities"
[docs] def _get_selected_class_names(self):
if len(self.selected_items) == 1:
return "'" + self.selected_items[0].entity_class_name + "'"
return "selected classes"
@Slot(bool)
[docs] def prune_selected_entities(self, checked=False):
"""Prunes selected items."""
entity_ids = {x.entity_id for x in self.selected_items}
key = self._get_selected_entity_names()
self.prunned_entity_ids[key] = entity_ids
action = self.ui.menuRestore_pruned.addAction(key)
action.triggered.connect(lambda checked=False, key=key: self.restore_pruned_items(key))
self.build_graph()
@Slot(bool)
[docs] def prune_selected_classes(self, checked=False):
"""Prunes selected items."""
class_ids = {x.entity_class_id for x in self.selected_items}
entity_ids = {
x["id"] for x in self.db_mngr.get_items(self.graph_db_map, "object") if x["class_id"] in class_ids
}
entity_ids |= {
x["id"] for x in self.db_mngr.get_items(self.graph_db_map, "relationship") if x["class_id"] in class_ids
}
key = self._get_selected_class_names()
self.prunned_entity_ids[key] = entity_ids
action = self.ui.menuRestore_pruned.addAction(key)
action.triggered.connect(lambda checked=False, key=key: self.restore_pruned_items(key))
self.build_graph()
@Slot(bool)
[docs] def restore_all_pruned_items(self, checked=False):
"""Reinstates all pruned items."""
self.prunned_entity_ids.clear()
self.build_graph()
[docs] def restore_pruned_items(self, key):
"""Reinstates last pruned items."""
if self.prunned_entity_ids.pop(key, None) is not None:
action = next(iter(a for a in self.ui.menuRestore_pruned.actions() if a.text() == key))
self.ui.menuRestore_pruned.removeAction(action)
self.build_graph()
[docs] def edit_entity_graph_items(self):
"""Starts editing given indexes."""
obj_items = [item for item in self.selected_items if isinstance(item, ObjectItem)]
rel_items = [item for item in self.selected_items if isinstance(item, RelationshipItem)]
self.show_edit_objects_form(obj_items)
self.show_edit_relationships_form(rel_items)
[docs] def remove_entity_graph_items(self, checked=False):
"""Removes all selected items in the graph."""
if not self.selected_items:
return
db_map_typed_data = {self.graph_db_map: {}}
for item in self.selected_items:
id_ = item.entity_id
db_map_typed_data[self.graph_db_map].setdefault(item.entity_type, []).add(id_)
self.db_mngr.remove_items(db_map_typed_data)
@Slot(bool)
[docs] def save_positions(self, checked=False):
items_per_class_id = {}
for item in self.selected_items:
items_per_class_id.setdefault(item.entity_class_id, []).append(item)
pos_def_class_ids = {
p["entity_class_id"]
for p in self.db_mngr.get_items_by_field(
self.graph_db_map, "parameter_definition", "parameter_name", self._POS_PARAM_NAME
)
}
defs_to_add = [
{"name": self._POS_PARAM_NAME, "entity_class_id": class_id}
for class_id in items_per_class_id.keys() - pos_def_class_ids
]
if defs_to_add:
self.db_mngr.add_parameter_definitions({self.graph_db_map: defs_to_add})
pos_def_id_lookup = {
p["entity_class_id"]: p["id"]
for p in self.db_mngr.get_items_by_field(
self.graph_db_map, "parameter_definition", "parameter_name", self._POS_PARAM_NAME
)
}
pos_val_id_lookup = {
(p["entity_class_id"], p["entity_id"]): p["id"]
for p in self.db_mngr.get_items_by_field(
self.graph_db_map, "parameter_value", "parameter_name", self._POS_PARAM_NAME
)
}
vals_to_add = list()
vals_to_update = list()
for class_id, items in items_per_class_id.items():
for item in items:
pos_val_id = pos_val_id_lookup.get((class_id, item.entity_id), None)
value = {"type": "map", "index_type": "str", "data": [["x", item.pos().x()], ["y", item.pos().y()]]}
if pos_val_id is None:
vals_to_add.append(
{
"name": "pos_x",
"entity_class_id": class_id,
"entity_id": item.entity_id,
"parameter_definition_id": pos_def_id_lookup[class_id],
"value": to_database(value),
}
)
else:
vals_to_update.append({"id": pos_val_id, "value": to_database(value)})
if vals_to_add:
self.db_mngr.add_parameter_values({self.graph_db_map: vals_to_add})
if vals_to_update:
self.db_mngr.update_parameter_values({self.graph_db_map: vals_to_update})
@Slot(bool)
[docs] def clear_saved_positions(self, checked=False):
entity_ids = {x.entity_id for x in self.selected_items}
value_ids_to_remove = [
p["id"]
for p in self.db_mngr.get_items_by_field(
self.graph_db_map, "parameter_value", "parameter_name", self._POS_PARAM_NAME
)
if p["entity_id"] in entity_ids
]
if value_ids_to_remove:
self.db_mngr.remove_items({self.graph_db_map: {"parameter_value": value_ids_to_remove}})
self.build_graph()
@Slot(bool)
[docs] def export_as_pdf(self, checked=False):
self.qsettings.beginGroup(self.settings_group)
file_path, _ = get_save_file_name_in_last_dir(
self.qsettings, "exportGraphAsPDF", self, "Export as PDF...", self._get_base_dir(), "PDF files (*.pdf)"
)
self.qsettings.endGroup()
if not file_path:
return
view = self.ui.graphicsView
source = view._get_viewport_scene_rect()
current_zoom_factor = view.zoom_factor
view._zoom(1.0 / current_zoom_factor)
self.scene.clearSelection()
printer = QPrinter()
printer.setPaperSize(source.size(), QPrinter.Point)
printer.setOutputFileName(file_path)
painter = QPainter(printer)
self.scene.render(painter, QRectF(), source)
painter.end()
view._zoom(current_zoom_factor)
self._insert_open_file_button(file_path)
[docs] def start_relationship(self, relationship_class, obj_item):
"""Starts a relationship from the given object item.
Args:
relationship_class (dict)
obj_item (..graphics_items.ObjectItem)
"""
object_class_ids_to_go = relationship_class["object_class_id_list"].copy()
object_class_ids_to_go.remove(obj_item.entity_class_id)
relationship_class["object_class_ids_to_go"] = object_class_ids_to_go
ch_item = CrossHairsItem(self, obj_item.pos().x(), obj_item.pos().y(), self._VERTEX_EXTENT)
ch_rel_item = CrossHairsRelationshipItem(
self, obj_item.pos().x(), obj_item.pos().y(), 0.5 * self._VERTEX_EXTENT
)
ch_arc_item1 = CrossHairsArcItem(ch_rel_item, obj_item, self._ARC_WIDTH)
ch_arc_item2 = CrossHairsArcItem(ch_rel_item, ch_item, self._ARC_WIDTH)
ch_rel_item.refresh_icon()
self.ui.graphicsView.set_cross_hairs_items(
relationship_class, [ch_item, ch_rel_item, ch_arc_item1, ch_arc_item2]
)
[docs] def finalize_relationship(self, relationship_class, *object_items):
"""Tries to add relationships between the given object items.
Args:
relationship_class (dict)
object_items (..graphics_items.ObjectItem)
"""
relationships = set()
object_class_id_list = relationship_class["object_class_id_list"]
for item_permutation in itertools.permutations(object_items):
if [item.entity_class_id for item in item_permutation] == object_class_id_list:
relationship = tuple(item.entity_name for item in item_permutation)
relationships.add(relationship)
dialog = AddReadyRelationshipsDialog(self, relationship_class, list(relationships), self.db_mngr, *self.db_maps)
dialog.accepted.connect(self._begin_add_relationships)
dialog.show()
[docs] def _begin_add_relationships(self):
self._relationships_being_added = True
@Slot()
[docs] def _end_add_relationships(self):
self._relationships_being_added = False
@Slot("QAction")
[docs] def add_heat_map(self, action):
"""Adds heat map for the parameter in the action text.
"""
self._clean_up_heat_map_items()
point_value_tuples = self._point_value_tuples_per_parameter_name[action.text()]
x, y, values = zip(*point_value_tuples)
heat_map, xv, yv, min_x, min_y, max_x, max_y = make_heat_map(x, y, values)
heat_map_item, hm_figure = make_figure_graphics_item(self.scene, z=-3, static=True)
colorbar_item, cb_figure = make_figure_graphics_item(self.scene, z=3, static=False)
colormesh = hm_figure.gca().pcolormesh(xv, yv, heat_map)
cb_figure.colorbar(colormesh, fraction=1)
cb_figure.gca().set_visible(False)
width = max_x - min_x
height = max_y - min_y
heat_map_item.widget().setGeometry(min_x, min_y, width, height)
colorbar_item.widget().setGeometry(max_x + self._VERTEX_EXTENT, min_y, 2 * self._VERTEX_EXTENT, height)
self.heat_map_items += [heat_map_item, colorbar_item]
[docs] def _clean_up_heat_map_items(self):
for item in self.heat_map_items:
item.hide()
self.scene.removeItem(item)
self.heat_map_items.clear()
[docs] def closeEvent(self, event):
"""Handle close window.
Args:
event (QCloseEvent): Closing event
"""
super().closeEvent(event)
self.scene = None