package org.opensha2.geo;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static org.opensha2.internal.TextUtils.NEWLINE;
import org.opensha2.internal.Parsing;
import org.opensha2.internal.Parsing.Delimiter;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.Iterators;
import com.google.common.collect.Lists;
import com.google.common.primitives.Doubles;
import java.util.Iterator;
import java.util.List;
/**
* An immutable, ordered collection of {@link Location}s.
*
* <p>A {@code LocationList} must contain at least 1 {@code Location} and does
* not permit {@code null} elements. {@code LocationList} instances are
* immutable and calls to {@code remove()} when iterating will throw an
* {@code UnsupportedOperationException}. A variety of methods exist to simplify
* the creation of new lists via modification (e.g. {@link #resample(double)} ,
* {@link #reverse()}, and {@link #translate(LocationVector)}.
*
* <p>Consider using a {@link LocationList.Builder} (via {@link #builder()}) if
* a list is being compiled from numerous {@code Location}s that are not known
* in advance.
*
* <p><b>Note:</b> Although this class is not final, it cannot be subclassed
* outside its package as it has no public or protected constructors. Thus,
* instances of this type are guaranteed to be immutable.
*
* @author Peter Powers
*/
public abstract class LocationList implements Iterable<Location> {
/**
* Create a new {@code LocationList} containing all {@code Location}s in
* {@code locs}.
*
* @param locs to populate list with
* @throws IllegalArgumentException if {@code locs} is empty
*/
public static LocationList create(Location... locs) {
checkArgument(locs.length > 0);
return builder().add(locs).build();
}
/**
* Create a new {@code LocationList} containing all {@code Location}s in
* {@code locs}. This method avoids copying the supplied iterable if it is
* safe to do so.
*
* @param locs to populate list with
* @throws IllegalArgumentException if {@code locs} is empty
*/
public static LocationList create(Iterable<Location> locs) {
checkArgument(locs.iterator().hasNext());
if (locs instanceof LocationList) {
return (LocationList) locs;
}
if (locs instanceof ImmutableList) {
return new RegularLocationList((ImmutableList<Location>) locs);
}
return builder().addAll(locs).build();
}
/**
* Create a new {@code LocationList} from the supplied {@code String}. This
* method assumes that {@code s} is formatted as one or more space-delimited
* {@code [lat,lon,depth]} tuples (comma-delimited); see
* {@link Location#fromString(String)}.
*
* @param s {@code String} to read
* @return a new {@code LocationList}
* @see Location#fromString(String)
*/
public static LocationList fromString(String s) {
return create(Iterables.transform(
Parsing.split(checkNotNull(s), Delimiter.SPACE),
Location.stringConverter().reverse()));
}
LocationList() {}
/**
* Return the number of locations in this list.
*/
public abstract int size();
/**
* Return the location at {@code index}.
*
* @param index of {@code Location} to return
* @throws IndexOutOfBoundsException if the index is out of range (
* {@code index < 0 || index >= size()})
*/
public abstract Location get(int index);
/**
* Return the first {@code Location} in this list.
*/
public Location first() {
return get(0);
}
/**
* Return the last {@code Location} in this list.
*/
public Location last() {
return get(size() - 1);
}
/**
* Lazily compute the horizontal length of this {@code LocationList} in km.
* Method uses the {@link Locations#horzDistanceFast(Location, Location)}
* algorithm and ignores depth variation between locations. That is, it
* computes length as though all locations in the list have a depth of 0.0 km.
* Repeat calls to this method will recalculate the length each time.
*
* @return the length of the line connecting all {@code Location}s in this
* list, ignoring depth variations, or 0.0 if list only contains 1
* location
*/
public double length() {
double sum = 0.0;
if (size() == 1) {
return sum;
}
Location prev = first();
for (Location loc : Iterables.skip(this, 1)) {
sum += Locations.horzDistanceFast(prev, loc);
prev = loc;
}
return sum;
}
/**
* Lazily compute the average depth of the {@code Location}s in this list.
*/
public double depth() {
double depth = 0.0;
for (Location loc : this) {
depth += loc.depth();
}
return depth / size();
}
/**
* Lazily compute the bounds of the {@code Location}s in this list. Method
* delegates to {@link Locations#bounds(Iterable)}.
*/
public Bounds bounds() {
return Locations.bounds(this);
}
@Override
public String toString() {
return NEWLINE + Joiner.on(NEWLINE).join(this) + NEWLINE;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (!(obj instanceof LocationList)) {
return false;
}
LocationList other = (LocationList) obj;
if (this.size() != other.size()) {
return false;
}
return Iterators.elementsEqual(this.iterator(), other.iterator());
}
@Override
public int hashCode() {
return Lists.newArrayList(this).hashCode();
}
/**
* Return a new {@code LocationList} created by resampling {@code this} with
* the desired maximum spacing. The actual spacing of the returned list will
* likely differ, as spacing is adjusted down to maintain uniform divisions.
* The original vertices are also not preserved such that some corners might
* be adversely clipped if {@code spacing} is large. Buyer beware.
*
* <p>If the length of this list is less than the desired spacing, this list
* is returned.
*
* @param spacing resample interval
*/
public LocationList resample(double spacing) {
checkArgument(
Doubles.isFinite(spacing) && spacing > 0.0,
"Spacing must be positive, real number");
double length = this.length();
if (length <= spacing) {
return this;
}
/*
* TODO Consider using rint() which will keep the actual spacing closer to
* the target spacing, albeit sometimes larger.
*/
spacing = length / Math.ceil(length / spacing);
List<Location> resampled = Lists.newArrayList();
Location start = this.first();
resampled.add(start);
double walker = spacing;
for (Location loc : Iterables.skip(this, 1)) {
LocationVector v = LocationVector.create(start, loc);
double distance = Locations.horzDistanceFast(start, loc);
while (walker <= distance) {
resampled.add(Locations.location(start, v.azimuth(), walker));
walker += spacing;
}
start = loc;
walker -= distance;
}
// replace last point to be exact
resampled.set(resampled.size() - 1, this.last());
return LocationList.create(resampled);
}
/**
* Partition this {@code LocationList} into sub-lists of desired
* {@code length}. The actual length of the returned lists will likely differ,
* as the lengths of the sub-lists are adjusted up or down to a value closest
* to the target length that yields sub-lists of equal length.
*
* <p>If this list is shorter than the sub-list target length, the method will
* return {@code this}.
*
* @param length target length of sub-lists
*/
public List<LocationList> partition(double length) {
checkArgument(
Doubles.isFinite(length) && length > 0.0,
"Length must be positive, real number");
double totalLength = this.length();
if (totalLength <= length) {
return ImmutableList.of(this);
}
ImmutableList.Builder<LocationList> partitions = ImmutableList.builder();
double partitionLength = totalLength / Math.rint(totalLength / length);
double[] distances = distances();
LocationList.Builder partition = LocationList.builder();
double residual = 0.0;
for (int i = 0; i < distances.length; i++) {
Location start = get(i);
partition.add(start);
double distance = distances[i] + residual;
while (partitionLength < distance) {
/*
* Catch the edge case where, on the last segment of a trace that needs
* to be partitioned we just undershoot the last point. In the absence
* of this we can end up with a final section of length ≈ 0.
*
*/
if (i == distances.length - 1 && distance < 1.5 * partitionLength) {
break;
}
LocationVector v = LocationVector.create(start, get(i + 1));
Location end = Locations.location(start, v.azimuth(), partitionLength - residual);
partition.add(end);
partitions.add(partition.build());
partition = LocationList.builder();
partition.add(end);
start = end;
distance -= partitionLength;
residual = 0.0;
}
residual = distance;
}
partition.add(last());
partitions.add(partition.build());
return partitions.build();
}
private double[] distances() {
double[] distances = new double[size() - 1];
for (int i = 0; i < size() - 1; i++) {
distances[i] = Locations.horzDistanceFast(get(i), get(i + 1));
}
return distances;
}
/**
* Return a new {@code LocationList} with {@code Location}s in reverse order.
*/
public LocationList reverse() {
if (this instanceof RegularLocationList) {
return new RegularLocationList(((RegularLocationList) this).locs.reverse());
}
return create(ImmutableList.copyOf(this).reverse());
}
/**
* Return a new {@code LocationList} with each {@code Location} translated
* according to the supplied vector.
*
* @param vector to translate list by
*/
public LocationList translate(final LocationVector vector) {
return builder().addAll(Iterables.transform(this, new Function<Location, Location>() {
@Override
public Location apply(Location loc) {
return Locations.location(loc, vector);
}
})).build();
}
/* The default implementation that delegates to an ImmutableList. */
@VisibleForTesting
static class RegularLocationList extends LocationList {
final ImmutableList<Location> locs;
private RegularLocationList(ImmutableList<Location> locs) {
this.locs = locs;
}
@Override
public int size() {
return locs.size();
}
@Override
public Location get(int index) {
return locs.get(index);
}
@Override
public Iterator<Location> iterator() {
return locs.iterator();
}
}
/**
* Return a new {@code LocationList} builder.
*/
public static Builder builder() {
return new Builder();
}
/**
* A reusable builder of {@code LocationList}s. Repeat calls to
* {@code build()} will return multiple lists in series with each new list
* containing all the {@code Location}s of the one before it. Builders do not
* permit the addition of {@code null} elements.
*
* <p>Use {@link LocationList#builder()} to create new builder instances.
*/
public static final class Builder {
@VisibleForTesting
ImmutableList.Builder<Location> builder;
private Builder() {
builder = ImmutableList.builder();
}
/**
* Add a {@code Location} to the {@code LocationList}.
*
* @param loc to add
* @return this {@code Builder}
*/
public Builder add(Location loc) {
builder.add(loc);
return this;
}
/**
* Add a new {@code Location} specified by the supplied latitude and
* longitude and a depth of 0 km to the {@code LocationList}.
*
* @param lat latitude in decimal degrees
* @param lon longitude in decimal degrees
* @return this {@code Builder}
* @throws IllegalArgumentException if any values are out of range
* @see Coordinates
*/
public Builder add(double lat, double lon) {
builder.add(Location.create(lat, lon));
return this;
}
/**
* Add a new {@code Location} specified by the supplied latitude, longitude,
* and depth to the {@code LocationList}.
*
* @param lat latitude in decimal degrees
* @param lon longitude in decimal degrees
* @param depth in km (positive down)
* @return this {@code Builder}
* @throws IllegalArgumentException if any values are out of range
* @see Coordinates
*/
public Builder add(double lat, double lon, double depth) {
builder.add(Location.create(lat, lon, depth));
return this;
}
/**
* Add each {@code Location} in {@code locs} to the {@code LocationList} .
*
* @param locs to add
* @return this {@code Builder}
*/
public Builder add(Location... locs) {
builder.add(locs);
return this;
}
/**
* Add each {@code Location} in {@code locs} to the {@code LocationList} .
*
* @param locs to add
* @return this {@code Builder}
*/
public Builder addAll(Iterable<Location> locs) {
builder.addAll(locs);
return this;
}
/**
* Return a newly created {@code LocationList}.
*
* @return a new ordered collection of {@code Location}s
* @throws IllegalStateException if the list to be returned is empty
*/
public LocationList build() {
ImmutableList<Location> locs = builder.build();
checkState(locs.size() > 0, "LocationList is empty");
return new RegularLocationList(locs);
}
}
}