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

gov.nasa.worldwind.render.SurfaceImage Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (C) 2012 United States Government as represented by the Administrator of the
 * National Aeronautics and Space Administration.
 * All Rights Reserved.
 */
package gov.nasa.worldwind.render;

import gov.nasa.worldwind.*;
import gov.nasa.worldwind.drag.*;
import gov.nasa.worldwind.geom.*;
import gov.nasa.worldwind.layers.Layer;
import gov.nasa.worldwind.ogc.kml.KMLConstants;
import gov.nasa.worldwind.ogc.kml.gx.GXConstants;
import gov.nasa.worldwind.pick.PickSupport;
import gov.nasa.worldwind.util.Logging;

import com.jogamp.opengl.*;
import javax.xml.stream.*;
import java.awt.*;
import java.awt.image.*;
import java.io.*;
import java.net.URL;
import java.util.*;
import java.util.List;

/**
 * Renders a single image contained in a local file, a remote file, or a BufferedImage.
 * 

* Note: The view input handlers detect the surface image rather than the terrain as the top picked object in {@link * gov.nasa.worldwind.event.SelectEvent}s and will not respond to the user's attempts at navigation when the cursor is * over the image. If this is not the desired behavior, disable picking for the layer containing the surface image. * * @version $Id: SurfaceImage.java 3419 2015-08-28 00:09:50Z dcollins $ */ public class SurfaceImage extends WWObjectImpl implements SurfaceTile, OrderedRenderable, PreRenderable, Movable, Disposable, Exportable, Draggable { // TODO: Handle date-line spanning sectors private Sector sector; private Position referencePosition; private double opacity = 1.0; private boolean pickEnabled = true; protected boolean alwaysOnTop = false; protected PickSupport pickSupport; protected Layer pickLayer; protected Object imageSource; protected WWTexture sourceTexture; protected WWTexture generatedTexture; protected List corners; protected WWTexture previousSourceTexture; protected WWTexture previousGeneratedTexture; protected boolean generatedTextureExpired; protected boolean dragEnabled = true; protected DraggableSupport draggableSupport = null; /** * A list that contains only a reference to this instance. Used as an argument to the surface tile renderer to * prevent its having to create a list every time this surface image is rendered. */ protected List thisList = Arrays.asList(this); /** Create a new surface image with no image source. The image will not be rendered until an image source is set. */ public SurfaceImage() { } /** * Renders a single image tile from a local image source. * * @param imageSource either the file path to a local image or a BufferedImage reference. * @param sector the sector covered by the image. */ public SurfaceImage(Object imageSource, Sector sector) { if (imageSource == null) { String message = Logging.getMessage("nullValue.ImageSource"); Logging.logger().severe(message); throw new IllegalArgumentException(message); } if (sector == null) { String message = Logging.getMessage("nullValue.SectorIsNull"); Logging.logger().severe(message); throw new IllegalArgumentException(message); } this.setImageSource(imageSource, sector); } public SurfaceImage(Object imageSource, Iterable corners) { if (imageSource == null) { String message = Logging.getMessage("nullValue.ImageSource"); Logging.logger().severe(message); throw new IllegalArgumentException(message); } if (corners == null) { String message = Logging.getMessage("nullValue.LocationsListIsNull"); Logging.logger().severe(message); throw new IllegalArgumentException(message); } this.setImageSource(imageSource, corners); } public void dispose() { this.generatedTexture = null; } public void setImageSource(Object imageSource, Sector sector) { this.setImageSource(imageSource, (Iterable) sector); } public void setImageSource(Object imageSource, Iterable corners) { // If the current source texture or generated texture are non-null, keep track of them and remove their textures // from the GPU resource cache on the next frame. This prevents SurfaceImage from leaking memory when the caller // continuously specifies the image source to display an animation. We ignore null textures to avoid clearing // previous a texture that we're already tracking. If the caller specifies a new image source more than once per // frame, this still keeps track of the previous texture. Clearing of the source texture is necessary only for // BufferedImage sources. Other types will clear automatically through the GPU resource cache. Clearing of the // generated texture is necessary because those are FBO textures and therefore require significant memory. if (this.sourceTexture != null && imageSource instanceof BufferedImage) this.previousSourceTexture = this.sourceTexture; if (this.generatedTexture != null) this.previousGeneratedTexture = this.generatedTexture; // Assign the new image source and clear the current source texture. We initialize the source texture during the // next frame. This enables SurfaceImage to retrieve remote images during each frame on a separate thread. this.imageSource = imageSource; this.sourceTexture = null; // Update the surface image's geometry. This also clears the current generated texture (if any). initializeGeometry(corners); } public boolean isPickEnabled() { return this.pickEnabled; } public void setPickEnabled(boolean pickEnabled) { this.pickEnabled = pickEnabled; } /** * Indicates the state of this surface image's always-on-top flag. * * @return true if the always-on-top flag is set, otherwise false. * * @see #setAlwaysOnTop(boolean) */ public boolean isAlwaysOnTop() { return this.alwaysOnTop; } /** * Specifies whether this surface image should appear on top of other image layers and surface shapes in the scene. * If the flag is true, this surface image appears visually above other image layers and surface * shapes. Otherwise, this surface image appears interleaved with other image layers according to its relative order * in the layer list, and appears beneath all surface shapes. The default is false. * * @param alwaysOnTop true if the surface image should appear always on top, otherwise * false. */ public void setAlwaysOnTop(boolean alwaysOnTop) { this.alwaysOnTop = alwaysOnTop; } protected void initializeGeometry(Iterable corners) { this.corners = new ArrayList(4); for (LatLon ll : corners) { this.corners.add(ll); } this.sector = Sector.boundingSector(this.corners); this.referencePosition = new Position(sector.getCentroid(), 0); this.generatedTextureExpired = true; } public Object getImageSource() { return this.imageSource; } public double getOpacity() { return opacity; } public void setOpacity(double opacity) { this.opacity = opacity; } // SurfaceTile interface public Sector getSector() { return this.sector; } public void setCorners(Iterable corners) { if (corners == null) { String message = Logging.getMessage("nullValue.LocationsListIsNull"); Logging.logger().severe(message); throw new IllegalArgumentException(message); } this.initializeGeometry(corners); } public List getCorners() { return new ArrayList(this.corners); } public Extent getExtent(DrawContext dc) { if (dc == null) { String msg = Logging.getMessage("nullValue.DrawContextIsNull"); Logging.logger().severe(msg); throw new IllegalArgumentException(msg); } return Sector.computeBoundingCylinder(dc.getGlobe(), dc.getVerticalExaggeration(), this.getSector()); } public boolean bind(DrawContext dc) { return this.generatedTexture != null && this.generatedTexture.bind(dc); } public void applyInternalTransform(DrawContext dc, boolean textureIdentityActive) { if (this.generatedTexture != null) this.generatedTexture.applyInternalTransform(dc); } // Renderable interface @Override public double getDistanceFromEye() { return 0; // Method required by the ordered surface renderable contract but never used. The return value is meaningless. } public void preRender(DrawContext dc) { if (dc.isOrderedRenderingMode()) return; // preRender is called twice - during layer rendering then again during ordered surface rendering if (this.previousSourceTexture != null) { dc.getTextureCache().remove(this.previousSourceTexture.getImageSource()); this.previousSourceTexture = null; } // Initialize the source texture if the caller specified a new image source. if (this.getImageSource() != null && this.sourceTexture == null) this.initializeSourceTexture(dc); // Exit if the source texture could not be initialized. if (this.sourceTexture == null) return; if (this.generatedTexture == null || this.generatedTextureExpired) { WWTexture gt = this.initializeGeneratedTexture(dc); if (gt != null) { this.generatedTexture = gt; this.generatedTextureExpired = false; if (this.previousGeneratedTexture != null) { dc.getTextureCache().remove(this.previousGeneratedTexture); this.previousGeneratedTexture = null; } } } } public void render(DrawContext dc) { if (dc == null) { String message = Logging.getMessage("nullValue.DrawContextIsNull"); Logging.logger().severe(message); throw new IllegalStateException(message); } if (dc.isPickingMode() && !this.isPickEnabled()) return; if (this.getSector() == null || !this.getSector().intersects(dc.getVisibleSector())) return; if (this.sourceTexture == null && this.generatedTexture == null) return; if (!dc.isOrderedRenderingMode() && this.isAlwaysOnTop()) { this.pickLayer = dc.getCurrentLayer(); dc.addOrderedSurfaceRenderable(this); return; } this.draw(dc); } @Override public void pick(DrawContext dc, Point pickPoint) { // Lazily allocate the pick support property, since it's only used when alwaysOnTop is set to true. if (this.pickSupport == null) this.pickSupport = new PickSupport(); try { this.pickSupport.beginPicking(dc); java.awt.Color color = dc.getUniquePickColor(); dc.getGL().getGL2().glColor3ub((byte) color.getRed(), (byte) color.getGreen(), (byte) color.getBlue()); this.pickSupport.addPickableObject(color.getRGB(), this); this.draw(dc); } finally { this.pickSupport.endPicking(dc); this.pickSupport.resolvePick(dc, pickPoint, this.pickLayer); } } protected void draw(DrawContext dc) { GL2 gl = dc.getGL().getGL2(); // GL initialization checks for GL2 compatibility. try { if (!dc.isPickingMode()) { double opacity = dc.getCurrentLayer() != null ? this.getOpacity() * dc.getCurrentLayer().getOpacity() : this.getOpacity(); if (opacity < 1) { gl.glPushAttrib(GL2.GL_COLOR_BUFFER_BIT | GL2.GL_POLYGON_BIT | GL2.GL_CURRENT_BIT); // Enable blending using white premultiplied by the current opacity. gl.glColor4d(opacity, opacity, opacity, opacity); } else { gl.glPushAttrib(GL2.GL_COLOR_BUFFER_BIT | GL2.GL_POLYGON_BIT); } gl.glEnable(GL.GL_BLEND); gl.glBlendFunc(GL.GL_ONE, GL.GL_ONE_MINUS_SRC_ALPHA); } else { gl.glPushAttrib(GL2.GL_POLYGON_BIT); } gl.glPolygonMode(GL2.GL_FRONT, GL2.GL_FILL); gl.glEnable(GL.GL_CULL_FACE); gl.glCullFace(GL.GL_BACK); dc.getGeographicSurfaceTileRenderer().renderTiles(dc, this.thisList); } finally { gl.glPopAttrib(); } } @SuppressWarnings({"UnusedParameters"}) protected void initializeSourceTexture(DrawContext dc) { this.sourceTexture = new LazilyLoadedTexture(this.getImageSource(), true); } protected WWTexture initializeGeneratedTexture(DrawContext dc) { // If this SurfaceImage's is configured with a sector there's no need to generate a texture; we can // use the source texture to render the SurfaceImage. if (Sector.isSector(this.corners) && this.sector.isSameSector(this.corners)) { if (this.sourceTexture.bind(dc)) { this.generatedTexture = this.sourceTexture; return this.generatedTexture; } else { return null; } } else { FramebufferTexture t = dc.getGLRuntimeCapabilities().isUseFramebufferObject() ? new FBOTexture(this.sourceTexture, this.sector, this.corners) : new FramebufferTexture(this.sourceTexture, this.sector, this.corners); // Bind the texture to cause it to generate its internal texture. t.bind(dc); return t; } } // --- Movable interface --- 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)); } public void moveTo(Position position) { LatLon oldRef = this.getReferencePosition(); if (oldRef == null) return; for (int i = 0; i < this.corners.size(); i++) { LatLon p = this.corners.get(i); double distance = LatLon.greatCircleDistance(oldRef, p).radians; double azimuth = LatLon.greatCircleAzimuth(oldRef, p).radians; LatLon pp = LatLon.greatCircleEndPosition(position, azimuth, distance); this.corners.set(i, pp); } this.setCorners(this.corners); } public Position getReferencePosition() { return this.referencePosition; } @SuppressWarnings({"UnusedDeclaration"}) protected void setReferencePosition(Position referencePosition) { this.referencePosition = referencePosition; } @Override public boolean isDragEnabled() { return this.dragEnabled; } @Override public void setDragEnabled(boolean enabled) { this.dragEnabled = enabled; } @Override public void drag(DragContext dragContext) { if (!this.dragEnabled) return; if (this.draggableSupport == null) this.draggableSupport = new DraggableSupport(this, WorldWind.CLAMP_TO_GROUND); this.doDrag(dragContext); } protected void doDrag(DragContext dragContext) { this.draggableSupport.dragGlobeSizeConstant(dragContext); } public boolean equals(Object o) { if (this == o) return true; if (o == null || this.getClass() != o.getClass()) return false; SurfaceImage that = (SurfaceImage) o; if (this.getSector() == null || that.getSector() == null) return false; if (this.getImageSource() == null) return that.imageSource == null && this.getSector().equals(that.getSector()); return this.getImageSource().equals(that.getImageSource()) && this.getSector().equals(that.getSector()); } public int hashCode() { int result; result = this.getImageSource() != null ? this.getImageSource().hashCode() : 0; result = 31 * result + this.getSector().hashCode(); return result; } /** {@inheritDoc} */ public String isExportFormatSupported(String format) { if (KMLConstants.KML_MIME_TYPE.equalsIgnoreCase(format)) return Exportable.FORMAT_SUPPORTED; else return Exportable.FORMAT_NOT_SUPPORTED; } /** * Export the Surface Image. The {@code output} object will receive the exported data. The type of this object * depends on the export format. The formats and object types supported by this class are: *

*

     * Format                                         Supported output object types
     * ================================================================================
     * KML (application/vnd.google-earth.kml+xml)     java.io.Writer
     *                                                java.io.OutputStream
     *                                                javax.xml.stream.XMLStreamWriter
     * 
* * @param mimeType MIME type of desired export format. * @param output An object that will receive the exported data. The type of this object depends on the export * format (see above). * * @throws java.io.IOException If an exception occurs writing to the output object. */ public void export(String mimeType, Object output) throws IOException { if (mimeType == null) { String message = Logging.getMessage("nullValue.Format"); Logging.logger().severe(message); throw new IllegalArgumentException(message); } if (output == null) { String message = Logging.getMessage("nullValue.OutputBufferIsNull"); Logging.logger().severe(message); throw new IllegalArgumentException(message); } if (KMLConstants.KML_MIME_TYPE.equalsIgnoreCase(mimeType)) { try { exportAsKML(output); } catch (XMLStreamException e) { Logging.logger().throwing(getClass().getName(), "export", e); throw new IOException(e); } } else { String message = Logging.getMessage("Export.UnsupportedFormat", mimeType); Logging.logger().warning(message); throw new UnsupportedOperationException(message); } } /** * Export the surface image to KML as a {@code } element. The {@code output} object will receive the * data. This object must be one of: java.io.Writer java.io.OutputStream javax.xml.stream.XMLStreamWriter * * @param output Object to receive the generated KML. * * @throws XMLStreamException If an exception occurs while writing the KML * @throws IOException if an exception occurs while exporting the data. * @see #export(String, Object) */ protected void exportAsKML(Object output) throws IOException, XMLStreamException { XMLStreamWriter xmlWriter = null; XMLOutputFactory factory = XMLOutputFactory.newInstance(); boolean closeWriterWhenFinished = true; if (output instanceof XMLStreamWriter) { xmlWriter = (XMLStreamWriter) output; closeWriterWhenFinished = false; } else if (output instanceof Writer) { xmlWriter = factory.createXMLStreamWriter((Writer) output); } else if (output instanceof OutputStream) { xmlWriter = factory.createXMLStreamWriter((OutputStream) output); } if (xmlWriter == null) { String message = Logging.getMessage("Export.UnsupportedOutputObject"); Logging.logger().warning(message); throw new IllegalArgumentException(message); } xmlWriter.writeStartElement("GroundOverlay"); // Determine the type of the image source. If it's a string or a URL we can write it out. Otherwise there's // nothing we can do. String imgSourceStr = null; Object imgSource = this.getImageSource(); if (imgSource instanceof String || imgSource instanceof URL) imgSourceStr = imgSource.toString(); if (imgSourceStr != null) { // Write geometry xmlWriter.writeStartElement("Icon"); xmlWriter.writeStartElement("href"); xmlWriter.writeCharacters(imgSourceStr); xmlWriter.writeEndElement(); // href xmlWriter.writeEndElement(); // Icon } else { String message = Logging.getMessage("Export.UnableToExportImageSource", (imgSource != null ? imgSource.getClass().getName() : "null")); Logging.logger().info(message); } xmlWriter.writeStartElement("altitudeMode"); xmlWriter.writeCharacters("clampToGround"); xmlWriter.writeEndElement(); // If the corners of the image are aligned to a sector, we can export the position as a KML LatLonBox. If not, // we'll need to use a gx:LatLonQuad. if (Sector.isSector(this.corners)) { exportKMLLatLonBox(xmlWriter); } else { exportKMLLatLonQuad(xmlWriter); } xmlWriter.writeEndElement(); // GroundOverlay xmlWriter.flush(); if (closeWriterWhenFinished) xmlWriter.close(); } protected void exportKMLLatLonBox(XMLStreamWriter xmlWriter) throws XMLStreamException { xmlWriter.writeStartElement("LatLonBox"); xmlWriter.writeStartElement("north"); xmlWriter.writeCharacters(Double.toString(this.sector.getMaxLatitude().getDegrees())); xmlWriter.writeEndElement(); xmlWriter.writeStartElement("south"); xmlWriter.writeCharacters(Double.toString(this.sector.getMinLatitude().getDegrees())); xmlWriter.writeEndElement(); // south xmlWriter.writeStartElement("east"); xmlWriter.writeCharacters(Double.toString(this.sector.getMinLongitude().getDegrees())); xmlWriter.writeEndElement(); xmlWriter.writeStartElement("west"); xmlWriter.writeCharacters(Double.toString(this.sector.getMaxLongitude().getDegrees())); xmlWriter.writeEndElement(); // west xmlWriter.writeEndElement(); // LatLonBox } protected void exportKMLLatLonQuad(XMLStreamWriter xmlWriter) throws XMLStreamException { xmlWriter.writeStartElement(GXConstants.GX_NAMESPACE, "LatLonQuad"); xmlWriter.writeStartElement("coordinates"); for (LatLon ll : this.corners) { xmlWriter.writeCharacters(Double.toString(ll.getLongitude().getDegrees())); xmlWriter.writeCharacters(","); xmlWriter.writeCharacters(Double.toString(ll.getLatitude().getDegrees())); xmlWriter.writeCharacters(" "); } xmlWriter.writeEndElement(); // coordinates xmlWriter.writeEndElement(); // gx:LatLonQuad } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy