# ****************************************************************************
# This file is part of sage-flatsurf.
#
# Copyright (C) 2016-2019 Vincent Delecroix
# 2016-2018 W. Patrick Hooper
# 2022-2023 Julian Rüth
#
# sage-flatsurf 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 2 of the License, or
# (at your option) any later version.
#
# sage-flatsurf 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 sage-flatsurf. If not, see <http://www.gnu.org/licenses/>.
# ****************************************************************************
from sage.rings.real_double import RDF
from sage.modules.free_module import VectorSpace
from sage.plot.polygon import polygon2d
from sage.plot.text import text
from sage.plot.line import line2d
from sage.plot.point import point2d
from flatsurf.geometry.similarity import SimilarityGroup
V = VectorSpace(RDF, 2)
[docs]
class GraphicalPolygon:
r"""
Stores data necessary to draw one of the polygons from a surface.
Note that this involves converting between geometric coordinates, defined for the SimilaritySurface,
and graphical coordinates. We do this with a similarity (called transformation below).
"""
def __init__(self, polygon, transformation=None):
r"""
INPUT:
- ``polygon`` -- the actual polygon
- ``transformation`` -- a transformation to be applied to the polygon
- ``outline_color`` -- a color
- ``fill_color`` -- another color
- ``label`` -- an optional label for the polygon
- ``edge_labels`` -- one of ``False``, ``True`` or a list of labels
"""
self._p = polygon
# the following stores _transformation and _v
self.set_transformation(transformation)
[docs]
def copy(self):
r"""
Return a copy of this GraphicalPolygon.
"""
return GraphicalPolygon(self._p, self.transformation())
def __repr__(self):
r"""
String representation.
EXAMPLES::
sage: from flatsurf import similarity_surfaces
sage: s = similarity_surfaces.example()
sage: gs = s.graphical_surface()
sage: gs.graphical_polygon(0)
GraphicalPolygon with vertices [(0.0, 0.0), (2.0, -2.0), (2.0, 0.0)]
"""
return "GraphicalPolygon with vertices {}".format(self._v)
[docs]
def base_polygon(self):
r"""
Return the polygon of the surface in geometric coordinates.
"""
return self._p
[docs]
def xmin(self):
r"""
Return the minimal x-coordinate of a vertex.
.. TODO::
to fit with Sage conventions this should be xmin
"""
return min([v[0] for v in self._v])
[docs]
def ymin(self):
r"""
Return the minimal y-coordinate of a vertex.
.. TODO::
to fit with Sage conventions this should be ymin
"""
return min([v[1] for v in self._v])
[docs]
def xmax(self):
r"""
Return the maximal x-coordinate of a vertex.
.. TODO::
to fit with Sage conventions this should be xmax
"""
return max([v[0] for v in self._v])
[docs]
def ymax(self):
r"""
Return the minimal y-coordinate of a vertex
.. TODO::
To fit with Sage conventions this should be ymax
"""
return max([v[1] for v in self._v])
[docs]
def bounding_box(self):
r"""
Return the quadruple (x1,y1,x2,y2) where x1 and y1 are the minimal
x and y coordinates and x2 and y2 are the maximal x and y coordinates.
"""
return self.xmin(), self.ymin(), self.xmax(), self.ymax()
[docs]
def contains(self, point):
r"""
Return the transformation of point from graphical coordinates to the geometric coordinates
of the underlying SimilaritySurface.
"""
return self._p.contains_point(self.transform_back(point))
[docs]
def plot_polygon(self, **options):
r"""
Returns only the filled polygon.
Options are processed as in sage.plot.polygon.polygon2d except
that by default axes=False.
"""
if "axes" not in options:
options["axes"] = False
return polygon2d(self._v, **options)
[docs]
def plot_label(self, label, **options):
r"""
Write the label of the polygon as text.
Set ``position`` to a pair (x,y) to determine where
the label is drawn (in graphical coordinates). If this parameter
is not provided, the label is positioned in the baricenter
of the polygon.
Other options are processed as in sage.plot.text.text.
"""
if "position" in options:
return text(str(label), options.pop("position"), **options)
else:
return text(str(label), sum(self._v) / len(self._v), **options)
[docs]
def plot_edge(self, e, **options):
r"""
Plot the edge e, with e a number 0,...,n-1 with n being the number
of edges of the polygon.
Options are processed as in sage.plot.line.line2d.
"""
return line2d(
[self._v[e], self._v[(e + 1) % len(self.base_polygon().vertices())]],
**options,
)
[docs]
def plot_edge_label(self, i, label, **options):
r"""
Write label on the i-th edge.
A parameter ``t`` in the interval [0,1] can be provided to position the
label along the edge. A value of t=0 will position it at the starting
vertex and t=1 will position it at the terminating vertex. Defaults to
0.3.
If the parameter ``position`` can take the values "outside", "inside"
or "edge" to indicate if the label should be drawn outside the polygon,
inside the polygon or on the edge. Defaults to "inside".
A ``push_off`` perturbation parameter controls how far off the edge the label is pushed.
Other options are processed as in sage.plot.text.text.
"""
e = self._v[(i + 1) % len(self.base_polygon().vertices())] - self._v[i]
if "position" in options:
if options["position"] not in ["inside", "outside", "edge"]:
raise ValueError(
"The 'position' parameter must take the value 'inside', 'outside', or 'edge'."
)
pos = options.pop("position")
else:
pos = "inside"
if pos == "outside":
# position outside polygon.
if "horizontal_alignment" in options:
pass
elif e[1] > 0:
options["horizontal_alignment"] = "left"
elif e[1] < 0:
options["horizontal_alignment"] = "right"
else:
options["horizontal_alignment"] = "center"
if "vertical_alignment" in options:
pass
elif e[0] > 0:
options["vertical_alignment"] = "top"
elif e[0] < 0:
options["vertical_alignment"] = "bottom"
else:
options["vertical_alignment"] = "center"
elif pos == "inside":
# position inside polygon.
if "horizontal_alignment" in options:
pass
elif e[1] < 0:
options["horizontal_alignment"] = "left"
elif e[1] > 0:
options["horizontal_alignment"] = "right"
else:
options["horizontal_alignment"] = "center"
if "vertical_alignment" in options:
pass
elif e[0] < 0:
options["vertical_alignment"] = "top"
elif e[0] > 0:
options["vertical_alignment"] = "bottom"
else:
options["vertical_alignment"] = "center"
else:
# centered on edge.
if "horizontal_alignment" in options:
pass
else:
options["horizontal_alignment"] = "center"
if "vertical_alignment" in options:
pass
else:
options["vertical_alignment"] = "center"
if "t" in options:
t = RDF(options.pop("t"))
else:
t = 0.3
if "push_off" in options:
push_off = RDF(options.pop("push_off"))
else:
push_off = 0.03
if pos == "outside":
push_off = -push_off
# Now push_off stores the amount it should be pushed into the polygon
no = V((-e[1], e[0]))
return text(label, self._v[i] + t * e + push_off * no, **options)
[docs]
def plot_zero_flag(self, **options):
r"""
Draw a line segment from the zero vertex toward the baricenter.
A real parameter ``t`` can be provided. If t=1, then the segment will
go all the way to the baricenter. The value of ``t`` is linear in the
length of the segment. Defaults to t=0.5.
Other options are processed as in sage.plot.line.line2d.
"""
if "t" in options:
t = RDF(options.pop("t"))
else:
t = 0.5
return line2d(
[self._v[0], self._v[0] + t * (sum(self._v) / len(self._v) - self._v[0])],
**options,
)
[docs]
def plot_points(self, points, **options):
r"""
Plot the points in the given collection of points.
The options are passed to point2d.
If no "zorder" option is provided then we set "zorder" to 50.
By default coordinates are taken in the underlying surface. Call with coordinates="graphical"
to use graphical coordinates instead.
"""
if "zorder" not in options:
options["zorder"] = 50
if "coordinates" not in options:
points2 = [self.transform(point) for point in points]
elif options["coordinates"] == "graphical":
points2 = [V(point) for point in points]
del options["coordinates"]
else:
raise ValueError("Invalid value of 'coordinates' option")
return point2d(points=points2, **options)