/*******************************************************************************
* 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 <em>disjoint</em> X ranges keyed by maxX and with minX stored as the "value". */
private TreeMap<Double, Double> 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<Map.Entry<Double, Double>> entryIter = ranges.tailMap(minX, true/*inclusive*/).entrySet().iterator();
if (!entryIter.hasNext()) {
entryIter = ranges.entrySet().iterator();//wrapped across dateline
}
Map.Entry<Double, Double> 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<Double, Double> 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<Double, Double> prevRange = ranges.lastEntry();
double biggestGap = 0;
double possibleRemainingGap = 360; //calculating this enables us to exit early; often on the first lap!
for (Map.Entry<Double, Double> 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;
}
}