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   361223 use v5.12;
  50         143  
6 50     50   180 use warnings;
  50         77  
  50         2051  
7 50     50   734 use Graphics::Toolkit::Color::Space::Basis;
  50         72  
  50         1375  
8 50     50   163 use Graphics::Toolkit::Color::Space::Util qw/round_decimals is_nr/;
  50         70  
  50         152487  
9              
10             #### constructor #######################################################
11             sub new {
12 583     583 0 4398 my $pkg = shift;
13 583         1652 my ($basis, $type, $range, $precision, $constraint) = @_;
14 583 100       1352 return unless ref $basis eq 'Graphics::Toolkit::Color::Space::Basis';
15              
16             # expand axis type definition
17 582 100 100     1575 if (not defined $type){ $type = [ (1) x $basis->axis_count ] } # set all axis as linear per default
  395 100       930  
18             elsif (ref $type eq 'ARRAY' and @$type == $basis->axis_count ) {
19 185         403 for my $i ($basis->axis_iterator) {
20 553         649 my $atype = $type->[$i]; # type def of this axis
21 553 50       793 return unless defined $atype;
22 553 100 100     1929 if ($atype eq 'angular' or $atype eq 'circular' or $atype eq '0') { $type->[$i] = 0 }
  187 100 100     349  
    100 100        
      100        
23 360         543 elsif ($atype eq 'linear' or $atype eq '1') { $type->[$i] = 1 }
24 4         11 elsif ($atype eq 'no' or $atype eq '2') { $type->[$i] = 2 }
25 2         20 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 578         1302 $range = expand_range_definition(undef, $basis, $range );
30 578 100       1048 return $range unless ref $range;
31 570         974 $precision = expand_precision_definition( $basis, $precision );
32 570 100       957 return $precision unless ref $precision;
33              
34             # check constraint def
35 568 100       767 if (defined $constraint){
36 74 100 100     276 return 'color space constraint definition has to be a none empty HASH ref' if ref $constraint ne 'HASH' or not %$constraint;
37 72         170 for my $constraint_name (keys %$constraint){
38 73         132 my $properties = $constraint->{$constraint_name};
39 73 100 66     297 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         221 $properties = {%$properties};
41 71         166 my $error_msg = 'constraint "$constraint_name" in '.$basis->space_name.' color space';
42 71         165 for (qw/checker error remedy/){
43             return $error_msg." needs the string-propertiy '$_'"
44 207 100 33     857 unless exists $properties->{$_} and $properties->{$_} and not ref $properties->{$_};
      66        
45             }
46 68         134 $properties->{'checker_code'} = $properties->{'checker'};
47 68         6147 $properties->{'checker'} = eval 'sub {'.$properties->{'checker_code'}.'}';
48 68 50       210 return 'checker code of '.$error_msg.":'$properties->{checker_code}' does not eval - $@" if $@;
49 68         173 $properties->{'remedy_code'} = $properties->{'remedy'};
50 68         5673 $properties->{'remedy'} = eval 'sub {'.$properties->{'remedy_code'}.'}';
51 68 50       201 return 'remedy code of '.$error_msg.":'$properties->{remedy_code}' does not eval - $@" if $@;
52 68         163 $constraint->{ $constraint_name } = $properties;
53             }
54 494         568 } else { $constraint = '' }
55              
56 561         2594 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 3266 my ($self, $basis, $range) = @_;
62 2300 50       3887 $basis = $basis->{'basis'} if ref $basis eq __PACKAGE__;
63 2300         2490 my $error_msg = 'Bad value range definition!';
64 2300 100 100     6323 $range = 1 if not defined $range or $range eq 'normal';
65 2300 100       3462 $range = 100 if $range eq 'percent';
66 2300 50 66     6801 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       3595 if (ref $range eq 'HASH') {
70 6         8 my $range_array = [];
71 6         15 for my $axis_name (keys %$range){
72 13 50       55 next unless $basis->is_axis_name( $axis_name );
73 13         145 $range_array->[ $basis->pos_from_axis_name($axis_name) ] = $range->{$axis_name};
74             }
75 6         13 for my $axis_index ($basis->axis_iterator){
76 18 100 66     43 next if exists $range_array->[$axis_index] and defined $range_array->[$axis_index];
77 5 100 66     16 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       3586 $range = [$range] unless ref $range;
83 2300 100       4267 $range = [(@$range) x $basis->axis_count] if @$range == 1;
84 2300 100       3677 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         3248 for my $axis_index ($basis->axis_iterator) {
87 6935         6927 my $axis_range = $range->[$axis_index];
88 6935 100       8814 if (not ref $axis_range){
    50          
89 3449 100       5152 if ($axis_range eq 'normal') {$range->[$axis_index] = [0, 1]}
  3 50       10  
90 0         0 elsif ($axis_range eq 'percent') {$range->[$axis_index] = [0, 100]}
91 3446         5505 else {$range->[$axis_index] = [0, $axis_range+0]}
92             } elsif (ref $axis_range eq 'ARRAY') {
93 3486 100       4262 return $error_msg.' Array at axis number '.$axis_index.' has to have two elements' unless @$axis_range == 2;
94 3484 50       4323 return $error_msg.' None numeric value at lower bound for axis number '.$axis_index unless is_nr( $axis_range->[0] );
95 3484 100       4285 return $error_msg.' None numeric value at upper bound for axis number '.$axis_index unless is_nr( $axis_range->[1] );
96 3483 100       5645 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         3922 return $range;
100             }
101             sub try_check_range_definition { # check if range def is valid and eval (expand) it
102 4561     4561 0 5546 my ($self, $range) = @_;
103 4561 100       7600 return $self->{'range'} unless defined $range;
104 1722         2804 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 1503 my ($basis, $precision) = @_;
109 933 50       1728 $basis = $basis->{'basis'} if ref $basis eq __PACKAGE__;
110 933 100       1335 $precision = -1 unless defined $precision;
111 933 100       2079 $precision = [($precision) x $basis->axis_count] unless ref $precision;
112 933 50       1662 return 'need an ARRAY as definition of axis value precision' unless ref $precision eq 'ARRAY';
113 933 100       1591 return 'definition of axis value precision has to have same lengths as basis' unless @$precision == $basis->axis_count;
114 931         1451 return $precision;
115             }
116             sub try_check_precision_definition { # check if range def is valid and eval (expand) it
117 1533     1533 0 2143 my ($self, $precision) = @_;
118 1533 100       2976 return $self->{'precision'} unless defined $precision;
119 363         750 return expand_precision_definition( $self->{'basis'}, $precision );
120             }
121              
122             #### getter of space object ############################################
123 12391     12391 0 20531 sub basis { $_[0]{'basis'}}
124             # per axis
125             sub is_axis_numeric {
126 13151     13151 0 13532 my ($self, $axis_nr) = @_;
127 13151 100 66     26818 return 0 if not defined $axis_nr or not exists $self->{'type'}[$axis_nr];
128 13147 100       34637 $self->{'type'}[$axis_nr] < 2 ? 1 : 0;
129              
130             }
131             sub is_axis_euclidean {
132 2986     2986 0 3061 my ($self, $axis_nr) = @_;
133 2986 100 66     5898 return 0 if not defined $axis_nr or not exists $self->{'type'}[$axis_nr];
134 2985 100       4999 $self->{'type'}[$axis_nr] == 1 ? 1 : 0;
135              
136             }
137             sub is_axis_angular {
138 3052     3052 0 3126 my ($self, $axis_nr) = @_;
139 3052 100 66     6287 return 0 if not defined $axis_nr or not exists $self->{'type'}[$axis_nr];
140 3051 100       5557 $self->{'type'}[$axis_nr] == 0 ? 1 : 0;
141             }
142             sub axis_value_max { # --> +value
143 18     18 0 40 my ($self, $axis_nr, $range) = @_;
144 18         41 $range = $self->try_check_range_definition( $range );
145 18 50       37 return undef unless ref $range;
146 18 100       41 return undef unless $self->is_axis_numeric($axis_nr);
147 17         78 return $range->[$axis_nr][1];
148             }
149             sub axis_value_min { # --> +value
150 4     4 0 13 my ($self, $axis_nr, $range) = @_;
151 4         13 $range = $self->try_check_range_definition( $range );
152 4 50       13 return undef unless ref $range;
153 4 100       12 return undef unless $self->is_axis_numeric($axis_nr);
154 3         20 return $range->[$axis_nr][0];
155             }
156             sub axis_value_precision { # --> +precision?
157 16     16 0 2369 my ($self, $axis_nr, $precision) = @_;
158 16 50 33     174 return undef if not defined $axis_nr or not exists $self->{'type'}[$axis_nr];
159 16 100       50 return undef unless $self->is_axis_numeric($axis_nr);
160 15   33     74 $precision //= $self->{'precision'};
161 15 50 33     64 return undef unless ref $precision eq 'ARRAY' and exists $precision->[$axis_nr];
162 15         83 $precision->[$axis_nr];
163             }
164              
165             # all axis
166             sub is_euclidean { # all axis linear ?
167 36     36 0 1695 my ($self) = @_;
168 36 100       136 map { return 0 if $self->{'type'}[$_] != 1 } $self->basis->axis_iterator;
  92         417  
169 24         95 return 1;
170             }
171             sub is_cylindrical { # one axis angular, rest linear ?
172 36     36 0 94 my ($self) = @_;
173 36         94 my $angular_axis = 0;
174 36 100       111 map { $angular_axis++ if $self->{'type'}[$_] == 0;
  109         275  
175 109 100       301 return 0 if $self->{'type'}[$_] > 1; } $self->basis->axis_iterator;
176 35 100       211 return ($angular_axis == 1) ? 1 : 0;
177             }
178             sub is_int_valued { # all ranges int valued ?
179 3     3 0 13 my ($self) = @_;
180 3 100       11 map { return 0 if $self->{'precision'}[$_] != 0 } $self->basis->axis_iterator;
  4         24  
181 0         0 return 1;
182             }
183 1388 100   1388 0 1697 sub has_constraints { my ($self) = @_; return (ref $self->{'constraint'}) ? 1 : 0 } # --> ?
  1388         2801  
184              
185              
186             #### value checker #####################################################
187             sub check_value_shape { # @tuple -- $range, $precision --> $@vals | ~!
188 373     373 0 689 my ($self, $tuple, $range, $precision) = @_;
189 373 100       945 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         693 $range = $self->try_check_range_definition( $range );
192 303 50       671 return $range unless ref $range;
193 303         685 $precision = $self->try_check_precision_definition( $precision );
194 303 50       586 return $precision unless ref $precision;
195 303         580 my @names = $self->basis->long_axis_names;
196 303         597 for my $axis_index ($self->basis->axis_iterator){
197 696 50       1191 next unless $self->is_axis_numeric( $axis_index );
198 696 100       2154 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       1705 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     1389 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       244 if ($self->has_constraints){
207 11         30 my $tuple = $self->normalize($tuple, $range);
208 11         13 for my $constraint (values %{$self->{'constraint'}}){
  11         29  
209 11 100       251 return $constraint->{'error'} unless $constraint->{'checker'}->( $tuple );
210             }
211             }
212 78         452 return $tuple;
213             }
214              
215             sub is_equal { # @tuple_a, @tuple_b -- $precision --> ?
216 8     8 0 26 my ($self, $tuple_a, $tuple_b, $precision) = @_;
217 8 100 100     25 return 0 unless $self->basis->is_value_tuple( $tuple_a ) and $self->basis->is_value_tuple( $tuple_b );
218 3         10 $precision = $self->try_check_precision_definition( $precision );
219 3         9 for my $axis_nr ($self->basis->axis_iterator) {
220 9 50       26 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         46 return 1;
224             }
225              
226             sub is_in_constraints { # @tuple --> ? # normalized values only, so it works on any ranges
227 2     2 0 7 my ($self, $tuple) = @_;
228 2 50       6 return 0 unless $self->basis->is_number_tuple( $tuple );
229 2 50       8 return 1 unless $self->has_constraints;
230 2         4 for my $constraint (values %{$self->{'constraint'}}){
  2         8  
231 2 100       54 return 0 unless $constraint->{'checker'}->( $tuple );
232             }
233 1         6 return 1;
234             }
235              
236             sub is_in_bounds { # @tuple --> ?
237 2     2 0 7 my ($self, $tuple, $range) = @_;
238 2 50       8 return 0 unless $self->is_in_linear_bounds( $tuple, $range );
239 2         7 $range = $self->try_check_range_definition( $range );
240 2         6 for my $axis_nr ($self->basis->axis_iterator) {
241 4 100       12 next if $self->{'type'}[$axis_nr]; # skip none linear axis
242 1 50 33     11 return 0 if $tuple->[$axis_nr] < $range->[$axis_nr][0]
243             or $tuple->[$axis_nr] > $range->[$axis_nr][1];
244             }
245 1         7 return 1;
246             }
247              
248             sub is_in_linear_bounds { # @tuple --> ?
249 380     380 0 475 my ($self, $tuple, $range) = @_;
250 380 100       448 return 0 unless $self->basis->is_number_tuple( $tuple );
251 377         516 $range = $self->try_check_range_definition( $range );
252 377         415 for my $axis_nr ($self->basis->axis_iterator) {
253 1062 100       1314 next if $self->{'type'}[$axis_nr] != 1; # skip none linear axis
254 978 100 100     2175 return 0 if $tuple->[$axis_nr] < $range->[$axis_nr][0]
255             or $tuple->[$axis_nr] > $range->[$axis_nr][1];
256             }
257 298 50       386 if ($self->has_constraints){
258 0         0 return $self->is_in_constraints( $self->normalize( $tuple, $range) );
259             }
260 298         468 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 2857 my ($self, $tuple, $range) = @_;
266 995         1335 $range = $self->try_check_range_definition( $range );
267 995 50       1514 return $range unless ref $range;
268 995 50       1532 $tuple = [] unless ref $tuple eq 'ARRAY';
269 995         1543 pop @$tuple while @$tuple > $self->basis->axis_count;
270 995         1527 $tuple = [@$tuple];
271              
272 995         1305 for my $axis_nr ($self->basis->axis_iterator){
273 3003 50       3507 next unless $self->is_axis_numeric( $axis_nr );
274 3003 100       3494 if (not defined $tuple->[$axis_nr]){
275 80         92 my $default_value = 0;
276 80 100 66     292 $default_value = $range->[$axis_nr][0] if $default_value < $range->[$axis_nr][0]
277             or $default_value > $range->[$axis_nr][1];
278 80         142 $tuple->[$axis_nr] = $default_value;
279             } else {
280 2923 100       3396 next unless $self->is_axis_euclidean( $axis_nr );
281 2698 100       4149 $tuple->[$axis_nr] = $range->[$axis_nr][0] if $tuple->[$axis_nr] < $range->[$axis_nr][0];
282 2698 100       4131 $tuple->[$axis_nr] = $range->[$axis_nr][1] if $tuple->[$axis_nr] > $range->[$axis_nr][1];
283             }
284             }
285 995         1824 $tuple = $self->rotate($tuple, $range);
286 995 100       1918 if ($self->has_constraints){
287 29         71 $tuple = $self->normalize( $tuple, $range);
288 29         56 for my $constraint (values %{$self->{'constraint'}}){
  29         72  
289 29 100       643 $tuple = $constraint->{'remedy'}->($tuple) unless $constraint->{'checker'}->( $tuple );
290             }
291 29         65 $tuple = $self->denormalize( $tuple, $range);
292             }
293 995         2190 return $tuple;
294             }
295             sub rotate { # rotate values of circular dimensions into range
296 1010     1010 0 1789 my ($self, $tuple, $range) = @_;
297 1010 50       1309 return unless $self->basis->is_number_tuple( $tuple );
298 1010         1555 $range = $self->try_check_range_definition( $range );
299 1010 50       1518 return $range unless ref $range;
300 1010         1994 $tuple = [@$tuple];
301 1010         1408 for my $axis_nr ($self->basis->axis_iterator){
302 3048 100       3767 next unless $self->is_axis_angular( $axis_nr );
303 244 50       423 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         353 my $delta = $range->[$axis_nr][1] - $range->[$axis_nr][0];
310 244         532 $tuple->[$axis_nr] += $delta while $tuple->[$axis_nr] < $range->[$axis_nr][0];
311 244         657 $tuple->[$axis_nr] -= $delta while $tuple->[$axis_nr] > $range->[$axis_nr][1];
312 244 100       496 $tuple->[$axis_nr] = $range->[$axis_nr][0] if $tuple->[$axis_nr] == $range->[$axis_nr][1];
313             }
314             }
315 1010         1653 return $tuple;
316             }
317              
318             sub round { # $tuple -- $precision --> $tuple
319 1227     1227 0 3074 my ($self, $tuple, $precision) = @_;
320 1227 50       1859 return unless $self->basis->is_value_tuple( $tuple );
321 1227         1957 $precision = $self->try_check_precision_definition( $precision );
322 1227 50       1938 return "round got bad precision definition" unless ref $precision;
323 1227 100 66     1716 [ map { ($self->is_axis_numeric( $_ ) and $precision->[$_] >= 0) ? round_decimals ($tuple->[$_], $precision->[$_]) : $tuple->[$_] } $self->basis->axis_iterator ];
  3686         4781  
324             }
325              
326             # normalisation
327             sub normalize {
328 762     762 0 2056 my ($self, $tuple, $range) = @_;
329 762 50       1275 return unless $self->basis->is_value_tuple( $tuple );
330 762         1230 $range = $self->try_check_range_definition( $range );
331 762 50       1171 return $range unless ref $range;
332 762 50       1107 [ map { ($self->is_axis_numeric( $_ )) ? (($tuple->[$_] - $range->[$_][0]) / ($range->[$_][1]-$range->[$_][0]))
  2294         2906  
333             : $tuple->[$_] } $self->basis->axis_iterator ];
334             }
335              
336             sub denormalize {
337 1053     1053 0 2839 my ($self, $tuple, $range) = @_;
338 1053 50       1590 return unless $self->basis->is_value_tuple( $tuple );
339 1053         1632 $range = $self->try_check_range_definition( $range );
340 1053 50       1491 return $range unless ref $range;
341 1053 50       1502 return [ map { ($self->is_axis_numeric( $_ )) ? ($tuple->[$_] * ($range->[$_][1]-$range->[$_][0]) + $range->[$_][0])
  3166         3877  
342             : $tuple->[$_] } $self->basis->axis_iterator ];
343             }
344              
345             sub denormalize_delta {
346 30     30 0 491 my ($self, $delta_values, $range) = @_;
347 30 50       40 return unless $self->basis->is_value_tuple( $delta_values );
348 30         44 $range = $self->try_check_range_definition( $range );
349 30 50       44 return $range unless ref $range;
350 30 50       45 [ map { ($self->is_axis_numeric( $_ ))
  91         172  
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 4220 my ($self, $tuple1, $tuple2) = @_;
357 62 100 100     139 return unless $self->basis->is_value_tuple( $tuple1 ) and $self->basis->is_value_tuple( $tuple2 );
358             # ignore none numeric dimensions
359 56 50       125 my @delta = map { $self->is_axis_numeric($_) ? ($tuple2->[$_] - $tuple1->[$_]) : 0 } $self->basis->axis_iterator;
  169         244  
360 56 100       142 [ map { $self->{'type'}[$_] ? $delta[$_] : # adapt to circular dimensions
  169 100       408  
    100          
361             $delta[$_] < -0.5 ? ($delta[$_]+1) :
362             $delta[$_] > 0.5 ? ($delta[$_]-1) : $delta[$_] } $self->basis->axis_iterator ];
363             }
364              
365             1;