code: /*******************************************************************************
   * Copyright (c) 2015 Attila Kovacs <attila[AT]sigmyne.com>.
   * All rights reserved. 
   * 
   * This file is part of crush.
   * 
   *     crush is free software: you can redistribute it and/or modify
   *     it under the terms of the GNU General Public License as published by
   *     the Free Software Foundation, either version 3 of the License, or
   *     (at your option) any later version.
   * 
   *     crush is distributed in the hope that it will be useful,
   *     but WITHOUT ANY WARRANTY; without even the implied warranty of
   *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   *     GNU General Public License for more details.
   * 
   *     You should have received a copy of the GNU General Public License
   *     along with crush.  If not, see <http://www.gnu.org/licenses/>.
   * 
   * Contributors:
   *     Attila Kovacs <attila[AT]sigmyne.com> - initial API and implementation
   ******************************************************************************/
  
  package crush.instrument.scuba2;
  
  import crush.*;
  import crush.telescope.GroundBasedScan;
  import jnum.Constant;
  import jnum.Unit;
  import jnum.Util;
  import jnum.astro.AstroSystem;
  import jnum.astro.AstroTime;
  import jnum.astro.CoordinateEpoch;
  import jnum.astro.EquatorialCoordinates;
  import jnum.astro.GalacticCoordinates;
  import jnum.astro.GeodeticCoordinates;
  import jnum.astro.HorizontalCoordinates;
  import jnum.astro.JulianEpoch;
  import jnum.astro.Weather;
  import jnum.math.SphericalCoordinates;
  import nom.tam.fits.*;
  
  import java.io.*;
  import java.util.*;
  
  
  class Scuba2Scan extends GroundBasedScan<Scuba2Subscan> implements Weather {
  	/**
  	 * 
  	 */
  	private static final long serialVersionUID = 1608680718251250629L;
  
  	String date, endTime;
  	String obsMode, scanPattern, obsType;
  	int iDate;
  	
  	double tau225GHz, tau183GHz;
  	double ambientT, pressure, humidity, windAve, windPeak, windDirection;
  	String scanID;
  	int blankingValue;
  	
  	double dUT1 = 0.0;
  	
  	boolean[] hasSubarray = new boolean[Scuba2.SUBARRAYS];
  	int subarrays;
  	
  	Class<? extends SphericalCoordinates> trackingClass;
  	
  	Scuba2Scan(Scuba2 instrument) {
  		super(instrument);
  	}
  	
  	@Override
      public Scuba2 getInstrument() { return (Scuba2) super.getInstrument(); }
  	
  	@Override
  	public Scuba2Subscan getIntegrationInstance() {
  		return new Scuba2Subscan(this);
  	}
  	
  	@Override
  	public void mergeIntegrations() {
  		calcSamplingRate();
  		
  		super.mergeIntegrations();
  		
  		for(int i=size(); --i >= 0; ) {
  			Scuba2Subscan subscan = get(i);
  			subscan.integrationNo = i;
  			subscan.getInstrument().integrationTime = getInstrument().integrationTime;
  			subscan.getInstrument().samplingInterval = getInstrument().samplingInterval;
  		}
  	}
  	
  	
  	@Override
  	public void read(String scanDescriptor, boolean readFully) throws Exception {
  		ArrayList<Scuba2Fits> files = getFitsFiles(scanDescriptor);
  		if(files.isEmpty()) return;
  		
  		// Check fow which subarrays there is data in the dataset...
  		Arrays.fill(hasSubarray, false);
  		for(Scuba2Fits file : files) hasSubarray[file.getSubarrayIndex()] = true;
  		
  		// Sort by subscan and then by subarray index
  		Collections.sort(files);
  		
  		BasicHDU<?> mainHDU = files.get(0).getHDUs()[0];
  		parseScanPrimaryHDU(mainHDU);
  		
  		Scuba2 scuba2 = getInstrument();
  		scuba2.addPixelsFor(hasSubarray);
  		scuba2.parseScanPrimaryHDU(mainHDU);
  		scuba2.configure();	
  		scuba2.validate();
  		
  		// Count how many subarrays are active
  		subarrays = 0;
  		int channelOffset = 0;
  		for(int i=0; i<hasSubarray.length; i++) if(hasSubarray[i]) {
  			subarrays++;
  			scuba2.subarray[i].channelOffset = channelOffset;
  			channelOffset += Scuba2Subarray.PIXELS;
  		}
  		info("Found data for " + subarrays + " subarrays.");
  		
  		clear();
  		
  		// Read all the files for a given subscan...
  		Scuba2Subscan subscan = new Scuba2Subscan(this);
  		subscan.integrationNo = files.get(0).getSubscanNo();
  		
  		for(int i=0; i<files.size(); i++) {
  			Scuba2Fits file = files.get(i);
  			int subscanNo = file.getSubscanNo();
  	
  			if(subscanNo != subscan.integrationNo) {
  				addSubscan(subscan);
  				
  				subscan = new Scuba2Subscan(this);
  				subscan.integrationNo = subscanNo;
  			}
  			subscan.files.add(file);
  		}
  		addSubscan(subscan);
  		
  		Collections.sort(this);
  	}
  	
  	private void addSubscan(Scuba2Subscan subscan) throws FitsException {
  		if(subscan.files.isEmpty()) return;
  		try { 
  			subscan.read(); 
  			if(!subscan.isEmpty()) add(subscan);	
  		}
  		catch(DarkSubscanException e) { subscan.info("Subscan " + subscan.getID() + " is a dark measurement. Skipping."); }
  		catch(FastFlatSubscanException e) { subscan.info("Subscan " + subscan.getID() + " is a flatfield measurement. Skipping."); }
  		catch(NoiseSubscanException e) { subscan.info("Subscan " + subscan.getID() + " is a noise measurement. Skipping."); }
  		catch(UnsupportedIntegrationException e) {  subscan.info("Subscan " + subscan.getID() + " is not supported. Skipping."); }
  		catch(IllegalStateException e) { subscan.warning(e); }
  		catch(IOException e) { subscan.warning("FITS was not be closed."); }
  	}
  	
  	
  	private void calcSamplingRate() {
  		double totalTime = 0.0;
  		int nFrames = 0;
  		for(Scuba2Subscan subscan : this) {
  			totalTime += subscan.totalIntegrationTime;
  			nFrames += subscan.rawFrames;
  		}
  		getInstrument().samplingInterval = getInstrument().integrationTime = totalTime / nFrames;
  	
  	}
  		
  	@Override
  	public void validate() {
  		if(isEmpty()) return;	// TODO warning?
  		
  		calcSamplingRate();
  		info("Typical sampling rate is " + Util.f2.format(1.0 / getInstrument().integrationTime) + " Hz.");
  	
  		
  		Scuba2Frame firstFrame = getFirstIntegration().getFirstFrame();
  		Scuba2Frame lastFrame = getLastIntegration().getLastFrame();
  
  		if(horizontal == null) {
  			horizontal = new HorizontalCoordinates();
  			horizontal.setLongitude(0.5*(firstFrame.horizontal.x() + lastFrame.horizontal.x()));
  			horizontal.setLatitude(0.5*(firstFrame.horizontal.y() + lastFrame.horizontal.y()));
  		}
  
  		info("   Mean parallactic angle is " + Util.f1.format(getPositionAngle() / Unit.deg) + " deg.");
  		
  		super.validate();
  	}
  
  	private ArrayList<Scuba2Fits> getFitsFiles(String scanDescriptor) throws FileNotFoundException{
  		List<String> subIDs = hasOption("subarray") ? option("subarray").getList() : null;
  		
  		ArrayList<Scuba2Fits> files = null;
  		
  		if(subIDs == null) files = getFitsFiles(scanDescriptor, '*');
  		else {
  			files = new ArrayList<>();
  			for(String id : subIDs) files.addAll(getFitsFiles(scanDescriptor, id.toLowerCase().charAt(0)));
  		}
  		return files;
  	}
  	
  	private ArrayList<Scuba2Fits> getFitsFiles(String scanDescriptor, char subarrayCode) throws FileNotFoundException {
  		ArrayList<Scuba2Fits> scanFiles = new ArrayList<>();
  
  		String path = getDataPath();
  		descriptor = scanDescriptor;
  
  		String prefix = hasOption("450um") ? "s4" : "s8";
  		if(subarrayCode != '*') prefix += subarrayCode;
  		
  		// Try to read scan number with the help of 'object' and 'date' keys...
  		try {
  			String scanNo = Util.d5.format(Integer.parseInt(scanDescriptor));
  			if(hasOption("date")) {
  				File directory = new File(path);
  				String date = option("date").getValue().replaceAll("-", "");
  				String scanID = date + "_" + scanNo + "_";
  				
  				if(!directory.exists()) {
  					String message = "Cannot find scan directory " + path +
  						"\n    * Check that 'datapath' is correct:" + 
  						"\n      --> datapath = '" + option("datapath").getValue() + "'";
  					throw new FileNotFoundException(message);
  				}
  				else if(!directory.isDirectory()) {
  					throw new FileNotFoundException(path + " is not a directory.");
  				}
  				else {
  					String[] files = directory.list();
  					if(files == null) throw new FileNotFoundException("Not a directory or other I/O error.");
  					
  					for(int i=0; i<files.length; i++) if(files[i].startsWith(prefix)) if(files[i].substring(3).startsWith(scanID)) {
  						try { scanFiles.add(new Scuba2Fits(path + File.separator + files[i], getOptions())); }
  						catch(IllegalStateException e) { 
  							// there is a FITS alternative already...
  						}
  						catch(Exception e) { warning(e); }
  					}
  						
  					if(scanFiles.isEmpty()) 
  						throw new FileNotFoundException("Cannot find matching files in " + path);
  				}
  			}
  			else {
  				String message = "Cannot find scan " + scanDescriptor;
  
  				if(!hasOption("date")) message += "\n    * Specify 'date' for unique JCMT scan ID.";
  			
  				throw new FileNotFoundException(message);
  			}
  		}
  		
  		// Otherwise, just read as file names...
  		catch(NumberFormatException e) {
  			File scanFile = new File(scanDescriptor) ;	
  			
  			if(!scanFile.exists()) {
  				scanFile = new File(path + scanDescriptor);
  				if(!scanFile.exists()) throw new FileNotFoundException("Could not find scan " + scanDescriptor);
  			} 	
  			
  			try { scanFiles.add(new Scuba2Fits(scanFile.getPath(), getOptions())); }
  			catch(IllegalStateException e2) {
  				// There is a FITS alternative already...
  			}
  			catch(Exception e2) { warning(e2); }
  		}
  
  		
  		return scanFiles;
  	}	
  	
  
  	
  	protected void parseScanPrimaryHDU(BasicHDU<?> hdu) throws HeaderCardException, UnsupportedScanException {
  		Header header = hdu.getHeader();
  		
  		// Load any options based on the FITS header...
  		getInstrument().setFitsHeaderOptions(header);
  		
  		// Scan Info
  		setSerial(header.getIntValue("OBSNUM"));
  		if(getOptions().containsKey("serial")) getInstrument().setSerialOptions(getSerial());
  	
  		site = new GeodeticCoordinates(header.getDoubleValue("LONG-OBS") * Unit.deg, header.getDoubleValue("LAT-OBS") * Unit.deg);
  		creator = header.getStringValue("ORIGIN");
  		project = header.getStringValue("PROJECT");
  		
  		if(creator == null) creator = "Unknown";
  		if(observer == null) observer = "Unknown";
  		if(project == null) project = "Unknown";
  		
  		setSourceName(header.getStringValue("OBJECT"));
  		date = header.getStringValue("DATE-OBS");
  		endTime = header.getStringValue("DATE-END");
  		if(date == null) date = endTime;
  		
  		iDate = header.getIntValue("UTDATE");
  		scanID = header.getStringValue("OBSID");
  		
  		dUT1 = header.getDoubleValue("DUT1") * Unit.day;
  		
  		AstroTime timeStamp = new AstroTime();
  		try { 
  			timeStamp.parseFitsDate(date); 
  			setMJD(timeStamp.getMJD());
  		}
  		catch(Exception e) { warning("Could not parse DATE-OBS..."); }
  		
  		blankingValue = header.getIntValue("BLANK");
  		
  		info("[" + getSourceName() + "] observed on " + date);
  		
  		obsType = header.getStringValue("OBS_TYPE");	
  		if(obsType.equalsIgnoreCase("focus")) throw new UnsupportedScanException("Focus reduction not (yet) implemented.");
  		
  		// Weather
  		if(hasOption("tau.183ghz")) tau183GHz = option("tau.183ghz").getDouble();
  		else {
  			tau183GHz = 0.5 * (header.getDoubleValue("WVMTAUST") + header.getDoubleValue("WVMTAUEN"));
  			getInstrument().setOption("tau.183ghz=" + tau183GHz);
  		}
  		
  		if(hasOption("tau.225ghz")) tau225GHz = option("tau.225ghz").getDouble();
  		else {
  			tau225GHz = 0.5 * (header.getDoubleValue("TAU225ST") + header.getDoubleValue("TAU225EN"));
  			getInstrument().setOption("tau.225ghz=" + tau225GHz);
  		}
  		
  		ambientT = 0.5 * (header.getDoubleValue("ATSTART") + header.getDoubleValue("ATEND")) * Unit.K + Constant.zeroCelsius;
  		pressure = 0.5 * (header.getDoubleValue("BPSTART") + header.getDoubleValue("BPEND")) * Unit.mbar;
  		humidity = 0.5 * (header.getDoubleValue("HUMSTART") + header.getDoubleValue("HUMEND"));
  		windAve = 0.5 * (header.getDoubleValue("WINDSPDST") + header.getDoubleValue("WINDSPDEN")) * Unit.km / Unit.hour;
  		windDirection = 0.5 * (header.getDoubleValue("WINDDIRST") + header.getDoubleValue("WINDDIREN")) * Unit.deg;
  		
  		obsMode = header.getStringValue("SAM_MODE");
  		// TODO + Switching mode
  		// TODO + Chopper, jiggler parameters
  		// TODO + Scan details
  		scanPattern = header.getStringValue("SCAN_PAT");
  		
  		isTracking = true;		
  	}
  	
  	void parseCoordinateInfo(Header header) {
  		String trackingSystem = header.getStringValue("TRACKSYS");
  		
  		final double lon = header.getDoubleValue("BASEC1", Double.NaN) * Unit.deg;
  		final double lat = header.getDoubleValue("BASEC2", Double.NaN) * Unit.deg;
  		
  		if(trackingSystem == null) {
  			trackingClass = null;
  			return;
  		}
  		isTracking = true;
  		
  		if(trackingSystem.equals("AZEL")) {
  			trackingClass = HorizontalCoordinates.class;
  			horizontal = new HorizontalCoordinates(lon, lat);
  			info("Horizontal: " + horizontal.toString(1));
  			isTracking = false;
  		}
  		else if(trackingSystem.equals("APP")) {
  			trackingClass = EquatorialCoordinates.class;
  			apparent = new EquatorialCoordinates(lon, lat, JulianEpoch.forMJD(getMJD()));
  			equatorial = apparent.clone();
  			equatorial.precess(CoordinateEpoch.J2000);
  			info("Apparent: " + apparent.toString(1));
  			info("Equatorial: " + equatorial.toString(1));
  		}
  		else if(trackingSystem.equals("J2000")) {
  			trackingClass = EquatorialCoordinates.class;
  			equatorial = new EquatorialCoordinates(lon, lat, CoordinateEpoch.J2000);
  			info("Equatorial: " + equatorial.toString(1));
  		}
  		else if(trackingSystem.equals("B1950")) {
  			trackingClass = EquatorialCoordinates.class;
  			equatorial = new EquatorialCoordinates(lon, lat, CoordinateEpoch.B1950);
  			info("Equatorial: " + equatorial.toString(1));
  		}
  		else if(trackingSystem.equals("GAL")) {
  			trackingClass = GalacticCoordinates.class;
  			GalacticCoordinates galactic = new GalacticCoordinates(lon, lat);
  			equatorial = galactic.toEquatorial();
  			info("Galactic: " + galactic.toString(1));
  			info("Equatorial: " + equatorial.toString(1));
  		}
  		else {
  			trackingClass = null;
  			warning("Unsupported tracking system: " + trackingSystem);
  		}
  		
  		// GAPPT ?
  		
  	}
  	
  	@Override
  	public double getAmbientHumidity() {
  		return humidity;
  	}
  
  	@Override
  	public double getAmbientPressure() {
  		return pressure;
  	}
  
  	@Override
  	public double getAmbientKelvins() {
  		return ambientT;
  	}
  
  	@Override
  	public double getWindDirection() {
  		return windDirection;
  	}
  
  	@Override
  	public double getWindPeak() {
  		return windPeak;
  	}
  
  	@Override
  	public double getWindSpeed() {
  		return windAve;
  	}
  	
  	@Override
  	public String getID() {
  		return iDate + "." + getSerial();
  	}
  	
  	@Override
  	public void setSourceModel(SourceModel model) {
  		super.setSourceModel(model);
  		sourceModel.setID(getInstrument().filter);
  	}	
  	
  	@Override
  	public void editScanHeader(Header header) throws HeaderCardException {	
  		super.editScanHeader(header);
  	}
  	
  	@Override
  	public Object getTableEntry(String name) {
  		if(name.equals("obsmode")) return obsMode;
  		if(name.equals("obstype")) return obsMode;
  		if(name.equals("obspattern")) return scanPattern;
  		if(name.equals("dir")) return AstroSystem.getID(trackingClass);
  		return super.getTableEntry(name);
  	}
  	
  }
  