# coding=utf-8
"""Module for coloring geometry with attributes."""
from __future__ import division
from .shape import Shape
from .boundary import Boundary
from .search import get_attr_nested
from ladybug.graphic import GraphicContainer
from ladybug.legend import LegendParameters, LegendParametersCategorized
from ladybug_geometry.geometry3d.pointvector import Point3D
class _ColorObject(object):
"""Base class for visualization objects.
Properties:
* legend_parameters
* attr_name
* attr_name_end
* attributes
* attributes_unique
* attributes_original
* min_point
* max_point
* graphic_container
"""
__slots__ = ('_attr_name', '_legend_parameters', '_attr_name_end',
'_attributes', '_attributes_unique', '_attributes_original',
'_min_point', '_max_point')
def __init__(self, legend_parameters=None):
"""Initialize ColorObject."""
# assign the legend parameters of this object
self.legend_parameters = legend_parameters
self._attr_name = None
self._attr_name_end = None
self._attributes = None
self._attributes_unique = None
self._attributes_original = None
self._min_point = None
self._max_point = None
@property
def legend_parameters(self):
"""Get or set the legend parameters."""
return self._legend_parameters
@legend_parameters.setter
def legend_parameters(self, value):
if value is not None:
assert isinstance(value, LegendParameters) and not \
isinstance(value, LegendParametersCategorized), \
'Expected LegendParameters. Got {}.'.format(type(value))
self._legend_parameters = value
else:
self._legend_parameters = LegendParameters()
@property
def attr_name(self):
"""Get a text string of an attribute that the input objects should have."""
return self._attr_name
@property
def attr_name_end(self):
"""Get text for the last attribute in the attr_name.
Useful when attr_name is nested.
"""
return self._attr_name_end
@property
def attributes(self):
"""Get a tuple of text for the attributes assigned to the objects.
If the input attr_name is a valid attribute for the object but None is
assigned, the output will be 'None'. If the input attr_name is not valid
for the input object, 'N/A' will be returned.
"""
return self._attributes
@property
def attributes_unique(self):
"""Get a tuple of text for the unique attributes assigned to the objects."""
return self._attributes_unique
@property
def attributes_original(self):
"""Get a tuple of objects for the attributes assigned to the objects.
These will follow the original object typing of the attribute and won't
be strings like the attributes.
"""
return self._attributes_original
@property
def min_point(self):
"""Get a Point3D for the minimum of the box around the objects."""
return self._min_point
@property
def max_point(self):
"""Get a Point3D for the maximum of the box around the objects."""
return self._max_point
@property
def graphic_container(self):
"""Get a ladybug GraphicContainer that relates to this object.
The GraphicContainer possesses almost all things needed to visualize the
ColorShapes object including the legend, value_colors, etc.
"""
# produce a range of values from the collected attributes
attr_dict = {i: val for i, val in enumerate(self.attributes_unique)}
attr_dict_rev = {val: i for i, val in attr_dict.items()}
try:
values = tuple(attr_dict_rev[r_attr] for r_attr in self.attributes)
except KeyError: # possibly caused by float cast to -0.0
values = []
for r_attr in self.attributes:
if r_attr == '-0.0':
values.append(attr_dict_rev['0.0'])
else:
values.append(attr_dict_rev[r_attr])
# produce legend parameters with an ordinal dict for the attributes
l_par = self.legend_parameters.duplicate()
if l_par.is_segment_count_default:
l_par.segment_count = len(self.attributes_unique)
l_par.ordinal_dictionary = attr_dict
if l_par.is_title_default:
l_par.title = self.attr_name_end.replace('_', ' ').title()
return GraphicContainer(values, self.min_point, self.max_point, l_par)
def _process_attribute_name(self, attr_name):
"""Process the attribute name and assign it to this object."""
self._attr_name = str(attr_name)
at_split = self._attr_name.split('.')
if len(at_split) == 1:
self._attr_name_end = at_split[-1]
elif at_split[-1] == 'display_name':
self._attr_name_end = at_split[-2]
elif at_split[-1] == '__name__' and at_split[-2] == '__class__':
self._attr_name_end = at_split[-3]
else:
self._attr_name_end = at_split[-1]
def _process_attributes(self, ff_objs):
"""Process the attributes of fairyfly objects."""
nd = self.legend_parameters.decimal_count
attributes = [get_attr_nested(obj, self._attr_name, nd, False)
for obj in ff_objs]
attributes_unique = set(attributes)
float_attr = [atr for atr in attributes_unique if isinstance(atr, float)]
str_attr = [str(atr) for atr in attributes_unique if not isinstance(atr, float)]
float_attr.sort()
str_attr.sort()
self._attributes = tuple(str(val) for val in attributes)
self._attributes_unique = tuple(str_attr) + tuple(str(val) for val in float_attr)
self._attributes_original = \
tuple(get_attr_nested(obj, self._attr_name, cast_to_str=False)
for obj in ff_objs)
def _calculate_min_max(self, ff_objs):
"""Calculate maximum and minimum Point3D for a set of shapes."""
st_rm_min, st_rm_max = ff_objs[0].min, ff_objs[0].max
min_pt = [st_rm_min.x, st_rm_min.y, st_rm_min.z]
max_pt = [st_rm_max.x, st_rm_max.y, st_rm_max.z]
for shape in ff_objs[1:]:
rm_min, rm_max = shape.min, shape.max
if rm_min.x < min_pt[0]:
min_pt[0] = rm_min.x
if rm_min.y < min_pt[1]:
min_pt[1] = rm_min.y
if rm_min.z < min_pt[2]:
min_pt[2] = rm_min.z
if rm_max.x > max_pt[0]:
max_pt[0] = rm_max.x
if rm_max.y > max_pt[1]:
max_pt[1] = rm_max.y
if rm_max.z > max_pt[2]:
max_pt[2] = rm_max.z
self._min_point = Point3D(min_pt[0], min_pt[1], min_pt[2])
self._max_point = Point3D(max_pt[0], max_pt[1], max_pt[2])
def ToString(self):
"""Overwrite .NET ToString."""
return self.__repr__()
[docs]
class ColorShape(_ColorObject):
"""Object for visualizing shape attributes.
Args:
shapes: An array of fairyfly Shapes, which will be colored with the attribute.
attr_name: A text string of an attribute that the input shapes should have.
This can have '.' that separate the nested attributes from one another.
For example, 'properties.therm.materials'.
legend_parameters: An optional LegendParameter object to change the display
of the ColorShape (Default: None).
Properties:
* shapes
* attr_name
* legend_parameters
* attr_name_end
* attributes
* attributes_unique
* attributes_original
* geometry
* graphic_container
* min_point
* max_point
"""
__slots__ = ('_shapes',)
def __init__(self, shapes, attr_name, legend_parameters=None):
"""Initialize ColorShape."""
try: # check the input shapes
shapes = tuple(shapes)
except TypeError:
raise TypeError('Input shapes must be an array. Got {}.'.format(type(shapes)))
assert len(shapes) > 0, 'ColorShapes must have at least one shape.'
for shape in shapes:
assert isinstance(shape, Shape), 'Expected fairyfly Shape for ' \
'ColorShape shapes. Got {}.'.format(type(shape))
self._shapes = shapes
self._calculate_min_max(shapes)
# assign the legend parameters of this object
self.legend_parameters = legend_parameters
# get the attributes of the input shapes
self._process_attribute_name(attr_name)
self._process_attributes(shapes)
@property
def shapes(self):
"""Get a tuple of fairyfly Shapes assigned to this object."""
return self._shapes
@property
def geometry(self):
"""Get a nested array with each sub-array having the Face3D of each shape."""
return [s.geometry for s in self.shapes]
def __repr__(self):
"""Color Shape representation."""
return 'Color Shape:\n{} Shapes\n{}'.format(len(self.shapes), self.attr_name_end)
[docs]
class ColorBoundary(_ColorObject):
"""Object for visualizing boundary attributes.
Args:
boundaries: An array of fairyfly Boundaries which will be colored with
their attributes.
attr_name: A text string of an attribute that the input faces should have.
This can have '.' that separate the nested attributes from one another.
For example, 'properties.therm.condition.temperature'.
legend_parameters: An optional LegendParameter object to change the display
of the ColorBoundary (Default: None).
Properties:
* boundaries
* attr_name
* legend_parameters
* attr_name_end
* attributes_unique
* attributes
* attributes_original
* flat_geometry
* graphic_container
* min_point
* max_point
"""
__slots__ = ('_boundaries',)
def __init__(self, boundaries, attr_name, legend_parameters=None):
"""Initialize ColorBoundary."""
try: # check the input boundaries
boundaries = tuple(boundaries)
except TypeError:
raise TypeError(
'Input boundaries must be an array. Got {}.'.format(type(boundaries)))
assert len(boundaries) > 0, 'ColorBoundary must have at least one boundary.'
for bound in boundaries:
assert isinstance(bound, Boundary), 'Expected fairyfly Boundary for ' \
'ColorBoundary. Got {}.'.format(type(bound))
self._boundaries = boundaries
self._calculate_min_max(boundaries)
# assign the legend parameters of this object
self.legend_parameters = legend_parameters
# get the attributes of the input faces
self._process_attribute_name(attr_name)
self._process_attributes(boundaries)
@property
def boundaries(self):
"""Get the fairyfly Boundaries assigned to this object.
"""
return self._boundaries
@property
def attributes(self):
"""Get a tuple of text for the attributes assigned to the objects.
If the input attr_name is a valid attribute for the object but None is
assigned, the output will be 'None'. If the input attr_name is not valid
for the input object, 'N/A' will be returned.
"""
flat_attr = []
for bnd, attrib in zip(self._boundaries, self._attributes):
flat_attr.extend([attrib] * len(bnd))
return flat_attr
@property
def attributes_original(self):
"""Get a tuple of objects for the attributes assigned to the objects.
These will follow the original object typing of the attribute and won't
be strings like the attributes.
"""
flat_attr = []
for bnd, attrib in zip(self._boundaries, self._attributes_original):
flat_attr.extend([attrib] * len(bnd))
return flat_attr
@property
def flat_geometry(self):
"""Get an array of LineSegment3D on this object.
The geometries here align with the attributes and graphic_container colors.
"""
return [lin for bound in self._boundaries for lin in bound]
def __repr__(self):
"""Color Shape representation."""
return 'Color Boundary:\n{} Boundary\n{}'.format(
len(self.boundaries), self.attr_name_end)