inkstitch/lib/stitches/ConnectAndSamplePattern.py

772 wiersze
30 KiB
Python

from shapely.geometry.polygon import LineString, LinearRing
from shapely.geometry import Point, MultiPoint
from shapely.ops import nearest_points
from collections import namedtuple
from depq import DEPQ
import math
from ..stitches import LineStringSampling
from ..stitches import PointTransfer
from ..stitches import constants
nearest_neighbor_tuple = namedtuple(
"nearest_neighbor_tuple",
[
"nearest_point_parent",
"nearest_point_child",
"proj_distance_parent",
"child_node",
],
)
def cut(line, distance):
"""
Cuts a closed line so that the new closed line starts at the
point with "distance" to the beginning of the old line.
"""
if distance <= 0.0 or distance >= line.length:
return [LineString(line)]
coords = list(line.coords)
for i, p in enumerate(coords):
if i > 0 and p == coords[0]:
pd = line.length
else:
pd = line.project(Point(p))
if pd == distance:
if coords[0] == coords[-1]:
return LineString(coords[i:] + coords[1: i + 1])
else:
return LineString(coords[i:] + coords[:i])
if pd > distance:
cp = line.interpolate(distance)
if coords[0] == coords[-1]:
return LineString(
[(cp.x, cp.y)] + coords[i:] + coords[1:i] + [(cp.x, cp.y)]
)
else:
return LineString([(cp.x, cp.y)] + coords[i:] + coords[:i])
def connect_raster_tree_nearest_neighbor(
tree, used_offset, stitch_distance, close_point, offset_by_half
):
"""
Takes the offsetted curves organized as tree, connects and samples them.
Strategy: A connection from parent to child is made where both curves
come closest together.
Input:
-tree: contains the offsetted curves in a hierachical organized
data structure.
-used_offset: used offset when the offsetted curves were generated
-stitch_distance: maximum allowed distance between two points
after sampling
-close_point: defines the beginning point for stitching
(stitching starts always from the undisplaced curve)
-offset_by_half: If true the resulting points are interlaced otherwise not.
Returnvalues:
-All offsetted curves connected to one line and sampled with
points obeying stitch_distance and offset_by_half
-Tag (origin) of each point to analyze why a point was
placed at this position
"""
current_coords = tree.val
abs_offset = abs(used_offset)
result_coords = []
result_coords_origin = []
# We cut the current item so that its index 0 is closest to close_point
start_distance = tree.val.project(close_point)
if start_distance > 0:
current_coords = cut(current_coords, start_distance)
tree.val = current_coords
if not tree.transferred_point_priority_deque.is_empty():
new_DEPQ = DEPQ(iterable=None, maxlen=None)
for item, priority in tree.transferred_point_priority_deque:
new_DEPQ.insert(
item,
math.fmod(
priority - start_distance + current_coords.length,
current_coords.length,
),
)
tree.transferred_point_priority_deque = new_DEPQ
stitching_direction = 1
# This list should contain a tuple of nearest points between
# the current geometry and the subgeometry, the projected
# distance along the current geometry, and the belonging subtree node
nearest_points_list = []
for subnode in tree.children:
point_parent, point_child = nearest_points(current_coords, subnode.val)
proj_distance = current_coords.project(point_parent)
nearest_points_list.append(
nearest_neighbor_tuple(
nearest_point_parent=point_parent,
nearest_point_child=point_child,
proj_distance_parent=proj_distance,
child_node=subnode,
)
)
nearest_points_list.sort(
reverse=False, key=lambda tup: tup.proj_distance_parent)
if nearest_points_list:
start_distance = min(
abs_offset * constants.factor_offset_starting_points,
nearest_points_list[0].proj_distance_parent,
)
end_distance = max(
current_coords.length
- abs_offset * constants.factor_offset_starting_points,
nearest_points_list[-1].proj_distance_parent,
)
else:
start_distance = abs_offset * constants.factor_offset_starting_points
end_distance = (
current_coords.length - abs_offset * constants.factor_offset_starting_points
)
(
own_coords,
own_coords_origin,
) = LineStringSampling.raster_line_string_with_priority_points(
current_coords,
start_distance, # We add/subtract an offset to not sample
# the same point again (avoid double
# points for start and end)
end_distance,
stitch_distance,
stitching_direction,
tree.transferred_point_priority_deque,
abs_offset,
)
assert len(own_coords) == len(own_coords_origin)
own_coords_origin[0] = LineStringSampling.PointSource.ENTER_LEAVING_POINT
own_coords_origin[-1] = LineStringSampling.PointSource.ENTER_LEAVING_POINT
tree.stitching_direction = stitching_direction
tree.already_rastered = True
# Next we need to transfer our rastered points to siblings and childs
to_transfer_point_list = []
to_transfer_point_list_origin = []
for k in range(1, len(own_coords) - 1):
# Do not take the first and the last since they are ENTER_LEAVING_POINT
# points for sure
if (
not offset_by_half
and own_coords_origin[k] == LineStringSampling.PointSource.EDGE_NEEDED
):
continue
if (
own_coords_origin[k] == LineStringSampling.PointSource.ENTER_LEAVING_POINT
or own_coords_origin[k] == LineStringSampling.PointSource.FORBIDDEN_POINT
):
continue
to_transfer_point_list.append(Point(own_coords[k]))
point_origin = own_coords_origin[k]
to_transfer_point_list_origin.append(point_origin)
# Since the projection is only in ccw direction towards inner we need
# to use "-used_offset" for stitching_direction==-1
PointTransfer.transfer_points_to_surrounding(
tree,
stitching_direction * used_offset,
offset_by_half,
to_transfer_point_list,
to_transfer_point_list_origin,
overnext_neighbor=False,
transfer_forbidden_points=False,
transfer_to_parent=False,
transfer_to_sibling=True,
transfer_to_child=True,
)
# We transfer also to the overnext child to get a more straight
# arrangement of points perpendicular to the stitching lines
if offset_by_half:
PointTransfer.transfer_points_to_surrounding(
tree,
stitching_direction * used_offset,
False,
to_transfer_point_list,
to_transfer_point_list_origin,
overnext_neighbor=True,
transfer_forbidden_points=False,
transfer_to_parent=False,
transfer_to_sibling=True,
transfer_to_child=True,
)
if not nearest_points_list:
# If there is no child (inner geometry) we can simply take
# our own rastered coords as result
result_coords = own_coords
result_coords_origin = own_coords_origin
else:
# There are childs so we need to merge their coordinates +
# with our own rastered coords
# To create a closed ring
own_coords.append(own_coords[0])
own_coords_origin.append(own_coords_origin[0])
# own_coords does not start with current_coords but has an offset
# (see call of raster_line_string_with_priority_points)
total_distance = start_distance
cur_item = 0
result_coords = [own_coords[0]]
result_coords_origin = [
LineStringSampling.PointSource.ENTER_LEAVING_POINT]
for i in range(1, len(own_coords)):
next_distance = math.sqrt(
(own_coords[i][0] - own_coords[i - 1][0]) ** 2
+ (own_coords[i][1] - own_coords[i - 1][1]) ** 2
)
while (
cur_item < len(nearest_points_list)
and total_distance + next_distance + constants.eps
> nearest_points_list[cur_item].proj_distance_parent
):
item = nearest_points_list[cur_item]
(
child_coords,
child_coords_origin,
) = connect_raster_tree_nearest_neighbor(
item.child_node,
used_offset,
stitch_distance,
item.nearest_point_child,
offset_by_half,
)
d = item.nearest_point_parent.distance(
Point(own_coords[i - 1]))
if d > abs_offset * constants.factor_offset_starting_points:
result_coords.append(item.nearest_point_parent.coords[0])
result_coords_origin.append(
LineStringSampling.PointSource.ENTER_LEAVING_POINT
)
# reversing avoids crossing when entering and
# leaving the child segment
result_coords.extend(child_coords[::-1])
result_coords_origin.extend(child_coords_origin[::-1])
# And here we calculate the point for the leaving
d = item.nearest_point_parent.distance(Point(own_coords[i]))
if cur_item < len(nearest_points_list) - 1:
d = min(
d,
abs(
nearest_points_list[cur_item +
1].proj_distance_parent
- item.proj_distance_parent
),
)
if d > abs_offset * constants.factor_offset_starting_points:
result_coords.append(
current_coords.interpolate(
item.proj_distance_parent
+ abs_offset * constants.factor_offset_starting_points
).coords[0]
)
result_coords_origin.append(
LineStringSampling.PointSource.ENTER_LEAVING_POINT
)
cur_item += 1
if i < len(own_coords) - 1:
if (
Point(result_coords[-1]).distance(Point(own_coords[i]))
> abs_offset * constants.factor_offset_remove_points
):
result_coords.append(own_coords[i])
result_coords_origin.append(own_coords_origin[i])
# Since current_coords and temp are rastered differently
# there accumulate errors regarding the current distance.
# Since a projection of each point in temp would be very time
# consuming we project only every n-th point which resets
# the accumulated error every n-th point.
if i % 20 == 0:
total_distance = current_coords.project(Point(own_coords[i]))
else:
total_distance += next_distance
assert len(result_coords) == len(result_coords_origin)
return result_coords, result_coords_origin
def get_nearest_points_closer_than_thresh(travel_line, next_line, thresh):
"""
Takes a line and calculates the nearest distance along this
line to enter the next_line
Input:
-travel_line: The "parent" line for which the distance should
be minimized to enter next_line
-next_line: contains the next_line which need to be entered
-thresh: The distance between travel_line and next_line needs
to below thresh to be a valid point for entering
Output:
-tuple - the tuple structure is:
(nearest point in travel_line, nearest point in next_line)
"""
point_list = list(MultiPoint(travel_line.coords))
if point_list[0].distance(next_line) < thresh:
return nearest_points(point_list[0], next_line)
for i in range(len(point_list) - 1):
line_segment = LineString([point_list[i], point_list[i + 1]])
result = nearest_points(line_segment, next_line)
if result[0].distance(result[1]) < thresh:
return result
line_segment = LineString([point_list[-1], point_list[0]])
result = nearest_points(line_segment, next_line)
if result[0].distance(result[1]) < thresh:
return result
else:
return None
def create_nearest_points_list(
travel_line, children_list, threshold, threshold_hard, preferred_direction=0
):
"""
Takes a line and calculates the nearest distance along this line to
enter the childs in children_list
The method calculates the distances along the line and along the
reversed line to find the best direction which minimizes the overall
distance for all childs.
Input:
-travel_line: The "parent" line for which the distance should
be minimized to enter the childs
-children_list: contains the childs of travel_line which need to be entered
-threshold: The distance between travel_line and a child needs to be
below threshold to be a valid point for entering
-preferred_direction: Put a bias on the desired travel direction along
travel_line. If equals zero no bias is applied.
preferred_direction=1 means we prefer the direction of travel_line;
preferred_direction=-1 means we prefer the opposite direction.
Output:
-stitching direction for travel_line
-list of tuples (one tuple per child). The tuple structure is:
((nearest point in travel_line, nearest point in child),
distance along travel_line, belonging child)
"""
result_list_in_order = []
result_list_reversed_order = []
travel_line_reversed = LinearRing(travel_line.coords[::-1])
weight_in_order = 0
weight_reversed_order = 0
for child in children_list:
result = get_nearest_points_closer_than_thresh(
travel_line, child.val, threshold
)
if result is None:
# where holes meet outer borders a distance
# up to 2*used offset can arise
result = get_nearest_points_closer_than_thresh(
travel_line, child.val, threshold_hard
)
assert result is not None
proj = travel_line.project(result[0])
weight_in_order += proj
result_list_in_order.append(
nearest_neighbor_tuple(
nearest_point_parent=result[0],
nearest_point_child=result[1],
proj_distance_parent=proj,
child_node=child,
)
)
result = get_nearest_points_closer_than_thresh(
travel_line_reversed, child.val, threshold
)
if result is None:
# where holes meet outer borders a distance
# up to 2*used offset can arise
result = get_nearest_points_closer_than_thresh(
travel_line_reversed, child.val, threshold_hard
)
assert result is not None
proj = travel_line_reversed.project(result[0])
weight_reversed_order += proj
result_list_reversed_order.append(
nearest_neighbor_tuple(
nearest_point_parent=result[0],
nearest_point_child=result[1],
proj_distance_parent=proj,
child_node=child,
)
)
if preferred_direction == 1:
# Reduce weight_in_order to make in order stitching more preferred
weight_in_order = min(
weight_in_order / 2, max(0, weight_in_order - 10 * threshold)
)
if weight_in_order == weight_reversed_order:
return (1, result_list_in_order)
elif preferred_direction == -1:
# Reduce weight_reversed_order to make reversed
# stitching more preferred
weight_reversed_order = min(
weight_reversed_order /
2, max(0, weight_reversed_order - 10 * threshold)
)
if weight_in_order == weight_reversed_order:
return (-1, result_list_reversed_order)
if weight_in_order < weight_reversed_order:
return (1, result_list_in_order)
else:
return (-1, result_list_reversed_order)
def calculate_replacing_middle_point(line_segment, abs_offset, max_stitch_distance):
"""
Takes a line segment (consisting of 3 points!)
and calculates a new middle point if the line_segment is
straight enough to be resampled by points max_stitch_distance apart.
Returns None if the middle point is not needed.
"""
angles = LineStringSampling.calculate_line_angles(line_segment)
if angles[1] < abs_offset * constants.limiting_angle_straight:
if line_segment.length < max_stitch_distance:
return None
else:
return line_segment.interpolate(
line_segment.length - max_stitch_distance
).coords[0]
else:
return line_segment.coords[1]
def connect_raster_tree_from_inner_to_outer(
tree, used_offset, stitch_distance, close_point, offset_by_half
):
"""
Takes the offsetted curves organized as tree, connects and samples them.
Strategy: A connection from parent to child is made as fast as possible to
reach the innermost child as fast as possible in order to stitch afterwards
from inner to outer.
Input:
-tree: contains the offsetted curves in a hierachical organized
data structure.
-used_offset: used offset when the offsetted curves were generated
-stitch_distance: maximum allowed distance between two points
after sampling
-close_point: defines the beginning point for stitching
(stitching starts always from the undisplaced curve)
-offset_by_half: If true the resulting points are interlaced otherwise not.
Returnvalues:
-All offsetted curves connected to one line and sampled with points obeying
stitch_distance and offset_by_half
-Tag (origin) of each point to analyze why a point was placed
at this position
"""
current_coords = tree.val
abs_offset = abs(used_offset)
result_coords = []
result_coords_origin = []
start_distance = tree.val.project(close_point)
# We cut the current path so that its index 0 is closest to close_point
if start_distance > 0:
current_coords = cut(current_coords, start_distance)
tree.val = current_coords
if not tree.transferred_point_priority_deque.is_empty():
new_DEPQ = DEPQ(iterable=None, maxlen=None)
for item, priority in tree.transferred_point_priority_deque:
new_DEPQ.insert(
item,
math.fmod(
priority - start_distance + current_coords.length,
current_coords.length,
),
)
tree.transferred_point_priority_deque = new_DEPQ
# We try to use always the opposite stitching direction with respect to the
# parent to avoid crossings when entering and leaving the child
parent_stitching_direction = -1
if tree.parent is not None:
parent_stitching_direction = tree.parent.stitching_direction
# Find the nearest point in current_coords and its children and
# sort it along the stitching direction
stitching_direction, nearest_points_list = create_nearest_points_list(
current_coords,
tree.children,
1.5 * abs_offset,
2.05 * abs_offset,
parent_stitching_direction,
)
nearest_points_list.sort(
reverse=False, key=lambda tup: tup.proj_distance_parent)
# Have a small offset for the starting and ending to avoid double points
# at start and end point (since the paths are closed rings)
if nearest_points_list:
start_offset = min(
abs_offset * constants.factor_offset_starting_points,
nearest_points_list[0].proj_distance_parent,
)
end_offset = max(
current_coords.length
- abs_offset * constants.factor_offset_starting_points,
nearest_points_list[-1].proj_distance_parent,
)
else:
start_offset = abs_offset * constants.factor_offset_starting_points
end_offset = (
current_coords.length - abs_offset * constants.factor_offset_starting_points
)
if stitching_direction == 1:
(
own_coords,
own_coords_origin,
) = LineStringSampling.raster_line_string_with_priority_points(
current_coords,
start_offset, # We add start_offset to not sample the same
# point again (avoid double points for start
# and end)
end_offset,
stitch_distance,
stitching_direction,
tree.transferred_point_priority_deque,
abs_offset,
)
else:
(
own_coords,
own_coords_origin,
) = LineStringSampling.raster_line_string_with_priority_points(
current_coords,
current_coords.length - start_offset, # We subtract
# start_offset to not
# sample the same point
# again (avoid double
# points for start
# and end)
current_coords.length - end_offset,
stitch_distance,
stitching_direction,
tree.transferred_point_priority_deque,
abs_offset,
)
current_coords.coords = current_coords.coords[::-1]
assert len(own_coords) == len(own_coords_origin)
tree.stitching_direction = stitching_direction
tree.already_rastered = True
to_transfer_point_list = []
to_transfer_point_list_origin = []
for k in range(0, len(own_coords)):
# TODO: maybe do not take the first and the last
# since they are ENTER_LEAVING_POINT points for sure
if (
not offset_by_half
and own_coords_origin[k] == LineStringSampling.PointSource.EDGE_NEEDED
or own_coords_origin[k] == LineStringSampling.PointSource.FORBIDDEN_POINT
):
continue
if own_coords_origin[k] == LineStringSampling.PointSource.ENTER_LEAVING_POINT:
continue
to_transfer_point_list.append(Point(own_coords[k]))
to_transfer_point_list_origin.append(own_coords_origin[k])
assert len(to_transfer_point_list) == len(to_transfer_point_list_origin)
# Next we need to transfer our rastered points to siblings and childs
# Since the projection is only in ccw direction towards inner we
# need to use "-used_offset" for stitching_direction==-1
PointTransfer.transfer_points_to_surrounding(
tree,
stitching_direction * used_offset,
offset_by_half,
to_transfer_point_list,
to_transfer_point_list_origin,
overnext_neighbor=False,
transfer_forbidden_points=False,
transfer_to_parent=False,
transfer_to_sibling=True,
transfer_to_child=True,
)
# We transfer also to the overnext child to get a more straight
# arrangement of points perpendicular to the stitching lines
if offset_by_half:
PointTransfer.transfer_points_to_surrounding(
tree,
stitching_direction * used_offset,
False,
to_transfer_point_list,
to_transfer_point_list_origin,
overnext_neighbor=True,
transfer_forbidden_points=False,
transfer_to_parent=False,
transfer_to_sibling=True,
transfer_to_child=True,
)
if not nearest_points_list:
# If there is no child (inner geometry) we can simply
# take our own rastered coords as result
result_coords = own_coords
result_coords_origin = own_coords_origin
else:
# There are childs so we need to merge their coordinates
# with our own rastered coords
# Create a closed ring for the following code
own_coords.append(own_coords[0])
own_coords_origin.append(own_coords_origin[0])
# own_coords does not start with current_coords but has an offset
# (see call of raster_line_string_with_priority_points)
total_distance = start_offset
cur_item = 0
result_coords = [own_coords[0]]
result_coords_origin = [own_coords_origin[0]]
for i in range(1, len(own_coords)):
next_distance = math.sqrt(
(own_coords[i][0] - own_coords[i - 1][0]) ** 2
+ (own_coords[i][1] - own_coords[i - 1][1]) ** 2
)
while (
cur_item < len(nearest_points_list)
and total_distance + next_distance + constants.eps
> nearest_points_list[cur_item].proj_distance_parent
):
# The current and the next point in own_coords enclose the
# nearest point tuple between this geometry and child
# geometry. Hence we need to insert the child geometry points
# here before the next point of own_coords.
item = nearest_points_list[cur_item]
(
child_coords,
child_coords_origin,
) = connect_raster_tree_from_inner_to_outer(
item.child_node,
used_offset,
stitch_distance,
item.nearest_point_child,
offset_by_half,
)
# Imagine the nearest point of the child is within a long
# segment of the parent. Without additonal points
# on the parent side this would cause noticeable deviations.
# Hence we add here points shortly before and after
# the entering of the child to have only minor deviations to
# the desired shape.
# Here is the point for the entering:
if (
Point(result_coords[-1]
).distance(item.nearest_point_parent)
> constants.factor_offset_starting_points * abs_offset
):
result_coords.append(item.nearest_point_parent.coords[0])
result_coords_origin.append(
LineStringSampling.PointSource.ENTER_LEAVING_POINT
)
# Check whether the number of points of the connecting lines
# from child to child can be reduced
if len(child_coords) > 1:
point = calculate_replacing_middle_point(
LineString(
[result_coords[-1], child_coords[0], child_coords[1]]
),
abs_offset,
stitch_distance,
)
if point is not None:
result_coords.append(point)
result_coords_origin.append(child_coords_origin[0])
result_coords.extend(child_coords[1:])
result_coords_origin.extend(child_coords_origin[1:])
else:
result_coords.extend(child_coords)
result_coords_origin.extend(child_coords_origin)
# And here is the point for the leaving of the child
# (distance to the own following point should not be too large)
d = item.nearest_point_parent.distance(Point(own_coords[i]))
if cur_item < len(nearest_points_list) - 1:
d = min(
d,
abs(
nearest_points_list[cur_item +
1].proj_distance_parent
- item.proj_distance_parent
),
)
if d > constants.factor_offset_starting_points * abs_offset:
result_coords.append(
current_coords.interpolate(
item.proj_distance_parent
+ 2 * constants.factor_offset_starting_points * abs_offset
).coords[0]
)
result_coords_origin.append(
LineStringSampling.PointSource.ENTER_LEAVING_POINT
)
# Check whether this additional point makes the last point
# of the child unnecessary
point = calculate_replacing_middle_point(
LineString(
[result_coords[-3], result_coords[-2], result_coords[-1]]
),
abs_offset,
stitch_distance,
)
if point is None:
result_coords.pop(-2)
result_coords_origin.pop(-2)
cur_item += 1
if i < len(own_coords) - 1:
if (
Point(result_coords[-1]).distance(Point(own_coords[i]))
> abs_offset * constants.factor_offset_remove_points
):
result_coords.append(own_coords[i])
result_coords_origin.append(own_coords_origin[i])
# Since current_coords and own_coords are rastered differently
# there accumulate errors regarding the current distance.
# Since a projection of each point in own_coords would be very
# time consuming we project only every n-th point which resets
# the accumulated error every n-th point.
if i % 20 == 0:
total_distance = current_coords.project(Point(own_coords[i]))
else:
total_distance += next_distance
assert len(result_coords) == len(result_coords_origin)
return result_coords, result_coords_origin