From 159062bedda0dcd4caad7c4d7f84f0ae46b616b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A4my=20Zehnder?= Date: Fri, 17 Aug 2018 17:45:58 +0200 Subject: [PATCH] Add user cache Users that were loaded from the network are now kept in a cache and only discarded when the app is closed. This means, that if a user navigates away from the map and then comes back to it no more loading needs to be done for the previously loaded areas. Also, the loading is area aware. That means that for overlapping areas requests are now only made for the non-loaded parts. Fixes: #101 --- .../fi/bitrite/android/ws/ui/MapFragment.java | 36 +- .../bitrite/android/ws/util/LoadedArea.java | 333 ++++++++++++++ .../android/ws/util/UserRegionalCache.java | 81 ++++ .../android/ws/util/LoadedAreaTest.java | 434 ++++++++++++++++++ 4 files changed, 870 insertions(+), 14 deletions(-) create mode 100644 app/src/main/java/fi/bitrite/android/ws/util/LoadedArea.java create mode 100644 app/src/main/java/fi/bitrite/android/ws/util/UserRegionalCache.java create mode 100644 app/src/test/java/fi/bitrite/android/ws/util/LoadedAreaTest.java 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 + )); + } +}