Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.

...

Code Block
java
java
package nl.deltares.hydrotel;

import nl.wldelft.util.Box;
import nl.wldelft.util.FastDateFormat;
import nl.wldelft.util.FileUtils;
import nl.wldelft.util.LongArrayUtils;
import nl.wldelft.util.StringArrayUtils;
import nl.wldelft.util.TextUtils;
import nl.wldelft.util.TimeUnit;
import nl.wldelft.util.io.LineReader;
import nl.wldelft.util.io.LineWriter;
import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
import org.apache.log4j.PropertyConfigurator;
import ucar.ma2.Array;
import ucar.nc2.Attribute;
import ucar.nc2.Dimension;
import ucar.nc2.NetcdfFile;
import ucar.nc2.Variable;
import ucar.nc2.units.DateUnit;

import java.io.File;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.Path;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.TimeZone;

public class HydrotelPreAdapter {
    private static final Logger log = Logger.getLogger(HydrotelPreAdapter.class);
    private static final String PROPERTIES = "properties";
    private static final String SORTIES_HYDROTEL = "sortiesHydrotel";
    private static final String SAUVEGARDE_ETAT_FIN_SIMULATION = "sauvegardeEtatFinSimulation";
    private static final String PR = "pr";
    private static final String STATION_ID = "station_id";

    private final File runFile;
    private Path runPath = null;
    private NetcdfFile netcdfRunFile = null;
    private String logLevel = "INFO";
    private static final String LOG_LEVEL = "log_level";
    private static final String WORK_DIR = "work_dir";
    private static final String END_TIME = "end_time";
    private static final String START_TIME = "start_time";
    private static final String INPUT_NETCDF_FILES_VARIABLE_NAME = "input_netcdf_files";
    private File workDirFile = null;
    private String[] inputTimeSeriesFiles = null;
    private float[] temperatureAdditif1D = null;
    private float[] precipitationMultiplicatif1D = null;
    private float[] precipitationAdditif1D = null;
    private long[] correctionTimes = null;
    private File simulationFolder = null;
    private static final FastDateFormat YYYYMMDDHH = FastDateFormat.getInstance("yyyy/MM/dd HH", TimeZone.getTimeZone("EST"), Locale.CANADA, null);
    private static final FastDateFormat YYYY_MM_DDHH = FastDateFormat.getInstance("yyyy-MM-dd HH", TimeZone.getTimeZone("EST"), Locale.CANADA, null);
    private static final FastDateFormat DDMMYYYYKK = FastDateFormat.getInstance("dd/MM/yyyy kk", TimeZone.getTimeZone("EST"), Locale.CANADA, null);
    private double startDateTime = 0.0;
    private double endDateTime = 0.0;
    private boolean saveStatesAtEnd = false;
    private static final DecimalFormatSymbols DECIMAL_FORMAT_SYMBOLS = new DecimalFormatSymbols(Locale.CANADA);
    private static final DecimalFormat DECIMAL_FORMAT = new  DecimalFormat("0.0000",DECIMAL_FORMAT_SYMBOLS);

    private HydrotelPreAdapter(File runFile) {
        this.runFile = runFile;
    }

    public static void main(String[] args) throws Exception {
        if (args.length != 1)
            throw new IllegalArgumentException("Specify either one argument that is the path to the run file, or 4 arguments that are 'work dir', input file, output file and 'log HZ reeks'.");
        File runFile = new File(args[0]);
        if (!runFile.exists()) throw new Exception("Can not find run file specified as argument " + runFile);
        HydrotelPreAdapter adapter = new HydrotelPreAdapter(runFile);
        try {
            adapter.run();
            if (log.isInfoEnabled()) log.info("HydrotelAdapter run finished without exception");
        } catch (Exception e) {
            log.error(e.getMessage(), e);
            throw e;
        } finally {
            LogManager.shutdown();
        }
    }

    private void run() throws Exception {

        try {
            runPath = runFile.getParentFile().toPath();
            String absolutePath = runFile.getAbsolutePath();
            netcdfRunFile = NetcdfFile.open(absolutePath);
            readRunInfoFile();
        } finally {
            netcdfRunFile.close();
        }
        convertTimeSeries();
        editSimulationFile();
    }

    private void editSimulationFile() throws IOException {
        File simulationCsv = new File(simulationFolder, "simulation.csv");
        String[] simulationLines = FileUtils.readAllLines(simulationCsv);
        double dayAfterStartDatewriteStateTime = startDateTime + TimeUnit.DAY_MILLISgetWriteStateTime();
        for (int i = 1; i < simulationLines.length; i++) {
            String line = simulationLines[i];
            String[] splitLines = line.split(";");
            if (splitLines.length == 0) continue;
            String key = splitLines[0];
            if (splitLines.length == 1) splitLines = new String[]{key, null};
            switch (key) {
                case "DATE DEBUT":
                    splitLines[1] = YYYY_MM_DDHH.format(startDateTime);
                    break;
                case "DATE FIN":
                    splitLines[1] = YYYY_MM_DDHH.format(endDateTime);
                    break;
                case "ECRITURE ETAT FONTE NEIGE":
                case "ECRITURE ETAT BILAN VERTICAL":
                case "ECRITURE ETAT RUISSELEMENT SURFACE":
                case "ECRITURE ETAT ACHEMINEMENT RIVIERE":
                    splitLines[1] = YYYY_MM_DDHH.format(dayAfterStartDatewriteStateTime);
                    break;
                default:
                    continue;
            }
            simulationLines[i] = TextUtils.join(splitLines, ';');
        }
        FileUtils.deleteIfExists(simulationCsv);
        FileUtils.writeText(simulationCsv, TextUtils.join(simulationLines, '\n'));
    }

    private double getWriteStateTime() {
        if (saveStatesAtEnd) return endDateTime;
        return startDateTime + TimeUnit.DAY_MILLIS;
    }

    private void readRunInfoFile() throws Exception {
        logLevel = netcdfRunFile.findVariable(LOG_LEVEL).readScalarString();
        getWorkDir();
        configureLogging();
        if (log.isDebugEnabled()) log.debug("HydrotelAdapter started");
        inputTimeSeriesFiles = getFilePaths(INPUT_NETCDF_FILES_VARIABLE_NAME);
        simulationFolder = new File(workDirFile, "simulation/simulation");
        readStartEndTime();
        handleProperties();
    }

    private void readStartEndTime() throws Exception {
        Variable startTimeVar = netcdfRunFile.findVariable(START_TIME);
        double relativeStartDateTime = startTimeVar.readScalarDouble();
        double relativeEndDateTime = netcdfRunFile.findVariable(END_TIME).readScalarDouble();
        Attribute units = startTimeVar.findAttribute("units");
        String tunitsString = units.getStringValue();
        if (log.isInfoEnabled()) log.info("INFO: Start time: " + relativeStartDateTime + ' ' + tunitsString);
        if (log.isInfoEnabled()) log.info("INFO: End time: " + relativeEndDateTime + ' ' + tunitsString);
        DateUnit referenceUnit = new DateUnit(units.getStringValue());
        long currentReferenceTime = referenceUnit.getDateOrigin().getTime();
        ucar.nc2.units.TimeUnit timeUnit = referenceUnit.getTimeUnit();
        startDateTime = timeUnit.getValueInSeconds(relativeStartDateTime) * 1000 + currentReferenceTime;
        endDateTime = timeUnit.getValueInSeconds(relativeEndDateTime) * 1000 + currentReferenceTime;
    }

    private void getWorkDir() throws IOException {
        String relativeWorkDirString = String.valueOf((char[]) netcdfRunFile.findVariable(WORK_DIR).read().copyToNDJavaArray()).trim();
        String workDirString = getWorkDirString(relativeWorkDirString);
        workDirFile = new File(workDirString);
        if (log.isInfoEnabled()) log.info("Work dir: " + workDirString);
        if (!workDirFile.exists()) throw new RuntimeException("Work dir not found: " + workDirString);
    }

    private String getWorkDirString(String relativeWorkDirString) {
        if (TextUtils.equals(relativeWorkDirString, ".")) return runPath.toString();
        return runPath.resolve(new File(relativeWorkDirString).toPath()).toString();
    }

    private void handleProperties() throws Exception {
        Variable var = netcdfRunFile.findVariable(PROPERTIES);
        String sortiesHydrotel = getPropertyStringValue(var, SORTIES_HYDROTEL);
        convertOutputSettings(sortiesHydrotel);
        String saveStateAtEndStringValue = getPropertyStringValue(var, SAUVEGARDE_ETAT_FIN_SIMULATION);
        if (saveStateAtEndStringValue != null) saveStatesAtEnd = Boolean.valueOf(saveStateAtEndStringValue);
    }

    private void convertOutputSettings(String sortiesHydrotel) throws IOException {
        boolean debitAvalOnly = false;
        String[] outputValuesHydrotel = new String[0];
        if (sortiesHydrotel == null) {
            debitAvalOnly = true;
        } else {
            outputValuesHydrotel = sortiesHydrotel.split(";");
            if (outputValuesHydrotel.length == 1 && outputValuesHydrotel[0].equals("DEBIT_AVAL")) {
                debitAvalOnly = true;
            }
        }
        File outputCsv = new File(simulationFolder, "output.csv");
        if (debitAvalOnly) {
            File exutoireCsv = new File(simulationFolder, "output_exutoire.csv");
            FileUtils.copy(exutoireCsv, outputCsv);
        } else {
            File bassinCsv = new File(simulationFolder, "output_bassin.csv");
            String[] bassinLines = FileUtils.readAllLines(bassinCsv);
            for (int i = 1; i < bassinLines.length; i++) {
                String line = bassinLines[i];
                String[] splitLines = line.split(";");
                if (StringArrayUtils.contains(outputValuesHydrotel, splitLines[0])) {
                    splitLines[1] = "1";
                }
                bassinLines[i] = TextUtils.join(splitLines, ';');
            }
            FileUtils.writeText(outputCsv, TextUtils.join(bassinLines, '\n'));
        }
    }

    private String[] getFilePaths(String variableName) throws IOException {
        Variable inputVar = netcdfRunFile.findVariable(variableName);
        Array inputArray = inputVar.read();
        Object inputFilesNDArrayObject = inputArray.copyToNDJavaArray();
        char[][] inputFilesChar = (char[][]) inputFilesNDArrayObject;

        List<String> inputFilesList = new ArrayList<>();

        for (char[] chArray : inputFilesChar) {
            File inputFileRelativePath = new File(String.valueOf(chArray).trim());
            String absoluteFilePath = runPath.resolve(inputFileRelativePath.toPath()).toString();
            inputFilesList.add(absoluteFilePath);
        }

        String[] inputFiles = new String[inputFilesList.size()];
        inputFilesList.toArray(inputFiles);
        return inputFiles;
    }

    private static List<String> getStringsFromNetcdfVariable(NetcdfFile netcdfFile, String variableName) throws IOException {
        Variable inputVar = netcdfFile.findVariable(variableName);
        Array inputArray = inputVar.read();
        Object inputFilesNDArrayObject = inputArray.copyToNDJavaArray();
        char[][] inputFilesChar = (char[][]) inputFilesNDArrayObject;

        List<String> stationIds = new ArrayList<>();

        for (char[] chArray : inputFilesChar) {
            String stationId = String.valueOf(chArray).trim();
            stationIds.add(stationId);
        }

        return stationIds;
    }

    private void configureLogging() {
        File logFile = new File(workDirFile, "hydrotel_pre_adapter.log");
        if (logFile.exists()) {
            //noinspection ResultOfMethodCallIgnored
            logFile.delete();
        }
        Properties props = new Properties();
        props.setProperty("log4j.rootLogger", logLevel + ", hydrotel");
        props.setProperty("log4j.appender.hydrotel.File", logFile.getAbsolutePath());
        props.setProperty("log4j.appender.hydrotel", "org.apache.log4j.RollingFileAppender");
        props.setProperty("log4j.appender.hydrotel.layout", "org.apache.log4j.PatternLayout");
        props.setProperty("log4j.appender.hydrotel.layout.ConversionPattern", "%p - %m \n");
        LogManager.resetConfiguration();
        PropertyConfigurator.configure(props);
        if (log.isDebugEnabled()) log.debug("Debug log level enabled");
        if (log.isInfoEnabled()) log.info("Info log level enabled");
    }

    private void convertTimeSeries() throws Exception {
        String meteoFile = null;
        for (String inputTimeSeriesFile : inputTimeSeriesFiles) {
            if (inputTimeSeriesFile.endsWith("meteo.nc")) {
                meteoFile = inputTimeSeriesFile;
            } else if (inputTimeSeriesFile.endsWith("corrections.nc")) {
                convertCorrections(inputTimeSeriesFile);
            } else if (inputTimeSeriesFile.endsWith("debit.nc")) {
                convertHydro(inputTimeSeriesFile);
            }
        }
        convertMeteo(meteoFile);
    }

    private void convertHydro(String inputTimeSeriesFile) throws IOException {
        File hydroDir = new File(workDirFile, "hydro");
        File stationsSth = new File(hydroDir, "station.sth");
        List<String> requiredStations = getRequiredStations(stationsSth);
        NetcdfFile netcdfFile = null;
        long[] times;
        float[][] floats;
        List<String> stationIds;
        try {
            netcdfFile = NetcdfFile.open(inputTimeSeriesFile);
            Variable timeVar = netcdfFile.findVariable("time");
            times = readTimes(timeVar);
            boolean useExtraEnsembleDimension = useExtraEnsembleDimension(netcdfFile);
            floats = read2DArray(netcdfFile, "Q_obs", useExtraEnsembleDimension);
            stationIds = getStringsFromNetcdfVariable(netcdfFile, STATION_ID);
        } finally {
            if (netcdfFile != null) netcdfFile.close();
        }

        writeHydroFiles(hydroDir, stationsSth, requiredStations, times, floats, stationIds, netcdfFile);
    }

    private static void writeHydroFiles(File hydroDir, File stationsSth, List<String> requiredStations, long[] times, float[][] floats, List<String> stationIds, NetcdfFile netcdfFile) throws IOException {
        for (int i = 0; i < stationIds.size(); i++) {
            String id = stationIds.get(i);
            if (!requiredStations.contains(id)) {
                log.warn("Data exported for station " + id + " but not present in " + stationsSth);
                continue;
            }
            requiredStations.remove(id);
            writeHydroStationFile(hydroDir, times, floats, i, id);
        }
        // Log errors for stations not removed in loop above
        for (String requiredStation : requiredStations) {
            log.error("No data for station " + requiredStation + " found in " + netcdfFile.getLocation());
        }
    }

    private static void writeHydroStationFile(File hydroDir, long[] times, float[][] floats, int i, String id) throws IOException {
        File hydroStationFile = new File(hydroDir, id + ".hyd");
        try (LineWriter lineWriter = new LineWriter(hydroStationFile, Charset.defaultCharset())) {
            lineWriter.writeLine("1 3");
            lineWriter.writeLine("");
            for (int j = 0; j < times.length; j++) {
                String dateTime = DDMMYYYYKK.format(times[j]);
                String value = TextUtils.padLeft(DECIMAL_FORMAT.format(floats[j][i]), 8);
                lineWriter.writeLine(dateTime + ' ' + value);
            }
        }
    }

    private static List<String> getRequiredStations(File stationsSth) throws IOException {
        List<String> requiredStations = new ArrayList<>();
        try (LineReader lineReader = new LineReader(stationsSth, Charset.defaultCharset())) {
            lineReader.skipLines(3);
            String line = lineReader.readLine();
            while (line != null) {
                String[] columns = line.split(" ");
                requiredStations.add(columns[0]);
                line = lineReader.readLine();
            }
        }
        return requiredStations;
    }

    private void convertCorrections(String inputTimeSeriesFile) throws IOException {
        NetcdfFile netcdfFile = null;
        try {
            netcdfFile = NetcdfFile.open(inputTimeSeriesFile);
            Variable timeVar = netcdfFile.findVariable("time");
            correctionTimes = readTimes(timeVar);
            useExtraEnsembleDimension(netcdfFile);
            float[] eenAdditif1D = read1DArrayFloat(netcdfFile, "eenAdditif");
            float[] reserveEauSolMultiplicatif1D = read1DArrayFloat(netcdfFile, "reserveEauSolMultiplicatif");

            File correctionCsv = new File(simulationFolder, "correction.csv");
            String correctionText = FileUtils.readText(correctionCsv);
            File correctionTmp = new File(simulationFolder, "correction_tmp.csv");
            try (LineWriter lineWriter = new LineWriter(correctionTmp, Charset.defaultCharset())) {
                lineWriter.write(correctionText);
                lineWriter.writeLine("");
                writeEenAdditiveCorrection(eenAdditif1D, lineWriter, DECIMAL_FORMAT);
                writeReserveEauSolMultiplicatifCorrection(reserveEauSolMultiplicatif1D, lineWriter, DECIMAL_FORMAT);
            }
            FileUtils.deleteIfExists(correctionCsv);
            FileUtils.copy(correctionTmp, correctionCsv);
            FileUtils.deleteIfExists(correctionTmp);

            precipitationAdditif1D = read1DArrayFloat(netcdfFile, "precipitationAdditif");
            precipitationMultiplicatif1D = read1DArrayFloat(netcdfFile, "precipitationMultiplicatif");
            temperatureAdditif1D = read1DArrayFloat(netcdfFile, "temperatureAdditif");
        } finally {
            if (netcdfFile != null) netcdfFile.close();
        }
    }

    private void writeEenAdditiveCorrection(float[] values, LineWriter lineWriter, DecimalFormat decimalFormat) throws IOException {
        String[] strings = new String[8];
        for (int k = 0; k < correctionTimes.length; k++) {
            float value = values[k];
            if (value == 0.0f) continue;
            String dateTimeString = YYYYMMDDHH.format(correctionTimes[k]);
            if (Float.isNaN(value)) throw new RuntimeException("NaN value found for eenAdditif at " + dateTimeString);
            strings[0] = "1";
            strings[1] = dateTimeString;
            strings[2] = dateTimeString;
            strings[3] = "5";
            strings[4] = decimalFormat.format(value);
            strings[5] = "1";
            strings[6] = "0";
            strings[7] = "tous";
            String line = TextUtils.join(strings, ';');
            lineWriter.writeLine(line);
        }
    }

    private void writeReserveEauSolMultiplicatifCorrection(float[] values, LineWriter lineWriter, DecimalFormat decimalFormat) throws IOException {
        String[] strings = new String[8];
        for (int k = 0; k < correctionTimes.length; k++) {
            float value = values[k];
            if (value == 1.0f) continue;
            String dateTimeString = YYYYMMDDHH.format(correctionTimes[k]);
            if (Float.isNaN(value))
                throw new RuntimeException("NaN value found for reserveEauSolMultiplicatif at " + dateTimeString);
            strings[0] = "1";
            strings[1] = dateTimeString;
            strings[2] = dateTimeString;
            strings[3] = "4";
            strings[4] = "0";
            strings[5] = decimalFormat.format(value);
            strings[6] = "0";
            strings[7] = "tous";
            String line = TextUtils.join(strings, ';');
            lineWriter.writeLine(line);
        }
    }

    private static double[] read1DArrayDouble(NetcdfFile netcdfFile, String varName) throws IOException {
        Variable eenAdditif = netcdfFile.findVariable(varName);
        Array eenAdditifArray = eenAdditif.read();
        return (double[]) eenAdditifArray.copyTo1DJavaArray();
    }

    private static float[] read1DArrayFloat(NetcdfFile netcdfFile, String varName) throws IOException {
        Variable variable = netcdfFile.findVariable(varName);
        Array array = variable.read();
        return (float[]) array.copyTo1DJavaArray();
    }

    private void convertMeteo(String inputTimeSeriesFile) throws IOException {
        File meteoDir = new File(workDirFile, "meteo");
        Map<Box<Float, Float>, String> requiredXYCoords = getRequiredCoords(meteoDir);

        NetcdfFile netcdfFile = null;
        try {
            netcdfFile = NetcdfFile.open(inputTimeSeriesFile);
            double[] x = read1DArrayDouble(netcdfFile, "x");
            double[] y = read1DArrayDouble(netcdfFile, "y");
            boolean useExtraEnsembleDimension = useExtraEnsembleDimension(netcdfFile);
            Variable timeVar = netcdfFile.findVariable("time");
            long[] times = readTimes(timeVar);
            //noinspection ResultOfMethodCallIgnored
            meteoDir.mkdirs();
            float[][][] prFloats = read3DArray(netcdfFile, PR, useExtraEnsembleDimension);
            float[][][] tasMinFloats = read3DArray(netcdfFile, "tasmin", useExtraEnsembleDimension);
            float[][][] tasMaxFloats = read3DArray(netcdfFile, "tasmax", useExtraEnsembleDimension);
            writeRequiredMeteoFiles(meteoDir, requiredXYCoords, x, y, times, DDMMYYYYKK, prFloats, tasMinFloats, tasMaxFloats);
        } finally {
            if (netcdfFile != null) netcdfFile.close();
        }
    }

    private static boolean useExtraEnsembleDimension(NetcdfFile netcdfFile) {
        Dimension realization = netcdfFile.findDimension("realization");
        if (realization == null) return false;
        if (realization.getLength() > 1) throw new RuntimeException("Multiple ensembles at once not supported");
        return true;
    }

    private void writeRequiredMeteoFiles(File meteoDir, Map<Box<Float, Float>, String> requiredXYCoords, double[] x, double[] y, long[] times, FastDateFormat dateFormat, float[][][] prFloats, float[][][] tasMinFloats, float[][][] tasMaxFloats) throws IOException {
        for (int i = 0; i < x.length; i++) {
            float xCoord = (float) x[i];
            for (int j = 0; j < y.length; j++) {
                float yCoord = (float) y[j];
                Box<Float, Float> foundCoords = new Box<>(xCoord, yCoord);
                String fileName = requiredXYCoords.get(foundCoords);
                if (fileName == null) continue;
                File meteoFile = new File(meteoDir, fileName + ".met");
                writeMeteoFile(times, dateFormat, prFloats, tasMinFloats, tasMaxFloats, i, j, meteoFile);
            }
        }
    }

    private void writeMeteoFile(long[] times, FastDateFormat dateFormat, float[][][] prFloats, float[][][] tasMinFloats, float[][][] tasMaxFloats, int i, int j, File meteoFile) throws IOException {
        try (LineWriter lineWriter = new LineWriter(meteoFile, Charset.defaultCharset())) {
            lineWriter.writeLine("1 3");
            String[] strings = new String[5];
            for (int k = 0; k < times.length; k++) {
                assert correctionTimes[k] == times[k];
                String[] dateTimeStrings = dateFormat.format(times[k]).split(" ");
                strings[0] = dateTimeStrings[0];
                strings[1] = dateTimeStrings[1];
                strings[2] = TextUtils.padLeft(String.valueOf(tasMaxFloats[k][j][i] + temperatureAdditif1D[k]), 5);
                strings[3] = TextUtils.padLeft(String.valueOf(tasMinFloats[k][j][i] + temperatureAdditif1D[k]), 5);
                strings[4] = String.valueOf(prFloats[k][j][i] * precipitationMultiplicatif1D[k] + precipitationAdditif1D[k]);
                String line = TextUtils.join(strings, '_').replaceAll("_", " ");
                lineWriter.writeLine(line);
            }
        }
    }

    private static Map<Box<Float, Float>, String> getRequiredCoords(File meteoDir) throws IOException {
        File stationStm = new File(meteoDir, "station.stm");

        Map<Box<Float, Float>, String> requiredXYCoords = new HashMap<>();
        try (LineReader lineReader = new LineReader(stationStm, Charset.defaultCharset())) {
            lineReader.skipLines(3);
            String line = lineReader.readLine();
            while (line != null) {
                String[] columns = line.split(" ");
                float x = Float.valueOf(columns[1]);
                float y = Float.valueOf(columns[2]);
                Box<Float, Float> coordBox = new Box<>(x, y);
                requiredXYCoords.put(coordBox, columns[0]);
                line = lineReader.readLine();
            }
        }
        return requiredXYCoords;
    }


    private static float[][][] read3DArray(NetcdfFile netcdfFile, String varName, boolean useExtraEnsembleDimension) throws IOException {
        Variable variable = netcdfFile.findVariable(varName);
        Array values = variable.read();
        if (!useExtraEnsembleDimension) return (float[][][]) values.copyToNDJavaArray();
        int[] shape = variable.getShape();
        if (shape.length == 3) {
            log.warn("Not all variables contain an ensemble dimension");
            return (float[][][]) values.copyToNDJavaArray();
        }
        float[][][][] floats4D = (float[][][][]) values.copyToNDJavaArray();
        float[][][] floats3D = new float[floats4D.length][][];
        for (int i = 0; i < floats4D.length; i++) {
            floats3D[i] = floats4D[i][0];
        }
        return floats3D;
    }


    @SuppressWarnings("SameParameterValue")
    private static float[][] read2DArray(NetcdfFile netcdfFile, String varName, boolean useExtraEnsembleDimension) throws IOException {
        Variable variable = netcdfFile.findVariable(varName);
        Array values = variable.read();
        if (!useExtraEnsembleDimension) return (float[][]) values.copyToNDJavaArray();
        int[] shape = variable.getShape();
        if (shape.length == 2) {
            log.warn("Not all variables contain an ensemble dimension");
            return (float[][]) values.copyToNDJavaArray();
        }
        float[][][] floats3D = (float[][][]) values.copyToNDJavaArray();
        float[][] floats2D = new float[floats3D.length][];
        for (int i = 0; i < floats2D.length; i++) {
            floats2D[i] = floats3D[i][0];
        }
        return floats2D;
    }


    private static String getPropertyStringValue(Variable propertiesVariable, String propertyName) throws Exception {
        if (propertiesVariable == null) return null;
        Attribute attribute = propertiesVariable.findAttribute(propertyName);
        if (attribute == null) return null;

        String value = attribute.getStringValue();
        if (value == null || value.trim().isEmpty()) {
            throw new Exception("Property '" + propertyName + "' in netcdf run file is empty or is not of type String.");
        }

        String trimmedValue = value.trim();
        if (log.isDebugEnabled())
            log.debug("Read property " + propertyName + " with value '" + trimmedValue + "' from netcdf run file.");
        return trimmedValue;
    }


    private static long[] readTimes(Variable timeVariable) throws IOException {
        if (timeVariable == null) return LongArrayUtils.EMPTY_ARRAY;

        //read times.
        Array timeArray = timeVariable.read();
        double[] times = (double[]) timeArray.get1DJavaArray(Double.class);

        //convert times.
        long[] convertedTimes = new long[times.length];
        DateUnit dateUnit = readTimeUnit(timeVariable);
        if (dateUnit == null)
            throw new IOException("Invalid date time unit string has been coded in the file: '" + timeVariable.getUnitsString() + "'.  Unit string should be for example: 'seconds since 2012-01-30 00:00:00'");
        for (int i = 0; i < times.length; i++) {
            Date date = dateUnit.makeDate(times[i]);
            if (date == null) throw new RuntimeException("Invalid time the file: '" + times[i]);
            convertedTimes[i] = date.getTime();
        }
        return convertedTimes;
    }


    private static DateUnit readTimeUnit(Variable timeVariable) {
        try {
            String unitString = timeVariable.getUnitsString();

            //if present, replace . by : in the timeZone specification in the unitString,
            //e.g. change "days since 2011-09-19 06:0:0.0 0.00" to "days since 2011-09-19 06:0:0.0 0:00".
            //This is a workaround implemented for FEWS-6544.
            String[] parts = unitString.split("\\s+");
            if (parts.length > 0 && parts[parts.length - 1].matches(".?\\d{1,2}\\.\\d{2}")) {
                //if a timeZone specification is present, then it is always the last part, see
                //http://cf-pcmdi.llnl.gov/documents/cf-conventions/1.5/cf-conventions.html#time-coordinate
                parts[parts.length - 1] = parts[parts.length - 1].replaceFirst("\\.", ":");
                StringBuilder buffer = new StringBuilder(parts[0]);
                for (int n = 1; n < parts.length; n++) {
                    buffer.append(' ').append(parts[n]);
                }
                unitString = buffer.toString();
            }

            return new DateUnit(unitString);
        } catch (Exception e) {
            //if the given variable does not have a unit of time.
            return null;
        }
    }

}