diff --git a/app/src/main/java/fi/bitrite/android/ws/ui/MapFragment.java b/app/src/main/java/fi/bitrite/android/ws/ui/MapFragment.java index 1bc47351..60beb3ab 100644 --- a/app/src/main/java/fi/bitrite/android/ws/ui/MapFragment.java +++ b/app/src/main/java/fi/bitrite/android/ws/ui/MapFragment.java @@ -10,6 +10,7 @@ import android.graphics.drawable.Drawable; import android.location.Location; import android.os.Bundle; +import android.os.Handler; import android.preference.PreferenceManager; import android.support.annotation.NonNull; import android.support.annotation.Nullable; @@ -64,7 +65,6 @@ import fi.bitrite.android.ws.repository.FavoriteRepository; import fi.bitrite.android.ws.repository.Resource; import fi.bitrite.android.ws.repository.SettingsRepository; -import fi.bitrite.android.ws.repository.UserRepository; import fi.bitrite.android.ws.ui.listadapter.UserListAdapter; import fi.bitrite.android.ws.ui.util.NavigationController; import fi.bitrite.android.ws.ui.util.UserMarker; @@ -72,6 +72,7 @@ import fi.bitrite.android.ws.util.LocationManager; import fi.bitrite.android.ws.util.LoggedInUserHelper; import fi.bitrite.android.ws.util.Tools; +import fi.bitrite.android.ws.util.UserRegionalCache; import io.reactivex.Completable; import io.reactivex.Observable; import io.reactivex.android.schedulers.AndroidSchedulers; @@ -83,7 +84,7 @@ public class MapFragment extends BaseFragment { private static final String TAG = "MapFragment"; @Inject LoggedInUserHelper mLoggedInUserHelper; - @Inject UserRepository mUserRepository; + @Inject UserRegionalCache mUserRegionalCache; @Inject FavoriteRepository mFavoriteRepository; @Inject SettingsRepository mSettingsRepository; @@ -163,8 +164,6 @@ public void onCreate(Bundle savedInstanceState) { mMarkerClusterer.setSingleLocationMarkerFactory(singleLocationMarkerFactory); mMarkerClusterer.setMultiLocationMarkerFactory(multiLocationMarkerFactory); mMarkerClusterer.setOnClusterClickListener(this::onClusterClick); - - loadOfflineUsers(); } @Nullable @@ -229,6 +228,9 @@ public boolean onZoom(ZoomEvent event) { doInitialMapMove(); }); + loadOfflineUsers(); + loadCachedUsers(); + return view; } @@ -440,10 +442,12 @@ private void fetchUsersForCurrentMapPosition() { if (mLastPosition.zoom < getResources().getInteger(R.integer.map_zoom_min_load)) { sendMessage(R.string.users_dont_load); } else { - sendMessage(R.string.loading_users); + Handler handler = new Handler(); + Runnable r = () -> sendMessage(R.string.loading_users); + handler.postDelayed(r, 500); getResumePauseDisposable().add( - mUserRepository.searchByLocation(mMap.getBoundingBox()) + mUserRegionalCache.searchByLocation(mMap.getBoundingBox()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(searchResult -> { if (searchResult.isEmpty()) { @@ -456,9 +460,10 @@ private void fetchUsersForCurrentMapPosition() { mMarkerClusterer.invalidate(); mMap.invalidate(); }, throwable -> { + handler.removeCallbacks(r); // TODO(saemy): Error handling. Log.e(TAG, throwable.getMessage()); - })); + }, () -> handler.removeCallbacks(r))); } } @@ -468,21 +473,24 @@ private void loadOfflineUsers() { mLoadOfflineUserDisposable = Observable.merge(mFavoriteRepository.getFavorites()) .filter(Resource::hasData) .map(userResource -> userResource.data) + // Users pop up twice as one is the error since we might not be able to load it + // from the network. + .filter(user -> mOfflineUserIds.add(user.id)) .observeOn(AndroidSchedulers.mainThread()) .subscribe(user -> { - // Users pop up twice as one is the error since we might not be able to load it - // from the network. - boolean added = mOfflineUserIds.add(user.id); - if (!added) { - return; - } - addUserToCluster(user); mMarkerClusterer.invalidate(); }); getCreateDestroyDisposable().add(mLoadOfflineUserDisposable); } + private void loadCachedUsers() { + for (UserSearchByLocationResponse.User user : mUserRegionalCache.getAllCached()) { + addUserToCluster(user.toSimpleUser()); + } + mMarkerClusterer.invalidate(); + } + private void addUserToCluster(SimpleUser user) { // Only add to the cluster if it wasn't before or when its location changed. final Marker existingMarker = mClusteredUsers.get(user.id); diff --git a/app/src/main/java/fi/bitrite/android/ws/util/LoadedArea.java b/app/src/main/java/fi/bitrite/android/ws/util/LoadedArea.java new file mode 100644 index 00000000..fa2581cf --- /dev/null +++ b/app/src/main/java/fi/bitrite/android/ws/util/LoadedArea.java @@ -0,0 +1,333 @@ +package fi.bitrite.android.ws.util; + +import org.osmdroid.util.BoundingBox; + +import java.util.Comparator; +import java.util.LinkedList; +import java.util.List; +import java.util.SortedSet; +import java.util.TreeSet; + +/** + * Allows for queries to substract the loaded areas from a given query area that should be loaded. + * This is done by running over a list of loaded areas, comparing each with the query area, dividing + * the latter upon an overlap. + * + * If this ever became too slow we could be moving to some more sophisticated data structure such as + * a segment tree. + */ +public class LoadedArea { + private final static double ROUNDING_PRECISION = 0.0025; // ~300m + + // Keeps the loaded areas sorted by their left x-value. This allows one to only look at the + // ones in this set that have a smaller left x-value than the right x-value of the search box. + private final SortedSet mLoadedAreas = new TreeSet<>(mBoundingBoxComparator); + + public void addLoadedArea(BoundingBox rect) { + // The loaded rect is expected to be previously pushed through subtractLoadedAreas. So it is + // safe to round it up. + roundUpRect(rect); + synchronized (mLoadedAreas) { + mLoadedAreas.add(rect); + } + } + + private void addRect(List list, BoundingBox rect) { + if (rect.getDiagonalLengthInMeters() > 0.0d) { + list.add(rect); + } + } + public List subtractLoadedAreas(BoundingBox query) { + // Make the query rect bigger. + roundUpRect(query); + + LinkedList ret = new LinkedList<>(); + ret.add(query); + synchronized (mLoadedAreas) { + for (BoundingBox loadedRect : mLoadedAreas) { + final double ln = loadedRect.getLatNorth(); + final double le = loadedRect.getLonEast(); + final double ls = loadedRect.getLatSouth(); + final double lw = loadedRect.getLonWest(); + + if (ret.isEmpty()) { + // Completely overlapped. + break; + } + + if (lw >= query.getLonEast()) { // To the right + // None of the following rects will intersect ours. + break; + } + if (le <= query.getLonWest() // To the left + || ls >= query.getLatNorth() // To the top + || ln <= query.getLatSouth()) { // To the bottom + continue; + } + + // Search for intersections. + LinkedList nextRet = new LinkedList<>(); + for (BoundingBox querySubRect : ret) { + final double qn = querySubRect.getLatNorth(); + final double qe = querySubRect.getLonEast(); + final double qs = querySubRect.getLatSouth(); + final double qw = querySubRect.getLonWest(); + + final Overlap xOverlap = getOverlap(lw, le, qw, qe); + final Overlap yOverlap = getOverlap(ls, ln, qs, qn); + if (xOverlap == Overlap.NONE || yOverlap == Overlap.NONE) { + nextRet.add(querySubRect); + continue; + } + + switch (xOverlap) { + case OUTSIDE: + switch (yOverlap) { + case OUTSIDE: + // l -----l + // | q--q | + // | | | | + // | q--q | + // l------l + /* We just remove the query and do nothing else */ + break; + + case INSIDE: + // q---q + // | A | + // l-+---+-l + // | | | | + // l-+---+-l + // | C | + // q---q + addRect(nextRet, new BoundingBox(qn, qe, ln, qw)); // A + addRect(nextRet, new BoundingBox(ls, qe, qs, qw)); // C + break; + + case LEFT_OR_BOTTOM: // BOTTOM + // q---q + // | A | + // l-+---+-l + // | q---q | + // l-------l + addRect(nextRet, new BoundingBox(qn, qe, ln, qw)); // A + break; + + case RIGHT_OR_TOP: // TOP + // l-------l + // | q---q | + // l-+---+-l + // | C | + // q---q + addRect(nextRet, new BoundingBox(ls, qe, qs, qw)); // C + break; + } + break; + + case INSIDE: + switch (yOverlap) { + case OUTSIDE: + // l--l + // q---+--+---q + // | D | | B | + // q---+--+---q + // l--l + addRect(nextRet, new BoundingBox(qn, qe, qs, le)); // B + addRect(nextRet, new BoundingBox(qn, lw, qs, qw)); // D + break; + + case INSIDE: + // q ----------q + // | : A : | + // | l---l | + // | D | | B | + // | l---l | + // | : C : | + // q-----------q + addRect(nextRet, new BoundingBox(qn, le, ln, lw)); // A + addRect(nextRet, new BoundingBox(qn, qe, qs, le)); // B + addRect(nextRet, new BoundingBox(ls, le, qs, lw)); // C + addRect(nextRet, new BoundingBox(qn, lw, qs, qw)); // D + break; + + case LEFT_OR_BOTTOM: // BOTTOM + // q ----------q + // | : A : | + // | D l---l B | + // q---+---+---q + // l---l + addRect(nextRet, new BoundingBox(qn, le, ln, lw)); // A + addRect(nextRet, new BoundingBox(qn, qe, qs, le)); // B + addRect(nextRet, new BoundingBox(qn, lw, qs, qw)); // D + break; + + case RIGHT_OR_TOP: // TOP + // l---l + // q---+---+---q + // | D l---l B | + // | : C : | + // q ----------q + addRect(nextRet, new BoundingBox(qn, qe, qs, le)); // B + addRect(nextRet, new BoundingBox(ls, le, qs, lw)); // C + addRect(nextRet, new BoundingBox(qn, lw, qs, qw)); // D + break; + } + break; + + case LEFT_OR_BOTTOM: // LEFT + switch (yOverlap) { + case OUTSIDE: + // l---l + // | q-+---q + // | | | B | + // | q-+---q + // l---l + addRect(nextRet, new BoundingBox(qn, qe, qs, le)); // B + break; + + case INSIDE: + // q-------q + // | A : | + // l-+---l | + // | | | B | + // l-+---l | + // | C : | + // q-------q + addRect(nextRet, new BoundingBox(qn, le, ln, qw)); // A + addRect(nextRet, new BoundingBox(qn, qe, qs, le)); // B + addRect(nextRet, new BoundingBox(ls, le, qs, qw)); // C + break; + + case LEFT_OR_BOTTOM: // BOTTOM + // q-------q + // | A : | + // l-+---l | + // | | | B | + // | q---+---q + // l-----l + addRect(nextRet, new BoundingBox(qn, le, ln, qw)); // A + addRect(nextRet, new BoundingBox(qn, qe, qs, le)); // B + break; + + case RIGHT_OR_TOP: // TOP + // l-----l + // | q---+---q + // | | | | + // l-+---l B | + // | C : | + // q-------q + addRect(nextRet, new BoundingBox(qn, qe, qs, le)); // B + addRect(nextRet, new BoundingBox(ls, le, qs, qw)); // C + break; + } + break; + + case RIGHT_OR_TOP: // RIGHT + switch (yOverlap) { + case OUTSIDE: + // l---l + // q---+-q | + // | D | | | + // q---+-q | + // l---l + addRect(nextRet, new BoundingBox(qn, lw, qs, qw)); // D + break; + + case INSIDE: + // q-------q + // | : A | + // | l---+-l + // | D | | | + // | l---+-l + // | : C | + // q-------q + addRect(nextRet, new BoundingBox(qn, qe, ln, lw)); // A + addRect(nextRet, new BoundingBox(ls, qe, qs, lw)); // C + addRect(nextRet, new BoundingBox(qn, lw, qs, qw)); // D + break; + + case LEFT_OR_BOTTOM: // BOTTOM + // q-------q + // | : A | + // | D l---+-l + // | | | | + // q---+---q | + // l-----l + addRect(nextRet, new BoundingBox(qn, qe, ln, lw)); // A + addRect(nextRet, new BoundingBox(qn, lw, qs, qw)); // D + break; + + case RIGHT_OR_TOP: // TOP + // l-----l + // q---+---q | + // | | | | + // | D l---+-l + // | : C | + // q-------q + addRect(nextRet, new BoundingBox(ln, qe, qs, lw)); // C + addRect(nextRet, new BoundingBox(qn, lw, qs, qw)); // D + break; + } + break; + } + } + ret = nextRet; + } + } + return ret; + } + + /** + * Rounds up the coordinates of the given rect to avoid very small (non)overlaps. + */ + private static void roundUpRect(BoundingBox rect) { + rect.set( + roundUp(rect.getLatNorth()), + roundUp(rect.getLonEast()), + roundUp(rect.getLatSouth()), + roundUp(rect.getLonWest())); + } + private static double roundUp(double i) { + int sign = i < 0 ? -1 : 1; + return Math.ceil(Math.abs(i) / ROUNDING_PRECISION) * ROUNDING_PRECISION * sign; + } + + // As seen by 'a'. The intersection of 'a' is favored to be NONE or OUTSIDE in case values are + // equal. INSIDE is never favored. + private static Overlap getOverlap(double aLow, double aHigh, double bLow, double bHigh) { + if (aLow <= bLow) { + if (aHigh <= bLow) { + return Overlap.NONE; + } else if (aHigh >= bHigh) { + return Overlap.OUTSIDE; + } else { + return Overlap.LEFT_OR_BOTTOM; + } + } else { + if (aLow >= bHigh) { + return Overlap.NONE; + } else if (aHigh < bHigh) { + return Overlap.INSIDE; + } else { + return Overlap.RIGHT_OR_TOP; + } + } + } + + // Sorts the rects by their left-top corner from left-top to right-bottom. + private final static Comparator mBoundingBoxComparator = (left, right) -> { + double diff = left.getLonWest() - right.getLonWest(); + if (diff == 0.0d) { + diff = left.getLatNorth() - right.getLatNorth(); + } + return diff == 0 ? 0 : (diff < 0 ? -1 : 1); // Avoid rounding errors. + }; + + private enum Overlap { // Overlap of 'a' with 'b'. What is 'a'? + NONE, // a--a b==b or b==b a--a + OUTSIDE, // a-- b==b --a + INSIDE, // b== a--a ==b + LEFT_OR_BOTTOM, // a-- b== --a ==b + RIGHT_OR_TOP // b== a-- ==b --a + } +} diff --git a/app/src/main/java/fi/bitrite/android/ws/util/UserRegionalCache.java b/app/src/main/java/fi/bitrite/android/ws/util/UserRegionalCache.java new file mode 100644 index 00000000..cf82afcc --- /dev/null +++ b/app/src/main/java/fi/bitrite/android/ws/util/UserRegionalCache.java @@ -0,0 +1,81 @@ +package fi.bitrite.android.ws.util; + +import org.osmdroid.util.BoundingBox; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; + +import javax.inject.Inject; + +import fi.bitrite.android.ws.api.response.UserSearchByLocationResponse; +import fi.bitrite.android.ws.di.AppScope; +import fi.bitrite.android.ws.di.account.AccountScope; +import fi.bitrite.android.ws.repository.UserRepository; +import io.reactivex.Observable; + +/** + * Loads and caches users by region. If a region is queried twice, the cached users are returned. + * This is also true for overlapping areas where only the ones which are not yet covered are + * fetched. + */ +@AccountScope +public class UserRegionalCache { + @Inject AppScopeUserRegionalCache mAppScopeUserRegionalCache; + @Inject UserRepository mUserRepository; + + @Inject + UserRegionalCache() { + } + + public Collection getAllCached() { + return mAppScopeUserRegionalCache.mUserCache; + } + + public Observable> searchByLocation( + BoundingBox boundingBox) { + return mAppScopeUserRegionalCache.searchByLocation(boundingBox, mUserRepository); + } + + @AppScope + static class AppScopeUserRegionalCache { + private final LoadedArea mLoadedArea = new LoadedArea(); + private final Collection mUserCache = new HashSet<>(); + + @Inject + AppScopeUserRegionalCache() { + } + + Observable> searchByLocation( + BoundingBox boundingBox, UserRepository userRepository) { + List unloadedAreas = mLoadedArea.subtractLoadedAreas(boundingBox); + List>> observables = + new ArrayList<>(unloadedAreas.size()); + List successfullyLoadedAreas = new ArrayList<>(unloadedAreas.size()); + for (BoundingBox unloadedArea : unloadedAreas) { + observables.add(userRepository.searchByLocation(unloadedArea) + .map(users -> { + successfullyLoadedAreas.add(unloadedArea); + mUserCache.addAll(users); + return users; + })); + } + + return Observable.mergeDelayError(observables) + .doOnComplete(() -> { + // We add one big (possibly overlapping) rather than several small unloaded + // areas. + if (!unloadedAreas.isEmpty()) { + mLoadedArea.addLoadedArea(boundingBox); + } + }) + .doOnError(e -> { + // Some areas failed to load -> only put the successful ones. + for (BoundingBox loadedArea : successfullyLoadedAreas) { + mLoadedArea.addLoadedArea(loadedArea); + } + }); + } + } +} diff --git a/app/src/test/java/fi/bitrite/android/ws/util/LoadedAreaTest.java b/app/src/test/java/fi/bitrite/android/ws/util/LoadedAreaTest.java new file mode 100644 index 00000000..09b91cc4 --- /dev/null +++ b/app/src/test/java/fi/bitrite/android/ws/util/LoadedAreaTest.java @@ -0,0 +1,434 @@ +package fi.bitrite.android.ws.util; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.osmdroid.util.BoundingBox; +import org.robolectric.RobolectricTestRunner; + +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; + +import static org.assertj.core.api.Java6Assertions.assertThat; + +@RunWith(RobolectricTestRunner.class) +public class LoadedAreaTest { + + private static class ComparableBoundingBox { + private final BoundingBox mBoundingBox; + + ComparableBoundingBox(BoundingBox boundingBox) { + mBoundingBox = boundingBox; + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + BoundingBox bb = null; + if (other instanceof BoundingBox) { + bb = (BoundingBox) other; + } else if (other instanceof ComparableBoundingBox) { + bb = ((ComparableBoundingBox) other).mBoundingBox; + } + return bb != null + && mBoundingBox.getLatNorth() == bb.getLatNorth() + && mBoundingBox.getLonEast() == bb.getLonEast() + && mBoundingBox.getLatSouth() == bb.getLatSouth() + && mBoundingBox.getLonWest() == bb.getLonWest(); + } + } + + private static void subtractAndCheckEqual(List loadedAreas, BoundingBox query, + List expectedResult) { + LoadedArea la = new LoadedArea(); + for (BoundingBox rect : loadedAreas) { + la.addLoadedArea(rect); + } + subtractAndCheckEqual(la, query, expectedResult); + } + private static void subtractAndCheckEqual(LoadedArea la, BoundingBox query, + List expectedResult) { + List result = la.subtractLoadedAreas(query); + + assertThat(result.size()).isEqualTo(expectedResult.size()); + + List expectedResultComparable = new LinkedList<>(); + for (BoundingBox expected : expectedResult) { + expectedResultComparable.add(new ComparableBoundingBox(expected)); + } + for (BoundingBox r : result) { + expectedResultComparable.remove(new ComparableBoundingBox(r)); + } + if (!expectedResultComparable.isEmpty()) { + System.out.println("Query: " + getRectStr(query)); + System.out.println("Result rects:"); + for (BoundingBox r : result) { + System.out.println(" - " + getRectStr(r)); + } + System.out.println("Remaining expected rects:"); + for (ComparableBoundingBox cb : expectedResultComparable) { + System.out.println(" - " + getRectStr(cb.mBoundingBox)); + } + System.out.println(); + + assertThat(false).isTrue(); + } + } + private static List loaded(BoundingBox... loadedAreas) { + return Arrays.asList(loadedAreas); + } + private static List expected(BoundingBox... loadedAreas) { + return Arrays.asList(loadedAreas); + } + private static String getRectStr(BoundingBox r) { + return String.format("%.5f,%.5f, %.5f,%.5f", + r.getLonWest(), r.getLatSouth(), r.getLonEast(), r.getLatNorth()); + } + + // Translation from (left,bottom,right,top) to (north,east,south,west). + private static BoundingBox r(double left, double bottom, double right, double top) { + return new BoundingBox(top, right, bottom, left); + } + private static BoundingBox query(double left, double bottom, double right, double top) { + return r(left, bottom, right, top); + } + + // Overlapped is always in terms of the loaded area. + + @Test + public void fullyOverlappedTest() { + // l -----l + // | q--q | + // | | | | + // | q--q | + // l------l + subtractAndCheckEqual( + loaded( + r(0,1, 10,11) + ), + r(0,1, 10,11), + expected()); // Just touching. + subtractAndCheckEqual( + loaded( + r(0,0, 5,5), + r(0,5, 5,10), + r(5,0,10,5), + r(5,5,10,10) + ), + query(0,0, 10,10), + expected()); // Just touching by four non-overlapping areas. + subtractAndCheckEqual( + loaded( + r(0,0, 5,5), + r(0,5, 5,10), + r(5,0,10,5), + r(5,5,10,10) + ), + query(1,1, 9,9), + expected()); + } + + @Test + public void nonOverlappingTest() { + // l----l q--q + // | | | | + // l----l q--q + subtractAndCheckEqual( + loaded( + r(0,0, 10,10), // left of + r(0,10, 20,20), // top of + r(20,0, 30,15), // right of + r(-10,-10, 20,0) // bottom of + ), + query(10,0, 20,10), + expected( + r(10,0, 20,10) + ) + ); + } + + @Test + public void leftSideOverlappingTest() { + // l---l + // | q-+---q + // | | | B | + // | q-+---q + // l---l + subtractAndCheckEqual( + loaded( + r(0,1, 10,11) + ), + query(2,2, 12,8), + expected( + r(10,2, 12,8) // B + ) + ); + } + + @Test + public void topSideOverlappingTest() { + // l-------l + // | q---q | + // l-+---+-l + // | C | + // q---q + subtractAndCheckEqual( + loaded( + r(0,10, 11,20) + ), + query(2,4, 7,14), + expected( + r(2,4, 7,10) // C + ) + ); + } + + @Test + public void rightSideOverlappingTest() { + // l---l + // q---+-q | + // | D | | | + // q---+-q | + // l---l + subtractAndCheckEqual( + loaded( + r(10,0, 20,11) + ), + query(1,5, 12,8), + expected( + r(1,5, 10,8) // D + ) + ); + } + @Test + public void bottomSideOverlappingTest() { + // q---q + // | A | + // l-+---+-l + // | q---q | + // l-------l + subtractAndCheckEqual( + loaded( + r(-5,-10, 5,2) + ), + query(-2,-3, 4,6), + expected( + r(-2,2, 4,6) // A + ) + ); + } + + @Test + public void centerOverlappingTest() { + // q ----------q + // | : A : | + // | l---l | + // | D | | B | + // | l---l | + // | : C : | + // q-----------q + subtractAndCheckEqual( + loaded( + r(10,11, 15,16) + ), + query(0,1, 20,21), + expected( + r(10,16, 15,21), // A + r(15,1, 20,21), // B + r(10,1, 15,11), // C + r(0,1, 10,21) // D + )); + } + + public void topLeftCornerOverlappingTest() { + // l-----l + // | q---+---q + // | | | | + // l-+---l B | + // | C : | + // q-------q + subtractAndCheckEqual( + loaded( + r(0,6, 10,16) + ), + query(5,1, 15,11), + expected( + r(10,1, 15,11), // B + r(5,1, 10,6) // C + )); + } + + public void topRightCornerOverlappingTest() { + // l-----l + // q---+---q | + // | | | | + // | D l---+-l + // | : C | + // q-------q + subtractAndCheckEqual( + loaded( + r(5,6, 15,16) + ), + query(0,1, 10,11), + expected( + r(5,1, 10,6), // C + r(0,1, 5,11) // D + )); + } + + public void bottomRightCornerOverlappingTest() { + // q-------q + // | : A | + // | D l---+-l + // | | | | + // q---+---q | + // l-----l + subtractAndCheckEqual( + loaded( + r(5,1, 15,11) + ), + query(0,6, 10,16), + expected( + r(5,11, 10,16), // A + r(0,6, 5,16) // D + )); + } + + public void bottomLeftCornerOverlappingTest() { + // q-------q + // | A : | + // l-+---l | + // | | | B | + // | q---+---q + // l-----l + subtractAndCheckEqual( + loaded( + r(0,1, 10,11) + ), + query(5,6, 15,16), + expected( + r(5,11, 10,16), // A + r(10,6, 15,16) // B + )); + } + + @Test + public void horizontalBarFullyOverlappingTest() { + // q---q + // | A | + // l-+---+-l + // | | | | + // l-+---+-l + // | C | + // q---q + subtractAndCheckEqual( + loaded( + r(0,1, 10,11) + ), + query(5,-10, 7,20), + expected( + r(5,11, 7,20), // A + r(5,-10, 7,1) // C + )); + } + + @Test + public void horizontalBarPartiallyOverlappingLeftTest() { + // q-------q + // | A : | + // l-+---l | + // | | | B | + // l-+---l | + // | C : | + // q-------q + subtractAndCheckEqual( + loaded( + r(0,5, 10,11) + ), + query(6,1, 12,20), + expected( + r(6,11, 10,20), // A + r(10,1, 12,20), // B + r(6,1, 10,5) // C + )); + } + + @Test + public void horizontalBarPartiallyOverlappingRightTest() { + // q-------q + // | : A | + // | l---+-l + // | D | | | + // | l---+-l + // | : C | + // q-------q + subtractAndCheckEqual( + loaded( + r(3,5, 15,10) + ), + query(1,2, 12,20), + expected( + r(3,10, 12,20), // A + r(3,2, 12,5), // C + r(1,2, 3,20) // D + )); + } + + @Test + public void verticalBarFullyOverlappingTest() { + // l--l + // q---+--+---q + // | D | | B | + // q---+--+---q + // l--l + subtractAndCheckEqual( + loaded( + r(0,1, 10,11) + ), + query(-10,3, 13,6), + expected( + r(10,3, 13,6), // B + r(-10,3, 0,6) // D + )); + } + + @Test + public void verticalBarPartiallyOverlappingTopTest() { + // l---l + // q---+---+---q + // | D l---l B | + // | : C : | + // q ----------q + subtractAndCheckEqual( + loaded( + r(4,5, 8,16) + ), + query(0,1, 20,7), + expected( + r(8,1, 20,7), // B + r(4,1, 8,5), // C + r(0,1, 4,7) // D + )); + } + + @Test + public void verticalBarPartiallyOverlappingBottomTest() { + // q ----------q + // | : A : | + // | D l---l B | + // q---+---+---q + // l---l + subtractAndCheckEqual( + loaded( + r(4,1, 8,7) + ), + query(0,5, 20,16), + expected( + r(4,7, 8,16), // A + r(8,5, 20,16), // B + r(0,5, 4,16) // D + )); + } +}