All Downloads are FREE. Search and download functionalities are using the official Maven repository.

squidpony.squidgrid.SpatialMap Maven / Gradle / Ivy

Go to download

SquidLib platform-independent logic and utility code. Please refer to https://github.com/SquidPony/SquidLib .

There is a newer version: 3.0.6
Show newest version
package squidpony.squidgrid;

import squidpony.squidmath.*;

import java.util.*;

/**
 * A data structure that seems to be re-implemented often for games, this associates Coord positions and generic I
 * identities with generic E elements. You can get an element from a SpatialMap with either an identity or a position,
 * change the position of an element without changing its value or identity, modify an element given its identity and
 * a new value, and perform analogues to most of the features of the Map interface, though this does not implement Map
 * because it essentially has two key types and one value type. You can also iterate through the values in insertion
 * order, where insertion order should be stable even when elements are moved or modified (the relevant key is the
 * identity, which is never changed in this class). Uses two OrderedMap fields internally.
 * Created by Tommy Ettinger on 1/2/2016.
 */
public class SpatialMap implements Iterable {

    public static class SpatialTriple
    {
        public Coord position;
        public I id;
        public E element;

        public SpatialTriple()
        {
            position = Coord.get(0,0);
            id = null;
            element = null;
        }
        public SpatialTriple(Coord position, I id, E element) {
            this.position = position;
            this.id = id;
            this.element = element;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;

            SpatialTriple that = (SpatialTriple) o;

            if (position != null ? !position.equals(that.position) : that.position != null) return false;
            if (id != null ? !id.equals(that.id) : that.id != null) return false;
            return element != null ? element.equals(that.element) : that.element == null;

        }

        @Override
        public int hashCode() {
            int result = position != null ? position.hashCode() : 0;
            result = 31 * result + (id != null ? id.hashCode() : 0);
            result = 31 * result + (element != null ? element.hashCode() : 0);
            return result;
        }
    }

    protected OrderedMap> itemMapping;
    protected OrderedMap> positionMapping;

    /**
     * Constructs a SpatialMap with capacity 32.
     */
    public SpatialMap()
    {
        itemMapping = new OrderedMap<>(32);
        positionMapping = new OrderedMap<>(32);
    }

    /**
     * Constructs a SpatialMap with the given capacity
     * @param capacity the capacity for each of the internal OrderedMaps
     */
    public SpatialMap(int capacity)
    {
        itemMapping = new OrderedMap<>(capacity);
        positionMapping = new OrderedMap<>(capacity);
    }

    /**
     * Constructs a SpatialMap given arrays of Coord, identity, and element; all 3 arrays should have the same length,
     * since this will use only up to the minimum length of these arrays for how many it adds. Each unique id will be
     * added with the corresponding element at the corresponding Coord position if that position is not already filled.
     * @param coords a starting array of Coord positions; indices here correspond to the other parameters
     * @param ids a starting array of identities; indices here correspond to the other parameters
     * @param elements a starting array of elements; indices here correspond to the other parameters
     */
    public SpatialMap(Coord[] coords, I[] ids, E[] elements)
    {
        itemMapping = new OrderedMap<>(
                Math.min(coords.length, Math.min(ids.length, elements.length)));
        positionMapping = new OrderedMap<>(
                Math.min(coords.length, Math.min(ids.length, elements.length)));

        for (int i = 0; i < coords.length && i < ids.length && i < elements.length; i++) {
            add(coords[i], ids[i], elements[i]);
        }
    }

    /**
     * Constructs a SpatialMap given collections of Coord, identity, and element; all 3 collections should have the same
     * length, since this will use only up to the minimum length of these collections for how many it adds. Each unique
     * id will be added with the corresponding element at the corresponding Coord position if that position is not
     * already filled.
     * @param coords a starting collection of Coord positions; indices here correspond to the other parameters
     * @param ids a starting collection of identities; indices here correspond to the other parameters
     * @param elements a starting collection of elements; indices here correspond to the other parameters
     */
    public SpatialMap(Collection coords, Collection ids, Collection elements)
    {
        itemMapping = new OrderedMap<>(
                Math.min(coords.size(), Math.min(ids.size(), elements.size())));
        positionMapping = new OrderedMap<>(
                Math.min(coords.size(), Math.min(ids.size(), elements.size())));
        if(itemMapping.size() <= 0)
            return;
        Iterator cs = coords.iterator();
        Iterator is = ids.iterator();
        Iterator es = elements.iterator();
        Coord c = cs.next();
        I i = is.next();
        E e = es.next();
        for (; cs.hasNext() && is.hasNext() && es.hasNext(); c = cs.next(), i = is.next(), e = es.next()) {
            add(c, i, e);
        }
    }

    /**
     * Adds a new element with the given identity and Coord position. If the position is already occupied by an element
     * in this data structure, does nothing. If the identity is already used, this also does nothing. If the identity
     * and position are both unused, this adds element to the data structure.
     * 
* You should strongly avoid calling remove() and add() to change an element; prefer modify() and move(). * @param coord the Coord position to place the element at; should be empty * @param id the identity to associate the element with; should be unused * @param element the element to add */ public void add(Coord coord, I id, E element) { if(itemMapping.containsKey(id)) return; if(!positionMapping.containsKey(coord)) { SpatialTriple triple = new SpatialTriple<>(coord, id, element); itemMapping.put(id, triple); positionMapping.put(coord, triple); } } /** * Inserts a new element with the given identity and Coord position, potentially overwriting an existing element. *
* If you want to alter an existing element, use modify() or move(). * @param coord the Coord position to place the element at; should be empty * @param id the identity to associate the element with; should be unused * @param element the element to add */ public void put(Coord coord, I id, E element) { SpatialTriple triple = new SpatialTriple<>(coord, id, element); itemMapping.remove(id); positionMapping.remove(coord); itemMapping.put(id, triple); positionMapping.put(coord, triple); } /** * Inserts a SpatialTriple into this SpatialMap without changing it, potentially overwriting an existing element. * SpatialTriple objects can be obtained by the triples() or tripleIterator() methods, and can also be constructed * on their own. *
* If you want to alter an existing element, use modify() or move(). * @param triple a SpatialTriple (an inner class of SpatialMap) with the same type parameters as this class */ public void put(SpatialTriple triple) { itemMapping.remove(triple.id); positionMapping.remove(triple.position); itemMapping.put(triple.id, triple); positionMapping.put(triple.position, triple); } /** * Changes the element's value associated with id. The key id should exist before calling this; if there is no * matching id, this returns null. * @param id the identity of the element to modify * @param newValue the element value to replace the previous element with. * @return the previous element value associated with id */ public E modify(I id, E newValue) { SpatialTriple gotten = itemMapping.get(id); if(gotten != null) { E previous = gotten.element; gotten.element = newValue; return previous; } return null; } /** * Changes the element's value associated with pos. The key pos should exist before calling this; if there is no * matching position, this returns null. * @param pos the position of the element to modify * @param newValue the element value to replace the previous element with. * @return the previous element value associated with id */ public E positionalModify(Coord pos, E newValue) { SpatialTriple gotten = positionMapping.get(pos); if(gotten != null) { E previous = gotten.element; gotten.element = newValue; return previous; } return null; } /** * Move an element from one position to another; moves whatever is at the Coord position previous to the new Coord * position target. The element will not be present at its original position if target is unoccupied, but nothing * will change if target is occupied. * @param previous the starting Coord position of an element to move * @param target the Coord position to move the element to * @return the moved element if movement was successful or null otherwise */ public E move(Coord previous, Coord target) { if(positionMapping.containsKey(previous) && !positionMapping.containsKey(target)) { SpatialTriple gotten = positionMapping.remove(previous); gotten.position = target; positionMapping.put(target, gotten); return gotten.element; } return null; } /** * Move an element, picked by its identity, to a new Coord position. Finds the element using only the id, and does * not need the previous position. The target position must be empty for this to move successfully, and the id must * exist in this data structure for this to move anything. * @param id the identity of the element to move * @param target the Coord position to move the element to * @return the moved element if movement was successful or null otherwise */ public E move(I id, Coord target) { if(itemMapping.containsKey(id) && !positionMapping.containsKey(target)) { SpatialTriple gotten = itemMapping.get(id); positionMapping.remove(gotten.position); gotten.position = target; positionMapping.put(target, gotten); return gotten.element; } return null; } /** * Removes the element at the given position from all storage in this data structure. *
* You should strongly avoid calling remove() and add() to change an element; prefer modify() and move(). * @param coord the position of the element to remove * @return the value of the element that was removed or null if nothing was present at the position */ public E remove(Coord coord) { SpatialTriple gotten = positionMapping.remove(coord); if(gotten != null) { itemMapping.remove(gotten.id); return gotten.element; } return null; } /** * Removes the element with the given identity from all storage in this data structure. *
* You should strongly avoid calling remove() and add() to change an element; prefer modify() and move(). * @param id the identity of the element to remove * @return the value of the element that was removed or null if nothing was present at the position */ public E remove(I id) { SpatialTriple gotten = itemMapping.remove(id); if(gotten != null) { positionMapping.remove(gotten.position); return gotten.element; } return null; } /** * Checks whether this contains the given element. Slower than containsKey and containsPosition (linear time). * @param o an Object that should be an element if you expect this to possibly return true * @return true if o is contained as an element in this data structure */ public boolean containsValue(Object o) { if(o == null) { for(SpatialTriple v : itemMapping.values()) { if(v != null && v.element == null) return true; } } else { for (SpatialTriple v : itemMapping.values()) { if (v != null && v.element != null && v.element.equals(o)) return true; } } return false; } /** * Checks whether this contains the given identity key. * @param o an Object that should be of the generic I type if you expect this to possibly return true * @return true if o is an identity key that can be used with this data structure */ public boolean containsKey(Object o) { return itemMapping.containsKey(o); } /** * Checks whether this contains anything at the given position. * @param o an Object that should be a Coord if you expect this to possibly return true * @return true if o is a Coord that is associated with some element in this data structure */ public boolean containsPosition(Object o) { return positionMapping.containsKey(o); } /** * Gets the element at the given Coord position. * @param c the position to get an element from * @return the element if it exists or null otherwise */ public E get(Coord c) { SpatialTriple gotten = positionMapping.get(c); if(gotten != null) return gotten.element; return null; } /** * Gets the element with the given identity. * @param i the identity of the element to get * @return the element if it exists or null otherwise */ public E get(I i) { SpatialTriple gotten = itemMapping.get(i); if(gotten != null) return gotten.element; return null; } /** * Gets the position of the element with the given identity. * @param i the identity of the element to get a position from * @return the position of the element if it exists or null otherwise */ public Coord getPosition(I i) { SpatialTriple gotten = itemMapping.get(i); if(gotten != null) return gotten.position; return null; } /** * Gets the identity of the element at the given Coord position. * @param c the position to get an identity from * @return the identity of the element if it exists at the given position or null otherwise */ public I getIdentity(Coord c) { SpatialTriple gotten = positionMapping.get(c); if(gotten != null) return gotten.id; return null; } /** * Get a Set of all positions used for values in this data structure, returning a OrderedSet (defensively copying * the key set used internally) for its stable iteration order. * @return a OrderedSet of Coord corresponding to the positions present in this data structure. */ public OrderedSet positions() { return new OrderedSet<>(positionMapping.keySet()); } /** * Get a Set of all identities used for values in this data structure, returning a OrderedSet (defensively * copying the key set used internally) for its stable iteration order. * @return a OrderedSet of I corresponding to the identities present in this data structure. */ public OrderedSet identities() { return new OrderedSet<>(itemMapping.keySet()); } /** * Gets all data stored in this as a collection of values similar to Map.Entry, but containing a Coord, I, and E * value for each entry, in insertion order. The type is SpatialTriple, defined in a nested class. * @return a Collection of SpatialTriple of I, E */ public Collection> triples() { return itemMapping.values(); } /** * Given an Iterable (such as a List, Set, or other Collection) of Coord, gets all elements in this SpatialMap that * share a position with one of the Coord objects in positions and returns them as an ArrayList of elements. * @param positions an Iterable (such as a List or Set) of Coord * @return an ArrayList, possibly empty, of elements that share a position with a Coord in positions */ public ArrayList getManyPositions(Iterable positions) { ArrayList gotten = new ArrayList<>(); SpatialTriple ie; for(Coord p : positions) { if((ie = positionMapping.get(p)) != null) gotten.add(ie.element); } return gotten; } /** * Given an Iterable (such as a List, Set, or other Collection) of I, gets all elements in this SpatialMap that * share an identity with one of the I objects in identities and returns them as an ArrayList of elements. * @param identities an Iterable (such as a List or Set) of I * @return an ArrayList, possibly empty, of elements that share an Identity with an I in identities */ public ArrayList getManyIdentities(Iterable identities) { ArrayList gotten = new ArrayList<>(); SpatialTriple ie; for(I i : identities) { if((ie = itemMapping.get(i)) != null) gotten.add(ie.element); } return gotten; } /** * Given an array of Coord, gets all elements in this SpatialMap that share a position with one of the Coord objects * in positions and returns them as an ArrayList of elements. * @param positions an array of Coord * @return an ArrayList, possibly empty, of elements that share a position with a Coord in positions */ public ArrayList getManyPositions(Coord[] positions) { ArrayList gotten = new ArrayList<>(positions.length); SpatialTriple ie; for(Coord p : positions) { if((ie = positionMapping.get(p)) != null) gotten.add(ie.element); } return gotten; } /** * Given an array of I, gets all elements in this SpatialMap that share an identity with one of the I objects in * identities and returns them as an ArrayList of elements. * @param identities an array of I * @return an ArrayList, possibly empty, of elements that share an Identity with an I in identities */ public ArrayList getManyIdentities(I[] identities) { ArrayList gotten = new ArrayList<>(identities.length); SpatialTriple ie; for(I i : identities) { if((ie = itemMapping.get(i)) != null) gotten.add(ie.element); } return gotten; } public E randomElement(IRNG rng) { if(itemMapping.isEmpty()) return null; return itemMapping.randomValue(rng).element; } public Coord randomPosition(IRNG rng) { if(positionMapping.isEmpty()) return null; return positionMapping.randomKey(rng); } public I randomIdentity(IRNG rng) { if(itemMapping.isEmpty()) return null; return itemMapping.randomKey(rng); } public SpatialTriple randomEntry(IRNG rng) { if(itemMapping.isEmpty()) return null; return itemMapping.randomValue(rng); } /** * Given the size and position of a rectangular area, creates a new SpatialMap from this one that refers only to the * subsection of this SpatialMap shared with the rectangular area. Will not include any elements from this * SpatialMap with positions beyond the bounds of the given rectangular area, and will include all elements from * this that are in the area. * @param x the minimum x-coordinate of the rectangular area * @param y the minimum y-coordinate of the rectangular area * @param width the total width of the rectangular area * @param height the total height of the rectangular area * @return a new SpatialMap that refers to a subsection of this one */ public SpatialMap rectangleSection(int x, int y, int width, int height) { SpatialMap next = new SpatialMap<>(positionMapping.size()); Coord tmp; for(SpatialTriple ie : positionMapping.values()) { tmp = ie.position; if(tmp.x >= x && tmp.y >= y && tmp.x + width > x && tmp.y + height > y) next.put(ie); } return next; } /** * Given the center position, Radius to determine measurement, and maximum distance from the center, creates a new * SpatialMap from this one that refers only to the subsection of this SpatialMap shared with the area within the * given distance from the center as measured by measurement. Will not include any elements from this SpatialMap * with positions beyond the bounds of the given area, and will include all elements from this that are in the area. * @param x the center x-coordinate of the area * @param y the center y-coordinate of the area * @param measurement a Radius enum, such as Radius.CIRCLE or Radius.DIAMOND, that calculates distance * @param distance the maximum distance from the center to include in the area * @return a new SpatialMap that refers to a subsection of this one */ public SpatialMap radiusSection(int x, int y, Radius measurement, int distance) { SpatialMap next = new SpatialMap<>(positionMapping.size()); Coord tmp; for(SpatialTriple ie : positionMapping.values()) { tmp = ie.position; if(measurement.inRange(x, y, tmp.x, tmp.y, 0, distance)) next.put(ie); } return next; } /** * Given the center position and maximum distance from the center, creates a new SpatialMap from this one that * refers only to the subsection of this SpatialMap shared with the area within the given distance from the center, * measured with Euclidean distance to produce a circle shape. Will not include any elements from this SpatialMap * with positions beyond the bounds of the given area, and will include all elements from this that are in the area. * @param x the center x-coordinate of the area * @param y the center y-coordinate of the area * @param radius the maximum distance from the center to include in the area, using Euclidean distance * @return a new SpatialMap that refers to a subsection of this one */ public SpatialMap circleSection(int x, int y, int radius) { return radiusSection(x, y, Radius.CIRCLE, radius); } public void clear() { itemMapping.clear(); positionMapping.clear(); } public boolean isEmpty() { return itemMapping.isEmpty(); } public int size() { return itemMapping.size(); } public Object[] toArray() { Object[] contents = itemMapping.values().toArray(); for (int i = 0; i < contents.length; i++) { contents[i] = ((SpatialTriple)contents[i]).element; } return contents; } /** * Replaces the contents of the given array with the elements this holds, in insertion order, until either this * data structure or the array has been exhausted. * @param a the array to replace; should usually have the same length as this data structure's size. * @return an array of elements that should be the same as the changed array originally passed as a parameter. */ public E[] toArray(E[] a) { Collection> contents = itemMapping.values(); int i = 0; for (SpatialTriple triple : contents) { if(i < a.length) a[i] = triple.element; else break; i++; } return a; } /** * Iterates through values in insertion order. * @return an Iterator of generic type E */ @Override public Iterator iterator() { final Iterator> it = itemMapping.values().iterator(); return new Iterator() { @Override public boolean hasNext() { return it.hasNext(); } @Override public E next() { SpatialTriple triple = it.next(); if(triple != null) return triple.element; return null; } @Override public void remove() { throw new UnsupportedOperationException(); } }; } /** * Iterates through values similar to Map.Entry, but containing a Coord, I, and E value for each entry, in insertion * order. The type is SpatialTriple, defined in a nested class. * @return an Iterator of SpatialTriple of I, E */ public Iterator> tripleIterator() { return itemMapping.values().iterator(); } /** * Iterates through positions in insertion order; has less predictable iteration order than the other iterators. * @return an Iterator of Coord */ public Iterator positionIterator() { return positionMapping.keySet().iterator(); } /** * Iterates through identity keys in insertion order. * @return an Iterator of generic type I */ public Iterator identityIterator() { return itemMapping.keySet().iterator(); } /** * Iterates through positions in a rectangular region (starting at a minimum of x, y and extending to the specified * width and height) in left-to-right, then top-to-bottom order (the same as reading a page of text). * Any Coords this returns should be viable arguments to get() if you want a corresponding element. * @return an Iterator of Coord */ public Iterator rectanglePositionIterator(int x, int y, int width, int height) { return new RectangularIterator(x, y, width, height); } /** * Iterates through positions in a region defined by a Radius (starting at a minimum of x - distance, y - distance * and extending to x + distance, y + distance but skipping any positions where the Radius considers a position * further from x, y than distance) in left-to-right, then top-to-bottom order (the same as reading a page of text). * You can use Radius.SQUARE to make a square region (which could also be made with rectanglePositionIterator()), * Radius.DIAMOND to make a, well, diamond-shaped region, or Radius.CIRCLE to make a circle (which could also be * made with circlePositionIterator). * Any Coords this returns should be viable arguments to get() if you want a corresponding element. * @return an Iterator of Coord */ public Iterator radiusPositionIterator(int x, int y, Radius measurement, int distance) { return new RadiusIterator(x, y, measurement, distance); } /** * Iterates through positions in a circular region (starting at a minimum of x - distance, y - distance and * extending to x + distance, y + distance but skipping any positions where the Euclidean distance from x,y to the * position is more than distance) in left-to-right, then top-to-bottom order (the same as reading a page of text). * Any Coords this returns should be viable arguments to get() if you want a corresponding element. * @return an Iterator of Coord */ public Iterator circlePositionIterator(int x, int y, int distance) { return new RadiusIterator(x, y, Radius.CIRCLE, distance); } private class RectangularIterator implements Iterator { int x, y, width, height, idx, poolWidth = Coord.getCacheWidth(), poolHeight = Coord.getCacheHeight(); Set positions; Coord temp; RectangularIterator(int x, int y, int width, int height) { this.x = x; this.y = y; this.width = width; this.height = height; idx = -1; positions = positionMapping.keySet(); } @Override public boolean hasNext() { if (idx < width * height - 1) { Coord t2; int n = idx; do { n = findNext(n); if (idx < 0) return n >= 0; else { if(x + n % width >= 0 && x + n % width < poolWidth && y + n / width >= 0 && y + n / width < poolHeight) t2 = Coord.get(x + n % width, y + n / width); else t2 = Coord.get(-1, -1); } } while (!positions.contains(t2)); /* Not done && has next */ return n >= 0; } return false; } @Override public Coord next() { do { idx = findNext(idx); if (idx < 0) throw new NoSuchElementException(); if(x + idx % width >= 0 && x + idx % width < poolWidth && y + idx / width >= 0 && y + idx / width < poolHeight) temp = Coord.get(x + idx % width, y + idx / width); else temp = Coord.get(-1, -1); } while (!positions.contains(temp)); return temp; } @Override public void remove() { throw new UnsupportedOperationException(); } private int findNext(final int idx) { if (idx < 0) { /* First iteration */ return 0; } else { if (idx >= width * height - 1) { /* Done iterating */ return -1; } else { return idx + 1; } } } } private class RadiusIterator implements Iterator { int x, y, width, height, distance, idx, poolWidth = Coord.getCacheWidth(), poolHeight = Coord.getCacheHeight(); Set positions; Coord temp; Radius measurement; RadiusIterator(int x, int y, Radius measurement, int distance) { this.x = x; this.y = y; width = 1 + distance * 2; height = 1 + distance * 2; this.distance = distance; this.measurement = measurement; idx = -1; positions = positionMapping.keySet(); } @Override public boolean hasNext() { if (idx < width * height - 1) { Coord t2; int n = idx; do { n = findNext(n); if (idx < 0) return n >= 0; else { if(x - distance + n % width >= 0 && x - distance + n % width < poolWidth && y - distance + n / width >= 0 && y - distance + n / width < poolHeight && measurement.radius(x, y, x - distance + n % width, y - distance + n / width) <= distance) t2 = Coord.get(x - distance + n % width, y - distance + n / width); else t2 = Coord.get(-1, -1); } } while (!positions.contains(t2)); /* Not done && has next */ return n >= 0; } return false; } @Override public Coord next() { do { idx = findNext(idx); if (idx < 0) throw new NoSuchElementException(); if(x - distance + idx % width >= 0 && x - distance + idx % width < poolWidth && y - distance + idx / width >= 0 && y - distance + idx / width < poolHeight && measurement.radius(x, y, x - distance + idx % width, y - distance + idx / width) <= distance) temp = Coord.get(x - distance + idx % width, y - distance + idx / width); else temp = Coord.get(-1, -1); } while (!positions.contains(temp)); return temp; } @Override public void remove() { throw new UnsupportedOperationException(); } private int findNext(final int idx) { if (idx < 0) { /* First iteration */ return 0; } else { if (idx >= width * height - 1) { /* Done iterating */ return -1; } else { return idx + 1; } } } } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; SpatialMap that = (SpatialMap) o; if (itemMapping != null ? !itemMapping.equals(that.itemMapping) : that.itemMapping != null) return false; return positionMapping != null ? positionMapping.equals(that.positionMapping) : that.positionMapping == null; } @Override public int hashCode() { int result = itemMapping != null ? itemMapping.hashCode() : 0; result = 31 * result + (positionMapping != null ? positionMapping.hashCode() : 0); return result; } @Override public String toString() { return "SpatialMap{" + "itemMapping=" + itemMapping + ", positionMapping=" + positionMapping + '}'; } }