diff --git a/src/peery/Vorosaic.java b/src/peery/Vorosaic.java new file mode 100644 index 0000000..369514f --- /dev/null +++ b/src/peery/Vorosaic.java @@ -0,0 +1,42 @@ +package peery; + +import peery.pointgenerator.PointMap; +import peery.voronoi.VoronoiRender; +import peery.voronoi.simplevoronoi.GraphEdge; +import peery.voronoi.simplevoronoi.Voronoi; + +import java.awt.image.BufferedImage; +import java.io.File; +import java.util.List; + +import peery.file.FileHandler; +import peery.log.Log; +import peery.log.LogLevel; + +public class Vorosaic { + + public static void main(String[] args) { + FileHandler fh = new FileHandler("resources"); + BufferedImage img = fh.loadImage(fh.TargetImageFile); + Log.log(LogLevel.Debug, "Image size is "+img.getWidth()+"x"+img.getHeight()+"! Pixels:"+img.getWidth()*img.getHeight()); + Voronoi vor = new Voronoi(1); + PointMap p = new PointMap(img); + p.placePoints(89753, 0.1); + + List edges = vor.generateVoronoi(p.getXValues(), p.getYValues(), 0, p.getWidth(), 0, p.getHeight()); + + fh.saveImage(p.render(), new File(fh.OutputFolder+fh.fs+"Output-points.png")); + Log.log(LogLevel.Debug, edges.size()+" points have been placed!"); + fh.saveImage(VoronoiRender.renderGraph(edges, p.getWidth(), p.getHeight()), new File(fh.OutputFolder+fh.fs+"Output-voronoi.png")); + + Log.log(LogLevel.Info, "Starting cellMap ..."); + int[][] cellMap = VoronoiRender.getCellMap(p.getPoints(), p.getWidth(), p.getHeight()); + Log.log(LogLevel.Info, "Finished cellMap!"); + Log.log(LogLevel.Info, "Starting cellColors ..."); + int[] cellColors = VoronoiRender.getCellColors(img, p.getPoints(), cellMap); + Log.log(LogLevel.Info, "Finished cellColors!"); + Log.log(LogLevel.Info, "Starting Voronoi Render ..."); + fh.saveImage(VoronoiRender.renderColorVoronoi(p.getPoints(), cellColors, cellMap), new File(fh.OutputFolder+fh.fs+"Output-render.png")); + } + +} diff --git a/src/peery/file/FileHandler.java b/src/peery/file/FileHandler.java new file mode 100644 index 0000000..3b51e89 --- /dev/null +++ b/src/peery/file/FileHandler.java @@ -0,0 +1,304 @@ +package peery.file; + +import java.awt.Dimension; +import java.awt.image.BufferedImage; +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Scanner; + +import javax.imageio.ImageIO; + +import peery.log.Log; +import peery.log.LogLevel; + +public class FileHandler { + + public File sourceFolder, InputImagesFolder, TargetImageFile, OutputFolder, indexFile; + /* + * sourcePath/ -> all ressources + * sourcePath/Images/ -> Input pictures folder + * sourcePath/Target -> Target picture file + * sourcePath/Output/ -> Output picture folder + */ + public final String fs; + + public FileHandler(String sourcePath){ + if(!System.getProperty("os.name").startsWith("Windows")){ + fs = "/"; + }else{ + fs = "\\"; + } + this.sourceFolder = new File(sourcePath); + this.InputImagesFolder = new File(sourceFolder.getAbsolutePath()+fs+"Images"); + this.TargetImageFile = new File(sourceFolder.getAbsolutePath()+fs+"Target.png"); + this.OutputFolder = new File(sourceFolder.getAbsolutePath()+fs+"Output"); + this.indexFile = new File(this.InputImagesFolder.getAbsolutePath()+"Index.txt"); + if(!this.sourceFolder.exists()){ + this.sourceFolder.mkdirs(); + } + Log.initLog(this.sourceFolder.getAbsolutePath(), this); + if(fs == "\\"){ + Log.log(LogLevel.Debug, "Assumed Windows like Folder declaration. Therefore using "+fs+" as a separator."); + }else{ + Log.log(LogLevel.Debug, "Detected Linux or OSX."); + } + if(!this.validateFolderStructure()){ + Log.log(LogLevel.Error, "Could not validate folder structure! Things are missing!"); + Log.spawnReadMe(this); + System.exit(1); + } + } + + public boolean validateFolderStructure(){ + if(this.sourceFolder.isDirectory()){ + Log.log(LogLevel.Debug, "Detected source folder at "+this.sourceFolder.getAbsolutePath()); + if(this.InputImagesFolder.isDirectory()){ + Log.log(LogLevel.Debug, "Detected Input folder at "+this.InputImagesFolder.getAbsolutePath()); + if(this.OutputFolder.isDirectory()){ + Log.log(LogLevel.Debug, "Detected Output folder at "+this.OutputFolder.getAbsolutePath()); + }else{ + Log.log(LogLevel.Info, "No Output folder found."); + Log.log(LogLevel.Info, "Creating one at "+this.OutputFolder.getAbsolutePath()); + this.OutputFolder.mkdirs(); + } + if(this.TargetImageFile.isFile()){ + Log.log(LogLevel.Debug, "Detected Target Image at "+this.TargetImageFile.getAbsolutePath()); + if(!this.indexFile.isDirectory()){ + Log.log(LogLevel.Debug, "Found no directory blocking the index file."); + return true; + } + else{ + Log.log(LogLevel.Error, "Following folder collides with the index file name: "+this.indexFile.getAbsolutePath()); + return false; + } + }else{ + Log.log(LogLevel.Critical, "No Target Image found! Exiting..."); + return false; + } + }else{ + Log.log(LogLevel.Critical, "No Input folder found."); + Log.log(LogLevel.Critical, "Creating one at "+this.InputImagesFolder.getAbsolutePath()); + this.InputImagesFolder.mkdirs(); + } + }else{ + Log.log(LogLevel.Critical, "No source folder found (redundant check)."); + Log.log(LogLevel.Critical, "Creating one at "+this.sourceFolder.getAbsolutePath()); + this.sourceFolder.mkdirs(); + } + Log.log(LogLevel.Critical, "Folder validation failed. There could be a permission problem " + + "or a folder needed to be created! Please look for earlier errors."); + return false; + } + + public ArrayList listInputFiles(){ + Log.log(LogLevel.Info, "Listing files inside "+this.InputImagesFolder.getName()+" ..."); + ArrayList fileList = new ArrayList(); + for(File f: this.InputImagesFolder.listFiles()){ + if(f.isFile()){ + fileList.add(f.getAbsolutePath()); + } + } + return fileList; + } + + public BufferedImage loadImage(File file){ + Log.log(LogLevel.Info, "Loading image "+file.getName()+" ..."); + if(file.isFile() && file.canRead()){ + BufferedImage img; + try { + img = ImageIO.read(file); + Log.log(LogLevel.Debug, "Loaded image "+file.getName()+" !"); + return img; + } catch (IOException e) { + Log.log(LogLevel.Debug, "File "+file.getPath()+" failed to load as an Image. What did I just read?"); + e.printStackTrace(); + return null; + } + } + else{ + Log.log(LogLevel.Info, "Can't read file "+file.getPath()+" ! It could be a directory or no read permissions."); + return null; + } + } + + public void saveImage(BufferedImage img, File file){ + Log.log(LogLevel.Info, "Saving image as file "+file.getAbsolutePath()); + Log.log(LogLevel.Info, "This could take a moment ..."); + try { + ImageIO.write(img, "png", file); + } catch (IOException e) { + Log.log(LogLevel.Critical, "Couldn't write image "+file.getName()+" to file! Are write permissions missing?" + + " Attempted to write at "+file.getAbsolutePath()); + e.printStackTrace(); + } + Log.log(LogLevel.Info, "Saved "+file.getName()+" !"); + } + + /** + * Plainly appends the given file and rgb value to the end of the index. + * + * CHECK FOR DUPLICATES BEFOREHAND. + * @param file + * @param rgb + */ + public synchronized void appendToIndex(File file, int rgb){ + try{ + BufferedWriter bw = new BufferedWriter(new FileWriter(indexFile, true)); + bw.write(rgb+";"+file.getName()+"\n"); + bw.flush(); + bw.close(); + Log.log(LogLevel.Info, "Wrote index entry for "+file.getName()+" !"); + } catch (IOException e) { + Log.log(LogLevel.Critical, "Couldn't create or write index file "+indexFile.getAbsolutePath()+" ." + + "Are write permissions missing?"); + e.printStackTrace(); + return; + } + } + + /** + * Loads an index entry for a single file. + * + * Loads and parses the whole index in the background if not given the indexData. + * @param indexData + * @param file + * @return + */ + public Integer loadIndexEntry(HashMap indexData, File file){ + Log.log(LogLevel.Debug, "Searching for index data of "+file.getName()+" ..."); + if(indexData == null){ + indexData = loadIndex(); + } + return indexData.get(file.getName()); + } + + /** + * Loads the whole index file into a Hashmap. + * The second value (file name) is used as a String-key, the rgb value is the int-value. + * @param file + * @return + */ + public HashMap loadIndex(){ + if(!this.indexFile.exists()){ + Log.log(LogLevel.Info, "No Index file found. Nothing to load then. Creating empty one ..."); + try { + indexFile.createNewFile(); + } catch (IOException e) { + Log.log(LogLevel.Error, "Couldn't create Index file! Are write permissions missing?"); + e.printStackTrace(); + } + return null; + } + HashMap indexData = new HashMap(); + try { + Scanner sc = new Scanner(new BufferedReader(new FileReader(this.indexFile))); + while(sc.hasNext()){ + @SuppressWarnings("rawtypes") + ArrayList data = parseIndexData(sc.nextLine()); + indexData.put((String)data.get(1), (int)data.get(0)); + } + sc.close(); + } catch (FileNotFoundException e) { + Log.log(LogLevel.Critical, "Could not open index file! Do I have read permissions?"); + e.printStackTrace(); + } + Log.log(LogLevel.Debug, "Sucessfully loaded index!"); + return indexData; + } + + public void overwriteIndex(HashMap index){ + try{ + BufferedWriter bw = new BufferedWriter(new FileWriter(indexFile, false)); + for(String key: index.keySet()){ + bw.write(index.get(key)+";"+key+"\n"); + } + bw.flush(); + bw.close(); + Log.log(LogLevel.Info, "Overwrote index file with new index!"); + } catch (IOException e) { + Log.log(LogLevel.Critical, "Couldn't create or write index file "+indexFile.getAbsolutePath()+" ." + + "Are write permissions missing?"); + e.printStackTrace(); + return; + } + } + + /** + * Parses a line of indexData into an ArrayList containing the rgb int and the name + * @param indexData + * @return + */ + @SuppressWarnings({ "unchecked", "rawtypes" }) + private ArrayList parseIndexData(String indexData){ + String[] data = indexData.split(";"); + ArrayList result = new ArrayList(); + result.add(Integer.parseInt(data[0])); + result.add(data[1]); + + return result; + } + + // could use a single image variant to check synchronized to the classification (saves IO) + public Dimension loadBiggestDimension(){ + File[] files = this.InputImagesFolder.listFiles(); + int width = 0, height = 0, img_count = 0; + for(File f: files){ + if(f.isFile() && f.canRead()){ + BufferedImage img = loadImage(f); + if(img == null){ + continue; + } + img_count++; + if(width < img.getWidth()){ + width = img.getWidth(); + } + if(height < img.getHeight()){ + height = img.getHeight(); + } + } + else{ + Log.log(LogLevel.Info, "Can't read file"+f.toString()+"! It could be a directory or no read permissions."); + } + } + Log.log(LogLevel.Info, img_count+" image(s) were loaded..."); + if(width == 0 || height == 0){ + Log.log(LogLevel.Critical, "Incomplete or no dimension values! Could I load any Image?"); + } + Log.log(LogLevel.Debug, "Biggest dimension is "+width+"x"+height); + return new Dimension(width, height); + } + + // Would probably kill memory (and performance). + @Deprecated + public BufferedImage[] loadAllImages(){ + File[] files = this.InputImagesFolder.listFiles(); + ArrayList imgs = new ArrayList(); + int img_count = 0; + for(File f: files){ + BufferedImage img = loadImage(f); + if(img == null){ + continue; + } + imgs.add(img); + img_count++; + } + if(imgs.size() == 0){ + Log.log(LogLevel.Critical, "No Images found in "+this.InputImagesFolder.getAbsolutePath()); + return null; + } + Log.log(LogLevel.Info, img_count+" image(s) were loaded..."); + BufferedImage[] bfs = new BufferedImage[imgs.size()]; + for(int i = 0; i < imgs.size(); i++){ + bfs[i] = imgs.get(i); + } + return bfs; + } + +} diff --git a/src/peery/log/Log.java b/src/peery/log/Log.java new file mode 100644 index 0000000..9c8b772 --- /dev/null +++ b/src/peery/log/Log.java @@ -0,0 +1,175 @@ +package peery.log; + +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.util.ArrayList; + +import peery.file.FileHandler; + +public class Log { + + public static Log log; + public static final boolean silenceDebug = false, + appendEvents = false, appendErrors = false; + + private final static String readmeText = + "This is the ReadMe for Mosaic.\n" + + "The folders that were created serve following purpose: \n" + + "resources - Is the main folder where all images (Input AND Output are stored). \n" + + "resources%sImages - Is the Input folder for all images you want to use as \"paint\" in the Mosaic. Put all your images in there!\n" + + "resources%sOutput - Stores all resulting Mosaic pictures. There are all named Output-{number}.png \n" + + "resources%sTarget - Is an Image (or symbolic Link to one) of your choice. Mosaic will try to create the mosaic after this inspiration. The name MUST be \"Target\" without any file extension or it will not be recognized." + + "resources%sERROR.log - Is a log file where all non-fatal erros are stored. Take a peek if problems occur. \n" + + "resources%seventLog.log - Is a log for more genereal events. Like progress, events and such with time stamps. Most useful for debugging problems."; + + private String location; + public final File eventFile, errorFile; + private File perfFile; + private BufferedWriter eventWriter, errorWriter, perfWriter; + + private ArrayList nanoTimes; + + public Log(String location, FileHandler fh){ + this.location = location; + this.eventFile = new File(location+fh.fs+"eventLog.log"); + this.errorFile = new File(location+fh.fs+"ERROR.log"); + this.perfFile = new File(location+fh.fs+"perf.log"); + this.nanoTimes = new ArrayList(); + + try { + if(!this.eventFile.exists()){ + this.eventFile.createNewFile(); + } + if(!this.errorFile.exists()){ + this.errorFile.createNewFile(); + } + if(!this.perfFile.exists()){ + this.perfFile.createNewFile(); + } + this.eventWriter = new BufferedWriter(new FileWriter(eventFile, appendEvents)); + this.errorWriter = new BufferedWriter(new FileWriter(errorFile, appendErrors)); + this.perfWriter = new BufferedWriter(new FileWriter(perfFile, true)); + perfWriter.write("\n\n"); + } catch (IOException e) { + e.printStackTrace(); + System.exit(1); + } + } + + public static synchronized void initLog(String location, FileHandler fh){ + if(Log.log != null){ + return; + } + + Log.log = new Log(location, fh); + } + + public static void shutdownLog(){ + if(Log.log == null){ + return; + } + try { + Log.log.errorWriter.close(); + Log.log.eventWriter.close(); + Log.log.perfWriter.close(); + Log.log = null; + } catch (IOException e) { + e.printStackTrace(); + } + } + + public static void log(int logLvl, String message){ + Log.log(LogLevel.values()[logLvl], message); + } + + public static void log(LogLevel lv, String message){ + if(silenceDebug && LogLevel.Debug == lv){ + return; + } + Log.log.logs(lv.ordinal(), message); + } + + @SuppressWarnings("unused") + public synchronized void logs(int logLvl, String message){ + String prefix = LogLevel.values()[logLvl].toString(); + prefix = "["+prefix+"]"; + BufferedWriter logWriter; + if(silenceDebug && logLvl == LogLevel.Debug.ordinal()){ + return; + } + + if(logLvl == LogLevel.Error.ordinal()){ + logWriter = this.errorWriter; + } + else{ + logWriter = this.eventWriter; + } + String timeStamp = new java.util.Date().toString(); + String msg = "["+timeStamp+"]"+prefix+" "+message; + System.out.println(msg); + try { + logWriter.write(msg+"\n"); + if(LogLevel.Info.ordinal() < logLvl){ //Saves perfomance? + logWriter.flush(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + + public synchronized void perfLog(String message){ + long currentTime = System.nanoTime(); + String timeStamp = new java.util.Date().toString()+"|"+currentTime; + this.nanoTimes.add(currentTime); + try { + perfWriter.write(message+"\n"); + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + + public void finishPerfLog(){ + String[] stages = { + "Indexing & Rasterization", // 2 - 1 1 + "Matching", // 4 - 3 2 + "Placement", // 6 - 5 3 + "Saving" // 8 - 7 4 + }; + for(int i = 1; i <= stages.length; i++){ + long duration = nanoTimes.get(i*2) - nanoTimes.get(i*2-1); + try { + perfWriter.write(stages[i-1]+": "+duration+"\n"); + } catch (IOException e) { + e.printStackTrace(); + } + } + try { + perfWriter.flush(); + perfWriter.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + public static void spawnReadMe(FileHandler fh){ + File readme = new File(Log.log.location+fh.fs+"README.txt"); + Log.log(LogLevel.Info, "Spawning README file at "+readme.getAbsolutePath()); + try { + BufferedWriter bw = new BufferedWriter(new FileWriter(readme)); + String rdme = readmeText.replaceAll("%s", fh.fs); + bw.write(rdme); + bw.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + /* + public static void main(String[] args){ + Log.initLog("/home/mosaic/Software_Projects/EclipseWorkspace/Picture Mosaic/resources/"); + Log.log(LogLevel.Debug, "Test!"); + Log.log(LogLevel.Error, "TEST ERROR"); + }*/ +} diff --git a/src/peery/log/LogLevel.java b/src/peery/log/LogLevel.java new file mode 100644 index 0000000..2dfce12 --- /dev/null +++ b/src/peery/log/LogLevel.java @@ -0,0 +1,9 @@ +package peery.log; + +public enum LogLevel { + Info, + Debug, + Critical, + Error + +} diff --git a/src/peery/pointgenerator/PointMap.java b/src/peery/pointgenerator/PointMap.java new file mode 100644 index 0000000..13c86c7 --- /dev/null +++ b/src/peery/pointgenerator/PointMap.java @@ -0,0 +1,93 @@ +package peery.pointgenerator; + +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.image.BufferedImage; +import java.awt.image.ColorModel; +import java.util.ArrayList; +import java.util.Random; + +public class PointMap { + + private BufferedImage greyScale; + + private double[][] pValues; // + private ArrayList points; + + public PointMap(BufferedImage img){ + greyScale = new BufferedImage(img.getWidth(), img.getHeight(), BufferedImage.TYPE_BYTE_GRAY); + + Graphics2D g2 = (Graphics2D) greyScale.getGraphics(); + g2.drawImage(img, 0, 0, img.getWidth(), img.getHeight(), null); + g2.dispose(); + + pValues = new double[img.getWidth()][img.getHeight()]; + ColorModel cm = ColorModel.getRGBdefault(); + for(int y = 0; y < img.getHeight(); y++){ + for(int x = 0; x < img.getWidth(); x++) { + int color = greyScale.getRGB(x, y); + pValues[x][y] = cm.getRed(color)/255.0; + //System.out.println("pValue: "+pValues[x][y]); + } + } + } + + public void placePoints(int seed, double dampening) { + Random rnd = new Random(seed); + setPoints(new ArrayList()); + + for(int x = 0; x < pValues.length; x++) { + for(int y = 0; y < pValues[x].length; y++) { + double r = rnd.nextDouble(); + if(pValues[x][y]*dampening >= r) { //hit + int[] point = {x, y}; + getPoints().add(point); + } + } + } + } + + public BufferedImage render() { + BufferedImage img = new BufferedImage(greyScale.getWidth(), greyScale.getHeight(), BufferedImage.TYPE_INT_RGB); + Graphics2D g2 = (Graphics2D) img.getGraphics(); + g2.setColor(Color.RED); + for(int[] point: getPoints()) { + g2.drawRect(point[0]-1, point[1]-1, 2, 2); + } + g2.dispose(); + + return img; + } + + public double[] getXValues() { + double[] xValues = new double[getPoints().size()]; + for(int i = 0; i < getPoints().size(); i++) { + xValues[i] = getPoints().get(i)[0]; + } + return xValues; + } + + public double[] getYValues() { + double[] yValues = new double[getPoints().size()]; + for(int i = 0; i < getPoints().size(); i++) { + yValues[i] = getPoints().get(i)[1]; + } + return yValues; + } + + public int getWidth() { + return greyScale.getWidth(); + } + + public int getHeight() { + return greyScale.getHeight(); + } + + public ArrayList getPoints() { + return points; + } + + public void setPoints(ArrayList points) { + this.points = points; + } +} diff --git a/src/peery/voronoi/VoronoiRender.java b/src/peery/voronoi/VoronoiRender.java new file mode 100644 index 0000000..72a3b00 --- /dev/null +++ b/src/peery/voronoi/VoronoiRender.java @@ -0,0 +1,114 @@ +package peery.voronoi; + +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.image.BufferedImage; +import java.awt.image.ColorModel; +import java.util.ArrayList; +import java.util.List; + +import peery.log.Log; +import peery.log.LogLevel; +import peery.voronoi.simplevoronoi.GraphEdge; + +public class VoronoiRender { + + public static BufferedImage renderGraph(List edges, int width, int height) { + BufferedImage img = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); + Graphics2D g2 = (Graphics2D) img.getGraphics(); + g2.setColor(Color.BLACK); + g2.drawRect(0, 0, width, height); + + g2.setColor(Color.RED); + for(GraphEdge edge: edges) { + g2.drawLine((int)edge.x1, (int)edge.y1, (int)edge.x2, (int)edge.y2); + } + + g2.dispose(); + return img; + } + + //int = pointIndex in points + /** + * Brute-force algorithm to determine the closest point in 'points' to the pixel (x,y) + * @param points ArrayList of points to choose from + * @param x x-coordinate for the pixel + * @param y y-coodinate for the pixel + * @return Index of the closest point in 'points' + */ + private static int getCellIndex(ArrayList points, int x, int y) { + ArrayList distances = new ArrayList(); + for(int i = 0; i < points.size(); i++) { + double d = Math.sqrt(Math.pow(points.get(i)[0]-x, 2) + Math.pow(points.get(i)[1]-y, 2)); + distances.add(d); + } + int smallestI = 0; + double smallestD = distances.get(0); + for(int i = 0; i < distances.size(); i++) { + if(smallestD > distances.get(i)) { + smallestD = distances.get(i); + smallestI = i; + } + } + + return smallestI; + } + + //int[x][y] = pointIndex in points + public static int[][] getCellMap(ArrayList points, int width, int height){ + int[][] map = new int[width][height]; + for(int y = 0; y < height; y++) { + for(int x = 0; x < width; x++) { + map[x][y] = getCellIndex(points, x, y); + } + Log.log(LogLevel.Debug, "Finished cell index row y:"+y+"/"+height+"!"); + } + return map; + } + + //int[pointIndex in points] = average RGB + public static int[] getCellColors(BufferedImage img, ArrayList points, int[][] cellMap) { + int[][] colors = new int[points.size()][4]; // 0 = red, 1 = green , 2 = blue, amount + ColorModel cm = ColorModel.getRGBdefault(); + int[] amount = new int[points.size()]; + for(int x = 0; x < cellMap.length; x++) { + for(int y = 0; y < cellMap[x].length; y++) { + int color = img.getRGB(x, y); + + int red = cm.getRed(color); + int green = cm.getGreen(color); + int blue = cm.getBlue(color); + + colors[cellMap[x][y]][0] += red; + colors[cellMap[x][y]][1] += green; + colors[cellMap[x][y]][2] += blue; + colors[cellMap[x][y]][3] += 1; + } + } + + int[] colorMap = new int[points.size()]; + for(int i = 0; i < colorMap.length; i++) { + int red = colors[i][0] / colors[i][3]; //red / amount + int green = colors[i][1] / colors[i][3]; // green / amount + int blue = colors[i][2] / colors[i][3]; // blue / amount + colorMap[i] = new Color(red, green, blue).getRGB(); + } + + return colorMap; + } + + public static BufferedImage renderColorVoronoi(ArrayList points, int[] colorMap, int[][] cellMap) { + int width = cellMap.length; + int height = cellMap[0].length; + BufferedImage img = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); + + + for(int x = 0; x < width; x++) { + for(int y = 0; y < height; y++) { + img.setRGB(x, y, colorMap[cellMap[x][y]]); + } + } + return img; + } + +} diff --git a/src/peery/voronoi/simplevoronoi/Edge.java b/src/peery/voronoi/simplevoronoi/Edge.java new file mode 100644 index 0000000..90db17b --- /dev/null +++ b/src/peery/voronoi/simplevoronoi/Edge.java @@ -0,0 +1,47 @@ +/* + Copyright 2011 James Humphreys. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are +permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this list of + conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, this list + of conditions and the following disclaimer in the documentation and/or other materials + provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY James Humphreys ``AS IS'' AND ANY EXPRESS OR IMPLIED +WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL OR +CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +The views and conclusions contained in the software and documentation are those of the +authors and should not be interpreted as representing official policies, either expressed +or implied, of James Humphreys. + */ + +package peery.voronoi.simplevoronoi; + +/** + * + * @author James Humphreys + */ +class Edge +{ + public double a = 0, b = 0, c = 0; + Site[] ep; // JH: End points? + Site[] reg; // JH: Sites this edge bisects? + int edgenbr; + + Edge() + { + ep = new Site[2]; + reg = new Site[2]; + } +} diff --git a/src/peery/voronoi/simplevoronoi/GraphEdge.java b/src/peery/voronoi/simplevoronoi/GraphEdge.java new file mode 100644 index 0000000..7f5ca72 --- /dev/null +++ b/src/peery/voronoi/simplevoronoi/GraphEdge.java @@ -0,0 +1,37 @@ +/* + Copyright 2011 James Humphreys. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are +permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this list of + conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, this list + of conditions and the following disclaimer in the documentation and/or other materials + provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY James Humphreys ``AS IS'' AND ANY EXPRESS OR IMPLIED +WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL OR +CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +The views and conclusions contained in the software and documentation are those of the +authors and should not be interpreted as representing official policies, either expressed +or implied, of James Humphreys. + */ + +package peery.voronoi.simplevoronoi; + +public class GraphEdge +{ + public double x1, y1, x2, y2; + + public int site1; + public int site2; +} diff --git a/src/peery/voronoi/simplevoronoi/Halfedge.java b/src/peery/voronoi/simplevoronoi/Halfedge.java new file mode 100644 index 0000000..a15d16a --- /dev/null +++ b/src/peery/voronoi/simplevoronoi/Halfedge.java @@ -0,0 +1,45 @@ +/* + Copyright 2011 James Humphreys. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are +permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this list of + conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, this list + of conditions and the following disclaimer in the documentation and/or other materials + provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY James Humphreys ``AS IS'' AND ANY EXPRESS OR IMPLIED +WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL OR +CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +The views and conclusions contained in the software and documentation are those of the +authors and should not be interpreted as representing official policies, either expressed +or implied, of James Humphreys. + */ + +package peery.voronoi.simplevoronoi; + +public class Halfedge +{ + Halfedge ELleft, ELright; + Edge ELedge; + boolean deleted; + int ELpm; + Site vertex; + double ystar; + Halfedge PQnext; + + public Halfedge() + { + PQnext = null; + } +} diff --git a/src/peery/voronoi/simplevoronoi/Point.java b/src/peery/voronoi/simplevoronoi/Point.java new file mode 100644 index 0000000..0dc0501 --- /dev/null +++ b/src/peery/voronoi/simplevoronoi/Point.java @@ -0,0 +1,44 @@ +/* + Copyright 2011 James Humphreys. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are +permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this list of + conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, this list + of conditions and the following disclaimer in the documentation and/or other materials + provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY James Humphreys ``AS IS'' AND ANY EXPRESS OR IMPLIED +WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL OR +CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +The views and conclusions contained in the software and documentation are those of the +authors and should not be interpreted as representing official policies, either expressed +or implied, of James Humphreys. + */ + +package peery.voronoi.simplevoronoi; + +class Point +{ + double x, y; + + public Point() + { + } + + public void setPoint(double x, double y) + { + this.x = x; + this.y = y; + } +} diff --git a/src/peery/voronoi/simplevoronoi/Site.java b/src/peery/voronoi/simplevoronoi/Site.java new file mode 100644 index 0000000..f6246e9 --- /dev/null +++ b/src/peery/voronoi/simplevoronoi/Site.java @@ -0,0 +1,41 @@ +/* + Copyright 2011 James Humphreys. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are +permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this list of + conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, this list + of conditions and the following disclaimer in the documentation and/or other materials + provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY James Humphreys ``AS IS'' AND ANY EXPRESS OR IMPLIED +WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL OR +CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +The views and conclusions contained in the software and documentation are those of the +authors and should not be interpreted as representing official policies, either expressed +or implied, of James Humphreys. + */ + +package peery.voronoi.simplevoronoi; + +// used both for sites and for vertices +public class Site +{ + Point coord; + int sitenbr; + + public Site() + { + coord = new Point(); + } +} diff --git a/src/peery/voronoi/simplevoronoi/Voronoi.java b/src/peery/voronoi/simplevoronoi/Voronoi.java new file mode 100644 index 0000000..4e166a1 --- /dev/null +++ b/src/peery/voronoi/simplevoronoi/Voronoi.java @@ -0,0 +1,1020 @@ +package peery.voronoi.simplevoronoi; + +/* + * The author of this software is Steven Fortune. Copyright (c) 1994 by AT&T + * Bell Laboratories. + * Permission to use, copy, modify, and distribute this software for any + * purpose without fee is hereby granted, provided that this entire notice + * is included in all copies of any software which is or includes a copy + * or modification of this software and in all copies of the supporting + * documentation for such software. + * THIS SOFTWARE IS BEING PROVIDED "AS IS", WITHOUT ANY EXPRESS OR IMPLIED + * WARRANTY. IN PARTICULAR, NEITHER THE AUTHORS NOR AT&T MAKE ANY + * REPRESENTATION OR WARRANTY OF ANY KIND CONCERNING THE MERCHANTABILITY + * OF THIS SOFTWARE OR ITS FITNESS FOR ANY PARTICULAR PURPOSE. + */ + +/* + * This code was originally written by Stephan Fortune in C code. I, Shane O'Sullivan, + * have since modified it, encapsulating it in a C++ class and, fixing memory leaks and + * adding accessors to the Voronoi Edges. + * Permission to use, copy, modify, and distribute this software for any + * purpose without fee is hereby granted, provided that this entire notice + * is included in all copies of any software which is or includes a copy + * or modification of this software and in all copies of the supporting + * documentation for such software. + * THIS SOFTWARE IS BEING PROVIDED "AS IS", WITHOUT ANY EXPRESS OR IMPLIED + * WARRANTY. IN PARTICULAR, NEITHER THE AUTHORS NOR AT&T MAKE ANY + * REPRESENTATION OR WARRANTY OF ANY KIND CONCERNING THE MERCHANTABILITY + * OF THIS SOFTWARE OR ITS FITNESS FOR ANY PARTICULAR PURPOSE. + */ + +/* + * Java Version by Zhenyu Pan + * Permission to use, copy, modify, and distribute this software for any + * purpose without fee is hereby granted, provided that this entire notice + * is included in all copies of any software which is or includes a copy + * or modification of this software and in all copies of the supporting + * documentation for such software. + * THIS SOFTWARE IS BEING PROVIDED "AS IS", WITHOUT ANY EXPRESS OR IMPLIED + * WARRANTY. IN PARTICULAR, NEITHER THE AUTHORS NOR AT&T MAKE ANY + * REPRESENTATION OR WARRANTY OF ANY KIND CONCERNING THE MERCHANTABILITY + * OF THIS SOFTWARE OR ITS FITNESS FOR ANY PARTICULAR PURPOSE. + */ + + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.LinkedList; +import java.util.List; + +public class Voronoi +{ + // ************* Private members ****************** + private double borderMinX, borderMaxX, borderMinY, borderMaxY; + private int siteidx; + private double xmin, xmax, ymin, ymax, deltax, deltay; + private int nvertices; + private int nedges; + private int nsites; + private Site[] sites; + private Site bottomsite; + private int sqrt_nsites; + private double minDistanceBetweenSites; + private int PQcount; + private int PQmin; + private int PQhashsize; + private Halfedge PQhash[]; + + private final static int LE = 0; + private final static int RE = 1; + + private int ELhashsize; + private Halfedge ELhash[]; + private Halfedge ELleftend, ELrightend; + private List allEdges; + + /********************************************************* + * Public methods + ********************************************************/ + + public Voronoi(double minDistanceBetweenSites) + { + siteidx = 0; + sites = null; + + allEdges = null; + this.minDistanceBetweenSites = minDistanceBetweenSites; + } + + /** + * + * @param xValuesIn Array of X values for each site. + * @param yValuesIn Array of Y values for each site. Must be identical length to yValuesIn + * @param minX The minimum X of the bounding box around the voronoi + * @param maxX The maximum X of the bounding box around the voronoi + * @param minY The minimum Y of the bounding box around the voronoi + * @param maxY The maximum Y of the bounding box around the voronoi + * @return + */ + public List generateVoronoi(double[] xValuesIn, double[] yValuesIn, + double minX, double maxX, double minY, double maxY) + { + sort(xValuesIn, yValuesIn, xValuesIn.length); + + // Check bounding box inputs - if mins are bigger than maxes, swap them + double temp = 0; + if (minX > maxX) + { + temp = minX; + minX = maxX; + maxX = temp; + } + if (minY > maxY) + { + temp = minY; + minY = maxY; + maxY = temp; + } + borderMinX = minX; + borderMinY = minY; + borderMaxX = maxX; + borderMaxY = maxY; + + siteidx = 0; + voronoi_bd(); + + return allEdges; + } + + + + /********************************************************* + * Private methods - implementation details + ********************************************************/ + + private void sort(double[] xValuesIn, double[] yValuesIn, int count) + { + sites = null; + allEdges = new LinkedList(); + + nsites = count; + nvertices = 0; + nedges = 0; + + double sn = (double) nsites + 4; + sqrt_nsites = (int) Math.sqrt(sn); + + // Copy the inputs so we don't modify the originals + double[] xValues = new double[count]; + double[] yValues = new double[count]; + for (int i = 0; i < count; i++) + { + xValues[i] = xValuesIn[i]; + yValues[i] = yValuesIn[i]; + } + sortNode(xValues, yValues, count); + } + + private void qsort(Site[] sites) + { + List listSites = new ArrayList(sites.length); + for (Site s: sites) + { + listSites.add(s); + } + + Collections.sort(listSites, new Comparator() + { + @Override + public final int compare(Site p1, Site p2) + { + Point s1 = p1.coord, s2 = p2.coord; + if (s1.y < s2.y) + { + return (-1); + } + if (s1.y > s2.y) + { + return (1); + } + if (s1.x < s2.x) + { + return (-1); + } + if (s1.x > s2.x) + { + return (1); + } + return (0); + } + }); + + // Copy back into the array + for (int i=0; i xmax) + { + xmax = xValues[i]; + } + + if (yValues[i] < ymin) + { + ymin = yValues[i]; + } else if (yValues[i] > ymax) + { + ymax = yValues[i]; + } + } + qsort(sites); + deltay = ymax - ymin; + deltax = xmax - xmin; + } + + /* return a single in-storage site */ + private Site nextone() + { + Site s; + if (siteidx < nsites) + { + s = sites[siteidx]; + siteidx += 1; + return (s); + } else + { + return (null); + } + } + + private Edge bisect(Site s1, Site s2) + { + double dx, dy, adx, ady; + Edge newedge; + + newedge = new Edge(); + + // store the sites that this edge is bisecting + newedge.reg[0] = s1; + newedge.reg[1] = s2; + // to begin with, there are no endpoints on the bisector - it goes to + // infinity + newedge.ep[0] = null; + newedge.ep[1] = null; + + // get the difference in x dist between the sites + dx = s2.coord.x - s1.coord.x; + dy = s2.coord.y - s1.coord.y; + // make sure that the difference in positive + adx = dx > 0 ? dx : -dx; + ady = dy > 0 ? dy : -dy; + newedge.c = (double) (s1.coord.x * dx + s1.coord.y * dy + (dx * dx + dy + * dy) * 0.5);// get the slope of the line + + if (adx > ady) + { + newedge.a = 1.0f; + newedge.b = dy / dx; + newedge.c /= dx;// set formula of line, with x fixed to 1 + } else + { + newedge.b = 1.0f; + newedge.a = dx / dy; + newedge.c /= dy;// set formula of line, with y fixed to 1 + } + + newedge.edgenbr = nedges; + + nedges += 1; + return (newedge); + } + + private void makevertex(Site v) + { + v.sitenbr = nvertices; + nvertices += 1; + } + + private boolean PQinitialize() + { + PQcount = 0; + PQmin = 0; + PQhashsize = 4 * sqrt_nsites; + PQhash = new Halfedge[PQhashsize]; + + for (int i = 0; i < PQhashsize; i += 1) + { + PQhash[i] = new Halfedge(); + } + return true; + } + + private int PQbucket(Halfedge he) + { + int bucket; + + bucket = (int) ((he.ystar - ymin) / deltay * PQhashsize); + if (bucket < 0) + { + bucket = 0; + } + if (bucket >= PQhashsize) + { + bucket = PQhashsize - 1; + } + if (bucket < PQmin) + { + PQmin = bucket; + } + return (bucket); + } + + // push the HalfEdge into the ordered linked list of vertices + private void PQinsert(Halfedge he, Site v, double offset) + { + Halfedge last, next; + + he.vertex = v; + he.ystar = (double) (v.coord.y + offset); + last = PQhash[PQbucket(he)]; + while ((next = last.PQnext) != null + && (he.ystar > next.ystar || (he.ystar == next.ystar && v.coord.x > next.vertex.coord.x))) + { + last = next; + } + he.PQnext = last.PQnext; + last.PQnext = he; + PQcount += 1; + } + + // remove the HalfEdge from the list of vertices + private void PQdelete(Halfedge he) + { + Halfedge last; + + if (he.vertex != null) + { + last = PQhash[PQbucket(he)]; + while (last.PQnext != he) + { + last = last.PQnext; + } + + last.PQnext = he.PQnext; + PQcount -= 1; + he.vertex = null; + } + } + + private boolean PQempty() + { + return (PQcount == 0); + } + + private Point PQ_min() + { + Point answer = new Point(); + + while (PQhash[PQmin].PQnext == null) + { + PQmin += 1; + } + answer.x = PQhash[PQmin].PQnext.vertex.coord.x; + answer.y = PQhash[PQmin].PQnext.ystar; + return (answer); + } + + private Halfedge PQextractmin() + { + Halfedge curr; + + curr = PQhash[PQmin].PQnext; + PQhash[PQmin].PQnext = curr.PQnext; + PQcount -= 1; + return (curr); + } + + private Halfedge HEcreate(Edge e, int pm) + { + Halfedge answer; + answer = new Halfedge(); + answer.ELedge = e; + answer.ELpm = pm; + answer.PQnext = null; + answer.vertex = null; + return (answer); + } + + private boolean ELinitialize() + { + int i; + ELhashsize = 2 * sqrt_nsites; + ELhash = new Halfedge[ELhashsize]; + + for (i = 0; i < ELhashsize; i += 1) + { + ELhash[i] = null; + } + ELleftend = HEcreate(null, 0); + ELrightend = HEcreate(null, 0); + ELleftend.ELleft = null; + ELleftend.ELright = ELrightend; + ELrightend.ELleft = ELleftend; + ELrightend.ELright = null; + ELhash[0] = ELleftend; + ELhash[ELhashsize - 1] = ELrightend; + + return true; + } + + private Halfedge ELright(Halfedge he) + { + return (he.ELright); + } + + private Halfedge ELleft(Halfedge he) + { + return (he.ELleft); + } + + private Site leftreg(Halfedge he) + { + if (he.ELedge == null) + { + return (bottomsite); + } + return (he.ELpm == LE ? he.ELedge.reg[LE] : he.ELedge.reg[RE]); + } + + private void ELinsert(Halfedge lb, Halfedge newHe) + { + newHe.ELleft = lb; + newHe.ELright = lb.ELright; + (lb.ELright).ELleft = newHe; + lb.ELright = newHe; + } + + /* + * This delete routine can't reclaim node, since pointers from hash table + * may be present. + */ + private void ELdelete(Halfedge he) + { + (he.ELleft).ELright = he.ELright; + (he.ELright).ELleft = he.ELleft; + he.deleted = true; + } + + /* Get entry from hash table, pruning any deleted nodes */ + private Halfedge ELgethash(int b) + { + Halfedge he; + + if (b < 0 || b >= ELhashsize) + { + return (null); + } + he = ELhash[b]; + if (he == null || !he.deleted) + { + return (he); + } + + /* Hash table points to deleted half edge. Patch as necessary. */ + ELhash[b] = null; + return (null); + } + + private Halfedge ELleftbnd(Point p) + { + int i, bucket; + Halfedge he; + + /* Use hash table to get close to desired halfedge */ + // use the hash function to find the place in the hash map that this + // HalfEdge should be + bucket = (int) ((p.x - xmin) / deltax * ELhashsize); + + // make sure that the bucket position in within the range of the hash + // array + if (bucket < 0) + { + bucket = 0; + } + if (bucket >= ELhashsize) + { + bucket = ELhashsize - 1; + } + + he = ELgethash(bucket); + if (he == null) + // if the HE isn't found, search backwards and forwards in the hash map + // for the first non-null entry + { + for (i = 1; i < ELhashsize; i += 1) + { + if ((he = ELgethash(bucket - i)) != null) + { + break; + } + if ((he = ELgethash(bucket + i)) != null) + { + break; + } + } + } + /* Now search linear list of halfedges for the correct one */ + if (he == ELleftend || (he != ELrightend && right_of(he, p))) + { + // keep going right on the list until either the end is reached, or + // you find the 1st edge which the point isn't to the right of + do + { + he = he.ELright; + } while (he != ELrightend && right_of(he, p)); + he = he.ELleft; + } else + // if the point is to the left of the HalfEdge, then search left for + // the HE just to the left of the point + { + do + { + he = he.ELleft; + } while (he != ELleftend && !right_of(he, p)); + } + + /* Update hash table and reference counts */ + if (bucket > 0 && bucket < ELhashsize - 1) + { + ELhash[bucket] = he; + } + return (he); + } + + private void pushGraphEdge(Site leftSite, Site rightSite, double x1, double y1, double x2, double y2) + { + GraphEdge newEdge = new GraphEdge(); + allEdges.add(newEdge); + newEdge.x1 = x1; + newEdge.y1 = y1; + newEdge.x2 = x2; + newEdge.y2 = y2; + + newEdge.site1 = leftSite.sitenbr; + newEdge.site2 = rightSite.sitenbr; + } + + private void clip_line(Edge e) + { + double pxmin, pxmax, pymin, pymax; + Site s1, s2; + double x1 = 0, x2 = 0, y1 = 0, y2 = 0; + + x1 = e.reg[0].coord.x; + x2 = e.reg[1].coord.x; + y1 = e.reg[0].coord.y; + y2 = e.reg[1].coord.y; + + // if the distance between the two points this line was created from is + // less than the square root of 2, then ignore it + if (Math.sqrt(((x2 - x1) * (x2 - x1)) + ((y2 - y1) * (y2 - y1))) < minDistanceBetweenSites) + { + return; + } + pxmin = borderMinX; + pxmax = borderMaxX; + pymin = borderMinY; + pymax = borderMaxY; + + if (e.a == 1.0 && e.b >= 0.0) + { + s1 = e.ep[1]; + s2 = e.ep[0]; + } else + { + s1 = e.ep[0]; + s2 = e.ep[1]; + } + + if (e.a == 1.0) + { + y1 = pymin; + if (s1 != null && s1.coord.y > pymin) + { + y1 = s1.coord.y; + } + if (y1 > pymax) + { + y1 = pymax; + } + x1 = e.c - e.b * y1; + y2 = pymax; + if (s2 != null && s2.coord.y < pymax) + { + y2 = s2.coord.y; + } + + if (y2 < pymin) + { + y2 = pymin; + } + x2 = (e.c) - (e.b) * y2; + if (((x1 > pxmax) & (x2 > pxmax)) | ((x1 < pxmin) & (x2 < pxmin))) + { + return; + } + if (x1 > pxmax) + { + x1 = pxmax; + y1 = (e.c - x1) / e.b; + } + if (x1 < pxmin) + { + x1 = pxmin; + y1 = (e.c - x1) / e.b; + } + if (x2 > pxmax) + { + x2 = pxmax; + y2 = (e.c - x2) / e.b; + } + if (x2 < pxmin) + { + x2 = pxmin; + y2 = (e.c - x2) / e.b; + } + } else + { + x1 = pxmin; + if (s1 != null && s1.coord.x > pxmin) + { + x1 = s1.coord.x; + } + if (x1 > pxmax) + { + x1 = pxmax; + } + y1 = e.c - e.a * x1; + x2 = pxmax; + if (s2 != null && s2.coord.x < pxmax) + { + x2 = s2.coord.x; + } + if (x2 < pxmin) + { + x2 = pxmin; + } + y2 = e.c - e.a * x2; + if (((y1 > pymax) & (y2 > pymax)) | ((y1 < pymin) & (y2 < pymin))) + { + return; + } + if (y1 > pymax) + { + y1 = pymax; + x1 = (e.c - y1) / e.a; + } + if (y1 < pymin) + { + y1 = pymin; + x1 = (e.c - y1) / e.a; + } + if (y2 > pymax) + { + y2 = pymax; + x2 = (e.c - y2) / e.a; + } + if (y2 < pymin) + { + y2 = pymin; + x2 = (e.c - y2) / e.a; + } + } + + pushGraphEdge(e.reg[0], e.reg[1], x1, y1, x2, y2); + } + + private void endpoint(Edge e, int lr, Site s) + { + e.ep[lr] = s; + if (e.ep[RE - lr] == null) + { + return; + } + clip_line(e); + } + + /* returns 1 if p is to right of halfedge e */ + private boolean right_of(Halfedge el, Point p) + { + Edge e; + Site topsite; + boolean right_of_site; + boolean above, fast; + double dxp, dyp, dxs, t1, t2, t3, yl; + + e = el.ELedge; + topsite = e.reg[1]; + if (p.x > topsite.coord.x) + { + right_of_site = true; + } else + { + right_of_site = false; + } + if (right_of_site && el.ELpm == LE) + { + return (true); + } + if (!right_of_site && el.ELpm == RE) + { + return (false); + } + + if (e.a == 1.0) + { + dyp = p.y - topsite.coord.y; + dxp = p.x - topsite.coord.x; + fast = false; + if ((!right_of_site & (e.b < 0.0)) | (right_of_site & (e.b >= 0.0))) + { + above = dyp >= e.b * dxp; + fast = above; + } else + { + above = p.x + p.y * e.b > e.c; + if (e.b < 0.0) + { + above = !above; + } + if (!above) + { + fast = true; + } + } + if (!fast) + { + dxs = topsite.coord.x - (e.reg[0]).coord.x; + above = e.b * (dxp * dxp - dyp * dyp) < dxs * dyp + * (1.0 + 2.0 * dxp / dxs + e.b * e.b); + if (e.b < 0.0) + { + above = !above; + } + } + } else /* e.b==1.0 */ + + { + yl = e.c - e.a * p.x; + t1 = p.y - yl; + t2 = p.x - topsite.coord.x; + t3 = yl - topsite.coord.y; + above = t1 * t1 > t2 * t2 + t3 * t3; + } + return (el.ELpm == LE ? above : !above); + } + + private Site rightreg(Halfedge he) + { + if (he.ELedge == (Edge) null) + // if this halfedge has no edge, return the bottom site (whatever + // that is) + { + return (bottomsite); + } + + // if the ELpm field is zero, return the site 0 that this edge bisects, + // otherwise return site number 1 + return (he.ELpm == LE ? he.ELedge.reg[RE] : he.ELedge.reg[LE]); + } + + private double dist(Site s, Site t) + { + double dx, dy; + dx = s.coord.x - t.coord.x; + dy = s.coord.y - t.coord.y; + return (double) (Math.sqrt(dx * dx + dy * dy)); + } + + // create a new site where the HalfEdges el1 and el2 intersect - note that + // the Point in the argument list is not used, don't know why it's there + private Site intersect(Halfedge el1, Halfedge el2) + { + Edge e1, e2, e; + Halfedge el; + double d, xint, yint; + boolean right_of_site; + Site v; + + e1 = el1.ELedge; + e2 = el2.ELedge; + if (e1 == null || e2 == null) + { + return null; + } + + // if the two edges bisect the same parent, return null + if (e1.reg[1] == e2.reg[1]) + { + return null; + } + + d = e1.a * e2.b - e1.b * e2.a; + if (-1.0e-10 < d && d < 1.0e-10) + { + return null; + } + + xint = (e1.c * e2.b - e2.c * e1.b) / d; + yint = (e2.c * e1.a - e1.c * e2.a) / d; + + if ((e1.reg[1].coord.y < e2.reg[1].coord.y) + || (e1.reg[1].coord.y == e2.reg[1].coord.y && e1.reg[1].coord.x < e2.reg[1].coord.x)) + { + el = el1; + e = e1; + } else + { + el = el2; + e = e2; + } + + right_of_site = xint >= e.reg[1].coord.x; + if ((right_of_site && el.ELpm == LE) + || (!right_of_site && el.ELpm == RE)) + { + return null; + } + + // create a new site at the point of intersection - this is a new vector + // event waiting to happen + v = new Site(); + v.coord.x = xint; + v.coord.y = yint; + return (v); + } + + /* + * implicit parameters: nsites, sqrt_nsites, xmin, xmax, ymin, ymax, deltax, + * deltay (can all be estimates). Performance suffers if they are wrong; + * better to make nsites, deltax, and deltay too big than too small. (?) + */ + private boolean voronoi_bd() + { + Site newsite, bot, top, temp, p; + Site v; + Point newintstar = null; + int pm; + Halfedge lbnd, rbnd, llbnd, rrbnd, bisector; + Edge e; + + PQinitialize(); + ELinitialize(); + + bottomsite = nextone(); + newsite = nextone(); + while (true) + { + if (!PQempty()) + { + newintstar = PQ_min(); + } + // if the lowest site has a smaller y value than the lowest vector + // intersection, + // process the site otherwise process the vector intersection + + if (newsite != null + && (PQempty() || newsite.coord.y < newintstar.y || (newsite.coord.y == newintstar.y && newsite.coord.x < newintstar.x))) + { + /* new site is smallest -this is a site event */ + // get the first HalfEdge to the LEFT of the new site + lbnd = ELleftbnd((newsite.coord)); + // get the first HalfEdge to the RIGHT of the new site + rbnd = ELright(lbnd); + // if this halfedge has no edge,bot =bottom site (whatever that + // is) + bot = rightreg(lbnd); + // create a new edge that bisects + e = bisect(bot, newsite); + + // create a new HalfEdge, setting its ELpm field to 0 + bisector = HEcreate(e, LE); + // insert this new bisector edge between the left and right + // vectors in a linked list + ELinsert(lbnd, bisector); + + // if the new bisector intersects with the left edge, + // remove the left edge's vertex, and put in the new one + if ((p = intersect(lbnd, bisector)) != null) + { + PQdelete(lbnd); + PQinsert(lbnd, p, dist(p, newsite)); + } + lbnd = bisector; + // create a new HalfEdge, setting its ELpm field to 1 + bisector = HEcreate(e, RE); + // insert the new HE to the right of the original bisector + // earlier in the IF stmt + ELinsert(lbnd, bisector); + + // if this new bisector intersects with the new HalfEdge + if ((p = intersect(bisector, rbnd)) != null) + { + // push the HE into the ordered linked list of vertices + PQinsert(bisector, p, dist(p, newsite)); + } + newsite = nextone(); + } else if (!PQempty()) + /* intersection is smallest - this is a vector event */ + { + // pop the HalfEdge with the lowest vector off the ordered list + // of vectors + lbnd = PQextractmin(); + // get the HalfEdge to the left of the above HE + llbnd = ELleft(lbnd); + // get the HalfEdge to the right of the above HE + rbnd = ELright(lbnd); + // get the HalfEdge to the right of the HE to the right of the + // lowest HE + rrbnd = ELright(rbnd); + // get the Site to the left of the left HE which it bisects + bot = leftreg(lbnd); + // get the Site to the right of the right HE which it bisects + top = rightreg(rbnd); + + v = lbnd.vertex; // get the vertex that caused this event + makevertex(v); // set the vertex number - couldn't do this + // earlier since we didn't know when it would be processed + endpoint(lbnd.ELedge, lbnd.ELpm, v); + // set the endpoint of + // the left HalfEdge to be this vector + endpoint(rbnd.ELedge, rbnd.ELpm, v); + // set the endpoint of the right HalfEdge to + // be this vector + ELdelete(lbnd); // mark the lowest HE for + // deletion - can't delete yet because there might be pointers + // to it in Hash Map + PQdelete(rbnd); + // remove all vertex events to do with the right HE + ELdelete(rbnd); // mark the right HE for + // deletion - can't delete yet because there might be pointers + // to it in Hash Map + pm = LE; // set the pm variable to zero + + if (bot.coord.y > top.coord.y) + // if the site to the left of the event is higher than the + // Site + { // to the right of it, then swap them and set the 'pm' + // variable to 1 + temp = bot; + bot = top; + top = temp; + pm = RE; + } + e = bisect(bot, top); // create an Edge (or line) + // that is between the two Sites. This creates the formula of + // the line, and assigns a line number to it + bisector = HEcreate(e, pm); // create a HE from the Edge 'e', + // and make it point to that edge + // with its ELedge field + ELinsert(llbnd, bisector); // insert the new bisector to the + // right of the left HE + endpoint(e, RE - pm, v); // set one endpoint to the new edge + // to be the vector point 'v'. + // If the site to the left of this bisector is higher than the + // right Site, then this endpoint + // is put in position 0; otherwise in pos 1 + + // if left HE and the new bisector intersect, then delete + // the left HE, and reinsert it + if ((p = intersect(llbnd, bisector)) != null) + { + PQdelete(llbnd); + PQinsert(llbnd, p, dist(p, bot)); + } + + // if right HE and the new bisector intersect, then + // reinsert it + if ((p = intersect(bisector, rrbnd)) != null) + { + PQinsert(bisector, p, dist(p, bot)); + } + } else + { + break; + } + } + + for (lbnd = ELright(ELleftend); lbnd != ELrightend; lbnd = ELright(lbnd)) + { + e = lbnd.ELedge; + clip_line(e); + } + + return true; + } + + +}