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.
106 Creates a new sector. To add the sector to the database, see L<"insert">.
108 Note that this stores the hash reference, not a distinct copy of the hash it
109 points to. You can ask the object for a copy with the I<hash> method.
113 sub table { 'tower_sector'; }
117 Adds this record to the database. If there is an error, returns the error,
118 otherwise returns false.
125 my $oldAutoCommit = $FS::UID::AutoCommit;
126 local $FS::UID::AutoCommit = 0;
129 my $error = $self->SUPER::insert;
130 return $error if $error;
132 unless ($noexport_hack) {
133 foreach my $part_export ($self->part_export) {
134 my $error = $part_export->export_insert($self);
136 $dbh->rollback if $oldAutoCommit;
137 return "exporting to ".$part_export->exporttype.
138 " (transaction rolled back): $error";
144 if (scalar($self->need_fields_for_coverage) == 0) {
145 $self->queue_generate_coverage;
152 my $oldAutoCommit = $FS::UID::AutoCommit;
153 local $FS::UID::AutoCommit = 0;
156 my $old = shift || $self->replace_old;
157 my $error = $self->SUPER::replace($old);
158 return $error if $error;
160 unless ( $noexport_hack ) {
161 foreach my $part_export ($self->part_export) {
162 my $error = $part_export->export_replace($self, $old);
164 $dbh->rollback if $oldAutoCommit;
165 return "exporting to ".$part_export->exporttype.
166 " (transaction rolled back): $error";
172 my $regen_coverage = 0;
173 if ( !$self->get('no_regen') ) {
174 foreach (qw(height freq_mhz direction width downtilt
175 v_width db_high db_low))
177 $regen_coverage = 1 if ($self->get($_) ne $old->get($_));
182 if ($regen_coverage) {
183 $self->queue_generate_coverage;
189 Delete this record from the database.
196 my $oldAutoCommit = $FS::UID::AutoCommit;
197 local $FS::UID::AutoCommit = 0;
200 #not the most efficient, not not awful, and its not like deleting a sector
201 # with customers is a common operation
202 return "Can't delete a sector with customers" if $self->svc_broadband;
204 unless ($noexport_hack) {
205 foreach my $part_export ($self->part_export) {
206 my $error = $part_export->export_delete($self);
208 $dbh->rollback if $oldAutoCommit;
209 return "exporting to ".$part_export->exporttype.
210 " (transaction rolled back): $error";
215 my $error = $self->SUPER::delete;
217 $dbh->rollback if $oldAutoCommit;
225 Checks all fields to make sure this is a valid sector. If there is
226 an error, returns the error, otherwise returns false. Called by the insert
235 $self->ut_numbern('sectornum')
236 || $self->ut_number('towernum', 'tower', 'towernum')
237 || $self->ut_text('sectorname')
238 || $self->ut_textn('ip_addr')
239 || $self->ut_floatn('height')
240 || $self->ut_numbern('freq_mhz')
241 || $self->ut_numbern('direction')
242 || $self->ut_numbern('width')
243 || $self->ut_numbern('v_width')
244 || $self->ut_numbern('downtilt')
245 || $self->ut_floatn('sector_range')
246 || $self->ut_decimaln('power')
247 || $self->ut_decimaln('line_loss')
248 || $self->ut_decimaln('antenna_gain')
249 || $self->ut_numbern('hardware_typenum')
250 || $self->ut_textn('title')
251 # all of these might get relocated as part of coverage refactoring
252 || $self->ut_anything('image')
253 || $self->ut_sfloatn('west')
254 || $self->ut_sfloatn('east')
255 || $self->ut_sfloatn('south')
256 || $self->ut_sfloatn('north')
257 || $self->ut_numbern('db_high')
258 || $self->ut_numbern('db_low')
260 return $error if $error;
267 Returns the tower for this sector, as an FS::tower object (see L<FS::tower>).
271 Returns a description for this sector including tower name.
277 if ( $self->sectorname eq '_default' ) {
278 $self->tower->towername
281 $self->tower->towername. ' sector '. $self->sectorname
287 Returns the services on this tower sector.
289 =item need_fields_for_coverage
291 Returns a list of required fields for the coverage map that aren't yet filled.
295 sub need_fields_for_coverage {
296 # for now assume exports require all of this
298 my $tower = $self->tower;
301 freq_mhz => 'Frequency',
302 direction => 'Direction',
303 downtilt => 'Downtilt',
304 width => 'Horiz. width',
305 v_width => 'Vert. width',
306 db_high => 'High quality signal margin',
307 db_low => 'Low quality signal margin',
308 latitude => 'Latitude',
309 longitude => 'Longitude',
312 foreach (keys %fields) {
313 if ($self->get($_) eq '' and $tower->get($_) eq '') {
314 push @need, $fields{$_};
320 =item queue_generate_coverage
322 Starts a job to recalculate the coverage map.
326 # XXX move to an export
328 sub queue_generate_coverage {
330 my $need_fields = join(',', $self->need_fields_for_coverage);
331 return "$need_fields required" if $need_fields;
332 $self->set('no_regen', 1); # avoid recursion
333 if ( length($self->image) > 0 ) {
334 foreach (qw(image west south east north)) {
337 my $error = $self->replace;
338 return $error if $error;
340 my $job = FS::queue->new({
341 job => 'FS::tower_sector::process_generate_coverage',
343 $job->insert('_JOB', { sectornum => $self->sectornum});
354 Returns all sector exports. Eventually this may be refined to the level
355 of enabling exports on specific sectors.
360 my $info = $FS::part_export::exports{'tower_sector'} or return;
361 my @exporttypes = map { dbh->quote($_) } keys %$info or return;
363 'table' => 'part_export',
364 'extra_sql' => 'WHERE exporttype IN(' . join(',', @exporttypes) . ')'
374 =item process_generate_coverage JOB, PARAMS
376 Queueable routine to fetch the sector coverage map from the tower mapping
377 server and store it. Highly experimental. Requires L<Map::Splat> to be
380 PARAMS must include 'sectornum'.
384 sub process_generate_coverage {
387 $job->update_statustext('0,generating map') if $job;
388 my $sectornum = $param->{sectornum};
389 my $sector = FS::tower_sector->by_key($sectornum)
390 or die "sector $sectornum does not exist";
391 $sector->set('no_regen', 1); # avoid recursion
392 my $tower = $sector->tower;
394 load_class('Map::Splat');
396 # since this is still experimental, put it somewhere we can find later
397 my $workdir = "$FS::UID::cache_dir/cache.$FS::UID::datasrc/" .
398 "generate_coverage/sector$sectornum-". time;
400 my $splat = Map::Splat->new(
401 lon => $tower->longitude,
402 lat => $tower->latitude,
403 height => ($sector->height || $tower->height || 0),
404 freq => $sector->freq_mhz,
405 azimuth => $sector->direction,
406 h_width => $sector->width,
407 tilt => $sector->downtilt,
408 v_width => $sector->v_width,
409 db_levels => [ $sector->db_low, $sector->db_high ],
411 #simplify => 0.0004, # remove stairstepping in SRTM3 data?
415 my $box = $splat->box;
416 foreach (qw(west east south north)) {
417 $sector->set($_, $box->{$_});
419 $sector->set('image', $splat->png);
420 my $error = $sector->replace;
421 die $error if $error;
423 foreach ($sector->sector_coverage) {
425 die $error if $error;
427 # XXX undecided whether Map::Splat should even do this operation
431 my $data = decode_json( $splat->polygonize_json );
432 for my $feature (@{ $data->{features} }) {
433 my $db = $feature->{properties}{level};
434 my $coverage = FS::sector_coverage->new({
435 sectornum => $sectornum,
437 geometry => encode_json($feature->{geometry})
439 $error = $coverage->insert;
442 die $error if $error;
449 L<FS::tower>, L<FS::Record>, schema.html from the base documentation.