File Coverage

blib/lib/Wiki/Toolkit/Plugin/Locator/Grid.pm
Criterion Covered Total %
statement 12 68 17.6
branch 0 26 0.0
condition 0 33 0.0
subroutine 4 11 36.3
pod 6 6 100.0
total 22 144 15.2


line stmt bran cond sub pod time code
1             package Wiki::Toolkit::Plugin::Locator::Grid;
2              
3 3     3   3042 use strict;
  3         8  
  3         144  
4              
5 3     3   18 use vars qw( $VERSION @ISA );
  3         6  
  3         253  
6             $VERSION = '0.05';
7              
8 3     3   58 use Carp qw( croak );
  3         6  
  3         243  
9 3     3   3534 use Wiki::Toolkit::Plugin;
  3         9728  
  3         3227  
10              
11             @ISA = qw( Wiki::Toolkit::Plugin );
12              
13             =head1 NAME
14              
15             Wiki::Toolkit::Plugin::Locator::Grid - A Wiki::Toolkit plugin to manage co-ordinate data.
16              
17             =head1 DESCRIPTION
18              
19             Access to and calculations using co-ordinate metadata supplied to a
20             Wiki::Toolkit 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 Wiki::Toolkit;
32             use Wiki::Toolkit::Plugin::Locator::Grid;
33              
34             my $wiki = Wiki::Toolkit->new( ... );
35             my $locator = Wiki::Toolkit::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 = Wiki::Toolkit::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 = Wiki::Toolkit::Plugin::Locator::Grid->new;
67              
68             # But maybe our wiki calls the x and y co-ordinates something else.
69             my $locator = Wiki::Toolkit::Plugin::Locator::Grid->new(
70             x => "os_x",
71             y => "os_y",
72             );
73              
74             =cut
75              
76             sub new {
77 0     0 1   my $class = shift;
78 0           my $self = {};
79 0           bless $self, $class;
80 0           return $self->_init( @_ );
81             }
82              
83             sub _init {
84 0     0     my ($self, %args) = @_;
85 0   0       $self->{x} = $args{x} || "x";
86 0   0       $self->{y} = $args{y} || "y";
87 0           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 0     0 1   my $self = shift;
101 0           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 0     0 1   my $self = shift;
115 0           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 0     0 1   my ($self, %args) = @_;
129 0           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 0           my %node_data = $store->retrieve_node( $args{node} );
133 0           my %metadata = %{$node_data{metadata}};
  0            
134 0           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 0     0 1   my ($self, %args) = @_;
164              
165 0   0       $args{unit} ||= "metres";
166 0           my (@from, @to);
167              
168 0 0 0       if ( $args{from_node} ) {
    0          
169 0           @from = $self->coordinates( node => $args{from_node} );
170             } elsif ( $args{from_x} and $args{from_y} ) {
171 0           @from = @args{ qw( from_x from_y ) };
172             }
173              
174 0 0 0       if ( $args{to_node} ) {
    0          
175 0           @to = $self->coordinates( node => $args{to_node} );
176             } elsif ( $args{to_x} and $args{to_y} ) {
177 0           @to = @args{ qw( to_x to_y ) };
178             }
179              
180 0 0 0       return undef unless ( $from[0] and $from[1] and $to[0] and $to[1] );
      0        
      0        
181              
182 0           my $metres = int( sqrt( ($from[0] - $to[0])**2
183             + ($from[1] - $to[1])**2 ) + 0.5 );
184              
185 0 0         if ( $args{unit} eq "metres" ) {
186 0           return $metres;
187             } else {
188 0           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 0     0 1   my ($self, %args) = @_;
211 0           my $store = $self->datastore;
212 0 0         my $dbh = eval { $store->dbh; }
  0            
213             or croak "find_within_distance is only implemented for database stores";
214 0   0       my $metres = $args{metres}
215             || ($args{kilometres} * 1000)
216             || croak "Please supply a distance";
217 0           my ($sx, $sy);
218 0 0 0       if ( $args{node} ) {
    0          
219 0           ($sx, $sy) = $self->coordinates( node => $args{node} );
220             } elsif ( $args{x} and $args{y} ) {
221 0           ($sx, $sy) = @args{ qw( x y ) };
222             } else {
223 0           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 0           my $sql = "SELECT DISTINCT x.name ".
230             "FROM node AS x ".
231             "INNER JOIN metadata AS mx ".
232             " ON (mx.node_id = x.id AND mx.version = x.version) ".
233             "INNER JOIN node AS y ".
234             " ON (x.id = y.id) ".
235             "INNER JOIN metadata my ".
236             " ON (my.node_id = y.id AND my.version = y.version) ".
237             " WHERE mx.metadata_type = '$self->{x}' ".
238             " AND my.metadata_type = '$self->{y}' ".
239             " AND mx.metadata_value >= " . ($sx - $metres) .
240             " AND mx.metadata_value <= " . ($sx + $metres) .
241             " AND my.metadata_value >= " . ($sy - $metres) .
242             " AND my.metadata_value <= " . ($sy + $metres);
243 0 0         $sql .= " AND x.name != " . $dbh->quote($args{node})
244             if $args{node};
245             # Postgres is a fussy bugger.
246 0 0         if ( ref $store eq "Wiki::Toolkit::Store::Pg" ) {
247 0           $sql =~ s/metadata_value/metadata_value::integer/gs;
248             }
249             # SQLite 3 is even fussier.
250 0 0 0       if ( ref $store eq "Wiki::Toolkit::Store::SQLite"
251             && $DBD::SQLite::VERSION >= "1.00" ) {
252 0           $sql =~ s/metadata_value/metadata_value+0/gs; # yuck
253             }
254 0           my $sth = $dbh->prepare($sql);
255 0           $sth->execute;
256 0           my @results;
257 0           while ( my ($result) = $sth->fetchrow_array ) {
258 0           my $dist = $self->distance( from_x => $sx,
259             from_y => $sy,
260             to_node => $result,
261             unit => "metres" );
262 0 0 0       if ( defined $dist && $dist <= $metres ) {
263 0           push @results, $result;
264             }
265             }
266 0           return @results;
267             }
268              
269             =head1 SEE ALSO
270              
271             =over 4
272              
273             =item * L
274              
275             =item * L - an application that uses this plugin.
276              
277             =back
278              
279             =head1 AUTHOR
280              
281             Kake Pugh (kake@earth.li).
282             The Wiki::Toolkit team (http://www.wiki-toolkit.org/)
283              
284             =head1 COPYRIGHT
285              
286             Copyright (C) 2004 Kake L Pugh. All Rights Reserved.
287             Copyright (C) 2006 the Wiki::Toolkit Team. All Rights Reserved.
288              
289             This module is free software; you can redistribute it and/or modify it
290             under the same terms as Perl itself.
291              
292             =head1 CREDITS
293              
294             This module is based heavily on (and is the replacement for)
295             L.
296              
297             The following thanks are due to people who helped with
298             L: Nicholas Clark found a very silly
299             bug in a pre-release version, oops :) Stephen White got me thinking in
300             the right way to implement C. Marcel Gruenauer
301             helped me make C work properly with postgres.
302              
303             =cut
304              
305              
306             1;