File Coverage

lib/Graphics/Toolkit/Color/Space/Shape.pm
Criterion Covered Total %
statement 230 234 98.2
branch 175 214 81.7
condition 61 81 75.3
subroutine 30 30 100.0
pod 0 26 0.0
total 496 585 84.7


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 46     46   322569 use v5.12;
  46         213  
6 46     46   336 use warnings;
  46         88  
  46         2771  
7 46     46   1571 use Graphics::Toolkit::Color::Space::Basis;
  46         163  
  46         1664  
8 46     46   337 use Graphics::Toolkit::Color::Space::Util qw/round_decimals is_nr/;
  46         144  
  46         195189  
9              
10             #### constructor #######################################################
11             sub new {
12 505     505 0 18628 my $pkg = shift;
13 505         1821 my ($basis, $type, $range, $precision, $constraint) = @_;
14 505 100       1547 return unless ref $basis eq 'Graphics::Toolkit::Color::Space::Basis';
15              
16             # expand axis type definition
17 504 100 100     1675 if (not defined $type){ $type = [ (1) x $basis->axis_count ] } # set all axis as linear per default
  373 100       1057  
18             elsif (ref $type eq 'ARRAY' and @$type == $basis->axis_count ) {
19 129         396 for my $i ($basis->axis_iterator) {
20 385         561 my $atype = $type->[$i]; # type def of this axis
21 385 50       710 return unless defined $atype;
22 385 100 100     1810 if ($atype eq 'angular' or $atype eq 'circular' or $atype eq '0') { $type->[$i] = 0 }
  131 100 100     367  
    100 100        
      100        
23 248         559 elsif ($atype eq 'linear' or $atype eq '1') { $type->[$i] = 1 }
24 4         28 elsif ($atype eq 'no' or $atype eq '2') { $type->[$i] = 2 }
25 2         19 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 500         1311 $range = expand_range_definition(undef, $basis, $range );
30 500 100       1165 return $range unless ref $range;
31 492         1043 $precision = expand_precision_definition( $basis, $precision );
32 492 100       1133 return $precision unless ref $precision;
33              
34             # check constraint def
35 490 100       917 if (defined $constraint){
36 55 100 100     314 return 'color space constraint definition has to be a none empty HASH ref' if ref $constraint ne 'HASH' or not %$constraint;
37 53         175 for my $constraint_name (keys %$constraint){
38 54         105 my $properties = $constraint->{$constraint_name};
39 54 100 66     256 return 'a color space constraint has to be a HASH ref with three keys' unless ref $properties eq 'HASH' and keys(%$properties) == 3;
40 52         230 $properties = {%$properties};
41 52         369 my $error_msg = 'constraint "$constraint_name" in '.$basis->space_name.' color space';
42 52         196 for (qw/checker error remedy/){
43             return $error_msg." needs the string-propertiy '$_'"
44 150 100 33     895 unless exists $properties->{$_} and $properties->{$_} and not ref $properties->{$_};
      66        
45             }
46 49         138 $properties->{'checker_code'} = $properties->{'checker'};
47 49         5588 $properties->{'checker'} = eval 'sub {'.$properties->{'checker_code'}.'}';
48 49 50       203 return 'checker code of '.$error_msg.":'$properties->{checker_code}' does not eval - $@" if $@;
49 49         227 $properties->{'remedy_code'} = $properties->{'remedy'};
50 49         5718 $properties->{'remedy'} = eval 'sub {'.$properties->{'remedy_code'}.'}';
51 49 50       203 return 'remedy code of '.$error_msg.":'$properties->{remedy_code}' does not eval - $@" if $@;
52 49         167 $constraint->{ $constraint_name } = $properties;
53             }
54 435         736 } else { $constraint = '' }
55              
56 483         3415 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 1194     1194 0 2293 my ($self, $basis, $range) = @_;
62 1194 50       2822 $basis = $basis->{'basis'} if ref $basis eq __PACKAGE__;
63 1194         1596 my $error_msg = 'Bad value range definition!';
64 1194 100 100     4288 $range = 1 if not defined $range or $range eq 'normal';
65 1194 100       2861 $range = 100 if $range eq 'percent';
66 1194 50 66     4265 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 1194 100       2776 if (ref $range eq 'HASH') {
70 6         10 my $range_array = [];
71 6         17 for my $axis_name (keys %$range){
72 13 50       22 next unless $basis->is_axis_name( $axis_name );
73 13         24 $range_array->[ $basis->pos_from_axis_name($axis_name) ] = $range->{$axis_name};
74             }
75 6         16 for my $axis_index ($basis->axis_iterator){
76 18 100 66     49 next if exists $range_array->[$axis_index] and defined $range_array->[$axis_index];
77 5 100 66     21 next unless ref $self and ref $self->{'range'};
78 2         6 $range_array->[$axis_index] = $self->{'range'}[$axis_index];
79             }
80 6         12 $range = $range_array;
81             }
82 1194 100       3066 $range = [$range] unless ref $range;
83 1194 100       3884 $range = [(@$range) x $basis->axis_count] if @$range == 1;
84 1194 100       2941 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 1190         2414 for my $axis_index ($basis->axis_iterator) {
87 3594         5296 my $axis_range = $range->[$axis_index];
88 3594 100       7080 if (not ref $axis_range){
    50          
89 3165 100       8503 if ($axis_range eq 'normal') {$range->[$axis_index] = [0, 1]}
  3 50       13  
90 0         0 elsif ($axis_range eq 'percent') {$range->[$axis_index] = [0, 100]}
91 3162         6941 else {$range->[$axis_index] = [0, $axis_range+0]}
92             } elsif (ref $axis_range eq 'ARRAY') {
93 429 100       975 return $error_msg.' Array at axis number '.$axis_index.' has to have two elements' unless @$axis_range == 2;
94 427 50       946 return $error_msg.' None numeric value at lower bound for axis number '.$axis_index unless is_nr( $axis_range->[0] );
95 427 100       918 return $error_msg.' None numeric value at upper bound for axis number '.$axis_index unless is_nr( $axis_range->[1] );
96 426 100       1180 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 1186         2873 return $range;
100             }
101             sub try_check_range_definition { # check if range def is valid and eval (expand) it
102 3252     3252 0 5360 my ($self, $range) = @_;
103 3252 100       7688 return $self->{'range'} unless defined $range;
104 694         2829 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 777     777 0 1548 my ($basis, $precision) = @_;
109 777 50       2060 $basis = $basis->{'basis'} if ref $basis eq __PACKAGE__;
110 777 100       1714 $precision = -1 unless defined $precision;
111 777 100       2214 $precision = [($precision) x $basis->axis_count] unless ref $precision;
112 777 50       1847 return 'need an ARRAY as definition of axis value precision' unless ref $precision eq 'ARRAY';
113 777 100       1796 return 'definition of axis value precision has to have same lengths as basis' unless @$precision == $basis->axis_count;
114 775         1581 return $precision;
115             }
116             sub try_check_precision_definition { # check if range def is valid and eval (expand) it
117 1407     1407 0 2465 my ($self, $precision) = @_;
118 1407 100       3407 return $self->{'precision'} unless defined $precision;
119 285         736 return expand_precision_definition( $self->{'basis'}, $precision );
120             }
121              
122             #### getter of space object ############################################
123 9527     9527 0 20968 sub basis { $_[0]{'basis'}}
124             # per axis
125             sub is_axis_numeric {
126 12010     12010 0 16304 my ($self, $axis_nr) = @_;
127 12010 100 66     33086 return 0 if not defined $axis_nr or not exists $self->{'type'}[$axis_nr];
128 12006 100       54531 $self->{'type'}[$axis_nr] == 2 ? 0 : 1;
129              
130             }
131             sub is_axis_euclidean {
132 65     65 0 111 my ($self, $axis_nr) = @_;
133 65 100 66     300 return 0 if not defined $axis_nr or not exists $self->{'type'}[$axis_nr];
134 64 100       187 $self->{'type'}[$axis_nr] == 1 ? 1 : 0;
135              
136             }
137             sub axis_value_max { # --> +value
138 9     9 0 34 my ($self, $axis_nr, $range) = @_;
139 9         40 $range = $self->try_check_range_definition( $range );
140 9 50       32 return undef unless ref $range;
141 9 100       34 return undef unless $self->is_axis_numeric($axis_nr);
142 8         40 return $range->[$axis_nr][1];
143             }
144             sub axis_value_min { # --> +value
145 4     4 0 13 my ($self, $axis_nr, $range) = @_;
146 4         13 $range = $self->try_check_range_definition( $range );
147 4 50       13 return undef unless ref $range;
148 4 100       13 return undef unless $self->is_axis_numeric($axis_nr);
149 3         19 return $range->[$axis_nr][0];
150             }
151             sub axis_value_precision { # --> +precision?
152 16     16 0 3065 my ($self, $axis_nr, $precision) = @_;
153 16 50 33     103 return undef if not defined $axis_nr or not exists $self->{'type'}[$axis_nr];
154 16 100       48 return undef unless $self->is_axis_numeric($axis_nr);
155 15   33     82 $precision //= $self->{'precision'};
156 15 50 33     72 return undef unless ref $precision eq 'ARRAY' and exists $precision->[$axis_nr];
157 15         80 $precision->[$axis_nr];
158             }
159              
160             # all axis
161             sub is_euclidean { # all axis linear ?
162 33     33 0 2317 my ($self) = @_;
163 33 100       175 map { return 0 if $self->{'type'}[$_] != 1 } $self->basis->axis_iterator;
  89         401  
164 24         143 return 1;
165             }
166             sub is_cylindrical { # one axis angular, rest linear ?
167 33     33 0 101 my ($self) = @_;
168 33         80 my $angular_axis = 0;
169 33 100       135 map { $angular_axis++ if $self->{'type'}[$_] == 0;
  100         356  
170 100 100       421 return 0 if $self->{'type'}[$_] > 1; } $self->basis->axis_iterator;
171 32 100       506 return ($angular_axis == 1) ? 1 : 0;
172             }
173             sub is_int_valued { # all ranges int valued ?
174 3     3 0 18 my ($self) = @_;
175 3 100       10 map { return 0 if $self->{'precision'}[$_] != 0 } $self->basis->axis_iterator;
  4         30  
176 0         0 return 1;
177             }
178 1348 100   1348 0 2166 sub has_constraints { my ($self) = @_; return (ref $self->{'constraint'}) ? 1 : 0 } # --> ?
  1348         3612  
179              
180              
181             #### value checker #####################################################
182             sub check_value_shape { # @tuple -- $range, $precision --> $@vals | ~!
183 343     343 0 876 my ($self, $tuple, $range, $precision) = @_;
184 343 100       954 return 'color value tuple in '.$self->basis->space_name.' space needs to be ARRAY ref with '.$self->basis->axis_count.' elements'
185             unless $self->basis->is_value_tuple( $tuple );
186 279         787 $range = $self->try_check_range_definition( $range );
187 279 50       822 return $range unless ref $range;
188 279         672 $precision = $self->try_check_precision_definition( $precision );
189 279 50       712 return $precision unless ref $precision;
190 279         558 my @names = $self->basis->long_axis_names;
191 279         781 for my $axis_index ($self->basis->axis_iterator){
192 642 50       1436 next unless $self->is_axis_numeric( $axis_index );
193 642 100       2755 return $names[$axis_index]." value is below minimum of ".$range->[$axis_index][0]
194             if $tuple->[$axis_index] < $range->[$axis_index][0];
195 551 100       2224 return $names[$axis_index]." value is above maximum of ".$range->[$axis_index][1]
196             if $tuple->[$axis_index] > $range->[$axis_index][1];
197 456 100 100     1684 return $names[$axis_index]." value is not properly rounded "
198             if $precision->[$axis_index] >= 0
199             and round_decimals($tuple->[$axis_index], $precision->[$axis_index]) != $tuple->[$axis_index];
200             }
201 74 100       290 if ($self->has_constraints){
202 9         47 my $tuple = $self->normalize($tuple, $range);
203 9         17 for my $constraint (values %{$self->{'constraint'}}){
  9         28  
204 9 100       262 return $constraint->{'error'} unless $constraint->{'checker'}->( $tuple );
205             }
206             }
207 72         624 return $tuple;
208             }
209              
210             sub is_equal { # @tuple_a, @tuple_b -- $precision --> ?
211 8     8 0 27 my ($self, $tuple_a, $tuple_b, $precision) = @_;
212 8 100 100     27 return 0 unless $self->basis->is_value_tuple( $tuple_a ) and $self->basis->is_value_tuple( $tuple_b );
213 3         10 $precision = $self->try_check_precision_definition( $precision );
214 3         10 for my $axis_nr ($self->basis->axis_iterator) {
215 9 50       23 return 0 if round_decimals($tuple_a->[$axis_nr], $precision->[$axis_nr])
216             != round_decimals($tuple_b->[$axis_nr], $precision->[$axis_nr]);
217             }
218 3         21 return 1;
219             }
220              
221             sub is_in_constraints { # @tuple --> ? # normalized values only, so it works on any ranges
222 2     2 0 7 my ($self, $tuple) = @_;
223 2 50       8 return 0 unless $self->basis->is_number_tuple( $tuple );
224 2 50       7 return 1 unless $self->has_constraints;
225 2         4 for my $constraint (values %{$self->{'constraint'}}){
  2         8  
226 2 100       67 return 0 unless $constraint->{'checker'}->( $tuple );
227             }
228 1         6 return 1;
229             }
230              
231             sub is_in_bounds { # @tuple --> ?
232 2     2 0 8 my ($self, $tuple, $range) = @_;
233 2 50       6 return 0 unless $self->is_in_linear_bounds( $tuple, $range );
234 2         6 $range = $self->try_check_range_definition( $range );
235 2         7 for my $axis_nr ($self->basis->axis_iterator) {
236 4 100       11 next if $self->{'type'}[$axis_nr]; # skip none linear axis
237 1 50 33     14 return 0 if $tuple->[$axis_nr] < $range->[$axis_nr][0]
238             or $tuple->[$axis_nr] > $range->[$axis_nr][1];
239             }
240 1         5 return 1;
241             }
242              
243             sub is_in_linear_bounds { # @tuple --> ?
244 366     366 0 500 my ($self, $tuple, $range) = @_;
245 366 100       472 return 0 unless $self->basis->is_number_tuple( $tuple );
246 363         610 $range = $self->try_check_range_definition( $range );
247 363         533 for my $axis_nr ($self->basis->axis_iterator) {
248 1036 100       1575 next if $self->{'type'}[$axis_nr] != 1; # skip none linear axis
249 953 100 100     2425 return 0 if $tuple->[$axis_nr] < $range->[$axis_nr][0]
250             or $tuple->[$axis_nr] > $range->[$axis_nr][1];
251             }
252 297 50       446 if ($self->has_constraints){
253 0         0 return $self->is_in_constraints( $self->normalize( $tuple, $range) );
254             }
255 297         593 return 1;
256             }
257              
258             #### value ops #########################################################
259             sub clamp { # change values if outside of range to nearest boundary, angles get rotated into range
260 963     963 0 14457 my ($self, $tuple, $range) = @_;
261 963         1743 $range = $self->try_check_range_definition( $range );
262 963 50       1900 return $range unless ref $range;
263 963 50       2042 $tuple = [] unless ref $tuple eq 'ARRAY';
264 963         1971 pop @$tuple while @$tuple > $self->basis->axis_count;
265 963         1811 for my $axis_nr ($self->basis->axis_iterator){
266 2904 50       4733 next unless $self->is_axis_numeric( $axis_nr ); # touch only numeric values
267 2904 100       4810 if (not defined $tuple->[$axis_nr]){
268 80         441 my $default_value = 0;
269 80 100 66     429 $default_value = $range->[$axis_nr][0] if $default_value < $range->[$axis_nr][0]
270             or $default_value > $range->[$axis_nr][1];
271 80         182 $tuple->[$axis_nr] = $default_value;
272 80         277 next;
273             }
274 2824 100       4148 if ($self->{'type'}[$axis_nr]){
275 2610 100       5103 $tuple->[$axis_nr] = $range->[$axis_nr][0] if $tuple->[$axis_nr] < $range->[$axis_nr][0];
276 2610 100       5524 $tuple->[$axis_nr] = $range->[$axis_nr][1] if $tuple->[$axis_nr] > $range->[$axis_nr][1];
277             } else {
278 214         417 my $delta = $range->[$axis_nr][1] - $range->[$axis_nr][0];
279 214         711 $tuple->[$axis_nr] += $delta while $tuple->[$axis_nr] < $range->[$axis_nr][0];
280 214         583 $tuple->[$axis_nr] -= $delta while $tuple->[$axis_nr] > $range->[$axis_nr][1];
281 214 100       580 $tuple->[$axis_nr] = $range->[$axis_nr][0] if $tuple->[$axis_nr] == $range->[$axis_nr][1];
282             }
283             }
284 963 100       2026 if ($self->has_constraints){
285 23         65 $tuple = $self->normalize( $tuple, $range);
286 23         45 for my $constraint (values %{$self->{'constraint'}}){
  23         74  
287 23 100       653 $tuple = $constraint->{'remedy'}->($tuple) unless $constraint->{'checker'}->( $tuple );
288             }
289 23         70 $tuple = $self->denormalize( $tuple, $range);
290             }
291 963         3032 return $tuple;
292             }
293              
294             sub round { # $tuple -- $precision --> $tuple
295 1125     1125 0 15402 my ($self, $tuple, $precision) = @_;
296 1125 50       2120 return unless $self->basis->is_value_tuple( $tuple );
297 1125         2542 $precision = $self->try_check_precision_definition( $precision );
298 1125 50       2214 return "round got bad precision definition" unless ref $precision;
299 1125 100 66     2078 [ map { ($self->is_axis_numeric( $_ ) and $precision->[$_] >= 0) ? round_decimals ($tuple->[$_], $precision->[$_]) : $tuple->[$_] } $self->basis->axis_iterator ];
  3380         5387  
300             }
301              
302             # normalisation
303             sub normalize {
304 641     641 0 9066 my ($self, $tuple, $range) = @_;
305 641 50       1248 return unless $self->basis->is_value_tuple( $tuple );
306 641         1348 $range = $self->try_check_range_definition( $range );
307 641 50       1307 return $range unless ref $range;
308 641 50       1114 [ map { ($self->is_axis_numeric( $_ )) ? (($tuple->[$_] - $range->[$_][0]) / ($range->[$_][1]-$range->[$_][0]))
  1927         2956  
309             : $tuple->[$_] } $self->basis->axis_iterator ];
310             }
311              
312             sub denormalize {
313 954     954 0 17146 my ($self, $tuple, $range) = @_;
314 954 50       1994 return unless $self->basis->is_value_tuple( $tuple );
315 954         1889 $range = $self->try_check_range_definition( $range );
316 954 50       1715 return $range unless ref $range;
317 954 50       1540 return [ map { ($self->is_axis_numeric( $_ )) ? ($tuple->[$_] * ($range->[$_][1]-$range->[$_][0]) + $range->[$_][0])
  2869         4398  
318             : $tuple->[$_] } $self->basis->axis_iterator ];
319             }
320              
321             sub denormalize_delta {
322 30     30 0 3885 my ($self, $delta_values, $range) = @_;
323 30 50       102 return unless $self->basis->is_value_tuple( $delta_values );
324 30         77 $range = $self->try_check_range_definition( $range );
325 30 50       68 return $range unless ref $range;
326 30 50       85 [ map { ($self->is_axis_numeric( $_ ))
  91         166  
327             ? ($delta_values->[$_] * ($range->[$_][1]-$range->[$_][0]))
328             : $delta_values->[$_] } $self->basis->axis_iterator ];
329             }
330              
331             sub delta { # @normal_tuple_a, @normal_tuple_b --> @delta_tuple
332 59     59 0 10097 my ($self, $tuple1, $tuple2) = @_;
333 59 100 100     176 return unless $self->basis->is_value_tuple( $tuple1 ) and $self->basis->is_value_tuple( $tuple2 );
334             # ignore none numeric dimensions
335 53 50       150 my @delta = map { $self->is_axis_numeric($_) ? ($tuple2->[$_] - $tuple1->[$_]) : 0 } $self->basis->axis_iterator;
  160         393  
336 53 100       258 [ map { $self->{'type'}[$_] ? $delta[$_] : # adapt to circular dimensions
  160 100       584  
    100          
337             $delta[$_] < -0.5 ? ($delta[$_]+1) :
338             $delta[$_] > 0.5 ? ($delta[$_]-1) : $delta[$_] } $self->basis->axis_iterator ];
339             }
340              
341             1;