1 package FS::tower_sector;
3 use Class::Load qw(load_class);
4 use File::Path qw(make_path);
8 use base qw( FS::Record );
9 use FS::Record qw( qsearch qsearchs );
11 use FS::svc_broadband;
15 FS::tower_sector - Object methods for tower_sector records
21 $record = new FS::tower_sector \%hash;
22 $record = new FS::tower_sector { 'column' => 'value' };
24 $error = $record->insert;
26 $error = $new_record->replace($old_record);
28 $error = $record->delete;
30 $error = $record->check;
34 An FS::tower_sector object represents a tower sector. FS::tower_sector
35 inherits from FS::Record. The following fields are currently supported:
57 The height of this antenna on the tower, measured from ground level. This
58 plus the tower's altitude should equal the height of the antenna above sea
63 The band center frequency in MHz.
67 The antenna beam direction in degrees from north.
71 The -3dB horizontal beamwidth in degrees.
75 The antenna beam elevation in degrees below horizontal.
79 The -3dB vertical beamwidth in degrees.
83 The signal loss margin allowed on the sector, in dB. This is normally
84 transmitter EIRP minus receiver sensitivity.
88 The coverage map, as a PNG.
90 =item west, east, south, north
92 The coordinate boundaries of the coverage map.
102 Creates a new sector. To add the sector to the database, see L<"insert">.
104 Note that this stores the hash reference, not a distinct copy of the hash it
105 points to. You can ask the object for a copy with the I<hash> method.
109 sub table { 'tower_sector'; }
113 Adds this record to the database. If there is an error, returns the error,
114 otherwise returns false.
118 Delete this record from the database.
125 #not the most efficient, not not awful, and its not like deleting a sector
126 # with customers is a common operation
127 return "Can't delete a sector with customers" if $self->svc_broadband;
129 $self->SUPER::delete;
134 Checks all fields to make sure this is a valid sector. If there is
135 an error, returns the error, otherwise returns false. Called by the insert
144 $self->ut_numbern('sectornum')
145 || $self->ut_number('towernum', 'tower', 'towernum')
146 || $self->ut_text('sectorname')
147 || $self->ut_textn('ip_addr')
148 || $self->ut_floatn('height')
149 || $self->ut_numbern('freq_mhz')
150 || $self->ut_numbern('direction')
151 || $self->ut_numbern('width')
152 || $self->ut_numbern('v_width')
153 || $self->ut_numbern('downtilt')
154 || $self->ut_floatn('sector_range')
155 || $self->ut_numbern('margin')
156 || $self->ut_anything('image')
157 || $self->ut_sfloatn('west')
158 || $self->ut_sfloatn('east')
159 || $self->ut_sfloatn('south')
160 || $self->ut_sfloatn('north')
162 return $error if $error;
169 Returns the tower for this sector, as an FS::tower object (see L<FS::tower>).
175 qsearchs('tower', { 'towernum'=>$self->towernum } );
180 Returns a description for this sector including tower name.
186 if ( $self->sectorname eq '_default' ) {
187 $self->tower->towername
190 $self->tower->towername. ' sector '. $self->sectorname
196 Returns the services on this tower sector.
202 qsearch('svc_broadband', { 'sectornum' => $self->sectornum });
205 =item need_fields_for_coverage
207 Returns a list of required fields for the coverage map that aren't yet filled.
211 sub need_fields_for_coverage {
213 my $tower = $self->tower;
216 freq_mhz => 'Frequency',
217 direction => 'Direction',
218 downtilt => 'Downtilt',
219 width => 'Horiz. width',
220 v_width => 'Vert. width',
221 margin => 'Signal margin',
222 latitude => 'Latitude',
223 longitude => 'Longitude',
226 foreach (keys %fields) {
227 if ($self->get($_) eq '' and $tower->get($_) eq '') {
228 push @need, $fields{$_};
234 =item queue_generate_coverage
236 Starts a job to recalculate the coverage map.
240 sub queue_generate_coverage {
242 if ( length($self->image) > 0 ) {
243 foreach (qw(image west south east north)) {
246 my $error = $self->replace;
247 return $error if $error;
249 my $job = FS::queue->new({
250 job => 'FS::tower_sector::process_generate_coverage',
252 $job->insert('_JOB', { sectornum => $self->sectornum});
261 =item process_generate_coverage JOB, PARAMS
263 Queueable routine to fetch the sector coverage map from the tower mapping
264 server and store it. Highly experimental. Requires L<Map::Splat> to be
267 PARAMS must include 'sectornum'.
271 sub process_generate_coverage {
274 $job->update_statustext('0,generating map') if $job;
275 my $sectornum = $param->{sectornum};
276 my $sector = FS::tower_sector->by_key($sectornum)
277 or die "sector $sectornum does not exist";
278 my $tower = $sector->tower;
280 load_class('Map::Splat');
281 # since this is still experimental, put it somewhere we can find later
282 my $workdir = "$FS::UID::cache_dir/cache.$FS::UID::datasrc/" .
283 "generate_coverage/sector$sectornum-". time;
285 my $splat = Map::Splat->new(
286 lon => $tower->longitude,
287 lat => $tower->latitude,
288 height => ($sector->height || $tower->height || 0),
289 freq => $sector->freq_mhz,
290 azimuth => $sector->direction,
291 h_width => $sector->width,
292 tilt => $sector->downtilt,
293 v_width => $sector->v_width,
294 max_loss => $sector->margin,
295 min_loss => $sector->margin - 80,
300 my $box = $splat->box;
301 foreach (qw(west east south north)) {
302 $sector->set($_, $box->{$_});
304 $sector->set('image', $splat->mask);
305 # mask returns a PNG where everything below max_loss is solid colored,
306 # and everything above it is transparent. More useful for our purposes.
307 my $error = $sector->replace;
308 die $error if $error;
315 L<FS::tower>, L<FS::Record>, schema.html from the base documentation.