2014-06-30 20:38:48 -07:00
|
|
|
|
package DDG::Goodie::ConvertLatLon;
|
|
|
|
|
# ABSTRACT: Convert between latitudes and longitudes expressed in degrees of arc and decimal
|
|
|
|
|
|
2015-02-22 12:09:29 -08:00
|
|
|
|
use strict;
|
2014-06-30 20:38:48 -07:00
|
|
|
|
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';
|
2014-06-30 20:38:48 -07:00
|
|
|
|
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;
|
2014-06-30 20:38:48 -07:00
|
|
|
|
|
|
|
|
|
#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;
|
2014-06-30 20:38:48 -07:00
|
|
|
|
|
|
|
|
|
#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;
|
2014-06-30 20:38:48 -07:00
|
|
|
|
|
|
|
|
|
#Match a decimal or integer number
|
2014-07-01 20:28:55 -07:00
|
|
|
|
my $numQR = qr/[\d\.]+/;
|
2014-06-30 20:38:48 -07:00
|
|
|
|
|
|
|
|
|
#Match a minus sign or attempt at minus sign or word processor
|
|
|
|
|
# interpretation of minus sign
|
2014-07-05 05:19:53 -07:00
|
|
|
|
my $minusQR = qr/
|
|
|
|
|
[-−﹣-‒–—‐]|
|
|
|
|
|
(minus)|
|
|
|
|
|
(negative)
|
|
|
|
|
/ix;
|
2014-06-30 20:38:48 -07:00
|
|
|
|
|
|
|
|
|
#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-06-30 20:38:48 -07:00
|
|
|
|
|
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-06-30 20:38:48 -07:00
|
|
|
|
);
|
|
|
|
|
|
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'
|
2014-06-30 20:38:48 -07:00
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
handle query_nowhitespace => sub {
|
|
|
|
|
|
2014-07-04 22:00:59 -07:00
|
|
|
|
return unless /$latLonQR/;
|
2014-06-30 20:38:48 -07:00
|
|
|
|
|
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-06-30 20:38:48 -07:00
|
|
|
|
|
2014-07-04 22:00:59 -07:00
|
|
|
|
my $minus = $+{minus};
|
|
|
|
|
my $degrees = $+{degrees};
|
|
|
|
|
my $minutes = $+{minutes};
|
|
|
|
|
my $seconds = $+{seconds};
|
|
|
|
|
my $cardinal = $+{cardinal};
|
2014-06-30 20:38:48 -07:00
|
|
|
|
|
2014-07-04 22:00:59 -07:00
|
|
|
|
#Validation: must have minutes if has seconds
|
|
|
|
|
return unless (($minutes && $seconds) || ! $seconds);
|
2014-06-30 20:38:48 -07:00
|
|
|
|
|
2014-07-04 22:00:59 -07:00
|
|
|
|
#Validation: can't supply both minus sign and cardinal direction
|
|
|
|
|
return if $minus && $cardinal;
|
2014-06-30 20:38:48 -07:00
|
|
|
|
|
2014-07-04 22:00:59 -07:00
|
|
|
|
#Convert cardinal to standardised name if provided
|
|
|
|
|
$cardinal = $cardinalName{lc($cardinal)} if $cardinal;
|
2014-06-30 20:38:48 -07:00
|
|
|
|
|
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-06-30 20:38:48 -07:00
|
|
|
|
|
2014-07-04 22:00:59 -07:00
|
|
|
|
#Determine type of conversion (dms -> decimal or decimal -> dms)
|
|
|
|
|
# and perform as appropriate
|
2014-06-30 20:38:48 -07:00
|
|
|
|
|
2014-07-04 22:00:59 -07:00
|
|
|
|
#If the degrees are expressed in decimal...
|
|
|
|
|
if ($degrees =~ /\./) {
|
2014-06-30 20:38:48 -07:00
|
|
|
|
|
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-06-30 20:38:48 -07:00
|
|
|
|
|
2014-07-04 22:00:59 -07:00
|
|
|
|
#Validation: must not have provided minutes and seconds
|
|
|
|
|
return if $minutes || $seconds;
|
2014-06-30 20:38:48 -07:00
|
|
|
|
|
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-06-30 20:38:48 -07:00
|
|
|
|
|
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-06-30 20:38:48 -07:00
|
|
|
|
|
2014-07-04 22:00:59 -07:00
|
|
|
|
#Convert
|
|
|
|
|
(my $dmsDegrees, my $dmsMinutes, my $dmsSeconds, my $dmsSign) = decimal2dms($degrees * $sign);
|
2014-06-30 20:38:48 -07:00
|
|
|
|
|
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-06-30 20:38:48 -07:00
|
|
|
|
|
2014-07-04 22:00:59 -07:00
|
|
|
|
#If seconds is fractional, take the mantissa
|
|
|
|
|
$dmsSeconds = round($dmsSeconds);
|
2014-06-30 20:38:48 -07:00
|
|
|
|
|
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-06-30 20:38:48 -07:00
|
|
|
|
|
2014-07-04 22:00:59 -07:00
|
|
|
|
push(@queries, $formattedQuery);
|
|
|
|
|
push(@results, $formattedDMS);
|
2014-06-30 20:38:48 -07:00
|
|
|
|
|
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-06-30 20:38:48 -07:00
|
|
|
|
|
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-06-30 20:38:48 -07:00
|
|
|
|
|
2014-07-04 22:00:59 -07:00
|
|
|
|
#Round to 8 significant figures
|
|
|
|
|
$decDegrees = FormatSigFigs($decDegrees, 8);
|
|
|
|
|
$decDegrees =~ s/\.$//g;
|
2014-06-30 20:38:48 -07:00
|
|
|
|
|
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-06-30 20:38:48 -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;
|
2014-06-30 20:38:48 -07:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
#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-06-30 20:38:48 -07:00
|
|
|
|
|
2014-07-04 20:51:17 -07:00
|
|
|
|
my $formatted = abs($dmsDegrees) . '°';
|
|
|
|
|
$formatted .= ' ' . $dmsMinutes . '′' if $dmsMinutes;
|
|
|
|
|
$formatted .= ' ' . $dmsSeconds . '″' if $dmsSeconds;
|
2014-06-30 20:38:48 -07:00
|
|
|
|
|
2014-07-04 20:51:17 -07:00
|
|
|
|
#If a cardinal direction was supplied, use the cardinal
|
|
|
|
|
if ($cardinal) {
|
|
|
|
|
$formatted .= ' ' . uc($cardinal);
|
2014-06-30 20:38:48 -07:00
|
|
|
|
|
2014-07-04 20:51:17 -07:00
|
|
|
|
#Otherwise, add a minus sign if negative
|
|
|
|
|
} elsif ($dmsSign == -1) {
|
|
|
|
|
$formatted = '−' . $formatted;
|
|
|
|
|
}
|
2014-06-30 20:38:48 -07:00
|
|
|
|
|
2014-07-04 20:51:17 -07:00
|
|
|
|
return $formatted;
|
2014-06-30 20:38:48 -07:00
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#Format a decimal expression
|
|
|
|
|
sub format_decimal {
|
|
|
|
|
|
2014-07-05 00:20:26 -07:00
|
|
|
|
(my $decDegrees, my $cardinal) = @_;
|
2014-06-30 20:38:48 -07:00
|
|
|
|
|
2014-07-04 20:51:17 -07:00
|
|
|
|
my $formatted = abs($decDegrees) . '°';
|
2014-06-30 20:38:48 -07:00
|
|
|
|
|
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-06-30 20:38:48 -07:00
|
|
|
|
|
2014-07-04 20:51:17 -07:00
|
|
|
|
return $formatted;
|
2014-06-30 20:38:48 -07:00
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
2014-07-04 22:00:59 -07:00
|
|
|
|
sub wrap_secondary {
|
|
|
|
|
my $secondary = shift;
|
|
|
|
|
return "<span class='text--secondary'>" . $secondary . "</span>";
|
|
|
|
|
}
|
|
|
|
|
|
2014-06-30 20:38:48 -07:00
|
|
|
|
sub wrap_html {
|
2014-07-04 22:00:59 -07:00
|
|
|
|
|
|
|
|
|
my @queries = @{$_[0]};
|
|
|
|
|
my @results = @{$_[1]};
|
|
|
|
|
my $toFormat = $_[2];
|
|
|
|
|
|
2014-08-20 12:03:16 -07:00
|
|
|
|
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>";
|
2014-08-26 12:32:09 -07:00
|
|
|
|
return $html;
|
2014-06-30 20:38:48 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
1;
|