/* * GeoTools - The Open Source Java GIS Toolkit * http://geotools.org * * (C) 2007 - 2016, 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.imagemosaic; import java.awt.Color; import java.awt.Rectangle; import java.awt.RenderingHints; import java.awt.Shape; import java.awt.geom.AffineTransform; import java.awt.geom.PathIterator; import java.awt.geom.Rectangle2D; import java.awt.image.ColorModel; import java.awt.image.ComponentColorModel; import java.awt.image.IndexColorModel; import java.awt.image.RenderedImage; import java.awt.image.SampleModel; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.FilenameFilter; import java.io.IOException; import java.io.InputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.OutputStream; import java.io.Serializable; import java.net.MalformedURLException; import java.net.URL; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.Queue; import java.util.Set; import java.util.TimeZone; import java.util.logging.Level; import java.util.logging.Logger; import javax.imageio.ImageIO; import javax.imageio.ImageReader; import javax.imageio.spi.ImageInputStreamSpi; import javax.imageio.spi.ImageReaderSpi; import javax.imageio.stream.ImageInputStream; import javax.media.jai.BorderExtender; import javax.media.jai.Histogram; import javax.media.jai.ImageLayout; import javax.media.jai.JAI; import javax.media.jai.TileCache; import javax.media.jai.TileScheduler; import javax.media.jai.remote.SerializableRenderedImage; import javax.xml.bind.JAXBContext; import javax.xml.bind.JAXBException; import javax.xml.bind.Unmarshaller; 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.DataAccessFactory.Param; import org.geotools.data.DataStoreFactorySpi; import org.geotools.data.DataUtilities; import org.geotools.data.shapefile.ShapefileDataStoreFactory; import org.geotools.factory.CommonFactoryFinder; import org.geotools.factory.Hints; import org.geotools.factory.Hints.Key; import org.geotools.filter.visitor.DefaultFilterVisitor; import org.geotools.gce.imagemosaic.catalog.CatalogConfigurationBean; import org.geotools.gce.imagemosaic.catalog.index.Indexer; import org.geotools.gce.imagemosaic.catalog.index.IndexerUtils; import org.geotools.gce.imagemosaic.catalog.index.ObjectFactory; import org.geotools.gce.imagemosaic.catalog.index.ParametersType.Parameter; import org.geotools.gce.imagemosaic.catalogbuilder.CatalogBuilderConfiguration; import org.geotools.gce.imagemosaic.granulecollector.ReprojectingSubmosaicProducerFactory; import org.geotools.geometry.jts.ReferencedEnvelope; import org.geotools.image.io.ImageIOExt; import org.geotools.referencing.CRS; import org.geotools.referencing.operation.matrix.XAffineTransform; import org.geotools.resources.coverage.CoverageUtilities; import org.geotools.resources.i18n.ErrorKeys; import org.geotools.resources.i18n.Errors; import org.geotools.util.Converters; import org.geotools.util.Range; import org.geotools.util.Utilities; import org.opengis.feature.simple.SimpleFeatureType; import org.opengis.feature.type.AttributeDescriptor; import org.opengis.filter.FilterFactory2; import org.opengis.filter.spatial.BBOX; import org.opengis.referencing.FactoryException; import org.opengis.referencing.crs.CoordinateReferenceSystem; import com.vividsolutions.jts.geom.Envelope; import com.vividsolutions.jts.geom.Geometry; import it.geosolutions.imageio.pam.PAMDataset; import it.geosolutions.imageio.pam.PAMDataset.PAMRasterBand; import it.geosolutions.imageio.pam.PAMDataset.PAMRasterBand.Metadata; import it.geosolutions.imageio.pam.PAMDataset.PAMRasterBand.Metadata.MDI; import net.sf.ehcache.Cache; import net.sf.ehcache.Element; /** * Sparse utilities for the various mosaic classes. I use them to extract complex code from other places. * * @author Simone Giannecchini, GeoSolutions S.A.S. * @source $URL$ */ public class Utils { public final static FilterFactory2 FF = CommonFactoryFinder.getFilterFactory2(); final private static String DATABASE_KEY = "database"; final private static String MVCC_KEY = "MVCC"; final private static double RESOLUTION_TOLERANCE_FACTOR = 1E-2; public final static Key EXCLUDE_MOSAIC = new Key(Boolean.class); public final static Key CHECK_AUXILIARY_METADATA = new Key(Boolean.class); public final static Key AUXILIARY_FILES_PATH = new Key(String.class); public final static Key AUXILIARY_DATASTORE_PATH = new Key(String.class); public final static Key PARENT_DIR = new Key(String.class); public final static Key MOSAIC_READER = new Key(ImageMosaicReader.class); public final static String RANGE_SPLITTER_CHAR = ";"; private static JAXBContext CONTEXT = null; public final static String PAM_DATASET = "PamDataset"; static final String DEFAULT = "default"; public final static String PROPERTIES_SEPARATOR = ";"; /** * EHCache instance to cache histograms */ private static Cache ehcache; /** * RGB to GRAY coefficients (for Luminance computation) */ public final static double RGB_TO_GRAY_MATRIX[][] = { { 0.114, 0.587, 0.299, 0 } }; /** * Flag indicating whether to compute optimized crop ops (instead of standard mosaicking op) when possible (As an instance when mosaicking a * single granule) */ final static boolean OPTIMIZE_CROP; /** * Logger. */ private final static Logger LOGGER = org.geotools.util.logging.Logging .getLogger(Utils.class.toString()); static { final String prop = System.getProperty("org.geotools.imagemosaic.optimizecrop"); if (prop != null && prop.equalsIgnoreCase("FALSE")) { OPTIMIZE_CROP = false; } else { OPTIMIZE_CROP = true; } try { CONTEXT = JAXBContext.newInstance("org.geotools.gce.imagemosaic.catalog.index"); } catch (JAXBException e) { LOGGER.log(Level.FINER, e.getMessage(), e); } CLEANUP_FILTER = initCleanUpFilter(); MOSAIC_SUPPORT_FILES_FILTER = initMosaicSupportFilesFilter(); } public static class Prop { public final static String LOCATION_ATTRIBUTE = "LocationAttribute"; public final static String ENVELOPE2D = "Envelope2D"; public final static String LEVELS_NUM = "LevelsNum"; public final static String LEVELS = "Levels"; public final static String SUGGESTED_SPI = "SuggestedSPI"; public final static String EXP_RGB = "ExpandToRGB"; public final static String ABSOLUTE_PATH = "AbsolutePath"; public final static String AUXILIARY_FILE = "AuxiliaryFile"; public final static String AUXILIARY_DATASTORE_FILE = "AuxiliaryDatastoreFile"; public final static String NAME = "Name"; public final static String INDEX_NAME = "Name"; public final static String INPUT_COVERAGE_NAME = "InputCoverageName"; public final static String FOOTPRINT_MANAGEMENT = "FootprintManagement"; public final static String HETEROGENEOUS = "Heterogeneous"; public static final String TIME_ATTRIBUTE = "TimeAttribute"; public static final String ELEVATION_ATTRIBUTE = "ElevationAttribute"; public static final String ADDITIONAL_DOMAIN_ATTRIBUTES = "AdditionalDomainAttributes"; public static final String CRS_ATTRIBUTE = "CrsAttribute"; /** * Sets if the target schema should be used to locate granules (default is FALSE)<br/> * {@value TRUE|FALSE} */ public final static String USE_EXISTING_SCHEMA = "UseExistingSchema"; public final static String TYPENAME = "TypeName"; public final static String PATH_TYPE = "PathType"; public final static String PARENT_LOCATION = "ParentLocation"; public final static String ROOT_MOSAIC_DIR = "RootMosaicDirectory"; public final static String INDEXING_DIRECTORIES = "IndexingDirectories"; public final static String HARVEST_DIRECTORY = "HarvestingDirectory"; public final static String CAN_BE_EMPTY = "CanBeEmpty"; /** * Sets if the reader should look for auxiliary metadata PAM files */ public static final String CHECK_AUXILIARY_METADATA = "CheckAuxiliaryMetadata"; // Indexer Properties specific properties public static final String RECURSIVE = "Recursive"; public static final String WILDCARD = "Wildcard"; public static final String SCHEMA = "Schema"; public static final String RESOLUTION_LEVELS = "ResolutionLevels"; public static final String PROPERTY_COLLECTORS = "PropertyCollectors"; public final static String CACHING = "Caching"; public static final String WRAP_STORE = "WrapStore"; public static final String GRANULE_ACCEPTORS = "GranuleAcceptors"; public static final String GEOMETRY_HANDLER = "GranuleHandler"; public static final String COVERAGE_NAME_COLLECTOR_SPI = "CoverageNameCollectorSPI"; public static final String MOSAIC_CRS = "MosaicCRS"; public static final String HETEROGENEOUS_CRS = "HeterogeneousCRS"; public static final String GRANULE_COLLECTOR_FACTORY = "GranuleCollectorFactory"; } /** * Extracts a bbox from a filter in case there is at least one. * <p> * I am simply looking for the BBOX filter but I am sure we could use other filters as well. I will leave this as a todo for the moment. * * @author Simone Giannecchini, GeoSolutions SAS. * @todo TODO use other spatial filters as well */ public static class BBOXFilterExtractor extends DefaultFilterVisitor { public ReferencedEnvelope getBBox() { return bbox; } private ReferencedEnvelope bbox; @Override public Object visit(BBOX filter, Object data) { final ReferencedEnvelope bbox = ReferencedEnvelope.reference(filter.getBounds()); if (this.bbox != null) { this.bbox = (ReferencedEnvelope) this.bbox.intersection(bbox); } else { this.bbox = bbox; } return super.visit(filter, data); } } /** * Given a source object, allow to retrieve (when possible) the related url, * the related file or the original input source object itself. */ public static class SourceGetter { private File file; private URL url; private Object source; public SourceGetter(Object inputSource) { source = inputSource; // 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) { file = (File) source; url = DataUtilities.fileToURL(file); } else if (source instanceof URL) { url = (URL) source; if (url.getProtocol().equals("file")) { file = DataUtilities.urlToFile(url); } } 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 { url = new URL(tempSource); source = DataUtilities.urlToFile(url); } catch (MalformedURLException e) { url = null; source = null; } } else { url = DataUtilities.fileToURL(tempFile); // so that we can do our magic here below file = tempFile; } } } /** Return the File (if any) of the source object */ public File getFile() { return file; } /** Return the URL (if any) of the source object */ public URL getUrl() { return url; } /** Return the original source object */ public Object getSource() { return source; } } /** * Default wildcard for creating mosaics. */ public static final String DEFAULT_WILCARD = "*.*"; /** * Default path behavior with respect to absolute paths. */ public static final boolean DEFAULT_PATH_BEHAVIOR = false; /** * Default behavior with respect to index caching. */ private static final boolean DEFAULT_CACHING_BEHAVIOR = false; /** * Creates a mosaic for the provided input parameters. * * @param location path to the directory where to gather the elements for the mosaic. * @param indexName name to give to this mosaic * @param wildcard wildcard to use for walking through files. We are using commonsIO for this task * @param absolutePath tells the catalogue builder to use absolute paths. * @param hints hints to control reader instantiations * @return <code>true</code> if everything is right, <code>false</code>if something bad happens, in which case the reason should be logged to the * logger. */ static boolean createMosaic(final String location, final String indexName, final String wildcard, final boolean absolutePath, final Hints hints) { // create a mosaic index builder and set the relevant elements final CatalogBuilderConfiguration configuration = new CatalogBuilderConfiguration(); // check if the indexer.properties is there configuration.setHints(hints);// retain hints as this may contain an instance of an ImageMosaicReader List<Parameter> parameterList = configuration.getIndexer().getParameters().getParameter(); IndexerUtils.setParam(parameterList, Prop.ABSOLUTE_PATH, Boolean.toString(absolutePath)); IndexerUtils.setParam(parameterList, Prop.ROOT_MOSAIC_DIR, location); IndexerUtils.setParam(parameterList, Prop.INDEX_NAME, indexName); IndexerUtils.setParam(parameterList, Prop.WILDCARD, wildcard); IndexerUtils.setParam(parameterList, Prop.INDEXING_DIRECTORIES, location); // create the builder // final ImageMosaicWalker catalogBuilder = new ImageMosaicWalker(configuration); final ImageMosaicEventHandlers eventHandler = new ImageMosaicEventHandlers(); final ImageMosaicConfigHandler catalogHandler = new ImageMosaicConfigHandler(configuration, eventHandler); final ImageMosaicWalker walker; if (catalogHandler.isUseExistingSchema()) { // walks existing granules in the origin store walker = new ImageMosaicDatastoreWalker(catalogHandler, eventHandler); } else { // collects granules from the file system walker = new ImageMosaicDirectoryWalker(catalogHandler, eventHandler); } // this is going to help us with catching exceptions and logging them final Queue<Throwable> exceptions = new LinkedList<Throwable>(); try { final ImageMosaicEventHandlers.ProcessingEventListener listener = new ImageMosaicEventHandlers.ProcessingEventListener() { @Override public void exceptionOccurred(ImageMosaicEventHandlers.ExceptionEvent event) { final Throwable t = event.getException(); exceptions.add(t); if (LOGGER.isLoggable(Level.SEVERE)) { LOGGER.log(Level.SEVERE, t.getLocalizedMessage(), t); } } @Override public void getNotification(ImageMosaicEventHandlers.ProcessingEvent event) { if (LOGGER.isLoggable(Level.FINE)) { LOGGER.fine(event.getMessage()); } } }; eventHandler.addProcessingEventListener(listener); walker.run(); } catch (Throwable e) { LOGGER.log(Level.SEVERE, "Unable to build mosaic", e); return false; } finally { catalogHandler.dispose(); } // check that nothing bad happened if (exceptions.size() > 0) { return false; } return true; } // Make additional filters pluggable private static IOFileFilter initCleanUpFilter() { IOFileFilter filesFilter = FileFilterUtils.or( FileFilterUtils.suffixFileFilter("properties"), FileFilterUtils.suffixFileFilter("shp"), FileFilterUtils.suffixFileFilter("dbf"), FileFilterUtils.suffixFileFilter("sbn"), FileFilterUtils.suffixFileFilter("sbx"), FileFilterUtils.suffixFileFilter("shx"), FileFilterUtils.suffixFileFilter("qix"), FileFilterUtils.suffixFileFilter("lyr"), FileFilterUtils.suffixFileFilter("prj"), FileFilterUtils.suffixFileFilter("ncx"), FileFilterUtils.suffixFileFilter("gbx9"), FileFilterUtils.suffixFileFilter("ncx2"), FileFilterUtils.suffixFileFilter("ncx3"), FileFilterUtils.nameFileFilter("error.txt"), FileFilterUtils.nameFileFilter("_metadata"), FileFilterUtils.suffixFileFilter(Utils.SAMPLE_IMAGE_NAME), FileFilterUtils.suffixFileFilter(Utils.SAMPLE_IMAGE_NAME_LEGACY), FileFilterUtils.nameFileFilter("error.txt.lck"), FileFilterUtils.suffixFileFilter("xml"), FileFilterUtils.suffixFileFilter("db")); return filesFilter; } private static IOFileFilter initMosaicSupportFilesFilter() { IOFileFilter filesFilter = FileFilterUtils.or( FileFilterUtils.suffixFileFilter("properties"), FileFilterUtils.suffixFileFilter("shp"), FileFilterUtils.suffixFileFilter("dbf"), FileFilterUtils.suffixFileFilter("sbn"), FileFilterUtils.suffixFileFilter("sbx"), FileFilterUtils.suffixFileFilter("shx"), FileFilterUtils.suffixFileFilter("qix"), FileFilterUtils.suffixFileFilter("lyr"), FileFilterUtils.suffixFileFilter("prj"), FileFilterUtils.suffixFileFilter(Utils.SAMPLE_IMAGE_NAME), FileFilterUtils.suffixFileFilter(Utils.SAMPLE_IMAGE_NAME_LEGACY), FileFilterUtils.suffixFileFilter("db")); return filesFilter; } public static String getMessageFromException(Exception exception) { if (exception.getLocalizedMessage() != null) return exception.getLocalizedMessage(); else return exception.getMessage(); } static MosaicConfigurationBean loadMosaicProperties(final URL sourceURL) { return loadMosaicProperties(sourceURL, null); } private static MosaicConfigurationBean loadMosaicProperties(final URL sourceURL, final Set<String> ignorePropertiesSet) { if (LOGGER.isLoggable(Level.FINE)) { LOGGER.log(Level.FINE, "Trying to load properties file from URL:" + sourceURL); } // ret value final MosaicConfigurationBean retValue = new MosaicConfigurationBean(); final CatalogConfigurationBean catalogConfigurationBean = new CatalogConfigurationBean(); retValue.setCatalogConfigurationBean(catalogConfigurationBean); final boolean ignoreSome = ignorePropertiesSet != null && !ignorePropertiesSet.isEmpty(); // // load the properties file // URL propsURL = sourceURL; if (!sourceURL.toExternalForm().endsWith(".properties")) { propsURL = DataUtilities.changeUrlExt(sourceURL, "properties"); if (propsURL.getProtocol().equals("file")) { final File sourceFile = DataUtilities.urlToFile(propsURL); if (!sourceFile.exists()) { if (LOGGER.isLoggable(Level.INFO)) { LOGGER.info("properties file doesn't exist"); } return null; } } } final Properties properties = CoverageUtilities.loadPropertiesFromURL(propsURL); if (properties == null) { if (LOGGER.isLoggable(Level.INFO)) LOGGER.info("Unable to load mosaic properties file"); return null; } String[] pairs = null; String pair[] = null; // // imposed bbox is optional // if (!ignoreSome || !ignorePropertiesSet.contains(Prop.ENVELOPE2D)) { String bboxString = properties.getProperty(Prop.ENVELOPE2D, null); if (bboxString != null) { bboxString = bboxString.trim(); try { ReferencedEnvelope bbox = parseEnvelope(bboxString); if (bbox != null) retValue.setEnvelope(bbox); else if (LOGGER.isLoggable(Level.INFO)) LOGGER.info("Cannot parse imposed bbox."); } catch (Exception e) { if (LOGGER.isLoggable(Level.INFO)) LOGGER.log(Level.INFO, "Cannot parse imposed bbox.", e); } } } if (!ignoreSome || !ignorePropertiesSet.contains(Prop.AUXILIARY_FILE)) { retValue.setAuxiliaryFilePath(properties.getProperty(Prop.AUXILIARY_FILE)); } if (!ignoreSome || !ignorePropertiesSet.contains(Prop.AUXILIARY_DATASTORE_FILE)) { retValue.setAuxiliaryDatastorePath( properties.getProperty(Prop.AUXILIARY_DATASTORE_FILE)); } if (!ignoreSome || !ignorePropertiesSet.contains(Prop.CHECK_AUXILIARY_METADATA)) { final boolean checkAuxiliaryMetadata = Boolean .valueOf(properties.getProperty(Prop.CHECK_AUXILIARY_METADATA, "false").trim()); retValue.setCheckAuxiliaryMetadata(checkAuxiliaryMetadata); } // // resolutions levels // if (!ignoreSome || !ignorePropertiesSet.contains(Prop.LEVELS)) { int levelsNumber = Integer .parseInt(properties.getProperty(Prop.LEVELS_NUM, "1").trim()); retValue.setLevelsNum(levelsNumber); if (!properties.containsKey(Prop.LEVELS)) { if (LOGGER.isLoggable(Level.INFO)) LOGGER.info("Required key Levels not found."); return null; } final String levels = properties.getProperty(Prop.LEVELS).trim(); pairs = levels.split(" "); if (pairs.length != levelsNumber) { if (LOGGER.isLoggable(Level.INFO)) LOGGER.info( "Levels number is different from the provided number of levels resoltion."); return null; } final double[][] resolutions = new double[levelsNumber][2]; for (int i = 0; i < levelsNumber; i++) { pair = pairs[i].split(","); if (pair == null || pair.length != 2) { if (LOGGER.isLoggable(Level.INFO)) LOGGER.info( "OverviewLevel number is different from the provided number of levels resoltion."); return null; } resolutions[i][0] = Double.parseDouble(pair[0]); resolutions[i][1] = Double.parseDouble(pair[1]); } retValue.setLevels(resolutions); } // // typename, is mandatory when we don't use shapeiles // if (!ignoreSome || !ignorePropertiesSet.contains(Prop.TYPENAME)) { String typeName = properties.getProperty(Prop.TYPENAME, null); catalogConfigurationBean.setTypeName(typeName); } // // suggested spi is optional // if (!ignoreSome || !ignorePropertiesSet.contains(Prop.SUGGESTED_SPI)) { if (properties.containsKey(Prop.SUGGESTED_SPI)) { final String suggestedSPI = properties.getProperty(Prop.SUGGESTED_SPI).trim(); catalogConfigurationBean.setSuggestedSPI(suggestedSPI); } } // // time attribute is optional // if (properties.containsKey(Prop.TIME_ATTRIBUTE)) { final String timeAttribute = properties.getProperty("TimeAttribute").trim(); retValue.setTimeAttribute(timeAttribute); } // // elevation attribute is optional // if (properties.containsKey(Prop.ELEVATION_ATTRIBUTE)) { final String elevationAttribute = properties.getProperty(Prop.ELEVATION_ATTRIBUTE) .trim(); retValue.setElevationAttribute(elevationAttribute); } // // crs attribute is optional // if (properties.containsKey(Prop.CRS_ATTRIBUTE)) { final String crsAttribute = properties.getProperty(Prop.CRS_ATTRIBUTE).trim(); retValue.setCRSAttribute(crsAttribute); } // // additional domain attribute is optional // if (properties.containsKey(Prop.ADDITIONAL_DOMAIN_ATTRIBUTES)) { final String additionalDomainAttributes = properties .getProperty(Prop.ADDITIONAL_DOMAIN_ATTRIBUTES).trim(); retValue.setAdditionalDomainAttributes(additionalDomainAttributes); } // // caching // if (properties.containsKey(Prop.CACHING)) { String caching = properties.getProperty(Prop.CACHING).trim(); try { catalogConfigurationBean.setCaching(Boolean.valueOf(caching)); } catch (Throwable e) { catalogConfigurationBean .setCaching(Boolean.valueOf(Utils.DEFAULT_CACHING_BEHAVIOR)); } } // // name is not optional // if (!ignoreSome || !ignorePropertiesSet.contains(Prop.NAME)) { if (!properties.containsKey(Prop.NAME)) { if (LOGGER.isLoggable(Level.SEVERE)) LOGGER.severe("Required key Name not found."); return null; } String coverageName = properties.getProperty(Prop.NAME).trim(); retValue.setName(coverageName); } // need a color expansion? // this is a newly added property we have to be ready to the case where // we do not find it. if (!ignoreSome || !ignorePropertiesSet.contains(Prop.EXP_RGB)) { final boolean expandMe = Boolean .valueOf(properties.getProperty(Prop.EXP_RGB, "false").trim()); retValue.setExpandToRGB(expandMe); } if (!ignoreSome || !ignorePropertiesSet.contains(Prop.WRAP_STORE)) { final boolean wrapStore = Boolean .valueOf(properties.getProperty(Prop.WRAP_STORE, "false").trim()); catalogConfigurationBean.setWrapStore(wrapStore); } // // Is heterogeneous granules mosaic // if (!ignoreSome || !ignorePropertiesSet.contains(Prop.HETEROGENEOUS)) { final boolean heterogeneous = Boolean .valueOf(properties.getProperty(Prop.HETEROGENEOUS, "false").trim()); catalogConfigurationBean.setHeterogeneous(heterogeneous); } if (!catalogConfigurationBean.isHeterogeneous() && (!ignoreSome || !ignorePropertiesSet.contains(Prop.HETEROGENEOUS_CRS))) { final boolean heterogeneous = Boolean .valueOf(properties.getProperty(Prop.HETEROGENEOUS_CRS, "false").trim()); catalogConfigurationBean.setHeterogeneous(heterogeneous); } // // Absolute or relative path // if (!ignoreSome || !ignorePropertiesSet.contains(Prop.ABSOLUTE_PATH)) { final boolean absolutePath = Boolean.valueOf(properties .getProperty(Prop.ABSOLUTE_PATH, Boolean.toString(Utils.DEFAULT_PATH_BEHAVIOR)) .trim()); catalogConfigurationBean.setAbsolutePath(absolutePath); } // // Footprint management // if (!ignoreSome || !ignorePropertiesSet.contains(Prop.FOOTPRINT_MANAGEMENT)) { final boolean footprintManagement = Boolean .valueOf(properties.getProperty(Prop.FOOTPRINT_MANAGEMENT, "false").trim()); retValue.setFootprintManagement(footprintManagement); } // // location // if (!ignoreSome || !ignorePropertiesSet.contains(Prop.LOCATION_ATTRIBUTE)) { catalogConfigurationBean.setLocationAttribute(properties .getProperty(Prop.LOCATION_ATTRIBUTE, Utils.DEFAULT_LOCATION_ATTRIBUTE).trim()); } // // CoverageNameCollectorSpi // if (!ignoreSome || !ignorePropertiesSet.contains(Prop.COVERAGE_NAME_COLLECTOR_SPI)) { String coverageNameCollectorSpi = properties.getProperty(Prop.COVERAGE_NAME_COLLECTOR_SPI); if (coverageNameCollectorSpi != null && ((coverageNameCollectorSpi = coverageNameCollectorSpi.trim()) != null)) { retValue.setCoverageNameCollectorSpi(coverageNameCollectorSpi); } } // target CRS if (!ignoreSome || !ignorePropertiesSet.contains(Prop.MOSAIC_CRS)) { String crsCode = properties.getProperty(Prop.MOSAIC_CRS); if (crsCode != null && !crsCode.isEmpty()) { try { retValue.setCrs(decodeSrs(crsCode)); } catch (FactoryException e) { LOGGER.log(Level.FINE, "Unable to decode CRS of mosaic properties file. Configured CRS " + "code was: " + crsCode, e); } } } // Also initialize the indexer here, since it will be needed later on. File mosaicParentFolder = DataUtilities.urlToFile(sourceURL).getParentFile(); Indexer indexer = loadIndexer(mosaicParentFolder); if (indexer != null) { retValue.setIndexer(indexer); String granuleCollectorFactorySPI = IndexerUtils.getParameter( Prop.GRANULE_COLLECTOR_FACTORY, indexer); if (granuleCollectorFactorySPI == null || granuleCollectorFactorySPI.length() <= 0) { boolean isHeterogeneousCRS = Boolean .parseBoolean(IndexerUtils.getParameter(Prop.HETEROGENEOUS_CRS, indexer)); if (isHeterogeneousCRS) { //in this case we know we need the reprojecting collector anyway, let's use it IndexerUtils.setParam(indexer, Prop.GRANULE_COLLECTOR_FACTORY, ReprojectingSubmosaicProducerFactory.class.getName()); } } } // return value return retValue; } private static CoordinateReferenceSystem decodeSrs(String property) throws FactoryException { return CRS.decode(property, true); } private static Indexer loadIndexer(File parentFolder) { Indexer defaultIndexer = IndexerUtils.createDefaultIndexer(); Indexer configuredIndexer = IndexerUtils.initializeIndexer(defaultIndexer.getParameters(), parentFolder); return configuredIndexer; } /** * Parses a bbox in the form of MIX,MINY MAXX,MAXY * * @param bboxString the string to parse the bbox from * @return a {@link ReferencedEnvelope} with the parse bbox or null */ public static ReferencedEnvelope parseEnvelope(final String bboxString) { if (bboxString == null || bboxString.length() == 0) return null; final String[] pairs = bboxString.split(" "); if (pairs != null && pairs.length == 2) { String[] pair1 = pairs[0].split(","); String[] pair2 = pairs[1].split(","); if (pair1 != null && pair1.length == 2 && pair2 != null && pair2.length == 2) return new ReferencedEnvelope(Double.parseDouble(pair1[0]), Double.parseDouble(pair2[0]), Double.parseDouble(pair1[1]), Double.parseDouble(pair2[1]), null); } // something bad happened return null; } public static IOFileFilter excludeFilters(final IOFileFilter inputFilter, IOFileFilter... filters) { IOFileFilter retFilter = inputFilter; for (IOFileFilter filter : filters) { retFilter = FileFilterUtils.and(retFilter, FileFilterUtils.notFileFilter(filter)); } return retFilter; } /** * Look for an {@link ImageReader} instance that is able to read the provided {@link ImageInputStream}, which must be non null. * <p> * <p> * In case no reader is found, <code>null</code> is returned. * * @param inStream an instance of {@link ImageInputStream} for which we need to find a suitable {@link ImageReader}. * @return a suitable instance of {@link ImageReader} or <code>null</code> if one cannot be found. */ static ImageReader getReader(final ImageInputStream inStream) { Utilities.ensureNonNull("inStream", inStream); // get a reader inStream.mark(); final Iterator<ImageReader> readersIt = ImageIO.getImageReaders(inStream); if (!readersIt.hasNext()) { return null; } return readersIt.next(); } /** * Retrieves the dimensions of the {@link RenderedImage} at index <code>imageIndex</code> for the provided {@link ImageReader} and * {@link ImageInputStream}. * <p> * <p> * Notice that none of the input parameters can be <code>null</code> or a {@link NullPointerException} will be thrown. Morevoer the * <code>imageIndex</code> cannot be negative or an {@link IllegalArgumentException} will be thrown. * * @param imageIndex the index of the image to get the dimensions for. * @param inStream the {@link ImageInputStream} to use as an input * @param reader the {@link ImageReader} to decode the image dimensions. * @return a {@link Rectangle} that contains the dimensions for the image at index <code>imageIndex</code> * @throws IOException in case the {@link ImageReader} or the {@link ImageInputStream} fail. */ static Rectangle getDimension(final int imageIndex, final ImageReader reader) throws IOException { Utilities.ensureNonNull("reader", reader); if (imageIndex < 0) throw new IllegalArgumentException( Errors.format(ErrorKeys.INDEX_OUT_OF_BOUNDS_$1, imageIndex)); return new Rectangle(0, 0, reader.getWidth(imageIndex), reader.getHeight(imageIndex)); } /** * Default priority for the underlying {@link Thread}. */ public static final int DEFAULT_PRIORITY = Thread.NORM_PRIORITY; /** * Default location attribute name. */ public static final String DEFAULT_LOCATION_ATTRIBUTE = "location"; public static final String DEFAULT_INDEX_NAME = "index"; /** * Checks that a {@link File} is a real file, exists and is readable. * * @param file the {@link File} instance to check. Must not be null. * @return <code>true</code> in case the file is a real file, exists and is readable; <code>false </code> otherwise. */ public static boolean checkFileReadable(final File file) { if (LOGGER.isLoggable(Level.FINE)) { final String message = getFileInfo(file); LOGGER.fine(message); } if (!file.exists() || !file.canRead() || !file.isFile()) return false; return true; } /** * Creates a human readable message that describe the provided {@link File} object in terms of its properties. * <p> * <p> * Useful for creating meaningful log messages. * * @param file the {@link File} object to create a descriptive message for * @return a {@link String} containing a descriptive message about the provided {@link File}. */ public static String getFileInfo(final File file) { final StringBuilder builder = new StringBuilder(); builder.append("Checking file:").append(FilenameUtils.getFullPath(file.getAbsolutePath())) .append("\n"); builder.append("isHidden:").append(file.isHidden()).append("\n"); builder.append("exists:").append(file.exists()).append("\n"); builder.append("isFile").append(file.isFile()).append("\n"); builder.append("canRead:").append(file.canRead()).append("\n"); builder.append("canWrite").append(file.canWrite()).append("\n"); builder.append("canExecute:").append(file.canExecute()).append("\n"); builder.append("isAbsolute:").append(file.isAbsolute()).append("\n"); builder.append("lastModified:").append(file.lastModified()).append("\n"); builder.append("length:").append(file.length()); final String message = builder.toString(); return message; } /** * @param testingDirectory * @return * @throws IllegalArgumentException * @throws IOException */ public static String checkDirectory(String testingDirectory, boolean writable) throws IllegalArgumentException { File inDir = new File(testingDirectory); boolean failure = !inDir.exists() || !inDir.isDirectory() || inDir.isHidden() || !inDir.canRead(); if (writable) { failure |= !inDir.canWrite(); } if (failure) { String message = "Unable to create the mosaic\n" + "location is:" + testingDirectory + "\n" + "location exists:" + inDir.exists() + "\n" + "location is a directory:" + inDir.isDirectory() + "\n" + "location is writable:" + inDir.canWrite() + "\n" + "location is readable:" + inDir.canRead() + "\n" + "location is hidden:" + inDir.isHidden() + "\n"; LOGGER.severe(message); throw new IllegalArgumentException(message); } try { testingDirectory = inDir.getCanonicalPath(); } catch (IOException e) { throw new IllegalArgumentException(e); } testingDirectory = FilenameUtils.normalize(testingDirectory); if (!testingDirectory.endsWith(File.separator)) testingDirectory = testingDirectory + File.separator; // test to see if things are still good inDir = new File(testingDirectory); failure = !inDir.exists() || !inDir.isDirectory() || inDir.isHidden() || !inDir.canRead(); if (writable) { failure |= !inDir.canWrite(); } if (failure) { String message = "Unable to create the mosaic\n" + "location is:" + testingDirectory + "\n" + "location exists:" + inDir.exists() + "\n" + "location is a directory:" + inDir.isDirectory() + "\n" + "location is writable:" + inDir.canWrite() + "\n" + "location is readable:" + inDir.canRead() + "\n" + "location is hidden:" + inDir.isHidden() + "\n"; LOGGER.severe(message); throw new IllegalArgumentException(message); } return testingDirectory; } static boolean checkURLReadable(URL url) { try { url.openStream().close(); } catch (Exception e) { return false; } return true; } public static final DataStoreFactorySpi SHAPE_SPI = new ShapefileDataStoreFactory(); static final String DIRECT_KAKADU_PLUGIN = "it.geosolutions.imageio.plugins.jp2k.JP2KKakaduImageReader"; public static final boolean DEFAULT_RECURSION_BEHAVIOR = true; /** * @param datastoreProperties * @return * @throws IOException */ public static Map<String, Serializable> createDataStoreParamsFromPropertiesFile( final URL datastoreProperties) throws IOException { // read the properties file Properties properties = CoverageUtilities.loadPropertiesFromURL(datastoreProperties); if (properties == null) return null; // SPI final String SPIClass = properties.getProperty("SPI"); try { // create a datastore as instructed final DataStoreFactorySpi spi = (DataStoreFactorySpi) Class.forName(SPIClass) .newInstance(); return createDataStoreParamsFromPropertiesFile(properties, spi); } catch (Exception e) { final IOException ioe = new IOException(); throw (IOException) ioe.initCause(e); } } /** * Store a sample image from which we can derive the default SM and CM * * @param sampleImageFile where we should store the image * @param defaultSM the {@link SampleModel} for the sample image. * @param defaultCM the {@link ColorModel} for the sample image. * @throws IOException in case something bad occurs during writing. */ public static void storeSampleImage(final File sampleImageFile, final SampleModel defaultSM, final ColorModel defaultCM) throws IOException { SampleImage sampleImage = new SampleImage(defaultSM, defaultCM); // serialize it OutputStream outStream = null; ObjectOutputStream ooStream = null; try { outStream = new BufferedOutputStream(new FileOutputStream(sampleImageFile)); ooStream = new ObjectOutputStream(outStream); ooStream.writeObject(sampleImage); } finally { try { if (ooStream != null) ooStream.close(); } catch (Throwable e) { IOUtils.closeQuietly(ooStream); } try { if (outStream != null) outStream.close(); } catch (Throwable e) { IOUtils.closeQuietly(outStream); } } } /** * Load a sample image from which we can take the sample model and color model to be used to fill holes in responses. * * @param sampleImageFile the path to sample image. * @return a sample image from which we can take the sample model and color model to be used to fill holes in responses. */ public static RenderedImage loadSampleImage(final File sampleImageFile) { // serialize it InputStream inStream = null; ObjectInputStream oiStream = null; try { // do we have the sample image?? if (Utils.checkFileReadable(sampleImageFile)) { inStream = new BufferedInputStream(new FileInputStream(sampleImageFile)); oiStream = new ObjectInputStream(inStream); // load the image Object object = oiStream.readObject(); if(object instanceof SampleImage) { SampleImage si = (SampleImage) object; return si.toBufferedImage(); } else if(object instanceof SerializableRenderedImage) { SerializableRenderedImage sri = (SerializableRenderedImage) object; // SerializableRenderedImage is a finalization thread killer, try to replace // it with SampleImage on disk instead if(sampleImageFile.canWrite()) { try { storeSampleImage(sampleImageFile, sri.getSampleModel(), sri.getColorModel()); } catch(Exception e) { if(LOGGER.isLoggable(Level.WARNING)) { LOGGER.log(Level.WARNING, "Failed to upgrade the sample image to the new storage format", e); } } } // note, disposing the SerializableRenderedImage here is not done on purpose, // as it will hang, timeout and fail, and then on finalize // it will do it again, so there is really no point in doing that return new SampleImage(sri.getSampleModel(), sri.getColorModel()).toBufferedImage(); } else { if(LOGGER.isLoggable(Level.WARNING)) { LOGGER.warning("Unrecognized sample_image content: " + object); } return null; } } else { if (LOGGER.isLoggable(Level.WARNING)) LOGGER.warning("Unable to find sample image for path " + sampleImageFile); return null; } } catch (FileNotFoundException e) { if (LOGGER.isLoggable(Level.WARNING)) LOGGER.log(Level.WARNING, e.getLocalizedMessage(), e); return null; } catch (IOException e) { if (LOGGER.isLoggable(Level.WARNING)) LOGGER.log(Level.WARNING, e.getLocalizedMessage(), e); return null; } catch (ClassNotFoundException e) { if (LOGGER.isLoggable(Level.WARNING)) LOGGER.log(Level.WARNING, e.getLocalizedMessage(), e); return null; } finally { try { if (inStream != null) inStream.close(); } catch (Throwable e) { if (LOGGER.isLoggable(Level.FINE)) LOGGER.log(Level.FINE, e.getLocalizedMessage(), e); } try { if (oiStream != null) oiStream.close(); } catch (Throwable e) { if (LOGGER.isLoggable(Level.FINE)) LOGGER.log(Level.FINE, e.getLocalizedMessage(), e); } } } /** * A transparent color for missing data. */ static final Color TRANSPARENT = new Color(0, 0, 0, 0); // final static Boolean IGNORE_FOOTPRINT = Boolean.getBoolean("org.geotools.footprint.ignore"); public static final boolean DEFAULT_FOOTPRINT_MANAGEMENT = true; public static final boolean DEFAULT_CONFIGURATION_CACHING = false; public static Map<String, Serializable> createDataStoreParamsFromPropertiesFile( Properties properties, DataStoreFactorySpi spi) throws IOException { // get the params final Map<String, Serializable> params = new HashMap<String, Serializable>(); final Param[] paramsInfo = spi.getParametersInfo(); for (Param p : paramsInfo) { // search for this param and set the value if found if (properties.containsKey(p.key)) { params.put(p.key, (Serializable) Converters.convert(properties.getProperty(p.key), p.type)); } else if (p.required && p.sample == null) throw new IOException("Required parameter missing: " + p.toString()); } return params; } public static Map<String, Serializable> filterDataStoreParams(Properties properties, DataStoreFactorySpi spi) throws IOException { // get the params final Map<String, Serializable> params = new HashMap<String, Serializable>(); final Param[] paramsInfo = spi.getParametersInfo(); for (Param p : paramsInfo) { // search for this param and set the value if found if (properties.containsKey(p.key)) { params.put(p.key, (Serializable) Converters.convert(properties.get(p.key), p.type)); } else if (p.required && p.sample == null) throw new IOException("Required parameter missing: " + p.toString()); } return params; } static URL checkSource(Object source, Hints hints) { SourceGetter sourceGetter = new SourceGetter(source); URL sourceURL = sourceGetter.getUrl(); File sourceFile = sourceGetter.getFile(); // // Check source // // // // // at this point we have tried to convert the thing to a File as hard as // we could, let's see what we can do // // // if (sourceFile != null) { if (!sourceFile.isDirectory()) // real file, can only be a shapefile at this stage or a // datastore.properties file sourceURL = DataUtilities.fileToURL(sourceFile); else { // it's a DIRECTORY, let's look for a possible properties files // that we want to load final String locationPath = sourceFile.getAbsolutePath(); final String defaultIndexName = getDefaultIndexName(locationPath); boolean datastoreFound = false; boolean buildMosaic = false; // // do we have a datastore properties file? It will preempt on // the shapefile // TODO: Refactor these checks once we integrate datastore on indexer.xml // File dataStoreProperties = new File(locationPath, "datastore.properties"); // File emptyFile = new File(locationPath,"empty"); // this can be used to look for properties files that do NOT // define a datastore final File[] properties = sourceFile.listFiles((FilenameFilter) FileFilterUtils.and( FileFilterUtils.notFileFilter( FileFilterUtils.nameFileFilter("datastore.properties")), FileFilterUtils .makeFileOnly(FileFilterUtils.suffixFileFilter(".properties")))); // do we have a valid datastore + mosaic properties pair? if (Utils.checkFileReadable(dataStoreProperties)) { // we have a datastore.properties file datastoreFound = true; // check the first valid mosaic properties boolean found = false; for (File propFile : properties) if (Utils.checkFileReadable(propFile)) { // load it if (null != Utils .loadMosaicProperties(DataUtilities.fileToURL(propFile))) { found = true; break; } } // we did not find any good candidate for mosaic.properties // file, this will signal it if (!found) buildMosaic = true; } else { // we did not find any good candidate for mosaic.properties // file, this will signal it buildMosaic = true; datastoreFound = false; } // // now let's try with shapefile and properties couple // File shapeFile = null; if (!datastoreFound) { for (File propFile : properties) { // load properties if (null == Utils.loadMosaicProperties(DataUtilities.fileToURL(propFile))) continue; // look for a couple shapefile, mosaic properties file shapeFile = new File(locationPath, FilenameUtils.getBaseName(propFile.getName()) + ".shp"); if (!Utils.checkFileReadable(shapeFile) && Utils.checkFileReadable(propFile)) buildMosaic = true; else { buildMosaic = false; break; } } } // did we find anything? If no, we try to build a new mosaic if (buildMosaic) { //// // // Creating a new mosaic // //// // try to build a mosaic inside this directory and see what // happens // preliminar checks final File mosaicDirectory = new File(locationPath); if (!mosaicDirectory.exists() || mosaicDirectory.isFile() || !mosaicDirectory.canWrite()) { if (LOGGER.isLoggable(Level.SEVERE)) { LOGGER.log(Level.SEVERE, "Unable to create the mosaic, check the location:\n" + "location is:" + locationPath + "\n" + "location exists:" + mosaicDirectory.exists() + "\n" + "location is a directory:" + mosaicDirectory.isDirectory() + "\n" + "location is writable:" + mosaicDirectory.canWrite() + "\n" + "location is readable:" + mosaicDirectory.canRead() + "\n" + "location is hidden:" + mosaicDirectory.isHidden() + "\n"); } return null; } // actual creation createMosaic(locationPath, defaultIndexName, DEFAULT_WILCARD, DEFAULT_PATH_BEHAVIOR, hints); // check that the mosaic properties file was created final File propertiesFile = new File(locationPath, defaultIndexName + ".properties"); if (!Utils.checkFileReadable(propertiesFile)) { // retrieve a null so that we shows that a problem occurred if (!checkMosaicHasBeenInitialized(locationPath, defaultIndexName)) { sourceURL = null; return sourceURL; } } // check that the shapefile was correctly created in case it // was needed sourceURL = updateSourceURL(sourceURL, datastoreFound, locationPath, defaultIndexName/* , emptyFile */); } else // now set the new source and proceed sourceURL = datastoreFound ? DataUtilities.fileToURL(dataStoreProperties) : DataUtilities.fileToURL(shapeFile); } } else { // SK: We don't set SourceURL to null now, just because it doesn't // point to a file // sourceURL=null; } return sourceURL; } private static String getDefaultIndexName(final String locationPath) { String name = getIndexerProperty(locationPath, Utils.Prop.NAME); if (name != null) { return name; } return FilenameUtils.getName(locationPath); } public static String getIndexerProperty(String locationPath, String propertyName) { if (locationPath == null) { return null; } File file = new File(locationPath); if (file.isDirectory()) { File indexer = new File(file, IndexerUtils.INDEXER_PROPERTIES); if (indexer.exists()) { URL indexerUrl = DataUtilities.fileToURL(indexer); Properties config = CoverageUtilities.loadPropertiesFromURL(indexerUrl); if (config != null && config.get(Utils.Prop.NAME) != null) { return (String) config.get(propertyName); } } indexer = new File(file, IndexerUtils.INDEXER_XML); String name = IndexerUtils.getParameter(Utils.Prop.NAME, indexer); if (name != null) { return name; } } return null; } /** * Look for a proper sourceURL to be returned. * * @param sourceURL * @param datastoreFound * @param locationPath * @param defaultIndexName * @param emptyFile * @return */ private static URL updateSourceURL(URL sourceURL, boolean datastoreFound, String locationPath, String defaultIndexName/* * , File emptyFile */) { if (!datastoreFound) { File shapeFile = new File(locationPath, defaultIndexName + ".shp"); if (!Utils.checkFileReadable(shapeFile)) { // if (!Utils.checkFileReadable(emptyFile)) { sourceURL = null; // } else { // sourceURL = DataUtilities.fileToURL(emptyFile); // } } else { // now set the new source and proceed sourceURL = DataUtilities.fileToURL(shapeFile); } } else { File dataStoreProperties = new File(locationPath, "datastore.properties"); // datastore.properties as the source if (!Utils.checkFileReadable(dataStoreProperties)) { sourceURL = null; } else { sourceURL = DataUtilities.fileToURL(dataStoreProperties); } } return sourceURL; } private static boolean checkMosaicHasBeenInitialized(String locationPath, String defaultIndexName) { File mosaicFile = new File(locationPath, defaultIndexName + ".xml"); if (Utils.checkFileReadable(mosaicFile)) { return true; } mosaicFile = new File(locationPath, defaultIndexName + ".properties"); if (Utils.checkFileReadable(mosaicFile)) { return true; } // Fallback on empty mosaic check on default indexers File indexFile = new File(locationPath, IndexerUtils.INDEXER_XML); if (Utils.checkFileReadable(indexFile)) { String canBeEmpty = IndexerUtils.getParameter(Prop.CAN_BE_EMPTY, indexFile); if (canBeEmpty != null) { if (Boolean.parseBoolean(canBeEmpty)) { return true; } } } indexFile = new File(locationPath, IndexerUtils.INDEXER_PROPERTIES); if (Utils.checkFileReadable(indexFile)) { URL url = DataUtilities.fileToURL(indexFile); final Properties properties = CoverageUtilities.loadPropertiesFromURL(url); if (properties != null) { String canBeEmpty = properties.getProperty(Prop.CAN_BE_EMPTY, null); if (canBeEmpty != null) { if (Boolean.parseBoolean(canBeEmpty)) { return true; } } } } return false; } static final double SAMEBBOX_THRESHOLD_FACTOR = 20; public static final boolean DEFAULT_COLOR_EXPANSION_BEHAVIOR = false; public static final TimeZone UTC_TIME_ZONE = TimeZone.getTimeZone("UTC"); static final String DESCENDING_ORDER_IDENTIFIER = " D"; // SortOrder.DESCENDING.identifier(); static final String ASCENDING_ORDER_IDENTIFIER = " A"; // SortOrder.ASCENDING.identifier(); public static final String SCAN_FOR_TYPENAMES = "TypeNames"; public static final String SAMPLE_IMAGE_NAME_LEGACY = "sample_image"; public static final String SAMPLE_IMAGE_NAME = "sample_image.dat"; public static final String BBOX = "BOUNDINGBOX"; public static final String TIME_DOMAIN = "TIME"; public static final String ELEVATION_DOMAIN = "ELEVATION"; public static final String ADDITIONAL_DOMAIN = "ADDITIONAL"; public static ObjectFactory OBJECT_FACTORY = new ObjectFactory(); static IOFileFilter CLEANUP_FILTER; static IOFileFilter MOSAIC_SUPPORT_FILES_FILTER; /** * Private constructor to initialize the ehCache instance. It can be configured through a Bean. * * @param ehcache */ private Utils(Cache ehcache) { Utils.ehcache = ehcache; } /** * Setup a {@link Histogram} object by deserializing a file representing a serialized Histogram. * * @param file * @return the deserialized histogram. */ public static Histogram getHistogram(final String file) { Utilities.ensureNonNull("file", file); Histogram histogram = null; // Firstly: check if the histogram have been already // deserialized and it is available in cache if (ehcache != null && ehcache.isKeyInCache(file)) { if (ehcache.isElementInMemory(file)) { final Element element = ehcache.get(file); if (element != null) { final Serializable value = element.getValue(); if (value != null && value instanceof Histogram) { histogram = (Histogram) value; return histogram; } } } } // No histogram in cache. Deserializing... if (histogram == null) { FileInputStream fileStream = null; ObjectInputStream objectStream = null; try { fileStream = new FileInputStream(file); objectStream = new ObjectInputStream(fileStream); histogram = (Histogram) objectStream.readObject(); if (ehcache != null) { ehcache.put(new Element(file, histogram)); } } catch (FileNotFoundException e) { if (LOGGER.isLoggable(Level.FINE)) { LOGGER.fine("Unable to parse Histogram:" + e.getLocalizedMessage()); } } catch (IOException e) { if (LOGGER.isLoggable(Level.FINE)) { LOGGER.fine("Unable to parse Histogram:" + e.getLocalizedMessage()); } } catch (ClassNotFoundException e) { if (LOGGER.isLoggable(Level.FINE)) { LOGGER.fine("Unable to parse Histogram:" + e.getLocalizedMessage()); } } finally { if (objectStream != null) { IOUtils.closeQuietly(objectStream); } if (fileStream != null) { IOUtils.closeQuietly(fileStream); } } } return histogram; } /** * Check if the provided granule's footprint covers the same area of the granule's bbox. * * @param granuleFootprint the granule Footprint * @param granuleBBOX the granule bbox * @return {@code true} in case the footprint isn't covering the FULL granule's bbox. */ static boolean areaIsDifferent(final Geometry granuleFootprint, final AffineTransform baseGridToWorld, final ReferencedEnvelope granuleBBOX) { // // // // First preliminar check: // check if the footprint's bbox corners are the same of the granule's bbox // (Using a threshold) // // // final Envelope envelope = granuleFootprint.getEnvelope().getEnvelopeInternal(); double deltaMinX = Math.abs(envelope.getMinX() - granuleBBOX.getMinX()); double deltaMinY = Math.abs(envelope.getMinY() - granuleBBOX.getMinY()); double deltaMaxX = Math.abs(envelope.getMaxX() - granuleBBOX.getMaxX()); double deltaMaxY = Math.abs(envelope.getMaxY() - granuleBBOX.getMaxY()); final double resX = XAffineTransform.getScaleX0(baseGridToWorld); final double resY = XAffineTransform.getScaleY0(baseGridToWorld); final double toleranceX = resX / Utils.SAMEBBOX_THRESHOLD_FACTOR; final double toleranceY = resY / Utils.SAMEBBOX_THRESHOLD_FACTOR; // Taking note of the area of a single cell final double cellArea = resX * resY; if (deltaMinX > toleranceX || deltaMaxX > toleranceX || deltaMinY > toleranceY || deltaMaxY > toleranceY) { // delta exceed tolerance. Area is not the same return true; } // // // // Second check: // Here, the footprint's bbox and the granule's bbox are equal. // However this is not enough: // - suppose the footprint is a diamond // - Create a rectangle by circumscribing the diamond // - If this rectangle match with the granule's bbox, this doesn't imply // that the diamond covers the same area of the bbox. // Therefore, we need to compute the area and compare them. // // // final double footprintArea = granuleFootprint.getArea(); // final double bboxArea = granuleBBOX.getArea(); final double bboxArea = granuleBBOX.getHeight() * granuleBBOX.getWidth(); // If 2 areas are different more than the cellArea, then they are not the same area if (Math.abs(footprintArea - bboxArea) > cellArea) return true; return false; } /** * Checks if the Shape equates to a Rectangle, if it does it performs a conversion, otherwise returns null * * @param shape * @return */ static Rectangle toRectangle(Shape shape) { if (shape instanceof Rectangle) { return (Rectangle) shape; } if (shape == null) { return null; } // check if it's equivalent to a rectangle PathIterator iter = shape.getPathIterator(new AffineTransform()); double[] coords = new double[2]; // not enough points? if (iter.isDone()) { return null; } // get the first and init the data structures iter.next(); int action = iter.currentSegment(coords); if (action != PathIterator.SEG_MOVETO && action != PathIterator.SEG_LINETO) { return null; } double minx = coords[0]; double miny = coords[1]; double maxx = minx; double maxy = miny; double prevx = minx; double prevy = miny; int i = 0; // at most 4 steps, if more it's not a strict rectangle for (; i < 4 && !iter.isDone(); i++) { iter.next(); action = iter.currentSegment(coords); if (action == PathIterator.SEG_CLOSE) { break; } if (action != PathIterator.SEG_LINETO) { return null; } // check orthogonal step (x does not change and y does, or vice versa) double x = coords[0]; double y = coords[1]; if (!(prevx == x && prevy != y) && !(prevx != x && prevy == y)) { return null; } // update mins and maxes if (x < minx) { minx = x; } else if (x > maxx) { maxx = x; } if (y < miny) { miny = y; } else if (y > maxy) { maxy = y; } // keep track of prev step prevx = x; prevy = y; } // if more than 4 other points it's not a standard rectangle iter.next(); if (!iter.isDone() || i != 3) { return null; } // turn it into a rectangle return new Rectangle2D.Double(minx, miny, maxx - minx, maxy - miny).getBounds(); } public static ImageLayout getImageLayoutHint(RenderingHints renderHints) { if (renderHints == null || !renderHints.containsKey(JAI.KEY_IMAGE_LAYOUT)) { return null; } else { return (ImageLayout) renderHints.get(JAI.KEY_IMAGE_LAYOUT); } } public static TileCache getTileCacheHint(RenderingHints renderHints) { if (renderHints == null || !renderHints.containsKey(JAI.KEY_TILE_CACHE)) { return null; } else { return (TileCache) renderHints.get(JAI.KEY_TILE_CACHE); } } public static BorderExtender getBorderExtenderHint(RenderingHints renderHints) { if (renderHints == null || !renderHints.containsKey(JAI.KEY_BORDER_EXTENDER)) { return null; } else { return (BorderExtender) renderHints.get(JAI.KEY_BORDER_EXTENDER); } } public static TileScheduler getTileSchedulerHint(RenderingHints renderHints) { if (renderHints == null || !renderHints.containsKey(JAI.KEY_TILE_SCHEDULER)) { return null; } else { return (TileScheduler) renderHints.get(JAI.KEY_TILE_SCHEDULER); } } /** * Create a Range of numbers from a couple of values. * * @param firstValue * @param secondValue * @return */ public static Range<? extends Number> createRange(Object firstValue, Object secondValue) { Class<? extends Object> targetClass = firstValue.getClass(); Class<? extends Object> target2Class = secondValue.getClass(); if (targetClass != target2Class) { throw new IllegalArgumentException( "The 2 values need to belong to the same class:\n" + "firstClass = " + targetClass.toString() + "; secondClass = " + targetClass.toString()); } if (targetClass == Byte.class) { return new Range<Byte>(Byte.class, (Byte) firstValue, (Byte) secondValue); } else if (targetClass == Short.class) { return new Range<Short>(Short.class, (Short) firstValue, (Short) secondValue); } else if (targetClass == Integer.class) { return new Range<Integer>(Integer.class, (Integer) firstValue, (Integer) secondValue); } else if (targetClass == Long.class) { return new Range<Long>(Long.class, (Long) firstValue, (Long) secondValue); } else if (targetClass == Float.class) { return new Range<Float>(Float.class, (Float) firstValue, (Float) secondValue); } else if (targetClass == Double.class) { return new Range<Double>(Double.class, (Double) firstValue, (Double) secondValue); } else return null; } /** * Simple minimal check which checks whether and indexer file exists * * @param source * @return */ public static boolean minimalIndexCheck(Object source) { File sourceFile = null; URL sourceURL = null; if (source instanceof File) { sourceFile = (File) source; } 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); // so that we can do our magic here below sourceFile = tempFile; } } final File indexerProperties = new File(sourceFile, IndexerUtils.INDEXER_PROPERTIES); if (Utils.checkFileReadable(indexerProperties)) { return true; } final File indexerXML = new File(sourceFile, IndexerUtils.INDEXER_XML); if (Utils.checkFileReadable(indexerXML)) { return true; } return false; } /** * Check whether 2 resolution levels sets are homogeneous (within a tolerance) * * @param numberOfLevels * @param resolutionLevels * @param compareLevels * @return */ public static boolean homogeneousCheck(final int numberOfLevels, double[][] resolutionLevels, double[][] compareLevels) { for (int k = 0; k < numberOfLevels; k++) { if (Math.abs(resolutionLevels[k][0] - compareLevels[k][0]) > RESOLUTION_TOLERANCE_FACTOR * compareLevels[k][0] || Math.abs(resolutionLevels[k][1] - compareLevels[k][1]) > RESOLUTION_TOLERANCE_FACTOR * compareLevels[k][1]) { return false; } } return true; } /** * Unmarshal the file and return and Indexer object. * * @param indexerFile * @return * @throws JAXBException */ public static Indexer unmarshal(File indexerFile) throws JAXBException { Unmarshaller unmarshaller = null; Indexer indexer = null; if (indexerFile != null) { unmarshaller = CONTEXT.createUnmarshaller(); indexer = (Indexer) unmarshaller.unmarshal(indexerFile); } return indexer; } /** * This method checks the {@link ColorModel} of the current image with the one of the first image in order to check if they are compatible or not * in order to perform a mosaic operation. * <p> * <p> * It is worth to point out that we also check if, in case we have two index color model image, we also try to suggest whether or not we should do * a color expansion. * * @param defaultCM * @param defaultPalette * @param actualCM * @return a boolean asking to skip this feature. */ public static boolean checkColorModels(ColorModel defaultCM, byte[][] defaultPalette, ColorModel actualCM) { // check the number of color components final int defNumComponents = defaultCM.getNumColorComponents(); int actualNumComponents = actualCM.getNumColorComponents(); int colorComponentsDifference = Math.abs(defNumComponents - actualNumComponents); if (colorComponentsDifference != 0) { if ((defNumComponents == 1 && defaultCM instanceof ComponentColorModel) || (actualNumComponents == 1 && actualCM instanceof ComponentColorModel)) { // gray expansion can be performed return false; } } else { return false; } // // if we get here this means that the two color models where completely // different, hence skip this feature. // return true; } /* * Checks if the provided factory spi builds a H2 store */ public static boolean isH2Store(DataStoreFactorySpi spi) { String spiName = spi == null ? null : spi.getClass().getName(); return "org.geotools.data.h2.H2DataStoreFactory".equals(spiName) || "org.geotools.data.h2.H2JNDIDataStoreFactory".equals(spiName); } public static void fixH2DatabaseLocation(Map<String, Serializable> params, String parentLocation) throws MalformedURLException { if (params.containsKey(DATABASE_KEY)) { String dbname = (String) params.get(DATABASE_KEY); // H2 database URLs must not be percent-encoded: see GEOT-4262. params.put(DATABASE_KEY, "file:" + (new File(DataUtilities.urlToFile(new URL(parentLocation)), dbname)) .getPath()); } } /** * Checks if the provided factory spi builds a Oracle store */ public static boolean isOracleStore(DataStoreFactorySpi spi) { String spiName = spi == null ? null : spi.getClass().getName(); return "org.geotools.data.oracle.OracleNGOCIDataStoreFactory".equals(spiName) || "org.geotools.data.oracle.OracleNGJNDIDataStoreFactory".equals(spiName) || "org.geotools.data.oracle.OracleNGDataStoreFactory".equals(spiName); } /** * Checks if the provided factory spi builds a Postgis store */ public static boolean isPostgisStore(DataStoreFactorySpi spi) { String spiName = spi == null ? null : spi.getClass().getName(); return "org.geotools.data.postgis.PostgisNGJNDIDataStoreFactory".equals(spiName) || "org.geotools.data.postgis.PostgisNGDataStoreFactory".equals(spiName); } /** * Merge statistics across datasets. * * @param pamDatasets * @return */ public static PAMDataset mergePamDatasets(PAMDataset[] pamDatasets) { PAMDataset merged = pamDatasets[0]; if (pamDatasets.length > 1) { merged = initRasterBands(pamDatasets[0]); if (merged != null) { for (PAMDataset pamDataset : pamDatasets) { updatePamDatasets(pamDataset, merged); } } } return merged; } /** * Merge basic statistics on destination {@link PAMDataset} {@link PAMRasterBand}s need to have same size. No checks are performed here * * @param inputPamDataset * @param outputPamDataset */ private static void updatePamDatasets(PAMDataset inputPamDataset, PAMDataset outputPamDataset) { List<PAMRasterBand> inputRasterBands = inputPamDataset.getPAMRasterBand(); List<PAMRasterBand> outputRasterBands = outputPamDataset.getPAMRasterBand(); for (int i = 0; i < inputRasterBands.size(); i++) { updateRasterBand(inputRasterBands.get(i), outputRasterBands.get(i)); } } /** * Merge basic statistics on {@link PAMRasterBand} by updating min/max Other statistics still need some work. {@link MDI}s need to have same size. * No checks are performed here * * @param inputPamRasterBand * @param outputPamRasterBand */ private static void updateRasterBand(PAMRasterBand inputPamRasterBand, PAMRasterBand outputPamRasterBand) { List<MDI> mdiInputs = inputPamRasterBand.getMetadata().getMDI(); List<MDI> mdiOutputs = outputPamRasterBand.getMetadata().getMDI(); for (int i = 0; i < mdiInputs.size(); i++) { MDI mdiInput = mdiInputs.get(i); MDI mdiOutput = mdiOutputs.get(i); updateMDI(mdiInput, mdiOutput); } } /** * Update min and max for mdiOutput. Other statistics need better management. For the moment we simply returns the min between them * * @param mdiInput * @param mdiOutput */ private static void updateMDI(MDI mdiInput, MDI mdiOutput) { Double current = Double.parseDouble(mdiInput.getValue()); Object value = mdiOutput.getValue(); if (value != null) { Double output = Double.parseDouble((String) value); if (mdiInput.getKey().toUpperCase().endsWith("_MAXIMUM")) { if (current < output) { current = output; } } else { if (output < current) { current = output; } } } mdiOutput.setValue(Double.toString(current)); } /** * Initialize a list of {@link PAMRasterBand}s having same size of the sample {@link PAMDataset} and same metadata names. * * @param merged * @param samplePam * @return */ private static PAMDataset initRasterBands(PAMDataset samplePam) { PAMDataset merged = null; if (samplePam != null) { merged = new PAMDataset(); final List<PAMRasterBand> samplePamRasterBands = samplePam.getPAMRasterBand(); final int numBands = samplePamRasterBands.size(); List<PAMRasterBand> pamRasterBands = merged.getPAMRasterBand(); PAMRasterBand sampleBand = samplePamRasterBands.get(0); List<MDI> sampleMetadata = sampleBand.getMetadata().getMDI(); for (int i = 0; i < numBands; i++) { final PAMRasterBand band = new PAMRasterBand(); final Metadata metadata = new Metadata(); List<MDI> mdiList = metadata.getMDI(); for (MDI mdi : sampleMetadata) { MDI addedMdi = new MDI(); addedMdi.setKey(mdi.getKey()); mdiList.add(addedMdi); } band.setMetadata(metadata); pamRasterBands.add(band); } } return merged; } public static IOFileFilter getCleanupFilter() { return CLEANUP_FILTER; } public static void fixH2MVCCParam(Map<String, Serializable> params) { if (params != null) { // H2 database URLs must not be percent-encoded: see GEOT-4262. params.put(MVCC_KEY, true); } } public static void fixPostgisDBCreationParams(Map<String, Serializable> datastoreParams) { datastoreParams.put("create database", true); } public static ImageReaderSpi getReaderSpiFromStream(ImageReaderSpi suggestedSPI, ImageInputStream inStream) throws IOException { ImageReaderSpi readerSPI = null; // get a reader and try to cache the suggested SPI first inStream.mark(); if (suggestedSPI != null && suggestedSPI.canDecodeInput(inStream)) { readerSPI = suggestedSPI; inStream.reset(); } else { inStream.mark(); ImageReader reader = ImageIOExt.getImageioReader(inStream); if (reader != null) readerSPI = reader.getOriginatingProvider(); inStream.reset(); } return readerSPI; } public static ImageInputStreamSpi getInputStreamSPIFromURL(URL granuleUrl) throws IOException { ImageInputStreamSpi streamSPI = ImageIOExt.getImageInputStreamSPI(granuleUrl, true); if (streamSPI == null) { final File file = DataUtilities.urlToFile(granuleUrl); if (file != null) { if (LOGGER.isLoggable(Level.WARNING)) { LOGGER.log(Level.WARNING, Utils.getFileInfo(file)); } } throw new IllegalArgumentException( "Unable to get an input stream for the provided granule " + granuleUrl.toString()); } return streamSPI; } /** * Extract the palette from an {@link IndexColorModel}. * * @param indexColorModel * @return */ public static byte[][] extractPalette(IndexColorModel indexColorModel) { Utilities.ensureNonNull("indexColorModel", indexColorModel); byte[][] palette = new byte[3][indexColorModel.getMapSize()]; int numBands = indexColorModel.getNumColorComponents(); indexColorModel.getReds(palette[0]); indexColorModel.getGreens(palette[0]); indexColorModel.getBlues(palette[0]); if (numBands == 4) { indexColorModel.getAlphas(palette[0]); } return palette; } /** * Returns true if the type is usable as a mosaic index */ public static boolean isValidMosaicSchema(SimpleFeatureType schema, String locationAttributeName) { // does it have a geometry? if (schema.getGeometryDescriptor() == null) { return false; } // does it have the location property? AttributeDescriptor location = schema.getDescriptor(locationAttributeName); return location != null && CharSequence.class.isAssignableFrom(location.getType().getBinding()); } }