avwx.station.station

Station handling and coordinate search

  1"""
  2Station handling and coordinate search
  3"""
  4
  5# pylint: disable=invalid-name,too-many-arguments,too-many-instance-attributes
  6
  7# stdlib
  8from contextlib import suppress
  9from copy import copy
 10from dataclasses import dataclass
 11from functools import lru_cache
 12from typing import Any, Dict, List, Optional, Tuple, Type, TypeVar, Union
 13
 14# library
 15import httpx
 16from geopy.distance import great_circle, Distance  # type: ignore
 17
 18# module
 19from avwx.exceptions import BadStation
 20from avwx.load_utils import LazyCalc
 21from avwx.station.meta import STATIONS
 22from avwx.structs import Coord
 23
 24
 25def _get_ip_location() -> Coord:
 26    """Returns the current location according to ipinfo.io"""
 27    lat, lon = httpx.get("https://ipinfo.io/loc").text.strip().split(",")
 28    return Coord(float(lat), float(lon))
 29
 30
 31@dataclass
 32class Runway:
 33    """Represents a runway at an airport"""
 34
 35    length_ft: int
 36    width_ft: int
 37    surface: str
 38    lights: bool
 39    ident1: str
 40    ident2: str
 41    bearing1: float
 42    bearing2: float
 43
 44
 45T = TypeVar("T", bound="Station")
 46_ICAO = LazyCalc(lambda: {v["icao"]: k for k, v in STATIONS.items() if v["icao"]})
 47_IATA = LazyCalc(lambda: {v["iata"]: k for k, v in STATIONS.items() if v["iata"]})
 48_GPS = LazyCalc(lambda: {v["gps"]: k for k, v in STATIONS.items() if v["gps"]})
 49_LOCAL = LazyCalc(lambda: {v["local"]: k for k, v in STATIONS.items() if v["local"]})
 50
 51
 52@dataclass
 53class Station:
 54    """
 55    The Station dataclass stores basic info about the desired station and
 56    available Runways.
 57
 58    The easiest way to get a station is to supply the ICAO, IATA, or GPS code.
 59    The example below uses `from_code` which checks against all three types,
 60    but you can also use `from_icao`, `from_iata`, or `from_gps` if you know
 61    what type of code you are using. This can be important if you may be using
 62    a code used by more than one station depending on the context. ICAO and
 63    IATA codes are guarenteed unique, but not all airports have them. That
 64    said, all stations available in AVWX have either an ICAO or GPS code.
 65
 66    ```python
 67    >>> from avwx import Station
 68    >>> klex = Station.from_code("KLEX")
 69    >>> f"{klex.name} in {klex.city}, {klex.state}"
 70    'Blue Grass Airport in Lexington, KY'
 71    >>> coord = round(klex.latitude, 3), round(klex.longitude, 3)
 72    >>> f"Located at {coord} at {klex.elevation_ft} feet ({klex.elevation_m} meters)"
 73    'Located at (38.036, -84.606) at 979 feet (298 meters)'
 74    >>> rw = max(klex.runways, key=lambda r: r.length_ft)
 75    >>> f"Its longest runway is {rw.ident1}/{rw.ident2} at {rw.length_ft} feet"
 76    'Its longest runway is 04/22 at 7003 feet'
 77    ```
 78
 79    This is also the same information you'd get from calling Report.station.
 80
 81    ```python
 82    >>> from avwx import Metar
 83    >>> klex = Metar('KLEX')
 84    >>> klex.station.name
 85    'Blue Grass Airport'
 86    ```
 87    """
 88
 89    # pylint: disable=too-many-instance-attributes
 90
 91    city: Optional[str]
 92    country: str
 93    elevation_ft: Optional[int]
 94    elevation_m: Optional[int]
 95    gps: Optional[str]
 96    iata: Optional[str]
 97    icao: Optional[str]
 98    latitude: float
 99    local: Optional[str]
100    longitude: float
101    name: str
102    note: Optional[str]
103    reporting: bool
104    runways: List[Runway]
105    state: Optional[str]
106    type: str
107    website: Optional[str]
108    wiki: Optional[str]
109
110    @classmethod
111    def _from_code(cls: Type[T], ident: str) -> T:
112        try:
113            info: Dict[str, Any] = copy(STATIONS[ident])
114            if info["runways"]:
115                info["runways"] = [Runway(**r) for r in info["runways"]]
116            return cls(**info)
117        except (KeyError, AttributeError) as not_found:
118            raise BadStation(
119                f"Could not find station with ident {ident}"
120            ) from not_found
121
122    @classmethod
123    def from_code(cls: Type[T], ident: str) -> T:
124        """Load a Station from ICAO, GPS, or IATA code in that order"""
125        if ident and isinstance(ident, str):
126            if len(ident) == 4:
127                with suppress(BadStation):
128                    return cls.from_icao(ident)
129                with suppress(BadStation):
130                    return cls.from_gps(ident)
131            if len(ident) == 3:
132                with suppress(BadStation):
133                    return cls.from_iata(ident)
134            with suppress(BadStation):
135                return cls.from_local(ident)
136        raise BadStation(f"Could not find station with ident {ident}")
137
138    @classmethod
139    def from_icao(cls: Type[T], ident: str) -> T:
140        """Load a Station from an ICAO station ident"""
141        try:
142            return cls._from_code(_ICAO.value[ident.upper()])
143        except (KeyError, AttributeError) as not_found:
144            raise BadStation(
145                f"Could not find station with ICAO ident {ident}"
146            ) from not_found
147
148    @classmethod
149    def from_iata(cls: Type[T], ident: str) -> T:
150        """Load a Station from an IATA code"""
151        try:
152            return cls._from_code(_IATA.value[ident.upper()])
153        except (KeyError, AttributeError) as not_found:
154            raise BadStation(
155                f"Could not find station with IATA ident {ident}"
156            ) from not_found
157
158    @classmethod
159    def from_gps(cls: Type[T], ident: str) -> T:
160        """Load a Station from a GPS code"""
161        try:
162            return cls._from_code(_GPS.value[ident.upper()])
163        except (KeyError, AttributeError) as not_found:
164            raise BadStation(
165                f"Could not find station with GPS ident {ident}"
166            ) from not_found
167
168    @classmethod
169    def from_local(cls: Type[T], ident: str) -> T:
170        """Load a Station from a local code"""
171        try:
172            return cls._from_code(_LOCAL.value[ident.upper()])
173        except (KeyError, AttributeError) as not_found:
174            raise BadStation(
175                f"Could not find station with local ident {ident}"
176            ) from not_found
177
178    @classmethod
179    def nearest(
180        cls: Type[T],
181        lat: Optional[float] = None,
182        lon: Optional[float] = None,
183        is_airport: bool = False,
184        sends_reports: bool = True,
185        max_coord_distance: float = 10,
186    ) -> Optional[Tuple[T, dict]]:
187        """Load the Station nearest to your location or a lat,lon coordinate pair
188
189        Returns the Station and distances from source
190
191        NOTE: Becomes less accurate toward poles and doesn't cross +/-180
192        """
193        if not (lat and lon):
194            lat, lon = _get_ip_location().pair
195        ret = nearest(lat, lon, 1, is_airport, sends_reports, max_coord_distance)
196        if not isinstance(ret, dict):
197            return None
198        station = ret.pop("station")
199        return station, ret
200
201    @property
202    def lookup_code(self) -> str:
203        """Returns the ICAO or GPS code for report fetch"""
204        if self.icao:
205            return self.icao
206        if self.gps:
207            return self.gps
208        raise BadStation("Station does not have a valid lookup code")
209
210    @property
211    def storage_code(self) -> str:
212        """Returns the first unique-ish code from what's available"""
213        if self.icao:
214            return self.icao
215        if self.iata:
216            return self.iata
217        if self.gps:
218            return self.gps
219        if self.local:
220            return self.local
221        raise BadStation("Station does not have any useable codes")
222
223    @property
224    def sends_reports(self) -> bool:
225        """Returns whether or not a Station likely sends weather reports"""
226        return self.reporting is True
227
228    @property
229    def coord(self) -> Coord:
230        """Returns the station location as a Coord"""
231        return Coord(lat=self.latitude, lon=self.longitude, repr=self.icao)
232
233    def distance(self, lat: float, lon: float) -> Distance:
234        """Returns a geopy Distance using the great circle method"""
235        return great_circle((lat, lon), (self.latitude, self.longitude))
236
237    def nearby(
238        self,
239        is_airport: bool = False,
240        sends_reports: bool = True,
241        max_coord_distance: float = 10,
242    ) -> List[Tuple[T, dict]]:
243        """Returns Stations nearest to current station and their distances
244
245        NOTE: Becomes less accurate toward poles and doesn't cross +/-180
246        """
247        stations = nearest(
248            self.latitude,
249            self.longitude,
250            11,
251            is_airport,
252            sends_reports,
253            max_coord_distance,
254        )
255        if isinstance(stations, dict):
256            return []
257        return [(s.pop("station"), s) for s in stations[1:]]
258
259
260# Coordinate search and resources
261
262
263def _make_coords() -> List[Tuple]:
264    return [
265        (
266            s["icao"] or s["gps"] or s["iata"] or s["local"],
267            s["latitude"],
268            s["longitude"],
269        )
270        for s in STATIONS.values()
271    ]
272
273
274_COORDS = LazyCalc(_make_coords)
275
276
277def _make_coord_tree():  # type: ignore
278    # pylint: disable=import-outside-toplevel
279    try:
280        from scipy.spatial import KDTree  # type: ignore
281
282        return KDTree([c[1:] for c in _COORDS.value])
283    except (NameError, ModuleNotFoundError) as name_error:
284        raise ModuleNotFoundError(
285            'scipy must be installed to use coordinate lookup. Run "pip install avwx-engine[scipy]" to enable this feature'
286        ) from name_error
287
288
289_COORD_TREE = LazyCalc(_make_coord_tree)
290
291
292def _query_coords(lat: float, lon: float, n: int, d: float) -> List[Tuple[str, float]]:
293    """Returns <= n number of ident, dist tuples <= d coord distance from lat,lon"""
294    dist, index = _COORD_TREE.value.query([lat, lon], n, distance_upper_bound=d)
295    if n == 1:
296        dist, index = [dist], [index]
297    # NOTE: index == len of list means Tree ran out of items
298    return [
299        (_COORDS.value[i][0], d) for i, d in zip(index, dist) if i < len(_COORDS.value)
300    ]
301
302
303def station_filter(station: Station, is_airport: bool, reporting: bool) -> bool:
304    """Return True if station matches given criteria"""
305    if is_airport and "airport" not in station.type:
306        return False
307    return bool(not reporting or station.sends_reports)
308
309
310@lru_cache(maxsize=128)
311def _query_filter(
312    lat: float, lon: float, n: int, d: float, is_airport: bool, reporting: bool
313) -> List[Tuple[Station, float]]:
314    """Returns <= n number of stations <= d distance from lat,lon matching the query params"""
315    k = n * 20
316    last = 0
317    stations: List[Tuple[Station, float]] = []
318    while True:
319        nodes = _query_coords(lat, lon, k, d)[last:]
320        # Ran out of new stations
321        if not nodes:
322            return stations
323        for code, dist in nodes:
324            if not code:
325                continue
326            stn = Station.from_code(code)
327            if station_filter(stn, is_airport, reporting):
328                stations.append((stn, dist))
329            # Reached the desired number of stations
330            if len(stations) >= n:
331                return stations
332        last = k
333        k += n * 100
334
335
336def nearest(
337    lat: float,
338    lon: float,
339    n: int = 1,
340    is_airport: bool = False,
341    sends_reports: bool = True,
342    max_coord_distance: float = 10,
343) -> Union[dict, List[dict]]:
344    """Finds the nearest n Stations to a lat,lon coordinate pair
345
346    Returns the Station and coordinate distance from source
347
348    NOTE: Becomes less accurate toward poles and doesn't cross +/-180
349    """
350    # Default state includes all, no filtering necessary
351    if is_airport or sends_reports:
352        stations = _query_filter(
353            lat, lon, n, max_coord_distance, is_airport, sends_reports
354        )
355    else:
356        data = _query_coords(lat, lon, n, max_coord_distance)
357        stations = [(Station.from_code(code), d) for code, d in data]
358    if not stations:
359        return []
360    ret = []
361    for station, coord_dist in stations:
362        dist = station.distance(lat, lon)
363        ret.append(
364            {
365                "station": station,
366                "coordinate_distance": coord_dist,
367                "nautical_miles": dist.nautical,
368                "miles": dist.miles,
369                "kilometers": dist.kilometers,
370            }
371        )
372    if n == 1:
373        return ret[0]
374    ret.sort(key=lambda x: x["miles"])
375    return ret
@dataclass
class Runway:
32@dataclass
33class Runway:
34    """Represents a runway at an airport"""
35
36    length_ft: int
37    width_ft: int
38    surface: str
39    lights: bool
40    ident1: str
41    ident2: str
42    bearing1: float
43    bearing2: float

Represents a runway at an airport

Runway( length_ft: int, width_ft: int, surface: str, lights: bool, ident1: str, ident2: str, bearing1: float, bearing2: float)
length_ft: int
width_ft: int
surface: str
lights: bool
ident1: str
ident2: str
bearing1: float
bearing2: float
@dataclass
class Station:
 53@dataclass
 54class Station:
 55    """
 56    The Station dataclass stores basic info about the desired station and
 57    available Runways.
 58
 59    The easiest way to get a station is to supply the ICAO, IATA, or GPS code.
 60    The example below uses `from_code` which checks against all three types,
 61    but you can also use `from_icao`, `from_iata`, or `from_gps` if you know
 62    what type of code you are using. This can be important if you may be using
 63    a code used by more than one station depending on the context. ICAO and
 64    IATA codes are guarenteed unique, but not all airports have them. That
 65    said, all stations available in AVWX have either an ICAO or GPS code.
 66
 67    ```python
 68    >>> from avwx import Station
 69    >>> klex = Station.from_code("KLEX")
 70    >>> f"{klex.name} in {klex.city}, {klex.state}"
 71    'Blue Grass Airport in Lexington, KY'
 72    >>> coord = round(klex.latitude, 3), round(klex.longitude, 3)
 73    >>> f"Located at {coord} at {klex.elevation_ft} feet ({klex.elevation_m} meters)"
 74    'Located at (38.036, -84.606) at 979 feet (298 meters)'
 75    >>> rw = max(klex.runways, key=lambda r: r.length_ft)
 76    >>> f"Its longest runway is {rw.ident1}/{rw.ident2} at {rw.length_ft} feet"
 77    'Its longest runway is 04/22 at 7003 feet'
 78    ```
 79
 80    This is also the same information you'd get from calling Report.station.
 81
 82    ```python
 83    >>> from avwx import Metar
 84    >>> klex = Metar('KLEX')
 85    >>> klex.station.name
 86    'Blue Grass Airport'
 87    ```
 88    """
 89
 90    # pylint: disable=too-many-instance-attributes
 91
 92    city: Optional[str]
 93    country: str
 94    elevation_ft: Optional[int]
 95    elevation_m: Optional[int]
 96    gps: Optional[str]
 97    iata: Optional[str]
 98    icao: Optional[str]
 99    latitude: float
100    local: Optional[str]
101    longitude: float
102    name: str
103    note: Optional[str]
104    reporting: bool
105    runways: List[Runway]
106    state: Optional[str]
107    type: str
108    website: Optional[str]
109    wiki: Optional[str]
110
111    @classmethod
112    def _from_code(cls: Type[T], ident: str) -> T:
113        try:
114            info: Dict[str, Any] = copy(STATIONS[ident])
115            if info["runways"]:
116                info["runways"] = [Runway(**r) for r in info["runways"]]
117            return cls(**info)
118        except (KeyError, AttributeError) as not_found:
119            raise BadStation(
120                f"Could not find station with ident {ident}"
121            ) from not_found
122
123    @classmethod
124    def from_code(cls: Type[T], ident: str) -> T:
125        """Load a Station from ICAO, GPS, or IATA code in that order"""
126        if ident and isinstance(ident, str):
127            if len(ident) == 4:
128                with suppress(BadStation):
129                    return cls.from_icao(ident)
130                with suppress(BadStation):
131                    return cls.from_gps(ident)
132            if len(ident) == 3:
133                with suppress(BadStation):
134                    return cls.from_iata(ident)
135            with suppress(BadStation):
136                return cls.from_local(ident)
137        raise BadStation(f"Could not find station with ident {ident}")
138
139    @classmethod
140    def from_icao(cls: Type[T], ident: str) -> T:
141        """Load a Station from an ICAO station ident"""
142        try:
143            return cls._from_code(_ICAO.value[ident.upper()])
144        except (KeyError, AttributeError) as not_found:
145            raise BadStation(
146                f"Could not find station with ICAO ident {ident}"
147            ) from not_found
148
149    @classmethod
150    def from_iata(cls: Type[T], ident: str) -> T:
151        """Load a Station from an IATA code"""
152        try:
153            return cls._from_code(_IATA.value[ident.upper()])
154        except (KeyError, AttributeError) as not_found:
155            raise BadStation(
156                f"Could not find station with IATA ident {ident}"
157            ) from not_found
158
159    @classmethod
160    def from_gps(cls: Type[T], ident: str) -> T:
161        """Load a Station from a GPS code"""
162        try:
163            return cls._from_code(_GPS.value[ident.upper()])
164        except (KeyError, AttributeError) as not_found:
165            raise BadStation(
166                f"Could not find station with GPS ident {ident}"
167            ) from not_found
168
169    @classmethod
170    def from_local(cls: Type[T], ident: str) -> T:
171        """Load a Station from a local code"""
172        try:
173            return cls._from_code(_LOCAL.value[ident.upper()])
174        except (KeyError, AttributeError) as not_found:
175            raise BadStation(
176                f"Could not find station with local ident {ident}"
177            ) from not_found
178
179    @classmethod
180    def nearest(
181        cls: Type[T],
182        lat: Optional[float] = None,
183        lon: Optional[float] = None,
184        is_airport: bool = False,
185        sends_reports: bool = True,
186        max_coord_distance: float = 10,
187    ) -> Optional[Tuple[T, dict]]:
188        """Load the Station nearest to your location or a lat,lon coordinate pair
189
190        Returns the Station and distances from source
191
192        NOTE: Becomes less accurate toward poles and doesn't cross +/-180
193        """
194        if not (lat and lon):
195            lat, lon = _get_ip_location().pair
196        ret = nearest(lat, lon, 1, is_airport, sends_reports, max_coord_distance)
197        if not isinstance(ret, dict):
198            return None
199        station = ret.pop("station")
200        return station, ret
201
202    @property
203    def lookup_code(self) -> str:
204        """Returns the ICAO or GPS code for report fetch"""
205        if self.icao:
206            return self.icao
207        if self.gps:
208            return self.gps
209        raise BadStation("Station does not have a valid lookup code")
210
211    @property
212    def storage_code(self) -> str:
213        """Returns the first unique-ish code from what's available"""
214        if self.icao:
215            return self.icao
216        if self.iata:
217            return self.iata
218        if self.gps:
219            return self.gps
220        if self.local:
221            return self.local
222        raise BadStation("Station does not have any useable codes")
223
224    @property
225    def sends_reports(self) -> bool:
226        """Returns whether or not a Station likely sends weather reports"""
227        return self.reporting is True
228
229    @property
230    def coord(self) -> Coord:
231        """Returns the station location as a Coord"""
232        return Coord(lat=self.latitude, lon=self.longitude, repr=self.icao)
233
234    def distance(self, lat: float, lon: float) -> Distance:
235        """Returns a geopy Distance using the great circle method"""
236        return great_circle((lat, lon), (self.latitude, self.longitude))
237
238    def nearby(
239        self,
240        is_airport: bool = False,
241        sends_reports: bool = True,
242        max_coord_distance: float = 10,
243    ) -> List[Tuple[T, dict]]:
244        """Returns Stations nearest to current station and their distances
245
246        NOTE: Becomes less accurate toward poles and doesn't cross +/-180
247        """
248        stations = nearest(
249            self.latitude,
250            self.longitude,
251            11,
252            is_airport,
253            sends_reports,
254            max_coord_distance,
255        )
256        if isinstance(stations, dict):
257            return []
258        return [(s.pop("station"), s) for s in stations[1:]]

The Station dataclass stores basic info about the desired station and available Runways.

The easiest way to get a station is to supply the ICAO, IATA, or GPS code. The example below uses from_code which checks against all three types, but you can also use from_icao, from_iata, or from_gps if you know what type of code you are using. This can be important if you may be using a code used by more than one station depending on the context. ICAO and IATA codes are guarenteed unique, but not all airports have them. That said, all stations available in AVWX have either an ICAO or GPS code.

>>> from avwx import Station
>>> klex = Station.from_code("KLEX")
>>> f"{klex.name} in {klex.city}, {klex.state}"
'Blue Grass Airport in Lexington, KY'
>>> coord = round(klex.latitude, 3), round(klex.longitude, 3)
>>> f"Located at {coord} at {klex.elevation_ft} feet ({klex.elevation_m} meters)"
'Located at (38.036, -84.606) at 979 feet (298 meters)'
>>> rw = max(klex.runways, key=lambda r: r.length_ft)
>>> f"Its longest runway is {rw.ident1}/{rw.ident2} at {rw.length_ft} feet"
'Its longest runway is 04/22 at 7003 feet'

This is also the same information you'd get from calling Report.station.

>>> from avwx import Metar
>>> klex = Metar('KLEX')
>>> klex.station.name
'Blue Grass Airport'
Station( city: Optional[str], country: str, elevation_ft: Optional[int], elevation_m: Optional[int], gps: Optional[str], iata: Optional[str], icao: Optional[str], latitude: float, local: Optional[str], longitude: float, name: str, note: Optional[str], reporting: bool, runways: List[Runway], state: Optional[str], type: str, website: Optional[str], wiki: Optional[str])
city: Optional[str]
country: str
elevation_ft: Optional[int]
elevation_m: Optional[int]
gps: Optional[str]
iata: Optional[str]
icao: Optional[str]
latitude: float
local: Optional[str]
longitude: float
name: str
note: Optional[str]
reporting: bool
runways: List[Runway]
state: Optional[str]
type: str
website: Optional[str]
wiki: Optional[str]
@classmethod
def from_code(cls: Type[~T], ident: str) -> ~T:
123    @classmethod
124    def from_code(cls: Type[T], ident: str) -> T:
125        """Load a Station from ICAO, GPS, or IATA code in that order"""
126        if ident and isinstance(ident, str):
127            if len(ident) == 4:
128                with suppress(BadStation):
129                    return cls.from_icao(ident)
130                with suppress(BadStation):
131                    return cls.from_gps(ident)
132            if len(ident) == 3:
133                with suppress(BadStation):
134                    return cls.from_iata(ident)
135            with suppress(BadStation):
136                return cls.from_local(ident)
137        raise BadStation(f"Could not find station with ident {ident}")

Load a Station from ICAO, GPS, or IATA code in that order

@classmethod
def from_icao(cls: Type[~T], ident: str) -> ~T:
139    @classmethod
140    def from_icao(cls: Type[T], ident: str) -> T:
141        """Load a Station from an ICAO station ident"""
142        try:
143            return cls._from_code(_ICAO.value[ident.upper()])
144        except (KeyError, AttributeError) as not_found:
145            raise BadStation(
146                f"Could not find station with ICAO ident {ident}"
147            ) from not_found

Load a Station from an ICAO station ident

@classmethod
def from_iata(cls: Type[~T], ident: str) -> ~T:
149    @classmethod
150    def from_iata(cls: Type[T], ident: str) -> T:
151        """Load a Station from an IATA code"""
152        try:
153            return cls._from_code(_IATA.value[ident.upper()])
154        except (KeyError, AttributeError) as not_found:
155            raise BadStation(
156                f"Could not find station with IATA ident {ident}"
157            ) from not_found

Load a Station from an IATA code

@classmethod
def from_gps(cls: Type[~T], ident: str) -> ~T:
159    @classmethod
160    def from_gps(cls: Type[T], ident: str) -> T:
161        """Load a Station from a GPS code"""
162        try:
163            return cls._from_code(_GPS.value[ident.upper()])
164        except (KeyError, AttributeError) as not_found:
165            raise BadStation(
166                f"Could not find station with GPS ident {ident}"
167            ) from not_found

Load a Station from a GPS code

@classmethod
def from_local(cls: Type[~T], ident: str) -> ~T:
169    @classmethod
170    def from_local(cls: Type[T], ident: str) -> T:
171        """Load a Station from a local code"""
172        try:
173            return cls._from_code(_LOCAL.value[ident.upper()])
174        except (KeyError, AttributeError) as not_found:
175            raise BadStation(
176                f"Could not find station with local ident {ident}"
177            ) from not_found

Load a Station from a local code

@classmethod
def nearest( cls: Type[~T], lat: Optional[float] = None, lon: Optional[float] = None, is_airport: bool = False, sends_reports: bool = True, max_coord_distance: float = 10) -> Optional[Tuple[~T, dict]]:
179    @classmethod
180    def nearest(
181        cls: Type[T],
182        lat: Optional[float] = None,
183        lon: Optional[float] = None,
184        is_airport: bool = False,
185        sends_reports: bool = True,
186        max_coord_distance: float = 10,
187    ) -> Optional[Tuple[T, dict]]:
188        """Load the Station nearest to your location or a lat,lon coordinate pair
189
190        Returns the Station and distances from source
191
192        NOTE: Becomes less accurate toward poles and doesn't cross +/-180
193        """
194        if not (lat and lon):
195            lat, lon = _get_ip_location().pair
196        ret = nearest(lat, lon, 1, is_airport, sends_reports, max_coord_distance)
197        if not isinstance(ret, dict):
198            return None
199        station = ret.pop("station")
200        return station, ret

Load the Station nearest to your location or a lat,lon coordinate pair

Returns the Station and distances from source

NOTE: Becomes less accurate toward poles and doesn't cross +/-180

lookup_code: str

Returns the ICAO or GPS code for report fetch

storage_code: str

Returns the first unique-ish code from what's available

sends_reports: bool

Returns whether or not a Station likely sends weather reports

Returns the station location as a Coord

def distance(self, lat: float, lon: float) -> geopy.distance.Distance:
234    def distance(self, lat: float, lon: float) -> Distance:
235        """Returns a geopy Distance using the great circle method"""
236        return great_circle((lat, lon), (self.latitude, self.longitude))

Returns a geopy Distance using the great circle method

def nearby( self, is_airport: bool = False, sends_reports: bool = True, max_coord_distance: float = 10) -> List[Tuple[~T, dict]]:
238    def nearby(
239        self,
240        is_airport: bool = False,
241        sends_reports: bool = True,
242        max_coord_distance: float = 10,
243    ) -> List[Tuple[T, dict]]:
244        """Returns Stations nearest to current station and their distances
245
246        NOTE: Becomes less accurate toward poles and doesn't cross +/-180
247        """
248        stations = nearest(
249            self.latitude,
250            self.longitude,
251            11,
252            is_airport,
253            sends_reports,
254            max_coord_distance,
255        )
256        if isinstance(stations, dict):
257            return []
258        return [(s.pop("station"), s) for s in stations[1:]]

Returns Stations nearest to current station and their distances

NOTE: Becomes less accurate toward poles and doesn't cross +/-180

def station_filter( station: Station, is_airport: bool, reporting: bool) -> bool:
304def station_filter(station: Station, is_airport: bool, reporting: bool) -> bool:
305    """Return True if station matches given criteria"""
306    if is_airport and "airport" not in station.type:
307        return False
308    return bool(not reporting or station.sends_reports)

Return True if station matches given criteria

def nearest( lat: float, lon: float, n: int = 1, is_airport: bool = False, sends_reports: bool = True, max_coord_distance: float = 10) -> Union[dict, List[dict]]:
337def nearest(
338    lat: float,
339    lon: float,
340    n: int = 1,
341    is_airport: bool = False,
342    sends_reports: bool = True,
343    max_coord_distance: float = 10,
344) -> Union[dict, List[dict]]:
345    """Finds the nearest n Stations to a lat,lon coordinate pair
346
347    Returns the Station and coordinate distance from source
348
349    NOTE: Becomes less accurate toward poles and doesn't cross +/-180
350    """
351    # Default state includes all, no filtering necessary
352    if is_airport or sends_reports:
353        stations = _query_filter(
354            lat, lon, n, max_coord_distance, is_airport, sends_reports
355        )
356    else:
357        data = _query_coords(lat, lon, n, max_coord_distance)
358        stations = [(Station.from_code(code), d) for code, d in data]
359    if not stations:
360        return []
361    ret = []
362    for station, coord_dist in stations:
363        dist = station.distance(lat, lon)
364        ret.append(
365            {
366                "station": station,
367                "coordinate_distance": coord_dist,
368                "nautical_miles": dist.nautical,
369                "miles": dist.miles,
370                "kilometers": dist.kilometers,
371            }
372        )
373    if n == 1:
374        return ret[0]
375    ret.sort(key=lambda x: x["miles"])
376    return ret

Finds the nearest n Stations to a lat,lon coordinate pair

Returns the Station and coordinate distance from source

NOTE: Becomes less accurate toward poles and doesn't cross +/-180