7171import ca .nrc .cadc .dali .EnergyConverter ;
7272import ca .nrc .cadc .dali .Interval ;
7373import ca .nrc .cadc .wcs .Transform ;
74+ import ca .nrc .cadc .wcs .WCSKeywords ;
75+ import ca .nrc .cadc .wcs .WCSKeywordsImpl ;
7476import ca .nrc .cadc .wcs .exceptions .NoSuchKeywordException ;
7577import ca .nrc .cadc .wcs .exceptions .WCSLibRuntimeException ;
7678import java .util .Arrays ;
@@ -130,14 +132,22 @@ public long[] getBounds(final Interval<Number> bounds) throws NoSuchKeywordExcep
130132 return null ;
131133 } else {
132134 final int naxis = spectralWCSKeywords .getIntValue (Standard .NAXIS .key ());
133- final Interval <Double > boundsIntervalPixel = getCutoutPixelInterval (bounds , energyAxis , naxis );
134- final Interval <Double > nativePixelsInterval =
135- new Interval <>(0.0D , (double ) spectralWCSKeywords .getIntValue (
136- Standard .NAXISn .n (energyAxis ).key ()));
135+ final String ctype = spectralWCSKeywords .getStringValue (Standard .CTYPEn .n (energyAxis ).key ());
136+ final boolean isVelocity = CoordTypeCode .fromCType (ctype ).isVelocity ();
137+ // Intersect user-requested wavelength (m) with the spectral range covered by the data at pixels 1..N
138+ // so that extended / infinite physical bounds (e.g. -Inf) map to a finite WCS call instead of
139+ // sky2pix endpoints that miss the field of view in pixel space.
140+ final Interval <Number > requestMetres = isVelocity ? bounds : clampWavelengthToSpectralFieldOfView (bounds , energyAxis );
141+ if (requestMetres == null ) {
142+ return null ;
143+ }
144+ final int nchan = spectralWCSKeywords .getIntValue (Standard .NAXISn .n (energyAxis ).key ());
145+ final Interval <Double > boundsIntervalPixel = getCutoutPixelInterval (requestMetres , energyAxis , naxis );
146+ // FITS pixel indices 1..N; clip() in FITSCutout is 1-based to len inclusive.
147+ final Interval <Double > nativePixelsInterval = new Interval <>(1.0D , (double ) nchan );
137148 final Interval <Double > intersectionPixels = getOverlap (nativePixelsInterval , boundsIntervalPixel );
138149
139150 if (intersectionPixels == null ) {
140- LOGGER .warn ("No overlap." );
141151 return null ;
142152 } else {
143153 final double low = intersectionPixels .getLower ();
@@ -149,39 +159,147 @@ public long[] getBounds(final Interval<Number> bounds) throws NoSuchKeywordExcep
149159 clip (maxSpectralLength , (long ) Math .floor (Math .min (low , up ) + 0.5D ),
150160 (long ) Math .ceil (Math .max (low , up ) - 0.5D ));
151161
152- final long [] entireBounds = clippedSpectralBounds == null ? null : new long [naxis * 2 ];
153-
154- if (entireBounds != null ) {
155- for (int i = 0 ; i < entireBounds .length ; i += 2 ) {
156- final int axis = (i + 2 ) / 2 ;
157- if (axis == energyAxis ) {
158- entireBounds [i ] = clippedSpectralBounds [0 ];
159- entireBounds [i + 1 ] = clippedSpectralBounds [1 ];
160- } else {
161- entireBounds [i ] = 1L ;
162- entireBounds [i + 1 ] = (long ) this .fitsHeaderWCSKeywords .getDoubleValue (
163- Standard .NAXISn .n (axis ).key ());
164- }
165- }
166- }
167-
168- return entireBounds ;
162+ final int fillNaxis = clippedSpectralBounds == null ? 0 : naxis ;
163+ return AxisBoundsFiller .fill (fillNaxis , clippedSpectralBounds , energyAxis ,
164+ AxisBoundsFiller .naxisSizes (spectralWCSKeywords , naxis ));
169165 }
170166 }
171167 }
172168
173- private Interval <Double > getOverlap (final Interval <Double > headerWCSInterval , final Interval <Double > cutoutBounds ) {
174- LOGGER .debug ("Checking overlap between header pixels ("
175- + headerWCSInterval .getLower () + ", " + headerWCSInterval .getUpper ()
176- + ") and requested bounds pixels ("
177- + cutoutBounds .getLower () + ", " + cutoutBounds .getUpper () + ")" );
178- if (headerWCSInterval .getLower () > cutoutBounds .getUpper ()
179- || headerWCSInterval .getUpper () < cutoutBounds .getLower ()) {
169+ private Interval <Double > getOverlap (final Interval <Double > a , final Interval <Double > b ) {
170+ final double lo = Math .max (a .getLower (), b .getLower ());
171+ final double hi = Math .min (a .getUpper (), b .getUpper ());
172+ LOGGER .debug ("Pixel interval intersection (" + a .getLower () + ", " + a .getUpper () + ") with ("
173+ + b .getLower () + ", " + b .getUpper () + ") -> (" + lo + ", " + hi + ")" );
174+ if (lo > hi ) {
180175 return null ;
176+ }
177+ return new Interval <>(lo , hi );
178+ }
179+
180+ /**
181+ * Wavelength in metres (barycentric) for channels 1 and nchan, using a linear WCS in the native
182+ * spectral unit (CUNIT) at the reference pixel (same approximation as a simple 1D grid).
183+ * @see <a href="https://github.com/opencadc/caom2/blob/main/caom2-compute/src/main/java/ca/nrc/cadc/caom2/compute/EnergyUtil.java#L496">caom2-compute source (toInterval)</a>
184+ * @return The ordered pair [min, max] in metres.
185+ */
186+ static Interval <Number > spectralWavelengthMetresAtBandEdges (final int energyAxis , final WCSKeywords wcsKeywords )
187+ throws NoSuchKeywordException , WCSLibRuntimeException {
188+ // wcslib translate/pix2sky require NAXIS to match the coordinate array length; use a 1D
189+ // spectral WCS as in caom2-compute WCSWrapper(SpectralWCS, 1).
190+ final WCSKeywords spectralWcs = EnergyCutout .extractSpectralAxis (wcsKeywords , energyAxis );
191+ Transform trans = new Transform (spectralWcs );
192+ final String ctype = spectralWcs .getStringValue (Standard .CTYPEn .n (1 ).key ());
193+ final WCSKeywords kw ;
194+ if (!ctype .startsWith (EnergyConverter .CORE_CTYPE )) {
195+ LOGGER .debug ("toInterval: transform from " + ctype + " to " + EnergyConverter .CORE_CTYPE + "-???" );
196+ kw = trans .translate (EnergyConverter .CORE_CTYPE + "-???" ); // any linearization algorithm
197+ trans = new Transform (kw );
181198 } else {
182- return new Interval <>(Math .max (headerWCSInterval .getLower (), cutoutBounds .getLower ()),
183- Math .min (headerWCSInterval .getUpper (), cutoutBounds .getUpper ()));
199+ kw = spectralWcs ;
200+ }
201+ double naxis = kw .getDoubleValue ("NAXIS1" );
202+ double p1 = 0.5 ;
203+ double p2 = naxis + 0.5 ;
204+ Transform .Result start = trans .pix2sky (new double [] {p1 });
205+ Transform .Result end = trans .pix2sky (new double [] {p2 });
206+
207+ double a = start .coordinates [0 ];
208+ double b = end .coordinates [0 ];
209+ LOGGER .debug ("toInterval: wcslib returned " + a + start .units [0 ] + "," + b + end .units [0 ]);
210+
211+ final String specsys = kw .getStringValue (CADCExt .SPECSYS .key ());
212+ final EnergyConverter energyConverter = new EnergyConverter ();
213+ if (!EnergyConverter .CORE_SPECSYS .equals (specsys )) {
214+ a = energyConverter .convertSpecsys (a , specsys );
215+ b = energyConverter .convertSpecsys (b , specsys );
216+ }
217+
218+ // wcslib convert to WAVE-??? but units might be a multiple of EnergyConverter.CORE_UNIT
219+ String cunit = start .units [0 ]; // assume same as end.units[0]
220+ if (!EnergyConverter .CORE_UNIT .equals (cunit )) {
221+ LOGGER .debug ("toInterval: converting " + a + " " + cunit );
222+ a = energyConverter .convert (a , EnergyConverter .CORE_CTYPE , cunit );
223+ LOGGER .debug ("toInterval: converting " + b + " " + cunit );
224+ b = energyConverter .convert (b , EnergyConverter .CORE_CTYPE , cunit );
225+ }
226+
227+ return new Interval <>(Math .min (a , b ), Math .max (a , b ));
228+ }
229+
230+ /**
231+ * Build a 1-axis WCS containing only the spectral axis, mapped to axis 1.
232+ */
233+ static WCSKeywords extractSpectralAxis (final WCSKeywords wcsKeywords , final int energyAxis ) {
234+ final WCSKeywordsImpl kw = new WCSKeywordsImpl ();
235+ kw .put ("NAXIS" , 1 );
236+ kw .put ("NAXIS1" , wcsKeywords .getIntValue (Standard .NAXISn .n (energyAxis ).key ()));
237+ kw .put ("CTYPE1" , wcsKeywords .getStringValue (Standard .CTYPEn .n (energyAxis ).key ()));
238+ kw .put ("CRPIX1" , wcsKeywords .getDoubleValue (Standard .CRPIXn .n (energyAxis ).key ()));
239+ kw .put ("CRVAL1" , wcsKeywords .getDoubleValue (Standard .CRVALn .n (energyAxis ).key ()));
240+
241+ final String cunitKey = CADCExt .CUNITn .n (energyAxis ).key ();
242+ if (wcsKeywords .containsKey (cunitKey )) {
243+ kw .put ("CUNIT1" , wcsKeywords .getStringValue (cunitKey ));
244+ }
245+
246+ final String cdeltKey = Standard .CDELTn .n (energyAxis ).key ();
247+ if (wcsKeywords .containsKey (cdeltKey )) {
248+ kw .put ("CDELT1" , wcsKeywords .getDoubleValue (cdeltKey ));
249+ }
250+
251+ EnergyCutout .copyOptionalStringKeyword (wcsKeywords , kw , CADCExt .SPECSYS .key ());
252+ EnergyCutout .copyOptionalDoubleKeyword (wcsKeywords , kw , CADCExt .RESTFRQ .key ());
253+ EnergyCutout .copyOptionalDoubleKeyword (wcsKeywords , kw , CADCExt .RESTWAV .key ());
254+ EnergyCutout .copyOptionalDoubleKeyword (wcsKeywords , kw , "RESTFREQ" );
255+ EnergyCutout .copyOptionalIntKeyword (wcsKeywords , kw , "VELREF" );
256+ return kw ;
257+ }
258+
259+ private static void copyOptionalStringKeyword (final WCSKeywords source , final WCSKeywordsImpl target ,
260+ final String key ) {
261+ if (source .containsKey (key )) {
262+ target .put (key , source .getStringValue (key ));
263+ }
264+ }
265+
266+ private static void copyOptionalDoubleKeyword (final WCSKeywords source , final WCSKeywordsImpl target ,
267+ final String key ) {
268+ if (source .containsKey (key )) {
269+ target .put (key , source .getDoubleValue (key ));
270+ }
271+ }
272+
273+ private static void copyOptionalIntKeyword (final WCSKeywords source , final WCSKeywordsImpl target ,
274+ final String key ) {
275+ if (source .containsKey (key )) {
276+ target .put (key , source .getIntValue (key ));
277+ }
278+ }
279+
280+ /**
281+ * Intersect the requested barycentric wavelength range (m) with the [min,max] in metres of the
282+ * spectral band on the data (linear in native unit at pixels 1 and nchan). Handles ±Infinity on
283+ * the request via {@code max}/{@code min} with the band; returns null if there is no intersection.
284+ */
285+ private Interval <Number > clampWavelengthToSpectralFieldOfView (final Interval <Number > bounds , final int energyAxis )
286+ throws NoSuchKeywordException , WCSLibRuntimeException {
287+ final double wmin = bounds .getLower ().doubleValue ();
288+ final double wmax = bounds .getUpper ().doubleValue ();
289+ final double rlo = Math .min (wmin , wmax );
290+ final double rhi = Math .max (wmin , wmax );
291+ final Interval <Number > m = EnergyCutout .spectralWavelengthMetresAtBandEdges (energyAxis , this .fitsHeaderWCSKeywords );
292+ final double mmin = m .getLower ().doubleValue ();
293+ final double mmax = m .getUpper ().doubleValue ();
294+ // max(rlo, mmin) lets -Inf select the in-band start; min(rhi, mmax) clips a request that runs past the band.
295+ final double cLo = Math .max (rlo , mmin );
296+ final double cHi = Math .min (rhi , mmax );
297+ LOGGER .debug ("Spectral (m) request [" + rlo + ", " + rhi + "] with header band ~[" + mmin + ", " + mmax
298+ + "] -> [" + cLo + ", " + cHi + "]" );
299+ if (cLo > cHi ) {
300+ return null ;
184301 }
302+ return new Interval <>(cLo , cHi );
185303 }
186304
187305 /**
0 commit comments