001/*
002 * This file is part of McIDAS-V
003 *
004 * Copyright 2007-2024
005 * Space Science and Engineering Center (SSEC)
006 * University of Wisconsin - Madison
007 * 1225 W. Dayton Street, Madison, WI 53706, USA
008 * https://www.ssec.wisc.edu/mcidas/
009 * 
010 * All Rights Reserved
011 * 
012 * McIDAS-V is built on Unidata's IDV and SSEC's VisAD libraries, and
013 * some McIDAS-V source code is based on IDV and VisAD source code.  
014 * 
015 * McIDAS-V is free software; you can redistribute it and/or modify
016 * it under the terms of the GNU Lesser Public License as published by
017 * the Free Software Foundation; either version 3 of the License, or
018 * (at your option) any later version.
019 * 
020 * McIDAS-V is distributed in the hope that it will be useful,
021 * but WITHOUT ANY WARRANTY; without even the implied warranty of
022 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
023 * GNU Lesser Public License for more details.
024 * 
025 * You should have received a copy of the GNU Lesser Public License
026 * along with this program.  If not, see https://www.gnu.org/licenses/.
027 */
028
029package edu.wisc.ssec.mcidasv.data;
030
031import java.io.BufferedReader;
032import java.io.File;
033import java.io.FileReader;
034import java.io.IOException;
035
036import java.util.ArrayList;
037import java.util.List;
038
039import org.slf4j.Logger;
040import org.slf4j.LoggerFactory;
041
042import ucar.unidata.geoloc.projection.UtmProjection;
043
044/**
045 * Representation of an ENVI header file.
046 */
047public class EnviInfo extends HeaderInfo {
048
049    /** The url */
050    private String dataFile = "";
051    private boolean isEnvi = false;
052    private boolean hasBounds = false;
053    private static final Logger logger = LoggerFactory.getLogger(EnviInfo.class);
054
055    // Map Info header field indices 
056    // See www.exelisvis.com/docs/ENVIHeaderFiles.html 
057    // for a description of this information
058    enum MapInfoIndex {
059
060        MAP_INFO_IDX_PROJ_NAME(0),
061        MAP_INFO_IDX_X_REF(1),
062        MAP_INFO_IDX_Y_REF(2),
063        MAP_INFO_IDX_EASTING(3),
064        MAP_INFO_IDX_NORTHING(4),
065        MAP_INFO_IDX_X_SIZE(5),
066        MAP_INFO_IDX_Y_SIZE(6),
067        MAP_INFO_IDX_ZONE(7),
068        MAP_INFO_IDX_N_OR_S(8),
069        MAP_INFO_IDX_DATUM(9),
070        MAP_INFO_IDX_UNITS(10);
071
072        private final int index;
073
074        private MapInfoIndex(int idx) {
075            index = idx;
076        }
077
078        public int getIndex() {
079            return index;
080        }
081    }
082
083    /**
084     * Ctor for xml encoding
085     */
086    public EnviInfo() {}
087
088    /**
089     * CTOR
090     *
091     * @param thisFile File to use. Cannot be {@code null}.
092     */
093    public EnviInfo(File thisFile) {
094        this(thisFile.getAbsolutePath());
095    }
096
097    /**
098     * CTOR
099     *
100     * @param filename The filename
101     */
102    public EnviInfo(String filename) {
103        super(filename);
104        this.dataFile = filename.replace(".hdr", ".img");
105    }
106
107    /**
108     * Is the file an ENVI header file?
109     * 
110     * @return {@code true} if the file appears to be an ENVI header file.
111     */
112    public boolean isEnviHeader() {
113        parseHeader();
114        return isEnvi;
115    }
116
117    /**
118     * Can we find a matching ENVI data file?
119     * 
120     * @return {@code true} if {@link #dataFile} exists.
121     */
122    public boolean hasEnviData() {
123        File testFile = new File(dataFile);
124        if (testFile.exists()) {
125            return true;
126        } else {
127            return false;
128        }
129    }
130
131    /**
132     * Is this a navigation header file?
133     * 
134     * @return {@code true} if {@link #dataFile} contains latitude and 
135     *         longitude bands, {@code false} otherwise.
136     */
137    public boolean isNavHeader() {
138        parseHeader();
139        List bandNames = getParameter(BANDNAMES, new ArrayList());
140        if (bandNames == null) {
141            return false;
142        }
143        if (bandNames.contains("Latitude") && bandNames.contains("Longitude")) {
144            return true;
145        }
146        return false;
147    }
148
149    /**
150     * Get the latitude band number.
151     * 
152     * @return Either the latitude band number or {code -1}. 
153     */
154    public int getLatBandNum() {
155        parseHeader();
156        List bandNames = getParameter(BANDNAMES, new ArrayList());
157        for (int i = 0; i < bandNames.size(); i++) {
158            if (bandNames.get(i).equals("Latitude")) {
159                return i + 1;
160            }
161        }
162        return -1;
163    }
164
165    /**
166     * 
167     *
168     * @return Either the latitude band file or an empty {@code String}.
169     */
170    public String getLatBandFile() {
171        parseHeader();
172        List bandFiles = getParameter(BANDFILES, new ArrayList());
173        int bandNum = getLatBandNum();
174        if (bandNum < 0) {
175            return "";
176        }
177        return (String)(bandFiles.get(bandNum - 1));
178    }
179
180    /**
181     * Get the longitude band number.
182     * 
183     * @return Either the longitude band number or {@code -1}.
184     */
185    public int getLonBandNum() {
186        parseHeader();
187        List bandNames = getParameter(BANDNAMES, new ArrayList());
188        for (int i = 0; i < bandNames.size(); i++) {
189            if (bandNames.get(i).equals("Longitude")) {
190                return i + 1;
191            }
192        }
193        return -1;
194    }
195
196    /**
197     * 
198     * 
199     * @return Either the longitude band file or an empty {@code String}.
200     */
201    public String getLonBandFile() {
202        parseHeader();
203        List bandFiles = getParameter(BANDFILES, new ArrayList());
204        int bandNum = getLonBandNum();
205        if (bandNum < 0) {
206            return "";
207        }
208        return (String)(bandFiles.get(bandNum - 1));
209    }
210
211//    /**
212//     * Return a FlatField representing the data
213//     */
214//      public FlatField getDataField() {
215//
216//      }
217
218//    /**
219//     * Return a Gridded2DSet representing navigation
220//     */
221//      public Gridded2DSet getNavField() {
222//
223//      }
224
225    /**
226     * Returns whether or not there are bounds.
227     * 
228     * @return Whether or not there are bounds.
229     */
230    public boolean isHasBounds() {
231        return hasBounds;
232    }
233
234    /**
235     * Control whether or not there are bounds.
236     * 
237     * @param hasBounds Whether or not there are bounds.
238     */
239    public void setHasBounds(boolean hasBounds) {
240        this.hasBounds = hasBounds;
241    }
242
243    /**
244     * Parse a potential ENVI header file.
245     */
246    protected void parseHeader() {
247        if (haveParsed()) {
248            return;
249        }
250        if (!doesExist()) {
251            isEnvi = false;
252            return;
253        }
254
255        try {
256            BufferedReader br = new BufferedReader(new FileReader(getFilename()));
257            String line;
258            String parameter = "";
259            String value = "";
260            boolean inValue = false;
261
262            List<String> bandNames = new ArrayList<>();
263            List bandFiles = new ArrayList();
264
265            while ((line = br.readLine()) != null) {
266                if (line.trim().equals("ENVI")) {
267                    isEnvi = true;
268                    continue;
269                }
270                if (!isEnvi) {
271                    break;
272                }
273
274                int indexOfEquals = line.indexOf("=");
275                int indexOfOpen = line.indexOf("{");
276                int indexOfClose = line.indexOf("}");
277                if (indexOfEquals >= 0) {
278                    parameter = line.substring(0, indexOfEquals).trim();
279                    value = "";
280                    inValue = false;
281                }
282                if (indexOfOpen >= 0) {
283                    if (indexOfClose >= 0) {
284                        value += line.substring(indexOfOpen+1, indexOfClose).trim();
285                        inValue = false;
286                    } else {
287                        value += line.substring(indexOfOpen+1).trim();
288                        inValue = true;
289                        continue;
290                    }
291                } else if (inValue) {
292                    if (indexOfClose >= 0) {
293                        value += line.substring(0, indexOfClose).trim();
294                        inValue = false;
295                    } else {
296                        value += line.trim();
297                        continue;
298                    }
299                } else {
300                    value += line.substring(indexOfEquals + 1).trim();
301                }
302
303                if (parameter.equals("")) {
304                    continue;
305                }
306
307                if (parameter.equals("description")) {
308                    setParameter(DESCRIPTION, value);
309                }
310
311                // TJJ Apr 2014
312                // NOTE: method signatures in parent class should be modified or extended
313                // I had to pass in an Integer object here in order to be able to retrieve
314                // anything other than default values later (both "lines" and "samples")
315
316                else if (parameter.equals("samples")) {
317                    setParameter(ELEMENTS, new Integer(value));
318                } else if (parameter.equals("lines")) {
319                    setParameter(LINES, new Integer(value));
320                } else if (parameter.equals("header offset")) {
321                    setParameter(OFFSET, Integer.parseInt(value));
322                } else if (parameter.equals("data type")) {
323                    setParameter(DATATYPE, Integer.parseInt(value));
324                } else if (parameter.equals("data ignore value") ||
325                    parameter.equals("bad value")) {
326                    setParameter(MISSINGVALUE, Float.parseFloat(value));
327                } else if (parameter.equals("interleave")) {
328                    setParameter(INTERLEAVE, value.toUpperCase());
329                } else if (parameter.equals("map info")) {
330                    logger.debug("Parsing Map Info, value: " + value);
331
332                    ArrayList<String> mapInfo = new ArrayList<>();
333                    String[] mapInfoSplit = value.split(",");
334                    for (int i = 0; i < mapInfoSplit.length; i++) {
335                        mapInfo.add(mapInfoSplit[i].trim());
336                    }
337
338                    // See www.exelisvis.com/docs/ENVIHeaderFiles.html 
339                    // for a description of this information
340                    // this code handles UTM files
341
342                    String projName = mapInfo.get(MapInfoIndex.MAP_INFO_IDX_PROJ_NAME.getIndex());
343
344                    if (projName.equals("UTM")) {
345
346                        // zone and hemisphere
347                        int utmZone = Integer.parseInt(mapInfo.get(MapInfoIndex.MAP_INFO_IDX_ZONE.getIndex()));
348                        boolean utmN = false;
349                        if (mapInfo.get(MapInfoIndex.MAP_INFO_IDX_N_OR_S.getIndex()).equals("North")) utmN = true;
350                        UtmProjection utmp = new UtmProjection(utmZone, utmN);
351
352                        // Java UTM class default units km, adjust if necessary
353                        float distFactor = 1.0f;
354                        if (mapInfo.get(MapInfoIndex.MAP_INFO_IDX_UNITS.getIndex()).contains("Meters")) distFactor = 1000.0f;
355
356                        // figure out Lat/Lon bounding box from Northing/Easting,
357                        // resolution, and grid size
358                        float upperLeftX = Float.parseFloat(mapInfo.get(MapInfoIndex.MAP_INFO_IDX_EASTING.getIndex())) / distFactor;
359                        float upperLeftY = Float.parseFloat(mapInfo.get(MapInfoIndex.MAP_INFO_IDX_NORTHING.getIndex())) / distFactor;
360
361                        // lines and samples were already seen
362                        int numLines = getParameter(LINES, 0);
363                        int numSamples = getParameter(ELEMENTS, 0);
364
365                        float xMag = Float.parseFloat(mapInfo.get(MapInfoIndex.MAP_INFO_IDX_X_SIZE.getIndex()));
366                        float yMag = Float.parseFloat(mapInfo.get(MapInfoIndex.MAP_INFO_IDX_Y_SIZE.getIndex()));
367
368                        float lowerRightX = upperLeftX + ((numSamples * xMag) / distFactor);
369                        float lowerRightY = upperLeftY + ((numLines * yMag) / distFactor);
370
371                        float [][] from = new float[2][2];
372                        from [0][0] = upperLeftX;
373                        from [1][0] = upperLeftY;
374                        from [0][1] = lowerRightX;
375                        from [1][1] = lowerRightY;
376                        float [][] to = new float[2][2];
377                        to = utmp.projToLatLon(from, to);
378
379                        // Need to check and see if we are correct in assuming which one is upper left
380                        if (to[0][0] > to[0][1]) {
381                            setParameter("BOUNDS.ULLAT", "" + to[0][0]);
382                            setParameter("BOUNDS.ULLON", "" + to[1][0]);
383                            setParameter("BOUNDS.LRLAT", "" + to[0][1]);
384                            setParameter("BOUNDS.LRLON", "" + to[1][1]);
385                        } else {
386                            from [0][0] = upperLeftX;
387                            from [1][0] = upperLeftY - ((numLines * yMag) / distFactor);
388                            from [0][1] = lowerRightX;
389                            from [1][1] = lowerRightY - ((numLines * yMag) / distFactor);
390                            to = utmp.projToLatLon(from, to);
391                            setParameter("BOUNDS.ULLAT", "" + to[0][1]);
392                            setParameter("BOUNDS.ULLON", "" + to[1][0]);
393                            setParameter("BOUNDS.LRLAT", "" + to[0][0]);
394                            setParameter("BOUNDS.LRLON", "" + to[1][1]);
395                        }
396                        hasBounds = true;
397                    }
398                } else if (parameter.equals("byte order")) {
399                    boolean bigEndian = false;
400                    if (value.equals("1")) {
401                        bigEndian = true;
402                    }
403                    setParameter(BIGENDIAN, bigEndian);
404                } else if (parameter.equals("bands")) {
405                    if (bandNames.size() <= 0 && bandFiles.size() <= 0) {
406                        int bandCount = Integer.parseInt(value);
407                        for (int i = 0; i < bandCount; i++) {
408                            bandNames.add("Band " + i+1);
409                            bandFiles.add(dataFile);
410                        }
411                        setParameter(BANDNAMES, bandNames);
412                        setParameter(BANDFILES, bandFiles);
413                    }
414                } else if (parameter.equals("band names")) {
415                    bandNames = new ArrayList<>();
416                    bandFiles = new ArrayList<String>();
417                    String[] bandNamesSplit = value.split(",");
418                    for (int i = 0; i < bandNamesSplit.length; i++) {
419                        bandNames.add(bandNamesSplit[i].trim());
420                        bandFiles.add(dataFile);
421                    }
422                    setParameter(BANDNAMES, bandNames);
423                    setParameter(BANDFILES, bandFiles);
424                }
425
426            }
427            br.close();
428        } catch (IOException ioe) {
429            logger.error("Problem parsing header", ioe);
430        }
431    }
432}