@@ -66,6 +66,8 @@ def query_crossid_async(self, coordinates, *, radius=5. * u.arcsec, timeout=TIME
6666
6767 This query returns the nearest `primary object`_.
6868
69+ Note that there is a server-side limit of 3 arcmin on ``radius``.
70+
6971 .. _`primary object`: https://www.sdss.org/dr17/help/glossary/#surveyprimary
7072
7173 Parameters
@@ -124,6 +126,14 @@ def query_crossid_async(self, coordinates, *, radius=5. * u.arcsec, timeout=TIME
124126 cache : bool, optional
125127 If ``True`` use the request caching mechanism.
126128
129+ Raises
130+ ------
131+ TypeError
132+ If the ``radius`` keyword could not be parsed as an angle.
133+ ValueError
134+ If the ``radius`` exceeds 3 arcmin, or if the sizes of
135+ ``coordinates`` and ``obj_names`` do not match.
136+
127137 Returns
128138 -------
129139 result : `~astropy.table.Table`
@@ -193,16 +203,31 @@ def query_crossid_async(self, coordinates, *, radius=5. * u.arcsec, timeout=TIME
193203 timeout = timeout , cache = cache )
194204 return response
195205
196- def query_region_async (self , coordinates , * , radius = 2. * u .arcsec , timeout = TIMEOUT ,
206+ def query_region_async (self , coordinates , * , radius = None ,
207+ width = None , height = None , timeout = TIMEOUT ,
197208 fields = None , photoobj_fields = None , specobj_fields = None , obj_names = None ,
198209 spectro = False , field_help = False , get_query_payload = False ,
199210 data_release = conf .default_release , cache = True ):
200- """
201- Used to query a circular region (a "cone search") around given coordinates.
202-
203- This function is equivalent to the object cross-ID (`query_crossid`),
204- with slightly different parameters. It returns all objects within the
205- search radius; this could potentially include duplicate observations
211+ r"""
212+ Used to query a region around given coordinates. Either ``radius`` or
213+ ``width`` must be specified.
214+
215+ When called with keyword ``radius``, a radial or "cone" search is
216+ performed, centered on each of the given coordinates. In this mode, internally,
217+ this function is equivalent to the object cross-ID (`query_crossid`),
218+ with slightly different parameters. Note that in this mode there is a server-side
219+ limit of 3 arcmin on ``radius``.
220+
221+ When called with keyword ``width``, and optionally a different ``height``,
222+ a rectangular search is performed, centered on each of the given
223+ coordinates. In this mode, internally, this function is equivalent to
224+ a general SQL query (`query_sql`). The shape of the rectangle is
225+ not corrected for declination (*i.e.* no :math:`\cos \delta` correction);
226+ conceptually, this means that the rectangle will become increasingly
227+ trapezoidal-shaped at high declination.
228+
229+ In both radial and rectangular modes, this function returns all objects
230+ within the search area; this could potentially include duplicate observations
206231 of the same object.
207232
208233 Parameters
@@ -221,8 +246,17 @@ def query_region_async(self, coordinates, *, radius=2. * u.arcsec, timeout=TIMEO
221246 radius : str or `~astropy.units.Quantity` object, optional
222247 The string must be parsable by `~astropy.coordinates.Angle`. The
223248 appropriate `~astropy.units.Quantity` object from
224- `astropy.units` may also be used. Defaults to 2 arcsec.
249+ `astropy.units` may also be used.
225250 The maximum allowed value is 3 arcmin.
251+ width : str or `~astropy.units.Quantity` object, optional
252+ The string must be parsable by `~astropy.coordinates.Angle`. The
253+ appropriate `~astropy.units.Quantity` object from
254+ `astropy.units` may also be used.
255+ height : str or `~astropy.units.Quantity` object, optional
256+ The string must be parsable by `~astropy.coordinates.Angle`. The
257+ appropriate `~astropy.units.Quantity` object from
258+ `astropy.units` may also be used. If not specified, it will be
259+ set to the same value as ``width``.
226260 timeout : float, optional
227261 Time limit (in seconds) for establishing successful connection with
228262 remote server. Defaults to `SDSSClass.TIMEOUT`.
@@ -256,6 +290,16 @@ def query_region_async(self, coordinates, *, radius=2. * u.arcsec, timeout=TIMEO
256290 cache : bool, optional
257291 If ``True`` use the request caching mechanism.
258292
293+ Raises
294+ ------
295+ TypeError
296+ If the ``radius``, ``width`` or ``height`` keywords could not be parsed as an angle.
297+ ValueError
298+ If both ``radius`` and ``width`` are set (or neither),
299+ or if the ``radius`` exceeds 3 arcmin,
300+ or if the sizes of ``coordinates`` and ``obj_names`` do not match,
301+ or if the units of ``width`` or ``height`` could not be parsed.
302+
259303 Examples
260304 --------
261305 >>> from astroquery.sdss import SDSS
@@ -277,16 +321,69 @@ def query_region_async(self, coordinates, *, radius=2. * u.arcsec, timeout=TIMEO
277321 The result of the query as a `~astropy.table.Table` object.
278322
279323 """
280- request_payload , files = self .query_crossid_async (coordinates = coordinates ,
281- radius = radius , fields = fields ,
282- photoobj_fields = photoobj_fields ,
283- specobj_fields = specobj_fields ,
284- obj_names = obj_names ,
285- spectro = spectro ,
286- region = True ,
287- field_help = field_help ,
288- get_query_payload = True ,
289- data_release = data_release )
324+ # Allow field_help requests to pass without requiring a radius or width.
325+ if field_help and radius is None and width is None :
326+ radius = 2.0 * u .arcsec
327+
328+ if radius is None and width is None :
329+ raise ValueError ("Either radius or width must be specified!" )
330+ if radius is not None and width is not None :
331+ raise ValueError ("Either radius or width must be specified, not both!" )
332+
333+ if radius is not None :
334+ request_payload , files = self .query_crossid_async (coordinates = coordinates ,
335+ radius = radius , fields = fields ,
336+ photoobj_fields = photoobj_fields ,
337+ specobj_fields = specobj_fields ,
338+ obj_names = obj_names ,
339+ spectro = spectro ,
340+ region = True ,
341+ field_help = field_help ,
342+ get_query_payload = True ,
343+ data_release = data_release )
344+
345+ if width is not None :
346+ width = u .Quantity (width , u .degree ).value
347+ if height is None :
348+ height = width
349+ else :
350+ height = u .Quantity (height , u .degree ).value
351+
352+ dummy_payload = self ._args_to_payload (coordinates = coordinates ,
353+ fields = fields ,
354+ spectro = spectro , region = True ,
355+ photoobj_fields = photoobj_fields ,
356+ specobj_fields = specobj_fields , field_help = field_help ,
357+ data_release = data_release )
358+
359+ sql_query = dummy_payload ['uquery' ].replace ('#upload u JOIN #x x ON x.up_id = u.up_id JOIN ' , '' )
360+
361+ if 'SpecObjAll' in dummy_payload ['uquery' ]:
362+ sql_query = sql_query .replace ('ON p.objID = x.objID ' , '' ).replace (' ORDER BY x.up_id' , '' )
363+ else :
364+ sql_query = sql_query .replace (' ON p.objID = x.objID ORDER BY x.up_id' , '' )
365+
366+ if (not isinstance (coordinates , list ) and not isinstance (coordinates , Column )
367+ and not (isinstance (coordinates , commons .CoordClasses ) and not coordinates .isscalar )):
368+ coordinates = [coordinates ]
369+
370+ rectangles = list ()
371+ for target in coordinates :
372+ # Query for a rectangle
373+ target = commons .parse_coordinates (target ).transform_to ('fk5' )
374+ rectangles .append (self ._rectangle_sql (target .ra .degree , target .dec .degree , width , height = height ))
375+
376+ rect = ' OR ' .join (rectangles )
377+
378+ # self._args_to_payload only returns a WHERE if e.g. plate, mjd, fiber
379+ # are set, which will not happen in this function.
380+ sql_query += f' WHERE ({ rect } )'
381+
382+ return self .query_sql_async (sql_query , timeout = timeout ,
383+ data_release = data_release ,
384+ cache = cache ,
385+ field_help = field_help ,
386+ get_query_payload = get_query_payload )
290387
291388 if get_query_payload or field_help :
292389 return request_payload
@@ -510,7 +607,7 @@ class = 'galaxy' \
510607 if data_release > 11 :
511608 request_payload ['searchtool' ] = 'SQL'
512609
513- if kwargs .get ('get_query_payload' ):
610+ if kwargs .get ('get_query_payload' ) or kwargs . get ( 'field_help' ) :
514611 return request_payload
515612
516613 url = self ._get_query_url (data_release )
@@ -1163,5 +1260,58 @@ def _get_crossid_url(self, data_release):
11631260 self ._last_url = url
11641261 return url
11651262
1263+ def _rectangle_sql (self , ra , dec , width , height = None , cosdec = False ):
1264+ """
1265+ Generate SQL for a rectangular query centered on ``ra``, ``dec``.
1266+
1267+ This assumes that RA is defined on the range ``[0, 360)``, and Dec on
1268+ ``[-90, 90]``.
1269+
1270+ Parameters
1271+ ----------
1272+ ra : float
1273+ Right Ascension in degrees.
1274+ dec : float
1275+ Declination in degrees.
1276+ width : float
1277+ Width of rectangle in degrees.
1278+ height : float, optional
1279+ Height of rectangle in degrees. If not specified, ``width`` is used.
1280+ cosdec : bool, optional
1281+ If ``True`` apply ``cos(dec)`` correction to the rectangle.
1282+ Otherwise, rectangles become increasingly triangle-like
1283+ near the poles.
1284+
1285+ Returns
1286+ -------
1287+ :class:`str`
1288+ A string defining the rectangle in SQL notation.
1289+ """
1290+ if height is None :
1291+ height = width
1292+ dr = width / 2.0
1293+ dd = height / 2.0
1294+ d0 = dec - dd
1295+ if d0 < - 90 :
1296+ d0 = - 90.0
1297+ d1 = dec + dd
1298+ if d1 > 90.0 :
1299+ d1 = 90.0
1300+ ra_wrap = False
1301+ r0 = ra - dr
1302+ if r0 < 0 :
1303+ ra_wrap = True
1304+ r0 += 360.0
1305+ r1 = ra + dr
1306+ if r1 > 360.0 :
1307+ ra_wrap = True
1308+ r1 -= 360.0
1309+ # BETWEEN is inclusive, so it is equivalent to the <=, >= operators.
1310+ if ra_wrap :
1311+ sql = f"(((p.ra >= { r0 :g} ) OR (p.ra <= { r1 :g} ))"
1312+ else :
1313+ sql = f"((p.ra BETWEEN { r0 :g} AND { r1 :g} )"
1314+ return sql + f" AND (p.dec BETWEEN { d0 :g} AND { d1 :g} ))"
1315+
11661316
11671317SDSS = SDSSClass ()
0 commit comments