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}