1"""The definition of the base geometrical entity with attributes common to
2all derived geometrical entities.
3
4Contains
5========
6
7GeometryEntity
8GeometricSet
9
10Notes
11=====
12
13A GeometryEntity is any object that has special geometric properties.
14A GeometrySet is a superclass of any GeometryEntity that can also
15be viewed as a sympy.sets.Set.  In particular, points are the only
16GeometryEntity not considered a Set.
17
18Rn is a GeometrySet representing n-dimensional Euclidean space. R2 and
19R3 are currently the only ambient spaces implemented.
20
21"""
22
23from sympy.core.basic import Basic
24from sympy.core.compatibility import is_sequence
25from sympy.core.containers import Tuple
26from sympy.core.sympify import sympify
27from sympy.functions import cos, sin
28from sympy.matrices import eye
29from sympy.multipledispatch import dispatch
30from sympy.sets import Set
31from sympy.sets.handlers.intersection import intersection_sets
32from sympy.sets.handlers.union import union_sets
33from sympy.utilities.misc import func_name
34
35
36# How entities are ordered; used by __cmp__ in GeometryEntity
37ordering_of_classes = [
38    "Point2D",
39    "Point3D",
40    "Point",
41    "Segment2D",
42    "Ray2D",
43    "Line2D",
44    "Segment3D",
45    "Line3D",
46    "Ray3D",
47    "Segment",
48    "Ray",
49    "Line",
50    "Plane",
51    "Triangle",
52    "RegularPolygon",
53    "Polygon",
54    "Circle",
55    "Ellipse",
56    "Curve",
57    "Parabola"
58]
59
60
61class GeometryEntity(Basic):
62    """The base class for all geometrical entities.
63
64    This class doesn't represent any particular geometric entity, it only
65    provides the implementation of some methods common to all subclasses.
66
67    """
68
69    def __cmp__(self, other):
70        """Comparison of two GeometryEntities."""
71        n1 = self.__class__.__name__
72        n2 = other.__class__.__name__
73        c = (n1 > n2) - (n1 < n2)
74        if not c:
75            return 0
76
77        i1 = -1
78        for cls in self.__class__.__mro__:
79            try:
80                i1 = ordering_of_classes.index(cls.__name__)
81                break
82            except ValueError:
83                i1 = -1
84        if i1 == -1:
85            return c
86
87        i2 = -1
88        for cls in other.__class__.__mro__:
89            try:
90                i2 = ordering_of_classes.index(cls.__name__)
91                break
92            except ValueError:
93                i2 = -1
94        if i2 == -1:
95            return c
96
97        return (i1 > i2) - (i1 < i2)
98
99    def __contains__(self, other):
100        """Subclasses should implement this method for anything more complex than equality."""
101        if type(self) == type(other):
102            return self == other
103        raise NotImplementedError()
104
105    def __getnewargs__(self):
106        """Returns a tuple that will be passed to __new__ on unpickling."""
107        return tuple(self.args)
108
109    def __ne__(self, o):
110        """Test inequality of two geometrical entities."""
111        return not self == o
112
113    def __new__(cls, *args, **kwargs):
114        # Points are sequences, but they should not
115        # be converted to Tuples, so use this detection function instead.
116        def is_seq_and_not_point(a):
117            # we cannot use isinstance(a, Point) since we cannot import Point
118            if hasattr(a, 'is_Point') and a.is_Point:
119                return False
120            return is_sequence(a)
121
122        args = [Tuple(*a) if is_seq_and_not_point(a) else sympify(a) for a in args]
123        return Basic.__new__(cls, *args)
124
125    def __radd__(self, a):
126        """Implementation of reverse add method."""
127        return a.__add__(self)
128
129    def __rtruediv__(self, a):
130        """Implementation of reverse division method."""
131        return a.__truediv__(self)
132
133    def __repr__(self):
134        """String representation of a GeometryEntity that can be evaluated
135        by sympy."""
136        return type(self).__name__ + repr(self.args)
137
138    def __rmul__(self, a):
139        """Implementation of reverse multiplication method."""
140        return a.__mul__(self)
141
142    def __rsub__(self, a):
143        """Implementation of reverse subtraction method."""
144        return a.__sub__(self)
145
146    def __str__(self):
147        """String representation of a GeometryEntity."""
148        from sympy.printing import sstr
149        return type(self).__name__ + sstr(self.args)
150
151    def _eval_subs(self, old, new):
152        from sympy.geometry.point import Point, Point3D
153        if is_sequence(old) or is_sequence(new):
154            if isinstance(self, Point3D):
155                old = Point3D(old)
156                new = Point3D(new)
157            else:
158                old = Point(old)
159                new = Point(new)
160            return  self._subs(old, new)
161
162    def _repr_svg_(self):
163        """SVG representation of a GeometryEntity suitable for IPython"""
164
165        from sympy.core.evalf import N
166
167        try:
168            bounds = self.bounds
169        except (NotImplementedError, TypeError):
170            # if we have no SVG representation, return None so IPython
171            # will fall back to the next representation
172            return None
173
174        if any([not x.is_number or not x.is_finite for x in bounds]):
175            return None
176
177        svg_top = '''<svg xmlns="http://www.w3.org/2000/svg"
178            xmlns:xlink="http://www.w3.org/1999/xlink"
179            width="{1}" height="{2}" viewBox="{0}"
180            preserveAspectRatio="xMinYMin meet">
181            <defs>
182                <marker id="markerCircle" markerWidth="8" markerHeight="8"
183                    refx="5" refy="5" markerUnits="strokeWidth">
184                    <circle cx="5" cy="5" r="1.5" style="stroke: none; fill:#000000;"/>
185                </marker>
186                <marker id="markerArrow" markerWidth="13" markerHeight="13" refx="2" refy="4"
187                       orient="auto" markerUnits="strokeWidth">
188                    <path d="M2,2 L2,6 L6,4" style="fill: #000000;" />
189                </marker>
190                <marker id="markerReverseArrow" markerWidth="13" markerHeight="13" refx="6" refy="4"
191                       orient="auto" markerUnits="strokeWidth">
192                    <path d="M6,2 L6,6 L2,4" style="fill: #000000;" />
193                </marker>
194            </defs>'''
195
196        # Establish SVG canvas that will fit all the data + small space
197        xmin, ymin, xmax, ymax = map(N, bounds)
198        if xmin == xmax and ymin == ymax:
199            # This is a point; buffer using an arbitrary size
200            xmin, ymin, xmax, ymax = xmin - .5, ymin -.5, xmax + .5, ymax + .5
201        else:
202            # Expand bounds by a fraction of the data ranges
203            expand = 0.1  # or 10%; this keeps arrowheads in view (R plots use 4%)
204            widest_part = max([xmax - xmin, ymax - ymin])
205            expand_amount = widest_part * expand
206            xmin -= expand_amount
207            ymin -= expand_amount
208            xmax += expand_amount
209            ymax += expand_amount
210        dx = xmax - xmin
211        dy = ymax - ymin
212        width = min([max([100., dx]), 300])
213        height = min([max([100., dy]), 300])
214
215        scale_factor = 1. if max(width, height) == 0 else max(dx, dy) / max(width, height)
216        try:
217            svg = self._svg(scale_factor)
218        except (NotImplementedError, TypeError):
219            # if we have no SVG representation, return None so IPython
220            # will fall back to the next representation
221            return None
222
223        view_box = "{} {} {} {}".format(xmin, ymin, dx, dy)
224        transform = "matrix(1,0,0,-1,0,{})".format(ymax + ymin)
225        svg_top = svg_top.format(view_box, width, height)
226
227        return svg_top + (
228            '<g transform="{}">{}</g></svg>'
229            ).format(transform, svg)
230
231    def _svg(self, scale_factor=1., fill_color="#66cc99"):
232        """Returns SVG path element for the GeometryEntity.
233
234        Parameters
235        ==========
236
237        scale_factor : float
238            Multiplication factor for the SVG stroke-width.  Default is 1.
239        fill_color : str, optional
240            Hex string for fill color. Default is "#66cc99".
241        """
242        raise NotImplementedError()
243
244    def _sympy_(self):
245        return self
246
247    @property
248    def ambient_dimension(self):
249        """What is the dimension of the space that the object is contained in?"""
250        raise NotImplementedError()
251
252    @property
253    def bounds(self):
254        """Return a tuple (xmin, ymin, xmax, ymax) representing the bounding
255        rectangle for the geometric figure.
256
257        """
258
259        raise NotImplementedError()
260
261    def encloses(self, o):
262        """
263        Return True if o is inside (not on or outside) the boundaries of self.
264
265        The object will be decomposed into Points and individual Entities need
266        only define an encloses_point method for their class.
267
268        See Also
269        ========
270
271        sympy.geometry.ellipse.Ellipse.encloses_point
272        sympy.geometry.polygon.Polygon.encloses_point
273
274        Examples
275        ========
276
277        >>> from sympy import RegularPolygon, Point, Polygon
278        >>> t  = Polygon(*RegularPolygon(Point(0, 0), 1, 3).vertices)
279        >>> t2 = Polygon(*RegularPolygon(Point(0, 0), 2, 3).vertices)
280        >>> t2.encloses(t)
281        True
282        >>> t.encloses(t2)
283        False
284
285        """
286
287        from sympy.geometry.point import Point
288        from sympy.geometry.line import Segment, Ray, Line
289        from sympy.geometry.ellipse import Ellipse
290        from sympy.geometry.polygon import Polygon, RegularPolygon
291
292        if isinstance(o, Point):
293            return self.encloses_point(o)
294        elif isinstance(o, Segment):
295            return all(self.encloses_point(x) for x in o.points)
296        elif isinstance(o, Ray) or isinstance(o, Line):
297            return False
298        elif isinstance(o, Ellipse):
299            return self.encloses_point(o.center) and \
300                self.encloses_point(
301                Point(o.center.x + o.hradius, o.center.y)) and \
302                not self.intersection(o)
303        elif isinstance(o, Polygon):
304            if isinstance(o, RegularPolygon):
305                if not self.encloses_point(o.center):
306                    return False
307            return all(self.encloses_point(v) for v in o.vertices)
308        raise NotImplementedError()
309
310    def equals(self, o):
311        return self == o
312
313    def intersection(self, o):
314        """
315        Returns a list of all of the intersections of self with o.
316
317        Notes
318        =====
319
320        An entity is not required to implement this method.
321
322        If two different types of entities can intersect, the item with
323        higher index in ordering_of_classes should implement
324        intersections with anything having a lower index.
325
326        See Also
327        ========
328
329        sympy.geometry.util.intersection
330
331        """
332        raise NotImplementedError()
333
334    def is_similar(self, other):
335        """Is this geometrical entity similar to another geometrical entity?
336
337        Two entities are similar if a uniform scaling (enlarging or
338        shrinking) of one of the entities will allow one to obtain the other.
339
340        Notes
341        =====
342
343        This method is not intended to be used directly but rather
344        through the `are_similar` function found in util.py.
345        An entity is not required to implement this method.
346        If two different types of entities can be similar, it is only
347        required that one of them be able to determine this.
348
349        See Also
350        ========
351
352        scale
353
354        """
355        raise NotImplementedError()
356
357    def reflect(self, line):
358        """
359        Reflects an object across a line.
360
361        Parameters
362        ==========
363
364        line: Line
365
366        Examples
367        ========
368
369        >>> from sympy import pi, sqrt, Line, RegularPolygon
370        >>> l = Line((0, pi), slope=sqrt(2))
371        >>> pent = RegularPolygon((1, 2), 1, 5)
372        >>> rpent = pent.reflect(l)
373        >>> rpent
374        RegularPolygon(Point2D(-2*sqrt(2)*pi/3 - 1/3 + 4*sqrt(2)/3, 2/3 + 2*sqrt(2)/3 + 2*pi/3), -1, 5, -atan(2*sqrt(2)) + 3*pi/5)
375
376        >>> from sympy import pi, Line, Circle, Point
377        >>> l = Line((0, pi), slope=1)
378        >>> circ = Circle(Point(0, 0), 5)
379        >>> rcirc = circ.reflect(l)
380        >>> rcirc
381        Circle(Point2D(-pi, pi), -5)
382
383        """
384        from sympy import atan, Point, Dummy, oo
385
386        g = self
387        l = line
388        o = Point(0, 0)
389        if l.slope.is_zero:
390            y = l.args[0].y
391            if not y:  # x-axis
392                return g.scale(y=-1)
393            reps = [(p, p.translate(y=2*(y - p.y))) for p in g.atoms(Point)]
394        elif l.slope is oo:
395            x = l.args[0].x
396            if not x:  # y-axis
397                return g.scale(x=-1)
398            reps = [(p, p.translate(x=2*(x - p.x))) for p in g.atoms(Point)]
399        else:
400            if not hasattr(g, 'reflect') and not all(
401                    isinstance(arg, Point) for arg in g.args):
402                raise NotImplementedError(
403                    'reflect undefined or non-Point args in %s' % g)
404            a = atan(l.slope)
405            c = l.coefficients
406            d = -c[-1]/c[1]  # y-intercept
407            # apply the transform to a single point
408            x, y = Dummy(), Dummy()
409            xf = Point(x, y)
410            xf = xf.translate(y=-d).rotate(-a, o).scale(y=-1
411                ).rotate(a, o).translate(y=d)
412            # replace every point using that transform
413            reps = [(p, xf.xreplace({x: p.x, y: p.y})) for p in g.atoms(Point)]
414        return g.xreplace(dict(reps))
415
416    def rotate(self, angle, pt=None):
417        """Rotate ``angle`` radians counterclockwise about Point ``pt``.
418
419        The default pt is the origin, Point(0, 0)
420
421        See Also
422        ========
423
424        scale, translate
425
426        Examples
427        ========
428
429        >>> from sympy import Point, RegularPolygon, Polygon, pi
430        >>> t = Polygon(*RegularPolygon(Point(0, 0), 1, 3).vertices)
431        >>> t # vertex on x axis
432        Triangle(Point2D(1, 0), Point2D(-1/2, sqrt(3)/2), Point2D(-1/2, -sqrt(3)/2))
433        >>> t.rotate(pi/2) # vertex on y axis now
434        Triangle(Point2D(0, 1), Point2D(-sqrt(3)/2, -1/2), Point2D(sqrt(3)/2, -1/2))
435
436        """
437        newargs = []
438        for a in self.args:
439            if isinstance(a, GeometryEntity):
440                newargs.append(a.rotate(angle, pt))
441            else:
442                newargs.append(a)
443        return type(self)(*newargs)
444
445    def scale(self, x=1, y=1, pt=None):
446        """Scale the object by multiplying the x,y-coordinates by x and y.
447
448        If pt is given, the scaling is done relative to that point; the
449        object is shifted by -pt, scaled, and shifted by pt.
450
451        See Also
452        ========
453
454        rotate, translate
455
456        Examples
457        ========
458
459        >>> from sympy import RegularPolygon, Point, Polygon
460        >>> t = Polygon(*RegularPolygon(Point(0, 0), 1, 3).vertices)
461        >>> t
462        Triangle(Point2D(1, 0), Point2D(-1/2, sqrt(3)/2), Point2D(-1/2, -sqrt(3)/2))
463        >>> t.scale(2)
464        Triangle(Point2D(2, 0), Point2D(-1, sqrt(3)/2), Point2D(-1, -sqrt(3)/2))
465        >>> t.scale(2, 2)
466        Triangle(Point2D(2, 0), Point2D(-1, sqrt(3)), Point2D(-1, -sqrt(3)))
467
468        """
469        from sympy.geometry.point import Point
470        if pt:
471            pt = Point(pt, dim=2)
472            return self.translate(*(-pt).args).scale(x, y).translate(*pt.args)
473        return type(self)(*[a.scale(x, y) for a in self.args])  # if this fails, override this class
474
475    def translate(self, x=0, y=0):
476        """Shift the object by adding to the x,y-coordinates the values x and y.
477
478        See Also
479        ========
480
481        rotate, scale
482
483        Examples
484        ========
485
486        >>> from sympy import RegularPolygon, Point, Polygon
487        >>> t = Polygon(*RegularPolygon(Point(0, 0), 1, 3).vertices)
488        >>> t
489        Triangle(Point2D(1, 0), Point2D(-1/2, sqrt(3)/2), Point2D(-1/2, -sqrt(3)/2))
490        >>> t.translate(2)
491        Triangle(Point2D(3, 0), Point2D(3/2, sqrt(3)/2), Point2D(3/2, -sqrt(3)/2))
492        >>> t.translate(2, 2)
493        Triangle(Point2D(3, 2), Point2D(3/2, sqrt(3)/2 + 2), Point2D(3/2, 2 - sqrt(3)/2))
494
495        """
496        newargs = []
497        for a in self.args:
498            if isinstance(a, GeometryEntity):
499                newargs.append(a.translate(x, y))
500            else:
501                newargs.append(a)
502        return self.func(*newargs)
503
504    def parameter_value(self, other, t):
505        """Return the parameter corresponding to the given point.
506        Evaluating an arbitrary point of the entity at this parameter
507        value will return the given point.
508
509        Examples
510        ========
511
512        >>> from sympy import Line, Point
513        >>> from sympy.abc import t
514        >>> a = Point(0, 0)
515        >>> b = Point(2, 2)
516        >>> Line(a, b).parameter_value((1, 1), t)
517        {t: 1/2}
518        >>> Line(a, b).arbitrary_point(t).subs(_)
519        Point2D(1, 1)
520        """
521        from sympy.geometry.point import Point
522        from sympy.core.symbol import Dummy
523        from sympy.solvers.solvers import solve
524        if not isinstance(other, GeometryEntity):
525            other = Point(other, dim=self.ambient_dimension)
526        if not isinstance(other, Point):
527            raise ValueError("other must be a point")
528        T = Dummy('t', real=True)
529        sol = solve(self.arbitrary_point(T) - other, T, dict=True)
530        if not sol:
531            raise ValueError("Given point is not on %s" % func_name(self))
532        return {t: sol[0][T]}
533
534
535class GeometrySet(GeometryEntity, Set):
536    """Parent class of all GeometryEntity that are also Sets
537    (compatible with sympy.sets)
538    """
539    def _contains(self, other):
540        """sympy.sets uses the _contains method, so include it for compatibility."""
541
542        if isinstance(other, Set) and other.is_FiniteSet:
543            return all(self.__contains__(i) for i in other)
544
545        return self.__contains__(other)
546
547@dispatch(GeometrySet, Set)  # type:ignore # noqa:F811
548def union_sets(self, o): # noqa:F811
549    """ Returns the union of self and o
550    for use with sympy.sets.Set, if possible. """
551
552    from sympy.sets import Union, FiniteSet
553
554    # if its a FiniteSet, merge any points
555    # we contain and return a union with the rest
556    if o.is_FiniteSet:
557        other_points = [p for p in o if not self._contains(p)]
558        if len(other_points) == len(o):
559            return None
560        return Union(self, FiniteSet(*other_points))
561    if self._contains(o):
562        return self
563    return None
564
565
566@dispatch(GeometrySet, Set)  # type: ignore # noqa:F811
567def intersection_sets(self, o): # noqa:F811
568    """ Returns a sympy.sets.Set of intersection objects,
569    if possible. """
570
571    from sympy.sets import FiniteSet, Union
572    from sympy.geometry import Point
573
574    try:
575        # if o is a FiniteSet, find the intersection directly
576        # to avoid infinite recursion
577        if o.is_FiniteSet:
578            inter = FiniteSet(*(p for p in o if self.contains(p)))
579        else:
580            inter = self.intersection(o)
581    except NotImplementedError:
582        # sympy.sets.Set.reduce expects None if an object
583        # doesn't know how to simplify
584        return None
585
586    # put the points in a FiniteSet
587    points = FiniteSet(*[p for p in inter if isinstance(p, Point)])
588    non_points = [p for p in inter if not isinstance(p, Point)]
589
590    return Union(*(non_points + [points]))
591
592def translate(x, y):
593    """Return the matrix to translate a 2-D point by x and y."""
594    rv = eye(3)
595    rv[2, 0] = x
596    rv[2, 1] = y
597    return rv
598
599
600def scale(x, y, pt=None):
601    """Return the matrix to multiply a 2-D point's coordinates by x and y.
602
603    If pt is given, the scaling is done relative to that point."""
604    rv = eye(3)
605    rv[0, 0] = x
606    rv[1, 1] = y
607    if pt:
608        from sympy.geometry.point import Point
609        pt = Point(pt, dim=2)
610        tr1 = translate(*(-pt).args)
611        tr2 = translate(*pt.args)
612        return tr1*rv*tr2
613    return rv
614
615
616def rotate(th):
617    """Return the matrix to rotate a 2-D point about the origin by ``angle``.
618
619    The angle is measured in radians. To Point a point about a point other
620    then the origin, translate the Point, do the rotation, and
621    translate it back:
622
623    >>> from sympy.geometry.entity import rotate, translate
624    >>> from sympy import Point, pi
625    >>> rot_about_11 = translate(-1, -1)*rotate(pi/2)*translate(1, 1)
626    >>> Point(1, 1).transform(rot_about_11)
627    Point2D(1, 1)
628    >>> Point(0, 0).transform(rot_about_11)
629    Point2D(2, 0)
630    """
631    s = sin(th)
632    rv = eye(3)*cos(th)
633    rv[0, 1] = s
634    rv[1, 0] = -s
635    rv[2, 2] = 1
636    return rv
637