Source code for mathmaker.lib.core.base_geometry

# -*- coding: utf-8 -*-

# Mathmaker creates automatically maths exercises sheets with their answers
# Copyright 2006-2017 Nicolas Hainaux <nh.techn@gmail.com>

# This file is part of Mathmaker.

# Mathmaker is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# any later version.

# Mathmaker 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 General Public License for more details.

# You should have received a copy of the GNU General Public License
# along with Mathmaker; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA

# ------------------------------------------------------------------------------
# --------------------------------------------------------------------------
# ------------------------------------------------------------------------------
##
# @package core.base_geometry
# @brief Mathematical elementary geometrical objects.

import math
from decimal import Decimal, ROUND_UP, ROUND_HALF_EVEN

from mathmakerlib import required
from mathmakerlib.calculus import is_number, Number

from mathmaker.lib.core.base import Drawable, Printable
from mathmaker.lib.core.base_calculus import Value
from mathmaker.lib.constants.latex import MARKUP


# the mark 'dashed' has been removed from the available list since it may
# produce buggy results sometimes from euktopst
AVAILABLE_ANGLE_MARKS = ['', 'simple', 'double', 'triple', 'right',
                         'forth', 'back', 'dotted']
AVAILABLE_SEGMENT_MARKS = ['', 'simple', 'double', 'triple', 'cross']


# ------------------------------------------------------------------------------
# --------------------------------------------------------------------------
# ------------------------------------------------------------------------------
##
# @class Point
# @brief
[docs]class Point(Drawable): def __init__(self, name=None, x=None, y=None): """ Initialize Point :param name: the Point's name (e.g. 'A') or another Point to copy :type name: str :param x: the Point's abscissa :type x: a number :param y: the Point's ordinate :type y: a number """ if isinstance(name, Point): Point.__init__(self, name=name.name, x=name.x, y=name.y) else: if type(name) is not str: raise TypeError('A Point\'s name must be a str') if any([not is_number(n) for n in [x, y]]): raise TypeError('x and y must be numbers') self._name = name self._x = Decimal(str(x)) self._y = Decimal(str(y)) self._x_exact = self._x self._y_exact = self._y def __repr__(self): return '#{}({}; {})#'.format(self.name, self.x, self.y) def __eq__(self, other): return all([self.x == other.x, self.y == other.y, self.name == other.name]) def __ne__(self, other): return any([self.x != other.x, self.y != other.y, self.name != other.name]) # -------------------------------------------------------------------------- ## # @brief Returns the abscissa of the Point, rounded up to the tenth @property def x(self): return Number(str(self._x)).rounded(Decimal('0.01')) # -------------------------------------------------------------------------- ## # @brief Returns the exact abscissa of the Point @property def x_exact(self): return self._x # -------------------------------------------------------------------------- ## # @brief Returns the ordinate of the Point, rounded up to the tenth @property def y(self): return Number(str(self._y)).rounded(Decimal('0.01')) # -------------------------------------------------------------------------- ## # @brief Returns the exact ordinate of the Point @property def y_exact(self): return self._y @property def xy(self): return str(self.x_exact) + str(self.y_exact) # -------------------------------------------------------------------------- ## # @brief Sets the abscissa of the Point @x.setter def x(self, arg): if not is_number(arg): raise ValueError('Instead of a number, got: ' + str(arg)) self._x = arg # -------------------------------------------------------------------------- ## # @brief Sets the ordinate of the Point @y.setter def y(self, arg): if not is_number(arg): raise ValueError('Instead of a number, got: ' + str(arg)) self._y = arg # -------------------------------------------------------------------------- ## # @brief Returns the name of the object @property def name(self): return self._name # -------------------------------------------------------------------------- ## # @brief Allows to rename Points (other Drawables are not allowed to). @name.setter def name(self, arg): if not (type(arg) == str): raise TypeError("Expected a string") if not (len(arg) == 1): raise ValueError("Expected a string of one char only") self._name = arg # -------------------------------------------------------------------------- ## # @brief Returns a new Point after rotation of self
[docs] def rotate(self, center, angle, **options): if not isinstance(center, Point): raise ValueError('Instead of a Point, got: ' + str(type(center))) if not is_number(angle): raise ValueError('Instead of a number, got: ' + str(type(angle))) delta_x = self.x_exact - center.x_exact delta_y = self.y_exact - center.y_exact rx = delta_x * Decimal(str(math.cos(math.radians(angle)))) \ - delta_y * Decimal(str(math.sin(math.radians(angle)))) \ + center.x_exact ry = delta_x * Decimal(str(math.sin(math.radians(angle)))) \ + delta_y * Decimal(str(math.cos(math.radians(angle)))) \ + center.y_exact new_name = self.name + "'" if 'keep_name' in options and options['keep_name']: new_name = self.name elif 'new_name' in options and type(options['new_name']) == str: new_name = options['new_name'] return Point(new_name, rx, ry)
[docs] def into_euk(self): return '{name} = point({x}, {y})\n'.format(name=self.name, x=self.x, y=self.y)
# ------------------------------------------------------------------------------ # -------------------------------------------------------------------------- # ------------------------------------------------------------------------------ ## # @class Segment # @brief
[docs]class Segment(Drawable): # -------------------------------------------------------------------------- ## # @brief Constructor. # @param arg (Point, Point) # Types details: # - # @param options # Options details: # - # @warning Might raise... def __init__(self, arg, **options): self._label = Value("") if not (type(arg) == tuple or isinstance(arg, Segment)): raise ValueError('Instead of tuple|Segment, got: ' + str(type(arg))) elif type(arg) == tuple: if not (isinstance(arg[0], Point) and isinstance(arg[1], Point)): # __ raise ValueError('Instead of (Point, Point) got: ' + str(arg)) self._points = (arg[0].clone(), arg[1].clone()) if 'label' in options and type(options['label']) == str: self._label = options['label'] else: self._points = (arg.points[0].clone(), arg.points[1].clone()) self._label = arg.label self._mark = "" self._name = "[" + self.points[0].name + self.points[1].name + "]" self._length = Value(0) self._length_has_been_set = False self._length_name = self._points[0].name + self._points[1].name # -------------------------------------------------------------------------- ## # @brief Returns the two points @property def points(self): return self._points
[docs] def revert(self): reverted = self.clone() reverted._points = reverted._points[::-1] return reverted
@property def label(self): """Label of the Segment (the displayed information).""" return self._label @property def real_length(self): """Real length (build length) of the Segment.""" x_delta = self.points[0].x - self.points[1].x y_delta = self.points[0].y - self.points[1].y return math.hypot(x_delta, y_delta) @property def length(self): """Fake length of the Segment (the one used in a problem).""" return self._length @property def length_has_been_set(self): """Whether the (fake) length has been set or not.""" return self._length_has_been_set @property def length_name(self): """Length's name of the Segment, like AB.""" return self._length_name
[docs] def invert_length_name(self): """Swap points' names in the length name. E.g. AB becomes BA.""" self._length_name = self._length_name[::-1]
# -------------------------------------------------------------------------- ## # @brief Returns the mark of the Segment @property def mark(self): return self._mark # -------------------------------------------------------------------------- ## # @brief Will set length as the Segment's label, or "?", or nothing # @param flag If flag evaluates to "?"|None, the Segment's label will be # set to "?". Otherwise, if it evaluates to False, it will be # set to '', and to True, it will be set to its length.
[docs] def setup_label(self, flag): if flag is None or flag == '?': self.label = Value('?') elif flag in ['hid', 'hidden', 'known_but_hidden']: self.label = Value('hidden') elif flag: self.label = Value(self.length) elif not flag: self.label = Value('')
# -------------------------------------------------------------------------- ## # @brief Sets the label of the Segment @label.setter def label(self, arg): if not type(arg) == Value: raise ValueError('Instead of Value, got: ' + str(type(arg))) self._label = arg # -------------------------------------------------------------------------- ## # @brief Sets the fake length of the Segment (the one used in a problem) @length.setter def length(self, arg): if not isinstance(arg, Value): raise TypeError('Expected a Value, got ' + str(type(arg)) + " " 'instead.') if not arg.is_numeric(): raise ValueError('Instead of numeric Value, got: ' 'a Value but not numeric, it contains ' + str(arg.raw_value)) self._length = arg self._length_has_been_set = True # -------------------------------------------------------------------------- ## # @brief Sets the mark of the Segment @mark.setter def mark(self, arg): if not type(arg) == str: raise ValueError('Instead of str, got: ' + str(type(arg))) if arg not in AVAILABLE_SEGMENT_MARKS: raise ValueError('Got: ' + str(arg) + ' instead of a string from this list: ' + str(AVAILABLE_SEGMENT_MARKS)) self._mark = arg
[docs] def dividing_points(self, n=1, prefix='a'): """ Create the list of Points that divide the Segment in n parts. :param n: the number of parts (so it will create n - 1 points) n must be greater or equal to 1 :type n: int """ if type(n) is not int: raise TypeError('n must be an int') if not n >= 1: raise ValueError('n must be greater or equal to 1') x0 = Decimal(str(self.points[0].x_exact)) x1 = Decimal(str(self.points[1].x_exact)) xstep = (x1 - x0) / n x_list = [x0 + (i + 1) * xstep for i in range(n - 1)] y0 = Decimal(str(self.points[0].y_exact)) y1 = Decimal(str(self.points[1].y_exact)) ystep = (y1 - y0) / n y_list = [y0 + (i + 1) * ystep for i in range(n - 1)] return [Point(prefix + str(i + 1), x, y) for i, (x, y) in enumerate(zip(x_list, y_list))]
[docs] def label_into_euk(self): """Return the label correctly positionned along the Segment.""" if self.label in [Value(''), Value('hidden')]: return '' else: result = '' x = self.real_length scale_factor = Number(str(1.6 * x))\ .rounded(Decimal('0.1'), rounding=ROUND_UP) if x <= 3: angle_correction = Number(str(-8 * x + 33))\ .rounded(Decimal('0.1'), rounding=ROUND_UP) else: angle_correction = \ Number(str(1.1 / (1 - 0.95 * math.exp(-0.027 * x))))\ .rounded(Decimal('0.1'), rounding=ROUND_UP) side_angle = Vector((self.points[0], self.points[1])).slope label_position_angle = Number(side_angle) \ .rounded(Decimal('1'), rounding=ROUND_HALF_EVEN) label_position_angle %= Decimal("360") rotate_box_angle = Decimal(label_position_angle) if (rotate_box_angle >= 90 and rotate_box_angle <= 270): rotate_box_angle -= Decimal("180") elif (rotate_box_angle <= -90 and rotate_box_angle >= -270): rotate_box_angle += Decimal("180") rotate_box_angle %= Decimal("360") result += " $\\rotatebox{" required.package['graphicx'] = True result += str(rotate_box_angle) result += "}{\sffamily " result += self.label.into_str(display_unit=True, textwrap=False) result += "}$ " result += self.points[0].name + " " result += str(label_position_angle) result += " - " result += str(angle_correction) + " deg " result += str(scale_factor) result += "\n" return result
[docs] def into_euk(self): return self.points[0].name + '.' + self.points[1].name
# ------------------------------------------------------------------------------ # -------------------------------------------------------------------------- # ------------------------------------------------------------------------------ ## # @class Vector # @brief
[docs]class Vector(Point): # -------------------------------------------------------------------------- ## # @brief Vector's constructor. # @param arg (Point, Point) | Point | (x, y) def __init__(self, arg, **options): self._x_exact = Decimal('1') self._y_exact = Decimal('1') if isinstance(arg, Point): Point.__init__(self, Point('', arg.x_exact, arg.y_exact)) elif isinstance(arg, tuple) and len(arg) == 2: if all([isinstance(elt, Point) for elt in arg]): Point.__init__(self, Point('', arg[1].x_exact - arg[0].x_exact, arg[1].y_exact - arg[0].y_exact)) elif all([is_number(elt) for elt in arg]): Point.__init__(self, Point('', arg[0], arg[1])) else: raise ValueError('Got a tuple not only of Points or numbers, ' 'instead of (Point,Point)|(x,y)') else: raise ValueError('Got: ' + str(type(arg)) + ' instead of Point|(,)') # -------------------------------------------------------------------------- ## # @brief Adds two vectors # @param arg Vector def __add__(self, arg): if not isinstance(arg, Vector): raise ValueError('Got: ' + str(type(arg)) + ' instead of a Vector') return Vector((self.x_exact + arg.x_exact, self.y_exact + arg.y_exact)) @property def norm(self): """Return the norm of self.""" return Decimal(str(math.hypot(self._x_exact, self._y_exact))) @property def slope(self): """Return the slope of self.""" theta = Number( str(math.degrees(math.acos(self._x_exact / self.norm))))\ .rounded(Decimal('0.1')) return theta if self._y_exact > 0 else Decimal("360") - theta
[docs] def unit_vector(self): """Return the unit vector built from self""" return Vector((self._x_exact / self.norm, self._y_exact / self.norm))
[docs] def bisector_vector(self, arg): """ Return a vector colinear to the bisector of self and another vector. :param arg: the other vector :type arg: Vector """ if not isinstance(arg, Vector): raise ValueError('Got: ' + str(type(arg)) + ' instead of a Vector') return self.unit_vector() + arg.unit_vector()
[docs] def orthogonal_unit_vector(self, clockwise=True): """ Return a unit vector that's (default clockwise) orthogonal to self. If clockwise is set to False, then the anti-clockwise orthogonal vector is returned. """ u = self.unit_vector() if clockwise: return Vector((u._y_exact, -u._x_exact)) else: return Vector((-u._y_exact, u._x_exact))
[docs] def into_euk(self): raise NotImplementedError
# ------------------------------------------------------------------------------ # -------------------------------------------------------------------------- # ------------------------------------------------------------------------------ ## # @class Ray # @brief
[docs]class Ray(Drawable): # -------------------------------------------------------------------------- ## # @brief Constructor. # @param arg: (Point, Point) # @param options: label # Options details: # @warning Might raise... def __init__(self, arg, **options): self._point0 = None # the initial point self._point1 = None self._name = None if (isinstance(arg, tuple) and len(arg) == 2 and isinstance(arg[0], Point) and isinstance(arg[1], Point)): # __ self._point0 = arg[0].clone() self._point1 = arg[1].clone() self._name = MARKUP['opening_square_bracket'] self._name += arg[0].name + arg[1].name self._name += MARKUP['closing_bracket'] elif isinstance(arg, Ray): self._point0 = arg._point0.clone() self._point1 = arg._point1.clone() self._name = arg._name
[docs] def into_euk(self): raise NotImplementedError
# ------------------------------------------------------------------------------ # -------------------------------------------------------------------------- # ------------------------------------------------------------------------------ ## # @class Angle # @brief
[docs]class Angle(Drawable, Printable): # -------------------------------------------------------------------------- ## # @brief Constructor. # @param arg: (Point, Point, Point) # @param options: label # Options details: # @warning Might raise... def __init__(self, arg, **options): self._ray0 = None self._ray1 = None self._points = None self._measure = None self._mark = "" self._label = Value("") self._name = None if (isinstance(arg, tuple) and len(arg) == 3 and isinstance(arg[0], Point) and isinstance(arg[1], Point) and isinstance(arg[2], Point)): # __ self._ray0 = Ray((arg[1], arg[0])) self._ray1 = Ray((arg[1], arg[2])) self._points = [arg[0].clone(), arg[1].clone(), arg[2].clone()] # Let's determine the measure of the angle: aux_side0 = Segment((self._points[0], self._points[1])) aux_side1 = Segment((self._points[1], self._points[2])) aux_side2 = Segment((self._points[2], self._points[0])) aux_num = aux_side0.real_length * aux_side0.real_length \ + aux_side1.real_length * aux_side1.real_length \ - aux_side2.real_length * aux_side2.real_length aux_denom = 2 * aux_side0.real_length * aux_side1.real_length aux_cos = aux_num / aux_denom self._measure = Decimal(str(math.degrees(math.acos(aux_cos)))) if 'label' in options and type(options['label']) == str: self._label = Value(options['label']) if 'mark' in options and type(options['mark']) == str: self._mark = options['mark'] self._name = MARKUP['opening_widehat'] self._name += arg[0].name + arg[1].name + arg[2].name self._name += MARKUP['closing_widehat'] self._label_display_angle = Number(str(self._measure)) \ .rounded(Decimal('0.1')) / 2 elif isinstance(arg, Angle): self._ray0 = arg._ray0.clone() self._ray1 = arg._ray1.clone() self._points = [p.clone() for p in arg._points] self._measure = arg._measure self._mark = arg._mark self._label = arg._label.clone() self._name = arg._name self._label_display_angle = arg._label_display_angle else: raise ValueError('Expected (Point, Point, Point) ' + ' instead of: ' + str(type(arg))) def __repr__(self): return ' ∡ ' + self._name + ' ∡ ' def __hash__(self): return hash(self._name) def __eq__(self, other_objct): if not isinstance(other_objct, Angle): return False return self._name == other_objct._name # -------------------------------------------------------------------------- ## # @brief Returns the measure of the angle @property def measure(self): return self._measure # -------------------------------------------------------------------------- ## # @brief Returns the points of the angle @property def points(self): return self._points # -------------------------------------------------------------------------- ## # @brief Returns the label of the angle @property def label(self): return self._label # -------------------------------------------------------------------------- ## # @brief Returns the angle (for display) of label's angle @property def label_display_angle(self): return self._label_display_angle # -------------------------------------------------------------------------- ## # @brief Returns the mark of the angle @property def mark(self): return self._mark # -------------------------------------------------------------------------- ## # @brief Returns the vertex of the angle @property def vertex(self): return self._points[1] @label.setter def label(self, arg): """Properly set the Angle's label.""" if type(arg) == str or isinstance(arg, Value): self._label = Value(arg) else: raise TypeError('Expected a str or a Value. Got {t} instead.' .format(t=str(type(arg)))) # -------------------------------------------------------------------------- ## # @brief Sets the angle (for display) of label's angle @label_display_angle.setter def label_display_angle(self, arg): if not is_number(arg): raise ValueError('arg should be a number ') else: self._label_display_angle = Number(str(arg))\ .rounded(Decimal('0.1')) # -------------------------------------------------------------------------- ## # @brief Sets the mark of the angle @mark.setter def mark(self, arg): if type(arg) == str: if arg in AVAILABLE_ANGLE_MARKS: self._mark = arg else: raise ValueError('Got: ' + arg + ' instead of a string from this list: ' + str(AVAILABLE_ANGLE_MARKS)) else: raise ValueError('arg should be a str')
[docs] def into_str(self, **options): return self._name
[docs] def into_euk(self): return ''