org.locationtech.spatial4j.shape.impl.BBoxCalculator Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of spatial4j Show documentation
Show all versions of spatial4j Show documentation
Spatial4j is a general purpose spatial / geospatial ASL licensed open-source Java library. It's
core capabilities are 3-fold: to provide common geospatially-aware shapes, to provide distance
calculations and other math, and to read shape formats like WKT and GeoJSON.
The newest version!
/*******************************************************************************
* Copyright (c) 2015 David Smiley
* 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.Rectangle;
import java.util.Iterator;
import java.util.Map;
import java.util.TreeMap;
/**
* (INTERNAL) Calculates the minimum bounding box given a bunch of rectangles (ranges). It's a temporary object and not
* thread-safe; throw it away when done.
* For a cartesian space, the calculations are trivial but it is not for geodetic. For
* geodetic, it must maintain an ordered set of disjoint ranges as each range is provided.
*/
public class BBoxCalculator {
private final SpatialContext ctx;
private double minY = Double.POSITIVE_INFINITY;
private double maxY = Double.NEGATIVE_INFINITY;
private double minX = Double.POSITIVE_INFINITY;
private double maxX = Double.NEGATIVE_INFINITY;
/** Sorted list of disjoint X ranges keyed by maxX and with minX stored as the "value". */
private TreeMap ranges; // maxX -> minX
// note: The use of a TreeMap of Double objects is a bit heavy for the points-only use-case. In such a case,
// we could instead maintain an array of longitudes we just add onto during expandXRange(). A simplified version
// of the processRanges() method could be used that initially sorts and then proceeds in a similar but simplified
// fashion.
public BBoxCalculator(SpatialContext ctx) {
this.ctx = ctx;
}
public void expandRange(Rectangle rect) {
expandRange(rect.getMinX(), rect.getMaxX(), rect.getMinY(), rect.getMaxY());
}
public void expandRange(final double minX, final double maxX, double minY, double maxY) {
this.minY = Math.min(this.minY, minY);
this.maxY = Math.max(this.maxY, maxY);
expandXRange(minX, maxX);
}//expandRange
public void expandXRange(double minX, double maxX) {
if (!ctx.isGeo()) {
this.minX = Math.min(this.minX, minX);
this.maxX = Math.max(this.maxX, maxX);
return;
}
if (doesXWorldWrap())
return;
if (ranges == null) {
ranges = new TreeMap<>();
ranges.put(maxX, minX);
return;
}
assert !ranges.isEmpty();
//now the hard part!
//Get an iterator starting from the first entry that either contains minX or it's to the right of minX
Iterator> entryIter = ranges.tailMap(minX, true/*inclusive*/).entrySet().iterator();
if (!entryIter.hasNext()) {
entryIter = ranges.entrySet().iterator();//wrapped across dateline
}
Map.Entry entry = entryIter.next();
Double entryMin = entry.getValue();
Double entryMax = entry.getKey();
//See if entry contains maxX
if (rangeContains(entryMin, entryMax, maxX)) {
// Easy: either minX is also within this entry in which case nothing to do, or it's below in which case
// we just need to update the minX of this entry.
//See if entry contains minX
if (rangeContains(entryMin, entryMax, minX)) {
// This entry & the new range together might wrap the world.
if ( (minX != entryMin || maxX != entryMax) //ranges not equal
&& rangeContains(minX, maxX, entryMin) && rangeContains(minX, maxX, entryMax)) {
this.minX = -180;
this.maxX = +180;
ranges = null;
}
// Done; nothing to do.
} else {
//Done: Update entry's start to be minX
// note: TreeMap's Map.Entry doesn't support setting the value :-( So we remove & add the entry.
entryIter.remove();
ranges.put(entryMax, minX);
}
} else {//entry does NOT contain maxX:
// We're going to insert an entry. Determine it's min & max. While finding the max, we'll delete entries
// that overlap with the new entry.
// newMinX is basically the lower of minX & entryMin
final Double newMinX = rangeContains(entryMin, entryMax, minX) ? entryMin : minX;
Double newMaxX = maxX;
//Loop through entries (starting with current) to see if we should remove it. At the last one, update newMaxX.
while (rangeContains(newMinX, newMaxX, entryMin)) {
entryIter.remove();//remove entry!
if (!rangeContains(minX, maxX, entryMax)) {
newMaxX = entryMax;//adjust newMaxX and stop.
break;
}
// get new entry:
if (!entryIter.hasNext()) {
if (ranges.isEmpty()) {
break;
}
//wrap around (can only happen once)
entryIter = ranges.entrySet().iterator();
}
entry = entryIter.next();
entryMin = entry.getValue();
entryMax = entry.getKey();
}
//Add entry
ranges.put(newMaxX, newMinX);
}
}
private void processRanges() {
if (ranges.size() == 1) { // an optimization
Map.Entry rangeEntry = ranges.firstEntry();
minX = rangeEntry.getValue();
maxX = rangeEntry.getKey();
} else {
// Find the biggest gap. Whenever we do, update minX & maxX for the rect opposite of the gap.
Map.Entry prevRange = ranges.lastEntry();
double biggestGap = 0;
double possibleRemainingGap = 360; //calculating this enables us to exit early; often on the first lap!
for (Map.Entry range : ranges.entrySet()) {
// calc width of this range and the gap before it.
double widthPlusGap = range.getKey() - prevRange.getKey();// this max - last max
if (widthPlusGap < 0) {
widthPlusGap += 360;
}
double gap = range.getValue() - prevRange.getKey(); // this min - last max
if (gap < 0) {
gap += 360;
}
// reduce possibleRemainingGap by this range width and trailing gap.
possibleRemainingGap -= widthPlusGap;
if (gap > biggestGap) {
biggestGap = gap;
minX = range.getValue();
maxX = prevRange.getKey();
if (possibleRemainingGap <= biggestGap) {
break;// no point in continuing
}
}
prevRange = range;
}
}
// Null out the ranges to signify we processed them
ranges = null;
}
private static boolean rangeContains(double minX, double maxX, double x) {
if (minX <= maxX)
return x >= minX && x <= maxX;
else
return x >= minX || x <= maxX;
}
public boolean doesXWorldWrap() {
assert ctx.isGeo();
//note: not dependent on "ranges", since once we expand to world bounds then ranges is null'ed out
return minX == -180 && maxX == 180;
}
public Rectangle getBoundary() {
return ctx.makeRectangle(getMinX(), getMaxX(), getMinY(), getMaxY());
}
public double getMinX() {
if (ranges != null) {
processRanges();
}
return minX;
}
public double getMaxX() {
if (ranges != null) {
processRanges();
}
return maxX;
}
public double getMinY() {
return minY;
}
public double getMaxY() {
return maxY;
}
}