File Coverage

lib/Geo/LibProj/cs2cs.pm
Criterion Covered Total %
statement 139 139 100.0
branch 86 88 97.7
condition 29 30 96.6
subroutine 19 19 100.0
pod 4 4 100.0
total 277 280 98.9


line stmt bran cond sub pod time code
1 5     5   9529 use 5.014;
  5         46  
2 5     5   24 use strict;
  5         8  
  5         103  
3 5     5   22 use warnings;
  5         7  
  5         318  
4              
5             package Geo::LibProj::cs2cs;
6             # ABSTRACT: IPC interface to PROJ cs2cs
7             $Geo::LibProj::cs2cs::VERSION = '1.02';
8              
9 5     5   31 use Carp qw(carp croak);
  5         15  
  5         542  
10 5     5   42 use File::Basename qw(basename);
  5         7  
  5         660  
11 5     5   31 use File::Spec;
  5         8  
  5         205  
12 5     5   30 use Scalar::Util 1.10 qw(looks_like_number);
  5         152  
  5         578  
13              
14 5     5   3385 use IPC::Run3 qw(run3);
  5         190798  
  5         10250  
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 21     21 1 14696 my $class = shift;
36            
37 21         48 my ($source_crs, $target_crs, $user_params);
38 21 100       88 if ( ref($_[0]) eq 'HASH' ) {
39 1         3 ($user_params, $source_crs, $target_crs) = @_;
40             }
41             else {
42 20         85 ($source_crs, $target_crs, $user_params) = @_;
43             }
44            
45 21         59 my $self = bless {}, $class;
46            
47 21 100       139 my $params = { %PARAMS, defined $user_params ? %$user_params : () };
48 21         94 $self->_special_params($params);
49 18         48 $self->{format_in} = $FORMAT_IN;
50            
51             # assemble cs2cs call line
52 18         72 for my $key (keys %$params) {
53 25 100       75 delete $params->{$key} unless defined $params->{$key};
54             }
55 18         107 $self->{cmd} = $self->_cmd();
56 18         133 $self->{call} = [$self->{cmd}, %$params, $source_crs, '+to', $target_crs, '-'];
57            
58 18         90 $self->_ffi_init($source_crs, $target_crs, $params);
59            
60 18         130 return $self;
61             }
62              
63              
64             sub _special_params {
65 24     24   6182 my ($self, $params) = @_;
66            
67             # support -d even for older cs2cs versions
68 24 100 100     117 if (defined $params->{-d} && defined $params->{-f}) {
69 1         7 $params->{-f} = '%.' . (0 + $params->{-d}) . 'f';
70 1         3 delete $params->{-d};
71             }
72            
73 24 100       88 croak "-E is unsupported" if defined $params->{'-E'};
74 23 100       91 croak "-t is unsupported" if defined $params->{'-t'};
75 22 100       70 croak "-v is unsupported" if defined $params->{'-v'};
76            
77             # -w3 must be supplied as a single parameter to cs2cs
78 21 100       62 if (defined $params->{-w}) {
79 2         27 $params->{"-w$params->{-w}"} = '';
80 2         13 delete $params->{-w};
81             }
82 21 100       61 if (defined $params->{-W}) {
83 2         85 $params->{"-W$params->{-W}"} = '';
84 2         14 delete $params->{-W};
85             }
86            
87             $self->{ffi} = $INC{'Geo/LibProj/FFI.pm'}
88 21   100     128 && ( $params->{XS} || ! defined $params->{XS} );
89 21         42 $self->{ffi_warn} = $params->{XS};
90 21         66 delete $params->{XS};
91             }
92              
93              
94             sub _cmd {
95             # try to find the cs2cs binary
96 28     28   6911 foreach my $path (@PATH) {
97 30 100       202 if (defined $path) {
98 28         638 my $cmd = File::Spec->catfile($path, $CMD);
99 28 100       1144 return $cmd if -e $cmd;
100             }
101             else {
102             # when the @PATH element is undefined, try the env PATH
103 2         5 eval { run3 [$CMD, '-lp'], \undef, \undef, \undef };
  2         20  
104 2 100 66     9704 return $CMD if ! $@ && $? == 0;
105             }
106             }
107            
108             # no luck; let's just hope it'll be on the PATH somewhere
109 5         124 return $CMD;
110             }
111              
112              
113             sub _ffi_init {
114 34     34   34229 my ($self, $source_crs, $target_crs, $params) = @_;
115            
116 34 100 100     171 carp "Geo::LibProj::FFI is not loaded; falling back to IPC mode" if $self->{ffi_warn} && ! $self->{ffi};
117 34 100       586 return unless $self->{ffi};
118            
119             my @params = grep {
120 15         45 $_ eq '-f'
121             ? defined $params->{$_} && $params->{$_} ne $FORMAT_OUT
122 9 100 100     123 : defined $params->{$_}
123             } keys %$params;
124 15 100 100     85 carp "cs2cs control parameters are unsupported in XS mode; falling back to IPC mode" if $self->{ffi_warn} && @params;
125 15 100       773 return $self->{ffi} = 0 if @params;
126            
127 11         127 my $ctx = $self->{ffi_ctx} = Geo::LibProj::FFI::proj_context_create();
128 11 100 100     78 carp "proj_context_create() failed; falling back to IPC mode" if $self->{ffi_warn} && ! $ctx;
129 11 100       417 return $self->{ffi} = 0 if ! $ctx;
130            
131 9         32 Geo::LibProj::FFI::proj_context_use_proj4_init_rules($ctx, 1);
132            
133 9         28197 my $pj = $self->{ffi_pj} = Geo::LibProj::FFI::proj_create_crs_to_crs(
134             $ctx, $source_crs, $target_crs, undef );
135 9 100 100     62 carp "proj_create_crs_to_crs() failed; falling back to IPC mode" if $self->{ffi_warn} && ! $pj;
136 9 100       471 return $self->{ffi} = 0 if ! $pj;
137             }
138              
139              
140             sub DESTROY {
141 25     25   236091 my ($self) = @_;
142            
143 25 100       308 Geo::LibProj::FFI::proj_destroy($self->{ffi_pj}) if $self->{ffi_pj};
144 25 100       2372 Geo::LibProj::FFI::proj_context_destroy($self->{ffi_ctx}) if $self->{ffi_ctx};
145 25         1112 $self->{ffi_pj} = $self->{ffi_ctx} = 0;
146             }
147              
148              
149             sub _ipc_error_check {
150 21     21   524 my ($self, $eval_err, $os_err, $code, $stderr) = @_;
151            
152 21         189 my $cmd = $CMD;
153 21 100       651 if (ref $self) {
154 17         199 $self->{stderr} = $stderr;
155 17         243 $self->{status} = $code >> 8;
156             }
157            
158 21 100       241 $stderr =~ s/^(.*\S)\s*\z/: $1/s if length $stderr;
159 21 100       179 croak "`$cmd` failed to execute: $os_err" if $code == -1;
160 20 50       109 croak "`$cmd` died with signal " . ($code & 0x7f) . $stderr if $code & 0x7f;
161 20 100       271 croak "`$cmd` exited with status " . ($code >> 8) . $stderr if $code;
162 18 50       109 croak $eval_err =~ s/\s+\z//r if $eval_err;
163             }
164              
165              
166             sub _format {
167 60     60   110 my ($self, $value) = @_;
168            
169 60 100       649 return sprintf $self->{format_in}, $value if looks_like_number $value;
170 4         35 return $value;
171             }
172              
173              
174             sub transform {
175 20     20 1 34177 my ($self, @source_points) = @_;
176            
177 20 100       92 return $self->_ffi_transform(@source_points) if $self->{ffi};
178            
179 16         38 my @in = ();
180 16         67 foreach my $i (0 .. $#source_points) {
181 20         43 my $p = $source_points[$i];
182 20   100     112 push @in, $self->_format($p->[0] || 0) . " "
      100        
      100        
183             . $self->_format($p->[1] || 0) . " "
184             . $self->_format($p->[2] || 0) . " $i";
185             }
186 16         55 my $in = join "\n", @in;
187            
188 16         32 my @out = ();
189 16         49 my $err = '';
190 16         25 eval {
191 16         83 local $/ = "\n";
192 16         139 run3 $self->{call}, \$in, \@out, \$err;
193             };
194 16         384737 $self->_ipc_error_check($@, $!, $?, $err);
195            
196 15         43 my @target_points = ();
197 15         85 foreach my $line (@out) {
198 18 100       395 next unless $line =~ m{\s(\d+)\s*$}xa;
199 17         228 my $aux = $source_points[$1]->[3];
200 17 100       236 next unless $line =~ m{^\s* (\S+) \s+ (\S+) \s+ (\S+) \s}xa;
201 16 100       316 my @p = defined $aux ? ($1, $2, $3, $aux) : ($1, $2, $3);
202            
203 16         86 foreach my $j (0..2) {
204 48 100       305 $p[$j] = 0 + $p[$j] if looks_like_number $p[$j];
205             }
206            
207 16         89 push @target_points, \@p;
208             }
209            
210 15 100       106 if ( (my $s = @source_points) != (my $t = @target_points) ) {
211 3         239 croak "Source/target point count doesn't match ($s/$t): Assertion failed";
212             }
213            
214 12 100       254 return @target_points if wantarray;
215 9 100       485 return $target_points[0] if @target_points < 2;
216 1         90 croak "transform() with list argument prohibited in scalar context";
217             }
218              
219              
220             sub _ffi_transform {
221 4     4   10 my ($self, @source_points) = @_;
222            
223             my @target_points = map {
224 4         11 my $p = Geo::LibProj::FFI::_trans( $self->{ffi_pj}, 1, [$_->[0], $_->[1], $_->[2], 'Inf'] );
  8         39  
225 8         131 $p->[3] = $_->[3];
226 8 100       23 delete $p->[3] unless defined $p->[3];
227 8         20 $p;
228             } @source_points;
229            
230 4 100       13 return @target_points if wantarray;
231 3 100       22 return $target_points[0] if @target_points < 2;
232 1         19 croak "transform() with list argument prohibited in scalar context";
233             }
234              
235              
236             sub version {
237 6     6 1 21062 my ($self) = @_;
238            
239 6 100       44 my $ffi = ref $self ? $self->{ffi} : $INC{'Geo/LibProj/FFI.pm'};
240 6 100       27 return Geo::LibProj::FFI::proj_info()->version if $ffi;
241            
242 5         23 my $out = '';
243 5         9 eval {
244 5         34 run3 [ $self->_cmd ], \undef, \$out, \$out;
245             };
246 5         31332 $self->_ipc_error_check($@, $!, $?, '');
247            
248 3 100       191 return $1 if $out =~ m/\b(\d+\.\d+(?:\.\d\w*)?)\b/;
249 1         56 return $out;
250             }
251              
252              
253 2     2 1 5154 sub xs { shift->{ffi} }
254              
255              
256             1;
257              
258             __END__