Page Menu
Home
c4science
Search
Configure Global Search
Log In
Files
F65376036
path.py
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Subscribers
None
File Metadata
Details
File Info
Storage
Attached
Created
Mon, Jun 3, 08:40
Size
11 KB
Mime Type
text/x-python
Expires
Wed, Jun 5, 08:40 (2 d)
Engine
blob
Format
Raw Data
Handle
18058582
Attached To
rNIETZSCHEPYTHON nietzsche-python
path.py
View Options
#!/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
Log In to Comment