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.
104 Up rate limit for sector.
106 =item down_rate_limit
108 down rate limit 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_ip46n('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_limit')
264 || $self->ut_numbern('down_rate_limit')
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) . ')'
382 =item part_export_svc_broadband
384 Returns all svc_broadband exports.
388 sub part_export_svc_broadband {
389 my $info = $FS::part_export::exports{'svc_broadband'} or return;
390 my @exporttypes = map { dbh->quote($_) } keys %$info or return;
392 'table' => 'part_export',
393 'extra_sql' => 'WHERE exporttype IN(' . join(',', @exporttypes) . ')'
403 =item process_generate_coverage JOB, PARAMS
405 Queueable routine to fetch the sector coverage map from the tower mapping
406 server and store it. Highly experimental. Requires L<Map::Splat> to be
409 PARAMS must include 'sectornum'.
413 sub process_generate_coverage {
416 $job->update_statustext('0,generating map') if $job;
417 my $sectornum = $param->{sectornum};
418 my $sector = FS::tower_sector->by_key($sectornum)
419 or die "sector $sectornum does not exist";
420 $sector->set('no_regen', 1); # avoid recursion
421 my $tower = $sector->tower;
423 load_class('Map::Splat');
425 # since this is still experimental, put it somewhere we can find later
426 my $workdir = "$FS::UID::cache_dir/cache.$FS::UID::datasrc/" .
427 "generate_coverage/sector$sectornum-". time;
429 my $splat = Map::Splat->new(
430 lon => $tower->longitude,
431 lat => $tower->latitude,
432 height => ($sector->height || $tower->height || 0),
433 freq => $sector->freq_mhz,
434 azimuth => $sector->direction,
435 h_width => $sector->width,
436 tilt => $sector->downtilt,
437 v_width => $sector->v_width,
438 db_levels => [ $sector->db_low, $sector->db_high ],
440 #simplify => 0.0004, # remove stairstepping in SRTM3 data?
444 my $box = $splat->box;
445 foreach (qw(west east south north)) {
446 $sector->set($_, $box->{$_});
448 $sector->set('image', $splat->png);
449 my $error = $sector->replace;
450 die $error if $error;
452 foreach ($sector->sector_coverage) {
454 die $error if $error;
456 # XXX undecided whether Map::Splat should even do this operation
460 my $data = decode_json( $splat->polygonize_json );
461 for my $feature (@{ $data->{features} }) {
462 my $db = $feature->{properties}{level};
463 my $coverage = FS::sector_coverage->new({
464 sectornum => $sectornum,
466 geometry => encode_json($feature->{geometry})
468 $error = $coverage->insert;
471 die $error if $error;
476 require FS::Misc::FixIPFormat;
477 FS::Misc::FixIPFormat::fix_bad_addresses_in_table(
478 'tower_sector', 'sectornum', 'ip_addr',
489 L<FS::tower>, L<FS::Record>, schema.html from the base documentation.