90d6a9cb5636b993ac6e00db34584506dab07290
[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_limit
96
97 Up rate limit for sector.
98
99 =item down_rate_limit
100
101 down rate limit 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_limit')
166     || $self->ut_numbern('down_rate_limit')
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 CLASS METHODS
255
256 =over 4
257
258 =item part_export_svc_broadband
259
260 Returns all svc_broadband exports.
261
262 =cut
263
264 sub part_export_svc_broadband {
265   my $info = $FS::part_export::exports{'svc_broadband'} or return;
266   my @exporttypes = map { dbh->quote($_) } keys %$info or return;
267   qsearch({
268     'table'     => 'part_export',
269     'extra_sql' => 'WHERE exporttype IN(' . join(',', @exporttypes) . ')'
270   });
271 }
272
273 =back
274
275 =head1 SUBROUTINES
276
277 =over 4
278
279 =item process_generate_coverage JOB, PARAMS
280
281 Queueable routine to fetch the sector coverage map from the tower mapping
282 server and store it. Highly experimental. Requires L<Map::Splat> to be
283 installed.
284
285 PARAMS must include 'sectornum'.
286
287 =cut
288
289 sub process_generate_coverage {
290   my $job = shift;
291   my $param = shift;
292   $job->update_statustext('0,generating map') if $job;
293   my $sectornum = $param->{sectornum};
294   my $sector = FS::tower_sector->by_key($sectornum)
295     or die "sector $sectornum does not exist";
296   my $tower = $sector->tower;
297
298   load_class('Map::Splat');
299   # since this is still experimental, put it somewhere we can find later
300   my $workdir = "$FS::UID::cache_dir/cache.$FS::UID::datasrc/" .
301                 "generate_coverage/sector$sectornum-". time;
302   make_path($workdir);
303   my $splat = Map::Splat->new(
304     lon         => $tower->longitude,
305     lat         => $tower->latitude,
306     height      => ($sector->height || $tower->height || 0),
307     freq        => $sector->freq_mhz,
308     azimuth     => $sector->direction,
309     h_width     => $sector->width,
310     tilt        => $sector->downtilt,
311     v_width     => $sector->v_width,
312     max_loss    => $sector->margin,
313     min_loss    => $sector->margin - 80,
314     dir         => $workdir,
315   );
316   $splat->calculate;
317
318   my $box = $splat->box;
319   foreach (qw(west east south north)) {
320     $sector->set($_, $box->{$_});
321   }
322   $sector->set('image', $splat->mask);
323   # mask returns a PNG where everything below max_loss is solid colored,
324   # and everything above it is transparent. More useful for our purposes.
325   my $error = $sector->replace;
326   die $error if $error;
327 }
328
329 =head1 BUGS
330
331 =head1 SEE ALSO
332
333 L<FS::tower>, L<FS::Record>, schema.html from the base documentation.
334
335 =cut
336
337 1;
338