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