/******************************************************************************* * Copyright 2012 Geoscience Australia * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. ******************************************************************************/ package au.gov.ga.earthsci.worldwind.common.layers.volume; import gov.nasa.worldwind.View; import gov.nasa.worldwind.WorldWindow; import gov.nasa.worldwind.avlist.AVKey; import gov.nasa.worldwind.avlist.AVList; import gov.nasa.worldwind.event.SelectEvent; import gov.nasa.worldwind.event.SelectListener; import gov.nasa.worldwind.geom.Angle; import gov.nasa.worldwind.geom.Extent; import gov.nasa.worldwind.geom.Intersection; import gov.nasa.worldwind.geom.LatLon; import gov.nasa.worldwind.geom.Line; import gov.nasa.worldwind.geom.Matrix; import gov.nasa.worldwind.geom.Plane; import gov.nasa.worldwind.geom.Position; import gov.nasa.worldwind.geom.Vec4; import gov.nasa.worldwind.globes.Globe; import gov.nasa.worldwind.layers.AbstractLayer; import gov.nasa.worldwind.render.DrawContext; import java.awt.AlphaComposite; import java.awt.Color; import java.awt.Dimension; import java.awt.Graphics2D; import java.awt.Point; import java.awt.Rectangle; import java.awt.event.MouseEvent; import java.awt.image.BufferedImage; import java.net.MalformedURLException; import java.net.URL; import java.util.Arrays; import java.util.Comparator; import javax.media.opengl.GL2; import org.gdal.osr.CoordinateTransformation; import au.gov.ga.earthsci.worldwind.common.WorldWindowRegistry; import au.gov.ga.earthsci.worldwind.common.layers.Bounds; import au.gov.ga.earthsci.worldwind.common.layers.Wireframeable; import au.gov.ga.earthsci.worldwind.common.render.fastshape.FastShape; import au.gov.ga.earthsci.worldwind.common.render.fastshape.FastShapeRenderListener; import au.gov.ga.earthsci.worldwind.common.util.AVKeyMore; import au.gov.ga.earthsci.worldwind.common.util.ColorMap; import au.gov.ga.earthsci.worldwind.common.util.CoordinateTransformationUtil; import au.gov.ga.earthsci.worldwind.common.util.GeometryUtil; import au.gov.ga.earthsci.worldwind.common.util.Util; import au.gov.ga.earthsci.worldwind.common.util.Validate; import com.jogamp.opengl.util.awt.TextureRenderer; /** * Basic implementation of the {@link VolumeLayer} interface. * * @author Michael de Hoog (michael.dehoog@ga.gov.au) */ public class BasicVolumeLayer extends AbstractLayer implements VolumeLayer, Wireframeable, SelectListener, FastShapeRenderListener { protected URL context; protected String url; protected String dataCacheName; protected VolumeDataProvider dataProvider; protected Double minimumDistance; protected double maxVariance = 0; protected CoordinateTransformation coordinateTransformation; protected String paintedVariable; protected ColorMap colorMap; protected Color noDataColor; protected boolean reverseNormals = false; protected boolean useOrderedRendering = false; protected final Object dataLock = new Object(); protected boolean dataAvailable = false; protected FastShape topSurface, bottomSurface; protected TopBottomFastShape minXCurtain, maxXCurtain, minYCurtain, maxYCurtain; protected FastShape boundingBoxShape; protected TextureRenderer topTexture, bottomTexture, minXTexture, maxXTexture, minYTexture, maxYTexture; protected int topOffset = 0, bottomOffset = 0, minXOffset = 0, maxXOffset = 0, minYOffset = 0, maxYOffset = 0; protected int lastTopOffset = -1, lastBottomOffset = -1, lastMinXOffset = -1, lastMaxXOffset = -1, lastMinYOffset = -1, lastMaxYOffset = -1; protected double lastVerticalExaggeration = -Double.MAX_VALUE; protected final double[] curtainTextureMatrix = new double[] { 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 }; protected boolean minXClipDirty = false, maxXClipDirty = false, minYClipDirty = false, maxYClipDirty = false, topClipDirty = false, bottomClipDirty = false; protected final double[] topClippingPlanes = new double[4 * 4]; protected final double[] bottomClippingPlanes = new double[4 * 4]; protected final double[] curtainClippingPlanes = new double[4 * 4]; protected boolean wireframe = false; protected boolean dragging = false; protected Position dragStartPosition; protected int dragStartSlice; protected Vec4 dragStartCenter; /** * Create a new {@link BasicVolumeLayer}, using the provided layer params. * * @param params */ public BasicVolumeLayer(AVList params) { context = (URL) params.getValue(AVKeyMore.CONTEXT_URL); url = params.getStringValue(AVKey.URL); dataCacheName = params.getStringValue(AVKey.DATA_CACHE_NAME); dataProvider = (VolumeDataProvider) params.getValue(AVKeyMore.DATA_LAYER_PROVIDER); minimumDistance = (Double) params.getValue(AVKeyMore.MINIMUM_DISTANCE); colorMap = (ColorMap) params.getValue(AVKeyMore.COLOR_MAP); noDataColor = (Color) params.getValue(AVKeyMore.NO_DATA_COLOR); Double d = (Double) params.getValue(AVKeyMore.MAX_VARIANCE); if (d != null) { maxVariance = d; } String s = (String) params.getValue(AVKey.COORDINATE_SYSTEM); if (s != null) { coordinateTransformation = CoordinateTransformationUtil.getTransformationToWGS84(s); } s = (String) params.getValue(AVKeyMore.PAINTED_VARIABLE); if (s != null) { paintedVariable = s; } Integer i = (Integer) params.getValue(AVKeyMore.INITIAL_OFFSET_MIN_U); if (i != null) { minXOffset = i; } i = (Integer) params.getValue(AVKeyMore.INITIAL_OFFSET_MAX_U); if (i != null) { maxXOffset = i; } i = (Integer) params.getValue(AVKeyMore.INITIAL_OFFSET_MIN_V); if (i != null) { minYOffset = i; } i = (Integer) params.getValue(AVKeyMore.INITIAL_OFFSET_MAX_V); if (i != null) { maxYOffset = i; } i = (Integer) params.getValue(AVKeyMore.INITIAL_OFFSET_MIN_W); if (i != null) { topOffset = i; } i = (Integer) params.getValue(AVKeyMore.INITIAL_OFFSET_MAX_W); if (i != null) { bottomOffset = i; } Boolean b = (Boolean) params.getValue(AVKeyMore.REVERSE_NORMALS); if (b != null) { reverseNormals = b; } b = (Boolean) params.getValue(AVKeyMore.ORDERED_RENDERING); if (b != null) { useOrderedRendering = b; } Validate.notBlank(url, "Model data url not set"); Validate.notBlank(dataCacheName, "Model data cache name not set"); Validate.notNull(dataProvider, "Model data provider is null"); WorldWindowRegistry.INSTANCE.addSelectListener(this); } @Override public URL getUrl() throws MalformedURLException { return new URL(context, url); } @Override public String getDataCacheName() { return dataCacheName; } @Override public boolean isLoading() { return dataProvider.isLoading(); } @Override public void addLoadingListener(LoadingListener listener) { dataProvider.addLoadingListener(listener); } @Override public void removeLoadingListener(LoadingListener listener) { dataProvider.removeLoadingListener(listener); } @Override public Bounds getBounds() { return dataProvider.getBounds(); } @Override public boolean isFollowTerrain() { return false; } @Override public void dataAvailable(VolumeDataProvider provider) { calculateSurfaces(); dataAvailable = true; } /** * Calculate the 4 curtain and 2 horizontal surfaces used to render this * volume. Should be called once after the {@link VolumeDataProvider} * notifies this layer that the data is available. */ protected void calculateSurfaces() { double topElevation = 0; double bottomElevation = -dataProvider.getDepth(); minXCurtain = dataProvider.createXCurtain(0); minXCurtain.addRenderListener(this); minXCurtain.setLighted(true); minXCurtain.setCalculateNormals(true); minXCurtain.setReverseNormals(reverseNormals); minXCurtain.setTopElevationOffset(topElevation); minXCurtain.setBottomElevationOffset(bottomElevation); minXCurtain.setTextureMatrix(curtainTextureMatrix); minXCurtain.setUseOrderedRendering(useOrderedRendering); maxXCurtain = dataProvider.createXCurtain(dataProvider.getXSize() - 1); maxXCurtain.addRenderListener(this); maxXCurtain.setLighted(true); maxXCurtain.setCalculateNormals(true); maxXCurtain.setReverseNormals(!reverseNormals); maxXCurtain.setTopElevationOffset(topElevation); maxXCurtain.setBottomElevationOffset(bottomElevation); maxXCurtain.setTextureMatrix(curtainTextureMatrix); maxXCurtain.setUseOrderedRendering(useOrderedRendering); minYCurtain = dataProvider.createYCurtain(0); minYCurtain.addRenderListener(this); minYCurtain.setLighted(true); minYCurtain.setCalculateNormals(true); minYCurtain.setReverseNormals(!reverseNormals); minYCurtain.setTopElevationOffset(topElevation); minYCurtain.setBottomElevationOffset(bottomElevation); minYCurtain.setTextureMatrix(curtainTextureMatrix); minYCurtain.setUseOrderedRendering(useOrderedRendering); maxYCurtain = dataProvider.createYCurtain(dataProvider.getYSize() - 1); maxYCurtain.addRenderListener(this); maxYCurtain.setLighted(true); maxYCurtain.setCalculateNormals(true); maxYCurtain.setReverseNormals(reverseNormals); maxYCurtain.setTopElevationOffset(topElevation); maxYCurtain.setBottomElevationOffset(bottomElevation); maxYCurtain.setTextureMatrix(curtainTextureMatrix); maxYCurtain.setUseOrderedRendering(useOrderedRendering); Rectangle rectangle = new Rectangle(0, 0, dataProvider.getXSize(), dataProvider.getYSize()); topSurface = dataProvider.createHorizontalSurface((float) maxVariance, rectangle); topSurface.addRenderListener(this); topSurface.setLighted(true); topSurface.setCalculateNormals(true); topSurface.setReverseNormals(reverseNormals); topSurface.setElevation(topElevation); topSurface.setUseOrderedRendering(useOrderedRendering); bottomSurface = dataProvider.createHorizontalSurface((float) maxVariance, rectangle); bottomSurface.addRenderListener(this); bottomSurface.setLighted(true); bottomSurface.setCalculateNormals(true); bottomSurface.setReverseNormals(!reverseNormals); bottomSurface.setElevation(bottomElevation); bottomSurface.setUseOrderedRendering(useOrderedRendering); //update each shape's wireframe property so they match the layer's setWireframe(isWireframe()); } /** * Recalculate any surfaces that require recalculation. This includes * regenerating textures when the user has dragged a surface to a different * slice. */ protected void recalculateSurfaces() { if (!dataAvailable) { return; } int xSize = dataProvider.getXSize(); int ySize = dataProvider.getYSize(); int zSize = dataProvider.getZSize(); //ensure the min/max offsets don't overlap one-another minXOffset = Util.clamp(minXOffset, 0, xSize - 1); maxXOffset = Util.clamp(maxXOffset, 0, xSize - 1 - minXOffset); minYOffset = Util.clamp(minYOffset, 0, ySize - 1); maxYOffset = Util.clamp(maxYOffset, 0, ySize - 1 - minYOffset); topOffset = Util.clamp(topOffset, 0, zSize - 1); bottomOffset = Util.clamp(bottomOffset, 0, zSize - 1 - topOffset); int maxXSlice = xSize - 1 - maxXOffset; int maxYSlice = ySize - 1 - maxYOffset; int bottomSlice = zSize - 1 - bottomOffset; //only recalculate those that have changed boolean recalculateMinX = lastMinXOffset != minXOffset; boolean recalculateMaxX = lastMaxXOffset != maxXOffset; boolean recalculateMinY = lastMinYOffset != minYOffset; boolean recalculateMaxY = lastMaxYOffset != maxYOffset; boolean recalculateTop = lastTopOffset != topOffset; boolean recalculateBottom = lastBottomOffset != bottomOffset; Dimension xTextureSize = new Dimension(ySize, zSize); Dimension yTextureSize = new Dimension(xSize, zSize); Dimension zTextureSize = new Dimension(xSize, ySize); double topPercent = dataProvider.getSliceElevationPercent(topOffset); double bottomPercent = dataProvider.getSliceElevationPercent(bottomSlice); if (recalculateMinX) { minXClipDirty = true; TopBottomFastShape newMinXCurtain = dataProvider.createXCurtain(minXOffset); minXCurtain.setPositions(newMinXCurtain.getPositions()); updateTexture(generateTexture(0, minXOffset, xTextureSize), minXTexture, minXCurtain); lastMinXOffset = minXOffset; } if (recalculateMaxX) { maxXClipDirty = true; TopBottomFastShape newMaxXCurtain = dataProvider.createXCurtain(xSize - 1 - maxXOffset); maxXCurtain.setPositions(newMaxXCurtain.getPositions()); updateTexture(generateTexture(0, maxXSlice, xTextureSize), maxXTexture, maxXCurtain); lastMaxXOffset = maxXOffset; } if (recalculateMinY) { minYClipDirty = true; TopBottomFastShape newMinYCurtain = dataProvider.createYCurtain(minYOffset); minYCurtain.setPositions(newMinYCurtain.getPositions()); updateTexture(generateTexture(1, minYOffset, yTextureSize), minYTexture, minYCurtain); lastMinYOffset = minYOffset; } if (recalculateMaxY) { maxYClipDirty = true; TopBottomFastShape newMaxYCurtain = dataProvider.createYCurtain(ySize - 1 - maxYOffset); maxYCurtain.setPositions(newMaxYCurtain.getPositions()); updateTexture(generateTexture(1, maxYSlice, yTextureSize), maxYTexture, maxYCurtain); lastMaxYOffset = maxYOffset; } if (recalculateTop) { topClipDirty = true; double elevation = -dataProvider.getDepth() * topPercent; updateTexture(generateTexture(2, topOffset, zTextureSize), topTexture, topSurface); lastTopOffset = topOffset; topSurface.setElevation(elevation); minXCurtain.setTopElevationOffset(elevation); maxXCurtain.setTopElevationOffset(elevation); minYCurtain.setTopElevationOffset(elevation); maxYCurtain.setTopElevationOffset(elevation); recalculateTextureMatrix(topPercent, bottomPercent); } if (recalculateBottom) { bottomClipDirty = true; double elevation = -dataProvider.getDepth() * bottomPercent; updateTexture(generateTexture(2, bottomSlice, zTextureSize), bottomTexture, bottomSurface); lastBottomOffset = bottomOffset; bottomSurface.setElevation(elevation); minXCurtain.setBottomElevationOffset(elevation); maxXCurtain.setBottomElevationOffset(elevation); minYCurtain.setBottomElevationOffset(elevation); maxYCurtain.setBottomElevationOffset(elevation); recalculateTextureMatrix(topPercent, bottomPercent); } } /** * Recalculate the curtain texture matrix. When the top and bottom surface * offsets aren't 0, the OpenGL texture matrix is used to offset the curtain * textures. * * @param topPercent * Location of the top surface (normalized to 0..1) * @param bottomPercent * Location of the bottom surface (normalized to 0..1) */ protected void recalculateTextureMatrix(double topPercent, double bottomPercent) { Matrix m = Matrix.fromTranslation(0, topPercent, 0).multiply(Matrix.fromScale(1, bottomPercent - topPercent, 1)); m.toArray(curtainTextureMatrix, 0, false); } /** * Recalculate the clipping planes used to clip the surfaces when the user * drags them. * * @param dc */ protected void recalculateClippingPlanes(DrawContext dc) { if (!dataAvailable || dataProvider.isSingleSliceVolume()) { return; } boolean verticalExaggerationChanged = lastVerticalExaggeration != dc.getVerticalExaggeration(); lastVerticalExaggeration = dc.getVerticalExaggeration(); boolean minX = minXClipDirty || verticalExaggerationChanged; boolean maxX = maxXClipDirty || verticalExaggerationChanged; boolean minY = minYClipDirty || verticalExaggerationChanged; boolean maxY = maxYClipDirty || verticalExaggerationChanged; boolean sw = minX || minY; boolean nw = minX || maxY; boolean se = maxX || minY; boolean ne = maxX || maxY; minX |= topClipDirty || bottomClipDirty; maxX |= topClipDirty || bottomClipDirty; minY |= topClipDirty || bottomClipDirty; maxY |= topClipDirty || bottomClipDirty; if (!(minX || maxX || minY || maxY)) { return; } int maxXSlice = dataProvider.getXSize() - 1 - maxXOffset; int maxYSlice = dataProvider.getYSize() - 1 - maxYOffset; int bottomSlice = dataProvider.getZSize() - 1 - bottomOffset; double top = dataProvider.getTop(); double depth = dataProvider.getDepth(); double topPercent = dataProvider.getSliceElevationPercent(topOffset); double bottomPercent = dataProvider.getSliceElevationPercent(bottomSlice); double topElevation = top - topPercent * depth; double bottomElevation = top - bottomPercent * depth; Position swPosTop = dataProvider.getPosition(minXOffset, minYOffset); Position nwPosTop = dataProvider.getPosition(minXOffset, maxYSlice); Position sePosTop = dataProvider.getPosition(maxXSlice, minYOffset); Position nePosTop = dataProvider.getPosition(maxXSlice, maxYSlice); if (depth != 0 && dc.getVerticalExaggeration() > 0) { Position origin = dataProvider.getPosition(0, 0); Position originPlusOne = dataProvider.getPosition(1, 1); Angle azimuth = LatLon.linearAzimuth(originPlusOne, origin); double delta = 0.005; double sin = azimuth.sin() * delta; double cos = azimuth.cos() * delta; if (se) { Position sePosBottom = new Position(sePosTop, sePosTop.elevation - depth); Position otherPos = sePosTop.add(Position.fromDegrees(cos, sin, 0)); insertClippingPlaneForPositions(dc, curtainClippingPlanes, 8, sePosTop, otherPos, sePosBottom); } if (ne) { Position nePosBottom = new Position(nePosTop, nePosTop.elevation - depth); Position otherPos = nePosTop.add(Position.fromDegrees(-sin, cos, 0)); insertClippingPlaneForPositions(dc, curtainClippingPlanes, 12, nePosTop, nePosBottom, otherPos); } if (nw) { Position nwPosBottom = new Position(nwPosTop, nwPosTop.elevation - depth); Position otherPos = nwPosTop.add(Position.fromDegrees(-cos, -sin, 0)); insertClippingPlaneForPositions(dc, curtainClippingPlanes, 4, nwPosTop, otherPos, nwPosBottom); } if (sw) { Position swPosBottom = new Position(swPosTop, swPosTop.elevation - depth); Position otherPos = swPosTop.add(Position.fromDegrees(sin, -cos, 0)); insertClippingPlaneForPositions(dc, curtainClippingPlanes, 0, swPosTop, swPosBottom, otherPos); } } //the following only works for a spherical earth (as opposed to flat earth), as it relies on adjacent //points not being colinear (3 points along a latitude are not colinear when wrapped around a sphere) if (minX) { Position middlePos = dataProvider.getPosition(minXOffset, (maxYSlice + minYOffset) / 2); middlePos = midpointPositionIfEqual(middlePos, nwPosTop, swPosTop); insertClippingPlaneForLatLons(dc, topClippingPlanes, 0, middlePos, nwPosTop, swPosTop, topElevation); insertClippingPlaneForLatLons(dc, bottomClippingPlanes, 0, middlePos, nwPosTop, swPosTop, bottomElevation); } if (maxX) { Position middlePos = dataProvider.getPosition(maxXSlice, (maxYSlice + minYOffset) / 2); middlePos = midpointPositionIfEqual(middlePos, sePosTop, nePosTop); insertClippingPlaneForLatLons(dc, topClippingPlanes, 4, middlePos, sePosTop, nePosTop, topElevation); insertClippingPlaneForLatLons(dc, bottomClippingPlanes, 4, middlePos, sePosTop, nePosTop, bottomElevation); } if (minY) { Position middlePos = dataProvider.getPosition((maxXSlice + minXOffset) / 2, minYOffset); middlePos = midpointPositionIfEqual(middlePos, swPosTop, sePosTop); insertClippingPlaneForLatLons(dc, topClippingPlanes, 8, middlePos, swPosTop, sePosTop, topElevation); insertClippingPlaneForLatLons(dc, bottomClippingPlanes, 8, middlePos, swPosTop, sePosTop, bottomElevation); } if (maxY) { Position middlePos = dataProvider.getPosition((maxXSlice + minXOffset) / 2, maxYSlice); middlePos = midpointPositionIfEqual(middlePos, nePosTop, nwPosTop); insertClippingPlaneForLatLons(dc, topClippingPlanes, 12, middlePos, nePosTop, nwPosTop, topElevation); insertClippingPlaneForLatLons(dc, bottomClippingPlanes, 12, middlePos, nePosTop, nwPosTop, bottomElevation); } minXClipDirty = maxXClipDirty = minYClipDirty = maxYClipDirty = topClipDirty = bottomClipDirty = false; } /** * Return the midpoint of the two end positions if the given middle position * is equal to one of the ends. * * @param middle * @param end1 * @param end2 * @return Midpoint between end1 and end2 if middle equals end1 or end2. */ protected Position midpointPositionIfEqual(Position middle, Position end1, Position end2) { if (middle.equals(end1) || middle.equals(end2)) { return Position.interpolate(0.5, end1, end2); } return middle; } /** * Insert a clipping plane vector into the given array. The vector is * calculated by finding a plane that intersects the three given positions. * * @param dc * @param clippingPlaneArray * Array to insert clipping plane vector into * @param arrayOffset * Array start offset to begin inserting values at * @param p1 * First position that the plane must intersect * @param p2 * Second position that the plane must intersect * @param p3 * Third position that the plane must intersect */ protected void insertClippingPlaneForPositions(DrawContext dc, double[] clippingPlaneArray, int arrayOffset, Position p1, Position p2, Position p3) { Globe globe = dc.getGlobe(); Vec4 v1 = globe.computePointFromPosition(p1, p1.getElevation() * dc.getVerticalExaggeration()); Vec4 v2 = globe.computePointFromPosition(p2, p2.getElevation() * dc.getVerticalExaggeration()); Vec4 v3 = globe.computePointFromPosition(p3, p3.getElevation() * dc.getVerticalExaggeration()); insertClippingPlaneForPoints(clippingPlaneArray, arrayOffset, v1, v2, v3); } /** * Insert a clipping plane vector into the given array. The vector is * calculated by finding a plane that intersects the three given latlons at * the given elevation. * * @param dc * @param clippingPlaneArray * Array to insert clipping plane vector into * @param arrayOffset * Array start offset to begin inserting values at * @param l1 * First latlon that the plane must intersect * @param l2 * Second latlon that the plane must intersect * @param l3 * Third latlon that the plane must intersect * @param elevation * Elevation of the latlons */ protected void insertClippingPlaneForLatLons(DrawContext dc, double[] clippingPlaneArray, int arrayOffset, LatLon l1, LatLon l2, LatLon l3, double elevation) { Globe globe = dc.getGlobe(); double exaggeratedElevation = elevation * dc.getVerticalExaggeration(); Vec4 v1 = globe.computePointFromPosition(l1, exaggeratedElevation); Vec4 v2 = globe.computePointFromPosition(l2, exaggeratedElevation); Vec4 v3 = globe.computePointFromPosition(l3, exaggeratedElevation); insertClippingPlaneForPoints(clippingPlaneArray, arrayOffset, v1, v2, v3); } /** * Insert a clipping plane vector into the given array. The vector is * calculated by finding a plane that intersects the three given points. * * @param clippingPlaneArray * Array to insert clipping plane vector into * @param arrayOffset * Array start offset to begin inserting values at * @param v1 * First point that the plane must intersect * @param v2 * Second point that the plane must intersect * @param v3 * Third point that the plane must intersect */ protected void insertClippingPlaneForPoints(double[] clippingPlaneArray, int arrayOffset, Vec4 v1, Vec4 v2, Vec4 v3) { if (v1 == null || v2 == null || v3 == null || v1.equals(v2) || v1.equals(v3)) { return; } Line l1 = Line.fromSegment(v1, v3); Line l2 = Line.fromSegment(v1, v2); Plane plane = GeometryUtil.createPlaneContainingLines(l1, l2); Vec4 v = plane.getVector(); clippingPlaneArray[arrayOffset + 0] = v.x; clippingPlaneArray[arrayOffset + 1] = v.y; clippingPlaneArray[arrayOffset + 2] = v.z; clippingPlaneArray[arrayOffset + 3] = v.w; } /** * Generate a texture slice through the volume at the given position. Uses a * {@link ColorMap} to map values to colors (or simply interpolates the hue * if no colormap is provided - assumes values between 0 and 1). * * @param axis * Slicing axis (0 for a longitude slice, 1 for a latitude slice, * 2 for an elevation slice). * @param position * Longitude, latitude, or elevation at which to slice. * @param size * Size of the texture to generate. * @return A {@link BufferedImage} containing a representation of the volume * slice. */ protected BufferedImage generateTexture(int axis, int position, Dimension size) { int zSubsamples = dataProvider.getZSubsamples(); boolean subsample = axis != 2 && zSubsamples > 1; int height = size.height; if (subsample) { height *= zSubsamples; } BufferedImage image = new BufferedImage(size.width, height, BufferedImage.TYPE_INT_ARGB); float minimum = dataProvider.getMinValue(); float maximum = dataProvider.getMaxValue(); for (int y = 0; y < height; y++) { for (int x = 0; x < size.width; x++) { int vx = axis == 2 ? x : axis == 1 ? x : position; int vy = axis == 2 ? y : axis == 1 ? position : x; int vz = axis == 2 ? position : y; float value; if (subsample) { double percent = y / (double) (height - 1); double z = dataProvider.getElevationPercentSlice(percent); int z1 = (int) Math.floor(z); int z2 = (int) Math.ceil(z); float value1 = dataProvider.getValue(vx, vy, z1); float value2 = dataProvider.getValue(vx, vy, z2); float zp = (float) (z % 1.0); value = value1 * (1f - zp) + value2 * zp; } else { value = dataProvider.getValue(vx, vy, vz); } int rgb = noDataColor != null ? noDataColor.getRGB() : 0; if (value != dataProvider.getNoDataValue()) { if (colorMap != null) { rgb = colorMap.calculateColorNotingIsValuesPercentages(value, minimum, maximum).getRGB(); } else { rgb = Color.HSBtoRGB(-0.3f - value * 0.7f, 1.0f, 1.0f); } } image.setRGB(x, y, rgb); } } return image; } /** * Update the given {@link TextureRenderer} with the provided image, and * sets the {@link FastShape}'s texture it. * * @param image * Image to update texture with * @param texture * Texture to update * @param shape * Shape to set texture in */ protected void updateTexture(BufferedImage image, TextureRenderer texture, FastShape shape) { Graphics2D g = null; try { g = (Graphics2D) texture.getImage().getGraphics(); g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC)); g.drawImage(image, 0, 0, null); } finally { if (g != null) { g.dispose(); } } texture.markDirty(0, 0, texture.getWidth(), texture.getHeight()); shape.setTexture(texture.getTexture()); } @Override public CoordinateTransformation getCoordinateTransformation() { return coordinateTransformation; } @Override public String getPaintedVariableName() { return paintedVariable; } @Override protected void doPick(DrawContext dc, Point point) { if (!dataProvider.isSingleSliceVolume()) { doRender(dc); } } @Override protected void doRender(DrawContext dc) { if (!isEnabled()) { return; } dataProvider.requestData(this); synchronized (dataLock) { if (!dataAvailable) { return; } if (topTexture == null) { topTexture = new TextureRenderer(dataProvider.getXSize(), dataProvider.getYSize(), true, true); bottomTexture = new TextureRenderer(dataProvider.getXSize(), dataProvider.getYSize(), true, true); minXTexture = new TextureRenderer(dataProvider.getYSize(), dataProvider.getZSize() * dataProvider.getZSubsamples(), true, true); maxXTexture = new TextureRenderer(dataProvider.getYSize(), dataProvider.getZSize() * dataProvider.getZSubsamples(), true, true); minYTexture = new TextureRenderer(dataProvider.getXSize(), dataProvider.getZSize() * dataProvider.getZSubsamples(), true, true); maxYTexture = new TextureRenderer(dataProvider.getXSize(), dataProvider.getZSize() * dataProvider.getZSubsamples(), true, true); } //recalculate surfaces and clipping planes each frame (in case user drags one of the surfaces) recalculateSurfaces(); recalculateClippingPlanes(dc); //when only one slice is shown in any given direction, only one of the curtains needs to be rendered boolean singleXSlice = dataProvider.getXSize() - minXOffset - maxXOffset <= 1; boolean singleYSlice = dataProvider.getYSize() - minYOffset - maxYOffset <= 1; boolean singleZSlice = dataProvider.getZSize() - topOffset - bottomOffset <= 1; boolean anySingleSlice = singleXSlice || singleYSlice || singleZSlice; FastShape[] shapes = anySingleSlice ? new FastShape[] { singleXSlice ? maxXCurtain : singleYSlice ? minYCurtain : topSurface } : new FastShape[] { topSurface, bottomSurface, minXCurtain, maxXCurtain, minYCurtain, maxYCurtain }; //sort the shapes from back-to-front if (!anySingleSlice) { Arrays.sort(shapes, new ShapeComparator(dc)); } //test all the shapes with the minimum distance, culling them if they are outside if (minimumDistance != null) { for (int i = 0; i < shapes.length; i++) { Extent extent = shapes[i].getExtent(); if (extent != null) { double distanceToEye = extent.getCenter().distanceTo3(dc.getView().getEyePoint()) - extent.getRadius(); if (distanceToEye > minimumDistance) { shapes[i] = null; } } } } //draw each shape for (FastShape shape : shapes) { if (shape != null) { shape.setTwoSidedLighting(anySingleSlice); if (dc.isPickingMode()) { shape.pick(dc, dc.getPickPoint()); } else { shape.render(dc); } } } if (dragging) { //render a bounding box around the data if the user is dragging a surface renderBoundingBox(dc); } } } @Override public void shapePreRender(DrawContext dc, FastShape shape) { //push the OpenGL clipping plane state on the attribute stack dc.getGL().getGL2().glPushAttrib(GL2.GL_TRANSFORM_BIT); setupClippingPlanes(dc, shape == topSurface, shape == bottomSurface); } @Override public void shapePostRender(DrawContext dc, FastShape shape) { dc.getGL().getGL2().glPopAttrib(); } protected void setupClippingPlanes(DrawContext dc, boolean top, boolean bottom) { boolean minX = minXOffset > 0; boolean maxX = maxXOffset > 0; boolean minY = minYOffset > 0; boolean maxY = maxYOffset > 0; boolean[] enabled; double[] array; GL2 gl = dc.getGL().getGL2(); if (top || bottom) { array = top ? topClippingPlanes : bottomClippingPlanes; enabled = new boolean[] { minX, maxX, minY, maxY }; } else { array = curtainClippingPlanes; boolean sw = minX || minY; boolean nw = minX || maxY; boolean se = maxX || minY; boolean ne = maxX || maxY; enabled = new boolean[] { sw, nw, se, ne }; } for (int i = 0; i < 4; i++) { gl.glClipPlane(GL2.GL_CLIP_PLANE0 + i, array, i * 4); if (enabled[i]) { gl.glEnable(GL2.GL_CLIP_PLANE0 + i); } else { gl.glDisable(GL2.GL_CLIP_PLANE0 + i); } } } /** * Render a bounding box around the data. Used when dragging surfaces, so * user has an idea of where the data extents lie when slicing. * * @param dc */ protected void renderBoundingBox(DrawContext dc) { if (boundingBoxShape == null) { boundingBoxShape = dataProvider.createBoundingBox(); } boundingBoxShape.render(dc); } @Override public boolean isWireframe() { return wireframe; } @Override public void setWireframe(boolean wireframe) { this.wireframe = wireframe; synchronized (dataLock) { if (topSurface != null) { topSurface.setWireframe(wireframe); bottomSurface.setWireframe(wireframe); minXCurtain.setWireframe(wireframe); maxXCurtain.setWireframe(wireframe); minYCurtain.setWireframe(wireframe); maxYCurtain.setWireframe(wireframe); } } } @Override public void selected(SelectEvent event) { //ignore this event if ctrl, alt, or shift are down if (event.getMouseEvent() != null) { int onmask = MouseEvent.SHIFT_DOWN_MASK | MouseEvent.CTRL_DOWN_MASK | MouseEvent.ALT_DOWN_MASK; if ((event.getMouseEvent().getModifiersEx() & onmask) != 0) { return; } } //don't allow dragging if there's only one layer in any one direction if (dataProvider.isSingleSliceVolume()) { return; } //we only care about drag events boolean drag = event.getEventAction().equals(SelectEvent.DRAG); boolean dragEnd = event.getEventAction().equals(SelectEvent.DRAG_END); if (!(drag || dragEnd)) { return; } Object topObject = event.getTopObject(); FastShape pickedShape = topObject instanceof FastShape ? (FastShape) topObject : null; if (pickedShape == null) { return; } boolean top = pickedShape == topSurface; boolean bottom = pickedShape == bottomSurface; boolean minX = pickedShape == minXCurtain; boolean maxX = pickedShape == maxXCurtain; boolean minY = pickedShape == minYCurtain; boolean maxY = pickedShape == maxYCurtain; if (top || bottom || minX || maxX || minY || maxY) { if (dragEnd) { dragging = false; event.consume(); } else if (drag) { if (!dragging || dragStartCenter == null) { Extent extent = pickedShape.getExtent(); if (extent != null) { dragStartCenter = extent.getCenter(); } } if (dragStartCenter != null) { WorldWindow wwd = WorldWindowRegistry.INSTANCE.getRendering(); if (wwd != null) { View view = wwd.getView(); if (top || bottom) { dragElevation(event.getPickPoint(), pickedShape, view); } else if (minX || maxX) { dragX(event.getPickPoint(), pickedShape, view); } else { dragY(event.getPickPoint(), pickedShape, view); } } } dragging = true; event.consume(); } } } /** * Drag an elevation surface up and down. * * @param pickPoint * Point at which the user is dragging the mouse. * @param shape * Shape to drag */ protected void dragElevation(Point pickPoint, FastShape shape, View view) { // Calculate the plane projected from screen y=pickPoint.y Line screenLeftRay = view.computeRayFromScreenPoint(pickPoint.x - 100, pickPoint.y); Line screenRightRay = view.computeRayFromScreenPoint(pickPoint.x + 100, pickPoint.y); // As the two lines are very close to parallel, use an arbitrary line joining them rather than the two lines to avoid precision problems Line joiner = Line.fromSegment(screenLeftRay.getPointAt(500), screenRightRay.getPointAt(500)); Plane screenPlane = GeometryUtil.createPlaneContainingLines(screenLeftRay, joiner); if (screenPlane == null) { return; } // Calculate the origin-marker ray Globe globe = view.getGlobe(); Line centreRay = Line.fromSegment(globe.getCenter(), dragStartCenter); Vec4 intersection = screenPlane.intersect(centreRay); if (intersection == null) { return; } Position intersectionPosition = globe.computePositionFromPoint(intersection); if (!dragging) { dragStartPosition = intersectionPosition; dragStartSlice = shape == topSurface ? topOffset : bottomOffset; } else { double deltaElevation = (dragStartPosition.elevation - intersectionPosition.elevation) / (lastVerticalExaggeration == 0 ? 1 : lastVerticalExaggeration); double deltaPercentage = deltaElevation / dataProvider.getDepth(); int sliceMovement = (int) (deltaPercentage * (dataProvider.getZSize() - 1)); if (shape == topSurface) { topOffset = Util.clamp(dragStartSlice + sliceMovement, 0, dataProvider.getZSize() - 1); bottomOffset = Util.clamp(bottomOffset, 0, dataProvider.getZSize() - 1 - topOffset); } else { bottomOffset = Util.clamp(dragStartSlice - sliceMovement, 0, dataProvider.getZSize() - 1); topOffset = Util.clamp(topOffset, 0, dataProvider.getZSize() - 1 - bottomOffset); } } } /** * Drag a X curtain left and right. * * @param pickPoint * Point at which the user is dragging the mouse. * @param shape * Shape to drag */ protected void dragX(Point pickPoint, FastShape shape, View view) { Globe globe = view.getGlobe(); double centerElevation = globe.computePositionFromPoint(dragStartCenter).elevation; // Compute the ray from the screen point Line ray = view.computeRayFromScreenPoint(pickPoint.x, pickPoint.y); Intersection[] intersections = globe.intersect(ray, centerElevation); if (intersections == null || intersections.length == 0) { return; } Vec4 intersection = ray.nearestIntersectionPoint(intersections); if (intersection == null) { return; } Position position = globe.computePositionFromPoint(intersection); if (!dragging) { dragStartPosition = position; dragStartSlice = shape == minXCurtain ? minXOffset : maxXOffset; } else { Position p0 = dataProvider.getPosition(0, dataProvider.getYSize() / 2); Position p1 = dataProvider.getPosition(dataProvider.getXSize() - 1, dataProvider.getYSize() / 2); Angle volumeAzimuth = LatLon.linearAzimuth(p0, p1); Angle volumeDistance = LatLon.linearDistance(p0, p1); Angle movementAzimuth = LatLon.linearAzimuth(position, dragStartPosition); Angle movementDistance = LatLon.linearDistance(position, dragStartPosition); Angle deltaAngle = volumeAzimuth.subtract(movementAzimuth); double delta = movementDistance.degrees * deltaAngle.cos(); double deltaPercentage = delta / volumeDistance.degrees; int sliceMovement = (int) (-deltaPercentage * (dataProvider.getXSize() - 1)); if (shape == minXCurtain) { minXOffset = Util.clamp(dragStartSlice + sliceMovement, 0, dataProvider.getXSize() - 1); maxXOffset = Util.clamp(maxXOffset, 0, dataProvider.getXSize() - 1 - minXOffset); } else { maxXOffset = Util.clamp(dragStartSlice - sliceMovement, 0, dataProvider.getXSize() - 1); minXOffset = Util.clamp(minXOffset, 0, dataProvider.getXSize() - 1 - maxXOffset); } } } /** * Drag a Y curtain left and right. * * @param pickPoint * Point at which the user is dragging the mouse. * @param shape * Shape to drag */ protected void dragY(Point pickPoint, FastShape shape, View view) { Globe globe = view.getGlobe(); double centerElevation = globe.computePositionFromPoint(dragStartCenter).elevation; // Compute the ray from the screen point Line ray = view.computeRayFromScreenPoint(pickPoint.x, pickPoint.y); Intersection[] intersections = globe.intersect(ray, centerElevation); if (intersections == null || intersections.length == 0) { return; } Vec4 intersection = ray.nearestIntersectionPoint(intersections); if (intersection == null) { return; } Position position = globe.computePositionFromPoint(intersection); if (!dragging) { dragStartPosition = position; dragStartSlice = shape == minYCurtain ? minYOffset : maxYOffset; } else { Position p0 = dataProvider.getPosition(dataProvider.getXSize() / 2, 0); Position p1 = dataProvider.getPosition(dataProvider.getXSize() / 2, dataProvider.getYSize() - 1); Angle volumeAzimuth = LatLon.linearAzimuth(p0, p1); Angle volumeDistance = LatLon.linearDistance(p0, p1); Angle movementAzimuth = LatLon.linearAzimuth(position, dragStartPosition); Angle movementDistance = LatLon.linearDistance(position, dragStartPosition); Angle deltaAngle = volumeAzimuth.subtract(movementAzimuth); double delta = movementDistance.degrees * deltaAngle.cos(); double deltaPercentage = delta / volumeDistance.degrees; int sliceMovement = (int) (-deltaPercentage * (dataProvider.getYSize() - 1)); if (shape == minYCurtain) { minYOffset = Util.clamp(dragStartSlice + sliceMovement, 0, dataProvider.getYSize() - 1); maxYOffset = Util.clamp(maxYOffset, 0, dataProvider.getYSize() - 1 - minYOffset); } else { maxYOffset = Util.clamp(dragStartSlice - sliceMovement, 0, dataProvider.getYSize() - 1); minYOffset = Util.clamp(minYOffset, 0, dataProvider.getYSize() - 1 - maxYOffset); } } } /** * {@link Comparator} used to sort {@link FastShape}s from back-to-front * (from the view eye point). */ protected class ShapeComparator implements Comparator<FastShape> { private final DrawContext dc; public ShapeComparator(DrawContext dc) { this.dc = dc; } @Override public int compare(FastShape o1, FastShape o2) { if (o1 == o2) { return 0; } if (o2 == null) { return -1; } if (o1 == null) { return 1; } Extent e1 = o1.getExtent(); Extent e2 = o2.getExtent(); if (e2 == null) { return -1; } if (e1 == null) { return 1; } Vec4 eyePoint = dc.getView().getEyePoint(); double d1 = e1.getCenter().distanceToSquared3(eyePoint); double d2 = e2.getCenter().distanceToSquared3(eyePoint); return -Double.compare(d1, d2); } } }