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