blob: 25e3b1409508d112e227ac60ff6dd25d1dc4f2e6 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.sis.internal.netcdf.impl;
import java.util.Map;
import java.util.List;
import java.util.Collection;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Spliterator;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
import java.util.function.Consumer;
import java.util.OptionalLong;
import java.io.IOException;
import org.apache.sis.math.Vector;
import org.apache.sis.coverage.grid.GridExtent;
import org.apache.sis.internal.netcdf.DataType;
import org.apache.sis.internal.netcdf.DiscreteSampling;
import org.apache.sis.internal.netcdf.Resources;
import org.apache.sis.internal.feature.MovingFeature;
import org.apache.sis.storage.DataStoreException;
import org.apache.sis.util.collection.BackingStoreException;
import ucar.nc2.constants.CF;
// Branch-dependent imports
import org.apache.sis.feature.AbstractFeature;
import org.apache.sis.feature.DefaultFeatureType;
import org.apache.sis.feature.DefaultAttributeType;
import org.apache.sis.feature.AbstractIdentifiedType;
/**
* Implementations of the discrete sampling features decoder. This implementation shall be able to decode at least the
* netCDF files encoded as specified in the OGC 16-114 (OGC Moving Features Encoding Extension: netCDF) specification.
*
* @author Martin Desruisseaux (Geomatys)
* @version 1.0
* @since 0.8
* @module
*/
final class FeaturesInfo extends DiscreteSampling {
/**
* The number of instances for each feature.
*/
private final Vector counts;
/**
* The moving feature identifiers ("mfIdRef").
* The amount of identifiers shall be the same than the length of the {@link #counts} vector.
*/
private final VariableInfo identifiers;
/**
* The variable that contains time.
*/
private final VariableInfo time;
/**
* The variable that contains <var>x</var> and <var>y</var> coordinate values (typically longitudes and latitudes).
* All variables in this array shall have the same length, and that length shall be the same than {@link #time}.
*/
private final VariableInfo[] coordinates;
/**
* Any custom properties.
*/
private final VariableInfo[] properties;
/**
* The type of all features to be read by this {@code FeaturesInfo}.
*/
private final DefaultFeatureType type;
/**
* Creates a new discrete sampling parser for features identified by the given variable.
*
* @param decoder the source of the features to create.
* @param counts the count of instances per feature.
* @param identifiers the feature identifiers.
* @param time the variable that contains time.
* @param coordinates the variable that contains <var>x</var> and <var>y</var> coordinate values.
* @param properties the variables that contain custom properties.
* @throws IllegalArgumentException if the given library is non-null but not available.
*/
@SuppressWarnings("rawtypes") // Because of generic array creation.
private FeaturesInfo(final ChannelDecoder decoder,
final Vector counts, final VariableInfo identifiers, final VariableInfo time,
final Collection<VariableInfo> coordinates, final Collection<VariableInfo> properties)
{
super(decoder.geomlib, decoder.listeners);
this.counts = counts;
this.identifiers = identifiers;
this.coordinates = coordinates.toArray(new VariableInfo[coordinates.size()]);
this.properties = properties .toArray(new VariableInfo[properties .size()]);
this.time = time;
/*
* Creates a description of the features to be read.
*/
final Map<String,Object> info = new HashMap<>(4);
final AbstractIdentifiedType[] pt = new AbstractIdentifiedType[this.properties.length + 2];
DefaultAttributeType[] characteristics = null;
for (int i=0; i<pt.length; i++) {
final VariableInfo variable;
final Class<?> valueClass;
int minOccurs = 1;
int maxOccurs = 1;
switch (i) {
case 0: {
variable = identifiers;
valueClass = Integer.class;
break;
}
case 1: {
variable = null;
valueClass = factory.polylineClass;
characteristics = new DefaultAttributeType[] {MovingFeature.TIME};
break;
}
default: {
// TODO: use more accurate Number subtype for value class.
variable = this.properties[i-2];
valueClass = (variable.meaning(0) != null) ? String.class : Number.class;
minOccurs = 0;
maxOccurs = Integer.MAX_VALUE;
break;
}
}
info.put(DefaultAttributeType.NAME_KEY, (variable != null) ? variable.getName() : "trajectory");
// TODO: add description.
pt[i] = new DefaultAttributeType<>(info, valueClass, minOccurs, maxOccurs, null, characteristics);
}
String name = "Features"; // TODO: find a better name.
info.put(DefaultAttributeType.NAME_KEY, decoder.nameFactory.createLocalName(decoder.namespace, name));
type = new DefaultFeatureType(info, false, null, pt);
}
/**
* Returns {@code true} if the given attribute value is one of the {@code cf_role} attribute values
* supported by this implementation.
*/
private static boolean isSupportedRole(final String role) {
return CF.TRAJECTORY_ID.equalsIgnoreCase(role);
}
/**
* Creates new discrete sampling parsers from the attribute values found in the given decoder.
*
* @throws IllegalArgumentException if the geometric object library is not available.
* @throws ArithmeticException if the size of a variable exceeds {@link Integer#MAX_VALUE}, or other overflow occurs.
*/
static FeaturesInfo[] create(final ChannelDecoder decoder) throws IOException, DataStoreException {
final List<FeaturesInfo> features = new ArrayList<>(3); // Will usually contain at most one element.
search: for (final VariableInfo counts : decoder.variables) {
/*
* Any one-dimensional integer variable having a "sample_dimension" attribute string value
* will be taken as an indication that we have Discrete Sampling Geometries. That variable
* shall be counting the number of feature instances, and another variable having the same
* dimension (optionally plus a character dimension) shall give the feature identifiers.
* Example:
*
* dimensions:
* identifiers = 100;
* points = UNLIMITED;
* variables:
* int identifiers(identifiers);
* identifiers:cf_role = "trajectory_id";
* int counts(identifiers);
* counts:sample_dimension = "points";
*/
if (counts.dimensions.length == 1 && counts.getDataType().isInteger) {
final String sampleDimName = counts.getAttributeAsString(CF.SAMPLE_DIMENSION);
if (sampleDimName != null) {
final DimensionInfo featureDimension = counts.dimensions[0];
final DimensionInfo sampleDimension = decoder.findDimension(sampleDimName);
if (sampleDimension == null) {
decoder.listeners.warning(decoder.resources().getString(Resources.Keys.DimensionNotFound_3,
decoder.getFilename(), counts.getName(), sampleDimName));
continue;
}
/*
* We should have another variable of the same name than the feature dimension name
* ("identifiers" in above example). That variable should have a "cf_role" attribute
* set to one of the values known to current implementation. If we do not find such
* variable, search among other variables before to give up. That second search is not
* part of CF convention and will be accepted only if there is no ambiguity.
*/
VariableInfo identifiers = decoder.findVariable(featureDimension.name);
if (identifiers == null || !isSupportedRole(identifiers.getAttributeAsString(CF.CF_ROLE))) {
VariableInfo replacement = null;
for (final VariableInfo alt : decoder.variables) {
if (alt.dimensions.length != 0 && alt.dimensions[0] == featureDimension
&& isSupportedRole(alt.getAttributeAsString(CF.CF_ROLE)))
{
if (replacement != null) {
replacement = null;
break; // Ambiguity found: consider that we found no replacement.
}
replacement = alt;
}
}
if (replacement != null) {
identifiers = replacement;
}
if (identifiers == null) {
decoder.listeners.warning(decoder.resources().getString(Resources.Keys.VariableNotFound_2,
decoder.getFilename(), featureDimension.name));
continue;
}
}
/*
* At this point we found a variable that should be the feature identifiers.
* Verify that the variable dimensions are valid.
*/
for (int i=0; i<identifiers.dimensions.length; i++) {
final boolean isValid;
switch (i) {
case 0: isValid = (identifiers.dimensions[0] == featureDimension); break;
case 1: isValid = (identifiers.getDataType() == DataType.CHAR); break;
default: isValid = false; break; // Too many dimensions
}
if (!isValid) {
decoder.listeners.warning(decoder.resources().getString(
Resources.Keys.UnexpectedDimensionForVariable_4,
decoder.getFilename(), identifiers.getName(),
featureDimension.getName(), identifiers.dimensions[i].name));
continue search;
}
}
/*
* At this point, all information have been verified as valid. Now search all variables having
* the expected sample dimension. Those variable contains the actual data. For example if the
* sample dimension name is "points", then we may have:
*
* double longitude(points);
* longitude:axis = "X";
* longitude:standard_name = "longitude";
* longitude:units = "degrees_east";
* double latitude(points);
* latitude:axis = "Y";
* latitude:standard_name = "latitude";
* latitude:units = "degrees_north";
* double time(points);
* time:axis = "T";
* time:standard_name = "time";
* time:units = "minutes since 2014-11-29 00:00:00";
* short myCustomProperty(points);
*/
final Map<String,VariableInfo> coordinates = new LinkedHashMap<>();
final List<VariableInfo> properties = new ArrayList<>();
for (final VariableInfo data : decoder.variables) {
if (data.dimensions.length == 1 && data.dimensions[0] == sampleDimension) {
final String axisType = data.getAttributeAsString(CF.AXIS);
if (axisType == null) {
properties.add(data);
} else if (coordinates.put(axisType, data) != null) {
continue search; // Two axes of the same type: abort.
}
}
}
final VariableInfo time = coordinates.remove("T");
if (time != null) {
features.add(new FeaturesInfo(decoder, counts.read(), identifiers, time, coordinates.values(), properties));
}
}
}
}
return features.toArray(new FeaturesInfo[features.size()]);
}
/**
* Returns the type of all features to be read by this {@code FeaturesInfo}.
*/
@Override
public DefaultFeatureType getType() {
return type;
}
/**
* Returns the number of features in this set.
*
* @return the number of features.
*/
@Override
protected OptionalLong getFeatureCount() {
return OptionalLong.of(counts.size());
}
/**
* Returns the stream of features.
*
* @param parallel ignored, since current version does not support parallelism.
*/
@Override
public Stream<AbstractFeature> features(boolean parallel) {
return StreamSupport.stream(new Iter(), false);
}
/**
* Implementation of the iterator returned by {@link #features(boolean)}.
*/
private final class Iter implements Spliterator<AbstractFeature> {
/**
* Index of the next feature to read.
*/
private int index;
/**
* Position in the data vectors of the next feature to read.
* This is the sum of the length of data in all previous features.
*/
private int position;
/**
* Creates a new iterator.
*/
Iter() {
}
/**
* Executes the given action only on the next feature, if any.
*
* @throws ArithmeticException if the size of a variable exceeds {@link Integer#MAX_VALUE}, or other overflow occurs.
* @throws BackingStoreException if an {@link IOException} or {@link DataStoreException} occurred.
*
* @todo current reading process implies lot of seeks, which is inefficient.
*/
@Override
public boolean tryAdvance(final Consumer<? super AbstractFeature> action) {
final int length = counts.intValue(index);
final GridExtent extent = new GridExtent(null, new long[] {position},
new long[] {Math.addExact(position, length)}, false);
final int[] step = {1};
final Vector id, t;
final Vector[] coords = new Vector[coordinates.length];
final Object[] props = new Object[properties.length];
try {
id = identifiers.read(); // Efficiency should be okay because of cached value.
t = time.read(extent, step);
for (int i=0; i<coordinates.length; i++) {
coords[i] = coordinates[i].read(extent, step);
}
for (int i=0; i<properties.length; i++) {
final VariableInfo p = properties[i];
final Vector data = p.read(extent, step);
if (p.isEnumeration()) {
final String[] meanings = new String[data.size()];
for (int j=0; j<meanings.length; j++) {
String m = p.meaning(data.intValue(j));
meanings[j] = (m != null) ? m : "";
}
props[i] = Arrays.asList(meanings);
} else {
props[i] = data;
}
}
} catch (IOException | DataStoreException e) {
throw new BackingStoreException(canNotReadFile(), e);
}
final AbstractFeature feature = type.newInstance();
feature.setPropertyValue(identifiers.getName(), id.intValue(index));
for (int i=0; i<properties.length; i++) {
feature.setPropertyValue(properties[i].getName(), props[i]);
// TODO: set time characteristic.
}
// TODO: temporary hack - to be replaced by support in Vector.
final int dimension = coordinates.length;
final double[] tmp = new double[length * dimension];
for (int i=0; i<tmp.length; i++) {
tmp[i] = coords[i % dimension].doubleValue(i / dimension);
}
feature.setPropertyValue("trajectory", factory.createPolyline(dimension, Vector.create(tmp)));
action.accept(feature);
position = Math.addExact(position, length);
return ++index < counts.size();
}
/**
* Current implementation can not split this iterator.
*/
@Override
public Spliterator<AbstractFeature> trySplit() {
return null;
}
/**
* Returns the number of features.
*/
@Override
public long estimateSize() {
return counts.size();
}
/**
* Returns the characteristics of the iteration over feature instances.
* The iteration is assumed {@link #ORDERED} in the declaration order in the netCDF file.
* The iteration is {@link #NONNULL} (i.e. {@link #tryAdvance(Consumer)} is not allowed
* to return null value) and {@link #IMMUTABLE} (i.e. we do not support modification of
* the netCDF file while an iteration is in progress).
*
* @return characteristics of iteration over the features in the netCDF file.
*/
@Override
public int characteristics() {
return ORDERED | NONNULL | IMMUTABLE | SIZED;
}
}
}