1
# Licensed under a 3-clause BSD style license - see LICENSE.rst
2 1
from astropy.table import Table, Column
3 1
import astropy.units as u
4 1
from astropy import coordinates
5 1
import six
6 1
from . import utils
7 1
from . import conf
8 1
from ..utils import commons
9 1
from ..query import BaseQuery
10

11

12
# TODO Add support for server url from JSON cache
13 1
__all__ = ["IrsaDust", "IrsaDustClass"]
14

15 1
EXT_DESC = "E(B-V) Reddening"
16 1
EM_DESC = "100 Micron Emission"
17 1
TEMP_DESC = "Dust Temperature"
18

19 1
INPUT = "input"
20 1
OBJ_NAME = "objname"
21 1
REG_SIZE = "regSize"
22 1
DESC = "desc"
23 1
IMAGE_URL = "imageUrl"
24

25 1
STATISTICS = "statistics"
26 1
REF_PIXEL_VALUE = "refPixelValue"
27 1
REF_COORDINATE = "refCoordinate"
28 1
MEAN_VALUE = "meanValue"
29 1
STD = "std"
30 1
MAX_VALUE = "maxValue"
31 1
MIN_VALUE = "minValue"
32 1
SANDF = "SandF"
33 1
SFD = "SFD"
34

35 1
DATA_IMAGE = "./data/image"
36 1
DATA_TABLE = "./data/table"
37

38

39 1
class IrsaDustClass(BaseQuery):
40

41 1
    DUST_SERVICE_URL = conf.server
42 1
    TIMEOUT = conf.timeout
43 1
    image_type_to_section = {
44
        'temperature': 't',
45
        'ebv': 'r',
46
        '100um': 'e'
47
    }
48

49 1
    def get_images(self, coordinate, radius=None,
50
                   image_type=None, timeout=TIMEOUT, get_query_payload=False,
51
                   show_progress=True):
52
        """
53
        A query function that performs a coordinate-based query to acquire
54
        Irsa-Dust images.
55

56
        Parameters
57
        ----------
58
        coordinate : str
59
            Can be either the name of an object or a coordinate string
60
            If a name, must be resolvable by NED, SIMBAD, 2MASS, or SWAS.
61
            Examples of acceptable coordinate strings, can be found `here.
62
            <https://irsa.ipac.caltech.edu/applications/DUST/docs/coordinate.html>`_
63
        radius : str / `~astropy.units.Quantity`, optional
64
            The size of the region to include in the dust query, in radian,
65
            degree or hour as per format specified by
66
            `~astropy.coordinates.Angle` or `~astropy.units.Quantity`.
67
            Defaults to 5 degrees.
68
        image_type : str, optional
69
            When missing returns for all the images. Otherwise returns only
70
            for image of the specified type which must be one of
71
            ``'temperature'``,  ``'ebv'``, ``'100um'``. Defaults to `None`.
72
        timeout : int, optional
73
            Time limit for establishing successful connection with remote
74
            server. Defaults to `~astroquery.irsa_dust.IrsaDustClass.TIMEOUT`.
75
        get_query_payload : bool, optional
76
            If `True` then returns the dictionary of query parameters, posted
77
            to remote server. Defaults to `False`.
78

79
        Returns
80
        -------
81
        A list of `~astropy.io.fits.HDUList` objects
82

83
        """
84

85 1
        if get_query_payload:
86 0
            return self._args_to_payload(coordinate, radius=radius)
87 1
        readable_objs = self.get_images_async(
88
            coordinate, radius=radius, image_type=image_type, timeout=timeout,
89
            get_query_payload=get_query_payload, show_progress=show_progress)
90 1
        return [obj.get_fits() for obj in readable_objs]
91

92 1
    def get_images_async(self, coordinate, radius=None, image_type=None,
93
                         timeout=TIMEOUT, get_query_payload=False,
94
                         show_progress=True):
95
        """
96
        A query function similar to
97
        `astroquery.irsa_dust.IrsaDustClass.get_images` but returns
98
        file-handlers to the remote files rather than downloading
99
        them. Useful for asynchronous queries so that the actual download
100
        may be performed later.
101

102
        Parameters
103
        ----------
104
        coordinate : str
105
            Can be either the name of an object or a coordinate string
106
            If a name, must be resolvable by NED, SIMBAD, 2MASS, or SWAS.
107
            Examples of acceptable coordinate strings, can be found `here.
108
            <https://irsa.ipac.caltech.edu/applications/DUST/docs/coordinate.html>`_
109
        radius : str / `~astropy.units.Quantity`, optional
110
            The size of the region to include in the dust query, in radian,
111
            degree or hour as per format specified by
112
            `~astropy.coordinates.Angle` or `~astropy.units.Quantity`.
113
            Defaults to 5 degrees.
114
        image_type : str, optional
115
            When missing returns for all the images. Otherwise returns only
116
            for image of the specified type which must be one of
117
            ``'temperature'``, ``'ebv'``, ``'100um'``. Defaults to `None`.
118
        timeout : int, optional
119
            Time limit for establishing successful connection with remote
120
            server. Defaults to `~astroquery.irsa_dust.IrsaDustClass.TIMEOUT`.
121
        get_query_payload : bool, optional
122
            If `True` then returns the dictionary of query parameters, posted
123
            to remote server. Defaults to `False`.
124

125
        Returns
126
        --------
127
        list : list
128
            A list of context-managers that yield readable file-like objects.
129
        """
130

131 1
        if get_query_payload:
132 0
            return self._args_to_payload(coordinate, radius=radius)
133 1
        image_urls = self.get_image_list(
134
            coordinate, radius=radius, image_type=image_type, timeout=timeout)
135

136
        # Images are assumed to be FITS files
137
        # they MUST be read as binary for python3 to parse them
138 1
        return [commons.FileContainer(U, encoding='binary',
139
                                      show_progress=show_progress)
140
                for U in image_urls]
141

142 1
    def get_image_list(self, coordinate, radius=None, image_type=None,
143
                       timeout=TIMEOUT):
144
        """
145
        Query function that performs coordinate-based query and returns a list
146
        of URLs to the Irsa-Dust images.
147

148
        Parameters
149
        -----------
150
        coordinate : str
151
            Can be either the name of an object or a coordinate string
152
            If a name, must be resolvable by NED, SIMBAD, 2MASS, or SWAS.
153
            Examples of acceptable coordinate strings, can be found `here.
154
            <https://irsa.ipac.caltech.edu/applications/DUST/docs/coordinate.html>`_
155
        radius : str / `~astropy.units.Quantity`, optional
156
            The size of the region to include in the dust query, in radian,
157
            degree or hour as per format specified by
158
            `~astropy.coordinates.Angle` or `~astropy.units.Quantity`.
159
            Defaults to 5 degrees.
160
        image_type : str, optional
161
            When missing returns for all the images. Otherwise returns only
162
            for image of the specified type which must be one of
163
            ``'temperature'``, ``'ebv'``, ``'100um'``. Defaults to `None`.
164
        timeout : int, optional
165
            Time limit for establishing successful connection with remote
166
            server. Defaults to `~astroquery.irsa_dust.IrsaDustClass.TIMEOUT`.
167
        get_query_payload : bool
168
            If `True` then returns the dictionary of query parameters, posted
169
            to remote server. Defaults to `False`.
170

171
        Returns
172
        --------
173
        url_list : list
174
            A list of URLs to the FITS images corresponding to the queried
175
            object.
176
        """
177 1
        url = self.DUST_SERVICE_URL
178 1
        request_payload = self._args_to_payload(coordinate, radius=radius)
179 1
        response = self._request("POST", url, data=request_payload,
180
                                 timeout=timeout)
181 1
        return self.extract_image_urls(response.text, image_type=image_type)
182

183 1
    def get_extinction_table(self, coordinate, radius=None, timeout=TIMEOUT,
184
                             show_progress=True):
185
        """
186
        Query function that fetches the extinction table from the query
187
        result.
188

189
        Parameters
190
        ----------
191
        coordinate : str
192
            Can be either the name of an object or a coordinate string
193
            If a name, must be resolvable by NED, SIMBAD, 2MASS, or SWAS.
194
            Examples of acceptable coordinate strings, can be found `here.
195
            <https://irsa.ipac.caltech.edu/applications/DUST/docs/coordinate.html>`_
196
        radius : str / `~astropy.units.Quantity`, optional
197
            The size of the region to include in the dust query, in radian,
198
            degree or hour as per format specified by
199
            `~astropy.coordinates.Angle` or `~astropy.units.Quantity`.
200
            Defaults to 5 degrees.
201
        timeout : int, optional
202
            Time limit for establishing successful connection with remote
203
            server. Defaults to `~astroquery.irsa_dust.IrsaDustClass.TIMEOUT`.
204

205
        Returns
206
        --------
207
        table : `~astropy.table.Table`
208
        """
209 1
        readable_obj = self.get_extinction_table_async(
210
            coordinate, radius=radius, timeout=timeout,
211
            show_progress=show_progress)
212
        # guess=False to suppress error messages related to bad guesses
213 1
        table = Table.read(readable_obj.get_string(), format='ipac',
214
                           guess=False)
215
        # Fix up units: 'micron' and 'mag', not 'microns' and 'mags'
216 1
        for column in table.columns.values():
217 1
            if str(column.unit) in {'microns', 'mags'}:
218 1
                column.unit = str(column.unit)[:-1]
219 1
        return table
220

221 1
    def get_extinction_table_async(self, coordinate, radius=None,
222
                                   timeout=TIMEOUT, show_progress=True):
223
        """
224
        A query function similar to
225
        `astroquery.irsa_dust.IrsaDustClass.get_extinction_table` but
226
        returns a file-handler to the remote files rather than downloading
227
        it.  Useful for asynchronous queries so that the actual download may
228
        be performed later.
229

230
        Parameters
231
        ----------
232
        coordinate : str
233
            Can be either the name of an object or a coordinate string
234
            If a name, must be resolvable by NED, SIMBAD, 2MASS, or SWAS.
235
            Examples of acceptable coordinate strings, can be found `here.
236
            <https://irsa.ipac.caltech.edu/applications/DUST/docs/coordinate.html>`_
237
        radius : str, optional
238
            The size of the region to include in the dust query, in radian,
239
            degree or hour as per format specified by
240
            `~astropy.coordinates.Angle`. Defaults to 5 degrees.
241
        timeout : int, optional
242
            Time limit for establishing successful connection with remote
243
            server. Defaults to `~astroquery.irsa_dust.IrsaDustClass.TIMEOUT`.
244

245
        Returns
246
        -------
247
        result : A context manager that yields a file like readable object.
248
        """
249 1
        url = self.DUST_SERVICE_URL
250 1
        request_payload = self._args_to_payload(coordinate, radius=radius)
251 1
        response = self._request("POST", url, data=request_payload,
252
                                 timeout=timeout)
253 1
        xml_tree = utils.xml(response.text)
254 1
        result = SingleDustResult(xml_tree, coordinate)
255 1
        return commons.FileContainer(result.ext_detail_table(),
256
                                     show_progress=show_progress)
257

258 1
    def get_query_table(self, coordinate, radius=None,
259
                        section=None, timeout=TIMEOUT, url=DUST_SERVICE_URL):
260
        """
261
        Create and return an `~astropy.table.Table` representing the query
262
        response(s).
263

264
        When ``section`` is missing, returns the full table. When a
265
        section is specified (``'location'``, ``'temperature'``, ``'ebv'``, or
266
        ``'100um'``), only that portion of the table is returned.
267

268
        Parameters
269
        ----------
270
        coordinate : str
271
            Can be either the name of an object or a coordinate string
272
            If a name, must be resolvable by NED, SIMBAD, 2MASS, or SWAS.
273
            Examples of acceptable coordinate strings, can be found `here.
274
            <https://irsa.ipac.caltech.edu/applications/DUST/docs/coordinate.html>`_
275
        radius : str / `~astropy.units.Quantity`, optional
276
            The size of the region to include in the dust query, in radian,
277
            degree or hour as per format specified by
278
            `~astropy.coordinates.Angle` or `~astropy.units.Quantity`.
279
            Defaults to 5 degrees.
280
        section : str, optional
281
            When missing, all the sections of the query result are returned.
282
            Otherwise only the specified section (``'ebv'``, ``'100um'``,
283
            ``'temperature'``, ``'location'``) is returned. Defaults to `None`.
284
        timeout : int, optional
285
            Time limit for establishing successful connection with remote
286
            server. Defaults to `~astroquery.irsa_dust.IrsaDustClass.TIMEOUT`.
287
        url : str, optional
288
            Only provided for debugging. Should generally not be assigned.
289
            Defaults to `~astroquery.irsa_dust.IrsaDustClass.DUST_SERVICE_URL`.
290

291
        Returns
292
        --------
293
        table : `~astropy.table.Table`
294
            Table representing the query results, (all or as per  specified).
295
        """
296 1
        request_payload = self._args_to_payload(coordinate, radius=radius)
297 1
        response = self._request("POST", url, data=request_payload,
298
                                 timeout=timeout)
299 1
        xml_tree = utils.xml(response.text)
300 1
        result = SingleDustResult(xml_tree, coordinate)
301 1
        if section is None or section in ["location", "loc", "l"]:
302 1
            return result.table(section=section)
303 1
        try:
304 1
            section = self.image_type_to_section[section]
305 1
            return result.table(section=section)
306 0
        except KeyError:
307 0
            msg = ('section must be one of the following:\n'
308
                   'ebv, temperature, location or 100um.')
309 0
            raise ValueError(msg)
310

311 1
    def _args_to_payload(self, coordinate, radius=None):
312
        """
313
        Accepts the query parameters and returns a dictionary
314
        suitable to be sent as data via a HTTP POST request.
315

316
        Parameters
317
        ----------
318
        coordinate : str
319
            Can be either the name of an object or a coordinate string
320
            If a name, must be resolvable by NED, SIMBAD, 2MASS, or SWAS.
321
            Examples of acceptable coordinate strings, can be found `here
322
            <https://irsa.ipac.caltech.edu/applications/DUST/docs/coordinate.html>`_
323
        radius : str / `~astropy.units.Quantity`, optional
324
            The size of the region to include in the dust query, in radian,
325
            degree or hour as per format specified by
326
            `~astropy.coordinates.Angle` or `~astropy.units.Quantity`.
327
            Defaults to 5 degrees.
328

329
        Returns
330
        -------
331
        payload : dict
332
            A dictionary that specifies the data for an HTTP POST request
333
        """
334 1
        if isinstance(coordinate, six.string_types):
335 1
            try:
336
                # If the coordinate is a resolvable name, pass that name
337
                # directly to irsa_dust because it can handle it (and that
338
                # changes the return value associated metadata)
339 1
                C = commons.ICRSCoord.from_name(coordinate)
340 1
                payload = {"locstr": coordinate}
341 0
            except coordinates.name_resolve.NameResolveError:
342 0
                C = commons.parse_coordinates(coordinate).transform_to('fk5')
343
                # check if this is resolvable?
344 0
                payload = {"locstr": "{0} {1}".format(C.ra.deg, C.dec.deg)}
345 1
        elif isinstance(coordinate, coordinates.SkyCoord):
346 1
            C = coordinate.transform_to('fk5')
347 1
            payload = {"locstr": "{0} {1}".format(C.ra.deg, C.dec.deg)}
348
        # check if radius is given with proper units
349 1
        if radius is not None:
350 1
            reg_size = coordinates.Angle(radius).deg
351
            # check if radius falls in the acceptable range
352 1
            if reg_size < 2 or reg_size > 37.5:
353 1
                raise ValueError("Radius (in any unit) must be in the"
354
                                 " range of 2.0 to 37.5 degrees")
355 1
            payload["regSize"] = reg_size
356 1
        return payload
357

358 1
    def extract_image_urls(self, raw_xml, image_type=None):
359
        """
360
        Extracts the image URLs from the query results and
361
        returns these as a list. If section is missing or
362
        ``'all'`` returns all the URLs, otherwise returns URL
363
        corresponding to the section specified (``'emission'``,
364
        ``'reddening'``, ``'temperature'``).
365

366
        Parameters
367
        ----------
368
        raw_xml : str
369
            XML response returned by the query as a string
370
        image_type : str, optional
371
            When missing returns for all the images. Otherwise returns only
372
            for image of the specified type which must be one of
373
            ``'temperature'``, ``'ebv'``, ``'100um'``. Defaults to `None`.
374

375
        Returns
376
        -------
377
        url_list : list
378
            list of URLs to images extracted from query results.
379
        """
380
        # get the xml tree from the response
381 1
        xml_tree = utils.xml(raw_xml)
382 1
        result = SingleDustResult(xml_tree)
383 1
        if image_type is None:
384 1
            url_list = [result.image(sec) for sec in
385
                        ['r', 'e', 't']]
386
        else:
387 1
            try:
388 1
                section = self.image_type_to_section[image_type]
389 1
                url_list = [result.image(section)]
390 1
            except KeyError:
391 1
                msg = ('image_type must be one of the following:\n'
392
                       'ebv, temperature or 100um.')
393 1
                raise ValueError(msg)
394 1
        return url_list
395

396 1
    def list_image_types(self):
397
        """
398
        Returns a list of image_types available in the Irsa Dust
399
        query results
400
        """
401 1
        return [key for key in self.image_type_to_section]
402

403

404 1
class SingleDustResult(object):
405

406
    """
407
    Represents the response to a dust query for a single object or location.
408

409
    Provides methods to return the response as an `~astropy.table.Table`,
410
    and to retrieve FITS images listed as urls in the initial response. It
411
    can also return the url to a detailed extinction table provided in the
412
    initial response. Not intended to be instantiated by the end user.
413
    """
414

415 1
    def __init__(self, xml_tree, query_loc=None):
416
        """
417
        Parameters
418
        ----------
419
        xml_root : `xml.etree.ElementTree`
420
            the xml tree representing the response to the query
421
        query_loc : str
422
            the location string specified in the query
423
        """
424 1
        self._xml = xml_tree
425 1
        self._query_loc = query_loc
426

427 1
        self._location_section = LocationSection(xml_tree)
428

429 1
        ext_node = utils.find_result_node(EXT_DESC, xml_tree)
430 1
        self._ext_section = ExtinctionSection(ext_node)
431

432 1
        em_node = utils.find_result_node(EM_DESC, xml_tree)
433 1
        self._em_section = EmissionSection(em_node)
434

435 1
        temp_node = utils.find_result_node(TEMP_DESC, xml_tree)
436 1
        self._temp_section = TemperatureSection(temp_node)
437

438 1
        self._result_sections = [self._location_section, self._ext_section,
439
                                 self._em_section, self._temp_section]
440

441 1
    @property
442
    def query_loc(self):
443
        """Return the location string used in the query."""
444 0
        return self._query_loc
445

446 1
    @property
447
    def xml(self):
448
        """Return the raw xml underlying this SingleDustResult."""
449 0
        return self._xml
450

451 1
    def table(self, section=None):
452
        """
453
        Create and return a `~astropy.table.Table` representing the query
454
        response.
455

456
        Parameters
457
        ----------
458
        section : str
459
            (optional) the name of the section to include in the table.
460
            If not provided, the entire table will be returned.
461
        """
462 1
        code = self._section_code(section)
463 1
        if code == "all":
464 1
            return self._table_all()
465
        else:
466 1
            return self._table(code)
467

468 1
    def values(self, section=None):
469
        """
470
        Return the data values contained in the query response,
471
        i.e. the list of values corresponding to a row in the result table.
472

473
        Parameters
474
        ----------
475
        section : str
476
            the name of the section to include in the response
477
            If no section is given, all sections will be included.
478
        """
479 1
        code = self._section_code(section)
480 1
        sections = self._sections(code)
481

482 1
        values = []
483 1
        for sec in sections:
484 1
            values.extend(sec.values())
485 1
        return values
486

487 1
    def _section_code(self, section):
488
        """
489
        Return a one-letter code identifying the section.
490

491
        Parameters
492
        ----------
493
        section : str
494
            the name or abbreviated name of the section
495

496
        Returns
497
        -------
498
            str: a one-letter code identifying the section.
499
        """
500 1
        if section is None or section == "all":
501 1
            return "all"
502
        else:
503 1
            if section in ["location", "loc", "l"]:
504 1
                return "l"
505 1
            elif section in ["reddening", "red", "r"]:
506 1
                return "r"
507 1
            elif section in ["emission", "em", "e"]:
508 1
                return "e"
509 1
            elif section in ["temperature", "temp", "t"]:
510 1
                return "t"
511
            else:
512 0
                msg = """section must be one of the following:
513
                        'all',
514
                        'location', 'loc', 'l',
515
                        'reddening', 'red', 'r',
516
                        'emission', 'em', 'e',
517
                        'temperature', 'temp', 't'."""
518 0
                raise ValueError(msg)
519

520 1
    def _sections(self, code):
521
        """
522
        Parameters
523
        ----------
524
        code : str
525
            the one-letter code name of the section
526

527
        Returns
528
        -------
529
        The section corresponding to the code, or a list containing all
530
        sections if no section is provided.
531
        """
532 1
        if code == 'l':
533 1
            return [self._location_section]
534 1
        elif code == 'r':
535 1
            return [self._ext_section]
536 1
        elif code == 'e':
537 1
            return [self._em_section]
538 1
        elif code == 't':
539 1
            return [self._temp_section]
540 1
        return [self._location_section, self._ext_section, self._em_section,
541
                self._temp_section]
542

543 1
    def _table_all(self):
544
        """
545
        Create and return the full table containing all four sections:
546
        location, extinction, emission, and temperature.
547

548
        Returns
549
        -------
550
        table : `~astropy.table.Table`
551
            table containing the data from the query response
552
        """
553 1
        columns = (self._location_section.columns + self._ext_section.columns +
554
                   self._em_section.columns + self._temp_section.columns)
555 1
        table = Table(data=columns)
556

557 1
        values = self.values()
558 1
        table.add_row(vals=values)
559 1
        return table
560

561 1
    def _table(self, section):
562
        """
563
        Create and return a smaller table containing only one section
564
        of the overall DustResult table.
565

566
        Parameters
567
        ----------
568
        section : str
569
            a string indicating the section to be returned
570
        """
571
        # Get the specified section
572 1
        section_object = self._sections(section)[0]
573

574
        # Create the table
575 1
        columns = section_object.columns
576 1
        table = Table(data=columns)
577

578
        # Populate the table
579 1
        values = section_object.values()
580 1
        table.add_row(vals=values)
581

582 1
        return table
583

584 1
    def ext_detail_table(self):
585
        """
586
        Get the url of the additional, detailed table of extinction data for
587
        various filters. There is a url for this table given in the initial
588
        response to the query.
589

590
        Returns
591
        -------
592
        table_url : str
593
            url of the detailed table of extinction data by filter
594
        """
595 1
        table_url = self._ext_section.table_url
596
        # response = utils.ext_detail_table(table_url)
597
        # if sys.version_info > (3, 0):
598
        #   read_response = response.read().decode("utf-8")
599
        # else:
600
        #   read_response = response.read()
601
        # table = Table.read(read_response, format="ipac")
602
        # return table
603 1
        return table_url
604

605 1
    def image(self, section):
606
        """
607
        Get the FITS image url associated with the given section.
608
        The extinction, emission, and temperature sections each provide
609
        a url to a FITS image.
610

611
        Parameters
612
        ----------
613
        section : str
614
            the name of the section
615

616
        Returns
617
        -------
618
        url : str
619
            the url to the FITS image
620
        """
621
        # Get the url of the image for the given section
622 1
        image_url = None
623 1
        if section in ["reddening", "red", "r"]:
624 1
            image_url = self._ext_section.image_url
625 1
        elif section in ["emission", "em", "e"]:
626 1
            image_url = self._em_section.image_url
627 1
        elif section in ["temperature", "temp", "t"]:
628 1
            image_url = self._temp_section.image_url
629

630 1
        if image_url is None:
631 0
            msg = """section must be one of the following values:
632
                    'reddening', 'red', 'r',
633
                    'emission', 'em', 'e',
634
                    'temperature', 'temp', 't'"""
635 0
            raise ValueError(msg)
636

637 1
        return image_url
638

639 1
    def __str__(self):
640
        """Return a string representation of the table."""
641 0
        string = "[DustResult: \n\t"
642 0
        for section in self._result_sections:
643 0
            if len(string) > 15:
644 0
                string += ",\n\t"
645 0
            string += section.__str__()
646 0
        string += "]"
647 0
        return string
648

649

650 1
class BaseDustNode(object):
651

652
    """
653
    A node in the result xml that has been enhanced to return values and
654
    Columns appropriate to its type (String, Number, or Coord).
655
    """
656

657 1
    def __init__(self, xml_node):
658
        """
659
        Parameters
660
        ----------
661
        xml_node : `xml.etree.ElementTree`
662
            the xml node that provides the raw data for this DustNode
663
        """
664 1
        self._name = xml_node.tag
665

666 1
    def set_value(self, node_text):
667
        """Override in subclasses."""
668 0
        pass
669

670 1
    @property
671
    def name(self):
672
        """Return the xml node name."""
673 0
        return self._name
674

675 1
    @property
676
    def value(self):
677
        """Return the value extracted from the node."""
678 0
        return self._value
679

680 1
    @property
681
    def columns(self):
682
        """Return the column or columns associated with this item in the
683
        `~astropy.table.Table`."""
684 0
        return self._columns
685

686 1
    def __str__(self):
687
        """Return a string representation of this item."""
688 0
        col_str = "[Column: "
689 0
        for column in self._columns:
690 0
            for format_str in column.pformat(show_units=True):
691 0
                col_str += format_str
692 0
        string = "name: " + self._name + ", " + col_str + "]"
693 0
        return string
694

695

696 1
class StringNode(BaseDustNode):
697

698
    """
699
    A node that contains text.
700
    """
701

702 1
    def __init__(self, xml_node, col_name, length):
703
        """
704
        Parameters
705
        ----------
706
        xml_node : `xml.etree.ElementTree`
707
            the xml node that provides the raw data for this DustNode
708
        col_name : str
709
            the name of the column associated with this item
710
        length : int
711
            the size of the column associated with this item
712
        """
713 1
        BaseDustNode.__init__(self, xml_node)
714

715 1
        self._value = xml_node.text.strip()
716

717 1
        self._length = length
718 1
        self._columns = [Column(name=col_name, dtype="S" + str(length))]
719

720 1
    def __str__(self):
721
        """Return a string representation of this item."""
722 0
        base_string = BaseDustNode.__str__(self)
723 0
        string = ("[StringNode: " + base_string +
724
                  ", value: " + self._value + "]")
725 0
        return string
726

727

728 1
class NumberNode(BaseDustNode):
729

730
    """
731
    A node that contains a number. Outputs a single column containing the
732
    number.
733
    """
734

735 1
    def __init__(self, xml_node, col_name, units=None):
736
        """
737
        Parameters
738
        ----------
739
        xml_node : `xml.etree.ElementTree`
740
            the xml node that provides the raw data for this DustNode
741
        col_name : str
742
            the name of the column associated with this item
743
        units : `~astropy.units.Unit`
744
            the units associated with this item
745
        """
746 1
        BaseDustNode.__init__(self, xml_node)
747 1
        self._value = utils.parse_number(xml_node.text)
748 1
        self._columns = [Column(name=col_name, unit=units)]
749

750 1
    def __str__(self):
751
        """Return a string representation of the item."""
752 0
        base_string = BaseDustNode.__str__(self)
753

754 0
        string = ("[NumberNode: " + base_string +
755
                  ", value: " + str(self._value) + "]")
756 0
        return string
757

758

759 1
class CoordNode(BaseDustNode):
760

761
    """
762
    A node that contains RA, Dec coordinates. Outputs three values /
763
    columns: RA, Dec and a coordinate system description string.
764
    """
765

766 1
    def __init__(self, xml_node, col_names):
767
        """
768
        Parameters
769
        ----------
770
        xml_node : `xml.etree.ElementTree`
771
            the xml node that provides the raw data for this DustNode
772
        col_names : str
773
            the names of the columns associated with this item
774
        """
775 1
        BaseDustNode.__init__(self, xml_node)
776 1
        self._value = utils.parse_coords(xml_node.text)
777 1
        units = u.deg
778 1
        self._columns = [Column(name=col_names[0], unit=units),
779
                         Column(name=col_names[1], unit=units),
780
                         Column(name=col_names[2], dtype="S25")]
781

782 1
    def __str__(self):
783
        """Return a string representation of the item."""
784 0
        base_string = BaseDustNode.__str__(self)
785 0
        values_str = ("values: " + str(self._value[0]) + ", " +
786
                      str(self._value[1]) + ", " + str(self._value[2]))
787 0
        string = ("[CoordNode: " + base_string + ", " + values_str + "]")
788 0
        return string
789

790

791 1
class BaseResultSection(object):
792

793
    """
794
    Represents a group of related nodes/columns in a DustResults object.
795
    A DustResults table contains four main sections:
796
        1-location
797
        2-extinction
798
        3-emission
799
        4-temperature
800
    In addition, the extinction, emission, and temperature sections
801
    each contain a nested statistics subsection.
802
    """
803

804 1
    def node_dict(self, names, xml_root):
805
        """
806
        Find all the nodes with the given names under the given root,
807
        and put them in a dictionary.
808

809
        Parameters
810
        ----------
811
        names : list[str]
812
            the names of the nodes to find
813
        xml_root : `xml.etree.ElementTree`
814
            the root of the xml tree to search
815

816
        Returns
817
        -------
818
        nodes : dictionary
819
            a dictionary of xml nodes, where the keys are the node names
820
        """
821 1
        nodes = {}
822 1
        for name in names:
823 1
            found_node = xml_root.find(name)
824 1
            if found_node is None:
825 0
                raise ValueError("Could not find node '" + name + "'")
826 1
            nodes[name] = found_node
827 1
        return nodes
828

829 1
    def create_columns(self):
830
        """Build the columns associated with this section."""
831 1
        columns = []
832 1
        for dust_node in self._dust_nodes:
833 1
            if isinstance(dust_node._columns, list):
834 1
                columns.extend(dust_node._columns)
835
            else:
836 0
                columns.append(dust_node._columns)
837 1
        self._columns = columns
838

839 1
    @property
840
    def columns(self):
841
        """Return the list of columns associated with this section."""
842 1
        return self._columns
843

844 1
    def values(self):
845
        """Return the list of data values associated with this section,
846
        i.e. the data corresponding to a single row in the results table."""
847 1
        values = []
848 1
        for dust_node in self._dust_nodes:
849 1
            if isinstance(dust_node._value, list):
850 1
                values.extend(dust_node._value)
851
            else:
852 1
                values.append(dust_node._value)
853 1
        return values
854

855 1
    def __str__(self):
856
        """Return a string representation of the section."""
857 0
        string = "\n\t\t"
858 0
        for dust_node in self._dust_nodes:
859 0
            if len(string) > 6:
860 0
                string += ",\n\t\t"
861 0
            string += dust_node.__str__()
862 0
        return string
863

864

865 1
class LocationSection(BaseResultSection):
866

867
    """
868
    The location section of the DustResults object.
869
    """
870

871 1
    def __init__(self, xml_root):
872
        """
873
        Parameters
874
        ----------
875
        xml_root : `xml.etree.ElementTree`
876
            the xml tree where the data for this section resides
877
        """
878 1
        location_node = xml_root.find(INPUT)
879 1
        names = [OBJ_NAME, REG_SIZE]
880 1
        xml_nodes = self.node_dict(names, location_node)
881

882
        # Create the section's DustNodes
883 1
        self._dust_nodes = [CoordNode(
884
            xml_nodes[OBJ_NAME], col_names=["RA", "Dec", "coord sys"]),
885
            NumberNode(xml_nodes[REG_SIZE], REG_SIZE, u.deg)]
886

887 1
        self.create_columns()
888

889 1
    def __str__(self):
890
        """Return a string representation of the section."""
891 0
        base_string = BaseResultSection.__str__(self)
892 0
        string = "[LocationSection: " + base_string + "]"
893 0
        return string
894

895

896 1
class StatsSection(BaseResultSection):
897

898
    """
899
    The statistics subsection of one of an extinction, emission, or temperature
900
    section.
901
    """
902

903 1
    def __init__(self, xml_root, col_prefix, suffix=""):
904
        """
905
        Parameters
906
        ----------
907
        xml_root : `xml.etree.ElementTree`
908
            The xml tree containing the data for this section
909
        col_prefix : str
910
            the prefix to use in column names for this section
911
        suffix : str
912
            the suffix that appears in the node names (e.g. 'SandF' or 'SDF')
913
        """
914

915 1
        names = [
916
            REF_PIXEL_VALUE + suffix,
917
            REF_COORDINATE,
918
            MEAN_VALUE + suffix,
919
            STD + suffix,
920
            MAX_VALUE + suffix,
921
            MIN_VALUE + suffix]
922 1
        xml_nodes = self.node_dict(names, xml_root)
923

924
        # Create the DustNodes
925 1
        self._dust_nodes = [
926
            NumberNode(xml_nodes[REF_PIXEL_VALUE + suffix],
927
                       col_prefix + " ref"),
928
            CoordNode(xml_nodes[REF_COORDINATE],
929
                      col_names=[col_prefix + " ref RA",
930
                                 col_prefix + " ref Dec",
931
                                 col_prefix + " ref coord sys"]),
932
            NumberNode(xml_nodes[MEAN_VALUE + suffix], col_prefix + " mean"),
933
            NumberNode(xml_nodes[STD + suffix], col_prefix + " std"),
934
            NumberNode(xml_nodes[MAX_VALUE + suffix], col_prefix + " max"),
935
            NumberNode(xml_nodes[MIN_VALUE + suffix], col_prefix + " min")]
936

937 1
        self._units = utils.parse_units(
938
            xml_nodes[REF_PIXEL_VALUE + suffix].text)
939 1
        self.create_columns()
940

941 1
    @property
942
    def units(self):
943
        """Return the units associated with this section."""
944 0
        return self._units
945

946 1
    @property
947
    def dust_nodes(self):
948
        """Return the list of DustNodes in this section."""
949 0
        return self._dust_nodes
950

951 1
    def __str__(self):
952
        """Return a string representation of the section."""
953 0
        base_string = "\n\t\t\t\t"
954 0
        for dust_node in self._dust_nodes:
955 0
            if len(base_string) > 6:
956 0
                base_string += ",\n\t\t\t\t"
957 0
            base_string += dust_node.__str__()
958 0
        string = "\n\t\t\t[StatisticsSection: " + base_string + "]"
959 0
        return string
960

961

962 1
class ExtinctionSection(BaseResultSection):
963

964
    """
965
    The extinction (reddening) section in a DustResults object.
966
    """
967

968 1
    def __init__(self, xml_root):
969
        """
970
        Parameters
971
        ----------
972
        xml_root : `xml.etree.ElementTree`
973
            The xml tree containing the data for this section
974
        """
975
        # Find the section's xml nodes
976 1
        names = [DESC, DATA_IMAGE, DATA_TABLE, STATISTICS]
977 1
        xml_nodes = self.node_dict(names, xml_root)
978

979
        # Build the DustNodes
980 1
        self._dust_nodes = [StringNode(xml_nodes[DESC], "ext desc", 100),
981
                            StringNode(xml_nodes[DATA_IMAGE],
982
                                       "ext image", 255),
983
                            StringNode(xml_nodes[DATA_TABLE],
984
                                       "ext table", 255)]
985

986
        # Create statistics subsections
987 1
        self._stats_sandf = StatsSection(xml_nodes[STATISTICS],
988
                                         "ext SandF", "SandF")
989 1
        self._stats_sfd = StatsSection(xml_nodes[STATISTICS], "ext SFD", "SFD")
990

991 1
        self.create_columns()
992

993 1
    def create_columns(self):
994
        """Build the columns associated with this section."""
995 1
        BaseResultSection.create_columns(self)
996 1
        self._columns.extend(self._stats_sandf.columns)
997 1
        self._columns.extend(self._stats_sfd.columns)
998

999 1
    @property
1000
    def table_url(self):
1001
        """Return the url where the extinction detail table can be found."""
1002 1
        table_url = self._dust_nodes[2]._value
1003 1
        return table_url
1004

1005 1
    @property
1006
    def image_url(self):
1007
        """Return the url of the FITS image associated with this section."""
1008 1
        return self._dust_nodes[1]._value
1009

1010 1
    def values(self):
1011
        """Return the data values associated with this section, i.e. the
1012
        list of values corresponding to a single row in the results
1013
        table."""
1014 1
        ext_values = BaseResultSection.values(self)
1015 1
        ext_values.extend(self._stats_sandf.values())
1016 1
        ext_values.extend(self._stats_sfd.values())
1017 1
        return ext_values
1018

1019 1
    def __str__(self):
1020
        """Return a string representation of the section."""
1021 0
        base_string = BaseResultSection.__str__(self)
1022 0
        string = ("[ExtinctionSection: " + base_string +
1023
                  self._stats_sandf.__str__() +
1024
                  self._stats_sfd.__str__() + "]")
1025

1026 0
        return string
1027

1028

1029 1
class EmissionSection(BaseResultSection):
1030

1031
    """
1032
    The emission section in a DustResults object.
1033
    """
1034

1035 1
    def __init__(self, xml_root):
1036
        """
1037
        Parameters
1038
        ----------
1039
        xml_root : `xml.etree.ElementTree`
1040
            The xml tree containing the data for this section
1041
        """
1042 1
        names = [DESC, DATA_IMAGE, STATISTICS]
1043 1
        xml_nodes = self.node_dict(names, xml_root)
1044

1045
        # Create the DustNodes
1046 1
        self._dust_nodes = [StringNode(xml_nodes[DESC], "em desc", 100),
1047
                            StringNode(xml_nodes[DATA_IMAGE], "em image", 255)]
1048

1049
        # Create the statistics subsection
1050 1
        self._stats = StatsSection(xml_nodes[STATISTICS], "em")
1051

1052 1
        self.create_columns()
1053

1054 1
    def create_columns(self):
1055
        """Build the columns associated with this section."""
1056 1
        BaseResultSection.create_columns(self)
1057 1
        self._columns.extend(self._stats.columns)
1058

1059 1
    def values(self):
1060
        """Return the data values associated with this section, i.e. the
1061
        list of values corresponding to a single row in the results table."""
1062 1
        values = BaseResultSection.values(self)
1063 1
        values.extend(self._stats.values())
1064 1
        return values
1065

1066 1
    @property
1067
    def image_url(self):
1068
        """Return the url of the FITS image associated with this section."""
1069 1
        return self._dust_nodes[1]._value
1070

1071 1
    def __str__(self):
1072
        """Return a string representation of the section."""
1073 0
        base_string = BaseResultSection.__str__(self)
1074 0
        string = "[EmissionSection: " + \
1075
            base_string + self._stats.__str__() + "]"
1076 0
        return string
1077

1078

1079 1
IrsaDust = IrsaDustClass()
1080

1081

1082 1
class TemperatureSection(BaseResultSection):
1083

1084
    """
1085
    The temperature section in a DustResults object.
1086
    """
1087

1088 1
    def __init__(self, xml_root):
1089
        """
1090
        Parameters
1091
        ----------
1092
        xml_root : `xml.etree.ElementTree`
1093
            The xml tree containing the data for this section
1094
        """
1095 1
        names = [DESC, DATA_IMAGE, STATISTICS]
1096 1
        xml_nodes = self.node_dict(names, xml_root)
1097

1098
        # Create the DustNodes
1099 1
        self._dust_nodes = [StringNode(xml_nodes[DESC], "temp desc", 100),
1100
                            StringNode(xml_nodes[DATA_IMAGE],
1101
                                       "temp image", 255)]
1102

1103
        # Create the statistics subsection
1104 1
        self._stats = StatsSection(xml_nodes[STATISTICS], "temp")
1105

1106 1
        self.create_columns()
1107

1108 1
    def create_columns(self):
1109
        """Build the columns associated with this section."""
1110 1
        BaseResultSection.create_columns(self)
1111 1
        self._columns.extend(self._stats.columns)
1112

1113 1
    def values(self):
1114
        """Return the data values associated with this section, i.e. the
1115
        list of values corresponding to a single row in the results
1116
        table."""
1117 1
        values = BaseResultSection.values(self)
1118 1
        values.extend(self._stats.values())
1119 1
        return values
1120

1121 1
    @property
1122
    def image_url(self):
1123
        """Return the url of the FITS image associated with this section."""
1124 1
        return self._dust_nodes[1]._value
1125

1126 1
    def __str__(self):
1127
        """Return a string representation of the section."""
1128 0
        base_string = BaseResultSection.__str__(self)
1129 0
        string = "[TemperatureSection: " + \
1130
            base_string + self._stats.__str__() + "]"
1131 0
        return string

Read our documentation on viewing source code .

Loading