6ccfe553d423d6ce8ee870cf5b776f826c88b126
[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 =back
97
98 =head1 METHODS
99
100 =over 4
101
102 =item new HASHREF
103
104 Creates a new sector.  To add the sector to the database, see L<"insert">.
105
106 Note that this stores the hash reference, not a distinct copy of the hash it
107 points to.  You can ask the object for a copy with the I<hash> method.
108
109 =cut
110
111 sub table { 'tower_sector'; }
112
113 =item insert
114
115 Adds this record to the database.  If there is an error, returns the error,
116 otherwise returns false.
117
118 =item delete
119
120 Delete this record from the database.
121
122 =cut
123
124 sub delete {
125   my $self = shift;
126
127   #not the most efficient, not not awful, and its not like deleting a sector
128   # with customers is a common operation
129   return "Can't delete a sector with customers" if $self->svc_broadband;
130
131   $self->SUPER::delete;
132 }
133
134 =item check
135
136 Checks all fields to make sure this is a valid sector.  If there is
137 an error, returns the error, otherwise returns false.  Called by the insert
138 and replace methods.
139
140 =cut
141
142 sub check {
143   my $self = shift;
144
145   my $error = 
146     $self->ut_numbern('sectornum')
147     || $self->ut_number('towernum', 'tower', 'towernum')
148     || $self->ut_text('sectorname')
149     || $self->ut_textn('ip_addr')
150     || $self->ut_floatn('height')
151     || $self->ut_numbern('freq_mhz')
152     || $self->ut_numbern('direction')
153     || $self->ut_numbern('width')
154     || $self->ut_numbern('v_width')
155     || $self->ut_numbern('downtilt')
156     || $self->ut_floatn('sector_range')
157     || $self->ut_numbern('margin')
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')
163   ;
164   return $error if $error;
165
166   $self->SUPER::check;
167 }
168
169 =item tower
170
171 Returns the tower for this sector, as an FS::tower object (see L<FS::tower>).
172
173 =cut
174
175 sub tower {
176   my $self = shift;
177   qsearchs('tower', { 'towernum'=>$self->towernum } );
178 }
179
180 =item description
181
182 Returns a description for this sector including tower name.
183
184 =cut
185
186 sub description {
187   my $self = shift;
188   if ( $self->sectorname eq '_default' ) {
189     $self->tower->towername
190   }
191   else {
192     $self->tower->towername. ' sector '. $self->sectorname
193   }
194 }
195
196 =item svc_broadband
197
198 Returns the services on this tower sector.
199
200 =cut
201
202 sub svc_broadband {
203   my $self = shift;
204   qsearch('svc_broadband', { 'sectornum' => $self->sectornum });
205 }
206
207 =item need_fields_for_coverage
208
209 Returns a list of required fields for the coverage map that aren't yet filled.
210
211 =cut
212
213 sub need_fields_for_coverage {
214   my $self = shift;
215   my $tower = $self->tower;
216   my %fields = (
217     height    => 'Height',
218     freq_mhz  => 'Frequency',
219     direction => 'Direction',
220     downtilt  => 'Downtilt',
221     width     => 'Horiz. width',
222     v_width   => 'Vert. width',
223     margin    => 'Signal margin',
224     latitude  => 'Latitude',
225     longitude => 'Longitude',
226   );
227   my @need;
228   foreach (keys %fields) {
229     if ($self->get($_) eq '' and $tower->get($_) eq '') {
230       push @need, $fields{$_};
231     }
232   }
233   @need;
234 }
235
236 =item queue_generate_coverage
237
238 Starts a job to recalculate the coverage map.
239
240 =cut
241
242 sub queue_generate_coverage {
243   my $self = shift;
244   if ( length($self->image) > 0 ) {
245     foreach (qw(image west south east north)) {
246       $self->set($_, '');
247     }
248     my $error = $self->replace;
249     return $error if $error;
250   }
251   my $job = FS::queue->new({
252       job => 'FS::tower_sector::process_generate_coverage',
253   });
254   $job->insert('_JOB', { sectornum => $self->sectornum});
255 }
256
257 =back
258
259 =head1 SUBROUTINES
260
261 =over 4
262
263 =item process_generate_coverage JOB, PARAMS
264
265 Queueable routine to fetch the sector coverage map from the tower mapping
266 server and store it. Highly experimental. Requires L<Map::Splat> to be
267 installed.
268
269 PARAMS must include 'sectornum'.
270
271 =cut
272
273 sub process_generate_coverage {
274   my $job = shift;
275   my $param = shift;
276   if (!ref($param)) {
277     $param = thaw(decode_base64($param));
278   }
279   $job->update_statustext('0,generating map') if $job;
280   my $sectornum = $param->{sectornum};
281   my $sector = FS::tower_sector->by_key($sectornum)
282     or die "sector $sectornum does not exist";
283   my $tower = $sector->tower;
284
285   load_class('Map::Splat');
286   # since this is still experimental, put it somewhere we can find later
287   my $workdir = "$FS::UID::cache_dir/cache.$FS::UID::datasrc/" .
288                 "generate_coverage/sector$sectornum-". time;
289   make_path($workdir);
290   my $splat = Map::Splat->new(
291     lon         => $tower->longitude,
292     lat         => $tower->latitude,
293     height      => ($sector->height || $tower->height || 0),
294     freq        => $sector->freq_mhz,
295     azimuth     => $sector->direction,
296     h_width     => $sector->width,
297     tilt        => $sector->downtilt,
298     v_width     => $sector->v_width,
299     max_loss    => $sector->margin,
300     min_loss    => $sector->margin - 80,
301     dir         => $workdir,
302   );
303   $splat->calculate;
304
305   my $box = $splat->box;
306   foreach (qw(west east south north)) {
307     $sector->set($_, $box->{$_});
308   }
309   $sector->set('image', $splat->mask);
310   # mask returns a PNG where everything below max_loss is solid colored,
311   # and everything above it is transparent. More useful for our purposes.
312   my $error = $sector->replace;
313   die $error if $error;
314 }
315
316 =head1 BUGS
317
318 =head1 SEE ALSO
319
320 L<FS::tower>, L<FS::Record>, schema.html from the base documentation.
321
322 =cut
323
324 1;
325