/* * 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.coverage.io.netcdf; import java.io.File; import java.io.IOException; import java.net.URL; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import org.geotools.coverage.grid.io.DimensionDescriptor; import org.geotools.coverage.io.catalog.CoverageSlice; import org.geotools.coverage.io.catalog.CoverageSlicesCatalog; import org.geotools.coverage.io.catalog.CoverageSlicesCatalog.WrappedCoverageSlicesCatalog; 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.filter.SortByImpl; import org.geotools.gce.imagemosaic.RasterManager; import org.geotools.gce.imagemosaic.Utils; 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.filter.sort.SortBy; import org.opengis.filter.sort.SortOrder; /** * {@link FileResourceInfo} implementation for NetCDF. */ class NetCDFFileResourceInfo extends DefaultResourceInfo implements FileResourceInfo { /** * A {@link CloseableIterator} implementation taking care of retrieving {@link FileGroup}s * from a {@link CoverageSlice}'s list. * * Note on files grouping: Each returned FileGroup should contain * a different file. When dealing with multidimensional data using a shared catalog, * 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. */ class WrappedCoverageSlicesToFileGroupIterator extends SimpleCoverageSlicesToFileGroupIterator { public WrappedCoverageSlicesToFileGroupIterator(List<CoverageSlice> slices) { super(slices); file = DataUtilities.urlToFile(sourceURL); location = file.getAbsolutePath(); } private File file; private String location; private CoverageSlice cachedNext = null; @Override public boolean hasNext() { return super.hasNext() || cachedNext != null; } @Override public FileGroup next() { CoverageSlice next = null; // look for cached feature if (cachedNext != null) { next = cachedNext; cachedNext = null; } else { next = slicesIterator.next(); } int groupedFeatures = 0; // resolve the location List<CoverageSlice> relevantSlices = new ArrayList<CoverageSlice>(); relevantSlices.add(next); File file = null; if (sourceURL != null) { file = DataUtilities.urlToFile(sourceURL); if (file != null && file.exists()) { groupedFeatures++; } } if (groupedFeatures == 0) { return null; } while (slicesIterator.hasNext()) { // Group features sharing same location next = slicesIterator.next(); relevantSlices.add(next); String nextLocation = (String) next.getOriginator().getAttribute(CoverageSlice.Attributes.LOCATION); if (location.equalsIgnoreCase(nextLocation)) { groupedFeatures++; } else { cachedNext = next; break; } } // I have to group the features to get the ranges. try { return buildFileGroup(relevantSlices); } catch (IOException e) { throw new RuntimeException("Exception occurred while populating the fileGroup:" , e); } } /** * 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 * @throws IOException */ private FileGroup buildFileGroup(List<CoverageSlice> slices) throws IOException { List<File> supportFiles = null; Map<String, Object> metadataMap = computeSlicesMetadata(slices); //Change this when we start supporting multiple BBOXes within same file metadataMap.put(Utils.BBOX, new ReferencedEnvelope(slices.get(0).getGranuleBBOX())); return new FileGroup(file, supportFiles, metadataMap); } } /** * A {@link CloseableIterator} implementation taking care of retrieving {@link FileGroup}s * from an {@link CoverageSlice}'s list. */ class SimpleCoverageSlicesToFileGroupIterator implements CloseableIterator<FileGroup> { public SimpleCoverageSlicesToFileGroupIterator(List<CoverageSlice> slices) { this.slices = slices; this.slicesIterator = slices.iterator(); } protected List<CoverageSlice> slices = null; protected Iterator<CoverageSlice> slicesIterator; @Override public boolean hasNext() { return slicesIterator.hasNext(); } @Override public void remove() { throw new UnsupportedOperationException("Remove operation isn't supported"); } @Override public FileGroup next() { File file = null; if (sourceURL != null) { file = DataUtilities.urlToFile(sourceURL); if (file == null || !file.exists()) { throw new IllegalArgumentException("Unable to get a FileGroup on top of file: " + file); } } while (slicesIterator.hasNext()) { // scroll all the slices slicesIterator.next(); } // I have to group the features to get the ranges. try { return buildFileGroupOnSlices(file); } catch (IOException e) { throw new RuntimeException("Exception occurred while populating the fileGroup:" , e); } } private FileGroup buildFileGroupOnSlices(File file) throws IOException { List<File> supportFiles = null; Map<String, Object> metadataMap = computeSlicesMetadata(slices); metadataMap.put(Utils.BBOX, new ReferencedEnvelope(reader.getOriginalEnvelope(coverageName))); return new FileGroup(file, supportFiles, metadataMap); } protected Map<String, Object> computeSlicesMetadata(List<CoverageSlice> slices) throws IOException { Map<String, Object> metadataMap = null; List<DimensionDescriptor> dimensionDescriptors = reader.getDimensionDescriptors(coverageName); // extract metadata for the available domains if (dimensionDescriptors != null && !dimensionDescriptors.isEmpty()) { metadataMap = new HashMap<String, Object>(); // scan dimensions for (DimensionDescriptor descriptor : dimensionDescriptors) { String attribute = descriptor.getStartAttribute(); String name = descriptor.getName(); Comparable max = null; Comparable min = null; Comparable val = null; for (CoverageSlice slice : slices) { val = (Comparable) slice.getOriginator().getAttribute(attribute); if (min == null) { min = val; } if (max == null) { max = val; } int minCheck = min.compareTo(val); int maxCheck = max.compareTo(val); min = minCheck < 0 ? min : val; max = maxCheck > 0 ? max : val; } addMetadaElement(name, min, max, metadataMap); } } return metadataMap; } /** * Add a metadata element to the FileGroup metadata map */ protected 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 { // Does nothing... the underlying iterator // is made on top of a List } } private NetCDFReader reader; private String coverageName; /** * The underlying slices catalog. * Needed to retrieve granules location */ CoverageSlicesCatalog slicesCatalog; private URL sourceURL; /** * ImageMosaicFileGroupProvider constructor * @param sourceURL * @param coverageSlicesCatalog * @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 NetCDFFileResourceInfo(NetCDFReader reader, String coverageName, CoverageSlicesCatalog slicesCatalog, URL sourceURL) { this.reader = reader; this.slicesCatalog = slicesCatalog; this.coverageName = coverageName; this.sourceURL = sourceURL; } @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... List<CoverageSlice> fc = null; try { // WrappedCoverageSlicesCatalog share the DB. Therefore I have to deal with // the location attribute boolean sharedCatalog = slicesCatalog instanceof WrappedCoverageSlicesCatalog; Query updatedQuery = (query != null && sharedCatalog) ? query : new Query(); if (sharedCatalog) { final List<SortBy> clauses = new ArrayList<SortBy>(1); clauses.add(new SortByImpl( FeatureUtilities.DEFAULT_FILTER_FACTORY.property(CoverageSlice.Attributes.LOCATION), SortOrder.ASCENDING)); final SortBy[] sb = clauses.toArray(new SortBy[] {}); final boolean isSortBySupported = slicesCatalog.getQueryCapabilities(coverageName).supportsSorting(sb); if (isSortBySupported) { updatedQuery.setSortBy(sb); } } updatedQuery.setTypeName(coverageName); //TODO: Make sure to add different iterator for stores //not supporting sortBy // Get all the features matching the query fc = slicesCatalog.getGranules(updatedQuery); // They are already an in memory list return sharedCatalog ? new WrappedCoverageSlicesToFileGroupIterator(fc) : new SimpleCoverageSlicesToFileGroupIterator(fc); } catch (IOException ioe) { throw new RuntimeException(ioe); } } }