1 """
2 Support code for the Simple Image Access Protocol.
3 """
4
5
6
7
8
9
10
11 import math
12 import re
13 import urllib
14
15 from gavo import base
16 from gavo import svcs
17 from gavo import utils
18 from gavo.base import coords
19 from gavo.protocols import products
20 from gavo.utils import fitstools
21 from gavo.utils import pgsphere
22
23 MS = base.makeStruct
24
25
26
28 """parses a SIAPv2 geometry spec to a pgsphere object.
29
30 Parse errors raise validation errors for fieldName.
31 """
32 mat = re.match("(CIRCLE|RANGE|POLYGON) (.*)", aString)
33 if not mat:
34 raise base.ValidationError("Invalid SIAPv2 geometry: '%s'"
35 " (expected a SIAPv2 shape name)"%utils.makeEllipsis(aString, 20),
36 fieldName)
37
38 geoName = mat.group(1)
39 try:
40 args = [float(s) for s in mat.group(2).split()]
41 except ValueError:
42 raise base.ValidationError("Invalid SIAPv2 coordinates: '%s'"
43 " (bad floating point literal '%s')"%(
44 utils.makeEllipsis(mat.group(2), 20), s),
45 fieldName)
46
47 if geoName=="CIRCLE":
48 if len(args)!=3:
49 raise base.ValidationError("Invalid SIAPv2 CIRCLE: '%s'"
50 " (need exactly three numbers)"%(
51 utils.makeEllipsis(aString, 20)),
52 fieldName)
53 return pgsphere.SCircle(pgsphere.SPoint.fromDegrees(args[0], args[1]),
54 args[2]*utils.DEG)
55
56 elif geoName=="RANGE":
57
58
59
60 if len(args)!=4:
61 raise base.ValidationError("Invalid SIAPv2 RANGE: '%s'"
62 " (need exactly four numbers)"%(
63 utils.makeEllipsis(aString, 20)),
64 fieldName)
65 if args[0]>args[1] or args[2]>args[3]:
66 raise base.ValidationError("Invalid SIAPv2 RANGE: '%s'"
67 " (lower limits must be smaller than upper limits)"%(
68 utils.makeEllipsis(aString, 20)),
69 fieldName)
70 return pgsphere.SBox(
71 pgsphere.SPoint.fromDegrees(args[0], args[2]),
72 pgsphere.SPoint.fromDegrees(args[1], args[3]))
73
74 elif geoName=="POLYGON":
75 if len(args)<6 or len(args)%2:
76 raise base.ValidationError("Invalid SIAPv2 POLYGON: '%s'"
77 " (need more than three coordinate *pairs*)"%(
78 utils.makeEllipsis(mat.group(2), 20)),
79 fieldName)
80 return pgsphere.SPoly([
81 pgsphere.SPoint.fromDegrees(*pair)
82 for pair in utils.iterConsecutivePairs(args)])
83
84 else:
85 assert False
86
87
88
89
90
91 _PGS_OPERATORS = {
92 "COVERS": "coverage ~ %%(%s)s",
93 "ENCLOSED": "%%(%s)s ~ coverage",
94 "CENTER": None,
95 "OVERLAPS": "%%(%s)s && coverage",
96 }
97
98 -def getPGSQuery(intersect, ra, dec, sizes, prefix, sqlPars):
99 """returns SQL for a SIAP query on pgsSIAP tables.
100 """
101 if intersect=='CENTER':
102 return "%%(%s)s @ coverage"%(base.getSQLKey(
103 prefix+"center", pgsphere.SPoint.fromDegrees(ra, dec), sqlPars))
104
105 expr = _PGS_OPERATORS[intersect]
106 try:
107 targetBox = pgsphere.SBox.fromSIAPPars(ra, dec, sizes[0], sizes[1])
108 return expr%base.getSQLKey(prefix+"area", targetBox, sqlPars)
109 except pgsphere.TwoSBoxes as ex:
110
111 return "( %s OR %s )"%(
112 expr%base.getSQLKey(prefix+"area1", ex.box1, sqlPars),
113 expr%base.getSQLKey(prefix+"area2", ex.box2, sqlPars))
114
115
116
117
119 """tries to infer RA and DEC from posStr.
120
121 In contrast to base.parseCooPair, we are quite strict here and just
122 try to cope with some bad clients that leave out the comma.
123 """
124 try:
125 ra, dec = map(float, posStr.split(","))
126 except ValueError:
127 if '+' in posStr:
128 ra, dec = map(float, posStr.split("+"))
129 elif '-' in posStr:
130 ra, dec = map(float, posStr.split("-"))
131 else:
132 raise ValueError("No pos")
133 return ra, dec
134
135
137 """returns a query making function for SIAP appropriate for queriedTable.
138
139 This used to have a function when we had different backends for SIAP.
140 Curently, we no longer have that, so this always returns getPGSQuery.
141 """
142 return getPGSQuery
143
144
145 -def getQuery(queriedTable, parameters, sqlPars, prefix="sia"):
146 """returns an SQL fragment for a SIAP query for bboxes.
147
148 The SQL is returned as a WHERE-fragment in a string. The parameters
149 are added in the sqlPars dictionary.
150
151 parameters is a dictionary that maps the SIAP keywords to the
152 values in the query. Parameters not defined by SIAP are ignored.
153 """
154 posStr = urllib.unquote(parameters["POS"])
155 try:
156 ra, dec = dissectPositions(posStr)
157 except (ValueError, TypeError):
158 raise base.ui.logOldExc(base.ValidationError(
159 "%s is not a RA,DEC pair."%posStr, "POS", posStr))
160 try:
161 sizes = map(float, parameters["SIZE"].split(","))
162 except ValueError:
163 raise base.ui.logOldExc(base.ValidationError("Size specification"
164 " has to be <degs> or <degs>,<degs>", "SIZE", parameters["SIZE"]))
165 if len(sizes)==1:
166 sizes = sizes*2
167 intersect = parameters.get("INTERSECT", "OVERLAPS")
168 query = _getQueryMaker(queriedTable)(
169 intersect, ra, dec, sizes, prefix, sqlPars)
170
171 sqlPars["_ra"], sqlPars["_dec"] = ra, dec
172 sqlPars["_sra"], sqlPars["_sdec"] = sizes
173 return query
174
175
177 """A core doing SIAP plus cutouts.
178
179 It has, by default, an additional column specifying the desired size of
180 the image to be retrieved. Based on this, the cutout core will tweak
181 its output table such that references to cutout images will be retrieved.
182
183 The actual process of cutting out is performed by the product core and
184 renderer.
185 """
186 name_ = "siapCutoutCore"
187
188
189
190 copiedCols = ["centerAlpha", "centerDelta", "imageTitle", "instId",
191 "dateObs", "nAxes", "pixelSize", "pixelScale", "mime",
192 "refFrame", "wcs_equinox", "wcs_projection", "wcs_refPixel",
193 "wcs_refValues", "wcs_cdmatrix", "bandpassId", "bandpassUnit",
194 "bandpassHi", "bandpassLo", "pixflags"]
195
205
206 - def _fixRecord(self, record, centerAlpha, centerDelta, sizeAlpha, sizeDelta):
207 """inserts estimates for WCS values into a cutout record.
208 """
209 origWCS = coords.getWCS({
210 "CUNIT1": "deg", "CUNIT2": "deg", "CTYPE1": "RA---TAN",
211 "CTYPE2": "DEC--TAN",
212 "CRVAL1": record["wcs_refValues"][0],
213 "CRVAL2": record["wcs_refValues"][1],
214 "CRPIX1": record["wcs_refPixel"][0],
215 "CRPIX2": record["wcs_refPixel"][1],
216 "CD1_1": record["wcs_cdmatrix"][0],
217 "CD1_2": record["wcs_cdmatrix"][1],
218 "CD2_1": record["wcs_cdmatrix"][2],
219 "CD2_2": record["wcs_cdmatrix"][3],
220 "LONPOLE": 180.,
221 "NAXIS": record["nAxes"],
222 "NAXIS1": record["pixelSize"][0],
223 "NAXIS2": record["pixelSize"][1],
224 })
225 limits = coords.getPixelLimits(
226 ((centerAlpha-sizeAlpha/2, centerDelta-sizeDelta/2),
227 (centerAlpha+sizeAlpha/2, centerDelta+sizeDelta/2)),
228 origWCS)
229 _, newHeader = fitstools.cutoutHeader(origWCS._dachs_header, *limits)
230
231 if newHeader["NAXIS1"]<3 or newHeader["NAXIS2"]<3:
232
233
234 raise utils.SkipThis("Too marginal a match")
235
236 record["wcs_refPixel"] = [newHeader["CRPIX1"], newHeader["CRPIX2"]]
237 record["wcs_refValues"] = [newHeader["CRVAL1"], newHeader["CRVAL2"]]
238 record["pixelSize"] = [newHeader["NAXIS1"], newHeader["NAXIS2"]]
239
240 record["accref"] = products.RAccref(record["accref"], {
241 "ra": centerAlpha, "dec": centerDelta,
242 "sra": sizeAlpha, "sdec": sizeDelta})
243 record["centerAlpha"] = centerAlpha
244 record["centerDelta"] = centerDelta
245
246 newWCS = coords.getWCS(newHeader)
247 corners = newWCS.calcFootprint()
248 record["coverage"] = pgsphere.SPoly.fromDALI([
249 corners[0][0], corners[0][1],
250 corners[1][0], corners[1][1],
251 corners[2][0], corners[2][1],
252 corners[3][0], corners[3][1]])
253
254 bytesPerPixel = 2
255 record["accsize"] = min(record["accsize"],
256 int(bytesPerPixel*newHeader["NAXIS1"]*newHeader["NAXIS2"]))
257
258 - def run(self, service, inputData, queryMeta):
259 res = svcs.DBCore.run(self, service, inputData, queryMeta)
260 sqlPars = queryMeta["sqlQueryPars"]
261 try:
262 sra = sdec = float(queryMeta.ctxArgs["cutoutSize"])
263 except (KeyError, ValueError):
264 try:
265 sra, sdec = sqlPars["_sra"], sqlPars["_sdec"]
266 except KeyError:
267 sra, sdec = 0.5, 0.5
268
269 if "_dec" in sqlPars:
270 cosD = math.cos(sqlPars["_dec"]/180*math.pi)
271 if abs(cosD)>1e-5:
272 sra = sra/cosD
273 else:
274 sra = 360
275
276 newRows = []
277 for record in res:
278 try:
279 self._fixRecord(record,
280 sqlPars.get("_ra", record["centerAlpha"]),
281 sqlPars.get("_dec", record["centerDelta"]), sra, sdec)
282 newRows.append(record)
283 except base.SkipThis:
284 pass
285 except ValueError as msg:
286
287
288 base.ui.notifyWarning("Botched WCS (%s) in the record %s"%(
289 msg, record))
290
291 res.rows = newRows
292
293 return res
294
295
297 import doctest, siap
298 doctest.testmod(siap)
299
300
301 if __name__=="__main__":
302 _test()
303