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);
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 to treat as "high quality".
85 The signal loss margin to treat as "low quality".
89 The coverage map, as a PNG.
91 =item west, east, south, north
93 The coordinate boundaries of the coverage map.
103 Creates a new sector. To add the sector to the database, see L<"insert">.
105 Note that this stores the hash reference, not a distinct copy of the hash it
106 points to. You can ask the object for a copy with the I<hash> method.
110 sub table { 'tower_sector'; }
114 Adds this record to the database. If there is an error, returns the error,
115 otherwise returns false.
121 my $error = $self->SUPER::insert;
122 return $error if $error;
124 if (scalar($self->need_fields_for_coverage) == 0) {
125 $self->queue_generate_coverage;
131 my $old = shift || $self->replace_old;
132 my $regen_coverage = 0;
133 if ( !$self->get('no_regen') ) {
134 foreach (qw(height freq_mhz direction width downtilt
135 v_width db_high db_low))
137 $regen_coverage = 1 if ($self->get($_) ne $old->get($_));
141 my $error = $self->SUPER::replace($old);
142 return $error if $error;
144 if ($regen_coverage) {
145 $self->queue_generate_coverage;
151 Delete this record from the database.
158 #not the most efficient, not not awful, and its not like deleting a sector
159 # with customers is a common operation
160 return "Can't delete a sector with customers" if $self->svc_broadband;
162 $self->SUPER::delete;
167 Checks all fields to make sure this is a valid sector. If there is
168 an error, returns the error, otherwise returns false. Called by the insert
177 $self->ut_numbern('sectornum')
178 || $self->ut_number('towernum', 'tower', 'towernum')
179 || $self->ut_text('sectorname')
180 || $self->ut_textn('ip_addr')
181 || $self->ut_floatn('height')
182 || $self->ut_numbern('freq_mhz')
183 || $self->ut_numbern('direction')
184 || $self->ut_numbern('width')
185 || $self->ut_numbern('v_width')
186 || $self->ut_numbern('downtilt')
187 || $self->ut_floatn('sector_range')
188 || $self->ut_numbern('db_high')
189 || $self->ut_numbern('db_low')
190 || $self->ut_anything('image')
191 || $self->ut_sfloatn('west')
192 || $self->ut_sfloatn('east')
193 || $self->ut_sfloatn('south')
194 || $self->ut_sfloatn('north')
196 return $error if $error;
203 Returns the tower for this sector, as an FS::tower object (see L<FS::tower>).
207 Returns a description for this sector including tower name.
213 if ( $self->sectorname eq '_default' ) {
214 $self->tower->towername
217 $self->tower->towername. ' sector '. $self->sectorname
223 Returns the services on this tower sector.
225 =item need_fields_for_coverage
227 Returns a list of required fields for the coverage map that aren't yet filled.
231 sub need_fields_for_coverage {
233 my $tower = $self->tower;
236 freq_mhz => 'Frequency',
237 direction => 'Direction',
238 downtilt => 'Downtilt',
239 width => 'Horiz. width',
240 v_width => 'Vert. width',
241 db_high => 'High quality',
242 latitude => 'Latitude',
243 longitude => 'Longitude',
246 foreach (keys %fields) {
247 if ($self->get($_) eq '' and $tower->get($_) eq '') {
248 push @need, $fields{$_};
254 =item queue_generate_coverage
256 Starts a job to recalculate the coverage map.
260 sub queue_generate_coverage {
262 my $need_fields = join(',', $self->need_fields_for_coverage);
263 return "Sector needs fields $need_fields" if $need_fields;
264 $self->set('no_regen', 1); # avoid recursion
265 if ( length($self->image) > 0 ) {
266 foreach (qw(image west south east north)) {
269 my $error = $self->replace;
270 return $error if $error;
272 my $job = FS::queue->new({
273 job => 'FS::tower_sector::process_generate_coverage',
275 $job->insert('_JOB', { sectornum => $self->sectornum});
284 =item process_generate_coverage JOB, PARAMS
286 Queueable routine to fetch the sector coverage map from the tower mapping
287 server and store it. Highly experimental. Requires L<Map::Splat> to be
290 PARAMS must include 'sectornum'.
294 sub process_generate_coverage {
297 $job->update_statustext('0,generating map') if $job;
298 my $sectornum = $param->{sectornum};
299 my $sector = FS::tower_sector->by_key($sectornum)
300 or die "sector $sectornum does not exist";
301 $sector->set('no_regen', 1); # avoid recursion
302 my $tower = $sector->tower;
304 load_class('Map::Splat');
306 # since this is still experimental, put it somewhere we can find later
307 my $workdir = "$FS::UID::cache_dir/cache.$FS::UID::datasrc/" .
308 "generate_coverage/sector$sectornum-". time;
310 my $splat = Map::Splat->new(
311 lon => $tower->longitude,
312 lat => $tower->latitude,
313 height => ($sector->height || $tower->height || 0),
314 freq => $sector->freq_mhz,
315 azimuth => $sector->direction,
316 h_width => $sector->width,
317 tilt => $sector->downtilt,
318 v_width => $sector->v_width,
319 db_levels => [ $sector->db_low, $sector->db_high ],
321 #simplify => 0.0004, # remove stairstepping in SRTM3 data?
325 my $box = $splat->box;
326 foreach (qw(west east south north)) {
327 $sector->set($_, $box->{$_});
329 $sector->set('image', $splat->png);
330 my $error = $sector->replace;
331 die $error if $error;
333 foreach ($sector->sector_coverage) {
335 die $error if $error;
337 # XXX undecided whether Map::Splat should even do this operation
341 my $data = decode_json( $splat->polygonize_json );
342 for my $feature (@{ $data->{features} }) {
343 my $db = $feature->{properties}{level};
344 my $coverage = FS::sector_coverage->new({
345 sectornum => $sectornum,
347 geometry => encode_json($feature->{geometry})
349 $error = $coverage->insert;
352 die $error if $error;
359 L<FS::tower>, L<FS::Record>, schema.html from the base documentation.