001/*
002 *   POIGeomReader  -- A class that can read a POI formatted geometry file.
003 *
004 *   Copyright (C) 2000-2024, Joseph A. Huwaldt
005 *   All rights reserved.
006 *
007 *   This library is free software; you can redistribute it and/or
008 *   modify it under the terms of the GNU Lesser General Public
009 *   License as published by the Free Software Foundation; either
010 *   version 2.1 of the License, or (at your option) any later version.
011 *
012 *   This library is distributed in the hope that it will be useful,
013 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
014 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
015 *   Lesser General Public License for more details.
016 *
017 *   You should have received a copy of the GNU Lesser General Public License
018 *   along with this program; if not, write to the Free Software
019 *   Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
020 *   Or visit:  http://www.gnu.org/licenses/lgpl.html
021 */
022package geomss.geom.reader;
023
024import geomss.geom.*;
025import static geomss.geom.reader.AbstractGeomReader.RESOURCES;
026import jahuwaldt.js.util.TextTokenizer;
027import java.io.*;
028import java.text.MessageFormat;
029import java.util.Locale;
030import static java.util.Objects.isNull;
031import static java.util.Objects.nonNull;
032import static java.util.Objects.requireNonNull;
033import javolution.text.Text;
034import javolution.text.TypeFormat;
035import javolution.util.FastTable;
036
037/**
038 * A {@link GeomReader} for reading vehicle geometry from a POINTS (POI) formatted
039 * geometry file. This is the geometry file format used by A502 (PANAIR) and A633
040 * (TRANAIR) for input geometry.
041 *
042 * <p> Modified by: Joseph A. Huwaldt </p>
043 *
044 * @author Joseph A. Huwaldt, Date: April 14, 2000
045 * @version January 1, 2024
046 */
047public class POIGeomReader extends AbstractGeomReader {
048
049    //  Debug output flag.
050    private static final boolean DEBUG = false;
051
052    //  A brief description of the data read by this reaader.
053    private static final String DESCRIPTION = RESOURCES.getString("poiDescription");
054
055    // Some error messages.
056    private static final String INC_ROWS_COLS = RESOURCES.getString("incRowsColsErr").
057            replace("<TYPE/>", DESCRIPTION);
058
059    // The number of characters per number stored in the POI file.
060    private static final int FIELDSIZE = 10;
061
062    //  The preferred file extension for files of this reader's type.
063    public static final String EXTENSION = "poi";
064
065    /**
066     * Returns a string representation of the object. This will return a brief description
067     * of the format read by this reader.
068     *
069     * @return A brief description of the format read by this reader.
070     */
071    @Override
072    public String toString() {
073        return DESCRIPTION;
074    }
075
076    /**
077     * Returns the preferred file extension (not including the ".") for files of this
078     * GeomReader's type.
079     *
080     * @return The preferred extension for this file type.
081     */
082    @Override
083    public String getExtension() {
084        return EXTENSION;
085    }
086
087    /**
088     * Method that determines if this reader can read paneled geometry from the specified
089     * input file.
090     *
091     * @param inputFile The input file containing the geometry to be read in.
092     * @return GeomReader.NO if the file format is not recognized by this reader.
093     *         GeomReader.YES if the file format is definitely recognized by this reader.
094     *         GeomReader.MAYBE if the file format might be readable by this reader, but
095     *         that can't easily be determined without actually reading the file.
096     * @throws java.io.IOException If there is a problem reading from the specified
097     * file.
098     */
099    @Override
100    public int canReadData(File inputFile) throws IOException {
101
102        int response = NO;
103        String name = inputFile.getName();
104        name = name.toLowerCase().trim();
105        if (name.endsWith(".poi"))
106            response = MAYBE;
107
108        //  POI files are required to be in ASCII with U.S. style number formatting.
109        //  Get the default locale and save it.  Then change the default locale to US.
110        Locale defLocale = Locale.getDefault();
111        Locale.setDefault(Locale.US);
112
113        //  Create an input stream from the file.
114        try (FileInputStream input = new FileInputStream(inputFile)) {
115
116            //  Create a buffer to hold the data read in from the file.
117            byte[] buffer = new byte[10240];
118
119            //  Read in up to 10k worth of data.
120            int byteCount = input.read(buffer, 0, 10240);
121            if (byteCount < 0)
122                return NO;
123
124            //  Turn the buffer into a (potentially 10k long) string.
125            String str = new String(buffer, 0, byteCount);
126
127            //  Convert the string into a line number reader.
128            LineNumberReader lnr = new LineNumberReader(new StringReader(str));
129
130            //  Search for a line that starts with "$POI".
131            String line = lnr.readLine();
132            while (nonNull(line)) {
133                if (line.startsWith("$POI")) {
134                    response = YES;
135                    break;
136                }
137                line = lnr.readLine();
138            }
139
140        } finally {
141            //  Restore the default locale.
142            Locale.setDefault(defLocale);
143        }
144
145        return response;
146    }
147
148    /**
149     * Reads in a POINTS (POI) geometry file from the specified input file and returns a
150     * {@link PointVehicle} object that contains the geometry from the POI file.
151     * <p>
152     * Each component will have an Integer stored in the user data under the key
153     * "A502A633TypeCode" that contains the A502-A633 array type code for all the networks
154     * contained in that component.
155     * </p>
156     * <p>
157     * WARNING: This file format is not unit aware. You must set the units
158     * to be used by calling "setFileUnits()" before calling this method!
159     * </p>
160     *
161     * @param inputFile The input file containing the geometry to be read in. May not be
162     *                  null.
163     * @return A {@link PointVehicle} object containing the geometry read in from the
164     *         file. If the file has no geometry in it, then this list will have no
165     *         components in it (will have a size() of zero).
166     * @throws IOException If there is a problem reading the specified file.
167     * @see #setFileUnits(javax.measure.unit.Unit) 
168     */
169    @Override
170    public PointVehicle read(File inputFile) throws IOException {
171        requireNonNull(inputFile);
172        _warnings.clear();
173
174        // Create an empty vehicle with the provided name as the vehicle name.
175        PointVehicle vehicle = PointVehicle.newInstance(inputFile.getName());
176
177        //  POI files are required to be in ASCII with U.S. style number formatting.
178        //  Get the default locale and save it.  Then change the default locale to US.
179        Locale defLocale = Locale.getDefault();
180        Locale.setDefault(Locale.US);
181
182        // Create a reader to access the ASCII file.
183        try (LineNumberReader reader = new LineNumberReader(new FileReader(inputFile))) {
184
185            // Loop over all the components stored in the file.
186            String aLine = reader.readLine();
187            while (nonNull(aLine)) {
188
189                // Skip ahead to the next "$POI" line.
190                while (nonNull(aLine) && !aLine.startsWith("$POI"))
191                    aLine = reader.readLine();
192                if (isNull(aLine))
193                    break;
194
195                // A $POI line was found, read in the component.
196                PointComponent comp = readComponent(reader);
197                vehicle.add(comp);
198
199                // Begin searching for the next component.
200                aLine = reader.readLine();
201            }
202
203        } finally {
204            //  Restore the default locale.
205            Locale.setDefault(defLocale);
206        }
207
208        return vehicle;
209    }
210
211    /**
212     * This method always returns <code>false</code> as POI files do not encode the units
213     * that are being used. You must call <code>setFileUnits</code> to set the units being
214     * used before reading from a file of this format.
215     *
216     * @return This implementation always returns false.
217     * @see #setFileUnits(javax.measure.unit.Unit) 
218     */
219    @Override
220    public boolean isUnitAware() {
221        return false;
222    }
223
224    /**
225     * Reads a single component made up of multiple networks of the same type from an
226     * input stream pointing to a POI file. This method assumes that the stream starts
227     * immediately after the $POI line in a POI file.
228     *
229     * @param in Reader for the POI file we are reading (positioned so that the next read
230     *           will occur on the line following the $POI line).
231     * @return The component read in from the file.
232     */
233    private PointComponent readComponent(LineNumberReader in) throws IOException {
234        PointComponent comp = null;
235
236        try {
237            // Read in the number of arrays.
238            String aLine = readLine(in);
239            String subStr = aLine.substring(0, FIELDSIZE - 1).trim();
240            int numArrs = Integer.parseInt(subStr);
241
242            // Read in the component type.
243            aLine = readLine(in);
244            subStr = aLine.substring(0, FIELDSIZE - 1).trim();
245            Integer typeCode = Integer.valueOf(subStr);
246            String typeStr = "Type " + typeCode;
247
248            // Loop over each array and read it in, adding it to a new component.
249            comp = PointComponent.newInstance(typeStr);
250            comp.putUserData("A502A633TypeCode", typeCode);
251            for (int i = 0; i < numArrs; ++i) {
252                PointArray net = readNetwork(in);
253                comp.add(net);
254            }
255
256        } catch (NumberFormatException e) {
257            throw new IOException(MessageFormat.format(
258                    RESOURCES.getString("parseErrMsg"), DESCRIPTION, in.getLineNumber()));
259        }
260
261        return comp;
262    }
263
264    /**
265     * Reads a single array or network from an input reader (pointing to a POI file). This
266     * method assumes that the stream starts immediately after the line containing the
267     * component type in a POI file (two lines after the $POI line).
268     *
269     * @param in Reader for the POI file we are reading (positioned so that the next read
270     *           will occur on the line following the line identifying component type).
271     * @return The network read in from the file.
272     * @throws java.io.IOException if there is a problem reading from the input reader.
273     */
274    private PointArray readNetwork(LineNumberReader in) throws IOException {
275        PointArray net = null;
276
277        // Create the needed tables.
278        FastTable<Point> pointList = FastTable.newInstance();
279        FastTable<PointString<Point>> stringList = FastTable.newInstance();
280
281        try {
282            // Read in the number of rows and columns.
283            String aLine = readLine(in);
284            TextTokenizer tokenizer = TextTokenizer.valueOf(aLine, " ");
285            if (tokenizer.countTokens() != 3)
286                throw new IOException(MessageFormat.format(
287                        RESOURCES.getString("parseErrMsg"), DESCRIPTION, in.getLineNumber()));
288
289            Text token = tokenizer.nextToken();
290            int numCols = (int)TypeFormat.parseDouble(token);
291
292            token = tokenizer.nextToken();
293            int numRows = (int)TypeFormat.parseDouble(token);
294
295            // Do a sanity check.
296            if (numRows < 0 || numRows > 1000000 || numCols < 0 || numCols > 1000000)
297                throw new IOException(INC_ROWS_COLS + in.getLineNumber() + ".");
298
299            // Read in the name of this network.
300            Text name = tokenizer.nextToken();
301
302            if (DEBUG)
303                System.out.println("name = " + name + ", numRows = " + numRows + ", numCols = " + numCols);
304
305            // Loop over each row.
306            for (int i = 0; i < numRows; ++i) {
307                if (DEBUG)
308                    System.out.println("row = " + i);
309                aLine = readLine(in);
310                tokenizer.setText(aLine);
311                if (tokenizer.countTokens() != 3 && tokenizer.countTokens() != 6)
312                    throw new IOException(MessageFormat.format(
313                            RESOURCES.getString("incPointCount"), in.getLineNumber()));
314
315                // Loop over each column.
316                int count = 0;
317                for (int j = 0; j < numCols; ++j) {
318
319                    // There are two points per line.
320                    if (count > 1) {
321                        count = 0;
322                        aLine = readLine(in);
323                        tokenizer.setText(aLine);
324                        if (tokenizer.countTokens() != 3 && tokenizer.countTokens() != 6)
325                            throw new IOException(MessageFormat.format(
326                                    RESOURCES.getString("incPointCount"), in.getLineNumber()));
327                    }
328
329                    // Read in X coordinate.
330                    token = tokenizer.nextToken();
331                    double xValue = TypeFormat.parseDouble(token);
332
333                    // Read in Y coordinate.
334                    token = tokenizer.nextToken();
335                    double yValue = TypeFormat.parseDouble(token);
336
337                    // Read in Z coordinate.
338                    token = tokenizer.nextToken();
339                    double zValue = TypeFormat.parseDouble(token);
340
341                    if (DEBUG)
342                        System.out.println("col = " + j + ", x,y,z = " + xValue + ", " + yValue + ", " + zValue);
343
344                    //  Create a Point object.
345                    Point point = Point.valueOf(xValue, yValue, zValue, getFileUnits());
346                    pointList.add(point);
347
348                    ++count;
349                }
350
351                //  Create a PointString object from the points.
352                PointString<Point> string = PointString.valueOf(null, pointList);
353                stringList.add(string);
354
355                //  Clear the point list for the next row.
356                pointList.clear();
357            }
358
359            // Create a new network from the points just read in.
360            net = PointArray.valueOf(null, stringList);
361
362        } catch (NumberFormatException e) {
363            e.printStackTrace();
364            throw new IOException(MessageFormat.format(
365                    RESOURCES.getString("parseErrMsg"), DESCRIPTION, in.getLineNumber()));
366
367        } finally {
368            //  Recycle the lists.
369            FastTable.recycle(pointList);
370            FastTable.recycle(stringList);
371        }
372
373        return net;
374    }
375
376}