1 package FS::tower_sector;
2 use base qw( FS::Record );
4 use Class::Load qw(load_class);
5 use File::Path qw(make_path);
12 FS::tower_sector - Object methods for tower_sector records
18 $record = new FS::tower_sector \%hash;
19 $record = new FS::tower_sector { 'column' => 'value' };
21 $error = $record->insert;
23 $error = $new_record->replace($old_record);
25 $error = $record->delete;
27 $error = $record->check;
31 An FS::tower_sector object represents a tower sector. FS::tower_sector
32 inherits from FS::Record. The following fields are currently supported:
54 The height of this antenna on the tower, measured from ground level. This
55 plus the tower's altitude should equal the height of the antenna above sea
60 The band center frequency in MHz.
64 The antenna beam direction in degrees from north.
68 The -3dB horizontal beamwidth in degrees.
72 The antenna beam elevation in degrees below horizontal.
76 The -3dB vertical beamwidth in degrees.
80 The signal loss margin allowed on the sector, in dB. This is normally
81 transmitter EIRP minus receiver sensitivity.
85 The coverage map, as a PNG.
87 =item west, east, south, north
89 The coordinate boundaries of the coverage map.
99 Creates a new sector. To add the sector to the database, see L<"insert">.
101 Note that this stores the hash reference, not a distinct copy of the hash it
102 points to. You can ask the object for a copy with the I<hash> method.
106 sub table { 'tower_sector'; }
110 Adds this record to the database. If there is an error, returns the error,
111 otherwise returns false.
115 Delete this record from the database.
122 #not the most efficient, not not awful, and its not like deleting a sector
123 # with customers is a common operation
124 return "Can't delete a sector with customers" if $self->svc_broadband;
126 $self->SUPER::delete;
131 Checks all fields to make sure this is a valid sector. If there is
132 an error, returns the error, otherwise returns false. Called by the insert
141 $self->ut_numbern('sectornum')
142 || $self->ut_number('towernum', 'tower', 'towernum')
143 || $self->ut_text('sectorname')
144 || $self->ut_textn('ip_addr')
145 || $self->ut_floatn('height')
146 || $self->ut_numbern('freq_mhz')
147 || $self->ut_numbern('direction')
148 || $self->ut_numbern('width')
149 || $self->ut_numbern('v_width')
150 || $self->ut_numbern('downtilt')
151 || $self->ut_floatn('sector_range')
152 || $self->ut_numbern('margin')
153 || $self->ut_anything('image')
154 || $self->ut_sfloatn('west')
155 || $self->ut_sfloatn('east')
156 || $self->ut_sfloatn('south')
157 || $self->ut_sfloatn('north')
159 return $error if $error;
166 Returns the tower for this sector, as an FS::tower object (see L<FS::tower>).
170 Returns a description for this sector including tower name.
176 if ( $self->sectorname eq '_default' ) {
177 $self->tower->towername
180 $self->tower->towername. ' sector '. $self->sectorname
186 Returns the services on this tower sector.
188 =item need_fields_for_coverage
190 Returns a list of required fields for the coverage map that aren't yet filled.
194 sub need_fields_for_coverage {
196 my $tower = $self->tower;
199 freq_mhz => 'Frequency',
200 direction => 'Direction',
201 downtilt => 'Downtilt',
202 width => 'Horiz. width',
203 v_width => 'Vert. width',
204 margin => 'Signal margin',
205 latitude => 'Latitude',
206 longitude => 'Longitude',
209 foreach (keys %fields) {
210 if ($self->get($_) eq '' and $tower->get($_) eq '') {
211 push @need, $fields{$_};
217 =item queue_generate_coverage
219 Starts a job to recalculate the coverage map.
223 sub queue_generate_coverage {
225 if ( length($self->image) > 0 ) {
226 foreach (qw(image west south east north)) {
229 my $error = $self->replace;
230 return $error if $error;
232 my $job = FS::queue->new({
233 job => 'FS::tower_sector::process_generate_coverage',
235 $job->insert('_JOB', { sectornum => $self->sectornum});
244 =item process_generate_coverage JOB, PARAMS
246 Queueable routine to fetch the sector coverage map from the tower mapping
247 server and store it. Highly experimental. Requires L<Map::Splat> to be
250 PARAMS must include 'sectornum'.
254 sub process_generate_coverage {
257 $job->update_statustext('0,generating map') if $job;
258 my $sectornum = $param->{sectornum};
259 my $sector = FS::tower_sector->by_key($sectornum)
260 or die "sector $sectornum does not exist";
261 my $tower = $sector->tower;
263 load_class('Map::Splat');
264 # since this is still experimental, put it somewhere we can find later
265 my $workdir = "$FS::UID::cache_dir/cache.$FS::UID::datasrc/" .
266 "generate_coverage/sector$sectornum-". time;
268 my $splat = Map::Splat->new(
269 lon => $tower->longitude,
270 lat => $tower->latitude,
271 height => ($sector->height || $tower->height || 0),
272 freq => $sector->freq_mhz,
273 azimuth => $sector->direction,
274 h_width => $sector->width,
275 tilt => $sector->downtilt,
276 v_width => $sector->v_width,
277 max_loss => $sector->margin,
278 min_loss => $sector->margin - 80,
283 my $box = $splat->box;
284 foreach (qw(west east south north)) {
285 $sector->set($_, $box->{$_});
287 $sector->set('image', $splat->mask);
288 # mask returns a PNG where everything below max_loss is solid colored,
289 # and everything above it is transparent. More useful for our purposes.
290 my $error = $sector->replace;
291 die $error if $error;
298 L<FS::tower>, L<FS::Record>, schema.html from the base documentation.