######################################################################################################################
# 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/>.
######################################################################################################################
"""
Models to represent alternatives, scenarios and scenario alternatives in a tree.
:authors: P. Vennström (VTT), M. Marin (KTH)
:date: 17.6.2020
"""
import json
from PySide2.QtCore import QMimeData, Qt, QModelIndex
from spinetoolbox.mvcmodels.minimal_tree_model import MinimalTreeModel
from .tree_item_utility import NonLazyDBItem
from .alternative_scenario_item import (
NonLazyTreeItem,
AlternativeRootItem,
ScenarioRootItem,
AlternativeLeafItem,
ScenarioLeafItem,
)
[docs]class AlternativeScenarioModel(MinimalTreeModel):
"""A model to display parameter_value_list data in a tree view.
Args:
parent (SpineDBEditor)
db_mngr (SpineDBManager)
db_maps (iter): DiffDatabaseMapping instances
"""
def __init__(self, parent, db_mngr, *db_maps):
"""Initialize class"""
super().__init__(parent)
self.db_mngr = db_mngr
self.db_maps = db_maps
[docs] def columnCount(self, parent=QModelIndex()):
"""Returns the number of columns under the given parent. Always 1.
"""
return 2
[docs] def build_tree(self):
"""Builds tree."""
self.beginResetModel()
self._invisible_root_item = NonLazyTreeItem(self)
self.endResetModel()
for db_map in self.db_maps:
db_item = NonLazyDBItem(db_map)
self._invisible_root_item.append_children(db_item)
alt_root_item = AlternativeRootItem()
scen_root_item = ScenarioRootItem()
db_item.append_children(alt_root_item, scen_root_item)
[docs] def _add_leaves(self, db_map_data, leaf_type):
root_number, leaf_maker = {"alternative": (0, AlternativeLeafItem), "scenario": (1, ScenarioLeafItem)}[
leaf_type
]
for db_item in self._invisible_root_item.children:
items = db_map_data.get(db_item.db_map)
if not items:
continue
root_item = db_item.child(root_number)
# First realize the ones added locally
ids = {x["name"]: x["id"] for x in items}
for leaf_item in root_item.children[:-1]:
id_ = ids.pop(leaf_item.name, None)
if not id_:
continue
leaf_item.handle_added_to_db(identifier=id_)
# Now append the ones added externally
children = [leaf_maker(id_) for id_ in ids.values()]
root_item.insert_children(root_item.child_count() - 1, *children)
[docs] def _update_leaves(self, db_map_data, leaf_type):
root_number = {"alternative": 0, "scenario": 1}[leaf_type]
self.layoutAboutToBeChanged.emit()
for db_item in self._invisible_root_item.children:
items = db_map_data.get(db_item.db_map)
if not items:
continue
root_item = db_item.child(root_number)
ids = {x["id"] for x in items}
leaf_items = {leaf_item.id: leaf_item for leaf_item in root_item.children[:-1]}
for id_ in ids.intersection(leaf_items):
leaf_items[id_].handle_updated_in_db()
self.layoutChanged.emit()
[docs] def _remove_leaves(self, db_map_data, leaf_type):
root_number = {"alternative": 0, "scenario": 1}[leaf_type]
self.layoutAboutToBeChanged.emit()
for db_item in self._invisible_root_item.children:
items = db_map_data.get(db_item.db_map)
if not items:
continue
root_item = db_item.child(root_number)
ids = {x["id"] for x in items}
removed_rows = []
for row, leaf_item in enumerate(root_item.children[:-1]):
if leaf_item.id in ids:
removed_rows.append(row)
for row in sorted(removed_rows, reverse=True):
root_item.remove_children(row, 1)
self.layoutChanged.emit()
[docs] def add_alternatives(self, db_map_data):
self._add_leaves(db_map_data, "alternative")
[docs] def add_scenarios(self, db_map_data):
self._add_leaves(db_map_data, "scenario")
[docs] def update_alternatives(self, db_map_data):
self._update_leaves(db_map_data, "alternative")
[docs] def update_scenarios(self, db_map_data):
self._update_leaves(db_map_data, "scenario")
[docs] def remove_alternatives(self, db_map_data):
self._remove_leaves(db_map_data, "alternative")
[docs] def remove_scenarios(self, db_map_data):
self._remove_leaves(db_map_data, "scenario")
@staticmethod
[docs] def db_item(item):
while item.item_type != "db":
item = item.parent_item
return item
[docs] def db_row(self, item):
return self.db_item(item).child_number()
[docs] def supportedDropActions(self):
return Qt.CopyAction | Qt.MoveAction
[docs] def mimeData(self, indexes):
"""
Builds a dict mapping db name to item type to a list of ids.
Returns:
QMimeData
"""
items = {self.item_from_index(ind): None for ind in indexes} # NOTE: this avoids dupes and keeps order
d = {}
for item in items:
parent_item = item.parent_item
db_row = self.db_row(parent_item)
parent_type = parent_item.item_type
master_key = ";;".join([str(db_row), parent_type, str(parent_item.child_number())])
d.setdefault(master_key, []).append(item.child_number())
data = json.dumps(d)
mime = QMimeData()
mime.setText(data)
return mime
[docs] def canDropMimeData(self, data, drop_action, row, column, parent):
if not data.hasText():
return False
try:
data = json.loads(data.text())
except ValueError:
return False
if not isinstance(data, dict):
return False
# Check that all source data comes from the same db and parent
if len(data) != 1:
return False
master_key = next(iter(data))
db_row, parent_type, parent_row = master_key.split(";;")
db_row = int(db_row)
if parent_type not in ("alternative root", "scenario"):
return False
# Check that target is in the same db as source
scenario_item = self.item_from_index(parent)
if db_row != self.db_row(scenario_item):
return False
if parent_type == "scenario":
# Check that reordering only happens within the same scenario
scenario_row = parent_row
if int(scenario_row) != scenario_item.child_number():
return False
return True
[docs] def dropMimeData(self, data, drop_action, row, column, parent):
scenario_item = self.item_from_index(parent)
alternative_id_list = scenario_item.alternative_id_list
if row == -1:
row = len(alternative_id_list)
master_key, alternative_rows = json.loads(data.text()).popitem()
db_row, parent_type, _parent_row = master_key.split(";;")
db_row = int(db_row)
if parent_type == "alternative root":
alt_root_item = self._invisible_root_item.child(db_row).child(0)
alternative_ids = [alt_root_item.child(row).id for row in alternative_rows]
alternative_ids = [id_ for id_ in alternative_ids if id_ not in set(alternative_id_list) | {None}]
elif parent_type == "scenario":
alternative_ids = [scenario_item.child(row).id for row in alternative_rows]
alternative_id_list = [id_ for id_ in alternative_id_list if id_ not in alternative_ids]
alternative_id_list[row:row] = alternative_ids
db_item = {"id": scenario_item.id, "alternative_id_list": ",".join([str(id_) for id_ in alternative_id_list])}
self.db_mngr.set_scenario_alternatives({scenario_item.db_map: [db_item]})
return True