File Coverage

blib/lib/CXC/Astro/Regions/DS9.pm
Criterion Covered Total %
statement 61 63 96.8
branch 5 6 83.3
condition 1 3 33.3
subroutine 18 19 94.7
pod 1 1 100.0
total 86 92 93.4


line stmt bran cond sub pod time code
1             package CXC::Astro::Regions::DS9;
2              
3             # ABSTRACT: DS9 Compatible Regions
4              
5 2     2   417094 use v5.20;
  2         8  
6 2     2   10 use warnings;
  2         5  
  2         132  
7 2     2   946 use experimental 'signatures', 'postderef', 'lexical_subs';
  2         6311  
  2         12  
8              
9             our $VERSION = '0.03';
10              
11             package CXC::Astro::Regions::DS9::Role::Region {
12 2     2   1547 use Moo::Role;
  2         30655  
  2         11  
13             }
14              
15 2     2   1089 use constant RegionRole => __PACKAGE__ . '::Role::Region';
  2         4  
  2         155  
16              
17 2     2   544 use parent 'Exporter::Tiny';
  2         1138  
  2         14  
18              
19 2     2   12415 use Import::Into;
  2         5828  
  2         107  
20 2         32 use CXC::Astro::Regions::DS9::Types qw(
21             Angle
22             ArrayRef
23             ConsumerOf
24             CoordSys
25             Enum
26             Length
27             LengthPair
28             NonEmptyStr
29             OneZero
30             PointType
31             PositiveInt
32             RulerCoords
33             Tuple
34             Vertex
35 2     2   1117 );
  2         205  
36              
37 2     2   15241 use CXC::Astro::Regions::DS9::Variant;
  2         26  
  2         14  
38 2     2   115 use List::Util ();
  2         3  
  2         49  
39              
40 2     2   1044 use namespace::clean;
  2         32888  
  2         17  
41              
42             my sub croak {
43 0     0   0 require Carp;
44 0         0 goto \&Carp::croak;
45             }
46              
47 66     66   114 my sub pkgpath ( @paths ) {
  66         154  
  66         124  
48 66         216 join q{::}, __PACKAGE__, map { ucfirst( $_ ) } @paths;
  66         431  
49             }
50              
51             my sub args ( @args ) {
52             return @args == 1 ? ( name => $args[0] ) : @args;
53             }
54              
55             my sub format_text ( $type, $label, $values ) {
56             return () unless $values->@*;
57             return $type eq 'prop' && defined $label
58             ? sprintf( '%s={%s}', $label, $values->@* )
59             : sprintf( '{%s}', $values->@* );
60             }
61              
62             my sub format_qstring ( $type, $label, $values ) {
63             return () unless $values->@*;
64             return $type eq 'prop' && defined $label
65             ? sprintf( '%s="%s"', $label, $values->@* )
66             : sprintf( '"%s"', $values->@* );
67             }
68              
69             my sub format_tags ( $type, $label, $values ) {
70             ( $values, my @rest ) = $values->@*;
71             croak( 'too many values passed to tags' ) if @rest;
72             return () unless $values->@*;
73             return join q{ }, map { sprintf( '%s={%s}', $label, $_ ) } $values->@*;
74             }
75              
76             my sub ANGLE { { name => 'angle', isa => Angle, args( @_ ) } }
77             my sub ANGLEPAIR { { name => 'angles', isa => Tuple [ Angle, Angle ], args( @_ ) } }
78             my sub ARROW { { name => 'arrow', isa => OneZero, coerce => !!1, args( @_ ) } }
79             my sub ARROWS { { name => 'arrows', isa => ArrayRef [ OneZero, 2 ], args( @_ ) } }
80             my sub BOOL { { name => undef, isa => OneZero, coerce => !!1, args( @_ ) } }
81             my sub COORDS { { name => 'coords', isa => CoordSys, coerce => !!1, args( @_ ) } }
82             my sub FILL { { name => 'fill', isa => OneZero, coerce => !!1, args( @_ ) } }
83             my sub FORMAT { { name => 'format', isa => NonEmptyStr, args( @_ ) } }
84             my sub LENGTH { { name => 'length', isa => Length, args( @_ ) } }
85             my sub LENGTHPAIR { { name => undef, isa => LengthPair, args( @_ ) } }
86             my sub LENGTHPAIR_ARRAY { { name => undef, isa => ArrayRef [LengthPair], args( @_ ) } }
87             my sub LENGTH_ARRAY { { name => undef, isa => ArrayRef [Length], args( @_ ) } }
88             my sub N { { name => 'n', isa => PositiveInt, args( @_ ) } }
89             my sub POINT { { name => 'symbol', isa => PointType, coerce => !!1, label => 'point', args( @_ ) } }
90             my sub POSINT { { name => undef, isa => PositiveInt, args( @_ ) } }
91             my sub QSTRING { { name => undef, isa => NonEmptyStr, format => \&format_text, args( @_ ) } }
92             my sub RULERCOORDS {
93             { name => 'coords', isa => RulerCoords, coerce => !!1, label => 'ruler', args( @_ ) };
94             }
95             my sub STRING { { name => undef, isa => NonEmptyStr, args( @_ ) } }
96             my sub TAGS {
97             {
98             name => 'tags',
99             isa => ArrayRef [NonEmptyStr],
100             format => \&format_tags,
101             label => 'tag',
102             args( @_ ) };
103             }
104             my sub TEXT { { name => 'text', isa => NonEmptyStr, format => \&format_text, args( @_ ) } }
105             my sub VERTEX { { name => undef, isa => Vertex, args( @_ ) } }
106             my sub VERTICES { { name => 'vertices', isa => ArrayRef [Vertex], args( @_ ) } }
107              
108             my @CommonProps = (
109             TEXT,
110             ANGLE( 'textangle' ),
111             STRING( 'color' ),
112             { name => 'dashlist', isa => ArrayRef [PositiveInt] },
113             POSINT( name => 'linewidth', label => 'width' ),
114             QSTRING( 'font' ),
115             BOOL( 'select' ),
116             BOOL( 'highlite' ),
117             BOOL( 'dash' ),
118             BOOL( 'fixed' ),
119             BOOL( 'edit' ),
120             BOOL( 'move' ),
121             BOOL( 'rotate' ),
122             BOOL( 'delete' ),
123             BOOL( name => 'include', default => !!1 ),
124             { name => 'srctype', isa => Enum [ 'source', 'background' ], label => undef },
125             TAGS,
126             );
127              
128 2     2   5878 use Package::Stash;
  2         4  
  2         3517  
129             our @EXPORT_OK = ( 'mkregion' );
130              
131             my $stash = Package::Stash->new( __PACKAGE__ );
132             my sub REGION ( $region, %spec ) {
133              
134             push( ( $spec{props} //= [] )->@*, @CommonProps );
135             $spec{with} //= [RegionRole];
136             my $package = pkgpath( $region );
137              
138             if ( exists $spec{name} && $spec{name} ne $region ) {
139             my $parent = pkgpath( $spec{name} );
140             Moo->import::into( $parent );
141             $spec{extends} = [$parent];
142             }
143              
144             my $variant = Variant( $region, %spec );
145             Package::Stash->new( $variant )->add_symbol( q{@CARP_NOT}, [__PACKAGE__] );
146              
147 38     38   33602 $stash->add_symbol( q{&} . $region, sub { $package->new( @_ ) } );
148             push @EXPORT_OK, $region;
149             }
150              
151              
152              
153              
154              
155              
156              
157              
158              
159              
160              
161              
162              
163              
164              
165              
166              
167              
168              
169             # Annulus
170             # Usage: annulus x y inner outer n=#
171             REGION annulus_n => (
172             name => 'annulus',
173             params => [ VERTEX( 'center' ), LENGTHPAIR( 'annuli' ), N( label => 'n' ) ],
174             );
175              
176             # Annulus
177             # Usage: annulus x y r1 r2 r3...
178             REGION annulus_annuli => (
179             name => 'annulus',
180             params => [ VERTEX( 'center' ), LENGTH_ARRAY( 'annuli' ) ],
181             );
182              
183              
184              
185              
186              
187              
188              
189              
190              
191              
192              
193              
194              
195              
196              
197              
198              
199              
200              
201              
202              
203              
204              
205              
206              
207              
208              
209              
210              
211             # Box
212             # Usage: box x y width height angle # fill=[0|1]
213             REGION box_plain => (
214             name => 'box',
215             params => [ VERTEX( 'center' ), LENGTH( 'width' ), LENGTH( 'height' ), ANGLE( required => !!0 ) ],
216             props => [FILL],
217             );
218              
219             # Box Annulus
220             # Usage: box x y w1 h1 w2 h2 n=# [angle]
221             REGION box_n => (
222             name => 'box',
223             params => [
224             VERTEX( 'center' ),
225             LENGTHPAIR( 'inner' ),
226             LENGTHPAIR( 'outer' ),
227             N( label => 'n' ),
228             ANGLE( required => !!0 ),
229             ],
230             );
231              
232             # Box Annulus
233             # Usage: box x y w1 h1 w2 h2 w3 h3 ... [angle]
234             REGION box_annuli => (
235             name => 'box',
236             params => [ VERTEX( 'center' ), LENGTHPAIR_ARRAY( 'annuli' ), ANGLE( required => !!0 ), ],
237             );
238              
239              
240              
241              
242              
243              
244              
245              
246              
247              
248              
249              
250              
251             # Bpanda
252             # Usage: bpanda x y startangle stopangle nangle inner outer nradius [angle]
253             REGION bpanda => (
254             params => [
255             VERTEX( 'center' ),
256             ANGLEPAIR,
257             N( 'nangles' ),
258             LENGTHPAIR( 'inner' ),
259             LENGTHPAIR( 'outer' ),
260             N( 'nannuli' ),
261             ANGLE( required => !!0 ),
262             ],
263             );
264              
265              
266              
267              
268              
269              
270              
271              
272              
273             # Circle
274             # Usage: circle x y radius # fill=[0|1]
275             REGION circle => (
276             params => [ VERTEX( 'center' ), LENGTH( 'radius' ) ],
277             props => [FILL],
278             );
279              
280              
281              
282              
283              
284              
285              
286              
287              
288              
289              
290              
291              
292              
293              
294              
295              
296              
297              
298              
299              
300              
301              
302              
303              
304              
305              
306              
307              
308              
309             # Compass
310             # Usage: compass x1 y1 length # compass= [0|1] [0|1]
311             REGION compass => (
312             params => [ VERTEX( 'base' ), LENGTH ],
313             props => [
314             COORDS( label => 'compass', default => 'physical' ),
315             TEXT( name => 'north', label => undef, default => 'N' ),
316             TEXT( name => 'east', label => undef, default => 'E' ),
317             ARROWS( label => undef, default => sub { [ 1, 1 ] } ),
318             ],
319             );
320              
321              
322              
323              
324              
325              
326              
327              
328              
329              
330              
331              
332              
333              
334              
335              
336             # Composite
337             # Usage: # composite x y angle
338             REGION composite => (
339             comment => !!1,
340             with => [], # we're not a normal region, so don't compose with RegionRole
341             params => [
342             VERTEX( 'center' ),
343             ANGLE( required => !!0 ),
344             {
345             name => 'regions',
346             isa => ArrayRef [ ConsumerOf [RegionRole] ],
347             render => !!0,
348             },
349             ],
350             around => [
351             render => sub ( $orig, $self ) {
352             return [ $self->$orig, map { $_->render } $self->regions->@* ];
353             },
354             ],
355             );
356              
357              
358              
359              
360              
361              
362              
363              
364              
365              
366              
367              
368              
369              
370              
371              
372              
373              
374              
375              
376              
377              
378              
379              
380              
381              
382              
383              
384              
385             # Ellipse
386             # Usage: ellipse x y radius radius angle # fill=[0|1]
387             REGION ellipse_plain => (
388             name => 'ellipse',
389             params => [ VERTEX( 'center' ), LENGTHPAIR( 'radii' ), ANGLE( required => !!0 ) ],
390             props => [FILL],
391             );
392              
393             # Ellipse Annulus
394             # Usage: ellipse x y r11 r12 r21 r22 n=# [angle]
395             REGION ellipse_n => (
396             name => 'ellipse',
397             params => [
398             VERTEX( 'center' ),
399             LENGTHPAIR( 'inner' ),
400             LENGTHPAIR( 'outer' ),
401             N( label => 'n' ),
402             ANGLE( required => !!0 ),
403             ],
404             );
405              
406             # Ellipse Annulus
407             # Usage: ellipse x y r11 r12 r21 r22 r31 r32 ... [angle]
408             REGION ellipse_annuli => (
409             name => 'ellipse',
410             params => [ VERTEX( 'center' ), LENGTHPAIR_ARRAY( 'annuli' ), ANGLE( required => !!0 ), ],
411             );
412              
413              
414              
415              
416              
417              
418              
419              
420              
421              
422              
423              
424              
425             # Epanda
426             # Usage: epanda x y startangle stopangle nangle inner outer nradius [angle]
427             REGION epanda => (
428             params => [
429             VERTEX( 'center' ),
430             ANGLEPAIR,
431             N( 'nangles' ),
432             LENGTHPAIR( 'inner' ),
433             LENGTHPAIR( 'outer' ),
434             N( 'nannuli' ),
435             ANGLE( required => !!0 ),
436             ],
437             );
438              
439              
440              
441              
442              
443              
444              
445              
446              
447              
448              
449             # Line
450             # Usage: line x1 y1 x2 y2 # line=[0|1] [0|1]
451             REGION line => (
452             params => [ VERTEX( 'v1' ), VERTEX( 'v2' ) ],
453             props => [ ARROWS( label => 'line' ) ],
454             );
455              
456              
457              
458              
459              
460              
461              
462              
463              
464              
465              
466             # Panda
467             # Usage: panda x y startangle stopangle nangle inner outer nradius
468             REGION panda => (
469             params => [
470             VERTEX( 'center' ),
471             ANGLEPAIR,
472             N( 'nangles' ),
473             LENGTH( 'inner' ),
474             LENGTH( 'outer' ),
475             N( 'nannuli' ),
476             ],
477             );
478              
479              
480              
481              
482              
483              
484              
485              
486              
487              
488              
489              
490              
491              
492              
493              
494              
495             # Point
496             # Usage: point x y # point=[circle|box|diamond|cross|x|arrow|boxcircle] [size]
497             # circle point x y
498             REGION point => (
499             params => [ VERTEX( 'center' ) ],
500             props => [ POINT, POSINT( name => 'size', label => undef ) ],
501             );
502              
503              
504              
505              
506              
507              
508              
509              
510             # Polygon
511             # Usage: polygon x1 y1 x2 y2 x3 y3 ...# fill=[0|1]
512             REGION polygon => (
513             params => [VERTICES],
514             props => [FILL],
515             );
516              
517              
518              
519              
520              
521              
522              
523              
524              
525             # Projection
526             # Usage: projection x1 y1 x2 y2 width
527             REGION projection => ( params => [ VERTEX( 'v1' ), VERTEX( 'v2' ), LENGTH( 'width' ) ], );
528              
529              
530              
531              
532              
533              
534              
535              
536              
537              
538              
539              
540              
541             # Ruler
542             # Usage: ruler x1 y1 x2 y2 # ruler=[pixels|degrees|arcmin|arcsec] [format=]
543             REGION ruler => (
544             params => [ VERTEX( 'v1' ), VERTEX( 'v2' ) ],
545             props => [ RULERCOORDS, FORMAT ],
546             );
547              
548              
549              
550              
551              
552              
553              
554             # Text
555             # Usage: text x y # text={Your Text Here}
556             # text x y {Your Text Here}
557             REGION text => ( params => [ VERTEX( 'center' ), TEXT ], );
558              
559              
560              
561              
562              
563              
564              
565              
566              
567              
568              
569              
570              
571             # Vector
572             # Usage: vector x1 y1 length angle # vector=[0|1]
573             REGION vector => (
574             params => [ VERTEX( 'base' ), LENGTH, ANGLE ],
575             props => [ ARROW( label => 'vector' ) ],
576             );
577              
578             # set up dispatch classes to handle the different types of annulus and ellipse regions
579              
580             # The 'annulus' region can be
581             # * annuli with inner and outer radii and count
582             # * annuli with explicitly specified radii
583              
584             # The 'ellipse' region can be
585             # * a simple ellipse;
586             # * elliptical annuli with inner and outer radii pairs and count
587             # * elliptical annuli with explicitly specified radii pairs
588              
589             # The 'box' region can be
590             # * a simple box;
591             # * box annuli with inner and outer dims and count
592             # * box annuli with explicitly specified dims
593              
594 18     18   30 my sub dispatch ( $package, %args ) {
  18         37  
  18         78  
  18         34  
595             my $suffix
596 46         160 = ( List::Util::any { exists $args{$_} } qw( inner outer n ) )
597             ? 'n'
598 18 50       151 : exists $args{annuli} ? 'annuli'
    100          
    100          
599             # no such thing as annulus_plain; bounce to annulus_n and it'll
600             # croak because of bad args
601             : $package =~ /Annulus/ ? 'n'
602             : 'plain';
603              
604 18         118 return "${package}_${suffix}";
605             }
606              
607             for my $region ( 'annulus', 'ellipse', 'box' ) {
608             my $pkg = pkgpath( $region );
609 18         72 Package::Stash->new( $pkg )->add_symbol(
610             '&new',
611 18     18   38 sub ( $, %args ) {
  18         35  
612 18         70 my $class = dispatch( $pkg, %args );
613 18         481 return $class->new( %args );
614             } );
615 16     16   11551 $stash->add_symbol( q{&} . $region, sub { $pkg->new( @_ ) } );
616             push @EXPORT_OK, $region;
617             }
618              
619              
620             # No longer need this; clean it up.
621             undef $stash;
622              
623              
624              
625              
626              
627              
628              
629              
630              
631              
632              
633 2     2 1 446385 sub mkregion ( $shape, @args ) {
  2         5  
  2         9  
  2         5  
634 2         12 my $class = pkgpath( $shape );
635 2   33     34 my $new = $class->can( 'new' ) // croak( "unknown region: $shape" );
636 2         9 $class->$new( @args );
637             }
638              
639             1;
640              
641             #
642             # This file is part of CXC-Astro-Regions
643             #
644             # This software is Copyright (c) 2023 by Smithsonian Astrophysical Observatory.
645             #
646             # This is free software, licensed under:
647             #
648             # The GNU General Public License, Version 3, June 2007
649             #
650              
651             __END__