/*******************************************************************************
* 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.distance.DistanceUtils;
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.SpatialRelation;
import static org.locationtech.spatial4j.shape.SpatialRelation.CONTAINS;
import static org.locationtech.spatial4j.shape.SpatialRelation.DISJOINT;
import static org.locationtech.spatial4j.shape.SpatialRelation.INTERSECTS;
import static org.locationtech.spatial4j.shape.SpatialRelation.WITHIN;
/**
* INTERNAL: A line between two points with a buffer distance extending in every direction. By
* contrast, an un-buffered line covers no area and as such is extremely unlikely to intersect with
* a point. BufferedLine isn't yet aware of geodesics (e.g. the dateline); it operates in Euclidean
* space.
*/
public class BufferedLine extends BaseShape<SpatialContext> {
private final Point pA, pB;
private final double buf;
private final Rectangle bbox;
/**
* the primary line; passes through pA & pB
*/
private final InfBufLine linePrimary;
/**
* perpendicular to the primary line, centered between pA & pB
*/
private final InfBufLine linePerp;
/**
* Creates a buffered line from pA to pB. The buffer extends on both sides of
* the line, making the width 2x the buffer. The buffer extends out from
* pA & pB, making the line in effect 2x the buffer longer than pA to pB.
*
* @param pA start point
* @param pB end point
* @param buf the buffer distance in degrees
* @param ctx
*/
public BufferedLine(Point pA, Point pB, double buf, SpatialContext ctx) {
super(ctx);
assert buf >= 0;//TODO support buf=0 via another class ?
/**
* If true, buf should bump-out from the pA & pB, in effect
* extending the line a little.
*/
final boolean bufExtend = true;//TODO support false and make this a
// parameter
this.pA = pA;
this.pB = pB;
this.buf = buf;
double deltaY = pB.getY() - pA.getY();
double deltaX = pB.getX() - pA.getX();
PointImpl center = new PointImpl(pA.getX() + deltaX / 2,
pA.getY() + deltaY / 2, null);
double perpExtent = bufExtend ? buf : 0;
if (deltaX == 0 && deltaY == 0) {
linePrimary = new InfBufLine(0, center, buf);
linePerp = new InfBufLine(Double.POSITIVE_INFINITY, center, buf);
} else {
linePrimary = new InfBufLine(deltaY / deltaX, center, buf);
double length = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
linePerp = new InfBufLine(-deltaX / deltaY, center,
length / 2 + perpExtent);
}
double minY, maxY;
double minX, maxX;
if (deltaX == 0) { // vertical
if (pA.getY() <= pB.getY()) {
minY = pA.getY();
maxY = pB.getY();
} else {
minY = pB.getY();
maxY = pA.getY();
}
minX = pA.getX() - buf;
maxX = pA.getX() + buf;
minY = minY - perpExtent;
maxY = maxY + perpExtent;
} else {
if (!bufExtend) {
throw new UnsupportedOperationException("TODO");
//solve for B & A (C=buf), one is buf-x, other is buf-y.
}
//Given a right triangle of A, B, C sides, C (hypotenuse) ==
// buf, and A + B == the bounding box offset from pA & pB in x & y.
double bboxBuf = buf * (1 + Math.abs(linePrimary.getSlope()))
* linePrimary.getDistDenomInv();
assert bboxBuf >= buf && bboxBuf <= buf * 1.5;
if (pA.getX() <= pB.getX()) {
minX = pA.getX() - bboxBuf;
maxX = pB.getX() + bboxBuf;
} else {
minX = pB.getX() - bboxBuf;
maxX = pA.getX() + bboxBuf;
}
if (pA.getY() <= pB.getY()) {
minY = pA.getY() - bboxBuf;
maxY = pB.getY() + bboxBuf;
} else {
minY = pB.getY() - bboxBuf;
maxY = pA.getY() + bboxBuf;
}
}
Rectangle bounds = ctx.getWorldBounds();
bbox = ctx.makeRectangle(
Math.max(bounds.getMinX(), minX),
Math.min(bounds.getMaxX(), maxX),
Math.max(bounds.getMinY(), minY),
Math.min(bounds.getMaxY(), maxY));
}
@Override
public boolean isEmpty() {
return pA.isEmpty();
}
@Override
public Shape getBuffered(double distance, SpatialContext ctx) {
return new BufferedLine(pA, pB, buf + distance, ctx);
}
/**
* Calls {@link DistanceUtils#calcLonDegreesAtLat(double, double)} given pA or pB's latitude;
* whichever is farthest. It's useful to expand a buffer of a line segment when used in
* a geospatial context to cover the desired area.
*/
public static double expandBufForLongitudeSkew(Point pA, Point pB,
double buf) {
double absA = Math.abs(pA.getY());
double absB = Math.abs(pB.getY());
double maxLat = Math.max(absA, absB);
double newBuf = DistanceUtils.calcLonDegreesAtLat(maxLat, buf);
// if (newBuf + maxLat >= 90) {
// //TODO substitute spherical cap ?
// }
assert newBuf >= buf;
return newBuf;
}
@Override
public SpatialRelation relate(Shape other) {
if (other instanceof Point)
return contains((Point) other) ? CONTAINS : DISJOINT;
if (other instanceof Rectangle)
return relate((Rectangle) other);
throw new UnsupportedOperationException();
}
public SpatialRelation relate(Rectangle r) {
//Check BBox for disjoint & within.
SpatialRelation bboxR = bbox.relate(r);
if (bboxR == DISJOINT || bboxR == WITHIN)
return bboxR;
//Either CONTAINS, INTERSECTS, or DISJOINT
Point scratch = new PointImpl(0, 0, null);
Point prC = r.getCenter();
SpatialRelation result = linePrimary.relate(r, prC, scratch);
if (result == DISJOINT)
return DISJOINT;
SpatialRelation resultOpp = linePerp.relate(r, prC, scratch);
if (resultOpp == DISJOINT)
return DISJOINT;
if (result == resultOpp)//either CONTAINS or INTERSECTS
return result;
return INTERSECTS;
}
public boolean contains(Point p) {
//TODO check bbox 1st?
return linePrimary.contains(p) && linePerp.contains(p);
}
public Rectangle getBoundingBox() {
return bbox;
}
@Override
public boolean hasArea() {
return buf > 0;
}
@Override
public double getArea(SpatialContext ctx) {
return linePrimary.getBuf() * linePerp.getBuf() * 4;
}
@Override
public Point getCenter() {
return getBoundingBox().getCenter();
}
public Point getA() {
return pA;
}
public Point getB() {
return pB;
}
public double getBuf() {
return buf;
}
/**
* INTERNAL
*/
public InfBufLine getLinePrimary() {
return linePrimary;
}
/**
* INTERNAL
*/
public InfBufLine getLinePerp() {
return linePerp;
}
@Override
public String toString() {
return "BufferedLine(" + pA + ", " + pB + " b=" + buf + ")";
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
BufferedLine that = (BufferedLine) o;
if (Double.compare(that.buf, buf) != 0) return false;
if (!pA.equals(that.pA)) return false;
if (!pB.equals(that.pB)) return false;
return true;
}
@Override
public int hashCode() {
int result;
long temp;
result = pA.hashCode();
result = 31 * result + pB.hashCode();
temp = buf != +0.0d ? Double.doubleToLongBits(buf) : 0L;
result = 31 * result + (int) (temp ^ (temp >>> 32));
return result;
}
}