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