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