generate sector coverage maps with Splat, checkpoint, #37802
authorMark Wells <mark@freeside.biz>
Thu, 21 Apr 2016 19:07:29 +0000 (12:07 -0700)
committerMark Wells <mark@freeside.biz>
Tue, 26 Apr 2016 19:10:50 +0000 (12:10 -0700)
FS/FS/Schema.pm
FS/FS/tower_sector.pm
httemplate/edit/process/tower.html
httemplate/edit/tower.html
httemplate/elements/tower_sector.html
httemplate/misc/sector-create_map.html [new file with mode: 0644]
httemplate/search/elements/gmap.html
httemplate/search/sector.html [new file with mode: 0644]
httemplate/search/svc_broadband-map.html
httemplate/view/sector_map-png.cgi [new file with mode: 0644]

index d94f963..4f7dade 100644 (file)
@@ -4845,9 +4845,16 @@ sub tables_hashref {
         'freq_mhz',         'int', 'NULL',      '', '', '',
         'direction',        'int', 'NULL',      '', '', '',
         'width',            'int', 'NULL',      '', '', '',
-        #downtilt etc? rfpath has profile files for devices/antennas you upload?
         'sector_range', 'decimal', 'NULL',      '', '', '',  #?
-      ],
+        'downtilt',     'decimal', 'NULL',      '', '', '',
+        'v_width',          'int', 'NULL',      '', '', '',
+        'margin',       'decimal', 'NULL',     '', '', '',
+        'image',           'blob', 'NULL',     '', '', '',
+        'west',         'decimal', 'NULL', '10,7', '', '',
+        'east',         'decimal', 'NULL', '10,7', '', '',
+        'south',        'decimal', 'NULL', '10,7', '', '',
+        'north',        'decimal', 'NULL', '10,7', '', '',
+     ],
       'primary_key'  => 'sectornum',
       'unique'       => [ [ 'towernum', 'sectorname' ], [ 'ip_addr' ], ],
       'index'        => [ [ 'towernum' ] ],
index 4fbd89c..8b4c222 100644 (file)
@@ -1,6 +1,9 @@
 package FS::tower_sector;
 use base qw( FS::Record );
 
+use Class::Load qw(load_class);
+use Data::Dumper;
+
 use strict;
 
 =head1 NAME
@@ -24,7 +27,7 @@ FS::tower_sector - Object methods for tower_sector records
 
 =head1 DESCRIPTION
 
-An FS::tower_sector object represents an tower sector.  FS::tower_sector
+An FS::tower_sector object represents a tower sector.  FS::tower_sector
 inherits from FS::Record.  The following fields are currently supported:
 
 =over 4
@@ -45,6 +48,44 @@ sectorname
 
 ip_addr
 
+=item height
+
+The height of this antenna on the tower, measured from ground level. This
+plus the tower's altitude should equal the height of the antenna above sea
+level.
+
+=item freq_mhz
+
+The band center frequency in MHz.
+
+=item direction
+
+The antenna beam direction in degrees from north.
+
+=item width
+
+The -3dB horizontal beamwidth in degrees.
+
+=item downtilt
+
+The antenna beam elevation in degrees below horizontal.
+
+=item v_width
+
+The -3dB vertical beamwidth in degrees.
+
+=item margin
+
+The signal loss margin allowed on the sector, in dB. This is normally
+transmitter EIRP minus receiver sensitivity.
+
+=item image 
+
+The coverage map, as a PNG.
+
+=item west, east, south, north
+
+The coordinate boundaries of the coverage map.
 
 =back
 
@@ -84,11 +125,6 @@ sub delete {
   $self->SUPER::delete;
 }
 
-=item replace OLD_RECORD
-
-Replaces the OLD_RECORD with this one in the database.  If there is an error,
-returns the error, otherwise returns false.
-
 =item check
 
 Checks all fields to make sure this is a valid sector.  If there is
@@ -109,7 +145,15 @@ sub check {
     || $self->ut_numbern('freq_mhz')
     || $self->ut_numbern('direction')
     || $self->ut_numbern('width')
+    || $self->ut_numbern('v_width')
+    || $self->ut_numbern('downtilt')
     || $self->ut_floatn('sector_range')
+    || $self->ut_numbern('margin')
+    || $self->ut_anything('image')
+    || $self->ut_sfloatn('west')
+    || $self->ut_sfloatn('east')
+    || $self->ut_sfloatn('south')
+    || $self->ut_sfloatn('north')
   ;
   return $error if $error;
 
@@ -140,8 +184,107 @@ sub description {
 
 Returns the services on this tower sector.
 
+=item need_fields_for_coverage
+
+Returns a list of required fields for the coverage map that aren't yet filled.
+
+=cut
+
+sub need_fields_for_coverage {
+  my $self = shift;
+  my $tower = $self->tower;
+  my %fields = (
+    height    => 'Height',
+    freq_mhz  => 'Frequency',
+    direction => 'Direction',
+    downtilt  => 'Downtilt',
+    width     => 'Horiz. width',
+    v_width   => 'Vert. width',
+    margin    => 'Signal margin',
+    latitude  => 'Latitude',
+    longitude => 'Longitude',
+  );
+  my @need;
+  foreach (keys %fields) {
+    if ($self->get($_) eq '' and $tower->get($_) eq '') {
+      push @need, $fields{$_};
+    }
+  }
+  @need;
+}
+
+=item queue_generate_coverage
+
+Starts a job to recalculate the coverage map.
+
+=cut
+
+sub queue_generate_coverage {
+  my $self = shift;
+  if ( length($self->image) > 0 ) {
+    foreach (qw(image west south east north)) {
+      $self->set($_, '');
+    }
+    my $error = $self->replace;
+    return $error if $error;
+  }
+  my $job = FS::queue->new({
+      job => 'FS::tower_sector::process_generate_coverage',
+  });
+  $job->insert('_JOB', { sectornum => $self->sectornum});
+}
+
 =back
 
+=head1 SUBROUTINES
+
+=over 4
+
+=item process_generate_coverage JOB, PARAMS
+
+Queueable routine to fetch the sector coverage map from the tower mapping
+server and store it. Highly experimental. Requires L<Map::Splat> to be
+installed.
+
+PARAMS must include 'sectornum'.
+
+=cut
+
+sub process_generate_coverage {
+  my $job = shift;
+  my $param = shift;
+  warn Dumper($param);
+  $job->update_statustext('0,generating map');
+  my $sectornum = $param->{sectornum};
+  my $sector = FS::tower_sector->by_key($sectornum);
+  my $tower = $sector->tower;
+
+  load_class('Map::Splat');
+  my $splat = Map::Splat->new(
+    lon         => $tower->longitude,
+    lat         => $tower->latitude,
+    height      => ($sector->height || $tower->height || 0),
+    freq        => $sector->freq_mhz,
+    azimuth     => $sector->direction,
+    h_width     => $sector->width,
+    tilt        => $sector->downtilt,
+    v_width     => $sector->v_width,
+    max_loss    => $sector->margin,
+    min_loss    => $sector->margin - 80,
+  );
+  $splat->calculate;
+
+  my $box = $splat->box;
+  foreach (qw(west east south north)) {
+    $sector->set($_, $box->{$_});
+  }
+  $sector->set('image', $splat->mask);
+  # mask returns a PNG where everything below max_loss is solid colored,
+  # and everything above it is transparent. More useful for our purposes.
+  my $error = $sector->replace;
+  die $error if $error;
+}
+
 =head1 BUGS
 
 =head1 SEE ALSO
index 02362db..d14ac56 100644 (file)
@@ -4,6 +4,7 @@
     process_o2m => { 'table'  => 'tower_sector',
                      'fields' => [qw(
                        sectorname ip_addr height freq_mhz direction width
+                       downtilt v_width margin
                        sector_range
                      )],
                    },
index 4d8ad1e..377a33e 100644 (file)
@@ -38,7 +38,7 @@ my $m2_error_callback = sub { # reconstruct the list
   my ($cgi, $object) = @_;
 
   my @fields = qw(
-    sectorname ip_addr height freq_mhz direction width sector_range
+    sectorname ip_addr height freq_mhz direction width tilt v_width margin sector_range
   );
 
   map {
index 151d3ba..9871775 100644 (file)
@@ -56,8 +56,11 @@ tie my %label, 'Tie::IxHash',
   'height'       => 'Height',
   'freq_mhz'     => 'Freq. (MHz)',
   'direction'    => 'Direction', # or a button to set these to 0 for omni
-  'width'        => 'Width',     #
+  'downtilt'     => 'Downtilt',
+  'width'        => 'Horiz. width',
+  'v_width'      => 'Vert. width',
   'sector_range' => 'Range',
+  'margin'       => 'Signal margin (dB)',
 ;
 
 my @fields = keys %label;
diff --git a/httemplate/misc/sector-create_map.html b/httemplate/misc/sector-create_map.html
new file mode 100644 (file)
index 0000000..6af5fdd
--- /dev/null
@@ -0,0 +1,10 @@
+<% $server->process %>
+<%init>
+
+die "access denied"
+  unless $FS::CurrentUser::CurrentUser->access_right('Configuration'); # ???
+
+my $server =
+  new FS::UI::Web::JSRPC 'FS::tower_sector::process_generate_coverage', $cgi;
+
+</%init>
index 8b070eb..632a323 100644 (file)
@@ -1,5 +1,6 @@
 <%args>
 @features
+@overlays
 </%args>
 <%doc>
 Generic Google Maps front end.
@@ -24,6 +25,14 @@ Generic Google Maps front end.
       }
     }, # end of feature
   ],
+  overlays => [
+    { url => 'https://localhost/freeside/view/sector_overlay-png.html?102',
+      west  => -130.0,
+      east  => -128.0,
+      south => 10.0,
+      north => 12.0,
+    }, # make a ground overlay
+  ],
 &>
 
 </%doc>
@@ -54,6 +63,7 @@ body { height: 100%; margin: 0px; padding: 0px }
 <script type="text/javascript">
 
 var data_geojson = <% encode_json($tree) %>;
+var data_overlays = <% encode_json(\@overlays) %>;
 
 var baseStyle = {
   clickable: true,
@@ -75,6 +85,7 @@ var featureStyle = function(feature) {
 };
 
 var map;
+var overlays;
 function initMap() {
   var canvas = $('#map_canvas');
   map = new google.maps.Map(canvas[0], { zoom: 6 });
@@ -116,6 +127,14 @@ function initMap() {
     }
 
   }); // addListener()
+
+  data_overlays.forEach(function(x) {
+    var url = x.url;
+    delete x.url;
+    var overlay = new google.maps.GroundOverlay( url, x );
+    overlay.setMap(map);
+    overlays.push(overlay); 
+  });
 }
 
 $().ready( initMap );
diff --git a/httemplate/search/sector.html b/httemplate/search/sector.html
new file mode 100644 (file)
index 0000000..d039632
--- /dev/null
@@ -0,0 +1,85 @@
+<& /elements/header.html, {
+  'title'       => 'Sector coverage maps',
+  }
+&>
+<style>
+  a.createmap {
+    font-weight: bold;
+    color: blue;
+  }
+  a.viewmap {
+    font-weight: bold;
+    color: green;
+  }
+</style>
+<table class="grid">
+  <thead>
+    <tr>
+      <th>Tower / sector</th>
+      <th colspan=3>
+    </tr>
+  </thead>
+  <tbody>
+% foreach my $sector (@sectors) {
+%   my $sectornum = $sector->sectornum;
+  <tr>
+    <td>
+      <a href="<% $fsurl %>edit/tower.html?<% $sector->towernum |h %>">
+        <% $sector->description |h %>
+      </a>
+    </td>
+
+%     my @need_fields = $sector->need_fields_for_coverage;
+%     if ( @need_fields ) {
+    <td>Need fields:</td>
+    <td>
+        <% join('<br>', @need_fields) %>
+    </td>
+%     } else {
+    <td colspan="2" style="text-align: center">
+%       my $text = 'Create map';
+%       if ( length($sector->image) > 0 ) {
+%         $text = 'Reprocess';
+%       }
+        <form name="create_<% $sectornum |h %>">
+        <input type="hidden" name="sectornum" value="<% $sectornum |h %>">
+        <& /elements/progress-init.html,
+          'create_'.$sectornum,
+          [ 'sectornum' ],
+          $fsurl.'misc/sector-create_map.html',
+          { 'message' => 'Map generated' },
+          "sector$sectornum"
+        &>
+        <a class="createmap" href="#" onclick="sector<% $sectornum %>process()">
+          <% $text %>
+        </a>
+%     }
+    </td>
+    <td>
+%   if ( length($sector->image) > 0 ) {
+      <a class="viewmap" href="<% $fsurl %>search/svc_broadband-map.html?sectornum=<% $sectornum %>">
+        View map
+      </a>
+%   }
+    </td>
+  </tr>
+% } # foreach $sector
+  </tbody>
+</table>
+<& /elements/footer.html &>
+<%init>
+
+die "access denied"
+  unless $FS::CurrentUser::CurrentUser->access_right('Configuration');
+
+my $query = {
+  table   => 'tower_sector',
+  select  => 'tower_sector.*,
+              tower.latitude, tower.longitude, tower.color, tower.towername',
+  extra_sql => ' WHERE tower.disabled is null',
+  addl_from => ' JOIN tower USING (towernum)',
+  order_by  => ' ORDER BY towername, sectorname',
+};
+
+my @sectors = qsearch($query);
+</%init>
index 4c660b0..64a7f98 100755 (executable)
@@ -1,6 +1,6 @@
 <& /elements/header.html, 'Broadband Search Results' &>
   
-<& elements/gmap.html, features => \@features &>
+<& elements/gmap.html, features => \@features, overlays => \@overlays &>
 
 <& /elements/footer.html &>
 <%init>
@@ -76,7 +76,6 @@ foreach my $svc_broadband (@rows) {
 
   }
 
-  my $tower = $towers{$towernum};
   if ( $tower->latitude and $tower->longitude ) {
     push @features,
     {
@@ -146,6 +145,19 @@ foreach my $tower (values(%towers)) {
   };
 }
 
+my @overlays;
+foreach my $sector (values %sectors) {
+  if ( length($sector->image) > 0 ) {
+    push @overlays,
+      { url => $fsurl.'view/sector_map-png.cgi?' . $sector->sectornum,
+        west => $sector->west,
+        east => $sector->east,
+        south => $sector->south,
+        north => $sector->north,
+      };
+  };
+};
+
 </%init>
 <%def .svc_broadband>
 % my $svc = shift;
diff --git a/httemplate/view/sector_map-png.cgi b/httemplate/view/sector_map-png.cgi
new file mode 100644 (file)
index 0000000..7e7e799
--- /dev/null
@@ -0,0 +1,8 @@
+<%init>
+my ($sectornum) = $cgi->keywords;
+my $sector = FS::tower_sector->by_key($sectornum);
+if ( $sector and length($sector->image) > 0 ) {
+  http_header('Content-Type', 'image/png');
+  $m->print($sector->image);
+}
+</%init>