1
|
|
# Licensed under a 3-clause BSD style license - see LICENSE.rst
|
2
|
1
|
import pprint
|
3
|
1
|
from bs4 import BeautifulSoup
|
4
|
1
|
from six.moves.urllib import parse as urlparse
|
5
|
1
|
import six
|
6
|
1
|
from astropy import units as u
|
7
|
|
|
8
|
1
|
from . import conf
|
9
|
1
|
from ..query import BaseQuery
|
10
|
1
|
from ..utils import prepend_docstr_nosections, commons, async_to_sync
|
11
|
|
|
12
|
|
|
13
|
1
|
__doctest_skip__ = [
|
14
|
|
'SkyViewClass.get_images',
|
15
|
|
'SkyViewClass.get_images_async',
|
16
|
|
'SkyViewClass.get_image_list']
|
17
|
|
|
18
|
|
|
19
|
1
|
@async_to_sync
|
20
|
1
|
class SkyViewClass(BaseQuery):
|
21
|
1
|
URL = conf.url
|
22
|
|
|
23
|
1
|
def __init__(self):
|
24
|
1
|
super(SkyViewClass, self).__init__()
|
25
|
1
|
self._default_form_values = None
|
26
|
|
|
27
|
1
|
def _get_default_form_values(self, form):
|
28
|
|
"""
|
29
|
|
Return the already selected values of a given form (a BeautifulSoup
|
30
|
|
form node) as a dict.
|
31
|
|
"""
|
32
|
1
|
res = []
|
33
|
1
|
for elem in form.find_all(['input', 'select']):
|
34
|
|
# ignore the submit and reset buttons
|
35
|
1
|
if elem.get('type') in ['submit', 'reset']:
|
36
|
1
|
continue
|
37
|
|
# check boxes: enabled boxes have the value "on" if not specified
|
38
|
|
# otherwise. Found out by debugging, perhaps not documented.
|
39
|
1
|
if (elem.get('type') == 'checkbox' and
|
40
|
|
elem.get('checked') in ["", "checked"]):
|
41
|
1
|
value = elem.get('value', 'on')
|
42
|
1
|
res.append((elem.get('name'), value))
|
43
|
|
# radio buttons and simple input fields
|
44
|
1
|
if elem.get('type') == 'radio' and\
|
45
|
|
elem.get('checked') in ["", "checked"] or\
|
46
|
|
elem.get('type') in [None, 'text']:
|
47
|
1
|
res.append((elem.get('name'), elem.get('value')))
|
48
|
|
# dropdown menu, multi-section possible
|
49
|
1
|
if elem.name == 'select':
|
50
|
1
|
for option in elem.find_all('option'):
|
51
|
1
|
if option.get('selected') == '':
|
52
|
1
|
value = option.get('value', option.text.strip())
|
53
|
1
|
res.append((elem.get('name'), value))
|
54
|
1
|
return {k: v
|
55
|
|
for (k, v) in res
|
56
|
|
if v not in [None, u'None', u'null'] and v
|
57
|
|
}
|
58
|
|
|
59
|
1
|
def _generate_payload(self, input=None):
|
60
|
|
"""
|
61
|
|
Fill out the form of the SkyView site and submit it with the
|
62
|
|
values given in ``input`` (a dictionary where the keys are the form
|
63
|
|
element's names and the values are their respective values).
|
64
|
|
"""
|
65
|
1
|
if input is None:
|
66
|
0
|
input = {}
|
67
|
1
|
form_response = self._request('GET', self.URL)
|
68
|
1
|
bs = BeautifulSoup(form_response.content, "html.parser")
|
69
|
1
|
form = bs.find('form')
|
70
|
|
# cache the default values to save HTTP traffic
|
71
|
1
|
if self._default_form_values is None:
|
72
|
1
|
self._default_form_values = self._get_default_form_values(form)
|
73
|
|
# only overwrite payload's values if the `input` value is not None
|
74
|
|
# to avoid overwriting of the form's default values
|
75
|
1
|
payload = self._default_form_values.copy()
|
76
|
1
|
for k, v in six.iteritems(input):
|
77
|
1
|
if v is not None:
|
78
|
1
|
payload[k] = v
|
79
|
1
|
url = urlparse.urljoin(self.URL, form.get('action'))
|
80
|
1
|
return url, payload
|
81
|
|
|
82
|
1
|
def _submit_form(self, input=None, cache=True):
|
83
|
1
|
url, payload = self._generate_payload(input=input)
|
84
|
1
|
response = self._request('GET', url, params=payload, cache=cache)
|
85
|
1
|
return response
|
86
|
|
|
87
|
1
|
def get_images(self, position, survey, coordinates=None, projection=None,
|
88
|
|
pixels=None, scaling=None, sampler=None, resolver=None,
|
89
|
|
deedger=None, lut=None, grid=None, gridlabels=None,
|
90
|
|
radius=None, height=None, width=None, cache=True,
|
91
|
|
show_progress=True):
|
92
|
|
"""
|
93
|
|
Query the SkyView service, download the FITS file that will be
|
94
|
|
found and return a generator over the local paths to the
|
95
|
|
downloaded FITS files.
|
96
|
|
|
97
|
|
Note that the files will be downloaded when the generator will be
|
98
|
|
exhausted, i.e. just calling this method alone without iterating
|
99
|
|
over the result won't issue a connection to the SkyView server.
|
100
|
|
|
101
|
|
Parameters
|
102
|
|
----------
|
103
|
|
position : str
|
104
|
|
Determines the center of the field to be retrieved. Both
|
105
|
|
coordinates (also equatorial ones) and object names are
|
106
|
|
supported. Object names are converted to coordinates via the
|
107
|
|
SIMBAD or NED name resolver. See the reference for more info
|
108
|
|
on the supported syntax for coordinates.
|
109
|
|
survey : str or list of str
|
110
|
|
Select data from one or more surveys. The number of surveys
|
111
|
|
determines the number of resulting file downloads. Passing a
|
112
|
|
list with just one string has the same effect as passing this
|
113
|
|
string directly.
|
114
|
|
coordinates : str
|
115
|
|
Choose among common equatorial, galactic and ecliptic
|
116
|
|
coordinate systems (``"J2000"``, ``"B1950"``, ``"Galactic"``,
|
117
|
|
``"E2000"``, ``"ICRS"``) or pass a custom string.
|
118
|
|
projection : str
|
119
|
|
Choose among the map projections (the value in parentheses
|
120
|
|
denotes the string to be passed):
|
121
|
|
|
122
|
|
Gnomonic (Tan), default value
|
123
|
|
good for small regions
|
124
|
|
Rectangular (Car)
|
125
|
|
simplest projection
|
126
|
|
Aitoff (Ait)
|
127
|
|
Hammer-Aitoff, equal area projection good for all sky maps
|
128
|
|
Orthographic (Sin)
|
129
|
|
Projection often used in interferometry
|
130
|
|
Zenith Equal Area (Zea)
|
131
|
|
equal area, azimuthal projection
|
132
|
|
COBE Spherical Cube (Csc)
|
133
|
|
Used in COBE data
|
134
|
|
Arc (Arc)
|
135
|
|
Similar to Zea but not equal-area
|
136
|
|
pixels : str
|
137
|
|
Selects the pixel dimensions of the image to be produced. A
|
138
|
|
scalar value or a pair of values separated by comma may be
|
139
|
|
given. If the value is a scalar the number of width and height
|
140
|
|
of the image will be the same. By default a 300x300 image is
|
141
|
|
produced.
|
142
|
|
scaling : str
|
143
|
|
Selects the transformation between pixel intensity and
|
144
|
|
intensity on the displayed image. The supported values are:
|
145
|
|
``"Log"``, ``"Sqrt"``, ``"Linear"``, ``"HistEq"``,
|
146
|
|
``"LogLog"``.
|
147
|
|
sampler : str
|
148
|
|
The sampling algorithm determines how the data requested will
|
149
|
|
be resampled so that it can be displayed.
|
150
|
|
resolver : str
|
151
|
|
The name resolver allows to choose a name resolver to use when
|
152
|
|
looking up a name which was passed in the ``position`` parameter
|
153
|
|
(as opposed to a numeric coordinate value). The default choice
|
154
|
|
is to call the SIMBAD name resolver first and then the NED
|
155
|
|
name resolver if the SIMBAD search fails.
|
156
|
|
deedger : str
|
157
|
|
When multiple input images with different backgrounds are
|
158
|
|
resampled the edges between the images may be apparent because
|
159
|
|
of the background shift. This parameter makes it possible to
|
160
|
|
attempt to minimize these edges by applying a de-edging
|
161
|
|
algorithm. The user can elect to choose the default given for
|
162
|
|
that survey, to turn de-edging off, or to use the default
|
163
|
|
de-edging algorithm. The supported values are: ``"_skip_"`` to
|
164
|
|
use the survey default, ``"skyview.process.Deedger"`` (for
|
165
|
|
enabling de-edging), and ``"null"`` to disable.
|
166
|
|
lut : str
|
167
|
|
Choose from the color table selections to display the data in
|
168
|
|
false color.
|
169
|
|
grid : bool
|
170
|
|
overlay a coordinate grid on the image if True
|
171
|
|
gridlabels : bool
|
172
|
|
annotate the grid with coordinates positions if True
|
173
|
|
radius : `~astropy.units.Quantity` or None
|
174
|
|
The radius of the specified field. Overrides width and height.
|
175
|
|
width : `~astropy.units.Quantity` or None
|
176
|
|
The width of the specified field. Must be specified
|
177
|
|
with ``height``.
|
178
|
|
height : `~astropy.units.Quantity` or None
|
179
|
|
The height of the specified field. Must be specified
|
180
|
|
with ``width``.
|
181
|
|
|
182
|
|
References
|
183
|
|
----------
|
184
|
|
.. [1] http://skyview.gsfc.nasa.gov/current/help/fields.html
|
185
|
|
|
186
|
|
Examples
|
187
|
|
--------
|
188
|
|
>>> sv = SkyView()
|
189
|
|
>>> paths = sv.get_images(position='Eta Carinae',
|
190
|
|
... survey=['Fermi 5', 'HRI', 'DSS'])
|
191
|
|
>>> for path in paths:
|
192
|
|
... print('\tnew file:', path)
|
193
|
|
|
194
|
|
Returns
|
195
|
|
-------
|
196
|
|
A list of `~astropy.io.fits.HDUList` objects.
|
197
|
|
|
198
|
|
"""
|
199
|
0
|
readable_objects = self.get_images_async(position, survey, coordinates,
|
200
|
|
projection, pixels, scaling,
|
201
|
|
sampler, resolver, deedger,
|
202
|
|
lut, grid, gridlabels,
|
203
|
|
radius=radius, height=height,
|
204
|
|
width=width,
|
205
|
|
cache=cache,
|
206
|
|
show_progress=show_progress)
|
207
|
0
|
return [obj.get_fits() for obj in readable_objects]
|
208
|
|
|
209
|
1
|
@prepend_docstr_nosections(get_images.__doc__)
|
210
|
1
|
def get_images_async(self, position, survey, coordinates=None,
|
211
|
|
projection=None, pixels=None, scaling=None,
|
212
|
|
sampler=None, resolver=None, deedger=None, lut=None,
|
213
|
|
grid=None, gridlabels=None, radius=None, height=None,
|
214
|
|
width=None, cache=True, show_progress=True):
|
215
|
|
"""
|
216
|
|
Returns
|
217
|
|
-------
|
218
|
|
A list of context-managers that yield readable file-like objects
|
219
|
|
"""
|
220
|
0
|
image_urls = self.get_image_list(position, survey, coordinates,
|
221
|
|
projection, pixels, scaling, sampler,
|
222
|
|
resolver, deedger, lut, grid,
|
223
|
|
gridlabels, radius=radius,
|
224
|
|
height=height, width=width,
|
225
|
|
cache=cache)
|
226
|
0
|
return [commons.FileContainer(url, encoding='binary',
|
227
|
|
show_progress=show_progress)
|
228
|
|
for url in image_urls]
|
229
|
|
|
230
|
1
|
@prepend_docstr_nosections(get_images.__doc__, sections=['Returns', 'Examples'])
|
231
|
1
|
def get_image_list(self, position, survey, coordinates=None,
|
232
|
|
projection=None, pixels=None, scaling=None,
|
233
|
|
sampler=None, resolver=None, deedger=None, lut=None,
|
234
|
|
grid=None, gridlabels=None, radius=None, width=None,
|
235
|
|
height=None, cache=True):
|
236
|
|
"""
|
237
|
|
Returns
|
238
|
|
-------
|
239
|
|
list of image urls
|
240
|
|
|
241
|
|
Examples
|
242
|
|
--------
|
243
|
|
>>> SkyView().get_image_list(position='Eta Carinae',
|
244
|
|
... survey=['Fermi 5', 'HRI', 'DSS'])
|
245
|
|
[u'http://skyview.gsfc.nasa.gov/tempspace/fits/skv6183161285798_1.fits',
|
246
|
|
u'http://skyview.gsfc.nasa.gov/tempspace/fits/skv6183161285798_2.fits',
|
247
|
|
u'http://skyview.gsfc.nasa.gov/tempspace/fits/skv6183161285798_3.fits']
|
248
|
|
"""
|
249
|
|
|
250
|
1
|
self._validate_surveys(survey)
|
251
|
|
|
252
|
1
|
if radius is not None:
|
253
|
0
|
size_deg = str(radius.to(u.deg).value)
|
254
|
1
|
elif width and height:
|
255
|
0
|
size_deg = "{0},{1}".format(width.to(u.deg).value,
|
256
|
|
height.to(u.deg).value)
|
257
|
1
|
elif width and height:
|
258
|
0
|
raise ValueError("Must specify width and height if you "
|
259
|
|
"specify either.")
|
260
|
|
else:
|
261
|
1
|
size_deg = None
|
262
|
|
|
263
|
1
|
input = {
|
264
|
|
'Position': parse_coordinates(position),
|
265
|
|
'survey': survey,
|
266
|
|
'Deedger': deedger,
|
267
|
|
'lut': lut,
|
268
|
|
'projection': projection,
|
269
|
|
'gridlabels': '1' if gridlabels else '0',
|
270
|
|
'coordinates': coordinates,
|
271
|
|
'scaling': scaling,
|
272
|
|
'grid': grid,
|
273
|
|
'resolver': resolver,
|
274
|
|
'Sampler': sampler,
|
275
|
|
'imscale': size_deg,
|
276
|
|
'size': size_deg,
|
277
|
|
'pixels': pixels}
|
278
|
1
|
response = self._submit_form(input, cache=cache)
|
279
|
1
|
urls = self._parse_response(response)
|
280
|
1
|
return urls
|
281
|
|
|
282
|
1
|
def _parse_response(self, response):
|
283
|
1
|
bs = BeautifulSoup(response.content, "html.parser")
|
284
|
1
|
urls = []
|
285
|
1
|
for a in bs.find_all('a'):
|
286
|
1
|
if a.text == 'FITS':
|
287
|
1
|
href = a.get('href')
|
288
|
1
|
urls.append(urlparse.urljoin(response.url, href))
|
289
|
1
|
return urls
|
290
|
|
|
291
|
1
|
@property
|
292
|
|
def survey_dict(self):
|
293
|
1
|
if not hasattr(self, '_survey_dict'):
|
294
|
|
|
295
|
1
|
response = self._request('GET', self.URL, cache=False)
|
296
|
1
|
page = BeautifulSoup(response.content, "html.parser")
|
297
|
1
|
surveys = page.findAll('select', {'name': 'survey'})
|
298
|
|
|
299
|
1
|
self._survey_dict = {
|
300
|
|
sel['id']: [x.text for x in sel.findAll('option')]
|
301
|
|
for sel in surveys
|
302
|
|
if 'overlay' not in sel['id']
|
303
|
|
}
|
304
|
|
|
305
|
1
|
return self._survey_dict
|
306
|
|
|
307
|
1
|
@property
|
308
|
|
def _valid_surveys(self):
|
309
|
|
# Return a flat list of all valid surveys
|
310
|
1
|
return [x for v in self.survey_dict.values() for x in v]
|
311
|
|
|
312
|
1
|
def _validate_surveys(self, surveys):
|
313
|
1
|
if not isinstance(surveys, list):
|
314
|
0
|
surveys = [surveys]
|
315
|
|
|
316
|
1
|
for sv in surveys:
|
317
|
1
|
if sv not in self._valid_surveys:
|
318
|
1
|
raise ValueError("Survey is not among the surveys hosted "
|
319
|
|
"at skyview. See list_surveys or "
|
320
|
|
"survey_dict for valid surveys.")
|
321
|
|
|
322
|
1
|
def list_surveys(self):
|
323
|
|
"""
|
324
|
|
Print out a formatted version of the survey dict
|
325
|
|
"""
|
326
|
0
|
pprint.pprint(self.survey_dict)
|
327
|
|
|
328
|
|
|
329
|
1
|
def parse_coordinates(position):
|
330
|
1
|
coord = commons.parse_coordinates(position)
|
331
|
1
|
return coord.fk5.to_string()
|
332
|
|
|
333
|
|
|
334
|
1
|
SkyView = SkyViewClass()
|