001/**
002 * PointString -- A collection of GeomPoint objects that make up a "string of points".
003 *
004 * Copyright (C) 2003-2017, by Joseph A. Huwaldt. All rights reserved.
005 *
006 * This library is free software; you can redistribute it and/or modify it under the terms
007 * of the GNU Lesser General Public License as published by the Free Software Foundation;
008 * either version 2.1 of the License, or (at your option) any later version.
009 *
010 * This library is distributed in the hope that it will be useful, but WITHOUT ANY
011 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
012 * PARTICULAR PURPOSE. See the GNU Library General Public License for more details.
013 *
014 * You should have received a copy of the GNU Lesser General Public License along with
015 * this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place -
016 * Suite 330, Boston, MA 02111-1307, USA. Or visit: http://www.gnu.org/licenses/lgpl.html
017 */
018package geomss.geom;
019
020import jahuwaldt.js.param.Parameter;
021import java.io.Serializable;
022import java.text.MessageFormat;
023import java.util.Collection;
024import java.util.Collections;
025import java.util.Comparator;
026import java.util.Iterator;
027import static java.util.Objects.requireNonNull;
028import javax.measure.converter.ConversionException;
029import javax.measure.quantity.Length;
030import javax.measure.unit.NonSI;
031import javax.measure.unit.SI;
032import javax.measure.unit.Unit;
033import javolution.context.ObjectFactory;
034import javolution.context.StackContext;
035import javolution.util.FastTable;
036import javolution.xml.XMLFormat;
037import javolution.xml.stream.XMLStreamException;
038
039/**
040 * A <code>PointString</code> is a collection of {@link GeomPoint} objects that make up a
041 * "string of points". Any number of points may be added to a string, but all points in a
042 * string must have the same dimensions.
043 * <p>
044 * WARNING: This list allows geometry to be stored in different units. If consistent units
045 * are required, then the user must specifically convert the list items.
046 * </p>
047 * <p> Modified by: Joseph A. Huwaldt </p>
048 * 
049 * @author Joseph A. Huwaldt, Date: March 5, 2003
050 * @version October 29, 2017
051 * 
052 * @param <E> The type of GeomPoint contained in this list of points.
053 */
054@SuppressWarnings({"serial", "CloneableImplementsClone"})
055public final class PointString<E extends GeomPoint> extends AbstractPointGeomList<PointString<E>,E> {
056
057    private FastTable<E> _list;
058
059    /**
060     * Return the list underlying this geometry list.
061     *
062     * @return The list underlying this geometry list.
063     */
064    @Override
065    protected FastTable<E> getList() {
066        return _list;
067    }
068
069    /**
070     * Returns a new, preallocated or recycled <code>PointString</code> instance
071     * (on the stack when executing in a <code>StackContext</code>), that can
072     * store a list of {@link GeomPoint} objects.
073     *
074     * @return A new PointString instance.
075     */
076    public static PointString newInstance() {
077        PointString list = FACTORY.object();
078        list._list = FastTable.newInstance();
079        return list;
080    }
081
082    /**
083     * Returns a new, preallocated or recycled <code>PointString</code> instance
084     * (on the stack when executing in a <code>StackContext</code>) with the
085     * specified name, that can store a list of {@link GeomPoint} objects.
086     *
087     * @param name The name to be assigned to this list (may be <code>null</code>).
088     * @return A new PointString instance with the specified name.
089     */
090    public static PointString newInstance(String name) {
091        PointString list = PointString.newInstance();
092        list.setName(name);
093        return list;
094    }
095
096    /**
097     * Return a PointString made up of the {@link GeomPoint} objects in the
098     * specified collection.
099     *
100     * @param <E> The type of GeomPoint contained in this list of points.
101     * @param name The name to be assigned to this list (may be <code>null</code>).
102     * @param elements A collection of points. May not be null.
103     * @return A new PointString instance made up of the specified points.
104     */
105    public static <E extends GeomPoint> PointString<E> valueOf(String name, Collection<E> elements) {
106        for (Object element : elements) {
107            requireNonNull(element, RESOURCES.getString("collectionElementsNullErr"));
108            if (!(element instanceof GeomPoint))
109                throw new ClassCastException(MessageFormat.format(
110                        RESOURCES.getString("listElementTypeErr"), "PointString", "GeomPoint"));
111        }
112
113        PointString<E> list = PointString.newInstance(name);
114        list.addAll(elements);
115        
116        return list;
117    }
118
119    /**
120     * Return a PointString made up of the {@link GeomPoint} objects in the specified
121     * array.
122     *
123     * @param <E>      The type of GeomPoint contained in this list of points.
124     * @param name     The name to be assigned to this list (may be <code>null</code>).
125     * @param elements An array of points. May not be null.
126     * @return A new PointString instance made up of the specified points.
127     */
128    public static <E extends GeomPoint> PointString<E> valueOf(String name, E... elements) {
129        requireNonNull(elements);
130        PointString<E> list = PointString.newInstance(name);
131        list.addAll(elements);
132
133        return list;
134    }
135
136    /**
137     * Return a PointString made up of the {@link GeomPoint} objects in the
138     * specified array.
139     *
140     * @param <E> The type of GeomPoint contained in this list of points.
141     * @param elements An array of points. May not be null.
142     * @return A new PointString instance made up of the specified points.
143     */
144    public static <E extends GeomPoint> PointString<E> valueOf(E... elements) {
145        return PointString.valueOf(null, elements);
146    }
147
148    /**
149     * Return the total number of points in this geometry element.
150     *
151     * @return The total number of points in this geometry element.
152     */
153    @Override
154    public int getNumberOfPoints() {
155        return size();
156    }
157    
158    /**
159     * Returns the range of elements in this list from the specified start and
160     * ending indexes.
161     *
162     * @param first index of the first element to return (0 returns the 1st
163     *  element, -1 returns the last, etc).
164     * @param last index of the last element to return (0 returns the 1st
165     *  element, -1 returns the last, etc).
166     * @return the list of elements in the given range from this list.
167     * @throws IndexOutOfBoundsException if the given index is out of range:
168     *  <code>index >= size()</code>
169     */
170    @Override
171    public PointString<E> getRange(int first, int last) {
172        first = normalizeIndex(first);
173        last = normalizeIndex(last);
174
175        PointString<E> list = PointString.newInstance();
176        for (int i=first; i <= last; ++i)
177            list.add(get(i));
178
179        return list;
180    }
181
182    /**
183     * Returns an new {@link PointString} with the elements in this list in
184     * reverse order.
185     *
186     * @return A new PointString with the elements in this string in reverse order.
187     */
188    @Override
189    public PointString<E> reverse() {
190        PointString<E> list = PointString.newInstance();
191        copyState(list);
192        int size = this.size();
193        for (int i=size-1; i >= 0; --i) {
194            list.add(get(i));
195        }
196        return list;
197    }
198
199    /**
200     * Returns a new {@link PointString} that is identical to this string but
201     * with every other point removed. The 1st point (index = 0) and last point
202     * (index = size()-1) are always retained. If there are less than 3 points
203     * in the string, then a new string is returned that contains the same
204     * points as this string.
205     *
206     * @return A new PointString identical to this one but with every other
207     *      point removed.
208     */
209    public PointString<E> thin() {
210        PointString<E> list = PointString.newInstance();
211        copyState(list);
212        
213        int size = size();
214        if (size < 3) {
215            list.addAll(this);
216            return list;
217        }
218        
219        int sizem1 = size - 1;
220        for (int i=0; i < sizem1; i += 2) {
221            list.add(get(i));
222        }
223        list.add(get(size-1));
224        
225        return list;
226    }
227    
228    /**
229     * Returns a new {@link PointString} that is identical to this string but
230     * with a new point linearly interpolated half-way between each of the
231     * existing points. If there are less than 2 points in the string, then a
232     * new string is returned that contains the same point as this string.
233     *
234     * @return A new PointString that is identical to this string but with a new
235     * point linearly interpolated half-way between each of the existing points.
236     */
237    public PointString enrich() {
238        PointString list = PointString.newInstance();
239        copyState(list);
240        
241        int size = size();
242        if (size < 2) {
243            list.add(get(0));
244            return list;
245        }
246        
247        GeomPoint p1 = get(0);
248        list.add(p1);
249        for (int i=1; i < size; ++i) {
250            GeomPoint p2 = get(i);
251            Point pa = p1.plus(p2).divide(2);   //  Calculate the average point.
252            list.add(pa);
253            list.add(p2);
254            p1 = p2;
255        }
256        
257        return list;
258    }
259    
260    /**
261     * Returns the length of this PointString in terms of the sum of the distances between
262     * each consecutive point in the string.
263     *
264     * @return The length of the string: sum(point(i).distance(point(i-1))).
265     */
266    public Parameter<Length> length() {
267        StackContext.enter();
268        try {
269            int numPts = size();
270            Parameter d = Parameter.ZERO_LENGTH.to(get(0).getUnit());
271            for (int i = 1; i < numPts; ++i) {
272                Parameter<Length> dist = get(i).distance(get(i - 1));
273                if (!dist.isApproxZero())
274                    d = d.plus(dist);
275            }
276            return StackContext.outerCopy(d);
277        } finally {
278            StackContext.exit();
279        }
280    }
281
282    /**
283     * Calculate the average or centroid of all the points in this PointString.
284     *
285     * @return The average or centroid of all the points in this PointString.
286     */
287    public Point getAverage() {
288        return GeomUtil.averagePoints(this);
289    }
290    
291    /**
292     * Return a new {@link PointString} with the points in this list sorted into ascending
293     * order with respect to the specified coordinate dimension. This can also be used to
294     * remove duplicate points after the list has been sorted. To sort in descending
295     * order, use this method followed immediately by reverse() (e.g.: 
296     * <code>str.sort(Point.X,false,null).reverse();</code>).
297     *
298     * @param dim       The physical coordinate dimension to be sorted (e.g.: 0=X, 1=Y, etc).
299     * @param removeDup Duplicate points are removed from the output if this is true.
300     * @param tol       The tolerance for identifying duplicate points. If null is passed,
301     *                  then essentially exact equality is required (if removeDup is
302     *                  false, you may pass null).
303     * @return A new PointString with the points in this list sorted into ascending order
304     *         with respect to the specified coordinate dimension and possibly with
305     *         duplicate points removed.
306     */
307    public PointString<E> sort(int dim, boolean removeDup, Parameter<Length> tol) {
308        //  Create a new PointString and add the points to it.
309        PointString<E> str = PointString.newInstance();
310        str.addAll(this);
311        
312        //  Sort the new PointString
313        Collections.sort(str, new PntComparator(dim));
314        
315        //  Remove duplicates if requested.
316        if (removeDup) {
317            E old = null;
318            Iterator<E> it = str.iterator();
319            while (it.hasNext()) {
320                E p = it.next();
321                if (p.isApproxEqual(old, tol))
322                    it.remove();
323                old = p;
324            }
325        }
326        
327        return str;
328    }
329
330    /**
331     * A Comparator that compares the specified dimension of two points for order.
332     */
333    private class PntComparator implements Comparator<GeomPoint>, Serializable {
334        private final int _dim;
335
336        public PntComparator(int dim) {
337            _dim = dim;
338        }
339
340        @Override
341        public int compare(GeomPoint o1, GeomPoint o2) {
342            double v1 = o1.getValue(_dim);
343            double v2 = o2.getValue(_dim, o1.getUnit());
344            return Double.compare(v1, v2);
345        }
346    }
347
348    /**
349     *  Return the equivalent of this list converted to the specified number
350     *  of physical dimensions.  If the number of dimensions is greater than
351     *  this element, then zeros are added to the additional dimensions.
352     *  If the number of dimensions is less than this element, then
353     *  the extra dimensions are simply dropped (truncated).  If
354     *  the new dimensions are the same as the dimension of this element,
355     *  then this list is simply returned.
356     *
357     *  @param newDim  The dimension of the element to return.
358     *  @return The equivalent of this list converted to the new dimensions.
359     */
360    @Override
361    public PointString toDimension(int newDim) {
362        if (getPhyDimension() == newDim)
363            return this;
364        PointString newList = PointString.newInstance();
365        copyState(newList);
366        int size = this.size();
367        for (int i=0; i < size; ++i) {
368            E element = this.get(i);
369            newList.add(element.toDimension(newDim));
370        }
371        return newList;
372    }
373
374    /**
375     * Returns the equivalent to this list but with <I>all</I> the elements stated in the
376     * specified unit.
377     *
378     * @param unit the length unit of the list to be returned. May not be null.
379     * @return an equivalent to this list but stated in the specified unit.
380     * @throws ConversionException if the the input unit is not a length unit.
381     */
382    @Override
383    public PointString to(Unit<Length> unit) {
384        requireNonNull(unit);
385        PointString list = PointString.newInstance();
386        copyState(list);
387        int size = this.size();
388        for (int i = 0; i < size; ++i) {
389            E e = this.get(i);
390            list.add(e.to(unit));
391        }
392        return list;
393    }
394
395    /**
396     * Returns a copy of this <code>PointString</code> instance
397     * {@link javolution.context.AllocatorContext allocated} by the calling
398     * thread (possibly on the stack).
399     *
400     * @return an identical and independent copy of this object.
401     */
402    @Override
403    public PointString<E> copy() {
404        return copyOf(this);
405    }
406
407    /**
408     * Return a copy of this object with any transformations or subranges
409     * removed (applied).
410     *
411     * @return A copy of this object with any transformations or subranges
412     * removed (applied).
413     */
414    @Override
415    public PointString<E> copyToReal() {
416        PointString<E> newList = PointString.newInstance();
417        copyState(newList);
418        int size = this.size();
419        for (int i=0; i < size; ++i) {
420            E element = this.get(i);
421            newList.add((E)element.copyToReal());
422        }
423        return newList;
424    }
425    
426    /**
427     * Returns transformed version of this element. The returned object
428     * implements {@link GeomTransform} and contains transformed versions of the
429     * contents of this list as children.
430     *
431     * @param transform The transformation to apply to this geometry. May not be null.
432     * @return A new PointString that is identical to this one with the
433     *      specified transformation applied.
434     * @throws DimensionException if this element is not 3D.
435     */
436    @Override
437    public PointString getTransformed(GTransform transform) {
438        requireNonNull(transform);
439        PointString list = PointString.newInstance();
440        copyState(list);
441        int size = this.size();
442        for (int i=0; i < size; ++i) {
443            E element = this.get(i);
444            list.add(element.getTransformed(transform));
445        }
446        return list;
447    }
448
449    /**
450     * Replaces the {@link GeomPoint} at the specified position in this list with the
451     * specified element. Null elements are ignored. The input element must have the same
452     * physical dimensions as the other items in this list, or an exception is thrown.
453     *
454     * @param index   The index of the element to replace (0 returns the 1st element, -1
455     *                returns the last, -2 returns the 2nd from last, etc).
456     * @param element The element to be stored at the specified position. May not be null.
457     * @return The element previously at the specified position in this list.
458     * @throws java.lang.IndexOutOfBoundsException - if <code>index > size()</code>
459     * @throws DimensionException if the input element's dimensions are different from
460     * this list's dimensions.
461     */
462    @Override
463    public E set(int index, E element) {
464        return super.set(index, requireNonNull(element));
465    }
466
467    /**
468     * Inserts the specified {@link GeomPoint} at the specified position in this list.
469     * Shifts the element currently at that position (if any) and any subsequent elements
470     * to the right (adds one to their indices). Null values are ignored. The input
471     * value must have the same physical dimensions as the other items in this list, or
472     * an exception is thrown.
473     * <p>
474     * Note: If this method is used concurrent access must be synchronized (the list is
475     * not thread-safe).
476     * </p>
477     *
478     * @param index the index at which the specified element is to be inserted. (0 returns
479     *              the 1st element, -1 returns the last, -2 returns the 2nd from last,
480     *              etc).
481     * @param value the element to be inserted. May not be null.
482     * @throws IndexOutOfBoundsException if <code>index > size()</code>
483     * @throws DimensionException if the input value dimensions are different from
484     * this list's dimensions.
485     */
486    @Override
487    public void add(int index, E value) {
488        super.add(index, requireNonNull(value));
489    }
490
491    /**
492     * Inserts all of the {@link GeomPoint} objects in the specified collection into this
493     * list at the specified position. Shifts the element currently at that position (if
494     * any) and any subsequent elements to the right (increases their indices). The new
495     * elements will appear in this list in the order that they are returned by the
496     * specified collection's iterator. The behavior of this operation is unspecified if
497     * the specified collection is modified while the operation is in progress. Note that
498     * this will occur if the specified collection is this list, and it's nonempty.  The
499     * input elements must have the same physical dimensions as the other items in this
500     * list, or an exception is thrown.
501     *
502     * @param index index at which to insert first element from the specified collection.
503     * @param c     Elements to be inserted into this collection. May not be null.
504     * @return <code>true</code> if this collection changed as a result of the call
505     * @throws DimensionException if the input element's dimensions are different from
506     * this list's dimensions.
507     */
508    @Override
509    public boolean addAll(int index, Collection<? extends E> c) {
510        int thisSize = this.size();
511        for (Object element : c) {
512            requireNonNull(element, RESOURCES.getString("collectionElementsNullErr"));
513            if (!(element instanceof GeomPoint))
514                throw new ClassCastException(MessageFormat.format(
515                        RESOURCES.getString("listElementTypeErr"), "PointString", "GeomPoint"));
516            if (thisSize != 0) {
517                if (((GeomElement)element).getPhyDimension() != this.getPhyDimension())
518                    throw new DimensionException(RESOURCES.getString("dimensionErr"));
519            }
520        }
521        return super.addAll(index, c);
522    }
523
524    /**
525     * Holds the default XML representation for this object.
526     */
527    @SuppressWarnings("FieldNameHidesFieldInSuperclass")
528    protected static final XMLFormat<PointString> XML = new XMLFormat<PointString>(PointString.class) {
529
530        @Override
531        public PointString newInstance(Class<PointString> cls, XMLFormat.InputElement xml) throws XMLStreamException {
532            PointString obj = PointString.newInstance();
533            return obj;
534        }
535
536        @Override
537        public void read(XMLFormat.InputElement xml, PointString obj) throws XMLStreamException {
538            AbstractPointGeomList.XML.read(xml, obj);     // Call parent read.
539        }
540
541        @Override
542        public void write(PointString obj, XMLFormat.OutputElement xml) throws XMLStreamException {
543            AbstractPointGeomList.XML.write(obj, xml);    // Call parent write.
544        }
545    };
546
547    //////////////////////
548    // Factory Creation //
549    //////////////////////
550    private static final ObjectFactory<PointString> FACTORY = new ObjectFactory<PointString>() {
551        @Override
552        protected PointString create() {
553            return new PointString();
554        }
555        @Override
556        protected void cleanup(PointString obj) {
557            obj.reset();
558        }
559    };
560
561    /**
562     * Recycles a case instance immediately (on the stack when executing in a
563     * StackContext).
564     *
565     * @param instance The instance to be recycled immediately.
566     */
567    public static void recycle(PointString instance) {
568        FACTORY.recycle(instance);
569    }
570
571    /**
572     * Do not allow the default constructor to be used except by subclasses.
573     */
574    protected PointString() {}
575
576    private static <E2 extends GeomPoint> PointString<E2> copyOf(PointString<E2> original) {
577        PointString<E2> o = PointString.newInstance();
578        original.copyState(o);
579        int size = original.size();
580        for (int i=0; i < size; ++i) {
581            E2 element = original.get(i);
582            o.add((E2)element.copy());
583        }
584        return o;
585    }
586
587    /**
588     * Tests the methods in this class.
589     *
590     * @param args Command-line arguments (not used).
591     */
592    public static void main(String args[]) {
593        System.out.println("Testing PointString:");
594
595        Point p1 = Point.valueOf(1, 4, 6, NonSI.FOOT);
596        Point p2 = Point.valueOf(7, 2, 5, NonSI.FOOT);
597        Point p3 = Point.valueOf(10, 8, 3, NonSI.FOOT);
598        Point p4 = Point.valueOf(12, 11, 9, NonSI.FOOT);
599        PointString<Point> str1 = PointString.valueOf("A String", p1, p2, p3, p4);
600        str1.putUserData("Creator", "Joe Huwaldt");
601        str1.putUserData("Date", "June 18, 2013");
602        System.out.println("str1 = " + str1);
603        System.out.println("points = ");
604        for (GeomPoint point : str1) {
605            System.out.println(point);
606        }
607
608        Vector<Length> V = Vector.valueOf(SI.METER, 2, 0, 0);
609        PointString<?> str2 = str1.getTransformed(GTransform.newTranslation(V));
610        System.out.println("\nTranslate str1 by V = " + V);
611        System.out.println("str2 = " + str2);
612        System.out.println("points = ");
613        for (GeomPoint point : str2) {
614            System.out.println(point);
615        }
616
617        V = Vector.valueOf(NonSI.FOOT, 0,2,0);
618        PointString<?> str3 = str2.getTransformed(GTransform.newTranslation(V));
619        System.out.println("\nTranslate str2 by V = " + V);
620        System.out.println("str3 = " + str3);
621        System.out.println("points = ");
622        for (GeomPoint point : str3) {
623            System.out.println(point);
624        }
625
626        //  Write out XML data.
627        try {
628            System.out.println();
629
630            // Creates some useful aliases for class names.
631            javolution.xml.XMLBinding binding = new GeomXMLBinding();
632
633            javolution.xml.XMLObjectWriter writer = javolution.xml.XMLObjectWriter.newInstance(System.out);
634            writer.setIndentation("    ");
635            writer.setBinding(binding);
636            writer.write(str1, "PointString", PointString.class);
637            writer.flush();
638
639            System.out.println();
640        } catch (Exception e) {
641            e.printStackTrace();
642        }
643
644    }
645}
646
647