1 package FS::tower_sector;
4 use base qw( FS::Record );
5 use FS::Record qw( qsearch qsearchs );
8 use Class::Load qw(load_class);
9 use File::Path qw(make_path);
11 use Storable qw(thaw);
12 use MIME::Base64 qw(decode_base64);
17 FS::tower_sector - Object methods for tower_sector records
23 $record = new FS::tower_sector \%hash;
24 $record = new FS::tower_sector { 'column' => 'value' };
26 $error = $record->insert;
28 $error = $new_record->replace($old_record);
30 $error = $record->delete;
32 $error = $record->check;
36 An FS::tower_sector object represents a tower sector. FS::tower_sector
37 inherits from FS::Record. The following fields are currently supported:
59 The height of this antenna on the tower, measured from ground level. This
60 plus the tower's altitude should equal the height of the antenna above sea
65 The band center frequency in MHz.
69 The antenna beam direction in degrees from north.
73 The -3dB horizontal beamwidth in degrees.
77 The antenna beam elevation in degrees below horizontal.
81 The -3dB vertical beamwidth in degrees.
85 The signal loss margin allowed on the sector, in dB. This is normally
86 transmitter EIRP minus receiver sensitivity.
90 The coverage map, as a PNG.
92 =item west, east, south, north
94 The coordinate boundaries of the coverage map.
104 Creates a new sector. To add the sector to the database, see L<"insert">.
106 Note that this stores the hash reference, not a distinct copy of the hash it
107 points to. You can ask the object for a copy with the I<hash> method.
111 sub table { 'tower_sector'; }
115 Adds this record to the database. If there is an error, returns the error,
116 otherwise returns false.
120 Delete this record from the database.
127 #not the most efficient, not not awful, and its not like deleting a sector
128 # with customers is a common operation
129 return "Can't delete a sector with customers" if $self->svc_broadband;
131 $self->SUPER::delete;
136 Checks all fields to make sure this is a valid sector. If there is
137 an error, returns the error, otherwise returns false. Called by the insert
146 $self->ut_numbern('sectornum')
147 || $self->ut_number('towernum', 'tower', 'towernum')
148 || $self->ut_text('sectorname')
149 || $self->ut_textn('ip_addr')
150 || $self->ut_floatn('height')
151 || $self->ut_numbern('freq_mhz')
152 || $self->ut_numbern('direction')
153 || $self->ut_numbern('width')
154 || $self->ut_numbern('v_width')
155 || $self->ut_numbern('downtilt')
156 || $self->ut_floatn('sector_range')
157 || $self->ut_numbern('margin')
158 || $self->ut_anything('image')
159 || $self->ut_sfloatn('west')
160 || $self->ut_sfloatn('east')
161 || $self->ut_sfloatn('south')
162 || $self->ut_sfloatn('north')
164 return $error if $error;
171 Returns the tower for this sector, as an FS::tower object (see L<FS::tower>).
177 qsearchs('tower', { 'towernum'=>$self->towernum } );
182 Returns a description for this sector including tower name.
188 if ( $self->sectorname eq '_default' ) {
189 $self->tower->towername
192 $self->tower->towername. ' sector '. $self->sectorname
198 Returns the services on this tower sector.
204 qsearch('svc_broadband', { 'sectornum' => $self->sectornum });
207 =item need_fields_for_coverage
209 Returns a list of required fields for the coverage map that aren't yet filled.
213 sub need_fields_for_coverage {
215 my $tower = $self->tower;
218 freq_mhz => 'Frequency',
219 direction => 'Direction',
220 downtilt => 'Downtilt',
221 width => 'Horiz. width',
222 v_width => 'Vert. width',
223 margin => 'Signal margin',
224 latitude => 'Latitude',
225 longitude => 'Longitude',
228 foreach (keys %fields) {
229 if ($self->get($_) eq '' and $tower->get($_) eq '') {
230 push @need, $fields{$_};
236 =item queue_generate_coverage
238 Starts a job to recalculate the coverage map.
242 sub queue_generate_coverage {
244 if ( length($self->image) > 0 ) {
245 foreach (qw(image west south east north)) {
248 my $error = $self->replace;
249 return $error if $error;
251 my $job = FS::queue->new({
252 job => 'FS::tower_sector::process_generate_coverage',
254 $job->insert('_JOB', { sectornum => $self->sectornum});
263 =item process_generate_coverage JOB, PARAMS
265 Queueable routine to fetch the sector coverage map from the tower mapping
266 server and store it. Highly experimental. Requires L<Map::Splat> to be
269 PARAMS must include 'sectornum'.
273 sub process_generate_coverage {
277 $param = thaw(decode_base64($param));
279 $job->update_statustext('0,generating map') if $job;
280 my $sectornum = $param->{sectornum};
281 my $sector = FS::tower_sector->by_key($sectornum)
282 or die "sector $sectornum does not exist";
283 my $tower = $sector->tower;
285 load_class('Map::Splat');
286 # since this is still experimental, put it somewhere we can find later
287 my $workdir = "$FS::UID::cache_dir/cache.$FS::UID::datasrc/" .
288 "generate_coverage/sector$sectornum-". time;
290 my $splat = Map::Splat->new(
291 lon => $tower->longitude,
292 lat => $tower->latitude,
293 height => ($sector->height || $tower->height || 0),
294 freq => $sector->freq_mhz,
295 azimuth => $sector->direction,
296 h_width => $sector->width,
297 tilt => $sector->downtilt,
298 v_width => $sector->v_width,
299 max_loss => $sector->margin,
300 min_loss => $sector->margin - 80,
305 my $box = $splat->box;
306 foreach (qw(west east south north)) {
307 $sector->set($_, $box->{$_});
309 $sector->set('image', $splat->mask);
310 # mask returns a PNG where everything below max_loss is solid colored,
311 # and everything above it is transparent. More useful for our purposes.
312 my $error = $sector->replace;
313 die $error if $error;
320 L<FS::tower>, L<FS::Record>, schema.html from the base documentation.