code: /*******************************************************************************
   * Copyright (c) 2015 MITRE
   * All rights reserved. This program and the accompanying materials
   * are made available under the terms of the Apache License, Version 2.0 which
   * accompanies this distribution and is available at
   *    http://www.apache.org/licenses/LICENSE-2.0.txt
   ******************************************************************************/
  
  package org.locationtech.spatial4j.shape.impl;
  
  import org.locationtech.spatial4j.context.SpatialContext;
  import org.locationtech.spatial4j.shape.BaseShape;
  import org.locationtech.spatial4j.shape.Point;
  import org.locationtech.spatial4j.shape.Rectangle;
  import org.locationtech.spatial4j.shape.Shape;
  import org.locationtech.spatial4j.shape.ShapeCollection;
  import org.locationtech.spatial4j.shape.SpatialRelation;
  
  import java.util.AbstractList;
  import java.util.ArrayList;
  import java.util.Collections;
  import java.util.List;
  
  /**
   * A BufferedLineString is a collection of {@link org.locationtech.spatial4j.shape.impl.BufferedLine} shapes,
   * resulting in what some call a "Track" or "Polyline" (ESRI terminology).
   * The buffer can be 0.  Note that BufferedLine isn't yet aware of geodesics (e.g. the anti-meridian).
   */
  public class BufferedLineString extends BaseShape<SpatialContext> {
  
    //TODO add some geospatial awareness like:
    // segment that spans at the dateline (split it at DL?).
  
    private final ShapeCollection<BufferedLine> segments;
    private final double buf;
  
    /**
     * Needs at least 1 point, usually more than that.  If just one then it's
     * internally treated like 2 points.
     */
    public BufferedLineString(List<Point> points, double buf, SpatialContext ctx) {
      this(points, buf, false, ctx);
    }
  
    /**
     * @param points ordered control points. If empty then this shape is empty.
     * @param buf Buffer &gt;= 0
     * @param expandBufForLongitudeSkew See {@link BufferedLine
     * #expandBufForLongitudeSkew(org.locationtech.spatial4j.shape.Point,
     * org.locationtech.spatial4j.shape.Point, double)}.
     *                                  If true then the buffer for each segment
     *                                  is computed.
     */
    public BufferedLineString(List<Point> points, double buf, boolean expandBufForLongitudeSkew,
                              SpatialContext ctx) {
      super(ctx);
      this.buf = buf;
  
      if (points.isEmpty()) {
        this.segments = ctx.makeCollection(Collections.<BufferedLine>emptyList());
      } else {
        List<BufferedLine> segments = new ArrayList<>(points.size() - 1);
  
        Point prevPoint = null;
        for (Point point : points) {
          if (prevPoint != null) {
            double segBuf = buf;
            if (expandBufForLongitudeSkew) {
              //TODO this is faulty in that it over-buffers.  See Issue#60.
              segBuf = BufferedLine.expandBufForLongitudeSkew(prevPoint, point, buf);
            }
            segments.add(new BufferedLine(prevPoint, point, segBuf, ctx));
          }
          prevPoint = point;
        }
        if (segments.isEmpty()) {//TODO throw exception instead?
          segments.add(new BufferedLine(prevPoint, prevPoint, buf, ctx));
        }
        this.segments = ctx.makeCollection(segments);
      }
    }
  
    @Override
    public boolean isEmpty() {
      return segments.isEmpty();
    }
  
    @Override
    public Shape getBuffered(double distance, SpatialContext ctx) {
      return ctx.makeBufferedLineString(getPoints(), buf + distance);
    }
  
    public ShapeCollection<BufferedLine> getSegments() {
      return segments;
    }
  
    public double getBuf() {
      return buf;
    }
  
    @Override
    public double getArea(SpatialContext ctx) {
      return segments.getArea(ctx);
    }
  
    @Override
    public SpatialRelation relate(Shape other) {
      return segments.relate(other);
    }
  
    @Override
    public boolean hasArea() {
      return segments.hasArea();
    }
  
    @Override
    public Point getCenter() {
      return segments.getCenter();
    }
  
    @Override
    public Rectangle getBoundingBox() {
      return segments.getBoundingBox();
    }
  
    @Override
    public String toString() {
      StringBuilder str = new StringBuilder(100);
      str.append("BufferedLineString(buf=").append(buf).append(" pts=");
      boolean first = true;
      for (Point point : getPoints()) {
        if (first) {
          first = false;
        } else {
          str.append(", ");
        }
        str.append(point.getX()).append(' ').append(point.getY());
      }
      str.append(')');
      return str.toString();
    }
  
    public List<Point> getPoints() {
      if (segments.isEmpty())
        return Collections.emptyList();
      final List<BufferedLine> lines = segments.getShapes();
      return new AbstractList<Point>() {
        @Override
        public Point get(int index) {
          if (index == 0)
            return lines.get(0).getA();
          return lines.get(index - 1).getB();
        }
  
        @Override
        public int size() {
          return lines.size() + 1;
        }
      };
    }
  
    @Override
    public boolean equals(Object o) {
      if (this == o) return true;
      if (o == null || getClass() != o.getClass()) return false;
  
      BufferedLineString that = (BufferedLineString) o;
  
      if (Double.compare(that.buf, buf) != 0) return false;
      if (!segments.equals(that.segments)) return false;
  
      return true;
    }
  
    @Override
    public int hashCode() {
      int result;
      long temp;
      result = segments.hashCode();
      temp = buf != +0.0d ? Double.doubleToLongBits(buf) : 0L;
      result = 31 * result + (int) (temp ^ (temp >>> 32));
      return result;
    }
  }
  