001package squidpony.squidmath;
002
003import squidpony.squidgrid.mapping.DungeonUtility;
004
005import java.io.Serializable;
006import java.util.AbstractList;
007import java.util.Collection;
008
009import static squidpony.squidmath.CoordPacker.*;
010/**
011 * Represents an area or series of areas as one logical unit, and allows iterating over or altering that unit.
012 * This can be useful for getting an iterable data structure from a FOV map, Dijkstra map, or just a map from a dungeon
013 * generator. It can also work with some shapes (circles and rectangles).
014 * Regions must be no larger than 256x256 (actually, the Coords in them should fit in that), even if Coord has had its
015 * expandPool() method called. This is because CoordPacker relies on that maximum size to significantly improve
016 * efficiency, and this is really just a convenience class wrapping CoordPacker to avoid some of its complexity.
017 * More features are likely to be ported into Region as they are added to CoordPacker.
018 * Created by Tommy Ettinger on 5/12/2016.
019 */
020public class Region extends AbstractList<Coord> implements Serializable{
021
022    private static final long serialVersionUID = 4015272367863327093L;
023
024    protected short[] raw;
025    protected Coord[] coords;
026    private boolean dirty;
027
028    /**
029     * A constructor for a Region that takes a 2D double array, usually the kind produced by FOV, and stores only Coord
030     * positions that correspond to values greater than 0.0 (actually, greater than epsilon, which here is 0.0001).
031     * This won't behave as-expected if you give it a double[][] that DijkstraMap produces; there's a different
032     * constructor for that purpose.
033     * @param fovMap a 2D double array as produced by FOV
034     */
035    public Region(double[][] fovMap)
036    {
037        this(pack(fovMap));
038    }
039
040    /**
041     * A constructor for a Region that takes a 2D double array, usually produced by DijkstraMap, and a maximum value,
042     * and stores only Coord positions that correspond to values no greater than maximum.
043     * This won't behave as-expected if you give it a double[][] that FOV produces; there's a different
044     * constructor for that purpose.
045     * @param dijkstraMap a 2D double array as produced by DijkstraMap
046     * @param maximum the highest value that a position can have in dijkstraMap and still be given a Coord in this
047     */
048    public Region(double[][] dijkstraMap, double maximum)
049    {
050        this(pack(dijkstraMap, maximum));
051    }
052
053    /**
054     * A constructor for a Region that takes a 2D char array, the kind produced by most map/dungeon generators in this
055     * library, and a vararg or array of char that will have their Coord positions used where those chars appear in map.
056     * This is optimized for the common case of a single char in using if you only want to, for example, store '.' to
057     * make a Region of floors, or '#' for walls.
058     * @param map a 2D char array that is usually the kind returned by a dungeon or map generator
059     * @param using an array or vararg of char that will have their Coords used where they appear in map
060     */
061    public Region(char[][] map, char... using)
062    {
063        this(pack(map, using));
064    }
065
066    /**
067     * A constructor for a Region that takes an array or vararg of Coord and encodes all of them in the Region.
068     * @param points an array or vararg of Coord that will be stored in this Region, none can be null
069     */
070    public Region(Coord... points)
071    {
072        this(packSeveral(points));
073    }
074    /**
075     * A constructor for a Region that takes a Collection of Coord, such as a List or Set, and encodes all of them in
076     * the Region.
077     * @param points a Collection of Coord that will be stored in this Region, none can be null
078     */
079    public Region(Collection<Coord> points)
080    {
081        this(packSeveral(points));
082    }
083
084    /**
085     * A constructor that copies another Region so this Region will have the same contents. If the other Region is
086     * "dirty", this won't perform "cleanup" on it, but will ensure that this Region is "clean" at creation.
087     * None of the reference types in other will be used directly in this Region, so changes made to this Region won't
088     * be reflected in other.
089     * @param other another Region to copy into this one
090     */
091    public Region(Region other)
092    {
093        raw = new short[other.raw.length];
094        System.arraycopy(other.raw, 0, raw, 0, raw.length);
095        if(other.dirty)
096        {
097            coords = allPacked(raw);
098        }
099        else
100        {
101            coords = new Coord[other.coords.length];
102            System.arraycopy(other.coords, 0, coords, 0, coords.length);
103        }
104        dirty = false;
105
106    }
107
108    /**
109     * A constructor for a circular Region (possibly truncated at the edges) with a Coord center, an int radius, and a
110     * maximum width and height that the Coords in this Region will not exceed.
111     * @param center the center of the circular Region
112     * @param circleRadius the radius as an int
113     * @param mapWidth one more than the maximum x-position of any Coord this will contain
114     * @param mapHeight one more than the maximum y-position of any Coord this will contain
115     */
116    public Region(Coord center, int circleRadius, int mapWidth, int mapHeight)
117    {
118        this(circle(center, circleRadius, mapWidth, mapHeight));
119    }
120
121    /**
122     * A constructor for a rectangular Region that stores Coords for the area from (minX,minY) at the minimum corner to
123     * (width + minX - 1, height + minY - 1) at the maximum corner. All parameters should be non-negative and less than
124     * 256, and height and width will be reduced if a Coord in the rectangle would extend to 256 in either dimension.
125     * This doesn't take two Coords as parameters because that would be confusing with the constructor that takes a
126     * vararg or array of Coord for its parameters.
127     * @param minX lowest x-coordinate in the rectangle; should be between 0 and 255
128     * @param minY lowest y-coordinate in the rectangle; should be between 0 and 255
129     * @param width total width of the rectangle; must be non-negative
130     * @param height total height of the rectangle; must be non-negative
131     */
132    public Region(int minX, int minY, int width, int height)
133    {
134        this(rectangle(minX, minY, width, height));
135    }
136    /**
137     * A constructor for a Region that takes a specifically-formatted short array (packed data), as produced by
138     * CoordPacker or sometimes other classes, like RegionMap or RoomFinder. If you don't have such data, the other
139     * constructors are recommended instead.
140     * <br>
141     * Note: arrays of Hilbert indices are occasionally returned in CoordPacker as a different kind of short array, but
142     * those methods are always noted as such and those short arrays won't make sense if passed to this constructor.
143     * They may result in a technically-valid Region with random-seeming contents. In general, if a method in
144     * CoordPacker returns a short array (most of them do), but the name contains Hilbert, it may return the wrong kind
145     * (an array of Hilbert indices is wrong, packed data is right); check the documentation for that method.
146     * @param packedData a short array as produced by CoordPacker (usually), or sometimes RoomFinder or RegionMap
147     */
148    public Region(short[] packedData)
149    {
150        raw = new short[packedData.length];
151        System.arraycopy(packedData, 0, raw, 0, packedData.length);
152        coords = allPacked(raw);
153        dirty = false;
154    }
155
156    /**
157     * Gets a single random Coord from this using the given RNG (which can be seeded); returns null if this is empty.
158     * @param rng the source of random numbers used to get a random Coord from this Region
159     * @return a random Coord in this Region, or null if this is empty
160     */
161    public Coord getRandomCoord(RNG rng)
162    {
163        if(CoordPacker.isEmpty(raw))
164            return null;
165        return singleRandom(raw, rng);
166    }
167
168    /**
169     * Takes this region and walks through its Coords in chunks with length equal to separation, creating a new Region
170     * where one Coord in each chunk is kept and the others are discarded.
171     * @param separation an int where higher numbers mean there will be more distance between Coords, and fewer total
172     * @return a new Region made of the separated Coords
173     */
174    public Region separated(int separation)
175    {
176        return new Region(fractionPacked(raw, separation));
177    }
178
179    /**
180     * Takes this region and walks through its Coords in chunks with length equal to separation, creating a new Region
181     * where one randomly-chosen Coord in each chunk is kept and the others are discarded.
182     * @param separation an int where higher numbers mean there will be more distance between Coords, and fewer total
183     * @param rng the source of random numbers used to randomize Coords used, removing any noticeable pattern
184     * @return a new Region made of the separated Coords
185     */
186    public Region randomSeparated(int separation, RNG rng)
187    {
188        return new Region(CoordPacker.randomSeparated(raw, separation, rng));
189    }
190
191    /**
192     * Gets a representation of this Region as a 2D boolean array with the given width and height. If this contains a
193     * Coord with x and y less than width and height, that position will be true in the 2D array this returns, otherwise
194     * it will be false.
195     * @param width the width of the 2D array to return
196     * @param height the height of the 2D array to return
197     * @return a 2D boolean array where present Coords in this Region will be true, and everything else will be false
198     */
199    public boolean[][] toBooleanArray(int width, int height)
200    {
201        return CoordPacker.unpack(raw, width, height);
202    }
203
204    /**
205     * Gets a representation of this Region as a 2D char array with the given width and height, using on to represent
206     * present Coords and off to represent absent ones. If this contains a Coord with x and y less than width and
207     * height, that position will be equal to on in the 2D array this returns, otherwise it will be equal to off.
208     * @param width the width of the 2D array to return
209     * @param height the height of the 2D array to return
210     * @param on the char to use when a Coord is present in this Region
211     * @param off the char to use where no Coord is present in this Region
212     * @return a 2D char array where present Coords in this Region will be on, and everything else will be off
213     */
214    public char[][] toCharArray(int width, int height, char on, char off)
215    {
216        return CoordPacker.unpackChar(raw, width, height, on, off);
217    }
218
219    /**
220     * Gets the Coord stored at the given index in this Region, or null if index is out of bounds.
221     * The ordering this class uses may seem unusual, but it is predictable, using the pattern a Hilbert Curve takes to
222     * wind through a 256x256 space (at its maximum). Any two given Coords will always have the same before-after
223     * relationship, regardless of other Coords in the Region. A Region is a dense data structure, like a List or array,
224     * so valid indices shouldn't ever return null.
225     * @param index the index into this Region to get a Coord at; should be less than size() and non-negative.
226     * @return the Coord this Region holds at index, if index is valid; otherwise null
227     */
228    @Override
229    public Coord get(int index) {
230        if(dirty)
231        {
232            coords = allPacked(raw);
233            dirty = false;
234        }
235        if(index >= 0 && index < coords.length)
236            return coords[index];
237        return null;
238    }
239
240    /**
241     * Gets the size of this Region as measured in Coords stored.
242     * Performs "cleanup" if the Region is "dirty," even though this doesn't specifically request a Coord. As such, it
243     * may take longer than a call to size() might be expected to, but only just after a "dirtying" method was called.
244     * @return the number of Coords stored in this Region.
245     */
246    @Override
247    public int size() {
248        if(dirty)
249        {
250            coords = allPacked(raw);
251            dirty = false;
252        }
253        return coords.length;
254    }
255
256    /**
257     * Returns true if there are no Coords in this Region, or false otherwise. Can be more efficient than checking if
258     * size() is greater than 0 because it doesn't depend on the "dirty or clean" state, and can quickly check.
259     * @return
260     */
261    @Override
262    public boolean isEmpty() {
263        return CoordPacker.isEmpty(raw);
264    }
265
266    /**
267     * Adds a Coord to this Region, returning false if the Coord is null or already included in this, or true otherwise.
268     * Causes the Region to be considered "dirty", which will make anything that gets a Coord from this to perform some
269     * "cleanup" on the Coords this holds when a Coord is requested. You can perform multiple "dirtying" operations in
270     * succession without needing more "cleanups" than the one when a Coord is next requested.
271     * @param coord a Coord to add to this region
272     * @return true if the Coord was added and was not already present; false if the Coord as null or already present
273     */
274    @Override
275    public boolean add(Coord coord) {
276        if(coord == null || queryPacked(raw, coord.x, coord.y))
277            return false;
278        raw = insertPacked(raw, coord.x, coord.y);
279        dirty = true;
280        return true;
281    }
282
283    /**
284     * Gets a direct reference to this Region's "raw packed data"; don't modify it unless you know what you're doing!
285     * It's fine to pass this to methods in CoordPacker, since essentially all of those methods won't modify packed data
286     * given as arguments.
287     * @return the raw packed data this class uses; should not be modified carelessly
288     */
289    public short[] getRaw() {
290        return raw;
291    }
292
293    /**
294     * Sets the "raw packed data" to the given short array, as generated by CoordPacker or some parts of RegionMap.
295     * This hopefully won't need to be consumed too much externally, but is an important bridge between this and
296     * CoordPacker's API, which deals mostly with these special short arrays.
297     * Causes the Region to be considered "dirty", which will make anything that gets a Coord from this to perform some
298     * "cleanup" on the Coords this holds when a Coord is requested. You can perform multiple "dirtying" operations in
299     * succession without needing more "cleanups" than the one when a Coord is next requested.
300     * @param raw a short array of packed data; has a very specific format and should usually not be made manually
301     */
302    public void setRaw(short[] raw) {
303        this.raw = raw;
304        dirty = true;
305    }
306
307    /**
308     * Gets the Coords contained in this as an array, a direct reference that does permit modifying the Coords in your
309     * own code without altering "dirty/clean" status. This method does "cleanup" in itself if necessary, but once the
310     * Coords are returned you can change them at-will. The Coords may not reflect changes made by this object if it
311     * creates a new Coord array, as it often does.
312     * @return the Coords contained in this Region as an array
313     */
314    public Coord[] getCoords() {
315        if(dirty)
316        {
317            coords = allPacked(raw);
318            dirty = false;
319        }
320        return coords;
321    }
322
323    /**
324     * Changes this Region to include the given Coords and disregard its previous contents. If any elements of coords
325     * are null, this Region will hold no Coords (a sort of escape hatch to avoid throwing an exception, since all other
326     * methods in this class fail on null entries without potentially crashing a program).
327     * @param coords an array or vararg of Coord that will be used as the entirety of this Region
328     */
329    public void setCoords(Coord... coords) {
330        if (coords == null)
331            return;
332        raw = packSeveral(coords);
333        if (raw == ALL_WALL) {
334            this.coords = new Coord[0];
335            dirty = false;
336        } else {
337            this.coords = new Coord[coords.length];
338            System.arraycopy(coords, 0, this.coords, 0, coords.length);
339            dirty = false;
340        }
341    }
342
343    /**
344     * Prints this Region to System.out as a grid of chars with the given width and height, using '.' for Coords this
345     * contains and '#' for empty space. Consider using toCharArray() instead if you need more customization for what
346     * you want printed.
347     * @param width the width in chars of the grid to print
348     * @param height the height in chars of the grid to print
349     */
350    public void debugPrint(int width, int height)
351    {
352        DungeonUtility.debugPrint(toCharArray(width, height, '.', '#'));
353    }
354}