/* * GeoTools - The Open Source Java GIS Toolkit * http://geotools.org * * (C) 2015, 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.io.File; import java.io.IOException; import java.net.URL; import java.util.ArrayList; import java.util.Arrays; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import org.apache.commons.io.FilenameUtils; import org.geotools.coverage.grid.io.DimensionDescriptor; import org.geotools.data.CloseableIterator; import org.geotools.data.DataUtilities; import org.geotools.data.DefaultResourceInfo; import org.geotools.data.FileResourceInfo; import org.geotools.data.Query; import org.geotools.data.simple.SimpleFeatureCollection; import org.geotools.data.simple.SimpleFeatureIterator; import org.geotools.feature.visitor.BoundsVisitor; import org.geotools.feature.visitor.MaxVisitor; import org.geotools.feature.visitor.MinVisitor; import org.geotools.filter.AttributeExpressionImpl; import org.geotools.filter.LikeFilterImpl; import org.geotools.filter.SortByImpl; import org.geotools.gce.imagemosaic.catalog.GranuleCatalog; import org.geotools.geometry.jts.ReferencedEnvelope; import org.geotools.resources.coverage.FeatureUtilities; import org.geotools.util.DateRange; import org.geotools.util.NumberRange; import org.geotools.util.Range; import org.opengis.feature.simple.SimpleFeature; import org.opengis.filter.Filter; import org.opengis.filter.FilterFactory2; import org.opengis.filter.sort.SortBy; import org.opengis.filter.sort.SortOrder; /** * {@link FileResourceInfo} implementation for ImageMosaic. The specific implementation is able to retrieve support files such as, as an instance, prj * and world file for TIFFs. */ public class ImageMosaicFileResourceInfo extends DefaultResourceInfo implements FileResourceInfo { private final static String PRJ = ".PRJ"; private final static String PRJ_ = ".prj"; private final static String TFW = ".TFW"; private final static String TFW_ = ".tfw"; private final static String WLD = ".WLD"; private final static String WLD_ = ".wld"; private final static String JGW = ".JGW"; private final static String JGW_ = ".jgw"; private final static String MSK = ".TIF.MSK"; private final static String MSK_ = ".tif.msk"; private final static String OVR = ".TIF.OVR"; private final static String OVR_ = ".tif.ovr"; /** * A collector of supportFiles. * * It will look for ancillary files having same name but different extension, trying from a set of possible values. * */ static class SupportFilesCollector { private String[] supportingExtensions; public SupportFilesCollector(String[] supportingExtensions) { this.supportingExtensions = supportingExtensions; } /** * Return supportFiles (if found) for the specified file * * @param filePath * @return */ public List<File> getSupportFiles(String filePath) { List<File> supportFiles = null; String parent = FilenameUtils.getFullPath(filePath); String mainName = FilenameUtils.getName(filePath); String baseName = FilenameUtils.removeExtension(mainName); for (String extension : supportingExtensions) { String newFilePath = parent + baseName + extension; File file = new File(newFilePath); if (file.exists()) { if (supportFiles == null) { supportFiles = new ArrayList<File>(); } supportFiles.add(file); } } return supportFiles; } } final static Map<String, SupportFilesCollector> COLLECTORS; static { // Improve that, to be pluggable in future versions COLLECTORS = new HashMap<String, SupportFilesCollector>(); String[] JPG_ = new String[] { WLD_, PRJ_, JGW_, WLD, JGW, PRJ }; String[] GIF_ = new String[] { WLD_, PRJ_, WLD, PRJ }; String[] TIF_ = new String[] { TFW_, PRJ_, WLD_, OVR_, MSK_, TFW, WLD, PRJ, OVR, MSK }; SupportFilesCollector JPG = new SupportFilesCollector(JPG_); SupportFilesCollector TIF = new SupportFilesCollector(TIF_); SupportFilesCollector GIF = new SupportFilesCollector(GIF_); COLLECTORS.put("JPG", JPG); COLLECTORS.put("JPEG", JPG); COLLECTORS.put("GIF", GIF); COLLECTORS.put("TIF", TIF); COLLECTORS.put("TIFF", TIF); } /** * A {@link CloseableIterator} implementation taking care of retrieving {@link FileGroup}s from a {@link SimpleFeatureIterator}. * * Note on files grouping: Each returned FileGroup should contain a different file. When dealing with multidimensional data, multiple features can * contain same file (records will be different in terms of time value, elevation value, and so on. Therefore, we need to aggregate features * related to the same file location by scanning the underlying iterator and caching the next feature which doesn't belong to same file. * * Important note: the featureIterator need to be get using a sortBy clause on location to make sure underlying features come sorted by location. */ class CloseableFileGroupIterator implements CloseableIterator<FileGroup> { public CloseableFileGroupIterator(SimpleFeatureIterator featureIterator) { this.featureIterator = featureIterator; } SimpleFeature cachedNext = null; SimpleFeatureIterator featureIterator; @Override public boolean hasNext() { return featureIterator.hasNext() || cachedNext != null; } @Override public void remove() { throw new UnsupportedOperationException("Remove operation isn't supported"); } @Override public FileGroup next() { SimpleFeature next = null; // look for cached feature if (cachedNext != null) { next = cachedNext; cachedNext = null; } else { next = featureIterator.next(); } // Avoid adding the feature to a collection to reduce memory consumption // we only take note of the firstFeature int groupedFeatures = 0; SimpleFeature firstFeature = null; // resolve the location String granuleLocation = (String) next.getAttribute(locationAttributeName); URL resolved = pathType.resolvePath(parentLocation, granuleLocation); File file = null; if (resolved != null) { file = DataUtilities.urlToFile(resolved); if (file != null && file.exists()) { groupedFeatures++; firstFeature = next; } } if (groupedFeatures == 0) { return null; } while (featureIterator.hasNext()) { // Group features sharing same location next = featureIterator.next(); String nextLocation = (String) next.getAttribute(locationAttributeName); if (granuleLocation.equalsIgnoreCase(nextLocation)) { groupedFeatures++; } else { cachedNext = next; break; } } // I have to group the features to get the ranges. return buildFileGroup(file, groupedFeatures > 1, firstFeature); } /** * Aggregate multipleFeatures related to the same file, on the same {@link FileGroup}. This is usually needed when the underlying coverage * isn't a simple 2D coverage but it has multiple dimensions (as an instance, time, elevation, custom...) * * The method also look for supportFiles. * * @param file * @paran aggregate, whether aggregation queries should be invoked to extract domains * @param firstFeature, sample feature to be used when no aggregation is needed * @return */ private FileGroup buildFileGroup(File file, boolean aggregate, SimpleFeature firstFeature) { // Looking for supportFiles for the current file // As an instance .prj and .tfw for un-georeferenced tifs String filePath = file.getAbsolutePath(); String ext = FilenameUtils.getExtension(filePath).toUpperCase(); List<File> supportFiles = null; if (COLLECTORS.containsKey(ext)) { supportFiles = COLLECTORS.get(ext).getSupportFiles(filePath); } Map<String, Object> metadataMap = computeGroupMetadata(filePath, aggregate, firstFeature); return new FileGroup(file, supportFiles, metadataMap); } /** * Collects features domain to be exposed as metadata * * @param filePath * @param aggregate * @param firstFeature * @return */ private Map<String, Object> computeGroupMetadata(String filePath, boolean aggregate, SimpleFeature firstFeature) { Map<String, Object> metadataMap = null; List<DimensionDescriptor> dimensionDescriptors = rasterManager .getDimensionDescriptors(); // extract metadata for the available domains if (dimensionDescriptors != null && !dimensionDescriptors.isEmpty()) { Filter filter = FF.equals(FF.property("location"), FF.literal(filePath)); metadataMap = new HashMap<String, Object>(); try { // scan dimensions for (DimensionDescriptor descriptor : dimensionDescriptors) { String attribute = descriptor.getStartAttribute(); String name = descriptor.getName(); Comparable max = null; Comparable min = null; if (aggregate) { Query query = new Query(typeName); query.setFilter(filter); query.setPropertyNames(Arrays.asList(attribute)); // Repeat the queries to avoid using a in-Memory // featureCollection // We may consider caching the features in case // the collection size isn't too big final MaxVisitor maxVisitor = new MaxVisitor(attribute); granuleCatalog.computeAggregateFunction(query, maxVisitor); max = maxVisitor.getMax(); MinVisitor minVisitor = new MinVisitor(attribute); granuleCatalog.computeAggregateFunction(query, minVisitor); min = minVisitor.getMin(); } else { max = min = (Comparable) firstFeature.getAttribute(attribute); } addMetadaElement(name, min, max, metadataMap); } addBBOX(aggregate, filter, firstFeature, metadataMap); } catch (IOException e) { throw new RuntimeException( "Exception occurred while parsing the feature domains", e); } } return metadataMap; } /** Add the bbox element to the metadata Map */ private void addBBOX(boolean aggregate, Filter filter, SimpleFeature firstFeature, Map<String, Object> metadataMap) throws IOException { ReferencedEnvelope envelope = null; if (aggregate) { BoundsVisitor boundsVisitor = new BoundsVisitor(); Query query = new Query(typeName); query.setFilter(filter); granuleCatalog.computeAggregateFunction(query, boundsVisitor); envelope = boundsVisitor.getBounds(); } else { envelope = new ReferencedEnvelope(firstFeature.getBounds()); } if (envelope != null) { metadataMap.put(Utils.BBOX, envelope); } } /** * Add a metadata element to the FileGroup metadata map */ private void addMetadaElement(String name, Comparable min, Comparable max, Map<String, Object> metadataMap) { if (Utils.TIME_DOMAIN.equalsIgnoreCase(name) || min instanceof Date) { metadataMap.put(name.toUpperCase(), new DateRange((Date) min, (Date) max)); } else if (Utils.ELEVATION_DOMAIN.equalsIgnoreCase(name) || min instanceof Number) { metadataMap.put(name.toUpperCase(), NumberRange.create(((Number) min).doubleValue(), true, ((Number) max).doubleValue(), true)); } else { metadataMap.put(name, new Range(String.class, (String) min, (String) max)); } } @Override public void close() throws IOException { featureIterator.close(); } } private final static FilterFactory2 FF = FeatureUtilities.DEFAULT_FILTER_FACTORY; /** * parentLocation used to rebuild full file paths in case the imageMosaic is storing granules location on DB with relative paths */ private String parentLocation; /** * location attribute name on DB */ private String locationAttributeName; /** * The RasterManager used to retrieve granule index info such as typeName, PathType, granuleCatalog, dimensions and attributes */ private RasterManager rasterManager; /** * Whether the granules are stored on DB as relative or absolute paths. */ private PathType pathType; /** * typeName to retrieve granules for a specific coverage */ private String typeName; /** * The underlying granules catalog. Needed to retrieve granules location */ private GranuleCatalog granuleCatalog; /** * ImageMosaicFileResourceInfo constructor * * @param rasterManager manager the {@link RasterManager} instance for underlying index info retrieval and management * @param parentLocation the granules parentLocation (relative paths refer to that) * @param locationAttributeName the actual location attribute name */ public ImageMosaicFileResourceInfo(RasterManager rasterManager, String parentLocation, String locationAttributeName) { this.rasterManager = rasterManager; this.granuleCatalog = rasterManager.getGranuleCatalog(); this.typeName = rasterManager.getTypeName(); this.pathType = rasterManager.getPathType(); this.parentLocation = parentLocation; this.locationAttributeName = locationAttributeName; } @Override public CloseableIterator<FileGroup> getFiles(Query query) { // normally the different type names are actually sharing the same files, but we cannot be // sure, a manually setup mosaic could indeed have multiple types with different files in // them... SimpleFeatureCollection fc = null; try { Query updatedQuery = query != null ? query : new Query(); Filter filter = updatedQuery.getFilter(); // TODO: Improve this check since it may contain multiple filters if (!"location".equalsIgnoreCase(locationAttributeName) && filter instanceof LikeFilterImpl) { // Rewrap the filter to update the file search LikeFilterImpl likeFilter = (LikeFilterImpl) filter; AttributeExpressionImpl impl = (AttributeExpressionImpl) likeFilter.getExpression(); String attribute = impl.getPropertyName(); String value = likeFilter.getLiteral(); if ("location".equalsIgnoreCase(attribute)) { // The invoker provided a default "location" attribute. // make sure to remap it to the actual location attribute attribute = locationAttributeName; updatedQuery.setFilter(FF.like(FF.property(attribute), value)); } } final List<SortBy> clauses = new ArrayList<SortBy>(1); clauses.add(new SortByImpl( FeatureUtilities.DEFAULT_FILTER_FACTORY.property(locationAttributeName), SortOrder.ASCENDING)); final SortBy[] sb = clauses.toArray(new SortBy[] {}); final boolean isSortBySupported = granuleCatalog.getQueryCapabilities(typeName) .supportsSorting(sb); if (isSortBySupported) { updatedQuery.setSortBy(sb); } updatedQuery.setTypeName(typeName); // TODO: Make sure to add different iterator for stores // not supporting sortBy (which DB based stores don't support sorting?) // Get all the features matching the query fc = granuleCatalog.getGranules(updatedQuery); return new CloseableFileGroupIterator(fc.features()); } catch (IOException ioe) { throw new RuntimeException(ioe); } } }