/******************************************************************************* * 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.tiled.image.delegate; import gov.nasa.worldwind.WorldWind; import gov.nasa.worldwind.avlist.AVKey; import gov.nasa.worldwind.avlist.AVList; import gov.nasa.worldwind.avlist.AVListImpl; import gov.nasa.worldwind.cache.FileStore; import gov.nasa.worldwind.exception.WWRuntimeException; import gov.nasa.worldwind.formats.dds.DDSCompressor; import gov.nasa.worldwind.formats.dds.DXTCompressionAttributes; import gov.nasa.worldwind.geom.Angle; import gov.nasa.worldwind.geom.LatLon; import gov.nasa.worldwind.geom.Sector; import gov.nasa.worldwind.geom.Vec4; import gov.nasa.worldwind.globes.Globe; import gov.nasa.worldwind.layers.BasicTiledImageLayer; import gov.nasa.worldwind.layers.TextureTile; import gov.nasa.worldwind.ogc.OGCConstants; import gov.nasa.worldwind.render.DrawContext; import gov.nasa.worldwind.retrieve.RetrievalPostProcessor; import gov.nasa.worldwind.retrieve.Retriever; import gov.nasa.worldwind.retrieve.URLRetriever; import gov.nasa.worldwind.util.DataConfigurationUtils; import gov.nasa.worldwind.util.ImageUtil; import gov.nasa.worldwind.util.Level; import gov.nasa.worldwind.util.Logging; import gov.nasa.worldwind.util.Tile; import gov.nasa.worldwind.util.TileKey; import gov.nasa.worldwind.util.WWIO; import gov.nasa.worldwind.util.WWXML; import gov.nasa.worldwind.wms.WMSTiledImageLayer; import java.awt.image.BufferedImage; import java.io.IOException; import java.io.InterruptedIOException; import java.net.MalformedURLException; import java.net.URISyntaxException; import java.net.URL; import java.nio.ByteBuffer; import java.util.ArrayList; import javax.imageio.ImageIO; import javax.media.opengl.GLProfile; import javax.xml.xpath.XPath; import org.w3c.dom.Document; import org.w3c.dom.Element; import au.gov.ga.earthsci.worldwind.common.layers.Bounded; import au.gov.ga.earthsci.worldwind.common.layers.Bounds; import au.gov.ga.earthsci.worldwind.common.layers.delegate.IDelegatorLayer; import au.gov.ga.earthsci.worldwind.common.layers.delegate.IDelegatorTile; import au.gov.ga.earthsci.worldwind.common.layers.delegate.ITileRequesterDelegate; import au.gov.ga.earthsci.worldwind.common.layers.tiled.image.URLTransformerBasicTiledImageLayer; import au.gov.ga.earthsci.worldwind.common.util.AVKeyMore; import au.gov.ga.earthsci.worldwind.common.util.DDSUncompressor; import au.gov.ga.earthsci.worldwind.common.util.XMLUtil; import com.jogamp.opengl.util.texture.TextureData; import com.jogamp.opengl.util.texture.TextureIO; import com.jogamp.opengl.util.texture.awt.AWTTextureIO; /** * TiledImageLayer which uses delegates to perform the various tiled image layer * functions such as downloading, saving, loading, and image transforming. This * allows full customisation of different functions of the layer. * <p> * It also uses the {@link FileLockSharer} to create/share the fileLock object. * This is so that multiple layers can point and write to the same data cache * name and synchronize with each other on the same fileLock object. (Note: this * has not yet been added to Bulk Download facility). * * @author Michael de Hoog (michael.dehoog@ga.gov.au) */ public class DelegatorTiledImageLayer extends URLTransformerBasicTiledImageLayer implements Bounded, IDelegatorLayer<DelegatorTextureTile> { protected final Object fileLock; protected final URL context; protected final ImageDelegateKit delegateKit; protected boolean extractZipEntry = false; protected boolean prerendered; protected Globe currentGlobe; public DelegatorTiledImageLayer(AVList params) { super(params); Object o = params.getValue(AVKeyMore.DELEGATE_KIT); if (o != null && o instanceof ImageDelegateKit) delegateKit = (ImageDelegateKit) o; else delegateKit = new ImageDelegateKit(); o = params.getValue(AVKeyMore.CONTEXT_URL); if (o != null && o instanceof URL) context = (URL) o; else context = null; Boolean b = (Boolean) params.getValue(AVKeyMore.EXTRACT_ZIP_ENTRY); if (b != null) this.setExtractZipEntry(b); //Share the filelock with other layers with the same cache name. This allows //multiple layers to save and load from the same cache location. fileLock = FileLockSharer.getLock(getLevels().getFirstLevel().getCacheName()); } public DelegatorTiledImageLayer(Element domElement, AVList params) { this(getParamsFromDocument(domElement, params)); } protected static AVList getParamsFromDocument(Element domElement, AVList params) { String serviceName = XMLUtil.getText(domElement, "Service/@serviceName"); if (OGCConstants.WMS_SERVICE_NAME.equals(serviceName)) { //if serviceName defines a WMS sources, then use the WMSTiledImageLayer to initialise params params = WMSTIL.wmsGetParamsFromDocument(domElement, params); } else { params = BasicTiledImageLayer.getParamsFromDocument(domElement, params); } //create the delegate kit from the XML element ImageDelegateKit delegateKit = new ImageDelegateKit().createFromXML(domElement, params); params.setValue(AVKeyMore.DELEGATE_KIT, delegateKit); XPath xpath = WWXML.makeXPath(); WWXML.checkAndSetBooleanParam(domElement, params, AVKeyMore.EXTRACT_ZIP_ENTRY, "ExtractZipEntry", xpath); return params; } public boolean isExtractZipEntry() { return extractZipEntry; } public void setExtractZipEntry(boolean extractZipEntry) { this.extractZipEntry = extractZipEntry; } /** * Extension of {@link WMSTiledImageLayer} that provides access to the * wmsGetParamsFromDocument function. * * @author Michael de Hoog */ protected static class WMSTIL extends WMSTiledImageLayer { private WMSTIL() { super(""); } public static AVList wmsGetParamsFromDocument(Element domElement, AVList params) { return WMSTiledImageLayer.wmsGetParamsFromDocument(domElement, params); } } /** * Create a new XML document describing a {@link DelegatorTiledImageLayer} * from an AVList. * * @param params * @return New XML document */ public static Document createDelegatorTiledImageLayerConfigDocument(AVList params) { Document document = BasicTiledImageLayer.createTiledImageLayerConfigDocument(params); Element context = document.getDocumentElement(); createDelegatorTiledImageLayerConfigElements(params, context); return document; } /** * Add XML elements specific to the {@link DelegatorTiledImageLayer} from an * AVList to an XML element. * * @param params * @param context * XML element to add to * @return context */ public static Element createDelegatorTiledImageLayerConfigElements(AVList params, Element context) { Object o = params.getValue(AVKeyMore.DELEGATE_KIT); if (o != null && o instanceof ImageDelegateKit) { ((ImageDelegateKit) o).saveToXML(context); } return context; } @Override public void render(DrawContext dc) { if (dc != null) { currentGlobe = dc.getGlobe(); } super.render(dc); } @Override protected void setBlendingFunction(DrawContext dc) { super.setBlendingFunction(dc); //call the delegate's preRender function here, after the blending function //is set up, so that any delegates can modify the blending if they wish delegateKit.preRender(dc); prerendered = true; } @Override protected void draw(DrawContext dc) { prerendered = false; super.draw(dc); if(prerendered) { delegateKit.postRender(dc); } } @Override public boolean isUseTransparentTextures() { //ensure that setBlendingFunction is called, so that the delegate's preRender //function is called return true; } @Override public Bounds getBounds() { return Bounds.fromSector(getLevels().getSector()); } @Override public boolean isFollowTerrain() { return true; } @Override public boolean isTextureFileExpired(DelegatorTextureTile tile, URL textureURL, FileStore fileStore) { return super.isTextureFileExpired(tile, textureURL, fileStore); } @Override protected void forceTextureLoad(TextureTile tile) { validateTileClass(tile); //pass request to delegate delegateKit.forceTextureLoad((DelegatorTextureTile) tile, this); } @Override protected void requestTexture(DrawContext dc, TextureTile tile) { //Only request textures for tiles that intersect the layer's sector. //This makes perfect sense, and am unsure why the TiledImageLayer doesn't do this. if (!tile.getSector().intersects(getLevels().getSector())) { markResourceAbsent(tile); return; } validateTileClass(tile); currentGlobe = dc.getGlobe(); Vec4 centroid = tile.getCentroidPoint(currentGlobe); Vec4 referencePoint = this.getReferencePoint(dc); if (referencePoint != null) tile.setPriority(centroid.distanceTo3(referencePoint)); //pass request to delegate Runnable task = delegateKit.createRequestTask((DelegatorTextureTile) tile, this); //if returned task is null, the task has already been run by //the immediate delegates, so don't add to queue if (task != null) { this.getRequestQ().add(task); } } protected void validateTileClass(Object tile) { if (!(tile instanceof DelegatorTextureTile)) { throw new IllegalArgumentException("Tile must be a " + DelegatorTextureTile.class.getName()); } } /** * Load a texture from a URL (must be file protocol) and set the tile's * texture data to the loaded texture. Should be called by the * {@link ITileRequesterDelegate}. * * @param tile * Tile to set texture data * @param textureURL * File URL of the texture * @return true if the texture data was loaded successfully */ @Override public boolean loadTexture(DelegatorTextureTile tile, URL textureURL) { //public for delegate access TextureData textureData; synchronized (fileLock) { textureData = readTexture(tile, textureURL); } if (textureData == null) return false; tile.setTextureData(textureData); if (tile.getLevelNumber() != 0 || !isRetainLevelZeroTiles()) { addTileToCache((IDelegatorTile) tile); } return true; } @Override public void addTileToCache(IDelegatorTile tile) { TextureTile.getMemoryCache().add(tile.getTransformedTileKey(), tile); } @Override public TextureData readTexture(DelegatorTextureTile tile, URL url) { try { //if the file is a DDS file, just read it directly (skip all delegate readers/transformers) if (url.toString().toLowerCase().endsWith("dds")) return TextureIO.newTextureData(GLProfile.get(GLProfile.GL2), url, isUseMipMaps(), null); BufferedImage image = readImage(tile, url); if ("image/dds".equalsIgnoreCase(getTextureFormat())) { //if required to compress textures, then compress the image to a DDS image DXTCompressionAttributes attributes = DDSCompressor.getDefaultCompressionAttributes(); attributes.setBuildMipmaps(isUseMipMaps()); ByteBuffer buffer; if (image != null) { buffer = new DDSCompressor().compressImage(image, attributes); } else { buffer = DDSCompressor.compressImageURL(url, attributes); } //return the dds image as TextureData return TextureIO.newTextureData(GLProfile.get(GLProfile.GL2), WWIO.getInputStreamFromByteBuffer(buffer), isUseMipMaps(), null); } //return the image as TextureData return AWTTextureIO.newTextureData(GLProfile.get(GLProfile.GL2), image, isUseMipMaps()); } catch (Exception e) { String msg = Logging.getMessage("layers.TextureLayer.ExceptionAttemptingToReadTextureFile", url); Logging.logger().log(java.util.logging.Level.SEVERE, msg, e); } return null; } /** * Read image from a File URL and return it as a {@link BufferedImage}. * * @param tile * Tile for which to read an image * @param url * File URL to read from * @return Image read from URL * @throws IOException * If image could not be read */ protected BufferedImage readImage(DelegatorTextureTile tile, URL url) throws IOException { //first try to read the image via the ImageReaderDelegates BufferedImage image = delegateKit.readImage(tile, url, currentGlobe); if (image == null) { if (url.toString().toLowerCase().endsWith(".dds")) { ByteBuffer buffer = WWIO.readURLContentToBuffer(url, false); image = DDSUncompressor.readDxt3(buffer); } else { //if that doesn't work, just read it with ImageIO class image = ImageIO.read(url); } if (image == null) { throw new IOException("Could not read image"); } } //perform any transformations on the image image = delegateKit.transformImage(image, tile); //manually do the TRANSPARENCY_COLORS transform, for compatibility with AbstractRetrievalPostProcessor int[] colors = (int[]) this.getValue(AVKey.TRANSPARENCY_COLORS); if (colors != null) image = ImageUtil.mapTransparencyColors(image, colors); return image; } /** * Extension to superclass' DownloadPostProcessor which returns this class' * fileLock instead of the superclass'. * * @author Michael de Hoog */ protected static class DownloadPostProcessor extends BasicTiledImageLayer.DownloadPostProcessor { private final DelegatorTiledImageLayer layer; public DownloadPostProcessor(TextureTile tile, DelegatorTiledImageLayer layer) { super(tile, layer); this.layer = layer; } @Override protected Object getFileLock() { return layer.fileLock; } } @Override protected BufferedImage requestImage(TextureTile tile, String mimeType) throws URISyntaxException, InterruptedIOException, MalformedURLException { validateTileClass(tile); return requestImage((DelegatorTextureTile) tile, mimeType); } protected BufferedImage requestImage(DelegatorTextureTile tile, String mimeType) throws URISyntaxException, InterruptedIOException, MalformedURLException { //ignores mimeType parameter URL url = delegateKit.getLocalTileURL(tile, this, false); if (url != null) { try { return readImage(tile, url); } catch (IOException e) { String msg = Logging.getMessage("layers.TextureLayer.ExceptionAttemptingToReadTextureFile", url); Logging.logger().log(java.util.logging.Level.SEVERE, msg, e); } } return null; } @Override protected void downloadImage(TextureTile tile, String mimeType, int timeout) throws Exception { //ignores mimeType parameter Retriever retriever = createRetriever(tile, null); retriever.setConnectTimeout(10000); retriever.setReadTimeout(timeout); retriever.call(); } /* ********************************************************************************************** * Below here is copied from BasicTiledImageLayer, with some modifications to use the delegates * ********************************************************************************************** */ @Override protected void createTopLevelTiles() { Sector sector = this.getLevels().getSector(); Level level = this.getLevels().getFirstLevel(); Angle dLat = level.getTileDelta().getLatitude(); Angle dLon = level.getTileDelta().getLongitude(); Angle latOrigin = this.getLevels().getTileOrigin().getLatitude(); Angle lonOrigin = this.getLevels().getTileOrigin().getLongitude(); // Determine the row and column offset from the common World Wind global tiling origin. int firstRow = Tile.computeRow(dLat, sector.getMinLatitude(), latOrigin); int firstCol = Tile.computeColumn(dLon, sector.getMinLongitude(), lonOrigin); int lastRow = Tile.computeRow(dLat, sector.getMaxLatitude(), latOrigin); int lastCol = Tile.computeColumn(dLon, sector.getMaxLongitude(), lonOrigin); int nLatTiles = lastRow - firstRow + 1; int nLonTiles = lastCol - firstCol + 1; this.topLevels = new ArrayList<TextureTile>(nLatTiles * nLonTiles); Angle p1 = Tile.computeRowLatitude(firstRow, dLat, latOrigin); for (int row = firstRow; row <= lastRow; row++) { Angle p2; p2 = p1.add(dLat); Angle t1 = Tile.computeColumnLongitude(firstCol, dLon, lonOrigin); for (int col = firstCol; col <= lastCol; col++) { Angle t2; t2 = t1.add(dLon); //MODIFIED this.topLevels.add(delegateKit.createTextureTile(new Sector(p1, p2, t1, t2), level, row, col)); //MODIFIED t1 = t2; } p1 = p2; } } @Override public TextureTile[][] getTilesInSector(Sector sector, int levelNumber) { if (sector == null) { String msg = Logging.getMessage("nullValue.SectorIsNull"); Logging.logger().severe(msg); throw new IllegalArgumentException(msg); } Level targetLevel = this.getLevels().getLastLevel(); if (levelNumber >= 0) { for (int i = levelNumber; i < this.getLevels().getLastLevel().getLevelNumber(); i++) { if (this.getLevels().isLevelEmpty(i)) continue; targetLevel = this.getLevels().getLevel(i); break; } } // Collect all the tiles intersecting the input sector. LatLon delta = targetLevel.getTileDelta(); LatLon origin = this.getLevels().getTileOrigin(); final int nwRow = Tile.computeRow(delta.getLatitude(), sector.getMaxLatitude(), origin.getLatitude()); final int nwCol = Tile.computeColumn(delta.getLongitude(), sector.getMinLongitude(), origin.getLongitude()); final int seRow = Tile.computeRow(delta.getLatitude(), sector.getMinLatitude(), origin.getLatitude()); final int seCol = Tile.computeColumn(delta.getLongitude(), sector.getMaxLongitude(), origin.getLongitude()); int numRows = nwRow - seRow + 1; int numCols = seCol - nwCol + 1; TextureTile[][] sectorTiles = new TextureTile[numRows][numCols]; for (int row = nwRow; row >= seRow; row--) { for (int col = nwCol; col <= seCol; col++) { TileKey key = new TileKey(targetLevel.getLevelNumber(), row, col, targetLevel.getCacheName()); Sector tileSector = this.getLevels().computeSectorForKey(key); //MODIFIED sectorTiles[nwRow - row][col - nwCol] = delegateKit.createTextureTile(tileSector, targetLevel, row, col); //new TextureTile(tileSector, targetLevel, row, col); //MODIFIED } } return sectorTiles; } @Override public void retrieveRemoteTexture(TextureTile tile, BasicTiledImageLayer.DownloadPostProcessor postProcessor) { createAndRunRetriever(tile, postProcessor); } @Override public void retrieveRemoteTexture(DelegatorTextureTile tile, RetrievalPostProcessor postProcessor) { createAndRunRetriever(tile, postProcessor); } protected void createAndRunRetriever(TextureTile tile, RetrievalPostProcessor postProcessor) { Retriever retriever = createRetriever(tile, postProcessor); if (retriever != null) WorldWind.getRetrievalService().runRetriever(retriever, tile.getPriority()); } protected Retriever createRetriever(final TextureTile tile, RetrievalPostProcessor postProcessor) { //copied from BasicTiledImageLayer.downloadTexture(), with the following modifications: // - uses the delegateKit to instanciate the Retriever // - returns the Retriever instead of adding it to the RetrievalService if (!this.isNetworkRetrievalEnabled()) { this.getLevels().markResourceAbsent(tile); return null; } if (!WorldWind.getRetrievalService().isAvailable()) return null; java.net.URL url; try { //MODIFIED validateTileClass(tile); url = delegateKit.getRemoteTileURL((DelegatorTextureTile) tile, null); //MODIFIED if (url == null) return null; if (WorldWind.getNetworkStatus().isHostUnavailable(url)) { this.getLevels().markResourceAbsent(tile); return null; } } catch (java.net.MalformedURLException e) { Logging.logger().log(java.util.logging.Level.SEVERE, Logging.getMessage("layers.TextureLayer.ExceptionCreatingTextureUrl", tile), e); return null; } Retriever retriever; if ("http".equalsIgnoreCase(url.getProtocol()) || "https".equalsIgnoreCase(url.getProtocol())) { if (postProcessor == null) postProcessor = new DownloadPostProcessor(tile, this); //MODIFIED //retriever = new HTTPRetriever(url, postProcessor); retriever = delegateKit.createRetriever(url, postProcessor); //MODIFIED } else { Logging.logger().severe(Logging.getMessage("layers.TextureLayer.UnknownRetrievalProtocol", url.toString())); return null; } // Apply any overridden timeouts. Integer cto = AVListImpl.getIntegerValue(this, AVKey.URL_CONNECT_TIMEOUT); if (cto != null && cto > 0) retriever.setConnectTimeout(cto); Integer cro = AVListImpl.getIntegerValue(this, AVKey.URL_READ_TIMEOUT); if (cro != null && cro > 0) retriever.setReadTimeout(cro); Integer srl = AVListImpl.getIntegerValue(this, AVKey.RETRIEVAL_QUEUE_STALE_REQUEST_LIMIT); if (srl != null && srl > 0) retriever.setStaleRequestLimit(srl); //MODIFIED if (isExtractZipEntry()) { retriever.setValue(URLRetriever.EXTRACT_ZIP_ENTRY, "true"); } //WorldWind.getRetrievalService().runRetriever(retriever, tile.getPriority()); return retriever; //MODIFIED } @Override protected void writeConfigurationParams(FileStore fileStore, AVList params) { // Determine what the configuration file name should be based on the configuration parameters. Assume an XML // configuration document type, and append the XML file suffix. String fileName = DataConfigurationUtils.getDataConfigFilename(params, ".xml"); if (fileName == null) { String message = Logging.getMessage("nullValue.FilePathIsNull"); Logging.logger().severe(message); throw new WWRuntimeException(message); } // Check if this component needs to write a configuration file. This happens outside of the synchronized block // to improve multithreaded performance for the common case: the configuration file already exists, this just // need to check that it's there and return. If the file exists but is expired, do not remove it - this // removes the file inside the synchronized block below. if (!this.needsConfigurationFile(fileStore, fileName, params, false)) return; synchronized (this.fileLock) { // Check again if the component needs to write a configuration file, potentially removing any existing file // which has expired. This additional check is necessary because the file could have been created by // another thread while we were waiting for the lock. if (!this.needsConfigurationFile(fileStore, fileName, params, true)) return; this.doWriteConfigurationParams(fileStore, fileName, params); } } @Override public void unmarkResourceAbsent(DelegatorTextureTile tile) { getLevels().unmarkResourceAbsent(tile); } @Override public void markResourceAbsent(DelegatorTextureTile tile) { getLevels().markResourceAbsent(tile); } public void markResourceAbsent(TextureTile tile) { getLevels().markResourceAbsent(tile); } @Override public URL getContext() { return context; } }