debuging changes
[spider.git] / perl / Prefix.pm
1 #
2 # prefix handling
3 #
4 # Copyright (c) - Dirk Koopman G1TLH
5 #
6 # $Id$
7 #
8
9 package Prefix;
10
11 use IO::File;
12 use DXVars;
13 use DB_File;
14 use Data::Dumper;
15 use DXDebug;
16 use DXUtil;
17
18
19 use strict;
20
21 use vars qw($VERSION $BRANCH);
22 $VERSION = sprintf( "%d.%03d", q$Revision$ =~ /(\d+)\.(\d+)/ );
23 $BRANCH = sprintf( "%d.%03d", q$Revision$ =~ /\d+\.\d+\.(\d+)\.(\d+)/  || (0,0));
24 $main::build += $VERSION;
25 $main::branch += $BRANCH;
26
27 use vars qw($db  %prefix_loc %pre %cache $lasttime $hits $matchtotal);
28
29 $db = undef;                                    # the DB_File handle
30 %prefix_loc = ();                               # the meat of the info
31 %pre = ();                                              # the prefix list
32 %cache = ();                                    # a runtime cache of matched prefixes
33 $lasttime = 0;                                  # last time this cache was cleared
34 $hits = $matchtotal = 1;                # cache stats
35
36 sub load
37 {
38         if ($db) {
39                 undef $db;
40                 untie %pre;
41                 %pre = ();
42                 %prefix_loc = ();
43         }
44         $db = tie(%pre, "DB_File", undef, O_RDWR|O_CREAT, 0666, $DB_BTREE) or confess "can't tie \%pre ($!)";  
45         my $out = $@ if $@;
46         do "$main::data/prefix_data.pl" if !$out;
47         $out = $@ if $@;
48         #  print Data::Dumper->Dump([\%pre, \%prefix_loc], [qw(pre prefix_loc)]);
49         return $out;
50 }
51
52 sub store
53 {
54         my ($k, $l);
55         my $fh = new IO::File;
56         my $fn = "$main::data/prefix_data.pl";
57   
58         confess "Prefix system not started" if !$db;
59   
60         # save versions!
61         rename "$fn.oooo", "$fn.ooooo" if -e "$fn.oooo";
62         rename "$fn.ooo", "$fn.oooo" if -e "$fn.ooo";
63         rename "$fn.oo", "$fn.ooo" if -e "$fn.oo";
64         rename "$fn.o", "$fn.oo" if -e "$fn.o";
65         rename "$fn", "$fn.o" if -e "$fn";
66   
67         $fh->open(">$fn") or die "Can't open $fn ($!)";
68
69         # prefix location data
70         $fh->print("%prefix_loc = (\n");
71         foreach $l (sort {$a <=> $b} keys %prefix_loc) {
72                 my $r = $prefix_loc{$l};
73                 $fh->printf("   $l => bless( { name => '%s', dxcc => %d, itu => %d, utcoff => %d, lat => %f, long => %f }, 'Prefix'),\n",
74                                         $r->{name}, $r->{dxcc}, $r->{itu}, $r->{cq}, $r->{utcoff}, $r->{lat}, $r->{long});
75         }
76         $fh->print(");\n\n");
77
78         # prefix data
79         $fh->print("%pre = (\n");
80         foreach $k (sort keys %pre) {
81                 $fh->print("   '$k' => [");
82                 my @list = @{$pre{$k}};
83                 my $l;
84                 my $str;
85                 foreach $l (@list) {
86                         $str .= " $l,";
87                 }
88                 chop $str;  
89                 $fh->print("$str ],\n");
90         }
91         $fh->print(");\n");
92         undef $fh;
93         untie %pre; 
94 }
95
96 # what you get is a list that looks like:-
97
98 # prefix => @list of blessed references to prefix_locs 
99 #
100 # This routine will only do what you ask for, if you wish to be intelligent
101 # then that is YOUR problem!
102 #
103
104 sub get
105 {
106         my $key = shift;
107         my $ref;
108         my $gotkey = $key;
109         return () if $db->seq($gotkey, $ref, R_CURSOR);
110         return () if $key ne substr $gotkey, 0, length $key;
111
112         return ($gotkey,  map { $prefix_loc{$_} } split ',', $ref);
113 }
114
115 #
116 # get the next key that matches, this assumes that you have done a 'get' first
117 #
118
119 sub next
120 {
121         my $key = shift;
122         my $ref;
123         my $gotkey;
124   
125         return () if $db->seq($gotkey, $ref, R_NEXT);
126         return () if $key ne substr $gotkey, 0, length $key;
127   
128         return ($gotkey, map { $prefix_loc{$_} } split ',', $ref);
129 }
130
131
132 # search for the nearest match of a prefix string (starting
133 # from the RH end of the string passed)
134 #
135
136 sub matchprefix
137 {
138         my $pref = shift;
139
140         for (my $i = length $pref; $i; $i--) {
141                 $matchtotal++;
142                 my $s = substr($pref, 0, $i);
143                 my $p = $cache{$s};
144                 if ($p) {
145                         $hits++;
146                         if (isdbg('prefix')) {
147                                 my $percent = $hits * 100 / $matchtotal;
148                                 dbg("Partial Prefix Cache Hit: $s Hits: $hits of $matchtotal = $percent\%");
149                         }
150                         return @$p;
151                 } else {
152                         my @out = get($s);
153                         if (isdbg('prefix')) {
154                                 my $part = $out[0] || "*";
155                                 $part .= '*' unless $part eq '*' || $part eq $s;
156                                 dbg("Partial prefix: $pref $s $part" );
157                         } 
158                         if (@out && $out[0] eq $s) {
159                                 $cache{$s} = \@out;
160                                 return @out;
161                         }
162                 }
163         }
164         return ();
165 }
166
167 #
168 # extract a 'prefix' from a callsign, in other words the largest entity that will
169 # obtain a result from the prefix table.
170 #
171 # This is done by repeated probing, callsigns of the type VO1/G1TLH or
172 # G1TLH/VO1 (should) return VO1
173 #
174
175 sub extract
176 {
177         my $calls = uc shift;
178         my @out;
179         my $p;
180         my @parts;
181         my ($call, $sp, $i);
182   
183         # clear out the cache periodically to stop it growing for ever.
184         if ($main::systime - $lasttime >= 15*60) {
185                 if (isdbg('prefix')) {
186                         my $percent = $hits * 100 / $matchtotal;
187                         dbg("Prefix Cache Cleared, Hits: $hits of $matchtotal = $percent\%") ;
188                 }
189                 my $percent = $hits * 100 / $matchtotal;
190                 dbg("Prefix Cache Cleared, $percent\% hits") if isdbg('prefix');
191                 %cache =();
192                 $lasttime = $main::systime;
193                 $hits = $matchtotal = 0;
194         } 
195
196 LM:     foreach $call (split /,/, $calls) {
197
198                 # first check if the whole thing succeeds either because it is cached
199                 # or because it simply is a stored prefix as callsign (or even a prefix)
200                 $matchtotal++;
201                 my $p = $cache{$call};
202                 my @nout;
203                 if ($p) {
204                         $hits++;
205                         if (isdbg('prefix')) {
206                                 my $percent = $hits * 100 / $matchtotal;
207                                 dbg("Prefix Cache Hit: $call Hits: $hits of $matchtotal = $percent\%");
208                         }
209                         push @out, @$p;
210                         next;
211                 } else {
212                         @nout =  get($call);
213                         if (@nout && $nout[0] eq $call) {
214                                 $cache{$call} = \@nout;
215                                 dbg("got exact prefix: $nout[0]") if isdbg('prefix');
216                                 push @out, @nout;
217                                 next;
218                         }
219                 }
220
221                 # now split the call into parts if required
222                 @parts = ($call =~ '/') ? split('/', $call) : ($call);
223                 dbg("Parts: $call = " . join(' ', @parts))      if isdbg('prefix');
224
225                 # remove any /0-9 /P /A /M /MM /AM suffixes etc
226                 if (@parts > 1) {
227                         @parts = grep { !/^\d+$/ && !/^[PABM]$/ && !/^(?:|AM|MM|BCN|JOTA|SIX|WEB|NET|Q\w+)$/; } @parts;
228
229                         # can we resolve them by direct lookup
230                         my $s = join('/', @parts); 
231                         @nout = get($s);
232                         if (@nout && $nout[0] eq $s) {
233                                 dbg("got exact multipart prefix: $call $s") if isdbg('prefix');
234                                 $cache{$call} = \@nout;
235                                 push @out, @nout;
236                                 next;
237                         }
238                 }
239                 dbg("Parts now: $call = " . join(' ', @parts))  if isdbg('prefix');
240   
241                 # at this point we should have two or three parts
242                 # if it is three parts then join the first and last parts together
243                 # to get an answer
244
245                 # first deal with prefix/x00xx/single letter things
246                 if (@parts == 3 && length $parts[0] <= length $parts[1]) {
247                         @nout = matchprefix($parts[0]);
248                         if (@nout) {
249                                 my $s = join('/', $nout[0], $parts[2]);
250                                 my @try = get($s);
251                                 if (@try && $try[0] eq $s) {
252                                         dbg("got 3 part prefix: $call $s") if isdbg('prefix');
253                                         $cache{$call} = \@try;
254                                         push @out, @try;
255                                         next;
256                                 }
257                                 
258                                 # if the second part is a callsign and the last part is one letter
259                                 if (is_callsign($parts[1]) && length $parts[2] == 1) {
260                                         pop @parts;
261                                 }
262                         }
263                 }
264
265                 # if it is a two parter 
266                 if (@parts == 2) {
267
268                         # try it as it is as compound, taking the first part as the prefix
269                         @nout = matchprefix($parts[0]);
270                         if (@nout) {
271                                 my $s = join('/', $nout[0], $parts[1]);
272                                 my @try = get($s);
273                                 if (@try && $try[0] eq $s) {
274                                         dbg("got 2 part prefix: $call $s") if isdbg('prefix');
275                                         $cache{$call} = \@try;
276                                         push @out, @try;
277                                         next;
278                                 }
279                         }
280                 }
281
282                 # remove the problematic /J suffix
283                 pop @parts if @parts > 1 && $parts[$#parts] eq 'J';
284
285                 # single parter
286                 if (@parts == 1) {
287                         @nout = matchprefix($parts[0]);
288                         if (@nout) {
289                                 dbg("got prefix: $call = $nout[0]") if isdbg('prefix');
290                                 $cache{$call} = \@nout;
291                                 push @out, @nout;
292                                 next;
293                         }
294                 }
295
296                 # try ALL the parts
297         my @checked;
298                 my $n;
299 L1:             for ($n = 0; $n < @parts; $n++) {
300                         my $sp = '';
301                         my ($k, $i);
302                         for ($i = $k = 0; $i < @parts; $i++) {
303                                 next if $checked[$i];
304                                 my $p = $parts[$i];
305                                 if (!$sp || length $p < length $sp) {
306                                         dbg("try part: $p") if isdbg('prefix');
307                                         $k = $i;
308                                         $sp = $p;
309                                 }
310                         }
311                         $checked[$k] = 1;
312                         $sp =~ s/-\d+$//;     # remove any SSID
313                         
314                         # now start to resolve it from the right hand end
315                         @nout = matchprefix($sp);
316                         
317                         # try and search for it in the descriptions as
318                         # a whole callsign if it has multiple parts and the output
319                         # is more two long, this should catch things like
320                         # FR5DX/T without having to explicitly stick it into
321                         # the prefix table.
322                         
323                         if (@nout) {
324                                 if (@parts > 1) {
325                                         $parts[$k] = $nout[0];
326                                         my $try = join('/', @parts);
327                                         my @try = get($try);
328                                         if (isdbg('prefix')) {
329                                                 my $part = $try[0] || "*";
330                                                 $part .= '*' unless $part eq '*' || $part eq $try;
331                                                 dbg("Compound prefix: $try $part" );
332                                         }
333                                         if (@try && $try eq $try[0]) {
334                                                 $cache{$call} = \@try;
335                                                 push @out, @try;
336                                         } else {
337                                                 $cache{$call} = \@nout;
338                                                 push @out, @nout;
339                                         }
340                                 } else {
341                                         $cache{$call} = \@nout;
342                                         push @out, @nout;
343                                 }
344                                 next LM;
345                         }
346                 }
347
348                 # we are a pirate!
349                 @nout = matchprefix('Q');
350                 $cache{$call} = \@nout;
351                 push @out, @nout;
352         }
353         
354         if (isdbg('prefix')) {
355                 my $dd = new Data::Dumper([ \@out ], [qw(@out)]);
356                 dbg($dd->Dumpxs);
357         }
358         return @out;
359 }
360
361 my %valid = (
362                          lat => '0,Latitude,slat',
363                          long => '0,Longitude,slong',
364                          dxcc => '0,DXCC',
365                          name => '0,Name',
366                          itu => '0,ITU',
367                          cq => '0,CQ',
368                          utcoff => '0,UTC offset',
369                          cont => '0,Continent',
370                         );
371
372 no strict;
373 sub AUTOLOAD
374 {
375         my $self = shift;
376         my $name = $AUTOLOAD;
377   
378         return if $name =~ /::DESTROY$/;
379         $name =~ s/.*:://o;
380   
381         confess "Non-existant field '$AUTOLOAD'" if !$valid{$name};
382         # this clever line of code creates a subroutine which takes over from autoload
383         # from OO Perl - Conway
384         *{$AUTOLOAD} = sub {@_ > 1 ? $_[0]->{$name} = $_[1] : $_[0]->{$name}} ;
385         if (@_) {
386                 $self->{$name} = shift;
387         }
388         return $self->{$name};
389 }
390 use strict;
391
392 #
393 # return a prompt for a field
394 #
395
396 sub field_prompt
397
398         my ($self, $ele) = @_;
399         return $valid{$ele};
400 }
401 1;
402
403 __END__