The London Perl and Raku Workshop takes place on 26th Oct 2024. If your company depends on Perl, please consider sponsoring and/or attending.

NAME

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

VERSION

version 1.01

SYNOPSIS

 use Geo::LibProj::cs2cs;
 
 $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). However, this module is definitely less efficient and possibly also less robust with regards to potential changes to the cs2cs input/output format.

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. See "PERFORMANCE CONSIDERATIONS" for details.

version

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

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

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}, ...);

There is a small chance that future versions of Geo::LibProj::cs2cs might automatically switch to an XS implementation if a suitable third-party module is installed (such as Geo::Proj4). This might improve speed dramatically, but it might also change some of the semantics of this module's interface in certain edge cases. If this matters to you, you can already now opt out of this behaviour by setting the internal parameter XS to a defined non-truthy value.

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. Note that these semantics are not yet finalised; they may change in future.

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.

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

PERFORMANCE CONSIDERATIONS

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];
 }

BUGS

To communicate with cs2cs, this software uses IPC::Run3. Instead of directly interacting with the cs2cs process, temp files are created for every call to transform(). This is probably reliable, but slow.

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

Please report new issues on GitHub.

SEE ALSO

Alien::proj

AUTHOR

Arne Johannessen <ajnn@cpan.org>

COPYRIGHT AND LICENSE

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

This is free software, licensed under:

  The Artistic License 2.0 (GPL Compatible)