export tower/sector data to TowerCoverage API, #39776
authorMark Wells <mark@freeside.biz>
Tue, 11 Oct 2016 06:54:05 +0000 (23:54 -0700)
committerMark Wells <mark@freeside.biz>
Tue, 11 Oct 2016 06:54:05 +0000 (23:54 -0700)
FS/FS/Schema.pm
FS/FS/hardware_type.pm
FS/FS/part_export/tower_towercoverage.pm [new file with mode: 0644]
FS/FS/tower_sector.pm
httemplate/edit/process/tower.html
httemplate/elements/tr-tower_sector.html [deleted file]
httemplate/elements/tr-tower_sectors.html
httemplate/search/tower-map.html

index 66b9a51..a1615b7 100644 (file)
@@ -4139,6 +4139,7 @@ sub tables_hashref {
         'classnum',    'int',     '',      '', '', '',
         'model',   'varchar',     '', $char_d, '', '',
         'revision','varchar', 'NULL', $char_d, '', '',
+        'title',   'varchar', 'NULL', $char_d, '', '', # external id
       ],
       'primary_key'  => 'typenum',
       'unique'       => [ [ 'classnum', 'model', 'revision' ] ],
@@ -4886,6 +4887,10 @@ sub tables_hashref {
         'sector_range', 'decimal', 'NULL',      '', '', '',  #?
         'downtilt',     'decimal', 'NULL',      '', '', '',
         'v_width',          'int', 'NULL',      '', '', '',
+        'power',        'decimal', 'NULL',      '', '', '',
+        'line_loss',    'decimal', 'NULL',      '', '', '',
+        'antenna_gain', 'decimal', 'NULL',     '', '', '',
+        'hardware_typenum', 'int', 'NULL',     '', '', '',
         'db_high',          'int', 'NULL',     '', '', '',
         'db_low',           'int', 'NULL',     '', '', '',
         'image',           'blob', 'NULL',     '', '', '',
@@ -4893,6 +4898,8 @@ sub tables_hashref {
         'east',         'decimal', 'NULL', '10,7', '', '',
         'south',        'decimal', 'NULL', '10,7', '', '',
         'north',        'decimal', 'NULL', '10,7', '', '',
+
+        'title',        'varchar', 'NULL', $char_d,'', '',
      ],
       'primary_key'  => 'sectornum',
       'unique'       => [ [ 'towernum', 'sectorname' ], [ 'ip_addr' ], ],
@@ -4901,6 +4908,10 @@ sub tables_hashref {
                           { columns    => [ 'towernum' ],
                             table      => 'tower',
                           },
+                          { columns    => [ 'hardware_typenum' ],
+                            table      => 'hardware_type',
+                            references => [ 'typenum' ],
+                          },
                         ],
     },
 
index 615c314..8547312 100644 (file)
@@ -40,6 +40,8 @@ to which this device type belongs.
 
 =item revision - revision name/number, subordinate to model
 
+=item title - external ID
+
 =back
 
 =head1 METHODS
@@ -104,6 +106,7 @@ sub check {
     || $self->ut_foreign_key('classnum', 'hardware_class', 'classnum')
     || $self->ut_text('model')
     || $self->ut_textn('revision')
+    || $self->ut_textn('title')
   ;
   return $error if $error;
 
diff --git a/FS/FS/part_export/tower_towercoverage.pm b/FS/FS/part_export/tower_towercoverage.pm
new file mode 100644 (file)
index 0000000..5d3f835
--- /dev/null
@@ -0,0 +1,420 @@
+package FS::part_export::tower_towercoverage;
+
+use strict;
+use base qw( FS::part_export );
+use FS::Record qw(qsearch qsearchs dbh);
+use FS::hardware_class;
+use FS::hardware_type;
+
+use vars qw( %options %info
+             %frequency_id %antenna_type_id );
+
+use Color::Scheme;
+use LWP::UserAgent;
+use XML::LibXML::Simple qw(XMLin);
+use Data::Dumper;
+
+# note this is not https
+our $base_url = 'http://api.towercoverage.com/towercoverage.asmx/';
+
+our $DEBUG = 0;
+our $me = '[towercoverage.com]';
+
+sub debug {
+  warn "$me ".join("\n",@_)."\n"
+    if $DEBUG;
+}
+
+# hardware class to use for antenna defs
+my $classname = 'TowerCoverage.com antenna';
+
+tie %options, 'Tie::IxHash', (
+  'debug'       => { label => 'Enable debugging', type => 'checkbox' },
+
+  'Account'     => { label  => 'Account ID' },
+  'key'         => { label  => 'API key' },
+  'use_coverage'  => { label => 'Enable coverage maps', type => 'checkbox' },
+  'FrequencyID' => { label    => 'Frequency band',
+                     type     => 'select',
+                     options  => [ keys(%frequency_id) ],
+                     option_labels => \%frequency_id,
+                   },
+  'MaximumRange'  => { label => 'Maximum range (miles)', default => '10' },
+  '1'           => { type => 'title', label => 'Client equipment' },
+  'ClientAverageAntennaHeight' => { label => 'Typical antenna height (feet)' },
+  'ClientAntennaGain'   => { label => 'Antenna gain (dB)' },
+  'RxLineLoss'          => { label => 'Line loss (dB)',
+                             default => 0,
+                           },
+  '2'           => { type => 'title', label => 'Performance requirements' },
+  'WeakRxThreshold'     => { label => 'Low quality (dBm)', },
+  'StrongRxThreshold'   => { label => 'High quality (dBm)', },
+  'RequiredReliability' => { label => 'Reliability %',
+                             default => 70
+                           },
+);
+
+%info = (
+  'svc'     => [qw( tower_sector )],
+  'desc'    => 'TowerCoverage.com coverage mapping and site qualification',
+  'options' => \%options,
+  'no_machine' => 1,
+  'notes'   => <<'END',
+Export tower/sector configurations to TowerCoverage.com for coverage map
+generation.
+END
+);
+
+sub insert {
+  my $self = shift;
+
+  my $oldAutoCommit = $FS::UID::AutoCommit;
+  local $FS::UID::AutoCommit = 0;
+
+  my $error = $self->SUPER::insert(@_);
+  return $error if $error;
+
+  my $hwclass = _hardware_class();
+  if (!$hwclass) {
+
+    $hwclass = FS::hardware_class->new({ classname => $classname });
+    $error = $hwclass->insert;
+    if ($error) {
+      dbh->rollback if $oldAutoCommit;
+      return "error creating hardware class for antenna types: $error";
+    }
+
+    foreach my $id (keys %antenna_type_id) {
+      my $name = $antenna_type_id{$id};
+      my $hardware_type = FS::hardware_type->new({
+        classnum  => $hwclass->classnum,
+        model     => $name,
+        title     => $id,
+      });
+      $error = $hardware_type->insert;
+      if ($error) {
+        dbh->rollback if $oldAutoCommit;
+        return "error creating hardware class for antenna types: $error";
+      }
+    }
+  }
+  dbh->commit if $oldAutoCommit;
+  '';
+}
+
+sub export_insert {
+  my ($self, $sector) = @_;
+
+  return unless $self->option('use_coverage');
+  local $DEBUG = $self->option('debug') ? 1 : 0;
+
+  my $tower = $sector->tower;
+  my $height_m = sprintf('%.0f', ($sector->height || $tower->height) / 3.28);
+  my $clientheight_m = sprintf('%.0f', $self->option('ClientAverageAntennaHeight') / 3.28);
+  my $maximumrange_km = sprintf('%.0f', $self->option('MaximumRange') * 1.61);
+  my $strongmargin = $self->option('StrongRxThreshold')
+                   - $self->option('WeakRxThreshold');
+
+  my $scheme = Color::Scheme->new->from_hex($tower->color || '00FF00');
+
+  my $antenna = qsearchs('hardware_type', {
+    typenum => $sector->hardware_typenum
+  });
+  return "antenna type required" unless $antenna;
+
+  # - ALL parameters must be present (or it throws a generic 500 error).
+  # - ONLY Coverageid and TowerSiteid are allowed to be empty.
+  # - ALL parameter names are case sensitive.
+  # - ALL numeric parameters are required to be integers, except for the
+  #   coordinates, line loss factors, and center frequency.
+  # - Export options (like RxLineLoss) have a problem where if they're set
+  #   to numeric zero, they get removed; make sure we actually send zero.
+  my $data = [
+    'Account'                     => $self->option('Account'),
+    'key'                         => $self->option('key'),
+    'Coverageid'                  => $sector->title,
+    'Coveragename'                => $sector->description,
+    'TowerSiteid'                 => '',
+    'Latitude'                    => $tower->latitude,
+    'Longitude'                   => $tower->longitude,
+    'AntennaHeight'               => $height_m,
+    'ClientAverageAntennaHeight'  => $clientheight_m,
+    'ClientAntennaGain'           => $self->option('ClientAntennaGain'),
+    'RxLineLoss'                  => sprintf('%.1f', $self->option('RxLineLoss')),
+    'AntennaType'                 => $antenna->title,
+    'AntennaAzimuth'              => int($sector->direction),
+    # note that TowerCoverage bases their coverage map on the antenna
+    # radiation pattern, not on this number.
+    'BeamwidthFilter'             => $sector->width,
+    'AntennaTilt'                 => int($sector->downtilt),
+    'AntennaGain'                 => int($sector->antenna_gain),
+    'Frequency'                   => $self->option('FrequencyID'),
+    'ExactCenterFrequency'        => $sector->freq_mhz,
+    'TXPower'                     => int($sector->power),
+    'TxLineLoss'                  => sprintf('%.1f', $sector->line_loss),
+    'RxThreshold'                 => $self->option('WeakRxThreshold'),
+    'RequiredReliability'         => $self->option('RequiredReliability'),
+    'StrongSignalMargin'          => $strongmargin,
+    'StrongSignalColor'           => ($scheme->colors)[0],
+    'WeakSignalColor'             => ($scheme->colors)[2],
+    'Opacity'                     => 50,
+    'MaximumRange'                => $maximumrange_km,
+    # this could be selectable but there's no reason to do that
+    'RenderingQuality'            => 3,
+    'UseLandCover'                => 1,
+    'UseTwoRays'                  => 1,
+    'CreateViewshed'              => 0,
+  ];
+  debug Dumper($data);
+  $self->http_queue(
+    'action'    => 'insert',
+    'path'      => 'CoverageAPI',
+    'sectornum' => $sector->sectornum,
+    'data'      => $data
+  );
+
+}
+
+sub export_replace { # do the same thing as insert
+  my $self = shift;
+  $self->export_insert(@_);
+}
+
+sub export_delete { '' }
+
+=item http_queue
+
+Queue a job to send an API request.
+Takes arguments:
+'action'    => what we're doing (for triggering after_* callback)
+'path'      => the path under TowerCoverage.asmx/
+'sectornum' => the sectornum
+'data'      => arrayref/hashref of params to send 
+to which it will add
+'exportnum' => the exportnum
+
+=cut
+sub http_queue {
+  my $self = shift;
+  my $queue = new FS::queue { 'job' => "FS::part_export::tower_towercoverage::http" };
+  return $queue->insert(
+    exportnum => $self->exportnum,
+    @_
+  );
+}
+
+sub http {
+  my %params = @_;
+  my $self = FS::part_export->by_key($params{'exportnum'});
+  local $DEBUG = $self->option('debug') ? 1 : 0;
+
+  local $FS::tower_sector::noexport_hack = 1; # avoid recursion
+
+  my $url = $base_url . $params{'path'};
+
+  my $ua = LWP::UserAgent->new;
+
+  # URL is the same for insert and replace.
+  my $req = HTTP::Request::Common::POST( $url, $params{'data'} );
+  debug("sending $url", $req->content);
+  my $response = $ua->request($req);
+
+  die $response->error_as_HTML if $response->is_error;
+  debug "received ".$response->decoded_content;
+
+  # throws exception on parse error
+  my $response_data = XMLin($response->decoded_content);
+  my $method = "after_" . $params{action};
+  if ($self->can($method)) {
+    # should be some kind of event handler, that would be sweet
+    my $sector = FS::tower_sector->by_key($params{'sectornum'});
+    $self->$method($sector, $response_data);
+  }
+}
+
+sub after_insert {
+  my ($self, $sector, $data) = @_;
+  my ($png_path, $kml_path) = split("\n", $data->{content});
+  die "$me no coverage map paths in response\n" unless $png_path;
+  if ( $png_path =~ /(\d+).png$/ ) {
+    $sector->set('title', $1);
+    my $error = $sector->replace;
+    die $error if $error;
+  } else {
+    die "$me can't parse map path '$png_path'\n";
+  }
+}
+
+sub _hardware_class {
+  qsearchs( 'hardware_class', { classname => $classname });
+}
+
+sub get_antenna_types {
+  my $hardware_class = _hardware_class() or return;
+  # return hardware typenums, not TowerCoverage IDs.
+  tie my %t, 'Tie::IxHash';
+
+  foreach my $type (qsearch({
+    table     => 'hardware_type',
+    hashref   => { 'classnum' => $hardware_class->classnum },
+    order_by  => ' order by title::integer'
+  })) {
+    $t{$type->typenum} = $type->model;
+  }
+
+  return \%t;
+}
+
+sub export_links {
+  my $self = shift;
+  my ($sector, $arrayref) = @_;
+  if ( $sector->title =~ /^\d+$/ ) {
+    my $link = "http://www.towercoverage.com/En-US/Dashboard/editcoverages/".
+               $sector->title;
+    push @$arrayref, qq!<a href="$link" target="_blank">TowerCoverage map</a>!;
+  }
+}
+
+# we can query this from them, but that requires the account id and key...
+# XXX do some jquery magic in the UI to grab the account ID and key from
+# those fields, and then look it up right there
+
+BEGIN {
+  tie our %frequency_id, 'Tie::IxHash', (
+    1 => "2400 MHz",
+    2 => "5700 MHz",
+    3 => "5300 MHz",
+    4 => "900 MHz",
+    5 => "3650 MHz",
+    12 => "584 MHz",
+    13 => "24000 MHz",
+    14 => "11000 MHz Licensed",
+    15 => "815 MHz",
+    16 => "860 MHz",
+    17 => "1800 MHz CDMA 3G",
+    18 => "18000 MHz Licensed",
+    19 => "1700 MHz",
+    20 => "2100 MHz AWS",
+    21 => "2500-2700 MHz EBS/BRS",
+    22 => "6000 MHz Licensed",
+    23 => "476 MHz",
+    24 => "4900 MHz - Public Safety",
+    25 => "2300 MHz",
+    28 => "7000 MHz 4PSK",
+    29 => "12000 MHz 4PSK",
+    30 => "60 MHz",
+    31 => "260 MHz",
+    32 => "70 MHz",
+    34 => "155 MHz",
+    35 => "365 MHz",
+    36 => "435 MHz",
+    38 => "3500 MHz",
+    39 => "750 MHz",
+    40 => "27 MHz",
+    41 => "10000 MHz",
+    42 => "10250 Mhz",
+    43 => "10250 Mhz",
+    44 => "160 MHz",
+    45 => "700 MHz",
+    46 => "722 MHz",
+    47 => "38000 Mhz",
+    49 => "551 MHz",
+    50 => "600 MHz",
+    51 => "2300 MHz",
+    52 => "5100 MHz",
+    53 => "1900Mhz",
+  );
+
+  # there has to be a better way to handle this. load it during upgrade?
+  # provide a proxy method like get_dids?
+
+  tie our %antenna_type_id, 'Tie::IxHash', (
+    1 => 'Generic - Omni',
+    5 => 'Generic - 120 Degree',
+    8 => 'Generic - 45 Degree Panel',
+    9 => 'Generic - 60 Degree Panel',
+    10 => 'Generic - 60 Degree x 8 Sectors',
+    11 => 'Generic - 90 Degree',
+    12 => 'Alvarion 3.65 WiMax Base Satation',
+    24 => 'Tranzeo - 3.5 GHz 17db 60 Sector',
+    31 => 'Alpha - 2.3 2033 Omni',
+    32 => "PMP450 - 60&deg; Sector",
+    33 => "PMP450 - 90&deg; Sector",
+    34 => 'PMP450 - SM Panel',
+    36 => 'KPPA - 2GHZDP90S-45 17 dBi',
+    37 => 'KPPA - 2GHZDP120S-45 14.2 dBi',
+    38 => 'KPPA - 3GHZDP60S-45 16.3 dBi',
+    39 => 'KPPA - 3GHZDP90S-45 16.7 dBi',
+    40 => 'KPPA - 3GHZDP120S-45 14.8 dBi',
+    41 => 'KPPA - 5GHZDP40S-17 18.2 dBi',
+    42 => 'KPPA - 5GHZDP60S 17.7 dBi',
+    43 => 'KPPA - 5GHZDP60S-17 18.2 dBi',
+    44 => 'KPPA - 5GHZDP90S 17 dBi',
+    45 => 'KPPA - 5GHZDP120S 16.3 dBi',
+    46 => 'KPPA - OMNI-DP-2 13 dBi',
+    47 => 'KPPA - OMNI-DP-2.4-45 10.7 dBi',
+    48 => 'KPPA - OMNI-DP-3 13 dBi',
+    49 => 'KPPA - OMNI-DP-3-45 11 dBi',
+    51 => 'KPPA - OMNI-DP-5 14 dBi',
+    53 => 'Telrad - 65 Degree 3.65 Ghz',
+    54 => 'KPPA - 2GHZDP60S-17-45 15.1 dBi',
+    55 => 'KPPA - 2GHZDP60S-45 17.9 dBi',
+    56 => 'UBNT - AG-2G20',
+    57 => 'UBNT - AG-5G23',
+    58 => 'UBNT - AG-5G27',
+    59 => 'UBNT - AM-2G15-120',
+    60 => 'UBNT - AM-2G16-90',
+    61 => 'UBNT - AM-3G18-120',
+    62 => 'UBNT - AM-5G16-120',
+    63 => 'UBNT - AM-5G17-90',
+    64 => 'UBNT - AM-5G19-120',
+    65 => 'UBNT - AM-5G20-90',
+    66 => 'UBNT - AM-9G15-90',
+    67 => 'UBNT - AMO-2G10',
+    68 => 'UBNT - AMO-2G13',
+    69 => 'UBNT - AMO-5G10',
+    70 => 'UBNT - AMO-5G13',
+    71 => 'UBNT - AMY-9M16',
+    72 => 'UBNT - LOCOM2',
+    73 => 'UBNT - LOCOM5',
+    74 => 'UBNT - LOCOM9',
+    75 => 'UBNT - NB-2G18',
+    76 => 'UBNT - NB-5G22',
+    77 => 'UBNT - NB-5G25',
+    78 => 'UBNT - NBM3',
+    79 => 'UBNT - NBM9',
+    80 => 'UBNT - NSM2',
+    81 => 'UBNT - NSM3',
+    82 => 'UBNT - NSM5',
+    83 => 'UBNT - NSM9',
+    84 => 'UBNT - PBM3',
+    85 => 'UBNT - PBM5',
+    86 => 'UBNT - PBM10',
+    87 => 'UBNT - RD-2G23',
+    88 => 'UBNT - RD-3G25',
+    89 => 'UBNT - RD-5G30',
+    90 => 'UBNT - RD-5G34',
+    92 => 'TerraWave - 2.3-2.7 18db 65-Degree Panel',
+    93 => 'UBNT - AM-M521-60-AC',
+    94 => 'UBNT - AM-M522-45-AC',
+    101 => 'RF Elements - SH-TP-5-30',
+    104 => 'RF Elements - SH-TP-5-40',
+    105 => 'RF Elements - SH-TP-5-50',
+    106 => 'RF Elements - SH-TP-5-60',
+    107 => 'RF Elements - SH-TP-5-70',
+    108 => 'RF Elements - SH-TP-5-80',
+    109 => 'RF Elements - SH-TP-5-90',
+    110 => 'UBNT - Test',
+    111 => '60 Titanium',
+    112 => '3.65GHz - 6x6',
+    113 => 'AW3015-t0-c4(EOS)',
+    114 => 'AW3035 (EOS)',
+    122 => 'RF Elements - SEC-CC-5-20',
+    135 => 'RF Elements - SEC-CC-2-14',
+    137 => 'RF Elements - SEC-CC-5-17',
+    168 => 'KPPA - Mimosa - 5GHZZHV4P65S-17',
+  );
+}
+
+1;
index 08e8cc0..2e92323 100644 (file)
@@ -1,6 +1,7 @@
 package FS::tower_sector;
 use base qw( FS::Record );
 
+use FS::Record qw(dbh qsearch);
 use Class::Load qw(load_class);
 use File::Path qw(make_path);
 use Data::Dumper;
@@ -8,6 +9,8 @@ use Cpanel::JSON::XS;
 
 use strict;
 
+our $noexport_hack = 0;
+
 =head1 NAME
 
 FS::tower_sector - Object methods for tower_sector records
@@ -118,9 +121,26 @@ otherwise returns false.
 
 sub insert {
   my $self = shift;
+
+  my $oldAutoCommit = $FS::UID::AutoCommit;
+  local $FS::UID::AutoCommit = 0;
+  my $dbh = dbh;
+
   my $error = $self->SUPER::insert;
   return $error if $error;
 
+  unless ($noexport_hack) {
+    foreach my $part_export ($self->part_export) {
+      my $error = $part_export->export_insert($self);
+      if ( $error ) {
+        $dbh->rollback if $oldAutoCommit;
+        return "exporting to ".$part_export->exporttype.
+               " (transaction rolled back): $error";
+      }
+    }
+  }
+
+  # XXX exportify
   if (scalar($self->need_fields_for_coverage) == 0) {
     $self->queue_generate_coverage;
   }
@@ -128,7 +148,27 @@ sub insert {
 
 sub replace {
   my $self = shift;
+
+  my $oldAutoCommit = $FS::UID::AutoCommit;
+  local $FS::UID::AutoCommit = 0;
+  my $dbh = dbh;
+
   my $old = shift || $self->replace_old;
+  my $error = $self->SUPER::replace($old);
+  return $error if $error;
+
+  unless ( $noexport_hack ) {
+    foreach my $part_export ($self->part_export) {
+      my $error = $part_export->export_replace($self, $old);
+      if ( $error ) {
+        $dbh->rollback if $oldAutoCommit;
+        return "exporting to ".$part_export->exporttype.
+               " (transaction rolled back): $error";
+      }
+    }
+  }
+
+  #XXX exportify
   my $regen_coverage = 0;
   if ( !$self->get('no_regen') ) {
     foreach (qw(height freq_mhz direction width downtilt
@@ -138,8 +178,6 @@ sub replace {
     }
   }
 
-  my $error = $self->SUPER::replace($old);
-  return $error if $error;
 
   if ($regen_coverage) {
     $self->queue_generate_coverage;
@@ -155,11 +193,31 @@ Delete this record from the database.
 sub delete {
   my $self = shift;
 
+  my $oldAutoCommit = $FS::UID::AutoCommit;
+  local $FS::UID::AutoCommit = 0;
+  my $dbh = dbh;
+
   #not the most efficient, not not awful, and its not like deleting a sector
   # with customers is a common operation
   return "Can't delete a sector with customers" if $self->svc_broadband;
 
-  $self->SUPER::delete;
+  unless ($noexport_hack) {
+    foreach my $part_export ($self->part_export) {
+      my $error = $part_export->export_delete($self);
+      if ( $error ) {
+        $dbh->rollback if $oldAutoCommit;
+        return "exporting to ".$part_export->exporttype.
+               " (transaction rolled back): $error";
+      }
+    }
+  }
+
+  my $error = $self->SUPER::delete;
+  if ( $error ) {
+    $dbh->rollback if $oldAutoCommit;
+    return $error;
+  }
+
 }
 
 =item check
@@ -185,13 +243,19 @@ sub check {
     || $self->ut_numbern('v_width')
     || $self->ut_numbern('downtilt')
     || $self->ut_floatn('sector_range')
-    || $self->ut_numbern('db_high')
-    || $self->ut_numbern('db_low')
+    || $self->ut_decimaln('power')
+    || $self->ut_decimaln('line_loss')
+    || $self->ut_decimaln('antenna_gain')
+    || $self->ut_numbern('hardware_typenum')
+    || $self->ut_textn('title')
+    # all of these might get relocated as part of coverage refactoring
     || $self->ut_anything('image')
     || $self->ut_sfloatn('west')
     || $self->ut_sfloatn('east')
     || $self->ut_sfloatn('south')
     || $self->ut_sfloatn('north')
+    || $self->ut_numbern('db_high')
+    || $self->ut_numbern('db_low')
   ;
   return $error if $error;
 
@@ -229,6 +293,7 @@ Returns a list of required fields for the coverage map that aren't yet filled.
 =cut
 
 sub need_fields_for_coverage {
+  # for now assume exports require all of this
   my $self = shift;
   my $tower = $self->tower;
   my %fields = (
@@ -238,7 +303,8 @@ sub need_fields_for_coverage {
     downtilt  => 'Downtilt',
     width     => 'Horiz. width',
     v_width   => 'Vert. width',
-    db_high   => 'High quality',
+    db_high   => 'High quality signal margin',
+    db_low    => 'Low quality signal margin',
     latitude  => 'Latitude',
     longitude => 'Longitude',
   );
@@ -257,10 +323,12 @@ Starts a job to recalculate the coverage map.
 
 =cut
 
+# XXX move to an export
+
 sub queue_generate_coverage {
   my $self = shift;
   my $need_fields = join(',', $self->need_fields_for_coverage);
-  return "Sector needs fields $need_fields" if $need_fields;
+  return "$need_fields required" if $need_fields;
   $self->set('no_regen', 1); # avoid recursion
   if ( length($self->image) > 0 ) {
     foreach (qw(image west south east north)) {
@@ -277,6 +345,28 @@ sub queue_generate_coverage {
 
 =back
 
+=head1 CLASS METHODS
+
+=over 4
+
+=item part_export
+
+Returns all sector exports. Eventually this may be refined to the level
+of enabling exports on specific sectors.
+
+=cut
+
+sub part_export {
+  my $info = $FS::part_export::exports{'tower_sector'} or return;
+  my @exporttypes = map { dbh->quote($_) } keys %$info or return;
+  qsearch({
+    'table'     => 'part_export',
+    'extra_sql' => 'WHERE exporttype IN(' . join(',', @exporttypes) . ')'
+  });
+}
+
+=back
+
 =head1 SUBROUTINES
 
 =over 4
index 588a68e..cfbb4ff 100644 (file)
@@ -4,7 +4,8 @@
     process_o2m => { 'table'  => 'tower_sector',
                      'fields' => [qw(
                        sectorname ip_addr height freq_mhz direction width
-                       downtilt v_width db_high db_low 
+                       downtilt v_width db_high db_low power line_loss
+                       antenna_gain hardware_typenum
                        sector_range
                      )],
                    },
diff --git a/httemplate/elements/tr-tower_sector.html b/httemplate/elements/tr-tower_sector.html
deleted file mode 100644 (file)
index 871c7fd..0000000
+++ /dev/null
@@ -1,24 +0,0 @@
-%   unless ( $opt{'js_only'} ) {
-
-      <% include('tr-td-label.html', %opt) %>
-        <TD <% $cell_style %>>
-
-%   }
-%
-            <% include( '/elements/sector.html', %opt ) %>
-%
-%   unless ( $opt{'js_only'} ) {
-
-        </TD>
-      </TR>
-
-%   }
-<%init>
-
-my( %opt ) = @_;
-
-my $cell_style = $opt{'cell_style'} ? 'STYLE="'. $opt{'cell_style'}. '"' : '';
-
-$opt{'label'} ||= 'Sector';
-
-</%init>
index 4e8f3fb..106fc76 100644 (file)
@@ -1,3 +1,11 @@
+<%shared>
+# kind of a hack...
+my ($export) = FS::tower_sector->part_export;
+my $antenna_types; # will be an ordered hash
+if ($export and $export->can('get_antenna_types')) {
+  $antenna_types = $export->get_antenna_types;
+}
+</%shared>
 <%init>
 my %opt = @_;
 my $tower = $opt{'object'};
@@ -7,8 +15,9 @@ my $cgi = $opt{'cgi'};
 my $tabcounter = 0;
 
 my @fields = qw(
-  sectorname ip_addr height freq_mhz direction width tilt v_width db_high
-  db_low sector_range
+  sectorname ip_addr height freq_mhz direction width downtilt v_width
+  db_high db_low sector_range
+  power line_loss antenna_gain hardware_typenum
 );
 
 my @sectors;
@@ -74,6 +83,11 @@ my $id = $opt{id} || $opt{field} || 'sectornum';
     border: none;
     text-align: left;
   }
+  .ui-tabs p {
+    margin-top: 8px;
+    margin-bottom: 8px;
+  }
+
 </style>
 
 
@@ -216,6 +230,38 @@ $(function() {
   </p>
 
   <p>
+    <label for="<% $id %>_power"><% emt('Transmit power') %></label>
+    <input size="3"
+           id="<% $id %>_power"
+           name="<% $id %>_power"
+           value="<% $sector->power |h %>">
+    <% emt('dBm') %><br>
+    <label for="<% $id %>_antenna_gain">+ </label>
+    <input size="3"
+           id="<% $id %>_antenna_gain"
+           name="<% $id %>_antenna_gain"
+           value="<% $sector->antenna_gain |h %>">
+    <% emt('dB antenna gain') %><br>
+    <label for="<% $id %>_line_loss">&ndash; </label>
+    <input size="3"
+           id="<% $id %>_line_loss"
+           name="<% $id %>_line_loss"
+           value="<% $sector->line_loss |h %>">
+    <% emt('dB line loss') %>
+
+% if ( $antenna_types ) {
+  <p>
+    <label for="<% $id %>_hardware_typenum"><% emt('Antenna type') %></label>
+    <& /elements/select.html,
+      field   => $id.'_hardware_typenum',
+      options => [ '', keys %$antenna_types ],
+      labels  => $antenna_types,
+      curr_value => $sector->hardware_typenum,
+    &>
+  </p>
+% }
+% # this next section might not be necessary if you enter an antenna type
+  <p> 
     <label for="<% $id %>_width"><% emt('Horizontal beam') %></label>
     <input size="3"
            id="<% $id %>_width"
@@ -229,7 +275,7 @@ $(function() {
   </p>
 
   <label><% emt('Signal margin') %></label>
-      <div style="display: inline-block; vertical-align: top">
+  <div style="display: inline-block; vertical-align: top">
       <input class="dbspinner"
              size="4"
              id="<% $id %>_db_high"
@@ -244,7 +290,7 @@ $(function() {
              name="<% $id %>_db_low"
              value="<% $sector->db_low |h %>">
       <% emt('dB (low quality)') %>
-      </div>
+  </div>
 
 </div>
 </%def>
index 559d83d..d87e19e 100755 (executable)
@@ -8,6 +8,9 @@ html { height: 100% }
 span.is_up { font-weight: bold; color: green }
 span.is_down { font-weight: bold; color: red }
 #search_location { width: 300px }
+
+.sector_list li { list-style: none }
+.sector_list li a { width: 150px }
 </style>
 
 <div id="map_canvas"></div>
@@ -300,4 +303,16 @@ Tower #<% $tower->towernum %> | <% $tower->towername %>
 <br>
 <input type="checkbox" name="show_coverage" value="<% $tower->towernum %>">
 <% emt('Show coverage') %>
+<ul class="sector_list">
+% foreach my $sector ($tower->tower_sector) {
+%   # could be more descriptive here
+  <li><% emt($sector->sectorname) %>
+%   my @links_array;
+%   foreach my $export ($sector->part_export) {
+%     $export->export_links($sector, \@links_array); # already HTML, do not escape
+%   }
+<% join(' ', @links_array) %>
+  </li>
+% }
+</ul>
 </%def>