zeroclickinfo-goodies/lib/DDG/Goodie/ConvertLatLon.pm

280 lines
8.4 KiB
Perl
Raw Normal View History

package DDG::Goodie::ConvertLatLon;
# ABSTRACT: Convert between latitudes and longitudes expressed in degrees of arc and decimal
use strict;
use DDG::Goodie;
use utf8;
use Geo::Coordinates::DecimalDegrees;
use Math::SigFigs qw(:all);
use Math::Round;
zci is_cached => 1;
name 'Convert Latitude and Longitude';
description 'Convert between latitudes and longitudes expressed in degrees of arc and decimal';
primary_example_queries '71º 10\' 3" in decimal';
2014-07-04 22:00:59 -07:00
secondary_example_queries '71 degrees 10 minutes 3 seconds east in decimal', '- 16º 30\' 0" - 68º 9\' 0" as decimal';
category 'transformations';
topics 'geography', 'math', 'science';
code_url 'https://github.com/duckduckgo/zeroclickinfo-goodies/blob/master/lib/DDG/Goodie/ConvertLatLon.pm';
attribution github => ['http://github.com/wilkox', 'wilkox'];
triggers any => "convert", "dms", "decimal", "latitude", "longitude", "minutes", "seconds";
#Regexes for latitude/longitude, in either dms or decimal format
# http://msdn.microsoft.com/en-us/library/aa578799.aspx has a good
# overview of the most common representations of latitude/longitude
#Potential Unicode and other representations of "degrees"
2014-07-01 20:28:55 -07:00
my $degQR = qr/
2014-07-04 20:51:17 -07:00
[º°]|
((arc[-]?)?deg(ree)?s?)
/ix;
#Potential Unicode and other representations of "minutes"
2014-07-01 20:28:55 -07:00
my $minQR = qr/
2014-07-04 20:51:17 -07:00
['`ʹ]|
((arc[-]?)?min(ute)?s?)
/ix;
#Potential Unicode and other representations of "seconds"
2014-07-01 20:28:55 -07:00
my $secQR = qr/
2014-07-04 20:51:17 -07:00
["]|
$minQR{2}|
(arc[-]?)?sec(ond)?s?
/ix;
#Match a decimal or integer number
2014-07-01 20:28:55 -07:00
my $numQR = qr/[\d\.]+/;
#Match a minus sign or attempt at minus sign or word processor
# interpretation of minus sign
my $minusQR = qr/
[-]|
(minus)|
(negative)
/ix;
#Match a latitude/longitude representation
2014-07-01 20:28:55 -07:00
my $latLonQR = qr/
2014-07-04 20:51:17 -07:00
(?<minus>$minusQR)?
(?<degrees>$numQR)$degQR
((?<minutes>$numQR)$minQR
((?<seconds>$numQR)$secQR)?)?
(?<cardinal>[NSEW]|(north)|(south)|(east)|(west))?
/ix;
2014-07-01 20:28:55 -07:00
my %cardinalSign = (
2014-07-04 20:51:17 -07:00
N => 1,
S => -1,
E => 1,
W => -1,
);
2014-07-01 20:28:55 -07:00
my %cardinalName = (
2014-07-04 20:51:17 -07:00
n => 'N',
north => 'N',
s => 'S',
south => 'S',
e => 'E',
east => 'E',
w => 'W',
west => 'W'
);
handle query_nowhitespace => sub {
2014-07-04 22:00:59 -07:00
return unless /$latLonQR/;
2014-07-04 22:00:59 -07:00
#Loop over all provided latitudes/longitudes
# Not going to try and enforce strict latitude/longitude
# pairing — if the user wants to pass in a long list
# of latitudes or a single longitude, no problem as long
# as they're all in the same format
2014-07-05 00:20:26 -07:00
my @queries;
my @results;
my $toFormat;
2014-07-04 22:00:59 -07:00
while (/$latLonQR/g) {
2014-07-04 22:00:59 -07:00
my $minus = $+{minus};
my $degrees = $+{degrees};
my $minutes = $+{minutes};
my $seconds = $+{seconds};
my $cardinal = $+{cardinal};
2014-07-04 22:00:59 -07:00
#Validation: must have minutes if has seconds
return unless (($minutes && $seconds) || ! $seconds);
2014-07-04 22:00:59 -07:00
#Validation: can't supply both minus sign and cardinal direction
return if $minus && $cardinal;
2014-07-04 22:00:59 -07:00
#Convert cardinal to standardised name if provided
$cardinal = $cardinalName{lc($cardinal)} if $cardinal;
2014-07-04 22:00:59 -07:00
#Set the sign
my $sign;
if ($cardinal) {
$sign = $cardinalSign{$cardinal};
2014-07-04 20:51:17 -07:00
} else {
2014-07-04 22:00:59 -07:00
$sign = $minus ? -1 : 1;
2014-07-04 20:51:17 -07:00
}
2014-07-04 22:00:59 -07:00
#Determine type of conversion (dms -> decimal or decimal -> dms)
# and perform as appropriate
2014-07-04 22:00:59 -07:00
#If the degrees are expressed in decimal...
if ($degrees =~ /\./) {
2014-10-28 11:36:15 -07:00
#If this isn't the first conversion, make sure the
2014-07-04 22:00:59 -07:00
# user hasn't passed a mix of decimal and DMS
return if $toFormat && ! $toFormat eq 'DMS';
$toFormat = 'DMS';
2014-07-04 22:00:59 -07:00
#Validation: must not have provided minutes and seconds
return if $minutes || $seconds;
2014-07-04 22:00:59 -07:00
#Validation: if only degrees were provided, make sure
# the user isn't looking for a temperature or trigonometric conversion
2014-07-06 00:32:30 -07:00
my $rejectQR = qr/temperature|farenheit|celsius|radians|kelvin|centigrade|\b$degQR\s?[FCK]/i;
2014-07-04 22:00:59 -07:00
return if $_ =~ /$rejectQR/i;
2014-07-04 22:00:59 -07:00
#Validation: can't exceed 90 degrees (if latitude) or 180 degrees
# (if longitude or unknown)
if ($cardinal && $cardinal =~ /[NS]/) {
return if abs($degrees) > 90;
} else {
return if abs($degrees) > 180;
}
2014-07-04 22:00:59 -07:00
#Convert
(my $dmsDegrees, my $dmsMinutes, my $dmsSeconds, my $dmsSign) = decimal2dms($degrees * $sign);
2014-07-04 22:00:59 -07:00
#Annoyingly, Geo::Coordinates::DecimalDegrees will sign the degrees as
# well as providing a sign
$dmsDegrees = abs($dmsDegrees);
2014-07-04 22:00:59 -07:00
#If seconds is fractional, take the mantissa
$dmsSeconds = round($dmsSeconds);
2014-07-04 22:00:59 -07:00
#Format nicely
my $formattedDMS = format_dms($dmsDegrees, $dmsMinutes, $dmsSeconds, $dmsSign, $cardinal);
2014-07-05 00:20:26 -07:00
my $formattedQuery = format_decimal(($degrees * $sign), $cardinal);
2014-07-04 22:00:59 -07:00
push(@queries, $formattedQuery);
push(@results, $formattedDMS);
2014-07-04 22:00:59 -07:00
#Otherwise, we assume type is DMS (even if no
# minutes/seconds given)
2014-07-04 20:51:17 -07:00
} else {
2014-10-28 11:36:15 -07:00
#If this isn't the first conversion, make sure the
2014-07-04 22:00:59 -07:00
# user hasn't passed a mix of decimal and DMS
return if $toFormat && ! $toFormat eq 'decimal';
$toFormat = 'decimal';
#Validation: must have given at least minutes
return unless $minutes;
#Validation: can't have decimal minutes if there are seconds
return if $minutes =~ /\./ && $seconds;
#Validation: minutes and seconds can't exceed 60
return if $minutes >= 60;
return if $seconds && $seconds >= 60;
#Apply the sign
$degrees = $sign * $degrees;
#Convert
# Note that unlike decimal2dms, dms2decimal requires a signed degrees
# and returns a signed degrees (not a separate sign variable)
my $decDegrees = $seconds ? dms2decimal($degrees, $minutes, $seconds) : dm2decimal($degrees, $minutes);
2014-07-04 22:00:59 -07:00
#Round to 8 significant figures
$decDegrees = FormatSigFigs($decDegrees, 8);
$decDegrees =~ s/\.$//g;
2014-07-04 22:00:59 -07:00
#Validation: can't exceed 90 degrees (if latitude) or 180 degrees
# (if longitude or unknown)
if ($cardinal && $cardinal =~ /[NS]/) {
return if abs($decDegrees) > 90;
} else {
return if abs($decDegrees) > 180;
}
#Format nicely
2014-07-05 00:20:26 -07:00
my $formattedDec = format_decimal($decDegrees, $cardinal);
2014-07-04 22:00:59 -07:00
my $formattedQuery = format_dms($degrees, $minutes, $seconds, $sign, $cardinal);
push(@queries, $formattedQuery);
push(@results, $formattedDec);
}
2014-07-04 20:51:17 -07:00
}
2014-07-04 22:00:59 -07:00
my $answer = join(' ' , @results);
my $html = wrap_html(\@queries, \@results, $toFormat);
return $answer, html => $html;
};
#Format a degrees-minutes-seconds expression
sub format_dms {
2014-07-04 20:51:17 -07:00
(my $dmsDegrees, my $dmsMinutes, my $dmsSeconds, my $dmsSign, my $cardinal) = @_;
2014-07-04 20:51:17 -07:00
my $formatted = abs($dmsDegrees) . '°';
$formatted .= ' ' . $dmsMinutes . '' if $dmsMinutes;
$formatted .= ' ' . $dmsSeconds . '″' if $dmsSeconds;
2014-07-04 20:51:17 -07:00
#If a cardinal direction was supplied, use the cardinal
if ($cardinal) {
$formatted .= ' ' . uc($cardinal);
2014-07-04 20:51:17 -07:00
#Otherwise, add a minus sign if negative
} elsif ($dmsSign == -1) {
$formatted = '' . $formatted;
}
2014-07-04 20:51:17 -07:00
return $formatted;
}
#Format a decimal expression
sub format_decimal {
2014-07-05 00:20:26 -07:00
(my $decDegrees, my $cardinal) = @_;
2014-07-04 20:51:17 -07:00
my $formatted = abs($decDegrees) . '°';
2014-07-05 00:20:26 -07:00
#If a cardinal direction was supplied, use the cardinal
if ($cardinal) {
$formatted .= ' ' . uc($cardinal);
} elsif ($decDegrees / abs($decDegrees) == -1) {
2014-07-04 20:51:17 -07:00
$formatted = '' . $formatted;
}
2014-07-04 20:51:17 -07:00
return $formatted;
}
2014-07-04 22:00:59 -07:00
sub wrap_secondary {
my $secondary = shift;
return "<span class='text--secondary'>" . $secondary . "</span>";
}
sub wrap_html {
2014-07-04 22:00:59 -07:00
my @queries = @{$_[0]};
my @results = @{$_[1]};
my $toFormat = $_[2];
my $queries = join wrap_secondary(', '), html_enc(@queries);
my $results = join wrap_secondary(', '), html_enc(@results);
2014-07-04 22:00:59 -07:00
my $html = "<div class='zci--conversions text--primary'>" . $queries . wrap_secondary(' in ' . $toFormat . ': ') . $results . "</div>";
return $html;
}
1;