--- Weather-NOAA-Alert-0.90/lib/Weather/NOAA/Alert.pm 2011-05-01 13:26:48.437500000 -0500
+++ Weather-NOAA-Alert/lib/Weather/NOAA/Alert.pm 2011-05-02 02:20:59.934619600 -0500
@@ -19,11 +19,11 @@
=head1 VERSION
-Version 0.90
+Version 0.91
=cut
-our $VERSION = '0.90';
+our $VERSION = '0.91';
=head1 SYNOPSIS
@@ -49,16 +49,38 @@
=item * Zone Maps: L<
http://www.weather.gov/mirs/public/prods/maps/pfzones_list.htm>
=back
+
+=head 2 Affected Area
+
+The affected area for NOAA NWS watches, warnings, and advisories fall into
+one of three categories. They are either in a forecast zone, forecast county,
+or within a polygon defined in the alert. An alert is considered to affect the
+entire forecast zone or county if the alert is supplied in the respective zone
+or county file and the C<E<lt>areaE<gt>E<lt>polygonE<gt>> enclosure is empty.
+If an C<E<lt>areaE<gt>E<lt>polygonE<gt>> enclosure is supplied then the alert
+only applies to the area enclosed by the C<E<lt>areaE<gt>E<lt>polygonE<gt>>.
+
+Weather::NOAA::Alert can be armed with zones and latitude/longitude points which
+are compared to the alert area. The C<inDetectionZone> flag will be set in
+the C<events> hash if one or more of the zones and/or points are within the
+alert area. This permits monitoring other counties and reacting differently
+for alerts within the detection are vs. alerts outside the detection area. In
+the simplest case set the same zones for polling as for detection and provide a
+latitude/longitude point for polygon based alerts.
+
=head1 EXAMPLE
use Weather::NOAA::Alert;
use Data::Dumper;
-
+ $Data::Dumper::Indent = 1;
+
my $SLEEP_SECS = 60; #Poll every 1 minute
#my $alert = Weather::NOAA::Alert->new(['US']);
my $alert = Weather::NOAA::Alert->new(['TXC085', 'TXZ097']);
+ $alert->detectionZones(['TXC085']);
+ $alert->detectionPoints([[33.090394, -96.753218]]);
$alert->printLog(1);
$alert->errorLog(1);
@@ -99,6 +121,8 @@
my $class = shift;
my $self = {};
+ $self->{detectionPoints} = [];
+ $self->{detectionZones} = [];
$self->{events} = {};
$self->{formatTime} = 0;
$self->{formatAsterisk} = 0;
@@ -122,19 +146,53 @@
return $self;
}
-=head2 B<zones> - Set the monitored zone list
+=head2 B<zones> - Set the polled zone list
$object->zones([zone1, zone2, ...]);
@zones = $object->zones();
-Setting the zone list will overwrite the existing list with the
-supplied list. If called with no arguments, returns a reference
-to the current zone array. To return data for all zones use "US".
+List of zones whose ATOM files will be polled and monitored for
+new alerts. Setting the zone list will overwrite the existing list
+with the supplied list. If called with no arguments, returns a
+reference to the current zone array. To return data for all zones
+use "US".
=cut
sub zones { $_[0]->{zones}=$_[1] if defined $_[1]; return $_[0]->{zones}; }
+=head2 B<detectionZones> - Set the detection zone list
+
+ $object->detectionZones([zone1, zone2, ...]);
+ @zones = $object->detectionZones();
+
+Events for zones in the C<detectionZone> list will set the
+C<inDetectionZone> flag for the event in the events hash unless the
+event contains a non-empty C<E<lt>areaE<gt>E<lt>polygonE<gt>>
+enclosure. Setting the zone list will overwrite the existing list
+with the supplied list. If called with no arguments, returns a
+reference to the current C<detectionZone> array.
+
+=cut
+
+sub detectionZones { $_[0]->{detectionZones}=$_[1] if defined $_[1]; return $_[0]->{detectionZones}; }
+
+=head2 B<detectionPoints> - Set the detection point list
+
+ $object->detectionPoints([[lat1, lon1], [lat2, lon2], ...]);
+ @zones = $object->detectionPoints();
+
+Events that contain a non-empty C<E<lt>areaE<gt>E<lt>polygonE<gt>>
+enclosure and where one or more detectionPoints are within the polygon
+will set the C<inDetectionZone> flag for the event in the events hash.
+Setting the C<detectionPoints> list will overwrite the existing
+list with the supplied list. If called with no arguments, returns
+a reference to the current C<detectionPoints> array.
+
+=cut
+
+sub detectionPoints { $_[0]->{detectionPoints}=$_[1] if defined $_[1]; return $_[0]->{detectionPoints}; }
+
=head2 B<get_events> - get a reference to the alert object events hash
%events = $object->get_events();
@@ -158,6 +216,7 @@
C< -E<gt>{'effective'}>
C< -E<gt>{'headline'}>
C< -E<gt>{'expires'}>
+ C< -E<gt>{'inDetectionZone'}>
Note that the hash keys are dynamically created from the <info> section of the
event. If NOAA adds, renames, or removes an XML parameter it will also
@@ -419,13 +478,13 @@
my ($self, $zone, $capId) = @_;
my $capTwig= new XML::Twig(
- TwigRoots => {'info' => 1},
-# TwigHandlers => {'info' => \&capInfoHandler},
- TwigHandlers => {'info' => sub { capInfoHandler( $self->{formatTime}, $self->{formatAsterisk}, @_) } },
+ TwigRoots => {'/alert/info' => 1},
+ TwigHandlers => {'/alert/info' => sub { capInfoHandler( $self->{formatTime}, $self->{formatAsterisk}, @_) } },
pretty_print => 'indented',
);
-
+
my $capContent;
+ my $polygon = '';
if ($capContent = get($capId)) {
if( defined( $self->{diagFile}) and $self->{diagDump}) {
@@ -438,11 +497,32 @@
#Parse only the first <info> enclosure
#Loop through it appending items to the event hash
foreach my $child ($capTwig->root->first_child->children) {
- #ignore nested items: eventCode, parameter, area; they're too hard :^)
+ #ignore nested items: eventCode, parameter; they're too hard :^)
if($child->tag ne 'eventCode' and $child->tag ne 'parameter' and $child->tag ne 'area') {
$self->{events}->{$zone}{$capId}{$child->tag} = $child->text;
+ } elsif( $child->tag eq 'area') {
+ foreach my $childArea ($child->children) {
+ $polygon = $childArea->text if($childArea->tag eq 'polygon');
+ }
+ }
+ }
+ #check for inDetectionZone
+ #if there was a polygon check all the detectionPoints else
+ #search for the currently polled zone in detectionZones
+ if($polygon eq '') {
+ $self->{events}->{$zone}{$capId}{'inDetectionZone'} =
+ grep(/\b$zone\b/,@{$self->{detectionZones}});
+ } else {
+ $self->{events}->{$zone}{$capId}{'polygon'} = $polygon;
+ $self->{events}->{$zone}{$capId}{'inDetectionZone'} = 0;
+ foreach my $point (@{$self->{detectionPoints}}) {
+ my @polyPoints = map { [split(/,/)] } split(/ /, $polygon);
+ if(pointInPolygon( \@polyPoints, $point)) {
+ $self->{events}->{$zone}{$capId}{'inDetectionZone'} = 1;
+ }
}
}
+
$self->{events}->{$zone}{$capId}{'delete'} = 0;
} else {
@@ -495,6 +575,7 @@
#except the description and instruction; they need other adjustments.
my @children = $capInfo->children;
foreach my $child (@children) {
+ next if($child->tag eq 'area');
my $childText = $child->text;
if ($child->tag ne 'description' and $child->tag ne 'instruction') {
#Adjust the formatting a little. Why would a CAP file
@@ -505,13 +586,13 @@
$childText =~ s/\s+$//; #remove trailing spaces
} else {
if( $formatTime) {
-# if( 1 ) {
#Try to add colons to all the time fields. This allows
#the MS SAPI engine to correctly pronounce the time
- $childText =~ s/(\d{1,2}?)(\d{2})\s{1}(AM|PM)\s{1}[A-Z]{3}/$1:$2 $3/g;
+ $childText =~ s/(\d{1,2}?)(\d{2})\s{1}(AM|PM)\s{1}[A-Z]{1}[D|S]T/$1:$2 $3/g;
+ $childText =~ s/(\d{1,2}?)(\d{2})\s{1}(AM|PM)\s*\n/$1:$2 $3\n/g;
+ $childText =~ s/(\d{1,2}?)(\d{2})\s{1}(AM|PM)\s/$1:$2 $3 /g;
}
if( $formatAsterisk) {
-# if( 1 ) {
#Remove any "*" because it sounds real funny when SAPI
#pronounces "asterisk" in the middle of the speech stream
$childText =~ s/\*/ /g;
@@ -521,6 +602,32 @@
}
}
+sub pointInPolygon {
+ my ($polygon, $point) = @_;
+ my $lat = $point->[0];
+ my $lon = $point->[1];
+
+ my $i;
+ my $j = (scalar @$polygon) - 1;
+ my $inPoly = 0;
+
+ #look for an odd number of intesecting points with line through point and
+ #the boundary of the polygon
+ for ( $i = 0; $i < scalar @$polygon; $i++) {
+ if ($polygon->[$i][1] < $lon && $polygon->[$j][1] >= $lon
+ || $polygon->[$j][1] < $lon && $polygon->[$i][1] >= $lon) {
+ if ($polygon->[$i][0] + ( $lon - $polygon->[$i][1]) /
+ ($polygon->[$j][1] - $polygon->[$i][1]) * ($polygon->[$j][0]
+ - $polygon->[$i][0]) < $lat) {
+ $inPoly = !$inPoly;
+ }
+ }
+ $j = $i;
+ }
+ return $inPoly;
+}
+
+
=head1 NOAA CAP CHALLENGES
The following items represent fragile parts of the code caused by ambiguous
@@ -561,22 +668,6 @@
=back
-=head1 TODO
-
-=over 4
-
-=item * B<Implement point detection based on latitude / longitude>
-
-Events contain a C<E<lt>polygonE<gt>E<lt>/polygonE<gt>> that could be
-used to determine if a specific latitude / longitude is in the event
-area. This would greatly reduce the number of alerts for a specific
-point in a county since the NWS has recently started issuing and
-expiring events for specific areas that are not bound by county.
-Tracking at the county level can trigger several events over a short
-period as a storm progresses through the county.
-
-=back
-
=head1 SEE ALSO
=over 4