RT# 78356 - updated documentation and added ability to create access points as Saisei...
[freeside.git] / FS / FS / tower_sector.pm
1 package FS::tower_sector;
2
3 use strict;
4 use base qw( FS::Record );
5 use FS::Record qw( qsearch qsearchs );
6 use FS::tower;
7 use FS::svc_broadband;
8 use Class::Load qw(load_class);
9 use File::Path qw(make_path);
10 use Data::Dumper;
11 use Storable qw(thaw);
12 use MIME::Base64 qw(decode_base64);
13
14
15 =head1 NAME
16
17 FS::tower_sector - Object methods for tower_sector records
18
19 =head1 SYNOPSIS
20
21   use FS::tower_sector;
22
23   $record = new FS::tower_sector \%hash;
24   $record = new FS::tower_sector { 'column' => 'value' };
25
26   $error = $record->insert;
27
28   $error = $new_record->replace($old_record);
29
30   $error = $record->delete;
31
32   $error = $record->check;
33
34 =head1 DESCRIPTION
35
36 An FS::tower_sector object represents a tower sector.  FS::tower_sector
37 inherits from FS::Record.  The following fields are currently supported:
38
39 =over 4
40
41 =item sectornum
42
43 primary key
44
45 =item towernum
46
47 towernum
48
49 =item sectorname
50
51 sectorname
52
53 =item ip_addr
54
55 ip_addr
56
57 =item height
58
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
61 level.
62
63 =item freq_mhz
64
65 The band center frequency in MHz.
66
67 =item direction
68
69 The antenna beam direction in degrees from north.
70
71 =item width
72
73 The -3dB horizontal beamwidth in degrees.
74
75 =item downtilt
76
77 The antenna beam elevation in degrees below horizontal.
78
79 =item v_width
80
81 The -3dB vertical beamwidth in degrees.
82
83 =item margin
84
85 The signal loss margin allowed on the sector, in dB. This is normally
86 transmitter EIRP minus receiver sensitivity.
87
88 =item image 
89
90 The coverage map, as a PNG.
91
92 =item west, east, south, north
93
94 The coordinate boundaries of the coverage map.
95
96 =item title
97
98 The sector title.
99
100 =item up_rate
101
102 Up rate for sector.
103
104 =item down_rate
105
106 down rate for sector.
107
108 =back
109
110 =head1 METHODS
111
112 =over 4
113
114 =item new HASHREF
115
116 Creates a new sector.  To add the sector to the database, see L<"insert">.
117
118 Note that this stores the hash reference, not a distinct copy of the hash it
119 points to.  You can ask the object for a copy with the I<hash> method.
120
121 =cut
122
123 sub table { 'tower_sector'; }
124
125 =item insert
126
127 Adds this record to the database.  If there is an error, returns the error,
128 otherwise returns false.
129
130 =item delete
131
132 Delete this record from the database.
133
134 =cut
135
136 sub delete {
137   my $self = shift;
138
139   #not the most efficient, not not awful, and its not like deleting a sector
140   # with customers is a common operation
141   return "Can't delete a sector with customers" if $self->svc_broadband;
142
143   $self->SUPER::delete;
144 }
145
146 =item check
147
148 Checks all fields to make sure this is a valid sector.  If there is
149 an error, returns the error, otherwise returns false.  Called by the insert
150 and replace methods.
151
152 =cut
153
154 sub check {
155   my $self = shift;
156
157   my $error = 
158     $self->ut_numbern('sectornum')
159     || $self->ut_number('towernum', 'tower', 'towernum')
160     || $self->ut_text('sectorname')
161     || $self->ut_textn('ip_addr')
162     || $self->ut_floatn('height')
163     || $self->ut_numbern('freq_mhz')
164     || $self->ut_numbern('direction')
165     || $self->ut_numbern('width')
166     || $self->ut_numbern('v_width')
167     || $self->ut_numbern('downtilt')
168     || $self->ut_floatn('sector_range')
169     || $self->ut_numbern('margin')
170     || $self->ut_numbern('up_rate')
171     || $self->ut_numbern('down_rate')
172     || $self->ut_anything('image')
173     || $self->ut_sfloatn('west')
174     || $self->ut_sfloatn('east')
175     || $self->ut_sfloatn('south')
176     || $self->ut_sfloatn('north')
177   ;
178   return $error if $error;
179
180   $self->SUPER::check;
181 }
182
183 =item tower
184
185 Returns the tower for this sector, as an FS::tower object (see L<FS::tower>).
186
187 =cut
188
189 sub tower {
190   my $self = shift;
191   qsearchs('tower', { 'towernum'=>$self->towernum } );
192 }
193
194 =item description
195
196 Returns a description for this sector including tower name.
197
198 =cut
199
200 sub description {
201   my $self = shift;
202   if ( $self->sectorname eq '_default' ) {
203     $self->tower->towername
204   }
205   else {
206     $self->tower->towername. ' sector '. $self->sectorname
207   }
208 }
209
210 =item svc_broadband
211
212 Returns the services on this tower sector.
213
214 =cut
215
216 sub svc_broadband {
217   my $self = shift;
218   qsearch('svc_broadband', { 'sectornum' => $self->sectornum });
219 }
220
221 =item need_fields_for_coverage
222
223 Returns a list of required fields for the coverage map that aren't yet filled.
224
225 =cut
226
227 sub need_fields_for_coverage {
228   my $self = shift;
229   my $tower = $self->tower;
230   my %fields = (
231     height    => 'Height',
232     freq_mhz  => 'Frequency',
233     direction => 'Direction',
234     downtilt  => 'Downtilt',
235     width     => 'Horiz. width',
236     v_width   => 'Vert. width',
237     margin    => 'Signal margin',
238     latitude  => 'Latitude',
239     longitude => 'Longitude',
240   );
241   my @need;
242   foreach (keys %fields) {
243     if ($self->get($_) eq '' and $tower->get($_) eq '') {
244       push @need, $fields{$_};
245     }
246   }
247   @need;
248 }
249
250 =item queue_generate_coverage
251
252 Starts a job to recalculate the coverage map.
253
254 =cut
255
256 sub queue_generate_coverage {
257   my $self = shift;
258   if ( length($self->image) > 0 ) {
259     foreach (qw(image west south east north)) {
260       $self->set($_, '');
261     }
262     my $error = $self->replace;
263     return $error if $error;
264   }
265   my $job = FS::queue->new({
266       job => 'FS::tower_sector::process_generate_coverage',
267   });
268   $job->insert('_JOB', { sectornum => $self->sectornum});
269 }
270
271 =back
272
273 =head1 SUBROUTINES
274
275 =over 4
276
277 =item process_generate_coverage JOB, PARAMS
278
279 Queueable routine to fetch the sector coverage map from the tower mapping
280 server and store it. Highly experimental. Requires L<Map::Splat> to be
281 installed.
282
283 PARAMS must include 'sectornum'.
284
285 =cut
286
287 sub process_generate_coverage {
288   my $job = shift;
289   my $param = shift;
290   if (!ref($param)) {
291     $param = thaw(decode_base64($param));
292   }
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;
298
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;
303   make_path($workdir);
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,
315     dir         => $workdir,
316   );
317   $splat->calculate;
318
319   my $box = $splat->box;
320   foreach (qw(west east south north)) {
321     $sector->set($_, $box->{$_});
322   }
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;
328 }
329
330 =head1 BUGS
331
332 =head1 SEE ALSO
333
334 L<FS::tower>, L<FS::Record>, schema.html from the base documentation.
335
336 =cut
337
338 1;
339