TER1 (Statement): 90.06%
TER2 (Branch): 81.25%
TER3 (LCSAJ): 91.7% (11/12)
Approximate LCSAJ segments: 97
โ Covered โ this LCSAJ path was executed during testing.
โ Not covered โ this LCSAJ path was never executed. These are the paths to focus on.
Multiple dots on a line indicate that multiple control-flow paths begin at that line. Hovering over any dot shows:
start โ end โ jump
Uncovered paths show [NOT COVERED] in the tooltip.
1: package Geo::Location::Point; 2: 3: use 5.10.0; # For the //= operator 4: use strict; 5: use warnings; 6: 7: use Carp; 8: use GIS::Distance; 9: use Params::Get 0.13; 10: use Readonly; 11: use Scalar::Util; 12: 13: use overload ( 14: '==' => \&equal, 15: '!=' => \¬_equal, 16: '""' => \&as_string, 17: bool => sub { 1 }, 18: fallback => 1 # So that boolean tests don't cause as_string to be called 19: ); 20: 21: =head1 NAME 22: 23: Geo::Location::Point - Location information 24: 25: =head1 VERSION 26: 27: Version 0.19 28: 29: =cut 30: 31: our $VERSION = '0.19'; 32: 33: =head1 SYNOPSIS 34: 35: Geo::Location::Point encapsulates geographical point data with latitude and longitude. 36: It supports distance calculations, 37: comparison between points, 38: and provides various convenience methods for attributes like latitude, longitude, and related string representations. 39: 40: use Geo::Location::Point; 41: 42: my $location = Geo::Location::Point->new(latitude => 0.01, longitude => -71); 43: 44: =head1 SUBROUTINES/METHODS 45: 46: =head2 new 47: 48: Initialise a new object, accepting latitude and longitude via a hash or hash reference. 49: Takes one optional argument 'key' which is an API key for L<https://timezonedb.com> for looking up timezone data. 50: 51: $location = Geo::Location::Point->new({ latitude => 0.01, longitude => -71 }); 52: 53: =cut 54: 55: sub new { โ56 โ 59 โ 67 56: my $class = shift; 57: my $params = Params::Get::get_params(undef, \@_); 58: 59: if(!defined($class)) {Mutants (Total: 1, Killed: 1, Survived: 0)
60: carp(__PACKAGE__, ' use ->new() not ::new() to instantiate'); 61: return; 62: } elsif(Scalar::Util::blessed($class)) { 63: # If $class is an object, clone it with new arguments 64: return bless { %{$class}, %{$params} }, ref($class);
Mutants (Total: 2, Killed: 2, Survived: 0)
65: } 66: โ67 โ 68 โ 72 67: $params->{'lat'} //= $params->{'latitude'} // $params->{'Latitude'}; 68: if(!defined($params->{'lat'})) {
Mutants (Total: 1, Killed: 1, Survived: 0)
69: Carp::carp(__PACKAGE__, ': latitude not given'); 70: return; 71: } โ72 โ 72 โ 77 72: if(abs($params->{'lat'}) > 90) {
Mutants (Total: 4, Killed: 4, Survived: 0)
73: Carp::carp(__PACKAGE__, ': ', $params->{'lat'}, ': invalid latitude'); 74: return; 75: } 76: โ77 โ 78 โ 82 77: $params->{'long'} //= $params->{'longitude'} // $params->{'Longitude'}; 78: if(!defined($params->{'long'})) {
Mutants (Total: 1, Killed: 1, Survived: 0)
79: Carp::carp(__PACKAGE__, ': longitude not given'); 80: return; 81: } โ82 โ 82 โ 86 82: if(abs($params->{'long'}) > 180) {
Mutants (Total: 4, Killed: 4, Survived: 0)
83: Carp::carp(__PACKAGE__, ': ', $params->{'long'}, ': invalid longitude'); 84: return; 85: } 86: $params->{'lng'} = $params->{'long'}; 87: 88: # Return the blessed object 89: return bless $params, $class;
Mutants (Total: 2, Killed: 2, Survived: 0)
90: } 91: 92: =head2 lat 93: 94: print 'Latitude: ', $location->lat(), "\n"; 95: print 'Longitude: ', $location->long(), "\n"; 96: 97: =cut 98: 99: sub lat { 100: my $self = shift; 101: 102: return $self->{'lat'};
Mutants (Total: 2, Killed: 2, Survived: 0)
103: } 104: 105: =head2 latitude 106: 107: Synonym for lat(). 108: 109: =cut 110: 111: sub latitude { 112: my $self = shift; 113: 114: return $self->{'lat'};
Mutants (Total: 2, Killed: 2, Survived: 0)
115: } 116: 117: =head2 long 118: 119: print 'Latitude: ', $location->lat(), "\n"; 120: print 'Longitude: ', $location->long(), "\n"; 121: 122: =cut 123: 124: sub long { 125: my $self = shift; 126: 127: return $self->{'long'};
Mutants (Total: 2, Killed: 2, Survived: 0)
128: } 129: 130: =head2 lng 131: 132: Synonym for long(). 133: 134: =cut 135: 136: sub lng { 137: my $self = shift; 138: 139: return $self->{'long'};
140: } 141: 142: =head2 longitude 143: 144: Synonym for long(). 145: 146: =cut 147: 148: sub longitude { 149: my $self = shift; 150: 151: return $self->{'long'};Mutants (Total: 2, Killed: 0, Survived: 2)
- BOOL_NEGATE_139_2: Negate boolean return expression
MEDIUM: Add tests asserting both true and false outcomes๐งช Suggested Test# Boolean branch test suggestion ok( !func(INPUT), 'Verify boolean branch behaviour' );- RETURN_UNDEF_139_2: Replace return expression with undef
LOW: Mutation survived, but impact may be minor๐งช Suggested Test# Return value assertion is( func(INPUT), EXPECTED, 'Verify correct return value' );Mutants (Total: 2, Killed: 2, Survived: 0)
152: } 153: 154: =head2 distance 155: 156: Determine the distance between two geographical points, 157: returns a L<Class::Measure::Length> object. 158: 159: =cut 160: 161: sub distance { โ162 โ 164 โ 169 162: my ($self, $location) = @_; 163: 164: if(!defined($location)) {
Mutants (Total: 1, Killed: 1, Survived: 0)
165: Carp::carp('Usage: ', __PACKAGE__, '->distance($location)'); 166: return; 167: } 168: 169: $self->{'gis'} //= GIS::Distance->new(); 170: 171: return $self->{'gis'}->distance($self->{'lat'}, $self->{'long'}, $location->lat(), $location->long());
Mutants (Total: 2, Killed: 2, Survived: 0)
172: } 173: 174: =head2 equal 175: 176: Check if two points are identical within a small tolerance. 177: 178: my $loc1 = Geo::Location::Point->new(lat => 2, long => 2); 179: my $loc2 = Geo::Location::Point->new(lat => 2, long => 2); 180: print ($loc1 == $loc2), "\n"; # Prints 1 181: 182: =cut 183: 184: sub equal { 185: my $self = shift; 186: my $other = shift; 187: 188: # return ($self->distance($other) <= 1e-5); 189: # 1e-5 is about 1m, that's tolerant enough 190: return((abs($self->lat() - $other->lat()) <= 1e-5) && (abs(($self->long() - $other->long())) <= 1e-5));
191: } 192: 193: =head2 not_equal 194: 195: Are two points different? 196: 197: my $loc1 = Geo::Location::Point->new(lat => 2, long => 2); 198: my $loc2 = Geo::Location::Point->new(lat => 2, long => 2); 199: print ($loc1 != $loc2), "\n"; # Prints 0 200: 201: =cut 202: 203: sub not_equal { 204: my $self = shift; 205: 206: return(!$self->equal(shift)); 207: } 208: 209: =head2 tz 210: 211: Returns the timezone of the location. 212: Needs L<TimeZone::TimeZoneDB>. 213: 214: =cut 215: 216: sub tz { โ[NOT COVERED] 217 โ 219 โ 0 217: my $self = shift; 218: 219: if(defined($self->{'key'})) {Mutants (Total: 3, Killed: 0, Survived: 3)
- NUM_BOUNDARY_190_44_<: Numeric boundary flip <= to <
HIGH: Likely missing edge-case test (boundary value)๐งช Suggested Test# Boundary test suggestion is( func(VALUE_AT_BOUNDARY), EXPECTED, 'Test boundary behaviour' );- NUM_BOUNDARY_190_44_>: Numeric boundary flip <= to >
HIGH: Likely missing edge-case test (boundary value)๐งช Suggested Test# Boundary test suggestion is( func(VALUE_AT_BOUNDARY), EXPECTED, 'Test boundary behaviour' );- NUM_BOUNDARY_190_44_>=: Numeric boundary flip <= to >=
HIGH: Likely missing edge-case test (boundary value)๐งช Suggested Test# Boundary test suggestion is( func(VALUE_AT_BOUNDARY), EXPECTED, 'Test boundary behaviour' );220: return $self->{'tz'} if(defined($self->{'tz'}));Mutants (Total: 1, Killed: 0, Survived: 1)
- COND_INV_219_2: Invert condition if to unless
MEDIUM: Add tests asserting both true and false outcomes221: 222: if(!defined($self->{'timezonedb'})) {Mutants (Total: 2, Killed: 0, Survived: 2)
- BOOL_NEGATE_220_3: Negate boolean return expression
MEDIUM: Add tests asserting both true and false outcomes๐งช Suggested Test# Boolean branch test suggestion ok( !func(INPUT), 'Verify boolean branch behaviour' );- RETURN_UNDEF_220_3: Replace return expression with undef
LOW: Mutation survived, but impact may be minor๐งช Suggested Test# Return value assertion is( func(INPUT), EXPECTED, 'Verify correct return value' );223: unless(TimeZone::TimeZoneDB->can('get_time_zone')) {Mutants (Total: 1, Killed: 0, Survived: 1)
- COND_INV_222_3: Invert condition if to unless
MEDIUM: Add tests asserting both true and false outcomes224: require TimeZone::TimeZoneDB; 225: TimeZone::TimeZoneDB->import(); 226: } 227: 228: $self->{'timezonedb'} = TimeZone::TimeZoneDB->new(key => $self->{'key'}); 229: } 230: $self->{'tz'} = $self->{'timezonedb'}->get_time_zone($self)->{'zoneName'}; 231: 232: return $self->{'tz'};Mutants (Total: 1, Killed: 0, Survived: 1)
- COND_INV_223_4: Invert condition unless to if
MEDIUM: Add tests asserting both true and false outcomes233: } 234: } 235: 236: =head2 timezone 237: 238: Synonym for tz(). 239: 240: =cut 241: 242: sub timezone { 243: my $self = shift; 244: 245: return $self->tz();Mutants (Total: 2, Killed: 0, Survived: 2)
- BOOL_NEGATE_232_3: Negate boolean return expression
MEDIUM: Add tests asserting both true and false outcomes๐งช Suggested Test# Boolean branch test suggestion ok( !func(INPUT), 'Verify boolean branch behaviour' );- RETURN_UNDEF_232_3: Replace return expression with undef
LOW: Mutation survived, but impact may be minor๐งช Suggested Test# Return value assertion is( func(INPUT), EXPECTED, 'Verify correct return value' );246: } 247: 248: =head2 as_string 249: 250: Generate a human-readable string describing the point, 251: incorporating additional attributes like city or country if available. 252: 253: =cut 254: 255: sub as_string { โ256 โ 258 โ 262 256: my $self = shift; 257: 258: if($self->{'location'}) {Mutants (Total: 2, Killed: 0, Survived: 2)
- BOOL_NEGATE_245_2: Negate boolean return expression
MEDIUM: Add tests asserting both true and false outcomes๐งช Suggested Test# Boolean branch test suggestion ok( !func(INPUT), 'Verify boolean branch behaviour' );- RETURN_UNDEF_245_2: Replace return expression with undef
LOW: Mutation survived, but impact may be minor๐งช Suggested Test# Return value assertion is( func(INPUT), EXPECTED, 'Verify correct return value' );Mutants (Total: 1, Killed: 1, Survived: 0)
259: return $self->{'location'};
Mutants (Total: 2, Killed: 2, Survived: 0)
260: } 261: โ262 โ 263 โ 269 262: my $rc = $self->{'name'}; 263: if($rc) {
Mutants (Total: 1, Killed: 1, Survived: 0)
264: $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 โ 269 โ 315 269: foreach my $field('house_number', 'number', 'road', 'street', 'city', 'county', 'region', 'state_district', 'state', 'country') { 270: if(my $value = ($self->{$field} || $self->{ucfirst($field)})) {
Mutants (Total: 1, Killed: 1, Survived: 0)
271: if($rc) {
Mutants (Total: 1, Killed: 1, Survived: 0)
272: if(($field eq 'street') || ($field eq 'road')) {
Mutants (Total: 1, Killed: 1, Survived: 0)
273: if($self->{'number'} || $self->{'house_number'}) {
Mutants (Total: 1, Killed: 1, Survived: 0)
274: $rc .= ' '; 275: } else { 276: $rc .= ', ' 277: } 278: } else { 279: $rc .= ', '; 280: } 281: } 282: my $leave_case = 0; 283: if(my $country = $self->{'country'} // $self->{'Country'}) {
Mutants (Total: 1, Killed: 1, Survived: 0)
284: if(uc($country) eq 'US') {
Mutants (Total: 1, Killed: 1, Survived: 0)
285: if(($field eq 'state') || ($field eq 'region') || ($field eq 'country')) {
Mutants (Total: 1, Killed: 1, Survived: 0)
286: $leave_case = 1; 287: if(lc($field) eq 'country') {
Mutants (Total: 1, Killed: 1, Survived: 0)
288: $value = 'US'; 289: } 290: } 291: } elsif(($country eq 'Canada') || ($country eq 'Australia')) { 292: if($field eq 'state') {
293: $leave_case = 1; 294: } 295: } elsif(uc($country) eq 'GB') { 296: if($field eq 'country') {Mutants (Total: 1, Killed: 0, Survived: 1)
- COND_INV_292_6: Invert condition if to unless
MEDIUM: Add tests asserting both true and false outcomesMutants (Total: 1, Killed: 1, Survived: 0)
297: $leave_case = 1; 298: $value = 'GB'; 299: } 300: } 301: } 302: if($leave_case) {
Mutants (Total: 1, Killed: 1, Survived: 0)
303: $rc .= $value; 304: } else { 305: $rc .= $self->_sortoutcase($value); 306: if((($field eq 'street') || ($field eq 'road')) &&
Mutants (Total: 1, Killed: 1, Survived: 0)
307: ($rc =~ /(.+)\s([NS][ew])$/)) { 308: # e.g South Street NW 309: $rc = "$1 " . uc($2); 310: } 311: } 312: } 313: } 314: 315: return $self->{'location'} = $rc;
Mutants (Total: 2, Killed: 2, Survived: 0)
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: return join ' ', map { ucfirst } split ' ', lc($_[1]);
Mutants (Total: 2, Killed: 2, Survived: 0)
325: } 326: 327: =head2 as_uri 328: 329: Convert the point to a Geo URI scheme string (geo:latitude,longitude). 330: See L<https://en.wikipedia.org/wiki/Geo_URI_scheme>. 331: Arguably it should return a L<URI> object instead. 332: 333: =cut 334: 335: sub as_uri 336: { 337: my $self = shift; 338: 339: return 'geo:' . $self->{'lat'} . ',' . $self->{'long'};
Mutants (Total: 2, Killed: 2, Survived: 0)
340: } 341: 342: =head2 attr 343: 344: Get or set arbitrary attributes, such as city or country. 345: A call with an argument sets the attribute and returns the value that was set; 346: a call without one reads it back and returns C<undef> if it was never set. 347: 348: $location->city('London'); 349: $location->country('UK'); 350: print $location->as_string(), "\n"; 351: print "$location\n"; # Calls as_string 352: 353: It is the argument list, not the value, that decides whether a call is a read or a write, 354: so false values are attributes like any other and are stored and returned unchanged: 355: 356: $location->confidence(0); 357: print 'matched: ', defined($location->confidence()) && $location->confidence(), "\n"; 358: 359: The upper case variant of the key is consulted 360: (but not overwritten) when the key as given has never been set, 361: which is how the MaxMind field names are read. An attribute that has been set, 362: however false, takes precedence over it. 363: 364: =cut 365: 366: sub AUTOLOAD { โ367 โ 379 โ 387 367: our $AUTOLOAD; 368: my $key = $AUTOLOAD; 369: 370: $key =~ s/.*:://; 371: 372: return if($key eq 'DESTROY'); 373: 374: 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: if(@_) {
Mutants (Total: 1, Killed: 1, Survived: 0)
380: my $value = shift; 381: delete $self->{'location'}; # Invalidate the cache 382: delete $self->{'tz'}; # Invalidate the cache 383: return $self->{$key} = $value;
Mutants (Total: 2, Killed: 2, Survived: 0)
384: } 385: 386: # Defined, not true: a stored 0 or empty string is the caller's data, so return it 387: return $self->{$key} if(defined($self->{$key}));
Mutants (Total: 2, Killed: 2, Survived: 0)
388: 389: return $self->{ucfirst($key)};
Mutants (Total: 2, Killed: 2, Survived: 0)
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: return \@ACCURACY_LEVELS;
Mutants (Total: 2, Killed: 2, Survived: 0)
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 โ 438 โ 449 436: my %field = map { lc($_) => 1 } @_; 437: 438: for my $level (@ACCURACY_LEVELS) { 439: next unless(my $fields = $FIELDS_FOR_ACCURACY{$level}); 440: 441: for my $field (@{$fields}) { 442: return $level if($field{$field});
Mutants (Total: 2, Killed: 2, Survived: 0)
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: return 'locality';
Mutants (Total: 2, Killed: 2, Survived: 0)
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: my ($self, $key) = @_; 457: 458: return $self->{$key} if(defined($self->{$key}));
Mutants (Total: 2, Killed: 2, Survived: 0)
459: 460: return $self->{ucfirst($key)};
Mutants (Total: 2, Killed: 2, Survived: 0)
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: my ($self, $key, $value) = @_; 468: 469: delete $self->{'location'}; 470: delete $self->{'tz'}; 471: 472: return $self->{$key} = $value;
Mutants (Total: 2, Killed: 2, Survived: 0)
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: my ($field, $value) = @_; 479: 480: return if(!defined($value)); 481: Carp::croak(__PACKAGE__, ": $field must be a string, not a ", ref($value)) if(ref($value)); 482: 483: return $value;
Mutants (Total: 2, Killed: 2, Survived: 0)
484: } 485: 486: sub _check_confidence { 487: my ($value) = @_; 488: 489: return if(!defined($value)); 490: 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));
Mutants (Total: 6, Killed: 6, Survived: 0)
493: 494: return $value;
Mutants (Total: 2, Killed: 2, Survived: 0)
495: } 496: 497: sub _check_accuracy { 498: my ($value) = @_; 499: 500: return if(!defined($value)); 501: Carp::croak(__PACKAGE__, ': unknown accuracy: ', ref($value) ? ref($value) : $value) 502: if(ref($value) || !$IS_ACCURACY_LEVEL{$value}); 503: 504: return $value;
Mutants (Total: 2, Killed: 2, Survived: 0)
505: } 506: 507: =head2 geocoder 508: 509: The distribution that produced the point, such as C<Geo::Coder::Free>. 510: 511: $location->geocoder('Geo::Coder::Free'); 512: 513: =cut 514: 515: sub geocoder { 516: my $self = shift; 517: 518: return $self->_attribute('geocoder') unless(@_);
Mutants (Total: 2, Killed: 2, Survived: 0)
519: 520: return $self->_set_attribute('geocoder', _check_string('geocoder', shift));
Mutants (Total: 2, Killed: 2, Survived: 0)
521: } 522: 523: =head2 database 524: 525: The database the distribution answered from, such as C<OpenAddresses>, C<Local> 526: or C<MaxMind>. 527: 528: $location->database('OpenAddresses'); 529: 530: =cut 531: 532: sub database { 533: my $self = shift; 534: 535: return $self->_attribute('database') unless(@_);
536: 537: return $self->_set_attribute('database', _check_string('database', shift));Mutants (Total: 2, Killed: 0, Survived: 2)
- BOOL_NEGATE_535_2: Negate boolean return expression
MEDIUM: Add tests asserting both true and false outcomes๐งช Suggested Test# Boolean branch test suggestion ok( !func(INPUT), 'Verify boolean branch behaviour' );- RETURN_UNDEF_535_2: Replace return expression with undef
LOW: Mutation survived, but impact may be minor๐งช Suggested Test# Return value assertion is( func(INPUT), EXPECTED, 'Verify correct return value' );Mutants (Total: 2, Killed: 2, Survived: 0)
538: } 539: 540: =head2 confidence 541: 542: How good the match is, from C<0> to C<1> inclusive, where C<1> is an exact 543: match. A confidence that is not a number, or that falls outside that range, is 544: rejected rather than stored. 545: 546: $location->confidence(0); # Tried and rejected 547: $location->confidence(0.8); # A good match 548: 549: =cut 550: 551: sub confidence { 552: my $self = shift; 553: 554: return $self->_attribute('confidence') unless(@_);
Mutants (Total: 2, Killed: 2, Survived: 0)
555: 556: return $self->_set_attribute('confidence', _check_confidence(shift));
Mutants (Total: 2, Killed: 2, Survived: 0)
557: } 558: 559: =head2 ACCURACY_LEVELS 560: 561: The accuracy levels a point may claim, most precise first: C<address> (a house 562: number), C<street>, C<postcode>, C<locality>, C<city>, C<region> and 563: C<country>. 564: 565: This is the vocabulary, written down once here and read from here by 566: L<Geo::Coder::Free::Provenance> rather than copied, so the levels a point 567: accepts and the levels a backend reports cannot drift apart. C<accuracy()> 568: accepts these and refuses anything else. 569: 570: The rule that turns the fields a backend matched into one of these levels is 571: L</accuracy_for>, beside the list it returns a level from. 572: 573: The list is read-only, and a caller that tries to add to it is refused, because 574: it is the list C<accuracy()> checks against: were it writable, a level pushed 575: into it would be published to every reader of this list while the module went on 576: refusing it. 577: 578: my $levels = Geo::Location::Point::ACCURACY_LEVELS(); 579: 580: =cut 581: 582: =head2 accuracy_for 583: 584: The accuracy a match can claim, from the fields that made it. The most precise 585: field present decides: a house number makes it an C<address>, a road a 586: C<street>, a postcode a C<postcode>, a name or a city a C<locality>, a state or 587: region a C<region>, and a country a C<country>. So a dataset row carrying only 588: a city, state and country is a C<locality>: a named place, not an address. 589: 590: my $level = Geo::Location::Point::accuracy_for(qw(house_number road city)); 591: 592: Always returns one of the L</ACCURACY_LEVELS>: a match on a row with none of the 593: fields above is still a named place, so it is reported as a C<locality> rather 594: than left unsaid, because every result promises an accuracy. 595: 596: It is a function, not a method - the fields are what a backend matched, not 597: something a point carries - and it lives beside the vocabulary it returns a 598: level from, so the levels a point accepts and the rules that choose one of them 599: have a single home. 600: 601: =cut 602: 603: =head2 accuracy 604: 605: How precise the coordinates behind a match can claim to be. The levels are 606: those in L</ACCURACY_LEVELS>; anything else is rejected, because a reader 607: deciding what a point may claim about itself has to be able to trust the 608: value. 609: 610: $location->accuracy('city'); 611: 612: =cut 613: 614: sub accuracy { 615: my $self = shift; 616: 617: return $self->_attribute('accuracy') unless(@_);
Mutants (Total: 2, Killed: 2, Survived: 0)
618: 619: return $self->_set_attribute('accuracy', _check_accuracy(shift));
Mutants (Total: 2, Killed: 2, Survived: 0)
620: } 621: 622: =head2 country 623: 624: The country of the point, as the data spells it: C<GB>, C<US>, C<Canada>. It 625: is stored as given rather than normalised, so it is whatever the backend 626: matched. 627: 628: $location->country('GB'); 629: 630: =cut 631: 632: sub country { 633: my $self = shift; 634: 635: return $self->_attribute('country') unless(@_);
Mutants (Total: 2, Killed: 2, Survived: 0)
636: 637: return $self->_set_attribute('country', _check_string('country', shift));
Mutants (Total: 2, Killed: 2, Survived: 0)
638: } 639: 640: =head2 city 641: 642: The city, town or similar locality of the point. 643: 644: $location->city('London'); 645: 646: =cut 647: 648: sub city { 649: my $self = shift; 650: 651: return $self->_attribute('city') unless(@_);
Mutants (Total: 2, Killed: 2, Survived: 0)
652: 653: return $self->_set_attribute('city', _check_string('city', shift));
Mutants (Total: 2, Killed: 2, Survived: 0)
654: } 655: 656: =head2 state 657: 658: The state, province or region of the point. 659: 660: $location->state('KY'); 661: 662: =cut 663: 664: sub state { 665: my $self = shift; 666: 667: return $self->_attribute('state') unless(@_);
Mutants (Total: 2, Killed: 2, Survived: 0)
668: 669: return $self->_set_attribute('state', _check_string('state', shift));
Mutants (Total: 2, Killed: 2, Survived: 0)
670: } 671: 672: =head1 SUPPORT 673: 674: This module is provided as-is without any warranty. 675: 676: =head1 AUTHOR 677: 678: Nigel Horne, C<< <njh at nigelhorne.com> >> 679: 680: =head1 BUGS 681: 682: There is no validation on the attribute in the AUTOLOAD method, 683: so typos such as "citty" will not be caught. 684: C<geocoder>, C<database>, C<confidence>, C<accuracy>, C<country>, C<city> and 685: C<state> have documented accessors of their own that do check what they are 686: given, 687: but they are the only attributes that do, 688: and C<new()> stores whatever it is handed without checking it, 689: so a provenance field set through the constructor can still be any value. 690: 691: =head1 SEE ALSO 692: 693: =over 4 694: 695: =item * L<Test Dashboard|https://nigelhorne.github.io/Geo-Location-Point/coverage/> 696: 697: =back 698: 699: L<GIS::Distance>, 700: L<Geo::Point>, 701: L<TimeZone::TimeZoneDB>. 702: 703: =head1 LICENSE AND COPYRIGHT 704: 705: Copyright 2019-2026 Nigel Horne. 706: 707: Usage is subject to the GPL2 licence terms. 708: If you use it, 709: please let me know. 710: 711: =cut 712: 713: 1;