001/**
002 * Copyright (c) 2008-2014 Ardor Labs, Inc.
003 *
004 * This file is part of Ardor3D.
005 *
006 * Ardor3D is free software: you can redistribute it and/or modify it 
007 * under the terms of its license which may be found in the accompanying
008 * LICENSE file or at <http://www.ardor3d.com/LICENSE>.
009 */
010
011/**
012 * 
013 */
014
015package com.ardor3d.spline;
016
017import java.io.Serializable;
018import java.util.ArrayList;
019import java.util.HashMap;
020import java.util.List;
021import java.util.Map;
022import java.util.logging.Level;
023import java.util.logging.Logger;
024
025import com.ardor3d.math.Vector3;
026import com.ardor3d.scenegraph.controller.ComplexSpatialController;
027import com.ardor3d.scenegraph.controller.interpolation.InterpolationController;
028
029/**
030 * ArcLengthTable class contains methods for generating and storing arc lengths of a curve. Arc Lengths are used to get
031 * constant speed interpolation over a curve.
032 * <p>
033 * This class does not automatically generate the look up tables, you must manually call {@link #generate(int, boolean)}
034 * to generate the table.
035 * </p>
036 */
037public class ArcLengthTable {
038
039    /** Classes logger */
040    private static final Logger LOGGER = Logger.getLogger(ArcLengthTable.class.getName());
041
042    /** Table containing arc lengths for look up */
043    private Map<Integer, List<ArcLengthEntry>> _lookupTable;
044
045    /** The curve who's values to cache */
046    private final Curve _curve;
047
048    /**
049     * Creates a new instance of <code>ArcLengthTable</code>.
050     * 
051     * @param curve
052     *            The curve to create the table for, can not be <code>null</code>.
053     */
054    public ArcLengthTable(final Curve curve) {
055        super();
056
057        if (null == curve) {
058            throw new IllegalArgumentException("curve was null!");
059        }
060
061        _curve = curve;
062    }
063
064    /**
065     * @param index
066     *            The index of the control point you want the length for.
067     * @return The total approximate length of the segment starting at the given index.
068     */
069    public double getLength(final int index) {
070        if (null == _lookupTable) {
071            throw new IllegalStateException(
072                    "You must generate the look up table before calling this method! see generate()");
073        }
074
075        final List<ArcLengthEntry> entries = _lookupTable.get(index);
076
077        if (null == entries) {
078            throw new IllegalArgumentException("entries was null, the index parameter was invalid. index=" + index);
079        }
080
081        final ArcLengthEntry arcLength = entries.get(entries.size() - 1);
082
083        return arcLength.getLength();
084    }
085
086    /**
087     * @param index
088     *            The index of the first control point you are interpolating from.
089     * @param distance
090     *            The distance you want the spatial to travel.
091     * @return The delta you should use to travel the specified distance from the control point given, will not be
092     *         negative but may be greater than 1.0 if the distance is greater than the length of the segment.
093     */
094    public double getDelta(final int index, final double distance) {
095        if (null == _lookupTable) {
096            throw new IllegalStateException(
097                    "You must generate the look up table before calling this method! see generate()");
098        }
099
100        ArcLengthEntry previous = null;
101        ArcLengthEntry next = null;
102
103        final List<ArcLengthEntry> entries = _lookupTable.get(index);
104
105        if (null == entries) {
106            throw new IllegalArgumentException("entries was null, the index parameter was invalid. index=" + index);
107        }
108
109        for (final ArcLengthEntry entry : entries) {
110            if (entry.getLength() <= distance) {
111                previous = entry;
112            }
113            if (entry.getLength() >= distance) {
114                next = entry;
115                break;
116            }
117        }
118
119        if (null == previous) {
120            throw new IllegalArgumentException(
121                    "previous was null, either the index or distance parameters were invalid. index=" + index
122                            + ", distance=" + distance);
123        }
124
125        final double delta;
126
127        /*
128         * If next is null then the length we need to travel is longer than the length of the segment, in this case we
129         * work out the delta required to travel from the start of the segment minus what we've already travelled. We
130         * then add that delta to the delta required to traverse the next segment and return that value, it's up to the
131         * controller to handle this value correctly (update indices etc)
132         * 
133         * We need to be careful about wrapping around the end of the curve.
134         */
135        if (null == next) {
136            final int newIndex = (index + 1 >= _lookupTable.size()) ? 1 : index + 1;
137
138            delta = getDelta(newIndex, distance - previous.getLength()) + previous.getDelta();
139
140        } else {
141            if (previous.equals(next)) {
142                delta = previous.getDelta();
143
144            } else {
145                final double d0 = previous.getDelta();
146                final double d1 = next.getDelta();
147                final double l0 = previous.getLength();
148                final double l1 = next.getLength();
149
150                delta = (d0 + ((distance - l0) / (l1 - l0)) * (d1 - d0));
151            }
152        }
153
154        return delta;
155    }
156
157    /**
158     * Actually generates the arc length table, this needs to be called before this class can actually perform any
159     * useful functions.
160     * 
161     * @param step
162     *            The larger the step value used the more accurate the resulting table will be and thus the smoother the
163     *            motion will be, must be greater than zero.
164     * @param reverse
165     *            <code>true</code> to generate the table while stepping from the end of the curve to the beginning,
166     *            <code>false</code> to generate the table from the beginning of the curve. You only need to generate a
167     *            reverse table if you are using the {@link ComplexSpatialController.RepeatType#CYCLE cycle} repeat
168     *            type.
169     */
170    public void generate(final int step, final boolean reverse) {
171        if (step <= 0) {
172            throw new IllegalArgumentException("step must be > 0! step=" + step);
173        }
174
175        _lookupTable = new HashMap<>();
176
177        final Vector3 target = Vector3.fetchTempInstance();
178        final Vector3 previous = Vector3.fetchTempInstance();
179
180        final int loopStart = reverse ? (_curve.getControlPointCount() - 2) : 1;
181        final double tStep = InterpolationController.DELTA_MAX / step;
182
183        for (int i = loopStart; continueLoop(i, reverse); i = updateCounter(i, reverse)) {
184            final int startIndex = i;
185            double t = 0f;
186            double length = 0;
187
188            previous.set(_curve.getControlPoints().get(i));
189
190            final ArrayList<ArcLengthEntry> entries = new ArrayList<>();
191            entries.add(new ArcLengthEntry(0f, 0));
192
193            final int endIndex = reverse ? startIndex - 1 : startIndex + 1;
194
195            while (true) {
196                t += tStep;
197
198                /*
199                 * If we are over delta max force to 1. We need to do this to avoid precision issues causing errors
200                 * later on (e.g. if last entry for an index had a delta of 0.996 and later during an update we passed
201                 * 0.998 for that index we'd fail to find a valid entry and error out)
202                 */
203                if (t > InterpolationController.DELTA_MAX) {
204                    t = InterpolationController.DELTA_MAX;
205                }
206
207                _curve.interpolate(startIndex, endIndex, t, target);
208
209                length += previous.distance(target);
210
211                previous.set(target);
212
213                entries.add(new ArcLengthEntry(t, length));
214
215                if (t == InterpolationController.DELTA_MAX) {
216                    break;
217                }
218            }
219
220            _lookupTable.put(i, entries);
221        }
222
223        if (LOGGER.isLoggable(Level.FINE)) {
224            LOGGER.fine("look up table = " + _lookupTable);
225        }
226
227        Vector3.releaseTempInstance(target);
228        Vector3.releaseTempInstance(previous);
229    }
230
231    private boolean continueLoop(final int i, final boolean reverse) {
232        return (reverse ? i > 0 : i < _curve.getControlPointCount() - 2);
233    }
234
235    private int updateCounter(final int i, final boolean reverse) {
236        return (reverse ? i - 1 : i + 1);
237    }
238
239    /**
240     * A private inner class used to store the required arc length variables for the table
241     */
242    private static class ArcLengthEntry implements Serializable {
243        /** Serial UID */
244        private static final long serialVersionUID = 1L;
245
246        private final double _delta;
247        private final double _length;
248
249        public ArcLengthEntry(final double delta, final double length) {
250            super();
251
252            _delta = delta;
253            _length = length;
254        }
255
256        public double getDelta() {
257            return _delta;
258        }
259
260        public double getLength() {
261            return _length;
262        }
263
264        @Override
265        public String toString() {
266            return "ArcLengthEntry[length=" + _length + ", delta=" + _delta + ']';
267        }
268    }
269
270}