View Javadoc
1   /*
2    * Copyright (C) 2012 Alberto Irurueta Carro (alberto@irurueta.com)
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *         http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package com.irurueta.geometry.io;
17  
18  import com.irurueta.geometry.Point3D;
19  import com.irurueta.geometry.Triangle3D;
20  import com.irurueta.geometry.Triangulator3D;
21  import com.irurueta.geometry.TriangulatorException;
22  
23  import java.io.File;
24  import java.io.IOException;
25  import java.util.ArrayList;
26  import java.util.Collections;
27  import java.util.HashSet;
28  import java.util.LinkedList;
29  import java.util.List;
30  import java.util.Set;
31  import java.util.TreeMap;
32  
33  /**
34   * Loads an OBJ file.
35   * If a LoaderListenerOBJ is provided, this class might also attempt to load the
36   * associated material file if available.
37   */
38  public class LoaderOBJ extends Loader {
39  
40      /**
41       * Constant defining the default value of maximum number of vertices to keep
42       * in a chunk. This is 65535, which corresponds to the maximum value allowed
43       * by graphical layer such as OpenGL when working with Vertex Buffer Objects.
44       */
45      public static final int DEFAULT_MAX_VERTICES_IN_CHUNK = 0xffff;
46  
47      /**
48       * Minimum allowed value for maximum number of vertices in chunk, which is
49       * one.
50       */
51      public static final int MIN_MAX_VERTICES_IN_CHUNK = 1;
52  
53      /**
54       * Constant indicating that duplicated vertices are allowed by default,
55       * which allows faster loading.
56       */
57      public static final boolean DEFAULT_ALLOW_DUPLICATE_VERTICES_IN_CHUNK = true;
58  
59      /**
60       * Maximum number of stream positions to be cached by default.
61       */
62      public static final int DEFAULT_MAX_STREAM_POSITIONS = 1000000;
63  
64      /**
65       * Minimum allowed number of stream positions.
66       */
67      public static final int MIN_STREAM_POSITIONS = 1;
68  
69      /**
70       * Amount of progress variation (1%) used to notify progress.
71       */
72      public static final float PROGRESS_DELTA = 0.01f;
73  
74      /**
75       * Indicates that loading should continue even if triangulation of some
76       * polygons fails.
77       */
78      public static final boolean DEFAULT_CONTINUE_IF_TRIANGULATION_ERROR = true;
79  
80      /**
81       * Identifies materials.
82       */
83      private static final String USEMTL = "usemtl ";
84  
85      /**
86       * Iterator to load OBJ file data in small chunks.
87       * Usually data is divided in chunks that can be directly loaded by
88       * graphic layers such as OpenGL.
89       */
90      private LoaderIteratorOBJ loaderIterator;
91  
92      /**
93       * Maximum number of vertices allowed in a chunk. Once this value is
94       * exceeded when loading a file, a new chunk of data is created.
95       */
96      private int maxVerticesInChunk;
97  
98      /**
99       * To allow faster file loading, it might be allowed to repeat points in a
100      * chunk. When representing data graphically, this has no visual.
101      * consequences but chunks will take up more memory. This value represents
102      * a trade-off between loading speed and memory usage.
103      */
104     private boolean allowDuplicateVerticesInChunk;
105 
106     /**
107      * Maximum number of file stream positions to be cached.
108      * This class keeps a cache of positions in the file to allow faster file
109      * loading at the expense of larger memory usage.
110      * If the geometry of a file reuses a large number of points, keeping a
111      * large cache will increase the speed of loading a file, otherwise the
112      * impact of this parameter will be low.
113      * The default value will work fine for most cases.
114      */
115     private long maxStreamPositions;
116 
117     /**
118      * List containing comments contained in the file.
119      */
120     private final List<String> comments;
121 
122     /**
123      * Collection of materials contained in the material's file associated to an
124      * OBJ file.
125      */
126     private Set<Material> materials;
127 
128     /**
129      * Determines if file loading should continue even if the triangulation of
130      * a polygon fails. The triangulation of a polygon might fail if the polygon
131      * is degenerate or has invalid numerical values such as NaN of infinity.
132      * If true, loading will continue but the result will lack the polygons that
133      * failed.
134      */
135     private boolean continueIfTriangulationError;
136 
137     /**
138      * Constructor.
139      */
140     public LoaderOBJ() {
141         loaderIterator = null;
142         maxVerticesInChunk = DEFAULT_MAX_VERTICES_IN_CHUNK;
143         allowDuplicateVerticesInChunk = DEFAULT_ALLOW_DUPLICATE_VERTICES_IN_CHUNK;
144         maxStreamPositions = DEFAULT_MAX_STREAM_POSITIONS;
145         comments = new LinkedList<>();
146         continueIfTriangulationError = DEFAULT_CONTINUE_IF_TRIANGULATION_ERROR;
147     }
148 
149     /**
150      * Constructor.
151      *
152      * @param maxVerticesInChunk Maximum number of vertices allowed in a chunk.
153      *                           Once this value is exceeded when loading a file, a new chunk of data is
154      *                           created.
155      * @throws IllegalArgumentException if maximum number of vertices allowed in
156      *                                  a chunk is lower than 1.
157      */
158     public LoaderOBJ(final int maxVerticesInChunk) {
159         loaderIterator = null;
160         internalSetMaxVerticesInChunk(maxVerticesInChunk);
161         allowDuplicateVerticesInChunk = DEFAULT_ALLOW_DUPLICATE_VERTICES_IN_CHUNK;
162         maxStreamPositions = DEFAULT_MAX_STREAM_POSITIONS;
163         comments = new LinkedList<>();
164         continueIfTriangulationError = DEFAULT_CONTINUE_IF_TRIANGULATION_ERROR;
165     }
166 
167     /**
168      * Constructor.
169      *
170      * @param maxVerticesInChunk            Maximum number of vertices allowed in a chunk.
171      *                                      Once this value is exceeded when loading a file, a new chunk of data is
172      *                                      created.
173      * @param allowDuplicateVerticesInChunk indicates if repeated vertices in a
174      *                                      chunk are allowed to provide faster file loading. When representing data
175      *                                      graphically, this has no visual consequences but chunks will take up more
176      *                                      memory.
177      * @throws IllegalArgumentException if maximum number of vertices allowed in
178      *                                  a chunk is lower than 1.
179      */
180     public LoaderOBJ(final int maxVerticesInChunk, final boolean allowDuplicateVerticesInChunk) {
181         loaderIterator = null;
182         internalSetMaxVerticesInChunk(maxVerticesInChunk);
183         this.allowDuplicateVerticesInChunk = allowDuplicateVerticesInChunk;
184         maxStreamPositions = DEFAULT_MAX_STREAM_POSITIONS;
185         comments = new LinkedList<>();
186         continueIfTriangulationError = DEFAULT_CONTINUE_IF_TRIANGULATION_ERROR;
187     }
188 
189     /**
190      * Constructor.
191      *
192      * @param maxVerticesInChunk            Maximum number of vertices allowed in a chunk.
193      *                                      Once this value is exceeded when loading a file, a new chunk of data is
194      *                                      created.
195      * @param allowDuplicateVerticesInChunk indicates if repeated vertices in a
196      *                                      chunk are allowed to provide faster file loading. When representing data
197      *                                      graphically, this has no visual consequences but chunks will take up more
198      *                                      memory.
199      * @param maxStreamPositions            Maximum number of file stream positions to be
200      *                                      cached.
201      * @throws IllegalArgumentException if maximum number of vertices allowed in
202      *                                  a chunk is lower than 1.
203      */
204     public LoaderOBJ(final int maxVerticesInChunk, final boolean allowDuplicateVerticesInChunk,
205                      final long maxStreamPositions) {
206         loaderIterator = null;
207         internalSetMaxVerticesInChunk(maxVerticesInChunk);
208         this.allowDuplicateVerticesInChunk = allowDuplicateVerticesInChunk;
209         internalSetMaxStreamPositions(maxStreamPositions);
210         comments = new LinkedList<>();
211         continueIfTriangulationError = DEFAULT_CONTINUE_IF_TRIANGULATION_ERROR;
212     }
213 
214     /**
215      * Constructor.
216      *
217      * @param f file to be loaded.
218      * @throws IOException if an I/O error occurs.
219      */
220     public LoaderOBJ(final File f) throws IOException {
221         super(f);
222         loaderIterator = null;
223         maxVerticesInChunk = DEFAULT_MAX_VERTICES_IN_CHUNK;
224         allowDuplicateVerticesInChunk = DEFAULT_ALLOW_DUPLICATE_VERTICES_IN_CHUNK;
225         maxStreamPositions = DEFAULT_MAX_STREAM_POSITIONS;
226         comments = new LinkedList<>();
227         continueIfTriangulationError = DEFAULT_CONTINUE_IF_TRIANGULATION_ERROR;
228     }
229 
230     /**
231      * Constructor.
232      *
233      * @param f                  file to be loaded.
234      * @param maxVerticesInChunk Maximum number of vertices allowed in a chunk.
235      *                           Once this value is exceeded when loading a file, a new chunk of data is
236      *                           created.
237      * @throws IllegalArgumentException if maximum number of vertices allowed in
238      *                                  a chunk is lower than 1.
239      * @throws IOException              if an I/O error occurs.
240      */
241     public LoaderOBJ(final File f, final int maxVerticesInChunk) throws IOException {
242         super(f);
243         loaderIterator = null;
244         internalSetMaxVerticesInChunk(maxVerticesInChunk);
245         allowDuplicateVerticesInChunk = DEFAULT_ALLOW_DUPLICATE_VERTICES_IN_CHUNK;
246         maxStreamPositions = DEFAULT_MAX_STREAM_POSITIONS;
247         comments = new LinkedList<>();
248         continueIfTriangulationError = DEFAULT_CONTINUE_IF_TRIANGULATION_ERROR;
249     }
250 
251     /**
252      * Constructor.
253      *
254      * @param f                             file to be loaded.
255      * @param maxVerticesInChunk            Maximum number of vertices allowed in a chunk.
256      *                                      Once this value is exceeded when loading a file, a new chunk of data is
257      *                                      created.
258      * @param allowDuplicateVerticesInChunk indicates if repeated vertices in a
259      *                                      chunk are allowed to provide faster file loading. When representing data
260      *                                      graphically, this has no visual consequences but chunks will take up more
261      *                                      memory.
262      * @throws IllegalArgumentException if maximum number of vertices allowed in
263      *                                  a chunk is lower than 1.
264      * @throws IOException              if an I/O error occurs.
265      */
266     public LoaderOBJ(final File f, final int maxVerticesInChunk, final boolean allowDuplicateVerticesInChunk)
267             throws IOException {
268         super(f);
269         loaderIterator = null;
270         internalSetMaxVerticesInChunk(maxVerticesInChunk);
271         this.allowDuplicateVerticesInChunk = allowDuplicateVerticesInChunk;
272         maxStreamPositions = DEFAULT_MAX_STREAM_POSITIONS;
273         comments = new LinkedList<>();
274         continueIfTriangulationError = DEFAULT_CONTINUE_IF_TRIANGULATION_ERROR;
275     }
276 
277     /**
278      * Constructor.
279      *
280      * @param f                             file to be loaded.
281      * @param maxVerticesInChunk            Maximum number of vertices allowed in a chunk.
282      *                                      Once this value is exceeded when loading a file, a new chunk of data is
283      *                                      created.
284      * @param allowDuplicateVerticesInChunk indicates if repeated vertices in a
285      *                                      chunk are allowed to provide faster file loading. When representing data
286      *                                      graphically, this has no visual consequences but chunks will take up more
287      *                                      memory.
288      * @param maxStreamPositions            Maximum number of file stream positions to be
289      *                                      cached.
290      * @throws IllegalArgumentException if maximum number of vertices allowed in
291      *                                  a chunk is lower than 1.
292      * @throws IOException              if an I/O error occurs.
293      */
294     public LoaderOBJ(final File f, final int maxVerticesInChunk, final boolean allowDuplicateVerticesInChunk,
295                      final long maxStreamPositions) throws IOException {
296         super(f);
297         loaderIterator = null;
298         internalSetMaxVerticesInChunk(maxVerticesInChunk);
299         this.allowDuplicateVerticesInChunk = allowDuplicateVerticesInChunk;
300         internalSetMaxStreamPositions(maxStreamPositions);
301         comments = new LinkedList<>();
302         continueIfTriangulationError = DEFAULT_CONTINUE_IF_TRIANGULATION_ERROR;
303     }
304 
305     /**
306      * Constructor.
307      *
308      * @param listener listener to be notified of loading progress and when
309      *                 loading process starts or finishes.
310      */
311     public LoaderOBJ(final LoaderListener listener) {
312         super(listener);
313         maxVerticesInChunk = DEFAULT_MAX_VERTICES_IN_CHUNK;
314         allowDuplicateVerticesInChunk = DEFAULT_ALLOW_DUPLICATE_VERTICES_IN_CHUNK;
315         maxStreamPositions = DEFAULT_MAX_STREAM_POSITIONS;
316         comments = new LinkedList<>();
317         continueIfTriangulationError = DEFAULT_CONTINUE_IF_TRIANGULATION_ERROR;
318     }
319 
320     /**
321      * Constructor.
322      *
323      * @param listener           listener to be notified of loading progress and when
324      *                           loading process starts or finishes.
325      * @param maxVerticesInChunk Maximum number of vertices allowed in a chunk.
326      *                           Once this value is exceeded when loading a file, a new chunk of data is
327      *                           created.
328      * @throws IllegalArgumentException if maximum number of vertices allowed in
329      *                                  a chunk is lower than 1.
330      */
331     public LoaderOBJ(final LoaderListener listener, final int maxVerticesInChunk) {
332         super(listener);
333         loaderIterator = null;
334         internalSetMaxVerticesInChunk(maxVerticesInChunk);
335         allowDuplicateVerticesInChunk = DEFAULT_ALLOW_DUPLICATE_VERTICES_IN_CHUNK;
336         maxStreamPositions = DEFAULT_MAX_STREAM_POSITIONS;
337         comments = new LinkedList<>();
338         continueIfTriangulationError = DEFAULT_CONTINUE_IF_TRIANGULATION_ERROR;
339     }
340 
341     /**
342      * Constructor.
343      *
344      * @param listener                      listener to be notified of loading progress and when
345      *                                      loading process starts or finishes.
346      * @param maxVerticesInChunk            Maximum number of vertices allowed in a chunk.
347      *                                      Once this value is exceeded when loading a file, a new chunk of data is
348      *                                      created.
349      * @param allowDuplicateVerticesInChunk indicates if repeated vertices in a
350      *                                      chunk are allowed to provide faster file loading. When representing data
351      *                                      graphically, this has no visual consequences but chunks will take up more
352      *                                      memory.
353      * @throws IllegalArgumentException if maximum number of vertices allowed in
354      *                                  a chunk is lower than 1.
355      */
356     public LoaderOBJ(final LoaderListener listener, final int maxVerticesInChunk,
357                      final boolean allowDuplicateVerticesInChunk) {
358         super(listener);
359         loaderIterator = null;
360         internalSetMaxVerticesInChunk(maxVerticesInChunk);
361         this.allowDuplicateVerticesInChunk = allowDuplicateVerticesInChunk;
362         maxStreamPositions = DEFAULT_MAX_STREAM_POSITIONS;
363         comments = new LinkedList<>();
364         continueIfTriangulationError = DEFAULT_CONTINUE_IF_TRIANGULATION_ERROR;
365     }
366 
367     /**
368      * Constructor.
369      *
370      * @param listener                      listener to be notified of loading progress and when
371      *                                      loading process starts or finishes.
372      * @param maxVerticesInChunk            Maximum number of vertices allowed in a chunk.
373      *                                      Once this value is exceeded when loading a file, a new chunk of data is
374      *                                      created.
375      * @param allowDuplicateVerticesInChunk indicates if repeated vertices in a
376      *                                      chunk are allowed to provide faster file loading. When representing data
377      *                                      graphically, this has no visual consequences but chunks will take up more
378      *                                      memory.
379      * @param maxStreamPositions            Maximum number of file stream positions to be
380      *                                      cached.
381      * @throws IllegalArgumentException if maximum number of vertices allowed in
382      *                                  a chunk is lower than 1.
383      */
384     public LoaderOBJ(final LoaderListener listener, final int maxVerticesInChunk,
385                      final boolean allowDuplicateVerticesInChunk, final long maxStreamPositions) {
386         super(listener);
387         loaderIterator = null;
388         internalSetMaxVerticesInChunk(maxVerticesInChunk);
389         this.allowDuplicateVerticesInChunk = allowDuplicateVerticesInChunk;
390         internalSetMaxStreamPositions(maxStreamPositions);
391         comments = new LinkedList<>();
392         continueIfTriangulationError = DEFAULT_CONTINUE_IF_TRIANGULATION_ERROR;
393     }
394 
395     /**
396      * Constructor.
397      *
398      * @param f        file to be loaded.
399      * @param listener listener to be notified of loading progress and when
400      *                 loading process starts or finishes.
401      * @throws IOException if an I/O error occurs.
402      */
403     public LoaderOBJ(final File f, final LoaderListener listener) throws IOException {
404         super(f, listener);
405         loaderIterator = null;
406         maxVerticesInChunk = DEFAULT_MAX_VERTICES_IN_CHUNK;
407         allowDuplicateVerticesInChunk = DEFAULT_ALLOW_DUPLICATE_VERTICES_IN_CHUNK;
408         maxStreamPositions = DEFAULT_MAX_STREAM_POSITIONS;
409         comments = new LinkedList<>();
410         continueIfTriangulationError = DEFAULT_CONTINUE_IF_TRIANGULATION_ERROR;
411     }
412 
413     /**
414      * Constructor.
415      *
416      * @param f                  file to be loaded.
417      * @param listener           listener to be notified of loading progress and when
418      *                           loading process starts or finishes.
419      * @param maxVerticesInChunk Maximum number of vertices allowed in a chunk.
420      *                           Once this value is exceeded when loading a file, a new chunk of data is
421      *                           created.
422      * @throws IllegalArgumentException if maximum number of vertices allowed in
423      *                                  a chunk is lower than 1.
424      * @throws IOException              if an I/O error occurs.
425      */
426     public LoaderOBJ(final File f, final LoaderListener listener, final int maxVerticesInChunk) throws IOException {
427         super(f, listener);
428         loaderIterator = null;
429         internalSetMaxVerticesInChunk(maxVerticesInChunk);
430         allowDuplicateVerticesInChunk = DEFAULT_ALLOW_DUPLICATE_VERTICES_IN_CHUNK;
431         maxStreamPositions = DEFAULT_MAX_STREAM_POSITIONS;
432         comments = new LinkedList<>();
433         continueIfTriangulationError = DEFAULT_CONTINUE_IF_TRIANGULATION_ERROR;
434     }
435 
436     /**
437      * Constructor.
438      *
439      * @param f                             file to be loaded.
440      * @param listener                      listener to be notified of loading progress and when
441      *                                      loading process starts or finishes.
442      * @param maxVerticesInChunk            Maximum number of vertices allowed in a chunk.
443      *                                      Once this value is exceeded when loading a file, a new chunk of data is
444      *                                      created.
445      * @param allowDuplicateVerticesInChunk indicates if repeated vertices in a
446      *                                      chunk are allowed to provide faster file loading. When representing data
447      *                                      graphically, this has no visual consequences but chunks will take up more
448      *                                      memory.
449      * @throws IllegalArgumentException if maximum number of vertices allowed in
450      *                                  a chunk is lower than 1.
451      * @throws IOException              if an I/O error occurs.
452      */
453     public LoaderOBJ(final File f, final LoaderListener listener, final int maxVerticesInChunk,
454                      final boolean allowDuplicateVerticesInChunk) throws IOException {
455         super(f, listener);
456         loaderIterator = null;
457         internalSetMaxVerticesInChunk(maxVerticesInChunk);
458         this.allowDuplicateVerticesInChunk = allowDuplicateVerticesInChunk;
459         maxStreamPositions = DEFAULT_MAX_STREAM_POSITIONS;
460         comments = new LinkedList<>();
461         continueIfTriangulationError = DEFAULT_CONTINUE_IF_TRIANGULATION_ERROR;
462     }
463 
464     /**
465      * Constructor.
466      *
467      * @param f                             file to be loaded.
468      * @param listener                      listener to be notified of loading progress and when
469      *                                      loading process starts or finishes.
470      * @param maxVerticesInChunk            Maximum number of vertices allowed in a chunk.
471      *                                      Once this value is exceeded when loading a file, a new chunk of data is
472      *                                      created.
473      * @param allowDuplicateVerticesInChunk indicates if repeated vertices in a
474      *                                      chunk are allowed to provide faster file loading. When representing data
475      *                                      graphically, this has no visual consequences but chunks will take up more
476      *                                      memory.
477      * @param maxStreamPositions            Maximum number of file stream positions to be
478      *                                      cached.
479      * @throws IllegalArgumentException if maximum number of vertices allowed in
480      *                                  a chunk is lower than 1.
481      * @throws IOException              if an I/O error occurs.
482      */
483     public LoaderOBJ(final File f, final LoaderListener listener, final int maxVerticesInChunk,
484                      final boolean allowDuplicateVerticesInChunk, final long maxStreamPositions) throws IOException {
485         super(f, listener);
486         loaderIterator = null;
487         internalSetMaxVerticesInChunk(maxVerticesInChunk);
488         this.allowDuplicateVerticesInChunk = allowDuplicateVerticesInChunk;
489         internalSetMaxStreamPositions(maxStreamPositions);
490         comments = new LinkedList<>();
491         continueIfTriangulationError = DEFAULT_CONTINUE_IF_TRIANGULATION_ERROR;
492     }
493 
494     /**
495      * Returns maximum number of vertices allowed in a chunk.
496      * Once this value is exceeded when loading a file, a new chunk of data is
497      * created.
498      *
499      * @return maximum number of vertices allowed in a chunk.
500      */
501     public int getMaxVerticesInChunk() {
502         return maxVerticesInChunk;
503     }
504 
505     /**
506      * Sets maximum number of vertices allowed in a chunk.
507      * Once this value is exceeded when loading a file, a new chunk of data is
508      * created.
509      *
510      * @param maxVerticesInChunk maximum allowed number of vertices to be set.
511      * @throws IllegalArgumentException if provided value is lower than 1.
512      * @throws LockedException          if this loader is currently loading a file.
513      */
514     public void setMaxVerticesInChunk(final int maxVerticesInChunk) throws LockedException {
515         if (isLocked()) {
516             throw new LockedException();
517         }
518         internalSetMaxVerticesInChunk(maxVerticesInChunk);
519     }
520 
521     /**
522      * Returns boolean indicating if repeated vertices in a chunk are allowed to
523      * provide faster file loading. When representing data graphically, this has
524      * no visual consequences but chunks will take up more memory.
525      *
526      * @return true if duplicate vertices are allowed, false otherwise.
527      */
528     public boolean areDuplicateVerticesInChunkAllowed() {
529         return allowDuplicateVerticesInChunk;
530     }
531 
532     /**
533      * Sets boolean indicating if repeated vertices in a chunk are allowed to
534      * provide faster file loading. When representing data graphically, this has
535      * no visual consequences but chunks will take up more memory.
536      *
537      * @param allow true if duplicate vertices are allowed, false otherwise.
538      * @throws LockedException if this loader is currently loading a file.
539      */
540     public void setAllowDuplicateVerticesInChunk(final boolean allow) throws LockedException {
541         if (isLocked()) {
542             throw new LockedException();
543         }
544         allowDuplicateVerticesInChunk = allow;
545     }
546 
547     /**
548      * Returns maximum number of file stream positions to be cached.
549      * This class keeps a cache of positions in the file to allow faster file
550      * loading at the expense of larger memory usage.
551      * If the geometry of a file reuses a large number of points, keeping a
552      * large cache will increase the speed of loading a file, otherwise the
553      * impact of this parameter will be low.
554      * The default value will work fine for most cases.
555      *
556      * @return maximum number of file stream positions to be cached.
557      */
558     public long getMaxStreamPositions() {
559         return maxStreamPositions;
560     }
561 
562     /**
563      * Sets maximum number of file stream positions to be cached.
564      * This class keeps a cache of positions in the file to allow faster file
565      * loading at the expense of larger memory usage.
566      * If the geometry of a file reuses a large number of points, keeping a
567      * large cache will increase the speed of loading a file, otherwise the
568      * impact of this parameter will be low.
569      * The default value will work fine for most cases.
570      *
571      * @param maxStreamPositions maximum number of file stream positions to be
572      *                           set.
573      * @throws IllegalArgumentException if provided value is lower than 1.
574      * @throws LockedException          if this loader is currently loading a file.
575      */
576     public void setMaxStreamPositions(final long maxStreamPositions) throws LockedException {
577         if (isLocked()) {
578             throw new LockedException();
579         }
580         internalSetMaxStreamPositions(maxStreamPositions);
581     }
582 
583     /**
584      * Returns boolean indicating if file loading should continue even if the
585      * triangulation of a polygon fails. The triangulation of a polygon might
586      * fail if the polygon is degenerate or has invalid numerical values such as
587      * NaN of infinity.
588      *
589      * @return If true, loading will continue but the result will lack the
590      * polygons that failed.
591      */
592     public boolean isContinueIfTriangulationError() {
593         return continueIfTriangulationError;
594     }
595 
596     /**
597      * Sets boolean indicating if file loading should continue even if the
598      * triangulation of a polygon fails. The triangulation of a polygon might
599      * fail if the polygon is degenerate or has invalid numerical values such as
600      * NaN or infinity.
601      *
602      * @param continueIfTriangulationError if ture, loading will continue but
603      *                                     the result will lack the polygons that failed.
604      */
605     public void setContinueIfTriangulationError(final boolean continueIfTriangulationError) {
606         this.continueIfTriangulationError = continueIfTriangulationError;
607     }
608 
609     /**
610      * Returns a list of the comments contained in the file.
611      *
612      * @return list of the comments contained in the file.
613      */
614     public List<String> getComments() {
615         return Collections.unmodifiableList(comments);
616     }
617 
618     /**
619      * Gets collection of materials contained in the materials file associated to an
620      * OBJ file.
621      *
622      * @return collection of material.
623      */
624     public Set<Material> getMaterials() {
625         return materials;
626     }
627 
628     /**
629      * If loader is ready to start loading a file.
630      * This is true once a file has been provided.
631      *
632      * @return true if ready to start loading a file, false otherwise.
633      */
634     @Override
635     public boolean isReady() {
636         return hasFile();
637     }
638 
639     /**
640      * Returns mesh format supported by this class, which is MESH_FORMAT_OBJ.
641      *
642      * @return mesh format supported by this class.
643      */
644     @Override
645     public MeshFormat getMeshFormat() {
646         return MeshFormat.MESH_FORMAT_OBJ;
647     }
648 
649     /**
650      * Determines if provided file is a valid file that can be read by this
651      * loader.
652      *
653      * @return true if file is valid, false otherwise.
654      * @throws LockedException raised if this instance is already locked.
655      * @throws IOException     if an I/O error occurs..
656      */
657     @Override
658     public boolean isValidFile() throws LockedException, IOException {
659         if (!hasFile()) {
660             throw new IOException();
661         }
662         if (isLocked()) {
663             throw new LockedException();
664         }
665         return true;
666     }
667 
668     /**
669      * Starts the loading process of provided file.
670      * This method returns a LoaderIterator to start the iterative process to
671      * load a file in small chunks of data.
672      *
673      * @return a loader iterator to read the file in a step-by-step process.
674      * @throws LockedException   raised if this instance is already locked.
675      * @throws NotReadyException raised if this instance is not yet ready.
676      * @throws IOException       if an I/O error occurs.
677      * @throws LoaderException   if file is corrupted or cannot be interpreted.
678      */
679     @Override
680     public LoaderIterator load() throws LockedException, NotReadyException, IOException, LoaderException {
681         if (isLocked()) {
682             throw new LockedException();
683         }
684         if (!isReady()) {
685             throw new NotReadyException();
686         }
687 
688         setLocked(true);
689         if (listener != null) {
690             listener.onLoadStart(this);
691         }
692 
693         loaderIterator = new LoaderIteratorOBJ(this);
694         loaderIterator.setListener(new LoaderIteratorListenerImpl(this));
695         return loaderIterator;
696     }
697 
698     /**
699      * Internal method to set maximum number of vertices allowed in a chunk.
700      * This method is reused both in the constructor and in the setter of
701      * maximum number of vertices allowed in a chunk.
702      *
703      * @param maxVerticesInChunk maximum allowed number of vertices to be set.
704      * @throws IllegalArgumentException if provided value is lower than 1.
705      */
706     private void internalSetMaxVerticesInChunk(final int maxVerticesInChunk) {
707         if (maxVerticesInChunk < MIN_MAX_VERTICES_IN_CHUNK) {
708             throw new IllegalArgumentException();
709         }
710 
711         this.maxVerticesInChunk = maxVerticesInChunk;
712     }
713 
714     /**
715      * Internal method to set maximum number of file stream positions to be
716      * cached.
717      * This method is reused both in the constructor and in the setter of
718      * maximum number stream positions.
719      *
720      * @param maxStreamPositions maximum number of file stream positions to be
721      *                           cached.
722      * @throws IllegalArgumentException if provided value is lower than 1.
723      */
724     private void internalSetMaxStreamPositions(final long maxStreamPositions) {
725         if (maxStreamPositions < MIN_STREAM_POSITIONS) {
726             throw new IllegalArgumentException();
727         }
728 
729         this.maxStreamPositions = maxStreamPositions;
730     }
731 
732     /**
733      * Internal listener to be notified when loading process finishes.
734      * This listener is used to free resources when loading process finishes.
735      */
736     private class LoaderIteratorListenerImpl implements LoaderIteratorListener {
737 
738         /**
739          * Reference to Loader loading an OBJ file.
740          */
741         private final LoaderOBJ loader;
742 
743         /**
744          * Constructor.
745          *
746          * @param loader reference to Loader.
747          */
748         public LoaderIteratorListenerImpl(final LoaderOBJ loader) {
749             this.loader = loader;
750         }
751 
752         /**
753          * Method to be notified when the loading process finishes.
754          *
755          * @param iterator iterator loading the file in chunks.
756          */
757         @Override
758         public void onIteratorFinished(final LoaderIterator iterator) {
759             // because iterator is finished, we should allow subsequent calls to
760             // load method
761             try {
762                 // attempt restart stream to initial position
763                 reader.seek(0);
764             } catch (final Exception ignore) {
765                 // this is the best effort operation, if it fails it is ignored
766             }
767 
768             // on subsequent calls
769             if (listener != null) {
770                 listener.onLoadEnd(loader);
771             }
772             setLocked(false);
773         }
774     }
775 
776     /**
777      * Loader iterator in charge of loading file data in small chunks.
778      * Usually data is divided in chunks small enough that can be directly
779      * loaded by graphical layers such as OpenGL (which has a limit of 65535
780      * indices when using Vertex Buffer Objects, which increase graphical
781      * performance).
782      */
783     private class LoaderIteratorOBJ implements LoaderIterator {
784 
785         /**
786          * Reference to loader loading OBJ file.
787          */
788         private final LoaderOBJ loader;
789 
790         /**
791          * X coordinate of the latest point that has been read.
792          */
793         private float coordX;
794 
795         /**
796          * Y coordinate of the latest point that has been read.
797          */
798         private float coordY;
799 
800         /**
801          * Z coordinate of the latest point that has been read.
802          */
803         private float coordZ;
804 
805         /**
806          * U texture coordinate of the latest point that has been read.
807          * U coordinate refers to the horizontal axis in the texture image and
808          * usually is a normalized value between 0.0 and 1.0. Larger values can
809          * be used to repeat textures, negative values can be used to reverse
810          * textures.
811          */
812         private float textureU;
813 
814         /**
815          * V texture coordinate of the latest point that has been read.
816          * V coordinate refers to the vertical axis in the texture image and
817          * usually is a normalized value between 0.0 and 1.0. Larger values can
818          * be used to repeat textures, negative values can be used to reverse
819          * textures.
820          */
821         private float textureV;
822 
823         /**
824          * X coordinate of the latest point normal that has been read.
825          */
826         private float nX;
827 
828         /**
829          * Y coordinate of the latest point normal that has been read.
830          */
831         private float nY;
832 
833         /**
834          * Z coordinate of the latest point normal that has been read.
835          */
836         private float nZ;
837 
838         /**
839          * Vertex index in the file of the latest point that has been read.
840          */
841         private int vertexIndex;
842 
843         /**
844          * Texture index in the file of the latest point that has been read.
845          */
846         private int textureIndex;
847 
848         /**
849          * Normal index in the file of the latest point that has been read.
850          */
851         private int normalIndex;
852 
853         // coordinates for bounding box in a chunk
854 
855         /**
856          * X coordinate of the minimum point forming the bounding box in a chunk
857          * of data. This value will be updated while the chunk is being filled.
858          */
859         private float minX;
860 
861         /**
862          * Y coordinate of the minimum point forming the bounding box in a chunk
863          * of data. This value will be updated while the chunk is being filled.
864          */
865         private float minY;
866 
867         /**
868          * Z coordinate of the minimum point forming the bounding box in a chunk
869          * of data. This value will be updated while the chunk is being filled.
870          */
871         private float minZ;
872 
873         /**
874          * X coordinate of the maximum point forming the bounding box in a chunk
875          * of data. This value will be updated while the chunk is being filled.
876          */
877         private float maxX;
878 
879         /**
880          * Y coordinate of the maximum point forming the bounding box in a chunk
881          * of data. This value will be updated while the chunk is being filled.
882          */
883         private float maxY;
884 
885         /**
886          * Z coordinate of the maximum point forming the bounding box in a chunk
887          * of data. This value will be updated while the chunk is being filled.
888          */
889         private float maxZ;
890 
891         /**
892          * Indicates if vertices have been loaded and must be added to current
893          * chunk being loaded.
894          */
895         private boolean verticesAvailable;
896 
897         /**
898          * Indicates if texture coordinates have been loaded and must be added
899          * to current chunk being loaded.
900          */
901         private boolean textureAvailable;
902 
903         /**
904          * Indicates if normals have been loaded and must be added to current
905          * chunk being loaded.
906          */
907         private boolean normalsAvailable;
908 
909         /**
910          * Indicates if indices have been loaded and must be added to current
911          * chunk being loaded.
912          */
913         private boolean indicesAvailable;
914 
915         /**
916          * Indicates if materials have been loaded and must be added to current
917          * chunk being loaded.
918          */
919         private boolean materialsAvailable;
920 
921         /**
922          * Number of vertices that have been loaded in current chunk.
923          */
924         private long numberOfVertices;
925 
926         /**
927          * Number of texture coordinates that have been loaded in current chunk.
928          */
929         private long numberOfTextureCoords;
930 
931         /**
932          * Number of normals that have been loaded in current chunk.
933          */
934         private long numberOfNormals;
935 
936         /**
937          * Number of faces (i.e. polygons) that have been loaded in current
938          * chunk.
939          */
940         private long numberOfFaces;
941 
942         /**
943          * Index of current face (i.e. polygon) that has been loaded.
944          */
945         private long currentFace;
946 
947         /**
948          * Position of first vertex in the file. This is stored to reduce
949          * fetching time when parsing the OBJ file.
950          */
951         private long firstVertexStreamPosition;
952 
953         /**
954          * Indicates if first vertex position has been found.
955          */
956         private boolean firstVertexStreamPositionAvailable;
957 
958         /**
959          * Position of first texture coordinate in the file. This is stored to
960          * reduce fetching time when parsing the OBJ file.
961          */
962         private long firstTextureCoordStreamPosition;
963 
964         /**
965          * Indicates if first texture coordinate has been found.
966          */
967         private boolean firstTextureCoordStreamPositionAvailable;
968 
969         /**
970          * Position of first normal coordinate in the file. This is stored to
971          * reduce fetching time when parsing the OBJ file.
972          */
973         private long firstNormalStreamPosition;
974 
975         /**
976          * Indicates if first normal coordinate has been found.
977          */
978         private boolean firstNormalStreamPositionAvailable;
979 
980         /**
981          * Position of first face (i.e. polygon) in the file. This is stored to
982          * reduce fetching time when parsing the OBJ file.
983          */
984         private long firstFaceStreamPosition;
985 
986         /**
987          * Indicates if first face has been found.
988          */
989         private boolean firstFaceStreamPositionAvailable;
990 
991         /**
992          * Indicates location of first material in the file. This is stored to
993          * reduce fetching time when parsing the OBJ file.
994          */
995         private long firstMaterialStreamPosition;
996 
997         /**
998          * Indicates if first material has been found.
999          */
1000         private boolean firstMaterialStreamPositionAvailable;
1001 
1002         /**
1003          * Contains position where file is currently being loaded.
1004          */
1005         private long currentStreamPosition;
1006 
1007         /**
1008          * Reference to the listener of this loader iterator. This listener will
1009          * be notified when the loading process finishes so that resources can
1010          * be freed.
1011          */
1012         private LoaderIteratorListener listener;
1013 
1014         /**
1015          * Array containing vertices coordinates to be added to current chunk
1016          * of data.
1017          */
1018         private float[] coordsInChunkArray;
1019 
1020         /**
1021          * Array containing texture coordinates to be added to current chunk of
1022          * data.
1023          */
1024         private float[] textureCoordsInChunkArray;
1025 
1026         /**
1027          * Array containing normal coordinates to be added to current chunk of
1028          * data.
1029          */
1030         private float[] normalsInChunkArray;
1031 
1032         /**
1033          * Array containing indices to be added to current chunk of data. Notice
1034          * that these indices are not the original indices appearing in the file.
1035          * Instead, they are indices referring to data in current chunk,
1036          * accounting for duplicate points, etc. This way, indices in a chunk
1037          * can be directly used to draw the chunk of data by the graphical layer.
1038          */
1039         private int[] indicesInChunkArray;
1040 
1041         /**
1042          * Array containing vertex indices as they appear in the OBJ file.
1043          * These indices are only used to fetch data, they will never appear in
1044          * resulting chunk of data.
1045          */
1046         private long[] originalVertexIndicesInChunkArray;
1047 
1048         /**
1049          * Array containing texture indices as they appear in the OBJ file.
1050          * These indices are only used to fetch data, they will never appear in
1051          * resulting chunk of data.
1052          */
1053         private long[] originalTextureIndicesInChunkArray;
1054 
1055         /**
1056          * Array containing normal indices as they appear in the OBJ file.
1057          * These indices are only used to fetch data, they will never appear in
1058          * resulting chunk of data.
1059          */
1060         private long[] originalNormalIndicesInChunkArray;
1061 
1062         /**
1063          * Map to relate vertex indices in a file respect to chunk indices.
1064          */
1065         private final TreeMap<Long, Integer> vertexIndicesMap;
1066 
1067         /**
1068          * Map to relate texture coordinates indices in a file respect to chunk
1069          * indices.
1070          */
1071         private final TreeMap<Long, Integer> textureCoordsIndicesMap;
1072 
1073         /**
1074          * Map to relate normals coordinates indices in a file respect to chunk
1075          * indices.
1076          */
1077         private final TreeMap<Long, Integer> normalsIndicesMap;
1078 
1079         /**
1080          * Map to cache vertex positions in a file.
1081          */
1082         private final TreeMap<Long, Long> verticesStreamPositionMap;
1083 
1084         /**
1085          * Map to cache texture coordinates positions in a file.
1086          */
1087         private final TreeMap<Long, Long> textureCoordsStreamPositionMap;
1088 
1089         /**
1090          * Map to cache normals coordinates positions in a file.
1091          */
1092         private final TreeMap<Long, Long> normalsStreamPositionMap;
1093 
1094         /**
1095          * Number of vertices stored in chunk.
1096          */
1097         private int verticesInChunk;
1098 
1099         /**
1100          * Number of indices stored in chunk.
1101          */
1102         private int indicesInChunk;
1103 
1104         /**
1105          * Size of indices stored in chunk.
1106          */
1107         private int indicesInChunkSize;
1108 
1109         /**
1110          * Vertex position in file.
1111          */
1112         private long vertexStreamPosition;
1113 
1114         /**
1115          * Texture coordinate position in file.
1116          */
1117         private long textureCoordStreamPosition;
1118 
1119         /**
1120          * Normal coordinate position in file.
1121          */
1122         private long normalStreamPosition;
1123 
1124         /**
1125          * Name of current material of data being loaded.
1126          */
1127         private String currentChunkMaterialName;
1128 
1129         /**
1130          * Reference to current material of data being loaded.
1131          */
1132         private MaterialOBJ currentMaterial;
1133 
1134         /**
1135          * Reference to material loader in charge of loading the associated MTL
1136          * of file to this OBJ file.
1137          */
1138         private MaterialLoaderOBJ materialLoader;
1139 
1140         /**
1141          * Constructor.
1142          *
1143          * @param loader reference to loader loading binary file.
1144          * @throws IOException     if an I/O error occurs.
1145          * @throws LoaderException if file data is corrupt or cannot be
1146          *                         understood.
1147          */
1148         public LoaderIteratorOBJ(final LoaderOBJ loader) throws IOException, LoaderException {
1149             this.loader = loader;
1150             nX = nY = nZ = 1.0f;
1151             vertexIndex = textureIndex = normalIndex = 0;
1152             verticesAvailable = textureAvailable = normalsAvailable = indicesAvailable = materialsAvailable = false;
1153             numberOfVertices = numberOfTextureCoords = numberOfNormals = numberOfFaces = 0;
1154             currentFace = 0;
1155             firstVertexStreamPosition = 0;
1156             firstVertexStreamPositionAvailable = false;
1157             firstTextureCoordStreamPosition = 0;
1158             firstTextureCoordStreamPositionAvailable = false;
1159             firstNormalStreamPosition = 0;
1160             firstNormalStreamPositionAvailable = false;
1161             firstFaceStreamPosition = 0;
1162             firstFaceStreamPositionAvailable = false;
1163             firstMaterialStreamPosition = 0;
1164             firstMaterialStreamPositionAvailable = false;
1165             currentStreamPosition = 0;
1166             listener = null;
1167             coordsInChunkArray = null;
1168             textureCoordsInChunkArray = null;
1169             normalsInChunkArray = null;
1170             indicesInChunkArray = null;
1171 
1172             originalVertexIndicesInChunkArray = null;
1173             originalTextureIndicesInChunkArray = null;
1174             originalNormalIndicesInChunkArray = null;
1175 
1176             vertexIndicesMap = new TreeMap<>();
1177             textureCoordsIndicesMap = new TreeMap<>();
1178             normalsIndicesMap = new TreeMap<>();
1179 
1180             verticesStreamPositionMap = new TreeMap<>();
1181             textureCoordsStreamPositionMap = new TreeMap<>();
1182             normalsStreamPositionMap = new TreeMap<>();
1183 
1184             verticesInChunk = indicesInChunk = 0;
1185             indicesInChunkSize = 0;
1186 
1187             vertexStreamPosition = 0;
1188             textureCoordStreamPosition = 0;
1189             normalStreamPosition = 0;
1190 
1191             minX = minY = minZ = Float.MAX_VALUE;
1192             maxX = maxY = maxZ = -Float.MAX_VALUE;
1193 
1194             currentChunkMaterialName = "";
1195 
1196             materialLoader = null;
1197 
1198             setUp();
1199         }
1200 
1201         /**
1202          * Method to set listener of this loader iterator.
1203          * This listener will be notified when the loading process finishes.
1204          *
1205          * @param listener listener of this loader iterator.
1206          */
1207         public void setListener(final LoaderIteratorListener listener) {
1208             this.listener = listener;
1209         }
1210 
1211         /**
1212          * Indicates if there is another chunk of data to be loaded.
1213          *
1214          * @return true if there is another chunk of data, false otherwise.
1215          */
1216         @Override
1217         public boolean hasNext() {
1218             return currentFace < numberOfFaces;
1219         }
1220 
1221         /**
1222          * Loads and returns next chunk of data, if available.
1223          *
1224          * @return next chunk of data.
1225          * @throws NotAvailableException thrown if no more data is available.
1226          * @throws LoaderException       if file data is corrupt or cannot be
1227          *                               understood.
1228          * @throws IOException           if an I/O error occurs.
1229          */
1230         @Override
1231         public DataChunk next() throws NotAvailableException, LoaderException, IOException {
1232             if (reader == null) {
1233                 throw new IOException();
1234             }
1235 
1236             if (!hasNext()) {
1237                 throw new NotAvailableException();
1238             }
1239 
1240             initChunkArrays();
1241 
1242             // reset chunk bounding box values
1243             minX = minY = minZ = Float.MAX_VALUE;
1244             maxX = maxY = maxZ = -Float.MAX_VALUE;
1245 
1246             final var progressStep = Math.max((long) (LoaderOBJ.PROGRESS_DELTA * numberOfFaces), 1);
1247 
1248             boolean materialChange = false;
1249 
1250             try {
1251                 while (currentFace < numberOfFaces) { // && !materialChange
1252 
1253                     final var faceStreamPos = reader.getPosition();
1254                     var str = reader.readLine();
1255                     if ((str == null) && (currentFace < (numberOfFaces - 1))) {
1256                         // unexpected end of file
1257                         throw new LoaderException();
1258                     } else if (str == null) {
1259                         break;
1260                     }
1261 
1262                     // check if line corresponds to face or material, otherwise,
1263                     // ignore
1264                     if (str.startsWith(USEMTL)) {
1265 
1266                         if (currentChunkMaterialName.isEmpty()) {
1267                             currentChunkMaterialName = str.substring(USEMTL.length()).trim();
1268                             // search current material on material library
1269                             currentMaterial = null;
1270                             if (materialLoader != null) {
1271                                 currentMaterial = materialLoader.getMaterialByName(currentChunkMaterialName);
1272                             }
1273                         } else {
1274                             // stop reading this chunk and reset position to
1275                             // beginning of line so that usemtl is read again
1276                             materialChange = true;
1277                             reader.seek(faceStreamPos);
1278                             break;
1279                         }
1280 
1281                     } else if (str.startsWith("f ")) {
1282 
1283                         // line is a face, so we keep data after "f"
1284                         str = str.substring("f ".length()).trim();
1285                         // retrieve words in data
1286                         final var valuesTemp = str.split(" ");
1287                         final var valuesSet = new HashSet<String[]>();
1288 
1289                         // check that each face contains three elements to define a
1290                         // triangle only
1291                         if (valuesTemp.length == 3) {
1292                             valuesSet.add(valuesTemp);
1293 
1294                         } else if (valuesTemp.length > 3) {
1295                             // if instead of a triangle we have a polygon then we
1296                             // to divide valuesTemp into a set of values forming
1297                             // triangles
1298                             final var verticesList = getFaceValues(valuesTemp);
1299                             try {
1300                                 valuesSet.addAll(buildTriangulatedIndices(verticesList));
1301                             } catch (final TriangulatorException e) {
1302                                 // triangulation failed for some reason, but
1303                                 // file reading continues if configured like that
1304                                 // (by default it is)
1305                                 if (!continueIfTriangulationError) {
1306                                     throw new LoaderException(e);
1307                                 }
1308                             }
1309                         } else {
1310                             throw new LoaderException();
1311                         }
1312 
1313 
1314                         // each word corresponds to a vertex/texture/normal index,
1315                         // so we check if such number of indices can be added into
1316                         // this chunk
1317                         if ((verticesInChunk + valuesSet.size() * 3) > loader.maxVerticesInChunk) { // values.length
1318                             // no more vertices can be added to chunk, so we reset
1319                             // stream to start on current face
1320                             reader.seek(faceStreamPos);
1321                             break;
1322                         }
1323 
1324                         // keep current stream position for next face
1325                         currentStreamPosition = reader.getPosition();
1326 
1327                         for (final var values : valuesSet) {
1328 
1329                             // otherwise values can be added into chunk, so we read
1330                             // vertex index, texture index and normal index
1331                             for (final var value : values) {
1332                                 // value can be of the form v/vt/vn, where v stands
1333                                 // for vertex index, vt for texture index and vn for
1334                                 // normal index, and where vt and vn are optional
1335                                 final var indices = value.split("/");
1336                                 var addExistingVertexCoords = false;
1337                                 var addExistingTextureCoords = false;
1338                                 var addExistingNormal = false;
1339                                 var vertexCoordsChunkIndex = -1;
1340                                 var textureCoordsChunkIndex = -1;
1341                                 var normalChunkIndex = -1;
1342 
1343                                 boolean addExisting;
1344                                 var chunkIndex = 0;
1345 
1346                                 // first check if vertex has to be added as new or
1347                                 // not
1348                                 if (indices.length >= 1 && (!indices[0].isEmpty())) {
1349                                     indicesAvailable = true;
1350                                     // indices start at 1 in OBJ
1351                                     vertexIndex = Integer.parseInt(indices[0]) - 1;
1352 
1353                                     // determine if vertex coordinates have to be
1354                                     // added as new, or they can be reused from an
1355                                     // existing vertex
1356                                     addExistingVertexCoords = !loader.allowDuplicateVerticesInChunk
1357                                             && (vertexCoordsChunkIndex = searchVertexIndexInChunk(vertexIndex)) >= 0;
1358                                 }
1359                                 if (indices.length >= 2 && (!indices[1].isEmpty())) {
1360                                     textureAvailable = true;
1361                                     // indices start at 1 in OBJ
1362                                     textureIndex = Integer.parseInt(indices[1]) - 1;
1363 
1364                                     // determine if texture coordinates have to be
1365                                     // added as new, or they can be reused from an
1366                                     // existing vertex
1367                                     addExistingTextureCoords = !loader.allowDuplicateVerticesInChunk
1368                                             && (textureCoordsChunkIndex = searchTextureCoordIndexInChunk(textureIndex)) >= 0;
1369                                 }
1370                                 if (indices.length >= 3 && (!indices[2].isEmpty())) {
1371                                     normalsAvailable = true;
1372                                     // indices start at 1 in OBJ
1373                                     normalIndex = Integer.parseInt(indices[2]) - 1;
1374 
1375                                     // determine if normal coordinates have to be
1376                                     // added as new, or they can be reused from an
1377                                     // existing vertex
1378                                     addExistingNormal = !loader.allowDuplicateVerticesInChunk
1379                                             && (normalChunkIndex = searchNormalIndexInChunk(normalIndex)) >= 0;
1380                                 }
1381 
1382                                 // if either vertex coordinates, texture coordinates
1383                                 // or normal indicate that a new vertex needs to be
1384                                 // added, then do so, only if all three use an
1385                                 // existing vertex into chunk use that existing
1386                                 // vertex. Also, in case that existing vertex is
1387                                 // added, if chunk indices of existing vertex,
1388                                 // texture and normal are not the same add as a new
1389                                 // vertex into chunk
1390 
1391                                 // if some chunk index is found, set add existing to
1392                                 // true
1393                                 addExisting = (vertexCoordsChunkIndex >= 0) || (textureCoordsChunkIndex >= 0)
1394                                         || (normalChunkIndex >= 0);
1395                                 // ensure that if index is present an existing vertex
1396                                 // in chunk exists
1397                                 if (indices.length >= 1 && (!indices[0].isEmpty())) {
1398                                     addExisting &= addExistingVertexCoords;
1399                                 }
1400                                 if (indices.length >= 2 && (!indices[1].isEmpty())) {
1401                                     addExisting &= addExistingTextureCoords;
1402                                 }
1403                                 if (indices.length >= 3 && (!indices[2].isEmpty())) {
1404                                     addExisting &= addExistingNormal;
1405                                 }
1406 
1407                                 if (addExisting) {
1408                                     // if finally an existing vertex is added, set
1409                                     // chunk index
1410                                     if (vertexCoordsChunkIndex >= 0) {
1411                                         chunkIndex = vertexCoordsChunkIndex;
1412                                     }
1413                                     if (textureCoordsChunkIndex >= 0) {
1414                                         chunkIndex = textureCoordsChunkIndex;
1415                                     }
1416                                     if (normalChunkIndex >= 0) {
1417                                         chunkIndex = normalChunkIndex;
1418                                     }
1419                                 }
1420 
1421                                 if (indices.length >= 1 && (!indices[0].isEmpty()) && !addExistingVertexCoords) {
1422                                     // new vertex needs to be added into chunk,
1423                                     // so we need to read vertex data
1424 
1425                                     // fetch vertex data position
1426                                     fetchVertex(vertexIndex);
1427                                     vertexStreamPosition = reader.getPosition();
1428 
1429                                     // read all vertex data
1430                                     String vertexLine = reader.readLine();
1431                                     if (!vertexLine.startsWith("v ")) {
1432                                         throw new LoaderException();
1433                                     }
1434                                     vertexLine = vertexLine.substring("v ".length()).trim();
1435                                     // retrieve words in vertexLine, which contain
1436                                     // vertex coordinates either as x, y, z or x,
1437                                     // y, z, w
1438                                     final var vertexCoordinates = vertexLine.split(" ");
1439                                     if (vertexCoordinates.length == 4) {
1440                                         // homogeneous coordinates x, y, z, w
1441 
1442                                         // check that values are valid
1443                                         if (vertexCoordinates[0].isEmpty()) {
1444                                             throw new LoaderException();
1445                                         }
1446                                         if (vertexCoordinates[1].isEmpty()) {
1447                                             throw new LoaderException();
1448                                         }
1449                                         if (vertexCoordinates[2].isEmpty()) {
1450                                             throw new LoaderException();
1451                                         }
1452                                         if (vertexCoordinates[3].isEmpty()) {
1453                                             throw new LoaderException();
1454                                         }
1455 
1456                                         final var w = Float.parseFloat(vertexCoordinates[3]);
1457                                         coordX = Float.parseFloat(vertexCoordinates[0]) / w;
1458                                         coordY = Float.parseFloat(vertexCoordinates[1]) / w;
1459                                         coordZ = Float.parseFloat(vertexCoordinates[2]) / w;
1460 
1461                                     } else if (vertexCoordinates.length >= 3) {
1462                                         // inhomogeneous coordinates x, y, z
1463 
1464                                         // check that values are valid
1465                                         if (vertexCoordinates[0].isEmpty()) {
1466                                             throw new LoaderException();
1467                                         }
1468                                         if (vertexCoordinates[1].isEmpty()) {
1469                                             throw new LoaderException();
1470                                         }
1471                                         if (vertexCoordinates[2].isEmpty()) {
1472                                             throw new LoaderException();
1473                                         }
1474 
1475                                         coordX = Float.parseFloat(vertexCoordinates[0]);
1476                                         coordY = Float.parseFloat(vertexCoordinates[1]);
1477                                         coordZ = Float.parseFloat(vertexCoordinates[2]);
1478 
1479                                     } else {
1480                                         // unsupported length
1481                                         throw new LoaderException();
1482                                     }
1483                                 }
1484                                 if (indices.length >= 2 && (!indices[1].isEmpty()) && !addExistingTextureCoords) {
1485                                     // new texture values need to be added into
1486                                     // chunk, so we need to read texture
1487                                     // coordinates data
1488 
1489                                     // fetch texture data position
1490                                     fetchTexture(textureIndex);
1491                                     textureCoordStreamPosition = reader.getPosition();
1492 
1493                                     // read all texture data
1494                                     var textureLine = reader.readLine();
1495                                     if (!textureLine.startsWith("vt ")) {
1496                                         throw new LoaderException();
1497                                     }
1498                                     textureLine = textureLine.substring("vt ".length()).trim();
1499                                     // retrieve words in textureLine, which contain
1500                                     // texture coordinates either as u, w or u, v, w
1501                                     final var textureCoordinates = textureLine.split(" ");
1502                                     if (textureCoordinates.length == 3) {
1503                                         // homogeneous coordinates u, v, w
1504 
1505                                         // check that values are valid
1506                                         if (textureCoordinates[0].isEmpty()) {
1507                                             throw new LoaderException();
1508                                         }
1509                                         if (textureCoordinates[1].isEmpty()) {
1510                                             throw new LoaderException();
1511                                         }
1512                                         if (textureCoordinates[2].isEmpty()) {
1513                                             throw new LoaderException();
1514                                         }
1515 
1516                                         final var w = Float.parseFloat(textureCoordinates[2]);
1517 
1518                                         textureU = Float.parseFloat(textureCoordinates[0]) / w;
1519                                         textureV = Float.parseFloat(textureCoordinates[1]) / w;
1520                                         if (Math.abs(w) < Float.MIN_VALUE || Float.isInfinite(textureU)
1521                                                 || Float.isNaN(textureU) || Float.isInfinite(textureV)
1522                                                 || Float.isNaN(textureV)) {
1523                                             textureU = Float.parseFloat(textureCoordinates[0]);
1524                                             textureV = Float.parseFloat(textureCoordinates[1]);
1525                                         }
1526 
1527                                     } else if (textureCoordinates.length >= 2) {
1528                                         // inhomogeneous coordinates u, v
1529 
1530                                         // check that values are valid
1531                                         if (textureCoordinates[0].isEmpty()) {
1532                                             throw new LoaderException();
1533                                         }
1534                                         if (textureCoordinates[1].isEmpty()) {
1535                                             throw new LoaderException();
1536                                         }
1537 
1538                                         textureU = Float.parseFloat(textureCoordinates[0]);
1539                                         textureV = Float.parseFloat(textureCoordinates[1]);
1540                                     } else {
1541                                         // unsupported length
1542                                         throw new LoaderException();
1543                                     }
1544                                 }
1545                                 if (indices.length >= 3 && (!indices[2].isEmpty()) && !addExistingNormal) {
1546                                     // new normal needs to be added into chunk,
1547                                     // so we need to read vertex data
1548 
1549                                     // fetch normal data position
1550                                     fetchNormal(normalIndex);
1551                                     normalStreamPosition = reader.getPosition();
1552 
1553                                     // read all normal data
1554                                     var normalLine = reader.readLine();
1555                                     if (!normalLine.startsWith("vn ")) {
1556                                         throw new LoaderException();
1557                                     }
1558                                     normalLine = normalLine.substring("vn ".length()).trim();
1559                                     // retrieve words in normalLine, which must
1560                                     // contain normal coordinates as x, y, z
1561                                     final var normalCoordinates = normalLine.split(" ");
1562                                     if (normalCoordinates.length == 3) {
1563                                         // normal coordinates x, y, z
1564 
1565                                         // check that values are valid
1566                                         if (normalCoordinates[0].isEmpty()) {
1567                                             throw new LoaderException();
1568                                         }
1569                                         if (normalCoordinates[1].isEmpty()) {
1570                                             throw new LoaderException();
1571                                         }
1572                                         if (normalCoordinates[2].isEmpty()) {
1573                                             throw new LoaderException();
1574                                         }
1575 
1576                                         nX = Float.parseFloat(normalCoordinates[0]);
1577                                         nY = Float.parseFloat(normalCoordinates[1]);
1578                                         nZ = Float.parseFloat(normalCoordinates[2]);
1579                                     } else {
1580                                         // unsupported length
1581                                         throw new LoaderException();
1582                                     }
1583                                 }
1584 
1585                                 if (addExisting) {
1586                                     addExistingVertexToChunk(chunkIndex);
1587                                 } else {
1588                                     addNewVertexDataToChunk();
1589                                 }
1590                             }
1591                         }
1592                         // reset face stream position
1593                         reader.seek(currentStreamPosition);
1594                         currentFace++;
1595                     }
1596 
1597                     // compute progress
1598                     if (loader.listener != null && (currentFace % progressStep) == 0) {
1599                         loader.listener.onLoadProgressChange(loader,
1600                                 (float) (currentFace) / (float) (numberOfFaces));
1601                     }
1602                 }
1603             } catch (final NumberFormatException e) {
1604                 throw new LoaderException(e);
1605             }
1606 
1607             // trim arrays to store only needed data
1608             trimArrays();
1609 
1610             // Instantiate DataChunk with chunk arrays
1611             final var dataChunk = new DataChunk();
1612 
1613             if (verticesAvailable) {
1614                 dataChunk.setVerticesCoordinatesData(coordsInChunkArray);
1615                 dataChunk.setMinX(minX);
1616                 dataChunk.setMinY(minY);
1617                 dataChunk.setMinZ(minZ);
1618                 dataChunk.setMaxX(maxX);
1619                 dataChunk.setMaxY(maxY);
1620                 dataChunk.setMaxZ(maxZ);
1621             } else {
1622                 // so it can be garbage collected
1623                 coordsInChunkArray = null;
1624             }
1625 
1626             if (textureAvailable) {
1627                 dataChunk.setTextureCoordinatesData(textureCoordsInChunkArray);
1628             } else {
1629                 // so it can be garbage collected
1630                 textureCoordsInChunkArray = null;
1631             }
1632 
1633             if (currentMaterial != null) {
1634                 dataChunk.setMaterial(currentMaterial);
1635             }
1636 
1637             if (materialChange) {
1638                 currentChunkMaterialName = "";
1639                 currentMaterial = null;
1640             }
1641 
1642             if (indicesAvailable) {
1643                 dataChunk.setIndicesData(indicesInChunkArray);
1644             } else {
1645                 // so it can be garbage collected
1646                 indicesInChunkArray = null;
1647             }
1648 
1649             if (normalsAvailable) {
1650                 dataChunk.setNormalsData(normalsInChunkArray);
1651             } else {
1652                 // so it can be garbage collected
1653                 normalsInChunkArray = null;
1654             }
1655 
1656             if (!hasNext() && listener != null) {
1657                 // notify iterator finished
1658                 listener.onIteratorFinished(this);
1659             }
1660 
1661             // if no more chunks are available, then close input reader
1662             if (!hasNext()) {
1663                 reader.close();
1664             }
1665 
1666             return dataChunk;
1667         }
1668 
1669         /**
1670          * Fetches vertex data in the file using provided index. Index refers
1671          * to indices contained in OBJ file.
1672          *
1673          * @param index index corresponding to vertex being fetched.
1674          * @throws LoaderException if data is corrupted or cannot be understood.
1675          * @throws IOException     if an I/O error occurs.
1676          */
1677         public void fetchVertex(long index) throws LoaderException, IOException {
1678             if (index > numberOfVertices) {
1679                 throw new LoaderException();
1680             }
1681 
1682             var startStreamPos = firstVertexStreamPosition;
1683             var startIndex = 0L;
1684 
1685             if (!verticesStreamPositionMap.isEmpty()) {
1686                 // with floorEntry, we will pick element immediately
1687                 // before or equal to index if any exists
1688                 final var entry = verticesStreamPositionMap.floorEntry(index);
1689                 if (entry != null) {
1690                     final var origIndex = entry.getKey();
1691                     final var pos = entry.getValue();
1692                     if ((origIndex <= index) && (pos >= 0)) {
1693                         startIndex = origIndex;
1694                         startStreamPos = pos;
1695                     }
1696                 }
1697             }
1698 
1699             // if we need to read next vertex, don't do anything, otherwise
1700             // move to next vertex location if reading some vertex located
1701             // further on the stream. For previous vertex indices, start
1702             // from beginning
1703             if (reader.getPosition() != startStreamPos) {
1704                 reader.seek(startStreamPos);
1705             }
1706 
1707             // read from stream until start of data of desired vertex
1708             var streamPosition = 0L;
1709             for (var i = startIndex; i <= index; i++) {
1710 
1711                 // when traversing stream of data until reaching desired
1712                 // index, we add all vertex, texture and normal positions
1713                 // into maps
1714                 String str;
1715                 var end = false;
1716                 do {
1717                     streamPosition = reader.getPosition();
1718                     str = reader.readLine();
1719                     if (str == null) {
1720                         end = true;
1721                         break;
1722                     }
1723 
1724                     if (str.startsWith("v ")) {
1725                         // line contains vertex coordinates, so we store
1726                         // stream position into corresponding map and exit
1727                         // while loop
1728                         addVertexPositionToMap(i, streamPosition);
1729                         break;
1730                     }
1731                 } while (true); // read until end of file when str == null
1732 
1733                 // unexpected end
1734                 if (end) {
1735                     throw new LoaderException();
1736                 }
1737             }
1738 
1739             // seek to last streamPosition which contains the desired data
1740             reader.seek(streamPosition);
1741         }
1742 
1743         /**
1744          * Fetches texture data in the file using provided index. Index refers
1745          * to indices contained in OBJ file.
1746          *
1747          * @param index index corresponding to texture being fetched.
1748          * @throws LoaderException if data is corrupted or cannot be understood.
1749          * @throws IOException     if an I/O error occurs.
1750          */
1751         public void fetchTexture(final long index) throws LoaderException, IOException {
1752             if (index > numberOfTextureCoords) {
1753                 throw new LoaderException();
1754             }
1755 
1756             var startStreamPos = firstTextureCoordStreamPosition;
1757             var startIndex = 0L;
1758 
1759             if (!textureCoordsStreamPositionMap.isEmpty()) {
1760                 // with floorEntry, we will pick element immediately
1761                 // before or equal to index if any exists
1762                 final var entry = textureCoordsStreamPositionMap.floorEntry(index);
1763                 if (entry != null) {
1764                     final var origIndex = entry.getKey();
1765                     final var pos = entry.getValue();
1766                     if ((origIndex <= index) && (pos >= 0)) {
1767                         startIndex = origIndex;
1768                         startStreamPos = pos;
1769                     }
1770                 }
1771             }
1772 
1773             // if we need to read next texture vertex, don't do anything,
1774             // otherwise move to next texture vertex located further on the
1775             // stream. For previous texture vertex indices, start from
1776             // beginning
1777             if (reader.getPosition() != startStreamPos) {
1778                 reader.seek(startStreamPos);
1779             }
1780 
1781             // read from stream until start of data of desired texture vertex
1782             var streamPosition = 0L;
1783             for (var i = startIndex; i <= index; i++) {
1784 
1785                 // when traversing stream of data until reaching desired
1786                 // index, we add all vertex, texture and normal positions
1787                 // into maps
1788                 String str;
1789                 var end = false;
1790                 do {
1791                     streamPosition = reader.getPosition();
1792                     str = reader.readLine();
1793                     if (str == null) {
1794                         end = true;
1795                         break;
1796                     }
1797 
1798                     if (str.startsWith("vt ")) {
1799                         // line contains texture coordinates, so we store
1800                         // stream position into corresponding map and exit
1801                         // while loop
1802                         addTextureCoordPositionToMap(i, streamPosition);
1803                         break;
1804                     }
1805                 } while (true); // read until end of file when str == null
1806 
1807                 // unexpected end
1808                 if (end) {
1809                     throw new LoaderException();
1810                 }
1811             }
1812 
1813             // seek to last streamPosition which contains the desired data
1814             reader.seek(streamPosition);
1815         }
1816 
1817         /**
1818          * Fetches normal data in the file using provided index. Index refers
1819          * to indices contained in OBJ file.
1820          *
1821          * @param index index corresponding to normal being fetched.
1822          * @throws LoaderException if data is corrupted or cannot be understood.
1823          * @throws IOException     if an I/O error occurs.
1824          */
1825         public void fetchNormal(final long index) throws LoaderException, IOException {
1826             if (index > numberOfNormals) {
1827                 throw new LoaderException();
1828             }
1829 
1830             var startStreamPos = firstNormalStreamPosition;
1831             var startIndex = 0L;
1832 
1833             if (!normalsStreamPositionMap.isEmpty()) {
1834                 // with floorEntry, we will pick element immediately before or
1835                 // equal to index if any exists
1836                 final var entry = normalsStreamPositionMap.floorEntry(index);
1837                 if (entry != null) {
1838                     final var origIndex = entry.getKey();
1839                     final var pos = entry.getValue();
1840                     if ((origIndex <= index) && (pos >= 0)) {
1841                         startIndex = origIndex;
1842                         startStreamPos = pos;
1843                     }
1844                 }
1845             }
1846 
1847             // if we need to read next normal, don't do anything, otherwise
1848             // move to next normal located further on the stream.
1849             // For previous normals indices, start from beginning
1850             if (reader.getPosition() != startStreamPos) {
1851                 reader.seek(startStreamPos);
1852             }
1853 
1854             // read from stream until start of data of desired normal
1855             var streamPosition = 0L;
1856             for (var i = startIndex; i <= index; i++) {
1857 
1858                 // when traversing stream of data until reaching desired
1859                 // index, we add all vertex, texture and normal positions
1860                 // into maps
1861                 String str;
1862                 var end = false;
1863                 do {
1864                     streamPosition = reader.getPosition();
1865                     str = reader.readLine();
1866                     if (str == null) {
1867                         end = true;
1868                         break;
1869                     }
1870 
1871                     if (str.startsWith("vn ")) {
1872                         // line contains normal, so we store stream position
1873                         // into corresponding map and exit while loop
1874                         addNormalPositionToMap(i, streamPosition);
1875                         break;
1876                     }
1877                 } while (true); // read until end of file when str == null
1878 
1879                 // unexpected end
1880                 if (end) {
1881                     throw new LoaderException();
1882                 }
1883             }
1884 
1885             // seek to last streamPosition which contains the desired data
1886             reader.seek(streamPosition);
1887         }
1888 
1889         /**
1890          * Internal method to decompose an array of vertices forming a polygon
1891          * in a set of arrays of vertices corresponding to triangles after
1892          * triangulation of the polygon. This method is used to triangulate
1893          * polygons with more than 3 vertices contained in the file.
1894          *
1895          * @param vertices list of vertices forming a polygon to be triangulated.
1896          * @return a set containing arrays of indices of vertices (in string
1897          * format) corresponding to the triangles forming the polygon after the
1898          * triangulation.
1899          * @throws TriangulatorException if triangulation fails (because polygon
1900          *                               is degenerate or contains invalid values such as NaN or infinity).
1901          */
1902         private Set<String[]> buildTriangulatedIndices(final List<VertexOBJ> vertices) throws TriangulatorException {
1903             final var polygonVertices = new ArrayList<Point3D>(vertices.size());
1904             for (final var v : vertices) {
1905                 if (v.getVertex() == null) {
1906                     throw new TriangulatorException();
1907                 }
1908                 polygonVertices.add(v.getVertex());
1909             }
1910             final var indices = new ArrayList<int[]>();
1911             final var triangulator = Triangulator3D.create();
1912             final var triangles = triangulator.triangulate(polygonVertices, indices);
1913 
1914             final var result = new HashSet<String[]>();
1915             String[] face;
1916             var counter = 0;
1917             int[] triangleIndices;
1918             int index;
1919             VertexOBJ vertex;
1920             StringBuilder builder;
1921             for (final var ignored : triangles) {
1922                 triangleIndices = indices.get(counter);
1923                 face = new String[Triangle3D.NUM_VERTICES];
1924                 for (var i = 0; i < Triangle3D.NUM_VERTICES; i++) {
1925                     index = triangleIndices[i];
1926                     vertex = vertices.get(index);
1927                     builder = new StringBuilder();
1928                     if (vertex.isVertexIndexAvailable()) {
1929                         builder.append(vertex.getVertexIndex());
1930                     }
1931                     if (vertex.isTextureIndexAvailable() || vertex.isNormalIndexAvailable()) {
1932                         builder.append("/");
1933                         if (vertex.isTextureIndexAvailable()) {
1934                             builder.append(vertex.getTextureIndex());
1935                         }
1936                         if (vertex.isNormalIndexAvailable()) {
1937                             builder.append("/");
1938                             builder.append(vertex.getNormalIndex());
1939                         }
1940                     }
1941 
1942                     face[i] = builder.toString();
1943                 }
1944                 counter++;
1945                 result.add(face);
1946             }
1947 
1948             return result;
1949         }
1950 
1951         /**
1952          * This method reads a line containing face (i.e. polygon) indices of
1953          * vertices and fetches those vertices coordinates and associated data
1954          * such as texture coordinates or normal coordinates.
1955          *
1956          * @param values a string containing vertex indices forming a polygon.
1957          *               Note that indices refer to the values contained in OBJ file, not the
1958          *               indices in the chunk of data.
1959          * @return a list of vertices forming a face (i.e, polygon).
1960          * @throws IOException     if an I/O error occurs.
1961          * @throws LoaderException if loading fails because data is corrupted or
1962          *                         cannot be interpreted.
1963          */
1964         private List<VertexOBJ> getFaceValues(final String[] values) throws IOException, LoaderException {
1965 
1966             VertexOBJ tmpVertex;
1967             Point3D point;
1968             final var vertices = new ArrayList<VertexOBJ>(values.length);
1969 
1970             // keep current stream position for next face
1971             final var tempPosition = reader.getPosition();
1972 
1973             for (final var value : values) {
1974                 tmpVertex = new VertexOBJ();
1975                 point = Point3D.create();
1976                 tmpVertex.setVertex(point);
1977 
1978                 final var indices = value.split("/");
1979 
1980                 if (indices.length >= 1 && (!indices[0].isEmpty())) {
1981                     vertexIndex = Integer.parseInt(indices[0]) - 1;
1982                     tmpVertex.setVertexIndex(vertexIndex + 1);
1983                     fetchVertex(vertexIndex);
1984                     vertexStreamPosition = reader.getPosition();
1985 
1986                     var vertexLine = reader.readLine();
1987                     if (!vertexLine.startsWith("v ")) {
1988                         throw new LoaderException();
1989                     }
1990                     vertexLine = vertexLine.substring("v ".length()).trim();
1991                     final var vertexCoordinates = vertexLine.split(" ");
1992 
1993                     if (vertexCoordinates.length == 4) {
1994                         // homogeneous coordinates x, y, z, w
1995                         // ensure that vertex coordinates are not empty
1996                         if (vertexCoordinates[0].isEmpty()) {
1997                             throw new LoaderException();
1998                         }
1999                         if (vertexCoordinates[1].isEmpty()) {
2000                             throw new LoaderException();
2001                         }
2002                         if (vertexCoordinates[2].isEmpty()) {
2003                             throw new LoaderException();
2004                         }
2005                         if (vertexCoordinates[3].isEmpty()) {
2006                             throw new LoaderException();
2007                         }
2008 
2009                         try {
2010                             point.setHomogeneousCoordinates(
2011                                     Double.parseDouble(vertexCoordinates[0]),
2012                                     Double.parseDouble(vertexCoordinates[1]),
2013                                     Double.parseDouble(vertexCoordinates[2]),
2014                                     Double.parseDouble(vertexCoordinates[3]));
2015                         } catch (final NumberFormatException e) {
2016                             // some vertex coordinate value could not be parsed
2017                             throw new LoaderException(e);
2018                         }
2019                     } else if (vertexCoordinates.length >= 3) {
2020                         // inhomogeneous coordinates x, y, z
2021                         // ensure that vertex coordinate are not empty
2022                         if (vertexCoordinates[0].isEmpty()) {
2023                             throw new LoaderException();
2024                         }
2025                         if (vertexCoordinates[1].isEmpty()) {
2026                             throw new LoaderException();
2027                         }
2028                         if (vertexCoordinates[2].isEmpty()) {
2029                             throw new LoaderException();
2030                         }
2031 
2032                         try {
2033                             point.setInhomogeneousCoordinates(
2034                                     Double.parseDouble(vertexCoordinates[0]),
2035                                     Double.parseDouble(vertexCoordinates[1]),
2036                                     Double.parseDouble(vertexCoordinates[2]));
2037                         } catch (final NumberFormatException e) {
2038                             // some vertex coordinate value could not be parsed
2039                             throw new LoaderException(e);
2040                         }
2041                     } else {
2042                         // unsupported length
2043                         throw new LoaderException();
2044                     }
2045                 }
2046                 if (indices.length >= 2 && (!indices[1].isEmpty())) {
2047                     tmpVertex.setTextureIndex(Integer.parseInt(indices[1]));
2048                 }
2049                 if (indices.length >= 3 && (!indices[2].isEmpty())) {
2050                     tmpVertex.setNormalIndex(Integer.parseInt(indices[2]));
2051                 }
2052 
2053                 vertices.add(tmpVertex);
2054             }
2055 
2056             reader.seek(tempPosition);
2057             return vertices;
2058         }
2059 
2060         /**
2061          * Initializes arrays forming current chunk of data.
2062          */
2063         private void initChunkArrays() {
2064             coordsInChunkArray = new float[loader.maxVerticesInChunk * 3];
2065             textureCoordsInChunkArray = new float[loader.maxVerticesInChunk * 2];
2066             normalsInChunkArray = new float[loader.maxVerticesInChunk * 3];
2067             indicesInChunkArray = new int[loader.maxVerticesInChunk];
2068 
2069             originalVertexIndicesInChunkArray = new long[loader.maxVerticesInChunk];
2070             originalTextureIndicesInChunkArray = new long[loader.maxVerticesInChunk];
2071             originalNormalIndicesInChunkArray = new long[loader.maxVerticesInChunk];
2072             verticesInChunk = 0;
2073             indicesInChunk = 0;
2074             indicesInChunkSize = loader.maxVerticesInChunk;
2075 
2076             vertexIndicesMap.clear();
2077             textureCoordsIndicesMap.clear();
2078             normalsIndicesMap.clear();
2079         }
2080 
2081         /**
2082          * Searches vertex index in current chunk of data by using the index
2083          * used in the OBJ file.
2084          * This method searches within the cached indices which relate indices
2085          * in the chunk of data respect to indices in the OBJ file.
2086          *
2087          * @param originalIndex vertex index used in the OBJ file.
2088          * @return vertex index used in current chunk of data or -1 if not found.
2089          */
2090         private int searchVertexIndexInChunk(final long originalIndex) {
2091             // returns chunk index array position where index is found
2092             final var chunkIndex = vertexIndicesMap.get(originalIndex);
2093 
2094             if (chunkIndex == null) {
2095                 return -1;
2096             }
2097 
2098             // returns index of vertex in chunk
2099             return indicesInChunkArray[chunkIndex];
2100         }
2101 
2102         /**
2103          * Searches texture index in current chunk of data by using the index
2104          * used in the OBJ file.
2105          * This method searches within the cached indices which relate indices
2106          * in the chunk of data respect to indices in the OBJ file.
2107          *
2108          * @param originalIndex texture index used in the OBJ file.
2109          * @return texture index used in current chunk of data or -1 if not
2110          * found.
2111          */
2112         private int searchTextureCoordIndexInChunk(final long originalIndex) {
2113             return searchVertexIndexInChunk(originalIndex);
2114         }
2115 
2116         /**
2117          * Searches normal index in current chunk of data by using the index
2118          * used in the OBJ file.
2119          * This method searches within the cached indices which relate indices
2120          * in the chunk of data respect to indices in the OBJ file.
2121          *
2122          * @param originalIndex normal index used in the OBJ file.
2123          * @return normal index used in current chunk of data or -1 if not found.
2124          */
2125         private int searchNormalIndexInChunk(final long originalIndex) {
2126             return searchVertexIndexInChunk(originalIndex);
2127         }
2128 
2129         /**
2130          * Add vertex position to cache of file positions.
2131          *
2132          * @param originalIndex  vertex index used in OBJ file.
2133          * @param streamPosition stream position where vertex is located.
2134          */
2135         private void addVertexPositionToMap(final long originalIndex, final long streamPosition) {
2136             if (verticesStreamPositionMap.size() > loader.maxStreamPositions) {
2137                 // Map is full. Remove 1st item before adding a new one
2138                 final var origIndex = verticesStreamPositionMap.firstKey();
2139                 verticesStreamPositionMap.remove(origIndex);
2140             }
2141             // add new item
2142             verticesStreamPositionMap.put(originalIndex, streamPosition);
2143         }
2144 
2145         /**
2146          * Add texture coordinate position to cache of file positions.
2147          *
2148          * @param originalIndex  texture coordinate index used in OBJ file.
2149          * @param streamPosition stream position where texture coordinate is
2150          *                       located.
2151          */
2152         private void addTextureCoordPositionToMap(final long originalIndex, final long streamPosition) {
2153             if (textureCoordsStreamPositionMap.size() > loader.maxStreamPositions) {
2154                 // Map is full. Remove 1st item before adding a new one
2155                 final var origIndex = textureCoordsStreamPositionMap.firstKey();
2156                 textureCoordsStreamPositionMap.remove(origIndex);
2157             }
2158             // add new item
2159             textureCoordsStreamPositionMap.put(originalIndex, streamPosition);
2160         }
2161 
2162         /**
2163          * Add normal coordinate to cache of file positions.
2164          *
2165          * @param originalIndex  normal coordinate index used in OBJ file.
2166          * @param streamPosition stream position where normal coordinate is
2167          *                       located.
2168          */
2169         private void addNormalPositionToMap(final long originalIndex, final long streamPosition) {
2170             if (normalsStreamPositionMap.size() > loader.maxStreamPositions) {
2171                 // Map is full. Remove 1st item before adding a new one
2172                 final var origIndex = normalsStreamPositionMap.firstKey();
2173                 normalsStreamPositionMap.remove(origIndex);
2174             }
2175             // add new item
2176             normalsStreamPositionMap.put(originalIndex, streamPosition);
2177         }
2178 
2179         /**
2180          * Adds data of last vertex being loaded to current chunk of data as a
2181          * new vertex.
2182          */
2183         private void addNewVertexDataToChunk() {
2184             var pos = 3 * verticesInChunk;
2185             var textPos = 2 * verticesInChunk;
2186 
2187             coordsInChunkArray[pos] = coordX;
2188             normalsInChunkArray[pos] = nX;
2189             textureCoordsInChunkArray[textPos] = textureU;
2190 
2191             pos++;
2192             textPos++;
2193 
2194             coordsInChunkArray[pos] = coordY;
2195             normalsInChunkArray[pos] = nY;
2196             textureCoordsInChunkArray[textPos] = textureV;
2197 
2198             pos++;
2199 
2200             coordsInChunkArray[pos] = coordZ;
2201             normalsInChunkArray[pos] = nZ;
2202 
2203             // update bounding box values
2204             if (coordX < minX) {
2205                 minX = coordX;
2206             }
2207             if (coordY < minY) {
2208                 minY = coordY;
2209             }
2210             if (coordZ < minZ) {
2211                 minZ = coordZ;
2212             }
2213 
2214             if (coordX > maxX) {
2215                 maxX = coordX;
2216             }
2217             if (coordY > maxY) {
2218                 maxY = coordY;
2219             }
2220             if (coordZ > maxZ) {
2221                 maxZ = coordZ;
2222             }
2223 
2224             // if arrays of indices become full, we need to resize them
2225             if (indicesInChunk >= indicesInChunkSize) {
2226                 increaseIndicesArraySize();
2227             }
2228             indicesInChunkArray[indicesInChunk] = verticesInChunk;
2229             originalVertexIndicesInChunkArray[indicesInChunk] = vertexIndex;
2230             originalTextureIndicesInChunkArray[indicesInChunk] = textureIndex;
2231             originalNormalIndicesInChunkArray[indicesInChunk] = normalIndex;
2232             // store original indices in maps, so we can search chunk index by
2233             // original indices of vertices, texture or normal
2234             vertexIndicesMap.put((long) vertexIndex, indicesInChunk);
2235             textureCoordsIndicesMap.put((long) textureIndex, indicesInChunk);
2236             normalsIndicesMap.put((long) normalIndex, indicesInChunk);
2237 
2238             // store vertex, texture and normal stream positions
2239             addVertexPositionToMap(vertexIndex, vertexStreamPosition);
2240             addTextureCoordPositionToMap(textureIndex, textureCoordStreamPosition);
2241             addNormalPositionToMap(normalIndex, normalStreamPosition);
2242 
2243             verticesInChunk++;
2244             indicesInChunk++;
2245         }
2246 
2247         /**
2248          * Adds index to current chunk of data referring to a previously
2249          * existing vertex in the chunk.
2250          *
2251          * @param existingIndex index of vertex that already exists in the chunk.
2252          */
2253         private void addExistingVertexToChunk(final int existingIndex) {
2254             // if arrays of indices become full, we need to resize them
2255             if (indicesInChunk >= indicesInChunkSize) {
2256                 increaseIndicesArraySize();
2257             }
2258             indicesInChunkArray[indicesInChunk] = existingIndex;
2259             originalVertexIndicesInChunkArray[indicesInChunk] = vertexIndex;
2260             originalTextureIndicesInChunkArray[indicesInChunk] = textureIndex;
2261             originalNormalIndicesInChunkArray[indicesInChunk] = normalIndex;
2262 
2263             indicesInChunk++;
2264         }
2265 
2266         /**
2267          * Increases size of arrays of data. This method is called when needed.
2268          */
2269         private void increaseIndicesArraySize() {
2270             final var newIndicesInChunkSize = indicesInChunkSize + loader.maxVerticesInChunk;
2271             final var newIndicesInChunkArray = new int[newIndicesInChunkSize];
2272             final var newOriginalVertexIndicesInChunkArray = new long[newIndicesInChunkSize];
2273             final var newOriginalTextureIndicesInChunkArray = new long[newIndicesInChunkSize];
2274             final var newOriginalNormalIndicesInChunkArray = new long[newIndicesInChunkSize];
2275 
2276             // copy contents of old array
2277             System.arraycopy(indicesInChunkArray, 0, newIndicesInChunkArray, 0, indicesInChunkSize);
2278             System.arraycopy(originalVertexIndicesInChunkArray, 0, newOriginalVertexIndicesInChunkArray,
2279                     0, indicesInChunkSize);
2280             System.arraycopy(originalTextureIndicesInChunkArray, 0, newOriginalTextureIndicesInChunkArray,
2281                     0, indicesInChunkSize);
2282             System.arraycopy(originalNormalIndicesInChunkArray, 0, newOriginalNormalIndicesInChunkArray,
2283                     0, indicesInChunkSize);
2284 
2285             // set new arrays and new size
2286             indicesInChunkArray = newIndicesInChunkArray;
2287             originalVertexIndicesInChunkArray = newOriginalVertexIndicesInChunkArray;
2288             originalTextureIndicesInChunkArray = newOriginalTextureIndicesInChunkArray;
2289             originalNormalIndicesInChunkArray = newOriginalNormalIndicesInChunkArray;
2290             indicesInChunkSize = newIndicesInChunkSize;
2291         }
2292 
2293         /**
2294          * Trims arrays of data to reduce size of arrays to fit chunk data. This
2295          * method is loaded just before copying data to chunk being returned.
2296          */
2297         private void trimArrays() {
2298             if (verticesInChunk > 0) {
2299                 final var elems = verticesInChunk * 3;
2300                 final var textElems = verticesInChunk * 2;
2301 
2302                 final var newCoordsInChunkArray = new float[elems];
2303                 final var newTextureCoordsInChunkArray = new float[elems];
2304                 final var newNormalsInChunkArray = new float[elems];
2305 
2306                 // copy contents of old arrays
2307                 System.arraycopy(coordsInChunkArray, 0, newCoordsInChunkArray, 0, elems);
2308                 System.arraycopy(textureCoordsInChunkArray, 0, newTextureCoordsInChunkArray, 0,
2309                         textElems);
2310                 System.arraycopy(normalsInChunkArray, 0, newNormalsInChunkArray, 0, elems);
2311 
2312                 // set new arrays
2313                 coordsInChunkArray = newCoordsInChunkArray;
2314                 textureCoordsInChunkArray = newTextureCoordsInChunkArray;
2315                 normalsInChunkArray = newNormalsInChunkArray;
2316             } else {
2317                 // allow garbage collection
2318                 coordsInChunkArray = null;
2319                 textureCoordsInChunkArray = null;
2320                 normalsInChunkArray = null;
2321             }
2322 
2323             if (indicesInChunk > 0) {
2324                 final var newIndicesInChunkArray = new int[indicesInChunk];
2325                 System.arraycopy(indicesInChunkArray, 0, newIndicesInChunkArray, 0, indicesInChunk);
2326 
2327                 // set new array
2328                 indicesInChunkArray = newIndicesInChunkArray;
2329             } else {
2330                 // allow garbage collection
2331                 indicesInChunkArray = null;
2332                 originalVertexIndicesInChunkArray = null;
2333                 originalTextureIndicesInChunkArray = null;
2334                 originalNormalIndicesInChunkArray = null;
2335             }
2336         }
2337 
2338         /**
2339          * Setups loader iterator. This method is called when constructing
2340          * this iterator.
2341          *
2342          * @throws IOException     if an I/O error occurs.
2343          * @throws LoaderException if data is corrupted or cannot be understood.
2344          */
2345         private void setUp() throws IOException, LoaderException {
2346             numberOfVertices = numberOfTextureCoords = numberOfNormals = numberOfFaces = 0;
2347 
2348             do {
2349                 final var streamPosition = reader.getPosition();
2350                 final var str = reader.readLine();
2351                 if (str == null) {
2352                     break;
2353                 }
2354 
2355                 if (str.startsWith("#")) {
2356                     // line is a comment, so we should add it to the list of
2357                     // comments
2358                     loader.comments.add(str.substring("#".length()).trim());
2359                 } else if (str.startsWith("vt ")) {
2360                     // line contains texture coordinates, so we keep its stream
2361                     // position and indicate that chunks will contain texture
2362                     // coordinates
2363                     if (!firstTextureCoordStreamPositionAvailable) {
2364                         firstTextureCoordStreamPosition = streamPosition;
2365                         firstTextureCoordStreamPositionAvailable = true;
2366                         textureAvailable = true;
2367                     }
2368                     numberOfTextureCoords++;
2369                 } else if (str.startsWith("vn ")) {
2370                     // line contains normal, so we keep its stream position and
2371                     // indicate that chunks will contain normals
2372                     if (!firstNormalStreamPositionAvailable) {
2373                         firstNormalStreamPosition = streamPosition;
2374                         firstNormalStreamPositionAvailable = true;
2375                         normalsAvailable = true;
2376                     }
2377                     numberOfNormals++;
2378                 } else if (str.startsWith("v ")) {
2379                     // line contains vertex coordinates, so we keep its stream
2380                     // position and indicate that chunks will contain vertex
2381                     // coordinates
2382                     if (!firstVertexStreamPositionAvailable) {
2383                         firstVertexStreamPosition = streamPosition;
2384                         firstVertexStreamPositionAvailable = true;
2385                         verticesAvailable = true;
2386                     }
2387                     numberOfVertices++;
2388                 } else if (str.startsWith("f ")) {
2389                     // line contains face definition, so we keep its stream
2390                     // position and indicate that chunks will contain indices
2391                     if (!firstFaceStreamPositionAvailable) {
2392                         firstFaceStreamPosition = streamPosition;
2393                         firstFaceStreamPositionAvailable = true;
2394                         indicesAvailable = true;
2395                     }
2396 
2397                     numberOfFaces++;
2398 
2399                 } else if (str.startsWith("mtllib ")) {
2400                     // a material library is found
2401                     final var path = str.substring("mtllib ".length()).trim();
2402                     if (loader.listener instanceof LoaderListenerOBJ loaderListener) {
2403                         materialLoader = loaderListener.onMaterialLoaderRequested(loader, path);
2404                     } else {
2405                         materialLoader = new MaterialLoaderOBJ(new File(path));
2406                     }
2407 
2408                     // now load library of materials
2409                     try {
2410                         if (materialLoader != null) {
2411                             loader.materials = materialLoader.load();
2412                             // to release file resources
2413                             materialLoader.close();
2414                         }
2415                     } catch (final LoaderException e) {
2416                         throw e;
2417                     } catch (final Exception e) {
2418                         throw new LoaderException(e);
2419                     }
2420 
2421                 } else if (str.startsWith(USEMTL) && !firstMaterialStreamPositionAvailable) {
2422                     firstMaterialStreamPositionAvailable = true;
2423                     firstMaterialStreamPosition = streamPosition;
2424                     materialsAvailable = true;
2425                 }
2426 
2427                 // ignore any other line
2428             } while (true); // read until end of file when str == null
2429 
2430             // move to first face tream position
2431             if (!firstFaceStreamPositionAvailable) {
2432                 throw new LoaderException();
2433             }
2434 
2435             if (materialsAvailable && firstMaterialStreamPosition < firstFaceStreamPosition) {
2436                 reader.seek(firstMaterialStreamPosition);
2437             } else {
2438                 reader.seek(firstFaceStreamPosition);
2439             }
2440         }
2441     }
2442 }