1# Licensed under a 3-clause BSD style license - see LICENSE.rst
2"""
3Currently the only site accessible without internet access is the Royal
4Greenwich Observatory, as an example (and for testing purposes).  In future
5releases, a canonical set of sites may be bundled into astropy for when the
6online registry is unavailable.
7
8Additions or corrections to the observatory list can be submitted via Pull
9Request to the [astropy-data GitHub repository](https://github.com/astropy/astropy-data),
10updating the ``location.json`` file.
11"""
12
13
14import json
15from difflib import get_close_matches
16from collections.abc import Mapping
17
18from astropy.utils.data import get_pkg_data_contents, get_file_contents
19from .earth import EarthLocation
20from .errors import UnknownSiteException
21from astropy import units as u
22
23
24class SiteRegistry(Mapping):
25    """
26    A bare-bones registry of EarthLocation objects.
27
28    This acts as a mapping (dict-like object) but with the important caveat that
29    it's always transforms its inputs to lower-case.  So keys are always all
30    lower-case, and even if you ask for something that's got mixed case, it will
31    be interpreted as the all lower-case version.
32    """
33    def __init__(self):
34        # the keys to this are always lower-case
35        self._lowercase_names_to_locations = {}
36        # these can be whatever case is appropriate
37        self._names = []
38
39    def __getitem__(self, site_name):
40        """
41        Returns an EarthLocation for a known site in this registry.
42
43        Parameters
44        ----------
45        site_name : str
46            Name of the observatory (case-insensitive).
47
48        Returns
49        -------
50        site : `~astropy.coordinates.EarthLocation`
51            The location of the observatory.
52        """
53        if site_name.lower() not in self._lowercase_names_to_locations:
54            # If site name not found, find close matches and suggest them in error
55            close_names = get_close_matches(site_name, self._lowercase_names_to_locations)
56            close_names = sorted(close_names, key=len)
57
58            raise UnknownSiteException(site_name, "the 'names' attribute", close_names=close_names)
59
60        return self._lowercase_names_to_locations[site_name.lower()]
61
62    def __len__(self):
63        return len(self._lowercase_names_to_locations)
64
65    def __iter__(self):
66        return iter(self._lowercase_names_to_locations)
67
68    def __contains__(self, site_name):
69        return site_name.lower() in self._lowercase_names_to_locations
70
71    @property
72    def names(self):
73        """
74        The names in this registry.  Note that these are *not* exactly the same
75        as the keys: keys are always lower-case, while `names` is what you
76        should use for the actual readable names (which may be case-sensitive)
77
78        Returns
79        -------
80        site : list of str
81            The names of the sites in this registry
82        """
83        return sorted(self._names)
84
85    def add_site(self, names, locationobj):
86        """
87        Adds a location to the registry.
88
89        Parameters
90        ----------
91        names : list of str
92            All the names this site should go under
93        locationobj : `~astropy.coordinates.EarthLocation`
94            The actual site object
95        """
96        for name in names:
97            self._lowercase_names_to_locations[name.lower()] = locationobj
98            self._names.append(name)
99
100    @classmethod
101    def from_json(cls, jsondb):
102        reg = cls()
103        for site in jsondb:
104            site_info = jsondb[site].copy()
105            location = EarthLocation.from_geodetic(site_info.pop('longitude') * u.Unit(site_info.pop('longitude_unit')),
106                                                   site_info.pop('latitude') * u.Unit(site_info.pop('latitude_unit')),
107                                                   site_info.pop('elevation') * u.Unit(site_info.pop('elevation_unit')))
108            location.info.name = site_info.pop('name')
109            aliases = site_info.pop('aliases')
110            location.info.meta = site_info  # whatever is left
111
112            reg.add_site([site] + aliases, location)
113
114        reg._loaded_jsondb = jsondb
115        return reg
116
117
118def get_builtin_sites():
119    """
120    Load observatory database from data/observatories.json and parse them into
121    a SiteRegistry.
122    """
123    jsondb = json.loads(get_pkg_data_contents('data/sites.json'))
124    return SiteRegistry.from_json(jsondb)
125
126
127def get_downloaded_sites(jsonurl=None):
128    """
129    Load observatory database from data.astropy.org and parse into a SiteRegistry
130    """
131
132    # we explicitly set the encoding because the default is to leave it set by
133    # the users' locale, which may fail if it's not matched to the sites.json
134    if jsonurl is None:
135        content = get_pkg_data_contents('coordinates/sites.json', encoding='UTF-8')
136    else:
137        content = get_file_contents(jsonurl, encoding='UTF-8')
138
139    jsondb = json.loads(content)
140    return SiteRegistry.from_json(jsondb)
141