File Coverage

blib/lib/CGI/Wiki/Plugin/Locator/Grid.pm
Criterion Covered Total %
statement 10 12 83.3
branch n/a
condition n/a
subroutine 4 4 100.0
pod n/a
total 14 16 87.5


line stmt bran cond sub pod time code
1             package CGI::Wiki::Plugin::Locator::Grid;
2              
3 3     3   3408 use strict;
  3         6  
  3         115  
4              
5 3     3   17 use vars qw( $VERSION @ISA );
  3         6  
  3         225  
6             $VERSION = '0.03';
7              
8 3     3   35 use Carp qw( croak );
  3         6  
  3         211  
9 3     3   4871 use CGI::Wiki::Plugin;
  0            
  0            
10              
11             @ISA = qw( CGI::Wiki::Plugin );
12              
13             =head1 NAME
14              
15             CGI::Wiki::Plugin::Locator::Grid - A CGI::Wiki plugin to manage co-ordinate data.
16              
17             =head1 DESCRIPTION
18              
19             Access to and calculations using co-ordinate metadata supplied to a
20             CGI::Wiki wiki when writing a node.
21              
22             B This is I access. If you want to write to a node's
23             metadata, you need to do it using the C method of
24             L.
25              
26             We assume that the points are located on a flat, square grid with unit
27             squares of side 1 metre.
28              
29             =head1 SYNOPSIS
30              
31             use CGI::Wiki;
32             use CGI::Wiki::Plugin::Locator::Grid;
33              
34             my $wiki = CGI::Wiki->new( ... );
35             my $locator = CGI::Wiki::Plugin::Locator::Grid->new;
36             $wiki->register_plugin( plugin => $locator );
37              
38             $wiki->write_node( "Jerusalem Tavern", "A good pub", $checksum,
39             { x => 531674, y => 181950 } ) or die "argh";
40              
41             # Just retrieve the co-ordinates.
42             my ( $x, $y ) = $locator->coordinates( node => "Jerusalem Tavern" );
43              
44             # Find the straight-line distance between two nodes, in metres.
45             my $distance = $locator->distance( from_node => "Jerusalem Tavern",
46             to_node => "Calthorpe Arms" );
47              
48             # Find all the things within 200 metres of a given place.
49             my @others = $locator->find_within_distance( node => "Albion",
50             metres => 200 );
51              
52             # Maybe our wiki calls the x and y co-ordinates something else.
53             my $locator = CGI::Wiki::Plugin::Locator::Grid->new(
54             x => "os_x",
55             y => "os_y",
56             );
57              
58             =head1 METHODS
59              
60             =over 4
61              
62             =item B
63              
64             # By default we assume that x and y co-ordinates are stored in
65             # metadata called "x" and "y".
66             my $locator = CGI::Wiki::Plugin::Locator::Grid->new;
67              
68             # But maybe our wiki calls the x and y co-ordinates something else.
69             my $locator = CGI::Wiki::Plugin::Locator::Grid->new(
70             x => "os_x",
71             y => "os_y",
72             );
73              
74             =cut
75              
76             sub new {
77             my $class = shift;
78             my $self = {};
79             bless $self, $class;
80             return $self->_init( @_ );
81             }
82              
83             sub _init {
84             my ($self, %args) = @_;
85             $self->{x} = $args{x} || "x";
86             $self->{y} = $args{y} || "y";
87             return $self;
88             }
89              
90             =item B
91              
92             my $x_field = $locator->x_field;
93              
94             An accessor, returns the name of the metadata field used to store the
95             x-coordinate.
96              
97             =cut
98              
99             sub x_field {
100             my $self = shift;
101             return $self->{x};
102             }
103              
104             =item B
105              
106             my $y_field = $locator->y_field;
107              
108             An accessor, returns the name of the metadata field used to store the
109             y-coordinate.
110              
111             =cut
112              
113             sub y_field {
114             my $self = shift;
115             return $self->{y};
116             }
117              
118             =item B
119              
120             my ($x, $y) = $locator->coordinates( node => "Jerusalem Tavern" );
121              
122             Returns the x and y co-ordinates stored as metadata last time the node
123             was written.
124              
125             =cut
126              
127             sub coordinates {
128             my ($self, %args) = @_;
129             my $store = $self->datastore;
130             # This is the slightly inefficient but neat and tidy way to do it -
131             # calling on as much existing stuff as possible.
132             my %node_data = $store->retrieve_node( $args{node} );
133             my %metadata = %{$node_data{metadata}};
134             return ($metadata{$self->{x}}[0], $metadata{$self->{y}}[0]);
135             }
136              
137             =item B
138              
139             # Find the straight-line distance between two nodes, in metres.
140             my $distance = $locator->distance( from_node => "Jerusalem Tavern",
141             to_node => "Calthorpe Arms" );
142              
143             # Or in kilometres, and between a node and a point.
144             my $distance = $locator->distance( from_x => 531467,
145             from_y => 183246,
146             to_node => "Duke of Cambridge",
147             unit => "kilometres" );
148              
149             Defaults to metres if C is not supplied or is not recognised.
150             Recognised units at the moment: C, C.
151              
152             Returns C if one of the endpoints does not exist, or does not
153             have both co-ordinates defined. The C specification of an
154             endpoint overrides the x/y co-ords if both specified (but don't do
155             that).
156              
157             B Works to the nearest metre. Well, actually, calls C and
158             rounds down, but if anyone cares about that they can send a patch.
159              
160             =cut
161              
162             sub distance {
163             my ($self, %args) = @_;
164              
165             $args{unit} ||= "metres";
166             my (@from, @to);
167              
168             if ( $args{from_node} ) {
169             @from = $self->coordinates( node => $args{from_node} );
170             } elsif ( $args{from_x} and $args{from_y} ) {
171             @from = @args{ qw( from_x from_y ) };
172             }
173              
174             if ( $args{to_node} ) {
175             @to = $self->coordinates( node => $args{to_node} );
176             } elsif ( $args{to_x} and $args{to_y} ) {
177             @to = @args{ qw( to_x to_y ) };
178             }
179              
180             return undef unless ( $from[0] and $from[1] and $to[0] and $to[1] );
181              
182             my $metres = int( sqrt( ($from[0] - $to[0])**2
183             + ($from[1] - $to[1])**2 ) + 0.5 );
184              
185             if ( $args{unit} eq "metres" ) {
186             return $metres;
187             } else {
188             return $metres/1000;
189             }
190             }
191              
192             =item B
193              
194             # Find all the things within 200 metres of a given place.
195             my @others = $locator->find_within_distance( node => "Albion",
196             metres => 200 );
197              
198             # Or within 200 metres of a given location.
199             my @things = $locator->find_within_distance( x => 530774,
200             y => 182260,
201             metres => 200 );
202              
203             Units currently understood: C, C. If both C
204             and C/C are supplied then C takes precedence. Croaks if
205             insufficient start point data supplied.
206              
207             =cut
208              
209             sub find_within_distance {
210             my ($self, %args) = @_;
211             my $store = $self->datastore;
212             my $dbh = eval { $store->dbh; }
213             or croak "find_within_distance is only implemented for database stores";
214             my $metres = $args{metres}
215             || ($args{kilometres} * 1000)
216             || croak "Please supply a distance";
217             my ($sx, $sy);
218             if ( $args{node} ) {
219             ($sx, $sy) = $self->coordinates( node => $args{node} );
220             } elsif ( $args{x} and $args{y} ) {
221             ($sx, $sy) = @args{ qw( x y ) };
222             } else {
223             croak "Insufficient start location data supplied";
224             }
225              
226             # Only consider nodes within the square containing the circle of
227             # radius $distance. The SELECT DISTINCT is needed because we might
228             # have multiple versions in the table.
229             my $sql = "SELECT DISTINCT x.node
230             FROM metadata AS x, metadata AS y
231             WHERE x.metadata_type = '$self->{x}'
232             AND y.metadata_type = '$self->{y}'
233             AND x.metadata_value >= " . ($sx - $metres)
234             . " AND x.metadata_value <= " . ($sx + $metres)
235             . " AND y.metadata_value >= " . ($sy - $metres)
236             . " AND y.metadata_value <= " . ($sy + $metres)
237             . " AND x.node = y.node";
238             $sql .= " AND x.node != " . $dbh->quote($args{node})
239             if $args{node};
240             # Postgres is a fussy bugger.
241             if ( ref $store eq "CGI::Wiki::Store::Pg" ) {
242             $sql =~ s/metadata_value/metadata_value::integer/gs;
243             }
244             # SQLite 3 is even fussier.
245             if ( ref $store eq "CGI::Wiki::Store::SQLite"
246             && $DBD::SQLite::VERSION >= "1.00" ) {
247             $sql =~ s/metadata_value/metadata_value+0/gs; # yuck
248             }
249             my $sth = $dbh->prepare($sql);
250             $sth->execute;
251             my @results;
252             while ( my ($result) = $sth->fetchrow_array ) {
253             my $dist = $self->distance( from_x => $sx,
254             from_y => $sy,
255             to_node => $result,
256             unit => "metres" );
257             if ( defined $dist && $dist <= $metres ) {
258             push @results, $result;
259             }
260             }
261             return @results;
262             }
263              
264             =head1 SEE ALSO
265              
266             =over 4
267              
268             =item * L
269              
270             =item * L - an application that uses this plugin.
271              
272             =back
273              
274             =head1 AUTHOR
275              
276             Kake Pugh (kake@earth.li).
277              
278             =head1 COPYRIGHT
279              
280             Copyright (C) 2004 Kake L Pugh. All Rights Reserved.
281              
282             This module is free software; you can redistribute it and/or modify it
283             under the same terms as Perl itself.
284              
285             =head1 CREDITS
286              
287             This module is based heavily on (and is the replacement for)
288             L.
289              
290             The following thanks are due to people who helped with
291             L: Nicholas Clark found a very silly
292             bug in a pre-release version, oops :) Stephen White got me thinking in
293             the right way to implement C. Marcel Gruenauer
294             helped me make C work properly with postgres.
295              
296             =cut
297              
298              
299             1;