package URI::geo; use warnings; use strict; use Carp; use URI::Split qw( uri_split uri_join ); use base qw( URI ); our $VERSION = '5.27'; sub _MINIMUM_LATITUDE { return -90 } sub _MAXIMUM_LATITUDE { return 90 } sub _MINIMUM_LONGITUDE { return -180 } sub _MAXIMUM_LONGITUDE { return 180 } sub _MAX_POINTY_PARAMETERS { return 3 } sub _can { my ($can_pt, @keys) = @_; for my $key (@keys) { return $key if $can_pt->can($key); } return; } sub _has { my ($has_pt, @keys) = @_; for my $key (@keys) { return $key if exists $has_pt->{$key}; } return; } # Try hard to extract location information from something. We handle lat, # lon, alt as scalars, arrays containing lat, lon, alt, hashes with # suitably named keys and objects with suitably named methods. sub _location_of_pointy_thing { my ($class, @parameters) = @_; my @lat = qw( lat latitude ); my @lon = qw( lon long longitude lng ); my @ele = qw( ele alt elevation altitude ); if (ref $parameters[0]) { my $pt = shift @parameters; if (@parameters) { croak q[Too many arguments]; } if (eval { $pt->can('can') }) { for my $m (qw( location latlong )) { return $pt->$m() if _can($pt, $m); } my $latk = _can($pt, @lat); my $lonk = _can($pt, @lon); my $elek = _can($pt, @ele); if (defined $latk && defined $lonk) { return $pt->$latk(), $pt->$lonk(), defined $elek ? $pt->$elek() : undef; } } elsif ('ARRAY' eq ref $pt) { return $class->_location_of_pointy_thing(@{$pt}); } elsif ('HASH' eq ref $pt) { my $latk = _has($pt, @lat); my $lonk = _has($pt, @lon); my $elek = _has($pt, @ele); if (defined $latk && defined $lonk) { return $pt->{$latk}, $pt->{$lonk}, defined $elek ? $pt->{$elek} : undef; } } croak q[Don't know how to convert point]; } else { croak q[Need lat, lon or lat, lon, alt] if @parameters < 2 || @parameters > _MAX_POINTY_PARAMETERS(); return my ($lat, $lon, $alt) = @parameters; } } sub _num { my ($class, $n) = @_; if (!defined $n) { return q[]; } (my $rep = sprintf '%f', $n) =~ s/[.]0*$//smx; return $rep; } sub new { my ($self, @parameters) = @_; my $class = ref $self || $self; my $uri = uri_join 'geo', undef, $class->_path(@parameters); return bless \$uri, $class; } sub _init { my ($class, $uri, $scheme) = @_; my $self = $class->SUPER::_init($uri, $scheme); # Normalise at poles. my $lat = $self->latitude; if ($lat == _MAXIMUM_LATITUDE() || $lat == _MINIMUM_LATITUDE()) { $self->longitude(0); } return $self; } sub location { my ($self, @parameters) = @_; if (@parameters) { my ($lat, $lon, $alt) = @parameters; return $self->latitude($lat)->longitude($lon)->altitude($alt); } return $self->latitude, $self->longitude, $self->altitude; } sub latitude { my ($self, @parameters) = @_; return $self->field('latitude', @parameters); } sub longitude { my ($self, @parameters) = @_; return $self->field('longitude', @parameters); } sub altitude { my ($self, @parameters) = @_; return $self->field('altitude', @parameters); } sub crs { my ($self, @parameters) = @_; return $self->field('crs', @parameters); } sub uncertainty { my ($self, @parameters) = @_; return $self->field('uncertainty', @parameters); } sub field { my ($self, $name, @remainder) = @_; my ($scheme, $auth, $v, $query, $frag) = $self->_parse; if (!exists $v->{$name}) { croak "No such field: $name"; } if (!@remainder) { return $v->{$name}; } $v->{$name} = shift @remainder; ${$self} = uri_join $scheme, $auth, $self->_format($v), $query, $frag; return $self; } { my $pnum = qr{\d+(?:[.]\d+)?}smx; my $num = qr{-?$pnum}smx; my $crsp = qr{(?:;crs=(\w+))}smx; my $uncp = qr{(?:;u=($pnum))}smx; my $parm = qr{(?:;\w+=[^;]*)+}smx; sub _parse { my $self = shift; my ($scheme, $auth, $path, $query, $frag) = uri_split ${$self}; $path =~ m{^ ($num), ($num) (?: , ($num) ) ? (?: $crsp ) ? (?: $uncp ) ? ( $parm ) ? $}smx or croak 'Badly formed geo uri'; # No named captures before 5.10.0 return $scheme, $auth, { latitude => $1, longitude => $2, altitude => $3, crs => $4, uncertainty => $5, parameters => (defined $6 ? substr $6, 1 : undef), }, $query, $frag; } } sub _format { my ($class, $v) = @_; return join q[;], ( join q[,], map { $class->_num($_) } @{$v}{'latitude', 'longitude'}, (defined $v->{altitude} ? ($v->{altitude}) : ()) ), (defined $v->{crs} ? ('crs=' . $class->_num($v->{crs})) : ()), ( defined $v->{uncertainty} ? ('u=' . $class->_num($v->{uncertainty})) : ()), (defined $v->{parameters} ? ($v->{parameters}) : ()); } sub _path { my ($class, @parameters) = @_; my ($lat, $lon, $alt) = $class->_location_of_pointy_thing(@parameters); croak 'Latitude out of range' if $lat < _MINIMUM_LATITUDE() || $lat > _MAXIMUM_LATITUDE(); croak 'Longitude out of range' if $lon < _MINIMUM_LONGITUDE() || $lon > _MAXIMUM_LONGITUDE(); if ($lat == _MINIMUM_LATITUDE() || $lat == _MAXIMUM_LATITUDE()) { $lat = 0; } return $class->_format( {latitude => $lat, longitude => $lon, altitude => $alt}); } 1; __END__ =head1 NAME URI::geo - URI scheme for geo Identifiers =head1 SYNOPSIS use URI; # Geo URI from textual uri my $guri = URI->new( 'geo:54.786989,-2.344214' ); # From coordinates my $guri = URI::geo->new( 54.786989, -2.344214 ); # Decode my ( $lat, $lon, $alt ) = $guri->location; my $latitude = $guri->latitude; # Update $guri->location( 55, -1 ); $guri->longitude( -43.23 ); =head1 DESCRIPTION From L: More and more protocols and data formats are being extended by methods to add geographic information. However, all of those options are tied to that specific protocol or data format. A dedicated Uniform Resource Identifier (URI) scheme for geographic locations would be independent from any protocol, usable by any software/data format that can handle generich URIs. Like a "mailto:" URI launches your favourite mail application today, a "geo:" URI could soon launch your favourite mapping service, or queue that location for a navigation device. =head1 SUBROUTINES/METHODS =head2 C<< new >> Create a new URI::geo. The arguments should be either =over =item * latitude, longitude and optionally altitude =item * a reference to an array containing lat, lon, alt =item * a reference to a hash with suitably named keys or =item * a reference to an object with suitably named accessors =back To maximize the likelihood that you can pass in some object that represents a geographical location and have URI::geo do the right thing we try a number of different accessor names. If the object has a C method (e.g. L) we'll use that. If there's a C method we call that. Otherwise we look for accessors called C, C, C, C, C, C, C, C or C and use them. Often if you have an object or hash reference that represents a point you can pass it directly to C; so for example this will work: use URI::geo; use Geo::Point; my $pt = Geo::Point->latlong( 48.208333, 16.372778 ); my $guri = URI::geo->new( $pt ); As will this: my $guri = URI::geo->new( { lat => 55, lon => -1 } ); and this: my $guri = URI::geo->new( 55, -1 ); Note that you can also create a new C by passing a Geo URI to C: use URI; my $guri = URI->new( 'geo:55,-1' ); =head2 C Get or set the location of this geo URI. my ( $lat, $lon, $alt ) = $guri->location; $guri->location( 55.3, -3.7, 120 ); When setting the location it is possible to pass any of the argument types that can be passed to C. =head2 C Get or set the latitude of this geo URI. =head2 C Get or set the longitude of this geo URI. =head2 C Get or set the L of this geo URI. To delete the altitude set it to C. =head2 C Get or set the L of this geo URI. To delete the CRS set it to C. =head2 C Get or set the L of this geo URI. To delete the uncertainty set it to C. =head2 C =head1 CONFIGURATION AND ENVIRONMENT URI::geo requires no configuration files or environment variables. =head1 DEPENDENCIES L =head1 DIAGNOSTICS =over =item C<< Too many arguments >> The L method can only accept three parameters; latitude, longitude and altitude. =item C<< Don't know how to convert point >> The L method doesn't know how to convert the supplied parameters into a URI::geo object. =item C<< Need lat, lon or lat, lon, alt >> The L method needs two (latitude and longitude) or three (latitude, longitude and altitude) parameters in a list. Any less or more than this is an error. =item C<< No such field: %s >> This field is not a known field for the L object. =item C<< Badly formed geo uri >> The L cannot be parsed as a URI =item C<< Badly formed geo uri >> The L cannot be parsed as a URI =item C<< Latitude out of range >> Latitude may only be from -90 to +90 =item C<< Longitude out of range >> Longitude may only be from -180 to +180 =back =head1 INCOMPATIBILITIES None reported. =head1 BUGS AND LIMITATIONS To report a bug, or view the current list of bugs, please visit L =head1 AUTHOR Andy Armstrong C<< >> =head1 LICENSE AND COPYRIGHT Copyright (c) 2009, Andy Armstrong C<< >>. This module is free software; you can redistribute it and/or modify it under the same terms as Perl itself. See L.