001package squidpony.squidmath;
002
003import squidpony.squidgrid.Radius;
004
005import java.util.ArrayList;
006import java.util.Collections;
007import java.util.HashSet;
008
009/**
010 * This provides a Uniform Poisson Disk Sampling technique that can be used to generate random points that have a
011 * uniform minimum distance between each other. Due to Coord in SquidLib using ints and most Poisson Disk algorithms
012 * using floating-point numbers, some imprecision is to be expected from rounding to the nearest integers x and y.
013 *
014 * The algorithm is from the "Fast Poisson Disk Sampling in Arbitrary Dimensions" paper by Robert Bridson
015 * http://www.cs.ubc.ca/~rbridson/docs/bridson-siggraph07-poissondisk.pdf
016 *
017 * Adapted from C# by Renaud Bedard, which was adapted from Java source by Herman Tulleken
018 * http://theinstructionlimit.com/fast-uniform-poisson-disk-sampling-in-c
019 * Created by Tommy Ettinger on 10/20/2015.
020 */
021public class PoissonDisk {
022    private static final float rootTwo = (float) Math.sqrt(2),
023            pi2 = (float) (Math.PI * 2.0);
024    
025    private static final int defaultPointsPlaced = 10;
026    private static final Radius disk = Radius.CIRCLE;
027
028    private PoissonDisk() {
029    }
030
031    /**
032     * Get a list of Coords, each randomly positioned around the given center out to the given radius (measured with
033     * Euclidean distance, so a true circle), but with the given minimum distance from any other Coord in the list.
034     * The parameters maxX and maxY should typically correspond to the width and height of the map; no points will have
035     * positions with x equal to or greater than maxX and the same for y and maxY; similarly, no points will have
036     * negative x or y.
037     * @param center the center of the circle to spray Coords into
038     * @param radius the radius of the circle to spray Coords into
039     * @param minimumDistance the minimum distance between Coords, in Euclidean distance as a float.
040     * @param maxX one more than the highest x that can be assigned; typically an array length
041     * @param maxY one more than the highest y that can be assigned; typically an array length
042     * @return an ArrayList of Coord that satisfy the minimum distance; the length of the array can vary
043     */
044    public static OrderedSet<Coord> sampleCircle(Coord center, float radius, float minimumDistance,
045                                                int maxX, int maxY)
046    {
047        return sampleCircle(center, radius, minimumDistance, maxX, maxY, defaultPointsPlaced, new StatefulRNG());
048    }
049
050    /**
051     * Get a list of Coords, each randomly positioned around the given center out to the given radius (measured with
052     * Euclidean distance, so a true circle), but with the given minimum distance from any other Coord in the list.
053     * The parameters maxX and maxY should typically correspond to the width and height of the map; no points will have
054     * positions with x equal to or greater than maxX and the same for y and maxY; similarly, no points will have
055     * negative x or y.
056     * @param center the center of the circle to spray Coords into
057     * @param radius the radius of the circle to spray Coords into
058     * @param minimumDistance the minimum distance between Coords, in Euclidean distance as a float.
059     * @param maxX one more than the highest x that can be assigned; typically an array length
060     * @param maxY one more than the highest y that can be assigned; typically an array length
061     * @param pointsPerIteration with small radii, this can be around 5; with larger ones, 30 is reasonable
062     * @param rng an IRNG to use for all random sampling.
063     * @return an ArrayList of Coord that satisfy the minimum distance; the length of the array can vary
064     */
065    public static OrderedSet<Coord> sampleCircle(Coord center, float radius, float minimumDistance,
066                                                int maxX, int maxY, int pointsPerIteration, IRNG rng)
067    {
068        int radius2 = Math.round(radius);
069        return sample(center.translate(-radius2, -radius2), center.translate(radius2, radius2), radius, minimumDistance, maxX, maxY, pointsPerIteration, rng);
070    }
071
072    /**
073     * Get a list of Coords, each randomly positioned within the rectangle between the given minPosition and
074     * maxPosition, but with the given minimum distance from any other Coord in the list.
075     * The parameters maxX and maxY should typically correspond to the width and height of the map; no points will have
076     * positions with x equal to or greater than maxX and the same for y and maxY; similarly, no points will have
077     * negative x or y.
078     * @param minPosition the Coord with the lowest x and lowest y to be used as a corner for the bounding box
079     * @param maxPosition the Coord with the highest x and highest y to be used as a corner for the bounding box
080     * @param minimumDistance the minimum distance between Coords, in Euclidean distance as a float.
081     * @param maxX one more than the highest x that can be assigned; typically an array length
082     * @param maxY one more than the highest y that can be assigned; typically an array length
083     * @return an ArrayList of Coord that satisfy the minimum distance; the length of the array can vary
084     */
085    public static OrderedSet<Coord> sampleRectangle(Coord minPosition, Coord maxPosition, float minimumDistance,
086                                                   int maxX, int maxY)
087    {
088        return sampleRectangle(minPosition, maxPosition, minimumDistance, maxX, maxY, defaultPointsPlaced, new StatefulRNG());
089    }
090
091    /**
092     * Get a list of Coords, each randomly positioned within the rectangle between the given minPosition and
093     * maxPosition, but with the given minimum distance from any other Coord in the list.
094     * The parameters maxX and maxY should typically correspond to the width and height of the map; no points will have
095     * positions with x equal to or greater than maxX and the same for y and maxY; similarly, no points will have
096     * negative x or y.
097     * @param minPosition the Coord with the lowest x and lowest y to be used as a corner for the bounding box
098     * @param maxPosition the Coord with the highest x and highest y to be used as a corner for the bounding box
099     * @param minimumDistance the minimum distance between Coords, in Euclidean distance as a float.
100     * @param maxX one more than the highest x that can be assigned; typically an array length
101     * @param maxY one more than the highest y that can be assigned; typically an array length
102     * @param pointsPerIteration with small areas, this can be around 5; with larger ones, 30 is reasonable
103     * @param rng an IRNG to use for all random sampling.
104     * @return an ArrayList of Coord that satisfy the minimum distance; the length of the array can vary
105     */
106    public static OrderedSet<Coord> sampleRectangle(Coord minPosition, Coord maxPosition, float minimumDistance,
107                                                   int maxX, int maxY, int pointsPerIteration, IRNG rng)
108    {
109        return sample(minPosition, maxPosition, 0f, minimumDistance, maxX, maxY, pointsPerIteration, rng);
110    }
111
112    private static OrderedSet<Coord> sample(Coord minPosition, Coord maxPosition, float rejectionDistance,
113                                           float minimumDistance, int maxX, int maxY, int pointsPerIteration, IRNG rng)
114    {
115
116        Coord center = minPosition.average(maxPosition);
117        Coord dimensions = maxPosition.subtract(minPosition);
118        float cellSize = Math.max(minimumDistance / rootTwo, 0.25f);
119        int gridWidth = (int)(dimensions.x / cellSize) + 1;
120        int gridHeight = (int)(dimensions.y / cellSize) + 1;
121        Coord[][] grid = new Coord[gridWidth][gridHeight];
122        ArrayList<Coord> activePoints = new ArrayList<>();
123        OrderedSet<Coord> points = new OrderedSet<>(128);
124
125        //add first point
126        boolean added = false;
127        while (!added)
128        {
129            float d = rng.nextFloat();
130            int xr = Math.round(minPosition.x + dimensions.x * d);
131
132            d = rng.nextFloat();
133            int yr = Math.round(minPosition.y + dimensions.y * d);
134
135            if (rejectionDistance > 0 && disk.radius(center.x, center.y, xr, yr) > rejectionDistance)
136                continue;
137            added = true;
138            Coord p = Coord.get(Math.min(xr, maxX - 1), Math.min(yr, maxY - 1));
139            Coord index = p.subtract(minPosition).divide(cellSize);
140
141            grid[index.x][index.y] = p;
142
143            activePoints.add(p);
144            points.add(p);
145        }
146        //end add first point
147
148        while (activePoints.size() != 0)
149        {
150            int listIndex = rng.nextInt(activePoints.size());
151
152            Coord point = activePoints.get(listIndex);
153            boolean found = false;
154
155            for (int k = 0; k < pointsPerIteration; k++)
156            {
157                //add next point
158                //get random point around
159                float d = rng.nextFloat();
160                float radius = minimumDistance + minimumDistance * d;
161                float angle = pi2 * rng.nextFloat();
162
163                float newX = radius * NumberTools.sin(angle);
164                float newY = radius * NumberTools.cos(angle);
165                Coord q = point.translateCapped(Math.round(newX), Math.round(newY), maxX, maxY);
166                //end get random point around
167
168                if (q.x >= minPosition.x && q.x <= maxPosition.x &&
169                        q.y >= minPosition.y && q.y <= maxPosition.y &&
170                        (rejectionDistance <= 0 || disk.radius(center.x, center.y, q.x, q.y) <= rejectionDistance))
171                {
172                    Coord qIndex = q.subtract(minPosition).divide((int)Math.ceil(cellSize));
173                    boolean tooClose = false;
174
175                    for (int i = Math.max(0, qIndex.x - 2); i < Math.min(gridWidth, qIndex.x + 3) && !tooClose; i++) {
176                        for (int j = Math.max(0, qIndex.y - 2); j < Math.min(gridHeight, qIndex.y + 3); j++) {
177                            if (grid[i][j] != null && disk.radius(grid[i][j], q) < minimumDistance) {
178                                tooClose = true;
179                                break;
180                            }
181                        }
182                    }
183                    if (!tooClose)
184                    {
185                        found = true;
186                        activePoints.add(q);
187                        points.add(q);
188                        grid[qIndex.x][qIndex.y] = q;
189                    }
190                }
191                //end add next point
192            }
193
194            if (!found)
195            {
196                activePoints.remove(listIndex);
197            }
198        }
199        return points;
200    }
201
202    public static OrderedSet<Coord> sampleMap(char[][] map,
203                                              float minimumDistance, IRNG rng, Character... blocking)
204    {
205        return sampleMap(Coord.get(1, 1), Coord.get(map.length - 2, map[0].length - 2),
206                map, minimumDistance, rng, blocking);
207    }
208
209    public static OrderedSet<Coord> sampleMap(Coord minPosition, Coord maxPosition, char[][] map,
210                                             float minimumDistance, IRNG rng, Character... blocking) {
211        int width = map.length;
212        int height = map[0].length;
213        HashSet<Character> blocked = new HashSet<>();
214        Collections.addAll(blocked, blocking);
215        boolean restricted = false;
216        if (blocked.size() > 0) {
217            restricted = true;
218        }
219        Coord dimensions = maxPosition.subtract(minPosition);
220        float cellSize = Math.max(minimumDistance / rootTwo, 1f);
221        int gridWidth = (int) (dimensions.x / cellSize) + 1;
222        int gridHeight = (int) (dimensions.y / cellSize) + 1;
223        Coord[][] grid = new Coord[gridWidth][gridHeight];
224        ArrayList<Coord> activePoints = new ArrayList<>();
225        OrderedSet<Coord> points = new OrderedSet<>(128);
226
227        //add first point
228
229        Coord p = randomUnblockedTile(minPosition, maxPosition, map, rng, blocked);
230        if (p == null)
231            return points;
232        Coord index = p.subtract(minPosition).divide(cellSize);
233
234        grid[index.x][index.y] = p;
235
236        activePoints.add(p);
237        points.add(p);
238
239        //end add first point
240
241        while (activePoints.size() != 0) {
242            int listIndex = rng.nextInt(activePoints.size());
243
244            Coord point = activePoints.get(listIndex);
245            boolean found = false;
246
247            for (int k = 0; k < 20; k++) {
248                //add next point
249                //get random point around
250                float d = rng.nextFloat();
251                float radius = minimumDistance + minimumDistance * d;
252                d = rng.nextFloat();
253                float angle = pi2 * d;
254
255                float newX = radius * NumberTools.sin(angle);
256                float newY = radius * NumberTools.cos(angle);
257                Coord q = point.translateCapped(Math.round(newX), Math.round(newY), width, height);
258                int frustration = 0;
259                while(restricted && blocked.contains(map[q.x][q.y]) && frustration < 8)
260                {
261                    d = rng.nextFloat();
262                    angle = pi2 * d;
263                    newX = radius * NumberTools.sin(angle);
264                    newY = radius * NumberTools.cos(angle);
265                    q = point.translateCapped(Math.round(newX), Math.round(newY), width, height);
266                    frustration++;
267                }
268
269                //end get random point around
270
271                if (q.x >= minPosition.x && q.x <= maxPosition.x &&
272                        q.y >= minPosition.y && q.y <= maxPosition.y) {
273                    Coord qIndex = q.subtract(minPosition).divide((int) Math.ceil(cellSize));
274                    boolean tooClose = false;
275
276                    for (int i = Math.max(0, qIndex.x - 2); i < Math.min(gridWidth, qIndex.x + 3) && !tooClose; i++) {
277                        for (int j = Math.max(0, qIndex.y - 2); j < Math.min(gridHeight, qIndex.y + 3); j++) {
278                            if (grid[i][j] != null && disk.radius(grid[i][j], q) < minimumDistance) {
279                                tooClose = true;
280                                break;
281                            }
282                        }
283                    }
284                    if (!tooClose) {
285                        found = true;
286                        activePoints.add(q);
287                        if(!restricted || !blocked.contains(map[q.x][q.y]))
288                            points.add(q);
289                        grid[qIndex.x][qIndex.y] = q;
290                    }
291                }
292                //end add next point
293            }
294
295            if (!found)
296                activePoints.remove(listIndex);
297        }
298        return points;
299    }
300    /**
301     * Finds a random Coord where the x and y match up to a [x][y] location on map that has any value not in blocking.
302     * Uses the given IRNG for pseudo-random number generation.
303     * @param minPosition the Coord with the lowest x and lowest y to be used as a corner for the bounding box
304     * @param maxPosition the Coord with the highest x and highest y to be used as a corner for the bounding box
305     * @param map a dungeon map or something, x then y
306     * @param rng a IRNG to generate random choices
307     * @param blocked a Set of Characters that block a tile from being chosen
308     * @return a Coord that corresponds to a map element equal to tile, or null if tile cannot be found or if map is too small.
309     */
310    public static Coord randomUnblockedTile(Coord minPosition, Coord maxPosition, char[][] map, IRNG rng, HashSet<Character> blocked)
311    {
312        int width = map.length;
313        int height = map[0].length;
314        if(width < 3 || height < 3)
315            return null;
316        if(blocked.size() == 0) {
317            return Coord.get(rng.between(minPosition.x, maxPosition.x), rng.between(minPosition.y, maxPosition.y));
318        }
319
320        int x = rng.between(minPosition.x, maxPosition.x), y = rng.between(minPosition.y, maxPosition.y);
321        for(int i = 0; i < (width + height) / 4; i++)
322        {
323            if(!blocked.contains(map[x][y]))
324            {
325                return Coord.get(x, y);
326            }
327            else
328            {
329                x = rng.between(minPosition.x, maxPosition.x);
330                y = rng.between(minPosition.y, maxPosition.y);
331            }
332        }
333        x = 1;
334        y = 1;
335        if(!blocked.contains(map[x][y]))
336            return Coord.get(x, y);
337
338        while(blocked.contains(map[x][y]))
339        {
340            x += 1;
341            if(x >= width - 1)
342            {
343                x = 1;
344                y += 1;
345            }
346            if(y >= height - 1)
347                return null;
348        }
349        return Coord.get(x, y);
350    }
351
352}