File Coverage

lib/Graphics/Toolkit/Color/Space/Shape.pm
Criterion Covered Total %
statement 170 179 94.9
branch 128 166 77.1
condition 47 75 62.6
subroutine 25 26 96.1
pod 0 22 0.0
total 370 468 79.0


line stmt bran cond sub pod time code
1              
2             # geometry of space: value range checks, normalisation and computing distance
3              
4             package Graphics::Toolkit::Color::Space::Shape;
5 34     34   406639 use v5.12;
  34         163  
6 34     34   201 use warnings;
  34         71  
  34         2124  
7 34     34   885 use Graphics::Toolkit::Color::Space::Basis;
  34         233  
  34         1386  
8 34     34   180 use Graphics::Toolkit::Color::Space::Util qw/round_decimals is_nr/;
  34         66  
  34         126665  
9              
10             #### constructor #######################################################
11              
12             sub new {
13 316     316 0 23731 my $pkg = shift;
14 316         1072 my ($basis, $type, $range, $precision) = @_;
15 316 100       988 return unless ref $basis eq 'Graphics::Toolkit::Color::Space::Basis';
16              
17             # check axis type definition
18 315 100 100     2427 if (not defined $type){ $type = [ (1) x $basis->axis_count ] } # set all axis as linear per default
  185 100       561  
19             elsif (ref $type eq 'ARRAY' and @$type == $basis->axis_count ) {
20 128         346 for my $i ($basis->axis_iterator) {
21 382         598 my $atype = $type->[$i]; # type def of this axis
22 382 50       774 return unless defined $atype;
23 382 100 100     3097 if ($atype eq 'angular' or $atype eq 'circular' or $atype eq '0') { $type->[$i] = 0 }
  130 100 100     350  
    100 100        
      100        
24 246         575 elsif ($atype eq 'linear' or $atype eq '1') { $type->[$i] = 1 }
25 4         12 elsif ($atype eq 'no' or $atype eq '2') { $type->[$i] = 2 }
26 2         17 else { return 'invalid axis type at element '.$i.'. It has to be "angular", "linear" or "no".' }
27             }
28 2         6 } else { return 'invalid axis type definition in color space '.$basis->space_name }
29              
30 311         3203 $range = check_range_definition( $basis, $range );
31 311 100       1041 return $range unless ref $range;
32 303         769 $precision = check_precision_definition( $basis, $precision );
33 303 100       719 return $precision unless ref $precision;
34              
35 301         3318 bless { basis => $basis, type => $type, range => $range, precision => $precision, constraint => {} }
36             }
37             sub check_range_definition { # check if range def is valid and eval (expand) it
38 1490     1490 0 2771 my ($basis, $range) = @_;
39 1490 50       3781 $basis = $basis->{'basis'} if ref $basis eq __PACKAGE__;
40 1490         2238 my $error_msg = 'Bad value range definition!';
41 1490 100 100     5829 $range = 1 if not defined $range or $range eq 'normal';
42 1490 100       3665 $range = 100 if $range eq 'percent';
43 1490 100 66     9184 return $error_msg." It has to be 'normal', 'percent', a number or ARRAY of numbers or ARRAY of ARRAY's with two number!"
      100        
44             unless (not ref $range and is_nr( $range )) or (ref $range eq 'ARRAY') ;
45 1489 100       4704 $range = [$range] unless ref $range;
46 1489 100       4818 $range = [(@$range) x $basis->axis_count] if @$range == 1;
47 1489 100       3809 return "Range definition needs inside an ARRAY either one definition for all axis or one definition".
48             " for each axis!" if @$range != $basis->axis_count;
49 1486         3364 for my $axis_index ($basis->axis_iterator) {
50 4489         33797 my $axis_range = $range->[$axis_index];
51 4489 100       7684 if (not ref $axis_range){
    50          
52 4227 100       11034 if ($axis_range eq 'normal') {$range->[$axis_index] = [0, 1]}
  3 50       20  
53 0         0 elsif ($axis_range eq 'percent') {$range->[$axis_index] = [0, 100]}
54 4224         9725 else {$range->[$axis_index] = [0, $axis_range+0]}
55             } elsif (ref $axis_range eq 'ARRAY') {
56 262 100       587 return $error_msg.' Array at axis number '.$axis_index.' has to have two elements' unless @$axis_range == 2;
57 260 50       2925 return $error_msg.' None numeric value at lower bound for axis number '.$axis_index unless is_nr( $axis_range->[0] );
58 260 100       692 return $error_msg.' None numeric value at upper bound for axis number '.$axis_index unless is_nr( $axis_range->[1] );
59 259 100       896 return $error_msg.' Lower bound (first value) is >= than upper bound at axis number '.$axis_index if $axis_range->[0] >= $axis_range->[1];
60 0         0 } else { return "Range definitin for axis $axis_index was not an two element ARRAY!" }
61             }
62 1482         3675 return $range;
63             }
64             sub check_precision_definition { # check if precision def is valid and eval (exapand) it
65 312     312 0 722 my ($basis, $precision) = @_;
66 312 50       927 $basis = $basis->{'basis'} if ref $basis eq __PACKAGE__;
67 312 100       753 $precision = -1 unless defined $precision;
68 312 100       1023 $precision = [($precision) x $basis->axis_count] unless ref $precision;
69 312 50       3144 return 'need an ARRAY as definition of axis value precision' unless ref $precision eq 'ARRAY';
70 312 100       5296 return 'definition of axis value precision has to have same lengths as basis' unless @$precision == $basis->axis_count;
71 310         672 return $precision;
72             }
73             sub add_constraint {
74 0     0 0 0 my ($self, $name, $error_msg, $checker, $remedy) = @_;
75 0 0 0     0 return unless defined $name and not exists $self->{'constraint'}{$name}
      0        
      0        
      0        
      0        
      0        
76             and defined $error_msg and not ref $error_msg and length($error_msg) > 10
77             and ref $checker eq 'CODE' and ref $remedy eq 'CODE';
78 0         0 $self->{'constraint'}{$name} = {checker => $checker, remedy => $remedy, error => $error_msg};
79             }
80              
81             #### getter ############################################################
82 9917     9917 0 29419 sub basis { $_[0]{'basis'}}
83             sub is_linear { # overall linear space ?
84 3     3 0 13456 my ($self) = @_;
85 3 100       37 map { return 0 if $self->{'type'}[$_] != 1 } $self->basis->axis_iterator;
  8         38  
86 2         12 return 1;
87             }
88              
89             sub is_int_valued { # all ranges int valued ?
90 3     3 0 14 my ($self) = @_;
91 3 100       10 map { return 0 if $self->{'precision'}[$_] != 0 } $self->basis->axis_iterator;
  4         31  
92 0         0 return 1;
93             }
94              
95             sub is_axis_numeric {
96 13172     13172 0 20662 my ($self, $axis_nr) = @_;
97 13172 100 66     41656 return 0 if not defined $axis_nr or not exists $self->{'type'}[$axis_nr];
98 13168 100       46810 $self->{'type'}[$axis_nr] == 2 ? 0 : 1;
99              
100             }
101             sub axis_value_max { # --> +value
102 9     9 0 37 my ($self, $axis_nr, $range) = @_;
103 9         37 $range = $self->try_check_range_definition( $range );
104 9 50       57 return undef unless ref $range;
105 9 100       40 return undef unless $self->is_axis_numeric($axis_nr);
106 8         53 return $range->[$axis_nr][1];
107             }
108             sub axis_value_min { # --> +value
109 4     4 0 15 my ($self, $axis_nr, $range) = @_;
110 4         10 $range = $self->try_check_range_definition( $range );
111 4 50       14 return undef unless ref $range;
112 4 100       10 return undef unless $self->is_axis_numeric($axis_nr);
113 3         19 return $range->[$axis_nr][0];
114             }
115             sub axis_value_precision { # --> +precision?
116 16     16 0 9142 my ($self, $axis_nr, $precision) = @_;
117 16 50 33     116 return undef if not defined $axis_nr or not exists $self->{'type'}[$axis_nr];
118 16 100       50 return undef unless $self->is_axis_numeric($axis_nr);
119 15   33     80 $precision //= $self->{'precision'};
120 15 50 33     71 return undef unless ref $precision eq 'ARRAY' and exists $precision->[$axis_nr];
121 15         113 $precision->[$axis_nr];
122             }
123              
124             #### data checker ######################################################
125             sub try_check_range_definition { # check if range def is valid and eval (expand) it
126 3578     3578 0 6407 my ($self, $range) = @_;
127 3578 100       9193 return $self->{'range'} unless defined $range;
128 1179         2682 return check_range_definition( $self->{'basis'}, $range );
129             }
130             sub try_check_precision_definition { # check if range def is valid and eval (expand) it
131 992     992 0 1727 my ($self, $precision) = @_;
132 992 100       2552 return $self->{'precision'} unless defined $precision;
133 9         42 return check_precision_definition( $self->{'basis'}, $precision );
134             }
135              
136             sub check_value_shape { # $vals -- $range, $precision --> $@vals | ~!
137 220     220 0 547 my ($self, $values, $range, $precision) = @_;
138 220 100       581 return 'color value tuple in '.$self->basis->space_name.' space needs to be ARRAY ref with '.$self->basis->axis_count.' elements'
139             unless $self->basis->is_value_tuple( $values );
140 180         432 $range = $self->try_check_range_definition( $range );
141 180 50       423 return $range unless ref $range;
142 180         363 $precision = $self->try_check_precision_definition( $precision );
143 180 50       405 return $precision unless ref $precision;
144 180         326 my @names = $self->basis->long_axis_names;
145 180         356 for my $axis_index ($self->basis->axis_iterator){
146 417 50       798 next unless $self->is_axis_numeric( $axis_index );
147 417 100       1514 return $names[$axis_index]." value is below minimum of ".$range->[$axis_index][0]
148             if $values->[$axis_index] < $range->[$axis_index][0];
149 362 100       1277 return $names[$axis_index]." value is above maximum of ".$range->[$axis_index][1]
150             if $values->[$axis_index] > $range->[$axis_index][1];
151 303 100 100     1017 return $names[$axis_index]." value is not properly rounded "
152             if $precision->[$axis_index] >= 0
153             and round_decimals($values->[$axis_index], $precision->[$axis_index]) != $values->[$axis_index];
154             }
155 47         94 for my $constraint (values %{$self->{'constraint'}}){
  47         145  
156 0 0       0 return $constraint->{'error'} unless $constraint->{'checker'}->( $values );
157             }
158 47         404 return $values;
159             }
160              
161             sub is_in_linear_bounds { # :values --> ?
162 349     349 0 649 my ($self, $values) = @_;
163 349 100       678 return 0 unless $self->basis->is_number_tuple( $values );
164 346         727 for my $axis_nr ($self->basis->axis_iterator) {
165             return 0 if $self->{'type'}[$axis_nr] == 1
166             and ( $values->[$axis_nr] < $self->{'range'}[$axis_nr][0]
167 993 100 100     9076 or $values->[$axis_nr] > $self->{'range'}[$axis_nr][1] );
      100        
168             }
169 286         434 for my $constraint (values %{$self->{'constraint'}}){
  286         546  
170 0 0       0 return 0 unless $constraint->{'checker'}->( $values );
171             }
172 286         759 return 1;
173             }
174              
175             sub is_equal {
176 8     8 0 25 my ($self, $values_a, $values_b, $precision) = @_;
177 8 100 100     24 return 0 unless $self->basis->is_value_tuple( $values_a ) and $self->basis->is_value_tuple( $values_b );
178 3         11 $precision = $self->try_check_precision_definition( $precision );
179 3         8 for my $axis_nr ($self->basis->axis_iterator) {
180 9 50       26 return 0 if round_decimals($values_a->[$axis_nr], $precision->[$axis_nr])
181             != round_decimals($values_b->[$axis_nr], $precision->[$axis_nr]);
182             }
183 3         18 return 1;
184             }
185              
186             #### value shape #######################################################
187             sub clamp { # change values if outside of range to nearest boundary, angles get rotated into range
188 1945     1945 0 22494 my ($self, $values, $range) = @_;
189 1945         4576 $range = $self->try_check_range_definition( $range );
190 1945 50       4072 return $range unless ref $range;
191 1945 50       4278 $values = [] unless ref $values eq 'ARRAY';
192 1945         4190 pop @$values while @$values > $self->basis->axis_count;
193 1945         12074 for my $axis_nr ($self->basis->axis_iterator){
194 5861 50       11283 next unless $self->is_axis_numeric( $axis_nr ); # touch only numeric values
195 5861 100       10985 if (not defined $values->[$axis_nr]){
196 29         47 my $default_value = 0;
197 29 100 66     153 $default_value = $range->[$axis_nr][0] if $default_value < $range->[$axis_nr][0]
198             or $default_value > $range->[$axis_nr][1];
199 29         57 $values->[$axis_nr] = $default_value;
200 29         67 next;
201             }
202 5832 100       10217 if ($self->{'type'}[$axis_nr]){
203 5448 100       11945 $values->[$axis_nr] = $range->[$axis_nr][0] if $values->[$axis_nr] < $range->[$axis_nr][0];
204 5448 100       12333 $values->[$axis_nr] = $range->[$axis_nr][1] if $values->[$axis_nr] > $range->[$axis_nr][1];
205             } else {
206 384         737 my $delta = $range->[$axis_nr][1] - $range->[$axis_nr][0];
207 384         1120 $values->[$axis_nr] += $delta while $values->[$axis_nr] < $range->[$axis_nr][0];
208 384         2096 $values->[$axis_nr] -= $delta while $values->[$axis_nr] > $range->[$axis_nr][1];
209 384 100       1008 $values->[$axis_nr] = $range->[$axis_nr][0] if $values->[$axis_nr] == $range->[$axis_nr][1];
210             }
211             }
212 1945         3014 for my $constraint (values %{$self->{'constraint'}}){
  1945         4478  
213 0 0       0 $values = $constraint->{'remedy'}->( $values ) unless $constraint->{'checker'}->( $values );
214             }
215 1945         6137 return $values;
216             }
217              
218             sub round {
219 809     809 0 15595 my ($self, $values, $precision) = @_;
220 809 50       1500 return unless $self->basis->is_value_tuple( $values );
221 809         2955 $precision = $self->try_check_precision_definition( $precision );
222 809 50       1694 return "round got bad precision definition" unless ref $precision;
223 809 100 66     2217 [ map { ($self->is_axis_numeric( $_ ) and $precision->[$_] >= 0) ? round_decimals ($values->[$_], $precision->[$_]) : $values->[$_] } $self->basis->axis_iterator ];
  2434         5412  
224             }
225              
226             #### normalisation #####################################################
227             sub normalize {
228 554     554 0 9330 my ($self, $values, $range) = @_;
229 554 50       1217 return unless $self->basis->is_value_tuple( $values );
230 554         1432 $range = $self->try_check_range_definition( $range );
231 554 50       1393 return $range unless ref $range;
232 554 50       1143 [ map { ($self->is_axis_numeric( $_ )) ? (($values->[$_] - $range->[$_][0]) / ($range->[$_][1]-$range->[$_][0]))
  1666         2966  
233             : $values->[$_] } $self->basis->axis_iterator ];
234             }
235              
236             sub denormalize {
237 849     849 0 14851 my ($self, $values, $range) = @_;
238 849 50       1903 return unless $self->basis->is_value_tuple( $values );
239 849         2061 $range = $self->try_check_range_definition( $range );
240 849 50       1875 return $range unless ref $range;
241 849 50       1669 return [ map { ($self->is_axis_numeric( $_ )) ? ($values->[$_] * ($range->[$_][1]-$range->[$_][0]) + $range->[$_][0])
  2554         4693  
242             : $values->[$_] } $self->basis->axis_iterator ];
243             }
244              
245             sub denormalize_delta {
246 30     30 0 4261 my ($self, $delta_values, $range) = @_;
247 30 50       65 return unless $self->basis->is_value_tuple( $delta_values );
248 30         85 $range = $self->try_check_range_definition( $range );
249 30 50       109 return $range unless ref $range;
250 30 50       73 [ map { ($self->is_axis_numeric( $_ ))
  91         178  
251             ? ($delta_values->[$_] * ($range->[$_][1]-$range->[$_][0]))
252             : $delta_values->[$_] } $self->basis->axis_iterator ];
253             }
254              
255             sub delta { # values have to be normalized
256 43     43 0 10115 my ($self, $values1, $values2) = @_;
257 43 100 100     142 return unless $self->basis->is_value_tuple( $values1 ) and $self->basis->is_value_tuple( $values2 );
258             # ignore none numeric dimensions
259 37 50       104 my @delta = map { $self->is_axis_numeric($_) ? ($values2->[$_] - $values1->[$_]) : 0 } $self->basis->axis_iterator;
  112         262  
260 37 100       102 [ map { $self->{'type'}[$_] ? $delta[$_] : # adapt to circular dimensions
  112 100       494  
    100          
261             $delta[$_] < -0.5 ? ($delta[$_]+1) :
262             $delta[$_] > 0.5 ? ($delta[$_]-1) : $delta[$_] } $self->basis->axis_iterator ];
263             }
264              
265             1;