From 04f53daab621710db56b075e1aaf56e7c52f9ba9 Mon Sep 17 00:00:00 2001
From: Mark Wells
Date: Mon, 10 Oct 2016 23:54:05 -0700
Subject: [PATCH 1/1] export tower/sector data to TowerCoverage API, #39776
---
FS/FS/Schema.pm | 11 +
FS/FS/hardware_type.pm | 3 +
FS/FS/part_export/tower_towercoverage.pm | 420 ++++++++++++++++++++++++++++++
FS/FS/tower_sector.pm | 104 +++++++-
httemplate/edit/process/tower.html | 3 +-
httemplate/elements/tr-tower_sector.html | 24 --
httemplate/elements/tr-tower_sectors.html | 54 +++-
httemplate/search/tower-map.html | 15 ++
8 files changed, 598 insertions(+), 36 deletions(-)
create mode 100644 FS/FS/part_export/tower_towercoverage.pm
delete mode 100644 httemplate/elements/tr-tower_sector.html
diff --git a/FS/FS/Schema.pm b/FS/FS/Schema.pm
index 66b9a51c3..a1615b71a 100644
--- a/FS/FS/Schema.pm
+++ b/FS/FS/Schema.pm
@@ -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' ],
+ },
],
},
diff --git a/FS/FS/hardware_type.pm b/FS/FS/hardware_type.pm
index 615c314b0..85473125c 100644
--- a/FS/FS/hardware_type.pm
+++ b/FS/FS/hardware_type.pm
@@ -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
index 000000000..5d3f8351a
--- /dev/null
+++ b/FS/FS/part_export/tower_towercoverage.pm
@@ -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!TowerCoverage map!;
+ }
+}
+
+# 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° Sector",
+ 33 => "PMP450 - 90° 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;
diff --git a/FS/FS/tower_sector.pm b/FS/FS/tower_sector.pm
index 08e8cc0df..2e9232307 100644
--- a/FS/FS/tower_sector.pm
+++ b/FS/FS/tower_sector.pm
@@ -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
diff --git a/httemplate/edit/process/tower.html b/httemplate/edit/process/tower.html
index 588a68e8d..cfbb4ffa3 100644
--- a/httemplate/edit/process/tower.html
+++ b/httemplate/edit/process/tower.html
@@ -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
index 871c7fd9c..000000000
--- a/httemplate/elements/tr-tower_sector.html
+++ /dev/null
@@ -1,24 +0,0 @@
-% unless ( $opt{'js_only'} ) {
-
- <% include('tr-td-label.html', %opt) %>
- >
-
-% }
-%
- <% include( '/elements/sector.html', %opt ) %>
-%
-% unless ( $opt{'js_only'} ) {
-
-
-
-
-% }
-<%init>
-
-my( %opt ) = @_;
-
-my $cell_style = $opt{'cell_style'} ? 'STYLE="'. $opt{'cell_style'}. '"' : '';
-
-$opt{'label'} ||= 'Sector';
-
-%init>
diff --git a/httemplate/elements/tr-tower_sectors.html b/httemplate/elements/tr-tower_sectors.html
index 4e8f3fb47..106fc76f6 100644
--- a/httemplate/elements/tr-tower_sectors.html
+++ b/httemplate/elements/tr-tower_sectors.html
@@ -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;
+ }
+
@@ -216,6 +230,38 @@ $(function() {
+
+
+ <% emt('dBm') %>
+
+
+ <% emt('dB antenna gain') %>
+
+
+ <% emt('dB line loss') %>
+
+% if ( $antenna_types ) {
+
+ + <& /elements/select.html, + field => $id.'_hardware_typenum', + options => [ '', keys %$antenna_types ], + labels => $antenna_types, + curr_value => $sector->hardware_typenum, + &> +
+% } +% # this next section might not be necessary if you enter an antenna type +-