# -*- coding: utf-8 -*-
"""
Written by Daniel M. Aukes and CONTRIBUTORS
Email: danaukes<at>asu.edu.
Please see LICENSE for full license.
"""
#import shapely.geometry as sg
#from .shape import Base
#from . import shape
import shapely.geometry
import shapely.geometry as sg
import shapely.affinity as sa
from foldable_robotics.class_algebra import ClassAlgebra
import shapely.ops as so
import shapely.wkt as sw
import matplotlib.pyplot as plt
import numpy
import foldable_robotics
import foldable_robotics.jupyter_support as fj
[docs]class NoGeoms(Exception):
pass
[docs]def is_collection(item):
'''
determines whether the geometry defined by item contains multiple geometries
:param item: the shapely geometry
:type item: class from shapely.geometry
:rtype: boolean
'''
collections = [
shapely.geometry.MultiPolygon,
shapely.geometry.GeometryCollection,
shapely.geometry.MultiLineString,
shapely.geometry.MultiPoint]
iscollection = [isinstance(item, cls) for cls in collections]
return any(iscollection)
[docs]def flatten(geoms):
'''
eliminate any collections of geometries by flattening all into a single list.
:param geoms: the shapely geometries
:type geoms: list or iterable
:rtype: list
'''
geom = so.unary_union(geoms)
entities = extract_r(geom)
# entities = [item for item in entities if any([isinstance(item,classitem) for classitem in [shapely.geometry.Polygon,shapely.geometry.LineString,shapely.geometry.Point]])]
# entities = [item for item in entities if not item.is_empty]
return entities
[docs]def from_shapely_to_layer(new_geoms):
'''
convert from shapely geometry to Layer class
:param new_geoms: the shapely geometries
:type new_geoms: list or iterable
:rtype: Layer
'''
new_geoms = flatten(new_geoms)
new_layer = Layer(*new_geoms)
return new_layer
[docs]def from_layer_to_shapely(layer):
'''
convert from Layer class to shapely geometry
:param layer: the layer instance
:type layer: Layer
:rtype: class from shapely.geometry
'''
geoms = so.unary_union(layer.geoms)
return geoms
[docs]def plot_poly(poly,color = None,edgecolor = None, facecolor =None, linewidth = .25):
'''
plot a shapely geometry
:param poly: the layer instance
:type poly: class from shapely.geometry
:param edgecolor: tuple of r,g,b,a scalars from 0 to 1
:type edgecolor: tuple
:param facecolor: tuple of r,g,b,a scalars from 0 to 1
:type facecolor: tuple
:param linewidth: width of the line
:type linewidth: scalar or None for default
'''
color = color or (1,0,0,.25)
facecolor = facecolor or color
edgecolor = edgecolor or (0,0,0,.5)
import numpy
from matplotlib.patches import PathPatch
from matplotlib.path import Path
import matplotlib.pyplot as plt
axes = plt.gca()
vertices = []
codes = []
color = list(color)
if isinstance(poly,sg.Polygon):
exterior = list(poly.exterior.coords)
if poly.exterior.is_ccw:
exterior = exterior[::-1]
interiors = []
for interior in poly.interiors:
item = list(interior.coords)
if not interior.is_ccw:
item=item[::-1]
interiors.append(item)
for item in [exterior]+interiors:
vertices.extend(item+[(0,0)])
codes.extend([Path.MOVETO]+([Path.LINETO]*(len(item)-1))+[Path.CLOSEPOLY])
path = Path(vertices,codes)
patch = PathPatch(path,facecolor=facecolor,edgecolor=edgecolor, linewidth= linewidth)
axes.add_patch(patch)
elif isinstance(poly,sg.LineString):
exterior = numpy.array(poly.coords)
axes.plot(exterior[:,0],exterior[:,1],color=color[:3]+[.5])
plt.axis('equal')
[docs]def check_loop(loop):
'''
remove the last element of loop if it is the same as the first
:param loop: list of coordinates
:type loop: iterable of tuples
:rtype: iterable of tuples
'''
if loop[-1]==loop[0]:
return loop[:-1]
[docs]def triangulate_geom(geom):
'''
triangulate a shapely geometry
:param geom: the geometry to triangulate
:type geom: shapely.Polygon
:rtype: array of points, array of triangle indeces
'''
if isinstance(geom,sg.Polygon):
import pypoly2tri
from pypoly2tri.cdt import CDT
import numpy
exterior = list(geom.exterior.coords)
exterior = check_loop(exterior)
exterior2 = [pypoly2tri.shapes.Point(*item) for item in exterior]
cdt = CDT(exterior2)
interiors = []
for interior in geom.interiors:
interior= list(interior.coords)
interior = check_loop(interior)
interiors.append(interior)
for interior in interiors:
interior2 = [pypoly2tri.shapes.Point(*item) for item in interior]
cdt.AddHole(interior2)
cdt.Triangulate()
tris =cdt.GetTriangles()
points = cdt.GetPoints()
points2 = numpy.array([item.toTuple() for item in points])
tris2 = numpy.array([[points.index(point) for point in tri.points_] for tri in tris],dtype = int)
return points2,tris2
[docs]def points_2d_to_3d(points_2d,z_val):
'''
convert a list of 2d points to a list of 3d points
:param points_2d: the geometry to triangulate
:type points_2d: numpy.array
:param z_val: the padded z value
:type z_val: float
:rtype: numpy.array
'''
z = points_2d[:,0:1]*0+z_val
points3 = numpy.c_[points_2d,z]
return points3
[docs]def extrude(points,tris,z_lower,z_upper):
'''
create 3d tetrahedra from 2d triangles
:param points: array of 3d points
:type points: numpy.array
:param tris: the triangle coordinates
:type tris: numpy.array
:param z_lower: the lower z value
:type z_lower: float
:param z_upper: the upper z value
:type z_upper: float
:rtype: list of Tetrahedron
'''
from idealab_tools.geometry.triangle import Triangle
tris3 = [Triangle(*(points[tri])) for tri in tris]
tets = [tet for tri in tris3 for tet in tri.extrude(z_lower,z_upper)]
return tets
[docs]def inertia_tensor(about_point,density,z_lower,z_upper,points,tris):
'''
create 3d inertia tensor from a 2d list of triangles
:param about_point: the point about which to compute the inertia
:type about_point: 3d coordinate
:param density: the density of the material
:type density: float
:param z_lower: the lower z value
:type z_lower: float
:param z_upper: the upper z value
:type z_upper: float
:param points: array of 3d points
:type points: numpy.array
:param tris: the triangle coordinates
:type tris: numpy.array
:rtype: list of Tetrahedron
'''
import numpy
tets = extrude(points,tris,z_lower,z_upper)
Is = numpy.array([tet.I(density,about_point) for tet in tets])
I = Is.sum(0)
return I
[docs]class Layer(ClassAlgebra):
'''
The Layer class is essentially a list of 2d polygons which all exist on the same plane.
'''
def __init__(self, *geoms):
'''
create a new class instance
:param geoms: a list of shapely geometries contained by the layer
:type geoms: list of shapely.geometry classes
:rtype: Layer
'''
geoms = flatten(geoms)
self.geoms = geoms
self.id = id(self)
[docs] @classmethod
def new(cls,*geoms):
'''
create a new class instance
:param geoms: a list of shapely geometries contained by the layer
:type geoms: list of shapely.geometry classes
:rtype: Layer
'''
geoms = flatten(geoms)
new = cls(*geoms)
return new
[docs] def copy(self,identical = True):
'''
creates a copy of the instance
:param identical: whether to use the same id or not.
:type identical: boolean
:rtype: Layer
'''
new = type(self)(*[sw.loads(geom.to_wkt()) for geom in self.geoms])
if identical:
new.id = self.id
return new
[docs] def export_dict(self):
'''
converts the layer to a dict.
:rtype: dict
'''
d = {}
d['geoms'] = [item.to_wkt() for item in self.geoms]
d['id'] = self.id
return d
[docs] @classmethod
def import_dict(cls,d):
'''
converts a dict to a Layer instance
:param d: the laminate in dict form
:type d: dict
:rtype: Laminate
'''
new = cls(*[sw.loads(item) for item in d['geoms']])
new.id = d['id']
return new
[docs] def plot(self,*args,**kwargs):
'''
plots the layer using matplotlib.
:param new: whether to create a new figure
:type new: boolean
'''
if 'new' in kwargs:
new = kwargs.pop('new')
else:
new = False
if new:
plt.figure()
for geom in self.geoms:
plot_poly(geom,*args,**kwargs)
if len(self.geoms)>0:
d,e=self.bounding_box_coords()
ax = plt.gca()
ax.axis([d[0],e[0],d[1],e[1]])
def _repr_svg_(self):
"""SVG representation for iPython notebook"""
svg_top = '<svg xmlns="http://www.w3.org/2000/svg" ' \
'xmlns:xlink="http://www.w3.org/1999/xlink" '
if not self.geoms:
return svg_top + '/>'
else:
# Establish SVG canvas that will fit all the data + small space
(xmin, ymin), (xmax, ymax) = self.bounding_box_coords()
if xmin == xmax and ymin == ymax:
# This is a point; buffer using an arbitrary size
(xmin, ymin), (xmax, ymax) = self.buffer(1).bounds
else:
# Expand bounds by a fraction of the data ranges
expand = 0.04 # or 4%, same as R plots
widest_part = max([xmax - xmin, ymax - ymin])
expand_amount = widest_part * expand
xmin -= expand_amount
ymin -= expand_amount
xmax += expand_amount
ymax += expand_amount
dx = xmax - xmin
dy = ymax - ymin
width = min([max([100., dx]), 300])
height = min([max([100., dy]), 300])
try:
scale_factor = max([dx, dy]) / max([width, height])
except ZeroDivisionError:
scale_factor = 1.
view_box = "{} {} {} {}".format(xmin, ymin, dx, dy)
transform = "matrix(1,0,0,-1,0,{})".format(ymax + ymin)
return svg_top + ('width="{1}" height="{2}" viewBox="{0}" ' 'preserveAspectRatio="xMinYMin meet">' '<g transform="{3}">{4}</g></svg>').format(view_box, width, height, transform,self.svg(scale_factor))
[docs] def svg(self, scale_factor=1.):
'''
returns the svg
'''
fill_color=foldable_robotics.layer_fill_color
if not self.geoms:
return '<g />'
return '<g>' + \
''.join(g.svg(scale_factor, fill_color) for g in self.geoms) + '</g>'
[docs] def create_material_property(self,color=None):
from foldable_robotics.dynamics_info import MaterialProperty
width,height = self.get_dimensions()
l=max(width,height)
color = color or foldable_robotics.layer_fill_color
m = MaterialProperty('',color,l/100,0,0,0,0,0,0,0,0)
return m
[docs] def get_dimensions(self):
min1,max1 = self.bounding_box_coords()
min1=numpy.array(min1)
max1=numpy.array(max1)
width,height = max1-min1
return width, height
[docs] def binary_operation(self,other,function_name,*args,**kwargs):
'''
performs a binary operation between self and other.
:param function_name: the layer-based function to be performed
:type function_name: string
:param other: the layer-based function to be performed
:type other: Layer
:param args: tuple of arguments passed to subfunction
:type args: tuple
:param kwargs: keyword arguments passed to subfunction
:type kwargs: dict
:rtype: Layer
'''
a = from_layer_to_shapely(self)
b = from_layer_to_shapely(other)
function = getattr(a,function_name)
c = function(b,*args,**kwargs)
return from_shapely_to_layer(c)
[docs] def union(self,other):
'''
returns the union between self and other.
:param other: the other layer
:type other: Layer
:rtype: Layer
'''
return self.binary_operation(other,'union')
[docs] def difference(self,other):
'''
returns the difference between self and other.
:param other: the other layer
:type other: Layer
:rtype: Layer
'''
return self.binary_operation(other,'difference')
[docs] def symmetric_difference(self,other):
'''
returns the symmetric difference between self and other.
:param other: the other layer
:type other: Layer
:rtype: Layer
'''
return self.binary_operation(other,'symmetric_difference')
[docs] def intersection(self,other):
'''
returns the intersection between self and other.
:param other: the other layer
:type other: Layer
:rtype: Layer
'''
return self.binary_operation(other,'intersection')
[docs] def buffer(self,value,resolution = None):
'''
dilate (or erode) the geometries in the layer
:param value: the positive (or negative) radius of the dilation (or erosion)
:type value: float
:param resolution: the number of interpolanting vertices to use
:type resolution: int
:rtype: Layer
'''
resolution = resolution or foldable_robotics.resolution
return self.dilate(value,resolution)
[docs] def dilate(self,value,resolution = None):
'''
dilate the geometries in the layer
:param value: the radius of the dilation
:type value: float
:param resolution: the number of interpolanting vertices to use
:type resolution: int
:rtype: Layer
'''
resolution = resolution or foldable_robotics.resolution
geoms = from_layer_to_shapely(self)
new_geoms = (geoms.buffer(value,resolution))
return from_shapely_to_layer(new_geoms)
[docs] def erode(self,value,resolution = None):
'''
erode the geometries in the layer
:param value: the radius of the erotion
:type value: float
:param resolution: the number of interpolanting vertices to use
:type resolution: int
:rtype: Layer
'''
resolution = resolution or foldable_robotics.resolution
return self.dilate(-value,resolution)
[docs] def translate(self,xoff=0.0, yoff=0.0, zoff=0.0):
'''
translate the layer
:param xoff: the amount of x translation
:type xoff: float
:param yoff: the amount of y translation
:type yoff: float
:param zoff: the amount of z translation
:type zoff: float
:rtype: Layer
'''
geoms = from_layer_to_shapely(self)
new_geoms = sa.translate(geoms,xoff,yoff,zoff)
return from_shapely_to_layer(new_geoms)
[docs] def scale(self, xfact=1.0, yfact=1.0, zfact=1.0, origin=(0,0)):
'''
scale the layer
:param xfact: the amount of x scaling
:type xfact: float
:param yfact: the amount of y scaling
:type yfact: float
:param zfact: the amount of z scaling
:type zfact: float
:rtype: Layer
'''
geoms = from_layer_to_shapely(self)
new_geoms = sa.scale(geoms,xfact,yfact,zfact,origin)
return from_shapely_to_layer(new_geoms)
[docs] def rotate(self, angle, origin=(0,0), use_radians=False):
'''
rotate the layer
:param angle: the amount of rotation
:type angle: float
:param origin: the origin to use in calculating the rotation
:type origin: string or tuple
:param use_radians: whether to use radians or degrees
:type use_radians: boolean
:rtype: Layer
'''
geoms = from_layer_to_shapely(self)
new_geoms = sa.rotate(geoms,angle,origin,use_radians)
return from_shapely_to_layer(new_geoms)
[docs] def simplify(self,tolerance):
'''
simplify the layer by reducing the number of coordinates. uses shapely's simplify function.
:param tolerance: the length scale by which to simplify
:type tolerance: float
:rtype: Layer
'''
geoms = foldable_robotics.layer.from_layer_to_shapely(self)
new_geoms = (geoms.simplify(tolerance))
return foldable_robotics.layer.from_shapely_to_layer(new_geoms)
[docs] def export_dxf(self,name):
'''
export the layer to a dxf
:param name: the filename to write
:type name: string
'''
import ezdxf
dwg = ezdxf.new('R2010')
msp = dwg.modelspace()
# loops = self.exteriors()+self.interiors()
for loop in self.get_paths():
msp.add_lwpolyline(loop)
dwg.saveas(name+'.dxf')
[docs] def map_line_stretch(self,p1,p2,p3,p4):
'''
Transforms a layer or laminate by using the translation and rotation between two lines to compute the stretch, scale, and rotation.
:param self: input shape
:type self: Layer or Laminate
:param p1: point 1 of line 1 in (x,y) format
:type p1: tuple
:param p1: point 2 of line 1 in (x,y) format
:type p1: tuple
:param p1: point 1 of line 2 in (x,y) format
:type p1: tuple
:param p1: point 2 of line 2 in (x,y) format
:type p1: tuple
:rtype: Layer
'''
import foldable_robotics.manufacturing
return foldable_robotics.manufacturing.map_line_stretch(self,p1,p2,p3,p4)
[docs] def triangulation(self):
'''
triangulate a layer
:rtype: array of points, array of triangle indeces
'''
points = []
tris = []
ii = 0
for geom in self.geoms:
if isinstance(geom,sg.Polygon):
points2,tris2 = triangulate_geom(geom)
points.append(points2)
tris.append(tris2+ii)
ii+=len(points2)
points = numpy.vstack(points)
tris = numpy.vstack(tris)
return points,tris
[docs] def mesh_items_inner(self,z_offset = 0,color = (1,0,0,1)):
'''inner loop for meshing the layer'''
verts_outer = []
colors_outer = []
for geom in self.geoms:
if isinstance(geom,sg.Polygon):
points2,tris2 = triangulate_geom(geom)
points3 = points_2d_to_3d(points2,z_offset)
verts =points3[tris2]
verts_colors = [[color]*3]*len(tris2)
verts_outer.append(verts)
colors_outer.append(verts_colors)
verts_outer = numpy.vstack(verts_outer)
colors_outer = numpy.vstack(colors_outer)
return verts_outer,colors_outer
[docs] def mesh_items(self,z_offset = 0,color = (1,0,0,1)):
'''Return a pyqtgraph.opengl.GLMeshItem for the layer'''
import pyqtgraph.opengl as gl
verts_outer,colors_outer = self.mesh_items_inner(z_offset,color)
mi=gl.GLMeshItem(vertexes=verts_outer,vertexColors=colors_outer,smooth=False,shader='balloon',drawEdges=False)
return mi
[docs] def mass_props(self,material_property,bottom,top):
'''compute the mass properties of the layer'''
area_i = 0
mass_i=0
volume_i=0
centroid_x_i=0
centroid_y_i=0
centroid_z_i=0
for geom in self.geoms:
area_i = geom.area
volume_ii = geom.area*material_property.thickness
mass_ii = volume_ii*material_property.density
volume_i+=volume_ii
mass_i+=mass_ii
centroid = list(geom.centroid.coords)[0]
centroid_x_i += centroid[0]*mass_ii
centroid_y_i += centroid[1]*mass_ii
centroid_z_i += (bottom+top)/2*mass_ii
centroid_i = centroid_x_i/mass_i,centroid_y_i/mass_i,centroid_z_i/mass_i
return area_i,volume_i,mass_i,centroid_i
[docs] def inertia(self,about_point,z_lower,material_property):
'''compute the inertia tensor for the layer'''
I=numpy.zeros((3,3))
z_upper = z_lower+material_property.thickness
for geom in self.geoms:
points,tris = triangulate_geom(geom)
I+=inertia_tensor(about_point,material_property.density,z_lower,z_upper,points,tris)
return I
[docs] def bounding_box_coords(self):
'''compute the lower left hand and upper right coordinates for computing a bounding box of the layer'''
try:
a = numpy.array([vertex for path in self.get_paths() for vertex in path])
box = [tuple(a.min(0)),tuple(a.max(0))]
except ValueError as e:
print(e.args)
raise NoGeoms
return box
[docs] def bounding_box(self):
'''create a bounding box of the layer and return as a layer'''
a,b = self.bounding_box_coords()
p = sg.box(*a,*b)
l = Layer(p)
return l
[docs] def exteriors(self):
'''return the clockwise exterior coordinates of all closed shapes in the layer'''
paths = []
for geom in self.geoms:
if isinstance(geom,sg.Polygon):
exterior = list(geom.exterior.coords)
if geom.exterior.is_ccw:
exterior = exterior[::-1]
paths.extend([exterior])
return paths
[docs] def interiors(self):
'''return the interior coordinates of all shapes in the layer'''
paths = []
for geom in self.geoms:
if isinstance(geom,sg.Polygon):
interiors = [list(interior.coords) for interior in geom.interiors]
paths.extend(interiors)
return paths
[docs] def extrude(self,z_lower,material_property):
'''create tetrahedra from the layer and return as a set of points and tetrahedra indeces'''
z_upper = z_lower+material_property.thickness
points, tris =self.triangulation()
m = points.shape[0]
n = tris.shape[0]
points2 = numpy.r_[numpy.c_[points,[z_lower]*len(points)],numpy.c_[points,[z_upper]*len(points)]]
tris2 = numpy.r_[numpy.c_[tris[:,(0)]+m,tris[:,(1,0,2)]+m],numpy.c_[tris[:,(0,)]+m,tris[:,(1,)],tris[:,(2,)]+m,tris[:,(2,)]],numpy.c_[tris[:,(0,)]+m,tris[:,(1,)],tris[:,(1,2)]+m]]
return points2,tris2
[docs] def to_laminate(self,value):
'''
convert a layer to a laminate of "value" layers
:param self: the layer itself
:type self: shapely.layer.Layer
:rtype self: a layer
:param value: the number of layers in the laminate
:type self: integer
'''
from foldable_robotics.laminate import Laminate
laminate = Laminate(*([self]*value))
return laminate
[docs] def is_null(self):
return not self.geoms
[docs] def contains(self,*args):
geom = from_layer_to_shapely(self)
bools = [geom.contains(sg.Point(*item)) for item in args]
return bools
[docs] def get_segments(self):
'''
get the line segments of a layer or linestring
:param poly: the geometry
:type poly: shapely.geometry.Polygon or shapely.geometry.LineString
:rtype: list of two-coordinate segments
'''
all_segments = []
for geom in self.geoms:
if isinstance(geom,sg.Polygon):
exterior = list(geom.exterior.coords)
interiors = [list(interior.coords) for interior in geom.interiors]
loops = [exterior]+interiors
for loop in loops:
segments = list(zip(loop[:-1],loop[1:]))
all_segments.extend(segments)
elif isinstance(geom,sg.LineString):
line = list(geom.coords)
segments = list(zip(line[:-1],line[1:]))
all_segments.extend(segments)
return all_segments
[docs] def get_paths(self):
'''
get the inner and outer paths of a layer's geometry
:param self: the geometry
:type self: foldable_robotics.layer.Layer
:rtype: list of list of coordinate tuples
'''
paths = []
for geom in self.geoms:
if isinstance(geom,sg.Polygon):
exterior = list(geom.exterior.coords)
interiors = [list(interior.coords) for interior in geom.interiors]
paths.extend([exterior]+interiors)
elif isinstance(geom,sg.LineString):
line = list(geom.coords)
paths.extend([line])
return paths
[docs] @classmethod
def make_text(cls,text,*args,**kwargs):
'''
makes a layer of text
'''
import idealab_tools.text_to_polygons
p = idealab_tools.text_to_polygons.text_to_polygons(text,*args,**kwargs)
layers = [cls(sg.Polygon(item)) for item in p]
l = Layer()
for item in layers:
l ^= item
return l
[docs] def unary_union(self,*others):
a = from_layer_to_shapely(self)
b = [from_layer_to_shapely(other) for other in others]
c = so.unary_union([a]+b)
return from_shapely_to_layer(c)
[docs]def layer_representer(dumper, v):
'''function for representing layer as a dictionary for use by yaml'''
d = v.export_dict()
output = dumper.represent_mapping(u'!Layer',d)
return output
[docs]def layer_constructor(loader, node):
'''function for constructing layer from a dictionary for use by yaml'''
item = loader.construct_mapping(node)
new = Layer.import_dict(item)
return new
import yaml
yaml.add_representer(Layer, layer_representer)
yaml.add_constructor(u'!Layer', layer_constructor)