The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.

NAME

Geo::LibProj::cs2cs - IPC interface to PROJ cs2cs

VERSION

version 1.03

SYNOPSIS

 use Geo::LibProj::cs2cs;
 use Geo::LibProj::FFI;  # optional module - see below
 
 $cs2cs = Geo::LibProj::cs2cs->new("EPSG:25833" => "EPSG:4326");
 $point = $cs2cs->transform( [500_000, 6094_800] );  # UTM 33U
 # result geographic lat, lon: [55.0, 15.0]
 
 @points_utm = ([500_000, 6094_800], [504_760, 6093_880]);
 @points_geo = $cs2cs->transform( @points_geo );
 
 $params = {-r => ''};  # control parameter -r: reverse input coords
 $cs2cs = Geo::LibProj::cs2cs->new("EPSG:4326" => "EPSG:25833", $params);
 $point = $cs2cs->transform( [q(15d4'28"E), q(54d59'30"N)] );
 # result easting, northing: [504763.08827, 6093866.63099]
 
 # old PROJ string syntax
 $source_crs = '+init=epsg:4326';
 $target_crs = '+proj=merc +lon_0=110';
 $cs2cs = Geo::LibProj::cs2cs->new($source_crs => $target_crs);
 ...

DESCRIPTION

This module is a Perl interprocess communication interface to the cs2cs(1) utility, which is a part of the PROJ coordinate transformation library.

Unlike Geo::Proj4, this module is pure Perl. It does require the PROJ library to be installed, but it does not use the PROJ API via XS. Instead, it communicates with the cs2cs utility using the standard input/output streams, just like you might do at a command line. Data is formatted using sprintf and parsed using regular expressions.

As a result, this module may be expected to work with many different versions of the PROJ library, whereas Geo::Proj4 is limited to version 4 (at time of this writing).

Because the interprocess communication (IPC) with cs2cs(1) has significant performance constraints, this module will try to emulate the behaviour of cs2cs(1) using Geo::LibProj::FFI if the latter module is loaded. This emulation is much faster than IPC, but does not support all features of cs2cs(1). See "XS MODE" below.

METHODS

Geo::LibProj::cs2cs implements the following methods.

new

 $cs2cs = Geo::LibProj::cs2cs->new($source_crs => $target_crs);

Construct a new Geo::LibProj::cs2cs object that can transform points from the specified source CRS to the target CRS (coordinate reference system).

Each CRS may be specified using any method the PROJ version installed on your system supports for the cs2cs utility. The legacy "PROJ string" format is currently supported on all PROJ versions:

 $source_crs = '+init=epsg:4326';
 $target_crs = '+proj=merc +lon_0=110';
 $cs2cs = Geo::LibProj::cs2cs->new($source_crs => $target_crs);

PROJ 6 and newer support additional formats to express a CRS, such as a WKT string or an AUTHORITY:CODE. Note that the axis order might differ between some of these choices. See your PROJ version's cs2cs(1) documentation for details.

Control parameters may optionally be supplied to cs2cs in a hash ref using one of the following forms:

 $cs2cs = Geo::LibProj::cs2cs->new(\%params, $source_crs => $target_crs);
 $cs2cs = Geo::LibProj::cs2cs->new($source_crs => $target_crs, \%params);

Each of the %params hash's keys represents a single control parameter. Parameters are supplied exactly like in a cs2cs call on a command line, with a leading -. The value must be a defined value; a value of undef will unset the parameter.

 %params = (
   -I => '',      # inverse ON (switch $source_crs and $target_crs)
   -f => '%.5f',  # output format (5 decimal digits)
   -r => undef,   # reverse coord input OFF (the default)
 );

See the "CONTROL PARAMETERS" section below for implementation details of specific control parameters.

transform

 $point_1 = [$x1, $y1];
 $point_2 = [$x2, $y2, $z2, $aux];
 @input_points  = ( $point_1, $point_2, ... );
 @output_points = $cs2cs->transform( @input_points );
 
 # transforming coordinates of just a single point:
 $output_point = $cs2cs->transform( [$x3, $y3, $z3] );

Execute cs2cs to perform a CRS transformation of the specified point or points. At least two coordinates (x/y) are required, a third (z) may optionally be supplied.

Additionally, auxiliary data may be included in a fourth array element. Just like cs2cs, this value is simply passed through from the input point to the output point. Geo::LibProj::cs2cs doesn't stringify this value for cs2cs, so you can safely use Perl references as auxiliary data, even blessed ones.

Coordinates are stringified for cs2cs as numbers with at least the same precision as specified in the -f control parameter.

Each point in a list is a simple unblessed array reference. When just a single input point is given, transform() may be called in scalar context to directly obtain a reference to the output point. For lists of multiple input points, calling in scalar context is prohibited.

Each call to transform() creates a new cs2cs process and runs through the PROJ initialisation. Avoid calling this method in a loop (except in "XS MODE"). See "PERFORMANCE CONSIDERATIONS" for details.

version

 $version = Geo::LibProj::cs2cs->version;

Attempt to determine the version of PROJ installed on your system.

xs

 $cs2cs = Geo::LibProj::cs2cs->new(...);
 $bool = $cs2cs->xs;

Indicates whether a Geo::LibProj::cs2cs instance is using "XS MODE".

CONTROL PARAMETERS

Geo::LibProj::cs2cs implements special handling for the following control parameters. Parameters not mentioned here are passed on to cs2cs as-is. See your PROJ version's cs2cs(1) documentation for a full list of supported options.

-d

 Geo::LibProj::cs2cs->new({-d => 7}, ...);

Fully supported shorthand to -f %f. Specifies the number of decimals in the output.

-f

 Geo::LibProj::cs2cs->new({-f => '%.7f'}, ...);

Fully supported (albeit with the limitations inherent in cs2cs). Specifies a printf format string to control the output values.

For Geo::LibProj::cs2cs, the default value is currently '%.12g', which allows easy further processing with Perl while keeping loss of floating point precision low enough for any cartographic use case. To enable the cs2cs DMS string format (54d59'30.43"N), you need to explicitly unset this parameter by supplying undef. This will make cs2cs use its built-in default format.

Unsupported parameters

 Geo::LibProj::cs2cs->new({-E => '' }, ...);  # fails
 Geo::LibProj::cs2cs->new({-t => '#'}, ...);  # fails
 Geo::LibProj::cs2cs->new({-v => '' }, ...);  # fails

The -E, -t, and -v parameters disrupt parsing of the transformation result and are unsupported.

XS

 Geo::LibProj::cs2cs->new({XS => 0}, ...);
 Geo::LibProj::cs2cs->new({XS => 1}, ...);
 Geo::LibProj::cs2cs->new({XS => undef}, ...);  # the default

By default, this module will in certain cases try to use a foreign function interface provided by Geo::LibProj::FFI to emulate cs2cs, rather than use cs2cs(1) itself. This can give a dramatic performance boost, but does not support all features of cs2cs(1). See "XS MODE" for details.

To opt-out of this behaviour and force this module to only use an actual cs2cs(1) utility through IPC, the internal parameter XS may be set to a defined non-truthy value (XS => 0).

The XS parameter can also be set to a truthy value to indicate a preference for the emulation. With XS => 1 set, this module will emit warnings if it must fall back to IPC due to an error in the emulation's initialisation.

ENVIRONMENT

The cs2cs binary is expected to be on the environment's PATH. However, if Alien::proj is available, its share install will be preferred.

If this doesn't suit you, you can control the selection of the cs2cs binary by modifying the value of @Geo::LibProj::cs2cs::PATH. The directories listed will be tried in order, and the first match will be used. An explicit value of undef in the list will cause the environment's PATH to be used at that position in the search.

DIAGNOSTICS

When cs2cs detects data errors (such as an input value of 91dN latitude), it returns an error string in place of the result coordinates. The error string can be controlled by the -e parameter as described in the cs2cs(1) documentation.

In "XS MODE", Inf is used instead of an error string.

Geo::LibProj::cs2cs dies as soon as any other error condition is discovered. Use eval, Try::Tiny or similar to catch this.

PERFORMANCE CONSIDERATIONS

Note: This section does not apply in "XS MODE".

The transform() method has enormous overhead. Profiling shows the rate of transform() calls you can expect to be of the order of maybe 20/s or so, depending on your system.

The primary reason seems to be that each call to transform() spawns a new cs2cs process, which must run through complete PROJ initialisation each time. Additionally, this module could probably improve the interprocess communication overhead, but so far profiling suggests this is a minor problem by comparison.

Once transform() is past that initialisation, it actually is reasonably fast. This means that what you need to do in order to get good performance is simply to keep the number of transform() calls in your code as low as possible. Obviously, it still won't be quite as fast as XS code such as Geo::Proj4, but it will be fast enough that the difference likely won't matter to many applications.

You should never be calling transform() from within a loop that runs through all your coordinate pairs. That may be a typical pattern in existing code for Geo::Proj4, but if you try that with Geo::LibProj::cs2cs, it'll just take forever. (Well, almost.)

 # Don't do this!
 for my $p ( @thousands_of_points ) {
   push @result, $cs2cs->transform( $p );
 }

Instead, gather your points in a single list, and pass that one big list to a single transform() call.

 # Do this:
 @result = $cs2cs->transform( @thousands_of_points );

Depending on your data structure, however, it may not be as simple as that. Imagine a structure looking like this, with coordinate pairs you need to transform into another CRS:

 $r1 = bless {
   some_data => { ... },
   coords => { east => $e1, north => $n1 },
 }, 'Record';
 ...
 @records = ( $r1, $r2, ... $rN );
 #@result = $cs2cs->transform(@records);  # fails

You can't simply pass @records to transform() because it has no way of knowing how to deal with Record type objects. So, as a first step, you need to create a list containing points in the proper format:

 @points = map { [
   $_->{coords}->{east},   # x
   $_->{coords}->{north},  # y
   0,                      # z
   $_,                     # backref - see below
 ] } @records;
 @result = $cs2cs->transform(@points);  # succeeds

The @points list can be passed to transform(). To re-insert the transformed coordinates into your original @records data structure, you could iterate over both lists at the same time, as their length and order of elements should correspond to one another.

Alternatively, Geo::LibProj::cs2cs allows for pass-through of Perl references in the fourth field of a point array, so you can use it to easily get back to the original Record and insert the transformed coordinates as required:

 for my $p ( @result ) {
   my $record = $p->[3];   # get the backref
   $record->{coords}->{lon} = $p->[0];
   $record->{coords}->{lat} = $p->[1];
 }

XS MODE

In order to improve performance, this module is able use a foreign function interface provided by Geo::LibProj::FFI to emulate cs2cs. This is dramatically faster in most situations, and should also be thread-safe.

In this document, the term "XS mode" is used for this emulation due to historical reasons. perlxs is not actually used by this module at present.

XS mode will be used if, and only if, all of the following conditions are met:

  • Geo::LibProj::FFI is loaded (e. g. with use) before the Geo::LibProj::cs2cs instance is created with new().

  • The internal "XS" control parameter is either set to a truthy value, set to undef, or is missing entirely.

  • No other control parameters are specified.

  • Both the source CRS and the target CRS given to the new() method can be successfully interpreted as CRS by proj_create().

  • Creating a new PROJ threading context using proj_context_create() succeeds.

  • Creating a new PROJ transformation object using proj_create_crs_to_crs() succeeds.

For example, the following code should reliably result in the use of XS mode:

 use Geo::LibProj::cs2cs 1.02;
 use Geo::LibProj::FFI;
 
 $cs2cs = Geo::LibProj::cs2cs->new("EPSG:4326" => "EPSG:25833");
 $point = $cs2cs->transform( [54 + 59/60 + 30.43/3600, -4.2061] );
 
 say $cs2cs->xs ? "FFI/XS mode" : "IPC mode";

Note that XS mode doesn't support the cs2cs DMS string format (54d59'30.43"N). You must use numbers, as shown in the example.

BUGS

To communicate with cs2cs, this software uses IPC::Run3. On most platforms, that module is not threads-safe. Instead of directly interacting with the cs2cs process, IPC::Run3 creates temp files for every call to transform(). Except when using threads, this is reliable, but somewhat slow.

The -l... list parameters have not been implemented.

This module doesn't seem to work on Win32. Try Cygwin.

Please report new issues on GitHub.

SEE ALSO

Alien::proj

Geo::LibProj::FFI

AUTHOR

Arne Johannessen <ajnn@cpan.org>

COPYRIGHT AND LICENSE

This software is Copyright (c) 2020-2021 by Arne Johannessen.

This is free software, licensed under:

  The Artistic License 2.0 (GPL Compatible)