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