/* Copyright (C) 2001, 2006 United States Government as represented by the Administrator of the National Aeronautics and Space Administration. All Rights Reserved. */ package gov.nasa.worldwind.render; import com.sun.opengl.util.texture.TextureData; import gov.nasa.worldwind.*; import gov.nasa.worldwind.globes.Globe; import gov.nasa.worldwind.geom.*; import gov.nasa.worldwind.layers.TextureTile; import gov.nasa.worldwind.util.Logging; import javax.media.opengl.GL; import java.awt.*; import java.awt.image.*; import java.util.*; /** * @author tag * @version $Id: SurfaceShape.java 3725 2007-12-05 18:17:13Z patrickmurris $ */ public abstract class SurfaceShape implements Renderable, Disposable, Movable { public static Dimension TEXTURE_SIZE_1024 = new Dimension(1024, 1024); public static Dimension TEXTURE_SIZE_512 = new Dimension(512, 512); public static Dimension TEXTURE_SIZE_256 = new Dimension(256, 256); public static Dimension TEXTURE_SIZE_128 = new Dimension(128, 128); public static Dimension TEXTURE_SIZE_64 = new Dimension(64, 64); public static Dimension TEXTURE_SIZE_32 = new Dimension(32, 32); public static Dimension TEXTURE_SIZE_16 = new Dimension(16, 16); public static Dimension TEXTURE_SIZE_8 = new Dimension(8, 8); private static final Color DEFAULT_COLOR = new Color(1f, 1f, 0f, 0.4f); private static final Color DEFAULT_BORDER_COLOR = new Color(1f, 1f, 0f, 0.7f); private static final Dimension DEFAULT_TEXTURE_SIZE = TEXTURE_SIZE_64; private static final double DEFAULT_NUM_EDGE_INTERVALS_PER_DEGREE = 1; private static final double TEXTURE_MARGIN_PIXELS = 3; private ArrayList<TextureTile> tiles = new ArrayList<TextureTile>(); private Dimension textureSize = DEFAULT_TEXTURE_SIZE; protected Globe globe; private Paint paint; private Color borderColor; private Stroke stroke = new BasicStroke(); private boolean drawBorder = true; private boolean drawInterior = true; private boolean antiAlias = true; private double numEdgeIntervalsPerDegree = DEFAULT_NUM_EDGE_INTERVALS_PER_DEGREE; protected ArrayList<LatLon> positions = new ArrayList<LatLon>(); protected abstract BufferedImage drawShape(Globe globe, Sector sector, BufferedImage image); public SurfaceShape(Iterable<LatLon> positions, Color color, Color borderColor, Dimension textureSize) { if (positions == null) { String message = Logging.getMessage("nullValue.PositionsListIsNull"); Logging.logger().severe(message); throw new IllegalArgumentException(message); } if (textureSize != null) this.textureSize = textureSize; // Set draw attributes this.paint = color != null ? color : DEFAULT_COLOR; this.borderColor = borderColor != null ? borderColor : DEFAULT_BORDER_COLOR; // Copy positions list. this.replacePositions(positions); // Make tile(s) createTextureTiles(); } private void replacePositions(Iterable<LatLon> newPositions) { this.positions.clear(); for (LatLon position : newPositions) { this.positions.add(position); } } // // protected void createTextureTiles() // { // this.tiles.clear(); // if (!LatLon.positionsCrossDateLine(this.getPositions())) // { // this.tiles.add( // new TextureTile(this.computeProportionedSector(Sector.boundingSectorfromLatLons(this.getPositions())))); // } // else // { // Sector[] sectors = this.computeSplitSectors(this.getPositions()); // this.tiles.add(new TextureTile(this.computeProportionedSector(sectors[0]))); // this.tiles.add(new TextureTile(this.computeProportionedSector(sectors[1]))); // } // } protected void createTextureTiles() { this.tiles.clear(); if (!LatLon.positionsCrossDateLine(this.getPositions())) { this.tiles.add( new TextureTile(Sector.boundingSectorfromLatLons(this.getPositions()))); } else { Sector[] sectors = this.computeSplitSectors(this.getPositions()); this.tiles.add(new TextureTile(sectors[0])); this.tiles.add(new TextureTile(sectors[1])); } } /** * Returns a sector that will have the same apparent proportions as the texture when projected on the globe, and * that contains a given sector. Also adds a margin around. * * @param sector the sector to be included * @return the appropriate sector */ private Sector computeProportionedSector(Sector sector) { //if(true) return sector; // uncomment to disable // Make it look the same aspect ratio as the texture - without going over the edges // taking into account the sector centroid latitude distortion Angle latSpan = sector.getDeltaLat(); Angle lonSpan = sector.getDeltaLon(); Double midLatCos = sector.getCentroid().getLatitude().cos(); Double aspectRatio = this.getTextureSize().getWidth() / this.getTextureSize().getHeight(); if (lonSpan.degrees / latSpan.degrees * midLatCos < aspectRatio) { // Adjust longitude extent Angle halfDelta = latSpan.divide(midLatCos).multiply(aspectRatio).subtract(lonSpan).divide(2d); if (halfDelta.degrees * 2 > 360 - lonSpan.degrees) halfDelta = Angle.fromDegrees((360 - lonSpan.degrees) / 2); // Add latitude margin Angle latMargin = latSpan.divide(this.getTextureSize().getHeight()).multiply(TEXTURE_MARGIN_PIXELS); Angle northMargin = sector.getMaxLatitude().add(latMargin).degrees > 90 ? Angle.fromDegrees( 90 - sector.getMaxLatitude().degrees) : latMargin; Angle southMargin = sector.getMinLatitude().subtract(latMargin).degrees < -90 ? Angle.fromDegrees( sector.getMinLatitude().degrees + 90) : latMargin; if (sector.getMinLongitude().degrees - halfDelta.degrees < -180) { // -180 degrees longitude crossing Angle westDelta = Angle.fromDegrees(sector.getMinLongitude().degrees + 180); Angle eastDelta = halfDelta.add(halfDelta).subtract(westDelta); sector = new Sector(sector.getMinLatitude().subtract(southMargin), sector.getMaxLatitude().add(northMargin), sector.getMinLongitude().subtract(westDelta), sector.getMaxLongitude().add(eastDelta)); } else if (sector.getMaxLongitude().degrees + halfDelta.degrees > 180) { // +180 degrees longitude crossing Angle eastDelta = Angle.fromDegrees(180 - sector.getMaxLongitude().degrees); Angle westDelta = halfDelta.add(halfDelta).subtract(eastDelta); sector = new Sector(sector.getMinLatitude().subtract(southMargin), sector.getMaxLatitude().add(northMargin), sector.getMinLongitude().subtract(westDelta), sector.getMaxLongitude().add(eastDelta)); } else { // No edge crossing sector = new Sector(sector.getMinLatitude().subtract(southMargin), sector.getMaxLatitude().add(northMargin), sector.getMinLongitude().subtract(halfDelta), sector.getMaxLongitude().add(halfDelta)); } } else if (lonSpan.degrees / latSpan.degrees * midLatCos > aspectRatio) { // Adjust latitude extent Angle halfDelta = lonSpan.multiply(midLatCos).divide(aspectRatio).subtract(latSpan).divide(2d); if (halfDelta.degrees * 2 > 180 - latSpan.degrees) halfDelta = Angle.fromDegrees((180 - latSpan.degrees) / 2); // Add longitude margin Angle lonMargin = lonSpan.divide(this.getTextureSize().getWidth()).multiply(TEXTURE_MARGIN_PIXELS); Angle eastMargin = sector.getMaxLongitude().add(lonMargin).degrees > 180 ? Angle.fromDegrees( 180 - sector.getMaxLongitude().degrees) : lonMargin; Angle westMargin = sector.getMinLongitude().subtract(lonMargin).degrees < -180 ? Angle.fromDegrees( sector.getMinLongitude().degrees + 180) : lonMargin; if (sector.getMinLatitude().degrees - halfDelta.degrees < -90) { // -90 degrees latitude crossing Angle southDelta = Angle.fromDegrees(sector.getMinLatitude().degrees + 90); Angle northDelta = halfDelta.add(halfDelta).subtract(southDelta); sector = new Sector(sector.getMinLatitude().subtract(southDelta), sector.getMaxLatitude().add(northDelta), sector.getMinLongitude().subtract(westMargin), sector.getMaxLongitude().add(eastMargin)); } else if (sector.getMaxLatitude().degrees + halfDelta.degrees > 90) { // +90 degrees latitude crossing Angle northDelta = Angle.fromDegrees(90 - sector.getMaxLatitude().degrees); Angle southDelta = halfDelta.add(halfDelta).subtract(northDelta); sector = new Sector(sector.getMinLatitude().subtract(southDelta), sector.getMaxLatitude().add(northDelta), sector.getMinLongitude().subtract(westMargin), sector.getMaxLongitude().add(eastMargin)); } else { // No edge crossing sector = new Sector(sector.getMinLatitude().subtract(halfDelta), sector.getMaxLatitude().add(halfDelta), sector.getMinLongitude().subtract(westMargin), sector.getMaxLongitude().add(eastMargin)); } } else { // Proportions ok, just add margin // Add latitude margin Angle latMargin = latSpan.divide(this.getTextureSize().getHeight()).multiply(TEXTURE_MARGIN_PIXELS); Angle northMargin = sector.getMaxLatitude().add(latMargin).degrees > 90 ? Angle.fromDegrees( 90 - sector.getMaxLatitude().degrees) : latMargin; Angle southMargin = sector.getMinLatitude().subtract(latMargin).degrees < -90 ? Angle.fromDegrees( sector.getMinLatitude().degrees + 90) : latMargin; // Add longitude margin Angle lonMargin = lonSpan.divide(this.getTextureSize().getWidth()).multiply(TEXTURE_MARGIN_PIXELS); Angle eastMargin = sector.getMaxLongitude().add(lonMargin).degrees > 180 ? Angle.fromDegrees( 180 - sector.getMaxLongitude().degrees) : lonMargin; Angle westMargin = sector.getMinLongitude().subtract(lonMargin).degrees < -180 ? Angle.fromDegrees( sector.getMinLongitude().degrees + 180) : lonMargin; sector = new Sector(sector.getMinLatitude().subtract(southMargin), sector.getMaxLatitude().add(northMargin), sector.getMinLongitude().subtract(westMargin), sector.getMaxLongitude().add(eastMargin)); } //System.out.println(sector.toString()); return sector; } /** * Returns two 'mirror' sectors each on one side of the longitude boundary - for boundary crossing shapes * * @param positions the shape positions * @return an array of two sectors representing the shape */ private Sector[] computeSplitSectors(Iterable<LatLon> positions) { Sector[] sectors = new Sector[2]; // Find out longitude extremes for each sides double maxWest = Angle.NEG180.getDegrees(); double minEast = Angle.POS180.getDegrees(); // Find out absolute latitude extremes double minSouth = Angle.POS90.getDegrees(); double maxNorth = Angle.NEG90.getDegrees(); LatLon lastPos = null; for (LatLon pos : positions) { double lat = pos.getLatitude().getDegrees(); if (lat > maxNorth) maxNorth = lat; if (lat < minSouth) minSouth = lat; double lon = pos.getLongitude().getDegrees(); if (lon <= 0 && lon > maxWest) maxWest = lon; if (lon >= 0 && lon < minEast) minEast = lon; if (lastPos != null) { double lastLon = lastPos.getLongitude().getDegrees(); if (Math.signum(lon) != Math.signum(lastLon)) if (Math.abs(lon - lastLon) < 180) { // Crossing the zero longitude line too maxWest = 0; minEast = 0; } } lastPos = pos; } // Mirror the two sectors - same longitude span maxWest = minEast < -maxWest ? -minEast : maxWest; minEast = minEast > -maxWest ? -maxWest : minEast; sectors[0] = Sector.fromDegrees(minSouth, maxNorth, minEast, 180d); // East side sectors[1] = Sector.fromDegrees(minSouth, maxNorth, -180d, maxWest); // West side return sectors; } public void dispose() { tiles.clear(); } public ArrayList<Sector> getSectors() { ArrayList<Sector> sectors = new ArrayList<Sector>(); for (TextureTile tile : this.tiles) { sectors.add(tile.getSector()); } return sectors; } public Iterable<LatLon> getPositions() { return this.positions; } public void setPositions(Iterable<LatLon> positions) { this.replacePositions(positions); this.createTextureTiles(); } public Paint getPaint() { return paint; } public void setPaint(Paint paint) { this.paint = paint; this.clearTextureData(); } public Color getBorderColor() { return borderColor; } public void setBorderColor(Color borderColor) { this.borderColor = borderColor; this.clearTextureData(); } public Dimension getTextureSize() { return textureSize; } public void setTextureSize(Dimension textureSize) { this.textureSize = textureSize; this.createTextureTiles(); // Rebuild tile(s) sectors } public Stroke getStroke() { return stroke; } public void setStroke(Stroke stroke) { this.stroke = stroke; this.clearTextureData(); } public boolean isDrawBorder() { return drawBorder; } public void setDrawBorder(boolean drawBorder) { this.drawBorder = drawBorder; this.clearTextureData(); } public boolean isDrawInterior() { return drawInterior; } public void setDrawInterior(boolean drawInterior) { this.drawInterior = drawInterior; this.clearTextureData(); } public boolean isAntiAlias() { return antiAlias; } public void setAntiAlias(boolean antiAlias) { this.antiAlias = antiAlias; this.clearTextureData(); } public double getNumEdgeIntervalsPerDegree() { return numEdgeIntervalsPerDegree; } public void setNumEdgeIntervalsPerDegree(double numEdgeIntervals) { this.numEdgeIntervalsPerDegree = numEdgeIntervals; this.clearTextureData(); } private boolean intersects(Sector sector) { for (TextureTile tile : this.tiles) { if (tile.getSector().intersects(sector)) return true; } return false; } public void render(DrawContext dc) { this.globe = dc.getGlobe(); // retain the globe used, for potential subsequent move if (this.tiles.size() == 0) this.createTextureTiles(); if (!this.intersects(dc.getVisibleSector())) return; if (!this.tiles.get(0).isTextureInMemory(dc.getTextureCache())) makeTextureData(dc, this.textureSize); GL gl = dc.getGL(); gl.glPushAttrib(GL.GL_COLOR_BUFFER_BIT | GL.GL_POLYGON_BIT); try { if (!dc.isPickingMode()) { gl.glEnable(GL.GL_BLEND); gl.glBlendFunc(GL.GL_SRC_ALPHA, GL.GL_ONE_MINUS_SRC_ALPHA); } gl.glPolygonMode(GL.GL_FRONT, GL.GL_FILL); gl.glEnable(GL.GL_CULL_FACE); gl.glCullFace(GL.GL_BACK); dc.getGeographicSurfaceTileRenderer().renderTiles(dc, this.tiles); } finally { gl.glPopAttrib(); } } private void makeTextureData(DrawContext dc, Dimension size) { for (TextureTile tile : this.tiles) { BufferedImage image = new BufferedImage((int) size.getWidth(), (int) size.getHeight(), BufferedImage.TYPE_4BYTE_ABGR); /*// Debug - show tile extent with fill color Graphics2D g2 = image.createGraphics(); g2.setPaint(this.getPaint()); g2.fillRect(0, 0, (int) size.getWidth(), (int) size.getHeight()); // end debug */ TextureData td = new TextureData(GL.GL_RGBA, GL.GL_RGBA, false, this.drawShape(dc.getGlobe(), tile.getSector(), image)); td.setMustFlipVertically(false); tile.setTextureData(td); } } private void clearTextureData() { tiles.clear(); } public Position getReferencePosition() { LatLon centroid = this.tiles.get(0).getSector().getCentroid(); return new Position(centroid, 0); } public void move(Position delta) { if (delta == null) { String msg = Logging.getMessage("nullValue.PositionIsNull"); Logging.logger().severe(msg); throw new IllegalArgumentException(msg); } this.moveTo(this.getReferencePosition().add(delta)); } /** * Move the shape over the sphereoid surface without maintaining its original azimuth -- its orientation relative to * North. * * @param position the new position to move the shapes reference position to. */ public void shiftTo(Position position) { if (this.globe == null) return; if (position == null) { String msg = Logging.getMessage("nullValue.PositionIsNull"); Logging.logger().severe(msg); throw new IllegalArgumentException(msg); } Vec4 p1 = globe.computePointFromPosition(this.getReferencePosition().getLatitude(), this.getReferencePosition().getLongitude(), 0); Vec4 p2 = globe.computePointFromPosition(position.getLatitude(), position.getLongitude(), 0); Vec4 delta = p2.subtract3(p1); for (int i = 0; i < this.positions.size(); i++) { LatLon ll = this.positions.get(i); Vec4 p = globe.computePointFromPosition(ll.getLatitude(), ll.getLongitude(), 0); p = p.add3(delta); Position pos = globe.computePositionFromPoint(p); this.positions.set(i, new LatLon(pos.getLatitude(), pos.getLongitude())); } this.createTextureTiles(); } /** * Move the shape over the sphereoid surface while maintaining its original azimuth -- its orientation relative to * North. * * @param position the new position to move the shapes reference position to. */ public void moveTo(Position position) { if (LatLon.positionsCrossDateLine(this.positions)) { // TODO: Replace this hack by figuring out how to *accurately* move date-line crossing shapes using the // distance/azimuth method used below for shapes that do not cross the dateline. shiftTo(position); return; } LatLon oldRef = this.getReferencePosition().getLatLon(); LatLon newRef = position.getLatLon(); for (int i = 0; i < this.positions.size(); i++) { LatLon p = this.positions.get(i); double distance = LatLon.greatCircleDistance(oldRef, p).radians; double azimuth = LatLon.greatCircleAzimuth(oldRef, p).radians; LatLon pp = LatLon.greatCircleEndPosition(newRef, azimuth, distance); this.positions.set(i, pp); } this.createTextureTiles(); } public static SurfaceShape createEllipse(Globe globe, LatLon center, double majorAxisLength, double minorAxisLength, Angle orientation, int intervals, Color interiorColor, Color borderColor, Dimension textureSize) { if (orientation == null) orientation = Angle.ZERO; if (majorAxisLength <= 0) { String message = Logging.getMessage("Geom.MajorAxisInvalid"); Logging.logger().severe(message); throw new IllegalArgumentException(message); } if (minorAxisLength <= 0) { String message = Logging.getMessage("Geom.MajorAxisInvalid"); Logging.logger().severe(message); throw new IllegalArgumentException(message); } int numPositions = 1 + Math.max(intervals, 4); final ArrayList<LatLon> positions = new ArrayList<LatLon>(); double radius = globe.getRadiusAt(center.getLatitude(), center.getLongitude()); double da = 2 * Math.PI / (numPositions - 1); for (int i = 0; i < numPositions; i++) { // azimuth runs positive clockwise from north and through 360 degrees. double angle = (i != numPositions - 1) ? i * da : 0; double azimuth = Math.PI / 2 - (angle + orientation.radians); double xLength = majorAxisLength * Math.cos(angle); double yLength = minorAxisLength * Math.sin(angle); double distance = Math.sqrt(xLength * xLength + yLength * yLength); LatLon p = LatLon.greatCircleEndPosition(center, azimuth, distance / radius); positions.add(p); } return new SurfacePolygon(positions, interiorColor, borderColor, textureSize); } }