/* A class to read data from a grid file and then render that data. The grid data can be either ints or floats, as it is read it will be down-converted to ints for simplicity's sake. Proper data files have six lines of header, followed by lines of a constant length as specified in the ncols variable, with nrows rows in the whole file. I guess your data could be longer, I'm fairly sure this class would gracefully ignore the extra data. That's life. P.S. - why are Regular Expressions in Java so horridly useless? I could write like 3 lines of Perl to elegantly read a header section of an arbitrary length and not freak out, here I had to make it count how many lines it had gone through... yuck. Nathan Merritt - March 27, 2008 */ import java.util.*; import java.io.*; //import java.util.regex.*; import java.awt.*; public class Grid { // dimensions of the array of terrain values int nrows, ncols; // these variables aren't needed for now // the gps (?) offset of the lower-left corner of this map //int xllcorner, yllcorner; // space between each data entry //int cellsize; // value in the file for null entries int NODATA_value; // min, maximum heights and range in this data set int maxHeight, heightRange; // set absurdly high so it'll actually find the minimum int minHeight = 9999; // the nodata value we'll use in this class for consistency private int local_NODATA_value = -9999; private int terrain_elevations[][]; // how we render on the next pass (color or grayscale, defaults to grayscale) String renderMethod = "grayscale"; /* the constructor: uses Scanner to read a file one line at a time, adding the text values to terrain_elevations[][] */ public Grid (String gridFile) { try { // the file we're loading all our data from Scanner scan = new Scanner(new File(gridFile)); /* the header section, should be of the format \w+ \d+ (variable then the value) EDITORS NOTE: apparently a number won't be properly identified as an integer unless it's prefaced by a negative sign, so with most files I can't use the syntactically corrent while (scan.hasNext("\\w+") Glad to figure out how broken regexps are under Java. So yeah, thats why it's hacked to load the first 6 lines only. */ for (int i = 0; i < 6; i++) { String variable = scan.next(); int data_value; if (scan.hasNextInt()) { data_value = (int)scan.nextInt(); } // otherwise we should read it in as a double and convert else { data_value = (int)scan.nextFloat(); } if (variable.compareTo("nrows") == 0) { nrows = data_value; continue; } if (variable.compareTo("ncols") == 0) { ncols = data_value; continue; } if (variable.compareTo("xllcorner") == 0) { //xllcorner = data_value; continue; } if (variable.compareTo("yllcorner") == 0) { //yllcorner = data_value; continue; } if (variable.compareTo("cellsize") == 0) { //cellsize = data_value; continue; } if (variable.compareTo("NODATA_value") == 0) { NODATA_value = data_value; } } // and the rest of the file! terrain_elevations = new int[nrows][ncols]; for (int row = 0; row < terrain_elevations.length; row++) { for (int col = 0; col < terrain_elevations[0].length; col++) { int height; if (scan.hasNextInt()) { height = (int)scan.nextInt(); } // this should hopefully let us run the brunswick file (floats) else { height = (int)scan.nextFloat(); } /* so that grids that use different NODATA_values can be handled the same by our drawing algorithm */ if (height == NODATA_value) { height = local_NODATA_value; } // assign the min and max data values from this set if (height > maxHeight && height != NODATA_value) { maxHeight = height; } if (height < minHeight && height != NODATA_value) { minHeight = height; } // and set the value in the array (finally!) terrain_elevations[row][col] = height; } } // the range of height values in our data, useful for colors heightRange = maxHeight - minHeight; scan.close(); } catch (java.io.FileNotFoundException e) { System.out.println("file not found"); } } /* renders a grid of terrain_elevation[][] data into a JFrame window, using [x,y]WindowSize as the stretch coefficients. The longest side of the grid will completely fill the window (minus centering). Renders in either Grayscale or Color mode, depending on renderMethod */ public void renderGrid(int xWindowSize, int yWindowSize, Graphics g) { /* for every point in terrain_elevations[][], starting upper left */ for (int row = 0; row < terrain_elevations.length; row++) { for (int col = 0; col < terrain_elevations[0].length; col++) { // skip if this is a local_NODATA_value point if (terrain_elevations[row][col] == local_NODATA_value) { continue; } /* now the neighbors: the points to the E, SE, S */ for (int i = row; i <= row + 1; i++) { for (int j = col; j <= col + 1; j++) { // skip ourselves if (i == row && j == col) { continue; } if (!goodNeighbor(i, j)) { continue; } // set the color, depends on what render mode we're in Color lineColor; float relHeight = getGrayscaleValue(row, col); // color mode! // I just picked a r g or b value to make relHeight/2 for each mode // it looks pretty cool I think.. if (renderMethod.compareTo("color") == 0) { if (relHeight < .33) { lineColor = new Color(relHeight, relHeight/2, relHeight); } else if (relHeight < .66) { lineColor = new Color (relHeight, relHeight, relHeight/2); } else { lineColor = new Color (relHeight/2, relHeight, relHeight); } } // grayscale mode: {r,g,b} = relHeight else { lineColor = new Color(relHeight, relHeight, relHeight); } g.setColor(lineColor); /* now we figure out the stretch constants to map the array data onto our screen */ // add an offset to every point for centering int OFFSET = 30; // everything has to be floats or it breaks when rows or cols // gets larger than our window dimensions - no fun :( float rowPos, colPos, rowNPos, colNPos; rowPos = colPos = rowNPos = colNPos = OFFSET; // and make sure to take the offset into account for stretch float xStretch = (float)(xWindowSize-2*OFFSET)/terrain_elevations.length; float yStretch = (float)(yWindowSize-2*OFFSET)/terrain_elevations[0].length; // the start point rowPos += yStretch * col; colPos += xStretch * row; // the neighbors rowNPos += yStretch * j; colNPos += xStretch * i; g.drawLine((int)rowPos, (int)colPos, (int)rowNPos, (int)colNPos); } } // end of neighbors loop } } // end of for every row,col loop } /* returns true if the point is within terrain_elevations[][], false otherwise */ public boolean pointOnGrid(int x, int y) { if (x >= terrain_elevations.length || x < 0) { return false; } if (y >= terrain_elevations[0].length || y < 0) { return false; } return true; } /* A point is a "good neighbor" iff it is both on the board and its terrain_elevations[][] != local_NODATA_value */ public boolean goodNeighbor(int x, int y) { if (pointOnGrid(x,y)) { if (terrain_elevations[x][y] != local_NODATA_value) { return true; } } return false; } /* returns the color to use on the line between two points, by dividing the elevation by the range to get a relative height for the data set. This will give shades of gray, with black being the lowest elevation. This function makes use of the minHeight and maxHeight values that were set by the constructor to build its absolute scale. */ public float getGrayscaleValue(int x1, int y1) { return getGrayscaleValue(terrain_elevations[x1][y1]); } public float getGrayscaleValue(int height1) { // so we can easily average two heights if we want to later int heightAverage = height1; /* now figure out where this point lies on our absolute range we subtract our answer from one so that: lower values of heightAverage -> lower relativeHeight */ float relativeHeight = (float) (maxHeight - heightAverage) / heightRange; relativeHeight = 1 - relativeHeight; assert (relativeHeight <= 1); assert (relativeHeight >= 0); return relativeHeight; } public void setRenderMethod(String method) { renderMethod = method; } public String getRenderMethod() { return renderMethod; } }