org.asoem.greyfish.core.space.WalledPointSpace.java Source code

Java tutorial

Introduction

Here is the source code for org.asoem.greyfish.core.space.WalledPointSpace.java

Source

/*
 * Copyright (C) 2015 The greyfish authors
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package org.asoem.greyfish.core.space;

import com.google.common.base.*;
import com.google.common.base.Optional;
import com.google.common.collect.BinaryTreeTraverser;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import org.apache.commons.math3.util.MathUtils;
import org.asoem.greyfish.utils.base.Builder;
import org.asoem.greyfish.utils.base.SingleElementCache;
import org.asoem.greyfish.utils.space.*;

import javax.annotation.Nullable;
import java.util.*;

import static com.google.common.base.Preconditions.*;
import static org.asoem.greyfish.utils.space.Geometry2D.intersection;
import static org.asoem.greyfish.utils.space.Geometry2D.polarToCartesian;

/**
 * @author christoph This class is used to handle a 2D space implemented as a Matrix of Locations.
 */
public final class WalledPointSpace<O> implements TiledSpace<O, Point2D, WalledTile> {

    private final int height;

    private final int width;

    private final WalledTile[][] tileMatrix;

    private final SingleElementCache<TwoDimTree<O>> tree = SingleElementCache
            .memoize(new Supplier<TwoDimTree<O>>() {
                @Override
                public TwoDimTree<O> get() {
                    final TwoDimTree<O> twoDimTree = treeFactory.create(point2DMap.keySet(),
                            Functions.forMap(point2DMap));
                    checkNotNull(twoDimTree, "The tree factory must not return null");
                    return twoDimTree;
                }
            });

    private final TwoDimTreeFactory<O> treeFactory;

    private final Map<O, Point2D> point2DMap = Maps.newHashMap();

    private WalledPointSpace(final WalledPointSpace<O> space) {
        this(checkNotNull(space).colCount(), space.rowCount(), space.treeFactory);
        setWalledTiles(space.getWalledTiles());
    }

    private WalledPointSpace(final int width, final int height, final TwoDimTreeFactory<O> twoDimTreeFactory) {
        this(width, height, new WalledTile[0], twoDimTreeFactory);
    }

    @SuppressWarnings("UnusedDeclaration") // Needed for deserialization
    private WalledPointSpace(final int width, final int height, final Map<O, Point2D> projectables,
            final WalledTile[] walledTiles, final TwoDimTreeFactory<O> treeFactory) {
        this(width, height, walledTiles, treeFactory);
        this.point2DMap.putAll(projectables);
    }

    private WalledPointSpace(final TiledSpaceBuilder<O> builder) {
        this(builder.width, builder.height, builder.treeFactory);
        for (final TiledSpaceBuilder.WallDefinition wallDefinition : builder.wallDefinitions) {
            wallDefinition.apply(this);
        }
    }

    public WalledPointSpace(final int width, final int height, final WalledTile[] walledTiles,
            final TwoDimTreeFactory<O> treeFactory) {
        checkArgument(width >= 0);
        checkArgument(height >= 0);
        checkNotNull(treeFactory);

        this.width = width;
        this.height = height;
        this.treeFactory = treeFactory;

        this.tileMatrix = new WalledTile[width][height];
        for (int i = 0; i < width; i++) {
            for (int j = 0; j < height; j++) {
                this.tileMatrix[i][j] = new WalledTile(this, i, j);
            }
        }

        setWalledTiles(walledTiles);
    }

    public static <O> WalledPointSpace<O> create(final int width, final int height,
            final TwoDimTreeFactory<O> twoDimTreeFactory) {
        return new WalledPointSpace<O>(width, height, twoDimTreeFactory);
    }

    public static <O> WalledPointSpace<O> create(final WalledPointSpace<O> space) {
        return new WalledPointSpace<O>(space);
    }

    private WalledTile[] getWalledTiles() {
        return Iterables.toArray(Iterables.filter(getTiles(), new Predicate<WalledTile>() {
            @Override
            public boolean apply(final WalledTile tileLocation) {
                return checkNotNull(tileLocation).getWallFlags() != 0;
            }
        }), WalledTile.class);
    }

    private void setWalledTiles(final WalledTile[] tiles) {
        if (tiles != null) {
            for (final WalledTile location : tiles) {
                getTileAt(location.getX(), location.getY()).setWallFlags(location.getWallFlags());
            }
        }
    }

    @Override
    public int rowCount() {
        return height;
    }

    @Override
    public int colCount() {
        return width;
    }

    @Override
    public boolean contains(final double x, final double y) {
        return x >= 0 && x < width && y >= 0 && y < height;
    }

    @Override
    public boolean hasTileAt(final int x, final int y) {
        return contains(x, y);
    }

    @Override
    public WalledTile getTileAt(final int x, final int y) {
        checkArgument(contains(x, y));
        return tileMatrix[x][y];
    }

    @Override
    public String toString() {
        return "Tiled Space: dim=" + width + "x" + height + "; oc=" + Iterables.size(getObjects());
    }

    public WalledTile getTileAt(final Point2D point2D) {
        checkArgument(contains(point2D.getX(), point2D.getY()), "There is no tile for location [%s, %s]",
                point2D.getX(), point2D.getY());
        return getTileAt(point2D.getX(), point2D.getY());
    }

    private WalledTile getTileAt(final double x, final double y) {
        assert x >= 0 && y >= 0; // than we can simply cast the values to int, rather than using Math.floor
        return getTileAt((int) x, (int) y);
    }

    @Override
    public void moveObject(final O object, final Motion2D motion) {
        checkNotNull(object);
        checkNotNull(motion);

        final double translation = motion.getTranslation();
        final double rotation = motion.getRotation();

        if (translation == 0 && rotation == 0) {
            return;
        }
        if (translation < 0) {
            throw new IllegalStateException("Translations < 0 are not supported: " + translation);
        }

        final double newOrientation = (rotation == 0) ? 0
                : ((rotation) % MathUtils.TWO_PI + MathUtils.TWO_PI) % MathUtils.TWO_PI;

        final Point2D currentProjection = getProjection(object);
        final Point2D anchorPoint = checkNotNull(currentProjection, "Projection of {} is null", object)
                .getCentroid();
        if (translation != 0) {
            final Point2D preferredPoint = ImmutablePoint2D.sum(anchorPoint,
                    polarToCartesian(newOrientation, translation));
            final Point2D maxPoint = maxTransition(anchorPoint, preferredPoint);
            //final MotionObject2D projection = MotionObject2DImpl.of(maxPoint.getX(),
            // maxPoint.getY(), !preferredPoint.equals(maxPoint));
            //object.setProjection(projection);
        }
        // else: object.setProjection(MotionObject2DImpl.of(anchorPoint.getX(),
        // anchorPoint.getY(), newOrientation, false));

        tree.invalidate();
    }

    /**
     * Get the location of the transition from {@code origin} to {@code destination} respecting collision with walls.
     * So, if there is no wall between {@code origin} and {@code destination} that this method returns {@code
     * destination}. Otherwise it returns the first {@code Point2D} at which the line from {@code origin} to {@code
     * destination} intersects with a wall of any crossing tile.
     *
     * @param origin      The origin of the transition
     * @param destination The destination of the transition
     * @return The point of the first collision, or {@code destination} if none occurs
     */
    public Point2D maxTransition(final Point2D origin, final Point2D destination) {
        final Point2D collision = collision(origin.getX(), origin.getY(), destination.getX(), destination.getY());
        assert collision == null || contains(collision.getX(), collision.getY()) : "Calculated maxTransition from "
                + origin + " to " + destination + " is " + collision + ", which not contained by this space "
                + this;
        return collision != null ? collision : destination;
    }

    @Nullable
    private Point2D collision(final double x, final double y, final double x1, final double y1) {
        return collision(getTileAt(x, y), x, y, x1, y1);
    }

    /**
     * Checks if the line {@code xo, yo, xd, yd} crosses an edge of the {@code tile} or any adjacent tile in the
     * direction of movement which has a wall present. If such a crossing is found, than the point closest to this
     * crossing is returned, {@code null}, otherwise.
     *
     * @param tile the tile to check for a collision
     * @param xo   Movement line x origin
     * @param yo   Movement line y origin
     * @param xd   Movement line x destination
     * @param yd   Movement line x destination
     * @return the location on the line closest to the point of a collision with a wall or {@code null} if none could be
     * found
     */
    @Nullable
    private Point2D collision(final WalledTile tile, final double xo, final double yo, final double xd,
            final double yd) {
        assert tile != null;

        if (tile.covers(xd, yd)) {
            return null;
        }

        TileDirection follow1 = null;
        TileDirection follow2 = null;

        if (yd < yo) { // north
            final ImmutablePoint2D intersection = intersection(tile.getX(), tile.getY(),
                    Math.nextAfter(tile.getX() + 1.0, -Double.MIN_VALUE), tile.getY(), xo, yo, xd, yd);

            if (intersection != null) {
                if (tile.hasWall(TileDirection.NORTH)) {
                    return intersection;
                } else {
                    follow1 = TileDirection.NORTH;
                }
            }
        }

        if (xd > xo) { // east
            final ImmutablePoint2D intersection = intersection(Math.nextAfter(tile.getX() + 1.0, -Double.MIN_VALUE),
                    tile.getY(), Math.nextAfter(tile.getX() + 1.0, -Double.MIN_VALUE),
                    Math.nextAfter(tile.getY() + 1.0, -Double.MIN_VALUE), xo, yo, xd, yd);

            if (intersection != null) {
                if (tile.hasWall(TileDirection.EAST)) {
                    return intersection;
                } else {
                    if (follow1 == null) {
                        follow1 = TileDirection.EAST;
                    } else {
                        follow2 = TileDirection.EAST;
                    }
                }
            }
        }

        if (yd > yo) { // south
            final ImmutablePoint2D intersection = intersection(tile.getX(),
                    Math.nextAfter(tile.getY() + 1.0, -Double.MIN_VALUE),
                    Math.nextAfter(tile.getX() + 1.0, -Double.MIN_VALUE),
                    Math.nextAfter(tile.getY() + 1.0, -Double.MIN_VALUE), xo, yo, xd, yd);

            if (intersection != null) {
                if (tile.hasWall(TileDirection.SOUTH)) {
                    return intersection;
                } else {
                    if (follow1 == null) {
                        follow1 = TileDirection.SOUTH;
                    } else {
                        follow2 = TileDirection.SOUTH;
                    }
                }
            }
        }

        if (xd < xo) { // west
            final ImmutablePoint2D intersection = intersection(tile.getX(),
                    Math.nextAfter(tile.getY() + 1.0, -Double.MIN_VALUE), tile.getX(), tile.getY(), xo, yo, xd, yd);

            if (intersection != null) {
                if (tile.hasWall(TileDirection.WEST)) {
                    return intersection;
                } else {
                    if (follow1 == null) {
                        follow1 = TileDirection.WEST;
                    } else {
                        follow2 = TileDirection.WEST;
                    }
                }
            }
        }

        if (follow1 != null && hasAdjacentTile(tile, follow1)) {
            final Point2D collision = collision(getAdjacentTile(tile, follow1), xo, yo, xd, yd);
            if (collision != null) {
                return collision;
            } else if (follow2 != null && hasAdjacentTile(tile, follow2)) {
                final Point2D collision1 = collision(getAdjacentTile(tile, follow2), xo, yo, xd, yd);
                if (collision1 != null) {
                    return collision1;
                }
            }
        }

        return null;
    }

    private boolean hasAdjacentTile(final WalledTile tile, final TileDirection direction) {
        return hasTileAt(tile.getX() + direction.getXTranslation(), tile.getY() + direction.getYTranslation());
    }

    @Override
    public Iterable<O> findObjects(final double x, final double y, final double radius) {
        return Iterables.transform(tree.get().findNodes(x, y, radius),
                new Function<DistantObject<TwoDimTree.Node<O>>, O>() {
                    @Override
                    public O apply(final DistantObject<TwoDimTree.Node<O>> input) {
                        return input.object().value();
                    }
                });
    }

    @Override
    public Iterable<O> getVisibleNeighbours(final O object, final double range) {
        final Point2D projection = getProjection(object);
        if (projection != null) {
            final Point2D anchorPoint = projection.getCentroid();
            return Iterables.filter(findObjects(anchorPoint.getX(), anchorPoint.getY(), range), new Predicate<O>() {
                @Override
                public boolean apply(final O t) {
                    if (t.equals(object)) {
                        return false;
                    }

                    final Point2D neighborProjection = getProjection(t);
                    assert neighborProjection != null;
                    final Point2D neighborProjectionAnchorPoint = neighborProjection.getCentroid();
                    return collision(anchorPoint.getX(), anchorPoint.getY(), neighborProjectionAnchorPoint.getX(),
                            neighborProjectionAnchorPoint.getY()) == null;
                }
            });
        } else {
            throw new IllegalArgumentException("Projectable has no projection");
        }
    }

    @Override
    public boolean insertObject(final O object, final Point2D projection) {
        checkNotNull(object, "projectable is null");
        checkNotNull(projection, "projection is null");

        synchronized (this) {
            final Point2D previous = point2DMap.put(object, projection);
            checkState(previous == null, "no duplicate objects allowed: " + object);
            tree.invalidate();
            return true;
        }
    }

    @Override
    public boolean isEmpty() {
        return point2DMap.isEmpty();
    }

    @Override
    public double width() {
        return width;
    }

    @Override
    public double height() {
        return height;
    }

    @Override
    @Nullable
    public Point2D getProjection(final O object) {

        final BinaryTreeTraverser<TwoDimTree.Node<O>> traverser = new BinaryTreeTraverser<TwoDimTree.Node<O>>() {
            @Override
            public Optional<TwoDimTree.Node<O>> leftChild(final TwoDimTree.Node<O> root) {
                return root.leftChild();
            }

            @Override
            public Optional<TwoDimTree.Node<O>> rightChild(final TwoDimTree.Node<O> root) {
                return root.rightChild();
            }
        };

        final TwoDimTree.Node<O> root = tree.get().rootNode().orNull();

        if (root != null) {
            final Optional<TwoDimTree.Node<O>> node = traverser.postOrderTraversal(root)
                    .firstMatch(new Predicate<TwoDimTree.Node<O>>() {
                        @Override
                        public boolean apply(final TwoDimTree.Node<O> input) {
                            return object.equals(input.value());
                        }
                    });
            return node.isPresent() ? ImmutablePoint2D.at(node.get().xCoordinate(), node.get().yCoordinate())
                    : null;
        } else {
            return null;
        }
    }

    @Override
    public Map<O, Point2D> asMap() {
        return point2DMap;
    }

    @Override
    public double distance(final O agent, final double degrees) {
        checkNotNull(agent);
        checkArgument(degrees >= 0 && degrees < MathUtils.TWO_PI, "Degrees must be in [0, TWO_PI), was %s",
                degrees);

        Point2D borderIntersection;

        final Point2D origin = getProjection(agent);
        if (origin == null) {
            throw new IllegalArgumentException("Has no projection: " + agent);
        }

        final ImmutablePoint2D destination = ImmutablePoint2D.sum(origin,
                polarToCartesian(degrees, Double.MAX_VALUE));

        if (degrees < 90) {
            borderIntersection = intersection(origin.getX(), origin.getY(), destination.getX(), destination.getY(),
                    0, 0, width(), 0);
            if (borderIntersection == null) {
                borderIntersection = intersection(origin.getX(), origin.getY(), destination.getX(),
                        destination.getY(), width(), 0, width(), height());
            }
        } else if (degrees < 180) {
            borderIntersection = intersection(origin.getX(), origin.getY(), destination.getX(), destination.getY(),
                    width(), 0, width(), height());
            if (borderIntersection == null) {
                borderIntersection = intersection(origin.getX(), origin.getY(), destination.getX(),
                        destination.getY(), 0, height(), width(), height());
            }
        } else if (degrees < 270) {
            borderIntersection = intersection(origin.getX(), origin.getY(), destination.getX(), destination.getY(),
                    0, height(), width(), height());
            if (borderIntersection == null) {
                borderIntersection = intersection(origin.getX(), origin.getY(), destination.getX(),
                        destination.getY(), 0, 0, 0, height());
            }
        } else {
            borderIntersection = intersection(origin.getX(), origin.getY(), destination.getX(), destination.getY(),
                    0, 0, 0, height());
            if (borderIntersection == null) {
                borderIntersection = intersection(origin.getX(), origin.getY(), destination.getX(),
                        destination.getY(), 0, 0, width(), 0);
            }
        }

        assert borderIntersection != null; // There must always be an intersection with one border

        return origin.distance(maxTransition(origin, borderIntersection));
    }

    @Override
    public Collection<O> getObjects() {
        return Collections.unmodifiableSet(point2DMap.keySet());
    }

    @Override
    public Iterable<O> getObjects(final Iterable<? extends Tile> tiles) {
        return Iterables.concat(Iterables.transform(tiles, new Function<Tile, Iterable<O>>() {
            @Override
            public Iterable<O> apply(final Tile tile) {
                final Tile checkedTile = checkNotNull(tile);
                return Iterables.filter(findObjects(checkedTile.getX() + 0.5, checkedTile.getY() + 0.5,
                        0.70710678118 /* sqrt(0.5^2) */), new Predicate<O>() {
                            @Override
                            public boolean apply(final O t) {
                                assert t != null;
                                final Point2D projection = getProjection(t);
                                assert projection != null;
                                final Point2D anchorPoint = projection.getCentroid();
                                return Geometry2D.rectangleContains(checkedTile.getX(), checkedTile.getY(), 1, 1,
                                        anchorPoint.getX(), anchorPoint.getY());
                            }
                        });
            }
        }));
    }

    @Override
    public boolean removeObject(final O agent) {
        checkNotNull(agent);

        synchronized (this) {
            if (point2DMap.remove(agent) != null) {
                tree.invalidate();
                return true;
            } else {
                return false;
            }
        }
    }

    @Override
    public boolean removeIf(final Predicate<O> predicate) {
        synchronized (this) {
            if (Iterables.removeIf(point2DMap.keySet(), predicate)) {
                tree.invalidate();
                return true;
            }
            return false;
        }
    }

    @Override
    public int countObjects() {
        return point2DMap.size();
    }

    @Override
    public Iterable<WalledTile> getTiles() {
        return Iterables.concat(
                Iterables.transform(Arrays.asList(tileMatrix), new Function<WalledTile[], Iterable<WalledTile>>() {
                    @Override
                    public Iterable<WalledTile> apply(final WalledTile[] tileLocations) {
                        return Arrays.asList(tileLocations);
                    }
                }));
    }

    @Override
    public WalledTile getAdjacentTile(final WalledTile tile, final TileDirection direction) {
        return getTileAt(tile.getX() + direction.getXTranslation(), tile.getY() + direction.getYTranslation());
    }

    public static <O> WalledPointSpace<O> copyOf(final WalledPointSpace<O> space) {
        return create(space);
    }

    public static <O> WalledPointSpace<O> ofSize(final int width, final int height,
            final TwoDimTreeFactory<O> twoDimTreeFactory) {
        return create(width, height, twoDimTreeFactory);
    }

    public static <O> TiledSpaceBuilder<O> builder(final int width, final int height) {
        return new TiledSpaceBuilder<O>(width, height);
    }

    @SuppressWarnings("UnusedDeclaration")
    public static final class TiledSpaceBuilder<O> implements Builder<WalledPointSpace<O>> {

        private final int width;
        private final int height;
        private final List<WallDefinition> wallDefinitions = Lists.newArrayList();
        private TwoDimTreeFactory<O> treeFactory;

        public TiledSpaceBuilder(final int width, final int height) {

            this.width = width;
            this.height = height;
        }

        public TiledSpaceBuilder<O> addWall(final int x, final int y, final TileDirection direction) {
            checkArgument(x >= 0 && x < width && y >= 0 && y < height);
            checkNotNull(direction);
            wallDefinitions.add(new WallDefinition() {
                @Override
                public void apply(final WalledPointSpace<?> space) {
                    space.setTileWall(x, y, direction, true);
                }
            });
            return this;
        }

        public TiledSpaceBuilder<O> addWallsVertical(final int x, final int y1, final int y2,
                final TileDirection direction) {
            checkArgument(x >= 0 && x < width);
            checkArgument(y1 >= 0 && y1 < height);
            checkArgument(y2 >= 0 && y2 < height);
            checkNotNull(direction);

            wallDefinitions.add(new WallDefinition() {
                @Override
                public void apply(final WalledPointSpace<?> space) {
                    for (int i = y1; i <= y2; i++) {
                        space.setTileWall(x, i, direction, true);
                    }
                }
            });
            return this;
        }

        public TiledSpaceBuilder<O> addWallsHorizontal(final int x1, final int x2, final int y,
                final TileDirection direction) {
            checkArgument(x1 >= 0 && x1 < width);
            checkArgument(x2 >= 0 && x2 < width);
            checkArgument(y >= 0 && y < height);
            checkNotNull(direction);

            wallDefinitions.add(new WallDefinition() {
                @Override
                public void apply(final WalledPointSpace<?> space) {
                    for (int i = x1; i <= x2; i++) {
                        space.setTileWall(i, y, direction, true);
                    }
                }
            });
            return this;
        }

        public TiledSpaceBuilder<O> treeFactory(final TwoDimTreeFactory<O> treeFactory) {
            this.treeFactory = checkNotNull(treeFactory);
            return this;
        }

        @Override
        public WalledPointSpace<O> build() {
            checkState(treeFactory != null, "You must provide a tree factory");
            return new WalledPointSpace<O>(this);
        }

        private interface WallDefinition {
            void apply(WalledPointSpace<?> space);
        }
    }

    /**
     * Set (b={@code true}) or unset (b={@code false}) the wall of the tile at x,y in the given direction. Automatically
     * adjusts the wall in the opposite direction at the adjacent tile in the given {@code direction}
     *
     * @param x         x of the tile location
     * @param y         y of the tile location
     * @param direction the side of the tile which will get modified
     * @param b         indicates if the wall will be set or unset
     */
    private void setTileWall(final int x, final int y, final TileDirection direction, final boolean b) {
        final WalledTile tile = getTileAt(x, y);
        tile.setWall(direction, b);

        if (hasAdjacentTile(tile, direction)) {
            getAdjacentTile(tile, direction).setWall(direction.opposite(), b);
        }
    }

}