├── .travis.yml ├── .gitignore ├── src └── main │ └── java │ └── skylinequeries │ ├── rtree │ ├── geometry │ │ ├── HasGeometry.java │ │ ├── Group.java │ │ ├── Geometries.java │ │ ├── Intersects.java │ │ ├── ListPair.java │ │ ├── Geometry.java │ │ ├── Point.java │ │ ├── Circle.java │ │ └── Rectangle.java │ ├── Pair.java │ ├── ImageSaver.java │ ├── RectangleDepth.java │ ├── Splitter.java │ ├── Node.java │ ├── NodePosition.java │ ├── SelectorMinimalAreaIncrease.java │ ├── SelectorMinimalOverlapArea.java │ ├── ObjectsHelper.java │ ├── Selector.java │ ├── SelectorRStar.java │ ├── NodeAndEntries.java │ ├── Context.java │ ├── OperatorBoundedPriorityQueue.java │ ├── Functions.java │ ├── Entry.java │ ├── ImmutableStack.java │ ├── Util.java │ ├── OnSubscribeSearch.java │ ├── Leaf.java │ ├── Comparators.java │ ├── Backpressure.java │ ├── SplitterRStar.java │ ├── SplitterQuadratic.java │ ├── Visualizer.java │ ├── NonLeaf.java │ └── RTree.java │ ├── tools │ ├── Constants.java │ ├── database │ │ ├── DBConnection.java │ │ └── Query.java │ ├── Drawer.java │ ├── Utilities.java │ └── StdDraw.java │ ├── algs │ ├── nn │ │ └── NN.java │ └── bbs │ │ ├── BBSHeapElement.java │ │ └── BBS.java │ ├── demo │ ├── NNWorker.java │ ├── BBSWorker.java │ └── TestRunner.java │ └── sql │ └── skyline.sql ├── LICENSE.md ├── pom.xml └── README.md /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | jdk: 3 | - oraclejdk8 4 | - oraclejdk7 5 | - openjdk7 6 | - openjdk6 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | .project 3 | .classpath 4 | .settings 5 | /bin/ 6 | /.idea/ 7 | /lib 8 | /test/ 9 | SkylineQueries.iml 10 | -------------------------------------------------------------------------------- /src/main/java/skylinequeries/rtree/geometry/HasGeometry.java: -------------------------------------------------------------------------------- 1 | package skylinequeries.rtree.geometry; 2 | 3 | public interface HasGeometry { 4 | 5 | Geometry geometry(); 6 | } 7 | -------------------------------------------------------------------------------- /src/main/java/skylinequeries/rtree/Pair.java: -------------------------------------------------------------------------------- 1 | package skylinequeries.rtree; 2 | 3 | public final class Pair { 4 | 5 | private final T value1; 6 | private final T value2; 7 | 8 | public Pair(T value1, T value2) { 9 | this.value1 = value1; 10 | this.value2 = value2; 11 | } 12 | 13 | public T value1() { 14 | return value1; 15 | } 16 | 17 | public T value2() { 18 | return value2; 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/skylinequeries/tools/Constants.java: -------------------------------------------------------------------------------- 1 | package skylinequeries.tools; 2 | 3 | import skylinequeries.tools.database.DBConnection; 4 | import skylinequeries.tools.database.Query; 5 | 6 | /** 7 | * @author Vinh Nguyen 8 | */ 9 | public interface Constants { 10 | String DB_URL = "jdbc:mysql://localhost:3306/skyline"; 11 | String DB_USERNAME = "root"; 12 | String DB_PASSWORD = "root"; 13 | Query DB_QUERY = new Query(DBConnection.initialize()); 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/skylinequeries/rtree/ImageSaver.java: -------------------------------------------------------------------------------- 1 | package skylinequeries.rtree; 2 | 3 | import javax.imageio.ImageIO; 4 | import java.awt.image.BufferedImage; 5 | import java.io.File; 6 | import java.io.IOException; 7 | 8 | final class ImageSaver { 9 | 10 | static void save(BufferedImage image, File file, String imageFormat) { 11 | try { 12 | ImageIO.write(image, imageFormat, file); 13 | } catch (final IOException e) { 14 | throw new RuntimeException(e); 15 | } 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/skylinequeries/rtree/RectangleDepth.java: -------------------------------------------------------------------------------- 1 | package skylinequeries.rtree; 2 | 3 | import skylinequeries.rtree.geometry.Rectangle; 4 | 5 | final class RectangleDepth { 6 | private final Rectangle rectangle; 7 | private final int depth; 8 | 9 | RectangleDepth(Rectangle rectangle, int depth) { 10 | super(); 11 | this.rectangle = rectangle; 12 | this.depth = depth; 13 | } 14 | 15 | Rectangle getRectangle() { 16 | return rectangle; 17 | } 18 | 19 | int getDepth() { 20 | return depth; 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/skylinequeries/rtree/Splitter.java: -------------------------------------------------------------------------------- 1 | package skylinequeries.rtree; 2 | 3 | import skylinequeries.rtree.geometry.HasGeometry; 4 | import skylinequeries.rtree.geometry.ListPair; 5 | 6 | import java.util.List; 7 | 8 | public interface Splitter { 9 | 10 | /** 11 | * Splits a list of items into two lists of at least minSize. 12 | * 13 | * @param geometry type 14 | * @param items list of items to split 15 | * @param minSize min size of each list 16 | * @return two lists 17 | */ 18 | ListPair split(List items, int minSize); 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/skylinequeries/rtree/geometry/Group.java: -------------------------------------------------------------------------------- 1 | package skylinequeries.rtree.geometry; 2 | 3 | import skylinequeries.rtree.Util; 4 | 5 | import java.util.List; 6 | 7 | public class Group implements HasGeometry { 8 | 9 | private final List list; 10 | private final Rectangle mbr; 11 | 12 | public Group(List list) { 13 | this.list = list; 14 | this.mbr = Util.mbr(list); 15 | } 16 | 17 | public List list() { 18 | return list; 19 | } 20 | 21 | @Override 22 | public Geometry geometry() { 23 | return mbr; 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/skylinequeries/rtree/Node.java: -------------------------------------------------------------------------------- 1 | package skylinequeries.rtree; 2 | 3 | import rx.Subscriber; 4 | import rx.functions.Func1; 5 | import skylinequeries.rtree.geometry.Geometry; 6 | import skylinequeries.rtree.geometry.HasGeometry; 7 | 8 | import java.util.List; 9 | 10 | public interface Node extends HasGeometry { 11 | 12 | List> add(Entry entry); 13 | 14 | NodeAndEntries delete(Entry entry, boolean all); 15 | 16 | void search(Func1 condition, Subscriber> subscriber); 17 | 18 | int count(); 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/skylinequeries/rtree/NodePosition.java: -------------------------------------------------------------------------------- 1 | package skylinequeries.rtree; 2 | 3 | import skylinequeries.rtree.geometry.Geometry; 4 | 5 | final class NodePosition { 6 | 7 | private final Node node; 8 | private final int position; 9 | 10 | NodePosition(Node node, int position) { 11 | this.node = node; 12 | this.position = position; 13 | } 14 | 15 | Node node() { 16 | return node; 17 | } 18 | 19 | int position() { 20 | return position; 21 | } 22 | 23 | NodePosition nextPosition() { 24 | return new NodePosition(node, position + 1); 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/skylinequeries/rtree/SelectorMinimalAreaIncrease.java: -------------------------------------------------------------------------------- 1 | package skylinequeries.rtree; 2 | 3 | import skylinequeries.rtree.geometry.Geometry; 4 | 5 | import java.util.List; 6 | 7 | import static java.util.Collections.min; 8 | import static skylinequeries.rtree.Comparators.*; 9 | 10 | /** 11 | * Uses minimal area increase to select a node from a list. 12 | */ 13 | public final class SelectorMinimalAreaIncrease implements Selector { 14 | 15 | @SuppressWarnings("unchecked") 16 | @Override 17 | public Node select(Geometry g, List> nodes) { 18 | return min(nodes, compose(areaIncreaseComparator(g.mbr()), areaComparator(g.mbr()))); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/skylinequeries/rtree/SelectorMinimalOverlapArea.java: -------------------------------------------------------------------------------- 1 | package skylinequeries.rtree; 2 | 3 | import skylinequeries.rtree.geometry.Geometry; 4 | 5 | import java.util.List; 6 | 7 | import static java.util.Collections.min; 8 | 9 | public final class SelectorMinimalOverlapArea implements Selector { 10 | 11 | @SuppressWarnings("unchecked") 12 | @Override 13 | public Node select(Geometry g, List> nodes) { 14 | return min( 15 | nodes, 16 | Comparators.compose(Comparators.overlapAreaComparator(g.mbr(), nodes), Comparators.areaIncreaseComparator(g.mbr()), 17 | Comparators.areaComparator(g.mbr()))); 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/skylinequeries/rtree/geometry/Geometries.java: -------------------------------------------------------------------------------- 1 | package skylinequeries.rtree.geometry; 2 | 3 | public final class Geometries { 4 | 5 | private Geometries() { 6 | // prevent instantiation 7 | } 8 | 9 | public static Point point(double x, double y) { 10 | return Point.create(x, y); 11 | } 12 | 13 | public static Rectangle rectangle(double x1, double y1, double x2, double y2) { 14 | return Rectangle.create(x1, y1, x2, y2); 15 | } 16 | 17 | public static Rectangle rectangle(Point p, double x, double y) { 18 | return Rectangle.create(p.x(), p.y(), x, y); 19 | } 20 | 21 | public static Circle circle(double x, double y, double radius) { 22 | return Circle.create(x, y, radius); 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/skylinequeries/rtree/ObjectsHelper.java: -------------------------------------------------------------------------------- 1 | package skylinequeries.rtree; 2 | 3 | import com.google.common.base.Optional; 4 | 5 | import static com.google.common.base.Optional.absent; 6 | 7 | public final class ObjectsHelper { 8 | 9 | private ObjectsHelper() { 10 | // prevent instantiation 11 | } 12 | 13 | static void instantiateForTestCoveragePurposesOnly() { 14 | new ObjectsHelper(); 15 | } 16 | 17 | @SuppressWarnings("unchecked") 18 | public static Optional asClass(Object object, Class cls) { 19 | if (object == null) 20 | return absent(); 21 | else if (object.getClass() != cls) 22 | return absent(); 23 | else 24 | return Optional.of((T) object); 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/skylinequeries/rtree/Selector.java: -------------------------------------------------------------------------------- 1 | package skylinequeries.rtree; 2 | 3 | import skylinequeries.rtree.geometry.Geometry; 4 | 5 | import java.util.List; 6 | 7 | /** 8 | * The heuristic used on insert to select which node to add an Entry to. 9 | */ 10 | public interface Selector { 11 | 12 | /** 13 | * Returns the node from a list of nodes that an object with the given 14 | * geometry would be added to. 15 | * 16 | * @param type of value of entry in tree 17 | * @param type of geometry of entry in tree 18 | * @param g geometry 19 | * @param nodes nodes to select from 20 | * @return one of the given nodes 21 | */ 22 | Node select(Geometry g, List> nodes); 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/skylinequeries/rtree/geometry/Intersects.java: -------------------------------------------------------------------------------- 1 | package skylinequeries.rtree.geometry; 2 | 3 | import rx.functions.Func2; 4 | 5 | public class Intersects { 6 | 7 | private Intersects() { 8 | // prevent instantiation 9 | } 10 | 11 | public static final Func2 rectangleIntersectsCircle = new Func2() { 12 | @Override 13 | public Boolean call(Rectangle rectangle, Circle circle) { 14 | return circle.intersects(rectangle); 15 | } 16 | }; 17 | 18 | public static final Func2 pointIntersectsCircle = new Func2() { 19 | @Override 20 | public Boolean call(Point point, Circle circle) { 21 | return circle.intersects(point); 22 | } 23 | }; 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/skylinequeries/rtree/SelectorRStar.java: -------------------------------------------------------------------------------- 1 | package skylinequeries.rtree; 2 | 3 | import skylinequeries.rtree.geometry.Geometry; 4 | 5 | import java.util.List; 6 | 7 | /** 8 | * Uses minimal overlap area selector for leaf nodes and minimal areea increase 9 | * selector for non-leaf nodes. 10 | */ 11 | public final class SelectorRStar implements Selector { 12 | 13 | private static Selector overlapAreaSelector = new SelectorMinimalOverlapArea(); 14 | private static Selector areaIncreaseSelector = new SelectorMinimalAreaIncrease(); 15 | 16 | @Override 17 | public Node select(Geometry g, List> nodes) { 18 | boolean leafNodes = nodes.get(0) instanceof Leaf; 19 | if (leafNodes) 20 | return overlapAreaSelector.select(g, nodes); 21 | else 22 | return areaIncreaseSelector.select(g, nodes); 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/skylinequeries/tools/database/DBConnection.java: -------------------------------------------------------------------------------- 1 | package skylinequeries.tools.database; 2 | 3 | import skylinequeries.tools.Constants; 4 | 5 | import java.sql.Connection; 6 | import java.sql.DriverManager; 7 | import java.sql.SQLException; 8 | 9 | /** 10 | * @author Vinh Nguyen 11 | */ 12 | public class DBConnection implements Constants { 13 | private DBConnection() { 14 | } 15 | 16 | public static Connection initialize() { 17 | Connection connection = null; 18 | try { 19 | connection = DriverManager.getConnection(Constants.DB_URL, 20 | Constants.DB_USERNAME, Constants.DB_PASSWORD); 21 | } catch (SQLException e) { 22 | e.printStackTrace(); 23 | } 24 | 25 | return connection; 26 | } 27 | 28 | public static void terminate(Connection connection) { 29 | try { 30 | if (connection != null) { 31 | connection.close(); 32 | } 33 | } catch (SQLException e) { 34 | e.printStackTrace(); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Vinh Nguyen & Kien Hoang 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/main/java/skylinequeries/rtree/geometry/ListPair.java: -------------------------------------------------------------------------------- 1 | package skylinequeries.rtree.geometry; 2 | 3 | import java.util.List; 4 | 5 | /** 6 | * Not thread safe. 7 | * 8 | * @param list type 9 | */ 10 | public final class ListPair { 11 | private final Group group1; 12 | private final Group group2; 13 | // these non-final variable mean that this class is not thread-safe 14 | // because access to them is not synchronized 15 | private Float areaSum = null; 16 | private final Float marginSum; 17 | 18 | public ListPair(List list1, List list2) { 19 | this.group1 = new Group(list1); 20 | this.group2 = new Group(list2); 21 | this.marginSum = group1.geometry().mbr().perimeter() + group2.geometry().mbr().perimeter(); 22 | } 23 | 24 | public Group group1() { 25 | return group1; 26 | } 27 | 28 | public Group group2() { 29 | return group2; 30 | } 31 | 32 | public float areaSum() { 33 | if (areaSum == null) 34 | areaSum = group1.geometry().mbr().area() + group2.geometry().mbr().area(); 35 | return areaSum; 36 | } 37 | 38 | public float marginSum() { 39 | return marginSum; 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/skylinequeries/rtree/geometry/Geometry.java: -------------------------------------------------------------------------------- 1 | package skylinequeries.rtree.geometry; 2 | 3 | 4 | /** 5 | * A geometrical region that represents an Entry spatially. It is recommended 6 | * that implementations of this interface implement equals() and hashCode() 7 | * appropriately that entry equality checks work as expected. 8 | */ 9 | public interface Geometry { 10 | 11 | /** 12 | *

13 | * Returns the distance to the given {@link Rectangle}. For a 14 | * {@link Rectangle} this might be Euclidean distance but for an EPSG4326 15 | * lat-long Rectangle might be great-circle distance. The distance function 16 | * should satisfy the following properties: 17 | *

18 | *

19 | *

20 | * distance(r) >= 0 21 | *

22 | *

23 | *

24 | * if r1 contains r2 then distance(r1)<=distance(r2) 25 | *

26 | * 27 | * @param r rectangle to measure distance to 28 | * @return distance to the rectangle r from the geometry 29 | */ 30 | double distance(Rectangle r); 31 | 32 | /** 33 | * Returns the minimum bounding rectangle of this geometry. 34 | * 35 | * @return minimum bounding rectangle 36 | */ 37 | Rectangle mbr(); 38 | 39 | boolean intersects(Rectangle r); 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/skylinequeries/rtree/NodeAndEntries.java: -------------------------------------------------------------------------------- 1 | package skylinequeries.rtree; 2 | 3 | import com.google.common.base.Optional; 4 | import skylinequeries.rtree.geometry.Geometry; 5 | 6 | import java.util.List; 7 | 8 | /** 9 | * Used for tracking deletions through recursive calls. 10 | * 11 | * @param entry type 12 | */ 13 | class NodeAndEntries { 14 | 15 | private final Optional> node; 16 | private final List> entries; 17 | private final int count; 18 | 19 | /** 20 | * Constructor. 21 | * 22 | * @param node absent = whole node was deleted present = either an unchanged 23 | * node because of no removal or the newly created node without 24 | * the deleted entry 25 | * @param entries from nodes that dropped below minChildren in size and thus 26 | * their entries are to be redistributed (readded to the tree) 27 | * @param countDeleted count of the number of entries removed 28 | */ 29 | NodeAndEntries(Optional> node, List> entries, int countDeleted) { 30 | this.node = node; 31 | this.entries = entries; 32 | this.count = countDeleted; 33 | } 34 | 35 | Optional> node() { 36 | return node; 37 | } 38 | 39 | List> entriesToAdd() { 40 | return entries; 41 | } 42 | 43 | int countDeleted() { 44 | return count; 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/skylinequeries/tools/Drawer.java: -------------------------------------------------------------------------------- 1 | package skylinequeries.tools; 2 | 3 | import skylinequeries.rtree.geometry.Point; 4 | 5 | import java.util.List; 6 | import java.util.concurrent.Callable; 7 | 8 | /** 9 | * A utility class to draw the skyline (not thread-safe). 10 | * 11 | * @author Vinh Nguyen 12 | */ 13 | public class Drawer implements Callable { 14 | private List points; 15 | private List skylinePoints; 16 | private double xMin, yMin, xMax, yMax; 17 | 18 | public Drawer(List points, List skylinePoints, 19 | double xMin, double yMin, double xMax, double yMax) { 20 | this.points = points; 21 | this.skylinePoints = skylinePoints; 22 | this.xMin = xMin; 23 | this.yMin = yMin; 24 | this.xMax = xMax; 25 | this.yMax = yMax; 26 | } 27 | 28 | @Override 29 | public Object call() throws Exception { 30 | StdDraw.setCanvasSize(600, 600); 31 | StdDraw.setXscale(xMin, xMax); 32 | StdDraw.setYscale(yMin, yMax); 33 | 34 | for (Point currentPoint : points) { 35 | if (skylinePoints.contains(currentPoint)) { 36 | StdDraw.setPenRadius(0.005); 37 | StdDraw.setPenColor(StdDraw.RED); 38 | StdDraw.point(currentPoint.x(), currentPoint.y()); 39 | } else { 40 | StdDraw.setPenRadius(0.001); 41 | StdDraw.setPenColor(StdDraw.GRAY); 42 | StdDraw.point(currentPoint.x(), currentPoint.y()); 43 | } 44 | } 45 | 46 | return null; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/skylinequeries/rtree/Context.java: -------------------------------------------------------------------------------- 1 | package skylinequeries.rtree; 2 | 3 | import com.google.common.base.Preconditions; 4 | 5 | /** 6 | * Configures an RTree prior to instantiation of an {@link RTree}. 7 | */ 8 | public final class Context { 9 | 10 | private final int maxChildren; 11 | private final int minChildren; 12 | private final Splitter splitter; 13 | private final Selector selector; 14 | 15 | /** 16 | * Constructor. 17 | * 18 | * @param minChildren minimum number of children per node (at least 1) 19 | * @param maxChildren max number of children per node (minimum is 3) 20 | * @param selector algorithm to select search path 21 | * @param splitter algorithm to split the children across two new nodes 22 | */ 23 | public Context(int minChildren, int maxChildren, Selector selector, Splitter splitter) { 24 | Preconditions.checkNotNull(splitter); 25 | Preconditions.checkNotNull(selector); 26 | Preconditions.checkArgument(maxChildren > 2); 27 | Preconditions.checkArgument(minChildren >= 1); 28 | Preconditions.checkArgument(minChildren < maxChildren); 29 | this.selector = selector; 30 | this.maxChildren = maxChildren; 31 | this.minChildren = minChildren; 32 | this.splitter = splitter; 33 | } 34 | 35 | public int maxChildren() { 36 | return maxChildren; 37 | } 38 | 39 | public int minChildren() { 40 | return minChildren; 41 | } 42 | 43 | public Splitter splitter() { 44 | return splitter; 45 | } 46 | 47 | public Selector selector() { 48 | return selector; 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/skylinequeries/rtree/OperatorBoundedPriorityQueue.java: -------------------------------------------------------------------------------- 1 | package skylinequeries.rtree; 2 | 3 | import com.google.common.collect.MinMaxPriorityQueue; 4 | import rx.Observable.Operator; 5 | import rx.Subscriber; 6 | 7 | import java.util.Comparator; 8 | 9 | public final class OperatorBoundedPriorityQueue implements Operator { 10 | 11 | private final int maximumSize; 12 | private final Comparator comparator; 13 | 14 | public OperatorBoundedPriorityQueue(int maximumSize, Comparator comparator) { 15 | this.maximumSize = maximumSize; 16 | this.comparator = comparator; 17 | } 18 | 19 | @Override 20 | public Subscriber call(final Subscriber child) { 21 | final MinMaxPriorityQueue q = MinMaxPriorityQueue.orderedBy(comparator) 22 | .maximumSize(maximumSize).create(); 23 | return new Subscriber(child) { 24 | 25 | @Override 26 | public void onCompleted() { 27 | for (T t : q) { 28 | if (isUnsubscribed()) 29 | return; 30 | child.onNext(t); 31 | } 32 | if (isUnsubscribed()) 33 | return; 34 | child.onCompleted(); 35 | } 36 | 37 | @Override 38 | public void onError(Throwable t) { 39 | if (!isUnsubscribed()) 40 | child.onError(t); 41 | } 42 | 43 | @Override 44 | public void onNext(T t) { 45 | if (!isUnsubscribed()) 46 | q.add(t); 47 | } 48 | }; 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/skylinequeries/rtree/geometry/Point.java: -------------------------------------------------------------------------------- 1 | package skylinequeries.rtree.geometry; 2 | 3 | import com.google.common.base.Objects; 4 | import com.google.common.base.Optional; 5 | import skylinequeries.rtree.ObjectsHelper; 6 | 7 | public final class Point implements Geometry { 8 | 9 | private final Rectangle mbr; 10 | 11 | protected Point(float x, float y) { 12 | this.mbr = Rectangle.create(x, y, x, y); 13 | } 14 | 15 | public static Point create(double x, double y) { 16 | return new Point((float) x, (float) y); 17 | } 18 | 19 | @Override 20 | public Rectangle mbr() { 21 | return mbr; 22 | } 23 | 24 | @Override 25 | public double distance(Rectangle r) { 26 | return mbr.distance(r); 27 | } 28 | 29 | public double distance(Point p) { 30 | float dx = mbr().x1() - p.mbr().x1(); 31 | float dy = mbr().y1() - p.mbr().y1(); 32 | return Math.sqrt(dx * dx + dy * dy); 33 | } 34 | 35 | @Override 36 | public boolean intersects(Rectangle r) { 37 | return mbr.intersects(r); 38 | } 39 | 40 | public float x() { 41 | return mbr.x1(); 42 | } 43 | 44 | public float y() { 45 | return mbr.y1(); 46 | } 47 | 48 | @Override 49 | public int hashCode() { 50 | return Objects.hashCode(mbr); 51 | } 52 | 53 | @Override 54 | public boolean equals(Object obj) { 55 | Optional other = ObjectsHelper.asClass(obj, Point.class); 56 | if (other.isPresent()) { 57 | return Objects.equal(mbr, other.get().mbr()); 58 | } 59 | else 60 | return false; 61 | } 62 | 63 | @Override 64 | public String toString() { 65 | return "Point [x=" + x() + ", y=" + y() + "]"; 66 | } 67 | 68 | } -------------------------------------------------------------------------------- /src/main/java/skylinequeries/tools/database/Query.java: -------------------------------------------------------------------------------- 1 | package skylinequeries.tools.database; 2 | 3 | import skylinequeries.rtree.geometry.Geometries; 4 | import skylinequeries.rtree.geometry.Point; 5 | 6 | import java.sql.Connection; 7 | import java.sql.PreparedStatement; 8 | import java.sql.ResultSet; 9 | import java.sql.SQLException; 10 | import java.util.ArrayList; 11 | import java.util.List; 12 | 13 | /** 14 | * @author Vinh Nguyen 15 | */ 16 | public class Query { 17 | private Connection connection; 18 | 19 | public Query(Connection connection) { 20 | this.connection = connection; 21 | } 22 | 23 | public Connection getConnection() { 24 | return connection; 25 | } 26 | 27 | /** 28 | * Queries the database to get all rows from a table, 29 | * construct points from these rows and add these points to a list. 30 | * We also measure the execution time of query. 31 | * 32 | * @param tableName the table name 33 | * @return a list of points 34 | */ 35 | public List getAllRecords(final String tableName) { 36 | PreparedStatement statement = null; 37 | List points = new ArrayList<>(); 38 | String query = "SELECT * FROM " + String.format("%s", "`".concat(tableName).concat("`")); 39 | 40 | try { 41 | statement = getConnection().prepareStatement(query); 42 | ResultSet resultSet = statement.executeQuery(); 43 | while (resultSet.next()) { 44 | points.add(Geometries.point(resultSet.getDouble("x"), resultSet.getDouble("y"))); 45 | } 46 | } catch (SQLException e) { 47 | e.printStackTrace(); 48 | } finally { 49 | try { 50 | if (statement != null) { 51 | statement.close(); 52 | } 53 | } catch (SQLException e) { 54 | e.printStackTrace(); 55 | } 56 | } 57 | 58 | return points; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/skylinequeries/rtree/Functions.java: -------------------------------------------------------------------------------- 1 | package skylinequeries.rtree; 2 | 3 | import rx.functions.Func1; 4 | import skylinequeries.rtree.geometry.HasGeometry; 5 | import skylinequeries.rtree.geometry.ListPair; 6 | import skylinequeries.rtree.geometry.Rectangle; 7 | 8 | import java.util.List; 9 | 10 | /** 11 | * Utility functions for making {@link Selector}s and {@link Splitter}s. 12 | */ 13 | public final class Functions { 14 | 15 | private Functions() { 16 | // prevent instantiation 17 | } 18 | 19 | public static final Func1, Double> overlapListPair = new Func1, Double>() { 20 | 21 | @Override 22 | public Double call(ListPair pair) { 23 | return (double) pair.group1().geometry().mbr() 24 | .intersectionArea(pair.group2().geometry().mbr()); 25 | } 26 | }; 27 | 28 | public static Func1 overlapArea(final Rectangle r, 29 | final List list) { 30 | return new Func1() { 31 | 32 | @Override 33 | public Double call(HasGeometry g) { 34 | Rectangle gPlusR = g.geometry().mbr().add(r); 35 | double m = 0; 36 | for (HasGeometry other : list) { 37 | if (other != g) { 38 | m += gPlusR.intersectionArea(other.geometry().mbr()); 39 | } 40 | } 41 | return m; 42 | } 43 | }; 44 | } 45 | 46 | public static Func1 areaIncrease(final Rectangle r) { 47 | return new Func1() { 48 | @Override 49 | public Double call(HasGeometry g) { 50 | Rectangle gPlusR = g.geometry().mbr().add(r); 51 | return (double) (gPlusR.area() - g.geometry().mbr().area()); 52 | } 53 | }; 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/skylinequeries/rtree/geometry/Circle.java: -------------------------------------------------------------------------------- 1 | package skylinequeries.rtree.geometry; 2 | 3 | import com.google.common.base.Objects; 4 | import com.google.common.base.Optional; 5 | import skylinequeries.rtree.ObjectsHelper; 6 | 7 | public final class Circle implements Geometry { 8 | 9 | private final float x, y, radius; 10 | private final Rectangle mbr; 11 | 12 | protected Circle(float x, float y, float radius) { 13 | this.x = x; 14 | this.y = y; 15 | this.radius = radius; 16 | this.mbr = Rectangle.create(x - radius, y - radius, x + radius, y + radius); 17 | } 18 | 19 | public static Circle create(double x, double y, double radius) { 20 | return new Circle((float) x, (float) y, (float) radius); 21 | } 22 | 23 | public float x() { 24 | return x; 25 | } 26 | 27 | public float y() { 28 | return y; 29 | } 30 | 31 | @Override 32 | public Rectangle mbr() { 33 | return mbr; 34 | } 35 | 36 | @Override 37 | public double distance(Rectangle r) { 38 | return Math.max(0, new Point(x, y).distance(r) - radius); 39 | } 40 | 41 | @Override 42 | public boolean intersects(Rectangle r) { 43 | return distance(r) == 0; 44 | } 45 | 46 | public boolean intersects(Circle c) { 47 | return new Point(x, y).distance(new Point(c.x, c.y)) <= radius + c.radius; 48 | } 49 | 50 | @Override 51 | public int hashCode() { 52 | return Objects.hashCode(x, y, radius); 53 | } 54 | 55 | @Override 56 | public boolean equals(Object obj) { 57 | Optional other = ObjectsHelper.asClass(obj, Circle.class); 58 | if (other.isPresent()) { 59 | return Objects.equal(x, other.get().x) && Objects.equal(y, other.get().y) 60 | && Objects.equal(radius, other.get().radius); 61 | } 62 | else 63 | return false; 64 | } 65 | 66 | public boolean intersects(Point point) { 67 | return Math.sqrt(sqr(x - point.x()) + sqr(y - point.y())) <= radius; 68 | } 69 | 70 | private float sqr(float x) { 71 | return x * x; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/main/java/skylinequeries/algs/nn/NN.java: -------------------------------------------------------------------------------- 1 | package skylinequeries.algs.nn; 2 | 3 | import skylinequeries.rtree.Entry; 4 | import skylinequeries.rtree.RTree; 5 | import skylinequeries.rtree.geometry.Geometries; 6 | import skylinequeries.rtree.geometry.Point; 7 | 8 | import java.util.ArrayList; 9 | import java.util.List; 10 | import java.util.Stack; 11 | 12 | /** 13 | * Nearest Neighbor Algorithm. 14 | * 15 | * @author Vinh Nguyen + Kien Hoang 16 | */ 17 | public class NN { 18 | public static final double MAX_COORDINATE_VALUE = Double.POSITIVE_INFINITY; 19 | public static final Point origin = Geometries.point(0.0, 0.0); 20 | private RTree rTree; 21 | 22 | public NN(RTree rTree) { 23 | this.rTree = rTree; 24 | } 25 | 26 | public RTree getTree() { 27 | return rTree; 28 | } 29 | 30 | public List> execute() { 31 | if (getTree().isEmpty()) return null; 32 | List> skylineEntries = new ArrayList<>(); 33 | Stack todoList = new Stack<>(); 34 | 35 | List> list = getTree().nearest(origin, MAX_COORDINATE_VALUE, 1) 36 | .toList().toBlocking().singleOrDefault(null); 37 | Entry firstNN = list.get(0); 38 | skylineEntries.add(firstNN); 39 | todoList.push(Geometries.point(firstNN.geometry().x(), MAX_COORDINATE_VALUE)); 40 | todoList.push(Geometries.point(MAX_COORDINATE_VALUE, firstNN.geometry().y())); 41 | 42 | while (!todoList.empty()) { 43 | Point p = todoList.pop(); 44 | List> nnl = getTree().boundedNNSearch(origin, 45 | Geometries.rectangle(origin, p.x(), p.y()), 1) 46 | .toList().toBlocking().singleOrDefault(null); 47 | if (!nnl.isEmpty()) { 48 | Entry nn = nnl.remove(0); 49 | skylineEntries.add(nn); 50 | todoList.push(Geometries.point(nn.geometry().x(), p.y())); 51 | todoList.push(Geometries.point(p.x(), nn.geometry().y())); 52 | } 53 | } 54 | 55 | return skylineEntries; 56 | } 57 | } 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 4.0.0 5 | 6 | 7 | org.sonatype.oss 8 | oss-parent 9 | 9 10 | 11 | 12 | SkylineQueries 13 | 1.0 14 | 15 | 16 | UTF-8 17 | 1.7 18 | 19 | 3.0.0 20 | 2.10.1 21 | 2.7 22 | 0.20.7 23 | 24 | 25 | 26 | 27 | com.google.guava 28 | guava 29 | 18.0 30 | 31 | 32 | com.netflix.rxjava 33 | rxjava-core 34 | ${rxjava.version} 35 | 36 | 37 | mysql 38 | mysql-connector-java 39 | 8.0.16 40 | 41 | 42 | junit 43 | junit 44 | 4.13.1 45 | 46 | 47 | 48 | 49 | 50 | 51 | maven-compiler-plugin 52 | 3.1 53 | 54 | ${maven.compiler.target} 55 | ${maven.compiler.target} 56 | 57 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Spatial Skyline Query Algorithms 2 | 3 | A Java Implementation of Spatial Skyline Query Algorithms. 4 | 5 | ## References 6 | Algorithms are built based on descriptions in the following research papers: 7 | * [An Optimal and Progressive Algorithm for Skyline Queries](http://www.cs.ust.hk/~dimitris/publications.html) by Dimitris Papadias, Yufei Tao, Greg Fu, and Bernhard Seeger. 8 | * [Shooting Stars in the Sky: An Online Algorithm for Skyline Queries](http://www.informatik.uni-trier.de/~ley/pers/hd/k/Kossmann:Donald) by Donald Kossmann, Frank Ramsak, and Steffen Rost. 9 | 10 | ## Algorithms 11 | * Branch and Bound Skyline (BBS) 12 | * Nearest Neighbor (NN) 13 | 14 | ## Libraries 15 | * [RTree](https://github.com/davidmoten/rtree) by [Dave Moten](https://github.com/davidmoten). 16 | * [Java Standard Libraries - StdDraw](http://introcs.cs.princeton.edu/java/stdlib/StdDraw.java.html) by [Robert Sedgewick](http://www.cs.princeton.edu/~rs/) and [Kevin Wayne](http://www.cs.princeton.edu/~wayne/contact/). 17 | 18 | ## Sample Data 19 | * [Data files](https://github.com/dkanellis/SkySpark/tree/master/data/datasets) are taken from the [SkySpark](https://github.com/dkanellis/SkySpark) repository of [Dimitris Kanellis](https://github.com/dkanellis). 20 | * [Data files](https://github.com/dkanellis/SkySpark/tree/master/data/datasets) are imported into MySQL database, and queried later for testing algorithms. 21 | 22 | ## Demonstration 23 | ##### GUI application 24 | ![GUI application](http://i1368.photobucket.com/albums/ag182/vinhnguyenict/2015-09-01_1424_zpskdzddbbu.png) 25 | 26 | ##### Skyline Visualization 27 | ###### Anti-correlated 2D points dataset 28 | ![Anti-correlated](http://i1368.photobucket.com/albums/ag182/vinhnguyenict/2015-09-13_1730_zpspo9lk2wn.png) 29 | 30 | ###### Correlated 2D points dataset 31 | ![Correlated](http://i1368.photobucket.com/albums/ag182/vinhnguyenict/2015-09-13_1733_zpsavcc0d2v.png) 32 | 33 | ###### Uniformly Distributed 2D points dataset 34 | ![Uniform](http://i1368.photobucket.com/albums/ag182/vinhnguyenict/2015-09-13_1734_zpsbpdfkaep.png) 35 | 36 | ## License 37 | This application is open-source released under [MIT license](http://opensource.org/licenses/MIT). 38 | 39 | All 3rd party open sourced libraries distributed with this application are still under their own license. 40 | 41 | 42 | ## Authors 43 | * [Vinh Nguyen](https://github.com/vinhnguyen-fly) 44 | * [Kien Hoang](https://github.com/goddesss) 45 | -------------------------------------------------------------------------------- /src/main/java/skylinequeries/demo/NNWorker.java: -------------------------------------------------------------------------------- 1 | package skylinequeries.demo; 2 | 3 | import skylinequeries.tools.Utilities; 4 | import skylinequeries.tools.Drawer; 5 | import skylinequeries.algs.nn.NN; 6 | import skylinequeries.rtree.Entry; 7 | import skylinequeries.rtree.RTree; 8 | import skylinequeries.rtree.geometry.Point; 9 | 10 | import javax.swing.*; 11 | import java.time.Duration; 12 | import java.time.Instant; 13 | import java.util.List; 14 | import java.util.concurrent.ExecutorService; 15 | import java.util.concurrent.Executors; 16 | 17 | /** 18 | * @author Vinh Nguyen 19 | */ 20 | public class NNWorker extends SwingWorker>, Entry> { 21 | private RTree rTree = RTree.star().create(); 22 | private List> skylineEntries; 23 | private final String dataset; 24 | private final JTextArea viewer, logger; 25 | private List points, skylinePoints; 26 | private long executionTime; 27 | 28 | public NNWorker(String dataset, JTextArea viewer, JTextArea logger) { 29 | this.dataset = dataset; 30 | this.viewer = viewer; 31 | this.logger = logger; 32 | } 33 | 34 | @Override 35 | protected List> doInBackground() throws Exception { 36 | rTree = Utilities.constructRTree(rTree, dataset); 37 | final NN nn = new NN(rTree); 38 | 39 | Instant before = Instant.now(); 40 | skylineEntries = nn.execute(); 41 | Instant after = Instant.now(); 42 | executionTime = Duration.between(before, after).toMillis(); 43 | 44 | points = Utilities.constructRTree(dataset); 45 | skylinePoints = Utilities.entriesToPoints(skylineEntries); 46 | 47 | for (Entry entry : skylineEntries) { 48 | publish(entry); 49 | } 50 | 51 | return skylineEntries; 52 | } 53 | 54 | @Override 55 | protected void process(List> foundEntries) { 56 | int count = 1; 57 | for (Entry entry : foundEntries) { 58 | viewer.append(String.format("%3d - %s", count, entry.geometry().toString()) + "\n\n"); 59 | count++; 60 | } 61 | } 62 | 63 | @Override 64 | protected void done() { 65 | logger.append(Utilities.console(skylineEntries, points.size(), executionTime, 0)); 66 | ExecutorService executor = Executors.newFixedThreadPool(1); 67 | executor.submit(new Drawer(points, skylinePoints, 0.0, 0.0, 10000.0, 10000.0)); 68 | executor.shutdown(); 69 | System.gc(); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/main/java/skylinequeries/demo/BBSWorker.java: -------------------------------------------------------------------------------- 1 | package skylinequeries.demo; 2 | 3 | import skylinequeries.algs.bbs.BBS; 4 | import skylinequeries.tools.Drawer; 5 | import skylinequeries.tools.Utilities; 6 | import skylinequeries.rtree.Entry; 7 | import skylinequeries.rtree.RTree; 8 | import skylinequeries.rtree.geometry.Point; 9 | 10 | import javax.swing.*; 11 | import java.time.Duration; 12 | import java.time.Instant; 13 | import java.util.List; 14 | import java.util.concurrent.ExecutorService; 15 | import java.util.concurrent.Executors; 16 | 17 | /** 18 | * @author Vinh Nguyen 19 | */ 20 | public class BBSWorker extends SwingWorker>, Entry> { 21 | private RTree rTree = RTree.star().create(); 22 | private List> skylineEntries; 23 | private final String dataset; 24 | private List points, skylinePoints; 25 | private final JTextArea viewer, logger; 26 | private long executionTime; 27 | 28 | public BBSWorker(String dataset, JTextArea viewer, JTextArea logger) { 29 | this.dataset = dataset; 30 | this.viewer = viewer; 31 | this.logger = logger; 32 | } 33 | 34 | @Override 35 | protected List> doInBackground() throws Exception { 36 | rTree = Utilities.constructRTree(rTree, dataset); 37 | final BBS bbs = new BBS(rTree); 38 | 39 | Instant before = Instant.now(); 40 | skylineEntries = bbs.execute(); 41 | Instant after = Instant.now(); 42 | executionTime = Duration.between(before, after).toMillis(); 43 | 44 | points = Utilities.constructRTree(dataset); 45 | skylinePoints = Utilities.entriesToPoints(skylineEntries); 46 | 47 | for (Entry entry : skylineEntries) { 48 | publish(entry); 49 | } 50 | 51 | return skylineEntries; 52 | } 53 | 54 | @Override 55 | protected void process(List> foundEntries) { 56 | int count = 1; 57 | for (Entry entry : foundEntries) { 58 | viewer.append(String.format("%3d - %s", count, entry.geometry().toString()) + "\n\n"); 59 | count++; 60 | } 61 | } 62 | 63 | @Override 64 | protected void done() { 65 | logger.append(Utilities.console(skylineEntries, points.size(), executionTime, BBS.NODE_ACCESSES)); 66 | ExecutorService executor = Executors.newFixedThreadPool(1); 67 | executor.submit(new Drawer(points, skylinePoints, 0.0, 0.0, 10000.0, 10000.0)); 68 | executor.shutdown(); 69 | System.gc(); 70 | } 71 | } -------------------------------------------------------------------------------- /src/main/java/skylinequeries/rtree/Entry.java: -------------------------------------------------------------------------------- 1 | package skylinequeries.rtree; 2 | 3 | import com.google.common.base.Objects; 4 | import com.google.common.base.Optional; 5 | import com.google.common.base.Preconditions; 6 | import skylinequeries.rtree.geometry.Geometry; 7 | import skylinequeries.rtree.geometry.HasGeometry; 8 | 9 | /** 10 | * An entry in the R-tree which has a spatial representation. 11 | * 12 | * @param the type of Entry 13 | */ 14 | public final class Entry implements HasGeometry { 15 | private final T value; 16 | private final S geometry; 17 | 18 | /** 19 | * Constructor. 20 | * 21 | * @param value the value of the entry 22 | * @param geometry the geometry of the value 23 | */ 24 | public Entry(T value, S geometry) { 25 | Preconditions.checkNotNull(geometry); 26 | this.value = value; 27 | this.geometry = geometry; 28 | } 29 | 30 | /** 31 | * Factory method. 32 | * 33 | * @param type of value 34 | * @param type of geometry 35 | * @param value object being given a spatial context 36 | * @param geometry geometry associated with the value 37 | * @return entry wrapping value and associated geometry 38 | */ 39 | public static Entry entry(T value, S geometry) { 40 | return new Entry(value, geometry); 41 | } 42 | 43 | /** 44 | * Returns the value wrapped by this {@link Entry}. 45 | * 46 | * @return the entry value 47 | */ 48 | public T value() { 49 | return value; 50 | } 51 | 52 | @Override 53 | public S geometry() { 54 | return geometry; 55 | } 56 | 57 | @Override 58 | public String toString() { 59 | StringBuilder builder = new StringBuilder(); 60 | builder.append("Entry [value="); 61 | builder.append(value); 62 | builder.append(", geometry="); 63 | builder.append(geometry); 64 | builder.append("]"); 65 | return builder.toString(); 66 | } 67 | 68 | @Override 69 | public int hashCode() { 70 | return Objects.hashCode(value, geometry); 71 | } 72 | 73 | @Override 74 | public boolean equals(Object obj) { 75 | @SuppressWarnings("rawtypes") 76 | Optional other = ObjectsHelper.asClass(obj, Entry.class); 77 | if (other.isPresent()) { 78 | return Objects.equal(value, other.get().value) 79 | && Objects.equal(geometry, other.get().geometry); 80 | } 81 | else 82 | return false; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/main/java/skylinequeries/rtree/ImmutableStack.java: -------------------------------------------------------------------------------- 1 | package skylinequeries.rtree; 2 | 3 | import com.google.common.base.Optional; 4 | 5 | import java.util.Iterator; 6 | 7 | import static com.google.common.base.Optional.of; 8 | 9 | public final class ImmutableStack implements Iterable { 10 | private final Optional head; 11 | private final Optional> tail; 12 | 13 | private static ImmutableStack EMPTY = new ImmutableStack(); 14 | 15 | public ImmutableStack(final T head, final ImmutableStack tail) { 16 | this(of(head), of(tail)); 17 | } 18 | 19 | private ImmutableStack(Optional head, Optional> tail) { 20 | this.head = head; 21 | this.tail = tail; 22 | } 23 | 24 | public static ImmutableStack create(T t) { 25 | return new ImmutableStack(of(t), of(ImmutableStack. empty())); 26 | } 27 | 28 | public ImmutableStack() { 29 | this(Optional. absent(), Optional.> absent()); 30 | } 31 | 32 | @SuppressWarnings("unchecked") 33 | public static ImmutableStack empty() { 34 | return (ImmutableStack) EMPTY; 35 | } 36 | 37 | public boolean isEmpty() { 38 | return !head.isPresent(); 39 | } 40 | 41 | public T peek() { 42 | // if (isEmpty()) 43 | // throw new RuntimeException("cannot peek on empty stack"); 44 | // else 45 | return this.head.get(); 46 | } 47 | 48 | public ImmutableStack pop() { 49 | // if (isEmpty()) 50 | // throw new RuntimeException("cannot pop on empty stack"); 51 | // else 52 | return this.tail.get(); 53 | } 54 | 55 | public ImmutableStack push(T value) { 56 | return new ImmutableStack(value, this); 57 | } 58 | 59 | @Override 60 | public Iterator iterator() { 61 | return new StackIterator(this); 62 | } 63 | 64 | private static class StackIterator implements Iterator { 65 | private ImmutableStack stack; 66 | 67 | public StackIterator(final ImmutableStack stack) { 68 | this.stack = stack; 69 | } 70 | 71 | @Override 72 | public boolean hasNext() { 73 | return !this.stack.isEmpty(); 74 | } 75 | 76 | @Override 77 | public U next() { 78 | final U result = this.stack.peek(); 79 | this.stack = this.stack.pop(); 80 | return result; 81 | } 82 | 83 | @Override 84 | public void remove() { 85 | throw new RuntimeException("not supported"); 86 | } 87 | } 88 | 89 | } -------------------------------------------------------------------------------- /src/main/java/skylinequeries/algs/bbs/BBSHeapElement.java: -------------------------------------------------------------------------------- 1 | package skylinequeries.algs.bbs; 2 | 3 | import skylinequeries.rtree.Entry; 4 | import skylinequeries.rtree.Node; 5 | import skylinequeries.rtree.geometry.Point; 6 | 7 | /** 8 | * @author Vinh Nguyen 9 | */ 10 | public class BBSHeapElement implements Comparable { 11 | /** 12 | * A node in Rtree. In case the current accessed node is a NonLeaf, 13 | * its children are still intermediate nodes or Leafs. 14 | */ 15 | private Node node; 16 | 17 | /** 18 | * And entry in Rtree. In case the current accessed node is a Leaf, 19 | * its children are entries which point to the actual data points. 20 | */ 21 | private Entry entry; 22 | 23 | /** 24 | * The mindist of the Rtree entry (the sum of all dimensions). 25 | */ 26 | private double heapElementMinDist; 27 | 28 | /** 29 | * Checks if heap element contains node or entry. 30 | */ 31 | private boolean isNode; 32 | 33 | /** 34 | * Returns the node contained in the heap element. 35 | * 36 | * @return the heap node 37 | */ 38 | public Node getNode() { 39 | return node; 40 | } 41 | 42 | /** 43 | * Returns the entry contained in the heap element. 44 | * 45 | * @return the heap entry 46 | */ 47 | public Entry getEntry() { 48 | return entry; 49 | } 50 | 51 | /** 52 | * Returns a boolean value indicating whether heap element contains node or entry. 53 | * 54 | * @return true if node, false if entry 55 | */ 56 | public boolean isNode() { 57 | return isNode; 58 | } 59 | 60 | /** 61 | * Constructs heap element using a node. 62 | * 63 | * @param node the given node 64 | * @param isNode the node checker 65 | */ 66 | public BBSHeapElement(Node node, boolean isNode) { 67 | this.node = node; 68 | this.heapElementMinDist = node.geometry().mbr().x1() + node.geometry().mbr().y1(); 69 | this.isNode = isNode; 70 | } 71 | 72 | /** 73 | * Constructs heap element using an entry. 74 | * 75 | * @param entry the given entry 76 | * @param isNode the node checker 77 | */ 78 | public BBSHeapElement(Entry entry, boolean isNode) { 79 | this.entry = entry; 80 | this.heapElementMinDist = entry.geometry().x() + entry.geometry().y(); 81 | this.isNode = isNode; 82 | } 83 | 84 | /** 85 | * Compares heap elements according to their mindist values. 86 | * 87 | * @param o the heap element to compare 88 | * @return the comparison indicator 89 | */ 90 | @Override 91 | public int compareTo(BBSHeapElement o) { 92 | if (this.heapElementMinDist > o.heapElementMinDist) return 1; 93 | if (this.heapElementMinDist < o.heapElementMinDist) return -1; 94 | return 0; 95 | } 96 | } -------------------------------------------------------------------------------- /src/main/java/skylinequeries/rtree/Util.java: -------------------------------------------------------------------------------- 1 | package skylinequeries.rtree; 2 | 3 | import com.google.common.base.Preconditions; 4 | import skylinequeries.rtree.geometry.HasGeometry; 5 | import skylinequeries.rtree.geometry.Rectangle; 6 | 7 | import java.util.ArrayList; 8 | import java.util.Collection; 9 | import java.util.List; 10 | 11 | /** 12 | * @author dxm 13 | */ 14 | public final class Util { 15 | 16 | private Util() { 17 | // prevent instantiation 18 | } 19 | 20 | /** 21 | * Returns the minimum bounding rectangle of a number of items. Benchmarks 22 | * below indicate that when the number of items is >1 this method is more 23 | * performant than one using {@link skylinequeries.rtree.geometry.Rectangle#add(skylinequeries.rtree.geometry.Rectangle)}. 24 | *

25 | *

26 |      * Benchmark                             Mode  Samples         Score  Score error  Units
27 |      * c.g.d.r.BenchmarksMbr.mbrList1       thrpt       10  48450492.301   436127.960  ops/s
28 |      * c.g.d.r.BenchmarksMbr.mbrList2       thrpt       10  46658242.728   987901.581  ops/s
29 |      * c.g.d.r.BenchmarksMbr.mbrList3       thrpt       10  40357809.306   937827.660  ops/s
30 |      * c.g.d.r.BenchmarksMbr.mbrList4       thrpt       10  35930532.557   605535.237  ops/s
31 |      * c.g.d.r.BenchmarksMbr.mbrOldList1    thrpt       10  55848118.198  1342997.309  ops/s
32 |      * c.g.d.r.BenchmarksMbr.mbrOldList2    thrpt       10  25171873.903   395127.918  ops/s
33 |      * c.g.d.r.BenchmarksMbr.mbrOldList3    thrpt       10  19222116.139   246965.178  ops/s
34 |      * c.g.d.r.BenchmarksMbr.mbrOldList4    thrpt       10  14891862.638   198765.157  ops/s
35 |      * 
36 | * 37 | * @param items items to bound 38 | * @return the minimum bounding rectangle containings items 39 | */ 40 | public static Rectangle mbr(Collection items) { 41 | Preconditions.checkArgument(!items.isEmpty()); 42 | float minX1 = Float.MAX_VALUE; 43 | float minY1 = Float.MAX_VALUE; 44 | float maxX2 = Float.MIN_VALUE; 45 | float maxY2 = Float.MIN_VALUE; 46 | for (final HasGeometry item : items) { 47 | Rectangle r = item.geometry().mbr(); 48 | if (r.x1() < minX1) 49 | minX1 = r.x1(); 50 | if (r.y1() < minY1) 51 | minY1 = r.y1(); 52 | if (r.x2() > maxX2) 53 | maxX2 = r.x2(); 54 | if (r.y2() > maxY2) 55 | maxY2 = r.y2(); 56 | } 57 | return Rectangle.create(minX1, minY1, maxX2, maxY2); 58 | } 59 | 60 | static List add(List list, T element) { 61 | final ArrayList result = new ArrayList(list.size() + 2); 62 | result.addAll(list); 63 | result.add(element); 64 | return result; 65 | } 66 | 67 | static List remove(List list, List elements) { 68 | final ArrayList result = new ArrayList(list); 69 | result.removeAll(elements); 70 | return result; 71 | } 72 | 73 | static List replace(List list, T element, List replacements) { 74 | List list2 = new ArrayList(list.size() + replacements.size()); 75 | for (T node : list) 76 | if (node != element) 77 | list2.add(node); 78 | list2.addAll(replacements); 79 | return list2; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/main/java/skylinequeries/sql/skyline.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Import this sql file to create new database in MySQL. 3 | Use import wizard to import data for tables from text 4 | files in data folder. Note all tests use data from the 5 | database, so we need to to these above stuffs before 6 | executing those tests. 7 | 8 | Source Server : MySQL Connection 9 | Source Server Version : 50621 10 | Source Host : 127.0.0.1:3306 11 | Source Database : skyline 12 | 13 | Target Server Type : MYSQL 14 | Target Server Version : 50621 15 | File Encoding : 65001 16 | 17 | Date: 2015-01-05 22:36:23 18 | */ 19 | 20 | SET FOREIGN_KEY_CHECKS=0; 21 | 22 | -- ---------------------------- 23 | -- Table structure for medium-anti-correlated-2d-points-100000 24 | -- ---------------------------- 25 | DROP TABLE IF EXISTS `medium-anti-correlated-2d-points-100000`; 26 | CREATE TABLE `medium-anti-correlated-2d-points-100000` ( 27 | `id` int(11) NOT NULL AUTO_INCREMENT, 28 | `x` double(255,20) NOT NULL, 29 | `y` double(255,20) NOT NULL, 30 | PRIMARY KEY (`id`) 31 | ) ENGINE=InnoDB AUTO_INCREMENT=100000 DEFAULT CHARSET=utf8; 32 | 33 | -- ---------------------------- 34 | -- Table structure for medium-correlated-2d-points-100000 35 | -- ---------------------------- 36 | DROP TABLE IF EXISTS `medium-correlated-2d-points-100000`; 37 | CREATE TABLE `medium-correlated-2d-points-100000` ( 38 | `id` int(11) NOT NULL AUTO_INCREMENT, 39 | `x` double(255,20) NOT NULL, 40 | `y` double(255,20) NOT NULL, 41 | PRIMARY KEY (`id`) 42 | ) ENGINE=InnoDB AUTO_INCREMENT=100000 DEFAULT CHARSET=utf8; 43 | 44 | -- ---------------------------- 45 | -- Table structure for medium-uniformly-distributed-2d-points-100000 46 | -- ---------------------------- 47 | DROP TABLE IF EXISTS `medium-uniformly-distributed-2d-points-100000`; 48 | CREATE TABLE `medium-uniformly-distributed-2d-points-100000` ( 49 | `id` int(11) NOT NULL AUTO_INCREMENT, 50 | `x` double(255,20) NOT NULL, 51 | `y` double(255,20) NOT NULL, 52 | PRIMARY KEY (`id`) 53 | ) ENGINE=InnoDB AUTO_INCREMENT=100000 DEFAULT CHARSET=utf8; 54 | 55 | -- ---------------------------- 56 | -- Table structure for small-anti-correlated-2d-points-10000 57 | -- ---------------------------- 58 | DROP TABLE IF EXISTS `small-anti-correlated-2d-points-10000`; 59 | CREATE TABLE `small-anti-correlated-2d-points-10000` ( 60 | `id` int(11) NOT NULL AUTO_INCREMENT, 61 | `x` double(255,20) NOT NULL, 62 | `y` double(255,20) NOT NULL, 63 | PRIMARY KEY (`id`) 64 | ) ENGINE=InnoDB AUTO_INCREMENT=10000 DEFAULT CHARSET=utf8; 65 | 66 | -- ---------------------------- 67 | -- Table structure for small-correlated-2d-points-10000 68 | -- ---------------------------- 69 | DROP TABLE IF EXISTS `small-correlated-2d-points-10000`; 70 | CREATE TABLE `small-correlated-2d-points-10000` ( 71 | `id` int(11) NOT NULL AUTO_INCREMENT, 72 | `x` double(255,20) NOT NULL, 73 | `y` double(255,20) NOT NULL, 74 | PRIMARY KEY (`id`) 75 | ) ENGINE=InnoDB AUTO_INCREMENT=10000 DEFAULT CHARSET=utf8; 76 | 77 | -- ---------------------------- 78 | -- Table structure for small-uniformly-distributed-2d-points-10000 79 | -- ---------------------------- 80 | DROP TABLE IF EXISTS `small-uniformly-distributed-2d-points-10000`; 81 | CREATE TABLE `small-uniformly-distributed-2d-points-10000` ( 82 | `id` int(11) NOT NULL AUTO_INCREMENT, 83 | `x` double(255,20) NOT NULL, 84 | `y` double(255,20) NOT NULL, 85 | PRIMARY KEY (`id`) 86 | ) ENGINE=InnoDB AUTO_INCREMENT=10000 DEFAULT CHARSET=utf8; 87 | -------------------------------------------------------------------------------- /src/main/java/skylinequeries/tools/Utilities.java: -------------------------------------------------------------------------------- 1 | package skylinequeries.tools; 2 | 3 | import skylinequeries.rtree.Entry; 4 | import skylinequeries.rtree.RTree; 5 | import skylinequeries.rtree.geometry.Geometries; 6 | import skylinequeries.rtree.geometry.Point; 7 | 8 | import java.util.ArrayList; 9 | import java.util.List; 10 | 11 | /** 12 | * @author Vinh Nguyen 13 | */ 14 | public class Utilities implements Constants { 15 | private Utilities() { 16 | } 17 | 18 | /** 19 | * Gets points for RTree from a database table. 20 | * 21 | * @param rTree the Rtree to construct 22 | * @param tableName the database table name 23 | * @return the Rtree populated with points read from database table 24 | */ 25 | public static RTree constructRTree(RTree rTree, final String tableName) { 26 | List points = DB_QUERY.getAllRecords(tableName); 27 | for (Point point : points) { 28 | rTree = rTree.add(new Object(), point); 29 | } 30 | return rTree; 31 | } 32 | 33 | /** 34 | * Gets the list of points used to populate the Rtree form database table. 35 | * 36 | * @param tableName the database table name 37 | * @return the list containing points read from database table 38 | */ 39 | public static List constructRTree(final String tableName) { 40 | return Constants.DB_QUERY.getAllRecords(tableName); 41 | } 42 | 43 | /** 44 | * Converts a list of Rtree entries to a list of points. 45 | * 46 | * @param entries the list of entries to convert 47 | * @return the corresponding list of points 48 | */ 49 | public static List entriesToPoints(final List> entries) { 50 | List points = new ArrayList<>(); 51 | if (!entries.isEmpty()) { 52 | for (Entry entry : entries) { 53 | points.add(Geometries.point(entry.geometry().x(), entry.geometry().y())); 54 | } 55 | } 56 | 57 | return points; 58 | } 59 | 60 | /** 61 | * Prints algorithms experimental results to the console. 62 | * 63 | * @param skyline list of skyline points 64 | * @param size the size of the input data 65 | * @param executionTime algorithm execution time 66 | * @param accessedNodes the number of accessed nodes in RTree 67 | */ 68 | public static String console(final List skyline, final long size, 69 | final long executionTime, final long accessedNodes) { 70 | StringBuilder console = new StringBuilder(); 71 | Runtime runtime = Runtime.getRuntime(); 72 | 73 | console.append("\nAlgorithm performance\n") 74 | .append("\t+ Dataset = ").append(size).append(" points").append("\n") 75 | .append("\t+ Skyline = ").append(skyline.size()).append(" points").append("\n") 76 | .append("\t+ Execution time = ").append(executionTime).append(" ms").append("\n") 77 | .append("\t+ Accessed nodes = ").append(accessedNodes != 0 ? accessedNodes + " nodes" : "No data available") 78 | .append("\n").append("Memory information") 79 | .append("\n").append("\t+ Used Memory = ") 80 | .append((runtime.totalMemory() - runtime.freeMemory()) / 1000000).append(" MB").append("\n") 81 | .append("\t+ Free memory = ").append(runtime.freeMemory() / 1000000).append(" MB").append("\n") 82 | .append("\t+ Total memory = ").append(runtime.totalMemory() / 1000000).append(" MB").append("\n\n"); 83 | 84 | return console.toString(); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/main/java/skylinequeries/rtree/OnSubscribeSearch.java: -------------------------------------------------------------------------------- 1 | package skylinequeries.rtree; 2 | 3 | import com.google.common.annotations.VisibleForTesting; 4 | import rx.Observable.OnSubscribe; 5 | import rx.Producer; 6 | import rx.Subscriber; 7 | import rx.functions.Func1; 8 | import skylinequeries.rtree.geometry.Geometry; 9 | 10 | import java.util.concurrent.atomic.AtomicLong; 11 | 12 | final class OnSubscribeSearch implements OnSubscribe> { 13 | 14 | private final Node node; 15 | private final Func1 condition; 16 | 17 | OnSubscribeSearch(Node node, Func1 condition) { 18 | this.node = node; 19 | this.condition = condition; 20 | } 21 | 22 | @Override 23 | public void call(Subscriber> subscriber) { 24 | subscriber.setProducer(new SearchProducer(node, condition, subscriber)); 25 | } 26 | 27 | @VisibleForTesting 28 | static class SearchProducer implements Producer { 29 | 30 | private final Subscriber> subscriber; 31 | private final Node node; 32 | private final Func1 condition; 33 | private volatile ImmutableStack> stack; 34 | private final AtomicLong requested = new AtomicLong(0); 35 | 36 | SearchProducer(Node node, Func1 condition, 37 | Subscriber> subscriber) { 38 | this.node = node; 39 | this.condition = condition; 40 | this.subscriber = subscriber; 41 | stack = ImmutableStack.create(new NodePosition(node, 0)); 42 | } 43 | 44 | @Override 45 | public void request(long n) { 46 | try { 47 | if (requested.get() == Long.MAX_VALUE) 48 | // already started with fast path 49 | return; 50 | else if (n == Long.MAX_VALUE) { 51 | // fast path 52 | requestAll(); 53 | } 54 | else 55 | requestSome(n); 56 | } catch (RuntimeException e) { 57 | subscriber.onError(e); 58 | } 59 | } 60 | 61 | private void requestAll() { 62 | requested.set(Long.MAX_VALUE); 63 | node.search(condition, subscriber); 64 | if (!subscriber.isUnsubscribed()) 65 | subscriber.onCompleted(); 66 | } 67 | 68 | private void requestSome(long n) { 69 | // back pressure path 70 | // this algorithm copied roughly from 71 | // rxjava-core/OnSubscribeFromIterable.java 72 | 73 | // rxjava used AtomicLongFieldUpdater instead of AtomicLong 74 | // but benchmarks showed no benefit here so reverted to AtomicLong 75 | long previousCount = requested.getAndAdd(n); 76 | if (previousCount == 0) { 77 | while (true) { 78 | long r = requested.get(); 79 | long numToEmit = r; 80 | 81 | stack = Backpressure.search(condition, subscriber, stack, numToEmit); 82 | if (stack.isEmpty()) { 83 | if (!subscriber.isUnsubscribed()) 84 | subscriber.onCompleted(); 85 | else 86 | return; 87 | } 88 | else if (requested.addAndGet(-r) == 0) 89 | return; 90 | } 91 | } 92 | } 93 | } 94 | 95 | } 96 | -------------------------------------------------------------------------------- /src/main/java/skylinequeries/rtree/Leaf.java: -------------------------------------------------------------------------------- 1 | package skylinequeries.rtree; 2 | 3 | import com.google.common.base.Optional; 4 | import rx.Subscriber; 5 | import rx.functions.Func1; 6 | import skylinequeries.rtree.geometry.Geometry; 7 | import skylinequeries.rtree.geometry.ListPair; 8 | import skylinequeries.rtree.geometry.Rectangle; 9 | 10 | import java.util.ArrayList; 11 | import java.util.Collections; 12 | import java.util.List; 13 | 14 | import static com.google.common.base.Optional.of; 15 | 16 | public final class Leaf implements Node { 17 | 18 | private final List> entries; 19 | private final Rectangle mbr; 20 | private final Context context; 21 | 22 | Leaf(List> entries, Context context) { 23 | this.entries = entries; 24 | this.context = context; 25 | this.mbr = Util.mbr(entries); 26 | } 27 | 28 | @Override 29 | public Geometry geometry() { 30 | return mbr; 31 | } 32 | 33 | public List> entries() { 34 | return entries; 35 | } 36 | 37 | @Override 38 | public void search(Func1 condition, 39 | Subscriber> subscriber) { 40 | 41 | if (!condition.call(this.geometry().mbr())) 42 | return; 43 | 44 | for (final Entry entry : entries) { 45 | if (subscriber.isUnsubscribed()) 46 | return; 47 | else { 48 | if (condition.call(entry.geometry())) 49 | subscriber.onNext(entry); 50 | } 51 | } 52 | } 53 | 54 | @Override 55 | public int count() { 56 | return entries.size(); 57 | } 58 | 59 | @Override 60 | public List> add(Entry entry) { 61 | @SuppressWarnings("unchecked") 62 | final List> entries2 = Util.add(entries, (Entry) entry); 63 | if (entries2.size() <= context.maxChildren()) 64 | return Collections.singletonList((Node) new Leaf(entries2, context)); 65 | else { 66 | ListPair> pair = context.splitter().split(entries2, context.minChildren()); 67 | return makeLeaves(pair); 68 | } 69 | } 70 | 71 | private List> makeLeaves(ListPair> pair) { 72 | List> list = new ArrayList>(); 73 | list.add(new Leaf(pair.group1().list(), context)); 74 | list.add(new Leaf(pair.group2().list(), context)); 75 | return list; 76 | } 77 | 78 | @Override 79 | public NodeAndEntries delete(Entry entry, boolean all) { 80 | if (!entries.contains(entry)) { 81 | return new NodeAndEntries(of(this), Collections.> emptyList(), 0); 82 | } 83 | else { 84 | final List> entries2 = new ArrayList>(entries); 85 | entries2.remove(entry); 86 | int numDeleted = 1; 87 | // keep deleting if all specified 88 | while (all && entries2.remove(entry)) 89 | numDeleted += 1; 90 | 91 | if (entries2.size() >= context.minChildren()) { 92 | Leaf node = new Leaf(entries2, context); 93 | return new NodeAndEntries(of(node), Collections.> emptyList(), 94 | numDeleted); 95 | } 96 | else { 97 | return new NodeAndEntries(Optional.> absent(), entries2, 98 | numDeleted); 99 | } 100 | } 101 | } 102 | 103 | } 104 | -------------------------------------------------------------------------------- /src/main/java/skylinequeries/algs/bbs/BBS.java: -------------------------------------------------------------------------------- 1 | package skylinequeries.algs.bbs; 2 | 3 | import skylinequeries.rtree.*; 4 | import skylinequeries.rtree.geometry.Point; 5 | 6 | import java.util.ArrayList; 7 | import java.util.List; 8 | import java.util.PriorityQueue; 9 | 10 | /** 11 | * Branch and Bound skyline algorithm. 12 | * 13 | * @author Kien Hoang + Vinh Nguyen 14 | */ 15 | public class BBS { 16 | public static long NODE_ACCESSES = 0; 17 | private RTree rTree; 18 | 19 | public BBS(RTree rTree) { 20 | this.rTree = rTree; 21 | } 22 | 23 | public RTree getTree() { 24 | return rTree; 25 | } 26 | 27 | /** 28 | * Checks if a heap element is dominated by an entry. 29 | * 30 | * @param a the heap element 31 | * @param b the entry 32 | * @return true of a is dominated by b, false otherwise 33 | */ 34 | private boolean isDominated(BBSHeapElement a, Entry b) { 35 | if (a.isNode()) 36 | return (b.geometry().x() <= a.getNode().geometry().mbr().x1()) 37 | && (b.geometry().y() <= a.getNode().geometry().mbr().y1()); 38 | return (b.geometry().x() <= a.getEntry().geometry().mbr().x1()) 39 | && (b.geometry().y() <= a.getEntry().geometry().mbr().y1()); 40 | } 41 | 42 | /** 43 | * Checks if a heap element is dominated in a set of entries. 44 | * 45 | * @param a the heap element 46 | * @param entries the set of entries 47 | * @return true if a is dominated by some entry in entries, false otherwise 48 | */ 49 | private boolean isDominatedInSet(BBSHeapElement a, List> entries) { 50 | for (Entry entry : entries) 51 | if (isDominated(a, entry)) return true; 52 | return false; 53 | } 54 | 55 | /** 56 | * Executes the BBS (Branch and Bound Skyline) algorithm. 57 | * 58 | * @return the list of skyline entries 59 | */ 60 | public List> execute() { 61 | if (getTree().isEmpty()) return null; 62 | List> skylineEntries = new ArrayList<>(); 63 | PriorityQueue priorityQueue = new PriorityQueue<>(); 64 | priorityQueue.add(new BBSHeapElement(getTree().root().get(), true)); 65 | NODE_ACCESSES++; 66 | 67 | while (!priorityQueue.isEmpty()) { 68 | BBSHeapElement head = priorityQueue.poll(); 69 | if (!isDominatedInSet(head, skylineEntries)) { 70 | if (head.getNode() instanceof NonLeaf) { 71 | NonLeaf intermediateNode = (NonLeaf) head.getNode(); 72 | for (Node child : intermediateNode.children()) { 73 | NODE_ACCESSES++; 74 | BBSHeapElement element = new BBSHeapElement(child, true); 75 | if (!isDominatedInSet(element, skylineEntries)) { 76 | priorityQueue.add(element); 77 | } 78 | } 79 | } else if (head.getNode() instanceof Leaf) { 80 | Leaf intermediateNode = (Leaf) head.getNode(); 81 | for (Entry entry : intermediateNode.entries()) { 82 | NODE_ACCESSES++; 83 | BBSHeapElement element = new BBSHeapElement(entry, false); 84 | if (!isDominatedInSet(element, skylineEntries)) { 85 | priorityQueue.add(element); 86 | } 87 | } 88 | } else { 89 | skylineEntries.add(head.getEntry()); 90 | } 91 | } 92 | } 93 | 94 | return skylineEntries; 95 | } 96 | } -------------------------------------------------------------------------------- /src/main/java/skylinequeries/rtree/Comparators.java: -------------------------------------------------------------------------------- 1 | package skylinequeries.rtree; 2 | 3 | import rx.functions.Func1; 4 | import skylinequeries.rtree.geometry.Geometry; 5 | import skylinequeries.rtree.geometry.HasGeometry; 6 | import skylinequeries.rtree.geometry.ListPair; 7 | import skylinequeries.rtree.geometry.Rectangle; 8 | 9 | import java.util.Comparator; 10 | import java.util.List; 11 | 12 | /** 13 | * Utility functions asociated with {@link Comparator}s, especially for use with 14 | * {@link Selector}s and {@link Splitter}s. 15 | */ 16 | public final class Comparators { 17 | 18 | private Comparators() { 19 | // prevent instantiation 20 | } 21 | 22 | public static final Comparator> overlapListPairComparator = toComparator(Functions.overlapListPair); 23 | 24 | /** 25 | * Compares the sum of the areas of two ListPairs. 26 | */ 27 | public static final Comparator> areaPairComparator = new Comparator>() { 28 | 29 | @Override 30 | public int compare(ListPair p1, ListPair p2) { 31 | return ((Float) p1.areaSum()).compareTo(p2.areaSum()); 32 | } 33 | }; 34 | 35 | /** 36 | * Returns a {@link Comparator} that is a normal Double comparator for the 37 | * total of the areas of overlap of the members of the list with the 38 | * rectangle r. 39 | * 40 | * @param type of geometry being compared 41 | * @param r rectangle 42 | * @param list geometries to compare with the rectangle 43 | * @return the total of the areas of overlap of the geometries in the list 44 | * with the rectangle r 45 | */ 46 | public static Comparator overlapAreaComparator( 47 | final Rectangle r, final List list) { 48 | return toComparator(Functions.overlapArea(r, list)); 49 | } 50 | 51 | public static Comparator areaIncreaseComparator( 52 | final Rectangle r) { 53 | return toComparator(Functions.areaIncrease(r)); 54 | } 55 | 56 | public static Comparator areaComparator(final Rectangle r) { 57 | return new Comparator() { 58 | 59 | @Override 60 | public int compare(HasGeometry g1, HasGeometry g2) { 61 | return ((Float) g1.geometry().mbr().add(r).area()).compareTo(g2.geometry().mbr() 62 | .add(r).area()); 63 | } 64 | }; 65 | } 66 | 67 | public static > Comparator toComparator(final Func1 function) { 68 | return new Comparator() { 69 | 70 | @Override 71 | public int compare(R g1, R g2) { 72 | return function.call(g1).compareTo(function.call(g2)); 73 | } 74 | }; 75 | } 76 | 77 | public static Comparator compose(final Comparator... comparators) { 78 | return new Comparator() { 79 | @Override 80 | public int compare(T t1, T t2) { 81 | for (Comparator comparator : comparators) { 82 | int value = comparator.compare(t1, t2); 83 | if (value != 0) 84 | return value; 85 | } 86 | return 0; 87 | } 88 | }; 89 | } 90 | 91 | /** 92 | *

93 | * Returns a comparator that can be used to sort entries returned by search 94 | * methods. For example: 95 | *

96 | *

97 | * search(100).toSortedList(ascendingDistance(r)) 98 | *

99 | * 100 | * @param the value type 101 | * @param the entry type 102 | * @param r rectangle to measure distance to 103 | * @return a comparator to sort by ascending distance from the rectangle 104 | */ 105 | public static Comparator> ascendingDistance( 106 | final Rectangle r) { 107 | return new Comparator>() { 108 | @Override 109 | public int compare(Entry e1, Entry e2) { 110 | return ((Double) e1.geometry().distance(r)).compareTo(e2.geometry().distance(r)); 111 | } 112 | }; 113 | } 114 | 115 | } 116 | -------------------------------------------------------------------------------- /src/main/java/skylinequeries/rtree/geometry/Rectangle.java: -------------------------------------------------------------------------------- 1 | package skylinequeries.rtree.geometry; 2 | 3 | import com.google.common.base.Objects; 4 | import com.google.common.base.Optional; 5 | import com.google.common.base.Preconditions; 6 | import skylinequeries.rtree.ObjectsHelper; 7 | 8 | public final class Rectangle implements Geometry, HasGeometry { 9 | private final float x1, y1, x2, y2; 10 | 11 | protected Rectangle(float x1, float y1, float x2, float y2) { 12 | Preconditions.checkArgument(x2 >= x1); 13 | Preconditions.checkArgument(y2 >= y1); 14 | this.x1 = x1; 15 | this.y1 = y1; 16 | this.x2 = x2; 17 | this.y2 = y2; 18 | } 19 | 20 | public float x1() { 21 | return x1; 22 | } 23 | 24 | public float y1() { 25 | return y1; 26 | } 27 | 28 | public float x2() { 29 | return x2; 30 | } 31 | 32 | public float y2() { 33 | return y2; 34 | } 35 | 36 | public float area() { 37 | return (x2 - x1) * (y2 - y1); 38 | } 39 | 40 | public Rectangle add(Rectangle r) { 41 | return new Rectangle(Math.min(x1, r.x1), Math.min(y1, r.y1), Math.max(x2, r.x2), Math.max( 42 | y2, r.y2)); 43 | } 44 | 45 | public static Rectangle create(double x1, double y1, double x2, double y2) { 46 | return new Rectangle((float) x1, (float) y1, (float) x2, (float) y2); 47 | } 48 | 49 | public static Rectangle create(float x1, float y1, float x2, float y2) { 50 | return new Rectangle(x1, y1, x2, y2); 51 | } 52 | 53 | public boolean contains(double x, double y) { 54 | return x >= x1 && x <= x2 && y >= y1 && y <= y2; 55 | } 56 | 57 | public boolean strictlyContains(double x, double y) { 58 | return x > x1 && x < x2 && y > y1 && y < y2; 59 | } 60 | 61 | @Override 62 | public boolean intersects(Rectangle r) { 63 | float xMaxLeft = Math.max(x1(), r.x1()); 64 | float xMinRight = Math.min(x2(), r.x2()); 65 | if (xMinRight < xMaxLeft) 66 | return false; 67 | else { 68 | float yMaxBottom = Math.max(y1(), r.y1()); 69 | float yMinTop = Math.min(y2(), r.y2()); 70 | return yMinTop >= yMaxBottom; 71 | } 72 | } 73 | 74 | @Override 75 | public double distance(Rectangle r) { 76 | if (intersects(r)) 77 | return 0; 78 | else { 79 | Rectangle mostLeft = x1 < r.x1 ? this : r; 80 | Rectangle mostRight = x1 > r.x1 ? this : r; 81 | double xDifference = Math.max(0, mostLeft.x1 == mostRight.x1 ? 0 : mostRight.x1 82 | - mostLeft.x2); 83 | 84 | Rectangle upper = y1 < r.y1 ? this : r; 85 | Rectangle lower = y1 > r.y1 ? this : r; 86 | 87 | double yDifference = Math.max(0, upper.y1 == lower.y1 ? 0 : lower.y1 - upper.y2); 88 | 89 | return Math.sqrt(xDifference * xDifference + yDifference * yDifference); 90 | } 91 | } 92 | 93 | @Override 94 | public Rectangle mbr() { 95 | return this; 96 | } 97 | 98 | @Override 99 | public String toString() { 100 | return "Rectangle [x1=" + x1 + ", y1=" + y1 + ", x2=" + x2 + ", y2=" + y2 + "]"; 101 | } 102 | 103 | @Override 104 | public int hashCode() { 105 | return Objects.hashCode(x1, y1, x2, y2); 106 | } 107 | 108 | @Override 109 | public boolean equals(Object obj) { 110 | Optional other = ObjectsHelper.asClass(obj, Rectangle.class); 111 | if (other.isPresent()) { 112 | return Objects.equal(x1, other.get().x1) && Objects.equal(x2, other.get().x2) 113 | && Objects.equal(y1, other.get().y1) && Objects.equal(y2, other.get().y2); 114 | } 115 | else 116 | return false; 117 | } 118 | 119 | public float intersectionArea(Rectangle r) { 120 | if (!intersects(r)) 121 | return 0; 122 | else 123 | return create(Math.max(x1, r.x1), Math.max(y1, r.y1), Math.min(x2, r.x2), 124 | Math.min(y2, r.y2)).area(); 125 | } 126 | 127 | public float perimeter() { 128 | return 2 * (x2 - x1) + 2 * (y2 - y1); 129 | } 130 | 131 | @Override 132 | public Geometry geometry() { 133 | return this; 134 | } 135 | 136 | } -------------------------------------------------------------------------------- /src/main/java/skylinequeries/rtree/Backpressure.java: -------------------------------------------------------------------------------- 1 | package skylinequeries.rtree; 2 | 3 | import rx.Subscriber; 4 | import rx.functions.Func1; 5 | import skylinequeries.rtree.geometry.Geometry; 6 | 7 | /** 8 | * Utility methods for controlling backpressure of the tree search. 9 | */ 10 | final class Backpressure { 11 | 12 | private Backpressure() { 13 | // prevent instantiation 14 | } 15 | 16 | static ImmutableStack> search( 17 | final Func1 condition, 18 | final Subscriber> subscriber, 19 | final ImmutableStack> stack, final long request) { 20 | StackAndRequest> state = StackAndRequest.create(stack, request); 21 | return searchAndReturnStack(condition, subscriber, state); 22 | } 23 | 24 | private static ImmutableStack> searchAndReturnStack( 25 | final Func1 condition, 26 | final Subscriber> subscriber, StackAndRequest> state) { 27 | 28 | while (!state.stack.isEmpty()) { 29 | NodePosition np = state.stack.peek(); 30 | if (subscriber.isUnsubscribed()) 31 | return ImmutableStack.empty(); 32 | else if (state.request == 0) 33 | return state.stack; 34 | else if (np.position() == np.node().count()) { 35 | // handle after last in node 36 | state = StackAndRequest.create(searchAfterLastInNode(state.stack), state.request); 37 | } 38 | else if (np.node() instanceof NonLeaf) { 39 | // handle non-leaf 40 | state = StackAndRequest.create(searchNonLeaf(condition, state.stack, np), state.request); 41 | } 42 | else { 43 | // handle leaf 44 | state = searchLeaf(condition, subscriber, state, np); 45 | } 46 | } 47 | return state.stack; 48 | } 49 | 50 | private static class StackAndRequest { 51 | private final ImmutableStack stack; 52 | private final long request; 53 | 54 | StackAndRequest(ImmutableStack stack, long request) { 55 | this.stack = stack; 56 | this.request = request; 57 | } 58 | 59 | static StackAndRequest create(ImmutableStack stack, long request) { 60 | return new StackAndRequest(stack, request); 61 | } 62 | 63 | } 64 | 65 | private static StackAndRequest> searchLeaf( 66 | final Func1 condition, 67 | final Subscriber> subscriber, StackAndRequest> state, NodePosition np) { 68 | final long nextRequest; 69 | Entry entry = ((Leaf) np.node()).entries().get(np.position()); 70 | if (condition.call(entry.geometry())) { 71 | subscriber.onNext(entry); 72 | nextRequest = state.request - 1; 73 | } 74 | else 75 | nextRequest = state.request; 76 | return StackAndRequest.create(state.stack.pop().push(np.nextPosition()), nextRequest); 77 | } 78 | 79 | private static ImmutableStack> searchAfterLastInNode( 80 | ImmutableStack> stack) { 81 | ImmutableStack> stack2 = stack.pop(); 82 | if (stack2.isEmpty()) 83 | stack = stack2; 84 | else { 85 | NodePosition previous = stack2.peek(); 86 | stack = stack2.pop().push(previous.nextPosition()); 87 | } 88 | return stack; 89 | } 90 | 91 | private static ImmutableStack> searchNonLeaf( 92 | final Func1 condition, 93 | ImmutableStack> stack, NodePosition np) { 94 | Node child = ((NonLeaf) np.node()).children().get(np.position()); 95 | if (condition.call(child.geometry())) { 96 | stack = stack.push(new NodePosition(child, 0)); 97 | } 98 | else { 99 | stack = stack.pop().push(np.nextPosition()); 100 | } 101 | return stack; 102 | } 103 | 104 | } 105 | -------------------------------------------------------------------------------- /src/main/java/skylinequeries/rtree/SplitterRStar.java: -------------------------------------------------------------------------------- 1 | package skylinequeries.rtree; 2 | 3 | import com.google.common.base.Preconditions; 4 | import rx.functions.Func1; 5 | import skylinequeries.rtree.geometry.HasGeometry; 6 | import skylinequeries.rtree.geometry.ListPair; 7 | 8 | import java.util.*; 9 | 10 | public final class SplitterRStar implements Splitter { 11 | 12 | private final Comparator> comparator; 13 | 14 | @SuppressWarnings("unchecked") 15 | public SplitterRStar() { 16 | this.comparator = Comparators.compose(Comparators.overlapListPairComparator, 17 | Comparators.areaPairComparator); 18 | } 19 | 20 | @Override 21 | public ListPair split(List items, int minSize) { 22 | Preconditions.checkArgument(!items.isEmpty()); 23 | // sort nodes into increasing x, calculate min overlap where both groups 24 | // have more than minChildren 25 | 26 | Map>> map = new HashMap>>(5, 1.0f); 27 | map.put(SortType.X_LOWER, getPairs(minSize, sort(items, INCREASING_X_LOWER))); 28 | map.put(SortType.X_UPPER, getPairs(minSize, sort(items, INCREASING_X_UPPER))); 29 | map.put(SortType.Y_LOWER, getPairs(minSize, sort(items, INCREASING_Y_LOWER))); 30 | map.put(SortType.Y_UPPER, getPairs(minSize, sort(items, INCREASING_Y_UPPER))); 31 | 32 | // compute S the sum of all margin-values of the lists above 33 | // the list with the least S is then used to find minimum overlap 34 | 35 | SortType leastMarginSumSortType = Collections.min(sortTypes, marginSumComparator(map)); 36 | List> pairs = map.get(leastMarginSumSortType); 37 | 38 | return Collections.min(pairs, comparator); 39 | } 40 | 41 | private static enum SortType { 42 | X_LOWER, X_UPPER, Y_LOWER, Y_UPPER; 43 | } 44 | 45 | private static final List sortTypes = Collections.unmodifiableList(Arrays 46 | .asList(SortType.values())); 47 | 48 | private static Comparator marginSumComparator( 49 | final Map>> map) { 50 | return Comparators.toComparator(new Func1() { 51 | @Override 52 | public Double call(SortType sortType) { 53 | return (double) marginValueSum(map.get(sortType)); 54 | } 55 | }); 56 | } 57 | 58 | private static float marginValueSum(List> list) { 59 | float sum = 0; 60 | for (ListPair p : list) 61 | sum += p.marginSum(); 62 | return sum; 63 | } 64 | 65 | private static List> getPairs(int minSize, List list) { 66 | List> pairs = new ArrayList>(list.size() - 2 * minSize + 1); 67 | for (int i = minSize; i < list.size() - minSize; i++) { 68 | List list1 = list.subList(0, i); 69 | List list2 = list.subList(i, list.size()); 70 | ListPair pair = new ListPair(list1, list2); 71 | pairs.add(pair); 72 | } 73 | return pairs; 74 | } 75 | 76 | private static List sort(List items, 77 | Comparator comparator) { 78 | ArrayList list = new ArrayList(items); 79 | Collections.sort(list, comparator); 80 | return list; 81 | } 82 | 83 | private static Comparator INCREASING_X_LOWER = new Comparator() { 84 | 85 | @Override 86 | public int compare(HasGeometry n1, HasGeometry n2) { 87 | return ((Float) n1.geometry().mbr().x1()).compareTo(n2.geometry().mbr().x1()); 88 | } 89 | }; 90 | 91 | private static Comparator INCREASING_X_UPPER = new Comparator() { 92 | 93 | @Override 94 | public int compare(HasGeometry n1, HasGeometry n2) { 95 | return ((Float) n1.geometry().mbr().x2()).compareTo(n2.geometry().mbr().x2()); 96 | } 97 | }; 98 | 99 | private static Comparator INCREASING_Y_LOWER = new Comparator() { 100 | 101 | @Override 102 | public int compare(HasGeometry n1, HasGeometry n2) { 103 | return ((Float) n1.geometry().mbr().y1()).compareTo(n2.geometry().mbr().y1()); 104 | } 105 | }; 106 | 107 | private static Comparator INCREASING_Y_UPPER = new Comparator() { 108 | 109 | @Override 110 | public int compare(HasGeometry n1, HasGeometry n2) { 111 | return ((Float) n1.geometry().mbr().y2()).compareTo(n2.geometry().mbr().y2()); 112 | } 113 | }; 114 | 115 | } 116 | -------------------------------------------------------------------------------- /src/main/java/skylinequeries/rtree/SplitterQuadratic.java: -------------------------------------------------------------------------------- 1 | package skylinequeries.rtree; 2 | 3 | import com.google.common.annotations.VisibleForTesting; 4 | import com.google.common.base.Optional; 5 | import com.google.common.base.Preconditions; 6 | import com.google.common.collect.Lists; 7 | import skylinequeries.rtree.geometry.HasGeometry; 8 | import skylinequeries.rtree.geometry.ListPair; 9 | import skylinequeries.rtree.geometry.Rectangle; 10 | 11 | import java.util.ArrayList; 12 | import java.util.List; 13 | 14 | import static com.google.common.base.Optional.absent; 15 | import static com.google.common.base.Optional.of; 16 | 17 | public final class SplitterQuadratic implements Splitter { 18 | 19 | @SuppressWarnings("unchecked") 20 | @Override 21 | public ListPair split(List items, int minSize) { 22 | Preconditions.checkArgument(items.size() >= 2); 23 | 24 | // according to 25 | // http://en.wikipedia.org/wiki/R-tree#Splitting_an_overflowing_node 26 | 27 | // find the worst combination pairwise in the list and use them to start 28 | // the two groups 29 | final Pair worstCombination = worstCombination(items); 30 | 31 | // worst combination to have in the same node is now e1,e2. 32 | 33 | // establish a group around e1 and another group around e2 34 | final List group1 = Lists.newArrayList(worstCombination.value1()); 35 | final List group2 = Lists.newArrayList(worstCombination.value2()); 36 | 37 | final List remaining = new ArrayList(items); 38 | remaining.remove(worstCombination.value1()); 39 | remaining.remove(worstCombination.value2()); 40 | 41 | final int minGroupSize = items.size() / 2; 42 | 43 | // now add the remainder to the groups using least mbr area increase 44 | // except in the case where minimumSize would be contradicted 45 | while (remaining.size() > 0) { 46 | assignRemaining(group1, group2, remaining, minGroupSize); 47 | } 48 | return new ListPair(group1, group2); 49 | } 50 | 51 | private void assignRemaining(final List group1, 52 | final List group2, final List remaining, final int minGroupSize) { 53 | final Rectangle mbr1 = Util.mbr(group1); 54 | final Rectangle mbr2 = Util.mbr(group2); 55 | final T item1 = getBestCandidateForGroup(remaining, group1, mbr1); 56 | final T item2 = getBestCandidateForGroup(remaining, group2, mbr2); 57 | final boolean area1LessThanArea2 = item1.geometry().mbr().add(mbr1).area() <= item2 58 | .geometry().mbr().add(mbr2).area(); 59 | 60 | if (area1LessThanArea2 && (group2.size() + remaining.size() - 1 >= minGroupSize) 61 | || !area1LessThanArea2 && (group1.size() + remaining.size() == minGroupSize)) { 62 | group1.add(item1); 63 | remaining.remove(item1); 64 | } 65 | else { 66 | group2.add(item2); 67 | remaining.remove(item2); 68 | } 69 | } 70 | 71 | @VisibleForTesting 72 | static T getBestCandidateForGroup(List list, List group, 73 | Rectangle groupMbr) { 74 | Optional minEntry = absent(); 75 | Optional minArea = absent(); 76 | for (final T entry : list) { 77 | final double area = groupMbr.add(entry.geometry().mbr()).area(); 78 | if (!minArea.isPresent() || area < minArea.get()) { 79 | minArea = of(area); 80 | minEntry = of(entry); 81 | } 82 | } 83 | return minEntry.get(); 84 | } 85 | 86 | @VisibleForTesting 87 | static Pair worstCombination(List items) { 88 | Optional e1 = absent(); 89 | Optional e2 = absent(); 90 | { 91 | Optional maxArea = absent(); 92 | for (final T entry1 : items) { 93 | for (final T entry2 : items) { 94 | if (entry1 != entry2) { 95 | final double area = entry1.geometry().mbr().add(entry2.geometry().mbr()) 96 | .area(); 97 | if (!maxArea.isPresent() || area > maxArea.get()) { 98 | e1 = of(entry1); 99 | e2 = of(entry2); 100 | maxArea = of(area); 101 | } 102 | } 103 | } 104 | } 105 | } 106 | if (e1.isPresent()) 107 | return new Pair(e1.get(), e2.get()); 108 | else 109 | // all items are the same item 110 | return new Pair(items.get(0), items.get(1)); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/main/java/skylinequeries/rtree/Visualizer.java: -------------------------------------------------------------------------------- 1 | package skylinequeries.rtree; 2 | 3 | import com.google.common.base.Optional; 4 | import skylinequeries.rtree.geometry.Geometry; 5 | import skylinequeries.rtree.geometry.Rectangle; 6 | 7 | import java.awt.*; 8 | import java.awt.image.BufferedImage; 9 | import java.io.File; 10 | import java.util.ArrayList; 11 | import java.util.Collections; 12 | import java.util.Comparator; 13 | import java.util.List; 14 | 15 | public final class Visualizer { 16 | 17 | private final RTree tree; 18 | private final int width; 19 | private final int height; 20 | private final Rectangle view; 21 | private final int maxDepth; 22 | 23 | Visualizer(RTree tree, int width, int height, Rectangle view) { 24 | this.tree = tree; 25 | this.width = width; 26 | this.height = height; 27 | this.view = view; 28 | this.maxDepth = calculateMaxDepth(tree.root()); 29 | } 30 | 31 | private static int calculateMaxDepth(Optional> root) { 32 | if (!root.isPresent()) 33 | return 0; 34 | else 35 | return calculateDepth(root.get(), 0); 36 | } 37 | 38 | private static int calculateDepth(Node node, int depth) { 39 | if (node instanceof Leaf) 40 | return depth + 1; 41 | else 42 | return calculateDepth(((NonLeaf) node).children().get(0), depth + 1); 43 | } 44 | 45 | public BufferedImage createImage() { 46 | final BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); 47 | final Graphics2D g = (Graphics2D) image.getGraphics(); 48 | g.setBackground(Color.white); 49 | g.clearRect(0, 0, width, height); 50 | g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.75f)); 51 | 52 | if (tree.root().isPresent()) { 53 | final List nodeDepths = getNodeDepthsSortedByDepth(tree.root().get()); 54 | drawNode(g, nodeDepths); 55 | } 56 | return image; 57 | } 58 | 59 | private List getNodeDepthsSortedByDepth(Node root) { 60 | final List list = getRectangleDepths(root, 0); 61 | Collections.sort(list, new Comparator() { 62 | 63 | @Override 64 | public int compare(RectangleDepth n1, RectangleDepth n2) { 65 | return ((Integer) n1.getDepth()).compareTo(n2.getDepth()); 66 | } 67 | }); 68 | return list; 69 | } 70 | 71 | private List getRectangleDepths(Node node, 72 | int depth) { 73 | final List list = new ArrayList(); 74 | list.add(new RectangleDepth(node.geometry().mbr(), depth)); 75 | if (node instanceof Leaf) { 76 | final Leaf leaf = (Leaf) node; 77 | for (final Entry entry : leaf.entries()) { 78 | list.add(new RectangleDepth(entry.geometry().mbr(), depth + 2)); 79 | } 80 | } 81 | else { 82 | final NonLeaf n = (NonLeaf) node; 83 | for (final Node child : n.children()) { 84 | list.addAll(getRectangleDepths(child, depth + 1)); 85 | } 86 | } 87 | return list; 88 | } 89 | 90 | private void drawNode(Graphics2D g, List nodes) { 91 | for (final RectangleDepth node : nodes) { 92 | final Color color = Color.getHSBColor(node.getDepth() / (maxDepth + 1f), 1f, 1f); 93 | g.setStroke(new BasicStroke(Math.max(0.5f, maxDepth - node.getDepth() + 1 - 1))); 94 | g.setColor(color); 95 | final Rectangle r = node.getRectangle(); 96 | drawRectangle(g, r); 97 | } 98 | } 99 | 100 | private void drawRectangle(Graphics2D g, Rectangle r) { 101 | final double x1 = (r.x1() - view.x1()) / (view.x2() - view.x1()) * width; 102 | final double y1 = (r.y1() - view.y1()) / (view.y2() - view.y1()) * height; 103 | final double x2 = (r.x2() - view.x1()) / (view.x2() - view.x1()) * width; 104 | final double y2 = (r.y2() - view.y1()) / (view.y2() - view.y1()) * height; 105 | g.drawRect(rnd(x1), rnd(y1), rnd(x2 - x1), rnd(y2 - y1)); 106 | } 107 | 108 | private static int rnd(double d) { 109 | return (int) Math.round(d); 110 | } 111 | 112 | public void save(File file, String imageFormat) { 113 | ImageSaver.save(createImage(), file, imageFormat); 114 | } 115 | 116 | public void save(String filename, String imageFormat) { 117 | save(new File(filename), imageFormat); 118 | } 119 | 120 | public void save(String filename) { 121 | save(new File(filename), "PNG"); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/main/java/skylinequeries/demo/TestRunner.java: -------------------------------------------------------------------------------- 1 | package skylinequeries.demo; 2 | 3 | import javax.swing.*; 4 | import javax.swing.border.EmptyBorder; 5 | import javax.swing.border.TitledBorder; 6 | import java.awt.*; 7 | import java.awt.event.ActionEvent; 8 | import java.awt.event.ActionListener; 9 | 10 | /** 11 | * A simple GUI application to test algorithms. 12 | * 13 | * @author Vinh Nguyen 14 | */ 15 | public class TestRunner extends JFrame { 16 | private final JComboBox tables = new JComboBox(new ComboBox()); 17 | private final JTextArea viewer = new JTextArea(); 18 | private final JTextArea logger = new JTextArea(); 19 | private String selected, selection; 20 | 21 | public TestRunner() { 22 | super("Skyline Query Algorithms"); 23 | setLayout(new FlowLayout()); 24 | 25 | final Font font = new Font("Arial", 0, 15); 26 | final ButtonListener buttonListener = new ButtonListener(); 27 | 28 | JPanel panel1 = new JPanel(); 29 | panel1.setBorder(new TitledBorder(new EmptyBorder(0, 0, 0, 0), "DATASET")); 30 | tables.addActionListener(new ActionListener() { 31 | @Override 32 | public void actionPerformed(ActionEvent e) { 33 | if (tables.getSelectedIndex() != -1) { 34 | selected = (String) tables.getSelectedItem(); 35 | } 36 | } 37 | }); 38 | tables.setSelectedIndex(0); 39 | panel1.add(tables); 40 | 41 | JPanel panel2 = new JPanel(); 42 | panel2.setBorder(new TitledBorder(new EmptyBorder(0, 0, 0, 0), "ALGORITHMS")); 43 | final JButton bbsButton = new JButton("BBS"); 44 | final JButton nnButton = new JButton("NN"); 45 | bbsButton.addActionListener(buttonListener); 46 | nnButton.addActionListener(buttonListener); 47 | panel2.add(bbsButton); 48 | panel2.add(nnButton); 49 | 50 | JPanel panel3 = new JPanel(); 51 | panel3.setBorder(new TitledBorder(new EmptyBorder(0, 0, 0, 0), "FOUND SKYLINE ENTRIES")); 52 | viewer.setColumns(30); 53 | viewer.setRows(10); 54 | viewer.setEditable(false); 55 | viewer.setFont(font); 56 | viewer.setBorder(new EmptyBorder(0, 25, 0, 25)); 57 | panel3.add(new JScrollPane(viewer)); 58 | 59 | JPanel panel4 = new JPanel(); 60 | panel4.setBorder(new TitledBorder(new EmptyBorder(0, 0, 0, 0), "LOGS")); 61 | logger.setColumns(30); 62 | logger.setRows(15); 63 | logger.setEditable(false); 64 | logger.setFont(font); 65 | logger.setBorder(new EmptyBorder(10, 25, 10, 25)); 66 | logger.append("... Choose a dataset and an algorithm" + "\n"); 67 | panel4.add(new JScrollPane(logger)); 68 | 69 | add(panel1); 70 | add(panel2); 71 | add(panel3); 72 | add(panel4); 73 | setSize(new Dimension(500, 650)); 74 | setResizable(false); 75 | setVisible(true); 76 | } 77 | 78 | public static void main(String[] args) { 79 | final TestRunner testSuite = new TestRunner(); 80 | testSuite.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); 81 | } 82 | 83 | private class ButtonListener implements ActionListener { 84 | @Override 85 | public void actionPerformed(ActionEvent e) { 86 | final String command = e.getActionCommand(); 87 | if (selected.equals("")) { 88 | selected = (String) tables.getSelectedItem(); 89 | } 90 | 91 | switch (command) { 92 | case "BBS": 93 | logger.append("... Executing Branch and Bound Skyline (BBS) algorithm\n"); 94 | logger.append("... Dataset: " + selected + "\n"); 95 | viewer.setText(""); 96 | final BBSWorker bbs = new BBSWorker(selected, viewer, logger); 97 | bbs.execute(); 98 | break; 99 | case "NN": 100 | logger.append("... Executing Nearest Neighbor (NN) algorithm\n"); 101 | logger.append("... Dataset: " + selected + "\n"); 102 | viewer.setText(""); 103 | final NNWorker nn = new NNWorker(selected, viewer, logger); 104 | nn.execute(); 105 | break; 106 | } 107 | } 108 | } 109 | 110 | private class ComboBox extends AbstractListModel implements ComboBoxModel { 111 | private final String tables[] = { 112 | "medium-anti-correlated-2d-points-100000", 113 | "medium-correlated-2d-points-100000", 114 | "medium-uniformly-distributed-2d-points-100000", 115 | "small-anti-correlated-2d-points-10000", 116 | "small-correlated-2d-points-10000", 117 | "small-uniformly-distributed-2d-points-10000" 118 | }; 119 | 120 | @Override 121 | public void setSelectedItem(Object anItem) { 122 | selection = (String) anItem; 123 | } 124 | 125 | @Override 126 | public Object getSelectedItem() { 127 | return selection; 128 | } 129 | 130 | @Override 131 | public int getSize() { 132 | return tables.length; 133 | } 134 | 135 | @Override 136 | public Object getElementAt(int index) { 137 | return tables[index]; 138 | } 139 | } 140 | } -------------------------------------------------------------------------------- /src/main/java/skylinequeries/rtree/NonLeaf.java: -------------------------------------------------------------------------------- 1 | package skylinequeries.rtree; 2 | 3 | import com.google.common.base.Optional; 4 | import com.google.common.base.Preconditions; 5 | import rx.Subscriber; 6 | import rx.functions.Func1; 7 | import skylinequeries.rtree.geometry.Geometry; 8 | import skylinequeries.rtree.geometry.ListPair; 9 | import skylinequeries.rtree.geometry.Rectangle; 10 | 11 | import java.util.ArrayList; 12 | import java.util.Collections; 13 | import java.util.List; 14 | 15 | import static com.google.common.base.Optional.of; 16 | 17 | public final class NonLeaf implements Node { 18 | 19 | private final List> children; 20 | private final Rectangle mbr; 21 | private final Context context; 22 | 23 | NonLeaf(List> children, Context context) { 24 | Preconditions.checkArgument(!children.isEmpty()); 25 | this.context = context; 26 | this.children = children; 27 | this.mbr = Util.mbr(children); 28 | } 29 | 30 | @Override 31 | public Geometry geometry() { 32 | return mbr; 33 | } 34 | 35 | @Override 36 | public void search(Func1 criterion, 37 | Subscriber> subscriber) { 38 | 39 | if (!criterion.call(this.geometry().mbr())) 40 | return; 41 | 42 | for (final Node child : children) { 43 | if (subscriber.isUnsubscribed()) 44 | return; 45 | else 46 | child.search(criterion, subscriber); 47 | } 48 | } 49 | 50 | @Override 51 | public int count() { 52 | return children.size(); 53 | } 54 | 55 | public List> children() { 56 | return children; 57 | } 58 | 59 | @Override 60 | public List> add(Entry entry) { 61 | final Node child = context.selector().select(entry.geometry().mbr(), children); 62 | List> list = child.add(entry); 63 | List> children2 = Util.replace(children, child, list); 64 | if (children2.size() <= context.maxChildren()) 65 | return Collections.singletonList((Node) new NonLeaf(children2, context)); 66 | else { 67 | ListPair> pair = context.splitter().split(children2, 68 | context.minChildren()); 69 | return makeNonLeaves(pair); 70 | } 71 | } 72 | 73 | private List> makeNonLeaves(ListPair> pair) { 74 | List> list = new ArrayList>(); 75 | list.add(new NonLeaf(pair.group1().list(), context)); 76 | list.add(new NonLeaf(pair.group2().list(), context)); 77 | return list; 78 | } 79 | 80 | @Override 81 | public NodeAndEntries delete(Entry entry, boolean all) { 82 | // the result of performing a delete of the given entry from this node 83 | // will be that zero or more entries will be needed to be added back to 84 | // the root of the tree (because num entries of their node fell below 85 | // minChildren), 86 | // zero or more children will need to be removed from this node, 87 | // zero or more nodes to be added as children to this node(because 88 | // entries have been deleted from them and they still have enough 89 | // members to be active) 90 | List> addTheseEntries = new ArrayList>(); 91 | List> removeTheseNodes = new ArrayList>(); 92 | List> addTheseNodes = new ArrayList>(); 93 | int countDeleted = 0; 94 | 95 | for (final Node child : children) { 96 | if (entry.geometry().intersects(child.geometry().mbr())) { 97 | final NodeAndEntries result = child.delete(entry, all); 98 | if (result.node().isPresent()) { 99 | if (result.node().get() != child) { 100 | // deletion occurred and child is above minChildren so 101 | // we update it 102 | addTheseNodes.add(result.node().get()); 103 | removeTheseNodes.add(child); 104 | addTheseEntries.addAll(result.entriesToAdd()); 105 | countDeleted += result.countDeleted(); 106 | if (!all) 107 | break; 108 | } 109 | // else nothing was deleted from that child 110 | } 111 | else { 112 | // deletion occurred and brought child below minChildren 113 | // so we redistribute its entries 114 | removeTheseNodes.add(child); 115 | addTheseEntries.addAll(result.entriesToAdd()); 116 | countDeleted += result.countDeleted(); 117 | if (!all) 118 | break; 119 | } 120 | } 121 | } 122 | if (removeTheseNodes.isEmpty()) 123 | return new NodeAndEntries(of(this), Collections.> emptyList(), 0); 124 | else { 125 | List> nodes = Util.remove(children, removeTheseNodes); 126 | nodes.addAll(addTheseNodes); 127 | if (nodes.size() == 0) 128 | return new NodeAndEntries(Optional.> absent(), addTheseEntries, 129 | countDeleted); 130 | else { 131 | NonLeaf node = new NonLeaf(nodes, context); 132 | return new NodeAndEntries(of(node), addTheseEntries, countDeleted); 133 | } 134 | } 135 | } 136 | } -------------------------------------------------------------------------------- /src/main/java/skylinequeries/rtree/RTree.java: -------------------------------------------------------------------------------- 1 | package skylinequeries.rtree; 2 | 3 | import com.google.common.annotations.VisibleForTesting; 4 | import com.google.common.base.Optional; 5 | import com.google.common.collect.Lists; 6 | import rx.Observable; 7 | import rx.functions.Func1; 8 | import rx.functions.Func2; 9 | import skylinequeries.rtree.geometry.Geometry; 10 | import skylinequeries.rtree.geometry.Point; 11 | import skylinequeries.rtree.geometry.Rectangle; 12 | 13 | import java.util.List; 14 | 15 | import static com.google.common.base.Optional.absent; 16 | import static com.google.common.base.Optional.of; 17 | import static skylinequeries.rtree.geometry.Geometries.rectangle; 18 | 19 | /** 20 | * Immutable in-memory 2D R-Tree with configurable splitter heuristic. 21 | * 22 | * @param the entry value type 23 | * @param the entry geometry type 24 | */ 25 | public final class RTree { 26 | 27 | private final Optional> root; 28 | private final Context context; 29 | 30 | /** 31 | * Benchmarks show that this is a good choice for up to O(10,000) entries 32 | * when using Quadratic splitter (Guttman). 33 | */ 34 | public static final int MAX_CHILDREN_DEFAULT_GUTTMAN = 4; 35 | 36 | /** 37 | * Benchmarks show that this is the sweet spot for up to O(10,000) entries 38 | * when using R*-tree heuristics. 39 | */ 40 | public static final int MAX_CHILDREN_DEFAULT_STAR = 4; 41 | 42 | /** 43 | * Current size in Entries of the RTree. 44 | */ 45 | private int size; 46 | 47 | /** 48 | * Constructor. 49 | * 50 | * @param root the root node of the tree if present 51 | * @param context options for the R-tree 52 | */ 53 | private RTree(Optional> root, int size, Context context) { 54 | this.root = root; 55 | this.size = size; 56 | this.context = context; 57 | } 58 | 59 | /** 60 | * Constructor. 61 | * 62 | * @param root the root node of the R-tree 63 | * @param context options for the R-tree 64 | */ 65 | private RTree(Node root, int size, Context context) { 66 | this(of(root), size, context); 67 | } 68 | 69 | /** 70 | * Constructor. 71 | * 72 | * @param context specifies parameters and behaviour for the R-tree 73 | */ 74 | private RTree(Context context) { 75 | this(Optional.> absent(), 0, context); 76 | } 77 | 78 | /** 79 | * Returns a new Builder instance for {@link RTree}. Defaults to 80 | * maxChildren=128, minChildren=64, splitter=QuadraticSplitter. 81 | * 82 | * @param the value type of the entries in the tree 83 | * @param the geometry type of the entries in the tree 84 | * @return a new RTree instance 85 | */ 86 | public static RTree create() { 87 | return new Builder().create(); 88 | } 89 | 90 | /** 91 | * The tree is scanned for depth and the depth returned. This involves 92 | * recursing down to the leaf level of the tree to get the current depth. 93 | * Should be log(n) in complexity. 94 | * 95 | * @return depth of the R-tree 96 | */ 97 | public int calculateDepth() { 98 | return calculateDepth(root); 99 | } 100 | 101 | private static int calculateDepth(Optional> root) { 102 | if (!root.isPresent()) 103 | return 0; 104 | else 105 | return calculateDepth(root.get(), 0); 106 | } 107 | 108 | private static int calculateDepth(Node node, int depth) { 109 | if (node instanceof Leaf) 110 | return depth + 1; 111 | else 112 | return calculateDepth(((NonLeaf) node).children().get(0), depth + 1); 113 | } 114 | 115 | /** 116 | * When the number of children in an R-tree node drops below this number the 117 | * node is deleted and the children are added on to the R-tree again. 118 | * 119 | * @param minChildren less than this number of children in a node triggers a node 120 | * deletion and redistribution of its members 121 | * @return builder 122 | */ 123 | public static Builder minChildren(int minChildren) { 124 | return new Builder().minChildren(minChildren); 125 | } 126 | 127 | /** 128 | * Sets the max number of children in an R-tree node. 129 | * 130 | * @param maxChildren max number of children in an R-tree node 131 | * @return builder 132 | */ 133 | public static Builder maxChildren(int maxChildren) { 134 | return new Builder().maxChildren(maxChildren); 135 | } 136 | 137 | /** 138 | * Sets the {@link Splitter} to use when maxChildren is reached. 139 | * 140 | * @param splitter the splitter algorithm to use 141 | * @return builder 142 | */ 143 | public static Builder splitter(Splitter splitter) { 144 | return new Builder().splitter(splitter); 145 | } 146 | 147 | /** 148 | * Sets the node {@link Selector} which decides which branches to follow 149 | * when inserting or searching. 150 | * 151 | * @param selector determines which branches to follow when inserting or 152 | * searching 153 | * @return builder 154 | */ 155 | public static Builder selector(Selector selector) { 156 | return new Builder().selector(selector); 157 | } 158 | 159 | /** 160 | * Sets the splitter to {@link SplitterRStar} and selector to 161 | * {@link SelectorRStar} and defaults to minChildren=10. 162 | * 163 | * @return builder 164 | */ 165 | public static Builder star() { 166 | return new Builder().star(); 167 | } 168 | 169 | /** 170 | * RTree Builder. 171 | */ 172 | public static class Builder { 173 | 174 | /** 175 | * According to 176 | * http://dbs.mathematik.uni-marburg.de/publications/myPapers 177 | * /1990/BKSS90.pdf (R*-tree paper), best filling ratio is 0.4 for both 178 | * quadratic split and R*-tree split. 179 | */ 180 | private static final double DEFAULT_FILLING_FACTOR = 0.4; 181 | private Optional maxChildren = absent(); 182 | private Optional minChildren = absent(); 183 | private Splitter splitter = new SplitterQuadratic(); 184 | private Selector selector = new SelectorMinimalAreaIncrease(); 185 | private boolean star = false; 186 | 187 | private Builder() { 188 | } 189 | 190 | /** 191 | * When the number of children in an R-tree node drops below this number 192 | * the node is deleted and the children are added on to the R-tree 193 | * again. 194 | * 195 | * @param minChildren less than this number of children in a node triggers a 196 | * redistribution of its children. 197 | * @return builder 198 | */ 199 | public Builder minChildren(int minChildren) { 200 | this.minChildren = of(minChildren); 201 | return this; 202 | } 203 | 204 | /** 205 | * Sets the max number of children in an R-tree node. 206 | * 207 | * @param maxChildren max number of children in R-tree node. 208 | * @return builder 209 | */ 210 | public Builder maxChildren(int maxChildren) { 211 | this.maxChildren = of(maxChildren); 212 | return this; 213 | } 214 | 215 | /** 216 | * Sets the {@link Splitter} to use when maxChildren is reached. 217 | * 218 | * @param splitter node splitting method to use 219 | * @return builder 220 | */ 221 | public Builder splitter(Splitter splitter) { 222 | this.splitter = splitter; 223 | return this; 224 | } 225 | 226 | /** 227 | * Sets the node {@link Selector} which decides which branches to follow 228 | * when inserting or searching. 229 | * 230 | * @param selector selects the branch to follow when inserting or searching 231 | * @return builder 232 | */ 233 | public Builder selector(Selector selector) { 234 | this.selector = selector; 235 | return this; 236 | } 237 | 238 | /** 239 | * Sets the splitter to {@link SplitterRStar} and selector to 240 | * {@link SelectorRStar} and defaults to minChildren=10. 241 | * 242 | * @return builder 243 | */ 244 | public Builder star() { 245 | selector = new SelectorRStar(); 246 | splitter = new SplitterRStar(); 247 | star = true; 248 | return this; 249 | } 250 | 251 | /** 252 | * Builds the {@link RTree}. 253 | * 254 | * @param value type 255 | * @param geometry type 256 | * @return RTree 257 | */ 258 | public RTree create() { 259 | if (!maxChildren.isPresent()) 260 | if (star) 261 | maxChildren = of(MAX_CHILDREN_DEFAULT_STAR); 262 | else 263 | maxChildren = of(MAX_CHILDREN_DEFAULT_GUTTMAN); 264 | if (!minChildren.isPresent()) 265 | minChildren = of((int) Math.round(maxChildren.get() * DEFAULT_FILLING_FACTOR)); 266 | return new RTree(new Context(minChildren.get(), maxChildren.get(), selector, 267 | splitter)); 268 | } 269 | 270 | } 271 | 272 | /** 273 | * Returns an immutable copy of the RTree with the addition of given entry. 274 | * 275 | * @param entry item to add to the R-tree. 276 | * @return a new immutable R-tree including the new entry 277 | */ 278 | @SuppressWarnings("unchecked") 279 | public RTree add(Entry entry) { 280 | if (root.isPresent()) { 281 | List> nodes = root.get().add(entry); 282 | Node node; 283 | if (nodes.size() == 1) 284 | node = nodes.get(0); 285 | else { 286 | node = new NonLeaf(nodes, context); 287 | } 288 | return new RTree(node, size + 1, context); 289 | } 290 | else 291 | return new RTree( 292 | new Leaf(Lists.newArrayList((Entry) entry), context), size + 1, 293 | context); 294 | } 295 | 296 | /** 297 | * Returns an immutable copy of the RTree with the addition of an entry 298 | * comprised of the given value and Geometry. 299 | * 300 | * @param value the value of the {@link Entry} to be added 301 | * @param geometry the geometry of the {@link Entry} to be added 302 | * @return a new immutable R-tree including the new entry 303 | */ 304 | public RTree add(T value, S geometry) { 305 | return add(Entry.entry(value, geometry)); 306 | } 307 | 308 | /** 309 | * Returns an immutable RTree with the current entries and the additional 310 | * entries supplied as a parameter. 311 | * 312 | * @param entries entries to add 313 | * @return R-tree with entries added 314 | */ 315 | public RTree add(Iterable> entries) { 316 | RTree tree = this; 317 | for (Entry entry : entries) 318 | tree = tree.add(entry); 319 | return tree; 320 | } 321 | 322 | /** 323 | * Returns the Observable sequence of trees created by progressively adding 324 | * entries. 325 | * 326 | * @param entries the entries to add 327 | * @return a sequence of trees 328 | */ 329 | public Observable> add(Observable> entries) { 330 | return entries.scan(this, new Func2, Entry, RTree>() { 331 | 332 | @Override 333 | public RTree call(RTree tree, Entry entry) { 334 | return tree.add(entry); 335 | } 336 | }); 337 | } 338 | 339 | /** 340 | * Returns the Observable sequence of trees created by progressively 341 | * deleting entries. 342 | * 343 | * @param entries the entries to add 344 | * @param all if true delete all matching otherwise just first matching 345 | * @return a sequence of trees 346 | */ 347 | public Observable> delete(Observable> entries, final boolean all) { 348 | return entries.scan(this, new Func2, Entry, RTree>() { 349 | 350 | @Override 351 | public RTree call(RTree tree, Entry entry) { 352 | return tree.delete(entry, all); 353 | } 354 | }); 355 | } 356 | 357 | /** 358 | * Returns a new R-tree with the given entries deleted. If all 359 | * is false deletes only one if exists. If all is true deletes 360 | * all matching entries. 361 | * 362 | * @param entries entries to delete 363 | * @param all if false deletes one if exists else deletes all 364 | * @return R-tree with entries deleted 365 | */ 366 | public RTree delete(Iterable> entries, boolean all) { 367 | RTree tree = this; 368 | for (Entry entry : entries) 369 | tree = tree.delete(entry, all); 370 | return tree; 371 | } 372 | 373 | /** 374 | * Returns a new R-tree with the given entries deleted but only one 375 | * matching occurence of each entry is deleted. 376 | * 377 | * @param entries entries to delete 378 | * @return R-tree with entries deleted up to one matching occurence 379 | * per entry 380 | */ 381 | public RTree delete(Iterable> entries) { 382 | RTree tree = this; 383 | for (Entry entry : entries) 384 | tree = tree.delete(entry); 385 | return tree; 386 | } 387 | 388 | /** 389 | * If all is false deletes one entry matching the given value 390 | * and Geometry. If all is true deletes all entries matching 391 | * the given value and geometry. This method has no effect if the entry is 392 | * not present. The entry must match on both value and geometry to be 393 | * deleted. 394 | * 395 | * @param value the value of the {@link Entry} to be deleted 396 | * @param geometry the geometry of the {@link Entry} to be deleted 397 | * @param all if false deletes one if exists else deletes all 398 | * @return a new immutable R-tree without one or many instances of the 399 | * specified entry if it exists otherwise returns the original RTree 400 | * object 401 | */ 402 | public RTree delete(T value, S geometry, boolean all) { 403 | return delete(Entry.entry(value, geometry), all); 404 | } 405 | 406 | /** 407 | * Deletes maximum one entry matching the given value and geometry. This 408 | * method has no effect if the entry is not present. The entry must match on 409 | * both value and geometry to be deleted. 410 | * 411 | * @param value 412 | * @param geometry 413 | * @return an immutable RTree without one entry (if found) matching the 414 | * given value and geometry 415 | */ 416 | public RTree delete(T value, S geometry) { 417 | return delete(Entry.entry(value, geometry), false); 418 | } 419 | 420 | /** 421 | * Deletes one or all matching entries depending on the value of 422 | * all. If multiple copies of the entry are in the R-tree only 423 | * one will be deleted if all is false otherwise all matching entries will 424 | * be deleted. The entry must match on both value and geometry to be 425 | * deleted. 426 | * 427 | * @param entry the {@link Entry} to be deleted 428 | * @param all if true deletes all matches otherwise deletes first found 429 | * @return a new immutable R-tree without one instance of the specified 430 | * entry 431 | */ 432 | public RTree delete(Entry entry, boolean all) { 433 | if (root.isPresent()) { 434 | NodeAndEntries nodeAndEntries = root.get().delete(entry, all); 435 | if (nodeAndEntries.node().isPresent() && nodeAndEntries.node().get() == root.get()) 436 | return this; 437 | else 438 | return new RTree(nodeAndEntries.node(), size - nodeAndEntries.countDeleted() 439 | - nodeAndEntries.entriesToAdd().size(), context).add(nodeAndEntries 440 | .entriesToAdd()); 441 | } 442 | else 443 | return this; 444 | } 445 | 446 | /** 447 | * Deletes one entry if it exists, returning an immutable copy of the RTree 448 | * without that entry. If multiple copies of the entry are in the R-tree 449 | * only one will be deleted. The entry must match on both value and geometry 450 | * to be deleted. 451 | * 452 | * @param entry the {@link Entry} to be deleted 453 | * @return a new immutable R-tree without one instance of the specified 454 | * entry 455 | */ 456 | public RTree delete(Entry entry) { 457 | return delete(entry, false); 458 | } 459 | 460 | /** 461 | *

462 | * Returns an Observable sequence of {@link Entry} that satisfy the given 463 | * condition. Note that this method is well-behaved only if: 464 | *

465 | *

466 | * condition(g) is true for {@link Geometry} g implies condition(r) is true for the minimum bounding rectangles of the ancestor nodes 467 | *

468 | *

469 | * distance(g) < sD is an example of such a condition. 470 | *

471 | * 472 | * @param condition return Entries whose geometry satisfies the given condition 473 | * @return sequence of matching entries 474 | */ 475 | @VisibleForTesting 476 | public Observable> search(Func1 condition) { 477 | if (root.isPresent()) 478 | return Observable.create(new OnSubscribeSearch(root.get(), condition)); 479 | else 480 | return Observable.empty(); 481 | } 482 | 483 | /** 484 | * Returns a predicate function that indicates if {@link Geometry} 485 | * intersects with a given rectangle. 486 | * 487 | * @param r the rectangle to check intersection with 488 | * @return whether the geometry and the rectangle intersect 489 | */ 490 | public static Func1 intersects(final Rectangle r) { 491 | return new Func1() { 492 | @Override 493 | public Boolean call(Geometry g) { 494 | return g.intersects(r); 495 | } 496 | }; 497 | } 498 | 499 | /** 500 | * Returns a predicate function that indicates if {@link Geometry} 501 | * is inside a given rectangle. 502 | * 503 | * @param r the rectangle to check 504 | * @return whether the geometry is inside the rectangle 505 | * @author extended by Vinh Nguyen 506 | */ 507 | public static Func1 inside(final Rectangle r) { 508 | return new Func1() { 509 | @Override 510 | public Boolean call(Geometry g) { 511 | return r.strictlyContains(g.mbr().x1(), g.mbr().y1()); 512 | } 513 | }; 514 | } 515 | 516 | /** 517 | * Returns the always true predicate. See {@link RTree#entries()} for 518 | * example use. 519 | */ 520 | private static final Func1 ALWAYS_TRUE = new Func1() { 521 | @Override 522 | public Boolean call(Geometry rectangle) { 523 | return true; 524 | } 525 | }; 526 | 527 | /** 528 | * Returns an {@link Observable} sequence of all {@link Entry}s in the 529 | * R-tree whose minimum bounding rectangle intersects with the given 530 | * rectangle. 531 | * 532 | * @param r rectangle to check intersection with the entry mbr 533 | * @return entries that intersect with the rectangle r 534 | */ 535 | public Observable> search(final Rectangle r) { 536 | return search(intersects(r)); 537 | } 538 | 539 | /** 540 | * Returns an {@link Observable} sequence of all {@link Entry}s in the 541 | * R-tree which are contained in the given rectangle. 542 | * 543 | * @param r rectangle to check 544 | * @return entries that are contained in the rectangle r 545 | * @author extended by Vinh Nguyen 546 | */ 547 | public Observable> searchEntriesInRectangle(final Rectangle r) { 548 | return search(inside(r)); 549 | } 550 | 551 | /** 552 | * Returns an {@link Observable} sequence of all {@link Entry}s in the 553 | * R-tree whose minimum bounding rectangle intersects with the given point. 554 | * 555 | * @param p point to check intersection with the entry mbr 556 | * @return entries that intersect with the point p 557 | */ 558 | public Observable> search(final Point p) { 559 | return search(p.mbr()); 560 | } 561 | 562 | /** 563 | * Returns an {@link Observable} sequence of all {@link Entry}s in the 564 | * R-tree whose minimum bounding rectangles are strictly less than 565 | * maxDistance from the given rectangle. 566 | * 567 | * @param r rectangle to measure distance from 568 | * @param maxDistance entries returned must be within this distance from rectangle r 569 | * @return the sequence of matching entries 570 | */ 571 | public Observable> search(final Rectangle r, final double maxDistance) { 572 | return search(new Func1() { 573 | @Override 574 | public Boolean call(Geometry g) { 575 | return g.distance(r) < maxDistance; 576 | } 577 | }); 578 | } 579 | 580 | /** 581 | * Returns the intersections with the the given (arbitrary) geometry using 582 | * an intersection function to filter the search results returned from a 583 | * search of the mbr of g. 584 | * 585 | * @param type of geometry being searched for intersection with 586 | * @param g geometry being searched for intersection with 587 | * @param intersects function to determine if the two geometries intersect 588 | * @return a sequence of entries that intersect with g 589 | */ 590 | public Observable> search(final R g, 591 | final Func2 intersects) { 592 | return search(g.mbr()).filter(new Func1, Boolean>() { 593 | @Override 594 | public Boolean call(Entry entry) { 595 | return intersects.call(entry.geometry(), g); 596 | } 597 | }); 598 | } 599 | 600 | /** 601 | * Returns all entries strictly less than maxDistance from the 602 | * given geometry. Because the geometry may be of an arbitrary type it is 603 | * necessary to also pass a distance function. 604 | * 605 | * @param type of the geometry being searched for 606 | * @param g geometry to search for entries within maxDistance of 607 | * @param maxDistance strict max distance that entries must be from g 608 | * @param distance function to calculate the distance between geometries of type 609 | * S and R. 610 | * @return entries strictly less than maxDistance from g 611 | */ 612 | public Observable> search(final R g, final double maxDistance, 613 | final Func2 distance) { 614 | return search(new Func1() { 615 | @Override 616 | public Boolean call(Geometry entry) { 617 | // just use the mbr initially 618 | return entry.distance(g.mbr()) < maxDistance; 619 | } 620 | }) 621 | // refine with distance function 622 | .filter(new Func1, Boolean>() { 623 | @Override 624 | public Boolean call(Entry entry) { 625 | return distance.call(entry.geometry(), g) < maxDistance; 626 | } 627 | }); 628 | } 629 | 630 | /** 631 | * Returns an {@link Observable} sequence of all {@link Entry}s in the 632 | * R-tree whose minimum bounding rectangles are within maxDistance from the 633 | * given point. 634 | * 635 | * @param p point to measure distance from 636 | * @param maxDistance entries returned must be within this distance from point p 637 | * @return the sequence of matching entries 638 | */ 639 | public Observable> search(final Point p, final double maxDistance) { 640 | return search(p.mbr(), maxDistance); 641 | } 642 | 643 | /** 644 | * Returns the nearest k entries (k=maxCount) to the given rectangle where 645 | * the entries are strictly less than a given maximum distance from the 646 | * rectangle. 647 | * 648 | * @param r rectangle 649 | * @param maxDistance max distance of returned entries from the rectangle 650 | * @param maxCount max number of entries to return 651 | * @return nearest entries to maxCount, not in any particular order 652 | */ 653 | public Observable> nearest(final Rectangle r, final double maxDistance, int maxCount) { 654 | return search(r, maxDistance).lift( 655 | new OperatorBoundedPriorityQueue>(maxCount, Comparators 656 | . ascendingDistance(r))); 657 | } 658 | 659 | /** 660 | * Find nearest neighbors to a given point with constraint that these 661 | * nearest neighbors are inside a given region (a rectangle). 662 | * 663 | * @param p point 664 | * @param r search region 665 | * @param maxCount number of nearest neighbors to find 666 | * @return nearest entries to maxCount, inside the search region 667 | * @author extended by Vinh Nguyen 668 | */ 669 | public Observable> boundedNNSearch(final Point p, final Rectangle r, int maxCount) { 670 | return searchEntriesInRectangle(r).lift(new OperatorBoundedPriorityQueue>(maxCount, Comparators 671 | . ascendingDistance(p.mbr()))); 672 | } 673 | 674 | /** 675 | * Returns the nearest k entries (k=maxCount) to the given point where the 676 | * entries are strictly less than a given maximum distance from the point. 677 | * 678 | * @param p point 679 | * @param maxDistance max distance of returned entries from the point 680 | * @param maxCount max number of entries to return 681 | * @return nearest entries to maxCount, not in any particular order 682 | */ 683 | public Observable> nearest(final Point p, final double maxDistance, int maxCount) { 684 | return nearest(p.mbr(), maxDistance, maxCount); 685 | } 686 | 687 | /** 688 | * Returns all entries in the tree as an {@link Observable} sequence. 689 | * 690 | * @return all entries in the R-tree 691 | */ 692 | public Observable> entries() { 693 | return search(ALWAYS_TRUE); 694 | } 695 | 696 | /** 697 | * Returns a {@link Visualizer} for an image of given width and height and 698 | * restricted to the given view of the coordinates. The points in the view 699 | * are scaled to match the aspect ratio defined by the width and height. 700 | * 701 | * @param width of the image in pixels 702 | * @param height of the image in pixels 703 | * @param view using the coordinate system of the entries 704 | * @return visualizer 705 | */ 706 | @SuppressWarnings("unchecked") 707 | public Visualizer visualize(int width, int height, Rectangle view) { 708 | return new Visualizer((RTree) this, width, height, view); 709 | } 710 | 711 | /** 712 | * Returns a {@link Visualizer} for an image of given width and height and 713 | * restricted to the the smallest view that fully contains the coordinates. 714 | * The points in the view are scaled to match the aspect ratio defined by 715 | * the width and height. 716 | * 717 | * @param width of the image in pixels 718 | * @param height of the image in pixels 719 | * @return visualizer 720 | */ 721 | public Visualizer visualize(int width, int height) { 722 | return visualize(width, height, calculateMaxView(this)); 723 | } 724 | 725 | private Rectangle calculateMaxView(RTree tree) { 726 | return tree 727 | .entries() 728 | .reduce(Optional. absent(), 729 | new Func2, Entry, Optional>() { 730 | 731 | @Override 732 | public Optional call(Optional r, Entry entry) { 733 | if (r.isPresent()) 734 | return of(r.get().add(entry.geometry().mbr())); 735 | else 736 | return of(entry.geometry().mbr()); 737 | } 738 | }).toBlocking().single().or(rectangle(0, 0, 0, 0)); 739 | } 740 | 741 | public Optional> root() { 742 | return root; 743 | } 744 | 745 | /** 746 | * Returns true if and only if the R-tree is empty of entries. 747 | * 748 | * @return is R-tree empty 749 | */ 750 | public boolean isEmpty() { 751 | return size == 0; 752 | } 753 | 754 | /** 755 | * Returns the number of entries in the RTree. 756 | * 757 | * @return the number of entries 758 | */ 759 | public int size() { 760 | return size; 761 | } 762 | 763 | /** 764 | * Returns a {@link Context} containing the configuration of the RTree at 765 | * the time of instantiation. 766 | * 767 | * @return the configuration of the RTree prior to instantiation 768 | */ 769 | public Context context() { 770 | return context; 771 | } 772 | 773 | /** 774 | * Returns a human readable form of the RTree. Here's an example: 775 | *

776 | *

777 |      * mbr=Rectangle [x1=10.0, y1=4.0, x2=62.0, y2=85.0]
778 |      *   mbr=Rectangle [x1=28.0, y1=4.0, x2=34.0, y2=85.0]
779 |      *     entry=Entry [value=2, geometry=Point [x=29.0, y=4.0]]
780 |      *     entry=Entry [value=1, geometry=Point [x=28.0, y=19.0]]
781 |      *     entry=Entry [value=4, geometry=Point [x=34.0, y=85.0]]
782 |      *   mbr=Rectangle [x1=10.0, y1=45.0, x2=62.0, y2=63.0]
783 |      *     entry=Entry [value=5, geometry=Point [x=62.0, y=45.0]]
784 |      *     entry=Entry [value=3, geometry=Point [x=10.0, y=63.0]]
785 |      * 
786 | * 787 | * @return a string representation of the RTree 788 | */ 789 | public String asString() { 790 | if (!root.isPresent()) 791 | return ""; 792 | else 793 | return asString(root.get(), ""); 794 | } 795 | 796 | private String asString(Node node, String margin) { 797 | final String marginIncrement = " "; 798 | StringBuilder s = new StringBuilder(); 799 | if (node instanceof NonLeaf) { 800 | s.append(margin); 801 | s.append("mbr=" + node.geometry()); 802 | s.append('\n'); 803 | NonLeaf n = (NonLeaf) node; 804 | for (Node child : n.children()) { 805 | s.append(asString(child, margin + marginIncrement)); 806 | } 807 | } 808 | else { 809 | Leaf leaf = (Leaf) node; 810 | s.append(margin); 811 | s.append("mbr="); 812 | s.append(leaf.geometry()); 813 | s.append('\n'); 814 | for (Entry entry : leaf.entries()) { 815 | s.append(margin); 816 | s.append(marginIncrement); 817 | s.append("entry="); 818 | s.append(entry); 819 | s.append('\n'); 820 | } 821 | } 822 | return s.toString(); 823 | } 824 | 825 | } 826 | -------------------------------------------------------------------------------- /src/main/java/skylinequeries/tools/StdDraw.java: -------------------------------------------------------------------------------- 1 | package skylinequeries.tools; 2 | 3 | import javax.imageio.ImageIO; 4 | import javax.swing.*; 5 | import java.awt.*; 6 | import java.awt.event.*; 7 | import java.awt.geom.*; 8 | import java.awt.image.BufferedImage; 9 | import java.awt.image.DirectColorModel; 10 | import java.awt.image.WritableRaster; 11 | import java.io.File; 12 | import java.io.IOException; 13 | import java.net.URL; 14 | import java.util.LinkedList; 15 | import java.util.TreeSet; 16 | 17 | /** 18 | * Standard draw. This class provides a basic capability for 19 | * creating drawings with your programs. It uses a simple graphics model that 20 | * allows you to create drawings consisting of points, lines, and curves 21 | * in a window on your computer and to save the drawings to a file. 22 | *

23 | * For additional documentation, see Section 1.5 of 24 | * Introduction to Programming in Java: An Interdisciplinary Approach by Robert Sedgewick and Kevin Wayne. 25 | * 26 | * @author Robert Sedgewick 27 | * @author Kevin Wayne 28 | */ 29 | public final class StdDraw implements ActionListener, MouseListener, MouseMotionListener, KeyListener { 30 | 31 | // pre-defined colors 32 | public static final Color BLACK = Color.BLACK; 33 | public static final Color BLUE = Color.BLUE; 34 | public static final Color CYAN = Color.CYAN; 35 | public static final Color DARK_GRAY = Color.DARK_GRAY; 36 | public static final Color GRAY = Color.GRAY; 37 | public static final Color GREEN = Color.GREEN; 38 | public static final Color LIGHT_GRAY = Color.LIGHT_GRAY; 39 | public static final Color MAGENTA = Color.MAGENTA; 40 | public static final Color ORANGE = Color.ORANGE; 41 | public static final Color PINK = Color.PINK; 42 | public static final Color RED = Color.RED; 43 | public static final Color WHITE = Color.WHITE; 44 | public static final Color YELLOW = Color.YELLOW; 45 | 46 | /** 47 | * Shade of blue used in Introduction to Programming in Java. 48 | * It is Pantone 300U. The RGB values are approximately (9, 90, 166). 49 | */ 50 | public static final Color BOOK_BLUE = new Color(9, 90, 166); 51 | public static final Color BOOK_LIGHT_BLUE = new Color(103, 198, 243); 52 | 53 | /** 54 | * Shade of red used in Algorithms 4th edition. 55 | * It is Pantone 1805U. The RGB values are approximately (150, 35, 31). 56 | */ 57 | public static final Color BOOK_RED = new Color(150, 35, 31); 58 | 59 | // default colors 60 | private static final Color DEFAULT_PEN_COLOR = BLACK; 61 | private static final Color DEFAULT_CLEAR_COLOR = WHITE; 62 | // default canvas size is DEFAULT_SIZE-by-DEFAULT_SIZE 63 | private static final int DEFAULT_SIZE = 512; 64 | // default pen radius 65 | private static final double DEFAULT_PEN_RADIUS = 0.002; 66 | // boundary of drawing canvas, 0% border 67 | // private static final double BORDER = 0.05; 68 | private static final double BORDER = 0.00; 69 | private static final double DEFAULT_XMIN = 0.0; 70 | private static final double DEFAULT_XMAX = 1.0; 71 | private static final double DEFAULT_YMIN = 0.0; 72 | private static final double DEFAULT_YMAX = 1.0; 73 | // default font 74 | private static final Font DEFAULT_FONT = new Font("SansSerif", Font.PLAIN, 16); 75 | // current pen color 76 | private static Color penColor; 77 | private static int width = DEFAULT_SIZE; 78 | private static int height = DEFAULT_SIZE; 79 | // current pen radius 80 | private static double penRadius; 81 | // show we draw immediately or wait until next show? 82 | private static boolean defer = false; 83 | private static double xmin, ymin, xmax, ymax; 84 | // for synchronization 85 | private static Object mouseLock = new Object(); 86 | private static Object keyLock = new Object(); 87 | // current font 88 | private static Font font; 89 | 90 | // double buffered graphics 91 | private static BufferedImage offscreenImage, onscreenImage; 92 | private static Graphics2D offscreen, onscreen; 93 | 94 | // singleton for callbacks: avoids generation of extra .class files 95 | private static StdDraw std = new StdDraw(); 96 | 97 | // the frame for drawing to the screen 98 | private static JFrame frame; 99 | 100 | // mouse state 101 | private static boolean mousePressed = false; 102 | private static double mouseX = 0; 103 | private static double mouseY = 0; 104 | 105 | // queue of typed key characters 106 | private static LinkedList keysTyped = new LinkedList(); 107 | 108 | // set of key codes currently pressed down 109 | private static TreeSet keysDown = new TreeSet(); 110 | 111 | 112 | // singleton pattern: client can't instantiate 113 | private StdDraw() { 114 | } 115 | 116 | 117 | // static initializer 118 | static { 119 | init(); 120 | } 121 | 122 | /** 123 | * Set the window size to the default size 512-by-512 pixels. 124 | * This method must be called before any other commands. 125 | */ 126 | public static void setCanvasSize() { 127 | setCanvasSize(DEFAULT_SIZE, DEFAULT_SIZE); 128 | } 129 | 130 | /** 131 | * Set the window size to w-by-h pixels. 132 | * This method must be called before any other commands. 133 | * 134 | * @param w the width as a number of pixels 135 | * @param h the height as a number of pixels 136 | * @throws a IllegalArgumentException if the width or height is 0 or negative 137 | */ 138 | public static void setCanvasSize(int w, int h) { 139 | if (w < 1 || h < 1) throw new IllegalArgumentException("width and height must be positive"); 140 | width = w; 141 | height = h; 142 | init(); 143 | } 144 | 145 | // init 146 | private static void init() { 147 | if (frame != null) frame.setVisible(false); 148 | frame = new JFrame(); 149 | offscreenImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); 150 | onscreenImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); 151 | offscreen = offscreenImage.createGraphics(); 152 | onscreen = onscreenImage.createGraphics(); 153 | setXscale(); 154 | setYscale(); 155 | offscreen.setColor(DEFAULT_CLEAR_COLOR); 156 | offscreen.fillRect(0, 0, width, height); 157 | setPenColor(); 158 | setPenRadius(); 159 | setFont(); 160 | clear(); 161 | 162 | // add antialiasing 163 | RenderingHints hints = new RenderingHints(RenderingHints.KEY_ANTIALIASING, 164 | RenderingHints.VALUE_ANTIALIAS_ON); 165 | hints.put(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); 166 | offscreen.addRenderingHints(hints); 167 | 168 | // frame stuff 169 | ImageIcon icon = new ImageIcon(onscreenImage); 170 | JLabel draw = new JLabel(icon); 171 | 172 | draw.addMouseListener(std); 173 | draw.addMouseMotionListener(std); 174 | 175 | frame.setContentPane(draw); 176 | frame.addKeyListener(std); // JLabel cannot get keyboard focus 177 | frame.setResizable(false); 178 | frame.setDefaultCloseOperation(JFrame.HIDE_ON_CLOSE); // closes all windows 179 | // frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); // closes only current window 180 | frame.setTitle("Skyline Query Visualization"); 181 | frame.setJMenuBar(createMenuBar()); 182 | frame.pack(); 183 | frame.requestFocusInWindow(); 184 | frame.setVisible(true); 185 | } 186 | 187 | // create the menu bar (changed to private) 188 | private static JMenuBar createMenuBar() { 189 | JMenuBar menuBar = new JMenuBar(); 190 | JMenu menu = new JMenu("File"); 191 | menuBar.add(menu); 192 | JMenuItem menuItem1 = new JMenuItem(" Save... "); 193 | menuItem1.addActionListener(std); 194 | menuItem1.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_S, 195 | Toolkit.getDefaultToolkit().getMenuShortcutKeyMask())); 196 | menu.add(menuItem1); 197 | return menuBar; 198 | } 199 | 200 | 201 | /************************************************************************* 202 | * User and screen coordinate systems 203 | *************************************************************************/ 204 | 205 | /** 206 | * Set the x-scale to be the default (between 0.0 and 1.0). 207 | */ 208 | public static void setXscale() { 209 | setXscale(DEFAULT_XMIN, DEFAULT_XMAX); 210 | } 211 | 212 | /** 213 | * Set the y-scale to be the default (between 0.0 and 1.0). 214 | */ 215 | public static void setYscale() { 216 | setYscale(DEFAULT_YMIN, DEFAULT_YMAX); 217 | } 218 | 219 | /** 220 | * Set the x-scale 221 | * 222 | * @param min the minimum value of the x-scale 223 | * @param max the maximum value of the x-scale 224 | */ 225 | public static void setXscale(double min, double max) { 226 | double size = max - min; 227 | synchronized (mouseLock) { 228 | xmin = min - BORDER * size; 229 | xmax = max + BORDER * size; 230 | } 231 | } 232 | 233 | /** 234 | * Set the y-scale 235 | * 236 | * @param min the minimum value of the y-scale 237 | * @param max the maximum value of the y-scale 238 | */ 239 | public static void setYscale(double min, double max) { 240 | double size = max - min; 241 | synchronized (mouseLock) { 242 | ymin = min - BORDER * size; 243 | ymax = max + BORDER * size; 244 | } 245 | } 246 | 247 | /** 248 | * Set the x-scale and y-scale 249 | * 250 | * @param min the minimum value of the x- and y-scales 251 | * @param max the maximum value of the x- and y-scales 252 | */ 253 | public static void setScale(double min, double max) { 254 | double size = max - min; 255 | synchronized (mouseLock) { 256 | xmin = min - BORDER * size; 257 | xmax = max + BORDER * size; 258 | ymin = min - BORDER * size; 259 | ymax = max + BORDER * size; 260 | } 261 | } 262 | 263 | // helper functions that scale from user coordinates to screen coordinates and back 264 | private static double scaleX(double x) { 265 | return width * (x - xmin) / (xmax - xmin); 266 | } 267 | 268 | private static double scaleY(double y) { 269 | return height * (ymax - y) / (ymax - ymin); 270 | } 271 | 272 | private static double factorX(double w) { 273 | return w * width / Math.abs(xmax - xmin); 274 | } 275 | 276 | private static double factorY(double h) { 277 | return h * height / Math.abs(ymax - ymin); 278 | } 279 | 280 | private static double userX(double x) { 281 | return xmin + x * (xmax - xmin) / width; 282 | } 283 | 284 | private static double userY(double y) { 285 | return ymax - y * (ymax - ymin) / height; 286 | } 287 | 288 | 289 | /** 290 | * Clear the screen to the default color (white). 291 | */ 292 | public static void clear() { 293 | clear(DEFAULT_CLEAR_COLOR); 294 | } 295 | 296 | /** 297 | * Clear the screen to the given color. 298 | * 299 | * @param color the Color to make the background 300 | */ 301 | public static void clear(Color color) { 302 | offscreen.setColor(color); 303 | offscreen.fillRect(0, 0, width, height); 304 | offscreen.setColor(penColor); 305 | draw(); 306 | } 307 | 308 | /** 309 | * Get the current pen radius. 310 | */ 311 | public static double getPenRadius() { 312 | return penRadius; 313 | } 314 | 315 | /** 316 | * Set the radius of the pen to the given size. 317 | * 318 | * @param r the radius of the pen 319 | * @throws IllegalArgumentException if r is negative 320 | */ 321 | public static void setPenRadius(double r) { 322 | if (r < 0) throw new IllegalArgumentException("pen radius must be nonnegative"); 323 | penRadius = r; 324 | float scaledPenRadius = (float) (r * DEFAULT_SIZE); 325 | BasicStroke stroke = new BasicStroke(scaledPenRadius, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND); 326 | // BasicStroke stroke = new BasicStroke(scaledPenRadius); 327 | offscreen.setStroke(stroke); 328 | } 329 | 330 | /** 331 | * Set the pen size to the default (.002). 332 | */ 333 | public static void setPenRadius() { 334 | setPenRadius(DEFAULT_PEN_RADIUS); 335 | } 336 | 337 | /** 338 | * Get the current pen color. 339 | */ 340 | public static Color getPenColor() { 341 | return penColor; 342 | } 343 | 344 | /** 345 | * Set the pen color to the given color. The available pen colors are 346 | * BLACK, BLUE, CYAN, DARK_GRAY, GRAY, GREEN, LIGHT_GRAY, MAGENTA, 347 | * ORANGE, PINK, RED, WHITE, and YELLOW. 348 | * 349 | * @param color the Color to make the pen 350 | */ 351 | public static void setPenColor(Color color) { 352 | penColor = color; 353 | offscreen.setColor(penColor); 354 | } 355 | 356 | /** 357 | * Set the pen color to the default color (black). 358 | */ 359 | public static void setPenColor() { 360 | setPenColor(DEFAULT_PEN_COLOR); 361 | } 362 | 363 | /** 364 | * Set the pen color to the given RGB color. 365 | * 366 | * @param red the amount of red (between 0 and 255) 367 | * @param green the amount of green (between 0 and 255) 368 | * @param blue the amount of blue (between 0 and 255) 369 | * @throws IllegalArgumentException if the amount of red, green, or blue are outside prescribed range 370 | */ 371 | public static void setPenColor(int red, int green, int blue) { 372 | if (red < 0 || red >= 256) throw new IllegalArgumentException("amount of red must be between 0 and 255"); 373 | if (green < 0 || green >= 256) throw new IllegalArgumentException("amount of green must be between 0 and 255"); 374 | if (blue < 0 || blue >= 256) throw new IllegalArgumentException("amount of blue must be between 0 and 255"); 375 | setPenColor(new Color(red, green, blue)); 376 | } 377 | 378 | /** 379 | * Get the current font. 380 | */ 381 | public static Font getFont() { 382 | return font; 383 | } 384 | 385 | /** 386 | * Set the font to the given value. 387 | * 388 | * @param f the font to make text 389 | */ 390 | public static void setFont(Font f) { 391 | font = f; 392 | } 393 | 394 | /** 395 | * Set the font to the default font (sans serif, 16 point). 396 | */ 397 | public static void setFont() { 398 | setFont(DEFAULT_FONT); 399 | } 400 | 401 | 402 | /************************************************************************* 403 | * Drawing geometric shapes. 404 | *************************************************************************/ 405 | 406 | /** 407 | * Draw a line from (x0, y0) to (x1, y1). 408 | * 409 | * @param x0 the x-coordinate of the starting point 410 | * @param y0 the y-coordinate of the starting point 411 | * @param x1 the x-coordinate of the destination point 412 | * @param y1 the y-coordinate of the destination point 413 | */ 414 | public static void line(double x0, double y0, double x1, double y1) { 415 | offscreen.draw(new Line2D.Double(scaleX(x0), scaleY(y0), scaleX(x1), scaleY(y1))); 416 | draw(); 417 | } 418 | 419 | /** 420 | * Draw one pixel at (x, y). 421 | * 422 | * @param x the x-coordinate of the pixel 423 | * @param y the y-coordinate of the pixel 424 | */ 425 | private static void pixel(double x, double y) { 426 | offscreen.fillRect((int) Math.round(scaleX(x)), (int) Math.round(scaleY(y)), 1, 1); 427 | } 428 | 429 | /** 430 | * Draw a point at (x, y). 431 | * 432 | * @param x the x-coordinate of the point 433 | * @param y the y-coordinate of the point 434 | */ 435 | public static void point(double x, double y) { 436 | double xs = scaleX(x); 437 | double ys = scaleY(y); 438 | double r = penRadius; 439 | float scaledPenRadius = (float) (r * DEFAULT_SIZE); 440 | 441 | // double ws = factorX(2*r); 442 | // double hs = factorY(2*r); 443 | // if (ws <= 1 && hs <= 1) pixel(x, y); 444 | if (scaledPenRadius <= 1) pixel(x, y); 445 | else offscreen.fill(new Ellipse2D.Double(xs - scaledPenRadius / 2, ys - scaledPenRadius / 2, 446 | scaledPenRadius, scaledPenRadius)); 447 | draw(); 448 | } 449 | 450 | /** 451 | * Draw a circle of radius r, centered on (x, y). 452 | * 453 | * @param x the x-coordinate of the center of the circle 454 | * @param y the y-coordinate of the center of the circle 455 | * @param r the radius of the circle 456 | * @throws IllegalArgumentException if the radius of the circle is negative 457 | */ 458 | public static void circle(double x, double y, double r) { 459 | if (r < 0) throw new IllegalArgumentException("circle radius must be nonnegative"); 460 | double xs = scaleX(x); 461 | double ys = scaleY(y); 462 | double ws = factorX(2 * r); 463 | double hs = factorY(2 * r); 464 | if (ws <= 1 && hs <= 1) pixel(x, y); 465 | else offscreen.draw(new Ellipse2D.Double(xs - ws / 2, ys - hs / 2, ws, hs)); 466 | draw(); 467 | } 468 | 469 | /** 470 | * Draw filled circle of radius r, centered on (x, y). 471 | * 472 | * @param x the x-coordinate of the center of the circle 473 | * @param y the y-coordinate of the center of the circle 474 | * @param r the radius of the circle 475 | * @throws IllegalArgumentException if the radius of the circle is negative 476 | */ 477 | public static void filledCircle(double x, double y, double r) { 478 | if (r < 0) throw new IllegalArgumentException("circle radius must be nonnegative"); 479 | double xs = scaleX(x); 480 | double ys = scaleY(y); 481 | double ws = factorX(2 * r); 482 | double hs = factorY(2 * r); 483 | if (ws <= 1 && hs <= 1) pixel(x, y); 484 | else offscreen.fill(new Ellipse2D.Double(xs - ws / 2, ys - hs / 2, ws, hs)); 485 | draw(); 486 | } 487 | 488 | 489 | /** 490 | * Draw an ellipse with given semimajor and semiminor axes, centered on (x, y). 491 | * 492 | * @param x the x-coordinate of the center of the ellipse 493 | * @param y the y-coordinate of the center of the ellipse 494 | * @param semiMajorAxis is the semimajor axis of the ellipse 495 | * @param semiMinorAxis is the semiminor axis of the ellipse 496 | * @throws IllegalArgumentException if either of the axes are negative 497 | */ 498 | public static void ellipse(double x, double y, double semiMajorAxis, double semiMinorAxis) { 499 | if (semiMajorAxis < 0) throw new IllegalArgumentException("ellipse semimajor axis must be nonnegative"); 500 | if (semiMinorAxis < 0) throw new IllegalArgumentException("ellipse semiminor axis must be nonnegative"); 501 | double xs = scaleX(x); 502 | double ys = scaleY(y); 503 | double ws = factorX(2 * semiMajorAxis); 504 | double hs = factorY(2 * semiMinorAxis); 505 | if (ws <= 1 && hs <= 1) pixel(x, y); 506 | else offscreen.draw(new Ellipse2D.Double(xs - ws / 2, ys - hs / 2, ws, hs)); 507 | draw(); 508 | } 509 | 510 | /** 511 | * Draw an ellipse with given semimajor and semiminor axes, centered on (x, y). 512 | * 513 | * @param x the x-coordinate of the center of the ellipse 514 | * @param y the y-coordinate of the center of the ellipse 515 | * @param semiMajorAxis is the semimajor axis of the ellipse 516 | * @param semiMinorAxis is the semiminor axis of the ellipse 517 | * @throws IllegalArgumentException if either of the axes are negative 518 | */ 519 | public static void filledEllipse(double x, double y, double semiMajorAxis, double semiMinorAxis) { 520 | if (semiMajorAxis < 0) throw new IllegalArgumentException("ellipse semimajor axis must be nonnegative"); 521 | if (semiMinorAxis < 0) throw new IllegalArgumentException("ellipse semiminor axis must be nonnegative"); 522 | double xs = scaleX(x); 523 | double ys = scaleY(y); 524 | double ws = factorX(2 * semiMajorAxis); 525 | double hs = factorY(2 * semiMinorAxis); 526 | if (ws <= 1 && hs <= 1) pixel(x, y); 527 | else offscreen.fill(new Ellipse2D.Double(xs - ws / 2, ys - hs / 2, ws, hs)); 528 | draw(); 529 | } 530 | 531 | 532 | /** 533 | * Draw an arc of radius r, centered on (x, y), from angle1 to angle2 (in degrees). 534 | * 535 | * @param x the x-coordinate of the center of the circle 536 | * @param y the y-coordinate of the center of the circle 537 | * @param r the radius of the circle 538 | * @param angle1 the starting angle. 0 would mean an arc beginning at 3 o'clock. 539 | * @param angle2 the angle at the end of the arc. For example, if 540 | * you want a 90 degree arc, then angle2 should be angle1 + 90. 541 | * @throws IllegalArgumentException if the radius of the circle is negative 542 | */ 543 | public static void arc(double x, double y, double r, double angle1, double angle2) { 544 | if (r < 0) throw new IllegalArgumentException("arc radius must be nonnegative"); 545 | while (angle2 < angle1) angle2 += 360; 546 | double xs = scaleX(x); 547 | double ys = scaleY(y); 548 | double ws = factorX(2 * r); 549 | double hs = factorY(2 * r); 550 | if (ws <= 1 && hs <= 1) pixel(x, y); 551 | else offscreen.draw(new Arc2D.Double(xs - ws / 2, ys - hs / 2, ws, hs, angle1, angle2 - angle1, Arc2D.OPEN)); 552 | draw(); 553 | } 554 | 555 | /** 556 | * Draw a square of side length 2r, centered on (x, y). 557 | * 558 | * @param x the x-coordinate of the center of the square 559 | * @param y the y-coordinate of the center of the square 560 | * @param r radius is half the length of any side of the square 561 | * @throws IllegalArgumentException if r is negative 562 | */ 563 | public static void square(double x, double y, double r) { 564 | if (r < 0) throw new IllegalArgumentException("square side length must be nonnegative"); 565 | double xs = scaleX(x); 566 | double ys = scaleY(y); 567 | double ws = factorX(2 * r); 568 | double hs = factorY(2 * r); 569 | if (ws <= 1 && hs <= 1) pixel(x, y); 570 | else offscreen.draw(new Rectangle2D.Double(xs - ws / 2, ys - hs / 2, ws, hs)); 571 | draw(); 572 | } 573 | 574 | /** 575 | * Draw a filled square of side length 2r, centered on (x, y). 576 | * 577 | * @param x the x-coordinate of the center of the square 578 | * @param y the y-coordinate of the center of the square 579 | * @param r radius is half the length of any side of the square 580 | * @throws IllegalArgumentException if r is negative 581 | */ 582 | public static void filledSquare(double x, double y, double r) { 583 | if (r < 0) throw new IllegalArgumentException("square side length must be nonnegative"); 584 | double xs = scaleX(x); 585 | double ys = scaleY(y); 586 | double ws = factorX(2 * r); 587 | double hs = factorY(2 * r); 588 | if (ws <= 1 && hs <= 1) pixel(x, y); 589 | else offscreen.fill(new Rectangle2D.Double(xs - ws / 2, ys - hs / 2, ws, hs)); 590 | draw(); 591 | } 592 | 593 | 594 | /** 595 | * Draw a rectangle of given half width and half height, centered on (x, y). 596 | * 597 | * @param x the x-coordinate of the center of the rectangle 598 | * @param y the y-coordinate of the center of the rectangle 599 | * @param halfWidth is half the width of the rectangle 600 | * @param halfHeight is half the height of the rectangle 601 | * @throws IllegalArgumentException if halfWidth or halfHeight is negative 602 | */ 603 | public static void rectangle(double x, double y, double halfWidth, double halfHeight) { 604 | if (halfWidth < 0) throw new IllegalArgumentException("half width must be nonnegative"); 605 | if (halfHeight < 0) throw new IllegalArgumentException("half height must be nonnegative"); 606 | double xs = scaleX(x); 607 | double ys = scaleY(y); 608 | double ws = factorX(2 * halfWidth); 609 | double hs = factorY(2 * halfHeight); 610 | if (ws <= 1 && hs <= 1) pixel(x, y); 611 | else offscreen.draw(new Rectangle2D.Double(xs - ws / 2, ys - hs / 2, ws, hs)); 612 | draw(); 613 | } 614 | 615 | /** 616 | * Draw a filled rectangle of given half width and half height, centered on (x, y). 617 | * 618 | * @param x the x-coordinate of the center of the rectangle 619 | * @param y the y-coordinate of the center of the rectangle 620 | * @param halfWidth is half the width of the rectangle 621 | * @param halfHeight is half the height of the rectangle 622 | * @throws IllegalArgumentException if halfWidth or halfHeight is negative 623 | */ 624 | public static void filledRectangle(double x, double y, double halfWidth, double halfHeight) { 625 | if (halfWidth < 0) throw new IllegalArgumentException("half width must be nonnegative"); 626 | if (halfHeight < 0) throw new IllegalArgumentException("half height must be nonnegative"); 627 | double xs = scaleX(x); 628 | double ys = scaleY(y); 629 | double ws = factorX(2 * halfWidth); 630 | double hs = factorY(2 * halfHeight); 631 | if (ws <= 1 && hs <= 1) pixel(x, y); 632 | else offscreen.fill(new Rectangle2D.Double(xs - ws / 2, ys - hs / 2, ws, hs)); 633 | draw(); 634 | } 635 | 636 | 637 | /** 638 | * Draw a polygon with the given (x[i], y[i]) coordinates. 639 | * 640 | * @param x an array of all the x-coordindates of the polygon 641 | * @param y an array of all the y-coordindates of the polygon 642 | */ 643 | public static void polygon(double[] x, double[] y) { 644 | int N = x.length; 645 | GeneralPath path = new GeneralPath(); 646 | path.moveTo((float) scaleX(x[0]), (float) scaleY(y[0])); 647 | for (int i = 0; i < N; i++) 648 | path.lineTo((float) scaleX(x[i]), (float) scaleY(y[i])); 649 | path.closePath(); 650 | offscreen.draw(path); 651 | draw(); 652 | } 653 | 654 | /** 655 | * Draw a filled polygon with the given (x[i], y[i]) coordinates. 656 | * 657 | * @param x an array of all the x-coordindates of the polygon 658 | * @param y an array of all the y-coordindates of the polygon 659 | */ 660 | public static void filledPolygon(double[] x, double[] y) { 661 | int N = x.length; 662 | GeneralPath path = new GeneralPath(); 663 | path.moveTo((float) scaleX(x[0]), (float) scaleY(y[0])); 664 | for (int i = 0; i < N; i++) 665 | path.lineTo((float) scaleX(x[i]), (float) scaleY(y[i])); 666 | path.closePath(); 667 | offscreen.fill(path); 668 | draw(); 669 | } 670 | 671 | 672 | /** 673 | * ********************************************************************** 674 | * Drawing images. 675 | * *********************************************************************** 676 | */ 677 | 678 | // get an image from the given filename 679 | private static Image getImage(String filename) { 680 | 681 | // to read from file 682 | ImageIcon icon = new ImageIcon(filename); 683 | 684 | // try to read from DB_URL 685 | if ((icon == null) || (icon.getImageLoadStatus() != MediaTracker.COMPLETE)) { 686 | try { 687 | URL url = new URL(filename); 688 | icon = new ImageIcon(url); 689 | } catch (Exception e) { /* not a url */ } 690 | } 691 | 692 | // in case file is inside a .jar 693 | if ((icon == null) || (icon.getImageLoadStatus() != MediaTracker.COMPLETE)) { 694 | URL url = StdDraw.class.getResource(filename); 695 | if (url == null) throw new IllegalArgumentException("image " + filename + " not found"); 696 | icon = new ImageIcon(url); 697 | } 698 | 699 | return icon.getImage(); 700 | } 701 | 702 | /** 703 | * Draw picture (gif, jpg, or png) centered on (x, y). 704 | * 705 | * @param x the center x-coordinate of the image 706 | * @param y the center y-coordinate of the image 707 | * @param s the name of the image/picture, e.g., "ball.gif" 708 | * @throws IllegalArgumentException if the image is corrupt 709 | */ 710 | public static void picture(double x, double y, String s) { 711 | Image image = getImage(s); 712 | double xs = scaleX(x); 713 | double ys = scaleY(y); 714 | int ws = image.getWidth(null); 715 | int hs = image.getHeight(null); 716 | if (ws < 0 || hs < 0) throw new IllegalArgumentException("image " + s + " is corrupt"); 717 | 718 | offscreen.drawImage(image, (int) Math.round(xs - ws / 2.0), (int) Math.round(ys - hs / 2.0), null); 719 | draw(); 720 | } 721 | 722 | /** 723 | * Draw picture (gif, jpg, or png) centered on (x, y), 724 | * rotated given number of degrees 725 | * 726 | * @param x the center x-coordinate of the image 727 | * @param y the center y-coordinate of the image 728 | * @param s the name of the image/picture, e.g., "ball.gif" 729 | * @param degrees is the number of degrees to rotate counterclockwise 730 | * @throws IllegalArgumentException if the image is corrupt 731 | */ 732 | public static void picture(double x, double y, String s, double degrees) { 733 | Image image = getImage(s); 734 | double xs = scaleX(x); 735 | double ys = scaleY(y); 736 | int ws = image.getWidth(null); 737 | int hs = image.getHeight(null); 738 | if (ws < 0 || hs < 0) throw new IllegalArgumentException("image " + s + " is corrupt"); 739 | 740 | offscreen.rotate(Math.toRadians(-degrees), xs, ys); 741 | offscreen.drawImage(image, (int) Math.round(xs - ws / 2.0), (int) Math.round(ys - hs / 2.0), null); 742 | offscreen.rotate(Math.toRadians(+degrees), xs, ys); 743 | 744 | draw(); 745 | } 746 | 747 | /** 748 | * Draw picture (gif, jpg, or png) centered on (x, y), rescaled to w-by-h. 749 | * 750 | * @param x the center x coordinate of the image 751 | * @param y the center y coordinate of the image 752 | * @param s the name of the image/picture, e.g., "ball.gif" 753 | * @param w the width of the image 754 | * @param h the height of the image 755 | * @throws IllegalArgumentException if the width height are negative 756 | * @throws IllegalArgumentException if the image is corrupt 757 | */ 758 | public static void picture(double x, double y, String s, double w, double h) { 759 | Image image = getImage(s); 760 | double xs = scaleX(x); 761 | double ys = scaleY(y); 762 | if (w < 0) throw new IllegalArgumentException("width is negative: " + w); 763 | if (h < 0) throw new IllegalArgumentException("height is negative: " + h); 764 | double ws = factorX(w); 765 | double hs = factorY(h); 766 | if (ws < 0 || hs < 0) throw new IllegalArgumentException("image " + s + " is corrupt"); 767 | if (ws <= 1 && hs <= 1) pixel(x, y); 768 | else { 769 | offscreen.drawImage(image, (int) Math.round(xs - ws / 2.0), 770 | (int) Math.round(ys - hs / 2.0), 771 | (int) Math.round(ws), 772 | (int) Math.round(hs), null); 773 | } 774 | draw(); 775 | } 776 | 777 | 778 | /** 779 | * Draw picture (gif, jpg, or png) centered on (x, y), rotated 780 | * given number of degrees, rescaled to w-by-h. 781 | * 782 | * @param x the center x-coordinate of the image 783 | * @param y the center y-coordinate of the image 784 | * @param s the name of the image/picture, e.g., "ball.gif" 785 | * @param w the width of the image 786 | * @param h the height of the image 787 | * @param degrees is the number of degrees to rotate counterclockwise 788 | * @throws IllegalArgumentException if the image is corrupt 789 | */ 790 | public static void picture(double x, double y, String s, double w, double h, double degrees) { 791 | Image image = getImage(s); 792 | double xs = scaleX(x); 793 | double ys = scaleY(y); 794 | double ws = factorX(w); 795 | double hs = factorY(h); 796 | if (ws < 0 || hs < 0) throw new IllegalArgumentException("image " + s + " is corrupt"); 797 | if (ws <= 1 && hs <= 1) pixel(x, y); 798 | 799 | offscreen.rotate(Math.toRadians(-degrees), xs, ys); 800 | offscreen.drawImage(image, (int) Math.round(xs - ws / 2.0), 801 | (int) Math.round(ys - hs / 2.0), 802 | (int) Math.round(ws), 803 | (int) Math.round(hs), null); 804 | offscreen.rotate(Math.toRadians(+degrees), xs, ys); 805 | 806 | draw(); 807 | } 808 | 809 | 810 | /************************************************************************* 811 | * Drawing text. 812 | *************************************************************************/ 813 | 814 | /** 815 | * Write the given text string in the current font, centered on (x, y). 816 | * 817 | * @param x the center x-coordinate of the text 818 | * @param y the center y-coordinate of the text 819 | * @param s the text 820 | */ 821 | public static void text(double x, double y, String s) { 822 | offscreen.setFont(font); 823 | FontMetrics metrics = offscreen.getFontMetrics(); 824 | double xs = scaleX(x); 825 | double ys = scaleY(y); 826 | int ws = metrics.stringWidth(s); 827 | int hs = metrics.getDescent(); 828 | offscreen.drawString(s, (float) (xs - ws / 2.0), (float) (ys + hs)); 829 | draw(); 830 | } 831 | 832 | /** 833 | * Write the given text string in the current font, centered on (x, y) and 834 | * rotated by the specified number of degrees 835 | * 836 | * @param x the center x-coordinate of the text 837 | * @param y the center y-coordinate of the text 838 | * @param s the text 839 | * @param degrees is the number of degrees to rotate counterclockwise 840 | */ 841 | public static void text(double x, double y, String s, double degrees) { 842 | double xs = scaleX(x); 843 | double ys = scaleY(y); 844 | offscreen.rotate(Math.toRadians(-degrees), xs, ys); 845 | text(x, y, s); 846 | offscreen.rotate(Math.toRadians(+degrees), xs, ys); 847 | } 848 | 849 | 850 | /** 851 | * Write the given text string in the current font, left-aligned at (x, y). 852 | * 853 | * @param x the x-coordinate of the text 854 | * @param y the y-coordinate of the text 855 | * @param s the text 856 | */ 857 | public static void textLeft(double x, double y, String s) { 858 | offscreen.setFont(font); 859 | FontMetrics metrics = offscreen.getFontMetrics(); 860 | double xs = scaleX(x); 861 | double ys = scaleY(y); 862 | int hs = metrics.getDescent(); 863 | offscreen.drawString(s, (float) (xs), (float) (ys + hs)); 864 | draw(); 865 | } 866 | 867 | /** 868 | * Write the given text string in the current font, right-aligned at (x, y). 869 | * 870 | * @param x the x-coordinate of the text 871 | * @param y the y-coordinate of the text 872 | * @param s the text 873 | */ 874 | public static void textRight(double x, double y, String s) { 875 | offscreen.setFont(font); 876 | FontMetrics metrics = offscreen.getFontMetrics(); 877 | double xs = scaleX(x); 878 | double ys = scaleY(y); 879 | int ws = metrics.stringWidth(s); 880 | int hs = metrics.getDescent(); 881 | offscreen.drawString(s, (float) (xs - ws), (float) (ys + hs)); 882 | draw(); 883 | } 884 | 885 | 886 | /** 887 | * Display on screen, pause for t milliseconds, and turn on 888 | * animation mode: subsequent calls to 889 | * drawing methods such as line(), circle(), and square() 890 | * will not be displayed on screen until the next call to show(). 891 | * This is useful for producing animations (clear the screen, draw a bunch of shapes, 892 | * display on screen for a fixed amount of time, and repeat). It also speeds up 893 | * drawing a huge number of shapes (call show(0) to defer drawing 894 | * on screen, draw the shapes, and call show(0) to display them all 895 | * on screen at once). 896 | * 897 | * @param t number of milliseconds 898 | */ 899 | public static void show(int t) { 900 | defer = false; 901 | draw(); 902 | try { 903 | Thread.sleep(t); 904 | } catch (InterruptedException e) { 905 | System.out.println("Error sleeping"); 906 | } 907 | defer = true; 908 | } 909 | 910 | /** 911 | * Display on-screen and turn off animation mode: 912 | * subsequent calls to 913 | * drawing methods such as line(), circle(), and square() 914 | * will be displayed on screen when called. This is the default. 915 | */ 916 | public static void show() { 917 | defer = false; 918 | draw(); 919 | } 920 | 921 | // draw onscreen if defer is false 922 | private static void draw() { 923 | if (defer) return; 924 | onscreen.drawImage(offscreenImage, 0, 0, null); 925 | frame.repaint(); 926 | } 927 | 928 | 929 | /************************************************************************* 930 | * Save drawing to a file. 931 | *************************************************************************/ 932 | 933 | /** 934 | * Save onscreen image to file - suffix must be png, jpg, or gif. 935 | * 936 | * @param filename the name of the file with one of the required suffixes 937 | */ 938 | public static void save(String filename) { 939 | File file = new File(filename); 940 | String suffix = filename.substring(filename.lastIndexOf('.') + 1); 941 | 942 | // png files 943 | if (suffix.toLowerCase().equals("png")) { 944 | try { 945 | ImageIO.write(onscreenImage, suffix, file); 946 | } catch (IOException e) { 947 | e.printStackTrace(); 948 | } 949 | } 950 | 951 | // need to change from ARGB to RGB for jpeg 952 | // reference: http://archives.java.sun.com/cgi-bin/wa?A2=ind0404&L=java2d-interest&D=0&P=2727 953 | else if (suffix.toLowerCase().equals("jpg")) { 954 | WritableRaster raster = onscreenImage.getRaster(); 955 | WritableRaster newRaster; 956 | newRaster = raster.createWritableChild(0, 0, width, height, 0, 0, new int[]{0, 1, 2}); 957 | DirectColorModel cm = (DirectColorModel) onscreenImage.getColorModel(); 958 | DirectColorModel newCM = new DirectColorModel(cm.getPixelSize(), 959 | cm.getRedMask(), 960 | cm.getGreenMask(), 961 | cm.getBlueMask()); 962 | BufferedImage rgbBuffer = new BufferedImage(newCM, newRaster, false, null); 963 | try { 964 | ImageIO.write(rgbBuffer, suffix, file); 965 | } catch (IOException e) { 966 | e.printStackTrace(); 967 | } 968 | } 969 | 970 | else { 971 | System.out.println("Invalid image file type: " + suffix); 972 | } 973 | } 974 | 975 | /** 976 | * Is the mouse being pressed? 977 | * 978 | * @return true or false 979 | */ 980 | public static boolean mousePressed() { 981 | synchronized (mouseLock) { 982 | return mousePressed; 983 | } 984 | } 985 | 986 | 987 | /************************************************************************* 988 | * Mouse interactions. 989 | *************************************************************************/ 990 | 991 | /** 992 | * What is the x-coordinate of the mouse? 993 | * 994 | * @return the value of the x-coordinate of the mouse 995 | */ 996 | public static double mouseX() { 997 | synchronized (mouseLock) { 998 | return mouseX; 999 | } 1000 | } 1001 | 1002 | /** 1003 | * What is the y-coordinate of the mouse? 1004 | * 1005 | * @return the value of the y-coordinate of the mouse 1006 | */ 1007 | public static double mouseY() { 1008 | synchronized (mouseLock) { 1009 | return mouseY; 1010 | } 1011 | } 1012 | 1013 | /** 1014 | * Has the user typed a key? 1015 | * 1016 | * @return true if the user has typed a key, false otherwise 1017 | */ 1018 | public static boolean hasNextKeyTyped() { 1019 | synchronized (keyLock) { 1020 | return !keysTyped.isEmpty(); 1021 | } 1022 | } 1023 | 1024 | /** 1025 | * What is the next key that was typed by the user? This method returns 1026 | * a Unicode character corresponding to the key typed (such as 'a' or 'A'). 1027 | * It cannot identify action keys (such as F1 1028 | * and arrow keys) or modifier keys (such as control). 1029 | * 1030 | * @return the next Unicode key typed 1031 | */ 1032 | public static char nextKeyTyped() { 1033 | synchronized (keyLock) { 1034 | return keysTyped.removeLast(); 1035 | } 1036 | } 1037 | 1038 | /** 1039 | * Is the keycode currently being pressed? This method takes as an argument 1040 | * the keycode (corresponding to a physical key). It can handle action keys 1041 | * (such as F1 and arrow keys) and modifier keys (such as shift and control). 1042 | * See KeyEvent.java 1043 | * for a description of key codes. 1044 | * 1045 | * @return true if keycode is currently being pressed, false otherwise 1046 | */ 1047 | public static boolean isKeyPressed(int keycode) { 1048 | synchronized (keyLock) { 1049 | return keysDown.contains(keycode); 1050 | } 1051 | } 1052 | 1053 | /** 1054 | * This method cannot be called directly. 1055 | */ 1056 | public void actionPerformed(ActionEvent e) { 1057 | FileDialog chooser = new FileDialog(StdDraw.frame, "Use a .png or .jpg extension", FileDialog.SAVE); 1058 | chooser.setVisible(true); 1059 | String filename = chooser.getFile(); 1060 | if (filename != null) { 1061 | StdDraw.save(chooser.getDirectory() + File.separator + chooser.getFile()); 1062 | } 1063 | } 1064 | 1065 | /** 1066 | * This method cannot be called directly. 1067 | */ 1068 | public void mouseClicked(MouseEvent e) { 1069 | } 1070 | 1071 | /** 1072 | * This method cannot be called directly. 1073 | */ 1074 | public void mouseEntered(MouseEvent e) { 1075 | } 1076 | 1077 | /** 1078 | * This method cannot be called directly. 1079 | */ 1080 | public void mouseExited(MouseEvent e) { 1081 | } 1082 | 1083 | /** 1084 | * This method cannot be called directly. 1085 | */ 1086 | public void mousePressed(MouseEvent e) { 1087 | synchronized (mouseLock) { 1088 | mouseX = StdDraw.userX(e.getX()); 1089 | mouseY = StdDraw.userY(e.getY()); 1090 | mousePressed = true; 1091 | } 1092 | } 1093 | 1094 | 1095 | /************************************************************************* 1096 | * Keyboard interactions. 1097 | *************************************************************************/ 1098 | 1099 | /** 1100 | * This method cannot be called directly. 1101 | */ 1102 | public void mouseReleased(MouseEvent e) { 1103 | synchronized (mouseLock) { 1104 | mousePressed = false; 1105 | } 1106 | } 1107 | 1108 | /** 1109 | * This method cannot be called directly. 1110 | */ 1111 | public void mouseDragged(MouseEvent e) { 1112 | synchronized (mouseLock) { 1113 | mouseX = StdDraw.userX(e.getX()); 1114 | mouseY = StdDraw.userY(e.getY()); 1115 | } 1116 | } 1117 | 1118 | /** 1119 | * This method cannot be called directly. 1120 | */ 1121 | public void mouseMoved(MouseEvent e) { 1122 | synchronized (mouseLock) { 1123 | mouseX = StdDraw.userX(e.getX()); 1124 | mouseY = StdDraw.userY(e.getY()); 1125 | } 1126 | } 1127 | 1128 | /** 1129 | * This method cannot be called directly. 1130 | */ 1131 | public void keyTyped(KeyEvent e) { 1132 | synchronized (keyLock) { 1133 | keysTyped.addFirst(e.getKeyChar()); 1134 | } 1135 | } 1136 | 1137 | /** 1138 | * This method cannot be called directly. 1139 | */ 1140 | public void keyPressed(KeyEvent e) { 1141 | synchronized (keyLock) { 1142 | keysDown.add(e.getKeyCode()); 1143 | } 1144 | } 1145 | 1146 | /** 1147 | * This method cannot be called directly. 1148 | */ 1149 | public void keyReleased(KeyEvent e) { 1150 | synchronized (keyLock) { 1151 | keysDown.remove(e.getKeyCode()); 1152 | } 1153 | } 1154 | } 1155 | --------------------------------------------------------------------------------