File Coverage

blib/lib/URI/geo.pm
Criterion Covered Total %
statement 90 92 97.8
branch 33 44 75.0
condition 9 24 37.5
subroutine 19 19 100.0
pod 5 5 100.0
total 156 184 84.7


line stmt bran cond sub pod time code
1             package URI::geo;
2              
3 5     5   62049 use warnings;
  5         11  
  5         161  
4 5     5   27 use strict;
  5         7  
  5         143  
5              
6 5     5   26 use Carp;
  5         26  
  5         473  
7 5     5   5014 use URI::Split qw( uri_split uri_join );
  5         13175  
  5         485  
8              
9 5     5   35 use base qw( URI );
  5         7  
  5         5589  
10              
11             =head1 NAME
12              
13             URI::geo - The geo URI scheme.
14              
15             =head1 VERSION
16              
17             This document describes URI::geo version 0.05
18              
19             =cut
20              
21             our $VERSION = '0.05';
22              
23             =head1 SYNOPSIS
24              
25             use URI;
26              
27             # GeoURI from textual uri
28             my $guri = URI->new( 'geo:54.786989,-2.344214' );
29              
30             # From coordinates
31             my $guri = URI::geo->new( 54.786989, -2.344214 );
32              
33             # Decode
34             my ( $lat, $lon, $alt ) = $guri->location;
35             my $latitude = $guri->latitude;
36              
37             # Update
38             $guri->location( 55, -1 );
39             $guri->longitude( -43.23 );
40            
41             =head1 DESCRIPTION
42              
43             From L:
44              
45             More and more protocols and data formats are being extended by methods
46             to add geographic information. However, all of those options are tied
47             to that specific protocol or data format.
48              
49             A dedicated Uniform Resource Identifier (URI) scheme for geographic
50             locations would be independent from any protocol, usable by any
51             software/data format that can handle generich URIs. Like a "mailto:"
52             URI launches your favourite mail application today, a "geo:" URI could
53             soon launch your favourite mapping service, or queue that location for
54             a navigation device.
55              
56             =cut
57              
58             {
59             my $num = qr{-?\d{1,3}(?:\.\d+)?};
60              
61             sub _parse {
62 56     56   86 my ( $class, $path ) = @_;
63 56 100       762 croak "Badly formed geo uri"
64             unless $path =~ /^$num(?:,$num){1,2}$/;
65 55         307 return my ( $lat, $lon, $alt ) = split /,/, $path;
66             }
67             }
68              
69             # Try hard to extract location information from something. We handle lat,
70             # lon, alt as scalars, arrays containing lat, lon, alt, hashes with
71             # suitably named keys and objects with suitably named methods.
72              
73             sub _location_of_pointy_thing {
74 20     20   26 my $class = shift;
75              
76 20         41 my @lat = ( 'lat', 'latitude' );
77 20         41 my @lon = ( 'lon', 'long', 'longitude' );
78 20         40 my @ele = ( 'ele', 'alt', 'elevation', 'altitude' );
79              
80 20 100       47 if ( ref $_[0] ) {
81 6         7 my $pt = shift;
82              
83 6 50       14 croak "Too many arguments" if @_;
84              
85 6 100       51 if ( UNIVERSAL::can( $pt, 'can' ) ) {
    100          
    50          
86 3         8 for my $m ( qw( location latlong ) ) {
87 5 100       40 return $pt->$m() if $pt->can( $m );
88             }
89              
90             my $can = sub {
91 3     3   7 my ( $pt, @keys ) = @_;
92 3         5 for my $key ( @keys ) {
93 4 100       22 return $key if $pt->can( $key );
94             }
95 0         0 return;
96 1         6 };
97              
98 1         3 my $latk = $can->( $pt, @lat );
99 1         4 my $lonk = $can->( $pt, @lon );
100 1         3 my $elek = $can->( $pt, @ele );
101              
102 1 50 33     7 if ( defined $latk && defined $lonk ) {
103 1 50       5 return $pt->$latk(), $pt->$lonk(),
104             defined $elek ? $pt->$elek() : undef;
105             }
106             }
107             elsif ( 'ARRAY' eq ref $pt ) {
108 1         9 return $class->_location_of_pointy_thing( @$pt );
109             }
110             elsif ( 'HASH' eq ref $pt ) {
111             my $has = sub {
112 6     6   12 my ( $pt, @keys ) = @_;
113 6         8 for my $key ( @keys ) {
114 14 100       35 return $key if exists $pt->{$key};
115             }
116 1         3 return;
117 2         8 };
118              
119 2         5 my $latk = $has->( $pt, @lat );
120 2         5 my $lonk = $has->( $pt, @lon );
121 2         5 my $elek = $has->( $pt, @ele );
122              
123 2 50 33     14 if ( defined $latk && defined $lonk ) {
124 2 100       26 return $pt->{$latk}, $pt->{$lonk},
125             defined $elek ? $pt->{$elek} : undef;
126             }
127             }
128              
129 0         0 croak "Don't know how to convert point";
130             }
131             else {
132 14 50 33     79 croak "Need lat, lon or lat, lon, alt"
133             if @_ < 2 || @_ > 3;
134 14         61 return my ( $lat, $lon, $alt ) = @_;
135             }
136             }
137              
138             sub _num {
139 47     47   72 my ( $class, $n ) = @_;
140 47         1660 ( my $rep = sprintf '%f', $n ) =~ s/\.0*$//;
141 47         365 return $rep;
142             }
143              
144             sub _format {
145 19     19   36 my ( $class, $lat, $lon, $alt ) = @_;
146 19 50       41 croak "Missing or undefined latitude" unless defined $lat;
147 19 50       34 croak "Missing or undefined longitude" unless defined $lon;
148 47         434 return join ',', map { $class->_num( $_ ) }
  57         228  
149 19         36 grep { defined } $lat, $lon, $alt;
150             }
151              
152             sub _path {
153 19     19   26 my $class = shift;
154 19         54 my ( $lat, $lon, $alt ) = $class->_location_of_pointy_thing( @_ );
155 19 50 33     135 croak "Latitude out of range" if $lat < -90 || $lat > 90;
156 19 50 33     93 croak "Longitude out of range" if $lon < -180 || $lon > 180;
157 19 50 33     96 $lon = 0 if $lat == -90 || $lon == 90;
158 19         50 return $class->_format( $lat, $lon, $alt );
159             }
160              
161             =head1 INTERFACE
162              
163             =head2 C<< new >>
164              
165             Create a new URI::geo. The arguments should be either
166              
167             =over
168              
169             =item * latitude, longitude and optionally altitude
170              
171             =item * a reference to an array containing lat, lon, alt
172              
173             =item * a reference to a hash with suitably named keys or
174              
175             =item * a reference to an object with suitably named accessors
176              
177             =back
178              
179             To maximise the likelyhood that you can pass in some object that
180             represents a geographical location and have URI::geo do the right thing
181             we try a number of different accessor names.
182              
183             If the object has a C method (eg L) we'll use that.
184             If there's a C method we call that. Otherwise we look for
185             accessors called C, C, C, C, C,
186             C, C, C or C and use them.
187              
188             Often if you have an object or hash reference that represents a point
189             you can pass it directly to C; so for example this will work:
190              
191             use URI::geo;
192             use Geo::Point;
193              
194             my $pt = Geo::Point->latlong( 48.208333, 16.372778 );
195             my $guri = URI::geo->new( $pt );
196              
197             As will this:
198              
199             my $guri = URI::geo->new( { lat => 55, lon => -1 } );
200              
201             and this:
202              
203             my $guri = URI::geo->new( 55, -1 );
204              
205             Note that you can also create a new C by passing a GeoURI to
206             C:
207              
208             use URI;
209              
210             my $guri = URI->new( 'geo:55,-1' );
211              
212             =cut
213              
214             sub new {
215 10     10 1 2367 my $self = shift;
216 10   33     49 my $class = ref $self || $self;
217 10         29 my $uri = uri_join 'geo', undef, $class->_path( @_ );
218 10         681 return bless \$uri, $class;
219             }
220              
221             sub _init {
222 8     8   2983 my ( $class, $uri, $scheme ) = @_;
223              
224 8         32 my $self = $class->SUPER::_init( $uri, $scheme );
225              
226             # Normalise at poles.
227 8         186 my $lat = $self->latitude;
228 7 100 66     42 $self->longitude( 0 ) if $lat == 90 || $lat == -90;
229 7         49 return $self;
230             }
231              
232             =head2 C
233              
234             Get or set the location of this geo URI.
235              
236             my ( $lat, $lon, $alt ) = $guri->location;
237             $guri->location( 55.3, -3.7, 120 );
238              
239             When setting the location it is possible to pass any of the argument
240             types that can be passed to C.
241              
242             =cut
243              
244             sub location {
245 56     56 1 67 my $self = shift;
246              
247 56         205 my ( $scheme, $auth, $path, $query, $frag ) = uri_split $$self;
248              
249 56 100       479 if ( @_ ) {
250 9         24 $path = $self->_path( @_ );
251 9         34 $$self = uri_join 'geo', $auth, $path, $query, $frag;
252             }
253              
254 56         230 return $self->_parse( $path );
255             }
256              
257             sub _patch {
258 44     44   92 my $self = shift;
259 44         50 my $idx = shift;
260              
261 44         105 my @part = $self->location;
262 43 100       108 if ( @_ ) {
263 8         16 $part[$idx] = shift;
264 8         20 $self->location( @part );
265             }
266 43         200 return $part[$idx];
267             }
268              
269             =head2 C
270              
271             Get or set the latitude of this geo URI.
272              
273             =head2 C
274              
275             Get or set the longitude of this geo URI.
276              
277             =head2 C
278              
279             Get or set the altitude of this geo URI. To delete the altitude set it
280             to C.
281              
282             =cut
283              
284 19     19 1 4864 sub latitude { shift->_patch( 0, @_ ) }
285 13     13 1 39 sub longitude { shift->_patch( 1, @_ ) }
286 12     12 1 505 sub altitude { shift->_patch( 2, @_ ) }
287              
288             1;
289              
290             __END__