File Coverage

lib/Graph/Easy/Layout/Scout.pm
Criterion Covered Total %
statement 529 632 83.7
branch 281 382 73.5
condition 137 216 63.4
subroutine 24 26 92.3
pod n/a
total 971 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.76';
10              
11             #############################################################################
12             #############################################################################
13              
14             package Graph::Easy;
15              
16 48     48   153 use strict;
  48         51  
  48         1197  
17 48     48   143 use warnings;
  48         54  
  48         973  
18 48     48   147 use Graph::Easy::Node::Cell;
  48         47  
  48         2172  
19 48         70470 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 48     48   162 /;
  48         49  
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   78 my ($self, $edge, $coords, $dx, $dy) = @_;
80              
81 59 100       131 return $coords if $edge->undirected();
82              
83             # there are two cases (for each dx and dy)
84 57         60 my $i = 0; # index 0,1
85 57         53 my $co = 2;
86 57         41 my $case;
87              
88 57         90 for my $d ($dx,$dy,$dx,$dy)
89             {
90 228 100       263 next if $d == 0;
91              
92 204         186 my $type = $coords->[$co] & EDGE_TYPE_MASK;
93              
94 204 100       122 $case = 0; $case = 1 if $d == -1;
  204         249  
95              
96             # modify first/last cell
97 204         253 my $t = $start_points->{ $type }->[ $case + $i ];
98             # on bidirectional edges, turn START_X into END_X
99 204 100 100     292 $t = $start_to_end->{$t} || $t if $edge->{bidirectional};
100              
101 204         184 $coords->[$co] += $t;
102              
103             } continue {
104 228         162 $i += 2; # index 2,3, 4,5 etc
105 228 100       370 $co = -1 if $i == 4; # modify now last cell
106             }
107 57         152 $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   3095 my ($self, $src, $dst, $edge) = @_;
116              
117             # one node pointing back to itself?
118 905 100       1462 if ($src == $dst)
119             {
120 31         67 my $rc = $self->_find_path_loop($src,$edge);
121 31 50       111 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     1860 return $self->_find_path_astar($edge)
      100        
127             if ($src->is_multicelled() || $dst->is_multicelled() || $edge->has_ports());
128              
129 742         1365 my ($x0, $y0) = ($src->{x}, $src->{y});
130 742         981 my ($x1, $y1) = ($dst->{x}, $dst->{y});
131 742         877 my $dx = ($x1 - $x0) <=> 0;
132 742         727 my $dy = ($y1 - $y0) <=> 0;
133              
134 742         669 my $cells = $self->{cells};
135 742         539 my @coords;
136 742         634 my ($x,$y) = ($x0,$y0); # starting pos
137              
138             ###########################################################################
139             # below follow some shortcuts for easy things like straight paths:
140              
141 742 50       1227 print STDERR "# dx,dy: $dx,$dy\n" if $self->{debug};
142              
143 742 100 100     2131 if ($dx == 0 || $dy == 0)
144             {
145             # try straight path to target:
146              
147 679 50       1097 print STDERR "# $src->{x},$src->{y} => $dst->{x},$dst->{y} - trying short path\n" if $self->{debug};
148              
149             # distance to node:
150 679         716 my $dx1 = ($x1 - $x0);
151 679         544 my $dy1 = ($y1 - $y0);
152 679         798 ($x,$y) = ($x0+$dx,$y0+$dy); # starting pos
153              
154 679 100 100     1818 if ((abs($dx1) == 2) || (abs($dy1) == 2))
155             {
156 579 100       1354 if (!exists ($cells->{"$x,$y"}))
157             {
158             # a single step for this edge:
159 567         547 my $type = EDGE_LABEL_CELL;
160             # short path
161 567 100       1287 if ($edge->bidirectional())
    100          
162             {
163 5 100       11 $type += EDGE_SHORT_BD_EW if $dy == 0;
164 5 100       13 $type += EDGE_SHORT_BD_NS if $dx == 0;
165             }
166             elsif ($edge->undirected())
167             {
168 13 100       30 $type += EDGE_SHORT_UN_EW if $dy == 0;
169 13 100       24 $type += EDGE_SHORT_UN_NS if $dx == 0;
170             }
171             else
172             {
173 549 100 66     1445 $type += EDGE_SHORT_E if ($dx == 1 && $dy == 0);
174 549 100 100     1324 $type += EDGE_SHORT_S if ($dx == 0 && $dy == 1);
175 549 100 66     1103 $type += EDGE_SHORT_W if ($dx == -1 && $dy == 0);
176 549 100 100     1240 $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     1179 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       11 if ($dx > 0)
    50          
184             {
185 3         5 $type &= ~EDGE_START_E;
186             }
187             elsif ($dx < 0)
188             {
189 0         0 $type &= ~EDGE_START_W;
190             }
191             }
192 567 100 50     1158 if (($edge->{from}->attribute('shape') ||'') eq 'edge')
193             {
194 3         4 $type &= ~EDGE_START_MASK;
195             }
196              
197 567         1858 return [ $x, $y, $type ]; # return a short EDGE
198             }
199             }
200              
201 112 100       140 my $type = EDGE_HOR; $type = EDGE_VER if $dx == 0; # - or |
  112         221  
202 112         118 my $done = 0;
203 112         114 my $label_done = 0;
204 112         114 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       570 $done = 1, last if exists $cells->{"$x,$y"}; # cell already full
209              
210             # the first cell gets the label
211 181 100       117 my $t = $type; $t += EDGE_LABEL_CELL if $label_done++ == 0;
  181         223  
212              
213 181         215 push @coords, $x, $y, $t; # good one, is free
214 181         121 $x += $dx; $y += $dy; # next field
  181         106  
215 181 100 100     331 last if ($x == $x1) && ($y == $y1);
216             }
217              
218 112 100       232 if ($done == 0)
219             {
220 11 50       24 print STDERR "# success for ", scalar @coords / 3, " steps in path\n" if $self->{debug};
221             # return all fields of path
222 11         48 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         67 my $done = 0;
240              
241 63 50       123 print STDERR "# bend path from $x,$y\n" if $self->{debug};
242              
243             # try hor => ver
244 63         81 my $type = EDGE_HOR;
245              
246 63         55 my $label = 0; # attach label?
247 63 50 50     260 $label = 1 if ref($edge) && ($edge->label()||'') eq ''; # no label?
      33        
248 63         72 $x += $dx;
249 63         127 while ($x != $x1)
250             {
251 67 100       178 $done++, last if exists $cells->{"$x,$y"}; # cell already full
252 51 50       85 print STDERR "# at $x,$y\n" if $self->{debug};
253 51 50       49 my $t = $type; $t += EDGE_LABEL_CELL if $label++ == 0;
  51         83  
254 51         76 push @coords, $x, $y, $t; # good one, is free
255 51         86 $x += $dx; # next field
256             };
257              
258             # check the bend itself
259 63 100       154 $done++ if exists $cells->{"$x,$y"}; # cell already full
260              
261 63 100       114 if ($done == 0)
262             {
263 27         75 my $type_bend = _astar_edge_type ($x-$dx,$y, $x,$y, $x,$y+$dy);
264              
265 27         52 push @coords, $x, $y, $type_bend; # put in bend
266 27 50       59 print STDERR "# at $x,$y\n" if $self->{debug};
267 27         26 $y += $dy;
268 27         26 $type = EDGE_VER;
269 27         54 while ($y != $y1)
270             {
271 19 50       42 $done++, last if exists $cells->{"$x,$y"}; # cell already full
272 19 50       34 print STDERR "# at $x,$y\n" if $self->{debug};
273 19         36 push @coords, $x, $y, $type; # good one, is free
274 19         36 $y += $dy;
275             }
276             }
277              
278 63 100       122 if ($done != 0)
279             {
280 36         34 $done = 0;
281             # try ver => hor
282 36 50       68 print STDERR "# hm, now trying first vertical, then horizontal\n" if $self->{debug};
283 36         41 $type = EDGE_VER;
284              
285 36         50 @coords = (); # drop old version
286 36         49 ($x,$y) = ($x0, $y0 + $dy); # starting pos
287 36         67 while ($y != $y1)
288             {
289 67 100       123 $done++, last if exists $cells->{"$x,$y"}; # cell already full
290 59 50       90 print STDERR "# at $x,$y\n" if $self->{debug};
291 59         92 push @coords, $x, $y, $type; # good one, is free
292 59         83 $y += $dy; # next field
293             };
294              
295             # check the bend itself
296 36 100       89 $done++ if exists $cells->{"$x,$y"}; # cell already full
297              
298 36 100       74 if ($done == 0)
299             {
300 25         76 my $type_bend = _astar_edge_type ($x,$y-$dy, $x,$y, $x+$dx,$y);
301              
302 25         47 push @coords, $x, $y, $type_bend; # put in bend
303 25 50       54 print STDERR "# at $x,$y\n" if $self->{debug};
304 25         25 $x += $dx;
305 25         30 my $label = 0; # attach label?
306 25 50       58 $label = 1 if $edge->label() eq ''; # no label?
307 25         31 $type = EDGE_HOR;
308 25         49 while ($x != $x1)
309             {
310 31 100       75 $done++, last if exists $cells->{"$x,$y"}; # cell already full
311 27 50       50 print STDERR "# at $x,$y\n" if $self->{debug};
312 27 50       31 my $t = $type; $t += EDGE_LABEL_CELL if $label++ == 0;
  27         49  
313 27         47 push @coords, $x, $y, $t; # good one, is free
314 27         58 $x += $dx;
315             }
316             }
317             }
318              
319 63 100       117 if ($done == 0)
320             {
321 48 50       80 print STDERR "# success for ", scalar @coords / 3, " steps in path\n" if $self->{debug};
322             # return all fields of path
323 48         124 return $self->_end_points($edge, \@coords, $dx, $dy);
324             }
325              
326 15 50       42 print STDERR "# no success\n" if $self->{debug};
327              
328             } # end path with $dx and $dy
329              
330 116         370 $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   37 my ($self, $src, $edge) = @_;
337              
338 31 50       70 print STDERR "# Finding looping path from $src->{name} to $src->{name}\n" if $self->{debug};
339              
340 31         35 my ($n, $cells, $d, $type, $loose) = @_;
341              
342             # get a list of all places
343              
344             my @places = $src->_near_places(
345 31         113 $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         89 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         47 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       69 $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       61 $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       63 $index = [
383             EDGE_LOOP_EAST,
384             EDGE_LOOP_WEST,
385             EDGE_LOOP_NORTH,
386             EDGE_LOOP_SOUTH,
387             ] if $flow == 180;
388              
389 31         48 for my $this_try (@$index)
390             {
391 59         42 my $idx = 0;
392 59         98 while ($idx < @places)
393             {
394 114 50       157 print STDERR "# Trying $places[$idx+0],$places[$idx+1]\n" if $self->{debug};
395 114 100       241 next unless $places[$idx+2] == $this_try;
396              
397             # build a path from the returned piece
398 31         74 my @rc = ($places[$idx], $places[$idx+1], $places[$idx+2]);
399              
400 31 50       66 print STDERR "# Trying $rc[0],$rc[1]\n" if $self->{debug};
401              
402 31 50       81 next unless $self->_path_is_clear(\@rc);
403              
404 31 50       58 print STDERR "# Found looping path\n" if $self->{debug};
405 31         103 return \@rc;
406 83         126 } 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 48     48   234 use strict;
  48         142  
  48         52950  
422              
423             sub _init
424             {
425 745     745   835 my ($self,$args) = @_;
426              
427 745         1185 $self->{_heap} = [ ];
428              
429 745         1256 $self;
430             }
431              
432             sub add
433             {
434             # add one element to the heap
435 7154     7154   6279 my ($self,$elem) = @_;
436              
437 7154         5544 my $heap = $self->{_heap};
438              
439             # heap empty?
440 7154 100       14015 if (@$heap == 0)
    100          
    100          
441             {
442 1262         1513 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         1720 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         919 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         2792 my $el = $elem->[0];
462 3532 100       4029 if (scalar @$heap < 10)
463             {
464 1910         1303 my $i = 0;
465 1910         2044 for my $e (@$heap)
466             {
467 5808 100       7248 if ($e->[0] > $el)
468             {
469 862         1222 splice (@$heap, $i, 0, $elem); # insert $elem
470 862         1294 return undef;
471             }
472 4946         3630 $i++;
473             }
474             # else, append at the end
475 1048         1184 push @$heap, $elem;
476             }
477             else
478             {
479             # use binary search
480 1622         1328 my $l = 0; my $r = scalar @$heap;
  1622         1050  
481 1622         2323 while (($r - $l) > 2)
482             {
483 7550         6062 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       7198 if ($heap->[$m]->[0] <= $el)
486             {
487 5040         6114 $l = $m;
488             }
489             else
490             {
491 2510         3199 $r = $m;
492             }
493             }
494 1622         1999 while ($l < @$heap)
495             {
496 3951 100       4546 if ($heap->[$l]->[0] > $el)
497             {
498 1438         1581 splice (@$heap, $l, 0, $elem); # insert $elem
499 1438         1391 return undef;
500             }
501 2513         2792 $l++;
502             }
503             # else, append at the end
504 184         214 push @$heap, $elem;
505             }
506             }
507 4854         6566 undef;
508             }
509              
510             sub elements
511             {
512 523     523   944 scalar @{$_[0]->{_heap}};
  523         1777  
513             }
514              
515             sub extract_top
516             {
517             # remove and return the top elemt
518 6131     6131   14583 shift @{$_[0]->{_heap}};
  6131         13524  
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   567 my ($self) = shift;
545              
546 502         796 $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   6363 my ($x1,$y1,$x,$y,$px,$py, $cells) = @_;
561              
562 6564         3985 my $add = 1;
563              
564 6564 50       7509 if (defined $x1)
565             {
566 6564         5779 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     13846 $add += 30 if ref($cells->{$xy}) && $cells->{$xy}->isa('Graph::Easy::Edge');
570             }
571              
572 6564 100       7471 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         5268 my $dx1 = ($px-$x) <=> 0;
578 6563         4607 my $dy1 = ($py-$y) <=> 0;
579 6563         4511 my $dx2 = ($x-$x1) <=> 0;
580 6563         4261 my $dy2 = ($y-$y1) <=> 0;
581 6563 100 100     13944 $add += 6 unless $dx1 == $dx2 || $dy1 == $dy2;
582             }
583 6564         6359 $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   14880 my $dx = abs($_[2] - $_[0]);
592 20860         13843 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     49203 $dx++ if $dx != 0 && $dy != 0;
596              
597 20860         16418 $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   1212 my ($x,$y, $x1,$y1, $x2, $y2) = @_;
628              
629 1219         1017 my $dx1 = ($x1 - $x) <=> 0;
630 1219         859 my $dy1 = ($y1 - $y) <=> 0;
631              
632 1219         803 my $dx2 = ($x2 - $x1) <=> 0;
633 1219         835 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     2537 ($dx2,$dy2) = ($dx1,$dy1) if $dx2 == 0 && $dy2 == 0;
637             # can this case happen?
638 1219 50 66     2361 ($dx1,$dy1) = ($dx2,$dy2) if $dx1 == 0 && $dy1 == 0;
639              
640             # return correct type depending on differences
641 1219 50       3393 $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   3360 my ($self, $nx, $ny, $cells, $closed, $min_x, $min_y, $max_x, $max_y) = @_;
648              
649 3202         2701 my @places = ();
650              
651 3202         6055 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         2317 my $type = EDGE_CROSS;
660             # including flags, because only flagless edges may be crossed
661 3202 100       5511 $type = $cells->{"$nx,$ny"}->{type} if exists $cells->{"$nx,$ny"};
662 3202 100       5110 if ($type == EDGE_HOR)
    100          
663             {
664 258         545 @tries = (
665             $nx, $ny + 1, # down
666             $nx, $ny - 1, # up
667             );
668             }
669             elsif ($type == EDGE_VER)
670             {
671 71         164 @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         2169 my $i = 0;
682 3202         4305 while ($i < @tries)
683             {
684 12150         10529 my ($x,$y) = ($tries[$i], $tries[$i+1]);
685              
686 12150 50       14318 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     57195 next if $x < $min_x || $x > $max_x || $y < $min_y || $y > $max_y;
      100        
      100        
690              
691 10839         9229 my $p = "$x,$y";
692 10839 50       12122 print STDERR "# examining pos $p\n" if $self->{debug} > 2;
693              
694 10839 100       13062 next if exists $closed->{$p};
695              
696 6957 100 66     17850 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         976 my $type = $cells->{$p}->{type}; # including flags, because only flagless edges
700             # may be crossed
701              
702 956 100 100     2562 push @places, $x, $y if ($type == EDGE_HOR) || ($type == EDGE_VER);
703 956         803 next;
704             }
705 6001 100       6957 next if exists $cells->{$p}; # uncrossable cell
706              
707 5249         6533 push @places, $x, $y;
708              
709 12150         16181 } continue { $i += 2; }
710              
711 3202         8353 @places;
712             }
713              
714             sub _astar_boundaries
715             {
716             # Calculate boundaries for area that A* should not leave.
717 242     242   222 my $self = shift;
718              
719 242         282 my $cache = $self->{cache};
720              
721             return ( $cache->{min_x}-1, $cache->{min_y}-1,
722 242 100       879 $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         3 my $cells = $self->{cells};
727              
728 2         1 $min_x = 10000000;
729 2         2 $min_y = 10000000;
730 2         2 $max_x = -10000000;
731 2         1 $max_y = -10000000;
732              
733 2         7 for my $c (sort keys %$cells)
734             {
735 4         7 my ($x,$y) = split /,/, $c;
736 4 100       7 $min_x = $x if $x < $min_x;
737 4 100       6 $min_y = $y if $y < $min_y;
738 4 50       7 $max_x = $x if $x > $max_x;
739 4 100       7 $max_y = $y if $y > $max_y;
740             }
741              
742 2 50       3 print STDERR "# A* working space boundaries: $min_x, $min_y, $max_x, $max_y\n" if $self->{debug};
743              
744 2         7 ( $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         3 $min_x --; $min_y --; $max_x ++; $max_y ++;
  2         1  
  2         2  
  2         1  
749 2         4 ($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 48     48   245 use Graph::Easy::Util qw(ord_values);
  48         55  
  48         148021  
777              
778             sub _get_joints
779             {
780             # from a list of shared, already placed edges, get possible start/end fields
781 25     25   31 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         33 for my $e (@$shared)
787             {
788 41         26 for my $c (@{$e->{cells}})
  41         60  
789             {
790 110         94 my $type = $c->{type} & EDGE_TYPE_MASK;
791              
792 110 100       148 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             next if ( ($type == EDGE_HOR()) ||
798             ($type == EDGE_VER()) ) &&
799 94 100 100     380 ($c->{type} & $mask);
      100        
800              
801 61         42 my $fields = $next_fields->{$type};
802              
803 61         72 my ($px,$py) = ($c->{x},$c->{y});
804 61         42 my $i = 0;
805 61         88 while ($i < @$fields)
806             {
807 122         145 my ($sx,$sy, $jt) = ($fields->[$i], $fields->[$i+1], $fields->[$i+2]);
808 122         78 $sx += $px; $sy += $py; $i += 3;
  122         74  
  122         69  
809 122         117 my $sxsy = "$sx,$sy";
810             # don't add the field twice
811 122 100       147 next if exists $cells->{$sxsy};
812 116         236 $cells->{$sxsy} = [ $sx, $sy, undef, $px, $py ];
813             # keep eventually set start/end points on the original cell
814 116         242 $types->{$sxsy} = $jt + ($c->{type} & EDGE_FLAG_MASK);
815             }
816             }
817             }
818              
819 25         24 my @R;
820             # convert hash to array
821 25         54 for my $s (ord_values ( $cells ))
822             {
823 116         172 push @R, @$s;
824             }
825 25         173 @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   35 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         56 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             $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 31 100 66     110 ] if $end || $edge->{bidirectional};
855              
856 31         42 my $cells = $self->{cells};
857 31         85 my @places = $node->_near_places($cells, 1, # distance 1
858             $flags, 'loose');
859              
860 31         34 my $i = 0;
861 31         61 while ($i < @places)
862             {
863 108         118 my ($x,$y) = ($places[$i], $places[$i+1]); $i += 3;
  108         70  
864              
865 108 100       260 next unless exists $cells->{"$x,$y"}; # empty space?
866             # found some cell, check that it is a EDGE_HOR or EDGE_VER
867 13         24 my $cell = $cells->{"$x,$y"};
868 13 100       54 next unless $cell->isa('Graph::Easy::Edge::Cell');
869              
870 8         12 my $cell_type = $cell->{type} & EDGE_TYPE_MASK;
871              
872 8 50 66     22 next unless $cell_type == EDGE_HOR || $cell_type == EDGE_VER;
873              
874             # the cell must belong to one of the shared edges
875 8         8 my $e = $cell->{edge}; local $_;
  8         8  
876 8 100       10 next unless scalar grep { $e == $_ } @$shared;
  10         31  
877              
878             # make the cell at the current pos a joint
879 6         24 $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         18 Graph::Easy::Edge::Cell->new( type => EDGE_HOLE, edge => $edge, x => $x, y => $y );
884              
885 6         19 return []; # path is empty
886             }
887              
888 25         51 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   245 my ($self,$edge) = @_;
895              
896 248         568 my $cells = $self->{cells};
897 248         258 my $src = $edge->{from};
898 248         219 my $dst = $edge->{to};
899              
900 248 50       393 print STDERR "# A* from $src->{x},$src->{y} to $dst->{x},$dst->{y}\n" if $self->{debug};
901              
902 248         587 my $start_flags = [
903             EDGE_START_W,
904             EDGE_START_N,
905             EDGE_START_E,
906             EDGE_START_S,
907             ];
908              
909 248         524 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       605 if ( ($edge->{to}->attribute('shape')) eq 'edge')
918             {
919 0         0 $end_flags = [ 0,0,0,0 ];
920             }
921 248 50       598 if ( ($edge->{from}->attribute('shape')) eq 'edge')
922             {
923 0         0 $start_flags = [ 0,0,0,0 ];
924             }
925              
926 248         647 my ($s_p,@ss_p) = $edge->port('start');
927 248         549 my ($e_p,@ee_p) = $edge->port('end');
928 248         269 my (@A, @B); # Start/Stop positions
929 0         0 my @shared_start;
930 0         0 my @shared_end;
931              
932 248         284 my $joint_type = {};
933 248         232 my $joint_type_end = {};
934              
935 248         235 my $start_cells = {};
936 248         225 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     650 @shared_end = $edge->{to}->edges_at_port('end', $e_p, $ee_p[0]) if defined $e_p && @ee_p == 1;
943              
944 248         284 my @shared = ();
945             # filter out all non-placed edges (this will also filter out $edge)
946 248         335 for my $s (@shared_end)
947             {
948 84 100       68 push @shared, $s if @{$s->{cells}} > 0;
  84         149  
949             }
950              
951 248         288 my $per_field = 5; # for shared: x,y,undef, px,py
952 248 100       390 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             print STDERR "# edge from '$edge->{from}->{name}' to '$edge->{to}->{name}' shares end port with ",
958 18 50       38 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         52 my $path = $self->_join_edge($src,$edge,\@shared);
963 18 100       58 return $path if $path; # already done?
964              
965 12         30 @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         665 @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       555 @B = $dst->_allowed_places( \@B, $dst->_allow( $e_p, @ee_p ), 3)
974             if defined $e_p;
975              
976 230         244 $per_field = 3; # x,y,type
977             }
978              
979 242 50       423 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     647 @shared_start = $edge->{from}->edges_at_port('start', $s_p, $ss_p[0]) if defined $s_p && @ss_p == 1;
986              
987 242         242 @shared = ();
988             # filter out all non-placed edges (this will also filter out $edge)
989 242         331 for my $s (@shared_start)
990             {
991 62 100       38 push @shared, $s if @{$s->{cells}} > 0;
  62         120  
992             }
993              
994 242 100       383 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             print STDERR "# edge from '$edge->{from}->{name}' to '$edge->{to}->{name}' shares start port with ",
1000 13 50       26 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         30 my $path = $self->_join_edge($dst, $edge, \@shared, 'end');
1005 13 50       22 return $path if $path; # already done?
1006              
1007 13         32 @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       221 my $s = $start_flags; $s = $end_flags if $edge->{bidirectional};
  229         429  
1018 229         642 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       573 @start = $src->_allowed_places( \@start, $src->_allow( $s_p, @ss_p ), 3)
1022             if defined $s_p;
1023              
1024 229 50       429 return unless @start > 0; # no free slots on start node?
1025              
1026 229         206 my $i = 0;
1027 229         465 while ($i < scalar @start)
1028             {
1029 1112         929 my $sx = $start[$i]; my $sy = $start[$i+1]; my $type = $start[$i+2]; $i += 3;
  1112         953  
  1112         812  
  1112         661  
1030              
1031             # compute the field inside the node from where $sx,$sy is reached:
1032 1112         690 my $px = $sx; my $py = $sy;
  1112         655  
1033 1112 100 100     2844 if ($sy < $src->{y} || $sy >= $src->{y} + $src->{cy})
1034             {
1035 602 100       873 $py = $sy + 1 if $sy < $src->{y}; # above
1036 602 100       887 $py = $sy - 1 if $sy > $src->{y}; # below
1037             }
1038             else
1039             {
1040 510 100       752 $px = $sx + 1 if $sx < $src->{x}; # right
1041 510 100       755 $px = $sx - 1 if $sx > $src->{x}; # left
1042             }
1043              
1044 1112         2390 push @A, ($sx, $sy, $type, $px, $py);
1045             }
1046             }
1047              
1048             ###########################################################################
1049             # use A* to finally find the path:
1050              
1051 242         633 my $path = $self->_astar(\@A,\@B,$edge, $per_field);
1052              
1053 242 100 100     1139 if (@$path > 0 && keys %$start_cells > 0)
1054             {
1055             # convert the edge piece of the starting edge-cell to a joint
1056 13         23 my ($x, $y) = ($path->[0],$path->[1]);
1057 13         19 my $xy = "$x,$y";
1058 13         17 my ($sx,$sy,$t,$px,$py) = @{$start_cells->{$xy}};
  13         22  
1059              
1060 13         20 my $jt = $joint_type->{"$sx,$sy"};
1061 13         48 $cells->{"$px,$py"}->_make_joint($edge,$jt);
1062             }
1063              
1064 242 100 100     918 if (@$path > 0 && keys %$end_cells > 0)
1065             {
1066             # convert the edge piece of the starting edge-cell to a joint
1067 12         18 my ($x, $y) = ($path->[-3],$path->[-2]);
1068 12         22 my $xy = "$x,$y";
1069 12         12 my ($sx,$sy,$t,$px,$py) = @{$end_cells->{$xy}};
  12         27  
1070              
1071 12         17 my $jt = $joint_type_end->{"$sx,$sy"};
1072 12         50 $cells->{"$px,$py"}->_make_joint($edge,$jt);
1073             }
1074              
1075 242         1698 $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   289 my ($self, $A, $B, $edge, $per_field) = @_;
1083              
1084 242         821 my @start = @$A;
1085 242         717 my @stop = @$B;
1086 242         244 my $stop = scalar @stop;
1087              
1088 242         309 my $src = $edge->{from};
1089 242         235 my $dst = $edge->{to};
1090 242         273 my $cells = $self->{cells};
1091              
1092 242         667 my $open = Graph::Easy::Heap->new(); # to find smallest elem fast
1093 242         248 my $open_by_pos = {}; # to find open nodes by pos
1094 242         238 my $closed = {}; # to find closed nodes by pos
1095              
1096 242         197 my $elem;
1097              
1098             # The boundaries of objects in $cell, e.g. the area that the algorithm shall
1099             # never leave.
1100 242         503 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         212 my $tries = 0; my $max_tries = 2000000;
  242         211  
1104              
1105             # count how many times we did A*
1106 242         364 $self->{stats}->{astar}++;
1107              
1108             ###########################################################################
1109             ###########################################################################
1110             # put the start positions into OPEN
1111              
1112 242         202 my $i = 0; my $bias = 0;
  242         213  
1113 242         477 while ($i < scalar @start)
1114             {
1115 1172         1911 my ($sx,$sy,$type,$px,$py) =
1116             ($start[$i],$start[$i+1],$start[$i+2],$start[$i+3],$start[$i+4]);
1117 1172         821 $i += 5;
1118              
1119 1172         1346 my $cell = $cells->{"$sx,$sy"}; my $rcell = ref($cell);
  1172         897  
1120 1172 100 100     2341 next if $rcell && $rcell !~ /::Edge/;
1121              
1122 1161 100       773 my $t = 0; $t = $cell->{type} & EDGE_NO_M_MASK if $rcell =~ /::Edge/;
  1161         1889  
1123 1161 100 100     2903 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         656 my $lowest_x = $stop[0]; my $lowest_y = $stop[1];
  895         688  
1128 895         1236 my $lowest = _astar_distance($sx,$sy, $stop[0], $stop[1]);
1129 895         1524 for (my $u = $per_field; $u < $stop; $u += $per_field)
1130             {
1131 3218         3219 my $dist = _astar_distance($sx,$sy, $stop[$u], $stop[$u+1]);
1132 3218 100       4516 ($lowest_x, $lowest_y) = ($stop[$u],$stop[$u+1]) if $dist < $lowest;
1133 3218 100       5907 $lowest = $dist if $dist < $lowest;
1134             }
1135              
1136              
1137             # add a penalty for crossings
1138 895 100       723 my $malus = 0; $malus = 30 if $t != 0;
  895         1114  
1139 895         1055 $malus += _astar_modifier($px,$py, $sx, $sy, $sx, $sy);
1140 895         2347 $open->add( [ $lowest, $sx, $sy, $px, $py, $type, 1 ] );
1141              
1142 895         953 my $o = $malus + $bias + $lowest;
1143             print STDERR "# adding open pos $sx,$sy ($o = $malus + $bias + $lowest) at ($lowest_x,$lowest_y)\n"
1144 895 50       1315 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 preferred order east/south/west/north. Instead
1149             # the algorithm exploring both way and terminating arbitrarily on the one that
1150             # first hits the target, it will explore only one.
1151 895         1436 $open_by_pos->{"$sx,$sy"} = $o;
1152              
1153 895   50     2261 $bias += $self->{_astar_bias} || 0;
1154             }
1155              
1156             ###########################################################################
1157             ###########################################################################
1158             # main A* loop
1159              
1160 242         310 my $stats = $self->{stats};
1161              
1162             STEP:
1163 242         403 while( defined( $elem = $open->extract_top() ) )
1164             {
1165 3442 50       5636 $stats->{astar_steps}++ if $self->{debug};
1166              
1167             # hard limit on number of steps todo
1168 3442 50       4375 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             print STDERR "# Smallest elem from ", $open->elements(),
1175 3442 50       4169 " elems is: weight=", $elem->[0], " at $elem->[1],$elem->[2]\n" if $self->{debug} > 1;
1176 3442         3917 my ($val, $x,$y, $px,$py, $type, $do_stop) = @$elem;
1177              
1178 3442         2961 my $key = "$x,$y";
1179             # move node into CLOSE and remove from OPEN
1180 3442   50     5080 my $g = $open_by_pos->{$key} || 0;
1181 3442         6669 $closed->{$key} = [ $px, $py, $val - $g, $g, $type, $do_stop ];
1182 3442         3336 delete $open_by_pos->{$key};
1183              
1184             # we are done when we hit one of the potential stop positions
1185 3442         4916 for (my $i = 0; $i < $stop; $i += $per_field)
1186             {
1187             # reached one stop position?
1188 13830 100 100     28966 if ($x == $stop[$i] && $y == $stop[$i+1])
1189             {
1190 240 100       498 $closed->{$key}->[4] += $stop[$i+2] if defined $stop[$i+2];
1191             # store the reached stop position if it is known
1192 240 100       577 if ($per_field > 3)
    50          
1193             {
1194 12         26 $closed->{$key}->[6] = $stop[$i+3];
1195 12         15 $closed->{$key}->[7] = $stop[$i+4];
1196 12 50       25 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         428 last STEP;
1202             }
1203             } # end test for stop position(s)
1204              
1205 3202 50 33     8796 $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         4745 my @p = $self->_astar_near_nodes($x,$y, $cells, $closed, $min_x, $min_y, $max_x, $max_y);
1210              
1211 3202         2417 my $n = 0;
1212 3202         4200 while ($n < scalar @p)
1213             {
1214 5666         4659 my $nx = $p[$n]; my $ny = $p[$n+1]; $n += 2;
  5666         4174  
  5666         3473  
1215              
1216 5666 50 33     10919 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         3606 my $lg = $g;
1222 5666 50 33     17039 $lg += _astar_modifier($px,$py,$x,$y,$nx,$ny,$cells) if defined $px && defined $py;
1223              
1224 5666         5063 my $n = "$nx,$ny";
1225              
1226             # was already open?
1227 5666 100       9598 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         4408 my $lowest_distance = _astar_distance($nx, $ny, $stop[0], $stop[1]);
1242 4100         5741 for (my $i = $per_field; $i < $stop; $i += $per_field)
1243             {
1244 12644         12125 my $d = _astar_distance($nx, $ny, $stop[$i], $stop[$i+1]);
1245 12644 100       22847 $lowest_distance = $d if $d < $lowest_distance;
1246             }
1247              
1248 4100 50       5189 print STDERR "# Opening pos $nx,$ny ($lowest_distance + $lg)\n" if $self->{debug} > 1;
1249              
1250             # open new position into OPEN
1251 4100         10255 $open->add( [ $lowest_distance + $lg, $nx, $ny, $x, $y, undef ] );
1252 4100         11726 $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         318 $self->{stats}->{astar_steps} += $tries;
1261              
1262             # no more nodes to follow, so we couldn't find a path
1263 242 100       389 if (!defined $elem)
1264             {
1265 2 50       6 print STDERR "# A* couldn't find a path after $max_tries steps.\n" if $self->{debug};
1266 2         124 return [];
1267             }
1268              
1269 240         291 my $path = [];
1270 240         325 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         182 my ($lx,$ly);
1274 0         0 my $type;
1275              
1276 240         174 my $label_cell = 0; # found a cell to attach the label to?
1277              
1278 240         173 my @bends; # record all bends in the path to straighten it out
1279              
1280 240         179 my $idx = 0;
1281             # follow $elem back to the source to find the path
1282 240         385 while (defined $cx)
1283             {
1284 1155 50       1763 last unless exists $closed->{"$cx,$cy"};
1285 1155         932 my $xy = "$cx,$cy";
1286              
1287 1155         1018 $type = $closed->{$xy}->[ 4 ];
1288              
1289 1155         753 my ($px,$py) = @{ $closed->{$xy} }; # get X,Y of parent cell
  1155         1257  
1290              
1291 1155   100     2300 my $edge_type = ($type||0) & EDGE_TYPE_MASK;
1292 1155 50       1398 if ($edge_type == 0)
1293             {
1294 1155   100     2035 my $edge_flags = ($type||0) & EDGE_FLAG_MASK;
1295              
1296             # either a start or a stop cell
1297 1155 50       1289 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       1263 if (!defined $lx)
1318             {
1319 240         265 $lx = $closed->{$xy}->[6];
1320 240         292 $ly = $closed->{$xy}->[7];
1321             }
1322              
1323             # still not known?
1324 1155 100       1282 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         226 ($lx,$ly) = ($cx, $cy); # start with same cell
1337              
1338 228 100       385 $ly ++ if ($edge_flags & EDGE_END_S) != 0;
1339 228 100       401 $ly -- if ($edge_flags & EDGE_END_N) != 0;
1340              
1341 228 100       413 $lx ++ if ($edge_flags & EDGE_END_E) != 0;
1342 228 100       437 $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         1462 $type += _astar_edge_type($px, $py, $cx, $cy, $lx,$ly);
1348             }
1349              
1350 1155 50       1684 print STDERR "# Following back from $lx,$ly over $cx,$cy to $px,$py\n" if $self->{debug} > 1;
1351              
1352 1155 0 66     2209 if ($px == $lx && $py == $ly && ($cx != $lx || $cy != $ly))
      0        
      33        
1353             {
1354             print STDERR
1355             "# Warning: A* detected loop in path-backtracking at $px,$py, $cx,$cy, $lx,$ly\n"
1356 0 0       0 if $self->{debug};
1357 0         0 last;
1358             }
1359              
1360 1155 50       1520 $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         808 my $t = $type & EDGE_TYPE_MASK;
1365              
1366             # Do not put the label on crossings:
1367 1155 100 66     3143 if ($label_cell == 0 && (!exists $cells->{"$cx,$cy"}) && ($t == EDGE_HOR || $t == EDGE_VER))
      100        
      66        
1368             {
1369 239         192 $label_cell++;
1370 239         202 $type += EDGE_LABEL_CELL;
1371             }
1372              
1373 1155 100 100     6003 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         1767 unshift @$path, $cx, $cy, $type; # unshift to reverse the path
1377              
1378 1155 100       2097 last if $closed->{"$cx,$cy"}->[ 5 ]; # stop here?
1379              
1380 915         730 ($lx,$ly) = ($cx,$cy);
1381 915         604 ($cx,$cy) = @{ $closed->{"$cx,$cy"} }; # get X,Y of next cell
  915         1955  
1382              
1383 915         1504 $idx += 3; # index into $path (for bends)
1384             }
1385              
1386 240 50 66     487 print STDERR "# Trying to straighten path\n" if @bends >= 3 && $self->{debug};
1387              
1388             # try to straighten unnec. inward bends
1389 240 100       447 $self->_straighten_path($path, \@bends, $edge) if @bends >= 3;
1390              
1391 240 50       320 return ($path,$closed,$open_by_pos) if wantarray;
1392 240         3009 $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   11 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         11 my $cells = $self->{cells};
1455              
1456 8         11 my $i = 0;
1457             BEND:
1458 8         23 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         24 my ($a,$b,$c) = ($bends->[$i],
1465             $bends->[$i+1],
1466             $bends->[$i+2]);
1467              
1468 10         12 my $dx = ($b->[1] - $a->[1]);
1469 10         11 my $dy = ($b->[2] - $a->[2]);
1470              
1471 10         20 my $p = 0;
1472 10         14 for my $pattern (@$bend_patterns)
1473             {
1474 80         45 $p++;
1475 80 0 100     171 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         22 } 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             $html .= " $c
1625 0 0 0       exists $cells->{$xy} and ref($cells->{$xy}) =~ /Node/;
      0        
1626             $html .= " $c
1627 0 0 0       exists $cells->{$xy} && !exists $path->{$xy};
      0        
1628              
1629             $html .= " $c
1630             exists $closed->{$xy} ||
1631 0 0 0       exists $open->{$xy};
      0        
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__