001package squidpony.squidai;
002
003import squidpony.squidgrid.Direction;
004import squidpony.squidgrid.Radius;
005import squidpony.squidmath.Coord;
006import squidpony.squidmath.CoordPacker;
007import squidpony.squidmath.OrderedSet;
008import squidpony.squidmath.ShortVLA;
009
010import java.io.Serializable;
011
012/**
013 * Calculates the Zone of Influence, also known as Zone of Control, for different points on a map.
014 * Uses CoordPacker for more memory-efficient storage and manipulation of zones; it's recommended if you use this class
015 * to be somewhat familiar with the methods for manipulating packed data in that class. This class is very similar in API and implementation to {@link ZOI}, but should be
016 * slightly faster on large maps at the expense of usually using more memory. The main reason to choose between ZOI and
017 * GreasedZOI is whether your existing code uses GreasedRegions, like GreasedZOI, or uses CoordPacker, like this class.
018 * If you don't currently use either, GreasedZOI is probably preferable because the {@link GreasedZOI#calculate()}
019 * method produces a value that can be reasonably consumed by Collection-based APIs, while {@link #calculate()} produces
020 * a harder-to-use {@code short[]} that must be read by CoordPacker; GreasedZOI is probably not significantly faster for
021 * most applications, and the memory usage difference is probably under a megabyte.
022 * <br>
023 * Created by Tommy Ettinger on 10/27/2015.
024 */
025public class ZOI implements Serializable {
026    private static final long serialVersionUID = 1L;
027    private char[][] map;
028    private DijkstraMap dijkstra;
029    private Coord[][] influences;
030    private short[][] packedGroups;
031    private boolean completed;
032    private Radius radius;
033    /**
034     * Constructs a Zone of Influence map. Takes a (quite possibly jagged) array of arrays of Coord influences, where
035     * the elements of the outer array represent different groups of influencing "factions" or groups that exert control
036     * over nearby areas, and the Coord elements of the inner array represent individual spots that are part of those
037     * groups and share influence with all Coord in the same inner array. Also takes a char[][] for a map, which can be
038     * the simplified map with only '#' for walls and '.' for floors, or the final map (with chars like '~' for deep
039     * water as well as walls and floors), and a Radius enum that will be used to determine how distance is calculated.
040     * <br>
041     * Call calculate() when you want information out of this.
042     * @param influences an outer array containing influencing groups, each an array containing Coords that influence
043     * @param map a char[][] that is used as a map; should be bounded
044     * @param radiusStrategy a Radius enum that corresponds to how distance should be measured
045     */
046    public ZOI(Coord[][] influences, char[][] map, Radius radiusStrategy) {
047        CoordPacker.init();
048        this.influences = influences;
049        packedGroups = new short[influences.length][];
050        this.map = map;
051        radius = radiusStrategy == null ? Radius.CIRCLE : radiusStrategy;
052        dijkstra = new DijkstraMap(map, radius.matchingMeasurement());
053    }
054    /**
055     * Constructs a Zone of Influence map. Takes an arrays of Coord influences, where each Coord is treated as both a
056     * one-element group of influencing "factions" or groups that exert control over nearby areas, and the individual
057     * spot that makes up one of those groups and spreads influence. Also takes a char[][] for a map, which can be the
058     * simplified map with only '#' for walls and '.' for floors, or the final map (with chars like '~' for deep water
059     * as well as walls and floors), and a Radius enum that will be used to determine how distance is calculated.
060     * <br>
061     * Essentially, this is the same as constructing a ZOI with a Coord[][] where each inner array has only one element.
062     * <br>
063     * Call calculate() when you want information out of this.
064     * @param influences an array containing Coords that each have their own independent influence
065     * @param map a char[][] that is used as a map; should be bounded
066     * @param radiusStrategy a Radius enum that corresponds to how distance should be measured
067     * @see squidpony.squidmath.PoissonDisk for a good way to generate evenly spaced Coords that can be used here
068     */
069    public ZOI(Coord[] influences, char[][] map, Radius radiusStrategy) {
070        CoordPacker.init();
071        this.influences = new Coord[influences.length][];
072        for (int i = 0; i < influences.length; i++) {
073            this.influences[i] = new Coord[] { influences[i] };
074        }
075        packedGroups = new short[influences.length][];
076        this.map = map;
077        radius = radiusStrategy == null ? Radius.CIRCLE : radiusStrategy;
078        dijkstra = new DijkstraMap(map, radius.matchingMeasurement());
079    }
080
081    /**
082     * Finds the zones of influence for each of the influences (inner arrays of Coord) this was constructed with, and
083     * returns them as packed data (using CoordPacker, which can also be used to unpack the data, merge zones, get
084     * shared borders, and all sorts of other tricks). This has each zone of influence overlap with its neighbors; this
085     * is useful to find borders using CoordPacker.intersectPacked(), and borders are typically between 1 and 2 cells
086     * wide. You can get a different region as packed data if you want region A without the overlapping areas it shares
087     * with region B by using {@code short[] different = CoordPacker.differencePacked(A, B)}. Merging two zones A and B
088     * can be done with {@code short[] merged = CoordPacker.unionPacked(A, B)} . You can unpack the data
089     * into a boolean[][] easily with CoordPacker.unpack(), where true is contained in the zone and false is not.
090     * The CoordPacker methods fringe(), expand(), singleRandom(), randomSample(), and randomPortion() are also
091     * potentially useful for this sort of data. You should save the short[][] for later use if you want to call
092     * nearestInfluences() in this class.
093     * <br>
094     * The first short[] in the returned short[][] will correspond to the area influenced by the first Coord[] in the
095     * nested array passed to the constructor (or the first Coord if a non-nested array was passed); the second will
096     * correspond to the second, and so on. The length of the short[][] this returns will equal the number of influence
097     * groups.
098     * @return an array of short[] storing the zones' areas; each can be used as packed data with CoordPacker
099     */
100    public short[][] calculate()
101    {
102        for (int i = 0; i < influences.length; i++) {
103            for (int j = 0; j < influences[i].length; j++) {
104                dijkstra.setGoal(influences[i][j]);
105            }
106        }
107        dijkstra.scan(null, null);
108        final double[][] scannedAll = dijkstra.gradientMap;
109
110        for (int i = 0; i < influences.length; i++) {
111
112            /*
113            dijkstra.clearGoals();
114            dijkstra.resetMap();
115            for (int j = 0; j < influences[i].length; j++) {
116                dijkstra.setGoal(influences[i][j]);
117            }
118            double[][] factionScanned = dijkstra.scan(null);
119            for (int y = 0; y < map[0].length; y++) {
120                for (int x = 0; x < map.length; x++) {
121                    influenced[x][y] = (scannedAll[x][y] < DijkstraMap.FLOOR) &&
122                            (factionScanned[x][y] - scannedAll[x][y] <= 1);
123                }
124            }*/
125            packedGroups[i] = CoordPacker.pack(increasing(scannedAll, influences[i]));
126        }
127        completed = true;
128        return packedGroups;
129    }
130    protected boolean[][] increasing(double[][] dm, Coord[] inf) {
131        OrderedSet<Coord> open = new OrderedSet<>(inf), fresh = new OrderedSet<>(64);
132        Direction[] dirs = (radius.equals2D(Radius.DIAMOND)) ? Direction.CARDINALS : Direction.OUTWARDS;
133        boolean[][] influenced = new boolean[map.length][map[0].length];
134
135        final int width = dm.length;
136        final int height = width == 0 ? 0 : dm[0].length;
137
138        int numAssigned = open.size();
139        double diff;
140        while (numAssigned > 0) {
141            numAssigned = 0;
142            for (Coord cell : open) {
143                influenced[cell.x][cell.y] = true;
144                for (int d = 0; d < dirs.length; d++) {
145                    Coord adj = cell.translate(dirs[d].deltaX, dirs[d].deltaY);
146                    if (adj.x < 0 || adj.y < 0 || width <= adj.x || height <= adj.y)
147                        /* Outside the map */
148                        continue;
149                    if (!open.contains(adj) && dm[adj.x][adj.y] < DijkstraMap.FLOOR && !influenced[adj.x][adj.y]) {
150                        //h = heuristic(dirs[d]);
151                        diff = dm[adj.x][adj.y] - dm[cell.x][cell.y];
152                        if (diff <= 1.0 && diff >= 0) {
153                            fresh.add(adj);
154                            influenced[adj.x][adj.y] = true;
155                            ++numAssigned;
156                        }
157                    }
158                }
159            }
160
161            open = new OrderedSet<>(fresh);
162            fresh.clear();
163        }
164
165        return influenced;
166    }
167
168    /**
169     * Given the zones resulting from this class' calculate method and a Coord to check, finds the indices of all
170     * influencing groups in zones that have the Coord in their area, and returns all such indices as an int array.
171     * @param zones a short[][] returned by calculate; not a multi-packed short[][] from CoordPacker !
172     * @param point the Coord to test
173     * @return an int[] where each element is the index of an influencing group in zones
174     */
175    public int[] nearestInfluences(short[][] zones, Coord point)
176    {
177        ShortVLA found = new ShortVLA(4);
178        for (short i = 0; i < zones.length; i++) {
179            if(CoordPacker.queryPacked(zones[i], point.x, point.y))
180                found.add(i);
181        }
182        return found.asInts();
183    }
184    /**
185     * This can be given a Coord to check in the results of the latest calculate() call. Finds the indices of all
186     * influencing groups in zones that have the Coord in their area, and returns all such indices as an int array.
187     * @param point the Coord to test
188     * @return an int[] where each element is the index of an influencing group in zones
189     */
190    public int[] nearestInfluences(Coord point)
191    {
192        if(!completed)
193            return new int[0];
194        ShortVLA found = new ShortVLA(4);
195        for (short i = 0; i < packedGroups.length; i++) {
196            if(CoordPacker.queryPacked(packedGroups[i], point.x, point.y))
197                found.add(i);
198        }
199        return found.asInts();
200    }
201
202    /**
203     * Gets the influencing groups; ideally the result should not be changed without setting it back with setInfluences.
204     * @return influences a jagged array of Coord arrays, where the inner arrays are groups of influences
205     */
206    public Coord[][] getInfluences() {
207        return influences;
208    }
209
210    /**
211     * Changes the influencing groups. This also invalidates the last calculation for the purposes of nearestInfluences,
212     * at least for the overload that takes only a Coord.
213     * @param influences a jagged array of Coord arrays, where the inner arrays are groups of influences
214     */
215    public void setInfluences(Coord[][] influences) {
216        this.influences = influences;
217        packedGroups = new short[influences.length][];
218        completed = false;
219    }
220}