Make --quiet quieter again.
[weather.git] / weather.py
1 """Contains various object definitions needed by the weather utility."""
2
3 weather_copyright = """\
4 # Copyright (c) 2006-2012 Jeremy Stanley <fungi@yuggoth.org>. Permission to
5 # use, copy, modify, and distribute this software is granted under terms
6 # provided in the LICENSE file distributed with this software.
7 #"""
8
9 weather_version = "2.0"
10
11 radian_to_km = 6372.795484
12 radian_to_mi = 3959.871528
13
14 def pyversion(ref=None):
15     """Determine the Python version and optionally compare to a reference."""
16     import platform
17     ver = platform.python_version()
18     if ref:
19         return [
20             int(x) for x in ver.split(".")[:2]
21         ] >= [
22             int(x) for x in ref.split(".")[:2]
23         ]
24     else: return ver
25
26 class Selections:
27     """An object to contain selection data."""
28     def __init__(self):
29         """Store the config, options and arguments."""
30         self.config = get_config()
31         self.options, self.arguments = get_options(self.config)
32         if self.get_bool("cache") and self.get_bool("cache_search") \
33             and not self.get_bool("longlist"):
34             integrate_search_cache(
35                 self.config,
36                 self.get("cachedir"),
37                 self.get("setpath")
38             )
39         if not self.arguments:
40             if "id" in self.options.__dict__ \
41                 and self.options.__dict__["id"]:
42                 self.arguments.append( self.options.__dict__["id"] )
43                 del( self.options.__dict__["id"] )
44                 import sys
45                 message = "WARNING: the --id option is deprecated and will eventually be removed\n"
46                 sys.stderr.write(message)
47             elif "city" in self.options.__dict__ \
48                 and self.options.__dict__["city"] \
49                 and "st" in self.options.__dict__ \
50                 and self.options.__dict__["st"]:
51                 self.arguments.append(
52                     "^%s city, %s" % (
53                         self.options.__dict__["city"],
54                         self.options.__dict__["st"]
55                     )
56                 )
57                 del( self.options.__dict__["city"] )
58                 del( self.options.__dict__["st"] )
59                 import sys
60                 message = "WARNING: the --city/--st options are deprecated and will eventually be removed\n"
61                 sys.stderr.write(message)
62     def get(self, option, argument=None):
63         """Retrieve data from the config or options."""
64         if argument:
65             if self.config.has_section(argument) and (
66                 self.config.has_option(argument, "city") \
67                     or self.config.has_option(argument, "id") \
68                     or self.config.has_option(argument, "st")
69             ):
70                 self.config.remove_section(argument)
71                 import sys
72                 message = "WARNING: the city/id/st options are now unsupported in aliases\n"
73                 sys.stderr.write(message)
74             if not self.config.has_section(argument):
75                 guessed = guess(
76                     argument,
77                     path=self.get("setpath"),
78                     info=self.get("info"),
79                     cache_search=(
80                         self.get("cache") and self.get("cache_search")
81                     ),
82                     cachedir=self.get("cachedir"),
83                     quiet=self.get_bool("quiet")
84                 )
85                 self.config.add_section(argument)
86                 for item in guessed.items():
87                     self.config.set(argument, *item)
88             if self.config.has_option(argument, option):
89                 return self.config.get(argument, option)
90         if option in self.options.__dict__:
91             return self.options.__dict__[option]
92         else:
93             import os, sys
94             message = "%s error: no URI defined for %s\n" % (
95                 os.path.basename( sys.argv[0] ),
96                 option
97             )
98             sys.stderr.write(message)
99             exit(1)
100     def get_bool(self, option, argument=None):
101         """Get data and coerce to a boolean if necessary."""
102         return bool(self.get(option, argument))
103     def getint(self, option, argument=None):
104         """Get data and coerce to an integer if necessary."""
105         value = self.get(option, argument)
106         if value: return int(value)
107         else: return 0
108
109 def average(coords):
110     """Average a list of coordinates."""
111     x = 0
112     y = 0
113     for coord in coords:
114         x += coord[0]
115         y += coord[1]
116     count = len(coords)
117     return (x/count, y/count)
118
119 def filter_units(line, units="imperial"):
120     """Filter or convert units in a line of text between US/UK and metric."""
121     import re
122     # filter lines with both pressures in the form of "X inches (Y hPa)" or
123     # "X in. Hg (Y hPa)"
124     dual_p = re.match(
125         "(.* )(\d*(\.\d+)? (inches|in\. Hg)) \((\d*(\.\d+)? hPa)\)(.*)",
126         line
127     )
128     if dual_p:
129         preamble, in_hg, i_fr, i_un, hpa, h_fr, trailer = dual_p.groups()
130         if units == "imperial": line = preamble + in_hg + trailer
131         elif units == "metric": line = preamble + hpa + trailer
132     # filter lines with both temperatures in the form of "X F (Y C)"
133     dual_t = re.match(
134         "(.* )(-?\d*(\.\d+)? F) \((-?\d*(\.\d+)? C)\)(.*)",
135         line
136     )
137     if dual_t:
138         preamble, fahrenheit, f_fr, celsius, c_fr, trailer = dual_t.groups()
139         if units == "imperial": line = preamble + fahrenheit + trailer
140         elif units == "metric": line = preamble + celsius + trailer
141     # if metric is desired, convert distances in the form of "X mile(s)" to
142     # "Y kilometer(s)"
143     if units == "metric":
144         imperial_d = re.match(
145             "(.* )(\d+)( mile\(s\))(.*)",
146             line
147         )
148         if imperial_d:
149             preamble, mi, m_u, trailer = imperial_d.groups()
150             line = preamble + str(int(round(int(mi)*1.609344))) \
151                 + " kilometer(s)" + trailer
152     # filter speeds in the form of "X MPH (Y KT)" to just "X MPH"; if metric is
153     # desired, convert to "Z KPH"
154     imperial_s = re.match(
155         "(.* )(\d+)( MPH)( \(\d+ KT\))(.*)",
156         line
157     )
158     if imperial_s:
159         preamble, mph, m_u, kt, trailer = imperial_s.groups()
160         if units == "imperial": line = preamble + mph + m_u + trailer
161         elif units == "metric": 
162             line = preamble + str(int(round(int(mph)*1.609344))) + " KPH" + \
163                 trailer
164     imperial_s = re.match(
165         "(.* )(\d+)( MPH)( \(\d+ KT\))(.*)",
166         line
167     )
168     if imperial_s:
169         preamble, mph, m_u, kt, trailer = imperial_s.groups()
170         if units == "imperial": line = preamble + mph + m_u + trailer
171         elif units == "metric": 
172             line = preamble + str(int(round(int(mph)*1.609344))) + " KPH" + \
173                 trailer
174     # if imperial is desired, qualify given forcast temperatures like "X F"; if
175     # metric is desired, convert to "Y C"
176     imperial_t = re.match(
177         "(.* )(High |high |Low |low )(\d+)(\.|,)(.*)",
178         line
179     )
180     if imperial_t:
181         preamble, parameter, fahrenheit, sep, trailer = imperial_t.groups()
182         if units == "imperial":
183             line = preamble + parameter + fahrenheit + " F" + sep + trailer
184         elif units == "metric":
185             line = preamble + parameter \
186                 + str(int(round((int(fahrenheit)-32)*5/9))) + " C" + sep \
187                 + trailer
188     # hand off the resulting line
189     return line
190
191 def get_uri(
192     uri,
193     ignore_fail=False,
194     cache_data=False,
195     cacheage=900,
196     cachedir="."
197 ):
198     """Return a string containing the results of a URI GET."""
199     if pyversion("3"):
200         import urllib, urllib.error, urllib.request
201         URLError = urllib.error.URLError
202         urlopen = urllib.request.urlopen
203     else:
204         import urllib2 as urllib
205         URLError = urllib.URLError
206         urlopen = urllib.urlopen
207     import os, time
208     if cache_data:
209         dcachedir = os.path.join( os.path.expanduser(cachedir), "datacache" )
210         if not os.path.exists(dcachedir):
211             try: os.makedirs(dcachedir)
212             except (IOError, OSError): pass
213         dcache_fn = os.path.join(
214             dcachedir,
215             uri.split(":")[1].replace("/","_")
216         )
217     now = time.time()
218     if cache_data and os.access(dcache_fn, os.R_OK) \
219         and now-cacheage < os.stat(dcache_fn).st_mtime <= now:
220         dcache_fd = open(dcache_fn)
221         data = dcache_fd.read()
222         dcache_fd.close()
223     else:
224         try:
225             if pyversion("3"): data = urlopen(uri).read().decode("utf-8")
226             else: data = urlopen(uri).read()
227         except URLError:
228             if ignore_fail: return ""
229             else:
230                 import os, sys, traceback
231                 message = "%s error: failed to retrieve\n   %s\n   %s" % (
232                         os.path.basename( sys.argv[0] ),
233                         uri,
234                         traceback.format_exception_only(
235                             sys.exc_type,
236                             sys.exc_value
237                         )[0]
238                     )
239                 sys.stderr.write(message)
240                 sys.exit(1)
241         if cache_data:
242             try:
243                 import codecs
244                 dcache_fd = codecs.open(dcache_fn, "w", "utf-8")
245                 dcache_fd.write(data)
246                 dcache_fd.close()
247             except (IOError, OSError): pass
248     return data
249
250 def get_metar(
251     uri=None,
252     verbose=False,
253     quiet=False,
254     headers=None,
255     imperial=False,
256     metric=False,
257     cache_data=False,
258     cacheage=900,
259     cachedir="."
260 ):
261     """Return a summarized METAR for the specified station."""
262     if not uri:
263         import os, sys
264         message = "%s error: METAR URI required for conditions\n" % \
265             os.path.basename( sys.argv[0] )
266         sys.stderr.write(message)
267         sys.exit(1)
268     metar = get_uri(
269         uri,
270         cache_data=cache_data,
271         cacheage=cacheage,
272         cachedir=cachedir
273     )
274     if pyversion("3") and type(metar) is bytes: metar = metar.decode("utf-8")
275     if verbose: return metar
276     else:
277         import re
278         lines = metar.split("\n")
279         if not headers:
280             headers = \
281                 "relative_humidity," \
282                 + "precipitation_last_hour," \
283                 + "sky conditions," \
284                 + "temperature," \
285                 + "heat index," \
286                 + "windchill," \
287                 + "weather," \
288                 + "wind"
289         headerlist = headers.lower().replace("_"," ").split(",")
290         output = []
291         if not quiet:
292             title = "Current conditions at %s"
293             place = lines[0].split(", ")
294             if len(place) > 1:
295                 place = "%s, %s" % ( place[0].title(), place[1] )
296             else: place = "<UNKNOWN>"
297             output.append(title%place)
298             output.append("Last updated " + lines[1])
299         header_match = False
300         for header in headerlist:
301             for line in lines:
302                 if line.lower().startswith(header + ":"):
303                     if re.match(r".*:\d+$", line): line = line[:line.rfind(":")]
304                     if imperial: line = filter_units(line, units="imperial")
305                     elif metric: line = filter_units(line, units="metric")
306                     if quiet: output.append(line)
307                     else: output.append("   " + line)
308                     header_match = True
309         if not header_match:
310             output.append(
311                 "(no conditions matched your header list, try with --verbose)"
312             )
313         return "\n".join(output)
314
315 def get_alert(
316     uri=None,
317     verbose=False,
318     quiet=False,
319     cache_data=False,
320     cacheage=900,
321     cachedir="."
322 ):
323     """Return alert notice for the specified URI."""
324     if not uri:
325         import os, sys
326         message = "%s error: Alert URI required for alerts\n" % \
327             os.path.basename( sys.argv[0] )
328         sys.stderr.write(message)
329         sys.exit(1)
330     alert = get_uri(
331         uri,
332         ignore_fail=True,
333         cache_data=cache_data,
334         cacheage=cacheage,
335         cachedir=cachedir
336     ).strip()
337     if pyversion("3") and type(alert) is bytes: alert = alert.decode("utf-8")
338     if alert:
339         if verbose: return alert
340         else:
341             if alert.find("\nNATIONAL WEATHER SERVICE") == -1:
342                 muted = False
343             else:
344                 muted = True
345             lines = alert.split("\n")
346             import time
347             valid_time = time.strftime("%Y%m%d%H%M")
348             output = []
349             for line in lines:
350                 if line.startswith("Expires:") \
351                     and "Expires:" + valid_time > line:
352                     return ""
353                 if muted and line.startswith("NATIONAL WEATHER SERVICE"):
354                     muted = False
355                     line = ""
356                 elif line == "&&":
357                     line = ""
358                 elif line == "$$":
359                     muted = True
360                 if line and not muted:
361                     if quiet: output.append(line)
362                     else: output.append("   " + line)
363             return "\n".join(output)
364
365 def get_options(config):
366     """Parse the options passed on the command line."""
367
368     # for optparse's builtin -h/--help option
369     usage = \
370         "usage: %prog [options] [alias1|search1 [alias2|search2 [...]]]"
371
372     # for optparse's builtin --version option
373     verstring = "%prog " + weather_version
374
375     # create the parser
376     import optparse
377     option_parser = optparse.OptionParser(usage=usage, version=verstring)
378     # separate options object from list of arguments and return both
379
380     # the -a/--alert option
381     if config.has_option("default", "alert"):
382         default_alert = bool(config.get("default", "alert"))
383     else: default_alert = False
384     option_parser.add_option("-a", "--alert",
385         dest="alert",
386         action="store_true",
387         default=default_alert,
388         help="include local alert notices")
389
390     # the --atypes option
391     if config.has_option("default", "atypes"):
392         default_atypes = config.get("default", "atypes")
393     else:
394         default_atypes = \
395             "coastal_flood_statement," \
396             + "flash_flood_statement," \
397             + "flash_flood_warning," \
398             + "flash_flood_watch," \
399             + "flood_statement," \
400             + "flood_warning," \
401             + "marine_weather_statement," \
402             + "river_statement," \
403             + "severe_thunderstorm_warning," \
404             + "severe_weather_statement," \
405             + "short_term_forecast," \
406             + "special_marine_warning," \
407             + "special_weather_statement," \
408             + "tornado_warning," \
409             + "urgent_weather_message"
410     option_parser.add_option("--atypes",
411         dest="atypes",
412         default=default_atypes,
413         help="list of alert notification types to display")
414
415     # the --build-sets option
416     option_parser.add_option("--build-sets",
417         dest="build_sets",
418         action="store_true",
419         default=False,
420         help="(re)build location correlation sets")
421
422     # the --cacheage option
423     if config.has_option("default", "cacheage"):
424         default_cacheage = config.getint("default", "cacheage")
425     else: default_cacheage = 900
426     option_parser.add_option("--cacheage",
427         dest="cacheage",
428         default=default_cacheage,
429         help="duration in seconds to refresh cached data")
430
431     # the --cachedir option
432     if config.has_option("default", "cachedir"):
433         default_cachedir = config.get("default", "cachedir")
434     else: default_cachedir = "~/.weather"
435     option_parser.add_option("--cachedir",
436         dest="cachedir",
437         default=default_cachedir,
438         help="directory for storing cached searches and data")
439
440     # the -f/--forecast option
441     if config.has_option("default", "forecast"):
442         default_forecast = bool(config.get("default", "forecast"))
443     else: default_forecast = False
444     option_parser.add_option("-f", "--forecast",
445         dest="forecast",
446         action="store_true",
447         default=default_forecast,
448         help="include a local forecast")
449
450     # the --headers option
451     if config.has_option("default", "headers"):
452         default_headers = config.get("default", "headers")
453     else:
454         default_headers = \
455             "temperature," \
456             + "relative_humidity," \
457             + "wind," \
458             + "heat_index," \
459             + "windchill," \
460             + "weather," \
461             + "sky_conditions," \
462             + "precipitation_last_hour"
463     option_parser.add_option("--headers",
464         dest="headers",
465         default=default_headers,
466         help="list of conditions headers to display")
467
468     # the --imperial option
469     if config.has_option("default", "imperial"):
470         default_imperial = bool(config.get("default", "imperial"))
471     else: default_imperial = False
472     option_parser.add_option("--imperial",
473         dest="imperial",
474         action="store_true",
475         default=default_imperial,
476         help="filter/convert conditions for US/UK units")
477
478     # the --info option
479     option_parser.add_option("--info",
480         dest="info",
481         action="store_true",
482         default=False,
483         help="output detailed information for your search")
484
485     # the -l/--list option
486     option_parser.add_option("-l", "--list",
487         dest="list",
488         action="store_true",
489         default=False,
490         help="list all configured aliases and cached searches")
491
492     # the --longlist option
493     option_parser.add_option("--longlist",
494         dest="longlist",
495         action="store_true",
496         default=False,
497         help="display details of all configured aliases")
498
499     # the -m/--metric option
500     if config.has_option("default", "metric"):
501         default_metric = bool(config.get("default", "metric"))
502     else: default_metric = False
503     option_parser.add_option("-m", "--metric",
504         dest="metric",
505         action="store_true",
506         default=default_metric,
507         help="filter/convert conditions for metric units")
508
509     # the -n/--no-conditions option
510     if config.has_option("default", "conditions"):
511         default_conditions = bool(config.get("default", "conditions"))
512     else: default_conditions = True
513     option_parser.add_option("-n", "--no-conditions",
514         dest="conditions",
515         action="store_false",
516         default=default_conditions,
517         help="disable output of current conditions")
518
519     # the --no-cache option
520     if config.has_option("default", "cache"):
521         default_cache = bool(config.get("default", "cache"))
522     else: default_cache = True
523     option_parser.add_option("--no-cache",
524         dest="cache",
525         action="store_false",
526         default=True,
527         help="disable all caching (searches and data)")
528
529     # the --no-cache-data option
530     if config.has_option("default", "cache_data"):
531         default_cache_data = bool(config.get("default", "cache_data"))
532     else: default_cache_data = True
533     option_parser.add_option("--no-cache-data",
534         dest="cache_data",
535         action="store_false",
536         default=True,
537         help="disable retrieved data caching")
538
539     # the --no-cache-search option
540     if config.has_option("default", "cache_search"):
541         default_cache_search = bool(config.get("default", "cache_search"))
542     else: default_cache_search = True
543     option_parser.add_option("--no-cache-search",
544         dest="cache_search",
545         action="store_false",
546         default=True,
547         help="disable search result caching")
548
549     # the -q/--quiet option
550     if config.has_option("default", "quiet"):
551         default_quiet = bool(config.get("default", "quiet"))
552     else: default_quiet = False
553     option_parser.add_option("-q", "--quiet",
554         dest="quiet",
555         action="store_true",
556         default=default_quiet,
557         help="skip preambles and don't indent")
558
559     # the --setpath option
560     if config.has_option("default", "setpath"):
561         default_setpath = config.get("default", "setpath")
562     else: default_setpath = ".:~/.weather"
563     option_parser.add_option("--setpath",
564         dest="setpath",
565         default=default_setpath,
566         help="directory search path for correlation sets")
567
568     # the -v/--verbose option
569     if config.has_option("default", "verbose"):
570         default_verbose = bool(config.get("default", "verbose"))
571     else: default_verbose = False
572     option_parser.add_option("-v", "--verbose",
573         dest="verbose",
574         action="store_true",
575         default=default_verbose,
576         help="show full decoded feeds")
577
578     # deprecated options
579     if config.has_option("default", "city"):
580         default_city = config.get("default", "city")
581     else: default_city = ""
582     option_parser.add_option("-c", "--city",
583         dest="city",
584         default=default_city,
585         help=optparse.SUPPRESS_HELP)
586     if config.has_option("default", "id"):
587         default_id = config.get("default", "id")
588     else: default_id = ""
589     option_parser.add_option("-i", "--id",
590         dest="id",
591         default=default_id,
592         help=optparse.SUPPRESS_HELP)
593     if config.has_option("default", "st"):
594         default_st = config.get("default", "st")
595     else: default_st = ""
596     option_parser.add_option("-s", "--st",
597         dest="st",
598         default=default_st,
599         help=optparse.SUPPRESS_HELP)
600
601     options, arguments = option_parser.parse_args()
602     return options, arguments
603
604 def get_config():
605     """Parse the aliases and configuration."""
606     if pyversion("3"): import configparser
607     else: import ConfigParser as configparser
608     config = configparser.ConfigParser()
609     import os
610     rcfiles = [
611         "/etc/weatherrc",
612         os.path.expanduser("~/.weather/weatherrc"),
613         os.path.expanduser("~/.weatherrc"),
614         "weatherrc"
615         ]
616     for rcfile in rcfiles:
617         if os.access(rcfile, os.R_OK): config.read(rcfile)
618     for section in config.sections():
619         if section != section.lower():
620             if config.has_section(section.lower()):
621                 config.remove_section(section.lower())
622             config.add_section(section.lower())
623             for option,value in config.items(section):
624                 config.set(section.lower(), option, value)
625     return config
626
627 def integrate_search_cache(config, cachedir, setpath):
628     """Add cached search results into the configuration."""
629     if pyversion("3"): import configparser
630     else: import ConfigParser as configparser
631     import os, time
632     scache_fn = os.path.join( os.path.expanduser(cachedir), "searches" )
633     if not os.access(scache_fn, os.R_OK): return config
634     scache_fd = open(scache_fn)
635     created = float( scache_fd.readline().split(":")[1].strip().split()[0] )
636     scache_fd.close()
637     now = time.time()
638     datafiles = data_index(setpath)
639     if datafiles:
640         data_freshness = sorted(
641             [ x[1] for x in datafiles.values() ],
642             reverse=True
643         )[0]
644     else: data_freshness = now
645     if created < data_freshness <= now:
646         try:
647             os.remove(scache_fn)
648             print( "[clearing outdated %s]" % scache_fn )
649         except (IOError, OSError):
650             pass
651         return config
652     scache = configparser.ConfigParser()
653     scache.read(scache_fn)
654     for section in scache.sections():
655         if not config.has_section(section):
656             config.add_section(section)
657             for option,value in scache.items(section):
658                 config.set(section, option, value)
659     return config
660
661 def list_aliases(config, detail=False):
662     """Return a formatted list of aliases defined in the config."""
663     if detail:
664         output = "\n# configured alias details..."
665         for section in sorted(config.sections()):
666             output += "\n\n[%s]" % section
667             for item in sorted(config.items(section)):
668                 output += "\n%s = %s" % item
669         output += "\n"
670     else:
671         output = "configured aliases and cached searches..."
672         for section in sorted(config.sections()):
673             if config.has_option(section, "description"):
674                 description = config.get(section, "description")
675             else: description = "(no description provided)"
676             output += "\n   %s: %s" % (section, description)
677     return output
678
679 def data_index(path):
680     import os
681     datafiles = {}
682     for filename in ("airports", "places", "stations", "zctas", "zones"):
683         for dirname in path.split(":"):
684             for extension in ("", ".gz", ".txt"):
685                 candidate = os.path.expanduser(
686                     os.path.join( dirname, "".join( (filename, extension) ) )
687                 )
688                 if os.path.exists(candidate):
689                     datafiles[filename] = (
690                         candidate,
691                         os.stat(candidate).st_mtime
692                     )
693                     break
694     return datafiles
695
696 def guess(
697     expression,
698     path=".",
699     max_results=20,
700     info=False,
701     cache_search=False,
702     cacheage=900,
703     cachedir=".",
704     quiet=False
705 ):
706     """Find URIs using airport, gecos, placename, station, ZCTA/ZIP, zone."""
707     import codecs, datetime, time, os, re, sys
708     if pyversion("3"): import configparser
709     else: import ConfigParser as configparser
710     datafiles = data_index(path)
711     if re.match("[A-Za-z]{3}$", expression): searchtype = "airport"
712     elif re.match("[A-Za-z0-9]{4}$", expression): searchtype = "station"
713     elif re.match("[A-Za-z]{2}[Zz][0-9]{3}$", expression): searchtype = "zone"
714     elif re.match("[0-9]{5}$", expression): searchtype = "ZCTA"
715     elif re.match(
716         r"[\+-]?\d+(\.\d+)?(-\d+){,2}[ENSWensw]?, *[\+-]?\d+(\.\d+)?(-\d+){,2}[ENSWensw]?$",
717         expression
718     ):
719         searchtype = "coordinates"
720     elif re.match(r"(FIPS|fips)\d+$", expression): searchtype = "FIPS"
721     else:
722         searchtype = "name"
723         cache_search = False
724     if cache_search: action = "caching"
725     else: action = "using"
726     if info:
727         scores = [
728             (0.005, "bad"),
729             (0.025, "poor"),
730             (0.160, "suspect"),
731             (0.500, "mediocre"),
732             (0.840, "good"),
733             (0.975, "great"),
734             (0.995, "excellent"),
735             (1.000, "ideal"),
736         ]
737     if not quiet: print("Searching via %s..."%searchtype)
738     stations = configparser.ConfigParser()
739     dataname = "stations"
740     if dataname in datafiles:
741         datafile = datafiles[dataname][0]
742         if datafile.endswith(".gz"):
743             import gzip
744             stations.readfp( gzip.open(datafile) )
745         else:
746             stations.read(datafile)
747     else:
748         message = "%s error: can't find \"%s\" data file\n" % (
749             os.path.basename( sys.argv[0] ),
750             dataname
751         )
752         sys.stderr.write(message)
753         exit(1)
754     zones = configparser.ConfigParser()
755     dataname = "zones"
756     if dataname in datafiles:
757         datafile = datafiles[dataname][0]
758         if datafile.endswith(".gz"):
759             import gzip
760             zones.readfp( gzip.open(datafile) )
761         else:
762             zones.read(datafile)
763     else:
764         message = "%s error: can't find \"%s\" data file\n" % (
765             os.path.basename( sys.argv[0] ),
766             dataname
767         )
768         sys.stderr.write(message)
769         exit(1)
770     search = None
771     station = ("", 0)
772     zone = ("", 0)
773     dataset = None
774     possibilities = []
775     uris = {}
776     if searchtype == "airport":
777         expression = expression.lower()
778         airports = configparser.ConfigParser()
779         dataname = "airports"
780         if dataname in datafiles:
781             datafile = datafiles[dataname][0]
782             if datafile.endswith(".gz"):
783                 import gzip
784                 airports.readfp( gzip.open(datafile) )
785             else:
786                 airports.read(datafile)
787         else:
788             message = "%s error: can't find \"%s\" data file\n" % (
789                 os.path.basename( sys.argv[0] ),
790                 dataname
791             )
792             sys.stderr.write(message)
793             exit(1)
794         if airports.has_section(expression) \
795             and airports.has_option(expression, "station"):
796             search = (expression, "IATA/FAA airport code %s" % expression)
797             station = ( airports.get(expression, "station"), 0 )
798             if stations.has_option(station[0], "zone"):
799                 zone = eval( stations.get(station[0], "zone") )
800                 dataset = stations
801             if not ( info or quiet ) \
802                 and stations.has_option( station[0], "description" ):
803                 print(
804                     "[%s result %s]" % (
805                         action,
806                         stations.get(station[0], "description")
807                     )
808                 )
809         else:
810             message = "No IATA/FAA airport code \"%s\" in the %s file.\n" % (
811                 expression,
812                 datafiles["airports"][0]
813             )
814             sys.stderr.write(message)
815             exit(1)
816     elif searchtype == "station":
817         expression = expression.lower()
818         if stations.has_section(expression):
819             station = (expression, 0)
820             if not search:
821                 search = (expression, "ICAO station code %s" % expression)
822             if stations.has_option(expression, "zone"):
823                 zone = eval( stations.get(expression, "zone") )
824                 dataset = stations
825             if not ( info or quiet ) \
826                 and stations.has_option(expression, "description"):
827                 print(
828                     "[%s result %s]" % (
829                         action,
830                         stations.get(expression, "description")
831                     )
832                 )
833         else:
834             message = "No ICAO weather station \"%s\" in the %s file.\n" % (
835                 expression,
836                 datafiles["stations"][0]
837             )
838             sys.stderr.write(message)
839             exit(1)
840     elif searchtype == "zone":
841         expression = expression.lower()
842         if zones.has_section(expression) \
843             and zones.has_option(expression, "station"):
844             zone = (expression, 0)
845             station = eval( zones.get(expression, "station") )
846             dataset = zones
847             search = (expression, "NWS/NOAA weather zone %s" % expression)
848             if not ( info or quiet ) \
849                 and zones.has_option(expression, "description"):
850                 print(
851                     "[%s result %s]" % (
852                         action,
853                         zones.get(expression, "description")
854                     )
855                 )
856         else:
857             message = "No usable NWS weather zone \"%s\" in the %s file.\n" % (
858                 expression,
859                 datafiles["zones"][0]
860             )
861             sys.stderr.write(message)
862             exit(1)
863     elif searchtype == "ZCTA":
864         zctas = configparser.ConfigParser()
865         dataname = "zctas"
866         if dataname in datafiles:
867             datafile = datafiles[dataname][0]
868             if datafile.endswith(".gz"):
869                 import gzip
870                 zctas.readfp( gzip.open(datafile) )
871             else:
872                 zctas.read(datafile)
873         else:
874             message = "%s error: can't find \"%s\" data file\n" % (
875                 os.path.basename( sys.argv[0] ),
876                 dataname
877             )
878             sys.stderr.write(message)
879             exit(1)
880         dataset = zctas
881         if zctas.has_section(expression) \
882             and zctas.has_option(expression, "station"):
883             station = eval( zctas.get(expression, "station") )
884             search = (expression, "Census ZCTA (ZIP code) %s" % expression)
885             if zctas.has_option(expression, "zone"):
886                 zone = eval( zctas.get(expression, "zone") )
887         else:
888             message = "No census ZCTA (ZIP code) \"%s\" in the %s file.\n" % (
889                 expression,
890                 datafiles["zctas"][0]
891             )
892             sys.stderr.write(message)
893             exit(1)
894     elif searchtype == "coordinates":
895         search = (expression, "Geographic coordinates %s" % expression)
896         stationtable = {}
897         for station in stations.sections():
898             if stations.has_option(station, "location"):
899                 stationtable[station] = {
900                     "location": eval( stations.get(station, "location") )
901                 }
902         station = closest( gecos(expression), stationtable, "location", 0.1 )
903         if not station[0]:
904             message = "No ICAO weather station found near %s.\n" % expression
905             sys.stderr.write(message)
906             exit(1)
907         zonetable = {}
908         for zone in zones.sections():
909             if zones.has_option(zone, "centroid"):
910                 zonetable[zone] = {
911                     "centroid": eval( zones.get(zone, "centroid") )
912                 }
913         zone = closest( gecos(expression), zonetable, "centroid", 0.1 )
914         if not zone[0]:
915             message = "No NWS weather zone near %s; forecasts unavailable.\n" \
916                 % expression
917             sys.stderr.write(message)
918     elif searchtype in ("FIPS", "name"):
919         places = configparser.ConfigParser()
920         dataname = "places"
921         if dataname in datafiles:
922             datafile = datafiles[dataname][0]
923             if datafile.endswith(".gz"):
924                 import gzip
925                 places.readfp( gzip.open(datafile) )
926             else:
927                 places.read(datafile)
928         else:
929             message = "%s error: can't find \"%s\" data file\n" % (
930                 os.path.basename( sys.argv[0] ),
931                 dataname
932             )
933             sys.stderr.write(message)
934             exit(1)
935         dataset = places
936         place = expression.lower()
937         if places.has_section(place) and places.has_option(place, "station"):
938             station = eval( places.get(place, "station") )
939             search = (expression, "Census Place %s" % expression)
940             if places.has_option(place, "description"):
941                 search = (
942                     search[0],
943                     search[1] + ", %s" % places.get(place, "description")
944                 )
945             if places.has_option(place, "zone"):
946                 zone = eval( places.get(place, "zone") )
947             if not ( info or quiet ) \
948                 and places.has_option(place, "description"):
949                 print(
950                     "[%s result %s]" % (
951                         action,
952                         places.get(place, "description")
953                     )
954                 )
955         else:
956             for place in places.sections():
957                 if places.has_option(place, "description") \
958                     and places.has_option(place, "station") \
959                     and re.search(
960                         expression,
961                         places.get(place, "description"),
962                         re.I
963                     ):
964                         possibilities.append(place)
965             for place in stations.sections():
966                 if stations.has_option(place, "description") \
967                     and re.search(
968                         expression,
969                         stations.get(place, "description"),
970                         re.I
971                     ):
972                         possibilities.append(place)
973             for place in zones.sections():
974                 if zones.has_option(place, "description") \
975                     and zones.has_option(place, "station") \
976                     and re.search(
977                         expression,
978                         zones.get(place, "description"),
979                         re.I
980                     ):
981                         possibilities.append(place)
982             if len(possibilities) == 1:
983                 place = possibilities[0]
984                 if places.has_section(place):
985                     station = eval( places.get(place, "station") )
986                     description = places.get(place, "description")
987                     if places.has_option(place, "zone"):
988                         zone = eval( places.get(place, "zone" ) )
989                     search = ( expression, "%s: %s" % (place, description) )
990                 elif stations.has_section(place):
991                     station = (place, 0.0)
992                     description = stations.get(place, "description")
993                     if stations.has_option(place, "zone"):
994                         zone = eval( stations.get(place, "zone" ) )
995                     search = ( expression, "ICAO station code %s" % place )
996                 elif zones.has_section(place):
997                     station = eval( zones.get(place, "station") )
998                     description = zones.get(place, "description")
999                     zone = (place, 0.0)
1000                     search = ( expression, "NWS/NOAA weather zone %s" % place )
1001                 if not ( info or quiet ):
1002                     print( "[%s result %s]" % (action, description) )
1003             if not possibilities and not station[0]:
1004                 message = "No FIPS code/census area match in the %s file.\n" % (
1005                     datafiles["places"][0]
1006                 )
1007                 sys.stderr.write(message)
1008                 exit(1)
1009     if station[0]:
1010         uris["metar"] = stations.get( station[0], "metar" )
1011         if zone[0]:
1012             for key,value in zones.items( zone[0] ):
1013                 if key not in ("centroid", "description", "station"):
1014                     uris[key] = value
1015     elif possibilities:
1016         count = len(possibilities)
1017         if count <= max_results:
1018             print( "Your search is ambiguous, returning %s matches:" % count )
1019             for place in sorted(possibilities):
1020                 if places.has_section(place):
1021                     print(
1022                         "   [%s] %s" % (
1023                             place,
1024                             places.get(place, "description")
1025                         )
1026                     )
1027                 elif stations.has_section(place):
1028                     print(
1029                         "   [%s] %s" % (
1030                             place,
1031                             stations.get(place, "description")
1032                         )
1033                     )
1034                 elif zones.has_section(place):
1035                     print(
1036                         "   [%s] %s" % (
1037                             place,
1038                             zones.get(place, "description")
1039                         )
1040                     )
1041         else:
1042             print(
1043                 "Your search is too ambiguous, returning %s matches." % count
1044             )
1045         exit(0)
1046     if info:
1047         stationlist = []
1048         zonelist = []
1049         if dataset:
1050             for section in dataset.sections():
1051                 if dataset.has_option(section, "station"):
1052                     stationlist.append(
1053                         eval( dataset.get(section, "station") )[1]
1054                     )
1055                 if dataset.has_option(section, "zone"):
1056                     zonelist.append( eval( dataset.get(section, "zone") )[1] )
1057         stationlist.sort()
1058         zonelist.sort()
1059         scount = len(stationlist)
1060         zcount = len(zonelist)
1061         sranks = []
1062         zranks = []
1063         for score in scores:
1064             if stationlist:
1065                 sranks.append( stationlist[ int( (1-score[0]) * scount ) ] )
1066             if zonelist:
1067                 zranks.append( zonelist[ int( (1-score[0]) * zcount ) ] )
1068         description = search[1]
1069         uris["description"] = description
1070         print(
1071             "%s\n%s" % ( description, "-" * len(description) )
1072         )
1073         print(
1074             "%s: %s" % (
1075                 station[0],
1076                 stations.get( station[0], "description" )
1077             )
1078         )
1079         km = radian_to_km*station[1]
1080         mi = radian_to_mi*station[1]
1081         if sranks and not description.startswith("ICAO station code "):
1082             for index in range(0, len(scores)):
1083                 if station[1] >= sranks[index]:
1084                     score = scores[index][1]
1085                     break
1086             print(
1087                 "   (proximity %s, %.3gkm, %.3gmi)" % ( score, km, mi )
1088             )
1089         elif searchtype is "coordinates":
1090             print( "   (%.3gkm, %.3gmi)" % (km, mi) )
1091         if zone[0]:
1092             print(
1093                 "%s: %s" % ( zone[0], zones.get( zone[0], "description" ) )
1094             )
1095         km = radian_to_km*zone[1]
1096         mi = radian_to_mi*zone[1]
1097         if zranks and not description.startswith("NWS/NOAA weather zone "):
1098             for index in range(0, len(scores)):
1099                 if zone[1] >= zranks[index]:
1100                     score = scores[index][1]
1101                     break
1102             print(
1103                 "   (proximity %s, %.3gkm, %.3gmi)" % ( score, km, mi )
1104             )
1105         elif searchtype is "coordinates" and zone[0]:
1106             print( "   (%.3gkm, %.3gmi)" % (km, mi) )
1107     if cache_search:
1108         now = time.time()
1109         nowstamp = "%s (%s)" % (
1110             now,
1111             datetime.datetime.isoformat(
1112                 datetime.datetime.fromtimestamp(now),
1113                 " "
1114             )
1115         )
1116         search_cache = ["\n"]
1117         search_cache.append( "[%s]\n" % search[0] ) 
1118         search_cache.append( "description = cached %s\n" % nowstamp )
1119         for uriname in sorted(uris.keys()):
1120             search_cache.append( "%s = %s\n" % ( uriname, uris[uriname] ) )
1121         real_cachedir = os.path.expanduser(cachedir)
1122         if not os.path.exists(real_cachedir):
1123             try: os.makedirs(real_cachedir)
1124             except (IOError, OSError): pass
1125         scache_fn = os.path.join(real_cachedir, "searches")
1126         if not os.path.exists(scache_fn):
1127             then = sorted(
1128                     [ x[1] for x in datafiles.values() ],
1129                     reverse=True
1130                 )[0]
1131             thenstamp = "%s (%s)" % (
1132                 then,
1133                 datetime.datetime.isoformat(
1134                     datetime.datetime.fromtimestamp(then),
1135                     " "
1136                 )
1137             )
1138             search_cache.insert(
1139                 0,
1140                 "# based on data files from: %s\n" % thenstamp
1141             )
1142         try:
1143             scache_existing = configparser.ConfigParser()
1144             scache_existing.read(scache_fn)
1145             if not scache_existing.has_section(search[0]):
1146                 scache_fd = codecs.open(scache_fn, "a", "utf-8")
1147                 scache_fd.writelines(search_cache)
1148                 scache_fd.close()
1149         except (IOError, OSError): pass
1150     if not info:
1151         return(uris)
1152
1153 def closest(position, nodes, fieldname, angle=None):
1154     import math
1155     if not angle: angle = 2*math.pi
1156     match = None
1157     for name in nodes:
1158         if fieldname in nodes[name]:
1159             node = nodes[name][fieldname]
1160             if node and abs( position[0]-node[0] ) < angle:
1161                 if abs( position[1]-node[1] ) < angle \
1162                     or abs( abs( position[1]-node[1] ) - 2*math.pi ) < angle:
1163                     if position == node:
1164                         angle = 0
1165                         match = name
1166                     else:
1167                         candidate = math.acos(
1168                             math.sin( position[0] ) * math.sin( node[0] ) \
1169                                 + math.cos( position[0] ) \
1170                                 * math.cos( node[0] ) \
1171                                 * math.cos( position[1] - node[1] )
1172                             )
1173                         if candidate < angle:
1174                             angle = candidate
1175                             match = name
1176     if match: match = str(match)
1177     return (match, angle)
1178
1179 def gecos(formatted):
1180     import math, re
1181     coordinates = formatted.split(",")
1182     for coordinate in range(0, 2):
1183         degrees, foo, minutes, bar, seconds, hemisphere = re.match(
1184             r"([\+-]?\d+\.?\d*)(-(\d+))?(-(\d+))?([ensw]?)$",
1185             coordinates[coordinate].strip().lower()
1186         ).groups()
1187         value = float(degrees)
1188         if minutes: value += float(minutes)/60
1189         if seconds: value += float(seconds)/3600
1190         if hemisphere and hemisphere in "sw": value *= -1
1191         coordinates[coordinate] = math.radians(value)
1192     return tuple(coordinates)
1193
1194 def correlate():
1195     import codecs, datetime, hashlib, os, re, sys, tarfile, time, zipfile
1196     if pyversion("3"): import configparser
1197     else: import ConfigParser as configparser
1198     gcounties_an = "Gaz_counties_national.zip"
1199     gcounties_fn = "Gaz_counties_national.txt"
1200     gcousubs_an = "Gaz_cousubs_national.zip"
1201     gcousubs_fn = "Gaz_cousubs_national.txt"
1202     gplaces_an = "Gaz_places_national.zip"
1203     gplaces_fn = "Gaz_places_national.txt"
1204     gzcta_an = "Gaz_zcta_national.zip"
1205     gzcta_fn = "Gaz_zcta_national.txt"
1206     for filename in os.listdir("."):
1207         if re.match("bp[0-9][0-9][a-z][a-z][0-9][0-9].dbx$", filename):
1208             cpfzcf_fn = filename
1209             break
1210     nsdcccc_fn = "nsd_cccc.txt"
1211     zcatalog_an = "zonecatalog.curr.tar"
1212     metartbl_fn = "metar.tbl"
1213     coopact_fn = "COOP-ACT.TXT"
1214     overrides_fn = "overrides.conf"
1215     overrideslog_fn = "overrides.log"
1216     slist_fn = "slist"
1217     zlist_fn = "zlist"
1218     qalog_fn = "qa.log"
1219     airports_fn = "airports"
1220     places_fn = "places"
1221     stations_fn = "stations"
1222     zctas_fn = "zctas"
1223     zones_fn = "zones"
1224     header = """\
1225 %s
1226 # generated by %s on %s from these public domain sources:
1227 #
1228 # http://www.census.gov/geo/www/gazetteer/gazetteer2010.html
1229 # %s %s %s
1230 # %s %s %s
1231 # %s %s %s
1232 # %s %s %s
1233 #
1234 # http://www.weather.gov/geodata/catalog/wsom/html/cntyzone.htm
1235 # %s %s %s
1236 #
1237 # http://weather.noaa.gov/data/nsd_cccc.txt
1238 # %s %s %s
1239 #
1240 # http://weather.noaa.gov/pub/data/zonecatalog.curr.tar
1241 # %s %s %s
1242 #
1243 # http://www.nco.ncep.noaa.gov/pmb/codes/nwprod/dictionaries/metar.tbl
1244 # %s %s %s
1245 #
1246 # ftp://ftp.ncdc.noaa.gov/pub/data/inventories/COOP-ACT.TXT
1247 # %s %s %s
1248 #
1249 # ...and these manually-generated or hand-compiled adjustments:
1250 # %s %s %s
1251 # %s %s %s
1252 # %s %s %s\
1253 """ % (
1254         weather_copyright,
1255         os.path.basename( sys.argv[0] ),
1256         datetime.date.isoformat(
1257             datetime.datetime.fromtimestamp( time.time() )
1258         ),
1259         hashlib.md5( open(gcounties_an, "rb").read() ).hexdigest(),
1260         datetime.date.isoformat(
1261             datetime.datetime.fromtimestamp( os.path.getmtime(gcounties_an) )
1262         ),
1263         gcounties_an,
1264         hashlib.md5( open(gcousubs_an, "rb").read() ).hexdigest(),
1265         datetime.date.isoformat(
1266             datetime.datetime.fromtimestamp( os.path.getmtime(gcousubs_an) )
1267         ),
1268         gcousubs_an,
1269         hashlib.md5( open(gplaces_an, "rb").read() ).hexdigest(),
1270         datetime.date.isoformat(
1271             datetime.datetime.fromtimestamp( os.path.getmtime(gplaces_an) )
1272         ),
1273         gplaces_an,
1274         hashlib.md5( open(gzcta_an, "rb").read() ).hexdigest(),
1275         datetime.date.isoformat(
1276             datetime.datetime.fromtimestamp( os.path.getmtime(gzcta_an) )
1277         ),
1278         gzcta_an,
1279         hashlib.md5( open(cpfzcf_fn, "rb").read() ).hexdigest(),
1280         datetime.date.isoformat(
1281             datetime.datetime.fromtimestamp( os.path.getmtime(cpfzcf_fn) )
1282         ),
1283         cpfzcf_fn,
1284         hashlib.md5( open(nsdcccc_fn, "rb").read() ).hexdigest(),
1285         datetime.date.isoformat(
1286             datetime.datetime.fromtimestamp( os.path.getmtime(nsdcccc_fn) )
1287         ),
1288         nsdcccc_fn,
1289         hashlib.md5( open(zcatalog_an, "rb").read() ).hexdigest(),
1290         datetime.date.isoformat(
1291             datetime.datetime.fromtimestamp( os.path.getmtime(zcatalog_an) )
1292         ),
1293         zcatalog_an,
1294         hashlib.md5( open(metartbl_fn, "rb").read() ).hexdigest(),
1295         datetime.date.isoformat(
1296             datetime.datetime.fromtimestamp( os.path.getmtime(metartbl_fn) )
1297         ),
1298         metartbl_fn,
1299         hashlib.md5( open(coopact_fn, "rb").read() ).hexdigest(),
1300         datetime.date.isoformat(
1301             datetime.datetime.fromtimestamp( os.path.getmtime(coopact_fn) )
1302         ),
1303         coopact_fn,
1304         hashlib.md5( open(overrides_fn, "rb").read() ).hexdigest(),
1305         datetime.date.isoformat(
1306             datetime.datetime.fromtimestamp( os.path.getmtime(overrides_fn) )
1307         ),
1308         overrides_fn,
1309         hashlib.md5( open(slist_fn, "rb").read() ).hexdigest(),
1310         datetime.date.isoformat(
1311             datetime.datetime.fromtimestamp( os.path.getmtime(slist_fn) )
1312         ),
1313         slist_fn,
1314         hashlib.md5( open(zlist_fn, "rb").read() ).hexdigest(),
1315         datetime.date.isoformat(
1316             datetime.datetime.fromtimestamp( os.path.getmtime(zlist_fn) )
1317         ),
1318         zlist_fn
1319     )
1320     airports = {}
1321     places = {}
1322     stations = {}
1323     zctas = {}
1324     zones = {}
1325     message = "Reading %s:%s..." % (gcounties_an, gcounties_fn)
1326     sys.stdout.write(message)
1327     sys.stdout.flush()
1328     count = 0
1329     gcounties = zipfile.ZipFile(gcounties_an).open(gcounties_fn, "rU")
1330     for line in gcounties:
1331         fields = line.decode("latin1").strip().split("\t")
1332         if len(fields) == 10 and fields[0] != "STUSPS":
1333             fips = "fips%s" % fields[1]
1334             description = "%s, %s" % ( fields[3], fields[0] )
1335             centroid = gecos( ",".join( fields[8:10] ) )
1336             if fips not in places: places[fips] = {}
1337             places[fips]["centroid"] = centroid
1338             places[fips]["description"] = description
1339             count += 1
1340     gcounties.close()
1341     print("done (%s lines)." % count)
1342     message = "Reading %s:%s..." % (gcousubs_an, gcousubs_fn)
1343     sys.stdout.write(message)
1344     sys.stdout.flush()
1345     count = 0
1346     gcousubs = zipfile.ZipFile(gcousubs_an).open(gcousubs_fn, "rU")
1347     for line in gcousubs:
1348         fields = line.decode("latin1").strip().split("\t")
1349         if len(fields) == 10 and fields[0] != "STUSPS":
1350             fips = "fips%s" % fields[1]
1351             description = "%s, %s" % ( fields[3], fields[0] )
1352             centroid = gecos( ",".join( fields[8:10] ) )
1353             if fips not in places: places[fips] = {}
1354             places[fips]["centroid"] = centroid
1355             places[fips]["description"] = description
1356             count += 1
1357     gcousubs.close()
1358     print("done (%s lines)." % count)
1359     message = "Reading %s:%s..." % (gplaces_an, gplaces_fn)
1360     sys.stdout.write(message)
1361     sys.stdout.flush()
1362     count = 0
1363     gplaces = zipfile.ZipFile(gplaces_an).open(gplaces_fn, "rU")
1364     for line in gplaces:
1365         fields = line.decode("latin1").strip().split("\t")
1366         if len(fields) == 10 and fields[0] != "STUSPS":
1367             fips = "fips%s" % fields[1]
1368             description = "%s, %s" % ( fields[3], fields[0] )
1369             centroid = gecos( ",".join( fields[8:10] ) )
1370             if fips not in places: places[fips] = {}
1371             places[fips]["centroid"] = centroid
1372             places[fips]["description"] = description
1373             count += 1
1374     gplaces.close()
1375     print("done (%s lines)." % count)
1376     message = "Reading %s..." % slist_fn
1377     sys.stdout.write(message)
1378     sys.stdout.flush()
1379     count = 0
1380     slist = codecs.open(slist_fn, "rU")
1381     for line in slist:
1382         icao = line.split("#")[0].strip()
1383         if icao:
1384             stations[icao] = {
1385                 "metar": "http://weather.noaa.gov/pub/data/observations/"\
1386                     + "metar/decoded/%s.TXT" % icao.upper()
1387             }
1388             count += 1
1389     slist.close()
1390     print("done (%s lines)." % count)
1391     message = "Reading %s..." % metartbl_fn
1392     sys.stdout.write(message)
1393     sys.stdout.flush()
1394     count = 0
1395     metartbl = codecs.open(metartbl_fn, "rU")
1396     for line in metartbl:
1397         icao = line[:4].strip().lower()
1398         if icao in stations:
1399             description = []
1400             name = " ".join(
1401                 line[16:48].replace("_", " ").strip().title().split()
1402             )
1403             if name: description.append(name)
1404             st = line[49:51].strip()
1405             if st: description.append(st)
1406             cn = line[52:54].strip()
1407             if cn: description.append(cn)
1408             if description:
1409                 stations[icao]["description"] = ", ".join(description)
1410             lat = line[55:60].strip()
1411             if lat:
1412                 lat = int(lat)/100.0
1413                 lon = line[61:67].strip()
1414                 if lon:
1415                     lon = int(lon)/100.0
1416                     stations[icao]["location"] = gecos( "%s,%s" % (lat, lon) )
1417         count += 1
1418     metartbl.close()
1419     print("done (%s lines)." % count)
1420     message = "Reading %s..." % nsdcccc_fn
1421     sys.stdout.write(message)
1422     sys.stdout.flush()
1423     count = 0
1424     nsdcccc = codecs.open(nsdcccc_fn, "rU", "latin1")
1425     for line in nsdcccc:
1426         line = str(line)
1427         fields = line.split(";")
1428         icao = fields[0].strip().lower()
1429         if icao in stations:
1430             description = []
1431             name = " ".join( fields[3].strip().title().split() )
1432             if name: description.append(name)
1433             st = fields[4].strip()
1434             if st: description.append(st)
1435             country = " ".join( fields[5].strip().title().split() )
1436             if country: description.append(country)
1437             if description:
1438                 stations[icao]["description"] = ", ".join(description)
1439             lat, lon = fields[7:9]
1440             if lat and lon:
1441                 stations[icao]["location"] = gecos( "%s,%s" % (lat, lon) )
1442             elif "location" not in stations[icao]:
1443                 lat, lon = fields[5:7]
1444                 if lat and lon:
1445                     stations[icao]["location"] = gecos( "%s,%s" % (lat, lon) )
1446         count += 1
1447     nsdcccc.close()
1448     print("done (%s lines)." % count)
1449     message = "Reading %s..." % coopact_fn
1450     sys.stdout.write(message)
1451     sys.stdout.flush()
1452     count = 0
1453     coopact = open(coopact_fn)
1454     for line in coopact:
1455         icao = line[33:37].strip().lower()
1456         if icao in stations:
1457             iata = line[22:26].strip().lower()
1458             if len(iata) == 3: airports[iata] = { "station": icao }
1459             if "description" not in stations[icao]:
1460                 description = []
1461                 name = " ".join( line[99:129].strip().title().split() )
1462                 if name: description.append(name)
1463                 st = line[59:61].strip()
1464                 if st: description.append(st)
1465                 country = " ".join( line[38:58].strip().title().split() )
1466                 if country: description.append(country)
1467                 if description:
1468                     stations[icao]["description"] = ", ".join(description)
1469             if "location" not in stations[icao]:
1470                 lat = line[130:139].strip()
1471                 if lat:
1472                     lat = lat.replace(" ", "-")
1473                     lon = line[140:150].strip()
1474                     if lon:
1475                         lon = lon.replace(" ", "-")
1476                         stations[icao]["location"] = gecos(
1477                             "%s,%s" % (lat, lon)
1478                         )
1479         count += 1
1480     coopact.close()
1481     print("done (%s lines)." % count)
1482     message = "Reading %s..." % zlist_fn
1483     sys.stdout.write(message)
1484     sys.stdout.flush()
1485     count = 0
1486     zlist = codecs.open(zlist_fn, "rU")
1487     for line in zlist:
1488         line = line.split("#")[0].strip()
1489         if line:
1490             zones[line] = {}
1491             count += 1
1492     zlist.close()
1493     print("done (%s lines)." % count)
1494     message = "Reading %s:*..." % zcatalog_an
1495     sys.stdout.write(message)
1496     sys.stdout.flush()
1497     count = 0
1498     zcatalog = tarfile.open(zcatalog_an)
1499     for entry in zcatalog.getmembers():
1500         if entry.isfile():
1501             fnmatch = re.match(
1502                 r"([a-z]+z[0-9]+)\.txt$",
1503                 os.path.basename(entry.name)
1504             )
1505             if fnmatch:
1506                 zone = fnmatch.group(1)
1507                 if zone in zones:
1508                     data = zcatalog.extractfile(entry).readlines()
1509                     description = data[0].decode("ascii").strip()
1510                     zones[zone]["description"] = description
1511                     for line in data[1:]:
1512                         line = line.decode("latin1").strip()
1513                         urimatch = re.match("/webdocs/(.+):(.+) for ", line)
1514                         if urimatch:
1515                             uritype = urimatch.group(2).lower().replace(" ","_")
1516                             zones[zone][uritype] \
1517                                 = "http://weather.noaa.gov/%s" \
1518                                 % urimatch.group(1)
1519         count += 1
1520     zcatalog.close()
1521     print("done (%s files)." % count)
1522     message = "Reading %s..." % cpfzcf_fn
1523     sys.stdout.write(message)
1524     sys.stdout.flush()
1525     count = 0
1526     cpfz = {}
1527     cpfzcf = open(cpfzcf_fn)
1528     for line in cpfzcf:
1529         fields = line.split("|")
1530         if len(fields) == 11 \
1531             and fields[0] and fields[1] and fields[9] and fields[10]:
1532             zone = "z".join( fields[:2] ).lower()
1533             if zone in zones:
1534                 zones[zone]["centroid"] = gecos( ",".join( fields[9:] ) )
1535             elif fields[6]:
1536                 state = fields[0]
1537                 description = fields[3]
1538                 county = fields[5]
1539                 fips = "fips%s"%fields[6]
1540                 possible = [
1541                     "%s, %s" % (county, state),
1542                     "%s County, %s" % (county, state),
1543                 ]
1544                 if description.endswith(" Counties"):
1545                     description = description[:-9]
1546                 for addition in description.split(" and "):
1547                     possible.append( "%s, %s" % (addition, state) )
1548                     possible.append( "%s County, %s" % (addition, state) )
1549                 if fips in places and "centroid" in places[fips]:
1550                     for candidate in zones:
1551                         if "centroid" not in zones[candidate] and \
1552                             "description" in zones[candidate] and \
1553                             zones[candidate]["description"] in possible:
1554                             zones[candidate]["centroid"] = \
1555                                 places[fips]["centroid"]
1556         count += 1
1557     cpfzcf.close()
1558     print("done (%s lines)." % count)
1559     message = "Reading %s:%s..." % (gzcta_an, gzcta_fn)
1560     sys.stdout.write(message)
1561     sys.stdout.flush()
1562     count = 0
1563     gzcta = zipfile.ZipFile(gzcta_an).open(gzcta_fn, "rU")
1564     for line in gzcta:
1565         fields = line.decode("latin1").strip().split("\t")
1566         if len(fields) == 7 and fields[0] != "GEOID":
1567             zcta = fields[0]
1568             if zcta not in zctas: zctas[zcta] = {}
1569             zctas[zcta]["centroid"] = gecos(
1570                 ",".join( ( fields[6], fields[5] ) )
1571             )
1572             count += 1
1573     gzcta.close()
1574     print("done (%s lines)." % count)
1575     message = "Reading %s..." % overrides_fn
1576     sys.stdout.write(message)
1577     sys.stdout.flush()
1578     count = 0
1579     added = 0
1580     removed = 0
1581     changed = 0
1582     overrides = configparser.ConfigParser()
1583     overrides.readfp( codecs.open(overrides_fn, "r", "utf8") )
1584     overrideslog = []
1585     for section in overrides.sections():
1586         addopt = 0
1587         chgopt = 0
1588         if section.startswith("-"):
1589             section = section[1:]
1590             delete = True
1591         else: delete = False
1592         if re.match("[A-Za-z]{3}$", section):
1593             if delete:
1594                 if section in airports:
1595                     del( airports[section] )
1596                     logact = "removed airport %s" % section
1597                     removed += 1
1598                 else:
1599                     logact = "tried to remove nonexistent airport %s" % section
1600             else:
1601                 if section in airports:
1602                     logact = "changed airport %s" % section
1603                     changed += 1
1604                 else:
1605                     airports[section] = {}
1606                     logact = "added airport %s" % section
1607                     added += 1
1608                 for key,value in overrides.items(section):
1609                     if key in airports[section]: chgopt += 1
1610                     else: addopt += 1
1611                     if key in ("centroid", "location"):
1612                         airports[section][key] = eval(value)
1613                     else:
1614                         airports[section][key] = value
1615                 if addopt and chgopt:
1616                     logact += " (+%s/!%s options)" % (addopt, chgopt)
1617                 elif addopt: logact += " (+%s options)" % addopt
1618                 elif chgopt: logact += " (!%s options)" % chgopt
1619         elif re.match("[A-Za-z0-9]{4}$", section):
1620             if delete:
1621                 if section in stations:
1622                     del( stations[section] )
1623                     logact = "removed station %s" % section
1624                     removed += 1
1625                 else:
1626                     logact = "tried to remove nonexistent station %s" % section
1627             else:
1628                 if section in stations:
1629                     logact = "changed station %s" % section
1630                     changed += 1
1631                 else:
1632                     stations[section] = {}
1633                     logact = "added station %s" % section
1634                     added += 1
1635                 for key,value in overrides.items(section):
1636                     if key in stations[section]: chgopt += 1
1637                     else: addopt += 1
1638                     if key in ("centroid", "location"):
1639                         stations[section][key] = eval(value)
1640                     else:
1641                         stations[section][key] = value
1642                 if addopt and chgopt:
1643                     logact += " (+%s/!%s options)" % (addopt, chgopt)
1644                 elif addopt: logact += " (+%s options)" % addopt
1645                 elif chgopt: logact += " (!%s options)" % chgopt
1646         elif re.match("[0-9]{5}$", section):
1647             if delete:
1648                 if section in zctas:
1649                     del( zctas[section] )
1650                     logact = "removed zcta %s" % section
1651                     removed += 1
1652                 else:
1653                     logact = "tried to remove nonexistent zcta %s" % section
1654             else:
1655                 if section in zctas:
1656                     logact = "changed zcta %s" % section
1657                     changed += 1
1658                 else:
1659                     zctas[section] = {}
1660                     logact = "added zcta %s" % section
1661                     added += 1
1662                 for key,value in overrides.items(section):
1663                     if key in zctas[section]: chgopt += 1
1664                     else: addopt += 1
1665                     if key in ("centroid", "location"):
1666                         zctas[section][key] = eval(value)
1667                     else:
1668                         zctas[section][key] = value
1669                 if addopt and chgopt:
1670                     logact += " (+%s/!%s options)" % (addopt, chgopt)
1671                 elif addopt: logact += " (+%s options)" % addopt
1672                 elif chgopt: logact += " (!%s options)" % chgopt
1673         elif re.match("[A-Za-z]{2}[Zz][0-9]{3}$", section):
1674             if delete:
1675                 if section in zones:
1676                     del( zones[section] )
1677                     logact = "removed zone %s" % section
1678                     removed += 1
1679                 else:
1680                     logact = "tried to remove nonexistent zone %s" % section
1681             else:
1682                 if section in zones:
1683                     logact = "changed zone %s" % section
1684                     changed += 1
1685                 else:
1686                     zones[section] = {}
1687                     logact = "added zone %s" % section
1688                     added += 1
1689                 for key,value in overrides.items(section):
1690                     if key in zones[section]: chgopt += 1
1691                     else: addopt += 1
1692                     if key in ("centroid", "location"):
1693                         zones[section][key] = eval(value)
1694                     else:
1695                         zones[section][key] = value
1696                 if addopt and chgopt:
1697                     logact += " (+%s/!%s options)" % (addopt, chgopt)
1698                 elif addopt: logact += " (+%s options)" % addopt
1699                 elif chgopt: logact += " (!%s options)" % chgopt
1700         elif re.match("fips[0-9]+$", section):
1701             if delete:
1702                 if section in places:
1703                     del( places[section] )
1704                     logact = "removed place %s" % section
1705                     removed += 1
1706                 else:
1707                     logact = "tried to remove nonexistent place %s" % section
1708             else:
1709                 if section in places:
1710                     logact = "changed place %s" % section
1711                     changed += 1
1712                 else:
1713                     places[section] = {}
1714                     logact = "added place %s" % section
1715                     added += 1
1716                 for key,value in overrides.items(section):
1717                     if key in places[section]: chgopt += 1
1718                     else: addopt += 1
1719                     if key in ("centroid", "location"):
1720                         places[section][key] = eval(value)
1721                     else:
1722                         places[section][key] = value
1723                 if addopt and chgopt:
1724                     logact += " (+%s/!%s options)" % (addopt, chgopt)
1725                 elif addopt: logact += " (+%s options)" % addopt
1726                 elif chgopt: logact += " (!%s options)" % chgopt
1727         count += 1
1728         overrideslog.append("%s\n" % logact)
1729     overrideslog.sort()
1730     if os.path.exists(overrideslog_fn):
1731         os.rename(overrideslog_fn, "%s_old"%overrideslog_fn)
1732     overrideslog_fd = codecs.open(overrideslog_fn, "w", "utf8")
1733     overrideslog_fd.writelines(overrideslog)
1734     overrideslog_fd.close()
1735     print("done (%s overridden sections: +%s/-%s/!%s)." % (
1736         count,
1737         added,
1738         removed,
1739         changed
1740     ) )
1741     estimate = 2*len(places) + len(stations) + 2*len(zctas) + len(zones)
1742     print(
1743         "Correlating places, stations, ZCTAs and zones (upper bound is %s):" % \
1744             estimate
1745     )
1746     count = 0
1747     milestones = list( range(51) )
1748     message = "   "
1749     sys.stdout.write(message)
1750     sys.stdout.flush()
1751     for fips in places:
1752         centroid = places[fips]["centroid"]
1753         if centroid:
1754             station = closest(centroid, stations, "location", 0.1)
1755         if station[0]:
1756             places[fips]["station"] = station
1757             count += 1
1758             if not count%100:
1759                 level = int(50*count/estimate)
1760                 if level in milestones:
1761                     for remaining in milestones[:milestones.index(level)+1]:
1762                         if remaining%5:
1763                             message = "."
1764                             sys.stdout.write(message)
1765                             sys.stdout.flush()
1766                         else:
1767                             message = "%s%%" % (remaining*2,)
1768                             sys.stdout.write(message)
1769                             sys.stdout.flush()
1770                         milestones.remove(remaining)
1771         if centroid:
1772             zone = closest(centroid, zones, "centroid", 0.1)
1773         if zone[0]:
1774             places[fips]["zone"] = zone
1775             count += 1
1776             if not count%100:
1777                 level = int(50*count/estimate)
1778                 if level in milestones:
1779                     for remaining in milestones[:milestones.index(level)+1]:
1780                         if remaining%5:
1781                             message = "."
1782                             sys.stdout.write(message)
1783                             sys.stdout.flush()
1784                         else:
1785                             message = "%s%%" % (remaining*2,)
1786                             sys.stdout.write(message)
1787                             sys.stdout.flush()
1788                         milestones.remove(remaining)
1789     for station in stations:
1790         if "location" in stations[station]:
1791             location = stations[station]["location"]
1792             if location:
1793                 zone = closest(location, zones, "centroid", 0.1)
1794             if zone[0]:
1795                 stations[station]["zone"] = zone
1796                 count += 1
1797                 if not count%100:
1798                     level = int(50*count/estimate)
1799                     if level in milestones:
1800                         for remaining in milestones[:milestones.index(level)+1]:
1801                             if remaining%5:
1802                                 message = "."
1803                                 sys.stdout.write(message)
1804                                 sys.stdout.flush()
1805                             else:
1806                                 message = "%s%%" % (remaining*2,)
1807                                 sys.stdout.write(message)
1808                                 sys.stdout.flush()
1809                             milestones.remove(remaining)
1810     for zcta in zctas.keys():
1811         centroid = zctas[zcta]["centroid"]
1812         if centroid:
1813             station = closest(centroid, stations, "location", 0.1)
1814         if station[0]:
1815             zctas[zcta]["station"] = station
1816             count += 1
1817             if not count%100:
1818                 level = int(50*count/estimate)
1819                 if level in milestones:
1820                     for remaining in milestones[ : milestones.index(level)+1 ]:
1821                         if remaining%5:
1822                             message = "."
1823                             sys.stdout.write(message)
1824                             sys.stdout.flush()
1825                         else:
1826                             message = "%s%%" % (remaining*2,)
1827                             sys.stdout.write(message)
1828                             sys.stdout.flush()
1829                         milestones.remove(remaining)
1830         if centroid:
1831             zone = closest(centroid, zones, "centroid", 0.1)
1832         if zone[0]:
1833             zctas[zcta]["zone"] = zone
1834             count += 1
1835             if not count%100:
1836                 level = int(50*count/estimate)
1837                 if level in milestones:
1838                     for remaining in milestones[:milestones.index(level)+1]:
1839                         if remaining%5:
1840                             message = "."
1841                             sys.stdout.write(message)
1842                             sys.stdout.flush()
1843                         else:
1844                             message = "%s%%" % (remaining*2,)
1845                             sys.stdout.write(message)
1846                             sys.stdout.flush()
1847                         milestones.remove(remaining)
1848     for zone in zones.keys():
1849         if "centroid" in zones[zone]:
1850             centroid = zones[zone]["centroid"]
1851             if centroid:
1852                 station = closest(centroid, stations, "location", 0.1)
1853             if station[0]:
1854                 zones[zone]["station"] = station
1855                 count += 1
1856                 if not count%100:
1857                     level = int(50*count/estimate)
1858                     if level in milestones:
1859                         for remaining in milestones[:milestones.index(level)+1]:
1860                             if remaining%5:
1861                                 message = "."
1862                                 sys.stdout.write(message)
1863                                 sys.stdout.flush()
1864                             else:
1865                                 message = "%s%%" % (remaining*2,)
1866                                 sys.stdout.write(message)
1867                                 sys.stdout.flush()
1868                             milestones.remove(remaining)
1869     for remaining in milestones:
1870         if remaining%5:
1871             message = "."
1872             sys.stdout.write(message)
1873             sys.stdout.flush()
1874         else:
1875             message = "%s%%" % (remaining*2,)
1876             sys.stdout.write(message)
1877             sys.stdout.flush()
1878     print("\n   done (%s correlations)." % count)
1879     message = "Writing %s..." % airports_fn
1880     sys.stdout.write(message)
1881     sys.stdout.flush()
1882     count = 0
1883     if os.path.exists(airports_fn):
1884         os.rename(airports_fn, "%s_old"%airports_fn)
1885     airports_fd = codecs.open(airports_fn, "w", "utf8")
1886     airports_fd.write(header)
1887     for airport in sorted( airports.keys() ):
1888         airports_fd.write("\n\n[%s]" % airport)
1889         for key, value in sorted( airports[airport].items() ):
1890             airports_fd.write( "\n%s = %s" % (key, value) )
1891         count += 1
1892     airports_fd.close()
1893     print("done (%s sections)." % count)
1894     message = "Writing %s..." % places_fn
1895     sys.stdout.write(message)
1896     sys.stdout.flush()
1897     count = 0
1898     if os.path.exists(places_fn):
1899         os.rename(places_fn, "%s_old"%places_fn)
1900     places_fd = codecs.open(places_fn, "w", "utf8")
1901     places_fd.write(header)
1902     for fips in sorted( places.keys() ):
1903         places_fd.write("\n\n[%s]" % fips)
1904         for key, value in sorted( places[fips].items() ):
1905             places_fd.write( "\n%s = %s" % (key, value) )
1906         count += 1
1907     places_fd.close()
1908     print("done (%s sections)." % count)
1909     message = "Writing %s..." % stations_fn
1910     sys.stdout.write(message)
1911     sys.stdout.flush()
1912     count = 0
1913     if os.path.exists(stations_fn):
1914         os.rename(stations_fn, "%s_old"%stations_fn)
1915     stations_fd = codecs.open(stations_fn, "w", "utf8")
1916     stations_fd.write(header)
1917     for station in sorted( stations.keys() ):
1918         stations_fd.write("\n\n[%s]" % station)
1919         for key, value in sorted( stations[station].items() ):
1920             stations_fd.write( "\n%s = %s" % (key, value) )
1921         count += 1
1922     stations_fd.close()
1923     print("done (%s sections)." % count)
1924     message = "Writing %s..." % zctas_fn
1925     sys.stdout.write(message)
1926     sys.stdout.flush()
1927     count = 0
1928     if os.path.exists(zctas_fn):
1929         os.rename(zctas_fn, "%s_old"%zctas_fn)
1930     zctas_fd = codecs.open(zctas_fn, "w", "utf8")
1931     zctas_fd.write(header)
1932     for zcta in sorted( zctas.keys() ):
1933         zctas_fd.write("\n\n[%s]" % zcta)
1934         for key, value in sorted( zctas[zcta].items() ):
1935             zctas_fd.write( "\n%s = %s" % (key, value) )
1936         count += 1
1937     zctas_fd.close()
1938     print("done (%s sections)." % count)
1939     message = "Writing %s..." % zones_fn
1940     sys.stdout.write(message)
1941     sys.stdout.flush()
1942     count = 0
1943     if os.path.exists(zones_fn):
1944         os.rename(zones_fn, "%s_old"%zones_fn)
1945     zones_fd = codecs.open(zones_fn, "w", "utf8")
1946     zones_fd.write(header)
1947     for zone in sorted( zones.keys() ):
1948         zones_fd.write("\n\n[%s]" % zone)
1949         for key, value in sorted( zones[zone].items() ):
1950             zones_fd.write( "\n%s = %s" % (key, value) )
1951         count += 1
1952     zones_fd.close()
1953     print("done (%s sections)." % count)
1954     message = "Starting QA check..."
1955     sys.stdout.write(message)
1956     sys.stdout.flush()
1957     airports = configparser.ConfigParser()
1958     airports.read(airports_fn)
1959     places = configparser.ConfigParser()
1960     places.read(places_fn)
1961     stations = configparser.ConfigParser()
1962     stations.read(stations_fn)
1963     zctas = configparser.ConfigParser()
1964     zctas.read(zctas_fn)
1965     zones = configparser.ConfigParser()
1966     zones.read(zones_fn)
1967     qalog = []
1968     places_nocentroid = 0
1969     places_nodescription = 0
1970     for place in sorted( places.sections() ):
1971         if not places.has_option(place, "centroid"):
1972             qalog.append("%s: no centroid\n" % place)
1973             places_nocentroid += 1
1974         if not places.has_option(place, "description"):
1975             qalog.append("%s: no description\n" % place)
1976             places_nodescription += 1
1977     stations_nodescription = 0
1978     stations_nolocation = 0
1979     stations_nometar = 0
1980     for station in sorted( stations.sections() ):
1981         if not stations.has_option(station, "description"):
1982             qalog.append("%s: no description\n" % station)
1983             stations_nodescription += 1
1984         if not stations.has_option(station, "location"):
1985             qalog.append("%s: no location\n" % station)
1986             stations_nolocation += 1
1987         if not stations.has_option(station, "metar"):
1988             qalog.append("%s: no metar\n" % station)
1989             stations_nometar += 1
1990     airports_badstation = 0
1991     airports_nostation = 0
1992     for airport in sorted( airports.sections() ):
1993         if not airports.has_option(airport, "station"):
1994             qalog.append("%s: no station\n" % airport)
1995             airports_nostation += 1
1996         else:
1997             station = airports.get(airport, "station")
1998             if station not in stations.sections():
1999                 qalog.append( "%s: bad station %s\n" % (airport, station) )
2000                 airports_badstation += 1
2001     zctas_nocentroid = 0
2002     for zcta in sorted( zctas.sections() ):
2003         if not zctas.has_option(zcta, "centroid"):
2004             qalog.append("%s: no centroid\n" % zcta)
2005             zctas_nocentroid += 1
2006     zones_nocentroid = 0
2007     zones_nodescription = 0
2008     zones_noforecast = 0
2009     zones_overlapping = 0
2010     zonetable = {}
2011     for zone in zones.sections():
2012         if zones.has_option(zone, "centroid"):
2013             zonetable[zone] = {
2014                 "centroid": eval( zones.get(zone, "centroid") )
2015             }
2016     for zone in sorted( zones.sections() ):
2017         if zones.has_option(zone, "centroid"):
2018             zonetable_local = zonetable.copy()
2019             del( zonetable_local[zone] )
2020             centroid = eval( zones.get(zone, "centroid") )
2021             if centroid:
2022                 nearest = closest(centroid, zonetable_local, "centroid", 0.1)
2023             if nearest[1]*radian_to_km < 1:
2024                 qalog.append( "%s: within one km of %s\n" % (
2025                     zone,
2026                     nearest[0]
2027                 ) )
2028                 zones_overlapping += 1
2029         else:
2030             qalog.append("%s: no centroid\n" % zone)
2031             zones_nocentroid += 1
2032         if not zones.has_option(zone, "description"):
2033             qalog.append("%s: no description\n" % zone)
2034             zones_nodescription += 1
2035         if not zones.has_option(zone, "zone_forecast"):
2036             qalog.append("%s: no forecast\n" % zone)
2037             zones_noforecast += 1
2038     if os.path.exists(qalog_fn):
2039         os.rename(qalog_fn, "%s_old"%qalog_fn)
2040     qalog_fd = codecs.open(qalog_fn, "w", "utf8")
2041     qalog_fd.writelines(qalog)
2042     qalog_fd.close()
2043     if qalog:
2044         print("issues found (see %s for details):"%qalog_fn)
2045         if airports_badstation:
2046             print("   %s airports with invalid station"%airports_badstation)
2047         if airports_nostation:
2048             print("   %s airports with no station"%airports_nostation)
2049         if places_nocentroid:
2050             print("   %s places with no centroid"%places_nocentroid)
2051         if places_nodescription:
2052             print("   %s places with no description"%places_nodescription)
2053         if stations_nodescription:
2054             print("   %s stations with no description"%stations_nodescription)
2055         if stations_nolocation:
2056             print("   %s stations with no location"%stations_nolocation)
2057         if stations_nometar:
2058             print("   %s stations with no METAR"%stations_nometar)
2059         if zctas_nocentroid:
2060             print("   %s ZCTAs with no centroid"%zctas_nocentroid)
2061         if zones_nocentroid:
2062             print("   %s zones with no centroid"%zones_nocentroid)
2063         if zones_nodescription:
2064             print("   %s zones with no description"%zones_nodescription)
2065         if zones_noforecast:
2066             print("   %s zones with no forecast"%zones_noforecast)
2067         if zones_overlapping:
2068             print("   %s zones within one km of another"%zones_overlapping)
2069     else: print("no issues found.")
2070     print("Indexing complete!")