/* * GeoTools - The Open Source Java GIS Toolkit * http://geotools.org * * (C) 2002-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.imageio.netcdf; import org.geotools.coverage.Category; import org.geotools.coverage.GridSampleDimension; import org.geotools.coverage.grid.GridEnvelope2D; import org.geotools.coverage.grid.GridGeometry2D; import org.geotools.coverage.grid.io.DefaultDimensionDescriptor; import org.geotools.coverage.grid.io.DimensionDescriptor; import org.geotools.coverage.io.CoverageSource.*; import org.geotools.coverage.io.CoverageSourceDescriptor; import org.geotools.coverage.io.RasterLayout; import org.geotools.coverage.io.catalog.CoverageSlice; import org.geotools.coverage.io.catalog.CoverageSlicesCatalog; import org.geotools.coverage.io.range.FieldType; import org.geotools.coverage.io.range.RangeType; import org.geotools.coverage.io.range.impl.DefaultFieldType; import org.geotools.coverage.io.range.impl.DefaultRangeType; import org.geotools.coverage.io.util.DateRangeComparator; import org.geotools.coverage.io.util.DateRangeTreeSet; import org.geotools.coverage.io.util.DoubleRangeTreeSet; import org.geotools.coverage.io.util.NumberRangeComparator; import org.geotools.data.DataUtilities; import org.geotools.data.collection.ListFeatureCollection; import org.geotools.factory.GeoTools; import org.geotools.feature.NameImpl; import org.geotools.gce.imagemosaic.Utils; import org.geotools.gce.imagemosaic.catalog.index.Indexer.Coverages.Coverage; import org.geotools.gce.imagemosaic.catalog.index.SchemaType; import org.geotools.geometry.jts.ReferencedEnvelope; import org.geotools.imageio.netcdf.NetCDFGeoreferenceManager.DimensionMapper; import org.geotools.imageio.netcdf.cv.CoordinateVariable; import org.geotools.imageio.netcdf.utilities.NetCDFCRSUtilities; import org.geotools.imageio.netcdf.utilities.NetCDFUtilities; import org.geotools.referencing.operation.transform.ProjectiveTransform; import org.geotools.resources.coverage.CoverageUtilities; import org.geotools.resources.i18n.Vocabulary; import org.geotools.resources.i18n.VocabularyKeys; import org.geotools.util.DateRange; import org.geotools.util.NumberRange; import org.geotools.util.SimpleInternationalString; import org.geotools.util.logging.Logging; import org.opengis.coverage.SampleDimension; import org.opengis.coverage.grid.GridEnvelope; import org.opengis.feature.simple.SimpleFeature; import org.opengis.feature.simple.SimpleFeatureType; import org.opengis.feature.type.AttributeDescriptor; import org.opengis.feature.type.Name; import org.opengis.geometry.BoundingBox; import org.opengis.geometry.MismatchedDimensionException; import org.opengis.metadata.spatial.PixelOrientation; import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.opengis.referencing.crs.TemporalCRS; import org.opengis.referencing.datum.PixelInCell; import org.opengis.referencing.operation.MathTransform; import org.opengis.referencing.operation.MathTransform2D; import org.opengis.util.InternationalString; import org.opengis.util.ProgressListener; import ucar.nc2.Dimension; import ucar.nc2.Variable; import ucar.nc2.constants.AxisType; import ucar.nc2.dataset.CoordinateAxis; import ucar.nc2.dataset.CoordinateSystem; import ucar.nc2.dataset.VariableDS; import javax.measure.unit.Unit; import java.awt.*; import java.awt.geom.AffineTransform; import java.awt.image.BandedSampleModel; import java.awt.image.SampleModel; import java.io.IOException; import java.util.*; import java.util.List; import java.util.logging.Level; /** * * @author Simone Giannecchini, GeoSolutions SAS * @todo lazy initialization * @todo management of data read with proper mangling */ public class VariableAdapter extends CoverageSourceDescriptor { private final static boolean QUICK_SCAN; private final static String QUICK_SCAN_KEY = "org.geotools.netcdf.quickscan"; public final static int Z = 0; public final static int T = 1; /** * Simple chars replacing classes to deal with "custom" * chars. * As an instance, to be compliant with the * javax.measure.unit.Unit parser, * we should replace kg.m-2 to kg*m^-2 * which means replacing the "." sign with the "*" * and the "-" sign with "^_". */ static class UnitCharReplacement { String from; String to; public UnitCharReplacement(String from, String to) { this.from = from; this.to = to; } String replace(String input) { if (input != null && input.contains(from)) { return input.replace(from, to); } return input; } } //TODO: support for more remappings through external configs final static Set<UnitCharReplacement> UNIT_CHARS_REPLACEMENTS; static { QUICK_SCAN = Boolean.getBoolean(QUICK_SCAN_KEY); UNIT_CHARS_REPLACEMENTS = new HashSet<UnitCharReplacement>(); UNIT_CHARS_REPLACEMENTS.add(new UnitCharReplacement("-", "^-")); UNIT_CHARS_REPLACEMENTS.add(new UnitCharReplacement(".", "*")); UNIT_CHARS_REPLACEMENTS.add(new UnitCharReplacement("1/s", "s^-1")); } public class UnidataSpatialDomain extends SpatialDomain { /** The spatial coordinate reference system */ private CoordinateReferenceSystem coordinateReferenceSystem; /** The spatial referenced envelope */ private ReferencedEnvelope referencedEnvelope; /** The gridGeometry of the spatial domain */ private GridGeometry2D gridGeometry; public ReferencedEnvelope getReferencedEnvelope() { return referencedEnvelope; } public void setReferencedEnvelope(ReferencedEnvelope referencedEnvelope) { this.referencedEnvelope = referencedEnvelope; } public GridGeometry2D getGridGeometry() { return gridGeometry; } public double[] getFullResolution() { AffineTransform gridToCRS = (AffineTransform) gridGeometry.getGridToCRS(); return CoverageUtilities.getResolution(gridToCRS); } public void setGridGeometry(GridGeometry2D gridGeometry) { this.gridGeometry = gridGeometry; } public void setCoordinateReferenceSystem(CoordinateReferenceSystem coordinateReferenceSystem) { this.coordinateReferenceSystem = coordinateReferenceSystem; } @Override public Set<? extends BoundingBox> getSpatialElements(boolean overall, ProgressListener listener) throws IOException { return Collections.singleton(referencedEnvelope); } @Override public CoordinateReferenceSystem getCoordinateReferenceSystem2D() { return coordinateReferenceSystem; } @Override public MathTransform2D getGridToWorldTransform(ProgressListener listener) throws IOException { return gridGeometry.getGridToCRS2D(PixelOrientation.CENTER); } @Override public Set<? extends RasterLayout> getRasterElements(boolean overall, ProgressListener listener) throws IOException { Rectangle bounds = gridGeometry.getGridRange2D().getBounds(); return Collections.singleton(new RasterLayout(bounds)); } } public class UnidataTemporalDomain extends TemporalDomain { /** * @param adaptee */ UnidataTemporalDomain(CoordinateVariable<?> adaptee) { if(!Date.class.isAssignableFrom(adaptee.getType())){ throw new IllegalArgumentException("Unable to wrap non temporal CoordinateVariable:"+adaptee.toString()); } this.adaptee = (CoordinateVariable<Date>)adaptee; } final CoordinateVariable<Date> adaptee; public SortedSet<DateRange> getTemporalExtent() { // Getting global Extent Date startTime; try { startTime = adaptee.getMinimum(); Date endTime = adaptee.getMaximum(); final DateRange global = new DateRange(startTime, endTime); final SortedSet<DateRange> globalTemporalExtent = new DateRangeTreeSet(); globalTemporalExtent.add(global); return globalTemporalExtent; } catch (IOException e) { throw new RuntimeException(e); } } @Override public SortedSet<? extends DateRange> getTemporalElements(boolean overall, ProgressListener listener) throws IOException { if (overall) { // Getting overall Extent final SortedSet<DateRange> extent = new TreeSet<DateRange>(new DateRangeComparator()); for(Date dd:adaptee.read()){ extent.add(new DateRange(dd,dd)); } return extent; } else { return getTemporalExtent(); } } @Override public CoordinateReferenceSystem getCoordinateReferenceSystem() { return adaptee.getCoordinateReferenceSystem(); } } public class UnidataVerticalDomain extends VerticalDomain { final CoordinateVariable<? extends Number> adaptee; /** * @param cv */ UnidataVerticalDomain(CoordinateVariable<?> cv) { if(!Number.class.isAssignableFrom(cv.getType())){ throw new IllegalArgumentException("Unable to wrap a non Number CoordinateVariable:"+cv.toString()); } this.adaptee = (CoordinateVariable<? extends Number>)cv; } public SortedSet<NumberRange<Double>> getVerticalExtent() { // Getting global Extent final CoordinateVariable<? extends Number> verticalDimension=this.adaptee; NumberRange<Double> global; try { global = NumberRange.create( verticalDimension.getMinimum().doubleValue(), verticalDimension.getMaximum().doubleValue()); } catch (IOException e) { throw new RuntimeException(e); } final SortedSet<NumberRange<Double>> globalVerticalExtent = new DoubleRangeTreeSet(); globalVerticalExtent.add(global); return globalVerticalExtent; } @Override public SortedSet<? extends NumberRange<Double>> getVerticalElements(boolean overall, ProgressListener listener) throws IOException { if (overall) { // Getting overall Extent final SortedSet<NumberRange<Double>> extent = new TreeSet<NumberRange<Double>>(new NumberRangeComparator()); for(Number vv:adaptee.read()){ final double doubleValue = vv.doubleValue(); extent.add(NumberRange.create(doubleValue,doubleValue)); } return extent; } else { return getVerticalExtent(); } } @Override public CoordinateReferenceSystem getCoordinateReferenceSystem() { return adaptee.getCoordinateReferenceSystem(); } } /** * TODO improve support for this */ public class UnidataAdditionalDomain extends AdditionalDomain { /** The detailed domain extent */ private final Set<Object> domainExtent = new TreeSet<Object>(); /** The merged domain extent */ private final Set<Object> globalDomainExtent = new TreeSet<Object>(new Comparator<Object>() { private NumberRangeComparator numberRangeComparator = new NumberRangeComparator(); private DateRangeComparator dateRangeComparator = new DateRangeComparator(); public int compare(Object o1, Object o2) { // assume that o1 and o2 are both not null boolean o1IsDateRange = true; boolean o2IsDateRange = true; if (o1 instanceof NumberRange) { o1IsDateRange = false; } else if (!(o1 instanceof DateRange)) { throw new ClassCastException(o1.getClass() + " is not an known range type"); } if (o2 instanceof NumberRange) { o2IsDateRange = false; } else if (!(o2 instanceof DateRange)) { throw new ClassCastException(o2.getClass() + " is not an known range type"); } if (o1IsDateRange && o2IsDateRange) { return dateRangeComparator.compare((DateRange) o1, (DateRange) o2); } else if (!o1IsDateRange && !o2IsDateRange) { return numberRangeComparator.compare((NumberRange<?>) o1, (NumberRange<?>) o2); } throw new ClassCastException("Incompatible range types: " + o1.getClass() + " is not the same as " + o2.getClass()); } public boolean equals(Object o) { return false; } }); /** The domain name */ private final String name; private final DomainType type; final CoordinateVariable<?> adaptee; /** * @param domainExtent * @param globalDomainExtent * @param name * @param type * @param adaptee * TODO missing support for Range * TODO missing support for String domains * @throws IOException */ UnidataAdditionalDomain(CoordinateVariable<?> adaptee) throws IOException { this.adaptee = adaptee; name=adaptee.getName(); // type Class<?> type=adaptee.getType(); if(Date.class.isAssignableFrom(type)){ this.type=DomainType.DATE; // global domain globalDomainExtent.add(new DateRange( (Date)adaptee.getMinimum(), (Date)adaptee.getMaximum())); } else if(Number.class.isAssignableFrom(type)){ this.type=DomainType.NUMBER; // global domain globalDomainExtent.add(new NumberRange<Double>( Double.class, ((Number)adaptee.getMinimum()).doubleValue(), ((Number)adaptee.getMaximum()).doubleValue())); } else { throw new UnsupportedOperationException("Unsupported CoordinateVariable:"+adaptee.toString()); } // domain domainExtent.addAll(adaptee.read()); } @Override public Set<Object> getElements(boolean overall, ProgressListener listener) throws IOException { if (overall) { return globalDomainExtent; } else { return domainExtent; } } @Override public String getName() { return name; } @Override public DomainType getType() { return type; } public Set<Object> getDomainExtent() { return domainExtent; } } final VariableDS variableDS; /** Following COARDS or CF Convention, custom dimensions are always at the beginning (lower indexes) */ Set<String> ignoredDimensions = new HashSet<String>(); private ucar.nc2.dataset.CoordinateSystem coordinateSystem; private NetCDFImageReader reader; private int numBands; private SampleModel sampleModel; private int numberOfSlices; private int width; private int height; private CoordinateReferenceSystem coordinateReferenceSystem; private Name coverageName; private SimpleFeatureType indexSchema; private int[] nDimensionIndex; private final static java.util.logging.Logger LOGGER = Logging.getLogger(VariableAdapter.class); /** Usual schema are the_geom, imageIndex, so the first attribute (time or elevation) will have index = 2 */ private static final int FIRST_ATTRIBUTE_INDEX = 2; /** * Extracts the compound {@link CoordinateReferenceSystem} from the unidata variable. * * @return the compound {@link CoordinateReferenceSystem}. * @throws Exception */ private void init() throws Exception { // initialize the various domains initSpatialElements(); // initialize rank and number of 2D slices initRange(); // initialize info about slice initSlicesInfo(); } public int getRank() { return variableDS.getRank() - ignoredDimensions.size(); } /** * @throws Exception * */ private void initSlicesInfo() throws Exception { int[] shape = variableDS.getShape(); numberOfSlices = 1; for (int i=0; i < variableDS.getShape().length - 2; i++){ if (!ignoredDimensions.contains(variableDS.getDimension(i).getFullName())){ numberOfSlices *= shape[i]; } } } /** * @throws IOException * */ private void initSpatialElements() throws Exception { final List<DimensionDescriptor> dimensions = new ArrayList<DimensionDescriptor>(); initCRS(dimensions); // SPATIAL DIMENSIONS initSpatialDomain(); setDimensionDescriptors(dimensions); if (reader.ancillaryFileManager.isImposedSchema()) { updateDimensions(getDimensionDescriptors()); } } /** * Update the dimensions to attributes mapping for this variable if needed. * Default behaviour is to get attributes from the name of the dimensions of the variable. * In case the indexer.xml contains an explicit schema with different attributes for time and elevation * we need to remap them and updates the dimensions mapping as well as the DimensionsDescriptors * @param dimensionDescriptors * @throws IOException */ private void updateDimensions(List<DimensionDescriptor> dimensionDescriptors) throws IOException { final Map<Name, String> mapping = reader.ancillaryFileManager.variablesMap; final Set<Name> keys = mapping.keySet(); final String varName = getName(); for (Name key: keys) { // Go to the current variable final String origName = mapping.get(key); if (origName.equalsIgnoreCase(varName)){ // Get the mapped coverage name (as an instance, NO2 for a GOME2 with var = 'z') final String coverageName = key.getLocalPart(); final Coverage coverage = reader.ancillaryFileManager.coveragesMapping.get(coverageName); final SchemaType schema = coverage.getSchema(); if (schema != null) { // look up the name String schName= schema.getName(); final CoverageSlicesCatalog catalog = reader.getCatalog(); if (catalog != null) { // Current assumption is that we have a typeName for each coverage but we should keep on working // with shared schemas // try with coveragename SimpleFeatureType schemaType = null; try{ if(schName!=null){ schemaType=catalog.getSchema(schName); } }catch (IOException e) { // ok, we did not use the schema name, let's use the coverage name schemaType = catalog.getSchema(coverageName); } if (schemaType != null) { // Schema found: proceed with remapping attributes updateMapping(schemaType, dimensionDescriptors); indexSchema = schemaType; break; } throw new IllegalStateException("Unable to find the table for this coverage: "+ coverageName); } } break; } } } /** * Update the dimensionDescriptor attributes mapping by checking the actual attribute names from the schema * @param indexSchema * @param descriptors * @throws IOException */ public void updateMapping(SimpleFeatureType indexSchema, List<DimensionDescriptor> descriptors) throws IOException { DimensionMapper mapper = reader.georeferencing.getDimensionMapper(); Set<String> dimensionNames = mapper.getDimensionNames(); // No need to do the mapping update in case one of these conditions apply if (dimensionNames == null || dimensionNames.isEmpty() || descriptors == null || descriptors.isEmpty() || indexSchema.getAttributeCount() <= FIRST_ATTRIBUTE_INDEX ) { return; } int indexAttribute = FIRST_ATTRIBUTE_INDEX; final AttributeDescriptor attributeDescriptor = indexSchema.getDescriptor(indexAttribute); final String updatedAttribute = attributeDescriptor.getLocalName(); if ("location".equalsIgnoreCase(updatedAttribute)) { // Skip location attribute indexAttribute++; } // Remap time String currentDimName = NetCDFUtilities.TIME_DIM; if (dimensionNames.contains(currentDimName)) { if (remapAttribute(indexSchema, currentDimName, indexAttribute, descriptors, mapper)) { indexAttribute++; } } // Remap elevation currentDimName = NetCDFUtilities.ELEVATION_DIM; if (dimensionNames.contains(currentDimName)) { if (remapAttribute(indexSchema, currentDimName, indexAttribute, descriptors, mapper)) { indexAttribute++; } } //Remap additional domains if (getAdditionalDomains() != null) { for (AdditionalDomain dom : getAdditionalDomains()) { currentDimName = dom.getName(); if (remapAttribute(indexSchema, currentDimName, indexAttribute, descriptors, mapper)) { indexAttribute++; } } } } /** * Remap an attribute for a specified dimension. Get it from the schemaType and update * both the related dimension Descriptor as well as the dimensions mapping. * * @param indexSchema * @param currentDimName * @param indexAttribute * @param descriptors * @param mapper * @return */ private boolean remapAttribute(final SimpleFeatureType indexSchema, final String currentDimName, final int indexAttribute, final List<DimensionDescriptor> descriptors, DimensionMapper mapper) { final int numAttributes = indexSchema.getAttributeCount(); if (numAttributes <= indexAttribute) { // Stop looking for attributes in case there aren't anymore return false; } // Get the attribute descriptor for that index final AttributeDescriptor attributeDescriptor = indexSchema.getDescriptor(indexAttribute); // Loop over dimensionDescriptors for (DimensionDescriptor descriptor : descriptors) { // Find the descriptor related to the current dimension if (descriptor.getName().toUpperCase().equalsIgnoreCase(currentDimName)) { final String updatedAttribute = attributeDescriptor.getLocalName(); if (!updatedAttribute.equals(((DefaultDimensionDescriptor) descriptor) .getStartAttribute())) { // Remap attributes in case the schema's attribute doesn't match the current attribute ((DefaultDimensionDescriptor) descriptor).setStartAttribute(updatedAttribute); // Update the dimensions mapping too mapper.remap(currentDimName, updatedAttribute); } // the attribute has been found, prepare for the next one return true; } } return false; } /** * @param dimensions * @return * @throws IllegalArgumentException * @throws RuntimeException * @throws IOException * @throws IllegalStateException */ private void initCRS(List<DimensionDescriptor> dimensions) throws IllegalArgumentException, RuntimeException, IOException, IllegalStateException { // from UnidataVariableAdapter this.coordinateSystem = NetCDFCRSUtilities.getCoordinateSystem(variableDS); if (coordinateSystem == null){ throw new IllegalArgumentException("Provided CoordinateSystem is null"); } // Wrapper for the CoordinateSystem coordinateSystem = new CoordinateSystemAdapter(coordinateSystem); //init nDimensionIndex List<Integer> nDimensionIndexList = new ArrayList<Integer>(2); nDimensionIndexList.add(-1); nDimensionIndexList.add(-1); /* * Adds the axis in reverse order, because the NetCDF image reader put the last dimensions in the rendered image. Typical NetCDF convention is * to put axis in the (time, depth, latitude, longitude) order, which typically maps to (longitude, latitude, depth, time) order in GeoTools * referencing framework. */ int index = -1; for(CoordinateAxis axis :coordinateSystem.getCoordinateAxes()){ index++; String fullName = axis.getFullName(); if (NetCDFUtilities.getIgnoredDimensions().contains(fullName)) { ignoredDimensions.add(fullName); continue; } CoordinateVariable<?> cv=reader.georeferencing.getCoordinateVariable(axis.getShortName()); if (cv == null) { if (LOGGER.isLoggable(Level.FINE)) { LOGGER.fine("Unable to find a coordinate variable for " + fullName); } index--; continue; } switch(cv.getAxisType()){ case Time: initTemporalDomain(cv, dimensions); nDimensionIndexList.set(T, index); continue; case GeoZ:case Height:case Pressure: String axisName = cv.getName(); if (NetCDFCRSUtilities.VERTICAL_AXIS_NAMES.contains(axisName)) { initVerticalDomain(cv, dimensions); nDimensionIndexList.set(Z, index); } else{ initAdditionalDomain(cv, dimensions); nDimensionIndexList.add(index); } continue; case GeoX: case GeoY: case Lat: case Lon: // do nothing continue; default: initAdditionalDomain(cv, dimensions); nDimensionIndexList.add(index); } } nDimensionIndex = nDimensionIndexList.stream().mapToInt(i -> i).toArray(); // //// // Creating the CoordinateReferenceSystem // //// ReferencedEnvelope bbox = reader.georeferencing.getBoundingBox(variableDS.getShortName()); coordinateReferenceSystem = bbox.getCoordinateReferenceSystem(); } /** * @param cv * @param dimensions * @throws IOException */ private void initVerticalDomain(CoordinateVariable<?> cv, List<DimensionDescriptor> dimensions) throws IOException { this.setHasVerticalDomain(true); final UnidataVerticalDomain verticalDomain = new UnidataVerticalDomain(cv); this.setVerticalDomain(verticalDomain); //TODO: Map ZAxis unit to UCUM UNIT (depending on type... elevation, level, pressure, ...) dimensions.add(new DefaultDimensionDescriptor(Utils.ELEVATION_DOMAIN, cv.getUnit(), CoverageUtilities.UCUM.ELEVATION_UNITS.getSymbol(), cv.getName(), null)); } /** * @param cv * @param dimensions * @throws IOException */ private void initTemporalDomain(CoordinateVariable<?> cv, List<DimensionDescriptor> dimensions) throws IOException { if(!cv.getType().equals(Date.class)){ throw new IllegalArgumentException("Unable to init temporal domain from CoordinateVariable that does not bind to Date"); } if(!(cv.getCoordinateReferenceSystem() instanceof TemporalCRS)){ throw new IllegalArgumentException("Unable to init temporal domain from CoordinateVariable that does not have a TemporalCRS"); } this.setHasTemporalDomain(true); final UnidataTemporalDomain temporalDomain = new UnidataTemporalDomain(cv); this.setTemporalDomain(temporalDomain); String timeAttribute = reader.uniqueTimeAttribute ? NetCDFUtilities.TIME : cv.getName(); dimensions.add(new DefaultDimensionDescriptor(Utils.TIME_DOMAIN, CoverageUtilities.UCUM.TIME_UNITS.getName(), CoverageUtilities.UCUM.TIME_UNITS.getSymbol(), timeAttribute, null)); } /** * @param cv * @param dimensions * @throws IOException */ private void initAdditionalDomain(CoordinateVariable<?> cv, List<DimensionDescriptor> dimensions) throws IOException { UnidataAdditionalDomain domain; try { domain = new UnidataAdditionalDomain(cv); if (getAdditionalDomains() == null) { setAdditionalDomains(new ArrayList<AdditionalDomain>()); } getAdditionalDomains().add(domain); } catch (IOException e) { LOGGER.log(Level.WARNING, e.getMessage(), e); return; } if(cv.getType().equals(Date.class)){ if(!(cv.getCoordinateReferenceSystem() instanceof TemporalCRS)){ throw new IllegalArgumentException("Unable to init temporal domain from CoordinateVariable that does not have a TemporalCRS"); } dimensions.add(new DefaultDimensionDescriptor(cv.getName(), CoverageUtilities.UCUM.TIME_UNITS.getName(), CoverageUtilities.UCUM.TIME_UNITS.getSymbol(), cv.getName(), null)); } else if (Number.class.isAssignableFrom(cv.getType())) { // TODO: Parse Units from axis and map them to UCUM units dimensions.add(new DefaultDimensionDescriptor(cv.getName(), cv.getUnit(), cv.getUnit(), cv.getName(), null)); } else { throw new IllegalArgumentException("Unable to init domain from CoordinateVariable of type: " + cv.getType().getName()); } this.setHasAdditionalDomains(true); } /** * @param coordinateReferenceSystem * @throws MismatchedDimensionException * @throws IOException */ private void initSpatialDomain() throws Exception { // SPATIAL DOMAIN final UnidataSpatialDomain spatialDomain = new UnidataSpatialDomain(); this.setSpatialDomain(spatialDomain); ReferencedEnvelope bbox = reader.georeferencing.getBoundingBox(variableDS.getShortName()); spatialDomain.setCoordinateReferenceSystem(coordinateReferenceSystem); spatialDomain.setReferencedEnvelope(bbox); spatialDomain.setGridGeometry(getGridGeometry()); } /** * */ private void initRange() { width = variableDS.getDimension(variableDS.getRank() - NetCDFUtilities.X_DIMENSION).getLength(); height = variableDS.getDimension(variableDS.getRank() - NetCDFUtilities.Y_DIMENSION).getLength(); // computing the number of bands, according to COARDS convention ignored dimension are at the beginning String candidateDimension = variableDS.getDimensions().get(0).getFullName(); MultipleBandsDimensionInfo multipleBands = reader.ancillaryFileManager.getMultipleBandsDimensionInfo(candidateDimension); if (multipleBands != null) { // multiple bands are defined for the ignored dimension numBands = multipleBands.getNumberOfBands(); } else { numBands = variableDS.getRank() > 2 ? variableDS.getDimension(2).getLength() : 1; } // let's check if we are in the context of an image mosaic request if (reader.getImageMosaicRequest() != null) { // if specific bands were selected we need to adapt the number of bands int[] selectedBands = reader.getImageMosaicRequest().getBands(); numBands = selectedBands == null ? numBands : selectedBands.length; } final int bufferType = NetCDFUtilities.getRawDataType(variableDS); sampleModel = new BandedSampleModel(bufferType, width, height, multipleBands == null ? 1 : numBands); final Number noData = NetCDFUtilities.getNodata(variableDS); Category[] categories = null; if (noData != null) { NumberRange noDataRange = NumberRange.create(noData.doubleValue(), true, noData.doubleValue(), true); categories = new Category[]{ new Category(Vocabulary.formatInternational(VocabularyKeys.NODATA), new Color[] { new Color(0, 0, 0, 0) }, noDataRange)}; } // range type String description = variableDS.getDescription(); if (description == null) { description = variableDS.getShortName(); } final Set<SampleDimension> sampleDims = new HashSet<SampleDimension>(); // Parsing the unit of measure of this variable Unit unit = null; String unitString = variableDS.getUnitsString(); if (unitString != null) { try { for (UnitCharReplacement replacement: UNIT_CHARS_REPLACEMENTS) { unitString = replacement.replace(unitString); } unit = Unit.valueOf(unitString); } catch (IllegalArgumentException iae) { if (LOGGER.isLoggable(Level.FINE)) { LOGGER.fine("Unable to parse the unit:" + unitString + "\nNo unit will be assigned"); } } } if (multipleBands == null) { // single band dimension, so we only need one sample dimension sampleDims.add(new GridSampleDimension(description, categories, unit)); } else { for (String bandName : multipleBands.getBandsNamesInOrder()) { // multiple bands for this dimension, only the bands names is different sampleDims.add(new GridSampleDimension(bandName, categories, unit)); } } InternationalString desc = null; if (description != null && !description.isEmpty()) { desc = new SimpleInternationalString(description); } final FieldType fieldType = new DefaultFieldType(new NameImpl(getName()), desc, sampleDims); final RangeType range = new DefaultRangeType(getName(), description, fieldType); this.setRangeType(range); } /** * Extracts the {@link GridGeometry2D grid geometry} from the unidata variable. * * @return the {@link GridGeometry2D}. * @throws IOException */ protected GridGeometry2D getGridGeometry() throws IOException { int[] low = new int[2]; int[] high = new int[2]; double[] origin = new double[2]; double scaleX = Double.POSITIVE_INFINITY, scaleY = Double.POSITIVE_INFINITY; for( CoordinateVariable<?> cv : reader.georeferencing.getCoordinatesVariables(variableDS.getShortName()) ) { if(!cv.isNumeric()){ continue; } final AxisType axisType = cv.getAxisType(); switch (axisType) { case Lon: case GeoX: // raster space low[0] = 0; high[0] = (int) cv.getSize(); // model space if (cv.isRegular()) { // regular model space origin[0] = cv.getStart(); scaleX = cv.getIncrement(); } else { // model space is not declared to be regular, but we kind of assume it is!!! @SuppressWarnings("unchecked") List<Number> vals = (List<Number>) cv.read(); double min = ((Number)cv.getMinimum()).doubleValue(); double max = ((Number)cv.getMaximum()).doubleValue(); // make sure we skip nodata coords, bah... if (!Double.isNaN(min) && !Double.isNaN(max)) { origin[0] = min; scaleX = (max-min) / vals.size(); } else { if (LOGGER.isLoggable(Level.FINE)) { LOGGER.log(Level.FINE, "Axis values contains NaN; finding first valid values"); } for( int j = 0; j < vals.size(); j++ ) { double v = ((Number)vals.get(j)).doubleValue(); if (!Double.isNaN(v)) { for( int k = vals.size(); k > j; k-- ) { double vv = ((Number)vals.get(k)).doubleValue(); if (!Double.isNaN(vv)) { origin[0] = v; scaleX = (vv - v) / vals.size(); } } } } } } break; case Lat: case GeoY: // raster space low[1] = 0; high[1] = (int) cv.getSize(); // model space if (cv.isRegular()) { if (cv.getIncrement() > 0) { // the latitude axis is increasing! This is a special case so we flip it around scaleY = -cv.getIncrement(); origin[1] = cv.getStart() - scaleY * (high[1] - 1); } else { scaleY = cv.getIncrement(); origin[1] = cv.getStart(); } } else { // model space is not declared to be regular, but we kind of assume it is!!! @SuppressWarnings("unchecked") List<Number> values = (List<Number>) cv.read(); double min = ((Number)cv.getMinimum()).doubleValue(); double max = ((Number)cv.getMaximum()).doubleValue(); // make sure we skip nodata coords, bah... if (!Double.isNaN(min) && !Double.isNaN(max)) { scaleY = -(max-min) / values.size(); origin[1] = max; } else { if (LOGGER.isLoggable(Level.FINE)) { LOGGER.log(Level.FINE, "Axis values contains NaN; finding first valid values"); } for( int j = 0; j < values.size(); j++ ) { double v = ((Number)values.get(j)).doubleValue(); if (!Double.isNaN(v)) { for( int k = values.size(); k > j; k-- ) { double vv = ((Number)values.get(k)).doubleValue(); if (!Double.isNaN(vv)) { origin[1] = v; scaleY = -(vv - v) / values.size(); } } } } } } break; default: break; } } final AffineTransform at = new AffineTransform(scaleX, 0, 0, scaleY, origin[0], origin[1]); final GridEnvelope gridRange = new GridEnvelope2D( low[0], low[1], high[0]-low[0], high[1]-low[1]); final MathTransform raster2Model = ProjectiveTransform.create(at); return new GridGeometry2D(gridRange, PixelInCell.CELL_CENTER, raster2Model, coordinateReferenceSystem, GeoTools.getDefaultHints()); } public int getNumBands() { return numBands; } public SampleModel getSampleModel() { return sampleModel; } public VariableAdapter(NetCDFImageReader reader, Name coverageName, VariableDS variable) throws Exception { this.variableDS = variable; this.reader = reader; this.coverageName = coverageName; setName(variable.getFullName()); init(); } @Override public UnidataSpatialDomain getSpatialDomain() { return (UnidataSpatialDomain) super.getSpatialDomain(); } @Override public UnidataTemporalDomain getTemporalDomain() { return (UnidataTemporalDomain) super.getTemporalDomain(); } @Override public UnidataVerticalDomain getVerticalDomain() { return (UnidataVerticalDomain) super.getVerticalDomain(); } /** * @return */ public int getWidth() { return width; } /** * @return */ public int getHeight() { return height; } /** * Utility method to retrieve the n-index of a Variable coverageDescriptor stored on * {@link NetCDFImageReader} NetCDF Flat Reader {@link HashMap} indexMap. * * @param n {@link int} the dimension * @param imageIndex {@link int} the image index * * @return n-index {@link int} -1 if not present */ public int getNIndex(int n, int imageIndex) { if (n < nDimensionIndex.length && nDimensionIndex[n] >= 0) { int factor = 1; for (int i = 0; i < n; i++) { if (nDimensionIndex[i] >= 0) { factor *= NetCDFUtilities.getDimensionLength(variableDS, nDimensionIndex[i]); } } return imageIndex % (NetCDFUtilities.getDimensionLength(variableDS, nDimensionIndex[n]) * factor) / factor; } return -1; } /** * Utility method to split the index of a Variable coverageDescriptor stored on * {@link NetCDFImageReader} NetCDF Flat Reader {@link HashMap} indexMap into dimensions. * * @param imageIndex {@link int} the index * @return splitted index */ public int[] splitIndex(int imageIndex) { int[] resultIndex = new int[nDimensionIndex.length]; for (int n = 0; n < nDimensionIndex.length; n++) { resultIndex[n] = getNIndex(n, imageIndex); } return resultIndex; } public Map<String, Integer> mapIndex(int[] splittedIndex) { Map<String, Integer> resultIndex = new HashMap<String, Integer>(); for (int n = 0; n < splittedIndex.length; n++) { if (nDimensionIndex[n] != -1) { resultIndex.put(variableDS.getDimension(nDimensionIndex[n]).getFullName(), splittedIndex[n]); } } return resultIndex; } /** * @return the numberOfSlices */ public int getNumberOfSlices() { return QUICK_SCAN ? 1 : numberOfSlices; } /** * @return the shape */ public int[] getShape() { return variableDS.getShape(); } /** * Return features for that variable adapter, starting from slices with index = "startIndex", and up to "limit" elements. * This allows for paging. Put the created features inside the provided collection * * @param startIndex the first slice to be returned * @param limit the max number of features to be created * @param collection the feature collection where features need to be stored */ public int getFeatures(final int startIndex, final int limit, final ListFeatureCollection collection) { final SimpleFeatureType indexSchema = collection.getSchema(); final int slicesNum = getNumberOfSlices(); if (startIndex > slicesNum) { throw new IllegalArgumentException("The paging start index can't be higher than the number of available slices"); } int lastIndex = startIndex + limit; if (lastIndex > slicesNum) { lastIndex = slicesNum; } final String varName = variableDS.getFullName(); for (int imageIndex = startIndex; imageIndex < lastIndex; imageIndex++) { int[] index = splitIndex(imageIndex); //Put a new sliceIndex in the list final Slice2DIndex variableIndex = new Slice2DIndex(index, varName); reader.ancillaryFileManager.addSlice(variableIndex); // Create a feature for that index to be put in the CoverageSlicesCatalog final SimpleFeature feature = createFeature( coverageName.toString(), index, coordinateSystem, imageIndex, indexSchema); if (feature != null) { collection.add(feature); } //or else it is a non-existing slice (not in catalog, but counted) } //return processed slices return lastIndex - startIndex; } /** * Create a SimpleFeature on top of the variable and indexes. * * @param tIndex the time index * @param zIndex the zeta index * @param cs the {@link CoordinateSystem} associated with that variable * @param imageIndex the index to be associated to the feature in the index * @param indexSchema the schema to be used to create the feature * @param geometry the geometry to be attached to the feature * @return the created {@link SimpleFeature} * TODO move to variable wrapper */ private SimpleFeature createFeature( final String coverageName, final int[] index, final CoordinateSystem cs, final int imageIndex, final SimpleFeatureType indexSchema) { final SimpleFeature feature = DataUtilities.template(indexSchema); feature.setAttribute(CoverageSlice.Attributes.GEOMETRY, NetCDFCRSUtilities.GEOM_FACTORY.toGeometry(reader.georeferencing.getBoundingBox(variableDS.getShortName()))); feature.setAttribute(CoverageSlice.Attributes.INDEX, imageIndex); Map<String, Integer> mappedIndex = mapIndex(index); // TIME management // Check if we have time and elevation domain and set the attribute if needed if (nDimensionIndex[T] >= 0) { final Date date = getValueByIndex(nDimensionIndex[T], mappedIndex); if (date == null) { //non-existing slice, not in catalog return null; } setFeatureTime(feature, date, cs); } // elevation if (nDimensionIndex[Z] >= 0) { final Number verticalValue = getValueByIndex(nDimensionIndex[Z], mappedIndex); if (verticalValue == null) { //non-existing slice, not in catalog return null; } feature.setAttribute(reader.georeferencing.getDimensionMapper().getDimension(NetCDFUtilities.ELEVATION_DIM), verticalValue); } //additional domains if (getAdditionalDomains() != null) { for (int i = 0; i < getAdditionalDomains().size(); i++) { AdditionalDomain domain = getAdditionalDomains().get(i); final Object value; if (domain.getType().equals(DomainType.DATE)) { value = getValueByIndex(nDimensionIndex[i + 2], mappedIndex); } else { value = getValueByIndex(nDimensionIndex[i + 2], mappedIndex); } if (value == null) { //non-existing slice, not in catalog return null; } feature.setAttribute(reader.georeferencing.getDimensionMapper().getDimension(domain.getName().toUpperCase()), value); } } return feature; } private String setFeatureTime(SimpleFeature feature, Date date, CoordinateSystem cs) { String originalTimeAttribute = null; if (date != null) { originalTimeAttribute = getTimeAttribute(cs); String timeAttribute = originalTimeAttribute; if (reader.uniqueTimeAttribute) { timeAttribute = NetCDFUtilities.TIME; } feature.setAttribute(timeAttribute, date); } return originalTimeAttribute; } private String getTimeAttribute(CoordinateSystem cs) { CoordinateAxis timeAxis = cs.getTaxis(); String name = timeAxis != null ? timeAxis.getFullName() : NetCDFUtilities.TIME_DIM; DimensionMapper dimensionMapper = reader.georeferencing.getDimensionMapper(); String timeAttribute = dimensionMapper.getDimension(name.toUpperCase()); if (timeAttribute == null) { //Fallback on standard name timeAttribute = dimensionMapper.getDimension(NetCDFUtilities.TIME_DIM); } return timeAttribute; } /** Return the value of a particular dimension. * * @param dimensionIndex the index of the dimension * @return the value */ @SuppressWarnings("unchecked") private <T> T getValueByIndex(int dimensionIndex, final Map<String, Integer> mappedIndex) { final Dimension dimension = variableDS.getDimension(dimensionIndex); return (T) reader.georeferencing.getCoordinateVariable(dimension.getFullName()).read(mappedIndex); } /** * Wrapper class used for setting the OSEQD dimension to Vertical, even if the {@link CoordinateSystem} does not handle it. * * @author Nicola Lagomarsini GeoSolutions S.A.S. * */ static class CoordinateSystemAdapter extends CoordinateSystem { /**Input coordinate system*/ private CoordinateSystem cs; /** Boolean indicating that the vertical axis is present*/ private final boolean vertical; CoordinateSystemAdapter(CoordinateSystem cs) { this.cs = cs; // Check if the Vertical axis is present if(cs.hasVerticalAxis()){ vertical = true; }else{ // Check if any of the unsupported dimensions is present Set<String> unsupported = NetCDFUtilities.getUnsupportedDimensions(); boolean present = false; for(String dimension : unsupported){ if(cs.containsAxis(dimension)){ present = true; break; } } if(present){ vertical = true; }else{ vertical = false; } } } @Override public boolean hasVerticalAxis() { return vertical; } @Override public boolean hasTimeAxis() { return cs.hasTimeAxis(); } @Override public CoordinateAxis getTaxis() { return cs.getTaxis(); } @Override public List<CoordinateAxis> getCoordinateAxes() { return cs.getCoordinateAxes(); } } }