├── .gitattributes ├── .gitignore ├── README.txt ├── pom.xml └── src └── main └── java └── com └── hoten └── delaunay ├── examples ├── TestDriver.java └── TestGraphImpl.java ├── geom ├── GenUtils.java ├── Point.java └── Rectangle.java └── voronoi ├── Center.java ├── Corner.java ├── Edge.java ├── VoronoiGraph.java ├── groundshapes ├── Blob.java ├── HeightAlgorithm.java ├── Perlin.java └── Radial.java └── nodename └── as3delaunay ├── Circle.java ├── Edge.java ├── EdgeList.java ├── EdgeReorderer.java ├── Halfedge.java ├── HalfedgePriorityQueue.java ├── ICoord.java ├── IDisposable.java ├── LR.java ├── LineSegment.java ├── Polygon.java ├── Site.java ├── SiteList.java ├── Triangle.java ├── Vertex.java ├── Voronoi.java └── Winding.java /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | *.sln merge=union 7 | *.csproj merge=union 8 | *.vbproj merge=union 9 | *.fsproj merge=union 10 | *.dbproj merge=union 11 | 12 | # Standard to msysgit 13 | *.doc diff=astextplain 14 | *.DOC diff=astextplain 15 | *.docx diff=astextplain 16 | *.DOCX diff=astextplain 17 | *.dot diff=astextplain 18 | *.DOT diff=astextplain 19 | *.pdf diff=astextplain 20 | *.PDF diff=astextplain 21 | *.rtf diff=astextplain 22 | *.RTF diff=astextplain 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ################# 2 | ## Eclipse 3 | ################# 4 | 5 | *.pydevproject 6 | .project 7 | .metadata 8 | bin/ 9 | tmp/ 10 | *.tmp 11 | *.bak 12 | *.swp 13 | *~.nib 14 | local.properties 15 | .classpath 16 | .settings/ 17 | .loadpath 18 | 19 | # External tool builders 20 | .externalToolBuilders/ 21 | 22 | # Locally stored "Eclipse launch configurations" 23 | *.launch 24 | 25 | # CDT-specific 26 | .cproject 27 | 28 | # PDT-specific 29 | .buildpath 30 | 31 | 32 | ################# 33 | ## IntelliJ IDEA 34 | ################# 35 | 36 | .idea 37 | *.iml 38 | 39 | ################# 40 | ## Visual Studio 41 | ################# 42 | 43 | ## Ignore Visual Studio temporary files, build results, and 44 | ## files generated by popular Visual Studio add-ons. 45 | 46 | # User-specific files 47 | *.suo 48 | *.user 49 | *.sln.docstates 50 | 51 | # Build results 52 | 53 | [Dd]ebug/ 54 | [Rr]elease/ 55 | x64/ 56 | build/ 57 | [Bb]in/ 58 | [Oo]bj/ 59 | 60 | # MSTest test Results 61 | [Tt]est[Rr]esult*/ 62 | [Bb]uild[Ll]og.* 63 | 64 | *_i.c 65 | *_p.c 66 | *.ilk 67 | *.meta 68 | *.obj 69 | *.pch 70 | *.pdb 71 | *.pgc 72 | *.pgd 73 | *.rsp 74 | *.sbr 75 | *.tlb 76 | *.tli 77 | *.tlh 78 | *.tmp 79 | *.tmp_proj 80 | *.log 81 | *.vspscc 82 | *.vssscc 83 | .builds 84 | *.pidb 85 | *.log 86 | *.scc 87 | 88 | # Visual C++ cache files 89 | ipch/ 90 | *.aps 91 | *.ncb 92 | *.opensdf 93 | *.sdf 94 | *.cachefile 95 | 96 | # Visual Studio profiler 97 | *.psess 98 | *.vsp 99 | *.vspx 100 | 101 | # Guidance Automation Toolkit 102 | *.gpState 103 | 104 | # ReSharper is a .NET coding add-in 105 | _ReSharper*/ 106 | *.[Rr]e[Ss]harper 107 | 108 | # TeamCity is a build add-in 109 | _TeamCity* 110 | 111 | # DotCover is a Code Coverage Tool 112 | *.dotCover 113 | 114 | # NCrunch 115 | *.ncrunch* 116 | .*crunch*.local.xml 117 | 118 | # Installshield output folder 119 | [Ee]xpress/ 120 | 121 | # DocProject is a documentation generator add-in 122 | DocProject/buildhelp/ 123 | DocProject/Help/*.HxT 124 | DocProject/Help/*.HxC 125 | DocProject/Help/*.hhc 126 | DocProject/Help/*.hhk 127 | DocProject/Help/*.hhp 128 | DocProject/Help/Html2 129 | DocProject/Help/html 130 | 131 | # Click-Once directory 132 | publish/ 133 | 134 | # Publish Web Output 135 | *.Publish.xml 136 | *.pubxml 137 | 138 | # NuGet Packages Directory 139 | ## TODO: If you have NuGet Package Restore enabled, uncomment the next line 140 | #packages/ 141 | 142 | # Windows Azure Build Output 143 | csx 144 | *.build.csdef 145 | 146 | # Windows Store app package directory 147 | AppPackages/ 148 | 149 | # Others 150 | sql/ 151 | *.Cache 152 | ClientBin/ 153 | [Ss]tyle[Cc]op.* 154 | ~$* 155 | *~ 156 | *.dbmdl 157 | *.[Pp]ublish.xml 158 | *.pfx 159 | *.publishsettings 160 | 161 | # RIA/Silverlight projects 162 | Generated_Code/ 163 | 164 | # Backup & report files from converting an old project file to a newer 165 | # Visual Studio version. Backup files are not needed, because we have git ;-) 166 | _UpgradeReport_Files/ 167 | Backup*/ 168 | UpgradeLog*.XML 169 | UpgradeLog*.htm 170 | 171 | # SQL Server files 172 | App_Data/*.mdf 173 | App_Data/*.ldf 174 | 175 | ############# 176 | ## Windows detritus 177 | ############# 178 | 179 | # Windows image file caches 180 | Thumbs.db 181 | ehthumbs.db 182 | 183 | # Folder config file 184 | Desktop.ini 185 | 186 | # Recycle Bin used on file shares 187 | $RECYCLE.BIN/ 188 | 189 | # Mac crap 190 | .DS_Store 191 | 192 | 193 | ############# 194 | ## Python 195 | ############# 196 | 197 | *.py[co] 198 | 199 | # Packages 200 | *.egg 201 | *.egg-info 202 | dist/ 203 | build/ 204 | eggs/ 205 | parts/ 206 | var/ 207 | sdist/ 208 | develop-eggs/ 209 | .installed.cfg 210 | 211 | # Installer logs 212 | pip-log.txt 213 | 214 | # Unit test / coverage reports 215 | .coverage 216 | .tox 217 | 218 | #Translations 219 | *.mo 220 | 221 | #Mr Developer 222 | .mr.developer.cfg 223 | /target/ -------------------------------------------------------------------------------- /README.txt: -------------------------------------------------------------------------------- 1 | /* 2 | * Java implementation by Connor Clark (www.hotengames.com). Pretty much a 1:1 3 | * translation of a wonderful map generating algorthim by Amit Patel of Red Blob Games, 4 | * which can be found here (http://www-cs-students.stanford.edu/~amitp/game-programming/polygon-map-generation/) 5 | * Hopefully it's of use to someone out there who needed it in Java like I did! 6 | * 7 | * FORTUNE'S ALGORTIHIM 8 | * 9 | * This is a java implementation of an AS3 (Flash) implementation of an algorthim 10 | * originally created in C++. Pretty much a 1:1 translation from as3 to java, save 11 | * for some necessary workarounds. Original as3 implementation by Alan Shaw (of nodename) 12 | * can be found here (https://github.com/nodename/as3delaunay). Original algorthim 13 | * by Steven Fortune (see lisence for c++ implementation below) 14 | * 15 | * The author of this software is Steven Fortune. Copyright (c) 1994 by AT&T 16 | * Bell Laboratories. 17 | * Permission to use, copy, modify, and distribute this software for any 18 | * purpose without fee is hereby granted, provided that this entire notice 19 | * is included in all copies of any software which is or includes a copy 20 | * or modification of this software and in all copies of the supporting 21 | * documentation for such software. 22 | * THIS SOFTWARE IS BEING PROVIDED "AS IS", WITHOUT ANY EXPRESS OR IMPLIED 23 | * WARRANTY. IN PARTICULAR, NEITHER THE AUTHORS NOR AT&T MAKE ANY 24 | * REPRESENTATION OR WARRANTY OF ANY KIND CONCERNING THE MERCHANTABILITY 25 | * OF THIS SOFTWARE OR ITS FITNESS FOR ANY PARTICULAR PURPOSE. 26 | */ 27 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | com.hoten.delaunay 5 | java-delaunay 6 | 0.1 7 | jar 8 | 9 | 10 | 11 | org.apache.maven.plugins 12 | maven-compiler-plugin 13 | 2.3.2 14 | 15 | false 16 | 17 | 18 | 19 | 20 | 21 | 22 | junit 23 | junit 24 | 4.10 25 | test 26 | 27 | 28 | 29 | UTF-8 30 | 1.8 31 | 1.8 32 | 33 | -------------------------------------------------------------------------------- /src/main/java/com/hoten/delaunay/examples/TestDriver.java: -------------------------------------------------------------------------------- 1 | package com.hoten.delaunay.examples; 2 | 3 | import com.hoten.delaunay.voronoi.VoronoiGraph; 4 | import com.hoten.delaunay.voronoi.groundshapes.*; 5 | import com.hoten.delaunay.voronoi.nodename.as3delaunay.Voronoi; 6 | 7 | import javax.imageio.ImageIO; 8 | import javax.swing.*; 9 | import java.awt.*; 10 | import java.awt.event.MouseAdapter; 11 | import java.awt.event.MouseEvent; 12 | import java.awt.event.MouseWheelEvent; 13 | import java.awt.image.BufferedImage; 14 | import java.io.File; 15 | import java.io.IOException; 16 | import java.util.HashMap; 17 | import java.util.Random; 18 | import java.util.regex.Matcher; 19 | import java.util.regex.Pattern; 20 | 21 | /** 22 | *

How to use:

23 | * Just change constants to customize graph, it's shape and image. 24 | */ 25 | public class TestDriver { 26 | 27 | /** Do you really need to save image? */ 28 | private static final boolean SAVE_FILE = false; 29 | 30 | /** The side of the square in which the graph will be fitted. */ 31 | private static final int GRAPH_BOUNDS = 2048; 32 | 33 | /** Size of image which will be drawn. */ 34 | private static final int FRAME_BOUNDS = 512; 35 | 36 | /** Number of pieces for the graph. */ 37 | private static final int SITES_AMOUNT = 8_000; 38 | 39 | /** 40 | * Each time a relaxation step is performed, the points are left in a slightly more even distribution: 41 | * closely spaced points move farther apart, and widely spaced points move closer together. 42 | */ 43 | private static final int LLOYD_RELAXATIONS = 2; 44 | 45 | /** Randomizing number. Use it with {@link #RANDOM_SEED} = false to get same image every time. */ 46 | private static long SEED = 123L; 47 | 48 | /** You can make it false if you want to check some changes in code or image/graph size. */ 49 | private static final boolean RANDOM_SEED = true; 50 | 51 | /** Random, radial, blob, etc. See {@link #getAlgorithmImplementation(Random, String)} */ 52 | private static final String ALGORITHM = "perlin"; 53 | 54 | public static void main(String[] args) throws IOException { 55 | if (RANDOM_SEED) SEED = System.nanoTime(); 56 | 57 | printInfo(); 58 | 59 | final BufferedImage img = createVoronoiGraph(GRAPH_BOUNDS, SITES_AMOUNT, LLOYD_RELAXATIONS, SEED, ALGORITHM).createMap(); 60 | 61 | saveFile(img); 62 | 63 | showGraph(img); 64 | } 65 | 66 | private static void printInfo() { 67 | System.out.println("Seed: " + SEED); 68 | System.out.println("Bounds: " + GRAPH_BOUNDS); 69 | System.out.println("Sites: " + SITES_AMOUNT); 70 | System.out.println("Shape: " + ALGORITHM); 71 | System.out.println("Relaxs: " + LLOYD_RELAXATIONS); 72 | System.out.println("============================="); 73 | } 74 | 75 | public static VoronoiGraph createVoronoiGraph(int bounds, int numSites, int numLloydRelaxations, long seed, String algorithmName) { 76 | final Random r = new Random(seed); 77 | HeightAlgorithm algorithm = getAlgorithmImplementation(r, algorithmName); 78 | 79 | //make the intial underlying voronoi structure 80 | final Voronoi v = new Voronoi(numSites, bounds, bounds, r, null); 81 | 82 | //assemble the voronoi strucutre into a usable graph object representing a map 83 | final TestGraphImpl graph = new TestGraphImpl(v, numLloydRelaxations, r, algorithm); 84 | 85 | return graph; 86 | } 87 | 88 | /** 89 | * Currently there are only 1 algorithm. You can choose one of algorithms exactly or random from this list: 90 | *
    91 | *
  1. random
  2. 92 | *
  3. radial
  4. 93 | *
  5. blob
  6. 94 | *
  7. perlin
  8. 95 | *
96 | * 97 | * @param r Randomizer. 98 | * @param name Name of the algorithm. 99 | * @return 100 | */ 101 | private static HeightAlgorithm getAlgorithmImplementation(Random r, String name) { 102 | HashMap implementations = new HashMap<>(); 103 | implementations.put("random", 0); 104 | implementations.put("radial", 1); 105 | implementations.put("blob", 2); 106 | implementations.put("perlin", 3); 107 | int i = implementations.getOrDefault(name, 0); 108 | if (i == 0) i = 1 + r.nextInt(implementations.size() - 1); 109 | switch (i) { 110 | case 1: return new Radial(1.07, 111 | r.nextInt(5) + 1, 112 | r.nextDouble() * 2 * Math.PI, 113 | r.nextDouble() * 2 * Math.PI, 114 | r.nextDouble() * .5 + .2); 115 | case 2: return new Blob(); 116 | case 3: return new Perlin(r, 7, 256, 256); 117 | default: throw new RuntimeException("Method \"getAlgorithmImplementation()\" is broken. " + 118 | "Check implementations map and switch statement. Their values and cases must match."); 119 | } 120 | } 121 | 122 | private static void saveFile(BufferedImage img) throws IOException { 123 | if (SAVE_FILE) { 124 | File file = new File("output/"); 125 | file.mkdirs(); 126 | file = new File(String.format("output/seed-%s-sites-%d-lloyds-%d.png", SEED, SITES_AMOUNT, LLOYD_RELAXATIONS)); 127 | while (file.exists()) file = new File(incrementFileName(file.getPath())); 128 | ImageIO.write(img, "PNG", file); 129 | } 130 | } 131 | 132 | /** 133 | * If you have equal filenames - use this method to change filename before creating it. 134 | * 135 | * @param oldName fileName_index1.format(fileName.format) 136 | * @return fileName_index2.format(fileName_1.format) 137 | */ 138 | private static String incrementFileName(String oldName) { 139 | String newName; 140 | int i = oldName.lastIndexOf('.'); 141 | Matcher m = Pattern.compile("\\((\\d+)\\).").matcher(oldName); 142 | if (m.find()) { 143 | String n = String.valueOf(Integer.valueOf(m.group(1)) + 1); 144 | newName = oldName.substring(0, m.start()) + "(" + n + ")" + oldName.substring(i); 145 | } else { 146 | newName = oldName.substring(0, i) + "(1)" + oldName.substring(i); 147 | } 148 | return newName; 149 | } 150 | 151 | private static int oldX = -1, oldY = -1; 152 | private static int drawX = 0, drawY = 0; 153 | private static int zoom = 1; 154 | private static float zoomModifier = 1; 155 | 156 | private static void showGraph(final BufferedImage img) { 157 | final JFrame frame = new JFrame() { 158 | @Override 159 | public void paint(Graphics g) { 160 | g.fillRect(0, 0, getWidth(), getHeight()); 161 | g.drawImage(img, 162 | getInsets().left + drawX, 163 | getInsets().top - drawY, 164 | (int)(GRAPH_BOUNDS * zoomModifier), 165 | (int)(GRAPH_BOUNDS * zoomModifier), 166 | null); 167 | } 168 | }; 169 | 170 | frame.setTitle("Java Fortune"); 171 | frame.setVisible(true); 172 | frame.setSize(FRAME_BOUNDS, FRAME_BOUNDS); 173 | frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); 174 | 175 | frame.addMouseListener(new MouseAdapter() { 176 | @Override 177 | public void mousePressed(MouseEvent e) { 178 | oldX = e.getX(); 179 | oldY = e.getY(); 180 | } 181 | @Override 182 | public void mouseReleased(MouseEvent e) { 183 | oldX = -1; 184 | oldY = -1; 185 | } 186 | }); 187 | frame.addMouseMotionListener(new MouseAdapter() { 188 | @Override 189 | public void mouseDragged(MouseEvent e) { 190 | if (oldX != -1) { 191 | int dx = e.getX() - oldX, dy = e.getY() - oldY; 192 | drawX = Math.min(0, Math.max(FRAME_BOUNDS - (int)(GRAPH_BOUNDS * zoomModifier), drawX + dx)); 193 | drawY = Math.min((int)(GRAPH_BOUNDS * zoomModifier) - FRAME_BOUNDS, Math.max(0, drawY - dy)); 194 | oldX += dx; 195 | oldY += dy; 196 | frame.repaint(); 197 | } 198 | } 199 | }); 200 | frame.addMouseWheelListener(new MouseAdapter() { 201 | @Override 202 | public void mouseWheelMoved(MouseWheelEvent e) { 203 | zoom = Math.min(10, Math.max(-5, zoom + e.getWheelRotation())); 204 | zoomModifier = zoom > 0 ? zoom : 1/(float)(1 - zoom); 205 | frame.repaint(); 206 | } 207 | }); 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /src/main/java/com/hoten/delaunay/examples/TestGraphImpl.java: -------------------------------------------------------------------------------- 1 | package com.hoten.delaunay.examples; 2 | 3 | import com.hoten.delaunay.voronoi.Center; 4 | import com.hoten.delaunay.voronoi.VoronoiGraph; 5 | import com.hoten.delaunay.voronoi.groundshapes.HeightAlgorithm; 6 | import com.hoten.delaunay.voronoi.nodename.as3delaunay.Voronoi; 7 | import java.awt.Color; 8 | import java.util.Random; 9 | 10 | /** 11 | * TestGraphImpl.java 12 | * 13 | * Supplies information for Voronoi graphing logic: 14 | * 15 | * 1) biome mapping information based on a Site's elevation and moisture 16 | * 17 | * 2) color mapping information based on biome, and for bodies of water 18 | * 19 | * @author Connor 20 | */ 21 | public class TestGraphImpl extends VoronoiGraph { 22 | 23 | public static enum ColorData { 24 | 25 | OCEAN(0x44447a), LAKE(0x336699), BEACH(0xa09077), SNOW(0xffffff), 26 | TUNDRA(0xbbbbaa), BARE(0x888888), SCORCHED(0x555555), TAIGA(0x99aa77), 27 | SHURBLAND(0x889977), TEMPERATE_DESERT(0xc9d29b), 28 | TEMPERATE_RAIN_FOREST(0x448855), TEMPERATE_DECIDUOUS_FOREST(0x679459), 29 | GRASSLAND(0x88aa55), SUBTROPICAL_DESERT(0xd2b98b), SHRUBLAND(0x889977), 30 | ICE(0x99ffff), MARSH(0x2f6666), TROPICAL_RAIN_FOREST(0x337755), 31 | TROPICAL_SEASONAL_FOREST(0x559944), COAST(0x33335a), 32 | LAKESHORE(0x225588), RIVER(0x225588); 33 | public Color color; 34 | 35 | ColorData(int color) { 36 | this.color = new Color(color); 37 | } 38 | } 39 | 40 | public TestGraphImpl(Voronoi v, int numLloydRelaxations, Random r, HeightAlgorithm algorithm) { 41 | super(v, numLloydRelaxations, r, algorithm); 42 | OCEAN = ColorData.OCEAN.color; 43 | LAKE = ColorData.LAKE.color; 44 | BEACH = ColorData.BEACH.color; 45 | RIVER = ColorData.RIVER.color; 46 | } 47 | 48 | @Override 49 | protected Color getColor(Enum biome) { 50 | return ((ColorData) biome).color; 51 | } 52 | 53 | @Override 54 | protected Enum getBiome(Center p) { 55 | if (p.ocean) { 56 | return ColorData.OCEAN; 57 | } else if (p.water) { 58 | if (p.elevation < 0.1) { 59 | return ColorData.MARSH; 60 | } 61 | if (p.elevation > 0.8) { 62 | return ColorData.ICE; 63 | } 64 | return ColorData.LAKE; 65 | } else if (p.coast) { 66 | return ColorData.BEACH; 67 | } else if (p.elevation > 0.8) { 68 | if (p.moisture > 0.50) { 69 | return ColorData.SNOW; 70 | } else if (p.moisture > 0.33) { 71 | return ColorData.TUNDRA; 72 | } else if (p.moisture > 0.16) { 73 | return ColorData.BARE; 74 | } else { 75 | return ColorData.SCORCHED; 76 | } 77 | } else if (p.elevation > 0.6) { 78 | if (p.moisture > 0.66) { 79 | return ColorData.TAIGA; 80 | } else if (p.moisture > 0.33) { 81 | return ColorData.SHRUBLAND; 82 | } else { 83 | return ColorData.TEMPERATE_DESERT; 84 | } 85 | } else if (p.elevation > 0.3) { 86 | if (p.moisture > 0.83) { 87 | return ColorData.TEMPERATE_RAIN_FOREST; 88 | } else if (p.moisture > 0.50) { 89 | return ColorData.TEMPERATE_DECIDUOUS_FOREST; 90 | } else if (p.moisture > 0.16) { 91 | return ColorData.GRASSLAND; 92 | } else { 93 | return ColorData.TEMPERATE_DESERT; 94 | } 95 | } else { 96 | if (p.moisture > 0.66) { 97 | return ColorData.TROPICAL_RAIN_FOREST; 98 | } else if (p.moisture > 0.33) { 99 | return ColorData.TROPICAL_SEASONAL_FOREST; 100 | } else if (p.moisture > 0.16) { 101 | return ColorData.GRASSLAND; 102 | } else { 103 | return ColorData.SUBTROPICAL_DESERT; 104 | } 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/main/java/com/hoten/delaunay/geom/GenUtils.java: -------------------------------------------------------------------------------- 1 | package com.hoten.delaunay.geom; 2 | 3 | /** 4 | * GenUtil.java 5 | * 6 | * @author Connor 7 | */ 8 | public class GenUtils { 9 | 10 | public static boolean closeEnough(double d1, double d2, double diff) { 11 | return Math.abs(d1 - d2) <= diff; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/hoten/delaunay/geom/Point.java: -------------------------------------------------------------------------------- 1 | package com.hoten.delaunay.geom; 2 | 3 | /** 4 | * Point.java 5 | * 6 | * @author Connor 7 | */ 8 | public class Point { 9 | 10 | public static double distance(Point _coord, Point _coord0) { 11 | return Math.sqrt((_coord.x - _coord0.x) * (_coord.x - _coord0.x) + (_coord.y - _coord0.y) * (_coord.y - _coord0.y)); 12 | } 13 | public double x, y; 14 | 15 | public Point(double x, double y) { 16 | this.x = x; 17 | this.y = y; 18 | } 19 | 20 | @Override 21 | public String toString() { 22 | return x + ", " + y; 23 | } 24 | 25 | public double l2() { 26 | return x * x + y * y; 27 | } 28 | 29 | public double length() { 30 | return Math.sqrt(x * x + y * y); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/hoten/delaunay/geom/Rectangle.java: -------------------------------------------------------------------------------- 1 | package com.hoten.delaunay.geom; 2 | 3 | /** 4 | * Rectangle.java 5 | * 6 | * @author Connor 7 | */ 8 | public class Rectangle { 9 | 10 | final public double x, y, width, height, right, bottom, left, top; 11 | 12 | public Rectangle(double x, double y, double width, double height) { 13 | left = this.x = x; 14 | top = this.y = y; 15 | this.width = width; 16 | this.height = height; 17 | right = x + width; 18 | bottom = y + height; 19 | } 20 | 21 | public boolean liesOnAxes(Point p) { 22 | return GenUtils.closeEnough(p.x, x, 1) || GenUtils.closeEnough(p.y, y, 1) || GenUtils.closeEnough(p.x, right, 1) || GenUtils.closeEnough(p.y, bottom, 1); 23 | } 24 | 25 | public boolean inBounds(Point p) { 26 | return inBounds(p.x, p.y); 27 | } 28 | 29 | public boolean inBounds(double x0, double y0) { 30 | return !(x0 < x || x0 > right || y0 < y || y0 > bottom); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/hoten/delaunay/voronoi/Center.java: -------------------------------------------------------------------------------- 1 | package com.hoten.delaunay.voronoi; 2 | 3 | import com.hoten.delaunay.geom.Point; 4 | import java.util.ArrayList; 5 | 6 | /** 7 | * Center.java 8 | * 9 | * @author Connor 10 | */ 11 | public class Center { 12 | 13 | public int index; 14 | public Point loc; 15 | public ArrayList corners = new ArrayList();//good 16 | public ArrayList
neighbors = new ArrayList();//good 17 | public ArrayList borders = new ArrayList(); 18 | public boolean border, ocean, water, coast; 19 | public double elevation; 20 | public double moisture; 21 | public Enum biome; 22 | public double area; 23 | 24 | public Center() { 25 | } 26 | 27 | public Center(Point loc) { 28 | this.loc = loc; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/hoten/delaunay/voronoi/Corner.java: -------------------------------------------------------------------------------- 1 | package com.hoten.delaunay.voronoi; 2 | 3 | import com.hoten.delaunay.geom.Point; 4 | import java.util.ArrayList; 5 | 6 | /** 7 | * Corner.java 8 | * 9 | * @author Connor 10 | */ 11 | public class Corner { 12 | 13 | public ArrayList
touches = new ArrayList(); //good 14 | public ArrayList adjacent = new ArrayList(); //good 15 | public ArrayList protrudes = new ArrayList(); 16 | public Point loc; 17 | public int index; 18 | public boolean border; 19 | public double elevation; 20 | public boolean water, ocean, coast; 21 | public Corner downslope; 22 | public int river; 23 | public double moisture; 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/hoten/delaunay/voronoi/Edge.java: -------------------------------------------------------------------------------- 1 | package com.hoten.delaunay.voronoi; 2 | 3 | import com.hoten.delaunay.geom.Point; 4 | 5 | /** 6 | * Edge.java 7 | * 8 | * @author Connor 9 | */ 10 | public class Edge { 11 | 12 | public int index; 13 | public Center d0, d1; // Delaunay edge 14 | public Corner v0, v1; // Voronoi edge 15 | public Point midpoint; // halfway between v0,v1 16 | public int river; 17 | 18 | public void setVornoi(Corner v0, Corner v1) { 19 | this.v0 = v0; 20 | this.v1 = v1; 21 | midpoint = new Point((v0.loc.x + v1.loc.x) / 2, (v0.loc.y + v1.loc.y) / 2); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/hoten/delaunay/voronoi/VoronoiGraph.java: -------------------------------------------------------------------------------- 1 | package com.hoten.delaunay.voronoi; 2 | 3 | import com.hoten.delaunay.geom.Point; 4 | import com.hoten.delaunay.geom.Rectangle; 5 | import com.hoten.delaunay.voronoi.groundshapes.HeightAlgorithm; 6 | import com.hoten.delaunay.voronoi.nodename.as3delaunay.LineSegment; 7 | import com.hoten.delaunay.voronoi.nodename.as3delaunay.Voronoi; 8 | 9 | import java.awt.*; 10 | import java.awt.image.BufferedImage; 11 | import java.util.*; 12 | 13 | /** 14 | * VoronoiGraph.java 15 | * 16 | * @author Connor 17 | */ 18 | public abstract class VoronoiGraph { 19 | 20 | final public ArrayList edges = new ArrayList(); 21 | final public ArrayList corners = new ArrayList(); 22 | final public ArrayList
centers = new ArrayList(); 23 | final public Rectangle bounds; 24 | final private Random r; 25 | protected Color OCEAN, RIVER, LAKE, BEACH; 26 | final public BufferedImage pixelCenterMap; 27 | 28 | public VoronoiGraph(Voronoi v, int numLloydRelaxations, Random r, HeightAlgorithm algorithm) { 29 | this.r = r; 30 | bounds = v.get_plotBounds(); 31 | for (int i = 0; i < numLloydRelaxations; i++) { 32 | ArrayList points = v.siteCoords(); 33 | for (Point p : points) { 34 | ArrayList region = v.region(p); 35 | double x = 0; 36 | double y = 0; 37 | for (Point c : region) { 38 | x += c.x; 39 | y += c.y; 40 | } 41 | x /= region.size(); 42 | y /= region.size(); 43 | p.x = x; 44 | p.y = y; 45 | } 46 | v = new Voronoi(points, null, v.get_plotBounds()); 47 | } 48 | buildGraph(v); 49 | improveCorners(); 50 | 51 | assignCornerElevations(algorithm); 52 | assignOceanCoastAndLand(); 53 | redistributeElevations(landCorners()); 54 | assignPolygonElevations(); 55 | 56 | calculateDownslopes(); 57 | //calculateWatersheds(); 58 | createRivers(); 59 | assignCornerMoisture(); 60 | redistributeMoisture(landCorners()); 61 | assignPolygonMoisture(); 62 | assignBiomes(); 63 | 64 | pixelCenterMap = new BufferedImage((int) bounds.width, (int) bounds.width, BufferedImage.TYPE_4BYTE_ABGR); 65 | } 66 | 67 | abstract protected Enum getBiome(Center p); 68 | 69 | abstract protected Color getColor(Enum biome); 70 | 71 | private void improveCorners() { 72 | Point[] newP = new Point[corners.size()]; 73 | for (Corner c : corners) { 74 | if (c.border) { 75 | newP[c.index] = c.loc; 76 | } else { 77 | double x = 0; 78 | double y = 0; 79 | for (Center center : c.touches) { 80 | x += center.loc.x; 81 | y += center.loc.y; 82 | } 83 | newP[c.index] = new Point(x / c.touches.size(), y / c.touches.size()); 84 | } 85 | } 86 | corners.stream().forEach((c) -> { 87 | c.loc = newP[c.index]; 88 | }); 89 | edges.stream().filter((e) -> (e.v0 != null && e.v1 != null)).forEach((e) -> { 90 | e.setVornoi(e.v0, e.v1); 91 | }); 92 | } 93 | 94 | private Edge edgeWithCenters(Center c1, Center c2) { 95 | for (Edge e : c1.borders) { 96 | if (e.d0 == c2 || e.d1 == c2) { 97 | return e; 98 | } 99 | } 100 | return null; 101 | } 102 | 103 | private void drawTriangle(Graphics2D g, Corner c1, Corner c2, Center center) { 104 | int[] x = new int[3]; 105 | int[] y = new int[3]; 106 | x[0] = (int) center.loc.x; 107 | y[0] = (int) center.loc.y; 108 | x[1] = (int) c1.loc.x; 109 | y[1] = (int) c1.loc.y; 110 | x[2] = (int) c2.loc.x; 111 | y[2] = (int) c2.loc.y; 112 | g.fillPolygon(x, y, 3); 113 | } 114 | 115 | private boolean closeEnough(double d1, double d2, double diff) { 116 | return Math.abs(d1 - d2) <= diff; 117 | } 118 | 119 | public BufferedImage createMap() { 120 | int width = (int) bounds.width; 121 | int height = (int) bounds.height; 122 | 123 | final BufferedImage img = new BufferedImage(width, height, BufferedImage.TYPE_4BYTE_ABGR); 124 | Graphics2D g = img.createGraphics(); 125 | 126 | paint(g); 127 | 128 | return img; 129 | } 130 | 131 | public void paint(Graphics2D g) { 132 | paint(g, true, true, false, false, false, true); 133 | } 134 | 135 | private void drawPolygon(Graphics2D g, Center c, Color color) { 136 | g.setColor(color); 137 | 138 | //only used if Center c is on the edge of the graph. allows for completely filling in the outer polygons 139 | Corner edgeCorner1 = null; 140 | Corner edgeCorner2 = null; 141 | c.area = 0; 142 | for (Center n : c.neighbors) { 143 | Edge e = edgeWithCenters(c, n); 144 | 145 | if (e.v0 == null) { 146 | //outermost voronoi edges aren't stored in the graph 147 | continue; 148 | } 149 | 150 | //find a corner on the exterior of the graph 151 | //if this Edge e has one, then it must have two, 152 | //finding these two corners will give us the missing 153 | //triangle to render. this special triangle is handled 154 | //outside this for loop 155 | Corner cornerWithOneAdjacent = e.v0.border ? e.v0 : e.v1; 156 | if (cornerWithOneAdjacent.border) { 157 | if (edgeCorner1 == null) { 158 | edgeCorner1 = cornerWithOneAdjacent; 159 | } else { 160 | edgeCorner2 = cornerWithOneAdjacent; 161 | } 162 | } 163 | 164 | drawTriangle(g, e.v0, e.v1, c); 165 | c.area += Math.abs(c.loc.x * (e.v0.loc.y - e.v1.loc.y) 166 | + e.v0.loc.x * (e.v1.loc.y - c.loc.y) 167 | + e.v1.loc.x * (c.loc.y - e.v0.loc.y)) / 2; 168 | } 169 | 170 | //handle the missing triangle 171 | if (edgeCorner2 != null) { 172 | //if these two outer corners are NOT on the same exterior edge of the graph, 173 | //then we actually must render a polygon (w/ 4 points) and take into consideration 174 | //one of the four corners (either 0,0 or 0,height or width,0 or width,height) 175 | //note: the 'missing polygon' may have more than just 4 points. this 176 | //is common when the number of sites are quite low (less than 5), but not a problem 177 | //with a more useful number of sites. 178 | //TODO: find a way to fix this 179 | 180 | if (closeEnough(edgeCorner1.loc.x, edgeCorner2.loc.x, 1)) { 181 | drawTriangle(g, edgeCorner1, edgeCorner2, c); 182 | } else { 183 | int[] x = new int[4]; 184 | int[] y = new int[4]; 185 | x[0] = (int) c.loc.x; 186 | y[0] = (int) c.loc.y; 187 | x[1] = (int) edgeCorner1.loc.x; 188 | y[1] = (int) edgeCorner1.loc.y; 189 | 190 | //determine which corner this is 191 | x[2] = (int) ((closeEnough(edgeCorner1.loc.x, bounds.x, 1) || closeEnough(edgeCorner2.loc.x, bounds.x, .5)) ? bounds.x : bounds.right); 192 | y[2] = (int) ((closeEnough(edgeCorner1.loc.y, bounds.y, 1) || closeEnough(edgeCorner2.loc.y, bounds.y, .5)) ? bounds.y : bounds.bottom); 193 | 194 | x[3] = (int) edgeCorner2.loc.x; 195 | y[3] = (int) edgeCorner2.loc.y; 196 | 197 | g.fillPolygon(x, y, 4); 198 | c.area += 0; //TODO: area of polygon given vertices 199 | } 200 | } 201 | } 202 | 203 | //also records the area of each voronoi cell 204 | public void paint(Graphics2D g, boolean drawBiomes, boolean drawRivers, boolean drawSites, boolean drawCorners, boolean drawDelaunay, boolean drawVoronoi) { 205 | final int numSites = centers.size(); 206 | 207 | Color[] defaultColors = null; 208 | if (!drawBiomes) { 209 | defaultColors = new Color[numSites]; 210 | for (int i = 0; i < defaultColors.length; i++) { 211 | defaultColors[i] = new Color(r.nextInt(255), r.nextInt(255), r.nextInt(255)); 212 | } 213 | } 214 | 215 | Graphics2D pixelCenterGraphics = pixelCenterMap.createGraphics(); 216 | 217 | //draw via triangles 218 | for (Center c : centers) { 219 | drawPolygon(g, c, drawBiomes ? getColor(c.biome) : defaultColors[c.index]); 220 | drawPolygon(pixelCenterGraphics, c, new Color(c.index)); 221 | } 222 | 223 | for (Edge e : edges) { 224 | if (drawDelaunay) { 225 | g.setStroke(new BasicStroke(1)); 226 | g.setColor(Color.YELLOW); 227 | g.drawLine((int) e.d0.loc.x, (int) e.d0.loc.y, (int) e.d1.loc.x, (int) e.d1.loc.y); 228 | } 229 | if (drawRivers && e.river > 0) { 230 | g.setStroke(new BasicStroke(1 + (int) Math.sqrt(e.river * 2))); 231 | g.setColor(RIVER); 232 | g.drawLine((int) e.v0.loc.x, (int) e.v0.loc.y, (int) e.v1.loc.x, (int) e.v1.loc.y); 233 | } 234 | } 235 | 236 | if (drawSites) { 237 | g.setColor(Color.BLACK); 238 | centers.stream().forEach((s) -> { 239 | g.fillOval((int) (s.loc.x - 2), (int) (s.loc.y - 2), 4, 4); 240 | }); 241 | } 242 | 243 | if (drawCorners) { 244 | g.setColor(Color.WHITE); 245 | corners.stream().forEach((c) -> { 246 | g.fillOval((int) (c.loc.x - 2), (int) (c.loc.y - 2), 4, 4); 247 | }); 248 | } 249 | g.setColor(Color.WHITE); 250 | g.drawRect((int) bounds.x, (int) bounds.y, (int) bounds.width, (int) bounds.height); 251 | } 252 | 253 | private void buildGraph(Voronoi v) { 254 | final HashMap pointCenterMap = new HashMap(); 255 | final ArrayList points = v.siteCoords(); 256 | points.stream().forEach((p) -> { 257 | Center c = new Center(); 258 | c.loc = p; 259 | c.index = centers.size(); 260 | centers.add(c); 261 | pointCenterMap.put(p, c); 262 | }); 263 | 264 | //bug fix 265 | centers.stream().forEach((c) -> { 266 | v.region(c.loc); 267 | }); 268 | 269 | final ArrayList libedges = v.edges(); 270 | final HashMap pointCornerMap = new HashMap(); 271 | 272 | for (com.hoten.delaunay.voronoi.nodename.as3delaunay.Edge libedge : libedges) { 273 | final LineSegment vEdge = libedge.voronoiEdge(); 274 | final LineSegment dEdge = libedge.delaunayLine(); 275 | 276 | final Edge edge = new Edge(); 277 | edge.index = edges.size(); 278 | edges.add(edge); 279 | 280 | edge.v0 = makeCorner(pointCornerMap, vEdge.p0); 281 | edge.v1 = makeCorner(pointCornerMap, vEdge.p1); 282 | edge.d0 = pointCenterMap.get(dEdge.p0); 283 | edge.d1 = pointCenterMap.get(dEdge.p1); 284 | 285 | // Centers point to edges. Corners point to edges. 286 | if (edge.d0 != null) { 287 | edge.d0.borders.add(edge); 288 | } 289 | if (edge.d1 != null) { 290 | edge.d1.borders.add(edge); 291 | } 292 | if (edge.v0 != null) { 293 | edge.v0.protrudes.add(edge); 294 | } 295 | if (edge.v1 != null) { 296 | edge.v1.protrudes.add(edge); 297 | } 298 | 299 | // Centers point to centers. 300 | if (edge.d0 != null && edge.d1 != null) { 301 | addToCenterList(edge.d0.neighbors, edge.d1); 302 | addToCenterList(edge.d1.neighbors, edge.d0); 303 | } 304 | 305 | // Corners point to corners 306 | if (edge.v0 != null && edge.v1 != null) { 307 | addToCornerList(edge.v0.adjacent, edge.v1); 308 | addToCornerList(edge.v1.adjacent, edge.v0); 309 | } 310 | 311 | // Centers point to corners 312 | if (edge.d0 != null) { 313 | addToCornerList(edge.d0.corners, edge.v0); 314 | addToCornerList(edge.d0.corners, edge.v1); 315 | } 316 | if (edge.d1 != null) { 317 | addToCornerList(edge.d1.corners, edge.v0); 318 | addToCornerList(edge.d1.corners, edge.v1); 319 | } 320 | 321 | // Corners point to centers 322 | if (edge.v0 != null) { 323 | addToCenterList(edge.v0.touches, edge.d0); 324 | addToCenterList(edge.v0.touches, edge.d1); 325 | } 326 | if (edge.v1 != null) { 327 | addToCenterList(edge.v1.touches, edge.d0); 328 | addToCenterList(edge.v1.touches, edge.d1); 329 | } 330 | } 331 | } 332 | 333 | // Helper functions for the following for loop; ideally these 334 | // would be inlined 335 | private void addToCornerList(ArrayList list, Corner c) { 336 | if (c != null && !list.contains(c)) { 337 | list.add(c); 338 | } 339 | } 340 | 341 | private void addToCenterList(ArrayList
list, Center c) { 342 | if (c != null && !list.contains(c)) { 343 | list.add(c); 344 | } 345 | } 346 | 347 | //ensures that each corner is represented by only one corner object 348 | private Corner makeCorner(HashMap pointCornerMap, Point p) { 349 | if (p == null) { 350 | return null; 351 | } 352 | int index = (int) ((int) p.x + (int) (p.y) * bounds.width * 2); 353 | Corner c = pointCornerMap.get(index); 354 | if (c == null) { 355 | c = new Corner(); 356 | c.loc = p; 357 | c.border = bounds.liesOnAxes(p); 358 | c.index = corners.size(); 359 | corners.add(c); 360 | pointCornerMap.put(index, c); 361 | } 362 | return c; 363 | } 364 | 365 | private void assignCornerElevations(HeightAlgorithm algorithm) { 366 | LinkedList queue = new LinkedList(); 367 | for (Corner c : corners) { 368 | c.water = algorithm.isWater(c.loc, bounds, r); 369 | if (c.border) { 370 | c.elevation = 0; 371 | queue.add(c); 372 | } else { 373 | c.elevation = Double.MAX_VALUE; 374 | } 375 | } 376 | 377 | while (!queue.isEmpty()) { 378 | Corner c = queue.pop(); 379 | for (Corner a : c.adjacent) { 380 | double newElevation = 0.01 + c.elevation; 381 | if (!c.water && !a.water) { 382 | newElevation += 1; 383 | } 384 | if (newElevation < a.elevation) { 385 | a.elevation = newElevation; 386 | queue.add(a); 387 | } 388 | } 389 | } 390 | } 391 | 392 | private void assignOceanCoastAndLand() { 393 | LinkedList
queue = new LinkedList(); 394 | final double waterThreshold = .3; 395 | for (final Center center : centers) { 396 | int numWater = 0; 397 | for (final Corner c : center.corners) { 398 | if (c.border) { 399 | center.border = center.water = center.ocean = true; 400 | queue.add(center); 401 | } 402 | if (c.water) { 403 | numWater++; 404 | } 405 | } 406 | center.water = center.ocean || ((double) numWater / center.corners.size() >= waterThreshold); 407 | } 408 | while (!queue.isEmpty()) { 409 | final Center center = queue.pop(); 410 | for (final Center n : center.neighbors) { 411 | if (n.water && !n.ocean) { 412 | n.ocean = true; 413 | queue.add(n); 414 | } 415 | } 416 | } 417 | for (Center center : centers) { 418 | boolean oceanNeighbor = false; 419 | boolean landNeighbor = false; 420 | for (Center n : center.neighbors) { 421 | oceanNeighbor |= n.ocean; 422 | landNeighbor |= !n.water; 423 | } 424 | center.coast = oceanNeighbor && landNeighbor; 425 | } 426 | 427 | for (Corner c : corners) { 428 | int numOcean = 0; 429 | int numLand = 0; 430 | for (Center center : c.touches) { 431 | numOcean += center.ocean ? 1 : 0; 432 | numLand += !center.water ? 1 : 0; 433 | } 434 | c.ocean = numOcean == c.touches.size(); 435 | c.coast = numOcean > 0 && numLand > 0; 436 | c.water = c.border || ((numLand != c.touches.size()) && !c.coast); 437 | } 438 | } 439 | 440 | private ArrayList landCorners() { 441 | final ArrayList list = new ArrayList(); 442 | for (Corner c : corners) { 443 | if (!c.ocean && !c.coast) { 444 | list.add(c); 445 | } 446 | } 447 | return list; 448 | } 449 | 450 | private void redistributeElevations(ArrayList landCorners) { 451 | Collections.sort(landCorners, new Comparator() { 452 | @Override 453 | public int compare(Corner o1, Corner o2) { 454 | if (o1.elevation > o2.elevation) { 455 | return 1; 456 | } else if (o1.elevation < o2.elevation) { 457 | return -1; 458 | } 459 | return 0; 460 | } 461 | }); 462 | 463 | final double SCALE_FACTOR = 1.1; 464 | for (int i = 0; i < landCorners.size(); i++) { 465 | double y = (double) i / landCorners.size(); 466 | double x = Math.sqrt(SCALE_FACTOR) - Math.sqrt(SCALE_FACTOR * (1 - y)); 467 | x = Math.min(x, 1); 468 | landCorners.get(i).elevation = x; 469 | } 470 | 471 | for (Corner c : corners) { 472 | if (c.ocean || c.coast) { 473 | c.elevation = 0.0; 474 | } 475 | } 476 | } 477 | 478 | private void assignPolygonElevations() { 479 | for (Center center : centers) { 480 | double total = 0; 481 | for (Corner c : center.corners) { 482 | total += c.elevation; 483 | } 484 | center.elevation = total / center.corners.size(); 485 | } 486 | } 487 | 488 | private void calculateDownslopes() { 489 | for (Corner c : corners) { 490 | Corner down = c; 491 | //System.out.println("ME: " + c.elevation); 492 | for (Corner a : c.adjacent) { 493 | //System.out.println(a.elevation); 494 | if (a.elevation <= down.elevation) { 495 | down = a; 496 | } 497 | } 498 | c.downslope = down; 499 | } 500 | } 501 | 502 | private void createRivers() { 503 | for (int i = 0; i < bounds.width / 2; i++) { 504 | Corner c = corners.get(r.nextInt(corners.size())); 505 | if (c.ocean || c.elevation < 0.3 || c.elevation > 0.9) { 506 | continue; 507 | } 508 | // Bias rivers to go west: if (q.downslope.x > q.x) continue; 509 | while (!c.coast) { 510 | if (c == c.downslope) { 511 | break; 512 | } 513 | Edge edge = lookupEdgeFromCorner(c, c.downslope); 514 | if (!edge.v0.water || !edge.v1.water) { 515 | edge.river++; 516 | c.river++; 517 | c.downslope.river++; // TODO: fix double count 518 | } 519 | c = c.downslope; 520 | } 521 | } 522 | } 523 | 524 | private Edge lookupEdgeFromCorner(Corner c, Corner downslope) { 525 | for (Edge e : c.protrudes) { 526 | if (e.v0 == downslope || e.v1 == downslope) { 527 | return e; 528 | } 529 | } 530 | return null; 531 | } 532 | 533 | private void assignCornerMoisture() { 534 | LinkedList queue = new LinkedList(); 535 | for (Corner c : corners) { 536 | if ((c.water || c.river > 0) && !c.ocean) { 537 | c.moisture = c.river > 0 ? Math.min(3.0, (0.2 * c.river)) : 1.0; 538 | queue.push(c); 539 | } else { 540 | c.moisture = 0.0; 541 | } 542 | } 543 | 544 | while (!queue.isEmpty()) { 545 | Corner c = queue.pop(); 546 | for (Corner a : c.adjacent) { 547 | double newM = .9 * c.moisture; 548 | if (newM > a.moisture) { 549 | a.moisture = newM; 550 | queue.add(a); 551 | } 552 | } 553 | } 554 | 555 | // Salt water 556 | for (Corner c : corners) { 557 | if (c.ocean || c.coast) { 558 | c.moisture = 1.0; 559 | } 560 | } 561 | } 562 | 563 | private void redistributeMoisture(ArrayList landCorners) { 564 | Collections.sort(landCorners, new Comparator() { 565 | @Override 566 | public int compare(Corner o1, Corner o2) { 567 | if (o1.moisture > o2.moisture) { 568 | return 1; 569 | } else if (o1.moisture < o2.moisture) { 570 | return -1; 571 | } 572 | return 0; 573 | } 574 | }); 575 | for (int i = 0; i < landCorners.size(); i++) { 576 | landCorners.get(i).moisture = (double) i / landCorners.size(); 577 | } 578 | } 579 | 580 | private void assignPolygonMoisture() { 581 | for (Center center : centers) { 582 | double total = 0; 583 | for (Corner c : center.corners) { 584 | total += c.moisture; 585 | } 586 | center.moisture = total / center.corners.size(); 587 | } 588 | } 589 | 590 | private void assignBiomes() { 591 | for (Center center : centers) { 592 | center.biome = getBiome(center); 593 | } 594 | } 595 | } 596 | -------------------------------------------------------------------------------- /src/main/java/com/hoten/delaunay/voronoi/groundshapes/Blob.java: -------------------------------------------------------------------------------- 1 | package com.hoten.delaunay.voronoi.groundshapes; 2 | 3 | import com.hoten.delaunay.geom.Point; 4 | import com.hoten.delaunay.geom.Rectangle; 5 | 6 | import java.util.Random; 7 | 8 | /** 9 | * A blob with eyes. 10 | */ 11 | public class Blob implements HeightAlgorithm { 12 | 13 | /** {@inheritDoc} */ 14 | @Override 15 | public boolean isWater(Point point, Rectangle bounds, Random random) { 16 | Point p = new Point(2 * (point.x / bounds.width - 0.5), 2 * (point.y / bounds.height - 0.5)); 17 | boolean eye1 = new Point(p.x - 0.2, p.y / 2 + 0.2).length() < 0.05; 18 | boolean eye2 = new Point(p.x + 0.2, p.y / 2 + 0.2).length() < 0.05; 19 | boolean body = p.length() < 0.8 - 0.18 * Math.sin(5 * Math.atan2(p.y, p.x)); 20 | return !body || eye1 || eye2; 21 | } 22 | } -------------------------------------------------------------------------------- /src/main/java/com/hoten/delaunay/voronoi/groundshapes/HeightAlgorithm.java: -------------------------------------------------------------------------------- 1 | package com.hoten.delaunay.voronoi.groundshapes; 2 | 3 | import com.hoten.delaunay.geom.Point; 4 | import com.hoten.delaunay.geom.Rectangle; 5 | 6 | import java.util.Random; 7 | 8 | /** 9 | * Use implementation of this interface to find out which points in graph are water and which - ground. 10 | */ 11 | public interface HeightAlgorithm { 12 | 13 | /** 14 | * Uses specific algorithm to check point. 15 | * 16 | * @param p Corner location. 17 | * @param bounds Graph bounds. 18 | * @param random Voronoi's randomizer to keep identical results for user's seed. 19 | * @return 20 | */ 21 | boolean isWater(Point p, Rectangle bounds, Random random); 22 | 23 | } -------------------------------------------------------------------------------- /src/main/java/com/hoten/delaunay/voronoi/groundshapes/Perlin.java: -------------------------------------------------------------------------------- 1 | package com.hoten.delaunay.voronoi.groundshapes; 2 | 3 | import com.hoten.delaunay.geom.Point; 4 | import com.hoten.delaunay.geom.Rectangle; 5 | 6 | import java.util.Random; 7 | 8 | /** 9 | * Code from article 10 | * How to Use Perlin Noise in Your, GamesHerman Tulleken. 11 | * 12 | * This algorithm do next steps: 13 | *
    14 | *
  1. Create white noise as base for perlin noise.
  2. 15 | *
  3. Smooth it {@code octaveCount} times.
  4. 16 | *
17 | */ 18 | public class Perlin implements HeightAlgorithm { 19 | 20 | private final Random r; 21 | private final float median; 22 | private final float noise[][]; 23 | 24 | /** 25 | * @param random Randomizer. 26 | * @param octaveCount Smooth value. 0 means there will be only white noise. 27 | * @param noiseWidth Width of noise map. Should be less than graph width. 28 | * @param noiseHeight Height of noise map. Should be less than graph width. 29 | */ 30 | public Perlin(Random random, int octaveCount, int noiseWidth, int noiseHeight) { 31 | this.r = random; 32 | 33 | float whiteNoise[][] = generateWhiteNoise(noiseWidth + 1, noiseHeight + 1); 34 | 35 | noise = generatePerlinNoise(whiteNoise, octaveCount); 36 | 37 | median = findMedian(); 38 | } 39 | 40 | /** 41 | * @return The value dividing ground and water. 42 | */ 43 | private float findMedian() { 44 | long count[] = new long[10]; 45 | 46 | for (float[] aNoise : noise) { 47 | for (float aaNoise : aNoise) { 48 | for (int k = 1; k < count.length; k++) { 49 | if (aaNoise * 10 < k) { 50 | count[k - 1]++; 51 | break; 52 | } 53 | } 54 | } 55 | } 56 | 57 | int n = 0; 58 | 59 | for (int i = 1; i < count.length; i++) { 60 | if (count[i] > count[n]) n = i; 61 | } 62 | 63 | return (float) (n + 0.5) / 10; 64 | } 65 | 66 | /** 67 | * 68 | * @param random Randomizer. 69 | * @param octaveCount Smooth value. 0 means there will be only white noise. 70 | */ 71 | public Perlin(Random random, float centrical, int octaveCount) { 72 | this(random, octaveCount, 256, 256); 73 | } 74 | 75 | /** {@inheritDoc} */ 76 | @Override 77 | public boolean isWater(Point p, Rectangle bounds, Random random) { 78 | int x = (int) (p.x / bounds.width * (noise.length - 1)); 79 | 80 | int y = (int) (p.y / bounds.height * (noise[0].length - 1)); 81 | 82 | return noise[x][y] < median; 83 | } 84 | 85 | /** 86 | * 87 | * @param width Width of noise map. 88 | * @param height height of noise map. 89 | * @return White noise map. 90 | */ 91 | private float[][] generateWhiteNoise(int width, int height) { 92 | float[][] noise = new float[width][height]; 93 | 94 | for (int i = width / 25; i < width * 0.96; i++) { 95 | for (int j = height / 25; j < height * 0.96; j++) { 96 | noise[i][j] = r.nextFloat(); 97 | } 98 | } 99 | 100 | return noise; 101 | } 102 | 103 | /** 104 | * 105 | * @param baseNoise Noise map which will be processed. 106 | * @param octaveCount Smooth value. 0 means there will be only white noise. 107 | * @return Perlin noise. 108 | */ 109 | private float[][] generatePerlinNoise(float[][] baseNoise, int octaveCount) { 110 | int width = baseNoise.length; 111 | int height = baseNoise[0].length; 112 | 113 | float[][][] smoothNoise = new float[octaveCount][][]; //an array of 2D arrays containing 114 | 115 | float persistance = 0.5f; 116 | 117 | //generate smooth noise 118 | for (int i = 0; i < octaveCount; i++) { 119 | smoothNoise[i] = generateSmoothNoise(baseNoise, i); 120 | } 121 | 122 | float[][] perlinNoise = new float[width][height]; 123 | float amplitude = 1.0f; 124 | float totalAmplitude = 0.0f; 125 | 126 | //blend noise together 127 | for (int octave = octaveCount - 1; octave >= 0; octave--) { 128 | amplitude *= persistance; 129 | totalAmplitude += amplitude; 130 | 131 | for (int i = 0; i < width; i++) { 132 | for (int j = 0; j < height; j++) { 133 | perlinNoise[i][j] += smoothNoise[octave][i][j] * amplitude; 134 | } 135 | } 136 | } 137 | 138 | //normalisation 139 | for (int i = 0; i < width; i++) { 140 | for (int j = 0; j < height; j++) { 141 | perlinNoise[i][j] /= totalAmplitude; 142 | } 143 | } 144 | 145 | return perlinNoise; 146 | } 147 | 148 | /** 149 | * 150 | * @param baseNoise Noise map which will be used to create a smoother noise map. 151 | * @param octave Smooth value. 0 means there will be only white noise. 152 | * @return Smoother noise map than a base map. 153 | */ 154 | private float[][] generateSmoothNoise(float[][] baseNoise, int octave) { 155 | int width = baseNoise.length; 156 | int height = baseNoise[0].length; 157 | 158 | float[][] smoothNoise = new float[width][height]; 159 | 160 | int samplePeriod = 1 << octave; // calculates 2 ^ k 161 | float sampleFrequency = 1.0f / samplePeriod; 162 | 163 | for (int i = 0; i < width; i++) { 164 | //calculate the horizontal sampling indices 165 | int sample_i0 = (i / samplePeriod) * samplePeriod; 166 | int sample_i1 = (sample_i0 + samplePeriod) % width; //wrap around 167 | float horizontal_blend = (i - sample_i0) * sampleFrequency; 168 | 169 | for (int j = 0; j < height; j++) { 170 | //calculate the vertical sampling indices 171 | int sample_j0 = (j / samplePeriod) * samplePeriod; 172 | int sample_j1 = (sample_j0 + samplePeriod) % height; //wrap around 173 | float vertical_blend = (j - sample_j0) * sampleFrequency; 174 | 175 | //blend the top two corners 176 | float top = interpolate(baseNoise[sample_i0][sample_j0], 177 | baseNoise[sample_i1][sample_j0], horizontal_blend); 178 | 179 | //blend the bottom two corners 180 | float bottom = interpolate(baseNoise[sample_i0][sample_j1], 181 | baseNoise[sample_i1][sample_j1], horizontal_blend); 182 | 183 | //final blend 184 | smoothNoise[i][j] = interpolate(top, bottom, vertical_blend); 185 | } 186 | } 187 | 188 | return smoothNoise; 189 | } 190 | 191 | /** 192 | * Function returns a linear interpolation between two values. 193 | * Essentially, the closer alpha is to 0, the closer the resulting value will be to x0; 194 | * the closer alpha is to 1, the closer the resulting value will be to x1. 195 | * 196 | * @param x0 First value. 197 | * @param x1 Second value. 198 | * @param alpha Transparency. 199 | * @return Linear interpolation between two values. 200 | */ 201 | private static float interpolate(float x0, float x1, float alpha) { 202 | return x0 * (1 - alpha) + alpha * x1; 203 | } 204 | } -------------------------------------------------------------------------------- /src/main/java/com/hoten/delaunay/voronoi/groundshapes/Radial.java: -------------------------------------------------------------------------------- 1 | package com.hoten.delaunay.voronoi.groundshapes; 2 | 3 | import com.hoten.delaunay.geom.Point; 4 | import com.hoten.delaunay.geom.Rectangle; 5 | 6 | import java.util.Random; 7 | 8 | /** 9 | * 10 | */ 11 | public class Radial implements HeightAlgorithm { 12 | 13 | private final double ISLAND_FACTOR; // 1.0 means no small islands; 2.0 leads to a lot 14 | private final int bumps; 15 | private final double startAngle; 16 | private final double dipAngle; 17 | private final double dipWidth; 18 | 19 | /** 20 | * 21 | * @param ISLAND_FACTOR 1.0 means no small islands, 2.0 leads to a lot 22 | * @param bumps 23 | * @param startAngle 24 | * @param dipAngle 25 | * @param dipWidth 26 | */ 27 | public Radial(double ISLAND_FACTOR, int bumps, double startAngle, double dipAngle, double dipWidth) { 28 | this.ISLAND_FACTOR = ISLAND_FACTOR; 29 | this.bumps = bumps; 30 | this.startAngle = startAngle; 31 | this.dipAngle = dipAngle; 32 | this.dipWidth = dipWidth; 33 | } 34 | 35 | /** {@inheritDoc} */ 36 | @Override 37 | public boolean isWater(Point p, Rectangle bounds, Random r) { 38 | p = new Point(2 * (p.x / bounds.width - 0.5), 2 * (p.y / bounds.height - 0.5)); 39 | 40 | double angle = Math.atan2(p.y, p.x); 41 | double length = 0.5 * (Math.max(Math.abs(p.x), Math.abs(p.y)) + p.length()); 42 | 43 | double r1 = 0.5 + 0.40 * Math.sin(startAngle + bumps * angle + Math.cos((bumps + 3) * angle)); 44 | double r2 = 0.7 - 0.20 * Math.sin(startAngle + bumps * angle - Math.sin((bumps + 2) * angle)); 45 | if (Math.abs(angle - dipAngle) < dipWidth 46 | || Math.abs(angle - dipAngle + 2 * Math.PI) < dipWidth 47 | || Math.abs(angle - dipAngle - 2 * Math.PI) < dipWidth) { 48 | r1 = r2 = 0.2; 49 | } 50 | return !(length < r1 || (length > r1 * ISLAND_FACTOR && length < r2)); 51 | } 52 | } -------------------------------------------------------------------------------- /src/main/java/com/hoten/delaunay/voronoi/nodename/as3delaunay/Circle.java: -------------------------------------------------------------------------------- 1 | package com.hoten.delaunay.voronoi.nodename.as3delaunay; 2 | 3 | import com.hoten.delaunay.geom.Point; 4 | 5 | public final class Circle extends Object { 6 | 7 | public Point center; 8 | public double radius; 9 | 10 | public Circle(double centerX, double centerY, double radius) { 11 | super(); 12 | this.center = new Point(centerX, centerY); 13 | this.radius = radius; 14 | } 15 | 16 | @Override 17 | public String toString() { 18 | return "Circle (center: " + center + "; radius: " + radius + ")"; 19 | } 20 | } -------------------------------------------------------------------------------- /src/main/java/com/hoten/delaunay/voronoi/nodename/as3delaunay/Edge.java: -------------------------------------------------------------------------------- 1 | package com.hoten.delaunay.voronoi.nodename.as3delaunay; 2 | 3 | import com.hoten.delaunay.geom.Point; 4 | import com.hoten.delaunay.geom.Rectangle; 5 | import java.util.HashMap; 6 | import java.util.Stack; 7 | 8 | /** 9 | * The line segment connecting the two Sites is part of the Delaunay 10 | * triangulation; the line segment connecting the two Vertices is part of the 11 | * Voronoi diagram 12 | * 13 | * @author ashaw 14 | * 15 | */ 16 | public final class Edge { 17 | 18 | final private static Stack _pool = new Stack(); 19 | 20 | /** 21 | * This is the only way to create a new Edge 22 | * 23 | * @param site0 24 | * @param site1 25 | * @return 26 | * 27 | */ 28 | public static Edge createBisectingEdge(Site site0, Site site1) { 29 | double dx, dy, absdx, absdy; 30 | double a, b, c; 31 | 32 | dx = site1.get_x() - site0.get_x(); 33 | dy = site1.get_y() - site0.get_y(); 34 | absdx = dx > 0 ? dx : -dx; 35 | absdy = dy > 0 ? dy : -dy; 36 | c = site0.get_x() * dx + site0.get_y() * dy + (dx * dx + dy * dy) * 0.5; 37 | if (absdx > absdy) { 38 | a = 1.0; 39 | b = dy / dx; 40 | c /= dx; 41 | } else { 42 | b = 1.0; 43 | a = dx / dy; 44 | c /= dy; 45 | } 46 | 47 | Edge edge = Edge.create(); 48 | 49 | edge.set_leftSite(site0); 50 | edge.set_rightSite(site1); 51 | site0.addEdge(edge); 52 | site1.addEdge(edge); 53 | 54 | edge._leftVertex = null; 55 | edge._rightVertex = null; 56 | 57 | edge.a = a; 58 | edge.b = b; 59 | edge.c = c; 60 | //trace("createBisectingEdge: a ", edge.a, "b", edge.b, "c", edge.c); 61 | 62 | return edge; 63 | } 64 | 65 | private static Edge create() { 66 | Edge edge; 67 | if (_pool.size() > 0) { 68 | edge = _pool.pop(); 69 | edge.init(); 70 | } else { 71 | edge = new Edge(); 72 | } 73 | return edge; 74 | } 75 | 76 | /*final private static LINESPRITE:Sprite = new Sprite(); 77 | final private static GRAPHICS:Graphics = LINESPRITE.graphics; 78 | 79 | private var _delaunayLineBmp:BitmapData; 80 | internal function get delaunayLineBmp():BitmapData 81 | { 82 | if (!_delaunayLineBmp) 83 | { 84 | _delaunayLineBmp = makeDelaunayLineBmp(); 85 | } 86 | return _delaunayLineBmp; 87 | } 88 | 89 | // making this available to Voronoi; running out of memory in AIR so I cannot cache the bmp 90 | internal function makeDelaunayLineBmp():BitmapData 91 | { 92 | var p0:Point = leftSite.coord; 93 | var p1:Point = rightSite.coord; 94 | 95 | GRAPHICS.clear(); 96 | // clear() resets line style back to undefined! 97 | GRAPHICS.lineStyle(0, 0, 1.0, false, LineScaleMode.NONE, CapsStyle.NONE); 98 | GRAPHICS.moveTo(p0.x, p0.y); 99 | GRAPHICS.lineTo(p1.x, p1.y); 100 | 101 | var w:int = int(Math.ceil(Math.max(p0.x, p1.x))); 102 | if (w < 1) 103 | { 104 | w = 1; 105 | } 106 | var h:int = int(Math.ceil(Math.max(p0.y, p1.y))); 107 | if (h < 1) 108 | { 109 | h = 1; 110 | } 111 | var bmp:BitmapData = new BitmapData(w, h, true, 0); 112 | bmp.draw(LINESPRITE); 113 | return bmp; 114 | }*/ 115 | public LineSegment delaunayLine() { 116 | // draw a line connecting the input Sites for which the edge is a bisector: 117 | return new LineSegment(get_leftSite().get_coord(), get_rightSite().get_coord()); 118 | } 119 | 120 | public LineSegment voronoiEdge() { 121 | if (!get_visible()) { 122 | return new LineSegment(null, null); 123 | } 124 | return new LineSegment(_clippedVertices.get(LR.LEFT), 125 | _clippedVertices.get(LR.RIGHT)); 126 | } 127 | private static int _nedges = 0; 128 | final public static Edge DELETED = new Edge(); 129 | // the equation of the edge: ax + by = c 130 | public double a, b, c; 131 | // the two Voronoi vertices that the edge connects 132 | // (if one of them is null, the edge extends to infinity) 133 | private Vertex _leftVertex; 134 | 135 | public Vertex get_leftVertex() { 136 | return _leftVertex; 137 | } 138 | private Vertex _rightVertex; 139 | 140 | public Vertex get_rightVertex() { 141 | return _rightVertex; 142 | } 143 | 144 | public Vertex vertex(LR leftRight) { 145 | return (leftRight == LR.LEFT) ? _leftVertex : _rightVertex; 146 | } 147 | 148 | public void setVertex(LR leftRight, Vertex v) { 149 | if (leftRight == LR.LEFT) { 150 | _leftVertex = v; 151 | } else { 152 | _rightVertex = v; 153 | } 154 | } 155 | 156 | public boolean isPartOfConvexHull() { 157 | return (_leftVertex == null || _rightVertex == null); 158 | } 159 | 160 | public double sitesDistance() { 161 | return Point.distance(get_leftSite().get_coord(), get_rightSite().get_coord()); 162 | } 163 | 164 | public static double compareSitesDistances_MAX(Edge edge0, Edge edge1) { 165 | double length0 = edge0.sitesDistance(); 166 | double length1 = edge1.sitesDistance(); 167 | if (length0 < length1) { 168 | return 1; 169 | } 170 | if (length0 > length1) { 171 | return -1; 172 | } 173 | return 0; 174 | } 175 | 176 | public static double compareSitesDistances(Edge edge0, Edge edge1) { 177 | return -compareSitesDistances_MAX(edge0, edge1); 178 | } 179 | // Once clipVertices() is called, this Dictionary will hold two Points 180 | // representing the clipped coordinates of the left and right ends... 181 | private HashMap _clippedVertices; 182 | 183 | public HashMap get_clippedEnds() { 184 | return _clippedVertices; 185 | } 186 | // unless the entire Edge is outside the bounds. 187 | // In that case visible will be false: 188 | 189 | public boolean get_visible() { 190 | return _clippedVertices != null; 191 | } 192 | // the two input Sites for which this Edge is a bisector: 193 | private HashMap _sites; 194 | 195 | public void set_leftSite(Site s) { 196 | _sites.put(LR.LEFT, s); 197 | } 198 | 199 | public Site get_leftSite() { 200 | return _sites.get(LR.LEFT); 201 | } 202 | 203 | public void set_rightSite(Site s) { 204 | _sites.put(LR.RIGHT, s); 205 | } 206 | 207 | public Site get_rightSite() { 208 | return _sites.get(LR.RIGHT); 209 | } 210 | 211 | public Site site(LR leftRight) { 212 | return _sites.get(leftRight); 213 | } 214 | private int _edgeIndex; 215 | 216 | public void dispose() { 217 | /*if (_delaunayLineBmp) 218 | { 219 | _delaunayLineBmp.dispose(); 220 | _delaunayLineBmp = null; 221 | }*/ 222 | _leftVertex = null; 223 | _rightVertex = null; 224 | if (_clippedVertices != null) { 225 | _clippedVertices.clear(); 226 | _clippedVertices = null; 227 | } 228 | _sites.clear(); 229 | _sites = null; 230 | 231 | _pool.push(this); 232 | } 233 | 234 | private Edge() { 235 | _edgeIndex = _nedges++; 236 | init(); 237 | } 238 | 239 | private void init() { 240 | _sites = new HashMap(); 241 | } 242 | 243 | public String toString() { 244 | return "Edge " + _edgeIndex + "; sites " + _sites.get(LR.LEFT) + ", " + _sites.get(LR.RIGHT) 245 | + "; endVertices " + (_leftVertex != null ? _leftVertex.get_vertexIndex() : "null") + ", " 246 | + (_rightVertex != null ? _rightVertex.get_vertexIndex() : "null") + "::"; 247 | } 248 | 249 | /** 250 | * Set _clippedVertices to contain the two ends of the portion of the 251 | * Voronoi edge that is visible within the bounds. If no part of the Edge 252 | * falls within the bounds, leave _clippedVertices null. 253 | * 254 | * @param bounds 255 | * 256 | */ 257 | public void clipVertices(Rectangle bounds) { 258 | double xmin = bounds.x; 259 | double ymin = bounds.y; 260 | double xmax = bounds.right; 261 | double ymax = bounds.bottom; 262 | 263 | Vertex vertex0, vertex1; 264 | double x0, x1, y0, y1; 265 | 266 | if (a == 1.0 && b >= 0.0) { 267 | vertex0 = _rightVertex; 268 | vertex1 = _leftVertex; 269 | } else { 270 | vertex0 = _leftVertex; 271 | vertex1 = _rightVertex; 272 | } 273 | 274 | if (a == 1.0) { 275 | y0 = ymin; 276 | if (vertex0 != null && vertex0.get_y() > ymin) { 277 | y0 = vertex0.get_y(); 278 | } 279 | if (y0 > ymax) { 280 | return; 281 | } 282 | x0 = c - b * y0; 283 | 284 | y1 = ymax; 285 | if (vertex1 != null && vertex1.get_y() < ymax) { 286 | y1 = vertex1.get_y(); 287 | } 288 | if (y1 < ymin) { 289 | return; 290 | } 291 | x1 = c - b * y1; 292 | 293 | if ((x0 > xmax && x1 > xmax) || (x0 < xmin && x1 < xmin)) { 294 | return; 295 | } 296 | 297 | if (x0 > xmax) { 298 | x0 = xmax; 299 | y0 = (c - x0) / b; 300 | } else if (x0 < xmin) { 301 | x0 = xmin; 302 | y0 = (c - x0) / b; 303 | } 304 | 305 | if (x1 > xmax) { 306 | x1 = xmax; 307 | y1 = (c - x1) / b; 308 | } else if (x1 < xmin) { 309 | x1 = xmin; 310 | y1 = (c - x1) / b; 311 | } 312 | } else { 313 | x0 = xmin; 314 | if (vertex0 != null && vertex0.get_x() > xmin) { 315 | x0 = vertex0.get_x(); 316 | } 317 | if (x0 > xmax) { 318 | return; 319 | } 320 | y0 = c - a * x0; 321 | 322 | x1 = xmax; 323 | if (vertex1 != null && vertex1.get_x() < xmax) { 324 | x1 = vertex1.get_x(); 325 | } 326 | if (x1 < xmin) { 327 | return; 328 | } 329 | y1 = c - a * x1; 330 | 331 | if ((y0 > ymax && y1 > ymax) || (y0 < ymin && y1 < ymin)) { 332 | return; 333 | } 334 | 335 | if (y0 > ymax) { 336 | y0 = ymax; 337 | x0 = (c - y0) / a; 338 | } else if (y0 < ymin) { 339 | y0 = ymin; 340 | x0 = (c - y0) / a; 341 | } 342 | 343 | if (y1 > ymax) { 344 | y1 = ymax; 345 | x1 = (c - y1) / a; 346 | } else if (y1 < ymin) { 347 | y1 = ymin; 348 | x1 = (c - y1) / a; 349 | } 350 | } 351 | 352 | _clippedVertices = new HashMap(); 353 | if (vertex0 == _leftVertex) { 354 | _clippedVertices.put(LR.LEFT, new Point(x0, y0)); 355 | _clippedVertices.put(LR.RIGHT, new Point(x1, y1)); 356 | } else { 357 | _clippedVertices.put(LR.RIGHT, new Point(x0, y0)); 358 | _clippedVertices.put(LR.LEFT, new Point(x1, y1)); 359 | } 360 | } 361 | } -------------------------------------------------------------------------------- /src/main/java/com/hoten/delaunay/voronoi/nodename/as3delaunay/EdgeList.java: -------------------------------------------------------------------------------- 1 | package com.hoten.delaunay.voronoi.nodename.as3delaunay; 2 | 3 | import com.hoten.delaunay.geom.Point; 4 | import java.util.ArrayList; 5 | 6 | public final class EdgeList implements IDisposable { 7 | 8 | private double _deltax; 9 | private double _xmin; 10 | private int _hashsize; 11 | private ArrayList _hash; 12 | public Halfedge leftEnd; 13 | public Halfedge rightEnd; 14 | 15 | @Override 16 | public void dispose() { 17 | Halfedge halfEdge = leftEnd; 18 | Halfedge prevHe; 19 | while (halfEdge != rightEnd) { 20 | prevHe = halfEdge; 21 | halfEdge = halfEdge.edgeListRightNeighbor; 22 | prevHe.dispose(); 23 | } 24 | leftEnd = null; 25 | rightEnd.dispose(); 26 | rightEnd = null; 27 | 28 | _hash.clear(); 29 | _hash = null; 30 | } 31 | 32 | public EdgeList(double xmin, double deltax, int sqrt_nsites) { 33 | _xmin = xmin; 34 | _deltax = deltax; 35 | _hashsize = 2 * sqrt_nsites; 36 | 37 | _hash = new ArrayList(_hashsize); 38 | 39 | // two dummy Halfedges: 40 | leftEnd = Halfedge.createDummy(); 41 | rightEnd = Halfedge.createDummy(); 42 | leftEnd.edgeListLeftNeighbor = null; 43 | leftEnd.edgeListRightNeighbor = rightEnd; 44 | rightEnd.edgeListLeftNeighbor = leftEnd; 45 | rightEnd.edgeListRightNeighbor = null; 46 | 47 | for(int i = 0; i < _hashsize; i++){ 48 | _hash.add(null); 49 | } 50 | 51 | _hash.set(0, leftEnd); 52 | _hash.set(_hashsize - 1, rightEnd); 53 | } 54 | 55 | /** 56 | * Insert newHalfedge to the right of lb 57 | * 58 | * @param lb 59 | * @param newHalfedge 60 | * 61 | */ 62 | public void insert(Halfedge lb, Halfedge newHalfedge) { 63 | newHalfedge.edgeListLeftNeighbor = lb; 64 | newHalfedge.edgeListRightNeighbor = lb.edgeListRightNeighbor; 65 | lb.edgeListRightNeighbor.edgeListLeftNeighbor = newHalfedge; 66 | lb.edgeListRightNeighbor = newHalfedge; 67 | } 68 | 69 | /** 70 | * This function only removes the Halfedge from the left-right list. We 71 | * cannot dispose it yet because we are still using it. 72 | * 73 | * @param halfEdge 74 | * 75 | */ 76 | public void remove(Halfedge halfEdge) { 77 | halfEdge.edgeListLeftNeighbor.edgeListRightNeighbor = halfEdge.edgeListRightNeighbor; 78 | halfEdge.edgeListRightNeighbor.edgeListLeftNeighbor = halfEdge.edgeListLeftNeighbor; 79 | halfEdge.edge = Edge.DELETED; 80 | halfEdge.edgeListLeftNeighbor = halfEdge.edgeListRightNeighbor = null; 81 | } 82 | 83 | /** 84 | * Find the rightmost Halfedge that is still left of p 85 | * 86 | * @param p 87 | * @return 88 | * 89 | */ 90 | public Halfedge edgeListLeftNeighbor(Point p) { 91 | int i, bucket; 92 | Halfedge halfEdge; 93 | 94 | /* Use hash table to get close to desired halfedge */ 95 | bucket = (int) ((p.x - _xmin) / _deltax * _hashsize); 96 | if (bucket < 0) { 97 | bucket = 0; 98 | } 99 | if (bucket >= _hashsize) { 100 | bucket = _hashsize - 1; 101 | } 102 | halfEdge = getHash(bucket); 103 | if (halfEdge == null) { 104 | for (i = 1; true; ++i) { 105 | if ((halfEdge = getHash(bucket - i)) != null) { 106 | break; 107 | } 108 | if ((halfEdge = getHash(bucket + i)) != null) { 109 | break; 110 | } 111 | } 112 | } 113 | /* Now search linear list of halfedges for the correct one */ 114 | if (halfEdge == leftEnd || (halfEdge != rightEnd && halfEdge.isLeftOf(p))) { 115 | do { 116 | halfEdge = halfEdge.edgeListRightNeighbor; 117 | } while (halfEdge != rightEnd && halfEdge.isLeftOf(p)); 118 | halfEdge = halfEdge.edgeListLeftNeighbor; 119 | } else { 120 | do { 121 | halfEdge = halfEdge.edgeListLeftNeighbor; 122 | } while (halfEdge != leftEnd && !halfEdge.isLeftOf(p)); 123 | } 124 | 125 | /* Update hash table and reference counts */ 126 | if (bucket > 0 && bucket < _hashsize - 1) { 127 | _hash.set(bucket, halfEdge); 128 | } 129 | return halfEdge; 130 | } 131 | 132 | /* Get entry from hash table, pruning any deleted nodes */ 133 | private Halfedge getHash(int b) { 134 | Halfedge halfEdge; 135 | 136 | if (b < 0 || b >= _hashsize) { 137 | return null; 138 | } 139 | halfEdge = _hash.get(b); 140 | if (halfEdge != null && halfEdge.edge == Edge.DELETED) { 141 | /* Hash table points to deleted halfedge. Patch as necessary. */ 142 | _hash.set(b, null); 143 | // still can't dispose halfEdge yet! 144 | return null; 145 | } else { 146 | return halfEdge; 147 | } 148 | } 149 | } -------------------------------------------------------------------------------- /src/main/java/com/hoten/delaunay/voronoi/nodename/as3delaunay/EdgeReorderer.java: -------------------------------------------------------------------------------- 1 | package com.hoten.delaunay.voronoi.nodename.as3delaunay; 2 | 3 | import java.util.ArrayList; 4 | 5 | public final class EdgeReorderer { 6 | 7 | private ArrayList _edges; 8 | private ArrayList _edgeOrientations; 9 | 10 | public ArrayList get_edges() { 11 | return _edges; 12 | } 13 | 14 | public ArrayList get_edgeOrientations() { 15 | return _edgeOrientations; 16 | } 17 | 18 | public EdgeReorderer(ArrayList origEdges, Class criterion) { 19 | if (criterion != Vertex.class && criterion != Site.class) { 20 | throw new Error("Edges: criterion must be Vertex or Site"); 21 | } 22 | _edges = new ArrayList(); 23 | _edgeOrientations = new ArrayList(); 24 | if (origEdges.size() > 0) { 25 | _edges = reorderEdges(origEdges, criterion); 26 | } 27 | } 28 | 29 | public void dispose() { 30 | _edges = null; 31 | _edgeOrientations = null; 32 | } 33 | 34 | private ArrayList reorderEdges(ArrayList origEdges, Class criterion) { 35 | int i, j; 36 | int n = origEdges.size(); 37 | Edge edge; 38 | // we're going to reorder the edges in order of traversal 39 | ArrayList done = new ArrayList(n); 40 | int nDone = 0; 41 | for (int k = 0; k < n; k++) { 42 | done.add( false); 43 | } 44 | ArrayList newEdges = new ArrayList(); 45 | 46 | i = 0; 47 | edge = origEdges.get(i); 48 | newEdges.add(edge); 49 | _edgeOrientations.add(LR.LEFT); 50 | ICoord firstPoint = (criterion == Vertex.class) ? edge.get_leftVertex() : edge.get_leftSite(); 51 | ICoord lastPoint = (criterion == Vertex.class) ? edge.get_rightVertex() : edge.get_rightSite(); 52 | 53 | if (firstPoint == Vertex.VERTEX_AT_INFINITY || lastPoint == Vertex.VERTEX_AT_INFINITY) { 54 | return new ArrayList(); 55 | } 56 | 57 | done.set(i, true); 58 | ++nDone; 59 | 60 | while (nDone < n) { 61 | for (i = 1; i < n; ++i) { 62 | if (done.get(i)) { 63 | continue; 64 | } 65 | edge = origEdges.get(i); 66 | ICoord leftPoint = (criterion == Vertex.class) ? edge.get_leftVertex() : edge.get_leftSite(); 67 | ICoord rightPoint = (criterion == Vertex.class) ? edge.get_rightVertex() : edge.get_rightSite(); 68 | if (leftPoint == Vertex.VERTEX_AT_INFINITY || rightPoint == Vertex.VERTEX_AT_INFINITY) { 69 | return new ArrayList(); 70 | } 71 | if (leftPoint == lastPoint) { 72 | lastPoint = rightPoint; 73 | _edgeOrientations.add(LR.LEFT); 74 | newEdges.add(edge); 75 | done.set(i, true); 76 | } else if (rightPoint == firstPoint) { 77 | firstPoint = leftPoint; 78 | _edgeOrientations.add(0, LR.LEFT); 79 | newEdges.add(0, edge); 80 | done.set(i, true); 81 | } else if (leftPoint == firstPoint) { 82 | firstPoint = rightPoint; 83 | _edgeOrientations.add(0, LR.RIGHT); 84 | newEdges.add(0, edge); 85 | 86 | done.set(i, true); 87 | } else if (rightPoint == lastPoint) { 88 | lastPoint = leftPoint; 89 | _edgeOrientations.add(LR.RIGHT); 90 | newEdges.add(edge); 91 | done.set(i, true); 92 | } 93 | if (done.get(i)) { 94 | ++nDone; 95 | } 96 | } 97 | } 98 | 99 | return newEdges; 100 | } 101 | } -------------------------------------------------------------------------------- /src/main/java/com/hoten/delaunay/voronoi/nodename/as3delaunay/Halfedge.java: -------------------------------------------------------------------------------- 1 | package com.hoten.delaunay.voronoi.nodename.as3delaunay; 2 | 3 | import com.hoten.delaunay.geom.Point; 4 | import java.util.Stack; 5 | 6 | public final class Halfedge { 7 | 8 | private static Stack _pool = new Stack(); 9 | 10 | public static Halfedge create(Edge edge, LR lr) { 11 | if (_pool.size() > 0) { 12 | return _pool.pop().init(edge, lr); 13 | } else { 14 | return new Halfedge(edge, lr); 15 | } 16 | } 17 | 18 | public static Halfedge createDummy() { 19 | return create(null, null); 20 | } 21 | public Halfedge edgeListLeftNeighbor, edgeListRightNeighbor; 22 | public Halfedge nextInPriorityQueue; 23 | public Edge edge; 24 | public LR leftRight; 25 | public Vertex vertex; 26 | // the vertex's y-coordinate in the transformed Voronoi space V* 27 | public double ystar; 28 | 29 | public Halfedge( Edge edge, LR lr) { 30 | init(edge, lr); 31 | } 32 | 33 | private Halfedge init(Edge edge, LR lr) { 34 | this.edge = edge; 35 | leftRight = lr; 36 | nextInPriorityQueue = null; 37 | vertex = null; 38 | return this; 39 | } 40 | 41 | @Override 42 | public String toString() { 43 | return "Halfedge (leftRight: " + leftRight + "; vertex: " + vertex + ")"; 44 | } 45 | 46 | public void dispose() { 47 | if (edgeListLeftNeighbor != null || edgeListRightNeighbor != null) { 48 | // still in EdgeList 49 | return; 50 | } 51 | if (nextInPriorityQueue != null) { 52 | // still in PriorityQueue 53 | return; 54 | } 55 | edge = null; 56 | leftRight = null; 57 | vertex = null; 58 | _pool.push(this); 59 | } 60 | 61 | public void reallyDispose() { 62 | edgeListLeftNeighbor = null; 63 | edgeListRightNeighbor = null; 64 | nextInPriorityQueue = null; 65 | edge = null; 66 | leftRight = null; 67 | vertex = null; 68 | _pool.push(this); 69 | } 70 | 71 | public boolean isLeftOf(Point p) { 72 | Site topSite; 73 | boolean rightOfSite, above, fast; 74 | double dxp, dyp, dxs, t1, t2, t3, yl; 75 | 76 | topSite = edge.get_rightSite(); 77 | rightOfSite = p.x > topSite.get_x(); 78 | if (rightOfSite && this.leftRight == LR.LEFT) { 79 | return true; 80 | } 81 | if (!rightOfSite && this.leftRight == LR.RIGHT) { 82 | return false; 83 | } 84 | 85 | if (edge.a == 1.0) { 86 | dyp = p.y - topSite.get_y(); 87 | dxp = p.x - topSite.get_x(); 88 | fast = false; 89 | if ((!rightOfSite && edge.b < 0.0) || (rightOfSite && edge.b >= 0.0)) { 90 | above = dyp >= edge.b * dxp; 91 | fast = above; 92 | } else { 93 | above = p.x + p.y * edge.b > edge.c; 94 | if (edge.b < 0.0) { 95 | above = !above; 96 | } 97 | if (!above) { 98 | fast = true; 99 | } 100 | } 101 | if (!fast) { 102 | dxs = topSite.get_x() - edge.get_leftSite().get_x(); 103 | above = edge.b * (dxp * dxp - dyp * dyp) 104 | < dxs * dyp * (1.0 + 2.0 * dxp / dxs + edge.b * edge.b); 105 | if (edge.b < 0.0) { 106 | above = !above; 107 | } 108 | } 109 | } else /* edge.b == 1.0 */ { 110 | yl = edge.c - edge.a * p.x; 111 | t1 = p.y - yl; 112 | t2 = p.x - topSite.get_x(); 113 | t3 = yl - topSite.get_y(); 114 | above = t1 * t1 > t2 * t2 + t3 * t3; 115 | } 116 | return this.leftRight == LR.LEFT ? above : !above; 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/main/java/com/hoten/delaunay/voronoi/nodename/as3delaunay/HalfedgePriorityQueue.java: -------------------------------------------------------------------------------- 1 | package com.hoten.delaunay.voronoi.nodename.as3delaunay; 2 | 3 | import com.hoten.delaunay.geom.Point; 4 | import java.util.ArrayList; 5 | 6 | public final class HalfedgePriorityQueue // also known as heap 7 | { 8 | 9 | private ArrayList _hash; 10 | private int _count; 11 | private int _minBucket; 12 | private int _hashsize; 13 | private double _ymin; 14 | private double _deltay; 15 | 16 | public HalfedgePriorityQueue(double ymin, double deltay, int sqrt_nsites) { 17 | _ymin = ymin; 18 | _deltay = deltay; 19 | _hashsize = 4 * sqrt_nsites; 20 | initialize(); 21 | } 22 | 23 | public void dispose() { 24 | // get rid of dummies 25 | for (int i = 0; i < _hashsize; ++i) { 26 | _hash.get(i).dispose(); 27 | } 28 | _hash.clear(); 29 | _hash = null; 30 | } 31 | 32 | private void initialize() { 33 | int i; 34 | 35 | _count = 0; 36 | _minBucket = 0; 37 | _hash = new ArrayList(_hashsize); 38 | // dummy Halfedge at the top of each hash 39 | for (i = 0; i < _hashsize; ++i) { 40 | _hash.add(Halfedge.createDummy()); 41 | _hash.get(i).nextInPriorityQueue = null; 42 | } 43 | } 44 | 45 | public void insert(Halfedge halfEdge) { 46 | Halfedge previous, next; 47 | int insertionBucket = bucket(halfEdge); 48 | if (insertionBucket < _minBucket) { 49 | _minBucket = insertionBucket; 50 | } 51 | previous = _hash.get(insertionBucket); 52 | while ((next = previous.nextInPriorityQueue) != null 53 | && (halfEdge.ystar > next.ystar || (halfEdge.ystar == next.ystar && halfEdge.vertex.get_x() > next.vertex.get_x()))) { 54 | previous = next; 55 | } 56 | halfEdge.nextInPriorityQueue = previous.nextInPriorityQueue; 57 | previous.nextInPriorityQueue = halfEdge; 58 | ++_count; 59 | } 60 | 61 | public void remove(Halfedge halfEdge) { 62 | Halfedge previous; 63 | int removalBucket = bucket(halfEdge); 64 | 65 | if (halfEdge.vertex != null) { 66 | previous = _hash.get(removalBucket); 67 | while (previous.nextInPriorityQueue != halfEdge) { 68 | previous = previous.nextInPriorityQueue; 69 | } 70 | previous.nextInPriorityQueue = halfEdge.nextInPriorityQueue; 71 | _count--; 72 | halfEdge.vertex = null; 73 | halfEdge.nextInPriorityQueue = null; 74 | halfEdge.dispose(); 75 | } 76 | } 77 | 78 | private int bucket(Halfedge halfEdge) { 79 | int theBucket = (int) ((halfEdge.ystar - _ymin) / _deltay * _hashsize); 80 | if (theBucket < 0) { 81 | theBucket = 0; 82 | } 83 | if (theBucket >= _hashsize) { 84 | theBucket = _hashsize - 1; 85 | } 86 | return theBucket; 87 | } 88 | 89 | private boolean isEmpty(int bucket) { 90 | return (_hash.get(bucket).nextInPriorityQueue == null); 91 | } 92 | 93 | /** 94 | * move _minBucket until it contains an actual Halfedge (not just the dummy 95 | * at the top); 96 | * 97 | */ 98 | private void adjustMinBucket() { 99 | while (_minBucket < _hashsize - 1 && isEmpty(_minBucket)) { 100 | ++_minBucket; 101 | } 102 | } 103 | 104 | public boolean empty() { 105 | return _count == 0; 106 | } 107 | 108 | /** 109 | * @return coordinates of the Halfedge's vertex in V*, the transformed 110 | * Voronoi diagram 111 | * 112 | */ 113 | public Point min() { 114 | adjustMinBucket(); 115 | Halfedge answer = _hash.get(_minBucket).nextInPriorityQueue; 116 | return new Point(answer.vertex.get_x(), answer.ystar); 117 | } 118 | 119 | /** 120 | * remove and return the min Halfedge 121 | * 122 | * @return 123 | * 124 | */ 125 | public Halfedge extractMin() { 126 | Halfedge answer; 127 | 128 | // get the first real Halfedge in _minBucket 129 | answer = _hash.get(_minBucket).nextInPriorityQueue; 130 | 131 | _hash.get(_minBucket).nextInPriorityQueue = answer.nextInPriorityQueue; 132 | _count--; 133 | answer.nextInPriorityQueue = null; 134 | 135 | return answer; 136 | } 137 | } -------------------------------------------------------------------------------- /src/main/java/com/hoten/delaunay/voronoi/nodename/as3delaunay/ICoord.java: -------------------------------------------------------------------------------- 1 | package com.hoten.delaunay.voronoi.nodename.as3delaunay; 2 | 3 | import com.hoten.delaunay.geom.Point; 4 | 5 | public interface ICoord { 6 | 7 | Point get_coord(); 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/com/hoten/delaunay/voronoi/nodename/as3delaunay/IDisposable.java: -------------------------------------------------------------------------------- 1 | package com.hoten.delaunay.voronoi.nodename.as3delaunay; 2 | 3 | public interface IDisposable { 4 | 5 | void dispose(); 6 | } 7 | -------------------------------------------------------------------------------- /src/main/java/com/hoten/delaunay/voronoi/nodename/as3delaunay/LR.java: -------------------------------------------------------------------------------- 1 | package com.hoten.delaunay.voronoi.nodename.as3delaunay; 2 | 3 | public final class LR { 4 | 5 | final public static LR LEFT = new LR( "left"); 6 | final public static LR RIGHT = new LR( "right"); 7 | private String _name; 8 | 9 | public LR(String name) { 10 | _name = name; 11 | } 12 | 13 | public static LR other(LR leftRight) { 14 | return leftRight == LEFT ? RIGHT : LEFT; 15 | } 16 | 17 | @Override 18 | public String toString() { 19 | return _name; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/com/hoten/delaunay/voronoi/nodename/as3delaunay/LineSegment.java: -------------------------------------------------------------------------------- 1 | package com.hoten.delaunay.voronoi.nodename.as3delaunay; 2 | 3 | import com.hoten.delaunay.geom.Point; 4 | 5 | public final class LineSegment extends Object { 6 | 7 | public static double compareLengths_MAX(LineSegment segment0, LineSegment segment1) { 8 | double length0 = Point.distance(segment0.p0, segment0.p1); 9 | double length1 = Point.distance(segment1.p0, segment1.p1); 10 | if (length0 < length1) { 11 | return 1; 12 | } 13 | if (length0 > length1) { 14 | return -1; 15 | } 16 | return 0; 17 | } 18 | 19 | public static double compareLengths(LineSegment edge0, LineSegment edge1) { 20 | return -compareLengths_MAX(edge0, edge1); 21 | } 22 | public Point p0, p1; 23 | 24 | public LineSegment(Point p0, Point p1) { 25 | super(); 26 | this.p0 = p0; 27 | this.p1 = p1; 28 | } 29 | } -------------------------------------------------------------------------------- /src/main/java/com/hoten/delaunay/voronoi/nodename/as3delaunay/Polygon.java: -------------------------------------------------------------------------------- 1 | package com.hoten.delaunay.voronoi.nodename.as3delaunay; 2 | 3 | import com.hoten.delaunay.geom.Point; 4 | import java.util.ArrayList; 5 | 6 | public final class Polygon { 7 | 8 | private ArrayList _vertices; 9 | 10 | public Polygon(ArrayList vertices) { 11 | _vertices = vertices; 12 | } 13 | 14 | public double area() { 15 | return Math.abs(signedDoubleArea() * 0.5); 16 | } 17 | 18 | public Winding winding() { 19 | double signedDoubleArea = signedDoubleArea(); 20 | if (signedDoubleArea < 0) { 21 | return Winding.CLOCKWISE; 22 | } 23 | if (signedDoubleArea > 0) { 24 | return Winding.COUNTERCLOCKWISE; 25 | } 26 | return Winding.NONE; 27 | } 28 | 29 | private double signedDoubleArea() { 30 | int index, nextIndex; 31 | int n = _vertices.size(); 32 | Point point, next; 33 | double signedDoubleArea = 0; 34 | for (index = 0; index < n; ++index) { 35 | nextIndex = (index + 1) % n; 36 | point = _vertices.get(index); 37 | next = _vertices.get(nextIndex); 38 | signedDoubleArea += point.x * next.y - next.x * point.y; 39 | } 40 | return signedDoubleArea; 41 | } 42 | } -------------------------------------------------------------------------------- /src/main/java/com/hoten/delaunay/voronoi/nodename/as3delaunay/Site.java: -------------------------------------------------------------------------------- 1 | package com.hoten.delaunay.voronoi.nodename.as3delaunay; 2 | 3 | import com.hoten.delaunay.geom.Point; 4 | import com.hoten.delaunay.geom.Rectangle; 5 | import java.awt.Color; 6 | import java.util.ArrayList; 7 | import java.util.Collections; 8 | import java.util.Comparator; 9 | import java.util.Stack; 10 | 11 | public final class Site implements ICoord { 12 | 13 | private static Stack _pool = new Stack(); 14 | 15 | public static Site create(Point p, int index, double weight, Color color) { 16 | if (_pool.size() > 0) { 17 | return _pool.pop().init(p, index, weight, color); 18 | } else { 19 | return new Site(p, index, weight, color); 20 | } 21 | } 22 | 23 | public static void sortSites(ArrayList sites) { 24 | //sites.sort(Site.compare); 25 | Collections.sort(sites, new Comparator() { 26 | @Override 27 | public int compare(Site o1, Site o2) { 28 | return (int) Site.compare(o1, o2); 29 | } 30 | }); 31 | } 32 | 33 | /** 34 | * sort sites on y, then x, coord also change each site's _siteIndex to 35 | * match its new position in the list so the _siteIndex can be used to 36 | * identify the site for nearest-neighbor queries 37 | * 38 | * haha "also" - means more than one responsibility... 39 | * 40 | */ 41 | private static double compare(Site s1, Site s2) { 42 | int returnValue = Voronoi.compareByYThenX(s1, s2); 43 | 44 | // swap _siteIndex values if necessary to match new ordering: 45 | int tempIndex; 46 | if (returnValue == -1) { 47 | if (s1._siteIndex > s2._siteIndex) { 48 | tempIndex = s1._siteIndex; 49 | s1._siteIndex = s2._siteIndex; 50 | s2._siteIndex = tempIndex; 51 | } 52 | } else if (returnValue == 1) { 53 | if (s2._siteIndex > s1._siteIndex) { 54 | tempIndex = s2._siteIndex; 55 | s2._siteIndex = s1._siteIndex; 56 | s1._siteIndex = tempIndex; 57 | } 58 | 59 | } 60 | 61 | return returnValue; 62 | } 63 | final private static double EPSILON = .005; 64 | 65 | private static boolean closeEnough(Point p0, Point p1) { 66 | return Point.distance(p0, p1) < EPSILON; 67 | } 68 | private Point _coord; 69 | 70 | @Override 71 | public Point get_coord() { 72 | return _coord; 73 | } 74 | public Color color; 75 | public double weight; 76 | private int _siteIndex; 77 | // the edges that define this Site's Voronoi region: 78 | public ArrayList _edges; 79 | // which end of each edge hooks up with the previous edge in _edges: 80 | private ArrayList _edgeOrientations; 81 | // ordered list of points that define the region clipped to bounds: 82 | private ArrayList _region; 83 | 84 | public Site(Point p, int index, double weight, Color color) { 85 | init(p, index, weight, color); 86 | } 87 | 88 | private Site init(Point p, int index, double weight, Color color) { 89 | _coord = p; 90 | _siteIndex = index; 91 | this.weight = weight; 92 | this.color = color; 93 | _edges = new ArrayList(); 94 | _region = null; 95 | return this; 96 | } 97 | 98 | @Override 99 | public String toString() { 100 | return "Site " + _siteIndex + ": " + get_coord(); 101 | } 102 | 103 | private void move(Point p) { 104 | clear(); 105 | _coord = p; 106 | } 107 | 108 | public void dispose() { 109 | _coord = null; 110 | clear(); 111 | _pool.push(this); 112 | } 113 | 114 | private void clear() { 115 | if (_edges != null) { 116 | _edges.clear(); 117 | _edges = null; 118 | } 119 | if (_edgeOrientations != null) { 120 | _edgeOrientations.clear(); 121 | _edgeOrientations = null; 122 | } 123 | if (_region != null) { 124 | _region.clear(); 125 | _region = null; 126 | } 127 | } 128 | 129 | void addEdge(Edge edge) { 130 | _edges.add(edge); 131 | } 132 | 133 | public Edge nearestEdge() { 134 | // _edges.sort(Edge.compareSitesDistances); 135 | Collections.sort(_edges, new Comparator() { 136 | @Override 137 | public int compare(Edge o1, Edge o2) { 138 | return (int) Edge.compareSitesDistances(o1, o2); 139 | } 140 | }); 141 | return _edges.get(0); 142 | } 143 | 144 | ArrayList neighborSites() { 145 | if (_edges == null || _edges.isEmpty()) { 146 | return new ArrayList(); 147 | } 148 | if (_edgeOrientations == null) { 149 | reorderEdges(); 150 | } 151 | ArrayList list = new ArrayList(); 152 | for (Edge edge : _edges) { 153 | list.add(neighborSite(edge)); 154 | } 155 | return list; 156 | } 157 | 158 | private Site neighborSite(Edge edge) { 159 | if (this == edge.get_leftSite()) { 160 | return edge.get_rightSite(); 161 | } 162 | if (this == edge.get_rightSite()) { 163 | return edge.get_leftSite(); 164 | } 165 | return null; 166 | } 167 | 168 | ArrayList region(Rectangle clippingBounds) { 169 | if (_edges == null || _edges.isEmpty()) { 170 | return new ArrayList(); 171 | } 172 | if (_edgeOrientations == null) { 173 | reorderEdges(); 174 | _region = clipToBounds(clippingBounds); 175 | if ((new Polygon(_region)).winding() == Winding.CLOCKWISE) { 176 | Collections.reverse(_region); 177 | } 178 | } 179 | return _region; 180 | } 181 | 182 | private void reorderEdges() { 183 | //trace("_edges:", _edges); 184 | EdgeReorderer reorderer = new EdgeReorderer(_edges, Vertex.class); 185 | _edges = reorderer.get_edges(); 186 | //trace("reordered:", _edges); 187 | _edgeOrientations = reorderer.get_edgeOrientations(); 188 | reorderer.dispose(); 189 | } 190 | 191 | private ArrayList clipToBounds(Rectangle bounds) { 192 | ArrayList points = new ArrayList(); 193 | int n = _edges.size(); 194 | int i = 0; 195 | Edge edge; 196 | while (i < n && (_edges.get(i).get_visible() == false)) { 197 | ++i; 198 | } 199 | 200 | if (i == n) { 201 | // no edges visible 202 | return new ArrayList(); 203 | } 204 | edge = _edges.get(i); 205 | LR orientation = _edgeOrientations.get(i); 206 | points.add(edge.get_clippedEnds().get(orientation)); 207 | points.add(edge.get_clippedEnds().get((LR.other(orientation)))); 208 | 209 | for (int j = i + 1; j < n; ++j) { 210 | edge = _edges.get(j); 211 | if (edge.get_visible() == false) { 212 | continue; 213 | } 214 | connect(points, j, bounds, false); 215 | } 216 | // close up the polygon by adding another corner point of the bounds if needed: 217 | connect(points, i, bounds, true); 218 | 219 | return points; 220 | } 221 | 222 | private void connect(ArrayList points, int j, Rectangle bounds, boolean closingUp) { 223 | Point rightPoint = points.get(points.size() - 1); 224 | Edge newEdge = _edges.get(j); 225 | LR newOrientation = _edgeOrientations.get(j); 226 | // the point that must be connected to rightPoint: 227 | Point newPoint = newEdge.get_clippedEnds().get(newOrientation); 228 | if (!closeEnough(rightPoint, newPoint)) { 229 | // The points do not coincide, so they must have been clipped at the bounds; 230 | // see if they are on the same border of the bounds: 231 | if (rightPoint.x != newPoint.x 232 | && rightPoint.y != newPoint.y) { 233 | // They are on different borders of the bounds; 234 | // insert one or two corners of bounds as needed to hook them up: 235 | // (NOTE this will not be correct if the region should take up more than 236 | // half of the bounds rect, for then we will have gone the wrong way 237 | // around the bounds and included the smaller part rather than the larger) 238 | int rightCheck = BoundsCheck.check(rightPoint, bounds); 239 | int newCheck = BoundsCheck.check(newPoint, bounds); 240 | double px, py; 241 | if ((rightCheck & BoundsCheck.RIGHT) != 0) { 242 | px = bounds.right; 243 | if ((newCheck & BoundsCheck.BOTTOM) != 0) { 244 | py = bounds.bottom; 245 | points.add(new Point(px, py)); 246 | } else if ((newCheck & BoundsCheck.TOP) != 0) { 247 | py = bounds.top; 248 | points.add(new Point(px, py)); 249 | } else if ((newCheck & BoundsCheck.LEFT) != 0) { 250 | if (rightPoint.y - bounds.y + newPoint.y - bounds.y < bounds.height) { 251 | py = bounds.top; 252 | } else { 253 | py = bounds.bottom; 254 | } 255 | points.add(new Point(px, py)); 256 | points.add(new Point(bounds.left, py)); 257 | } 258 | } else if ((rightCheck & BoundsCheck.LEFT) != 0) { 259 | px = bounds.left; 260 | if ((newCheck & BoundsCheck.BOTTOM) != 0) { 261 | py = bounds.bottom; 262 | points.add(new Point(px, py)); 263 | } else if ((newCheck & BoundsCheck.TOP) != 0) { 264 | py = bounds.top; 265 | points.add(new Point(px, py)); 266 | } else if ((newCheck & BoundsCheck.RIGHT) != 0) { 267 | if (rightPoint.y - bounds.y + newPoint.y - bounds.y < bounds.height) { 268 | py = bounds.top; 269 | } else { 270 | py = bounds.bottom; 271 | } 272 | points.add(new Point(px, py)); 273 | points.add(new Point(bounds.right, py)); 274 | } 275 | } else if ((rightCheck & BoundsCheck.TOP) != 0) { 276 | py = bounds.top; 277 | if ((newCheck & BoundsCheck.RIGHT) != 0) { 278 | px = bounds.right; 279 | points.add(new Point(px, py)); 280 | } else if ((newCheck & BoundsCheck.LEFT) != 0) { 281 | px = bounds.left; 282 | points.add(new Point(px, py)); 283 | } else if ((newCheck & BoundsCheck.BOTTOM) != 0) { 284 | if (rightPoint.x - bounds.x + newPoint.x - bounds.x < bounds.width) { 285 | px = bounds.left; 286 | } else { 287 | px = bounds.right; 288 | } 289 | points.add(new Point(px, py)); 290 | points.add(new Point(px, bounds.bottom)); 291 | } 292 | } else if ((rightCheck & BoundsCheck.BOTTOM) != 0) { 293 | py = bounds.bottom; 294 | if ((newCheck & BoundsCheck.RIGHT) != 0) { 295 | px = bounds.right; 296 | points.add(new Point(px, py)); 297 | } else if ((newCheck & BoundsCheck.LEFT) != 0) { 298 | px = bounds.left; 299 | points.add(new Point(px, py)); 300 | } else if ((newCheck & BoundsCheck.TOP) != 0) { 301 | if (rightPoint.x - bounds.x + newPoint.x - bounds.x < bounds.width) { 302 | px = bounds.left; 303 | } else { 304 | px = bounds.right; 305 | } 306 | points.add(new Point(px, py)); 307 | points.add(new Point(px, bounds.top)); 308 | } 309 | } 310 | } 311 | if (closingUp) { 312 | // newEdge's ends have already been added 313 | return; 314 | } 315 | points.add(newPoint); 316 | } 317 | Point newRightPoint = newEdge.get_clippedEnds().get(LR.other(newOrientation)); 318 | if (!closeEnough(points.get(0), newRightPoint)) { 319 | points.add(newRightPoint); 320 | } 321 | } 322 | 323 | public double get_x() { 324 | return _coord.x; 325 | } 326 | 327 | public double get_y() { 328 | return _coord.y; 329 | } 330 | 331 | public double dist(ICoord p) { 332 | return Point.distance(p.get_coord(), this._coord); 333 | } 334 | } 335 | 336 | final class BoundsCheck { 337 | 338 | final public static int TOP = 1; 339 | final public static int BOTTOM = 2; 340 | final public static int LEFT = 4; 341 | final public static int RIGHT = 8; 342 | 343 | /** 344 | * 345 | * @param point 346 | * @param bounds 347 | * @return an int with the appropriate bits set if the Point lies on the 348 | * corresponding bounds lines 349 | * 350 | */ 351 | public static int check(Point point, Rectangle bounds) { 352 | int value = 0; 353 | if (point.x == bounds.left) { 354 | value |= LEFT; 355 | } 356 | if (point.x == bounds.right) { 357 | value |= RIGHT; 358 | } 359 | if (point.y == bounds.top) { 360 | value |= TOP; 361 | } 362 | if (point.y == bounds.bottom) { 363 | value |= BOTTOM; 364 | } 365 | return value; 366 | } 367 | 368 | public BoundsCheck() { 369 | throw new Error("BoundsCheck constructor unused"); 370 | } 371 | } 372 | -------------------------------------------------------------------------------- /src/main/java/com/hoten/delaunay/voronoi/nodename/as3delaunay/SiteList.java: -------------------------------------------------------------------------------- 1 | package com.hoten.delaunay.voronoi.nodename.as3delaunay; 2 | 3 | import com.hoten.delaunay.geom.Point; 4 | import com.hoten.delaunay.geom.Rectangle; 5 | import java.util.ArrayList; 6 | 7 | public final class SiteList implements IDisposable { 8 | 9 | private ArrayList _sites; 10 | private int _currentIndex; 11 | private boolean _sorted; 12 | 13 | public SiteList() { 14 | _sites = new ArrayList(); 15 | _sorted = false; 16 | } 17 | 18 | @Override 19 | public void dispose() { 20 | if (_sites != null) { 21 | for (Site site : _sites) { 22 | site.dispose(); 23 | } 24 | _sites.clear(); 25 | _sites = null; 26 | } 27 | } 28 | 29 | public int push(Site site) { 30 | _sorted = false; 31 | _sites.add(site); 32 | return _sites.size(); 33 | } 34 | 35 | public int get_length() { 36 | return _sites.size(); 37 | } 38 | 39 | public Site next() { 40 | if (_sorted == false) { 41 | throw new Error("SiteList::next(): sites have not been sorted"); 42 | } 43 | if (_currentIndex < _sites.size()) { 44 | return _sites.get(_currentIndex++); 45 | } else { 46 | return null; 47 | } 48 | } 49 | 50 | public Rectangle getSitesBounds() { 51 | if (_sorted == false) { 52 | Site.sortSites(_sites); 53 | _currentIndex = 0; 54 | _sorted = true; 55 | } 56 | double xmin, xmax, ymin, ymax; 57 | if (_sites.isEmpty()) { 58 | return new Rectangle(0, 0, 0, 0); 59 | } 60 | xmin = Double.MAX_VALUE; 61 | xmax = Double.MIN_VALUE; 62 | for (Site site : _sites) { 63 | if (site.get_x() < xmin) { 64 | xmin = site.get_x(); 65 | } 66 | if (site.get_x() > xmax) { 67 | xmax = site.get_x(); 68 | } 69 | } 70 | // here's where we assume that the sites have been sorted on y: 71 | ymin = _sites.get(0).get_y(); 72 | ymax = _sites.get(_sites.size() - 1).get_y(); 73 | 74 | return new Rectangle(xmin, ymin, xmax - xmin, ymax - ymin); 75 | } 76 | 77 | /*public ArrayList siteColors(referenceImage:BitmapData = null) 78 | { 79 | var colors:Vector. = new Vector.(); 80 | for each (var site:Site in _sites) 81 | { 82 | colors.push(referenceImage ? referenceImage.getPixel(site.x, site.y) : site.color); 83 | } 84 | return colors; 85 | }*/ 86 | public ArrayList siteCoords() { 87 | ArrayList coords = new ArrayList(); 88 | for (Site site : _sites) { 89 | coords.add(site.get_coord()); 90 | } 91 | return coords; 92 | } 93 | 94 | /** 95 | * 96 | * @return the largest circle centered at each site that fits in its region; 97 | * if the region is infinite, return a circle of radius 0. 98 | * 99 | */ 100 | public ArrayList circles() { 101 | ArrayList circles = new ArrayList(); 102 | for (Site site : _sites) { 103 | double radius = 0; 104 | Edge nearestEdge = site.nearestEdge(); 105 | 106 | //!nearestEdge.isPartOfConvexHull() && (radius = nearestEdge.sitesDistance() * 0.5); 107 | if (!nearestEdge.isPartOfConvexHull()) { 108 | radius = nearestEdge.sitesDistance() * 0.5; 109 | } 110 | circles.add(new Circle(site.get_x(), site.get_y(), radius)); 111 | } 112 | return circles; 113 | } 114 | 115 | public ArrayList> regions(Rectangle plotBounds) { 116 | ArrayList> regions = new ArrayList(); 117 | for (Site site : _sites) { 118 | regions.add(site.region(plotBounds)); 119 | } 120 | return regions; 121 | } 122 | /** 123 | * 124 | * @param proximityMap a BitmapData whose regions are filled with the site 125 | * index values; see PlanePointsCanvas::fillRegions() 126 | * @param x 127 | * @param y 128 | * @return coordinates of nearest Site to (x, y) 129 | * 130 | */ 131 | /*public Point nearestSitePoint(proximityMap:BitmapData, double x, double y) 132 | { 133 | var index:uint = proximityMap.getPixel(x, y); 134 | if (index > _sites.length - 1) 135 | { 136 | return null; 137 | } 138 | return _sites[index].coord; 139 | }*/ 140 | } -------------------------------------------------------------------------------- /src/main/java/com/hoten/delaunay/voronoi/nodename/as3delaunay/Triangle.java: -------------------------------------------------------------------------------- 1 | package com.hoten.delaunay.voronoi.nodename.as3delaunay; 2 | 3 | import java.util.ArrayList; 4 | 5 | public final class Triangle { 6 | 7 | private ArrayList _sites; 8 | 9 | public ArrayList get_sites() { 10 | return _sites; 11 | } 12 | 13 | public Triangle(Site a, Site b, Site c) { 14 | _sites = new ArrayList(); 15 | _sites.add(a); 16 | _sites.add(b); 17 | _sites.add(c); 18 | } 19 | 20 | public void dispose() { 21 | _sites.clear(); 22 | _sites = null; 23 | } 24 | } -------------------------------------------------------------------------------- /src/main/java/com/hoten/delaunay/voronoi/nodename/as3delaunay/Vertex.java: -------------------------------------------------------------------------------- 1 | package com.hoten.delaunay.voronoi.nodename.as3delaunay; 2 | 3 | import com.hoten.delaunay.geom.Point; 4 | import java.util.Stack; 5 | 6 | final public class Vertex extends Object implements ICoord { 7 | 8 | final public static Vertex VERTEX_AT_INFINITY = new Vertex(Double.NaN, Double.NaN); 9 | final private static Stack _pool = new Stack(); 10 | 11 | private static Vertex create(double x, double y) { 12 | 13 | if (Double.isNaN(x) || Double.isNaN(y)) { 14 | return VERTEX_AT_INFINITY; 15 | } 16 | if (_pool.size() > 0) { 17 | 18 | return _pool.pop().init(x, y); 19 | } else { 20 | return new Vertex(x, y); 21 | } 22 | } 23 | private static int _nvertices = 0; 24 | private Point _coord; 25 | 26 | @Override 27 | public Point get_coord() { 28 | return _coord; 29 | } 30 | private int _vertexIndex; 31 | 32 | public int get_vertexIndex() { 33 | return _vertexIndex; 34 | } 35 | 36 | public Vertex(double x, double y) { 37 | init(x, y); 38 | } 39 | 40 | private Vertex init(double x, double y) { 41 | _coord = new Point(x, y); 42 | return this; 43 | } 44 | 45 | public void dispose() { 46 | _coord = null; 47 | _pool.push(this); 48 | } 49 | 50 | public void setIndex() { 51 | _vertexIndex = _nvertices++; 52 | } 53 | 54 | @Override 55 | public String toString() { 56 | return "Vertex (" + _vertexIndex + ")"; 57 | } 58 | 59 | /** 60 | * This is the only way to make a Vertex 61 | * 62 | * @param halfedge0 63 | * @param halfedge1 64 | * @return 65 | * 66 | */ 67 | public static Vertex intersect(Halfedge halfedge0, Halfedge halfedge1) { 68 | Edge edge0, edge1, edge; 69 | Halfedge halfedge; 70 | double determinant, intersectionX, intersectionY; 71 | boolean rightOfSite; 72 | 73 | edge0 = halfedge0.edge; 74 | edge1 = halfedge1.edge; 75 | if (edge0 == null || edge1 == null) { 76 | return null; 77 | } 78 | if (edge0.get_rightSite() == edge1.get_rightSite()) { 79 | return null; 80 | } 81 | 82 | determinant = edge0.a * edge1.b - edge0.b * edge1.a; 83 | if (-1.0e-10 < determinant && determinant < 1.0e-10) { 84 | // the edges are parallel 85 | return null; 86 | } 87 | 88 | intersectionX = (edge0.c * edge1.b - edge1.c * edge0.b) / determinant; 89 | intersectionY = (edge1.c * edge0.a - edge0.c * edge1.a) / determinant; 90 | 91 | if (Voronoi.compareByYThenX(edge0.get_rightSite(), edge1.get_rightSite()) < 0) { 92 | halfedge = halfedge0; 93 | edge = edge0; 94 | } else { 95 | halfedge = halfedge1; 96 | edge = edge1; 97 | } 98 | rightOfSite = intersectionX >= edge.get_rightSite().get_x(); 99 | if ((rightOfSite && halfedge.leftRight == LR.LEFT) 100 | || (!rightOfSite && halfedge.leftRight == LR.RIGHT)) { 101 | return null; 102 | } 103 | 104 | return Vertex.create(intersectionX, intersectionY); 105 | } 106 | 107 | public double get_x() { 108 | return _coord.x; 109 | } 110 | 111 | public double get_y() { 112 | return _coord.y; 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/main/java/com/hoten/delaunay/voronoi/nodename/as3delaunay/Voronoi.java: -------------------------------------------------------------------------------- 1 | package com.hoten.delaunay.voronoi.nodename.as3delaunay; 2 | 3 | /* 4 | * Java implementaition by Connor Clark (www.hotengames.com). Pretty much a 1:1 5 | * translation of a wonderful map generating algorthim by Amit Patel of Red Blob Games, 6 | * which can be found here (http://www-cs-students.stanford.edu/~amitp/game-programming/polygon-map-generation/) 7 | * Hopefully it's of use to someone out there who needed it in Java like I did! 8 | * Note, the only island mode implemented is Radial. Implementing more is something for another day. 9 | * 10 | * FORTUNE'S ALGORTIHIM 11 | * 12 | * This is a java implementation of an AS3 (Flash) implementation of an algorthim 13 | * originally created in C++. Pretty much a 1:1 translation from as3 to java, save 14 | * for some necessary workarounds. Original as3 implementation by Alan Shaw (of nodename) 15 | * can be found here (https://github.com/nodename/as3delaunay). Original algorthim 16 | * by Steven Fortune (see lisence for c++ implementation below) 17 | * 18 | * The author of this software is Steven Fortune. Copyright (c) 1994 by AT&T 19 | * Bell Laboratories. 20 | * Permission to use, copy, modify, and distribute this software for any 21 | * purpose without fee is hereby granted, provided that this entire notice 22 | * is included in all copies of any software which is or includes a copy 23 | * or modification of this software and in all copies of the supporting 24 | * documentation for such software. 25 | * THIS SOFTWARE IS BEING PROVIDED "AS IS", WITHOUT ANY EXPRESS OR IMPLIED 26 | * WARRANTY. IN PARTICULAR, NEITHER THE AUTHORS NOR AT&T MAKE ANY 27 | * REPRESENTATION OR WARRANTY OF ANY KIND CONCERNING THE MERCHANTABILITY 28 | * OF THIS SOFTWARE OR ITS FITNESS FOR ANY PARTICULAR PURPOSE. 29 | */ 30 | 31 | import com.hoten.delaunay.geom.Point; 32 | import com.hoten.delaunay.geom.Rectangle; 33 | 34 | import java.awt.Color; 35 | import java.util.ArrayList; 36 | import java.util.HashMap; 37 | import java.util.Random; 38 | 39 | public final class Voronoi { 40 | 41 | private SiteList _sites; 42 | private HashMap _sitesIndexedByLocation; 43 | private ArrayList _triangles; 44 | private ArrayList _edges; 45 | // TODO generalize this so it doesn't have to be a rectangle; 46 | // then we can make the fractal voronois-within-voronois 47 | private Rectangle _plotBounds; 48 | 49 | public Rectangle get_plotBounds() { 50 | return _plotBounds; 51 | } 52 | 53 | public void dispose() { 54 | int i, n; 55 | if (_sites != null) { 56 | _sites.dispose(); 57 | _sites = null; 58 | } 59 | if (_triangles != null) { 60 | n = _triangles.size(); 61 | for (i = 0; i < n; ++i) { 62 | _triangles.get(i).dispose(); 63 | } 64 | _triangles.clear(); 65 | _triangles = null; 66 | } 67 | if (_edges != null) { 68 | n = _edges.size(); 69 | for (i = 0; i < n; ++i) { 70 | _edges.get(i).dispose(); 71 | } 72 | _edges.clear(); 73 | _edges = null; 74 | } 75 | _plotBounds = null; 76 | _sitesIndexedByLocation = null; 77 | } 78 | 79 | public Voronoi(ArrayList points, ArrayList colors, Rectangle plotBounds) { 80 | init(points, colors, plotBounds); 81 | fortunesAlgorithm(); 82 | } 83 | 84 | //TODO check points for negative values. We must search min value too and translate whole list to fit it in positive quadrant. 85 | public Voronoi(ArrayList points, ArrayList colors) { 86 | double maxWidth = 0, maxHeight = 0; 87 | for (Point p : points) { 88 | maxWidth = Math.max(maxWidth, p.x); 89 | maxHeight = Math.max(maxHeight, p.y); 90 | } 91 | System.out.println(maxWidth + "," + maxHeight); 92 | init(points, colors, new Rectangle(0, 0, maxWidth, maxHeight)); 93 | fortunesAlgorithm(); 94 | } 95 | 96 | public Voronoi(int numSites, double maxWidth, double maxHeight, Random r, ArrayList colors) { 97 | ArrayList points = new ArrayList(); 98 | for (int i = 0; i < numSites; i++) { 99 | points.add(new Point(r.nextDouble() * maxWidth, r.nextDouble() * maxHeight)); 100 | } 101 | init(points, colors, new Rectangle(0, 0, maxWidth, maxHeight)); 102 | fortunesAlgorithm(); 103 | } 104 | 105 | private void init(ArrayList points, ArrayList colors, Rectangle plotBounds) { 106 | _sites = new SiteList(); 107 | _sitesIndexedByLocation = new HashMap(); 108 | addSites(points, colors); 109 | _plotBounds = plotBounds; 110 | _triangles = new ArrayList(); 111 | _edges = new ArrayList(); 112 | } 113 | 114 | private void addSites(ArrayList points, ArrayList colors) { 115 | int length = points.size(); 116 | for (int i = 0; i < length; ++i) { 117 | addSite(points.get(i), colors != null ? colors.get(i) : null, i); 118 | } 119 | } 120 | 121 | private void addSite(Point p, Color color, int index) { 122 | double weight = Math.random() * 100; 123 | Site site = Site.create(p, index, weight, color); 124 | _sites.push(site); 125 | _sitesIndexedByLocation.put(p, site); 126 | } 127 | 128 | public ArrayList edges() { 129 | return _edges; 130 | } 131 | 132 | public ArrayList region(Point p) { 133 | Site site = _sitesIndexedByLocation.get(p); 134 | if (site == null) { 135 | return new ArrayList(); 136 | } 137 | return site.region(_plotBounds); 138 | } 139 | 140 | // TODO: bug: if you call this before you call region(), something goes wrong :( 141 | public ArrayList neighborSitesForSite(Point coord) { 142 | ArrayList points = new ArrayList(); 143 | Site site = _sitesIndexedByLocation.get(coord); 144 | if (site == null) { 145 | return points; 146 | } 147 | ArrayList sites = site.neighborSites(); 148 | for (Site neighbor : sites) { 149 | points.add(neighbor.get_coord()); 150 | } 151 | return points; 152 | } 153 | 154 | public ArrayList circles() { 155 | return _sites.circles(); 156 | } 157 | 158 | private ArrayList selectEdgesForSitePoint(Point coord, ArrayList edgesToTest) { 159 | ArrayList filtered = new ArrayList(); 160 | 161 | for (Edge e : edgesToTest) { 162 | if (((e.get_leftSite() != null && e.get_leftSite().get_coord() == coord) 163 | || (e.get_rightSite() != null && e.get_rightSite().get_coord() == coord))) { 164 | filtered.add(e); 165 | } 166 | } 167 | return filtered; 168 | 169 | /*function myTest(edge:Edge, index:int, vector:Vector.):Boolean 170 | { 171 | return ((edge.leftSite && edge.leftSite.coord == coord) 172 | || (edge.rightSite && edge.rightSite.coord == coord)); 173 | }*/ 174 | } 175 | 176 | private ArrayList visibleLineSegments(ArrayList edges) { 177 | ArrayList segments = new ArrayList(); 178 | 179 | for (Edge edge : edges) { 180 | if (edge.get_visible()) { 181 | Point p1 = edge.get_clippedEnds().get(LR.LEFT); 182 | Point p2 = edge.get_clippedEnds().get(LR.RIGHT); 183 | segments.add(new LineSegment(p1, p2)); 184 | } 185 | } 186 | 187 | return segments; 188 | } 189 | 190 | private ArrayList delaunayLinesForEdges(ArrayList edges) { 191 | ArrayList segments = new ArrayList(); 192 | 193 | for (Edge edge : edges) { 194 | segments.add(edge.delaunayLine()); 195 | } 196 | 197 | return segments; 198 | } 199 | 200 | public ArrayList voronoiBoundaryForSite(Point coord) { 201 | return visibleLineSegments(selectEdgesForSitePoint(coord, _edges)); 202 | } 203 | 204 | public ArrayList delaunayLinesForSite(Point coord) { 205 | return delaunayLinesForEdges(selectEdgesForSitePoint(coord, _edges)); 206 | } 207 | 208 | public ArrayList voronoiDiagram() { 209 | return visibleLineSegments(_edges); 210 | } 211 | 212 | /*public ArrayList delaunayTriangulation(keepOutMask:BitmapData = null) 213 | { 214 | return delaunayLinesForEdges(selectNonIntersectingEdges(keepOutMask, _edges)); 215 | }*/ 216 | public ArrayList hull() { 217 | return delaunayLinesForEdges(hullEdges()); 218 | } 219 | 220 | private ArrayList hullEdges() { 221 | ArrayList filtered = new ArrayList(); 222 | 223 | for (Edge e : _edges) { 224 | if (e.isPartOfConvexHull()) { 225 | filtered.add(e); 226 | } 227 | } 228 | 229 | 230 | 231 | return filtered; 232 | 233 | /*function myTest(edge:Edge, index:int, vector:Vector.):Boolean 234 | { 235 | return (edge.isPartOfConvexHull()); 236 | }*/ 237 | } 238 | 239 | public ArrayList hullPointsInOrder() { 240 | ArrayList hullEdges = hullEdges(); 241 | 242 | ArrayList points = new ArrayList(); 243 | if (hullEdges.isEmpty()) { 244 | return points; 245 | } 246 | 247 | EdgeReorderer reorderer = new EdgeReorderer(hullEdges, Site.class); 248 | hullEdges = reorderer.get_edges(); 249 | ArrayList orientations = reorderer.get_edgeOrientations(); 250 | reorderer.dispose(); 251 | 252 | LR orientation; 253 | 254 | int n = hullEdges.size(); 255 | for (int i = 0; i < n; ++i) { 256 | Edge edge = hullEdges.get(i); 257 | orientation = orientations.get(i); 258 | points.add(edge.site(orientation).get_coord()); 259 | } 260 | return points; 261 | } 262 | 263 | /*public ArrayList spanningTree(String type, keepOutMask:BitmapData = null) 264 | { 265 | ArrayList edges = selectNonIntersectingEdges(keepOutMask, _edges); 266 | ArrayList segments = delaunayLinesForEdges(edges); 267 | return kruskal(segments, type); 268 | }*/ 269 | public ArrayList> regions() { 270 | return _sites.regions(_plotBounds); 271 | } 272 | 273 | /*public ArrayList siteColors(referenceImage:BitmapData = null) 274 | { 275 | return _sites.siteColors(referenceImage); 276 | }*/ 277 | /** 278 | * 279 | * @param proximityMap a BitmapData whose regions are filled with the site 280 | * index values; see PlanePointsCanvas::fillRegions() 281 | * @param x 282 | * @param y 283 | * @return coordinates of nearest Site to (x, y) 284 | * 285 | */ 286 | /*public Point nearestSitePoint(proximityMap:BitmapData,double x, double y) 287 | { 288 | return _sites.nearestSitePoint(proximityMap, x, y); 289 | }*/ 290 | public ArrayList siteCoords() { 291 | return _sites.siteCoords(); 292 | } 293 | 294 | private void fortunesAlgorithm() { 295 | Site newSite, bottomSite, topSite, tempSite; 296 | Vertex v, vertex; 297 | Point newintstar = null; 298 | LR leftRight; 299 | Halfedge lbnd, rbnd, llbnd, rrbnd, bisector; 300 | Edge edge; 301 | 302 | Rectangle dataBounds = _sites.getSitesBounds(); 303 | 304 | int sqrt_nsites = (int) Math.sqrt(_sites.get_length() + 4); 305 | HalfedgePriorityQueue heap = new HalfedgePriorityQueue(dataBounds.y, dataBounds.height, sqrt_nsites); 306 | EdgeList edgeList = new EdgeList(dataBounds.x, dataBounds.width, sqrt_nsites); 307 | ArrayList halfEdges = new ArrayList(); 308 | ArrayList vertices = new ArrayList(); 309 | 310 | Site bottomMostSite = _sites.next(); 311 | newSite = _sites.next(); 312 | 313 | for (;;) { 314 | if (heap.empty() == false) { 315 | newintstar = heap.min(); 316 | } 317 | 318 | if (newSite != null 319 | && (heap.empty() || compareByYThenX(newSite, newintstar) < 0)) { 320 | /* new site is smallest */ 321 | //trace("smallest: new site " + newSite); 322 | 323 | // Step 8: 324 | lbnd = edgeList.edgeListLeftNeighbor(newSite.get_coord()); // the Halfedge just to the left of newSite 325 | //trace("lbnd: " + lbnd); 326 | rbnd = lbnd.edgeListRightNeighbor; // the Halfedge just to the right 327 | //trace("rbnd: " + rbnd); 328 | bottomSite = rightRegion(lbnd, bottomMostSite); // this is the same as leftRegion(rbnd) 329 | // this Site determines the region containing the new site 330 | //trace("new Site is in region of existing site: " + bottomSite); 331 | 332 | // Step 9: 333 | edge = Edge.createBisectingEdge(bottomSite, newSite); 334 | //trace("new edge: " + edge); 335 | _edges.add(edge); 336 | 337 | bisector = Halfedge.create(edge, LR.LEFT); 338 | halfEdges.add(bisector); 339 | // inserting two Halfedges into edgeList constitutes Step 10: 340 | // insert bisector to the right of lbnd: 341 | edgeList.insert(lbnd, bisector); 342 | 343 | // first half of Step 11: 344 | if ((vertex = Vertex.intersect(lbnd, bisector)) != null) { 345 | vertices.add(vertex); 346 | heap.remove(lbnd); 347 | lbnd.vertex = vertex; 348 | lbnd.ystar = vertex.get_y() + newSite.dist(vertex); 349 | heap.insert(lbnd); 350 | } 351 | 352 | lbnd = bisector; 353 | bisector = Halfedge.create(edge, LR.RIGHT); 354 | halfEdges.add(bisector); 355 | // second Halfedge for Step 10: 356 | // insert bisector to the right of lbnd: 357 | edgeList.insert(lbnd, bisector); 358 | 359 | // second half of Step 11: 360 | if ((vertex = Vertex.intersect(bisector, rbnd)) != null) { 361 | vertices.add(vertex); 362 | bisector.vertex = vertex; 363 | bisector.ystar = vertex.get_y() + newSite.dist(vertex); 364 | heap.insert(bisector); 365 | } 366 | 367 | newSite = _sites.next(); 368 | } else if (heap.empty() == false) { 369 | /* intersection is smallest */ 370 | lbnd = heap.extractMin(); 371 | llbnd = lbnd.edgeListLeftNeighbor; 372 | rbnd = lbnd.edgeListRightNeighbor; 373 | rrbnd = rbnd.edgeListRightNeighbor; 374 | bottomSite = leftRegion(lbnd, bottomMostSite); 375 | topSite = rightRegion(rbnd, bottomMostSite); 376 | // these three sites define a Delaunay triangle 377 | // (not actually using these for anything...) 378 | //_triangles.push(new Triangle(bottomSite, topSite, rightRegion(lbnd))); 379 | 380 | v = lbnd.vertex; 381 | v.setIndex(); 382 | lbnd.edge.setVertex(lbnd.leftRight, v); 383 | rbnd.edge.setVertex(rbnd.leftRight, v); 384 | edgeList.remove(lbnd); 385 | heap.remove(rbnd); 386 | edgeList.remove(rbnd); 387 | leftRight = LR.LEFT; 388 | if (bottomSite.get_y() > topSite.get_y()) { 389 | tempSite = bottomSite; 390 | bottomSite = topSite; 391 | topSite = tempSite; 392 | leftRight = LR.RIGHT; 393 | } 394 | edge = Edge.createBisectingEdge(bottomSite, topSite); 395 | _edges.add(edge); 396 | bisector = Halfedge.create(edge, leftRight); 397 | halfEdges.add(bisector); 398 | edgeList.insert(llbnd, bisector); 399 | edge.setVertex(LR.other(leftRight), v); 400 | if ((vertex = Vertex.intersect(llbnd, bisector)) != null) { 401 | vertices.add(vertex); 402 | heap.remove(llbnd); 403 | llbnd.vertex = vertex; 404 | llbnd.ystar = vertex.get_y() + bottomSite.dist(vertex); 405 | heap.insert(llbnd); 406 | } 407 | if ((vertex = Vertex.intersect(bisector, rrbnd)) != null) { 408 | vertices.add(vertex); 409 | bisector.vertex = vertex; 410 | bisector.ystar = vertex.get_y() + bottomSite.dist(vertex); 411 | heap.insert(bisector); 412 | } 413 | } else { 414 | break; 415 | } 416 | } 417 | 418 | // heap should be empty now 419 | heap.dispose(); 420 | edgeList.dispose(); 421 | 422 | for (Halfedge halfEdge : halfEdges) { 423 | halfEdge.reallyDispose(); 424 | } 425 | halfEdges.clear(); 426 | 427 | // we need the vertices to clip the edges 428 | for (Edge e : _edges) { 429 | e.clipVertices(_plotBounds); 430 | } 431 | // but we don't actually ever use them again! 432 | for (Vertex v0 : vertices) { 433 | v0.dispose(); 434 | } 435 | vertices.clear(); 436 | 437 | 438 | } 439 | 440 | Site leftRegion(Halfedge he, Site bottomMostSite) { 441 | Edge edge = he.edge; 442 | if (edge == null) { 443 | return bottomMostSite; 444 | } 445 | return edge.site(he.leftRight); 446 | } 447 | 448 | Site rightRegion(Halfedge he, Site bottomMostSite) { 449 | Edge edge = he.edge; 450 | if (edge == null) { 451 | return bottomMostSite; 452 | } 453 | return edge.site(LR.other(he.leftRight)); 454 | } 455 | 456 | public static int compareByYThenX(Site s1, Site s2) { 457 | if (s1.get_y() < s2.get_y()) { 458 | return -1; 459 | } 460 | if (s1.get_y() > s2.get_y()) { 461 | return 1; 462 | } 463 | if (s1.get_x() < s2.get_x()) { 464 | return -1; 465 | } 466 | if (s1.get_x() > s2.get_x()) { 467 | return 1; 468 | } 469 | return 0; 470 | } 471 | 472 | public static int compareByYThenX(Site s1, Point s2) { 473 | if (s1.get_y() < s2.y) { 474 | return -1; 475 | } 476 | if (s1.get_y() > s2.y) { 477 | return 1; 478 | } 479 | if (s1.get_x() < s2.x) { 480 | return -1; 481 | } 482 | if (s1.get_x() > s2.x) { 483 | return 1; 484 | } 485 | return 0; 486 | } 487 | } 488 | -------------------------------------------------------------------------------- /src/main/java/com/hoten/delaunay/voronoi/nodename/as3delaunay/Winding.java: -------------------------------------------------------------------------------- 1 | package com.hoten.delaunay.voronoi.nodename.as3delaunay; 2 | 3 | public final class Winding { 4 | 5 | final public static Winding CLOCKWISE = new Winding("clockwise"); 6 | final public static Winding COUNTERCLOCKWISE = new Winding("counterclockwise"); 7 | final public static Winding NONE = new Winding("none"); 8 | private String _name; 9 | 10 | private Winding(String name) { 11 | super(); 12 | _name = name; 13 | } 14 | 15 | @Override 16 | public String toString() { 17 | return _name; 18 | } 19 | } --------------------------------------------------------------------------------