File Coverage

lib/Graphics/Toolkit/Color/Space/Shape.pm
Criterion Covered Total %
statement 243 250 97.2
branch 184 228 80.7
condition 63 87 72.4
subroutine 32 32 100.0
pod 0 28 0.0
total 522 625 83.5


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 50     50   269225 use v5.12;
  50         212  
6 50     50   183 use warnings;
  50         66  
  50         2131  
7 50     50   805 use Graphics::Toolkit::Color::Space::Basis;
  50         60  
  50         1298  
8 50     50   172 use Graphics::Toolkit::Color::Space::Util qw/round_decimals is_nr/;
  50         67  
  50         156302  
9              
10             #### constructor #######################################################
11             sub new {
12 583     583 0 4518 my $pkg = shift;
13 583         1497 my ($basis, $type, $range, $precision, $constraint) = @_;
14 583 100       1201 return unless ref $basis eq 'Graphics::Toolkit::Color::Space::Basis';
15              
16             # expand axis type definition
17 582 100 100     1403 if (not defined $type){ $type = [ (1) x $basis->axis_count ] } # set all axis as linear per default
  395 100       768  
18             elsif (ref $type eq 'ARRAY' and @$type == $basis->axis_count ) {
19 185         351 for my $i ($basis->axis_iterator) {
20 553         580 my $atype = $type->[$i]; # type def of this axis
21 553 50       683 return unless defined $atype;
22 553 100 100     1734 if ($atype eq 'angular' or $atype eq 'circular' or $atype eq '0') { $type->[$i] = 0 }
  187 100 100     345  
    100 100        
      100        
23 360         512 elsif ($atype eq 'linear' or $atype eq '1') { $type->[$i] = 1 }
24 4         6 elsif ($atype eq 'no' or $atype eq '2') { $type->[$i] = 2 }
25 2         13 else { return 'invalid axis type at element '.$i.'. It has to be "angular", "linear" or "no".' }
26             }
27 2         4 } else { return 'invalid axis type definition in color space '.$basis->space_name }
28              
29 578         1243 $range = expand_range_definition(undef, $basis, $range );
30 578 100       942 return $range unless ref $range;
31 570         983 $precision = expand_precision_definition( $basis, $precision );
32 570 100       845 return $precision unless ref $precision;
33              
34             # check constraint def
35 568 100       772 if (defined $constraint){
36 74 100 100     257 return 'color space constraint definition has to be a none empty HASH ref' if ref $constraint ne 'HASH' or not %$constraint;
37 72         152 for my $constraint_name (keys %$constraint){
38 73         106 my $properties = $constraint->{$constraint_name};
39 73 100 66     248 return 'a color space constraint has to be a HASH ref with three keys' unless ref $properties eq 'HASH' and keys(%$properties) == 3;
40 71         201 $properties = {%$properties};
41 71         164 my $error_msg = 'constraint "$constraint_name" in '.$basis->space_name.' color space';
42 71         121 for (qw/checker error remedy/){
43             return $error_msg." needs the string-propertiy '$_'"
44 207 100 33     752 unless exists $properties->{$_} and $properties->{$_} and not ref $properties->{$_};
      66        
45             }
46 68         105 $properties->{'checker_code'} = $properties->{'checker'};
47 68         5724 $properties->{'checker'} = eval 'sub {'.$properties->{'checker_code'}.'}';
48 68 50       202 return 'checker code of '.$error_msg.":'$properties->{checker_code}' does not eval - $@" if $@;
49 68         119 $properties->{'remedy_code'} = $properties->{'remedy'};
50 68         5275 $properties->{'remedy'} = eval 'sub {'.$properties->{'remedy_code'}.'}';
51 68 50       176 return 'remedy code of '.$error_msg.":'$properties->{remedy_code}' does not eval - $@" if $@;
52 68         152 $constraint->{ $constraint_name } = $properties;
53             }
54 494         513 } else { $constraint = '' }
55              
56 561         2368 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 2300     2300 0 3156 my ($self, $basis, $range) = @_;
62 2300 50       3874 $basis = $basis->{'basis'} if ref $basis eq __PACKAGE__;
63 2300         2459 my $error_msg = 'Bad value range definition!';
64 2300 100 100     5965 $range = 1 if not defined $range or $range eq 'normal';
65 2300 100       3701 $range = 100 if $range eq 'percent';
66 2300 50 66     6730 return $error_msg." It has to be 'normal', 'percent', a number or ARRAY of numbers (by axis position) or HASH (by axis name).'.
      100        
      66        
67             ' Instead of a number you can also insert ARRAY with two number!"
68             unless (not ref $range and is_nr( $range )) or ref $range eq 'ARRAY' or ref $range eq 'HASH';
69 2300 100       3728 if (ref $range eq 'HASH') {
70 6         10 my $range_array = [];
71 6         11 for my $axis_name (keys %$range){
72 13 50       21 next unless $basis->is_axis_name( $axis_name );
73 13         176 $range_array->[ $basis->pos_from_axis_name($axis_name) ] = $range->{$axis_name};
74             }
75 6         12 for my $axis_index ($basis->axis_iterator){
76 18 100 66     37 next if exists $range_array->[$axis_index] and defined $range_array->[$axis_index];
77 5 100 66     13 next unless ref $self and ref $self->{'range'};
78 2         3 $range_array->[$axis_index] = $self->{'range'}[$axis_index];
79             }
80 6         12 $range = $range_array;
81             }
82 2300 100       3772 $range = [$range] unless ref $range;
83 2300 100       4032 $range = [(@$range) x $basis->axis_count] if @$range == 1;
84 2300 100       3703 return "Range definition needs inside an ARRAY or HASH a number or pair of them in an ARRAY for each axis!"
85             if @$range != $basis->axis_count;
86 2296         3385 for my $axis_index ($basis->axis_iterator) {
87 6935         7146 my $axis_range = $range->[$axis_index];
88 6935 100       9165 if (not ref $axis_range){
    50          
89 3449 100       4929 if ($axis_range eq 'normal') {$range->[$axis_index] = [0, 1]}
  3 50       9  
90 0         0 elsif ($axis_range eq 'percent') {$range->[$axis_index] = [0, 100]}
91 3446         5604 else {$range->[$axis_index] = [0, $axis_range+0]}
92             } elsif (ref $axis_range eq 'ARRAY') {
93 3486 100       4654 return $error_msg.' Array at axis number '.$axis_index.' has to have two elements' unless @$axis_range == 2;
94 3484 50       4505 return $error_msg.' None numeric value at lower bound for axis number '.$axis_index unless is_nr( $axis_range->[0] );
95 3484 100       4626 return $error_msg.' None numeric value at upper bound for axis number '.$axis_index unless is_nr( $axis_range->[1] );
96 3483 100       6037 return $error_msg.' Lower bound (first value) is >= than upper bound at axis number '.$axis_index if $axis_range->[0] >= $axis_range->[1];
97 0         0 } else { return "Range definitin for axis $axis_index was not an two element ARRAY!" }
98             }
99 2292         15843 return $range;
100             }
101             sub try_check_range_definition { # check if range def is valid and eval (expand) it
102 4561     4561 0 5633 my ($self, $range) = @_;
103 4561 100       8060 return $self->{'range'} unless defined $range;
104 1722         3182 return $self->expand_range_definition( $self->{'basis'}, $range );
105             }
106              
107             sub expand_precision_definition { # check if precision def is valid and eval (exapand) it
108 933     933 0 1324 my ($basis, $precision) = @_;
109 933 50       1684 $basis = $basis->{'basis'} if ref $basis eq __PACKAGE__;
110 933 100       1346 $precision = -1 unless defined $precision;
111 933 100       1891 $precision = [($precision) x $basis->axis_count] unless ref $precision;
112 933 50       1599 return 'need an ARRAY as definition of axis value precision' unless ref $precision eq 'ARRAY';
113 933 100       1384 return 'definition of axis value precision has to have same lengths as basis' unless @$precision == $basis->axis_count;
114 931         1316 return $precision;
115             }
116             sub try_check_precision_definition { # check if range def is valid and eval (expand) it
117 1533     1533 0 1925 my ($self, $precision) = @_;
118 1533 100       2901 return $self->{'precision'} unless defined $precision;
119 363         714 return expand_precision_definition( $self->{'basis'}, $precision );
120             }
121              
122             #### getter of space object ############################################
123 12391     12391 0 20858 sub basis { $_[0]{'basis'}}
124             # per axis
125             sub is_axis_numeric {
126 13151     13151 0 13760 my ($self, $axis_nr) = @_;
127 13151 100 66     27070 return 0 if not defined $axis_nr or not exists $self->{'type'}[$axis_nr];
128 13147 100       37009 $self->{'type'}[$axis_nr] < 2 ? 1 : 0;
129              
130             }
131             sub is_axis_euclidean {
132 2986     2986 0 3168 my ($self, $axis_nr) = @_;
133 2986 100 66     6056 return 0 if not defined $axis_nr or not exists $self->{'type'}[$axis_nr];
134 2985 100       5312 $self->{'type'}[$axis_nr] == 1 ? 1 : 0;
135              
136             }
137             sub is_axis_angular {
138 3052     3052 0 3316 my ($self, $axis_nr) = @_;
139 3052 100 66     6557 return 0 if not defined $axis_nr or not exists $self->{'type'}[$axis_nr];
140 3051 100       5762 $self->{'type'}[$axis_nr] == 0 ? 1 : 0;
141             }
142             sub axis_value_max { # --> +value
143 18     18 0 36 my ($self, $axis_nr, $range) = @_;
144 18         41 $range = $self->try_check_range_definition( $range );
145 18 50       36 return undef unless ref $range;
146 18 100       36 return undef unless $self->is_axis_numeric($axis_nr);
147 17         136 return $range->[$axis_nr][1];
148             }
149             sub axis_value_min { # --> +value
150 4     4 0 8 my ($self, $axis_nr, $range) = @_;
151 4         8 $range = $self->try_check_range_definition( $range );
152 4 50       7 return undef unless ref $range;
153 4 100       8 return undef unless $self->is_axis_numeric($axis_nr);
154 3         11 return $range->[$axis_nr][0];
155             }
156             sub axis_value_precision { # --> +precision?
157 16     16 0 1822 my ($self, $axis_nr, $precision) = @_;
158 16 50 33     64 return undef if not defined $axis_nr or not exists $self->{'type'}[$axis_nr];
159 16 100       26 return undef unless $self->is_axis_numeric($axis_nr);
160 15   33     40 $precision //= $self->{'precision'};
161 15 50 33     41 return undef unless ref $precision eq 'ARRAY' and exists $precision->[$axis_nr];
162 15         46 $precision->[$axis_nr];
163             }
164              
165             # all axis
166             sub is_euclidean { # all axis linear ?
167 36     36 0 1192 my ($self) = @_;
168 36 100       129 map { return 0 if $self->{'type'}[$_] != 1 } $self->basis->axis_iterator;
  92         326  
169 24         91 return 1;
170             }
171             sub is_cylindrical { # one axis angular, rest linear ?
172 36     36 0 149 my ($self) = @_;
173 36         62 my $angular_axis = 0;
174 36 100       102 map { $angular_axis++ if $self->{'type'}[$_] == 0;
  109         296  
175 109 100       283 return 0 if $self->{'type'}[$_] > 1; } $self->basis->axis_iterator;
176 35 100       197 return ($angular_axis == 1) ? 1 : 0;
177             }
178             sub is_int_valued { # all ranges int valued ?
179 3     3 0 11 my ($self) = @_;
180 3 100       7 map { return 0 if $self->{'precision'}[$_] != 0 } $self->basis->axis_iterator;
  4         40  
181 0         0 return 1;
182             }
183 1388 100   1388 0 1767 sub has_constraints { my ($self) = @_; return (ref $self->{'constraint'}) ? 1 : 0 } # --> ?
  1388         2909  
184              
185              
186             #### value checker #####################################################
187             sub check_value_shape { # @tuple -- $range, $precision --> $@vals | ~!
188 373     373 0 673 my ($self, $tuple, $range, $precision) = @_;
189 373 100       719 return 'color value tuple in '.$self->basis->space_name.' space needs to be ARRAY ref with '.$self->basis->axis_count.' elements'
190             unless $self->basis->is_value_tuple( $tuple );
191 303         632 $range = $self->try_check_range_definition( $range );
192 303 50       1106 return $range unless ref $range;
193 303         603 $precision = $self->try_check_precision_definition( $precision );
194 303 50       524 return $precision unless ref $precision;
195 303         465 my @names = $self->basis->long_axis_names;
196 303         490 for my $axis_index ($self->basis->axis_iterator){
197 696 50       944 next unless $self->is_axis_numeric( $axis_index );
198 696 100       2034 return $names[$axis_index]." value is below minimum of ".$range->[$axis_index][0]
199             if $tuple->[$axis_index] < $range->[$axis_index][0];
200 596 100       1632 return $names[$axis_index]." value is above maximum of ".$range->[$axis_index][1]
201             if $tuple->[$axis_index] > $range->[$axis_index][1];
202 492 100 100     1251 return $names[$axis_index]." value is not properly rounded "
203             if $precision->[$axis_index] >= 0
204             and round_decimals($tuple->[$axis_index], $precision->[$axis_index]) != $tuple->[$axis_index];
205             }
206 80 100       278 if ($self->has_constraints){
207 11         96 my $tuple = $self->normalize($tuple, $range);
208 11         13 for my $constraint (values %{$self->{'constraint'}}){
  11         39  
209 11 100       234 return $constraint->{'error'} unless $constraint->{'checker'}->( $tuple );
210             }
211             }
212 78         429 return $tuple;
213             }
214              
215             sub is_equal { # @tuple_a, @tuple_b -- $precision --> ?
216 8     8 0 20 my ($self, $tuple_a, $tuple_b, $precision) = @_;
217 8 100 100     18 return 0 unless $self->basis->is_value_tuple( $tuple_a ) and $self->basis->is_value_tuple( $tuple_b );
218 3         9 $precision = $self->try_check_precision_definition( $precision );
219 3         7 for my $axis_nr ($self->basis->axis_iterator) {
220 9 50       20 return 0 if round_decimals($tuple_a->[$axis_nr], $precision->[$axis_nr])
221             != round_decimals($tuple_b->[$axis_nr], $precision->[$axis_nr]);
222             }
223 3         15 return 1;
224             }
225              
226             sub is_in_constraints { # @tuple --> ? # normalized values only, so it works on any ranges
227 2     2 0 6 my ($self, $tuple) = @_;
228 2 50       7 return 0 unless $self->basis->is_number_tuple( $tuple );
229 2 50       7 return 1 unless $self->has_constraints;
230 2         3 for my $constraint (values %{$self->{'constraint'}}){
  2         9  
231 2 100       113 return 0 unless $constraint->{'checker'}->( $tuple );
232             }
233 1         7 return 1;
234             }
235              
236             sub is_in_bounds { # @tuple --> ?
237 2     2 0 4 my ($self, $tuple, $range) = @_;
238 2 50       5 return 0 unless $self->is_in_linear_bounds( $tuple, $range );
239 2         3 $range = $self->try_check_range_definition( $range );
240 2         3 for my $axis_nr ($self->basis->axis_iterator) {
241 4 100       6 next if $self->{'type'}[$axis_nr]; # skip none linear axis
242 1 50 33     7 return 0 if $tuple->[$axis_nr] < $range->[$axis_nr][0]
243             or $tuple->[$axis_nr] > $range->[$axis_nr][1];
244             }
245 1         4 return 1;
246             }
247              
248             sub is_in_linear_bounds { # @tuple --> ?
249 380     380 0 493 my ($self, $tuple, $range) = @_;
250 380 100       533 return 0 unless $self->basis->is_number_tuple( $tuple );
251 377         591 $range = $self->try_check_range_definition( $range );
252 377         497 for my $axis_nr ($self->basis->axis_iterator) {
253 1062 100       1368 next if $self->{'type'}[$axis_nr] != 1; # skip none linear axis
254 978 100 100     2420 return 0 if $tuple->[$axis_nr] < $range->[$axis_nr][0]
255             or $tuple->[$axis_nr] > $range->[$axis_nr][1];
256             }
257 298 50       441 if ($self->has_constraints){
258 0         0 return $self->is_in_constraints( $self->normalize( $tuple, $range) );
259             }
260 298         572 return 1;
261             }
262              
263             #### value ops #########################################################
264             sub clamp { # change values if outside of range to nearest boundary, angles get rotated into range
265 995     995 0 2735 my ($self, $tuple, $range) = @_;
266 995         1377 $range = $self->try_check_range_definition( $range );
267 995 50       1627 return $range unless ref $range;
268 995 50       1582 $tuple = [] unless ref $tuple eq 'ARRAY';
269 995         1586 pop @$tuple while @$tuple > $self->basis->axis_count;
270 995         1494 $tuple = [@$tuple];
271              
272 995         1315 for my $axis_nr ($self->basis->axis_iterator){
273 3003 50       3544 next unless $self->is_axis_numeric( $axis_nr );
274 3003 100       3707 if (not defined $tuple->[$axis_nr]){
275 80         95 my $default_value = 0;
276 80 100 66     275 $default_value = $range->[$axis_nr][0] if $default_value < $range->[$axis_nr][0]
277             or $default_value > $range->[$axis_nr][1];
278 80         141 $tuple->[$axis_nr] = $default_value;
279             } else {
280 2923 100       3714 next unless $self->is_axis_euclidean( $axis_nr );
281 2698 100       4299 $tuple->[$axis_nr] = $range->[$axis_nr][0] if $tuple->[$axis_nr] < $range->[$axis_nr][0];
282 2698 100       4335 $tuple->[$axis_nr] = $range->[$axis_nr][1] if $tuple->[$axis_nr] > $range->[$axis_nr][1];
283             }
284             }
285 995         1913 $tuple = $self->rotate($tuple, $range);
286 995 100       1861 if ($self->has_constraints){
287 29         149 $tuple = $self->normalize( $tuple, $range);
288 29         54 for my $constraint (values %{$self->{'constraint'}}){
  29         77  
289 29 100       636 $tuple = $constraint->{'remedy'}->($tuple) unless $constraint->{'checker'}->( $tuple );
290             }
291 29         79 $tuple = $self->denormalize( $tuple, $range);
292             }
293 995         2344 return $tuple;
294             }
295             sub rotate { # rotate values of circular dimensions into range
296 1010     1010 0 8373 my ($self, $tuple, $range) = @_;
297 1010 50       1438 return unless $self->basis->is_number_tuple( $tuple );
298 1010         1667 $range = $self->try_check_range_definition( $range );
299 1010 50       1564 return $range unless ref $range;
300 1010         2094 $tuple = [@$tuple];
301 1010         1538 for my $axis_nr ($self->basis->axis_iterator){
302 3048 100       3717 next unless $self->is_axis_angular( $axis_nr );
303 244 50       384 if (not defined $tuple->[$axis_nr]){
304 0         0 my $default_value = 0;
305 0 0 0     0 $default_value = $range->[$axis_nr][0] if $default_value < $range->[$axis_nr][0]
306             or $default_value > $range->[$axis_nr][1];
307 0         0 $tuple->[$axis_nr] = $default_value;
308             } else {
309 244         363 my $delta = $range->[$axis_nr][1] - $range->[$axis_nr][0];
310 244         509 $tuple->[$axis_nr] += $delta while $tuple->[$axis_nr] < $range->[$axis_nr][0];
311 244         452 $tuple->[$axis_nr] -= $delta while $tuple->[$axis_nr] > $range->[$axis_nr][1];
312 244 100       456 $tuple->[$axis_nr] = $range->[$axis_nr][0] if $tuple->[$axis_nr] == $range->[$axis_nr][1];
313             }
314             }
315 1010         1873 return $tuple;
316             }
317              
318             sub round { # $tuple -- $precision --> $tuple
319 1227     1227 0 3208 my ($self, $tuple, $precision) = @_;
320 1227 50       1820 return unless $self->basis->is_value_tuple( $tuple );
321 1227         2153 $precision = $self->try_check_precision_definition( $precision );
322 1227 50       1885 return "round got bad precision definition" unless ref $precision;
323 1227 100 66     1746 [ map { ($self->is_axis_numeric( $_ ) and $precision->[$_] >= 0) ? round_decimals ($tuple->[$_], $precision->[$_]) : $tuple->[$_] } $self->basis->axis_iterator ];
  3686         4672  
324             }
325              
326             # normalisation
327             sub normalize {
328 762     762 0 2056 my ($self, $tuple, $range) = @_;
329 762 50       1322 return unless $self->basis->is_value_tuple( $tuple );
330 762         1324 $range = $self->try_check_range_definition( $range );
331 762 50       1202 return $range unless ref $range;
332 762 50       1092 [ map { ($self->is_axis_numeric( $_ )) ? (($tuple->[$_] - $range->[$_][0]) / ($range->[$_][1]-$range->[$_][0]))
  2294         2967  
333             : $tuple->[$_] } $self->basis->axis_iterator ];
334             }
335              
336             sub denormalize {
337 1053     1053 0 2997 my ($self, $tuple, $range) = @_;
338 1053 50       1613 return unless $self->basis->is_value_tuple( $tuple );
339 1053         1797 $range = $self->try_check_range_definition( $range );
340 1053 50       1525 return $range unless ref $range;
341 1053 50       1450 return [ map { ($self->is_axis_numeric( $_ )) ? ($tuple->[$_] * ($range->[$_][1]-$range->[$_][0]) + $range->[$_][0])
  3166         3799  
342             : $tuple->[$_] } $self->basis->axis_iterator ];
343             }
344              
345             sub denormalize_delta {
346 30     30 0 518 my ($self, $delta_values, $range) = @_;
347 30 50       43 return unless $self->basis->is_value_tuple( $delta_values );
348 30         59 $range = $self->try_check_range_definition( $range );
349 30 50       54 return $range unless ref $range;
350 30 50       43 [ map { ($self->is_axis_numeric( $_ ))
  91         106  
351             ? ($delta_values->[$_] * ($range->[$_][1]-$range->[$_][0]))
352             : $delta_values->[$_] } $self->basis->axis_iterator ];
353             }
354              
355             sub delta { # @normal_tuple_a, @normal_tuple_b --> @delta_tuple
356 62     62 0 5502 my ($self, $tuple1, $tuple2) = @_;
357 62 100 100     119 return unless $self->basis->is_value_tuple( $tuple1 ) and $self->basis->is_value_tuple( $tuple2 );
358             # ignore none numeric dimensions
359 56 50       142 my @delta = map { $self->is_axis_numeric($_) ? ($tuple2->[$_] - $tuple1->[$_]) : 0 } $self->basis->axis_iterator;
  169         243  
360 56 100       100 [ map { $self->{'type'}[$_] ? $delta[$_] : # adapt to circular dimensions
  169 100       419  
    100          
361             $delta[$_] < -0.5 ? ($delta[$_]+1) :
362             $delta[$_] > 0.5 ? ($delta[$_]-1) : $delta[$_] } $self->basis->axis_iterator ];
363             }
364              
365             1;