/*
* 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.awt.geom.AffineTransform;
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.resources.coverage.CoverageUtilities;
import org.geotools.util.logging.Logging;
import org.opengis.referencing.datum.PixelInCell;
/**
* 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());
}
mosaics.add(new MosaicInfo(subdir, mosaicFactory.getReader(subdir, hints)));
} 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();
double[] res = mosaics.get(i).getResolutions();
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();
properties.put("Name", 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(" ");
double[] resolutions = mi.getResolutions();
sbLevels.append(resolutions[0]).append(",").append(resolutions[1]).append(" ");
}
properties.put("LevelsDirs", sbDirNames.toString());
properties.put("Levels", sbLevels.toString());
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 propert 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);
}
/**
* 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;
ImageMosaicReader reader;
double[] resolutions;
MosaicInfo(File directory, ImageMosaicReader reader) {
this.directory = directory;
this.reader = reader;
this.resolutions = CoverageUtilities.getResolution((AffineTransform) reader.getOriginalGridToWorld(PixelInCell.CELL_CORNER));
}
double[] getResolutions() {
return resolutions;
}
String getPath() {
return directory.getPath();
}
String getName() {
return directory.getName();
}
GeneralEnvelope getEnvelope() {
return reader.getOriginalEnvelope();
}
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] > other.resolutions[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;
}
}
}
}