Page MenuHomec4science

path.py
No OneTemporary

File Metadata

Created
Mon, Jun 3, 08:40
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
""" This super class can be used to represent all svg path types.
"""
# Copyright (C) University of Basel 2019 {{{1
#
# This program 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
# (at your option) any later version.
#
# This program 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 this program. If not, see <https://www.gnu.org/licenses/> 1}}}
__author__ = "Christian Steiner"
__maintainer__ = __author__
__copyright__ = 'University of Basel'
__email__ = "christian.steiner@unibas.ch"
__status__ = "Development"
__license__ = "GPL v3"
__version__ = "0.0.1"
from lxml import etree as ET
import math
from os.path import isfile
from svgpathtools.parser import parse_path, parse_transform
from svgpathtools.path import transform, Line, Path as SVGPath
import sys
from .attachable_object import AttachableObject
sys.path.append('py2ttl')
from class_spec import SemanticClass
class Path(AttachableObject,SemanticClass):
"""
This super class represents all types of svg paths.
Args:
node (lxml.etree.Element) node, containing information
path (svgpathtools.path.Path) svg path representation.
"""
XML_TAG = 'path'
WORD_DELETION_PATH_TAG = 'word-deletion-path'
BOX_TAG = 'box-path'
def __init__(self, id=0, node=None, path=None, parent_path=None, d_string=None, style_class='', tag=XML_TAG):
self.intKeys = [ 'id' ]
self.stringKeys = [ 'style_class' ]
self.floatKeys = []
self.start_line_number = -1
self.parent_path = parent_path
if node is not None:
self.id = int(node.get('id')) if bool(node.get('id')) else 0
self.path = parse_path(node.get('d')) if bool(node.get('d')) else None
self.d_attribute = node.get('d')
self.style_class = node.get('style-class')
self.tag = node.tag
else:
self.tag = tag
self.id = id
self.path = path
if self.path is None\
and d_string is not None\
and d_string != '':
self.path = parse_path(d_string)
self.d_attribute = self.path.d() if self.path is not None else ''
self.style_class = style_class
def attach_object_to_tree(self, target_tree):
"""Attach object to tree.
"""
if target_tree.__class__.__name__ == '_ElementTree':
target_tree = target_tree.getroot()
obj_node = target_tree.xpath('.//' + self.tag + '[@id="%s"]' % self.id)[0] \
if(len(target_tree.xpath('.//' + self.tag + '[@id="%s"]' % self.id)) > 0) \
else ET.SubElement(target_tree, self.tag)
for key in self.floatKeys:
if self.__dict__[key] is not None:
obj_node.set(key.replace('_','-'), str(round(self.__dict__[key], 3)))
for key in self.intKeys + self.stringKeys:
if self.__dict__[key] is not None:
obj_node.set(key.replace('_','-'), str(self.__dict__[key]))
if self.path is not None:
obj_node.set('d', self.path.d())
@classmethod
def create_cls(cls, id=0, path=None, style_class='', page=None, tag=XML_TAG, stroke_width=0.0):
"""Create and return a cls.
"""
if path is not None\
and path.start.imag <= path.end.imag\
and page is not None\
and style_class != ''\
and len(path._segments) == 1\
and type(path._segments[0]) == Line\
and ((style_class in page.style_dict.keys()\
and 'stroke-width' in page.style_dict[style_class].keys())\
or stroke_width > 0.0):
# If path is a Line and its style_class specifies a stroke-width, correct path
stroke_width_correction = float(page.style_dict[style_class]['stroke-width'])/2\
if stroke_width == 0.0\
else stroke_width
xmin = path.start.real
xmax = path.end.real
ymin = path.start.imag-stroke_width_correction
ymax = path.end.imag+stroke_width_correction
#path = parse_path(f'M {xmin}, {ymin} L {xmax}, {ymin} L {xmax}, {ymax} L {xmin}, {ymax} z')
path = SVGPath(Line(start=(complex(f'{xmin}+{ymin}j')), end=(complex(f'{xmax}+{ymin}j'))),\
Line(start=(complex(f'{xmax}+{ymin}j')), end=(complex(f'{xmax}+{ymax}j'))),\
Line(start=(complex(f'{xmax}+{ymax}j')), end=(complex(f'{xmin}+{ymax}j'))),\
Line(start=(complex(f'{xmin}+{ymax}j')), end=(complex(f'{xmin}+{ymin}j'))))
return cls(id=id, path=path, style_class=style_class, tag=tag)
def contains_path(self, other_path):
"""Returns true if other_path is contained in this path.
"""
this_xmin, this_xmax, this_ymin, this_ymax = self.path.bbox()
other_xmin, other_xmax, other_ymin, other_ymax = other_path.path.bbox()
return other_xmin >= this_xmin and other_xmax <= this_xmax\
and other_ymin >= this_ymin and other_ymax <= this_ymax
def contains_start_of_path(self, other_path):
"""Returns true if start of other_path is contained in this path.
"""
this_xmin, this_xmax, this_ymin, this_ymax = self.path.bbox()
other_xmin, other_xmax, other_ymin, other_ymax = other_path.path.bbox()
return other_xmin >= this_xmin and other_xmin < this_xmax\
and other_ymin >= this_ymin and other_ymax <= this_ymax
def contains_end_of_path(self, other_path):
"""Returns true if end of other_path is contained in this path.
"""
this_xmin, this_xmax, this_ymin, this_ymax = self.path.bbox()
other_xmin, other_xmax, other_ymin, other_ymax = other_path.path.bbox()
return other_xmax >= this_xmin and other_xmax < this_xmax\
and other_ymin >= this_ymin and other_ymax <= this_ymax
@classmethod
def create_path_from_transkription_position(cls, transkription_position, tr_xmin=0.0, tr_ymin=0.0, include_pwps=True):
"""Create a .path.Path from a .transkription_position.TranskriptionPosition.
"""
if include_pwps and len(transkription_position.positional_word_parts) > 0:
first_pwp = transkription_position.positional_word_parts[0]
last_pwp = transkription_position.positional_word_parts[-1]
xmin = tr_xmin + first_pwp.left
xmax = tr_xmin + last_pwp.left + last_pwp.width
ymin = tr_ymin + sorted(pwp.top for pwp in transkription_position.positional_word_parts)[0]
ymax = tr_ymin + sorted([pwp.bottom for pwp in transkription_position.positional_word_parts], reverse=True)[0]
else:
xmin = tr_xmin + transkription_position.left
xmax = xmin + transkription_position.width
ymin = tr_ymin + transkription_position.top
ymax = ymin + transkription_position.height
word_path = parse_path('M {}, {} L {}, {} L {}, {} L {}, {} z'.format(xmin, ymin, xmax, ymin, xmax, ymax, xmin, ymax))
if transkription_position.transform is not None:
word_path = transform(parse_path('M {}, {} L {}, {} L {}, {} L {}, {} z'.format(xmin, ymin, xmax, ymin, xmax, ymax, xmin, ymax)),\
parse_transform(transkription_position.transform.toString()))
return cls(path=word_path)
def do_paths_intersect(self, other_path):
"""Returns true if paths intersect, false if not or if there was an exception.
"""
try:
return self.path.intersect(other_path.path, justonemode=True)\
or self.is_partially_contained_by(other_path)
except AssertionError:
return False
def get_median_y(self, tr_ymin=0.0):
"""Return the median of ymin + ymax.
"""
return (self.path.bbox()[2] + self.path.bbox()[3])/2 - tr_ymin
def get_x(self, tr_xmin=0.0):
"""Return xmin.
"""
return self.path.bbox()[0] - tr_xmin
@classmethod
def get_semantic_dictionary(cls):
""" Creates and returns a semantic dictionary as specified by SemanticClass.
"""
dictionary = {}
class_dict = cls.get_class_dictionary()
properties = {'d_attribute': { 'class': str, 'cardinality': 0,\
'name': 'hasDAttribute', 'label': 'svg path has d attribute',\
'comment': 'The d attribute defines a path to be drawn.'}}
#properties.update(cls.create_semantic_property_dictionary('style_class', str))
dictionary.update({cls.CLASS_KEY: class_dict})
dictionary.update({cls.PROPERTIES_KEY: properties})
return cls.return_dictionary_after_updating_super_classes(dictionary)
def is_partially_contained_by(self, other_path):
"""Returns true if other_path containes this path partially.
"""
return other_path.contains_start_of_path(self) or other_path.contains_end_of_path(self)
def _get_triangle_height(self, x, y, is_min=True):
"""Return height of triangle self.xmin,y[min|max], self,xmax,y[min|max], x,y
where [min|max] depends on is_min.
"""
path_y = self.path.bbox()[2]\
if is_min\
else self.path.bbox()[3]
path_xmin = self.path.bbox()[0]
path_xmax = self.path.bbox()[1]
if abs(path_xmax-path_xmin) < 10:
path_x = (path_xmin+path_xmax)/2
return parse_path(f'M {x},{y} L {path_x},{path_y}').length()
a = parse_path(f'M {x},{y} L {path_xmax},{path_y}').length()
b = parse_path(f'M {x},{y} L {path_xmin},{path_y}').length()
c = parse_path(f'M {path_xmin},{path_y} L {path_xmax},{path_y}').length()
s = (1/2)*(a+b+c)
return (2/c)*math.sqrt(s*(s-a)*(s-b)*(s-c))
def get_distance_to_point(self, x, y, near_offset=100) ->float:
"""Returns thei height of ymin-triangle (self.xmin,ymin, self.xmax,ymin, x,y)
or of ymax-triangle (self.xmin,ymax, self.xmax,ymax, x,y), whatever is closer to point.
"""
minHeight = self._get_triangle_height(x, y, is_min=True)
maxHeight = self._get_triangle_height(x, y, is_min=False)
return minHeight if minHeight <= maxHeight else maxHeight
@staticmethod
def get_nearest_paths(list_of_paths, x, y, near_offset=50) ->list:
"""Return a list of paths that are near this point (x, y), sort by distance.
"""
close_paths = []
for path in list_of_paths:
distance = path.get_distance_to_point(x, y)
if distance <= near_offset:
close_paths.append((distance, path))
return [ path_tuple[1] for path_tuple in sorted(close_paths, key=lambda dPath: dPath[0]) ]
@staticmethod
def is_path_contained(list_of_paths, path) ->bool:
"""Return whether a path is contained in a list of paths
"""
return len([ p for p in list_of_paths if p.d_attribute == path.d_attribute ]) > 0

Event Timeline