File Coverage

blib/lib/Math/PlanePath/SierpinskiCurve.pm
Criterion Covered Total %
statement 159 236 67.3
branch 56 102 54.9
condition 27 35 77.1
subroutine 23 38 60.5
pod 18 18 100.0
total 283 429 65.9


line stmt bran cond sub pod time code
1             # Copyright 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018 Kevin Ryde
2              
3             # This file is part of Math-PlanePath.
4             #
5             # Math-PlanePath is free software; you can redistribute it and/or modify
6             # it under the terms of the GNU General Public License as published by the
7             # Free Software Foundation; either version 3, or (at your option) any later
8             # version.
9             #
10             # Math-PlanePath is distributed in the hope that it will be useful, but
11             # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
12             # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
13             # for more details.
14             #
15             # You should have received a copy of the GNU General Public License along
16             # with Math-PlanePath. If not, see .
17              
18              
19             package Math::PlanePath::SierpinskiCurve;
20 2     2   9161 use 5.004;
  2         15  
21 2     2   11 use strict;
  2         5  
  2         63  
22 2     2   11 use List::Util 'sum','first';
  2         3  
  2         262  
23             #use List::Util 'min','max';
24             *min = \&Math::PlanePath::_min;
25             *max = \&Math::PlanePath::_max;
26              
27 2     2   14 use vars '$VERSION', '@ISA';
  2         3  
  2         130  
28             $VERSION = 127;
29 2     2   670 use Math::PlanePath;
  2         5  
  2         114  
30             @ISA = ('Math::PlanePath');
31             *_divrem_mutate = \&Math::PlanePath::_divrem_mutate;
32              
33             use Math::PlanePath::Base::Generic
34 2         106 'is_infinite',
35 2     2   13 'round_nearest';
  2         3  
36             use Math::PlanePath::Base::Digits
37 2         110 'round_up_pow',
38             'round_down_pow',
39 2     2   487 'digit_split_lowtohigh';
  2         4  
40              
41             # uncomment this to run the ### lines
42             # use Smart::Comments;
43              
44              
45 2     2   12 use constant n_start => 0;
  2         3  
  2         370  
46              
47             sub x_negative {
48 8     8 1 30 my ($self) = @_;
49 8         30 return ($self->{'arms'} >= 3);
50             }
51             sub y_negative {
52 8     8 1 16 my ($self) = @_;
53 8         27 return ($self->{'arms'} >= 5);
54             }
55              
56 2         446 use constant parameter_info_array =>
57             [
58             { name => 'arms',
59             share_key => 'arms_8',
60             display => 'Arms',
61             type => 'integer',
62             minimum => 1,
63             maximum => 8,
64             default => 1,
65             width => 1,
66             description => 'Arms',
67             },
68              
69             { name => 'straight_spacing',
70             display => 'Straight Spacing',
71             type => 'integer',
72             minimum => 1,
73             default => 1,
74             width => 1,
75             description => 'Spacing of the straight line points.',
76             },
77             { name => 'diagonal_spacing',
78             display => 'Diagonal Spacing',
79             type => 'integer',
80             minimum => 1,
81             default => 1,
82             width => 1,
83             description => 'Spacing of the diagonal points.',
84             },
85 2     2   22 ];
  2         4  
86              
87             # Ntop = (4^level)/2 - 1
88             # Xtop = 3*2^(level-1) - 1
89             # fill = Ntop / (Xtop*(Xtop-1)/2)
90             # -> 2 * ((4^level)/2 - 1) / (3*2^(level-1) - 1)^2
91             # -> 2 * ((4^level)/2) / (3*2^(level-1))^2
92             # = 4^level / (9*4^(level-1)
93             # = 4/9 = 0.444
94              
95             sub x_negative_at_n {
96 0     0 1 0 my ($self) = @_;
97 0 0       0 return $self->arms_count >= 3 ? 2 : undef;
98             }
99             sub y_negative_at_n {
100 0     0 1 0 my ($self) = @_;
101 0 0       0 return $self->arms_count >= 5 ? 4 : undef;
102             }
103              
104             {
105             # Note: shared by Math::PlanePath::SierpinskiCurveStair
106             my @x_minimum = (undef,
107             1, # 1 arm
108             0, # 2 arms
109             ); # more than 2 arm, X goes negative
110             sub x_minimum {
111 0     0 1 0 my ($self) = @_;
112 0         0 return $x_minimum[$self->arms_count];
113             }
114             }
115             {
116             # Note: shared by Math::PlanePath::SierpinskiCurveStair
117             my @sumxy_minimum = (undef,
118             1, # 1 arm, octant and X>=1 so X+Y>=1
119             1, # 2 arms, X>=1 or Y>=1 so X+Y>=1
120             0, # 3 arms, Y>=1 and X>=Y, so X+Y>=0
121             ); # more than 3 arm, Sum goes negative so undef
122             sub sumxy_minimum {
123 0     0 1 0 my ($self) = @_;
124 0         0 return $sumxy_minimum[$self->arms_count];
125             }
126             }
127 2     2   22 use constant sumabsxy_minimum => 1;
  2         4  
  2         181  
128              
129             # Note: shared by Math::PlanePath::SierpinskiCurveStair
130             # Math::PlanePath::AlternatePaper
131             # Math::PlanePath::AlternatePaperMidpoint
132             sub diffxy_minimum {
133 0     0 1 0 my ($self) = @_;
134 0 0       0 return ($self->arms_count == 1
135             ? 1 # octant Y<=X-1 so X-Y>=1
136             : undef); # more than 1 arm, DiffXY goes negative
137             }
138 2     2   14 use constant absdiffxy_minimum => 1; # X=Y never occurs
  2         4  
  2         128  
139 2     2   13 use constant rsquared_minimum => 1; # minimum X=1,Y=0
  2         4  
  2         840  
140              
141             sub dx_minimum {
142 0     0 1 0 my ($self) = @_;
143             return - max($self->{'straight_spacing'},
144 0         0 $self->{'diagonal_spacing'});
145             }
146             *dy_minimum = \&dx_minimum;
147              
148             sub dx_maximum {
149 0     0 1 0 my ($self) = @_;
150             return max($self->{'straight_spacing'},
151 0         0 $self->{'diagonal_spacing'});
152             }
153             *dy_maximum = \&dx_maximum;
154              
155             sub _UNDOCUMENTED__dxdy_list {
156 0     0   0 my ($self) = @_;
157 0         0 my $s = $self->{'straight_spacing'};
158 0         0 my $d = $self->{'diagonal_spacing'};
159 0 0       0 return ($s,0, # E eight scaled
    0          
    0          
    0          
    0          
    0          
    0          
160             ($d ? ( $d, $d) : ()), # NE except s=0
161             ($s ? ( 0, $s) : ()), # N or d=0 skips
162             ($d ? (-$d, $d) : ()), # NW
163             ($s ? (-$s, 0) : ()), # W
164             ($d ? (-$d,-$d) : ()), # SW
165             ($s ? ( 0,-$s) : ()), # S
166             ($d ? ( $d,-$d) : ())); # SE
167              
168             }
169             {
170             my @_UNDOCUMENTED__dxdy_list_at_n = (undef,
171             21, 20, 27, 36,
172             29, 12, 12, 13);
173             sub _UNDOCUMENTED__dxdy_list_at_n {
174 0     0   0 my ($self) = @_;
175 0         0 return $_UNDOCUMENTED__dxdy_list_at_n[$self->{'arms'}];
176             }
177             }
178             sub dsumxy_minimum {
179 0     0 1 0 my ($self) = @_;
180             return - max($self->{'straight_spacing'},
181 0         0 2*$self->{'diagonal_spacing'});
182             }
183             sub dsumxy_maximum {
184 0     0 1 0 my ($self) = @_;
185             return max($self->{'straight_spacing'},
186 0         0 2*$self->{'diagonal_spacing'});
187             }
188             *ddiffxy_minimum = \&dsumxy_minimum;
189             *ddiffxy_maximum = \&dsumxy_maximum;
190              
191 2     2   14 use constant dir_maximum_dxdy => (1,-1); # South-East
  2         3  
  2         119  
192 2     2   13 use constant turn_any_straight => 0; # never straight
  2         3  
  2         3457  
193              
194              
195             #------------------------------------------------------------------------------
196              
197             sub new {
198 256     256 1 13196 my $self = shift->SUPER::new(@_);
199              
200 256   100     1155 $self->{'arms'} = max(1, min(8, $self->{'arms'} || 1));
201 256   100     947 $self->{'straight_spacing'} ||= 1;
202 256   100     926 $self->{'diagonal_spacing'} ||= 1;
203 256         515 return $self;
204             }
205              
206             sub n_to_xy {
207 7496     7496 1 106572 my ($self, $n) = @_;
208             ### SierpinskiCurve n_to_xy(): $n
209              
210 7496 50       14311 if ($n < 0) {
211 0         0 return;
212             }
213 7496 50       13939 if (is_infinite($n)) {
214 0         0 return ($n,$n);
215             }
216              
217 7496         13540 my $int = int($n); # BigFloat int() gives BigInt, use that
218 7496         9844 $n -= $int; # preserve possible BigFloat
219             ### $int
220             ### $n
221              
222 7496         17312 my $arm = _divrem_mutate ($int, $self->{'arms'});
223              
224 7496         11988 my $s = $self->{'straight_spacing'};
225 7496         11105 my $d = $self->{'diagonal_spacing'};
226 7496         11941 my $base = 2*$d+$s;
227 7496         11037 my $x = my $y = ($int * 0); # inherit big 0
228 7496         10888 my $len = $x + $base; # inherit big
229              
230 7496         14551 foreach my $digit (digit_split_lowtohigh($int,4)) {
231             ### at: "$x,$y digit=$digit"
232              
233 13579 100       27201 if ($digit == 0) {
    100          
    100          
234 1754         2556 $x = $n*$d + $x;
235 1754         2503 $y = $n*$d + $y;
236 1754         2491 $n = 0;
237              
238             } elsif ($digit == 1) {
239 5700         11567 ($x,$y) = ($n*$s - $y + $len-$d-$s, # rotate +90
240             $x + $d);
241 5700         8035 $n = 0;
242              
243             } elsif ($digit == 2) {
244             # rotate -90
245 3552         8232 ($x,$y) = ($n*$d + $y + $len-$d,
246             -$n*$d - $x + $len-$d-$s);
247 3552         5117 $n = 0;
248              
249             } else { # digit==3
250 2573         3458 $x += $len;
251             }
252 13579         20499 $len *= 2;
253             }
254              
255             # n=0 or n=33..33
256 7496         11937 $x = $n*$d + $x;
257 7496         10319 $y = $n*$d + $y;
258              
259 7496         9995 $x += 1;
260 7496 100       14376 if ($arm & 1) {
261 3248         6022 ($x,$y) = ($y,$x); # mirror 45
262             }
263 7496 100       13710 if ($arm & 2) {
264 2768         4831 ($x,$y) = (-1-$y,$x); # rotate +90
265             }
266 7496 100       13540 if ($arm & 4) {
267 1781         2538 $x = -1-$x; # rotate 180
268 1781         2420 $y = -1-$y;
269             }
270              
271             # use POSIX 'floor';
272             # $x += floor($x/3);
273             # $y += floor($y/3);
274              
275             # $x += floor(($x-1)/3) + floor(($x-2)/3);
276             # $y += floor(($y-1)/3) + floor(($y-2)/3);
277              
278              
279             ### final: "$x,$y"
280 7496         17517 return ($x,$y);
281             }
282              
283             my @digit_to_dir = (0, -2, 2, 0);
284             my @dir8_to_dx = (1, 1, 0,-1, -1, -1, 0, 1);
285             my @dir8_to_dy = (0, 1, 1, 1, 0, -1, -1,-1);
286             my @digit_to_nextturn = (-1, # after digit=0
287             2, # digit=1
288             -1); # digit=2
289             sub n_to_dxdy {
290 0     0 1 0 my ($self, $n) = @_;
291             ### n_to_dxdy(): $n
292              
293 0 0       0 if ($n < 0) {
294 0         0 return; # first direction at N=0
295             }
296              
297 0         0 my $int = int($n);
298 0         0 $n -= $int;
299              
300 0         0 my $arm = _divrem_mutate($int,$self->{'arms'});
301 0         0 my $lowbit = _divrem_mutate($int,2);
302             ### $lowbit
303             ### $int
304              
305 0 0       0 if (is_infinite($int)) {
306 0         0 return ($int,$int);
307             }
308 0         0 my @ndigits = digit_split_lowtohigh($int,4);
309             ### @ndigits
310              
311 0         0 my $dir8 = sum(0, map {$digit_to_dir[$_]} @ndigits);
  0         0  
312 0 0       0 if ($arm & 1) {
313 0         0 $dir8 = - $dir8; # mirrored on second,fourth,etc arm
314             }
315 0         0 $dir8 += ($arm|1); # NE,NW,SW, or SE
316              
317 0         0 my $turn;
318 0 0 0     0 if ($n || $lowbit) {
319             # next turn
320              
321             # lowest non-3 digit, or zero if all 3s (implicit 0 above high digit)
322 0     0   0 $turn = $digit_to_nextturn[ first {$_!=3} @ndigits, 0 ];
  0         0  
323 0 0       0 if ($arm & 1) {
324 0         0 $turn = - $turn; # mirrored on second,fourth,etc arm
325             }
326             }
327              
328 0 0       0 if ($lowbit) {
329 0         0 $dir8 += $turn;
330             }
331              
332 0         0 my $s = $self->{'straight_spacing'};
333 0         0 my $d = $self->{'diagonal_spacing'};
334              
335 0         0 $dir8 &= 7;
336 0 0       0 my $spacing = ($dir8 & 1 ? $d : $s);
337 0         0 my $dx = $spacing * $dir8_to_dx[$dir8];
338 0         0 my $dy = $spacing * $dir8_to_dy[$dir8];
339              
340 0 0       0 if ($n) {
341 0         0 $dir8 += $turn;
342 0         0 $dir8 &= 7;
343 0 0       0 $spacing = ($dir8 & 1 ? $d : $s);
344 0         0 $dx += $n*($spacing * $dir8_to_dx[$dir8]
345             - $dx);
346 0         0 $dy += $n*($spacing * $dir8_to_dy[$dir8]
347             - $dy);
348             }
349              
350 0         0 return ($dx, $dy);
351             }
352              
353             # 2| . 3 .
354             # 1| 1 . 2
355             # 0| . 0 .
356             # +------
357             # 0 1 2
358             #
359             # 4| . . . 3 . # diagonal_spacing == 3
360             # 3| . . . . 2 4 # mod=2*3+1=7
361             # 2| . . . . . . .
362             # 1| 1 . . . . . . .
363             # 0| . 0 . . . . . . 6
364             # +------------------
365             # 0 1 2 3 4 5 6 7 8
366             #
367             sub _NOTWORKING__xy_is_visited {
368 0     0   0 my ($self, $x, $y) = @_;
369 0         0 $x = round_nearest($x);
370 0         0 $y = round_nearest($y);
371 0         0 my $mod = 2*$self->{'diagonal_spacing'} + $self->{'straight_spacing'};
372 0   0     0 return (_rect_within_arms($x,$y, $x,$y, $self->{'arms'})
373             && ((($x%$mod)+($y%$mod)) & 1));
374             }
375              
376             # x1 * x2 *
377             # +-----*-+y2*
378             # | *| *
379             # | * *
380             # | |* *
381             # | | **
382             # +-------+y1*
383             # ----------------
384             #
385             # arms=5 x1,y2 after X=Y-1 line, so x1 > y2-1, x1 >= y2
386             # ************
387             # x1 * x2
388             # +---*----+y2
389             # | * |
390             # | * |
391             # |* |
392             # * |
393             # *+--------+y1
394             # *
395             #
396             # arms=7 x1,y1 after X=-2-Y line, so x1 > -2-y1
397             # ************
398             # ** +------+
399             # * *| |
400             # * * |
401             # * |* |
402             # * | * |
403             # *y1+--*---+
404             # * x1 *
405             #
406             # _rect_within_arms() returns true if rectangle x1,y1,x2,y2 has some part
407             # within the extent of the $arms set of octants.
408             #
409             sub _rect_within_arms {
410 363     363   635 my ($x1,$y1, $x2,$y2, $arms) = @_;
411 363 100 66     1550 return ($arms <= 4
      66        
412             ? ($y2 >= 0 # y2 top edge must be positive
413             && ($arms <= 2
414             ? ($arms == 1 ? $x2 > $y1 # arms==1 bottom right
415             : $x2 >= 0) # arms==2 right edge
416             : ($arms == 4 # arms==4 anything
417             || $x2 >= -$y2))) # arms==3 top right
418              
419             # arms >= 5
420             : ($y2 >= 0 # y2 top edge positive is good, otherwise check
421             || ($arms <= 6
422             ? ($arms == 5 ? $x1 < $y2 # arms==5 top left
423             : $x1 < 0) # arms==6 left edge
424             : ($arms == 8 # arms==8 anything
425             || $x1 <= -2-$y1)))); # arms==7 bottom left
426             }
427              
428             sub xy_to_n {
429 57896     57896 1 260434 my ($self, $x, $y) = @_;
430             ### SierpinskiCurve xy_to_n(): "$x, $y"
431              
432 57896         103352 $x = round_nearest($x);
433 57896         106999 $y = round_nearest($y);
434              
435 57896         81148 my $arm = 0;
436 57896 100       98462 if ($y < 0) {
437 27206         35737 $arm = 4;
438 27206         36161 $x = -1-$x; # rotate -180
439 27206         35356 $y = -1-$y;
440             }
441 57896 100       96335 if ($x < 0) {
442 28816         38315 $arm += 2;
443 28816         47149 ($x,$y) = ($y, -1-$x); # rotate -90
444             }
445 57896 100       96059 if ($y > $x) { # second octant
446 25610         33110 $arm++;
447 25610         41181 ($x,$y) = ($y,$x); # mirror 45
448             }
449              
450 57896         87701 my $arms = $self->{'arms'};
451 57896 100       98192 if ($arm >= $arms) {
452 24000         43893 return undef;
453             }
454              
455 33896         44934 $x -= 1;
456 33896 100 100     95754 if ($x < 0 || $x < $y) {
457 4200         8079 return undef;
458             }
459             ### x adjust to zero: "$x,$y"
460             ### assert: $x >= 0
461             ### assert: $y >= 0
462              
463 29696         41430 my $s = $self->{'straight_spacing'};
464 29696         41069 my $d = $self->{'diagonal_spacing'};
465 29696         42581 my $base = (2*$d+$s);
466 29696   100     83437 my ($len,$level) = round_down_pow (($x+$y)/$base || 1, 2);
467             ### $level
468             ### $len
469 29696 50       59833 if (is_infinite($level)) {
470 0         0 return $level;
471             }
472              
473             # Xtop = 3*2^(level-1)-1
474             #
475 29696         51495 $len *= 2*$base;
476             ### initial len: $len
477              
478 29696         40415 my $n = 0;
479 29696         52247 foreach (0 .. $level) {
480 35785         49237 $n *= 4;
481             ### at: "loop=$_ len=$len x=$x,y=$y n=$n"
482             ### assert: $x >= 0
483             ### assert: $y >= 0
484              
485 35785         50863 my $len_sub_d = $len - $d;
486 35785 100       57373 if ($x < $len_sub_d) {
487             ### digit 0 or 1...
488 33042 100       54815 if ($x+$y+$s < $len) {
489             ### digit 0 ...
490             } else {
491             ### digit 1 ...
492 6605         11554 ($x,$y) = ($y-$d, $len-$s-$d-$x); # shift then rotate -90
493 6605         8964 $n += 1;
494             }
495             } else {
496 2743         4043 $x -= $len_sub_d;
497             ### digit 2 or 3 to: "x=$x y=$y"
498 2743 100       4325 if ($x < $y) { # before diagonal
499             ### digit 2...
500 1859         3483 ($x,$y) = ($len-$d-$s-$y, $x); # shift y-len then rotate +90
501 1859         2753 $n += 2;
502             } else {
503             #### digit 3...
504 884         1162 $x -= $d;
505 884         1242 $n += 3;
506             }
507 2743 100       5128 if ($x < 0) {
508 376         840 return undef;
509             }
510             }
511 35409         58908 $len /= 2;
512             }
513              
514             ### end at: "x=$x,y=$y n=$n"
515             ### assert: $x >= 0
516             ### assert: $y >= 0
517              
518 29320         38596 $n *= 4;
519 29320 100 100     125627 if ($y == 0 && $x == 0) {
    100 100        
    100 100        
    100 100        
520             ### final digit 0 ...
521             } elsif ($x == $d && $y == $d) {
522             ### final digit 1 ...
523 1762         2633 $n += 1;
524             } elsif ($x == $d+$s && $y == $d) {
525             ### final digit 2 ...
526 1609         2272 $n += 2;
527             } elsif ($x == $base && $y == 0) {
528             ### final digit 3 ...
529 1278         1763 $n += 3;
530             } else {
531 22874         49310 return undef;
532             }
533              
534 6446         13359 return $n*$arms + $arm;
535             }
536              
537             # not exact
538             sub rect_to_n_range {
539 363     363 1 8748 my ($self, $x1,$y1, $x2,$y2) = @_;
540             ### SierpinskiCurve rect_to_n_range(): "$x1,$y1 $x2,$y2"
541              
542 363         1094 $x1 = round_nearest ($x1);
543 363         710 $x2 = round_nearest ($x2);
544 363         640 $y1 = round_nearest ($y1);
545 363         688 $y2 = round_nearest ($y2);
546 363 50       741 ($x1,$x2) = ($x2,$x1) if $x1 > $x2;
547 363 100       611 ($y1,$y2) = ($y2,$y1) if $y1 > $y2;
548              
549 363         567 my $arms = $self->{'arms'};
550 363 100       669 unless (_rect_within_arms($x1,$y1, $x2,$y2, $arms)) {
551             ### rect outside octants, for arms: $arms
552 9         26 return (1,0);
553             }
554              
555 354         629 my $max = ($x2 + $y2);
556 354 100       647 if ($arms >= 3) {
557 307         690 _apply_max ($max, -1-$x1 + $y2);
558              
559 307 100       541 if ($arms >= 5) {
560 237         491 _apply_max ($max, -1-$x1 - $y1-1);
561              
562 237 100       479 if ($arms >= 7) {
563 135         223 _apply_max ($max, $x2 - $y1-1);
564             }
565             }
566             }
567              
568             # base=2d+s
569             # level begins at
570             # base*(2^level-1)-s = X+Y ... maybe
571             # base*2^level = X+base
572             # 2^level = (X+base)/base
573             # level = log2((X+base)/base)
574             # then
575             # Nlevel = 4^level-1
576              
577 354         634 my $base = 2 * $self->{'diagonal_spacing'} + $self->{'straight_spacing'};
578 354         968 my ($power) = round_down_pow (int(($max+$base-2)/$base),
579             2);
580 354         877 return (0, 4*$power*$power * $arms - 1);
581             }
582              
583             sub _apply_max {
584             ### _apply_max(): "$_[0] cf $_[1]"
585 679 100   679   1223 unless ($_[0] > $_[1]) {
586 277         379 $_[0] = $_[1];
587             }
588             }
589              
590             #------------------------------------------------------------------------------
591              
592             sub level_to_n_range {
593 9     9 1 653 my ($self, $level) = @_;
594 9         35 return (0, 4**$level * $self->{'arms'} - 1);
595             }
596             sub n_to_level {
597 0     0 1   my ($self, $n) = @_;
598 0 0         if ($n < 0) { return undef; }
  0            
599 0 0         if (is_infinite($n)) { return $n; }
  0            
600 0           $n = round_nearest($n);
601 0           _divrem_mutate ($n, $self->{'arms'});
602 0           my ($pow, $exp) = round_up_pow ($n+1, 4);
603 0           return $exp;
604             }
605              
606             #------------------------------------------------------------------------------
607             1;
608             __END__