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.
119 Delete this record from the database.
126 #not the most efficient, not not awful, and its not like deleting a sector
127 # with customers is a common operation
128 return "Can't delete a sector with customers" if $self->svc_broadband;
130 $self->SUPER::delete;
135 Checks all fields to make sure this is a valid sector. If there is
136 an error, returns the error, otherwise returns false. Called by the insert
145 $self->ut_numbern('sectornum')
146 || $self->ut_number('towernum', 'tower', 'towernum')
147 || $self->ut_text('sectorname')
148 || $self->ut_textn('ip_addr')
149 || $self->ut_floatn('height')
150 || $self->ut_numbern('freq_mhz')
151 || $self->ut_numbern('direction')
152 || $self->ut_numbern('width')
153 || $self->ut_numbern('v_width')
154 || $self->ut_numbern('downtilt')
155 || $self->ut_floatn('sector_range')
156 || $self->ut_numbern('db_high')
157 || $self->ut_numbern('db_low')
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>).
175 Returns a description for this sector including tower name.
181 if ( $self->sectorname eq '_default' ) {
182 $self->tower->towername
185 $self->tower->towername. ' sector '. $self->sectorname
191 Returns the services on this tower sector.
193 =item need_fields_for_coverage
195 Returns a list of required fields for the coverage map that aren't yet filled.
199 sub need_fields_for_coverage {
201 my $tower = $self->tower;
204 freq_mhz => 'Frequency',
205 direction => 'Direction',
206 downtilt => 'Downtilt',
207 width => 'Horiz. width',
208 v_width => 'Vert. width',
209 db_high => 'High quality',
210 latitude => 'Latitude',
211 longitude => 'Longitude',
214 foreach (keys %fields) {
215 if ($self->get($_) eq '' and $tower->get($_) eq '') {
216 push @need, $fields{$_};
222 =item queue_generate_coverage
224 Starts a job to recalculate the coverage map.
228 sub queue_generate_coverage {
230 if ( length($self->image) > 0 ) {
231 foreach (qw(image west south east north)) {
234 my $error = $self->replace;
235 return $error if $error;
237 my $job = FS::queue->new({
238 job => 'FS::tower_sector::process_generate_coverage',
240 $job->insert('_JOB', { sectornum => $self->sectornum});
249 =item process_generate_coverage JOB, PARAMS
251 Queueable routine to fetch the sector coverage map from the tower mapping
252 server and store it. Highly experimental. Requires L<Map::Splat> to be
255 PARAMS must include 'sectornum'.
259 sub process_generate_coverage {
262 $job->update_statustext('0,generating map') if $job;
263 my $sectornum = $param->{sectornum};
264 my $sector = FS::tower_sector->by_key($sectornum)
265 or die "sector $sectornum does not exist";
266 my $tower = $sector->tower;
268 load_class('Map::Splat');
270 # since this is still experimental, put it somewhere we can find later
271 my $workdir = "$FS::UID::cache_dir/cache.$FS::UID::datasrc/" .
272 "generate_coverage/sector$sectornum-". time;
274 my $splat = Map::Splat->new(
275 lon => $tower->longitude,
276 lat => $tower->latitude,
277 height => ($sector->height || $tower->height || 0),
278 freq => $sector->freq_mhz,
279 azimuth => $sector->direction,
280 h_width => $sector->width,
281 tilt => $sector->downtilt,
282 v_width => $sector->v_width,
283 db_levels => [ $sector->db_low, $sector->db_high ],
285 #simplify => 0.0004, # remove stairstepping in SRTM3 data?
289 my $box = $splat->box;
290 foreach (qw(west east south north)) {
291 $sector->set($_, $box->{$_});
293 $sector->set('image', $splat->png);
294 my $error = $sector->replace;
295 die $error if $error;
297 foreach ($sector->sector_coverage) {
299 die $error if $error;
301 # XXX undecided whether Map::Splat should even do this operation
305 my $data = decode_json( $splat->polygonize_json );
306 for my $feature (@{ $data->{features} }) {
307 my $db = $feature->{properties}{level};
308 my $coverage = FS::sector_coverage->new({
309 sectornum => $sectornum,
311 geometry => encode_json($feature->{geometry})
313 $error = $coverage->insert;
316 die $error if $error;
323 L<FS::tower>, L<FS::Record>, schema.html from the base documentation.