/** * Copyright (C) 2009 Michael A. MacDonald */ package com.iiordanov.android.drawing; import android.graphics.Rect; import java.util.ArrayList; import com.iiordanov.util.ObjectPool; /** * A list of rectangular regions that together represent an area of interest. Provides * a set of operations that apply to the whole area, adding, changing and mutating the * rectangles in the list as required. * <p> * Invariants: None of the rectangles in the list overlap; no pair of rectangles in the list * together make a single rectangle (none share a complete side) * </p> * <p> * Instances of this class are not thread safe * </p> * @author Michael A. MacDonald * */ public class RectList { enum OverlapType { NONE, SAME, CONTAINS, CONTAINED_BY, COALESCIBLE, PARTIAL } // Limit for level of recursion to avoid stack overflow. static final int MAXLEVELS = 20; static final int LEFT = 1; static final int TOP_LEFT = 2; static final int TOP = 4; static final int TOP_RIGHT = 8; static final int RIGHT = 16; static final int BOTTOM_RIGHT = 32; static final int BOTTOM = 64; static final int BOTTOM_LEFT = 128; /** * The part left over when one rectangle is subtracted from another * @author Michael A. MacDonald * */ static class NonOverlappingPortion { Rect leftPortion; Rect topLeftPortion; Rect topPortion; Rect topRightPortion; Rect rightPortion; Rect bottomRightPortion; Rect bottomPortion; Rect bottomLeftPortion; int r1Owns; int r2Owns; int common; int adjacent; boolean horizontalOverlap; boolean verticalOverlap; Rect coalesced; NonOverlappingPortion() { leftPortion = new Rect(); topLeftPortion = new Rect(); topPortion = new Rect(); topRightPortion = new Rect(); rightPortion = new Rect(); bottomRightPortion = new Rect(); bottomPortion = new Rect(); bottomLeftPortion = new Rect(); coalesced = new Rect(); } void setCornerOwnership(int side1, int side2, int corner) { int combined = (side1 | side2); if ((r1Owns & combined) == combined) r1Owns |= corner; else if ((r2Owns & combined) == combined) r2Owns |= corner; } void setCornerOwnership() { setCornerOwnership(LEFT,TOP,TOP_LEFT); setCornerOwnership(TOP,RIGHT,TOP_RIGHT); setCornerOwnership(BOTTOM,RIGHT,BOTTOM_RIGHT); setCornerOwnership(BOTTOM,LEFT,BOTTOM_LEFT); } /** * Populates with the borders remaining when r2 is subtracted from r1 * @param r1 * @param r2 * @return */ OverlapType overlap(Rect r1, Rect r2) { r1Owns = 0; r2Owns = 0; common = 0; adjacent = 0; OverlapType result = OverlapType.NONE; horizontalOverlap = false; verticalOverlap = false; if (r1.left < r2.left) { leftPortion.left = topLeftPortion.left = bottomLeftPortion.left = r1.left; if (r2.left < r1.right) { leftPortion.right = topLeftPortion.right = bottomLeftPortion.right = topPortion.left = bottomPortion.left = r2.left; horizontalOverlap = true; } else { leftPortion.right = topLeftPortion.right = bottomLeftPortion.right = topPortion.left = bottomPortion.left = r1.right; if (r2.left == r1.right) adjacent |= LEFT; } r1Owns |= LEFT; } else { leftPortion.left = topLeftPortion.left = bottomLeftPortion.left = r2.left; if (r1.left < r2.right) { leftPortion.right = topLeftPortion.right = bottomLeftPortion.right = topPortion.left = bottomPortion.left = r1.left; horizontalOverlap = true; } else { leftPortion.right = topLeftPortion.right = bottomLeftPortion.right = topPortion.left = bottomPortion.left = r2.right; if ( r1.left == r2.right) adjacent |= RIGHT; } if (r2.left < r1.left) r2Owns |= LEFT; else common |= LEFT; } if (r1.top < r2.top) { topPortion.top = topLeftPortion.top = topRightPortion.top = r1.top; if (r2.top < r1.bottom) { topPortion.bottom = topLeftPortion.bottom = topRightPortion.bottom = leftPortion.top = rightPortion.top = r2.top; verticalOverlap = true; } else { topPortion.bottom = topLeftPortion.bottom = topRightPortion.bottom = leftPortion.top = rightPortion.top = r1.bottom; if (r2.top == r1.bottom) adjacent |= TOP; } r1Owns |= TOP; } else { topPortion.top = topLeftPortion.top = topRightPortion.top = r2.top; if (r1.top < r2.bottom) { topPortion.bottom = topLeftPortion.bottom = topRightPortion.bottom = leftPortion.top = rightPortion.top = r1.top; verticalOverlap = true; } else { topPortion.bottom = topLeftPortion.bottom = topRightPortion.bottom = leftPortion.top = rightPortion.top = r2.bottom; if (r1.top == r2.bottom) adjacent |= BOTTOM; } if (r2.top < r1.top) r2Owns |= TOP; else common |= TOP; } if (r1.right > r2.right) { rightPortion.right = topRightPortion.right = bottomRightPortion.right = r1.right; if (r2.right > r1.left) { rightPortion.left = topRightPortion.left = bottomRightPortion.left = topPortion.right = bottomPortion.right = r2.right; horizontalOverlap = true; } else { rightPortion.left = topRightPortion.left = bottomRightPortion.left = topPortion.right = bottomPortion.right = r1.left; if (r2.right == r1.left) adjacent |= RIGHT; } r1Owns |= RIGHT; } else { rightPortion.right = topRightPortion.right = bottomRightPortion.right = r2.right; if (r1.right > r2.left) { rightPortion.left = topRightPortion.left = bottomRightPortion.left = topPortion.right = bottomPortion.right = r1.right; horizontalOverlap = true; } else { rightPortion.left = topRightPortion.left = bottomRightPortion.left = topPortion.right = bottomPortion.right = r2.left; if (r1.right==r2.left) adjacent |= LEFT; } if (r2.right > r1.right) r2Owns |= RIGHT; else common |= RIGHT; } if (r1.bottom > r2.bottom) { bottomPortion.bottom = bottomLeftPortion.bottom = bottomRightPortion.bottom = r1.bottom; if (r2.bottom > r1.top) { bottomPortion.top = bottomLeftPortion.top = bottomRightPortion.top = leftPortion.bottom = rightPortion.bottom = r2.bottom; verticalOverlap = true; } else { bottomPortion.top = bottomLeftPortion.top = bottomRightPortion.top = leftPortion.bottom = rightPortion.bottom = r1.top; if (r2.bottom==r1.top) adjacent |= BOTTOM; } r1Owns |= BOTTOM; } else { bottomPortion.bottom = bottomLeftPortion.bottom = bottomRightPortion.bottom = r2.bottom; if (r1.bottom > r2.top) { bottomPortion.top = bottomLeftPortion.top = bottomRightPortion.top = leftPortion.bottom = rightPortion.bottom = r1.bottom; verticalOverlap = true; } else { bottomPortion.top = bottomLeftPortion.top = bottomRightPortion.top = leftPortion.bottom = rightPortion.bottom = r2.top; if (r1.bottom==r2.top) adjacent |= TOP; } if (r2.bottom > r1.bottom) r2Owns |= BOTTOM; else common |= BOTTOM; } if ( common == (LEFT|RIGHT|TOP|BOTTOM)) { result = OverlapType.SAME; } else if ((common & (LEFT|RIGHT)) == (LEFT | RIGHT) && (verticalOverlap || (adjacent & (TOP | BOTTOM)) != 0)) { result = OverlapType.COALESCIBLE; coalesced.left = r1.left; coalesced.right = r1.right; coalesced.top = topPortion.top; coalesced.bottom = bottomPortion.bottom; } else if ((common & (TOP | BOTTOM)) == (TOP | BOTTOM) && (horizontalOverlap || (adjacent & (LEFT | RIGHT)) != 0)) { result = OverlapType.COALESCIBLE; coalesced.left = leftPortion.left; coalesced.right = rightPortion.right; coalesced.top = r1.top; coalesced.bottom = r1.bottom; } else if (verticalOverlap && horizontalOverlap) { if (r2Owns == 0) { result = OverlapType.CONTAINED_BY; } else if (r1Owns == 0) { result = OverlapType.CONTAINS; } else { // Partial overlap, non coalescible case result = OverlapType.PARTIAL; setCornerOwnership(); } } return result; } } /** * Up to 8 Rect objects * @author Michael A. MacDonald * */ static class NonOverlappingRects { ObjectPool.Entry<Rect>[] rectEntries; int count; static final int MAX_RECTS = 8; NonOverlappingRects() { rectEntries = new ObjectPool.Entry[MAX_RECTS]; } private void addOwnedRect(int owner, int direction, ObjectPool<Rect> pool, Rect r) { if ((owner & direction)==direction) { ObjectPool.Entry<Rect> entry = pool.reserve(); rectEntries[count++] = entry; entry.get().set(r); } } void Populate(NonOverlappingPortion p, ObjectPool<Rect> pool, int owner) { count = 0; for (int i=0; i<MAX_RECTS; i++) rectEntries[i] = null; addOwnedRect(owner,BOTTOM_LEFT,pool,p.bottomLeftPortion); addOwnedRect(owner,BOTTOM,pool,p.bottomPortion); addOwnedRect(owner,BOTTOM_RIGHT,pool,p.bottomRightPortion); addOwnedRect(owner,BOTTOM,pool,p.bottomPortion); addOwnedRect(owner,TOP_RIGHT,pool,p.topRightPortion); addOwnedRect(owner,TOP,pool,p.topPortion); addOwnedRect(owner,TOP_LEFT,pool,p.topLeftPortion); addOwnedRect(owner,LEFT,pool,p.leftPortion); } } private ArrayList<ObjectPool.Entry<Rect>> list; private ObjectPool<Rect> pool; private ObjectPool<NonOverlappingRects> nonOverlappingRectsPool = new ObjectPool<NonOverlappingRects>() { /* (non-Javadoc) * @see com.antlersoft.util.ObjectPool#itemForPool() */ @Override protected NonOverlappingRects itemForPool() { return new NonOverlappingRects(); } }; private ObjectPool<ArrayList<ObjectPool.Entry<Rect>>> listRectsPool = new ObjectPool<ArrayList<ObjectPool.Entry<Rect>>>() { /* (non-Javadoc) * @see com.antlersoft.util.ObjectPool#itemForPool() */ @Override protected ArrayList<Entry<Rect>> itemForPool() { return new ArrayList<Entry<Rect>>(NonOverlappingRects.MAX_RECTS); } }; private NonOverlappingPortion nonOverlappingPortion; public RectList(ObjectPool<Rect> pool) { this.pool = pool; list = new ArrayList<ObjectPool.Entry<Rect>>(); nonOverlappingPortion = new NonOverlappingPortion(); } public int getSize() { return list.size(); } public Rect get(int i) { return list.get(i).get(); } /** * Remove all rectangles from the list and release them from the pool */ public void clear() { for (int i=list.size()-1; i>=0; i--) { ObjectPool.Entry<Rect> r = list.get(i); pool.release(r); } list.clear(); } private void recursiveAdd(ObjectPool.Entry<Rect> toAdd, int level) { // Limit the level of recursion to avoid stack overflow. if (level >= MAXLEVELS) return; if (level>=list.size()) { list.add(toAdd); return; } Rect addRect = toAdd.get(); ObjectPool.Entry<Rect> thisEntry = list.get(level); Rect thisRect = thisEntry.get(); switch (nonOverlappingPortion.overlap(thisRect, addRect)) { case NONE : recursiveAdd(toAdd,level + 1); break; case SAME : case CONTAINS : pool.release(toAdd); break; case CONTAINED_BY : pool.release(thisEntry); list.remove(level); recursiveAdd(toAdd,level); break; case COALESCIBLE : pool.release(thisEntry); list.remove(level); addRect.set(nonOverlappingPortion.coalesced); recursiveAdd(toAdd,0); break; case PARTIAL : pool.release(toAdd); ObjectPool.Entry<NonOverlappingRects> rectsEntry = nonOverlappingRectsPool.reserve(); NonOverlappingRects rects = rectsEntry.get(); rects.Populate(nonOverlappingPortion,pool,nonOverlappingPortion.r2Owns); for (int i=0; i<rects.count; i++) { recursiveAdd(rects.rectEntries[i],0); } nonOverlappingRectsPool.release(rectsEntry); break; } } /** * Add a rectangle to the region of interest * @param toAdd */ public void add(Rect toAdd) { // Create a copy of the rect to work with ObjectPool.Entry<Rect> entry = pool.reserve(); Rect r = entry.get(); r.set(toAdd); try { recursiveAdd(entry,0); } catch (java.lang.StackOverflowError e) { // Don't add the rectangle if the stack overflowed (if the user is scrolling around too much). } } /** * Change the rectangle of interest to include only those portions * that fall inside bounds. * @param bounds */ public void intersect(Rect bounds) { int size = list.size(); ObjectPool.Entry<ArrayList<ObjectPool.Entry<Rect>>> listEntry = listRectsPool.reserve(); ArrayList<ObjectPool.Entry<Rect>> newList = listEntry.get(); newList.clear(); for (int i=0; i<size; i++) { ObjectPool.Entry<Rect> entry = list.get(i); Rect rect = entry.get(); if (rect.intersect(bounds)) { newList.add(entry); } else pool.release(entry); } list.clear(); size = newList.size(); for (int i=0; i<size; i++) { try { recursiveAdd(newList.get(i),0); } catch (java.lang.StackOverflowError e) { // Don't add the rectangle if the stack overflowed (if the user is scrolling around too much). } } listRectsPool.release(listEntry); } /** * Determines if a rectangle intersects any part of the area of interest * @param r * @return True if r intersects any rectangle in the list */ public boolean testIntersect(Rect r) { int l = list.size(); for (int i = 0; i<l; i++) { if (list.get(i).get().intersects(r.left, r.top, r.right, r.bottom)) return true; } return false; } /** * Remove the rectangle from the area of interest. * @param toSubtract */ public void subtract(Rect toSubtract) { int size = list.size(); ObjectPool.Entry<ArrayList<ObjectPool.Entry<Rect>>> listEntry = listRectsPool.reserve(); ArrayList<ObjectPool.Entry<Rect>> newList = listEntry.get(); newList.clear(); for (int i=0; i<size; i++) { ObjectPool.Entry<Rect> entry = list.get(i); Rect rect = entry.get(); switch (nonOverlappingPortion.overlap(rect, toSubtract)) { case SAME: pool.release(entry); newList.clear(); list.remove(i); return; case CONTAINED_BY: pool.release(entry); list.remove(i); i--; size--; break; case NONE: break; case COALESCIBLE: if (!nonOverlappingPortion.verticalOverlap || ! nonOverlappingPortion.horizontalOverlap) break; case CONTAINS : nonOverlappingPortion.setCornerOwnership(); case PARTIAL : { ObjectPool.Entry<NonOverlappingRects> rectsEntry = nonOverlappingRectsPool.reserve(); NonOverlappingRects rects = rectsEntry.get(); rects.Populate(nonOverlappingPortion, pool, nonOverlappingPortion.r1Owns); pool.release(entry); list.remove(i); i--; size--; for (int j=0; j<rects.count; j++) { newList.add(rects.rectEntries[j]); } nonOverlappingRectsPool.release(rectsEntry); } } } size = newList.size(); for (int i=0; i<size; i++) { try { recursiveAdd(newList.get(i),0); } catch (java.lang.StackOverflowError e) { // Don't add the rectangle if the stack overflowed (if the user is scrolling around too much). } } listRectsPool.release(listEntry); } /* (non-Javadoc) * @see java.lang.Object#toString() */ @Override public String toString() { StringBuilder sb=new StringBuilder(); sb.append("{\n"); for (int i=0; i<getSize(); i++) { sb.append(get(i).toString()); sb.append("\n"); } sb.append("}"); return sb.toString(); } }