blob: c60dbd9e0c6be962eeab11bc47a382ee1f9a2023 [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.feature;
import java.util.Map;
import java.util.HashMap;
import java.util.Date;
import java.util.Objects;
import java.util.logging.Level;
import java.util.logging.LogRecord;
import java.util.function.Consumer;
import java.lang.reflect.Array;
import java.time.Instant;
import org.opengis.util.LocalName;
import org.apache.sis.math.Vector;
import org.apache.sis.util.iso.Names;
import org.apache.sis.feature.DefaultAttributeType;
import org.apache.sis.util.CorruptedObjectException;
import org.apache.sis.internal.util.UnmodifiableArrayList;
// Branch-dependent imports
import org.opengis.feature.Attribute;
import org.opengis.feature.AttributeType;
import org.opengis.feature.Feature;
/**
* A feature implementation where the geometry is a trajectory and some property values may change with time.
* In current implementation this is a helper method for updating a {@code Feature} attribute.
* However in future versions, it could extend {@code DenseFeature} directly.
*
* @author Martin Desruisseaux (Geomatys)
* @version 0.8
* @since 0.8
* @module
*/
public final class MovingFeature {
/**
* Definition of characteristics containing a list of time instants in chronological order, without duplicates.
*/
public static final AttributeType<Instant> TIME;
static {
final LocalName scope = Names.createLocalName("OGC", null, "MF");
final Map<String,Object> properties = new HashMap<>(4);
properties.put(DefaultAttributeType.NAME_KEY, Names.createScopedName(scope, null, "datetimes"));
TIME = new DefaultAttributeType<>(properties, Instant.class, 0, Integer.MAX_VALUE, null);
}
/**
* The properties having values that may change in time.
* May contain the values of arbitrary properties (e.g. as {@link String} instances),
* or may contain the coordinates of part of a trajectory as array of primitive type like {@code float[]}.
* Trajectories may be specified in many parts if for example, the different parts are given on different
* lines of a CSV file.
*/
private final Period[] properties;
/**
* Number of {@code Property} instances added for each index of the {@link #properties} table.
*/
private final int[] count;
/**
* A dynamic property value together with the period of time in which this property is valid.
*/
private static final class Period {
/**
* Beginning in milliseconds since Java epoch of the period when the property value is valid.
*/
final long startTime;
/**
* End in milliseconds since Java epoch of the period when the the property value is valid.
* This end time will be adjusted if the property has the same valid in the next time step.
*/
long endTime;
/**
* The property value.
*/
final Object value;
/**
* Previous property in a chained list of properties.
*/
final Period previous;
/**
* Creates a new property value valid on the given period of time.
*/
Period(final Period previous, final long startTime, final long endTime, final Object value) {
this.previous = previous;
this.startTime = startTime;
this.endTime = endTime;
this.value = value;
}
}
/**
* Overall start and end time over all properties.
*/
private long tmin = Long.MAX_VALUE,
tmax = Long.MIN_VALUE;
/**
* Creates a new moving feature.
*
* @param numProperties maximal number of dynamic properties.
*/
public MovingFeature(final int numProperties) {
properties = new Period[numProperties];
count = new int [numProperties];
}
/**
* Adds a time range.
* The minimal and maximal values will be used by {@link #storeTimeRange(String, String, Feature)}.
*
* @param startTime beginning in milliseconds since Java epoch of the period when the property value is valid.
* @param endTime end in milliseconds since Java epoch of the period when the the property value is valid.
*/
public final void addTimeRange(final long startTime, final long endTime) {
if (startTime < tmin) tmin = startTime;
if ( endTime > tmax) tmax = endTime;
}
/**
* Adds a dynamic property value. This method shall be invoked with time periods in chronological order.
*
* @param index the property index.
* @param startTime beginning in milliseconds since Java epoch of the period when the property value is valid.
* @param endTime end in milliseconds since Java epoch of the period when the the property value is valid.
* @param value the property value which is valid during the given period.
*/
public final void addValue(final int index, final long startTime, final long endTime, final Object value) {
final Period p = properties[index];
if (p != null && p.endTime == startTime && Objects.equals(p.value, value)) {
p.endTime = endTime;
} else {
properties[index] = new Period(p, startTime, endTime, value);
count[index]++;
}
}
/**
* Stores the start time and end time in the given feature.
*
* @param startTime name of the property where to store the start time.
* @param endTime name of the property where to store the end time.
* @param dest feature where to store the start time and end time.
*/
public final void storeTimeRange(final String startTime, final String endTime, final Feature dest) {
if (tmin < tmax) {
final Instant t = Instant.ofEpochMilli(tmin);
dest.setPropertyValue(startTime, t);
dest.setPropertyValue(endTime, (tmin == tmax) ? t : Instant.ofEpochMilli(tmax));
}
}
/**
* Sets the values of the given attribute to the values collected by this {@code MovingFeatures}.
* This method sets also the {@code "datetimes"} characteristic.
*
* @param <V> the type of values in the given attribute.
* @param index index of the property for which values are desired.
* @param dest attribute where to store the value.
*/
@SuppressWarnings("unchecked")
public final <V> void storeAttribute(final int index, final Attribute<V> dest) {
int n = count[index];
final long[] times = new long[n];
final V[] values = (V[]) Array.newInstance(dest.getType().getValueClass(), n);
for (Period p = properties[index]; p != null; p = p.previous) {
times [--n] = p.startTime;
values[ n] = (V) p.value;
}
if (n != 0) {
// Should never happen unless this object has been modified concurrently in another thread.
throw new CorruptedObjectException();
}
dest.setValues(UnmodifiableArrayList.wrap(values));
final Attribute<Instant> c = TIME.newInstance();
c.setValues(new DateList(times));
dest.characteristics().values().add(c);
}
/**
* Sets the geometry of the given attribute to the values collected by this {@code MovingFeatures}.
* This method sets also the {@code "datetimes"} characteristic.
*
* @param <G> the type of the geometry value.
* @param featureName the name of the feature containing the attribute to update, for logging purpose.
* @param index index of the property for which geometry value is desired.
* @param dimension number of dimensions for all coordinates.
* @param factory the factory to use for creating the geometry object.
* @param dest attribute where to store the geometry value.
* @param warningListener where to report warnings. Implementation should set the source class name,
* source method name and logger name, then forward to a {@code WarningListener}.
*/
public final <G> void storeGeometry(final String featureName, final int index, final int dimension,
final Geometries<G> factory, final Attribute<G> dest, final Consumer<LogRecord> warningListener)
{
int n = count[index];
final Vector[] vectors = new Vector[n];
for (Period p = properties[index]; p != null; p = p.previous) {
vectors[--n] = Vector.create(p.value, false);
}
if (n != 0) {
// Should never happen unless this object has been modified concurrently in another thread.
throw new CorruptedObjectException();
}
int warnings = 10; // Maximal number of warnings, for avoiding to flood the logger.
int numPts = 0; // Total number of points in all vectors, ignoring null vectors.
Vector previous = null; // If non-null, shall be non-empty.
for (int i=0; i<vectors.length; i++) {
Vector v = vectors[i];
int length;
if (v == null || (length = v.size()) == 0) {
continue;
}
if ((length % dimension) != 0) {
if (--warnings >= 0) {
Period p = properties[index];
for (int j=i; --j >= 0;) { // This is inefficient but used only in case of warnings.
p = p.previous;
}
warningListener.accept(Resources.forLocale(null).getLogRecord(Level.WARNING,
Resources.Keys.UnexpectedNumberOfCoordinates_4, featureName, new Date(p.startTime), dimension, length));
}
continue;
}
/*
* At this point we have a non-empty valid sequence of coordinate values. If the first point of current
* vector is equals to the last point of previous vector, assume that they form a continuous polyline.
*/
if (previous != null) {
if (equals(previous, v, dimension)) {
v = v.subList(dimension, length); // Skip the first coordinate.
length -= dimension;
if (length == 0) {
vectors[i] = null;
continue;
}
vectors[i] = v;
}
}
numPts += length;
previous = v;
}
/*
* At this point we got the list of all coordinates to join together in a polyline.
* We will create the geometry at the end of this method. Before that, interpolate
* the dates and times.
*/
int i = vectors.length;
numPts /= dimension;
final long[] times = new long[numPts];
for (Period p = properties[index]; p != null; p = p.previous) {
final Vector v = vectors[--i];
if (v != null) {
int c = v.size() / dimension;
if (c == 1) {
times[--numPts] = p.endTime;
} else {
final long startTime = p.startTime;
final double scale = (p.endTime - startTime) / (double) (c-1);
while (--c >= 0) {
times[--numPts] = startTime + Math.round(scale * c);
}
}
}
}
if (numPts != 0) {
// Should never happen unless this object has been modified concurrently in another thread.
throw new CorruptedObjectException();
}
/*
* Store the geometry and characteristics in the attribute.
*/
dest.setValue(factory.createPolyline(dimension, vectors));
final Attribute<Instant> c = TIME.newInstance();
c.setValues(new DateList(times));
dest.characteristics().values().add(c);
}
/**
* Returns {@code true} if the last coordinate of the {@code previous} vector is equals to the first
* coordinate of the {@code next} vector.
*
* @param previous the previous vector.
* @param next the next vector.
* @param dimension number of dimension in each coordinate.
*/
private static boolean equals(final Vector previous, final Vector next, int dimension) {
int p = previous.size();
while (--dimension >= 0) {
if (next.doubleValue(dimension) != previous.doubleValue(--p)) {
return false;
}
}
return true;
}
}