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