| /* |
| * 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; |
| } |
| } |