1 package FS::tower_sector;
2 use base qw( FS::Record );
4 use FS::Record qw(dbh qsearch);
5 use Class::Load qw(load_class);
6 use File::Path qw(make_path);
12 our $noexport_hack = 0;
16 FS::tower_sector - Object methods for tower_sector records
22 $record = new FS::tower_sector \%hash;
23 $record = new FS::tower_sector { 'column' => 'value' };
25 $error = $record->insert;
27 $error = $new_record->replace($old_record);
29 $error = $record->delete;
31 $error = $record->check;
35 An FS::tower_sector object represents a tower sector. FS::tower_sector
36 inherits from FS::Record. The following fields are currently supported:
58 The height of this antenna on the tower, measured from ground level. This
59 plus the tower's altitude should equal the height of the antenna above sea
64 The band center frequency in MHz.
68 The antenna beam direction in degrees from north.
72 The -3dB horizontal beamwidth in degrees.
76 The antenna beam elevation in degrees below horizontal.
80 The -3dB vertical beamwidth in degrees.
84 The signal loss margin to treat as "high quality".
88 The signal loss margin to treat as "low quality".
92 The coverage map, as a PNG.
94 =item west, east, south, north
96 The coordinate boundaries of the coverage map.
108 down rate for sector.
118 Creates a new sector. To add the sector to the database, see L<"insert">.
120 Note that this stores the hash reference, not a distinct copy of the hash it
121 points to. You can ask the object for a copy with the I<hash> method.
125 sub table { 'tower_sector'; }
129 Adds this record to the database. If there is an error, returns the error,
130 otherwise returns false.
137 my $oldAutoCommit = $FS::UID::AutoCommit;
138 local $FS::UID::AutoCommit = 0;
141 my $error = $self->SUPER::insert;
142 return $error if $error;
144 unless ($noexport_hack) {
145 foreach my $part_export ($self->part_export) {
146 my $error = $part_export->export_insert($self);
148 $dbh->rollback if $oldAutoCommit;
149 return "exporting to ".$part_export->exporttype.
150 " (transaction rolled back): $error";
156 if (scalar($self->need_fields_for_coverage) == 0) {
157 $self->queue_generate_coverage;
164 my $oldAutoCommit = $FS::UID::AutoCommit;
165 local $FS::UID::AutoCommit = 0;
168 my $old = shift || $self->replace_old;
169 my $error = $self->SUPER::replace($old);
170 return $error if $error;
172 unless ( $noexport_hack ) {
173 foreach my $part_export ($self->part_export) {
174 my $error = $part_export->export_replace($self, $old);
176 $dbh->rollback if $oldAutoCommit;
177 return "exporting to ".$part_export->exporttype.
178 " (transaction rolled back): $error";
184 my $regen_coverage = 0;
185 if ( !$self->get('no_regen') ) {
186 foreach (qw(height freq_mhz direction width downtilt
187 v_width db_high db_low))
189 $regen_coverage = 1 if ($self->get($_) ne $old->get($_));
194 if ($regen_coverage) {
195 $self->queue_generate_coverage;
201 Delete this record from the database.
208 my $oldAutoCommit = $FS::UID::AutoCommit;
209 local $FS::UID::AutoCommit = 0;
212 #not the most efficient, not not awful, and its not like deleting a sector
213 # with customers is a common operation
214 return "Can't delete a sector with customers" if $self->svc_broadband;
216 unless ($noexport_hack) {
217 foreach my $part_export ($self->part_export) {
218 my $error = $part_export->export_delete($self);
220 $dbh->rollback if $oldAutoCommit;
221 return "exporting to ".$part_export->exporttype.
222 " (transaction rolled back): $error";
227 my $error = $self->SUPER::delete;
229 $dbh->rollback if $oldAutoCommit;
237 Checks all fields to make sure this is a valid sector. If there is
238 an error, returns the error, otherwise returns false. Called by the insert
247 $self->ut_numbern('sectornum')
248 || $self->ut_number('towernum', 'tower', 'towernum')
249 || $self->ut_text('sectorname')
250 || $self->ut_textn('ip_addr')
251 || $self->ut_floatn('height')
252 || $self->ut_numbern('freq_mhz')
253 || $self->ut_numbern('direction')
254 || $self->ut_numbern('width')
255 || $self->ut_numbern('v_width')
256 || $self->ut_numbern('downtilt')
257 || $self->ut_floatn('sector_range')
258 || $self->ut_decimaln('power')
259 || $self->ut_decimaln('line_loss')
260 || $self->ut_decimaln('antenna_gain')
261 || $self->ut_numbern('hardware_typenum')
262 || $self->ut_textn('title')
263 || $self->ut_numbern('up_rate')
264 || $self->ut_numbern('down_rate')
265 # all of these might get relocated as part of coverage refactoring
266 || $self->ut_anything('image')
267 || $self->ut_sfloatn('west')
268 || $self->ut_sfloatn('east')
269 || $self->ut_sfloatn('south')
270 || $self->ut_sfloatn('north')
271 || $self->ut_numbern('db_high')
272 || $self->ut_numbern('db_low')
274 return $error if $error;
281 Returns the tower for this sector, as an FS::tower object (see L<FS::tower>).
285 Returns a description for this sector including tower name.
291 if ( $self->sectorname eq '_default' ) {
292 $self->tower->towername
295 $self->tower->towername. ' sector '. $self->sectorname
301 Returns the services on this tower sector.
303 =item need_fields_for_coverage
305 Returns a list of required fields for the coverage map that aren't yet filled.
309 sub need_fields_for_coverage {
310 # for now assume exports require all of this
312 my $tower = $self->tower;
315 freq_mhz => 'Frequency',
316 direction => 'Direction',
317 downtilt => 'Downtilt',
318 width => 'Horiz. width',
319 v_width => 'Vert. width',
320 db_high => 'High quality signal margin',
321 db_low => 'Low quality signal margin',
322 latitude => 'Latitude',
323 longitude => 'Longitude',
326 foreach (keys %fields) {
327 if ($self->get($_) eq '' and $tower->get($_) eq '') {
328 push @need, $fields{$_};
334 =item queue_generate_coverage
336 Starts a job to recalculate the coverage map.
340 # XXX move to an export
342 sub queue_generate_coverage {
344 my $need_fields = join(',', $self->need_fields_for_coverage);
345 return "$need_fields required" if $need_fields;
346 $self->set('no_regen', 1); # avoid recursion
347 if ( length($self->image) > 0 ) {
348 foreach (qw(image west south east north)) {
351 my $error = $self->replace;
352 return $error if $error;
354 my $job = FS::queue->new({
355 job => 'FS::tower_sector::process_generate_coverage',
357 $job->insert('_JOB', { sectornum => $self->sectornum});
368 Returns all sector exports. Eventually this may be refined to the level
369 of enabling exports on specific sectors.
374 my $info = $FS::part_export::exports{'tower_sector'} or return;
375 my @exporttypes = map { dbh->quote($_) } keys %$info or return;
377 'table' => 'part_export',
378 'extra_sql' => 'WHERE exporttype IN(' . join(',', @exporttypes) . ')'
388 =item process_generate_coverage JOB, PARAMS
390 Queueable routine to fetch the sector coverage map from the tower mapping
391 server and store it. Highly experimental. Requires L<Map::Splat> to be
394 PARAMS must include 'sectornum'.
398 sub process_generate_coverage {
401 $job->update_statustext('0,generating map') if $job;
402 my $sectornum = $param->{sectornum};
403 my $sector = FS::tower_sector->by_key($sectornum)
404 or die "sector $sectornum does not exist";
405 $sector->set('no_regen', 1); # avoid recursion
406 my $tower = $sector->tower;
408 load_class('Map::Splat');
410 # since this is still experimental, put it somewhere we can find later
411 my $workdir = "$FS::UID::cache_dir/cache.$FS::UID::datasrc/" .
412 "generate_coverage/sector$sectornum-". time;
414 my $splat = Map::Splat->new(
415 lon => $tower->longitude,
416 lat => $tower->latitude,
417 height => ($sector->height || $tower->height || 0),
418 freq => $sector->freq_mhz,
419 azimuth => $sector->direction,
420 h_width => $sector->width,
421 tilt => $sector->downtilt,
422 v_width => $sector->v_width,
423 db_levels => [ $sector->db_low, $sector->db_high ],
425 #simplify => 0.0004, # remove stairstepping in SRTM3 data?
429 my $box = $splat->box;
430 foreach (qw(west east south north)) {
431 $sector->set($_, $box->{$_});
433 $sector->set('image', $splat->png);
434 my $error = $sector->replace;
435 die $error if $error;
437 foreach ($sector->sector_coverage) {
439 die $error if $error;
441 # XXX undecided whether Map::Splat should even do this operation
445 my $data = decode_json( $splat->polygonize_json );
446 for my $feature (@{ $data->{features} }) {
447 my $db = $feature->{properties}{level};
448 my $coverage = FS::sector_coverage->new({
449 sectornum => $sectornum,
451 geometry => encode_json($feature->{geometry})
453 $error = $coverage->insert;
456 die $error if $error;
463 L<FS::tower>, L<FS::Record>, schema.html from the base documentation.