File Coverage

blib/lib/Geo/Coordinates/OSGB/Grid.pm
Criterion Covered Total %
statement 179 192 93.2
branch 75 96 78.1
condition 23 39 58.9
subroutine 26 26 100.0
pod 7 7 100.0
total 310 360 86.1


line stmt bran cond sub pod time code
1             package Geo::Coordinates::OSGB::Grid;
2              
3 9     9   48051 use Geo::Coordinates::OSGB::Maps qw{%maps %name_for_map_series};
  9         461  
  9         4360  
4 9     9   11562 use Geo::Coordinates::OSGB qw{get_ostn02_shift_pair};
  9         51  
  9         688  
5              
6 9     9   72 use base qw(Exporter);
  9         19  
  9         808  
7 9     9   60 use strict;
  9         24  
  9         241  
8 9     9   52 use warnings;
  9         21  
  9         289  
9 9     9   49 use Carp;
  9         25  
  9         592  
10 9     9   247 use 5.008; # At least Perl 5.08 please, be sure to change POD below if you update
  9         39  
11              
12             our $VERSION = '2.18';
13              
14             our %EXPORT_TAGS = (all => [qw(
15             parse_grid
16             format_grid
17              
18             parse_trad_grid
19             parse_GPS_grid
20             parse_landranger_grid
21             parse_map_grid
22              
23             format_grid_trad
24             format_grid_GPS
25             format_grid_landranger
26             format_grid_map
27              
28             random_grid
29             )]);
30              
31             our @EXPORT_OK = ( @{ $EXPORT_TAGS{all} } );
32              
33 9     9   63 use constant GRID_SQ_LETTERS => 'VWXYZQRSTULMNOPFGHJKABCDE';
  9         24  
  9         897  
34 9     9   88 use constant GRID_SIZE => sqrt length GRID_SQ_LETTERS;
  9         26  
  9         400  
35 9     9   55 use constant MINOR_GRID_SQ_SIZE => 100_000;
  9         22  
  9         442  
36 9     9   54 use constant MAJOR_GRID_SQ_SIZE => GRID_SIZE * MINOR_GRID_SQ_SIZE;
  9         24  
  9         481  
37 9     9   56 use constant MAJOR_GRID_SQ_EASTING_OFFSET => 2 * MAJOR_GRID_SQ_SIZE;
  9         21  
  9         455  
38 9     9   56 use constant MAJOR_GRID_SQ_NORTHING_OFFSET => 1 * MAJOR_GRID_SQ_SIZE;
  9         25  
  9         475  
39 9     9   53 use constant MAX_GRID_SIZE => MINOR_GRID_SQ_SIZE * length GRID_SQ_LETTERS;
  9         21  
  9         19103  
40              
41             # Produce a random GR
42             # A simple approach would pick 0 < E < 700000 and 0 < N < 1250000 but that way
43             # many GRs produced would be in the sea, so pick a random map, and then find a
44             # random GR within its bbox and finally check that the resulting pair is
45             # inside the OSTN02 boundary and actually on some map.
46              
47             sub random_grid {
48 2     2 1 1619 my @preferred_sheets = @_;
49             # assume plain sheet numbers are Landranger
50 2         6 for my $s (@preferred_sheets) {
51 8 100       38 if ($s =~ m{\A\d+\Z}xsmio) {
52 7         19 $s =~ s/\A/A:/xsmio
53             }
54             }
55 2         4 my @sheets;
56 2 50       8 if (@preferred_sheets) {
57 2         6 @sheets = grep { exists $maps{$_} } @preferred_sheets;
  8         22  
58             }
59             else {
60 0         0 @sheets = keys %maps;
61             }
62 2         6 my $margin = 5000;
63 2         8 my ($map, $lle, $lln, $ure, $urn);
64 2         0 my ($easting, $northing);
65 2         3 while (1) {
66 2         10 $map = $maps{$sheets[int rand @sheets]};
67 2         5 ($lle, $lln) = @{$map->{bbox}->[0]};
  2         8  
68 2         5 ($ure, $urn) = @{$map->{bbox}->[1]};
  2         5  
69 2         27 $easting = sprintf '%.3f', $lle + rand ($ure-$lle);
70 2         12 $northing = sprintf '%.3f', $lln + rand ($urn-$lln);
71             # check we are well inside OSTN02 and actually on the chosen map
72             last if get_ostn02_shift_pair($easting, $northing)
73             && get_ostn02_shift_pair($easting, $northing+$margin)
74             && get_ostn02_shift_pair($easting, $northing-$margin)
75             && get_ostn02_shift_pair($easting+$margin, $northing)
76             && get_ostn02_shift_pair($easting-$margin, $northing)
77 2 50 33     17 && 0 != _winding_number($easting, $northing, $map->{polygon});
      33        
      33        
      33        
      33        
78             }
79 2         12 return ($easting, $northing);
80             }
81              
82             sub format_grid {
83 226     226 1 43575 my ($easting, $northing, $options) = @_;
84              
85 226 100       775 my $form = exists $options->{form} ? uc $options->{form} : 'SS EEE NNN';
86 226 100       565 my $with_maps = exists $options->{maps} ? $options->{maps} : 0;
87 226 100       1400 my $map_keys = exists $options->{series} ? uc $options->{series} : join '', sort keys %name_for_map_series;
88              
89 226         815 my $sq = _grid_to_sq($easting,$northing);
90 226 50       665 if ( !$sq ) {
91 0         0 croak "Too far off the grid: $easting $northing\n";
92             }
93              
94 226         577 my ($e,$n) = map { int } map { $_ % MINOR_GRID_SQ_SIZE } ($easting, $northing);
  452         1004  
  452         1046  
95              
96 226         509 my @sheets = ();
97 226 100       546 if ( $with_maps ) {
98 59         246 while (my ($k,$m) = each %maps) {
99 66847 100       165119 next if index($map_keys, substr($k,0,1)) == -1;
100 61142 100 100     346667 if ($m->{bbox}->[0][0] <= $easting && $easting < $m->{bbox}->[1][0]
      100        
      100        
101             && $m->{bbox}->[0][1] <= $northing && $northing < $m->{bbox}->[1][1]) {
102 209         776 my $w = _winding_number($easting, $northing, $m->{polygon});
103 209 100       566 if ($w != 0) {
104 200         901 push @sheets, $k;
105             }
106             }
107             }
108 59         366 @sheets = sort @sheets;
109             }
110              
111             # special cases
112 226 100       939 if ( $form eq 'TRAD' ) {
    100          
    100          
113 2         4 $form = 'SS EEE NNN';
114             }
115             elsif ( $form eq 'GPS' ) {
116 48         154 $form = 'SS EEEEE NNNNN';
117             }
118             elsif ( $form eq 'SS' ) {
119 1         5 return $sq;
120             }
121              
122 225 50       2389 if ( my ($space_a, $e_spec, $space_b, $n_spec) = $form =~ m{ \A S{1,2}(\s*)(E{1,5})(\s*)(N{1,5}) \Z }iosxm ) {
123 225         494 my $e_len = length $e_spec;
124 225         371 my $n_len = length $n_spec;
125 225         665 $e = int($e / 10**(5 - $e_len));
126 225         432 $n = int($n / 10**(5 - $n_len));
127              
128 225 100       587 if ( wantarray ) {
129 49         571 return ($sq, $e, $n, @sheets)
130             }
131              
132 176         715 my $gr = sprintf '%s%s%0.*d%s%0.*d', $sq, $space_a, $e_len, $e, $space_b, $n_len, $n;
133 176         993 $gr =~ s/\s+/ /g;
134              
135 176 100       477 if ( $with_maps ) {
136 12 50       40 if ( @sheets ) {
137 12         168 return sprintf '%s on %s', $gr, join ', ', @sheets;
138             }
139             else {
140 0         0 return sprintf '%s is not on any maps in series %s', $gr, $map_keys;
141             }
142             }
143             else {
144 164         835 return $gr;
145             }
146             }
147              
148 0         0 croak "Format $form was not recognized";
149             }
150              
151             sub format_grid_trad {
152 7     7 1 425 my ($easting, $northing) = @_;
153 7         29 return format_grid($easting, $northing, { form => 'SS EEE NNN' })
154             }
155              
156             sub format_grid_GPS {
157 10     10 1 417 my ($easting, $northing) = @_;
158 10         37 return format_grid($easting, $northing, { form => 'SS EEEEE NNNNN' })
159             }
160              
161             sub format_grid_map {
162 12     12 1 40 my ($easting, $northing, $options) = @_;
163 12 100       34 if ( defined $options ) {
164 2         7 $options->{maps} = 1
165             }
166             else {
167 10         33 $options = { maps => 1 }
168             }
169 12         42 return format_grid($easting, $northing, $options)
170             }
171              
172             sub format_grid_landranger {
173 4     4 1 326 my ($easting,$northing) = @_;
174              
175 4         25 my ($sq, $e, $n, @sheets) = format_grid($easting, $northing, { form => 'SS EEE NNN', maps => 1, series => 'A' });
176              
177 4         17 for my $s (@sheets) {
178 6         30 $s =~ s/\AA://xsm;
179             }
180              
181 4 100       33 return ($sq, $e, $n, @sheets) if wantarray;
182              
183 1 50       5 if (!@sheets ) { return sprintf '%s %03d %03d is not on any Landranger sheet', $sq, $e, $n }
  0         0  
184 1 50       4 if ( @sheets==1 ) { return sprintf '%s %03d %03d on Landranger sheet %s' , $sq, $e, $n, $sheets[0] }
  1         11  
185 0 0       0 if ( @sheets==2 ) { return sprintf '%s %03d %03d on Landranger sheets %s and %s', $sq, $e, $n, @sheets }
  0         0  
186              
187 0         0 my $phrase = join ', ', @sheets[0..($#sheets-1)], "and $sheets[-1]";
188 0         0 return sprintf '%s %03d %03d on Landranger sheets %s', $sq, $e, $n, $phrase;
189              
190             }
191              
192             sub _grid_to_sq {
193 226     226   502 my ($e, $n) = @_;
194              
195 226         499 $e += MAJOR_GRID_SQ_EASTING_OFFSET;
196 226         407 $n += MAJOR_GRID_SQ_NORTHING_OFFSET;
197 226 50 33     1865 return if !(0 <= $e && $e < MAX_GRID_SIZE && 0 <= $n && $n < MAX_GRID_SIZE);
      33        
      33        
198              
199 226         704 my $major_index = int $e / MAJOR_GRID_SQ_SIZE + GRID_SIZE * int $n / MAJOR_GRID_SQ_SIZE;
200 226         423 $e = $e % MAJOR_GRID_SQ_SIZE;
201 226         381 $n = $n % MAJOR_GRID_SQ_SIZE;
202 226         487 my $minor_index = int $e / MINOR_GRID_SQ_SIZE + GRID_SIZE * int $n / MINOR_GRID_SQ_SIZE;
203             return
204 226         778 substr(GRID_SQ_LETTERS, $major_index, 1) .
205             substr(GRID_SQ_LETTERS, $minor_index, 1);
206             }
207              
208             sub _get_grid_square_offsets {
209 117     117   244 my $s = shift;
210 117 50       399 return if length $s < 2;
211              
212 117         437 my $a = index GRID_SQ_LETTERS, uc substr $s, 0, 1;
213 117 100       365 return if 0 > $a;
214              
215 109         277 my $b = index GRID_SQ_LETTERS, uc substr $s, 1, 1;
216 109 100       420 return if 0 > $b;
217              
218 55         171 my ($X, $Y) = ($a % GRID_SIZE, int $a / GRID_SIZE);
219 55         125 my ($x, $y) = ($b % GRID_SIZE, int $b / GRID_SIZE);
220              
221             return (
222 55         239 MAJOR_GRID_SQ_SIZE * $X - MAJOR_GRID_SQ_EASTING_OFFSET + MINOR_GRID_SQ_SIZE * $x,
223             MAJOR_GRID_SQ_SIZE * $Y - MAJOR_GRID_SQ_NORTHING_OFFSET + MINOR_GRID_SQ_SIZE * $y
224             );
225             }
226              
227             sub _get_eastnorthings {
228 112     112   279 my $s = shift;
229 112         223 my $numbers = $s;
230 112         304 $numbers =~ tr/0-9//cd; # avoid using "r" here as it requires perl >= 5.14
231 112         253 my $len = length $numbers;
232 112 50       394 croak 'No easting or northing found' if $len == 0;
233 112 50       339 croak "Easting and northing have different lengths in $s" if $len % 2;
234 112 50       306 croak "Too many digits in $s" if $len > 10;
235              
236             # this trick lets us pad with zeros on the right
237 112         638 my $e = reverse sprintf '%05d', scalar reverse substr $numbers, 0, $len/2;
238 112         535 my $n = reverse sprintf '%05d', scalar reverse substr $numbers, $len/2;
239 224         375 return ($e, $n)
240             }
241              
242             sub parse_grid {
243              
244 117 100   117 1 6862 my $options = 'HASH' eq ref $_[-1] ? pop @_ : { };
245              
246 117 100       379 my $figs = exists $options->{figs} ? $options->{figs} : 3;
247              
248 117         221 my @out;
249              
250 117 100       776 my $s = @_ < 3 ? "@_" : sprintf '%s %0.*d %0.*d', $_[0], $figs, $_[1], $figs, $_[2];
251              
252             # normal case : TQ 123 456 etc
253 117 100       399 if ( my ($E, $N) = _get_grid_square_offsets($s) ) {
254 55 100       219 my ($e, $n) = length $s > 2 ? _get_eastnorthings(substr $s, 2) : (0,0);
255 55         219 @out = ($E+$e, $N+$n);
256 55 100       487 return wantarray ? @out : "@out";
257             }
258              
259             # sheet id instead of grid sq
260 62         421 my $sheet_ref_pattern = qr{\A([A-Z]:)?([0-9NEWSOL/]+?)(\.[a-z]+)?(?:[ -/.]([ 0-9]+))?\Z};
261 62         679 my ($prefix, $sheet, $suffix, $numbers) = $s =~ m/$sheet_ref_pattern/sioxm;
262              
263 62 50       219 if (defined $sheet) {
264              
265 62 100       191 if ( ! defined $prefix ) {
266 8         20 $prefix = 'A:';
267             }
268 62 100       198 if ( ! defined $suffix ) {
269 61         114 $suffix = q{};
270             }
271 62         156 $sheet = $prefix . $sheet . $suffix;
272              
273 62 50       238 if (exists $maps{$sheet}) {
274 62         127 my ($E, $N) = @{$maps{$sheet}->{bbox}[0]}; # NB we need the bbox corner so that it is left and below all points on the map
  62         227  
275              
276 62 100       201 if (defined $numbers) {
277 59         201 my ($e, $n) = _get_eastnorthings($numbers);
278 59         191 $E = $E + ($e-$E) % MINOR_GRID_SQ_SIZE;
279 59         140 $N = $N + ($n-$N) % MINOR_GRID_SQ_SIZE;
280              
281 59         218 my $w = _winding_number($E, $N, $maps{$sheet}->{polygon});
282 59 50       209 if ($w == 0) {
283 0         0 croak sprintf 'Grid reference %s = (%d, %d) is not on sheet %s', scalar format_grid($E,$N), $e, $n, $sheet;
284             }
285             }
286 62 100       532 return wantarray ? ($E, $N) : "$E $N";
287             }
288             }
289              
290             # just a pair of numbers
291 0 0       0 if ( @out = $s =~ m{\A (\d+(?:\.\d+)?) \s+ (\d+(?:\.\d+)?) \Z}xsmio ) { # eee nnn
292 0 0       0 return wantarray ? @out : "@out";
293             }
294              
295 0         0 croak "Failed to parse a grid reference from $s";
296             }
297              
298             *parse_trad_grid = \&parse_grid;
299             *parse_GPS_grid = \&parse_grid;
300             *parse_landranger_grid = \&parse_grid;
301             *parse_map_grid = \&parse_grid;
302              
303             # is $pt left of $a--$b?
304             sub _is_left {
305 540     540   1055 my ($x, $y, $a, $b) = @_;
306 540         2662 return ( ($b->[0] - $a->[0]) * ($y - $a->[1]) - ($x - $a->[0]) * ($b->[1] - $a->[1]) );
307             }
308              
309             # adapted from http://geomalgorithms.com/a03-_inclusion.html
310             sub _winding_number {
311 270     270   602 my ($x, $y, $poly) = @_;
312 270         471 my $w = 0;
313 270         820 for (my $i=0; $i < $#$poly; $i++ ) {
314 4194 100       8802 if ( $poly->[$i][1] <= $y ) {
315 1838 100 100     6662 if ($poly->[$i+1][1] > $y && _is_left($x, $y, $poly->[$i], $poly->[$i+1]) > 0 ) {
316 267         715 $w++;
317             }
318             }
319             else {
320 2356 100 100     8282 if ($poly->[$i+1][1] <= $y && _is_left($x, $y, $poly->[$i], $poly->[$i+1]) < 0 ) {
321 6         18 $w--;
322             }
323             }
324             }
325 270         605 return $w;
326             }
327              
328             1;
329              
330             __END__