File Coverage

lib/Graphics/Toolkit/Color/Space/Shape.pm
Criterion Covered Total %
statement 185 191 96.8
branch 146 182 80.2
condition 55 66 83.3
subroutine 26 26 100.0
pod 0 22 0.0
total 412 487 84.6


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   378183 use v5.12;
  34         158  
6 34     34   209 use warnings;
  34         75  
  34         1938  
7 34     34   1221 use Graphics::Toolkit::Color::Space::Basis;
  34         161  
  34         1240  
8 34     34   264 use Graphics::Toolkit::Color::Space::Util qw/round_decimals is_nr/;
  34         80  
  34         119411  
9              
10             #### constructor #######################################################
11             sub new {
12 325     325 0 16473 my $pkg = shift;
13 325         1287 my ($basis, $type, $range, $precision, $constraint) = @_;
14 325 100       1079 return unless ref $basis eq 'Graphics::Toolkit::Color::Space::Basis';
15              
16             # expand axis type definition
17 324 100 100     1241 if (not defined $type){ $type = [ (1) x $basis->axis_count ] } # set all axis as linear per default
  193 100       646  
18             elsif (ref $type eq 'ARRAY' and @$type == $basis->axis_count ) {
19 129         371 for my $i ($basis->axis_iterator) {
20 385         635 my $atype = $type->[$i]; # type def of this axis
21 385 50       725 return unless defined $atype;
22 385 100 100     2018 if ($atype eq 'angular' or $atype eq 'circular' or $atype eq '0') { $type->[$i] = 0 }
  131 100 100     347  
    100 100        
      100        
23 248         562 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         7 } else { return 'invalid axis type definition in color space '.$basis->space_name }
28              
29 320         958 $range = expand_range_definition( $basis, $range );
30 320 100       916 return $range unless ref $range;
31 312         785 $precision = expand_precision_definition( $basis, $precision );
32 312 100       782 return $precision unless ref $precision;
33              
34             # check constraint def
35 310 100       707 if (defined $constraint){
36 9 100 100     59 return 'color space constraint definition has to be not empty a HASH ref' if ref $constraint ne 'HASH' or not %$constraint;
37 7         24 for my $const_name (keys %$constraint){
38 8         17 my $const_properties = $constraint->{$const_name};
39             return 'color space constraint "$const_name" need the properties: "checker", "remedy" and error "remedy"'
40 8 100 66     64 unless exists $const_properties->{'checker'} and exists $const_properties->{'remedy'} and exists $const_properties->{'error'};
      100        
41 6 100       24 return 'color space constraint "$const_name" property: "checker" has to be a CODE ref' unless ref $const_properties->{'checker'} eq 'CODE';
42 5 100       20 return 'color space constraint "$const_name" property: "remedy" has to be a CODE ref' unless ref $const_properties->{'remedy'} eq 'CODE';
43 4 100       18 return 'color space constraint "$const_name" property: "error" has to be a error message string' if ref $const_properties->{'error'};
44             }
45 301         593 } else { $constraint = {} }
46              
47 303         2142 bless { basis => $basis, type => $type, range => $range, precision => $precision, constraint => $constraint }
48             }
49              
50             #### object attribute checker ##########################################
51             sub expand_range_definition { # check if range def is valid and eval (expand) it
52 1499     1499 0 2955 my ($basis, $range) = @_;
53 1499 50       4210 $basis = $basis->{'basis'} if ref $basis eq __PACKAGE__;
54 1499         2505 my $error_msg = 'Bad value range definition!';
55 1499 100 100     6199 $range = 1 if not defined $range or $range eq 'normal';
56 1499 100       9100 $range = 100 if $range eq 'percent';
57 1499 100 66     7386 return $error_msg." It has to be 'normal', 'percent', a number or ARRAY of numbers or ARRAY of ARRAY's with two number!"
      100        
58             unless (not ref $range and is_nr( $range )) or (ref $range eq 'ARRAY') ;
59 1498 100       5246 $range = [$range] unless ref $range;
60 1498 100       5669 $range = [(@$range) x $basis->axis_count] if @$range == 1;
61 1498 100       4279 return "Range definition needs inside an ARRAY either one definition for all axis or one definition".
62             " for each axis!" if @$range != $basis->axis_count;
63 1495         3536 for my $axis_index ($basis->axis_iterator) {
64 4516         7332 my $axis_range = $range->[$axis_index];
65 4516 100       8032 if (not ref $axis_range){
    50          
66 4254 100       9439 if ($axis_range eq 'normal') {$range->[$axis_index] = [0, 1]}
  3 50       11  
67 0         0 elsif ($axis_range eq 'percent') {$range->[$axis_index] = [0, 100]}
68 4251         11050 else {$range->[$axis_index] = [0, $axis_range+0]}
69             } elsif (ref $axis_range eq 'ARRAY') {
70 262 100       682 return $error_msg.' Array at axis number '.$axis_index.' has to have two elements' unless @$axis_range == 2;
71 260 50       649 return $error_msg.' None numeric value at lower bound for axis number '.$axis_index unless is_nr( $axis_range->[0] );
72 260 100       674 return $error_msg.' None numeric value at upper bound for axis number '.$axis_index unless is_nr( $axis_range->[1] );
73 259 100       842 return $error_msg.' Lower bound (first value) is >= than upper bound at axis number '.$axis_index if $axis_range->[0] >= $axis_range->[1];
74 0         0 } else { return "Range definitin for axis $axis_index was not an two element ARRAY!" }
75             }
76 1491         4182 return $range;
77             }
78             sub try_check_range_definition { # check if range def is valid and eval (expand) it
79 3591     3591 0 7060 my ($self, $range) = @_;
80 3591 100       10898 return $self->{'range'} unless defined $range;
81 1179         2839 return expand_range_definition( $self->{'basis'}, $range );
82             }
83              
84             sub expand_precision_definition { # check if precision def is valid and eval (exapand) it
85 321     321 0 648 my ($basis, $precision) = @_;
86 321 50       919 $basis = $basis->{'basis'} if ref $basis eq __PACKAGE__;
87 321 100       770 $precision = -1 unless defined $precision;
88 321 100       1234 $precision = [($precision) x $basis->axis_count] unless ref $precision;
89 321 50       2451 return 'need an ARRAY as definition of axis value precision' unless ref $precision eq 'ARRAY';
90 321 100       3144 return 'definition of axis value precision has to have same lengths as basis' unless @$precision == $basis->axis_count;
91 319         718 return $precision;
92             }
93             sub try_check_precision_definition { # check if range def is valid and eval (expand) it
94 999     999 0 2102 my ($self, $precision) = @_;
95 999 100       3133 return $self->{'precision'} unless defined $precision;
96 9         36 return expand_precision_definition( $self->{'basis'}, $precision );
97             }
98              
99             #### getter of space object ############################################
100 9996     9996 0 26825 sub basis { $_[0]{'basis'}}
101             # per axis
102             sub is_axis_numeric {
103 13232     13232 0 23211 my ($self, $axis_nr) = @_;
104 13232 100 66     45626 return 0 if not defined $axis_nr or not exists $self->{'type'}[$axis_nr];
105 13228 100       54640 $self->{'type'}[$axis_nr] == 2 ? 0 : 1;
106              
107             }
108             sub axis_value_max { # --> +value
109 9     9 0 33 my ($self, $axis_nr, $range) = @_;
110 9         40 $range = $self->try_check_range_definition( $range );
111 9 50       37 return undef unless ref $range;
112 9 100       34 return undef unless $self->is_axis_numeric($axis_nr);
113 8         41 return $range->[$axis_nr][1];
114             }
115             sub axis_value_min { # --> +value
116 4     4 0 11 my ($self, $axis_nr, $range) = @_;
117 4         14 $range = $self->try_check_range_definition( $range );
118 4 50       11 return undef unless ref $range;
119 4 100       13 return undef unless $self->is_axis_numeric($axis_nr);
120 3         17 return $range->[$axis_nr][0];
121             }
122             sub axis_value_precision { # --> +precision?
123 16     16 0 2830 my ($self, $axis_nr, $precision) = @_;
124 16 50 33     119 return undef if not defined $axis_nr or not exists $self->{'type'}[$axis_nr];
125 16 100       43 return undef unless $self->is_axis_numeric($axis_nr);
126 15   33     105 $precision //= $self->{'precision'};
127 15 50 33     73 return undef unless ref $precision eq 'ARRAY' and exists $precision->[$axis_nr];
128 15         95 $precision->[$axis_nr];
129             }
130              
131             # all axis
132             sub is_euclidean { # all axis linear ?
133 21     21 0 1390 my ($self) = @_;
134 21 100       93 map { return 0 if $self->{'type'}[$_] != 1 } $self->basis->axis_iterator;
  53         313  
135 12         64 return 1;
136             }
137              
138             sub is_cylindrical { # one axis angular, rest linear ?
139 21     21 0 60 my ($self) = @_;
140 21         46 my $angular_axis = 0;
141 21 100       77 map { $angular_axis++ if $self->{'type'}[$_] == 0;
  64         219  
142 64 100       204 return 0 if $self->{'type'}[$_] > 1; } $self->basis->axis_iterator;
143 20 100       181 return ($angular_axis == 1) ? 1 : 0;
144             }
145              
146             sub is_int_valued { # all ranges int valued ?
147 3     3 0 12 my ($self) = @_;
148 3 100       9 map { return 0 if $self->{'precision'}[$_] != 0 } $self->basis->axis_iterator;
  4         27  
149 0         0 return 1;
150             }
151              
152             #### value checker #####################################################
153             sub check_value_shape { # $vals -- $range, $precision --> $@vals | ~!
154 220     220 0 585 my ($self, $values, $range, $precision) = @_;
155 220 100       566 return 'color value tuple in '.$self->basis->space_name.' space needs to be ARRAY ref with '.$self->basis->axis_count.' elements'
156             unless $self->basis->is_value_tuple( $values );
157 180         507 $range = $self->try_check_range_definition( $range );
158 180 50       494 return $range unless ref $range;
159 180         416 $precision = $self->try_check_precision_definition( $precision );
160 180 50       1427 return $precision unless ref $precision;
161 180         351 my @names = $self->basis->long_axis_names;
162 180         1324 for my $axis_index ($self->basis->axis_iterator){
163 417 50       821 next unless $self->is_axis_numeric( $axis_index );
164 417 100       1625 return $names[$axis_index]." value is below minimum of ".$range->[$axis_index][0]
165             if $values->[$axis_index] < $range->[$axis_index][0];
166 362 100       1345 return $names[$axis_index]." value is above maximum of ".$range->[$axis_index][1]
167             if $values->[$axis_index] > $range->[$axis_index][1];
168 303 100 100     1112 return $names[$axis_index]." value is not properly rounded "
169             if $precision->[$axis_index] >= 0
170             and round_decimals($values->[$axis_index], $precision->[$axis_index]) != $values->[$axis_index];
171             }
172 47         125 for my $constraint (values %{$self->{'constraint'}}){
  47         214  
173 0 0       0 return $constraint->{'error'} unless $constraint->{'checker'}->( $values );
174             }
175 47         410 return $values;
176             }
177              
178             sub is_in_linear_bounds { # :values --> ?
179 349     349 0 727 my ($self, $values) = @_;
180 349 100       752 return 0 unless $self->basis->is_number_tuple( $values );
181 346         821 for my $axis_nr ($self->basis->axis_iterator) {
182             return 0 if $self->{'type'}[$axis_nr] == 1
183             and ( $values->[$axis_nr] < $self->{'range'}[$axis_nr][0]
184 993 100 100     5431 or $values->[$axis_nr] > $self->{'range'}[$axis_nr][1] );
      100        
185             }
186 286         490 for my $constraint (values %{$self->{'constraint'}}){
  286         662  
187 0 0       0 return 0 unless $constraint->{'checker'}->( $values );
188             }
189 286         841 return 1;
190             }
191              
192             sub is_equal {
193 8     8 0 28 my ($self, $values_a, $values_b, $precision) = @_;
194 8 100 100     25 return 0 unless $self->basis->is_value_tuple( $values_a ) and $self->basis->is_value_tuple( $values_b );
195 3         11 $precision = $self->try_check_precision_definition( $precision );
196 3         9 for my $axis_nr ($self->basis->axis_iterator) {
197 9 50       42 return 0 if round_decimals($values_a->[$axis_nr], $precision->[$axis_nr])
198             != round_decimals($values_b->[$axis_nr], $precision->[$axis_nr]);
199             }
200 3         20 return 1;
201             }
202              
203             #### value ops #########################################################
204             sub clamp { # change values if outside of range to nearest boundary, angles get rotated into range
205 1952     1952 0 15489 my ($self, $values, $range) = @_;
206 1952         4496 $range = $self->try_check_range_definition( $range );
207 1952 50       4641 return $range unless ref $range;
208 1952 50       4809 $values = [] unless ref $values eq 'ARRAY';
209 1952         5124 pop @$values while @$values > $self->basis->axis_count;
210 1952         4518 for my $axis_nr ($self->basis->axis_iterator){
211 5882 50       11254 next unless $self->is_axis_numeric( $axis_nr ); # touch only numeric values
212 5882 100       12729 if (not defined $values->[$axis_nr]){
213 29         49 my $default_value = 0;
214 29 100 66     134 $default_value = $range->[$axis_nr][0] if $default_value < $range->[$axis_nr][0]
215             or $default_value > $range->[$axis_nr][1];
216 29         54 $values->[$axis_nr] = $default_value;
217 29         62 next;
218             }
219 5853 100       11344 if ($self->{'type'}[$axis_nr]){
220 5469 100       13594 $values->[$axis_nr] = $range->[$axis_nr][0] if $values->[$axis_nr] < $range->[$axis_nr][0];
221 5469 100       14013 $values->[$axis_nr] = $range->[$axis_nr][1] if $values->[$axis_nr] > $range->[$axis_nr][1];
222             } else {
223 384         932 my $delta = $range->[$axis_nr][1] - $range->[$axis_nr][0];
224 384         1146 $values->[$axis_nr] += $delta while $values->[$axis_nr] < $range->[$axis_nr][0];
225 384         1018 $values->[$axis_nr] -= $delta while $values->[$axis_nr] > $range->[$axis_nr][1];
226 384 100       1111 $values->[$axis_nr] = $range->[$axis_nr][0] if $values->[$axis_nr] == $range->[$axis_nr][1];
227             }
228             }
229 1952         3173 for my $constraint (values %{$self->{'constraint'}}){
  1952         5549  
230 0 0       0 $values = $constraint->{'remedy'}->( $values ) unless $constraint->{'checker'}->( $values );
231             }
232 1952         8015 return $values;
233             }
234              
235             sub round {
236 816     816 0 12736 my ($self, $values, $precision) = @_;
237 816 50       1801 return unless $self->basis->is_value_tuple( $values );
238 816         1867 $precision = $self->try_check_precision_definition( $precision );
239 816 50       1976 return "round got bad precision definition" unless ref $precision;
240 816 100 66     1900 [ map { ($self->is_axis_numeric( $_ ) and $precision->[$_] >= 0) ? round_decimals ($values->[$_], $precision->[$_]) : $values->[$_] } $self->basis->axis_iterator ];
  2455         4912  
241             }
242              
243             # normalisation
244             sub normalize {
245 553     553 0 8291 my ($self, $values, $range) = @_;
246 553 50       1334 return unless $self->basis->is_value_tuple( $values );
247 553         1594 $range = $self->try_check_range_definition( $range );
248 553 50       1401 return $range unless ref $range;
249 553 50       1186 [ map { ($self->is_axis_numeric( $_ )) ? (($values->[$_] - $range->[$_][0]) / ($range->[$_][1]-$range->[$_][0]))
  1663         3348  
250             : $values->[$_] } $self->basis->axis_iterator ];
251             }
252              
253             sub denormalize {
254 856     856 0 11999 my ($self, $values, $range) = @_;
255 856 50       2029 return unless $self->basis->is_value_tuple( $values );
256 856         2285 $range = $self->try_check_range_definition( $range );
257 856 50       2040 return $range unless ref $range;
258 856 50       1821 return [ map { ($self->is_axis_numeric( $_ )) ? ($values->[$_] * ($range->[$_][1]-$range->[$_][0]) + $range->[$_][0])
  2575         5407  
259             : $values->[$_] } $self->basis->axis_iterator ];
260             }
261              
262             sub denormalize_delta {
263 30     30 0 3545 my ($self, $delta_values, $range) = @_;
264 30 50       65 return unless $self->basis->is_value_tuple( $delta_values );
265 30         80 $range = $self->try_check_range_definition( $range );
266 30 50       132 return $range unless ref $range;
267 30 50       71 [ map { ($self->is_axis_numeric( $_ ))
  91         210  
268             ? ($delta_values->[$_] * ($range->[$_][1]-$range->[$_][0]))
269             : $delta_values->[$_] } $self->basis->axis_iterator ];
270             }
271              
272             sub delta { # values have to be normalized
273 43     43 0 8797 my ($self, $values1, $values2) = @_;
274 43 100 100     141 return unless $self->basis->is_value_tuple( $values1 ) and $self->basis->is_value_tuple( $values2 );
275             # ignore none numeric dimensions
276 37 50       157 my @delta = map { $self->is_axis_numeric($_) ? ($values2->[$_] - $values1->[$_]) : 0 } $self->basis->axis_iterator;
  112         872  
277 37 100       98 [ map { $self->{'type'}[$_] ? $delta[$_] : # adapt to circular dimensions
  112 100       435  
    100          
278             $delta[$_] < -0.5 ? ($delta[$_]+1) :
279             $delta[$_] > 0.5 ? ($delta[$_]-1) : $delta[$_] } $self->basis->axis_iterator ];
280             }
281              
282             1;