/* * GeoTools - The Open Source Java GIS Toolkit * http://geotools.org * * (C) 2006-2008, Open Source Geospatial Foundation (OSGeo) * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; * version 2.1 of the License. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. */ package org.geotools.gce.imagepyramid; import java.io.File; import java.io.FileFilter; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.io.PrintWriter; import java.net.MalformedURLException; import java.net.URL; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Properties; import java.util.logging.Level; import java.util.logging.Logger; import org.apache.commons.io.FilenameUtils; import org.apache.commons.io.IOUtils; import org.apache.commons.io.filefilter.FileFilterUtils; import org.apache.commons.io.filefilter.IOFileFilter; import org.geotools.data.DataUtilities; import org.geotools.factory.Hints; import org.geotools.gce.imagemosaic.ImageMosaicFormat; import org.geotools.gce.imagemosaic.ImageMosaicReader; import org.geotools.geometry.GeneralEnvelope; import org.geotools.util.logging.Logging; /** * Code to build a pyramid from a gdal_retile output * * @author Andrea Aime - GeoSolutions SAS * @author Simone Giannecchini, GeoSolutions SAS * */ class Utils { static final Logger LOGGER = Logging.getLogger(Utils.class); static URL checkSource(Object source, Hints hints) { URL sourceURL = null; File sourceFile = null; // // Check source // // if it is a URL or a String let's try to see if we can get a file to // check if we have to build the index if (source instanceof File) { sourceFile = (File) source; sourceURL = DataUtilities.fileToURL(sourceFile); } else if (source instanceof URL) { sourceURL = (URL) source; if (sourceURL.getProtocol().equals("file")) { sourceFile = DataUtilities.urlToFile(sourceURL); } } else if (source instanceof String) { // is it a File? final String tempSource = (String) source; File tempFile = new File(tempSource); if (!tempFile.exists()) { // is it a URL try { sourceURL = new URL(tempSource); source = DataUtilities.urlToFile(sourceURL); } catch (MalformedURLException e) { sourceURL = null; source = null; } } else { sourceURL = DataUtilities.fileToURL(tempFile); sourceFile = tempFile; } } else { // we really don't know how to convert the thing... give up if (LOGGER.isLoggable(Level.WARNING)) { LOGGER.warning("we really don't know how to convert the thing: " + source != null ? source .toString() : "null"); } return null; } // logging if (LOGGER.isLoggable(Level.FINE)) { if (sourceFile != null) { final String message = fileStatus(sourceFile); LOGGER.fine(message); } } // // Handle cases where the pyramid descriptor file already exists // // can't do anything with it if (sourceFile == null || !sourceFile.exists()) { return sourceURL; } // if it's already a file we don't need to adjust it, will try to open as is if (!sourceFile.isDirectory()) { return sourceURL; } // it's a directory, let's see if it already has a pyramid description file inside File directory = sourceFile; sourceFile = new File(directory, directory.getName() + ".properties"); // logging if (LOGGER.isLoggable(Level.FINE)) { if (sourceFile != null) { final String message = fileStatus(sourceFile); LOGGER.fine(message); } } if (sourceFile.exists()) { return DataUtilities.fileToURL(sourceFile); } // // Try to build the sub-folders mosaics // // if the structure of the directories is gdal_retile like, move the root files in their // own sub directory File zeroLevelDirectory = new File(directory, "0"); IOFileFilter directoryFilter = FileFilterUtils.directoryFileFilter(); File[] numericDirectories = directory.listFiles(new NumericDirectoryFilter()); File[] directories = directory.listFiles((FileFilter) directoryFilter); // do we have at least one numeric? sub-directory? if (numericDirectories.length == 0) { if (LOGGER.isLoggable(Level.INFO)) { LOGGER.info("I was unable to determine a structure similar to the GDAL Retile one for the provided path: " + directory); } return null; } // check the gdal case and move files if necessary if (!zeroLevelDirectory.exists() && numericDirectories.length == directories.length) { LOGGER.log(Level.INFO, "Detected gdal_retile file structure, " + "moving root files to the '0' subdirectory"); if (zeroLevelDirectory.mkdir()) { if (LOGGER.isLoggable(Level.FINE)) { LOGGER.fine("Created '0' subidr, now moving files"); } FileFilter notDirFilter = FileFilterUtils.notFileFilter(directoryFilter); for (File f : directory.listFiles(notDirFilter)) { if (LOGGER.isLoggable(Level.FINE)) { LOGGER.fine("Moving file" + f.getAbsolutePath()); } if (LOGGER.isLoggable(Level.FINEST)) { LOGGER.finest(fileStatus(f)); } if (!f.renameTo(new File(zeroLevelDirectory, f.getName()))) LOGGER.log( Level.WARNING, "Could not move " + f.getAbsolutePath() + " to " + zeroLevelDirectory + " check the permission inside the source directory " + f.getParent() + " and target directory " + zeroLevelDirectory); } directories = directory.listFiles((FileFilter) directoryFilter); } else { if (LOGGER.isLoggable(Level.INFO)) { LOGGER.info("I was unable to create the 0 directory. check the file permission in the parent directory:" + sourceFile.getParent()); } return null; } } // scan each subdirectory and try to build a mosaic in it, accumulate the resulting mosaics List<MosaicInfo> mosaics = new ArrayList<MosaicInfo>(); ImageMosaicFormat mosaicFactory = new ImageMosaicFormat(); for (File subdir : directories) { if (mosaicFactory.accepts(subdir, hints)) { if (LOGGER.isLoggable(Level.FINE)) { LOGGER.fine("Trying to build mosaic for the directory:" + subdir.getAbsolutePath()); } ImageMosaicReader reader = null; try { reader = mosaicFactory.getReader(subdir, hints); String referenceName = checkConsistency(reader); MosaicInfo mosaicInfo = new MosaicInfo(subdir, reader, referenceName); mosaics.add(mosaicInfo); } finally { if (reader != null) { try { reader.dispose(); } catch (Throwable t) { // Does nothing } } } } else { if (LOGGER.isLoggable(Level.INFO)) { LOGGER.info("Unable to build mosaic for the directory:" + subdir.getAbsolutePath()); } } } // do we have at least one level? if (mosaics.size() == 0) { return null; } // sort the mosaics by resolution and check they are actually in ascending resolution order // for both X and Y resolutions Collections.sort(mosaics); for (int i = 1; i < mosaics.size(); i++) { double[] resprev = mosaics.get(i - 1).getResolutions()[0]; double[] res = mosaics.get(i).getResolutions()[0]; if (resprev[1] > res[1]) { LOGGER.log(Level.INFO, "Invalid mosaic, y resolution in " + mosaics.get(i - 1).getPath() + " is greater than the one in " + mosaics.get(i).getPath() + " whilst x resolutions " + "have the opposite relationship"); return null; } } // // We have everything we need, build the final pyramid descriptor info // // build the property file Properties properties = new Properties(); String coverageNames = mosaics.get(0).getCoverageNames(); properties.put("Name", coverageNames != null ? coverageNames : directory.getName()); properties.put("LevelsNum", String.valueOf(mosaics.size())); StringBuilder sbDirNames = new StringBuilder(); StringBuilder sbLevels = new StringBuilder(); for (MosaicInfo mi : mosaics) { sbDirNames.append(mi.getName()).append(" "); appendResolutionLevels(sbLevels, mi.getResolutions()); } properties.put("LevelsDirs", sbDirNames.toString()); properties.put("Levels", sbLevels.toString().trim()); GeneralEnvelope envelope = mosaics.get(0).getEnvelope(); properties.put("Envelope2D", envelope.getMinimum(0) + "," + envelope.getMinimum(1) + " " + envelope.getMaximum(0) + "," + envelope.getMaximum(1)); OutputStream os = null; try { os = new FileOutputStream(sourceFile); properties.store(os, "Automatically generated"); } catch (IOException e) { LOGGER.log(Level.INFO, "We could not generate the pyramid property file " + sourceFile.getPath(), e); return null; } finally { if (os != null) { IOUtils.closeQuietly(os); } } // build the .prj file if possible if (envelope.getCoordinateReferenceSystem() != null) { File prjFile = new File(directory, directory.getName() + ".prj"); PrintWriter pw = null; try { pw = new PrintWriter(new FileOutputStream(prjFile)); pw.print(envelope.getCoordinateReferenceSystem().toString()); } catch (IOException e) { LOGGER.log(Level.INFO, "We could not write out the projection file " + prjFile.getPath(), e); return null; } finally { pw.close(); } } return DataUtilities.fileToURL(sourceFile); } private static void appendResolutionLevels(StringBuilder sbLevels, double[][] resolutions) { final int numResolutions = resolutions.length; for (int i=0; i < numResolutions - 1;i++) { // separate overviews with ";" appendXYResolutions(sbLevels, resolutions[i]); sbLevels.append(";"); } appendXYResolutions(sbLevels, resolutions[numResolutions - 1]); sbLevels.append(" "); } private static void appendXYResolutions(StringBuilder sbLevels, double[] resolutions) { sbLevels.append(resolutions[0]).append(",").append(resolutions[1]); } private static String checkConsistency(ImageMosaicReader reader) { // Current assumption: different coverages stored into a single mosaic have the same: // - levels structure // - bbox // - resolutions // this allows us to use the first coverage of each mosaic as a reference to collect // properties. // Throws an exception if that condition isn't respected. final int count = reader.getGridCoverageCount(); double[][] resolutionLevels = null; String referenceName = ImageMosaicReader.UNSPECIFIED; try { if (count > 1) { for (String coverageName : reader.getGridCoverageNames()) { if (ImageMosaicReader.UNSPECIFIED.equalsIgnoreCase(referenceName)) { referenceName = coverageName; resolutionLevels = reader.getResolutionLevels(coverageName); continue; } double compareLevels[][] = reader.getResolutionLevels(coverageName); boolean homogeneous = org.geotools.gce.imagemosaic.Utils.homogeneousCheck( resolutionLevels.length, resolutionLevels, compareLevels); if (!homogeneous) { // Relax this in the future throw new IllegalArgumentException( "Coverages need to have same levels structure"); } } } } catch (IOException e) { throw new IllegalArgumentException(e); } return referenceName; } /** * Prepares a message with the status of the provided file. * * @param sourceFile The {@link File} to provided the status message for * @return a status message for the provided {@link File} or a {@link NullPointerException} in * case the {@link File}is <code>null</code>. */ private static String fileStatus(File sourceFile) { if (sourceFile == null) { throw new NullPointerException("Provided null input to fileStatus method"); } final StringBuilder builder = new StringBuilder(); builder.append("Checking file: ") .append(FilenameUtils.getFullPath(sourceFile.getAbsolutePath())).append("\n"); builder.append("exists: ").append(sourceFile.exists()).append("\n"); builder.append("isFile: ").append(sourceFile.isFile()).append("\n"); builder.append("canRead: ").append(sourceFile.canRead()).append("\n"); builder.append("canWrite: ").append(sourceFile.canWrite()).append("\n"); builder.append("canExecute: ").append(sourceFile.canExecute()).append("\n"); builder.append("isHidden: ").append(sourceFile.isHidden()).append("\n"); builder.append("lastModified: ").append(sourceFile.lastModified()).append("\n"); return builder.toString(); } /** * Stores informations about a mosaic */ static class MosaicInfo implements Comparable<MosaicInfo> { @Override public String toString() { return "MosaicInfo [directory=" + directory + ", resolutions=" + Arrays.toString(resolutions) + "]"; } File directory; double[][] resolutions; String coverageName; GeneralEnvelope envelope; String coverageNames = null; MosaicInfo(File directory, ImageMosaicReader reader, String coverageName) { this.directory = directory; this.coverageName = coverageName; try { this.envelope = reader.getOriginalEnvelope(coverageName); this.resolutions = reader.getResolutionLevels(coverageName); final int coverageCount = reader.getGridCoverageCount(); if (coverageCount > 1) { String[] coverages = reader.getGridCoverageNames(); StringBuilder sb = new StringBuilder(); for (int i = 0; i < coverageCount - 1; i++) { sb.append(coverages[i]).append(","); } sb.append(coverages[coverageCount - 1]); coverageNames = sb.toString(); } } catch (IOException ioe) { throw new IllegalArgumentException(ioe); } } double[][] getResolutions() { return resolutions; } String getCoverageNames() { return coverageNames; } String getPath() { return directory.getPath(); } String getName() { return directory.getName(); } GeneralEnvelope getEnvelope() { return envelope; } public int compareTo(MosaicInfo other) { // we make an easy comparison against the x resolution, we'll do a sanity // check about the y resolution later return resolutions[0][0] > other.resolutions[0][0] ? 1 : -1; } } /** * A file filter that only returns directories whose name is an integer number * * @author Andrea Aime - OpenGeo */ static class NumericDirectoryFilter implements FileFilter { public boolean accept(File pathname) { if (!pathname.isDirectory()) return false; try { Integer.parseInt(pathname.getName()); return true; } catch (NumberFormatException e) { return false; } } } }