All Downloads are FREE. Search and download functionalities are using the official Maven repository.

gov.nasa.worldwind.util.ContourBuilder Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (C) 2014 United States Government as represented by the Administrator of the
 * National Aeronautics and Space Administration.
 * All Rights Reserved.
 */

package gov.nasa.worldwind.util;

import gov.nasa.worldwind.geom.*;

import java.util.*;

/**
 * Generates contour lines at threshold values in a rectangular array of numeric values. ContourBuilder differs from the
 * ContourLine renderable shape in that ContourBuilder can compute the coordinates of contour lines within arbitrary
 * two-dimensional scalar data, whereas the ContourLine shape operates only on elevation values associated with a World
 * Wind globe. Note that ContourBuilder can be used to compute contour line coordinates within a rectangular array of
 * elevation values.
 * 

* ContourBuilder operates on a caller specified rectangular array. The array is specified as a one dimensional array of * floating point numbers, and is understood to be organized in row-major order, with the first index indicating the * value at the rectangle's upper-left corner. The domain of array values is any value that fits in a 64-bit floating * point number. *

* Contour lines may be computed at any threshold value (i.e. isovalue) by calling {@link #buildContourLines(double)} or * {@link #buildContourLines(double, gov.nasa.worldwind.geom.Sector, double)}. The latter method maps contour line * coordinates to geographic positions by associating the rectangular array with a geographic sector. It is valid to * compute contour lines for a threshold value that is less than the rectangular array's minimum value or greater than * the rectangular array's maximum value, though the result is an empty list of contour lines. The domain of contour * line coordinates is the XY Cartesian space defined by the rectangular array's width and height. X coordinates range * from 0 to width-1, and Y coordinates range from 0 to height-1. * * @author dcollins * @version $Id: ContourBuilder.java 2436 2014-11-14 23:20:50Z danm $ */ public class ContourBuilder { protected static class CellInfo { public final int x; public final int y; public final int contourMask; public final Map edgeWeights = new HashMap(); public final Set visitedDirections = new HashSet(4); public CellInfo(int x, int y, int contourMask) { this.x = x; this.y = y; this.contourMask = contourMask; } } protected static class CellKey { public final int x; public final int y; public CellKey(int x, int y) { this.x = x; this.y = y; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || this.getClass() != o.getClass()) return false; CellKey that = (CellKey) o; return this.x == that.x && this.y == that.y; } @Override public int hashCode() { return 31 * this.x + this.y; } } protected static enum Direction { NORTH, SOUTH, EAST, WEST } protected int width; protected int height; protected double[] values; protected Map contourCellMap = new HashMap(); protected List contourCellList = new ArrayList(); protected List> contourList = new ArrayList>(); protected List currentContour; protected static Map dirRev = new HashMap(); protected static Map> dirNext = new HashMap>(); static { dirRev.put(Direction.NORTH, Direction.SOUTH); dirRev.put(Direction.SOUTH, Direction.NORTH); dirRev.put(Direction.EAST, Direction.WEST); dirRev.put(Direction.WEST, Direction.EAST); // Use LinkedHaspMap to store the maps in dirNext in order to preserve enumeration order. The method // traverseContourCells requires that the directions are enumerated in the order listed here. LinkedHashMap map = new LinkedHashMap(); map.put(Direction.SOUTH, Direction.WEST); map.put(Direction.WEST, Direction.SOUTH); dirNext.put(1, map); map = new LinkedHashMap(); map.put(Direction.SOUTH, Direction.EAST); map.put(Direction.EAST, Direction.SOUTH); dirNext.put(2, map); map = new LinkedHashMap(); map.put(Direction.EAST, Direction.WEST); map.put(Direction.WEST, Direction.EAST); dirNext.put(3, map); map = new LinkedHashMap(); map.put(Direction.NORTH, Direction.EAST); map.put(Direction.EAST, Direction.NORTH); dirNext.put(4, map); map = new LinkedHashMap(); map.put(Direction.NORTH, Direction.WEST); map.put(Direction.WEST, Direction.NORTH); map.put(Direction.SOUTH, Direction.EAST); map.put(Direction.EAST, Direction.SOUTH); dirNext.put(5, map); map = new LinkedHashMap(); map.put(Direction.NORTH, Direction.SOUTH); map.put(Direction.SOUTH, Direction.NORTH); dirNext.put(6, map); map = new LinkedHashMap(); map.put(Direction.NORTH, Direction.WEST); map.put(Direction.WEST, Direction.NORTH); dirNext.put(7, map); map = new LinkedHashMap(); map.put(Direction.NORTH, Direction.WEST); map.put(Direction.WEST, Direction.NORTH); dirNext.put(8, map); map = new LinkedHashMap(); map.put(Direction.NORTH, Direction.SOUTH); map.put(Direction.SOUTH, Direction.NORTH); dirNext.put(9, map); map = new LinkedHashMap(); map.put(Direction.NORTH, Direction.EAST); map.put(Direction.EAST, Direction.NORTH); map.put(Direction.SOUTH, Direction.WEST); map.put(Direction.WEST, Direction.SOUTH); dirNext.put(10, map); map = new LinkedHashMap(); map.put(Direction.NORTH, Direction.EAST); map.put(Direction.EAST, Direction.NORTH); dirNext.put(11, map); map = new LinkedHashMap(); map.put(Direction.EAST, Direction.WEST); map.put(Direction.WEST, Direction.EAST); dirNext.put(12, map); map = new LinkedHashMap(); map.put(Direction.SOUTH, Direction.EAST); map.put(Direction.EAST, Direction.SOUTH); dirNext.put(13, map); map = new LinkedHashMap(); map.put(Direction.SOUTH, Direction.WEST); map.put(Direction.WEST, Direction.SOUTH); dirNext.put(14, map); } /** * Creates a new ContourBuilder with the specified rectangular array arguments. The array is understood to be * organized in row-major order, with the first index indicating the value at the rectangle's upper-left corner. * * @param width the rectangular array width. * @param height the rectangular array height. * @param values the rectangular array values, as a one-dimensional array. Must contain at least width * height * values. This array is understood to be organized in row-major order, with the first index * indicating the value at the rectangle's upper-left corner. * * @throws java.lang.IllegalArgumentException if either the width or the height are less than 1, if the array is * null, or if the array length is insufficient for the specified width * and height. */ public ContourBuilder(int width, int height, double[] values) { if (width < 1) { String msg = Logging.getMessage("generic.InvalidWidth", width); Logging.logger().severe(msg); throw new IllegalArgumentException(msg); } if (height < 1) { String msg = Logging.getMessage("generic.InvalidHeight", height); Logging.logger().severe(msg); throw new IllegalArgumentException(msg); } if (values == null) { String msg = Logging.getMessage("nullValue.ArrayIsNull"); Logging.logger().severe(msg); throw new IllegalArgumentException(msg); } if (values.length != width * height) { String msg = Logging.getMessage("generic.ArrayInvalidLength", values.length); Logging.logger().severe(msg); throw new IllegalArgumentException(msg); } this.width = width; this.height = height; this.values = values; } /** * Computes the contour lines at a specified threshold value. The returned list represents a collection of * individual geographic polylines, which may or may not represent a closed loop. Each polyline is represented as a * list of two-element arrays, with the X coordinate at index 0 and the Y coordinate at index 1. The domain of * contour line coordinates is the XY Cartesian space defined by the rectangular array's width and height. X * coordinates range from 0 to width-1, and Y coordinates range from 0 to height-1. *

*

* This returns an empty list if there are no contour lines associated with the value. This occurs when the value is * less than the rectangular array's minimum value, or when the value is greater than the rectangular array's * maximum value. * * @param value the threshold value (i.e. isovalue) to compute contour lines for. * * @return a list containing the contour lines for the threshold value. */ public List> buildContourLines(double value) { this.assembleContourCells(value); this.traverseContourCells(); List> result = new ArrayList>(this.contourList); // return a copy to insulate from changes this.clearContourCells(); // clears contourList return result; } /** * Computes the geographic contour lines at a specified threshold value. The returned list represents a collection * of individual geographic polylines, which may or may not represent a closed loop. This maps contour line * coordinates to geographic positions by associating the rectangular array with a geographic sector. The array's * upper left corner is mapped to the sector's Northwest corner, and the array's lower right corner is mapped to the * sector's Southeast corner. *

* The domain of contour line coordinates is the geographic space defined by the specified sector. Prior to the * mapping into geographic coordinates, contour line X coordinates range from 0 to width-1, and Y coordinates range * from 0 to height-1. After the mapping into geographic coordinates, contour line X coordinates range from * sector.getMinLongitude() to sector.getMaxLongitude(), and Y coordinates range from sector.getMaxLatitude() to * sector.getMinLatitude(). *

* This returns an empty list if there are no contour lines associated with the value. This occurs when the value is * less than the rectangular array's minimum value, or when the value is greater than the rectangular array's * maximum value. * * @param value the threshold value (i.e. isovalue) to compute contour lines for. * @param sector the sector to associate with the rectangular array. The array's upper left corner is mapped to * the sector's Northwest corner, and the array's lower right corner is mapped to the sector's * Southeast corner. * @param altitude the altitude to assign to the geographic positions. * * @return a list containing the geographic contour lines for the threshold value. * * @throws java.lang.IllegalArgumentException if the sector is null. */ public List> buildContourLines(double value, Sector sector, double altitude) { if (sector == null) { String msg = Logging.getMessage("nullValue.SectorIsNull"); Logging.logger().severe(msg); throw new IllegalArgumentException(msg); } this.assembleContourCells(value); this.traverseContourCells(); double maxLat = sector.getMaxLatitude().degrees; double minLon = sector.getMinLongitude().degrees; double deltaLat = sector.getDeltaLatDegrees(); double deltaLon = sector.getDeltaLonDegrees(); List> result = new ArrayList>(); for (List coordList : this.contourList) { ArrayList positionList = new ArrayList(); for (double[] coord : coordList) { double s = coord[0] / (this.width - 1); // normalized x coordinate in the range 0 to 1 double t = coord[1] / (this.height - 1); // normalized y coordinate in the range 0 to 1 double lat = maxLat - t * deltaLat; // map y coordinate to latitude double lon = minLon + s * deltaLon; // map x coordinate to longitude positionList.add(Position.fromDegrees(lat, lon, altitude)); } result.add(positionList); } this.clearContourCells(); // clears contourList return result; } protected void assembleContourCells(double value) { // Divide the 2D scalar field into a grid of evenly spaced contouring cells. Every 2x2 block of field values // forms a cell. The contouring grid's dimensions are therefore one less than the 2D scalar field. Based on // the approach outlined at http://en.wikipedia.org/wiki/Marching_squares this.contourCellMap.clear(); this.contourCellList.clear(); for (int y = 0; y < this.height - 1; y++) { for (int x = 0; x < this.width - 1; x++) { // Get the field values associated with the contouring cell's four corners. double nw = this.valueFor(x, y); double ne = this.valueFor(x + 1, y); double se = this.valueFor(x + 1, y + 1); double sw = this.valueFor(x, y + 1); // Assemble a 4-bit mask indicating whether or not the field values at the cell's corners are above or // below the threshold. The mask has 1 where the field value is above the threshold, and 0 otherwise. int mask = 0; mask |= (nw > value) ? 1 : 0; // 1000 mask <<= 1; mask |= (ne > value) ? 1 : 0; // 0100 mask <<= 1; mask |= (se > value) ? 1 : 0; // 0010 mask <<= 1; mask |= (sw > value) ? 1 : 0; // 0001 if (mask == 0 || mask == 15) continue; // no contour; all values above or below the threshold value // Disambiguate saddle point for masks 0x0101 and 0x1010, per Wikipedia page suggestion. if (mask == 5 || mask == 10) { double ctr = (nw + ne + se + sw) / 4; // sample center value as the average of four corners if (mask == 5 && ctr <= value) // center value causes change in direction; flip the mask to 10 mask = 10; else if (mask == 10 && ctr <= value) // center value causes change in direction; flip the mask to 5 mask = 5; } CellInfo cell = new CellInfo(x, y, mask); // Compute weights associated with edge intersections. if ((ne > value) ^ (nw > value)) cell.edgeWeights.put(Direction.NORTH, (value - nw) / (ne - nw)); if ((se > value) ^ (sw > value)) cell.edgeWeights.put(Direction.SOUTH, (value - sw) / (se - sw)); if ((se > value) ^ (ne > value)) cell.edgeWeights.put(Direction.EAST, (value - ne) / (se - ne)); if ((sw > value) ^ (nw > value)) cell.edgeWeights.put(Direction.WEST, (value - nw) / (sw - nw)); this.putContourCell(cell); } } } protected void traverseContourCells() { List> contours = new ArrayList>(); this.contourList.clear(); for (CellKey key : this.contourCellList) // iterate over all possible contour starting points { CellInfo cell = this.contourCellMap.get(key); for (Direction dir : dirNext.get(cell.contourMask).keySet()) // either 2 or 4 starting directions { if (cell.visitedDirections.contains(dir)) { continue; } this.currentContour = new ArrayList(); this.traverseContour(cell, dir); contours.add(this.currentContour); this.currentContour = null; if (contours.size() == 2) // combine each pair of starting directions into a single polyline { if (contours.get(0).size() == 0 && contours.get(1).size() == 0) { String msg = Logging.getMessage("generic.UnexpectedCondition", "both contours are of zero length"); Logging.logger().severe(msg); } else { Collections.reverse(contours.get(0)); contours.get(0).addAll(contours.get(1)); this.contourList.add(contours.get(0)); } contours.clear(); } } if (contours.size() != 0) { String msg = Logging.getMessage("generic.UnexpectedCondition", "non-empty contours list"); Logging.logger().severe(msg); } } } protected void traverseContour(CellInfo cell, Direction dir) { Direction dirNext = dir; Direction dirPrev = dir; // use Prev same as Next for first iteration (i.e., for seed cell) while (cell != null && !cell.visitedDirections.contains(dirNext)) { // Mark the contour cell as visited. cell.visitedDirections.add(dirNext); cell.visitedDirections.add(dirPrev); addIntersection(cell, dirNext); // Advance to the next cell. cell = this.nextCell(cell, dirNext); // guard cell use in computing dirNext if (cell != null) { // Advance to the next direction. dirPrev = ContourBuilder.dirRev.get(dirNext); dirNext = ContourBuilder.dirNext.get(cell.contourMask).get(dirPrev); } } } protected void addIntersection(CellInfo cell, Direction dir) { // Compute the intersection of the contour cell in the next direction. The cell's xy coordinates initially // indicate the cell's Southwest corner. double xIntersect = cell.x; double yIntersect = cell.y; switch (dir) { case NORTH: xIntersect += cell.edgeWeights.get(dir); // interpolate along the north edge break; case SOUTH: xIntersect += cell.edgeWeights.get(dir); // interpolate along the south edge yIntersect += 1; // move from the north to the south break; case EAST: xIntersect += 1; // move from the west to the east yIntersect += cell.edgeWeights.get(dir); // interpolate along the east edge break; case WEST: yIntersect += cell.edgeWeights.get(dir); // interpolate along the west edge break; default: String msg = Logging.getMessage("generic.UnexpectedDirection", dirNext); Logging.logger().severe(msg); break; } this.currentContour.add(new double[] {xIntersect, yIntersect}); } protected void clearContourCells() { this.contourCellMap.clear(); this.contourCellList.clear(); this.contourList.clear(); this.currentContour = null; } protected CellInfo nextCell(CellInfo cell, Direction dir) { int x = cell.x; int y = cell.y; switch (dir) { case NORTH: return this.getContourCell(x, y - 1); case SOUTH: return this.getContourCell(x, y + 1); case EAST: return this.getContourCell(x + 1, y); case WEST: return this.getContourCell(x - 1, y); default: String msg = Logging.getMessage("generic.UnexpectedDirection", dirNext); Logging.logger().severe(msg); return null; } } protected double valueFor(int x, int y) { return this.values[x + y * this.width]; } protected void putContourCell(CellInfo cell) { CellKey key = new CellKey(cell.x, cell.y); this.contourCellMap.put(key, cell); this.contourCellList.add(key); } protected CellInfo getContourCell(int x, int y) { return this.contourCellMap.get(new CellKey(x, y)); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy