RT# 78356 - fix for saisei integration
[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( dbh 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_limit
101
102 Up rate limit for sector.
103
104 =item down_rate_limit
105
106 down rate limit 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_limit')
171     || $self->ut_numbern('down_rate_limit')
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 part_export_svc_broadband
278
279 Returns all svc_broadband exports.
280
281 =cut
282
283 sub part_export_svc_broadband {
284   my $info = $FS::part_export::exports{'svc_broadband'} or return;
285   my @exporttypes = map { dbh->quote($_) } keys %$info or return;
286   qsearch({
287     'table'     => 'part_export',
288     'extra_sql' => 'WHERE exporttype IN(' . join(',', @exporttypes) . ')'
289   });
290 }
291
292 =back
293
294 =over 4
295
296 =item process_generate_coverage JOB, PARAMS
297
298 Queueable routine to fetch the sector coverage map from the tower mapping
299 server and store it. Highly experimental. Requires L<Map::Splat> to be
300 installed.
301
302 PARAMS must include 'sectornum'.
303
304 =cut
305
306 sub process_generate_coverage {
307   my $job = shift;
308   my $param = shift;
309   if (!ref($param)) {
310     $param = thaw(decode_base64($param));
311   }
312   $job->update_statustext('0,generating map') if $job;
313   my $sectornum = $param->{sectornum};
314   my $sector = FS::tower_sector->by_key($sectornum)
315     or die "sector $sectornum does not exist";
316   my $tower = $sector->tower;
317
318   load_class('Map::Splat');
319   # since this is still experimental, put it somewhere we can find later
320   my $workdir = "$FS::UID::cache_dir/cache.$FS::UID::datasrc/" .
321                 "generate_coverage/sector$sectornum-". time;
322   make_path($workdir);
323   my $splat = Map::Splat->new(
324     lon         => $tower->longitude,
325     lat         => $tower->latitude,
326     height      => ($sector->height || $tower->height || 0),
327     freq        => $sector->freq_mhz,
328     azimuth     => $sector->direction,
329     h_width     => $sector->width,
330     tilt        => $sector->downtilt,
331     v_width     => $sector->v_width,
332     max_loss    => $sector->margin,
333     min_loss    => $sector->margin - 80,
334     dir         => $workdir,
335   );
336   $splat->calculate;
337
338   my $box = $splat->box;
339   foreach (qw(west east south north)) {
340     $sector->set($_, $box->{$_});
341   }
342   $sector->set('image', $splat->mask);
343   # mask returns a PNG where everything below max_loss is solid colored,
344   # and everything above it is transparent. More useful for our purposes.
345   my $error = $sector->replace;
346   die $error if $error;
347 }
348
349 =head1 BUGS
350
351 =head1 SEE ALSO
352
353 L<FS::tower>, L<FS::Record>, schema.html from the base documentation.
354
355 =cut
356
357 1;
358