1package coordinate
2
3import (
4	"math"
5	"reflect"
6	"testing"
7	"time"
8)
9
10// verifyDimensionPanic will run the supplied func and make sure it panics with
11// the expected error type.
12func verifyDimensionPanic(t *testing.T, f func()) {
13	defer func() {
14		if r := recover(); r != nil {
15			if _, ok := r.(DimensionalityConflictError); !ok {
16				t.Fatalf("panic isn't the right type")
17			}
18		} else {
19			t.Fatalf("didn't get expected panic")
20		}
21	}()
22	f()
23}
24
25func TestCoordinate_NewCoordinate(t *testing.T) {
26	config := DefaultConfig()
27	c := NewCoordinate(config)
28	if uint(len(c.Vec)) != config.Dimensionality {
29		t.Fatalf("dimensionality not set correctly %d != %d",
30			len(c.Vec), config.Dimensionality)
31	}
32}
33
34func TestCoordinate_Clone(t *testing.T) {
35	c := NewCoordinate(DefaultConfig())
36	c.Vec[0], c.Vec[1], c.Vec[2] = 1.0, 2.0, 3.0
37	c.Error = 5.0
38	c.Adjustment = 10.0
39	c.Height = 4.2
40
41	other := c.Clone()
42	if !reflect.DeepEqual(c, other) {
43		t.Fatalf("coordinate clone didn't make a proper copy")
44	}
45
46	other.Vec[0] = c.Vec[0] + 0.5
47	if reflect.DeepEqual(c, other) {
48		t.Fatalf("cloned coordinate is still pointing at its ancestor")
49	}
50}
51
52func TestCoordinate_IsValid(t *testing.T) {
53	c := NewCoordinate(DefaultConfig())
54
55	var fields []*float64
56	for i := range c.Vec {
57		fields = append(fields, &c.Vec[i])
58	}
59	fields = append(fields, &c.Error)
60	fields = append(fields, &c.Adjustment)
61	fields = append(fields, &c.Height)
62
63	for i, field := range fields {
64		if !c.IsValid() {
65			t.Fatalf("field %d should be valid", i)
66		}
67
68		*field = math.NaN()
69		if c.IsValid() {
70			t.Fatalf("field %d should not be valid (NaN)", i)
71		}
72
73		*field = 0.0
74		if !c.IsValid() {
75			t.Fatalf("field %d should be valid", i)
76		}
77
78		*field = math.Inf(0)
79		if c.IsValid() {
80			t.Fatalf("field %d should not be valid (Inf)", i)
81		}
82
83		*field = 0.0
84		if !c.IsValid() {
85			t.Fatalf("field %d should be valid", i)
86		}
87	}
88}
89
90func TestCoordinate_IsCompatibleWith(t *testing.T) {
91	config := DefaultConfig()
92
93	config.Dimensionality = 3
94	c1 := NewCoordinate(config)
95	c2 := NewCoordinate(config)
96
97	config.Dimensionality = 2
98	alien := NewCoordinate(config)
99
100	if !c1.IsCompatibleWith(c1) || !c2.IsCompatibleWith(c2) ||
101		!alien.IsCompatibleWith(alien) {
102		t.Fatalf("coordinates should be compatible with themselves")
103	}
104
105	if !c1.IsCompatibleWith(c2) || !c2.IsCompatibleWith(c1) {
106		t.Fatalf("coordinates should be compatible with each other")
107	}
108
109	if c1.IsCompatibleWith(alien) || c2.IsCompatibleWith(alien) ||
110		alien.IsCompatibleWith(c1) || alien.IsCompatibleWith(c2) {
111		t.Fatalf("alien should not be compatible with the other coordinates")
112	}
113}
114
115func TestCoordinate_ApplyForce(t *testing.T) {
116	config := DefaultConfig()
117	config.Dimensionality = 3
118	config.HeightMin = 0
119
120	origin := NewCoordinate(config)
121
122	// This proves that we normalize, get the direction right, and apply the
123	// force multiplier correctly.
124	above := NewCoordinate(config)
125	above.Vec = []float64{0.0, 0.0, 2.9}
126	c := origin.ApplyForce(config, 5.3, above)
127	verifyEqualVectors(t, c.Vec, []float64{0.0, 0.0, -5.3})
128
129	// Scoot a point not starting at the origin to make sure there's nothing
130	// special there.
131	right := NewCoordinate(config)
132	right.Vec = []float64{3.4, 0.0, -5.3}
133	c = c.ApplyForce(config, 2.0, right)
134	verifyEqualVectors(t, c.Vec, []float64{-2.0, 0.0, -5.3})
135
136	// If the points are right on top of each other, then we should end up
137	// in a random direction, one unit away. This makes sure the unit vector
138	// build up doesn't divide by zero.
139	c = origin.ApplyForce(config, 1.0, origin)
140	verifyEqualFloats(t, origin.DistanceTo(c).Seconds(), 1.0)
141
142	// Enable a minimum height and make sure that gets factored in properly.
143	config.HeightMin = 10.0e-6
144	origin = NewCoordinate(config)
145	c = origin.ApplyForce(config, 5.3, above)
146	verifyEqualVectors(t, c.Vec, []float64{0.0, 0.0, -5.3})
147	verifyEqualFloats(t, c.Height, config.HeightMin+5.3*config.HeightMin/2.9)
148
149	// Make sure the height minimum is enforced.
150	c = origin.ApplyForce(config, -5.3, above)
151	verifyEqualVectors(t, c.Vec, []float64{0.0, 0.0, 5.3})
152	verifyEqualFloats(t, c.Height, config.HeightMin)
153
154	// Shenanigans should get called if the dimensions don't match.
155	bad := c.Clone()
156	bad.Vec = make([]float64, len(bad.Vec)+1)
157	verifyDimensionPanic(t, func() { c.ApplyForce(config, 1.0, bad) })
158}
159
160func TestCoordinate_DistanceTo(t *testing.T) {
161	config := DefaultConfig()
162	config.Dimensionality = 3
163	config.HeightMin = 0
164
165	c1, c2 := NewCoordinate(config), NewCoordinate(config)
166	c1.Vec = []float64{-0.5, 1.3, 2.4}
167	c2.Vec = []float64{1.2, -2.3, 3.4}
168
169	verifyEqualFloats(t, c1.DistanceTo(c1).Seconds(), 0.0)
170	verifyEqualFloats(t, c1.DistanceTo(c2).Seconds(), c2.DistanceTo(c1).Seconds())
171	verifyEqualFloats(t, c1.DistanceTo(c2).Seconds(), 4.104875150354758)
172
173	// Make sure negative adjustment factors are ignored.
174	c1.Adjustment = -1.0e6
175	verifyEqualFloats(t, c1.DistanceTo(c2).Seconds(), 4.104875150354758)
176
177	// Make sure positive adjustment factors affect the distance.
178	c1.Adjustment = 0.1
179	c2.Adjustment = 0.2
180	verifyEqualFloats(t, c1.DistanceTo(c2).Seconds(), 4.104875150354758+0.3)
181
182	// Make sure the heights affect the distance.
183	c1.Height = 0.7
184	c2.Height = 0.1
185	verifyEqualFloats(t, c1.DistanceTo(c2).Seconds(), 4.104875150354758+0.3+0.8)
186
187	// Shenanigans should get called if the dimensions don't match.
188	bad := c1.Clone()
189	bad.Vec = make([]float64, len(bad.Vec)+1)
190	verifyDimensionPanic(t, func() { _ = c1.DistanceTo(bad) })
191}
192
193// dist is a self-contained example that appears in documentation.
194func dist(a *Coordinate, b *Coordinate) time.Duration {
195	// Coordinates will always have the same dimensionality, so this is
196	// just a sanity check.
197	if len(a.Vec) != len(b.Vec) {
198		panic("dimensions aren't compatible")
199	}
200
201	// Calculate the Euclidean distance plus the heights.
202	sumsq := 0.0
203	for i := 0; i < len(a.Vec); i++ {
204		diff := a.Vec[i] - b.Vec[i]
205		sumsq += diff * diff
206	}
207	rtt := math.Sqrt(sumsq) + a.Height + b.Height
208
209	// Apply the adjustment components, guarding against negatives.
210	adjusted := rtt + a.Adjustment + b.Adjustment
211	if adjusted > 0.0 {
212		rtt = adjusted
213	}
214
215	// Go's times are natively nanoseconds, so we convert from seconds.
216	const secondsToNanoseconds = 1.0e9
217	return time.Duration(rtt * secondsToNanoseconds)
218}
219
220func TestCoordinate_dist_Example(t *testing.T) {
221	config := DefaultConfig()
222	c1, c2 := NewCoordinate(config), NewCoordinate(config)
223	c1.Vec = []float64{-0.5, 1.3, 2.4}
224	c2.Vec = []float64{1.2, -2.3, 3.4}
225	c1.Adjustment = 0.1
226	c2.Adjustment = 0.2
227	c1.Height = 0.7
228	c2.Height = 0.1
229	verifyEqualFloats(t, c1.DistanceTo(c2).Seconds(), dist(c1, c2).Seconds())
230}
231
232func TestCoordinate_rawDistanceTo(t *testing.T) {
233	config := DefaultConfig()
234	config.Dimensionality = 3
235	config.HeightMin = 0
236
237	c1, c2 := NewCoordinate(config), NewCoordinate(config)
238	c1.Vec = []float64{-0.5, 1.3, 2.4}
239	c2.Vec = []float64{1.2, -2.3, 3.4}
240
241	verifyEqualFloats(t, c1.rawDistanceTo(c1), 0.0)
242	verifyEqualFloats(t, c1.rawDistanceTo(c2), c2.rawDistanceTo(c1))
243	verifyEqualFloats(t, c1.rawDistanceTo(c2), 4.104875150354758)
244
245	// Make sure that the adjustment doesn't factor into the raw
246	// distance.
247	c1.Adjustment = 1.0e6
248	verifyEqualFloats(t, c1.rawDistanceTo(c2), 4.104875150354758)
249
250	// Make sure the heights affect the distance.
251	c1.Height = 0.7
252	c2.Height = 0.1
253	verifyEqualFloats(t, c1.rawDistanceTo(c2), 4.104875150354758+0.8)
254}
255
256func TestCoordinate_add(t *testing.T) {
257	vec1 := []float64{1.0, -3.0, 3.0}
258	vec2 := []float64{-4.0, 5.0, 6.0}
259	verifyEqualVectors(t, add(vec1, vec2), []float64{-3.0, 2.0, 9.0})
260
261	zero := []float64{0.0, 0.0, 0.0}
262	verifyEqualVectors(t, add(vec1, zero), vec1)
263}
264
265func TestCoordinate_diff(t *testing.T) {
266	vec1 := []float64{1.0, -3.0, 3.0}
267	vec2 := []float64{-4.0, 5.0, 6.0}
268	verifyEqualVectors(t, diff(vec1, vec2), []float64{5.0, -8.0, -3.0})
269
270	zero := []float64{0.0, 0.0, 0.0}
271	verifyEqualVectors(t, diff(vec1, zero), vec1)
272}
273
274func TestCoordinate_magnitude(t *testing.T) {
275	zero := []float64{0.0, 0.0, 0.0}
276	verifyEqualFloats(t, magnitude(zero), 0.0)
277
278	vec := []float64{1.0, -2.0, 3.0}
279	verifyEqualFloats(t, magnitude(vec), 3.7416573867739413)
280}
281
282func TestCoordinate_unitVectorAt(t *testing.T) {
283	vec1 := []float64{1.0, 2.0, 3.0}
284	vec2 := []float64{0.5, 0.6, 0.7}
285	u, mag := unitVectorAt(vec1, vec2)
286	verifyEqualVectors(t, u, []float64{0.18257418583505536, 0.511207720338155, 0.8398412548412546})
287	verifyEqualFloats(t, magnitude(u), 1.0)
288	verifyEqualFloats(t, mag, magnitude(diff(vec1, vec2)))
289
290	// If we give positions that are equal we should get a random unit vector
291	// returned to us, rather than a divide by zero.
292	u, mag = unitVectorAt(vec1, vec1)
293	verifyEqualFloats(t, magnitude(u), 1.0)
294	verifyEqualFloats(t, mag, 0.0)
295
296	// We can't hit the final clause without heroics so I manually forced it
297	// there to verify it works.
298}
299