File Coverage

lib/Graph/Easy/Layout/Scout.pm
Criterion Covered Total %
statement 529 632 83.7
branch 281 382 73.5
condition 138 216 63.8
subroutine 24 26 92.3
pod n/a
total 972 1256 77.3


line stmt bran cond sub pod time code
1             #############################################################################
2             # Find paths from node to node in a Manhattan-style grid via A*.
3             #
4             # (c) by Tels - part of Graph::Easy
5             #############################################################################
6              
7             package Graph::Easy::Layout::Scout;
8              
9             $VERSION = '0.75';
10              
11             #############################################################################
12             #############################################################################
13              
14             package Graph::Easy;
15              
16 49     49   253 use strict;
  49         106  
  49         2182  
17 49     49   247 use warnings;
  49         172  
  49         1831  
18 49     49   268 use Graph::Easy::Node::Cell;
  49         100  
  49         4499  
19 49         163668 use Graph::Easy::Edge::Cell qw/
20             EDGE_SHORT_E EDGE_SHORT_W EDGE_SHORT_N EDGE_SHORT_S
21              
22             EDGE_SHORT_BD_EW EDGE_SHORT_BD_NS
23             EDGE_SHORT_UN_EW EDGE_SHORT_UN_NS
24              
25             EDGE_START_E EDGE_START_W EDGE_START_N EDGE_START_S
26              
27             EDGE_END_E EDGE_END_W EDGE_END_N EDGE_END_S
28              
29             EDGE_N_E EDGE_N_W EDGE_S_E EDGE_S_W
30              
31             EDGE_N_W_S EDGE_S_W_N EDGE_E_S_W EDGE_W_S_E
32              
33             EDGE_LOOP_NORTH EDGE_LOOP_SOUTH EDGE_LOOP_WEST EDGE_LOOP_EAST
34              
35             EDGE_HOR EDGE_VER EDGE_HOLE
36              
37             EDGE_S_E_W EDGE_N_E_W EDGE_E_N_S EDGE_W_N_S
38              
39             EDGE_LABEL_CELL
40             EDGE_TYPE_MASK
41             EDGE_ARROW_MASK
42             EDGE_FLAG_MASK
43             EDGE_START_MASK
44             EDGE_END_MASK
45             EDGE_NO_M_MASK
46 49     49   267 /;
  49         103  
47              
48             #############################################################################
49              
50             # mapping edge type (HOR, VER, NW etc) and dx/dy to startpoint flag
51             my $start_points = {
52             # [ dx == 1, dx == -1, dy == 1, dy == -1 ,
53             # dx == 1, dx == -1, dy == 1, dy == -1 ]
54             EDGE_HOR() => [ EDGE_START_W, EDGE_START_E, 0, 0 ,
55             EDGE_END_E, EDGE_END_W, 0, 0, ],
56             EDGE_VER() => [ 0, 0, EDGE_START_N, EDGE_START_S ,
57             0, 0, EDGE_END_S, EDGE_END_N, ],
58             EDGE_N_E() => [ 0, EDGE_START_E, EDGE_START_N, 0 ,
59             EDGE_END_E, 0, 0, EDGE_END_N, ],
60             EDGE_N_W() => [ EDGE_START_W, 0, EDGE_START_N, 0 ,
61             0, EDGE_END_W, 0, EDGE_END_N, ],
62             EDGE_S_E() => [ 0, EDGE_START_E, 0, EDGE_START_S ,
63             EDGE_END_E, 0, EDGE_END_S, 0, ],
64             EDGE_S_W() => [ EDGE_START_W, 0, 0, EDGE_START_S ,
65             0, EDGE_END_W, EDGE_END_S, 0, ],
66             };
67              
68             my $start_to_end = {
69             EDGE_START_W() => EDGE_END_W(),
70             EDGE_START_E() => EDGE_END_E(),
71             EDGE_START_S() => EDGE_END_S(),
72             EDGE_START_N() => EDGE_END_N(),
73             };
74              
75             sub _end_points
76             {
77             # modify last field of path to be the correct endpoint; and the first field
78             # to be the correct startpoint:
79 59     59   140 my ($self, $edge, $coords, $dx, $dy) = @_;
80            
81 59 100       237 return $coords if $edge->undirected();
82              
83             # there are two cases (for each dx and dy)
84 57         101 my $i = 0; # index 0,1
85 57         80 my $co = 2;
86 57         73 my $case;
87              
88 57         129 for my $d ($dx,$dy,$dx,$dy)
89             {
90 228 100       1069 next if $d == 0;
91              
92 204         291 my $type = $coords->[$co] & EDGE_TYPE_MASK;
93              
94 204 100       267 $case = 0; $case = 1 if $d == -1;
  204         443  
95              
96             # modify first/last cell
97 204         543 my $t = $start_points->{ $type }->[ $case + $i ];
98             # on bidirectional edges, turn START_X into END_X
99 204 100 100     511 $t = $start_to_end->{$t} || $t if $edge->{bidirectional};
100              
101 204         311 $coords->[$co] += $t;
102              
103             } continue {
104 228         240 $i += 2; # index 2,3, 4,5 etc
105 228 100       547 $co = -1 if $i == 4; # modify now last cell
106             }
107 57         296 $coords;
108             }
109              
110             sub _find_path
111             {
112             # Try to find a path between two nodes. $options contains direction
113             # preferences. Returns a list of cells like:
114             # [ $x,$y,$type, $x1,$y1,$type1, ...]
115 905     905   7616 my ($self, $src, $dst, $edge) = @_;
116              
117             # one node pointing back to itself?
118 905 100       2793 if ($src == $dst)
119             {
120 31         145 my $rc = $self->_find_path_loop($src,$edge);
121 31 50       157 return $rc unless scalar @$rc == 0;
122             }
123              
124             # If one of the two nodes is bigger than 1 cell, use _find_path_astar(),
125             # because it automatically handles all the possibilities:
126 874 100 100     4017 return $self->_find_path_astar($edge)
      100        
127             if ($src->is_multicelled() || $dst->is_multicelled() || $edge->has_ports());
128            
129 742         2654 my ($x0, $y0) = ($src->{x}, $src->{y});
130 742         2076 my ($x1, $y1) = ($dst->{x}, $dst->{y});
131 742         1480 my $dx = ($x1 - $x0) <=> 0;
132 742         1213 my $dy = ($y1 - $y0) <=> 0;
133            
134 742         1336 my $cells = $self->{cells};
135 742         992 my @coords;
136 742         1634 my ($x,$y) = ($x0,$y0); # starting pos
137              
138             ###########################################################################
139             # below follow some shortcuts for easy things like straight paths:
140              
141 742 50       2042 print STDERR "# dx,dy: $dx,$dy\n" if $self->{debug};
142              
143 742 100 100     3898 if ($dx == 0 || $dy == 0)
144             {
145             # try straight path to target:
146            
147 679 50       8612 print STDERR "# $src->{x},$src->{y} => $dst->{x},$dst->{y} - trying short path\n" if $self->{debug};
148              
149             # distance to node:
150 679         1182 my $dx1 = ($x1 - $x0);
151 679         1192 my $dy1 = ($y1 - $y0);
152 679         1683 ($x,$y) = ($x0+$dx,$y0+$dy); # starting pos
153              
154 679 100 100     2766 if ((abs($dx1) == 2) || (abs($dy1) == 2))
155             {
156 579 100       2865 if (!exists ($cells->{"$x,$y"}))
157             {
158             # a single step for this edge:
159 567         1270 my $type = EDGE_LABEL_CELL;
160             # short path
161 567 100       2708 if ($edge->bidirectional())
    100          
162             {
163 5 100       24 $type += EDGE_SHORT_BD_EW if $dy == 0;
164 5 100       22 $type += EDGE_SHORT_BD_NS if $dx == 0;
165             }
166             elsif ($edge->undirected())
167             {
168 13 100       46 $type += EDGE_SHORT_UN_EW if $dy == 0;
169 13 100       43 $type += EDGE_SHORT_UN_NS if $dx == 0;
170             }
171             else
172             {
173 549 100 66     2952 $type += EDGE_SHORT_E if ($dx == 1 && $dy == 0);
174 549 100 100     2194 $type += EDGE_SHORT_S if ($dx == 0 && $dy == 1);
175 549 100 66     2071 $type += EDGE_SHORT_W if ($dx == -1 && $dy == 0);
176 549 100 100     2098 $type += EDGE_SHORT_N if ($dx == 0 && $dy == -1);
177             }
178             # if one of the end points of the edge is of shape 'edge'
179             # remove end/start flag
180 567 100 50     2749 if (($edge->{to}->attribute('shape') ||'') eq 'edge')
181             {
182             # we only need to remove one start point, namely the one at the "end"
183 4 100       17 if ($dx > 0)
    50          
184             {
185 3         7 $type &= ~EDGE_START_E;
186             }
187             elsif ($dx < 0)
188             {
189 0         0 $type &= ~EDGE_START_W;
190             }
191             }
192 567 100 50     4339 if (($edge->{from}->attribute('shape') ||'') eq 'edge')
193             {
194 3         5 $type &= ~EDGE_START_MASK;
195             }
196              
197 567         3456 return [ $x, $y, $type ]; # return a short EDGE
198             }
199             }
200              
201 112 100       245 my $type = EDGE_HOR; $type = EDGE_VER if $dx == 0; # - or |
  112         380  
202 112         197 my $done = 0;
203 112         212 my $label_done = 0;
204 112         167 while (3 < 5) # endless loop
205             {
206             # Since we do not handle crossings here, A* will be tried if we hit an
207             # edge in this test.
208 282 100       1253 $done = 1, last if exists $cells->{"$x,$y"}; # cell already full
209              
210             # the first cell gets the label
211 181 100       235 my $t = $type; $t += EDGE_LABEL_CELL if $label_done++ == 0;
  181         378  
212              
213 181         698 push @coords, $x, $y, $t; # good one, is free
214 181         197 $x += $dx; $y += $dy; # next field
  181         195  
215 181 100 100     679 last if ($x == $x1) && ($y == $y1);
216             }
217              
218 112 100       411 if ($done == 0)
219             {
220 11 50       35 print STDERR "# success for ", scalar @coords / 3, " steps in path\n" if $self->{debug};
221             # return all fields of path
222 11         65 return $self->_end_points($edge, \@coords, $dx, $dy);
223             }
224              
225             } # end else straight path try
226              
227             ###########################################################################
228             # Try paths with one bend:
229              
230             # ($dx != 0 && $dy != 0) => path with one bend
231             # XXX TODO:
232             # This could be handled by A*, too, but it would be probably a bit slower.
233             else
234             {
235             # straight path not possible, since x0 != x1 AND y0 != y1
236              
237             # " |" "| "
238             # try first "--+" (aka hor => ver), then "+---" (aka ver => hor)
239 63         145 my $done = 0;
240              
241 63 50       212 print STDERR "# bend path from $x,$y\n" if $self->{debug};
242              
243             # try hor => ver
244 63         128 my $type = EDGE_HOR;
245              
246 63         94 my $label = 0; # attach label?
247 63 50 50     459 $label = 1 if ref($edge) && ($edge->label()||'') eq ''; # no label?
      33        
248 63         112 $x += $dx;
249 63         192 while ($x != $x1)
250             {
251 67 100       306 $done++, last if exists $cells->{"$x,$y"}; # cell already full
252 51 50       153 print STDERR "# at $x,$y\n" if $self->{debug};
253 51 50       85 my $t = $type; $t += EDGE_LABEL_CELL if $label++ == 0;
  51         143  
254 51         138 push @coords, $x, $y, $t; # good one, is free
255 51         128 $x += $dx; # next field
256             };
257              
258             # check the bend itself
259 63 100       246 $done++ if exists $cells->{"$x,$y"}; # cell already full
260              
261 63 100       186 if ($done == 0)
262             {
263 27         113 my $type_bend = _astar_edge_type ($x-$dx,$y, $x,$y, $x,$y+$dy);
264            
265 27         109 push @coords, $x, $y, $type_bend; # put in bend
266 27 50       86 print STDERR "# at $x,$y\n" if $self->{debug};
267 27         38 $y += $dy;
268 27         34 $type = EDGE_VER;
269 27         101 while ($y != $y1)
270             {
271 19 50       70 $done++, last if exists $cells->{"$x,$y"}; # cell already full
272 19 50       53 print STDERR "# at $x,$y\n" if $self->{debug};
273 19         77 push @coords, $x, $y, $type; # good one, is free
274 19         56 $y += $dy;
275             }
276             }
277              
278 63 100       158 if ($done != 0)
279             {
280 36         65 $done = 0;
281             # try ver => hor
282 36 50       118 print STDERR "# hm, now trying first vertical, then horizontal\n" if $self->{debug};
283 36         59 $type = EDGE_VER;
284              
285 36         72 @coords = (); # drop old version
286 36         84 ($x,$y) = ($x0, $y0 + $dy); # starting pos
287 36         106 while ($y != $y1)
288             {
289 67 100       275 $done++, last if exists $cells->{"$x,$y"}; # cell already full
290 59 50       143 print STDERR "# at $x,$y\n" if $self->{debug};
291 59         140 push @coords, $x, $y, $type; # good one, is free
292 59         137 $y += $dy; # next field
293             };
294              
295             # check the bend itself
296 36 100       141 $done++ if exists $cells->{"$x,$y"}; # cell already full
297              
298 36 100       123 if ($done == 0)
299             {
300 25         120 my $type_bend = _astar_edge_type ($x,$y-$dy, $x,$y, $x+$dx,$y);
301              
302 25         75 push @coords, $x, $y, $type_bend; # put in bend
303 25 50       83 print STDERR "# at $x,$y\n" if $self->{debug};
304 25         36 $x += $dx;
305 25         45 my $label = 0; # attach label?
306 25 50       94 $label = 1 if $edge->label() eq ''; # no label?
307 25         49 $type = EDGE_HOR;
308 25         69 while ($x != $x1)
309             {
310 31 100       110 $done++, last if exists $cells->{"$x,$y"}; # cell already full
311 27 50       87 print STDERR "# at $x,$y\n" if $self->{debug};
312 27 50       52 my $t = $type; $t += EDGE_LABEL_CELL if $label++ == 0;
  27         66  
313 27         90 push @coords, $x, $y, $t; # good one, is free
314 27         82 $x += $dx;
315             }
316             }
317             }
318              
319 63 100       163 if ($done == 0)
320             {
321 48 50       131 print STDERR "# success for ", scalar @coords / 3, " steps in path\n" if $self->{debug};
322             # return all fields of path
323 48         339 return $self->_end_points($edge, \@coords, $dx, $dy);
324             }
325              
326 15 50       64 print STDERR "# no success\n" if $self->{debug};
327              
328             } # end path with $dx and $dy
329              
330 116         576 $self->_find_path_astar($edge); # try generic approach as last hope
331             }
332              
333             sub _find_path_loop
334             {
335             # find a path from one node back to itself
336 31     31   58 my ($self, $src, $edge) = @_;
337              
338 31 50       119 print STDERR "# Finding looping path from $src->{name} to $src->{name}\n" if $self->{debug};
339              
340 31         70 my ($n, $cells, $d, $type, $loose) = @_;
341              
342             # get a list of all places
343              
344 31         205 my @places = $src->_near_places(
345             $self->{cells}, 1, [
346             EDGE_LOOP_EAST,
347             EDGE_LOOP_SOUTH,
348             EDGE_LOOP_WEST,
349             EDGE_LOOP_NORTH,
350             ], 0, 90);
351            
352 31         153 my $flow = $src->flow();
353              
354             # We cannot use _shuffle_dir() here, because self-loops
355             # are tried in a different order:
356              
357             # the default (east)
358 31         211 my $index = [
359             EDGE_LOOP_NORTH,
360             EDGE_LOOP_SOUTH,
361             EDGE_LOOP_WEST,
362             EDGE_LOOP_EAST,
363             ];
364              
365             # west
366 31 100       111 $index = [
367             EDGE_LOOP_SOUTH,
368             EDGE_LOOP_NORTH,
369             EDGE_LOOP_EAST,
370             EDGE_LOOP_WEST,
371             ] if $flow == 270;
372              
373             # north
374 31 100       235 $index = [
375             EDGE_LOOP_WEST,
376             EDGE_LOOP_EAST,
377             EDGE_LOOP_SOUTH,
378             EDGE_LOOP_NORTH,
379             ] if $flow == 0;
380            
381             # south
382 31 100       105 $index = [
383             EDGE_LOOP_EAST,
384             EDGE_LOOP_WEST,
385             EDGE_LOOP_NORTH,
386             EDGE_LOOP_SOUTH,
387             ] if $flow == 180;
388            
389 31         75 for my $this_try (@$index)
390             {
391 59         80 my $idx = 0;
392 59         180 while ($idx < @places)
393             {
394 114 50       250 print STDERR "# Trying $places[$idx+0],$places[$idx+1]\n" if $self->{debug};
395 114 100       275 next unless $places[$idx+2] == $this_try;
396            
397             # build a path from the returned piece
398 31         115 my @rc = ($places[$idx], $places[$idx+1], $places[$idx+2]);
399              
400 31 50       100 print STDERR "# Trying $rc[0],$rc[1]\n" if $self->{debug};
401              
402 31 50       173 next unless $self->_path_is_clear(\@rc);
403              
404 31 50       107 print STDERR "# Found looping path\n" if $self->{debug};
405 31         150 return \@rc;
406 83         222 } continue { $idx += 3; }
407             }
408              
409 0         0 []; # no path found
410             }
411              
412             #############################################################################
413             #############################################################################
414              
415             # This package represents a simple/cheap/fast heap:
416             package Graph::Easy::Heap;
417              
418             require Graph::Easy::Base;
419             our @ISA = qw/Graph::Easy::Base/;
420              
421 49     49   596 use strict;
  49         269  
  49         130602  
422              
423             sub _init
424             {
425 745     745   2168 my ($self,$args) = @_;
426              
427 745         5709 $self->{_heap} = [ ];
428              
429 745         2969 $self;
430             }
431              
432             sub add
433             {
434             # add one element to the heap
435 7154     7154   12090 my ($self,$elem) = @_;
436              
437 7154         11799 my $heap = $self->{_heap};
438              
439             # heap empty?
440 7154 100       30312 if (@$heap == 0)
    100          
    100          
441             {
442 1262         3802 push @$heap, $elem;
443             }
444             # smaller than first elem?
445             elsif ($elem->[0] < $heap->[0]->[0])
446             {
447             #print STDERR "# $elem->[0] is smaller then first elem $heap->[0]->[0] (with ", scalar @$heap," elems on heap)\n";
448 1522         3594 unshift @$heap, $elem;
449             }
450             # bigger than or equal to last elem?
451             elsif ($elem->[0] > $heap->[-1]->[0])
452             {
453             #print STDERR "# $elem->[0] is bigger then last elem $heap->[-1]->[0] (with ", scalar @$heap," elems on heap)\n";
454 838         1806 push @$heap, $elem;
455             }
456             else
457             {
458             # insert the elem at the right position
459              
460             # if we have less than X elements, use linear search
461 3532         5372 my $el = $elem->[0];
462 3532 100       7548 if (scalar @$heap < 10)
463             {
464 1910         2895 my $i = 0;
465 1910         16047 for my $e (@$heap)
466             {
467 5808 100       13958 if ($e->[0] > $el)
468             {
469 862         2965 splice (@$heap, $i, 0, $elem); # insert $elem
470 862         2493 return undef;
471             }
472 4946         7874 $i++;
473             }
474             # else, append at the end
475 1048         2829 push @$heap, $elem;
476             }
477             else
478             {
479             # use binary search
480 1622         2041 my $l = 0; my $r = scalar @$heap;
  1622         1974  
481 1622         3264 while (($r - $l) > 2)
482             {
483 7550         10363 my $m = int((($r - $l) / 2) + $l);
484             # print "l=$l r=$r m=$m el=$el heap=$heap->[$m]->[0]\n";
485 7550 100       13143 if ($heap->[$m]->[0] <= $el)
486             {
487 5040         9787 $l = $m;
488             }
489             else
490             {
491 2510         4919 $r = $m;
492             }
493             }
494 1622         3030 while ($l < @$heap)
495             {
496 3951 100       7606 if ($heap->[$l]->[0] > $el)
497             {
498 1438         2331 splice (@$heap, $l, 0, $elem); # insert $elem
499 1438         2714 return undef;
500             }
501 2513         4566 $l++;
502             }
503             # else, append at the end
504 184         342 push @$heap, $elem;
505             }
506             }
507 4854         15681 undef;
508             }
509              
510             sub elements
511             {
512 523     523   1794 scalar @{$_[0]->{_heap}};
  523         3840  
513             }
514              
515             sub extract_top
516             {
517             # remove and return the top elemt
518 6131     6131   24012 shift @{$_[0]->{_heap}};
  6131         26636  
519             }
520              
521             sub delete
522             {
523             # Find an element by $x,$y and delete it
524 0     0   0 my ($self, $x, $y) = @_;
525              
526 0         0 my $heap = $self->{_heap};
527            
528 0         0 my $i = 0;
529 0         0 for my $e (@$heap)
530             {
531 0 0 0     0 if ($e->[1] == $x && $e->[2] == $y)
532             {
533 0         0 splice (@$heap, $i, 1);
534 0         0 return;
535             }
536 0         0 $i++;
537             }
538              
539 0         0 $self;
540             }
541              
542             sub sort_sub
543             {
544 502     502   15610 my ($self) = shift;
545              
546 502         1965 $self->{_sort} = shift;
547             }
548              
549             #############################################################################
550             #############################################################################
551              
552             package Graph::Easy;
553              
554             # Generic pathfinding via the A* algorithm:
555             # See http://bloodgate.com/perl/graph/astar.html for some background.
556              
557             sub _astar_modifier
558             {
559             # calculate the cost for the path at cell x1,y1
560 6564     6564   12314 my ($x1,$y1,$x,$y,$px,$py, $cells) = @_;
561              
562 6564         7649 my $add = 1;
563              
564 6564 50       13473 if (defined $x1)
565             {
566 6564         10232 my $xy = "$x1,$y1";
567             # add a harsh penalty for crossing an edge, meaning we can travel many
568             # fields to go around.
569 6564 100 100     27739 $add += 30 if ref($cells->{$xy}) && $cells->{$xy}->isa('Graph::Easy::Edge');
570             }
571            
572 6564 100       14841 if (defined $px)
573             {
574             # see whether the new position $x1,$y1 is a continuation from $px,$py => $x,$y
575             # e.g. if from we go down from $px,$py to $x,$y, then anything else then $x,$y+1 will
576             # get a penalty
577 6563         9802 my $dx1 = ($px-$x) <=> 0;
578 6563         8732 my $dy1 = ($py-$y) <=> 0;
579 6563         7759 my $dx2 = ($x-$x1) <=> 0;
580 6563         7615 my $dy2 = ($y-$y1) <=> 0;
581 6563 100 100     27194 $add += 6 unless $dx1 == $dx2 || $dy1 == $dy2;
582             }
583 6564         13901 $add;
584             }
585              
586             sub _astar_distance
587             {
588             # calculate the manhattan distance between x1,y1 and x2,y2
589             # my ($x1,$y1,$x2,$y2) = @_;
590              
591 20860     20860   28747 my $dx = abs($_[2] - $_[0]);
592 20860         26345 my $dy = abs($_[3] - $_[1]);
593              
594             # plus 1 because we need to go around one corner if $dx != 0 && $dx != 0
595 20860 100 100     83625 $dx++ if $dx != 0 && $dy != 0;
596              
597 20860         38074 $dx + $dy;
598             }
599              
600             my $edge_type = {
601             '0,1,-1,0' => EDGE_N_W,
602             '0,1,0,1' => EDGE_VER,
603             '0,1,1,0' => EDGE_N_E,
604              
605             '-1,0,0,-1' => EDGE_N_E,
606             '-1,0,-1,0' => EDGE_HOR,
607             '-1,0,0,1' => EDGE_S_E,
608              
609             '0,-1,-1,0' => EDGE_S_W,
610             '0,-1,0,-1' => EDGE_VER,
611             '0,-1,1,0' => EDGE_S_E,
612              
613             '1,0,0,-1' => EDGE_N_W,
614             '1,0,1,0' => EDGE_HOR,
615             '1,0,0,1' => EDGE_S_W,
616              
617             # loops (left-right-left etc)
618             '0,-1,0,1' => EDGE_N_W_S,
619             '0,1,0,-1' => EDGE_S_W_N,
620             '1,0,-1,0' => EDGE_E_S_W,
621             '-1,0,1,0' => EDGE_W_S_E,
622             };
623              
624             sub _astar_edge_type
625             {
626             # from three consecutive positions calculate the edge type (VER, HOR, N_W etc)
627 1219     1219   2082 my ($x,$y, $x1,$y1, $x2, $y2) = @_;
628              
629 1219         1777 my $dx1 = ($x1 - $x) <=> 0;
630 1219         2048 my $dy1 = ($y1 - $y) <=> 0;
631              
632 1219         1871 my $dx2 = ($x2 - $x1) <=> 0;
633 1219         1555 my $dy2 = ($y2 - $y1) <=> 0;
634              
635             # in some cases we get (0,-1,0,0), so set the missing parts
636 1219 100 100     3881 ($dx2,$dy2) = ($dx1,$dy1) if $dx2 == 0 && $dy2 == 0;
637             # can this case happen?
638 1219 50 66     3837 ($dx1,$dy1) = ($dx2,$dy2) if $dx1 == 0 && $dy1 == 0;
639              
640             # return correct type depending on differences
641 1219 50       6892 $edge_type->{"$dx1,$dy1,$dx2,$dy2"} || EDGE_HOR;
642             }
643              
644             sub _astar_near_nodes
645             {
646             # return possible next nodes from $nx,$ny
647 3202     3202   5841 my ($self, $nx, $ny, $cells, $closed, $min_x, $min_y, $max_x, $max_y) = @_;
648              
649 3202         5196 my @places = ();
650              
651 3202         12085 my @tries = ( # ordered E,S,W,N:
652             $nx + 1, $ny, # right
653             $nx, $ny + 1, # down
654             $nx - 1, $ny, # left
655             $nx, $ny - 1, # up
656             );
657              
658             # on crossings, only allow one direction (NS or EW)
659 3202         6190 my $type = EDGE_CROSS;
660             # including flags, because only flagless edges may be crossed
661 3202 100       8746 $type = $cells->{"$nx,$ny"}->{type} if exists $cells->{"$nx,$ny"};
662 3202 100       8418 if ($type == EDGE_HOR)
    100          
663             {
664 258         990 @tries = (
665             $nx, $ny + 1, # down
666             $nx, $ny - 1, # up
667             );
668             }
669             elsif ($type == EDGE_VER)
670             {
671 71         319 @tries = (
672             $nx + 1, $ny, # right
673             $nx - 1, $ny, # left
674             );
675             }
676              
677             # This loop does not check whether the position is already open or not,
678             # the caller will later check if the already-open position needs to be
679             # replaced by one with a lower cost.
680              
681 3202         4028 my $i = 0;
682 3202         7001 while ($i < @tries)
683             {
684 12150         20820 my ($x,$y) = ($tries[$i], $tries[$i+1]);
685              
686 12150 50       29093 print STDERR "# $min_x,$min_y => $max_x,$max_y\n" if $self->{debug} > 2;
687              
688             # drop cells outside our working space:
689 12150 100 100     97736 next if $x < $min_x || $x > $max_x || $y < $min_y || $y > $max_y;
      100        
      100        
690              
691 10839         21558 my $p = "$x,$y";
692 10839 50       22015 print STDERR "# examining pos $p\n" if $self->{debug} > 2;
693              
694 10839 100       24780 next if exists $closed->{$p};
695              
696 6957 100 66     47020 if (exists $cells->{$p} && ref($cells->{$p}) && $cells->{$p}->isa('Graph::Easy::Edge'))
      100        
697             {
698             # If the existing cell is an VER/HOR edge, then we may cross it
699 956         2422 my $type = $cells->{$p}->{type}; # including flags, because only flagless edges
700             # may be crossed
701              
702 956 100 100     4529 push @places, $x, $y if ($type == EDGE_HOR) || ($type == EDGE_VER);
703 956         1645 next;
704             }
705 6001 100       12628 next if exists $cells->{$p}; # uncrossable cell
706              
707 5249         11917 push @places, $x, $y;
708              
709 12150         30740 } continue { $i += 2; }
710            
711 3202         24446 @places;
712             }
713              
714             sub _astar_boundaries
715             {
716             # Calculate boundaries for area that A* should not leave.
717 242     242   480 my $self = shift;
718              
719 242         495 my $cache = $self->{cache};
720              
721 242 100       1788 return ( $cache->{min_x}-1, $cache->{min_y}-1,
722             $cache->{max_x}+1, $cache->{max_y}+1 ) if defined $cache->{min_x};
723              
724 2         4 my ($min_x, $min_y, $max_x, $max_y);
725              
726 2         18 my $cells = $self->{cells};
727              
728 2         7 $min_x = 10000000;
729 2         5 $min_y = 10000000;
730 2         2 $max_x = -10000000;
731 2         4 $max_y = -10000000;
732              
733 2         12 for my $c (sort keys %$cells)
734             {
735 4         13 my ($x,$y) = split /,/, $c;
736 4 100       15 $min_x = $x if $x < $min_x;
737 4 100       17 $min_y = $y if $y < $min_y;
738 4 50       13 $max_x = $x if $x > $max_x;
739 4 100       13 $max_y = $y if $y > $max_y;
740             }
741              
742 2 50       7 print STDERR "# A* working space boundaries: $min_x, $min_y, $max_x, $max_y\n" if $self->{debug};
743              
744 2         14 ( $cache->{min_x}, $cache->{min_y}, $cache->{max_x}, $cache->{max_y} ) =
745             ($min_x, $min_y, $max_x, $max_y);
746              
747             # make the area one bigger in each direction
748 2         5 $min_x --; $min_y --; $max_x ++; $max_y ++;
  2         2  
  2         4  
  2         3  
749 2         6 ($min_x, $min_y, $max_x, $max_y);
750             }
751              
752             # on edge pieces, select start fields (left/right of a VER, above/below of a HOR etc)
753             # contains also for each starting position the joint-type
754             my $next_fields =
755             {
756             EDGE_VER() => [ -1,0, EDGE_W_N_S, +1,0, EDGE_E_N_S ],
757             EDGE_HOR() => [ 0,-1, EDGE_N_E_W, 0,+1, EDGE_S_E_W ],
758             EDGE_N_E() => [ 0,+1, EDGE_E_N_S, -1,0, EDGE_N_E_W ], # |_
759             EDGE_N_W() => [ 0,+1, EDGE_W_N_S, +1,0, EDGE_N_E_W ], # _|
760             EDGE_S_E() => [ 0,-1, EDGE_E_N_S, -1,0, EDGE_S_E_W ],
761             EDGE_S_W() => [ 0,-1, EDGE_W_N_S, +1,0, EDGE_S_E_W ],
762             };
763              
764             # on edge pieces, select end fields (left/right of a VER, above/below of a HOR etc)
765             # contains also for each end position the joint-type
766             my $prev_fields =
767             {
768             EDGE_VER() => [ -1,0, EDGE_W_N_S, +1,0, EDGE_E_N_S ],
769             EDGE_HOR() => [ 0,-1, EDGE_N_E_W, 0,+1, EDGE_S_E_W ],
770             EDGE_N_E() => [ 0,+1, EDGE_E_N_S, -1,0, EDGE_N_E_W ], # |_
771             EDGE_N_W() => [ 0,+1, EDGE_W_N_S, +1,0, EDGE_N_E_W ], # _|
772             EDGE_S_E() => [ 0,-1, EDGE_E_N_S, -1,0, EDGE_S_E_W ],
773             EDGE_S_W() => [ 0,-1, EDGE_W_N_S, +1,0, EDGE_S_E_W ],
774             };
775              
776 49     49   458 use Graph::Easy::Util qw(ord_values);
  49         137  
  49         371197  
777              
778             sub _get_joints
779             {
780             # from a list of shared, already placed edges, get possible start/end fields
781 25     25   63 my ($self, $shared, $mask, $types, $cells, $next_fields) = @_;
782              
783             # XXX TODO: do not do this for edges with no free places for joints
784              
785             # take each cell from all edges shared, already placed edges as start-point
786 25         48 for my $e (@$shared)
787             {
788 41         66 for my $c (@{$e->{cells}})
  41         103  
789             {
790 110         214 my $type = $c->{type} & EDGE_TYPE_MASK;
791              
792 110 100       295 next unless exists $next_fields->{ $type };
793              
794             # don't consider end/start (depending on $mask) cells
795              
796             # do not join EDGE_HOR or EDGE_VER, but join corner pieces
797 94 100 100     586 next if ( ($type == EDGE_HOR()) ||
      100        
798             ($type == EDGE_VER()) ) &&
799             ($c->{type} & $mask);
800              
801 61         111 my $fields = $next_fields->{$type};
802              
803 61         164 my ($px,$py) = ($c->{x},$c->{y});
804 61         80 my $i = 0;
805 61         134 while ($i < @$fields)
806             {
807 122         285 my ($sx,$sy, $jt) = ($fields->[$i], $fields->[$i+1], $fields->[$i+2]);
808 122         220 $sx += $px; $sy += $py; $i += 3;
  122         192  
  122         143  
809 122         220 my $sxsy = "$sx,$sy";
810             # don't add the field twice
811 122 100       346 next if exists $cells->{$sxsy};
812 116         485 $cells->{$sxsy} = [ $sx, $sy, undef, $px, $py ];
813             # keep eventually set start/end points on the original cell
814 116         487 $types->{$sxsy} = $jt + ($c->{type} & EDGE_FLAG_MASK);
815             }
816             }
817             }
818            
819 25         50 my @R;
820             # convert hash to array
821 25         141 for my $s (ord_values ( $cells ))
822             {
823 116         416 push @R, @$s;
824             }
825 25         335 @R;
826             }
827              
828             sub _join_edge
829             {
830             # Find out whether an edge sharing an ending point with the source edge
831             # runs alongside the source node, if so, convert it to a joint:
832 31     31   76 my ($self, $node, $edge, $shared, $end) = @_;
833              
834             # we check the sides B,C,D and E for HOR and VER edge pices:
835             # --D--
836             # | +---+ |
837             # E | A | B
838             # | +---+ |
839             # --C--
840              
841 31         96 my $flags =
842             [
843             EDGE_W_N_S + EDGE_START_W,
844             EDGE_N_E_W + EDGE_START_N,
845             EDGE_E_N_S + EDGE_START_E,
846             EDGE_S_E_W + EDGE_START_S,
847             ];
848 31 100 100     231 $flags =
849             [
850             EDGE_W_N_S + EDGE_END_W,
851             EDGE_N_E_W + EDGE_END_N,
852             EDGE_E_N_S + EDGE_END_E,
853             EDGE_S_E_W + EDGE_END_S,
854             ] if $end || $edge->{bidirectional};
855            
856 31         74 my $cells = $self->{cells};
857 31         156 my @places = $node->_near_places($cells, 1, # distance 1
858             $flags, 'loose');
859              
860 31         156 my $i = 0;
861 31         171 while ($i < @places)
862             {
863 108         196 my ($x,$y) = ($places[$i], $places[$i+1]); $i += 3;
  108         119  
864            
865 108 100       402 next unless exists $cells->{"$x,$y"}; # empty space?
866             # found some cell, check that it is a EDGE_HOR or EDGE_VER
867 13         43 my $cell = $cells->{"$x,$y"};
868 13 100       98 next unless $cell->isa('Graph::Easy::Edge::Cell');
869              
870 8         20 my $cell_type = $cell->{type} & EDGE_TYPE_MASK;
871              
872 8 50 66     37 next unless $cell_type == EDGE_HOR || $cell_type == EDGE_VER;
873              
874             # the cell must belong to one of the shared edges
875 8         13 my $e = $cell->{edge}; local $_;
  8         14  
876 8 100       16 next unless scalar grep { $e == $_ } @$shared;
  10         50  
877              
878             # make the cell at the current pos a joint
879 6         90 $cell->_make_joint($edge,$places[$i-1]);
880              
881             # The layouter will check that each edge has a cell, so add a dummy one to
882             # $edge to make it happy:
883 6         33 Graph::Easy::Edge::Cell->new( type => EDGE_HOLE, edge => $edge, x => $x, y => $y );
884              
885 6         36 return []; # path is empty
886             }
887              
888 25         91 undef; # did not find an edge cell that can be used as joint
889             }
890              
891             sub _find_path_astar
892             {
893             # Find a path with the A* algorithm for the given edge (from node A to B)
894 248     248   498 my ($self,$edge) = @_;
895              
896 248         496 my $cells = $self->{cells};
897 248         571 my $src = $edge->{from};
898 248         529 my $dst = $edge->{to};
899              
900 248 50       659 print STDERR "# A* from $src->{x},$src->{y} to $dst->{x},$dst->{y}\n" if $self->{debug};
901              
902 248         1062 my $start_flags = [
903             EDGE_START_W,
904             EDGE_START_N,
905             EDGE_START_E,
906             EDGE_START_S,
907             ];
908              
909 248         1555 my $end_flags = [
910             EDGE_END_W,
911             EDGE_END_N,
912             EDGE_END_E,
913             EDGE_END_S,
914             ];
915              
916             # if the target/source node is of shape "edge", remove the endpoint
917 248 50       1137 if ( ($edge->{to}->attribute('shape')) eq 'edge')
918             {
919 0         0 $end_flags = [ 0,0,0,0 ];
920             }
921 248 50       1003 if ( ($edge->{from}->attribute('shape')) eq 'edge')
922             {
923 0         0 $start_flags = [ 0,0,0,0 ];
924             }
925              
926 248         1167 my ($s_p,@ss_p) = $edge->port('start');
927 248         933 my ($e_p,@ee_p) = $edge->port('end');
928 248         443 my (@A, @B); # Start/Stop positions
929 0         0 my @shared_start;
930 0         0 my @shared_end;
931              
932 248         517 my $joint_type = {};
933 248         445 my $joint_type_end = {};
934              
935 248         562 my $start_cells = {};
936 248         484 my $end_cells = {};
937              
938             ###########################################################################
939             # end fields first (because maybe an edge runs alongside the node)
940              
941             # has a end point restriction
942 248 100 100     1095 @shared_end = $edge->{to}->edges_at_port('end', $e_p, $ee_p[0]) if defined $e_p && @ee_p == 1;
943              
944 248         457 my @shared = ();
945             # filter out all non-placed edges (this will also filter out $edge)
946 248         562 for my $s (@shared_end)
947             {
948 84 100       97 push @shared, $s if @{$s->{cells}} > 0;
  84         311  
949             }
950              
951 248         517 my $per_field = 5; # for shared: x,y,undef, px,py
952 248 100       653 if (@shared > 0)
953             {
954             # more than one edge share the same end port, and one of the others was
955             # already placed
956              
957 18 50       61 print STDERR "# edge from '$edge->{from}->{name}' to '$edge->{to}->{name}' shares end port with ",
958             scalar @shared, " other edge(s)\n" if $self->{debug};
959              
960             # if there is one of the already-placed edges running alongside the src
961             # node, we can just convert the field to a joint and be done
962 18         91 my $path = $self->_join_edge($src,$edge,\@shared);
963 18 100       98 return $path if $path; # already done?
964              
965 12         64 @B = $self->_get_joints(\@shared, EDGE_START_MASK, $joint_type_end, $end_cells, $prev_fields);
966             }
967             else
968             {
969             # potential stop positions
970 230         1227 @B = $dst->_near_places($cells, 1, $end_flags, 1); # distance = 1: slots
971              
972             # the edge has a port description, limiting the end places
973 230 100       863 @B = $dst->_allowed_places( \@B, $dst->_allow( $e_p, @ee_p ), 3)
974             if defined $e_p;
975              
976 230         414 $per_field = 3; # x,y,type
977             }
978              
979 242 50       622 return unless scalar @B > 0; # no free slots on target node?
980              
981             ###########################################################################
982             # start fields
983              
984             # has a starting point restriction:
985 242 100 100     1077 @shared_start = $edge->{from}->edges_at_port('start', $s_p, $ss_p[0]) if defined $s_p && @ss_p == 1;
986              
987 242         519 @shared = ();
988             # filter out all non-placed edges (this will also filter out $edge)
989 242         639 for my $s (@shared_start)
990             {
991 62 100       76 push @shared, $s if @{$s->{cells}} > 0;
  62         215  
992             }
993              
994 242 100       629 if (@shared > 0)
995             {
996             # More than one edge share the same start port, and one of the others was
997             # already placed, so we just run along until we catch it up with a joint:
998              
999 13 50       45 print STDERR "# edge from '$edge->{from}->{name}' to '$edge->{to}->{name}' shares start port with ",
1000             scalar @shared, " other edge(s)\n" if $self->{debug};
1001              
1002             # if there is one of the already-placed edges running alongside the src
1003             # node, we can just convert the field to a joint and be done
1004 13         71 my $path = $self->_join_edge($dst, $edge, \@shared, 'end');
1005 13 50       40 return $path if $path; # already done?
1006              
1007 13         73 @A = $self->_get_joints(\@shared, EDGE_END_MASK, $joint_type, $start_cells, $next_fields);
1008             }
1009             else
1010             {
1011             # from SRC to DST
1012              
1013             # get all the starting positions
1014             # distance = 1: slots, generate starting types, the direction is shifted
1015             # by 90° counter-clockwise
1016              
1017 229 100       390 my $s = $start_flags; $s = $end_flags if $edge->{bidirectional};
  229         692  
1018 229         1217 my @start = $src->_near_places($cells, 1, $s, 1, $src->_shift(-90) );
1019              
1020             # the edge has a port description, limiting the start places
1021 229 100       1059 @start = $src->_allowed_places( \@start, $src->_allow( $s_p, @ss_p ), 3)
1022             if defined $s_p;
1023              
1024 229 50       862 return unless @start > 0; # no free slots on start node?
1025              
1026 229         379 my $i = 0;
1027 229         645 while ($i < scalar @start)
1028             {
1029 1112         1511 my $sx = $start[$i]; my $sy = $start[$i+1]; my $type = $start[$i+2]; $i += 3;
  1112         1462  
  1112         1506  
  1112         1186  
1030              
1031             # compute the field inside the node from where $sx,$sy is reached:
1032 1112         1202 my $px = $sx; my $py = $sy;
  1112         1180  
1033 1112 100 100     5440 if ($sy < $src->{y} || $sy >= $src->{y} + $src->{cy})
1034             {
1035 602 100       1373 $py = $sy + 1 if $sy < $src->{y}; # above
1036 602 100       1369 $py = $sy - 1 if $sy > $src->{y}; # below
1037             }
1038             else
1039             {
1040 510 100       1159 $px = $sx + 1 if $sx < $src->{x}; # right
1041 510 100       1374 $px = $sx - 1 if $sx > $src->{x}; # left
1042             }
1043              
1044 1112         5030 push @A, ($sx, $sy, $type, $px, $py);
1045             }
1046             }
1047              
1048             ###########################################################################
1049             # use A* to finally find the path:
1050              
1051 242         1405 my $path = $self->_astar(\@A,\@B,$edge, $per_field);
1052              
1053 242 100 100     1946 if (@$path > 0 && keys %$start_cells > 0)
1054             {
1055             # convert the edge piece of the starting edge-cell to a joint
1056 13         51 my ($x, $y) = ($path->[0],$path->[1]);
1057 13         35 my $xy = "$x,$y";
1058 13         30 my ($sx,$sy,$t,$px,$py) = @{$start_cells->{$xy}};
  13         72  
1059              
1060 13         39 my $jt = $joint_type->{"$sx,$sy"};
1061 13         101 $cells->{"$px,$py"}->_make_joint($edge,$jt);
1062             }
1063              
1064 242 100 100     1775 if (@$path > 0 && keys %$end_cells > 0)
1065             {
1066             # convert the edge piece of the starting edge-cell to a joint
1067 12         44 my ($x, $y) = ($path->[-3],$path->[-2]);
1068 12         35 my $xy = "$x,$y";
1069 12         21 my ($sx,$sy,$t,$px,$py) = @{$end_cells->{$xy}};
  12         45  
1070              
1071 12         38 my $jt = $joint_type_end->{"$sx,$sy"};
1072 12         98 $cells->{"$px,$py"}->_make_joint($edge,$jt);
1073             }
1074              
1075 242         4081 $path;
1076             }
1077              
1078             sub _astar
1079             {
1080             # The core A* algorithm, finds a path from a given list of start
1081             # positions @A to and of the given stop positions @B.
1082 242     242   484 my ($self, $A, $B, $edge, $per_field) = @_;
1083              
1084 242         1510 my @start = @$A;
1085 242         1567 my @stop = @$B;
1086 242         409 my $stop = scalar @stop;
1087              
1088 242         518 my $src = $edge->{from};
1089 242         427 my $dst = $edge->{to};
1090 242         530 my $cells = $self->{cells};
1091              
1092 242         1541 my $open = Graph::Easy::Heap->new(); # to find smallest elem fast
1093 242         882 my $open_by_pos = {}; # to find open nodes by pos
1094 242         545 my $closed = {}; # to find closed nodes by pos
1095              
1096 242         424 my $elem;
1097              
1098             # The boundaries of objects in $cell, e.g. the area that the algorithm shall
1099             # never leave.
1100 242         945 my ($min_x, $min_y, $max_x, $max_y) = $self->_astar_boundaries();
1101              
1102             # Max. steps to prevent endless searching in case of bugs like endless loops.
1103 242         530 my $tries = 0; my $max_tries = 2000000;
  242         424  
1104              
1105             # count how many times we did A*
1106 242         807 $self->{stats}->{astar}++;
1107              
1108             ###########################################################################
1109             ###########################################################################
1110             # put the start positions into OPEN
1111              
1112 242         467 my $i = 0; my $bias = 0;
  242         470  
1113 242         801 while ($i < scalar @start)
1114             {
1115 1172         3226 my ($sx,$sy,$type,$px,$py) =
1116             ($start[$i],$start[$i+1],$start[$i+2],$start[$i+3],$start[$i+4]);
1117 1172         1423 $i += 5;
1118              
1119 1172         2353 my $cell = $cells->{"$sx,$sy"}; my $rcell = ref($cell);
  1172         1699  
1120 1172 100 100     4232 next if $rcell && $rcell !~ /::Edge/;
1121              
1122 1161 100       1656 my $t = 0; $t = $cell->{type} & EDGE_NO_M_MASK if $rcell =~ /::Edge/;
  1161         3227  
1123 1161 100 100     4524 next if $t != 0 && $t != EDGE_HOR && $t != EDGE_VER;
      100        
1124              
1125             # For each start point, calculate the distance to each stop point, then use
1126             # the smallest as value:
1127 895         1106 my $lowest_x = $stop[0]; my $lowest_y = $stop[1];
  895         1315  
1128 895         2204 my $lowest = _astar_distance($sx,$sy, $stop[0], $stop[1]);
1129 895         2465 for (my $u = $per_field; $u < $stop; $u += $per_field)
1130             {
1131 3218         6791 my $dist = _astar_distance($sx,$sy, $stop[$u], $stop[$u+1]);
1132 3218 100       8480 ($lowest_x, $lowest_y) = ($stop[$u],$stop[$u+1]) if $dist < $lowest;
1133 3218 100       10381 $lowest = $dist if $dist < $lowest;
1134             }
1135              
1136              
1137             # add a penalty for crossings
1138 895 100       1317 my $malus = 0; $malus = 30 if $t != 0;
  895         1823  
1139 895         2021 $malus += _astar_modifier($px,$py, $sx, $sy, $sx, $sy);
1140 895         4903 $open->add( [ $lowest, $sx, $sy, $px, $py, $type, 1 ] );
1141              
1142 895         2263 my $o = $malus + $bias + $lowest;
1143 895 50       2331 print STDERR "# adding open pos $sx,$sy ($o = $malus + $bias + $lowest) at ($lowest_x,$lowest_y)\n"
1144             if $self->{debug} > 1;
1145              
1146             # The cost to reach the starting node is obviously 0. That means that there is
1147             # a tie between going down/up if both possibilities are equal likely. We insert
1148             # a small bias here that makes the prefered order east/south/west/north. Instead
1149             # the algorithmn exploring both way and terminating arbitrarily on the one that
1150             # first hits the target, it will explore only one.
1151 895         2674 $open_by_pos->{"$sx,$sy"} = $o;
1152              
1153 895   50     3854 $bias += $self->{_astar_bias} || 0;
1154             }
1155              
1156             ###########################################################################
1157             ###########################################################################
1158             # main A* loop
1159              
1160 242         673 my $stats = $self->{stats};
1161              
1162             STEP:
1163 242         842 while( defined( $elem = $open->extract_top() ) )
1164             {
1165 3442 50       11343 $stats->{astar_steps}++ if $self->{debug};
1166              
1167             # hard limit on number of steps todo
1168 3442 50       7637 if ($tries++ > $max_tries)
1169             {
1170 0         0 $self->warn("A* reached maximum number of tries ($max_tries), giving up.");
1171 0         0 return [];
1172             }
1173              
1174 3442 50       7036 print STDERR "# Smallest elem from ", $open->elements(),
1175             " elems is: weight=", $elem->[0], " at $elem->[1],$elem->[2]\n" if $self->{debug} > 1;
1176 3442         7591 my ($val, $x,$y, $px,$py, $type, $do_stop) = @$elem;
1177              
1178 3442         6146 my $key = "$x,$y";
1179             # move node into CLOSE and remove from OPEN
1180 3442   50     8702 my $g = $open_by_pos->{$key} || 0;
1181 3442         13123 $closed->{$key} = [ $px, $py, $val - $g, $g, $type, $do_stop ];
1182 3442         6372 delete $open_by_pos->{$key};
1183              
1184             # we are done when we hit one of the potential stop positions
1185 3442         9678 for (my $i = 0; $i < $stop; $i += $per_field)
1186             {
1187             # reached one stop position?
1188 13830 100 100     49377 if ($x == $stop[$i] && $y == $stop[$i+1])
1189             {
1190 240 100       1746 $closed->{$key}->[4] += $stop[$i+2] if defined $stop[$i+2];
1191             # store the reached stop position if it is known
1192 240 100       962 if ($per_field > 3)
    50          
1193             {
1194 12         52 $closed->{$key}->[6] = $stop[$i+3];
1195 12         32 $closed->{$key}->[7] = $stop[$i+4];
1196 12 50       42 print STDERR "# Reached stop position $x,$y (lx,ly $stop[$i+3], $stop[$i+4])\n" if $self->{debug} > 1;
1197             }
1198             elsif ($self->{debug} > 1) {
1199 0         0 print STDERR "# Reached stop position $x,$y\n";
1200             }
1201 240         938 last STEP;
1202             }
1203             } # end test for stop postion(s)
1204              
1205 3202 50 33     14256 $self->_croak("On of '$x,$y' is not defined")
1206             unless defined $x && defined $y;
1207            
1208             # get list of potential positions we need to explore from the current one
1209 3202         8760 my @p = $self->_astar_near_nodes($x,$y, $cells, $closed, $min_x, $min_y, $max_x, $max_y);
1210              
1211 3202         4643 my $n = 0;
1212 3202         7117 while ($n < scalar @p)
1213             {
1214 5666         7804 my $nx = $p[$n]; my $ny = $p[$n+1]; $n += 2;
  5666         7551  
  5666         6234  
1215              
1216 5666 50 33     18869 if (!defined $nx || !defined $ny)
1217             {
1218 0         0 require Carp;
1219 0         0 Carp::confess("On of '$nx,$ny' is not defined");
1220             }
1221 5666         7275 my $lg = $g;
1222 5666 50 33     28044 $lg += _astar_modifier($px,$py,$x,$y,$nx,$ny,$cells) if defined $px && defined $py;
1223              
1224 5666         9526 my $n = "$nx,$ny";
1225              
1226             # was already open?
1227 5666 100       17021 next if (exists $open_by_pos->{$n});
1228              
1229             # print STDERR "# Already open pos $nx,$ny with $open_by_pos->{$n} (would be $lg)\n"
1230             # if $self->{debug} && exists $open_by_pos->{$n};
1231             #
1232             # next if exists $open_by_pos->{$n} && $open_by_pos->{$n} <= $lg;
1233             #
1234             # if (exists $open_by_pos->{$n})
1235             # {
1236             # $open->delete($nx, $ny);
1237             # }
1238              
1239             # calculate distance to each possible stop position, and
1240             # use the lowest one
1241 4100         8981 my $lowest_distance = _astar_distance($nx, $ny, $stop[0], $stop[1]);
1242 4100         9239 for (my $i = $per_field; $i < $stop; $i += $per_field)
1243             {
1244 12644         25154 my $d = _astar_distance($nx, $ny, $stop[$i], $stop[$i+1]);
1245 12644 100       40399 $lowest_distance = $d if $d < $lowest_distance;
1246             }
1247              
1248 4100 50       10357 print STDERR "# Opening pos $nx,$ny ($lowest_distance + $lg)\n" if $self->{debug} > 1;
1249              
1250             # open new position into OPEN
1251 4100         21068 $open->add( [ $lowest_distance + $lg, $nx, $ny, $x, $y, undef ] );
1252 4100         22644 $open_by_pos->{$n} = $lg;
1253             }
1254             }
1255              
1256             ###########################################################################
1257             # A* is done, now build a path from the information we computed above:
1258              
1259             # count how many steps we did in A*
1260 242         682 $self->{stats}->{astar_steps} += $tries;
1261              
1262             # no more nodes to follow, so we couldn't find a path
1263 242 100       702 if (!defined $elem)
1264             {
1265 2 50       11 print STDERR "# A* couldn't find a path after $max_tries steps.\n" if $self->{debug};
1266 2         265 return [];
1267             }
1268              
1269 240         529 my $path = [];
1270 240         768 my ($cx,$cy) = ($elem->[1],$elem->[2]);
1271             # the "last" cell in the path. Since we follow it backwards, it
1272             # becomes actually the next cell
1273 240         583 my ($lx,$ly);
1274 0         0 my $type;
1275              
1276 240         310 my $label_cell = 0; # found a cell to attach the label to?
1277              
1278 240         336 my @bends; # record all bends in the path to straighten it out
1279              
1280 240         391 my $idx = 0;
1281             # follow $elem back to the source to find the path
1282 240         667 while (defined $cx)
1283             {
1284 1155 50       2930 last unless exists $closed->{"$cx,$cy"};
1285 1155         1688 my $xy = "$cx,$cy";
1286              
1287 1155         2007 $type = $closed->{$xy}->[ 4 ];
1288              
1289 1155         1277 my ($px,$py) = @{ $closed->{$xy} }; # get X,Y of parent cell
  1155         2518  
1290              
1291 1155   100     3860 my $edge_type = ($type||0) & EDGE_TYPE_MASK;
1292 1155 50       2455 if ($edge_type == 0)
1293             {
1294 1155   100     3235 my $edge_flags = ($type||0) & EDGE_FLAG_MASK;
1295              
1296             # either a start or a stop cell
1297 1155 50       2394 if (!defined $px)
1298             {
1299             # We can figure it out from the flag of the position of cx,cy
1300             # ................
1301             # : EDGE_START_S :
1302             # .......................................
1303             # START_E : px,py : EDGE_START_W :
1304             # .......................................
1305             # : EDGE_START_N :
1306             # ................
1307 0         0 ($px,$py) = ($cx, $cy); # start with same cell
1308 0 0       0 $py ++ if ($edge_flags & EDGE_START_S) != 0;
1309 0 0       0 $py -- if ($edge_flags & EDGE_START_N) != 0;
1310              
1311 0 0       0 $px ++ if ($edge_flags & EDGE_START_E) != 0;
1312 0 0       0 $px -- if ($edge_flags & EDGE_START_W) != 0;
1313             }
1314              
1315             # if lx, ly is undefined because px,py is a joint, get it via the stored
1316             # x,y pos of the very last cell in the path
1317 1155 100       2299 if (!defined $lx)
1318             {
1319 240         530 $lx = $closed->{$xy}->[6];
1320 240         472 $ly = $closed->{$xy}->[7];
1321             }
1322              
1323             # still not known?
1324 1155 100       2205 if (!defined $lx)
1325             {
1326              
1327             # If lx,ly is undefined because we are at the end of the path,
1328             # we can figure out from the flag of the position of cx,cy.
1329             # ..............
1330             # : EDGE_END_S :
1331             # .................................
1332             # END_E : lx,ly : EDGE_END_W :
1333             # .................................
1334             # : EDGE_END_N :
1335             # ..............
1336 228         376 ($lx,$ly) = ($cx, $cy); # start with same cell
1337              
1338 228 100       642 $ly ++ if ($edge_flags & EDGE_END_S) != 0;
1339 228 100       599 $ly -- if ($edge_flags & EDGE_END_N) != 0;
1340              
1341 228 100       589 $lx ++ if ($edge_flags & EDGE_END_E) != 0;
1342 228 100       560 $lx -- if ($edge_flags & EDGE_END_W) != 0;
1343             }
1344              
1345             # now figure out correct type for this cell from positions of
1346             # parent/following cell
1347 1155         2943 $type += _astar_edge_type($px, $py, $cx, $cy, $lx,$ly);
1348             }
1349              
1350 1155 50       3489 print STDERR "# Following back from $lx,$ly over $cx,$cy to $px,$py\n" if $self->{debug} > 1;
1351              
1352 1155 0 66     3378 if ($px == $lx && $py == $ly && ($cx != $lx || $cy != $ly))
      0        
      33        
1353             {
1354 0 0       0 print STDERR
1355             "# Warning: A* detected loop in path-backtracking at $px,$py, $cx,$cy, $lx,$ly\n"
1356             if $self->{debug};
1357 0         0 last;
1358             }
1359              
1360 1155 50       2563 $type = EDGE_HOR if ($type & EDGE_TYPE_MASK) == 0; # last resort
1361              
1362             # if this is the first hor edge, attach the label to it
1363             # XXX TODO: This clearly is not optimal. Look for left-most HOR CELL
1364 1155         1398 my $t = $type & EDGE_TYPE_MASK;
1365              
1366             # Do not put the label on crossings:
1367 1155 100 66     5130 if ($label_cell == 0 && (!exists $cells->{"$cx,$cy"}) && ($t == EDGE_HOR || $t == EDGE_VER))
      100        
      66        
1368             {
1369 239         342 $label_cell++;
1370 239         3987 $type += EDGE_LABEL_CELL;
1371             }
1372              
1373 1155 100 100     15521 push @bends, [ $type, $cx, $cy, -$idx ]
      100        
      100        
1374             if ($type == EDGE_S_E || $t == EDGE_S_W || $t == EDGE_N_E || $t == EDGE_N_W);
1375              
1376 1155         3242 unshift @$path, $cx, $cy, $type; # unshift to reverse the path
1377              
1378 1155 100       4978 last if $closed->{"$cx,$cy"}->[ 5 ]; # stop here?
1379              
1380 915         1568 ($lx,$ly) = ($cx,$cy);
1381 915         1128 ($cx,$cy) = @{ $closed->{"$cx,$cy"} }; # get X,Y of next cell
  915         4773  
1382              
1383 915         2611 $idx += 3; # index into $path (for bends)
1384             }
1385              
1386 240 50 66     1916 print STDERR "# Trying to straighten path\n" if @bends >= 3 && $self->{debug};
1387              
1388             # try to straighten unnec. inward bends
1389 240 100       617 $self->_straighten_path($path, \@bends, $edge) if @bends >= 3;
1390              
1391 240 50       1012 return ($path,$closed,$open_by_pos) if wantarray;
1392 240         6989 $path;
1393             }
1394              
1395             # 1:
1396             # | |
1397             # +----+ => |
1398             # | |
1399             # ----+ ------+
1400              
1401             # 2:
1402             # +--- +------
1403             # | |
1404             # +---+ => |
1405             # | |
1406              
1407             # 3:
1408             # ----+ ------+
1409             # | => |
1410             # +----+ |
1411             # | |
1412              
1413             # 4:
1414             # | |
1415             # +---+ |
1416             # | => |
1417             # +----+ +------
1418              
1419             my $bend_patterns = [
1420              
1421             # The patterns are duplicated to catch both directions of the path:
1422              
1423             # First five entries must match
1424             # dx, dy,
1425             # coordinates for new edge
1426             # (2 == y, 1 == x, first is
1427             # taken from A, second from B)
1428             # these replace the first & last bend
1429             # 1:
1430             [ EDGE_N_W, EDGE_S_E, EDGE_N_W, 0, -1, 2, 1, EDGE_HOR, EDGE_VER, 1,0, 0,-1 ], # 0
1431             [ EDGE_N_W, EDGE_S_E, EDGE_N_W, -1, 0, 1, 2, EDGE_VER, EDGE_HOR, 0,1, -1,0 ], # 1
1432              
1433             # 2:
1434             [ EDGE_S_E, EDGE_N_W, EDGE_S_E, 0, -1, 1, 2, EDGE_VER, EDGE_HOR, 0,-1, 1,0 ], # 2
1435             [ EDGE_S_E, EDGE_N_W, EDGE_S_E, -1, 0, 2, 1, EDGE_HOR, EDGE_VER, -1,0, 0,1 ], # 3
1436              
1437             # 3:
1438             [ EDGE_S_W, EDGE_N_E, EDGE_S_W, 0, 1, 2, 1, EDGE_HOR, EDGE_VER, 1,0, 0,1 ], # 4
1439             [ EDGE_S_W, EDGE_N_E, EDGE_S_W, -1, 0, 1, 2, EDGE_VER, EDGE_HOR, 0,-1, -1,0 ], # 5
1440              
1441             # 4:
1442             [ EDGE_N_E, EDGE_S_W, EDGE_N_E, 1, 0, 1, 2, EDGE_VER, EDGE_HOR, 0,1, 1,0 ], # 6
1443             [ EDGE_N_E, EDGE_S_W, EDGE_N_E, 0, -1, 2, 1, EDGE_HOR, EDGE_VER, -1,0, 0,-1 ], # 7
1444              
1445             ];
1446              
1447             sub _straighten_path
1448             {
1449 8     8   26 my ($self, $path, $bends, $edge) = @_;
1450              
1451             # XXX TODO:
1452             # in case of multiple bends, removes only one of them due to overlap
1453              
1454 8         24 my $cells = $self->{cells};
1455              
1456 8         17 my $i = 0;
1457             BEND:
1458 8         38 while ($i < (scalar @$bends - 2))
1459             {
1460             # for each bend, check it and the next two bends
1461              
1462             # print STDERR "Checking bend $i at $bends->[$i], $bends->[$i+1], $bends->[$i+2]\n";
1463              
1464 10         35 my ($a,$b,$c) = ($bends->[$i],
1465             $bends->[$i+1],
1466             $bends->[$i+2]);
1467              
1468 10         27 my $dx = ($b->[1] - $a->[1]);
1469 10         21 my $dy = ($b->[2] - $a->[2]);
1470              
1471 10         17 my $p = 0;
1472 10         30 for my $pattern (@$bend_patterns)
1473             {
1474 80         82 $p++;
1475 80 0 100     309 next if ($a->[0] != $pattern->[0]) ||
      66        
      33        
      33        
1476             ($b->[0] != $pattern->[1]) ||
1477             ($c->[0] != $pattern->[2]) ||
1478             ($dx != $pattern->[3]) ||
1479             ($dy != $pattern->[4]);
1480              
1481             # pattern matched
1482             # print STDERR "# Got bends for pattern ", $p-1," (@$pattern):\n";
1483             # print STDERR "# type x,y,\n# @$a\n# @$b\n# @$c\n";
1484              
1485             # check that the alternative path is empty
1486              
1487             # new corner:
1488 0         0 my $cx = $a->[$pattern->[5]];
1489 0         0 my $cy = $c->[$pattern->[6]];
1490 0 0       0 ($cx,$cy) = ($cy,$cx) if $pattern->[5] == 2; # need to swap?
1491              
1492 0 0       0 next BEND if exists $cells->{"$cx,$cy"};
1493              
1494             # print STDERR "# new corner at $cx,$cy (swap: $pattern->[5])\n";
1495              
1496             # check from A to new corner
1497 0         0 my $x = $a->[1];
1498 0         0 my $y = $a->[2];
1499              
1500 0         0 my @replace = ();
1501 0 0 0     0 push @replace, $cx, $cy, $pattern->[0] if ($x == $cx && $y == $cy);
1502              
1503 0         0 my $ddx = $pattern->[9];
1504 0         0 my $ddy = $pattern->[10];
1505             # print STDERR "# dx,dy: $ddx,$ddy\n";
1506 0   0     0 while ($x != $cx || $y != $cy)
1507             {
1508 0 0       0 next BEND if exists $cells->{"$x,$y"};
1509             # print STDERR "# at $x $y (go to $cx,$cy)\n"; sleep(1);
1510 0         0 push @replace, $x, $y, $pattern->[7];
1511 0         0 $x += $ddx;
1512 0         0 $y += $ddy;
1513             }
1514              
1515 0         0 $x = $cx; $y = $cy;
  0         0  
1516              
1517             # check from new corner to C
1518 0         0 $ddx = $pattern->[11];
1519 0         0 $ddy = $pattern->[12];
1520 0   0     0 while ($x != $c->[1] || $y != $c->[2])
1521             {
1522 0 0       0 next BEND if exists $cells->{"$x,$y"};
1523             # print STDERR "# at $x $y (go to $cx,$cy)\n"; sleep(1);
1524 0         0 push @replace, $x, $y, $pattern->[8];
1525            
1526             # set the correct type on the corner
1527 0 0 0     0 $replace[-1] = $pattern->[0] if ($x == $cx && $y == $cy);
1528 0         0 $x += $ddx;
1529 0         0 $y += $ddy;
1530             }
1531             # insert Corner
1532 0         0 push @replace, $x, $y, $pattern->[8];
1533              
1534             # use Data::Dumper; print STDERR Dumper(@replace);
1535             # print STDERR "# generated ", scalar @replace, " entries\n";
1536             # print STDERR "# idx A $a->[3] C $c->[3]\n";
1537              
1538             # the path is clear, so replace the inward bend with the new one
1539 0 0       0 my $diff = $a->[3] - $c->[3] ? -3 : 3;
1540              
1541 0         0 my $idx = 0; my $p_idx = $a->[3] + $diff;
  0         0  
1542 0         0 while ($idx < @replace)
1543             {
1544             # print STDERR "# replace $p_idx .. $p_idx + 2\n";
1545             # print STDERR "# replace $path->[$p_idx] with $replace[$idx]\n";
1546             # print STDERR "# replace $path->[$p_idx+1] with $replace[$idx+1]\n";
1547             # print STDERR "# replace $path->[$p_idx+2] with $replace[$idx+2]\n";
1548              
1549 0         0 $path->[$p_idx] = $replace[$idx];
1550 0         0 $path->[$p_idx+1] = $replace[$idx+1];
1551 0         0 $path->[$p_idx+2] = $replace[$idx+2];
1552 0         0 $p_idx += $diff;
1553 0         0 $idx += 3;
1554             }
1555             } # end for this pattern
1556              
1557 10         44 } continue { $i++; };
1558             }
1559              
1560             sub _map_as_html
1561             {
1562 0     0     my ($self, $cells, $p, $closed, $open, $w, $h) = @_;
1563              
1564 0   0       $w ||= 20;
1565 0   0       $h ||= 20;
1566              
1567 0           my $html = <
1568            
1569            
1570            
1571            
1591            
1592            
1593              
1594            

A* Map

1595              
1596            

1597             Nodes examined: ##closed##
1598             Nodes still to do (open): ##open##
1599             Nodes in path: ##path##
1600            

1601             EOF
1602             ;
1603              
1604 0           $html =~ s/##closed##/keys %$closed /eg;
  0            
1605 0           $html =~ s/##open##/keys %$open /eg;
  0            
1606 0           my $path = {};
1607 0           while (@$p)
1608             {
1609 0           my $x = shift @$p;
1610 0           my $y = shift @$p;
1611 0           my $t = shift @$p;
1612 0           $path->{"$x,$y"} = undef;
1613             }
1614 0           $html =~ s/##path##/keys %$path /eg;
  0            
1615 0           $html .= '' . "\n"; \n"; \n" and next if \n" and next if \n" and next unless \n"; \n";
1616              
1617 0           for my $y (0..$h)
1618             {
1619 0           $html .= "
1620 0           for my $x (0..$w)
1621             {
1622 0           my $xy = "$x,$y";
1623 0           my $c = ' ' x 4;
1624 0 0 0       $html .= " $c
      0        
1625             exists $cells->{$xy} and ref($cells->{$xy}) =~ /Node/;
1626 0 0 0       $html .= " $c
      0        
1627             exists $cells->{$xy} && !exists $path->{$xy};
1628              
1629 0 0 0       $html .= " $c
      0        
1630             exists $closed->{$xy} ||
1631             exists $open->{$xy};
1632              
1633 0           my $clr = '#a0a0a0';
1634 0 0         if (exists $closed->{$xy})
    0          
1635             {
1636 0   0       $c = ($closed->{$xy}->[3] || '0') . '+' . ($closed->{$xy}->[2] || '0');
      0        
1637 0   0       my $color = 0x10 + 8 * (($closed->{$xy}->[2] || 0));
1638 0   0       my $color2 = 0x10 + 8 * (($closed->{$xy}->[3] || 0));
1639 0           $clr = sprintf("%02x%02x",$color,$color2) . 'a0';
1640             }
1641             elsif (exists $open->{$xy})
1642             {
1643 0   0       $c = ' ' . $open->{$xy} || '0';
1644 0   0       my $color = 0xff - 8 * ($open->{$xy} || 0);
1645 0           $clr = 'a0' . sprintf("%02x",$color) . '00';
1646             }
1647 0           my $b = '';
1648 0 0         $b = 'border: 2px white solid;' if exists $path->{$xy};
1649 0           $html .= " $c
1650             }
1651 0           $html .= "
1652             }
1653            
1654 0           $html .= "\n
\n";
1655              
1656 0           $html;
1657             }
1658            
1659             1;
1660             __END__