Module soitool.soi
Includes datastructure used to represent a SOI.
Expand source code
"""Includes datastructure used to represent a SOI."""
from datetime import datetime
from PySide2.QtCore import QPoint
from rectpack import (
newPacker,
SORT_NONE,
PackingMode,
PackingBin,
maxrects,
skyline,
guillotine,
)
from soitool.modules.module_base import set_module_pos
# Functions to sort modules by different criteria
def modules_sort_by_none(modules):
"""Don't sort. Implemented for completeness. See SOI.sort_modules.
Parameters
----------
modules : list
List of modules.
Returns
-------
list
List of modules, untouched.
"""
return modules
def modules_sort_by_area(modules):
"""Sort modules by area. See SOI.sort_modules.
Parameters
----------
modules : list
List of modules to sort.
Returns
-------
list
List of modules sorted by area.
"""
def module_area_key(module):
width, height = module["widget"].get_size()
return width * height
return sorted(modules, key=module_area_key, reverse=True)
def modules_sort_by_width(modules):
"""Sort modules by width. See SOI.sort_modules.
Parameters
----------
modules : list
List of modules to sort.
Returns
-------
list
List of modules sorted by width.
"""
def module_width_key(module):
width, _ = module["widget"].get_size()
return width
return sorted(modules, key=module_width_key, reverse=True)
def modules_sort_by_height(modules):
"""Sort modules by height. See SOI.sort_modules.
Parameters
----------
modules : list
List of modules to sort.
Returns
-------
list
List of modules sorted by height.
"""
def module_height_key(module):
_, height = module["widget"].get_size()
return height
return sorted(modules, key=module_height_key, reverse=True)
# Dicts that map between name of algorithm and implementation of algorithm
STRING_TO_ALGORITHM_RECTPACK_BIN = {
"BFF": PackingBin.BFF,
"BBF": PackingBin.BBF,
}
STRING_TO_ALGORITHM_RECTPACK_PACK = {
"MaxRectsBl": maxrects.MaxRectsBl,
"SkylineBl": skyline.SkylineBl,
"GuillotineBssfSas": guillotine.GuillotineBssfSas,
}
STRING_TO_ALGORITHM_INITIAL_SORT = {
"none": modules_sort_by_none,
"area": modules_sort_by_area,
"width": modules_sort_by_width,
"height": modules_sort_by_height,
}
class ModuleLargerThanBinError(Exception):
"""At least one module is too large for the bin during rectpack packing."""
class ModuleNameTaken(Exception):
"""Module name is already taken by an existing module."""
class SOI:
"""Datastructure for SOI.
Holds all info about an SOI necessary to view and edit it.
## Note about rectpack
This class relies heavily on the rectpack library for optimal placement of
modules when placement_strategy is 'auto'. Refer to the rectpack
documentation for details beyond what is provided in this class:
https://github.com/secnot/rectpack
## Anatomy of an SOI
Below is an illustration of how an SOI with two pages is represented using
the class variables.
```text
SOI.WIDTH
|
v
_________________
SOI.PADDING
| SOI.HEADER_WIDTH
| | SOI.INNER_PADDING
| | | SOI.CONTENT_WIDTH
| | | | SOI.INNER_PADDING
| | | | | SOI.PADDING
| | | | | |
v v v v v v
_ ___ _ _________ _ _
+---------------------+
| | | <- SOI.PADDING | <- SOI.HEIGHT
| +---+-------------+ | |
| | H | | | | <- SOI.INNER_PADDING |
| | E | +---------+ | | |
| | A | | MODULES | | | | <- SOI.CONTENT_HEIGHT |
| | D | | | | | | |
| | E | | | | | | |
| | R | | | | | | |
| | | +---------+ | | |
| | | | | | <- SOI.INNER_PADDDING |
| +---+-------------+ | |
| | | <- SOI.PADDING |
+---------------------+
+---------------------+
| | | <- SOI.PADDING | <- SOI.HEIGHT
| +---+-------------+ | |
| | H | | | | <- SOI.INNER_PADDING |
| | E | +---------+ | | |
| | A | | MODULES | | | | <- SOI.CONTENT_HEIGHT |
| | D | | | | | | |
| | E | | | | | | |
| | R | | | | | | |
| | | +---------+ | | |
| | | | | | <- SOI.INNER_PADDDING |
| +---+-------------+ | |
| | | <- SOI.PADDING |
+---------------------+
```
`MODULES` is where all the modules of the page will show up. Each module
has two positions stored, one through it's `module["meta"]["x"]` and
`module["meta"]["y"]` values, and one through the position of it's
widget representation `module["widget"].pos()`. The "meta" position is
relative to the page indicated by `meta["meta"]["page"]`, whereas the
widget position is absolute and takes into consideration that the SOI is in
practice painted onto one continious surface. They are defined as follows:
* `module["meta"]["x"]` is a value between `0` and `SOI.CONTENT_WIDTH`.
This value is determined by the placement strategy.
* `module["meta"]["y"]` is a value between `0` and `SOI.CONTENT_HEIGHT`.
This value is determined by the placement strategy.
* `module["widget"].pos().x()` is calculated from `module["meta"]["x"]`
as follows:
```text
module["meta"]["x"] + SOI.PADDING + SOI.HEADER_WIDTH +
SOI.INNER_PADDDING
```
* `module["widget"].pos().y()` is calculated from `module["meta"]["y"]`
as follows:
```text
module["meta"]["y"] + SOI.PADDING + SOI.INNER_PADDING +
(SOI.PADDING + SOI.HEIGHT) * (module["meta"]["page"] - 1)
```
The function `SOI.update_module_widget_position` is responsible for
updating the widget positions based on the "meta" positions, using the
formulas above.
## Note about properties
To ensure other users of SOI are properly notified, all property updates
should happen through member functions. `update_properties` for general
properties, and `add_module` for modules and attachments.
Parameters
----------
title : string
description : string
version : string
date : string in format `YYYY-MM-DD`
valid_from : string in format `YYYY-MM-DD`
valid_to : string in format `YYYY-MM-DD`
icon : string
path to icon for SOI
classification : string
orientation : string
must be one of 'landscape' and 'portrait'
placement_strategy : string
must be one of 'manual' and 'auto'
algorithm_bin : string
which bin packing algorithm should be used for rectpack. Currently the
following are implemented: 'BFF', 'BBF'. Please refer to the
STRING_TO_ALGORITHM_RECTPACK_BIN variable.
algorithm_pack : string
which packing algorithm should be used for rectpack. Currently the
following are implemented: 'MaxRectsBl', 'SkylineBl',
'GuillotineBssfSas'. Please refer to the
STRING_TO_ALGORITHM_RECTPACK_PACK variable.
algorithm_sort : string
which sorting method should be applied to the modules before packing.
Currently the following are implemented: 'none', 'area', 'width',
'height'. Please refer to the STRING_TO_ALGORITHM_INITIAL_SORT
variable.
modules : list of modules
initial modules of SOI
attachments : list of attachment modules
initial attachment modules of SOI
"""
A4_RATIO = 1.414
# Height is adjusted to something that will look right when real widgets
# are placed inside the pages
HEIGHT = 1700
WIDTH = HEIGHT * A4_RATIO
PADDING = 100
HEADER_WIDTH = 110
HEADER_HEIGHT = HEIGHT - PADDING * 2
MODULE_PADDING = 10
INNER_PADDING = 1
CONTENT_WIDTH = WIDTH - PADDING * 2 - INNER_PADDING * 2 - HEADER_WIDTH
CONTENT_HEIGHT = HEIGHT - PADDING * 2 - INNER_PADDING * 2
@classmethod
def construct_from_compressed_soi_file(cls, filename):
"""Construct an SOI object from a compressed SOI file."""
with open(filename):
pass
raise NotImplementedError()
@classmethod
def construct_from_soi_file(cls, filename):
"""Construct an SOI object from a SOI file."""
with open(filename):
pass
raise NotImplementedError()
# Ignoring pylint's r0913 "Too many arguments" error here to keep this as
# simple as possible. The proper thing would probably be to put the
# properties in separate data classes (auto-placement stuff in one class,
# styling in another, etc).
# Ignoring pylint's r0914 "Too many local variables" for the same reason as
# r0913
# pylint: disable=r0913,r0914
def __init__(
self,
title="Default SOI title",
description="Default SOI description",
version="1",
date=None,
valid_from=None,
valid_to=None,
icon="soitool/media/HVlogo.png",
classification="UGRADERT",
orientation="landscape",
placement_strategy="auto",
algorithm_bin="BFF",
algorithm_pack="MaxRectsBl",
algorithm_sort="area",
modules=None,
attachments=None,
):
# Populate date-related arguments if they are not supplied by the user
if date is None:
now = datetime.date(datetime.now())
date = now.strftime("%Y-%m-%d")
if valid_from is None:
valid_from = date
if valid_to is None:
valid_to = date
# Reason to not just set to [] as default argument:
# https://stackoverflow.com/questions/9526465/best-practice-for-setting-the-default-value-of-a-parameter-thats-supposed-to-be
if modules is None:
modules = []
if attachments is None:
attachments = []
self.title = title
self.description = description
self.version = version
self.date = date
self.valid_from = valid_from
self.valid_to = valid_to
self.icon = icon
self.classification = classification
self.orientation = orientation
self.placement_strategy = placement_strategy
self.modules = modules
self.attachments = attachments
# The following properties are relevant when self.placement_strategy
# is "auto". They are used by rectpack
self.algorithm_bin = algorithm_bin
self.algorithm_pack = algorithm_pack
self.algorithm_sort = algorithm_sort
# Prepare listener lists: list of functions to call after certain
# events happen
self.reorganization_listeners = []
self.new_module_listeners = []
self.update_property_listeners = []
self.reorganize()
def add_reorganization_listener(self, function):
"""Add to list of functions to be called after reorganization.
This is useful for users of this class to handle changes to the SOI.
As an example a class displaying an SOI can be updated after changes.
Parameters
----------
function : function
Added to the list of functions to be called after reorganization.
"""
self.reorganization_listeners.append(function)
def add_new_module_listener(self, function):
"""Add to list of functions to be called after added module.
This is useful for users of this class to handle changes to the SOI.
As an example a class displaying an SOI can be updated after changes.
Parameters
----------
function : function
Added to the list of functions to be called after added module.
"""
self.new_module_listeners.append(function)
def add_update_property_listener(self, function):
"""Add to list of functions to be called after properties change.
This is useful for users of this class to handle changes to the SOI.
As an example a class displaying an SOI can be updated after changes.
Parameters
----------
function : function
Added to the list of functions to be called after properties
change.
"""
self.update_property_listeners.append(function)
def update_module_widget_position(self, module):
"""Update position of module widget based on meta position.
For reasoning on the calculations done in this function see the class
docstring.
Parameters
----------
module : see description
should be dict of fields "meta" and "widget", where "meta" is
itself a dict with fields "x", "y" and "page", and "widget" is a
widget based on "ModuleBase"
"""
distance_to_start_of_next_soi_content_y = self.HEIGHT + self.PADDING
scene_skip_distance_page_height = (
distance_to_start_of_next_soi_content_y
* (module["meta"]["page"] - 1)
)
new_x = (
module["meta"]["x"]
+ self.PADDING
+ self.HEADER_WIDTH
+ self.INNER_PADDING
)
new_y = (
module["meta"]["y"]
+ self.PADDING
+ scene_skip_distance_page_height
+ self.INNER_PADDING
)
set_module_pos(module["widget"], QPoint(new_x, new_y))
def get_module_with_name(self, name):
"""Return module with given name.
Parameters
----------
name : str
Name of module to look for.
Returns
-------
module in self.modules, or None if no module found
"""
for module in self.modules:
if module["meta"]["name"] == name:
return module
return None
def reorganize(self):
"""Reorganize modules using chosen strategy.
After successfull reorganization will call all listeners
Raises
------
Exception
If placement strategy is neither auto nor manual.
"""
if self.placement_strategy == "auto":
self.reorganize_rectpack()
elif self.placement_strategy == "manual":
# Nothing to do
pass
else:
raise Exception(
"Unknown placement strategy: {}".format(
self.placement_strategy
)
)
self.reorganize_attachments()
# Call listener functions
for listener in self.reorganization_listeners:
listener()
def reorganize_attachments(self):
"""Reorganize attachments. Each on own page in correct order.
Order taken from order in attachment list directly. Attachments appear
at the top-left corner always.
"""
for i, attachment in enumerate(self.attachments):
first_attachment_page = (
self.get_number_of_non_attachment_pages() + 1
)
attachment["meta"]["x"] = 0
attachment["meta"]["y"] = 0
attachment["meta"]["page"] = first_attachment_page + i
self.update_module_widget_position(attachment)
def get_number_of_non_attachment_pages(self):
"""Calculate how many pages non-attachment modules require.
The minimum page count is 1.
Returns
-------
int
Number of pages required for non-attachment modules.
"""
pages = 1
for module in self.modules:
if module["meta"]["page"] > pages:
pages += 1
return pages
def get_rectpack_packer(self):
"""Return rectpack packer set up for this SOI.
Returns
-------
packer : rectpack packer
"""
# Based on string stored in self.algorithm_... variables fetch real
# implementations of chosen algorithms
chosen_algorithm_bin = STRING_TO_ALGORITHM_RECTPACK_BIN[
self.algorithm_bin
]
chosen_algorithm_pack = STRING_TO_ALGORITHM_RECTPACK_PACK[
self.algorithm_pack
]
# NOTE that initial sorting is done outside of the packer, so it is set
# to SORT_NONE here to respect our sorting
packer = newPacker(
rotation=False,
mode=PackingMode.Offline,
bin_algo=chosen_algorithm_bin,
sort_algo=SORT_NONE,
pack_algo=chosen_algorithm_pack,
)
return packer
def sort_modules(self):
"""Sort modules in place using chosen sorting algorithm."""
chosen_algorithm_sort = STRING_TO_ALGORITHM_INITIAL_SORT[
self.algorithm_sort
]
self.modules = chosen_algorithm_sort(self.modules)
def reorganize_rectpack(self):
"""Reorganize modules optimally using the rectpack library.
Note that to add padding between the modules we add self.MODULE_PADDING
to each width,height of rectangles sent to rectpack for packing. We
also tell rectpack that our "bins" are self.MODULE_PADDING larger in
each dimension. As a result of this we get self.MODULE_PADDING amount
of padding between the packed modules.
Raises
------
ModuleLargerThanBinError
If at least one module is too large to fit into a bin.
A bin is a page in this context
"""
packer = self.get_rectpack_packer()
self.sort_modules()
for module in self.modules:
module_width, module_height = module["widget"].get_size()
padded_module_width = module_width + self.MODULE_PADDING
padded_module_height = module_height + self.MODULE_PADDING
packer.add_rect(
padded_module_width,
padded_module_height,
module["meta"]["name"],
)
# float("inf") to add infinite bins.
# See https://github.com/secnot/rectpack/blob/master/README.md#api
packer.add_bin(
self.CONTENT_WIDTH + self.MODULE_PADDING,
self.CONTENT_HEIGHT + self.MODULE_PADDING,
float("inf"),
)
packer.pack()
packed_rects = packer.rect_list()
# Explode if rectpack failed to pack all rects
if len(packed_rects) != len(self.modules):
raise ModuleLargerThanBinError()
# Update modules based on packed rects returned from rectpack
for packed_rect in packed_rects:
packed_rect_bin = packed_rect[0]
packed_rect_x = packed_rect[1]
packed_rect_y = packed_rect[2]
packed_rect_id = packed_rect[5]
corresponding_module = self.get_module_with_name(packed_rect_id)
if corresponding_module is None:
raise Exception("Module was lost during packing!")
corresponding_module["meta"]["x"] = packed_rect_x
corresponding_module["meta"]["y"] = packed_rect_y
corresponding_module["meta"]["page"] = packed_rect_bin + 1
self.update_module_widget_position(corresponding_module)
def module_name_taken(self, name):
"""Return True if module with name exists, False otherwise."""
for module in self.modules + self.attachments:
if name == module["meta"]["name"]:
return True
return False
def add_module(self, name, widget_implementation, is_attachment=False):
"""Add module to SOI, reorganize it, and notify listeners.
This function raises an exception if the given name is taken
Parameters
----------
name : str
Name of new module.
widget_implementation : subclass of ModuleBase
An instance of the widget implementation of the module.
is_attachment : bool
True if the module should be added as an attachment. False
otherwise.
Raises
------
ModuleNameTaken
If the module name is taken.
"""
if self.module_name_taken(name):
raise ModuleNameTaken
# NOTE that "x", "y" and "page" under "meta" are not valid until
# self.reorganize() has run
to_add = {
"meta": {"x": 0, "y": 0, "page": 0, "name": name},
"widget": widget_implementation,
}
if is_attachment:
self.attachments.append(to_add)
else:
self.modules.append(to_add)
self.reorganize()
# Call listener functions
for listener in self.new_module_listeners:
listener()
# Ignoring pylint "Too many branches" error as this function is a special
# case where the if-elif-elif-...-else makes sense
def update_properties(self, **kwargs): # pylint: disable=R0912
"""Update properties given as input, and call listeners.
This function exists solely because there are listeners on the
properties. A "cleaner" way to achieve the same goal would be to
create a "setter" for each property and call the listeners there, but
for bulk changes this would get unwieldy. Another way to achieve the
goal of calling listeners after changes to properties would be to
create a separate function that the user is expected to call after
changing the properties directly, but this would put unnecessary
responsibility on the user.
All properties except "modules" and "attachements" can be updated
using this function.
If a change is made that affects placement of modules this function
will call `reorganize`
Parameters
----------
**kwargs : key, value pairs of properties to update
Accepts both a normal dict, and passing arguments as normal:
`update_properties({'title': 'my title'})` and
update_properties(title='my title')`. Accepted keys are properties
of the SOI class, except 'modules' and 'attachements'. Explanation
of **kwargs:
https://stackoverflow.com/a/1769475/3545896
Raises
------
ValueError
If argument not referring to SOI property
modifiable from this function is passed.
"""
# For every key, value pair passed in kwargs, update corresponding
# property
for key, value in kwargs.items():
if key == "title":
self.title = value
elif key == "description":
self.description = value
elif key == "version":
self.version = value
elif key == "date":
self.date = value
elif key == "valid_from":
self.valid_from = value
elif key == "valid_to":
self.valid_to = value
elif key == "icon":
self.icon = value
elif key == "classification":
self.classification = value
elif key == "orientation":
self.orientation = value
elif key == "placement_strategy":
self.placement_strategy = value
elif key == "algorithm_bin":
self.algorithm_bin = value
elif key == "algorithm_pack":
self.algorithm_pack = value
elif key == "algorithm_sort":
self.algorithm_sort = value
else:
raise ValueError(
f"Unknown property name {key} passed with value {value}"
)
for listener in self.update_property_listeners:
listener()
# If any properties relating to module placement were touched,
# reorganize
if (
"placement_strategy" in kwargs
or "algorithm_bin" in kwargs
or "algorithm_pack" in kwargs
or "algorithm_sort" in kwargs
):
self.reorganize()
Functions
def modules_sort_by_area(modules)
-
Sort modules by area. See SOI.sort_modules.
Parameters
modules
:list
- List of modules to sort.
Returns
list
- List of modules sorted by area.
Expand source code
def modules_sort_by_area(modules): """Sort modules by area. See SOI.sort_modules. Parameters ---------- modules : list List of modules to sort. Returns ------- list List of modules sorted by area. """ def module_area_key(module): width, height = module["widget"].get_size() return width * height return sorted(modules, key=module_area_key, reverse=True)
def modules_sort_by_height(modules)
-
Sort modules by height. See SOI.sort_modules.
Parameters
modules
:list
- List of modules to sort.
Returns
list
- List of modules sorted by height.
Expand source code
def modules_sort_by_height(modules): """Sort modules by height. See SOI.sort_modules. Parameters ---------- modules : list List of modules to sort. Returns ------- list List of modules sorted by height. """ def module_height_key(module): _, height = module["widget"].get_size() return height return sorted(modules, key=module_height_key, reverse=True)
def modules_sort_by_none(modules)
-
Don't sort. Implemented for completeness. See SOI.sort_modules.
Parameters
modules
:list
- List of modules.
Returns
list
- List of modules, untouched.
Expand source code
def modules_sort_by_none(modules): """Don't sort. Implemented for completeness. See SOI.sort_modules. Parameters ---------- modules : list List of modules. Returns ------- list List of modules, untouched. """ return modules
def modules_sort_by_width(modules)
-
Sort modules by width. See SOI.sort_modules.
Parameters
modules
:list
- List of modules to sort.
Returns
list
- List of modules sorted by width.
Expand source code
def modules_sort_by_width(modules): """Sort modules by width. See SOI.sort_modules. Parameters ---------- modules : list List of modules to sort. Returns ------- list List of modules sorted by width. """ def module_width_key(module): width, _ = module["widget"].get_size() return width return sorted(modules, key=module_width_key, reverse=True)
Classes
class ModuleLargerThanBinError (...)
-
At least one module is too large for the bin during rectpack packing.
Expand source code
class ModuleLargerThanBinError(Exception): """At least one module is too large for the bin during rectpack packing."""
Ancestors
- builtins.Exception
- builtins.BaseException
class ModuleNameTaken (...)
-
Module name is already taken by an existing module.
Expand source code
class ModuleNameTaken(Exception): """Module name is already taken by an existing module."""
Ancestors
- builtins.Exception
- builtins.BaseException
class SOI (title='Default SOI title', description='Default SOI description', version='1', date=None, valid_from=None, valid_to=None, icon='soitool/media/HVlogo.png', classification='UGRADERT', orientation='landscape', placement_strategy='auto', algorithm_bin='BFF', algorithm_pack='MaxRectsBl', algorithm_sort='area', modules=None, attachments=None)
-
Datastructure for SOI.
Holds all info about an SOI necessary to view and edit it.
Note about rectpack
This class relies heavily on the rectpack library for optimal placement of modules when placement_strategy is 'auto'. Refer to the rectpack documentation for details beyond what is provided in this class: https://github.com/secnot/rectpack
Anatomy of an SOI
Below is an illustration of how an SOI with two pages is represented using the class variables.
SOI.WIDTH | v _________________ SOI.PADDING | SOI.HEADER_WIDTH | | SOI.INNER_PADDING | | | SOI.CONTENT_WIDTH | | | | SOI.INNER_PADDING | | | | | SOI.PADDING | | | | | | v v v v v v _ ___ _ _________ _ _ +---------------------+ | | | <- SOI.PADDING | <- SOI.HEIGHT | +---+-------------+ | | | | H | | | | <- SOI.INNER_PADDING | | | E | +---------+ | | | | | A | | MODULES | | | | <- SOI.CONTENT_HEIGHT | | | D | | | | | | | | | E | | | | | | | | | R | | | | | | | | | | +---------+ | | | | | | | | | <- SOI.INNER_PADDDING | | +---+-------------+ | | | | | <- SOI.PADDING | +---------------------+ +---------------------+ | | | <- SOI.PADDING | <- SOI.HEIGHT | +---+-------------+ | | | | H | | | | <- SOI.INNER_PADDING | | | E | +---------+ | | | | | A | | MODULES | | | | <- SOI.CONTENT_HEIGHT | | | D | | | | | | | | | E | | | | | | | | | R | | | | | | | | | | +---------+ | | | | | | | | | <- SOI.INNER_PADDDING | | +---+-------------+ | | | | | <- SOI.PADDING | +---------------------+
MODULES
is where all the modules of the page will show up. Each module has two positions stored, one through it'smodule["meta"]["x"]
andmodule["meta"]["y"]
values, and one through the position of it's widget representationmodule["widget"].pos()
. The "meta" position is relative to the page indicated bymeta["meta"]["page"]
, whereas the widget position is absolute and takes into consideration that the SOI is in practice painted onto one continious surface. They are defined as follows:module["meta"]["x"]
is a value between0
andSOI.CONTENT_WIDTH
. This value is determined by the placement strategy.module["meta"]["y"]
is a value between0
andSOI.CONTENT_HEIGHT
. This value is determined by the placement strategy.-
module["widget"].pos().x()
is calculated frommodule["meta"]["x"]
as follows:text module["meta"]["x"] + SOI.PADDING + SOI.HEADER_WIDTH + SOI.INNER_PADDDING
-
module["widget"].pos().y()
is calculated frommodule["meta"]["y"]
as follows:text module["meta"]["y"] + SOI.PADDING + SOI.INNER_PADDING + (SOI.PADDING + SOI.HEIGHT) * (module["meta"]["page"] - 1)
The function
SOI.update_module_widget_position()
is responsible for updating the widget positions based on the "meta" positions, using the formulas above.Note about properties
To ensure other users of SOI are properly notified, all property updates should happen through member functions.
update_properties
for general properties, andadd_module
for modules and attachments.Parameters
title
:string
description
:string
version
:string
date
:string
in
format
YYYY`-`MM`-`DD
valid_from
:string
in
format
YYYY`-`MM`-`DD
valid_to
:string
in
format
YYYY`-`MM`-`DD
icon
:string
- path to icon for SOI
classification
:string
orientation
:string
- must be one of 'landscape' and 'portrait'
placement_strategy
:string
- must be one of 'manual' and 'auto'
algorithm_bin
:string
- which bin packing algorithm should be used for rectpack. Currently the following are implemented: 'BFF', 'BBF'. Please refer to the STRING_TO_ALGORITHM_RECTPACK_BIN variable.
algorithm_pack
:string
- which packing algorithm should be used for rectpack. Currently the following are implemented: 'MaxRectsBl', 'SkylineBl', 'GuillotineBssfSas'. Please refer to the STRING_TO_ALGORITHM_RECTPACK_PACK variable.
algorithm_sort
:string
- which sorting method should be applied to the modules before packing. Currently the following are implemented: 'none', 'area', 'width', 'height'. Please refer to the STRING_TO_ALGORITHM_INITIAL_SORT variable.
modules
:list
ofmodules
- initial modules of SOI
attachments
:list
ofattachment
modules
- initial attachment modules of SOI
Expand source code
class SOI: """Datastructure for SOI. Holds all info about an SOI necessary to view and edit it. ## Note about rectpack This class relies heavily on the rectpack library for optimal placement of modules when placement_strategy is 'auto'. Refer to the rectpack documentation for details beyond what is provided in this class: https://github.com/secnot/rectpack ## Anatomy of an SOI Below is an illustration of how an SOI with two pages is represented using the class variables. ```text SOI.WIDTH | v _________________ SOI.PADDING | SOI.HEADER_WIDTH | | SOI.INNER_PADDING | | | SOI.CONTENT_WIDTH | | | | SOI.INNER_PADDING | | | | | SOI.PADDING | | | | | | v v v v v v _ ___ _ _________ _ _ +---------------------+ | | | <- SOI.PADDING | <- SOI.HEIGHT | +---+-------------+ | | | | H | | | | <- SOI.INNER_PADDING | | | E | +---------+ | | | | | A | | MODULES | | | | <- SOI.CONTENT_HEIGHT | | | D | | | | | | | | | E | | | | | | | | | R | | | | | | | | | | +---------+ | | | | | | | | | <- SOI.INNER_PADDDING | | +---+-------------+ | | | | | <- SOI.PADDING | +---------------------+ +---------------------+ | | | <- SOI.PADDING | <- SOI.HEIGHT | +---+-------------+ | | | | H | | | | <- SOI.INNER_PADDING | | | E | +---------+ | | | | | A | | MODULES | | | | <- SOI.CONTENT_HEIGHT | | | D | | | | | | | | | E | | | | | | | | | R | | | | | | | | | | +---------+ | | | | | | | | | <- SOI.INNER_PADDDING | | +---+-------------+ | | | | | <- SOI.PADDING | +---------------------+ ``` `MODULES` is where all the modules of the page will show up. Each module has two positions stored, one through it's `module["meta"]["x"]` and `module["meta"]["y"]` values, and one through the position of it's widget representation `module["widget"].pos()`. The "meta" position is relative to the page indicated by `meta["meta"]["page"]`, whereas the widget position is absolute and takes into consideration that the SOI is in practice painted onto one continious surface. They are defined as follows: * `module["meta"]["x"]` is a value between `0` and `SOI.CONTENT_WIDTH`. This value is determined by the placement strategy. * `module["meta"]["y"]` is a value between `0` and `SOI.CONTENT_HEIGHT`. This value is determined by the placement strategy. * `module["widget"].pos().x()` is calculated from `module["meta"]["x"]` as follows: ```text module["meta"]["x"] + SOI.PADDING + SOI.HEADER_WIDTH + SOI.INNER_PADDDING ``` * `module["widget"].pos().y()` is calculated from `module["meta"]["y"]` as follows: ```text module["meta"]["y"] + SOI.PADDING + SOI.INNER_PADDING + (SOI.PADDING + SOI.HEIGHT) * (module["meta"]["page"] - 1) ``` The function `SOI.update_module_widget_position` is responsible for updating the widget positions based on the "meta" positions, using the formulas above. ## Note about properties To ensure other users of SOI are properly notified, all property updates should happen through member functions. `update_properties` for general properties, and `add_module` for modules and attachments. Parameters ---------- title : string description : string version : string date : string in format `YYYY-MM-DD` valid_from : string in format `YYYY-MM-DD` valid_to : string in format `YYYY-MM-DD` icon : string path to icon for SOI classification : string orientation : string must be one of 'landscape' and 'portrait' placement_strategy : string must be one of 'manual' and 'auto' algorithm_bin : string which bin packing algorithm should be used for rectpack. Currently the following are implemented: 'BFF', 'BBF'. Please refer to the STRING_TO_ALGORITHM_RECTPACK_BIN variable. algorithm_pack : string which packing algorithm should be used for rectpack. Currently the following are implemented: 'MaxRectsBl', 'SkylineBl', 'GuillotineBssfSas'. Please refer to the STRING_TO_ALGORITHM_RECTPACK_PACK variable. algorithm_sort : string which sorting method should be applied to the modules before packing. Currently the following are implemented: 'none', 'area', 'width', 'height'. Please refer to the STRING_TO_ALGORITHM_INITIAL_SORT variable. modules : list of modules initial modules of SOI attachments : list of attachment modules initial attachment modules of SOI """ A4_RATIO = 1.414 # Height is adjusted to something that will look right when real widgets # are placed inside the pages HEIGHT = 1700 WIDTH = HEIGHT * A4_RATIO PADDING = 100 HEADER_WIDTH = 110 HEADER_HEIGHT = HEIGHT - PADDING * 2 MODULE_PADDING = 10 INNER_PADDING = 1 CONTENT_WIDTH = WIDTH - PADDING * 2 - INNER_PADDING * 2 - HEADER_WIDTH CONTENT_HEIGHT = HEIGHT - PADDING * 2 - INNER_PADDING * 2 @classmethod def construct_from_compressed_soi_file(cls, filename): """Construct an SOI object from a compressed SOI file.""" with open(filename): pass raise NotImplementedError() @classmethod def construct_from_soi_file(cls, filename): """Construct an SOI object from a SOI file.""" with open(filename): pass raise NotImplementedError() # Ignoring pylint's r0913 "Too many arguments" error here to keep this as # simple as possible. The proper thing would probably be to put the # properties in separate data classes (auto-placement stuff in one class, # styling in another, etc). # Ignoring pylint's r0914 "Too many local variables" for the same reason as # r0913 # pylint: disable=r0913,r0914 def __init__( self, title="Default SOI title", description="Default SOI description", version="1", date=None, valid_from=None, valid_to=None, icon="soitool/media/HVlogo.png", classification="UGRADERT", orientation="landscape", placement_strategy="auto", algorithm_bin="BFF", algorithm_pack="MaxRectsBl", algorithm_sort="area", modules=None, attachments=None, ): # Populate date-related arguments if they are not supplied by the user if date is None: now = datetime.date(datetime.now()) date = now.strftime("%Y-%m-%d") if valid_from is None: valid_from = date if valid_to is None: valid_to = date # Reason to not just set to [] as default argument: # https://stackoverflow.com/questions/9526465/best-practice-for-setting-the-default-value-of-a-parameter-thats-supposed-to-be if modules is None: modules = [] if attachments is None: attachments = [] self.title = title self.description = description self.version = version self.date = date self.valid_from = valid_from self.valid_to = valid_to self.icon = icon self.classification = classification self.orientation = orientation self.placement_strategy = placement_strategy self.modules = modules self.attachments = attachments # The following properties are relevant when self.placement_strategy # is "auto". They are used by rectpack self.algorithm_bin = algorithm_bin self.algorithm_pack = algorithm_pack self.algorithm_sort = algorithm_sort # Prepare listener lists: list of functions to call after certain # events happen self.reorganization_listeners = [] self.new_module_listeners = [] self.update_property_listeners = [] self.reorganize() def add_reorganization_listener(self, function): """Add to list of functions to be called after reorganization. This is useful for users of this class to handle changes to the SOI. As an example a class displaying an SOI can be updated after changes. Parameters ---------- function : function Added to the list of functions to be called after reorganization. """ self.reorganization_listeners.append(function) def add_new_module_listener(self, function): """Add to list of functions to be called after added module. This is useful for users of this class to handle changes to the SOI. As an example a class displaying an SOI can be updated after changes. Parameters ---------- function : function Added to the list of functions to be called after added module. """ self.new_module_listeners.append(function) def add_update_property_listener(self, function): """Add to list of functions to be called after properties change. This is useful for users of this class to handle changes to the SOI. As an example a class displaying an SOI can be updated after changes. Parameters ---------- function : function Added to the list of functions to be called after properties change. """ self.update_property_listeners.append(function) def update_module_widget_position(self, module): """Update position of module widget based on meta position. For reasoning on the calculations done in this function see the class docstring. Parameters ---------- module : see description should be dict of fields "meta" and "widget", where "meta" is itself a dict with fields "x", "y" and "page", and "widget" is a widget based on "ModuleBase" """ distance_to_start_of_next_soi_content_y = self.HEIGHT + self.PADDING scene_skip_distance_page_height = ( distance_to_start_of_next_soi_content_y * (module["meta"]["page"] - 1) ) new_x = ( module["meta"]["x"] + self.PADDING + self.HEADER_WIDTH + self.INNER_PADDING ) new_y = ( module["meta"]["y"] + self.PADDING + scene_skip_distance_page_height + self.INNER_PADDING ) set_module_pos(module["widget"], QPoint(new_x, new_y)) def get_module_with_name(self, name): """Return module with given name. Parameters ---------- name : str Name of module to look for. Returns ------- module in self.modules, or None if no module found """ for module in self.modules: if module["meta"]["name"] == name: return module return None def reorganize(self): """Reorganize modules using chosen strategy. After successfull reorganization will call all listeners Raises ------ Exception If placement strategy is neither auto nor manual. """ if self.placement_strategy == "auto": self.reorganize_rectpack() elif self.placement_strategy == "manual": # Nothing to do pass else: raise Exception( "Unknown placement strategy: {}".format( self.placement_strategy ) ) self.reorganize_attachments() # Call listener functions for listener in self.reorganization_listeners: listener() def reorganize_attachments(self): """Reorganize attachments. Each on own page in correct order. Order taken from order in attachment list directly. Attachments appear at the top-left corner always. """ for i, attachment in enumerate(self.attachments): first_attachment_page = ( self.get_number_of_non_attachment_pages() + 1 ) attachment["meta"]["x"] = 0 attachment["meta"]["y"] = 0 attachment["meta"]["page"] = first_attachment_page + i self.update_module_widget_position(attachment) def get_number_of_non_attachment_pages(self): """Calculate how many pages non-attachment modules require. The minimum page count is 1. Returns ------- int Number of pages required for non-attachment modules. """ pages = 1 for module in self.modules: if module["meta"]["page"] > pages: pages += 1 return pages def get_rectpack_packer(self): """Return rectpack packer set up for this SOI. Returns ------- packer : rectpack packer """ # Based on string stored in self.algorithm_... variables fetch real # implementations of chosen algorithms chosen_algorithm_bin = STRING_TO_ALGORITHM_RECTPACK_BIN[ self.algorithm_bin ] chosen_algorithm_pack = STRING_TO_ALGORITHM_RECTPACK_PACK[ self.algorithm_pack ] # NOTE that initial sorting is done outside of the packer, so it is set # to SORT_NONE here to respect our sorting packer = newPacker( rotation=False, mode=PackingMode.Offline, bin_algo=chosen_algorithm_bin, sort_algo=SORT_NONE, pack_algo=chosen_algorithm_pack, ) return packer def sort_modules(self): """Sort modules in place using chosen sorting algorithm.""" chosen_algorithm_sort = STRING_TO_ALGORITHM_INITIAL_SORT[ self.algorithm_sort ] self.modules = chosen_algorithm_sort(self.modules) def reorganize_rectpack(self): """Reorganize modules optimally using the rectpack library. Note that to add padding between the modules we add self.MODULE_PADDING to each width,height of rectangles sent to rectpack for packing. We also tell rectpack that our "bins" are self.MODULE_PADDING larger in each dimension. As a result of this we get self.MODULE_PADDING amount of padding between the packed modules. Raises ------ ModuleLargerThanBinError If at least one module is too large to fit into a bin. A bin is a page in this context """ packer = self.get_rectpack_packer() self.sort_modules() for module in self.modules: module_width, module_height = module["widget"].get_size() padded_module_width = module_width + self.MODULE_PADDING padded_module_height = module_height + self.MODULE_PADDING packer.add_rect( padded_module_width, padded_module_height, module["meta"]["name"], ) # float("inf") to add infinite bins. # See https://github.com/secnot/rectpack/blob/master/README.md#api packer.add_bin( self.CONTENT_WIDTH + self.MODULE_PADDING, self.CONTENT_HEIGHT + self.MODULE_PADDING, float("inf"), ) packer.pack() packed_rects = packer.rect_list() # Explode if rectpack failed to pack all rects if len(packed_rects) != len(self.modules): raise ModuleLargerThanBinError() # Update modules based on packed rects returned from rectpack for packed_rect in packed_rects: packed_rect_bin = packed_rect[0] packed_rect_x = packed_rect[1] packed_rect_y = packed_rect[2] packed_rect_id = packed_rect[5] corresponding_module = self.get_module_with_name(packed_rect_id) if corresponding_module is None: raise Exception("Module was lost during packing!") corresponding_module["meta"]["x"] = packed_rect_x corresponding_module["meta"]["y"] = packed_rect_y corresponding_module["meta"]["page"] = packed_rect_bin + 1 self.update_module_widget_position(corresponding_module) def module_name_taken(self, name): """Return True if module with name exists, False otherwise.""" for module in self.modules + self.attachments: if name == module["meta"]["name"]: return True return False def add_module(self, name, widget_implementation, is_attachment=False): """Add module to SOI, reorganize it, and notify listeners. This function raises an exception if the given name is taken Parameters ---------- name : str Name of new module. widget_implementation : subclass of ModuleBase An instance of the widget implementation of the module. is_attachment : bool True if the module should be added as an attachment. False otherwise. Raises ------ ModuleNameTaken If the module name is taken. """ if self.module_name_taken(name): raise ModuleNameTaken # NOTE that "x", "y" and "page" under "meta" are not valid until # self.reorganize() has run to_add = { "meta": {"x": 0, "y": 0, "page": 0, "name": name}, "widget": widget_implementation, } if is_attachment: self.attachments.append(to_add) else: self.modules.append(to_add) self.reorganize() # Call listener functions for listener in self.new_module_listeners: listener() # Ignoring pylint "Too many branches" error as this function is a special # case where the if-elif-elif-...-else makes sense def update_properties(self, **kwargs): # pylint: disable=R0912 """Update properties given as input, and call listeners. This function exists solely because there are listeners on the properties. A "cleaner" way to achieve the same goal would be to create a "setter" for each property and call the listeners there, but for bulk changes this would get unwieldy. Another way to achieve the goal of calling listeners after changes to properties would be to create a separate function that the user is expected to call after changing the properties directly, but this would put unnecessary responsibility on the user. All properties except "modules" and "attachements" can be updated using this function. If a change is made that affects placement of modules this function will call `reorganize` Parameters ---------- **kwargs : key, value pairs of properties to update Accepts both a normal dict, and passing arguments as normal: `update_properties({'title': 'my title'})` and update_properties(title='my title')`. Accepted keys are properties of the SOI class, except 'modules' and 'attachements'. Explanation of **kwargs: https://stackoverflow.com/a/1769475/3545896 Raises ------ ValueError If argument not referring to SOI property modifiable from this function is passed. """ # For every key, value pair passed in kwargs, update corresponding # property for key, value in kwargs.items(): if key == "title": self.title = value elif key == "description": self.description = value elif key == "version": self.version = value elif key == "date": self.date = value elif key == "valid_from": self.valid_from = value elif key == "valid_to": self.valid_to = value elif key == "icon": self.icon = value elif key == "classification": self.classification = value elif key == "orientation": self.orientation = value elif key == "placement_strategy": self.placement_strategy = value elif key == "algorithm_bin": self.algorithm_bin = value elif key == "algorithm_pack": self.algorithm_pack = value elif key == "algorithm_sort": self.algorithm_sort = value else: raise ValueError( f"Unknown property name {key} passed with value {value}" ) for listener in self.update_property_listeners: listener() # If any properties relating to module placement were touched, # reorganize if ( "placement_strategy" in kwargs or "algorithm_bin" in kwargs or "algorithm_pack" in kwargs or "algorithm_sort" in kwargs ): self.reorganize()
Class variables
var A4_RATIO
-
Convert a string or number to a floating point number, if possible.
var CONTENT_HEIGHT
-
int([x]) -> integer int(x, base=10) -> integer
Convert a number or string to an integer, or return 0 if no arguments are given. If x is a number, return x.int(). For floating point numbers, this truncates towards zero.
If x is not a number or if base is given, then x must be a string, bytes, or bytearray instance representing an integer literal in the given base. The literal can be preceded by '+' or '-' and be surrounded by whitespace. The base defaults to 10. Valid bases are 0 and 2-36. Base 0 means to interpret the base from the string as an integer literal.
>>> int('0b100', base=0) 4
var CONTENT_WIDTH
-
Convert a string or number to a floating point number, if possible.
var HEADER_HEIGHT
-
int([x]) -> integer int(x, base=10) -> integer
Convert a number or string to an integer, or return 0 if no arguments are given. If x is a number, return x.int(). For floating point numbers, this truncates towards zero.
If x is not a number or if base is given, then x must be a string, bytes, or bytearray instance representing an integer literal in the given base. The literal can be preceded by '+' or '-' and be surrounded by whitespace. The base defaults to 10. Valid bases are 0 and 2-36. Base 0 means to interpret the base from the string as an integer literal.
>>> int('0b100', base=0) 4
var HEADER_WIDTH
-
int([x]) -> integer int(x, base=10) -> integer
Convert a number or string to an integer, or return 0 if no arguments are given. If x is a number, return x.int(). For floating point numbers, this truncates towards zero.
If x is not a number or if base is given, then x must be a string, bytes, or bytearray instance representing an integer literal in the given base. The literal can be preceded by '+' or '-' and be surrounded by whitespace. The base defaults to 10. Valid bases are 0 and 2-36. Base 0 means to interpret the base from the string as an integer literal.
>>> int('0b100', base=0) 4
var HEIGHT
-
int([x]) -> integer int(x, base=10) -> integer
Convert a number or string to an integer, or return 0 if no arguments are given. If x is a number, return x.int(). For floating point numbers, this truncates towards zero.
If x is not a number or if base is given, then x must be a string, bytes, or bytearray instance representing an integer literal in the given base. The literal can be preceded by '+' or '-' and be surrounded by whitespace. The base defaults to 10. Valid bases are 0 and 2-36. Base 0 means to interpret the base from the string as an integer literal.
>>> int('0b100', base=0) 4
var INNER_PADDING
-
int([x]) -> integer int(x, base=10) -> integer
Convert a number or string to an integer, or return 0 if no arguments are given. If x is a number, return x.int(). For floating point numbers, this truncates towards zero.
If x is not a number or if base is given, then x must be a string, bytes, or bytearray instance representing an integer literal in the given base. The literal can be preceded by '+' or '-' and be surrounded by whitespace. The base defaults to 10. Valid bases are 0 and 2-36. Base 0 means to interpret the base from the string as an integer literal.
>>> int('0b100', base=0) 4
var MODULE_PADDING
-
int([x]) -> integer int(x, base=10) -> integer
Convert a number or string to an integer, or return 0 if no arguments are given. If x is a number, return x.int(). For floating point numbers, this truncates towards zero.
If x is not a number or if base is given, then x must be a string, bytes, or bytearray instance representing an integer literal in the given base. The literal can be preceded by '+' or '-' and be surrounded by whitespace. The base defaults to 10. Valid bases are 0 and 2-36. Base 0 means to interpret the base from the string as an integer literal.
>>> int('0b100', base=0) 4
var PADDING
-
int([x]) -> integer int(x, base=10) -> integer
Convert a number or string to an integer, or return 0 if no arguments are given. If x is a number, return x.int(). For floating point numbers, this truncates towards zero.
If x is not a number or if base is given, then x must be a string, bytes, or bytearray instance representing an integer literal in the given base. The literal can be preceded by '+' or '-' and be surrounded by whitespace. The base defaults to 10. Valid bases are 0 and 2-36. Base 0 means to interpret the base from the string as an integer literal.
>>> int('0b100', base=0) 4
var WIDTH
-
Convert a string or number to a floating point number, if possible.
Static methods
def construct_from_compressed_soi_file(filename)
-
Construct an SOI object from a compressed SOI file.
Expand source code
@classmethod def construct_from_compressed_soi_file(cls, filename): """Construct an SOI object from a compressed SOI file.""" with open(filename): pass raise NotImplementedError()
def construct_from_soi_file(filename)
-
Construct an SOI object from a SOI file.
Expand source code
@classmethod def construct_from_soi_file(cls, filename): """Construct an SOI object from a SOI file.""" with open(filename): pass raise NotImplementedError()
Methods
def add_module(self, name, widget_implementation, is_attachment=False)
-
Add module to SOI, reorganize it, and notify listeners.
This function raises an exception if the given name is taken
Parameters
name
:str
- Name of new module.
widget_implementation
:subclass
ofModuleBase
- An instance of the widget implementation of the module.
is_attachment
:bool
- True if the module should be added as an attachment. False otherwise.
Raises
ModuleNameTaken
- If the module name is taken.
Expand source code
def add_module(self, name, widget_implementation, is_attachment=False): """Add module to SOI, reorganize it, and notify listeners. This function raises an exception if the given name is taken Parameters ---------- name : str Name of new module. widget_implementation : subclass of ModuleBase An instance of the widget implementation of the module. is_attachment : bool True if the module should be added as an attachment. False otherwise. Raises ------ ModuleNameTaken If the module name is taken. """ if self.module_name_taken(name): raise ModuleNameTaken # NOTE that "x", "y" and "page" under "meta" are not valid until # self.reorganize() has run to_add = { "meta": {"x": 0, "y": 0, "page": 0, "name": name}, "widget": widget_implementation, } if is_attachment: self.attachments.append(to_add) else: self.modules.append(to_add) self.reorganize() # Call listener functions for listener in self.new_module_listeners: listener()
def add_new_module_listener(self, function)
-
Add to list of functions to be called after added module.
This is useful for users of this class to handle changes to the SOI. As an example a class displaying an SOI can be updated after changes.
Parameters
function
:function
- Added to the list of functions to be called after added module.
Expand source code
def add_new_module_listener(self, function): """Add to list of functions to be called after added module. This is useful for users of this class to handle changes to the SOI. As an example a class displaying an SOI can be updated after changes. Parameters ---------- function : function Added to the list of functions to be called after added module. """ self.new_module_listeners.append(function)
def add_reorganization_listener(self, function)
-
Add to list of functions to be called after reorganization.
This is useful for users of this class to handle changes to the SOI. As an example a class displaying an SOI can be updated after changes.
Parameters
function
:function
- Added to the list of functions to be called after reorganization.
Expand source code
def add_reorganization_listener(self, function): """Add to list of functions to be called after reorganization. This is useful for users of this class to handle changes to the SOI. As an example a class displaying an SOI can be updated after changes. Parameters ---------- function : function Added to the list of functions to be called after reorganization. """ self.reorganization_listeners.append(function)
def add_update_property_listener(self, function)
-
Add to list of functions to be called after properties change.
This is useful for users of this class to handle changes to the SOI. As an example a class displaying an SOI can be updated after changes.
Parameters
function
:function
- Added to the list of functions to be called after properties change.
Expand source code
def add_update_property_listener(self, function): """Add to list of functions to be called after properties change. This is useful for users of this class to handle changes to the SOI. As an example a class displaying an SOI can be updated after changes. Parameters ---------- function : function Added to the list of functions to be called after properties change. """ self.update_property_listeners.append(function)
def get_module_with_name(self, name)
-
Return module with given name.
Parameters
name
:str
- Name of module to look for.
Returns
module
in
self.modules
, orNone
if
no
module
found
Expand source code
def get_module_with_name(self, name): """Return module with given name. Parameters ---------- name : str Name of module to look for. Returns ------- module in self.modules, or None if no module found """ for module in self.modules: if module["meta"]["name"] == name: return module return None
def get_number_of_non_attachment_pages(self)
-
Calculate how many pages non-attachment modules require.
The minimum page count is 1.
Returns
int
- Number of pages required for non-attachment modules.
Expand source code
def get_number_of_non_attachment_pages(self): """Calculate how many pages non-attachment modules require. The minimum page count is 1. Returns ------- int Number of pages required for non-attachment modules. """ pages = 1 for module in self.modules: if module["meta"]["page"] > pages: pages += 1 return pages
def get_rectpack_packer(self)
-
Return rectpack packer set up for this SOI.
Returns
packer
:rectpack
packer
Expand source code
def get_rectpack_packer(self): """Return rectpack packer set up for this SOI. Returns ------- packer : rectpack packer """ # Based on string stored in self.algorithm_... variables fetch real # implementations of chosen algorithms chosen_algorithm_bin = STRING_TO_ALGORITHM_RECTPACK_BIN[ self.algorithm_bin ] chosen_algorithm_pack = STRING_TO_ALGORITHM_RECTPACK_PACK[ self.algorithm_pack ] # NOTE that initial sorting is done outside of the packer, so it is set # to SORT_NONE here to respect our sorting packer = newPacker( rotation=False, mode=PackingMode.Offline, bin_algo=chosen_algorithm_bin, sort_algo=SORT_NONE, pack_algo=chosen_algorithm_pack, ) return packer
def module_name_taken(self, name)
-
Return True if module with name exists, False otherwise.
Expand source code
def module_name_taken(self, name): """Return True if module with name exists, False otherwise.""" for module in self.modules + self.attachments: if name == module["meta"]["name"]: return True return False
def reorganize(self)
-
Reorganize modules using chosen strategy.
After successfull reorganization will call all listeners
Raises
Exception
- If placement strategy is neither auto nor manual.
Expand source code
def reorganize(self): """Reorganize modules using chosen strategy. After successfull reorganization will call all listeners Raises ------ Exception If placement strategy is neither auto nor manual. """ if self.placement_strategy == "auto": self.reorganize_rectpack() elif self.placement_strategy == "manual": # Nothing to do pass else: raise Exception( "Unknown placement strategy: {}".format( self.placement_strategy ) ) self.reorganize_attachments() # Call listener functions for listener in self.reorganization_listeners: listener()
def reorganize_attachments(self)
-
Reorganize attachments. Each on own page in correct order.
Order taken from order in attachment list directly. Attachments appear at the top-left corner always.
Expand source code
def reorganize_attachments(self): """Reorganize attachments. Each on own page in correct order. Order taken from order in attachment list directly. Attachments appear at the top-left corner always. """ for i, attachment in enumerate(self.attachments): first_attachment_page = ( self.get_number_of_non_attachment_pages() + 1 ) attachment["meta"]["x"] = 0 attachment["meta"]["y"] = 0 attachment["meta"]["page"] = first_attachment_page + i self.update_module_widget_position(attachment)
def reorganize_rectpack(self)
-
Reorganize modules optimally using the rectpack library.
Note that to add padding between the modules we add self.MODULE_PADDING to each width,height of rectangles sent to rectpack for packing. We also tell rectpack that our "bins" are self.MODULE_PADDING larger in each dimension. As a result of this we get self.MODULE_PADDING amount of padding between the packed modules.
Raises
ModuleLargerThanBinError
- If at least one module is too large to fit into a bin. A bin is a page in this context
Expand source code
def reorganize_rectpack(self): """Reorganize modules optimally using the rectpack library. Note that to add padding between the modules we add self.MODULE_PADDING to each width,height of rectangles sent to rectpack for packing. We also tell rectpack that our "bins" are self.MODULE_PADDING larger in each dimension. As a result of this we get self.MODULE_PADDING amount of padding between the packed modules. Raises ------ ModuleLargerThanBinError If at least one module is too large to fit into a bin. A bin is a page in this context """ packer = self.get_rectpack_packer() self.sort_modules() for module in self.modules: module_width, module_height = module["widget"].get_size() padded_module_width = module_width + self.MODULE_PADDING padded_module_height = module_height + self.MODULE_PADDING packer.add_rect( padded_module_width, padded_module_height, module["meta"]["name"], ) # float("inf") to add infinite bins. # See https://github.com/secnot/rectpack/blob/master/README.md#api packer.add_bin( self.CONTENT_WIDTH + self.MODULE_PADDING, self.CONTENT_HEIGHT + self.MODULE_PADDING, float("inf"), ) packer.pack() packed_rects = packer.rect_list() # Explode if rectpack failed to pack all rects if len(packed_rects) != len(self.modules): raise ModuleLargerThanBinError() # Update modules based on packed rects returned from rectpack for packed_rect in packed_rects: packed_rect_bin = packed_rect[0] packed_rect_x = packed_rect[1] packed_rect_y = packed_rect[2] packed_rect_id = packed_rect[5] corresponding_module = self.get_module_with_name(packed_rect_id) if corresponding_module is None: raise Exception("Module was lost during packing!") corresponding_module["meta"]["x"] = packed_rect_x corresponding_module["meta"]["y"] = packed_rect_y corresponding_module["meta"]["page"] = packed_rect_bin + 1 self.update_module_widget_position(corresponding_module)
def sort_modules(self)
-
Sort modules in place using chosen sorting algorithm.
Expand source code
def sort_modules(self): """Sort modules in place using chosen sorting algorithm.""" chosen_algorithm_sort = STRING_TO_ALGORITHM_INITIAL_SORT[ self.algorithm_sort ] self.modules = chosen_algorithm_sort(self.modules)
def update_module_widget_position(self, module)
-
Update position of module widget based on meta position.
For reasoning on the calculations done in this function see the class docstring.
Parameters
module
:see
description
- should be dict of fields "meta" and "widget", where "meta" is itself a dict with fields "x", "y" and "page", and "widget" is a widget based on "ModuleBase"
Expand source code
def update_module_widget_position(self, module): """Update position of module widget based on meta position. For reasoning on the calculations done in this function see the class docstring. Parameters ---------- module : see description should be dict of fields "meta" and "widget", where "meta" is itself a dict with fields "x", "y" and "page", and "widget" is a widget based on "ModuleBase" """ distance_to_start_of_next_soi_content_y = self.HEIGHT + self.PADDING scene_skip_distance_page_height = ( distance_to_start_of_next_soi_content_y * (module["meta"]["page"] - 1) ) new_x = ( module["meta"]["x"] + self.PADDING + self.HEADER_WIDTH + self.INNER_PADDING ) new_y = ( module["meta"]["y"] + self.PADDING + scene_skip_distance_page_height + self.INNER_PADDING ) set_module_pos(module["widget"], QPoint(new_x, new_y))
def update_properties(self, **kwargs)
-
Update properties given as input, and call listeners.
This function exists solely because there are listeners on the properties. A "cleaner" way to achieve the same goal would be to create a "setter" for each property and call the listeners there, but for bulk changes this would get unwieldy. Another way to achieve the goal of calling listeners after changes to properties would be to create a separate function that the user is expected to call after changing the properties directly, but this would put unnecessary responsibility on the user.
All properties except "modules" and "attachements" can be updated using this function.
If a change is made that affects placement of modules this function will call
reorganize
Parameters
**kwargs
:key
,value
pairs
ofproperties
to
update
- Accepts both a normal dict, and passing arguments as normal:
update_properties({'title': 'my title'})
and update_properties(title='my title')`. Accepted keys are properties of the SOI class, except 'modules' and 'attachements'. Explanation of **kwargs: https://stackoverflow.com/a/1769475/3545896
Raises
ValueError
- If argument not referring to SOI property modifiable from this function is passed.
Expand source code
def update_properties(self, **kwargs): # pylint: disable=R0912 """Update properties given as input, and call listeners. This function exists solely because there are listeners on the properties. A "cleaner" way to achieve the same goal would be to create a "setter" for each property and call the listeners there, but for bulk changes this would get unwieldy. Another way to achieve the goal of calling listeners after changes to properties would be to create a separate function that the user is expected to call after changing the properties directly, but this would put unnecessary responsibility on the user. All properties except "modules" and "attachements" can be updated using this function. If a change is made that affects placement of modules this function will call `reorganize` Parameters ---------- **kwargs : key, value pairs of properties to update Accepts both a normal dict, and passing arguments as normal: `update_properties({'title': 'my title'})` and update_properties(title='my title')`. Accepted keys are properties of the SOI class, except 'modules' and 'attachements'. Explanation of **kwargs: https://stackoverflow.com/a/1769475/3545896 Raises ------ ValueError If argument not referring to SOI property modifiable from this function is passed. """ # For every key, value pair passed in kwargs, update corresponding # property for key, value in kwargs.items(): if key == "title": self.title = value elif key == "description": self.description = value elif key == "version": self.version = value elif key == "date": self.date = value elif key == "valid_from": self.valid_from = value elif key == "valid_to": self.valid_to = value elif key == "icon": self.icon = value elif key == "classification": self.classification = value elif key == "orientation": self.orientation = value elif key == "placement_strategy": self.placement_strategy = value elif key == "algorithm_bin": self.algorithm_bin = value elif key == "algorithm_pack": self.algorithm_pack = value elif key == "algorithm_sort": self.algorithm_sort = value else: raise ValueError( f"Unknown property name {key} passed with value {value}" ) for listener in self.update_property_listeners: listener() # If any properties relating to module placement were touched, # reorganize if ( "placement_strategy" in kwargs or "algorithm_bin" in kwargs or "algorithm_pack" in kwargs or "algorithm_sort" in kwargs ): self.reorganize()