File Coverage

lib/Geo/LibProj/cs2cs.pm
Criterion Covered Total %
statement 142 142 100.0
branch 87 90 96.6
condition 33 34 97.0
subroutine 20 20 100.0
pod 4 4 100.0
total 286 290 98.6


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