/* * GeoTools - The Open Source Java GIS Toolkit * http://geotools.org * * (C) 2006 - 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.io.File; import java.io.FileInputStream; import java.io.FilenameFilter; import java.io.IOException; import java.io.Serializable; import java.net.MalformedURLException; import java.net.URL; import java.util.HashMap; import java.util.Map; import java.util.Properties; import java.util.logging.Level; import java.util.logging.Logger; import javax.media.jai.Interpolation; import org.apache.commons.io.FilenameUtils; import org.apache.commons.io.IOUtils; import org.apache.commons.io.filefilter.FileFilterUtils; import org.geotools.coverage.grid.io.AbstractGridFormat; import org.geotools.coverage.grid.io.imageio.GeoToolsWriteParams; import org.geotools.data.DataAccessFactory.Param; import org.geotools.data.DataStore; import org.geotools.data.DataStoreFactorySpi; import org.geotools.data.DataUtilities; import org.geotools.data.shapefile.ShapefileDataStore; import org.geotools.data.simple.SimpleFeatureSource; import org.geotools.factory.Hints; import org.geotools.gce.imagemosaic.catalog.CatalogConfigurationBean; import org.geotools.gce.imagemosaic.catalog.GranuleCatalog; import org.geotools.parameter.DefaultParameterDescriptor; import org.geotools.parameter.DefaultParameterDescriptorGroup; import org.geotools.parameter.ParameterGroup; import org.geotools.util.Converters; import org.geotools.util.Utilities; import org.opengis.coverage.grid.Format; import org.opengis.coverage.grid.GridCoverageWriter; import org.opengis.feature.simple.SimpleFeatureType; import org.opengis.filter.Filter; import org.opengis.parameter.GeneralParameterDescriptor; import org.opengis.parameter.ParameterDescriptor; import org.opengis.referencing.crs.CoordinateReferenceSystem; /** * {@link AbstractGridFormat} subclass for controlling {@link ImageMosaicReader} creation. As the name says, it handles mosaic of georeferenced * images, which means * <ol> * <li>tiff+tfw+prj</li> * <li>jpeg+tfw+prj</li> * <li>png+tfw+prj</li> * <li>geotiff</li> * </ol> * This does not mean that you throw there a couple of images and it will do the trick no matter how these images are. Requirements are: * <ul> * <li>(almost) equal spatial resolution</li> * <li>same number of bands</li> * <li>same data type</li> * <li>same projection</li> * </ul> * The first requirement can be relaxed a little but if they have the same spatial resolution the performances are much better. There are parameters * that you can use to control the behaviour of the mosaic in terms of thresholding and transparency. They are as follows: * <ul> * <li>--DefaultParameterDescriptor FINAL_ALPHA = new DefaultParameterDescriptor( "FinalAlpha", Boolean.class, null, Boolean.FALSE)-- It asks the * plugin to add transparency on the final created mosaic. IT simply performs a threshonding looking for areas where there is no data, i.e., intensity * is really low and transform them into transparent areas. It is obvious that depending on the nature of the input images it might interfere with the * original values.</li> * <li>---ALPHA_THRESHOLD = new DefaultParameterDescriptor( "AlphaThreshold", Double.class, null, new Double(1));--- Controls the transparency * addition by specifying the treshold to use.</li> * <li>INPUT_IMAGE_THRESHOLD = new DefaultParameterDescriptor( "InputImageROI", Boolean.class, null, Boolean.FALSE)--- INPUT_IMAGE_THRESHOLD_VALUE = * new DefaultParameterDescriptor( "InputImageROIThreshold", Integer.class, null, new Integer(1));--- These two can be used to control the application * of ROIs on the input images based on tresholding values. Basically using the threshold you can ask the mosaic plugin to load or not certain pixels * of the original images.</li> * * @author Simone Giannecchini (simboss), GeoSolutions * @author Stefan Alfons Krueger (alfonx), Wikisquare.de : Support for jar:file:foo.jar/bar.properties URLs * @source $URL$ * @since 2.3 */ @SuppressWarnings("rawtypes") public final class ImageMosaicFormat extends AbstractGridFormat implements Format { final static double DEFAULT_ARTIFACTS_FILTER_PTILE_THRESHOLD = 0.1; /** * Logger. */ private final static Logger LOGGER = org.geotools.util.logging.Logging .getLogger(ImageMosaicFormat.class.toString()); /** * Filter tiles based on attributes from the input coverage */ public static final ParameterDescriptor<Filter> FILTER = new DefaultParameterDescriptor<Filter>( "Filter", Filter.class, null, null); /** * Control the type of the final mosaic. */ public static final ParameterDescriptor<Boolean> FADING = new DefaultParameterDescriptor<Boolean>( "Fading", Boolean.class, new Boolean[] { Boolean.TRUE, Boolean.FALSE }, Boolean.FALSE); /** * Control the transparency of the output coverage. */ public static final ParameterDescriptor<Color> OUTPUT_TRANSPARENT_COLOR = new DefaultParameterDescriptor<Color>( "OutputTransparentColor", Color.class, null, null); /** * Control the thresholding on the input coverage */ public static final ParameterDescriptor<Integer> MAX_ALLOWED_TILES = new DefaultParameterDescriptor<Integer>( "MaxAllowedTiles", Integer.class, null, Integer.valueOf(-1)); /** * Control the default artifact filter luminance thresholding on the input coverages */ public static final ParameterDescriptor<Integer> DEFAULT_ARTIFACTS_FILTER_THRESHOLD = new DefaultParameterDescriptor<Integer>( "DefaultArtifactsFilterThreshold", Integer.class, null, Integer.MIN_VALUE); /** * Control the artifact filter ptile thresholding */ public static final ParameterDescriptor<Double> ARTIFACTS_FILTER_PTILE_THRESHOLD = new DefaultParameterDescriptor<Double>( "ArtifactsFilterPtileThreshold", Double.class, null, Double.valueOf(DEFAULT_ARTIFACTS_FILTER_PTILE_THRESHOLD)); /** * Control the threading behavior for this plugin. */ public static final ParameterDescriptor<Boolean> ALLOW_MULTITHREADING = new DefaultParameterDescriptor<Boolean>( "AllowMultithreading", Boolean.class, new Boolean[] { Boolean.TRUE, Boolean.FALSE }, Boolean.FALSE); /** * Control the background values for the output coverage */ public static final ParameterDescriptor<double[]> BACKGROUND_VALUES = new DefaultParameterDescriptor<double[]>( "BackgroundValues", double[].class, null, null); /** * Control the interpolation to be used in mosaicking */ public static final ParameterDescriptor<Interpolation> INTERPOLATION = AbstractGridFormat.INTERPOLATION; /** * Control the requested resolution calculation. */ public static final ParameterDescriptor<Boolean> ACCURATE_RESOLUTION = new DefaultParameterDescriptor<Boolean>( "Accurate resolution computation", Boolean.class, new Boolean[] { Boolean.TRUE, Boolean.FALSE }, Boolean.FALSE); /** * Optional Sorting for the granules of the mosaic. * <p> * <p> * It does work only with DBMS as indexes */ public static final ParameterDescriptor<String> SORT_BY = new DefaultParameterDescriptor<String>( "SORTING", String.class, null, null); /** * Merging behavior for the various granules of the mosaic we are going to produce. * <p> * <p> * This parameter controls whether we want to merge in a single mosaic or stack all the bands into the final mosaic. */ public static final ParameterDescriptor<String> MERGE_BEHAVIOR = new DefaultParameterDescriptor<String>( "MergeBehavior", String.class, MergeBehavior.valuesAsStrings(), MergeBehavior.getDefault().toString()); /** * Controls the removal of excess granules * <p> * <p> * This parameter controls whether the mosaic will attempt to remove excess granules, that is, granules not contributing * pixels to the output, before performing the mosaicking. This is useful only if granules are overlapping, do not * enable otherwise. */ public static final ParameterDescriptor<ExcessGranulePolicy> EXCESS_GRANULE_REMOVAL = new DefaultParameterDescriptor<ExcessGranulePolicy>( "ExcessGranuleRemoval", ExcessGranulePolicy.class, new ExcessGranulePolicy[] { ExcessGranulePolicy.NONE, ExcessGranulePolicy.ROI }, ExcessGranulePolicy.NONE); /** * Creates an instance and sets the metadata. */ public ImageMosaicFormat() { setInfo(); } /** * Sets the metadata information. */ private void setInfo() { final HashMap<String, String> info = new HashMap<String, String>(); info.put("name", "ImageMosaic"); info.put("description", "Image mosaicking plugin"); info.put("vendor", "Geotools"); info.put("docURL", ""); info.put("version", "1.0"); mInfo = info; // reading parameters readParameters = new ParameterGroup(new DefaultParameterDescriptorGroup(mInfo, new GeneralParameterDescriptor[] { READ_GRIDGEOMETRY2D, INPUT_TRANSPARENT_COLOR, OUTPUT_TRANSPARENT_COLOR, USE_JAI_IMAGEREAD, BACKGROUND_VALUES, SUGGESTED_TILE_SIZE, ALLOW_MULTITHREADING, MAX_ALLOWED_TILES, TIME, ELEVATION, FILTER, ACCURATE_RESOLUTION, SORT_BY, MERGE_BEHAVIOR, FOOTPRINT_BEHAVIOR, OVERVIEW_POLICY, BANDS, EXCESS_GRANULE_REMOVAL })); // reading parameters writeParameters = null; } /** * @see org.geotools.data.coverage.grid.AbstractGridFormat#getReader(Object) */ @Override public ImageMosaicReader getReader(Object source) { return getReader(source, null); } /** * */ @Override public GridCoverageWriter getWriter(Object destination) { throw new UnsupportedOperationException("This plugin does not support writing."); } @Override public boolean accepts(Object source, Hints hints) { Utilities.ensureNonNull("source", source); if (source instanceof ImageMosaicDescriptor) { return checkDescriptor((ImageMosaicDescriptor) source); } else { return checkForUrl(source, hints); } } /** * @see org.geotools.data.coverage.grid.AbstractGridFormat#accepts(Object input) */ @Override public boolean accepts(Object source) { return accepts(source, null); } /** * Checks that the provided {@link ImageMosaicDescriptor} is well formed. * * @param source * @return */ private static boolean checkDescriptor(final ImageMosaicDescriptor source) { // TODO: improve checks final GranuleCatalog catalog = source.getCatalog(); final MosaicConfigurationBean configuration = source.getConfiguration(); if (configuration == null) { if (LOGGER.isLoggable(Level.FINE)) { LOGGER.fine("Mosaic configuration is missing"); } return false; } if (configuration.getLevels() == null) { if (LOGGER.isLoggable(Level.FINE)) { LOGGER.fine("resolution leves is unavailable "); } return false; } if (catalog == null) { if (LOGGER.isLoggable(Level.FINE)) { LOGGER.fine("Granule Catalog is unavailable "); } return false; } return true; } @SuppressWarnings("unchecked") private boolean checkForUrl(Object source, Hints hints) { try { if (hints != null && hints.containsKey(Utils.EXCLUDE_MOSAIC) && ((Boolean) hints.get(Utils.EXCLUDE_MOSAIC) == true)) { return false; } // Minimal check. In case we found the indexer we say that we can deal with that mosaic // An additional getReader may confirm or deny that in case. boolean indexerFound = Utils.minimalIndexCheck(source); if (indexerFound) { return true; } // // 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 ImageMosaicReader reader = getReader(source, hints); if (reader != null) { // TODO: It's inefficient reader.dispose(); return true; } URL sourceURL = Utils.checkSource(source, hints); if (sourceURL == null) { return false; } if (source instanceof File) { File file = (File) source; if (!file.exists()) { return false; // file does not exist } } // // Load tiles informations, especially the bounds, which will be // reused // DataStore tileIndexStore = null; CoordinateReferenceSystem crs = null; boolean shapefile = true; try { final File sourceF = DataUtilities.urlToFile(sourceURL); if (FilenameUtils.getName(sourceF.getAbsolutePath()) .equalsIgnoreCase("datastore.properties")) { shapefile = false; // load spi anche check it // read the properties file final Properties properties = new Properties(); final FileInputStream stream = new FileInputStream(sourceF); try { properties.load(stream); } finally { IOUtils.closeQuietly(stream); } // SPI final String SPIClass = properties.getProperty("SPI"); // create a datastore as instructed final DataStoreFactorySpi spi = (DataStoreFactorySpi) Class.forName(SPIClass) .newInstance(); // 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) { if (LOGGER.isLoggable(Level.FINE)) LOGGER.fine("Required parameter missing: " + p.toString()); return false; } } // H2 workadound if (Utils.isH2Store(spi)) { Utils.fixH2DatabaseLocation(params, DataUtilities.fileToURL(sourceF.getParentFile()).toExternalForm()); } tileIndexStore = spi.createDataStore(params); if (tileIndexStore == null) return false; } else { URL testPropertiesUrl = DataUtilities.changeUrlExt(sourceURL, "properties"); File testFile = DataUtilities.urlToFile(testPropertiesUrl); if (!testFile.exists()) { return false; } ShapefileDataStore store = new ShapefileDataStore(sourceURL); store.setTimeZone(Utils.UTC_TIME_ZONE); tileIndexStore = store; } // // Now look for the properties file and try to parse relevant fields // URL propsUrl = null; if (shapefile) propsUrl = DataUtilities.changeUrlExt(sourceURL, "properties"); else { // // do we have a datastore properties file? It will preempt on the shapefile // final File parent = DataUtilities.urlToFile(sourceURL).getParentFile(); // this can be used to look for properties files that do NOT define a datastore final File[] properties = parent.listFiles((FilenameFilter) FileFilterUtils.and( FileFilterUtils.notFileFilter( FileFilterUtils.nameFileFilter("indexer.properties")), FileFilterUtils.and( FileFilterUtils.notFileFilter( FileFilterUtils.nameFileFilter("datastore.properties")), FileFilterUtils.makeFileOnly( FileFilterUtils.suffixFileFilter(".properties"))))); // do we have a valid datastore + mosaic properties pair? for (File propFile : properties) if (Utils.checkFileReadable(propFile) && Utils .loadMosaicProperties(DataUtilities.fileToURL(propFile)) != null) { propsUrl = DataUtilities.fileToURL(propFile); break; } } // get the properties file final MosaicConfigurationBean configuration = Utils.loadMosaicProperties(propsUrl); if (configuration == null) return false; CatalogConfigurationBean catalogBean = configuration.getCatalogConfigurationBean(); // we need the type name with a DB to pick up the right table // for shapefiles this can be null so taht we select the first and ony one String typeName = catalogBean.getTypeName(); if (typeName == null) { final String[] typeNames = tileIndexStore.getTypeNames(); if (typeNames.length <= 0) return false; typeName = typeNames[0]; } if (typeName == null) return false; // now try to connect to the index SimpleFeatureSource featureSource = null; try { featureSource = tileIndexStore.getFeatureSource(typeName); } catch (Exception e) { featureSource = tileIndexStore.getFeatureSource(typeName.toUpperCase()); } if (featureSource == null) { return false; } final SimpleFeatureType schema = featureSource.getSchema(); if (schema == null) return false; crs = featureSource.getSchema().getGeometryDescriptor() .getCoordinateReferenceSystem(); if (crs == null) return false; // looking for the location attribute final String locationAttributeName = catalogBean.getLocationAttribute(); if (schema.getDescriptor(locationAttributeName) == null && schema.getDescriptor(locationAttributeName.toUpperCase()) == null) return false; return true; } finally { try { if (tileIndexStore != null) tileIndexStore.dispose(); } catch (Throwable e) { if (LOGGER.isLoggable(Level.FINE)) LOGGER.log(Level.FINE, e.getLocalizedMessage(), e); } } } catch (Throwable e) { if (LOGGER.isLoggable(Level.FINE)) LOGGER.log(Level.FINE, e.getLocalizedMessage(), e); return false; } } /** * @see AbstractGridFormat#getReader(Object, Hints) */ @Override public ImageMosaicReader getReader(Object source, Hints hints) { try { if(hints == null) { hints = new Hints(Hints.FORCE_LONGITUDE_FIRST_AXIS_ORDER,Boolean.TRUE); } final ImageMosaicReader reader = new ImageMosaicReader(source, hints); return reader; } catch (MalformedURLException e) { if (LOGGER.isLoggable(Level.WARNING)) LOGGER.log(Level.WARNING, e.getLocalizedMessage(), e); return null; } catch (IOException e) { if (LOGGER.isLoggable(Level.FINE)) LOGGER.log(Level.FINE, e.getLocalizedMessage(), e); return null; } } /** * Throw an exception since this plugin is readonly. * * @return nothing. */ @Override public GeoToolsWriteParams getDefaultImageIOWriteParameters() { throw new UnsupportedOperationException("Unsupported method."); } @Override public GridCoverageWriter getWriter(Object destination, Hints hints) { throw new UnsupportedOperationException("This plugin does not support writing."); } }