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);
13 FS::tower_sector - Object methods for tower_sector records
19 $record = new FS::tower_sector \%hash;
20 $record = new FS::tower_sector { 'column' => 'value' };
22 $error = $record->insert;
24 $error = $new_record->replace($old_record);
26 $error = $record->delete;
28 $error = $record->check;
32 An FS::tower_sector object represents a tower sector. FS::tower_sector
33 inherits from FS::Record. The following fields are currently supported:
55 The height of this antenna on the tower, measured from ground level. This
56 plus the tower's altitude should equal the height of the antenna above sea
61 The band center frequency in MHz.
65 The antenna beam direction in degrees from north.
69 The -3dB horizontal beamwidth in degrees.
73 The antenna beam elevation in degrees below horizontal.
77 The -3dB vertical beamwidth in degrees.
81 The signal loss margin allowed on the sector, in dB. This is normally
82 transmitter EIRP minus receiver sensitivity.
86 The coverage map, as a PNG.
88 =item west, east, south, north
90 The coordinate boundaries of the coverage map.
98 Up rate limit for sector.
100 =item down_rate_limit
102 down rate limit for sector.
112 Creates a new sector. To add the sector to the database, see L<"insert">.
114 Note that this stores the hash reference, not a distinct copy of the hash it
115 points to. You can ask the object for a copy with the I<hash> method.
119 sub table { 'tower_sector'; }
123 Adds this record to the database. If there is an error, returns the error,
124 otherwise returns false.
128 Delete this record from the database.
135 #not the most efficient, not not awful, and its not like deleting a sector
136 # with customers is a common operation
137 return "Can't delete a sector with customers" if $self->svc_broadband;
139 $self->SUPER::delete;
144 Checks all fields to make sure this is a valid sector. If there is
145 an error, returns the error, otherwise returns false. Called by the insert
154 $self->ut_numbern('sectornum')
155 || $self->ut_number('towernum', 'tower', 'towernum')
156 || $self->ut_text('sectorname')
157 || $self->ut_textn('ip_addr')
158 || $self->ut_floatn('height')
159 || $self->ut_numbern('freq_mhz')
160 || $self->ut_numbern('direction')
161 || $self->ut_numbern('width')
162 || $self->ut_numbern('v_width')
163 || $self->ut_numbern('downtilt')
164 || $self->ut_floatn('sector_range')
165 || $self->ut_numbern('margin')
166 || $self->ut_numbern('up_rate_limit')
167 || $self->ut_numbern('down_rate_limit')
168 || $self->ut_anything('image')
169 || $self->ut_sfloatn('west')
170 || $self->ut_sfloatn('east')
171 || $self->ut_sfloatn('south')
172 || $self->ut_sfloatn('north')
174 return $error if $error;
181 Returns the tower for this sector, as an FS::tower object (see L<FS::tower>).
185 Returns a description for this sector including tower name.
191 if ( $self->sectorname eq '_default' ) {
192 $self->tower->towername
195 $self->tower->towername. ' sector '. $self->sectorname
201 Returns the services on this tower sector.
203 =item need_fields_for_coverage
205 Returns a list of required fields for the coverage map that aren't yet filled.
209 sub need_fields_for_coverage {
211 my $tower = $self->tower;
214 freq_mhz => 'Frequency',
215 direction => 'Direction',
216 downtilt => 'Downtilt',
217 width => 'Horiz. width',
218 v_width => 'Vert. width',
219 margin => 'Signal margin',
220 latitude => 'Latitude',
221 longitude => 'Longitude',
224 foreach (keys %fields) {
225 if ($self->get($_) eq '' and $tower->get($_) eq '') {
226 push @need, $fields{$_};
232 =item queue_generate_coverage
234 Starts a job to recalculate the coverage map.
238 sub queue_generate_coverage {
240 if ( length($self->image) > 0 ) {
241 foreach (qw(image west south east north)) {
244 my $error = $self->replace;
245 return $error if $error;
247 my $job = FS::queue->new({
248 job => 'FS::tower_sector::process_generate_coverage',
250 $job->insert('_JOB', { sectornum => $self->sectornum});
259 =item part_export_svc_broadband
261 Returns all svc_broadband exports.
265 sub part_export_svc_broadband {
266 my $info = $FS::part_export::exports{'svc_broadband'} or return;
267 my @exporttypes = map { dbh->quote($_) } keys %$info or return;
269 'table' => 'part_export',
270 'extra_sql' => 'WHERE exporttype IN(' . join(',', @exporttypes) . ')'
280 =item process_generate_coverage JOB, PARAMS
282 Queueable routine to fetch the sector coverage map from the tower mapping
283 server and store it. Highly experimental. Requires L<Map::Splat> to be
286 PARAMS must include 'sectornum'.
290 sub process_generate_coverage {
293 $job->update_statustext('0,generating map') if $job;
294 my $sectornum = $param->{sectornum};
295 my $sector = FS::tower_sector->by_key($sectornum)
296 or die "sector $sectornum does not exist";
297 my $tower = $sector->tower;
299 load_class('Map::Splat');
300 # since this is still experimental, put it somewhere we can find later
301 my $workdir = "$FS::UID::cache_dir/cache.$FS::UID::datasrc/" .
302 "generate_coverage/sector$sectornum-". time;
304 my $splat = Map::Splat->new(
305 lon => $tower->longitude,
306 lat => $tower->latitude,
307 height => ($sector->height || $tower->height || 0),
308 freq => $sector->freq_mhz,
309 azimuth => $sector->direction,
310 h_width => $sector->width,
311 tilt => $sector->downtilt,
312 v_width => $sector->v_width,
313 max_loss => $sector->margin,
314 min_loss => $sector->margin - 80,
319 my $box = $splat->box;
320 foreach (qw(west east south north)) {
321 $sector->set($_, $box->{$_});
323 $sector->set('image', $splat->mask);
324 # mask returns a PNG where everything below max_loss is solid colored,
325 # and everything above it is transparent. More useful for our purposes.
326 my $error = $sector->replace;
327 die $error if $error;
334 L<FS::tower>, L<FS::Record>, schema.html from the base documentation.