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