File Coverage

lib/Graphics/Toolkit/Color/Space/Shape.pm
Criterion Covered Total %
statement 220 225 97.7
branch 169 206 82.0
condition 56 72 77.7
subroutine 30 30 100.0
pod 0 26 0.0
total 475 559 84.9


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   360896 use v5.12;
  34         132  
6 34     34   142 use warnings;
  34         56  
  34         1580  
7 34     34   1140 use Graphics::Toolkit::Color::Space::Basis;
  34         76  
  34         902  
8 34     34   133 use Graphics::Toolkit::Color::Space::Util qw/round_decimals is_nr/;
  34         78  
  34         105102  
9              
10             #### constructor #######################################################
11             sub new {
12 325     325 0 19989 my $pkg = shift;
13 325         934 my ($basis, $type, $range, $precision, $constraint) = @_;
14 325 100       2388 return unless ref $basis eq 'Graphics::Toolkit::Color::Space::Basis';
15              
16             # expand axis type definition
17 324 100 100     2617 if (not defined $type){ $type = [ (1) x $basis->axis_count ] } # set all axis as linear per default
  193 100       535  
18             elsif (ref $type eq 'ARRAY' and @$type == $basis->axis_count ) {
19 129         285 for my $i ($basis->axis_iterator) {
20 385         507 my $atype = $type->[$i]; # type def of this axis
21 385 50       551 return unless defined $atype;
22 385 100 100     1532 if ($atype eq 'angular' or $atype eq 'circular' or $atype eq '0') { $type->[$i] = 0 }
  131 100 100     281  
    100 100        
      100        
23 248         399 elsif ($atype eq 'linear' or $atype eq '1') { $type->[$i] = 1 }
24 4         10 elsif ($atype eq 'no' or $atype eq '2') { $type->[$i] = 2 }
25 2         21 else { return 'invalid axis type at element '.$i.'. It has to be "angular", "linear" or "no".' }
26             }
27 2         8 } else { return 'invalid axis type definition in color space '.$basis->space_name }
28              
29 320         793 $range = expand_range_definition( $basis, $range );
30 320 100       695 return $range unless ref $range;
31 312         632 $precision = expand_precision_definition( $basis, $precision );
32 312 100       508 return $precision unless ref $precision;
33              
34             # check constraint def
35 310 100       495 if (defined $constraint){
36 40 100 100     211 return 'color space constraint definition has to be a none empty HASH ref' if ref $constraint ne 'HASH' or not %$constraint;
37 38         125 for my $constraint_name (keys %$constraint){
38 39         68 my $properties = $constraint->{$constraint_name};
39 39 100 66     175 return 'a color space constraint has to be a HASH ref with three keys' unless ref $properties eq 'HASH' and keys(%$properties) == 3;
40 37         135 $properties = {%$properties};
41 37         97 my $error_msg = 'constraint "$constraint_name" in '.$basis->space_name.' color space';
42 37         201 for (qw/checker error remedy/){
43             return $error_msg." needs the string-propertiy '$_'"
44 105 100 33     635 unless exists $properties->{$_} and $properties->{$_} and not ref $properties->{$_};
      66        
45             }
46 34         98 $properties->{'checker_code'} = $properties->{'checker'};
47 34         3227 $properties->{'checker'} = eval 'sub {'.$properties->{'checker_code'}.'}';
48 34 50       151 return 'checker code of '.$error_msg.":'$properties->{checker_code}' does not eval - $@" if $@;
49 34         79 $properties->{'remedy_code'} = $properties->{'remedy'};
50 34         2932 $properties->{'remedy'} = eval 'sub {'.$properties->{'remedy_code'}.'}';
51 34 50       158 return 'remedy code of '.$error_msg.":'$properties->{remedy_code}' does not eval - $@" if $@;
52 34         94 $constraint->{ $constraint_name } = $properties;
53             }
54 270         433 } else { $constraint = '' }
55              
56 303         1576 bless { basis => $basis, type => $type, range => $range, precision => $precision, constraint => $constraint }
57             }
58              
59             #### object attribute checker ##########################################
60             sub expand_range_definition { # check if range def is valid and eval (expand) it
61 1572     1572 0 2201 my ($basis, $range) = @_;
62 1572 50       2934 $basis = $basis->{'basis'} if ref $basis eq __PACKAGE__;
63 1572         1763 my $error_msg = 'Bad value range definition!';
64 1572 100 100     4417 $range = 1 if not defined $range or $range eq 'normal';
65 1572 100       6795 $range = 100 if $range eq 'percent';
66 1572 100 66     4262 return $error_msg." It has to be 'normal', 'percent', a number or ARRAY of numbers or ARRAY of ARRAY's with two number!"
      100        
67             unless (not ref $range and is_nr( $range )) or (ref $range eq 'ARRAY') ;
68 1571 100       3580 $range = [$range] unless ref $range;
69 1571 100       3466 $range = [(@$range) x $basis->axis_count] if @$range == 1;
70 1571 100       2908 return "Range definition needs inside an ARRAY either one definition for all axis or one definition".
71             " for each axis!" if @$range != $basis->axis_count;
72 1568         2423 for my $axis_index ($basis->axis_iterator) {
73 4735         5118 my $axis_range = $range->[$axis_index];
74 4735 100       5858 if (not ref $axis_range){
    50          
75 4335 100       6588 if ($axis_range eq 'normal') {$range->[$axis_index] = [0, 1]}
  3 50       13  
76 0         0 elsif ($axis_range eq 'percent') {$range->[$axis_index] = [0, 100]}
77 4332         7153 else {$range->[$axis_index] = [0, $axis_range+0]}
78             } elsif (ref $axis_range eq 'ARRAY') {
79 400 100       696 return $error_msg.' Array at axis number '.$axis_index.' has to have two elements' unless @$axis_range == 2;
80 398 50       770 return $error_msg.' None numeric value at lower bound for axis number '.$axis_index unless is_nr( $axis_range->[0] );
81 398 100       716 return $error_msg.' None numeric value at upper bound for axis number '.$axis_index unless is_nr( $axis_range->[1] );
82 397 100       916 return $error_msg.' Lower bound (first value) is >= than upper bound at axis number '.$axis_index if $axis_range->[0] >= $axis_range->[1];
83 0         0 } else { return "Range definitin for axis $axis_index was not an two element ARRAY!" }
84             }
85 1564         2912 return $range;
86             }
87             sub try_check_range_definition { # check if range def is valid and eval (expand) it
88 4085     4085 0 5446 my ($self, $range) = @_;
89 4085 100       7599 return $self->{'range'} unless defined $range;
90 1252         2020 return expand_range_definition( $self->{'basis'}, $range );
91             }
92              
93             sub expand_precision_definition { # check if precision def is valid and eval (exapand) it
94 321     321 0 532 my ($basis, $precision) = @_;
95 321 50       697 $basis = $basis->{'basis'} if ref $basis eq __PACKAGE__;
96 321 100       2815 $precision = -1 unless defined $precision;
97 321 100       4568 $precision = [($precision) x $basis->axis_count] unless ref $precision;
98 321 50       688 return 'need an ARRAY as definition of axis value precision' unless ref $precision eq 'ARRAY';
99 321 100       539 return 'definition of axis value precision has to have same lengths as basis' unless @$precision == $basis->axis_count;
100 319         546 return $precision;
101             }
102             sub try_check_precision_definition { # check if range def is valid and eval (expand) it
103 1515     1515 0 1886 my ($self, $precision) = @_;
104 1515 100       2921 return $self->{'precision'} unless defined $precision;
105 9         26 return expand_precision_definition( $self->{'basis'}, $precision );
106             }
107              
108             #### getter of space object ############################################
109 11322     11322 0 19197 sub basis { $_[0]{'basis'}}
110             # per axis
111             sub is_axis_numeric {
112 15197     15197 0 16841 my ($self, $axis_nr) = @_;
113 15197 100 66     31932 return 0 if not defined $axis_nr or not exists $self->{'type'}[$axis_nr];
114 15193 100       39541 $self->{'type'}[$axis_nr] == 2 ? 0 : 1;
115              
116             }
117             sub is_axis_euclidean {
118 65     65 0 101 my ($self, $axis_nr) = @_;
119 65 100 66     219 return 0 if not defined $axis_nr or not exists $self->{'type'}[$axis_nr];
120 64 100       132 $self->{'type'}[$axis_nr] == 1 ? 1 : 0;
121              
122             }
123             sub axis_value_max { # --> +value
124 9     9 0 49 my ($self, $axis_nr, $range) = @_;
125 9         37 $range = $self->try_check_range_definition( $range );
126 9 50       43 return undef unless ref $range;
127 9 100       28 return undef unless $self->is_axis_numeric($axis_nr);
128 8         36 return $range->[$axis_nr][1];
129             }
130             sub axis_value_min { # --> +value
131 4     4 0 14 my ($self, $axis_nr, $range) = @_;
132 4         13 $range = $self->try_check_range_definition( $range );
133 4 50       39 return undef unless ref $range;
134 4 100       12 return undef unless $self->is_axis_numeric($axis_nr);
135 3         37 return $range->[$axis_nr][0];
136             }
137             sub axis_value_precision { # --> +precision?
138 16     16 0 2931 my ($self, $axis_nr, $precision) = @_;
139 16 50 33     101 return undef if not defined $axis_nr or not exists $self->{'type'}[$axis_nr];
140 16 100       39 return undef unless $self->is_axis_numeric($axis_nr);
141 15   33     79 $precision //= $self->{'precision'};
142 15 50 33     64 return undef unless ref $precision eq 'ARRAY' and exists $precision->[$axis_nr];
143 15         81 $precision->[$axis_nr];
144             }
145              
146             # all axis
147             sub is_euclidean { # all axis linear ?
148 21     21 0 1932 my ($self) = @_;
149 21 100       87 map { return 0 if $self->{'type'}[$_] != 1 } $self->basis->axis_iterator;
  53         213  
150 12         77 return 1;
151             }
152              
153             sub is_cylindrical { # one axis angular, rest linear ?
154 21     21 0 91 my ($self) = @_;
155 21         48 my $angular_axis = 0;
156 21 100       72 map { $angular_axis++ if $self->{'type'}[$_] == 0;
  64         205  
157 64 100       182 return 0 if $self->{'type'}[$_] > 1; } $self->basis->axis_iterator;
158 20 100       135 return ($angular_axis == 1) ? 1 : 0;
159             }
160              
161             sub is_int_valued { # all ranges int valued ?
162 3     3 0 15 my ($self) = @_;
163 3 100       10 map { return 0 if $self->{'precision'}[$_] != 0 } $self->basis->axis_iterator;
  4         31  
164 0         0 return 1;
165             }
166              
167             #### value checker #####################################################
168             sub check_value_shape { # $vals -- $range, $precision --> $@vals | ~!
169 222     222 0 504 my ($self, $values, $range, $precision) = @_;
170 222 100       499 return 'color value tuple in '.$self->basis->space_name.' space needs to be ARRAY ref with '.$self->basis->axis_count.' elements'
171             unless $self->basis->is_value_tuple( $values );
172 182         447 $range = $self->try_check_range_definition( $range );
173 182 50       382 return $range unless ref $range;
174 182         379 $precision = $self->try_check_precision_definition( $precision );
175 182 50       351 return $precision unless ref $precision;
176 182         2067 my @names = $self->basis->long_axis_names;
177 182         380 for my $axis_index ($self->basis->axis_iterator){
178 423 50       759 next unless $self->is_axis_numeric( $axis_index );
179 423 100       1402 return $names[$axis_index]." value is below minimum of ".$range->[$axis_index][0]
180             if $values->[$axis_index] < $range->[$axis_index][0];
181 368 100       1214 return $names[$axis_index]." value is above maximum of ".$range->[$axis_index][1]
182             if $values->[$axis_index] > $range->[$axis_index][1];
183 309 100 100     1033 return $names[$axis_index]." value is not properly rounded "
184             if $precision->[$axis_index] >= 0
185             and round_decimals($values->[$axis_index], $precision->[$axis_index]) != $values->[$axis_index];
186             }
187 49 100       147 if ($self->has_constraints){
188 6         20 my $values = $self->normalize($values, $range);
189 6         8 for my $constraint (values %{$self->{'constraint'}}){
  6         17  
190 6 100       162 return $constraint->{'error'} unless $constraint->{'checker'}->( $values );
191             }
192             }
193 48         322 return $values;
194             }
195              
196             sub is_equal { # @values_a, @values_b -- $precision --> ?
197 8     8 0 47 my ($self, $values_a, $values_b, $precision) = @_;
198 8 100 100     21 return 0 unless $self->basis->is_value_tuple( $values_a ) and $self->basis->is_value_tuple( $values_b );
199 3         60 $precision = $self->try_check_precision_definition( $precision );
200 3         9 for my $axis_nr ($self->basis->axis_iterator) {
201 9 50       27 return 0 if round_decimals($values_a->[$axis_nr], $precision->[$axis_nr])
202             != round_decimals($values_b->[$axis_nr], $precision->[$axis_nr]);
203             }
204 3         18 return 1;
205             }
206              
207 1861 100   1861 0 2289 sub has_constraints { my ($self) = @_; return (ref $self->{'constraint'}) ? 1 : 0 } # --> ?
  1861         4086  
208              
209             sub is_in_constraints { # @values --> ? # normalized values only, so it works on any ranges
210 2     2 0 6 my ($self, $values) = @_;
211 2 50       8 return 1 unless $self->has_constraints;
212 2         4 for my $constraint (values %{$self->{'constraint'}}){
  2         10  
213 2 100       64 return 0 unless $constraint->{'checker'}->( $values );
214             }
215 1         6 return 1;
216             }
217              
218             sub is_in_bounds { # :values --> ?
219 6     6 0 19 my ($self, $values, $range) = @_;
220 6 50       24 return 0 unless $self->basis->is_number_tuple( $values );
221 6         26 $range = $self->try_check_range_definition( $range );
222 6         17 for my $axis_nr ($self->basis->axis_iterator) {
223 12 50       56 next if $self->{'type'}[$axis_nr] > 1; # skip none numeric axis
224 12 100 66     86 return 0 if $values->[$axis_nr] < $range->[$axis_nr][0]
225             or $values->[$axis_nr] > $range->[$axis_nr][1];
226             }
227 3 50       16 if ($self->has_constraints){
228 0         0 return $self->is_in_constraints( $self->normalize( $values, $range) );
229             }
230 3         26 return 1;
231             }
232              
233             sub is_in_linear_bounds { # :values --> ?
234 349     349 0 494 my ($self, $values, $range) = @_;
235 349 100       500 return 0 unless $self->basis->is_number_tuple( $values );
236 346         549 $range = $self->try_check_range_definition( $range );
237 346         491 for my $axis_nr ($self->basis->axis_iterator) {
238 993 100       1535 next if $self->{'type'}[$axis_nr] != 1; # skip none linear axis
239 919 100 100     2546 return 0 if $values->[$axis_nr] < $range->[$axis_nr][0]
240             or $values->[$axis_nr] > $range->[$axis_nr][1];
241             }
242 286 50       505 if ($self->has_constraints){
243 0         0 return $self->is_in_constraints( $self->normalize( $values, $range) );
244             }
245 286         551 return 1;
246             }
247              
248             #### value ops #########################################################
249             sub clamp { # change values if outside of range to nearest boundary, angles get rotated into range
250 1517     1517 0 14747 my ($self, $values, $range) = @_;
251 1517         2243 $range = $self->try_check_range_definition( $range );
252 1517 50       2320 return $range unless ref $range;
253 1517 50       2521 $values = [] unless ref $values eq 'ARRAY';
254 1517         2490 pop @$values while @$values > $self->basis->axis_count;
255 1517         2081 for my $axis_nr ($self->basis->axis_iterator){
256 4574 50       5757 next unless $self->is_axis_numeric( $axis_nr ); # touch only numeric values
257 4574 100       6036 if (not defined $values->[$axis_nr]){
258 29         50 my $default_value = 0;
259 29 100 66     148 $default_value = $range->[$axis_nr][0] if $default_value < $range->[$axis_nr][0]
260             or $default_value > $range->[$axis_nr][1];
261 29         60 $values->[$axis_nr] = $default_value;
262 29         94 next;
263             }
264 4545 100       5541 if ($self->{'type'}[$axis_nr]){
265 4260 100       6726 $values->[$axis_nr] = $range->[$axis_nr][0] if $values->[$axis_nr] < $range->[$axis_nr][0];
266 4260 100       6886 $values->[$axis_nr] = $range->[$axis_nr][1] if $values->[$axis_nr] > $range->[$axis_nr][1];
267             } else {
268 285         459 my $delta = $range->[$axis_nr][1] - $range->[$axis_nr][0];
269 285         609 $values->[$axis_nr] += $delta while $values->[$axis_nr] < $range->[$axis_nr][0];
270 285         556 $values->[$axis_nr] -= $delta while $values->[$axis_nr] > $range->[$axis_nr][1];
271 285 100       560 $values->[$axis_nr] = $range->[$axis_nr][0] if $values->[$axis_nr] == $range->[$axis_nr][1];
272             }
273             }
274 1517 100       2732 if ($self->has_constraints){
275 20         67 $values = $self->normalize( $values, $range);
276 20         38 for my $constraint (values %{$self->{'constraint'}}){
  20         53  
277 20 100       475 $values = $constraint->{'remedy'}->($values) unless $constraint->{'checker'}->( $values );
278             }
279 20         58 $values = $self->denormalize( $values, $range);
280             }
281 1517         3752 return $values;
282             }
283              
284             sub round {
285 1330     1330 0 16907 my ($self, $values, $precision) = @_;
286 1330 50       1669 return unless $self->basis->is_value_tuple( $values );
287 1330         1939 $precision = $self->try_check_precision_definition( $precision );
288 1330 50       1870 return "round got bad precision definition" unless ref $precision;
289 1330 100 66     1732 [ map { ($self->is_axis_numeric( $_ ) and $precision->[$_] >= 0) ? round_decimals ($values->[$_], $precision->[$_]) : $values->[$_] } $self->basis->axis_iterator ];
  3997         4647  
290             }
291              
292             # normalisation
293             sub normalize {
294 594     594 0 6294 my ($self, $values, $range) = @_;
295 594 50       1125 return unless $self->basis->is_value_tuple( $values );
296 594         1055 $range = $self->try_check_range_definition( $range );
297 594 50       1082 return $range unless ref $range;
298 594 50       913 [ map { ($self->is_axis_numeric( $_ )) ? (($values->[$_] - $range->[$_][0]) / ($range->[$_][1]-$range->[$_][0]))
  1786         2361  
299             : $values->[$_] } $self->basis->axis_iterator ];
300             }
301              
302             sub denormalize {
303 1390     1390 0 11516 my ($self, $values, $range) = @_;
304 1390 50       2181 return unless $self->basis->is_value_tuple( $values );
305 1390         2182 $range = $self->try_check_range_definition( $range );
306 1390 50       2131 return $range unless ref $range;
307 1390 50       1894 return [ map { ($self->is_axis_numeric( $_ )) ? ($values->[$_] * ($range->[$_][1]-$range->[$_][0]) + $range->[$_][0])
  4177         5168  
308             : $values->[$_] } $self->basis->axis_iterator ];
309             }
310              
311             sub denormalize_delta {
312 30     30 0 3834 my ($self, $delta_values, $range) = @_;
313 30 50       57 return unless $self->basis->is_value_tuple( $delta_values );
314 30         68 $range = $self->try_check_range_definition( $range );
315 30 50       73 return $range unless ref $range;
316 30 50       75 [ map { ($self->is_axis_numeric( $_ ))
  91         153  
317             ? ($delta_values->[$_] * ($range->[$_][1]-$range->[$_][0]))
318             : $delta_values->[$_] } $self->basis->axis_iterator ];
319             }
320              
321             sub delta { # values have to be normalized
322 43     43 0 10156 my ($self, $values1, $values2) = @_;
323 43 100 100     159 return unless $self->basis->is_value_tuple( $values1 ) and $self->basis->is_value_tuple( $values2 );
324             # ignore none numeric dimensions
325 37 50       94 my @delta = map { $self->is_axis_numeric($_) ? ($values2->[$_] - $values1->[$_]) : 0 } $self->basis->axis_iterator;
  112         214  
326 37 100       90 [ map { $self->{'type'}[$_] ? $delta[$_] : # adapt to circular dimensions
  112 100       389  
    100          
327             $delta[$_] < -0.5 ? ($delta[$_]+1) :
328             $delta[$_] > 0.5 ? ($delta[$_]-1) : $delta[$_] } $self->basis->axis_iterator ];
329             }
330              
331             1;