File Coverage

lib/Geo/LibProj/cs2cs.pm
Criterion Covered Total %
statement 105 105 100.0
branch 52 54 96.3
condition 11 12 91.6
subroutine 15 15 100.0
pod 3 3 100.0
total 186 189 98.4


line stmt bran cond sub pod time code
1 4     4   7129 use 5.014;
  4         31  
2 4     4   20 use strict;
  4         7  
  4         89  
3 4     4   18 use warnings;
  4         6  
  4         268  
4              
5             package Geo::LibProj::cs2cs;
6             # ABSTRACT: IPC interface to PROJ cs2cs
7             $Geo::LibProj::cs2cs::VERSION = '1.01';
8              
9 4     4   32 use Carp qw(croak);
  4         14  
  4         364  
10 4     4   32 use File::Basename qw(basename);
  4         8  
  4         505  
11 4     4   25 use File::Spec;
  4         6  
  4         177  
12 4     4   24 use Scalar::Util 1.10 qw(looks_like_number);
  4         136  
  4         441  
13              
14 4     4   2791 use IPC::Run3 qw(run3);
  4         143566  
  4         5874  
15              
16              
17             our $CMD = 'cs2cs';
18             our @PATH = ();
19             eval {
20             require # optional module; try to hide from static analysers
21             Alien::proj;
22             unshift @PATH, File::Spec->catdir(Alien::proj->dist_dir, 'bin');
23             };
24              
25             # default stringification formats for cs2cs stdin and stdout
26             our $FORMAT_IN = '%.15g';
27             our $FORMAT_OUT = '%.12g';
28              
29             our %PARAMS = (
30             -f => $FORMAT_OUT,
31             );
32              
33              
34             sub new {
35 20     20 1 65610 my $class = shift;
36            
37 20         59 my ($source_crs, $target_crs, $user_params);
38 20 100       88 if ( ref($_[0]) eq 'HASH' ) {
39 1         4 ($user_params, $source_crs, $target_crs) = @_;
40             }
41             else {
42 19         70 ($source_crs, $target_crs, $user_params) = @_;
43             }
44            
45 20         46 my $self = bless {}, $class;
46            
47 20 100       153 my $params = { %PARAMS, defined $user_params ? %$user_params : () };
48 20         83 $self->_special_params($params);
49 17         58 $self->{format_in} = $FORMAT_IN;
50            
51             # assemble cs2cs call line
52 17         91 for my $key (keys %$params) {
53 24 100       77 delete $params->{$key} unless defined $params->{$key};
54             }
55 17         96 $self->{cmd} = $self->_cmd();
56 17         124 $self->{call} = [$self->{cmd}, %$params, $source_crs, '+to', $target_crs, '-'];
57            
58 17         112 return $self;
59             }
60              
61              
62             sub _special_params {
63 20     20   54 my (undef, $params) = @_;
64            
65             # support -d even for older cs2cs versions
66 20 100 100     92 if (defined $params->{-d} && defined $params->{-f}) {
67 1         7 $params->{-f} = '%.' . (0 + $params->{-d}) . 'f';
68 1         1 delete $params->{-d};
69             }
70            
71 20 100       66 croak "-E is unsupported" if defined $params->{'-E'};
72 19 100       90 croak "-t is unsupported" if defined $params->{'-t'};
73 18 100       55 croak "-v is unsupported" if defined $params->{'-v'};
74            
75             # -w3 must be supplied as a single parameter to cs2cs
76 17 100       51 if (defined $params->{-w}) {
77 2         40 $params->{"-w$params->{-w}"} = '';
78 2         10 delete $params->{-w};
79             }
80 17 100       52 if (defined $params->{-W}) {
81 2         32 $params->{"-W$params->{-W}"} = '';
82 2         19 delete $params->{-W};
83             }
84            
85 17         43 delete $params->{XS};
86             }
87              
88              
89             sub _cmd {
90             # try to find the cs2cs binary
91 27     27   6391 foreach my $path (@PATH) {
92 29 100       137 if (defined $path) {
93 27         645 my $cmd = File::Spec->catfile($path, $CMD);
94 27 100       916 return $cmd if -e $cmd;
95             }
96             else {
97             # when the @PATH element is undefined, try the env PATH
98 2         4 eval { run3 [$CMD, '-lp'], \undef, \undef, \undef };
  2         13  
99 2 100 66     7546 return $CMD if ! $@ && $? == 0;
100             }
101             }
102            
103             # no luck; let's just hope it'll be on the PATH somewhere
104 5         112 return $CMD;
105             }
106              
107              
108             sub _ipc_error_check {
109 21     21   470 my ($self, $eval_err, $os_err, $code, $stderr) = @_;
110            
111 21         168 my $cmd = $CMD;
112 21 100       332 if (ref $self) {
113 17         240 $self->{stderr} = $stderr;
114 17         166 $self->{status} = $code >> 8;
115             }
116            
117 21 100       205 $stderr =~ s/^(.*\S)\s*\z/: $1/s if length $stderr;
118 21 100       130 croak "`$cmd` failed to execute: $os_err" if $code == -1;
119 20 50       100 croak "`$cmd` died with signal " . ($code & 0x7f) . $stderr if $code & 0x7f;
120 20 100       275 croak "`$cmd` exited with status " . ($code >> 8) . $stderr if $code;
121 18 50       108 croak $eval_err =~ s/\s+\z//r if $eval_err;
122             }
123              
124              
125             sub _format {
126 60     60   119 my ($self, $value) = @_;
127            
128 60 100       644 return sprintf $self->{format_in}, $value if looks_like_number $value;
129 4         45 return $value;
130             }
131              
132              
133             sub transform {
134 16     16 1 22937 my ($self, @source_points) = @_;
135            
136 16         39 my @in = ();
137 16         70 foreach my $i (0 .. $#source_points) {
138 20         48 my $p = $source_points[$i];
139 20   100     153 push @in, $self->_format($p->[0] || 0) . " "
      100        
      100        
140             . $self->_format($p->[1] || 0) . " "
141             . $self->_format($p->[2] || 0) . " $i";
142             }
143 16         53 my $in = join "\n", @in;
144            
145 16         35 my @out = ();
146 16         78 my $err = '';
147 16         31 eval {
148 16         116 local $/ = "\n";
149 16         150 run3 $self->{call}, \$in, \@out, \$err;
150             };
151 16         893411 $self->_ipc_error_check($@, $!, $?, $err);
152            
153 15         50 my @target_points = ();
154 15         128 foreach my $line (@out) {
155 18 100       460 next unless $line =~ m{\s(\d+)\s*$}xa;
156 17         232 my $aux = $source_points[$1]->[3];
157 17 100       197 next unless $line =~ m{^\s* (\S+) \s+ (\S+) \s+ (\S+) \s}xa;
158 16 100       295 my @p = defined $aux ? ($1, $2, $3, $aux) : ($1, $2, $3);
159            
160 16         123 foreach my $j (0..2) {
161 48 100       335 $p[$j] = 0 + $p[$j] if looks_like_number $p[$j];
162             }
163            
164 16         78 push @target_points, \@p;
165             }
166            
167 15 100       125 if ( (my $s = @source_points) != (my $t = @target_points) ) {
168 3         183 croak "Source/target point count doesn't match ($s/$t): Assertion failed";
169             }
170            
171 12 100       181 return @target_points if wantarray;
172 9 100       410 return $target_points[0] if @target_points < 2;
173 1         74 croak "transform() with list argument prohibited in scalar context";
174             }
175              
176              
177             sub version {
178 5     5 1 15449 my ($self) = @_;
179            
180 5         20 my $out = '';
181 5         8 eval {
182 5         16 run3 [ $self->_cmd ], \undef, \$out, \$out;
183             };
184 5         27651 $self->_ipc_error_check($@, $!, $?, '');
185            
186 3 100       145 return $1 if $out =~ m/\b(\d+\.\d+(?:\.\d\w*)?)\b/;
187 1         48 return $out;
188             }
189              
190              
191             1;
192              
193             __END__