| 1 | # |
|---|
| 2 | # vim: set sw=4 sts=4 et tw=80 fileencoding=utf-8: |
|---|
| 3 | # |
|---|
| 4 | """baken - Imports baken data files""" |
|---|
| 5 | # Copyright (C) 2007-2008 James Rowe |
|---|
| 6 | # |
|---|
| 7 | # This program is free software: you can redistribute it and/or modify |
|---|
| 8 | # it under the terms of the GNU General Public License as published by |
|---|
| 9 | # the Free Software Foundation, either version 3 of the License, or |
|---|
| 10 | # (at your option) any later version. |
|---|
| 11 | # |
|---|
| 12 | # This program is distributed in the hope that it will be useful, |
|---|
| 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|---|
| 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|---|
| 15 | # GNU General Public License for more details. |
|---|
| 16 | # |
|---|
| 17 | # You should have received a copy of the GNU General Public License |
|---|
| 18 | # along with this program. If not, see <http://www.gnu.org/licenses/>. |
|---|
| 19 | # |
|---|
| 20 | |
|---|
| 21 | import ConfigParser |
|---|
| 22 | import logging |
|---|
| 23 | import re |
|---|
| 24 | |
|---|
| 25 | from upoints import (point, utils) |
|---|
| 26 | |
|---|
| 27 | class Baken(point.Point): |
|---|
| 28 | """Class for representing location from baken data files |
|---|
| 29 | |
|---|
| 30 | :since: 0.4.0 |
|---|
| 31 | |
|---|
| 32 | :Ivariables: |
|---|
| 33 | latitude |
|---|
| 34 | Location's latitude |
|---|
| 35 | longitude |
|---|
| 36 | Locations's longitude |
|---|
| 37 | antenna |
|---|
| 38 | Location's antenna type |
|---|
| 39 | direction |
|---|
| 40 | Antenna's direction |
|---|
| 41 | frequency |
|---|
| 42 | Transmitter's frequency |
|---|
| 43 | height |
|---|
| 44 | Antenna's height |
|---|
| 45 | locator |
|---|
| 46 | Location's locator string |
|---|
| 47 | mode |
|---|
| 48 | Transmitter's mode |
|---|
| 49 | operator |
|---|
| 50 | Transmitter's operator |
|---|
| 51 | power |
|---|
| 52 | Transmitter's power |
|---|
| 53 | qth |
|---|
| 54 | Location's qth |
|---|
| 55 | |
|---|
| 56 | """ |
|---|
| 57 | |
|---|
| 58 | __slots__ = ('antenna', 'direction', 'frequency', 'height', '_locator', |
|---|
| 59 | 'mode', 'operator', 'power', 'qth') |
|---|
| 60 | |
|---|
| 61 | def __init__(self, latitude, longitude, antenna=None, direction=None, |
|---|
| 62 | frequency=None, height=None, locator=None, mode=None, |
|---|
| 63 | operator=None, power=None, qth=None): |
|---|
| 64 | """Initialise a new `Baken` object |
|---|
| 65 | |
|---|
| 66 | >>> Baken(14.460, 20.680, None, None, None, 0.000, None, None, None, |
|---|
| 67 | ... None, None) |
|---|
| 68 | Baken(14.46, 20.68, None, None, None, 0.0, None, None, None, None, None) |
|---|
| 69 | >>> Baken(None, None, "2 x Turnstile", None, 50.000, 460.000, "IO93BF", |
|---|
| 70 | ... "A1A", None, 25, None) |
|---|
| 71 | Baken(53.2291666667, -1.875, '2 x Turnstile', None, 50.0, 460.0, |
|---|
| 72 | 'IO93BF', 'A1A', None, 25, None) |
|---|
| 73 | >>> obj = Baken(None, None) |
|---|
| 74 | Traceback (most recent call last): |
|---|
| 75 | ... |
|---|
| 76 | LookupError: Unable to instantiate baken object, no latitude or |
|---|
| 77 | locator string |
|---|
| 78 | |
|---|
| 79 | :Parameters: |
|---|
| 80 | latitude : `float` or coercible to `float` |
|---|
| 81 | Location's latitude |
|---|
| 82 | longitude : `float` or coercible to `float` |
|---|
| 83 | Location's longitude |
|---|
| 84 | antenna : `str` |
|---|
| 85 | Location's antenna type |
|---|
| 86 | direction : `tuple` of `int` |
|---|
| 87 | Antenna's direction |
|---|
| 88 | frequency : `float` |
|---|
| 89 | Transmitter's frequency |
|---|
| 90 | height : `float` |
|---|
| 91 | Antenna's height |
|---|
| 92 | locator : `str` |
|---|
| 93 | Location's Maidenhead locator string |
|---|
| 94 | mode : `str` |
|---|
| 95 | Transmitter's mode |
|---|
| 96 | operator : `tuple` of `str` |
|---|
| 97 | Transmitter's operator |
|---|
| 98 | power : `float` |
|---|
| 99 | Transmitter's power |
|---|
| 100 | qth : `str` |
|---|
| 101 | Location's qth |
|---|
| 102 | :raise LookupError: No position data to use |
|---|
| 103 | |
|---|
| 104 | """ |
|---|
| 105 | if not latitude is None: |
|---|
| 106 | super(Baken, self).__init__(latitude, longitude) |
|---|
| 107 | elif not locator is None: |
|---|
| 108 | latitude, longitude = utils.from_grid_locator(locator) |
|---|
| 109 | super(Baken, self).__init__(latitude, longitude) |
|---|
| 110 | else: |
|---|
| 111 | raise LookupError("Unable to instantiate baken object, no " |
|---|
| 112 | "latitude or locator string") |
|---|
| 113 | |
|---|
| 114 | self.antenna = antenna |
|---|
| 115 | self.direction = direction |
|---|
| 116 | self.frequency = frequency |
|---|
| 117 | self.height = height |
|---|
| 118 | self._locator = locator |
|---|
| 119 | self.mode = mode |
|---|
| 120 | self.operator = operator |
|---|
| 121 | self.power = power |
|---|
| 122 | self.qth = qth |
|---|
| 123 | |
|---|
| 124 | def _set_locator(self, value): |
|---|
| 125 | """Update the locator, and trigger a latitude and longitude update |
|---|
| 126 | |
|---|
| 127 | >>> test = Baken(None, None, "2 x Turnstile", None, 50.000, 460.000, |
|---|
| 128 | ... "IO93BF", "A1A", None, 25, None) |
|---|
| 129 | >>> test.locator = "JN44FH" |
|---|
| 130 | >>> test |
|---|
| 131 | Baken(44.3125, 8.45833333333, '2 x Turnstile', None, 50.0, 460.0, |
|---|
| 132 | 'JN44FH', 'A1A', None, 25, None) |
|---|
| 133 | |
|---|
| 134 | :Parameters: |
|---|
| 135 | value : `str` |
|---|
| 136 | New Maidenhead locator string |
|---|
| 137 | |
|---|
| 138 | """ |
|---|
| 139 | self._locator = value |
|---|
| 140 | self._latitude, self._longitude = utils.from_grid_locator(value) |
|---|
| 141 | locator = property(lambda self: self._locator, |
|---|
| 142 | lambda self, value: self._set_locator(value)) |
|---|
| 143 | |
|---|
| 144 | def __str__(self, mode="dms"): |
|---|
| 145 | """Pretty printed location string |
|---|
| 146 | |
|---|
| 147 | >>> print(Baken(14.460, 20.680, None, None, None, 0.000, None, None, |
|---|
| 148 | ... None, None, None)) |
|---|
| 149 | 14°27'36"N, 020°40'48"E |
|---|
| 150 | >>> print(Baken(None, None, "2 x Turnstile", None, 50.000, 460.000, |
|---|
| 151 | ... "IO93BF", "A1A", None, 25, None)) |
|---|
| 152 | IO93BF (53°13'45"N, 001°52'30"W) |
|---|
| 153 | |
|---|
| 154 | :Parameters: |
|---|
| 155 | mode : `str` |
|---|
| 156 | Coordinate formatting system to use |
|---|
| 157 | :rtype: `str` |
|---|
| 158 | :return: Human readable string representation of `Baken` object |
|---|
| 159 | |
|---|
| 160 | """ |
|---|
| 161 | text = super(Baken, self).__str__(mode) |
|---|
| 162 | if self._locator: |
|---|
| 163 | text = "%s (%s)" % (self._locator, text) |
|---|
| 164 | return text |
|---|
| 165 | |
|---|
| 166 | |
|---|
| 167 | class Bakens(point.KeyedPoints): |
|---|
| 168 | """Class for representing a group of `Baken` objects |
|---|
| 169 | |
|---|
| 170 | :since: 0.5.1 |
|---|
| 171 | |
|---|
| 172 | """ |
|---|
| 173 | |
|---|
| 174 | def __init__(self, baken_file=None): |
|---|
| 175 | """Initialise a new `Bakens` object""" |
|---|
| 176 | super(Bakens, self).__init__() |
|---|
| 177 | if baken_file: |
|---|
| 178 | self.import_locations(baken_file) |
|---|
| 179 | |
|---|
| 180 | def import_locations(self, baken_file): |
|---|
| 181 | """Import baken data files |
|---|
| 182 | |
|---|
| 183 | `import_locations()` returns a dictionary with keys containing the |
|---|
| 184 | section title, and values consisting of a collection `Baken` objects. |
|---|
| 185 | |
|---|
| 186 | It expects data files in the format used by the baken amateur radio |
|---|
| 187 | package, which is Windows INI style files such as:: |
|---|
| 188 | |
|---|
| 189 | [Abeche, Chad] |
|---|
| 190 | latitude=14.460000 |
|---|
| 191 | longitude=20.680000 |
|---|
| 192 | height=0.000000 |
|---|
| 193 | |
|---|
| 194 | [GB3BUX] |
|---|
| 195 | frequency=50.000 |
|---|
| 196 | locator=IO93BF |
|---|
| 197 | power=25 TX |
|---|
| 198 | antenna=2 x Turnstile |
|---|
| 199 | height=460 |
|---|
| 200 | mode=A1A |
|---|
| 201 | |
|---|
| 202 | The reader uses `Python <http://www.python.org/>`__'s `ConfigParser` |
|---|
| 203 | module, so should be reasonably robust against encodings and such. The |
|---|
| 204 | above file processed by `import_locations()` will return the following |
|---|
| 205 | `dict` object:: |
|---|
| 206 | |
|---|
| 207 | {"Abeche, Chad": Baken(14.460, 20.680, None, None, None, 0.000, |
|---|
| 208 | None, None, None, None, None), |
|---|
| 209 | "GB3BUX": : Baken(None, None, "2 x Turnstile", None, 50.000, |
|---|
| 210 | 460.000, "IO93BF", "A1A", None, 25, None)} |
|---|
| 211 | |
|---|
| 212 | >>> locations = Bakens(open("baken_data")) |
|---|
| 213 | >>> for key, value in sorted(locations.items()): |
|---|
| 214 | ... print("%s - %s" % (key, value)) |
|---|
| 215 | Abeche, Chad - 14°27'36"N, 020°40'48"E |
|---|
| 216 | GB3BUX - IO93BF (53°13'45"N, 001°52'30"W) |
|---|
| 217 | IW1RCT - JN44FH (44°18'45"N, 008°27'29"E) |
|---|
| 218 | >>> locations = Bakens(open("no_valid_baken")) |
|---|
| 219 | >>> len(locations) |
|---|
| 220 | 0 |
|---|
| 221 | |
|---|
| 222 | :Parameters: |
|---|
| 223 | baken_file : `file`, `list` or `str` |
|---|
| 224 | Baken data to read |
|---|
| 225 | :rtype: `dict` |
|---|
| 226 | :return: Named locations and their associated values |
|---|
| 227 | |
|---|
| 228 | """ |
|---|
| 229 | data = ConfigParser.ConfigParser() |
|---|
| 230 | if hasattr(baken_file, "readlines"): |
|---|
| 231 | data.readfp(baken_file) |
|---|
| 232 | elif isinstance(baken_file, list): |
|---|
| 233 | data.read(baken_file) |
|---|
| 234 | elif isinstance(baken_file, basestring): |
|---|
| 235 | data.readfp(open(baken_file)) |
|---|
| 236 | else: |
|---|
| 237 | raise TypeError("Unable to handle data of type `%s`" |
|---|
| 238 | % type(baken_file)) |
|---|
| 239 | valid_locator = re.compile("[A-Z]{2}[0-9]{2}[A-Z]{2}") |
|---|
| 240 | for name in data.sections(): |
|---|
| 241 | elements = {} |
|---|
| 242 | for item in ("latitude", "longitude", "antenna", "direction", |
|---|
| 243 | "frequency", "height", "locator", "mode", "operator", |
|---|
| 244 | "power", "qth"): |
|---|
| 245 | if data.has_option(name, item): |
|---|
| 246 | if item in ("antenna", "locator", "mode", "power", "qth"): |
|---|
| 247 | elements[item] = data.get(name, item) |
|---|
| 248 | elif item == "operator": |
|---|
| 249 | elements[item] = elements[item].split(",") |
|---|
| 250 | elif item == "direction": |
|---|
| 251 | elements[item] = data.get(name, item).split(",") |
|---|
| 252 | else: |
|---|
| 253 | try: |
|---|
| 254 | elements[item] = data.getfloat(name, item) |
|---|
| 255 | except ValueError: |
|---|
| 256 | logging.debug("Multiple frequency workaround for " |
|---|
| 257 | "`%s' entry" % name) |
|---|
| 258 | elements[item] = map(float, |
|---|
| 259 | data.get(name, item).split(",")) |
|---|
| 260 | else: |
|---|
| 261 | elements[item] = None |
|---|
| 262 | if elements["latitude"] is None \ |
|---|
| 263 | and not valid_locator.match(elements["locator"]): |
|---|
| 264 | logging.info("Skipping `%s' entry, as it contains no location " |
|---|
| 265 | "data" % name) |
|---|
| 266 | continue |
|---|
| 267 | |
|---|
| 268 | self[name] = Baken(**elements) |
|---|
| 269 | |
|---|