| File: | blib/lib/Geo/Location/Point.pm |
| Coverage: | 86.6% |
| line | stmt | bran | cond | sub | time | code |
|---|---|---|---|---|---|---|
| 1 | package Geo::Location::Point; | |||||
| 2 | ||||||
| 3 | 10 10 | 665928 18 | use 5.10.0; # For the //= operator | |||
| 4 | 10 10 10 | 19 9 84 | use strict; | |||
| 5 | 10 10 10 | 17 6 186 | use warnings; | |||
| 6 | ||||||
| 7 | 10 10 10 | 18 7 240 | use Carp; | |||
| 8 | 10 10 10 | 1877 12511 188 | use GIS::Distance; | |||
| 9 | 10 10 10 | 1977 167039 219 | use Params::Get 0.13; | |||
| 10 | 10 10 10 | 30 7 170 | use Readonly; | |||
| 11 | 10 10 10 | 24 11 327 | use Scalar::Util; | |||
| 12 | ||||||
| 13 | use overload ( | |||||
| 14 | '==' => \&equal, | |||||
| 15 | '!=' => \¬_equal, | |||||
| 16 | '""' => \&as_string, | |||||
| 17 | 1 | 95 | bool => sub { 1 }, | |||
| 18 | 10 | 9081 | fallback => 1 # So that boolean tests don't cause as_string to be called | |||
| 19 | 10 10 | 18 7 | ); | |||
| 20 | ||||||
| 21 - 29 | =head1 NAME Geo::Location::Point - Location information =head1 VERSION Version 0.19 =cut | |||||
| 30 | ||||||
| 31 | our $VERSION = '0.19'; | |||||
| 32 | ||||||
| 33 - 53 | =head1 SYNOPSIS
Geo::Location::Point encapsulates geographical point data with latitude and longitude.
It supports distance calculations,
comparison between points,
and provides various convenience methods for attributes like latitude, longitude, and related string representations.
use Geo::Location::Point;
my $location = Geo::Location::Point->new(latitude => 0.01, longitude => -71);
=head1 SUBROUTINES/METHODS
=head2 new
Initialise a new object, accepting latitude and longitude via a hash or hash reference.
Takes one optional argument 'key' which is an API key for L<https://timezonedb.com> for looking up timezone data.
$location = Geo::Location::Point->new({ latitude => 0.01, longitude => -71 });
=cut | |||||
| 54 | ||||||
| 55 | sub new { | |||||
| 56 | 41 | 625418 | my $class = shift; | |||
| 57 | 41 | 94 | my $params = Params::Get::get_params(undef, \@_); | |||
| 58 | ||||||
| 59 | 40 | 689 | if(!defined($class)) { | |||
| 60 | 1 | 8 | carp(__PACKAGE__, ' use ->new() not ::new() to instantiate'); | |||
| 61 | 1 | 113 | return; | |||
| 62 | } elsif(Scalar::Util::blessed($class)) { | |||||
| 63 | # If $class is an object, clone it with new arguments | |||||
| 64 | 1 1 1 | 1 1 4 | return bless { %{$class}, %{$params} }, ref($class); | |||
| 65 | } | |||||
| 66 | ||||||
| 67 | 38 | 115 | $params->{'lat'} //= $params->{'latitude'} // $params->{'Latitude'}; | |||
| 68 | 38 | 40 | if(!defined($params->{'lat'})) { | |||
| 69 | 2 | 8 | Carp::carp(__PACKAGE__, ': latitude not given'); | |||
| 70 | 2 | 394 | return; | |||
| 71 | } | |||||
| 72 | 36 | 57 | if(abs($params->{'lat'}) > 90) { | |||
| 73 | 3 | 20 | Carp::carp(__PACKAGE__, ': ', $params->{'lat'}, ': invalid latitude'); | |||
| 74 | 3 | 459 | return; | |||
| 75 | } | |||||
| 76 | ||||||
| 77 | 33 | 90 | $params->{'long'} //= $params->{'longitude'} // $params->{'Longitude'}; | |||
| 78 | 33 | 39 | if(!defined($params->{'long'})) { | |||
| 79 | 2 | 8 | Carp::carp(__PACKAGE__, ': longitude not given'); | |||
| 80 | 2 | 220 | return; | |||
| 81 | } | |||||
| 82 | 31 | 43 | if(abs($params->{'long'}) > 180) { | |||
| 83 | 3 | 16 | Carp::carp(__PACKAGE__, ': ', $params->{'long'}, ': invalid longitude'); | |||
| 84 | 3 | 348 | return; | |||
| 85 | } | |||||
| 86 | 28 | 38 | $params->{'lng'} = $params->{'long'}; | |||
| 87 | ||||||
| 88 | # Return the blessed object | |||||
| 89 | 28 | 52 | return bless $params, $class; | |||
| 90 | } | |||||
| 91 | ||||||
| 92 - 97 | =head2 lat print 'Latitude: ', $location->lat(), "\n"; print 'Longitude: ', $location->long(), "\n"; =cut | |||||
| 98 | ||||||
| 99 | sub lat { | |||||
| 100 | 22 | 180 | my $self = shift; | |||
| 101 | ||||||
| 102 | 22 | 44 | return $self->{'lat'}; | |||
| 103 | } | |||||
| 104 | ||||||
| 105 - 109 | =head2 latitude Synonym for lat(). =cut | |||||
| 110 | ||||||
| 111 | sub latitude { | |||||
| 112 | 1 | 3 | my $self = shift; | |||
| 113 | ||||||
| 114 | 1 | 3 | return $self->{'lat'}; | |||
| 115 | } | |||||
| 116 | ||||||
| 117 - 122 | =head2 long print 'Latitude: ', $location->lat(), "\n"; print 'Longitude: ', $location->long(), "\n"; =cut | |||||
| 123 | ||||||
| 124 | sub long { | |||||
| 125 | 14 | 18 | my $self = shift; | |||
| 126 | ||||||
| 127 | 14 | 28 | return $self->{'long'}; | |||
| 128 | } | |||||
| 129 | ||||||
| 130 - 134 | =head2 lng Synonym for long(). =cut | |||||
| 135 | ||||||
| 136 | sub lng { | |||||
| 137 | 0 | 0 | my $self = shift; | |||
| 138 | ||||||
| 139 | 0 | 0 | return $self->{'long'}; | |||
| 140 | } | |||||
| 141 | ||||||
| 142 - 146 | =head2 longitude Synonym for long(). =cut | |||||
| 147 | ||||||
| 148 | sub longitude { | |||||
| 149 | 1 | 1 | my $self = shift; | |||
| 150 | ||||||
| 151 | 1 | 2 | return $self->{'long'}; | |||
| 152 | } | |||||
| 153 | ||||||
| 154 - 159 | =head2 distance Determine the distance between two geographical points, returns a L<Class::Measure::Length> object. =cut | |||||
| 160 | ||||||
| 161 | sub distance { | |||||
| 162 | 5 | 969 | my ($self, $location) = @_; | |||
| 163 | ||||||
| 164 | 5 | 9 | if(!defined($location)) { | |||
| 165 | 1 | 2 | Carp::carp('Usage: ', __PACKAGE__, '->distance($location)'); | |||
| 166 | 1 | 142 | return; | |||
| 167 | } | |||||
| 168 | ||||||
| 169 | 4 | 32 | $self->{'gis'} //= GIS::Distance->new(); | |||
| 170 | ||||||
| 171 | 4 | 46577 | return $self->{'gis'}->distance($self->{'lat'}, $self->{'long'}, $location->lat(), $location->long()); | |||
| 172 | } | |||||
| 173 | ||||||
| 174 - 182 | =head2 equal Check if two points are identical within a small tolerance. my $loc1 = Geo::Location::Point->new(lat => 2, long => 2); my $loc2 = Geo::Location::Point->new(lat => 2, long => 2); print ($loc1 == $loc2), "\n"; # Prints 1 =cut | |||||
| 183 | ||||||
| 184 | sub equal { | |||||
| 185 | 8 | 165 | my $self = shift; | |||
| 186 | 8 | 6 | my $other = shift; | |||
| 187 | ||||||
| 188 | # return ($self->distance($other) <= 1e-5); | |||||
| 189 | # 1e-5 is about 1m, that's tolerant enough | |||||
| 190 | 8 | 9 | return((abs($self->lat() - $other->lat()) <= 1e-5) && (abs(($self->long() - $other->long())) <= 1e-5)); | |||
| 191 | } | |||||
| 192 | ||||||
| 193 - 201 | =head2 not_equal Are two points different? my $loc1 = Geo::Location::Point->new(lat => 2, long => 2); my $loc2 = Geo::Location::Point->new(lat => 2, long => 2); print ($loc1 != $loc2), "\n"; # Prints 0 =cut | |||||
| 202 | ||||||
| 203 | sub not_equal { | |||||
| 204 | 4 | 4 | my $self = shift; | |||
| 205 | ||||||
| 206 | 4 | 5 | return(!$self->equal(shift)); | |||
| 207 | } | |||||
| 208 | ||||||
| 209 - 214 | =head2 tz Returns the timezone of the location. Needs L<TimeZone::TimeZoneDB>. =cut | |||||
| 215 | ||||||
| 216 | sub tz { | |||||
| 217 | 0 | 0 | my $self = shift; | |||
| 218 | ||||||
| 219 | 0 | 0 | if(defined($self->{'key'})) { | |||
| 220 | 0 | 0 | return $self->{'tz'} if(defined($self->{'tz'})); | |||
| 221 | ||||||
| 222 | 0 | 0 | if(!defined($self->{'timezonedb'})) { | |||
| 223 | 0 | 0 | unless(TimeZone::TimeZoneDB->can('get_time_zone')) { | |||
| 224 | 0 | 0 | require TimeZone::TimeZoneDB; | |||
| 225 | 0 | 0 | TimeZone::TimeZoneDB->import(); | |||
| 226 | } | |||||
| 227 | ||||||
| 228 | 0 | 0 | $self->{'timezonedb'} = TimeZone::TimeZoneDB->new(key => $self->{'key'}); | |||
| 229 | } | |||||
| 230 | 0 | 0 | $self->{'tz'} = $self->{'timezonedb'}->get_time_zone($self)->{'zoneName'}; | |||
| 231 | ||||||
| 232 | 0 | 0 | return $self->{'tz'}; | |||
| 233 | } | |||||
| 234 | } | |||||
| 235 | ||||||
| 236 - 240 | =head2 timezone Synonym for tz(). =cut | |||||
| 241 | ||||||
| 242 | sub timezone { | |||||
| 243 | 0 | 0 | my $self = shift; | |||
| 244 | ||||||
| 245 | 0 | 0 | return $self->tz(); | |||
| 246 | } | |||||
| 247 | ||||||
| 248 - 253 | =head2 as_string Generate a human-readable string describing the point, incorporating additional attributes like city or country if available. =cut | |||||
| 254 | ||||||
| 255 | sub as_string { | |||||
| 256 | 8 | 582 | my $self = shift; | |||
| 257 | ||||||
| 258 | 8 | 22 | if($self->{'location'}) { | |||
| 259 | 1 | 2 | return $self->{'location'}; | |||
| 260 | } | |||||
| 261 | ||||||
| 262 | 7 | 8 | my $rc = $self->{'name'}; | |||
| 263 | 7 | 10 | if($rc) { | |||
| 264 | 0 | 0 | $rc = ucfirst(lc($rc)); | |||
| 265 | } | |||||
| 266 | ||||||
| 267 | # TODO: make this order configurable | |||||
| 268 | # foreach my $field('house_number', 'number', 'road', 'street', 'AccentCity', 'city', 'county', 'region', 'state_district', 'state', 'country') { | |||||
| 269 | 7 | 11 | foreach my $field('house_number', 'number', 'road', 'street', 'city', 'county', 'region', 'state_district', 'state', 'country') { | |||
| 270 | 70 | 107 | if(my $value = ($self->{$field} || $self->{ucfirst($field)})) { | |||
| 271 | 18 | 13 | if($rc) { | |||
| 272 | 12 | 18 | if(($field eq 'street') || ($field eq 'road')) { | |||
| 273 | 2 | 10 | if($self->{'number'} || $self->{'house_number'}) { | |||
| 274 | 2 | 2 | $rc .= ' '; | |||
| 275 | } else { | |||||
| 276 | 0 | 0 | $rc .= ', ' | |||
| 277 | } | |||||
| 278 | } else { | |||||
| 279 | 10 | 9 | $rc .= ', '; | |||
| 280 | } | |||||
| 281 | } | |||||
| 282 | 18 | 12 | my $leave_case = 0; | |||
| 283 | 18 | 24 | if(my $country = $self->{'country'} // $self->{'Country'}) { | |||
| 284 | 17 | 20 | if(uc($country) eq 'US') { | |||
| 285 | 14 | 23 | if(($field eq 'state') || ($field eq 'region') || ($field eq 'country')) { | |||
| 286 | 6 | 5 | $leave_case = 1; | |||
| 287 | 6 | 7 | if(lc($field) eq 'country') { | |||
| 288 | 4 | 10 | $value = 'US'; | |||
| 289 | } | |||||
| 290 | } | |||||
| 291 | } elsif(($country eq 'Canada') || ($country eq 'Australia')) { | |||||
| 292 | 0 | 0 | if($field eq 'state') { | |||
| 293 | 0 | 0 | $leave_case = 1; | |||
| 294 | } | |||||
| 295 | } elsif(uc($country) eq 'GB') { | |||||
| 296 | 3 | 4 | if($field eq 'country') { | |||
| 297 | 1 | 1 | $leave_case = 1; | |||
| 298 | 1 | 1 | $value = 'GB'; | |||
| 299 | } | |||||
| 300 | } | |||||
| 301 | } | |||||
| 302 | 18 | 16 | if($leave_case) { | |||
| 303 | 7 | 9 | $rc .= $value; | |||
| 304 | } else { | |||||
| 305 | 11 | 13 | $rc .= $self->_sortoutcase($value); | |||
| 306 | 11 | 28 | if((($field eq 'street') || ($field eq 'road')) && | |||
| 307 | ($rc =~ /(.+)\s([NS][ew])$/)) { | |||||
| 308 | # e.g South Street NW | |||||
| 309 | 2 | 6 | $rc = "$1 " . uc($2); | |||
| 310 | } | |||||
| 311 | } | |||||
| 312 | } | |||||
| 313 | } | |||||
| 314 | ||||||
| 315 | 7 | 19 | return $self->{'location'} = $rc; | |||
| 316 | } | |||||
| 317 | ||||||
| 318 | sub _sortoutcase | |||||
| 319 | { | |||||
| 320 | # Use lc to ensure the input string is in lowercase before capitalisation, | |||||
| 321 | # split to break the string into words, | |||||
| 322 | # map to capitalise each word and | |||||
| 323 | # join to concatenate the capitalised words back into a single string with spaces | |||||
| 324 | 11 16 | 18 25 | return join ' ', map { ucfirst } split ' ', lc($_[1]); | |||
| 325 | } | |||||
| 326 | ||||||
| 327 - 333 | =head2 as_uri Convert the point to a Geo URI scheme string (geo:latitude,longitude). See L<https://en.wikipedia.org/wiki/Geo_URI_scheme>. Arguably it should return a L<URI> object instead. =cut | |||||
| 334 | ||||||
| 335 | sub as_uri | |||||
| 336 | { | |||||
| 337 | 2 | 241 | my $self = shift; | |||
| 338 | ||||||
| 339 | 2 | 20 | return 'geo:' . $self->{'lat'} . ',' . $self->{'long'}; | |||
| 340 | } | |||||
| 341 | ||||||
| 342 - 364 | =head2 attr
Get or set arbitrary attributes, such as city or country.
A call with an argument sets the attribute and returns the value that was set;
a call without one reads it back and returns C<undef> if it was never set.
$location->city('London');
$location->country('UK');
print $location->as_string(), "\n";
print "$location\n"; # Calls as_string
It is the argument list, not the value, that decides whether a call is a read or a write,
so false values are attributes like any other and are stored and returned unchanged:
$location->confidence(0);
print 'matched: ', defined($location->confidence()) && $location->confidence(), "\n";
The upper case variant of the key is consulted
(but not overwritten) when the key as given has never been set,
which is how the MaxMind field names are read. An attribute that has been set,
however false, takes precedence over it.
=cut | |||||
| 365 | ||||||
| 366 | sub AUTOLOAD { | |||||
| 367 | 36 | 4199 | our $AUTOLOAD; | |||
| 368 | 36 | 35 | my $key = $AUTOLOAD; | |||
| 369 | ||||||
| 370 | 36 | 106 | $key =~ s/.*:://; | |||
| 371 | ||||||
| 372 | 36 | 323 | return if($key eq 'DESTROY'); | |||
| 373 | ||||||
| 374 | 7 | 6 | my $self = shift; | |||
| 375 | ||||||
| 376 | # The argument list, not the value, decides read or write. Testing the value here | |||||
| 377 | # (my $value = shift) meant a false value could neither be stored nor read back, | |||||
| 378 | # so e.g. a confidence of 0 looked like no data at all. | |||||
| 379 | 7 | 8 | if(@_) { | |||
| 380 | 1 | 1 | my $value = shift; | |||
| 381 | 1 | 1 | delete $self->{'location'}; # Invalidate the cache | |||
| 382 | 1 | 1 | delete $self->{'tz'}; # Invalidate the cache | |||
| 383 | 1 | 6 | return $self->{$key} = $value; | |||
| 384 | } | |||||
| 385 | ||||||
| 386 | # Defined, not true: a stored 0 or empty string is the caller's data, so return it | |||||
| 387 | 6 | 16 | return $self->{$key} if(defined($self->{$key})); | |||
| 388 | ||||||
| 389 | 1 | 3 | return $self->{ucfirst($key)}; | |||
| 390 | } | |||||
| 391 | ||||||
| 392 | # The accuracy levels a point may claim, most precise first. This is the one | |||||
| 393 | # place the vocabulary is written down: Geo::Coder::Free::Provenance reads it | |||||
| 394 | # from here instead of keeping a copy of its own, so a level added to one of | |||||
| 395 | # them cannot go missing from the other. It is defined here because | |||||
| 396 | # Geo::Coder::Free depends on this module, and not the other way round. | |||||
| 397 | # | |||||
| 398 | # It is read-only because it is shared. %IS_ACCURACY_LEVEL below is taken from | |||||
| 399 | # this list when the module loads, so a caller that pushed a level into the list | |||||
| 400 | # would publish a vocabulary that accuracy() still refuses: the module would | |||||
| 401 | # describe one set of levels and accept another, and every module that reads the | |||||
| 402 | # list - Provenance's name for it included - would be speaking the same language | |||||
| 403 | # as the list and not as the points. A level that belongs in the vocabulary is | |||||
| 404 | # added here, beside the validator it is checked against. | |||||
| 405 | Readonly::Array my @ACCURACY_LEVELS => qw(address street postcode locality city region country); | |||||
| 406 | ||||||
| 407 | # The list itself, rather than a copy of it: every module that speaks this | |||||
| 408 | # vocabulary shares this one list, so the levels it reports are the levels a point | |||||
| 409 | # accepts. | |||||
| 410 | sub ACCURACY_LEVELS { | |||||
| 411 | 3 | 35 | return \@ACCURACY_LEVELS; | |||
| 412 | } | |||||
| 413 | ||||||
| 414 | my %IS_ACCURACY_LEVEL = map { $_ => 1 } @ACCURACY_LEVELS; | |||||
| 415 | ||||||
| 416 | # The fields a backend can match on, and the level each one justifies. Keyed by | |||||
| 417 | # level, because accuracy_for() walks ACCURACY_LEVELS in order: the most precise | |||||
| 418 | # level with a matched field wins, and a level that is not in the vocabulary | |||||
| 419 | # cannot come out of here. 'city' has no entry of its own - a city match is a | |||||
| 420 | # locality, the most precise claim the centre of a city supports. | |||||
| 421 | # | |||||
| 422 | # This is the other half of the vocabulary, in the same place: the levels a | |||||
| 423 | # point accepts and the rule that turns a backend's matched fields into one of | |||||
| 424 | # them are defined together, so a rule cannot name a level accuracy() refuses, | |||||
| 425 | # and a backend does not have to keep two modules in step to describe a match. | |||||
| 426 | Readonly::Hash my %FIELDS_FOR_ACCURACY => ( | |||||
| 427 | address => [qw(house_number number)], | |||||
| 428 | street => [qw(road street)], | |||||
| 429 | postcode => [qw(postcode post_code zip)], | |||||
| 430 | locality => [qw(city town name)], | |||||
| 431 | region => [qw(state state_district county region)], | |||||
| 432 | country => [qw(country)], | |||||
| 433 | ); | |||||
| 434 | ||||||
| 435 | sub accuracy_for { | |||||
| 436 | 35 37 | 4315 79 | my %field = map { lc($_) => 1 } @_; | |||
| 437 | ||||||
| 438 | 35 | 41 | for my $level (@ACCURACY_LEVELS) { | |||
| 439 | 135 | 650 | next unless(my $fields = $FIELDS_FOR_ACCURACY{$level}); | |||
| 440 | ||||||
| 441 | 123 123 | 555 110 | for my $field (@{$fields}) { | |||
| 442 | 277 | 883 | return $level if($field{$field}); | |||
| 443 | } | |||||
| 444 | } | |||||
| 445 | ||||||
| 446 | # A match on a row with none of these fields is still a named place, and | |||||
| 447 | # every result promises an accuracy, so it is reported as a locality rather | |||||
| 448 | # than left unsaid. | |||||
| 449 | 2 | 12 | return 'locality'; | |||
| 450 | } | |||||
| 451 | ||||||
| 452 | # Read a slot the way AUTOLOAD reads it: a lower case key that was never set | |||||
| 453 | # falls back to its upper case spelling, so a field the caller stored under the | |||||
| 454 | # upper case name is still found. | |||||
| 455 | sub _attribute { | |||||
| 456 | 20 | 16 | my ($self, $key) = @_; | |||
| 457 | ||||||
| 458 | 20 | 57 | return $self->{$key} if(defined($self->{$key})); | |||
| 459 | ||||||
| 460 | 5 | 13 | return $self->{ucfirst($key)}; | |||
| 461 | } | |||||
| 462 | ||||||
| 463 | # Write a slot the way AUTOLOAD writes it, invalidating the caches derived from | |||||
| 464 | # the attributes: a validated accessor has to keep that promise too, or setting | |||||
| 465 | # a city after reading as_string() would be invisible. | |||||
| 466 | sub _set_attribute { | |||||
| 467 | 28 | 25 | my ($self, $key, $value) = @_; | |||
| 468 | ||||||
| 469 | 28 | 24 | delete $self->{'location'}; | |||
| 470 | 28 | 16 | delete $self->{'tz'}; | |||
| 471 | ||||||
| 472 | 28 | 50 | return $self->{$key} = $value; | |||
| 473 | } | |||||
| 474 | ||||||
| 475 | # Passing undef clears an attribute, so every validated accessor accepts it; | |||||
| 476 | # anything else has to be the type the field promises. | |||||
| 477 | sub _check_string { | |||||
| 478 | 12 | 12 | my ($field, $value) = @_; | |||
| 479 | ||||||
| 480 | 12 | 13 | return if(!defined($value)); | |||
| 481 | 12 | 16 | Carp::croak(__PACKAGE__, ": $field must be a string, not a ", ref($value)) if(ref($value)); | |||
| 482 | ||||||
| 483 | 11 | 14 | return $value; | |||
| 484 | } | |||||
| 485 | ||||||
| 486 | sub _check_confidence { | |||||
| 487 | 9 | 9 | my ($value) = @_; | |||
| 488 | ||||||
| 489 | 9 | 8 | return if(!defined($value)); | |||
| 490 | 8 | 50 | Carp::croak(__PACKAGE__, ': confidence must be a number from 0 to 1, not ', | |||
| 491 | ref($value) ? ref($value) : $value) | |||||
| 492 | if(ref($value) || !Scalar::Util::looks_like_number($value) || ($value < 0) || ($value > 1)); | |||||
| 493 | ||||||
| 494 | 5 | 7 | return $value; | |||
| 495 | } | |||||
| 496 | ||||||
| 497 | sub _check_accuracy { | |||||
| 498 | 13 | 9 | my ($value) = @_; | |||
| 499 | ||||||
| 500 | 13 | 12 | return if(!defined($value)); | |||
| 501 | Carp::croak(__PACKAGE__, ': unknown accuracy: ', ref($value) ? ref($value) : $value) | |||||
| 502 | 12 | 29 | if(ref($value) || !$IS_ACCURACY_LEVEL{$value}); | |||
| 503 | ||||||
| 504 | 10 | 9 | return $value; | |||
| 505 | } | |||||
| 506 | ||||||
| 507 - 513 | =head2 geocoder
The distribution that produced the point, such as C<Geo::Coder::Free>.
$location->geocoder('Geo::Coder::Free');
=cut | |||||
| 514 | ||||||
| 515 | sub geocoder { | |||||
| 516 | 2 | 152 | my $self = shift; | |||
| 517 | ||||||
| 518 | 2 | 5 | return $self->_attribute('geocoder') unless(@_); | |||
| 519 | ||||||
| 520 | 1 | 2 | return $self->_set_attribute('geocoder', _check_string('geocoder', shift)); | |||
| 521 | } | |||||
| 522 | ||||||
| 523 - 530 | =head2 database
The database the distribution answered from, such as C<OpenAddresses>, C<Local>
or C<MaxMind>.
$location->database('OpenAddresses');
=cut | |||||
| 531 | ||||||
| 532 | sub database { | |||||
| 533 | 1 | 1 | my $self = shift; | |||
| 534 | ||||||
| 535 | 1 | 2 | return $self->_attribute('database') unless(@_); | |||
| 536 | ||||||
| 537 | 1 | 7 | return $self->_set_attribute('database', _check_string('database', shift)); | |||
| 538 | } | |||||
| 539 | ||||||
| 540 - 549 | =head2 confidence How good the match is, from C<0> to C<1> inclusive, where C<1> is an exact match. A confidence that is not a number, or that falls outside that range, is rejected rather than stored. $location->confidence(0); # Tried and rejected $location->confidence(0.8); # A good match =cut | |||||
| 550 | ||||||
| 551 | sub confidence { | |||||
| 552 | 18 | 1718 | my $self = shift; | |||
| 553 | ||||||
| 554 | 18 | 24 | return $self->_attribute('confidence') unless(@_); | |||
| 555 | ||||||
| 556 | 9 | 11 | return $self->_set_attribute('confidence', _check_confidence(shift)); | |||
| 557 | } | |||||
| 558 | ||||||
| 559 - 580 | =head2 ACCURACY_LEVELS The accuracy levels a point may claim, most precise first: C<address> (a house number), C<street>, C<postcode>, C<locality>, C<city>, C<region> and C<country>. This is the vocabulary, written down once here and read from here by L<Geo::Coder::Free::Provenance> rather than copied, so the levels a point accepts and the levels a backend reports cannot drift apart. C<accuracy()> accepts these and refuses anything else. The rule that turns the fields a backend matched into one of these levels is L</accuracy_for>, beside the list it returns a level from. The list is read-only, and a caller that tries to add to it is refused, because it is the list C<accuracy()> checks against: were it writable, a level pushed into it would be published to every reader of this list while the module went on refusing it. my $levels = Geo::Location::Point::ACCURACY_LEVELS(); =cut | |||||
| 581 | ||||||
| 582 - 601 | =head2 accuracy_for The accuracy a match can claim, from the fields that made it. The most precise field present decides: a house number makes it an C<address>, a road a C<street>, a postcode a C<postcode>, a name or a city a C<locality>, a state or region a C<region>, and a country a C<country>. So a dataset row carrying only a city, state and country is a C<locality>: a named place, not an address. my $level = Geo::Location::Point::accuracy_for(qw(house_number road city)); Always returns one of the L</ACCURACY_LEVELS>: a match on a row with none of the fields above is still a named place, so it is reported as a C<locality> rather than left unsaid, because every result promises an accuracy. It is a function, not a method - the fields are what a backend matched, not something a point carries - and it lives beside the vocabulary it returns a level from, so the levels a point accepts and the rules that choose one of them have a single home. =cut | |||||
| 602 | ||||||
| 603 - 612 | =head2 accuracy
How precise the coordinates behind a match can claim to be. The levels are
those in L</ACCURACY_LEVELS>; anything else is rejected, because a reader
deciding what a point may claim about itself has to be able to trust the
value.
$location->accuracy('city');
=cut | |||||
| 613 | ||||||
| 614 | sub accuracy { | |||||
| 615 | 17 | 942 | my $self = shift; | |||
| 616 | ||||||
| 617 | 17 | 22 | return $self->_attribute('accuracy') unless(@_); | |||
| 618 | ||||||
| 619 | 13 | 12 | return $self->_set_attribute('accuracy', _check_accuracy(shift)); | |||
| 620 | } | |||||
| 621 | ||||||
| 622 - 630 | =head2 country
The country of the point, as the data spells it: C<GB>, C<US>, C<Canada>. It
is stored as given rather than normalised, so it is whatever the backend
matched.
$location->country('GB');
=cut | |||||
| 631 | ||||||
| 632 | sub country { | |||||
| 633 | 6 | 184 | my $self = shift; | |||
| 634 | ||||||
| 635 | 6 | 11 | return $self->_attribute('country') unless(@_); | |||
| 636 | ||||||
| 637 | 2 | 6 | return $self->_set_attribute('country', _check_string('country', shift)); | |||
| 638 | } | |||||
| 639 | ||||||
| 640 - 646 | =head2 city
The city, town or similar locality of the point.
$location->city('London');
=cut | |||||
| 647 | ||||||
| 648 | sub city { | |||||
| 649 | 5 | 345 | my $self = shift; | |||
| 650 | ||||||
| 651 | 5 | 7 | return $self->_attribute('city') unless(@_); | |||
| 652 | ||||||
| 653 | 4 | 6 | return $self->_set_attribute('city', _check_string('city', shift)); | |||
| 654 | } | |||||
| 655 | ||||||
| 656 - 662 | =head2 state
The state, province or region of the point.
$location->state('KY');
=cut | |||||
| 663 | ||||||
| 664 | sub state { | |||||
| 665 | 5 | 5 | my $self = shift; | |||
| 666 | ||||||
| 667 | 5 | 9 | return $self->_attribute('state') unless(@_); | |||
| 668 | ||||||
| 669 | 4 | 6 | return $self->_set_attribute('state', _check_string('state', shift)); | |||
| 670 | } | |||||
| 671 | ||||||
| 672 - 711 | =head1 SUPPORT This module is provided as-is without any warranty. =head1 AUTHOR Nigel Horne, C<< <njh at nigelhorne.com> >> =head1 BUGS There is no validation on the attribute in the AUTOLOAD method, so typos such as "citty" will not be caught. C<geocoder>, C<database>, C<confidence>, C<accuracy>, C<country>, C<city> and C<state> have documented accessors of their own that do check what they are given, but they are the only attributes that do, and C<new()> stores whatever it is handed without checking it, so a provenance field set through the constructor can still be any value. =head1 SEE ALSO =over 4 =item * L<Test Dashboard|https://nigelhorne.github.io/Geo-Location-Point/coverage/> =back L<GIS::Distance>, L<Geo::Point>, L<TimeZone::TimeZoneDB>. =head1 LICENSE AND COPYRIGHT Copyright 2019-2026 Nigel Horne. Usage is subject to the GPL2 licence terms. If you use it, please let me know. =cut | |||||
| 712 | ||||||
| 713 | 1; | |||||