• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2012 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.util;
18 
19 /**
20  * Performs spline interpolation given a set of control points.
21  * @hide
22  */
23 public abstract class Spline {
24 
25     /**
26      * Interpolates the value of Y = f(X) for given X.
27      * Clamps X to the domain of the spline.
28      *
29      * @param x The X value.
30      * @return The interpolated Y = f(X) value.
31      */
interpolate(float x)32     public abstract float interpolate(float x);
33 
34     /**
35      * Creates an appropriate spline based on the properties of the control points.
36      *
37      * If the control points are monotonic then the resulting spline will preserve that and
38      * otherwise optimize for error bounds.
39      */
createSpline(float[] x, float[] y)40     public static Spline createSpline(float[] x, float[] y) {
41         if (!isStrictlyIncreasing(x)) {
42             throw new IllegalArgumentException("The control points must all have strictly "
43                     + "increasing X values.");
44         }
45 
46         if (isMonotonic(y)) {
47             return createMonotoneCubicSpline(x, y);
48         } else {
49             return createLinearSpline(x, y);
50         }
51     }
52 
53     /**
54      * Creates a monotone cubic spline from a given set of control points.
55      *
56      * The spline is guaranteed to pass through each control point exactly.
57      * Moreover, assuming the control points are monotonic (Y is non-decreasing or
58      * non-increasing) then the interpolated values will also be monotonic.
59      *
60      * This function uses the Fritsch-Carlson method for computing the spline parameters.
61      * http://en.wikipedia.org/wiki/Monotone_cubic_interpolation
62      *
63      * @param x The X component of the control points, strictly increasing.
64      * @param y The Y component of the control points, monotonic.
65      * @return
66      *
67      * @throws IllegalArgumentException if the X or Y arrays are null, have
68      * different lengths or have fewer than 2 values.
69      * @throws IllegalArgumentException if the control points are not monotonic.
70      */
createMonotoneCubicSpline(float[] x, float[] y)71     public static Spline createMonotoneCubicSpline(float[] x, float[] y) {
72         return new MonotoneCubicSpline(x, y);
73     }
74 
75     /**
76      * Creates a linear spline from a given set of control points.
77      *
78      * Like a monotone cubic spline, the interpolated curve will be monotonic if the control points
79      * are monotonic.
80      *
81      * @param x The X component of the control points, strictly increasing.
82      * @param y The Y component of the control points.
83      * @return
84      *
85      * @throws IllegalArgumentException if the X or Y arrays are null, have
86      * different lengths or have fewer than 2 values.
87      * @throws IllegalArgumentException if the X components of the control points are not strictly
88      * increasing.
89      */
createLinearSpline(float[] x, float[] y)90     public static Spline createLinearSpline(float[] x, float[] y) {
91         return new LinearSpline(x, y);
92     }
93 
isStrictlyIncreasing(float[] x)94     private static boolean isStrictlyIncreasing(float[] x) {
95         if (x == null || x.length < 2) {
96             throw new IllegalArgumentException("There must be at least two control points.");
97         }
98         float prev = x[0];
99         for (int i = 1; i < x.length; i++) {
100             float curr = x[i];
101             if (curr <= prev) {
102                 return false;
103             }
104             prev = curr;
105         }
106         return true;
107     }
108 
isMonotonic(float[] x)109     private static boolean isMonotonic(float[] x) {
110         if (x == null || x.length < 2) {
111             throw new IllegalArgumentException("There must be at least two control points.");
112         }
113         float prev = x[0];
114         for (int i = 1; i < x.length; i++) {
115             float curr = x[i];
116             if (curr < prev) {
117                 return false;
118             }
119             prev = curr;
120         }
121         return true;
122     }
123 
124     public static class MonotoneCubicSpline extends Spline {
125         private float[] mX;
126         private float[] mY;
127         private float[] mM;
128 
MonotoneCubicSpline(float[] x, float[] y)129         public MonotoneCubicSpline(float[] x, float[] y) {
130             if (x == null || y == null || x.length != y.length || x.length < 2) {
131                 throw new IllegalArgumentException("There must be at least two control "
132                         + "points and the arrays must be of equal length.");
133             }
134 
135             final int n = x.length;
136             float[] d = new float[n - 1]; // could optimize this out
137             float[] m = new float[n];
138 
139             // Compute slopes of secant lines between successive points.
140             for (int i = 0; i < n - 1; i++) {
141                 float h = x[i + 1] - x[i];
142                 if (h <= 0f) {
143                     throw new IllegalArgumentException("The control points must all "
144                             + "have strictly increasing X values.");
145                 }
146                 d[i] = (y[i + 1] - y[i]) / h;
147             }
148 
149             // Initialize the tangents as the average of the secants.
150             m[0] = d[0];
151             for (int i = 1; i < n - 1; i++) {
152                 m[i] = (d[i - 1] + d[i]) * 0.5f;
153             }
154             m[n - 1] = d[n - 2];
155 
156             // Update the tangents to preserve monotonicity.
157             for (int i = 0; i < n - 1; i++) {
158                 if (d[i] == 0f) { // successive Y values are equal
159                     m[i] = 0f;
160                     m[i + 1] = 0f;
161                 } else {
162                     float a = m[i] / d[i];
163                     float b = m[i + 1] / d[i];
164                     if (a < 0f || b < 0f) {
165                         throw new IllegalArgumentException("The control points must have "
166                                 + "monotonic Y values.");
167                     }
168                     float h = (float) Math.hypot(a, b);
169                     if (h > 3f) {
170                         float t = 3f / h;
171                         m[i] *= t;
172                         m[i + 1] *= t;
173                     }
174                 }
175             }
176 
177             mX = x;
178             mY = y;
179             mM = m;
180         }
181 
182         @Override
interpolate(float x)183         public float interpolate(float x) {
184             // Handle the boundary cases.
185             final int n = mX.length;
186             if (Float.isNaN(x)) {
187                 return x;
188             }
189             if (x <= mX[0]) {
190                 return mY[0];
191             }
192             if (x >= mX[n - 1]) {
193                 return mY[n - 1];
194             }
195 
196             // Find the index 'i' of the last point with smaller X.
197             // We know this will be within the spline due to the boundary tests.
198             int i = 0;
199             while (x >= mX[i + 1]) {
200                 i += 1;
201                 if (x == mX[i]) {
202                     return mY[i];
203                 }
204             }
205 
206             // Perform cubic Hermite spline interpolation.
207             float h = mX[i + 1] - mX[i];
208             float t = (x - mX[i]) / h;
209             return (mY[i] * (1 + 2 * t) + h * mM[i] * t) * (1 - t) * (1 - t)
210                     + (mY[i + 1] * (3 - 2 * t) + h * mM[i + 1] * (t - 1)) * t * t;
211         }
212 
213         // For debugging.
214         @Override
toString()215         public String toString() {
216             StringBuilder str = new StringBuilder();
217             final int n = mX.length;
218             str.append("MonotoneCubicSpline{[");
219             for (int i = 0; i < n; i++) {
220                 if (i != 0) {
221                     str.append(", ");
222                 }
223                 str.append("(").append(mX[i]);
224                 str.append(", ").append(mY[i]);
225                 str.append(": ").append(mM[i]).append(")");
226             }
227             str.append("]}");
228             return str.toString();
229         }
230     }
231 
232     public static class LinearSpline extends Spline {
233         private final float[] mX;
234         private final float[] mY;
235         private final float[] mM;
236 
LinearSpline(float[] x, float[] y)237         public LinearSpline(float[] x, float[] y) {
238             if (x == null || y == null || x.length != y.length || x.length < 2) {
239                 throw new IllegalArgumentException("There must be at least two control "
240                         + "points and the arrays must be of equal length.");
241             }
242             final int N = x.length;
243             mM = new float[N-1];
244             for (int i = 0; i < N-1; i++) {
245                 mM[i] = (y[i+1] - y[i]) / (x[i+1] - x[i]);
246             }
247             mX = x;
248             mY = y;
249         }
250 
251         @Override
interpolate(float x)252         public float interpolate(float x) {
253             // Handle the boundary cases.
254             final int n = mX.length;
255             if (Float.isNaN(x)) {
256                 return x;
257             }
258             if (x <= mX[0]) {
259                 return mY[0];
260             }
261             if (x >= mX[n - 1]) {
262                 return mY[n - 1];
263             }
264 
265             // Find the index 'i' of the last point with smaller X.
266             // We know this will be within the spline due to the boundary tests.
267             int i = 0;
268             while (x >= mX[i + 1]) {
269                 i += 1;
270                 if (x == mX[i]) {
271                     return mY[i];
272                 }
273             }
274             return mY[i] + mM[i] * (x - mX[i]);
275         }
276 
277         @Override
toString()278         public String toString() {
279             StringBuilder str = new StringBuilder();
280             final int n = mX.length;
281             str.append("LinearSpline{[");
282             for (int i = 0; i < n; i++) {
283                 if (i != 0) {
284                     str.append(", ");
285                 }
286                 str.append("(").append(mX[i]);
287                 str.append(", ").append(mY[i]);
288                 if (i < n-1) {
289                     str.append(": ").append(mM[i]);
290                 }
291                 str.append(")");
292             }
293             str.append("]}");
294             return str.toString();
295         }
296     }
297 }
298