# coding=utf-8
"""Module for computing geometry for the compass used by a variety of graphics."""
from __future__ import division
from ladybug_geometry.geometry2d.pointvector import Point2D, Vector2D
from ladybug_geometry.geometry2d.line import LineSegment2D
from ladybug_geometry.geometry2d.arc import Arc2D
from ladybug_geometry.geometry3d.pointvector import Point3D
import math
[docs]
class Compass(object):
"""Object for computing geometry for the compass used by a variety of graphics.
Methods to project points to orthographic and stereographic projections are
also within this class so that "domed" visualizations can be synchronized with
the compass in the 2D plane.
Args:
radius: A positive number for the radius of the compass. (Default: 100).
center: A ladybug_geometry Point2D for the center of the compass
in the scene. (Default: (0, 0) for the World origin).
north_angle: A number between -360 and 360 for the counterclockwise
difference between the North and the positive Y-axis in degrees.
90 is West and 270 is East (Default: 0).
spacing_factor: A positive number for the fraction of the radius that
labels and tick marks occupy around the compass. (Default: 0.15)
Properties:
* radius
* center
* north_angle
* north_vector
* spacing_factor
* min_point
* max_point
* inner_boundary_circle
* all_boundary_circles
* major_azimuth_points
* major_azimuth_ticks
* minor_azimuth_points
* minor_azimuth_ticks
* orthographic_altitude_circles
* orthographic_altitude_points
* stereographic_altitude_circles
* stereographic_altitude_points
"""
__slots__ = ('_radius', '_center', '_north_angle', '_north_vector',
'_spacing_factor')
MAJOR_AZIMUTHS = (0, 90, 180, 270)
MAJOR_TEXT = ('N', 'E', 'S', 'W')
MINOR_AZIMUTHS = (22.5, 45, 67.5, 112.5, 135, 157.5, 202.5, 225, 247.5,
292.5, 315, 337.5)
MINOR_TEXT = ('NNE', 'NE', 'ENE', 'ESE', 'SE', 'SSE', 'SSW', 'SW', 'WSW',
'WNW', 'NW', 'NNW')
ALTITUDES = (10, 20, 30, 40, 50, 60, 70, 80)
PI = math.pi
def __init__(self, radius=100, center=Point2D(), north_angle=0,
spacing_factor=0.15):
"""Initialize Compass."""
self.radius = radius
self.center = center
self.north_angle = north_angle
self.spacing_factor = spacing_factor
@property
def radius(self):
"""Get or set a positive number for the radius of the compass."""
return self._radius
@radius.setter
def radius(self, value):
self._radius = float(value)
assert self._radius > 0, \
'Compass radius must be a positive number. Got {}.'.format(value)
@property
def center(self):
"""Get or set a Point2D for the center of the compass in the scene."""
return self._center
@center.setter
def center(self, value):
assert isinstance(value, Point2D), 'Expected ladybug_geometry Point2D ' \
'for Compass center. Got {}.'.format(type(value))
self._center = value
@property
def north_angle(self):
"""Get or set a number between -360 and 360 for the north_angle in degrees."""
return math.degrees(self._north_angle)
@north_angle.setter
def north_angle(self, value):
self._north_angle = math.radians(float(value))
self._north_vector = Vector2D(0, 1).rotate(math.radians(-self._north_angle))
assert -self.PI * 2 <= self._north_angle <= self.PI * 2, \
'north_angle value should be between -360 and 360. Got {}.'.format(value)
@property
def north_vector(self):
"""Get or set a ladybug_geometry Vector2D for the north direction."""
return self._north_vector
@north_vector.setter
def north_vector(self, value):
assert isinstance(value, Vector2D), \
'Expected Vector2D for north_vector. Got {}.'.format(type(value))
self._north_vector = value
self._north_angle = \
math.degrees(Vector2D(0, 1).angle_clockwise(self._north_vector))
@property
def spacing_factor(self):
"""Get or set a number for the fraction of radius occupied by labels and ticks.
"""
return self._spacing_factor
@spacing_factor.setter
def spacing_factor(self, value):
self._spacing_factor = float(value)
assert self._spacing_factor > 0, \
'Compass spacing_factor must be a positive number. Got {}.'.format(value)
@property
def min_point(self):
"""Get a Point2D for the minimum around the entire compass."""
fac = (1 + self.spacing_factor) * self.radius
return Point2D(self.center.x - fac, self.center.y - fac)
@property
def max_point(self):
"""Get a Point2D for the minimum around the entire compass."""
fac = (1 + self.spacing_factor) * self.radius
return Point2D(self.center.x + fac, self.center.y + fac)
@property
def inner_boundary_circle(self):
"""Get a Arc2D for the inner circle of the compass.
This is essentially a circle with the compass radius.
"""
return Arc2D(self.center, self.radius)
@property
def all_boundary_circles(self):
"""Get an array of 3 Arc2Ds for the circles of the compass."""
arc2 = Arc2D(self.center, self.radius * (1 + self.spacing_factor * 0.1))
arc3 = Arc2D(self.center, self.radius * (1 + self.spacing_factor * 0.3))
return [self.inner_boundary_circle, arc2, arc3]
@property
def major_azimuth_points(self):
"""Get a list of Point2Ds for the major azimuth labels."""
return self.label_points_from_angles(self.MAJOR_AZIMUTHS)
@property
def major_azimuth_ticks(self):
"""Get a list of LineSegment2Ds for the major azimuth labels."""
return self.ticks_from_angles(self.MAJOR_AZIMUTHS, 0.5)
@property
def minor_azimuth_points(self):
"""Get a list of Point2Ds for the minor azimuth labels."""
return self.label_points_from_angles(self.MINOR_AZIMUTHS)
@property
def minor_azimuth_ticks(self):
"""Get a list of LineSegment2Ds for the minor azimuth labels."""
return self.ticks_from_angles(self.MINOR_AZIMUTHS, 0.3)
@property
def orthographic_altitude_circles(self):
"""Get a list of circles for the orthographic altitude labels."""
circles = []
for angle in self.ALTITUDES:
circles.append(
Arc2D(self.center, self.radius * math.cos(math.radians(angle)))
)
return circles
@property
def orthographic_altitude_points(self):
"""Get a list of Point2Ds for the orthographic altitude labels."""
pts = []
for angle in self.ALTITUDES:
spacing_fac = self.radius * 0.01 # spacing factor
add_y = (self.radius * math.cos(math.radians(angle))) - spacing_fac
pts.append(Point2D(self.center.x, self.center.y + add_y))
if self._north_angle != 0:
pts = [pt.rotate(self._north_angle, self.center) for pt in pts]
return pts
@property
def stereographic_altitude_circles(self):
"""Get a list of circles for the stereographic altitude labels."""
circles = []
for angle in self.ALTITUDES:
ang = math.radians(angle)
pt3d = Point3D(math.cos(ang), 0, math.sin(ang))
radius = self.point3d_to_stereographic(pt3d, 1).x * self.radius
circles.append(Arc2D(self.center, radius))
return circles
@property
def stereographic_altitude_points(self):
"""Get a list of Point2Ds for the stereographic altitude labels."""
pts = []
for angle in self.ALTITUDES:
spacing_fac = self.radius * 0.01 # spacing factor
ang = math.radians(angle)
pt3d = Point3D(math.cos(ang), 0, math.sin(ang))
add_y = (self.point3d_to_stereographic(pt3d, 1).x * self.radius) \
- spacing_fac
pts.append(Point2D(self.center.x, self.center.y + add_y))
if self._north_angle != 0:
pts = [pt.rotate(self._north_angle, self.center) for pt in pts]
return pts
[docs]
def label_points_from_angles(self, angles, factor=0.8):
"""Get a list of label points from a list of angles between 0 and 360.
Args:
angles: An array of numbers between 0 and 360 for the angles of
custom angle labels to be generated for the compass.
factor: A number between 0 and 1 for the fraction of the spacing_factor
at which the points should be generated.
"""
circle = Arc2D(self.center, self.radius * (1 + self.spacing_factor * factor))
return [circle.point_at_angle(self._north_angle - math.radians(angle - 90))
for angle in angles]
[docs]
def ticks_from_angles(self, angles, factor=0.3):
"""Get a list of Linesegment2Ds from a list of angles between 0 and 360."""
pts_in = self.label_points_from_angles(angles, 0)
pts_out = self.label_points_from_angles(angles, factor)
return [LineSegment2D.from_end_points(pi, po) for pi, po in zip(pts_in, pts_out)]
[docs]
def min_point3d(self, z=0):
"""Get a Point3D for the minimum around the entire compass."""
min_pt = self.min_point
return Point3D(min_pt.x, min_pt.y, z)
[docs]
def max_point3d(self, z=0):
"""Get a Point3D for the minimum around the entire compass."""
max_pt = self.max_point
return Point3D(max_pt.x, max_pt.y, z)
[docs]
def duplicate(self):
"""Get a copy of this object."""
return self.__copy__()
[docs]
@staticmethod
def point3d_to_orthographic(point):
"""Get a Point2D for a given Point3D using a orthographic projection.
Args:
point: A ladybug_geometry Point3D to be projected into 2D space via
stereographic projection.
"""
return Point2D(point.x, point.y)
[docs]
@staticmethod
def point3d_to_stereographic(point, radius=100, origin=Point3D()):
"""Get a Point2D for a given Point3D using a stereographic projection.
Args:
point: A ladybug_geometry Point3D to be projected into 2D space via
stereographic projection.
radius: A positive number for the radius of the sphere on which the
point exists. (Default: 100).
origin: An optional ladybug_geometry Point3D representing the origin
of the coordinate system in which the projection is happening.
(eg. the center of the compass).
"""
# move the point to the world origin
coords = ((point.x - origin.x), (point.y - origin.y), (point.z - origin.z))
# perform the stereographic projection while scaling it to the unit sphere
proj_pt = (coords[0] / (radius + coords[2]), coords[1] / (radius + coords[2]))
# move the point back to its original location and scale
return Point2D(proj_pt[0] * radius + origin.x, proj_pt[1] * radius + origin.y)
def __key(self):
"""A tuple based on the object properties, useful for hashing."""
return (self.radius, hash(self.center), self.north_angle, self.spacing_factor)
def __hash__(self):
return hash(self.__key())
def __eq__(self, other):
return isinstance(other, Compass) and self.__key() == other.__key()
def __ne__(self, other):
return not self.__eq__(other)
def __copy__(self):
return Compass(self.radius, self.center, self.north_angle, self.spacing_factor)
def __repr__(self):
"""Compass representation."""
return "Compass (radius:{}, center:{})".format(self.radius, self.center)