1 // Note, we are using GEX from trunk, r66
  2 
  3 /*
  4 Copyright 2009 Google Inc.
  5 
  6 Licensed under the Apache License, Version 2.0 (the "License");
  7 you may not use this file except in compliance with the License.
  8 You may obtain a copy of the License at
  9 
 10      http://www.apache.org/licenses/LICENSE-2.0
 11 
 12 Unless required by applicable law or agreed to in writing, software
 13 distributed under the License is distributed on an "AS IS" BASIS,
 14 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 15 See the License for the specific language governing permissions and
 16 limitations under the License.
 17 */
 18 (function() {
 19 /**
 20  * The geo namespace contains generic classes and namespaces for processing
 21  * geographic data in JavaScript. Where possible, an effort was made to keep
 22  * the library compatible with the Google Geo APIs (Maps, Earth, KML, etc.)
 23  * @namespace
 24  */
 25 var geo = {isnamespace_:true};
 26 /*
 27 Copyright 2009 Google Inc.
 28 
 29 Licensed under the Apache License, Version 2.0 (the "License");
 30 you may not use this file except in compliance with the License.
 31 You may obtain a copy of the License at
 32 
 33      http://www.apache.org/licenses/LICENSE-2.0
 34 
 35 Unless required by applicable law or agreed to in writing, software
 36 distributed under the License is distributed on an "AS IS" BASIS,
 37 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 38 See the License for the specific language governing permissions and
 39 limitations under the License.
 40 */
 41 // TODO: geo.ALTITUDE_NONE to differentiate 2D/3D coordinates
 42 geo.ALTITUDE_CLAMP_TO_GROUND = 0;
 43 geo.ALTITUDE_RELATIVE_TO_GROUND = 1;
 44 geo.ALTITUDE_ABSOLUTE = 2;
 45 geo.ALTITUDE_CLAMP_TO_SEA_FLOOR = 4;
 46 geo.ALTITUDE_RELATIVE_TO_SEA_FLOOR = 5;
 47 /*
 48  * This is an excerpt from the Sylvester linear algebra library, MIT-licensed.
 49  */
 50 // This file is required in order for any other classes to work. Some Vector methods work with the
 51 // other Sylvester classes and are useless unless they are included. Other classes such as Line and
 52 // Plane will not function at all without Vector being loaded first.
 53 
 54 var Sylvester = {
 55   precision: 1e-6
 56 };
 57 
 58 function Vector() {}
 59 Vector.prototype = {
 60 
 61   // Returns element i of the vector
 62   e: function(i) {
 63     return (i < 1 || i > this.elements.length) ? null : this.elements[i-1];
 64   },
 65 
 66   // Returns the number of elements the vector has
 67   dimensions: function() {
 68     return this.elements.length;
 69   },
 70 
 71   // Returns the modulus ('length') of the vector
 72   modulus: function() {
 73     return Math.sqrt(this.dot(this));
 74   },
 75 
 76   // Returns true iff the vector is equal to the argument
 77   eql: function(vector) {
 78     var n = this.elements.length;
 79     var V = vector.elements || vector;
 80     if (n != V.length) { return false; }
 81     while (n--) {
 82       if (Math.abs(this.elements[n] - V[n]) > Sylvester.precision) { return false; }
 83     }
 84     return true;
 85   },
 86 
 87   // Returns a copy of the vector
 88   dup: function() {
 89     return Vector.create(this.elements);
 90   },
 91 
 92   // Maps the vector to another vector according to the given function
 93   map: function(fn) {
 94     var elements = [];
 95     this.each(function(x, i) {
 96       elements.push(fn(x, i));
 97     });
 98     return Vector.create(elements);
 99   },
100 
101   // Calls the iterator for each element of the vector in turn
102   each: function(fn) {
103     var n = this.elements.length;
104     for (var i = 0; i < n; i++) {
105       fn(this.elements[i], i+1);
106     }
107   },
108 
109   // Returns a new vector created by normalizing the receiver
110   toUnitVector: function() {
111     var r = this.modulus();
112     if (r === 0) { return this.dup(); }
113     return this.map(function(x) { return x/r; });
114   },
115 
116   // Returns the angle between the vector and the argument (also a vector)
117   angleFrom: function(vector) {
118     var V = vector.elements || vector;
119     var n = this.elements.length, k = n, i;
120     if (n != V.length) { return null; }
121     var dot = 0, mod1 = 0, mod2 = 0;
122     // Work things out in parallel to save time
123     this.each(function(x, i) {
124       dot += x * V[i-1];
125       mod1 += x * x;
126       mod2 += V[i-1] * V[i-1];
127     });
128     mod1 = Math.sqrt(mod1); mod2 = Math.sqrt(mod2);
129     if (mod1*mod2 === 0) { return null; }
130     var theta = dot / (mod1*mod2);
131     if (theta < -1) { theta = -1; }
132     if (theta > 1) { theta = 1; }
133     return Math.acos(theta);
134   },
135 
136   // Returns true iff the vector is parallel to the argument
137   isParallelTo: function(vector) {
138     var angle = this.angleFrom(vector);
139     return (angle === null) ? null : (angle <= Sylvester.precision);
140   },
141 
142   // Returns true iff the vector is antiparallel to the argument
143   isAntiparallelTo: function(vector) {
144     var angle = this.angleFrom(vector);
145     return (angle === null) ? null : (Math.abs(angle - Math.PI) <= Sylvester.precision);
146   },
147 
148   // Returns true iff the vector is perpendicular to the argument
149   isPerpendicularTo: function(vector) {
150     var dot = this.dot(vector);
151     return (dot === null) ? null : (Math.abs(dot) <= Sylvester.precision);
152   },
153 
154   // Returns the result of adding the argument to the vector
155   add: function(vector) {
156     var V = vector.elements || vector;
157     if (this.elements.length != V.length) { return null; }
158     return this.map(function(x, i) { return x + V[i-1]; });
159   },
160 
161   // Returns the result of subtracting the argument from the vector
162   subtract: function(vector) {
163     var V = vector.elements || vector;
164     if (this.elements.length != V.length) { return null; }
165     return this.map(function(x, i) { return x - V[i-1]; });
166   },
167 
168   // Returns the result of multiplying the elements of the vector by the argument
169   multiply: function(k) {
170     return this.map(function(x) { return x*k; });
171   },
172 
173   x: function(k) { return this.multiply(k); },
174 
175   // Returns the scalar product of the vector with the argument
176   // Both vectors must have equal dimensionality
177   dot: function(vector) {
178     var V = vector.elements || vector;
179     var i, product = 0, n = this.elements.length;
180     if (n != V.length) { return null; }
181     while (n--) { product += this.elements[n] * V[n]; }
182     return product;
183   },
184 
185   // Returns the vector product of the vector with the argument
186   // Both vectors must have dimensionality 3
187   cross: function(vector) {
188     var B = vector.elements || vector;
189     if (this.elements.length != 3 || B.length != 3) { return null; }
190     var A = this.elements;
191     return Vector.create([
192       (A[1] * B[2]) - (A[2] * B[1]),
193       (A[2] * B[0]) - (A[0] * B[2]),
194       (A[0] * B[1]) - (A[1] * B[0])
195     ]);
196   },
197 
198   // Returns the (absolute) largest element of the vector
199   max: function() {
200     var m = 0, i = this.elements.length;
201     while (i--) {
202       if (Math.abs(this.elements[i]) > Math.abs(m)) { m = this.elements[i]; }
203     }
204     return m;
205   },
206 
207   // Returns the index of the first match found
208   indexOf: function(x) {
209     var index = null, n = this.elements.length;
210     for (var i = 0; i < n; i++) {
211       if (index === null && this.elements[i] == x) {
212         index = i + 1;
213       }
214     }
215     return index;
216   },
217 
218   // Returns a diagonal matrix with the vector's elements as its diagonal elements
219   toDiagonalMatrix: function() {
220     return Matrix.Diagonal(this.elements);
221   },
222 
223   // Returns the result of rounding the elements of the vector
224   round: function() {
225     return this.map(function(x) { return Math.round(x); });
226   },
227 
228   // Returns a copy of the vector with elements set to the given value if they
229   // differ from it by less than Sylvester.precision
230   snapTo: function(x) {
231     return this.map(function(y) {
232       return (Math.abs(y - x) <= Sylvester.precision) ? x : y;
233     });
234   },
235 
236   // Returns the vector's distance from the argument, when considered as a point in space
237   distanceFrom: function(obj) {
238     if (obj.anchor || (obj.start && obj.end)) { return obj.distanceFrom(this); }
239     var V = obj.elements || obj;
240     if (V.length != this.elements.length) { return null; }
241     var sum = 0, part;
242     this.each(function(x, i) {
243       part = x - V[i-1];
244       sum += part * part;
245     });
246     return Math.sqrt(sum);
247   },
248 
249   // Returns true if the vector is point on the given line
250   liesOn: function(line) {
251     return line.contains(this);
252   },
253 
254   // Return true iff the vector is a point in the given plane
255   liesIn: function(plane) {
256     return plane.contains(this);
257   },
258 
259   // Rotates the vector about the given object. The object should be a 
260   // point if the vector is 2D, and a line if it is 3D. Be careful with line directions!
261   rotate: function(t, obj) {
262     var V, R = null, x, y, z;
263     if (t.determinant) { R = t.elements; }
264     switch (this.elements.length) {
265       case 2:
266         V = obj.elements || obj;
267         if (V.length != 2) { return null; }
268         if (!R) { R = Matrix.Rotation(t).elements; }
269         x = this.elements[0] - V[0];
270         y = this.elements[1] - V[1];
271         return Vector.create([
272           V[0] + R[0][0] * x + R[0][1] * y,
273           V[1] + R[1][0] * x + R[1][1] * y
274         ]);
275         break;
276       case 3:
277         if (!obj.direction) { return null; }
278         var C = obj.pointClosestTo(this).elements;
279         if (!R) { R = Matrix.Rotation(t, obj.direction).elements; }
280         x = this.elements[0] - C[0];
281         y = this.elements[1] - C[1];
282         z = this.elements[2] - C[2];
283         return Vector.create([
284           C[0] + R[0][0] * x + R[0][1] * y + R[0][2] * z,
285           C[1] + R[1][0] * x + R[1][1] * y + R[1][2] * z,
286           C[2] + R[2][0] * x + R[2][1] * y + R[2][2] * z
287         ]);
288         break;
289       default:
290         return null;
291     }
292   },
293 
294   // Returns the result of reflecting the point in the given point, line or plane
295   reflectionIn: function(obj) {
296     if (obj.anchor) {
297       // obj is a plane or line
298       var P = this.elements.slice();
299       var C = obj.pointClosestTo(P).elements;
300       return Vector.create([C[0] + (C[0] - P[0]), C[1] + (C[1] - P[1]), C[2] + (C[2] - (P[2] || 0))]);
301     } else {
302       // obj is a point
303       var Q = obj.elements || obj;
304       if (this.elements.length != Q.length) { return null; }
305       return this.map(function(x, i) { return Q[i-1] + (Q[i-1] - x); });
306     }
307   },
308 
309   // Utility to make sure vectors are 3D. If they are 2D, a zero z-component is added
310   to3D: function() {
311     var V = this.dup();
312     switch (V.elements.length) {
313       case 3: break;
314       case 2: V.elements.push(0); break;
315       default: return null;
316     }
317     return V;
318   },
319 
320   // Returns a string representation of the vector
321   inspect: function() {
322     return '[' + this.elements.join(', ') + ']';
323   },
324 
325   // Set vector's elements from an array
326   setElements: function(els) {
327     this.elements = (els.elements || els).slice();
328     return this;
329   }
330 };
331 
332 // Constructor function
333 Vector.create = function(elements) {
334   var V = new Vector();
335   return V.setElements(elements);
336 };
337 var $V = Vector.create;
338 
339 // i, j, k unit vectors
340 Vector.i = Vector.create([1,0,0]);
341 Vector.j = Vector.create([0,1,0]);
342 Vector.k = Vector.create([0,0,1]);
343 
344 // Random vector of size n
345 Vector.Random = function(n) {
346   var elements = [];
347   while (n--) { elements.push(Math.random()); }
348   return Vector.create(elements);
349 };
350 
351 // Vector filled with zeros
352 Vector.Zero = function(n) {
353   var elements = [];
354   while (n--) { elements.push(0); }
355   return Vector.create(elements);
356 };// Matrix class - depends on Vector.
357 
358 function Matrix() {}
359 Matrix.prototype = {
360 
361   // Returns element (i,j) of the matrix
362   e: function(i,j) {
363     if (i < 1 || i > this.elements.length || j < 1 || j > this.elements[0].length) { return null; }
364     return this.elements[i-1][j-1];
365   },
366 
367   // Returns row k of the matrix as a vector
368   row: function(i) {
369     if (i > this.elements.length) { return null; }
370     return Vector.create(this.elements[i-1]);
371   },
372 
373   // Returns column k of the matrix as a vector
374   col: function(j) {
375     if (j > this.elements[0].length) { return null; }
376     var col = [], n = this.elements.length;
377     for (var i = 0; i < n; i++) { col.push(this.elements[i][j-1]); }
378     return Vector.create(col);
379   },
380 
381   // Returns the number of rows/columns the matrix has
382   dimensions: function() {
383     return {rows: this.elements.length, cols: this.elements[0].length};
384   },
385 
386   // Returns the number of rows in the matrix
387   rows: function() {
388     return this.elements.length;
389   },
390 
391   // Returns the number of columns in the matrix
392   cols: function() {
393     return this.elements[0].length;
394   },
395 
396   // Returns true iff the matrix is equal to the argument. You can supply
397   // a vector as the argument, in which case the receiver must be a
398   // one-column matrix equal to the vector.
399   eql: function(matrix) {
400     var M = matrix.elements || matrix;
401     if (typeof(M[0][0]) == 'undefined') { M = Matrix.create(M).elements; }
402     if (this.elements.length != M.length ||
403         this.elements[0].length != M[0].length) { return false; }
404     var i = this.elements.length, nj = this.elements[0].length, j;
405     while (i--) { j = nj;
406       while (j--) {
407         if (Math.abs(this.elements[i][j] - M[i][j]) > Sylvester.precision) { return false; }
408       }
409     }
410     return true;
411   },
412 
413   // Returns a copy of the matrix
414   dup: function() {
415     return Matrix.create(this.elements);
416   },
417 
418   // Maps the matrix to another matrix (of the same dimensions) according to the given function
419   map: function(fn) {
420     var els = [], i = this.elements.length, nj = this.elements[0].length, j;
421     while (i--) { j = nj;
422       els[i] = [];
423       while (j--) {
424         els[i][j] = fn(this.elements[i][j], i + 1, j + 1);
425       }
426     }
427     return Matrix.create(els);
428   },
429 
430   // Returns true iff the argument has the same dimensions as the matrix
431   isSameSizeAs: function(matrix) {
432     var M = matrix.elements || matrix;
433     if (typeof(M[0][0]) == 'undefined') { M = Matrix.create(M).elements; }
434     return (this.elements.length == M.length &&
435         this.elements[0].length == M[0].length);
436   },
437 
438   // Returns the result of adding the argument to the matrix
439   add: function(matrix) {
440     var M = matrix.elements || matrix;
441     if (typeof(M[0][0]) == 'undefined') { M = Matrix.create(M).elements; }
442     if (!this.isSameSizeAs(M)) { return null; }
443     return this.map(function(x, i, j) { return x + M[i-1][j-1]; });
444   },
445 
446   // Returns the result of subtracting the argument from the matrix
447   subtract: function(matrix) {
448     var M = matrix.elements || matrix;
449     if (typeof(M[0][0]) == 'undefined') { M = Matrix.create(M).elements; }
450     if (!this.isSameSizeAs(M)) { return null; }
451     return this.map(function(x, i, j) { return x - M[i-1][j-1]; });
452   },
453 
454   // Returns true iff the matrix can multiply the argument from the left
455   canMultiplyFromLeft: function(matrix) {
456     var M = matrix.elements || matrix;
457     if (typeof(M[0][0]) == 'undefined') { M = Matrix.create(M).elements; }
458     // this.columns should equal matrix.rows
459     return (this.elements[0].length == M.length);
460   },
461 
462   // Returns the result of multiplying the matrix from the right by the argument.
463   // If the argument is a scalar then just multiply all the elements. If the argument is
464   // a vector, a vector is returned, which saves you having to remember calling
465   // col(1) on the result.
466   multiply: function(matrix) {
467     if (!matrix.elements) {
468       return this.map(function(x) { return x * matrix; });
469     }
470     var returnVector = matrix.modulus ? true : false;
471     var M = matrix.elements || matrix;
472     if (typeof(M[0][0]) == 'undefined') { M = Matrix.create(M).elements; }
473     if (!this.canMultiplyFromLeft(M)) { return null; }
474     var i = this.elements.length, nj = M[0].length, j;
475     var cols = this.elements[0].length, c, elements = [], sum;
476     while (i--) { j = nj;
477       elements[i] = [];
478       while (j--) { c = cols;
479         sum = 0;
480         while (c--) {
481           sum += this.elements[i][c] * M[c][j];
482         }
483         elements[i][j] = sum;
484       }
485     }
486     var M = Matrix.create(elements);
487     return returnVector ? M.col(1) : M;
488   },
489 
490   x: function(matrix) { return this.multiply(matrix); },
491 
492   // Returns a submatrix taken from the matrix
493   // Argument order is: start row, start col, nrows, ncols
494   // Element selection wraps if the required index is outside the matrix's bounds, so you could
495   // use this to perform row/column cycling or copy-augmenting.
496   minor: function(a, b, c, d) {
497     var elements = [], ni = c, i, nj, j;
498     var rows = this.elements.length, cols = this.elements[0].length;
499     while (ni--) { i = c - ni - 1;
500       elements[i] = [];
501       nj = d;
502       while (nj--) { j = d - nj - 1;
503         elements[i][j] = this.elements[(a+i-1)%rows][(b+j-1)%cols];
504       }
505     }
506     return Matrix.create(elements);
507   },
508 
509   // Returns the transpose of the matrix
510   transpose: function() {
511     var rows = this.elements.length, i, cols = this.elements[0].length, j;
512     var elements = [], i = cols;
513     while (i--) { j = rows;
514       elements[i] = [];
515       while (j--) {
516         elements[i][j] = this.elements[j][i];
517       }
518     }
519     return Matrix.create(elements);
520   },
521 
522   // Returns true iff the matrix is square
523   isSquare: function() {
524     return (this.elements.length == this.elements[0].length);
525   },
526 
527   // Returns the (absolute) largest element of the matrix
528   max: function() {
529     var m = 0, i = this.elements.length, nj = this.elements[0].length, j;
530     while (i--) { j = nj;
531       while (j--) {
532         if (Math.abs(this.elements[i][j]) > Math.abs(m)) { m = this.elements[i][j]; }
533       }
534     }
535     return m;
536   },
537 
538   // Returns the indeces of the first match found by reading row-by-row from left to right
539   indexOf: function(x) {
540     var index = null, ni = this.elements.length, i, nj = this.elements[0].length, j;
541     for (i = 0; i < ni; i++) {
542       for (j = 0; j < nj; j++) {
543         if (this.elements[i][j] == x) { return {i: i+1, j: j+1}; }
544       }
545     }
546     return null;
547   },
548 
549   // If the matrix is square, returns the diagonal elements as a vector.
550   // Otherwise, returns null.
551   diagonal: function() {
552     if (!this.isSquare) { return null; }
553     var els = [], n = this.elements.length;
554     for (var i = 0; i < n; i++) {
555       els.push(this.elements[i][i]);
556     }
557     return Vector.create(els);
558   },
559 
560   // Make the matrix upper (right) triangular by Gaussian elimination.
561   // This method only adds multiples of rows to other rows. No rows are
562   // scaled up or switched, and the determinant is preserved.
563   toRightTriangular: function() {
564     var M = this.dup(), els;
565     var n = this.elements.length, i, j, np = this.elements[0].length, p;
566     for (i = 0; i < n; i++) {
567       if (M.elements[i][i] == 0) {
568         for (j = i + 1; j < n; j++) {
569           if (M.elements[j][i] != 0) {
570             els = [];
571             for (p = 0; p < np; p++) { els.push(M.elements[i][p] + M.elements[j][p]); }
572             M.elements[i] = els;
573             break;
574           }
575         }
576       }
577       if (M.elements[i][i] != 0) {
578         for (j = i + 1; j < n; j++) {
579           var multiplier = M.elements[j][i] / M.elements[i][i];
580           els = [];
581           for (p = 0; p < np; p++) {
582             // Elements with column numbers up to an including the number
583             // of the row that we're subtracting can safely be set straight to
584             // zero, since that's the point of this routine and it avoids having
585             // to loop over and correct rounding errors later
586             els.push(p <= i ? 0 : M.elements[j][p] - M.elements[i][p] * multiplier);
587           }
588           M.elements[j] = els;
589         }
590       }
591     }
592     return M;
593   },
594 
595   toUpperTriangular: function() { return this.toRightTriangular(); },
596 
597   // Returns the determinant for square matrices
598   determinant: function() {
599     if (!this.isSquare()) { return null; }
600     var M = this.toRightTriangular();
601     var det = M.elements[0][0], n = M.elements.length;
602     for (var i = 1; i < n; i++) {
603       det = det * M.elements[i][i];
604     }
605     return det;
606   },
607 
608   det: function() { return this.determinant(); },
609 
610   // Returns true iff the matrix is singular
611   isSingular: function() {
612     return (this.isSquare() && this.determinant() === 0);
613   },
614 
615   // Returns the trace for square matrices
616   trace: function() {
617     if (!this.isSquare()) { return null; }
618     var tr = this.elements[0][0], n = this.elements.length;
619     for (var i = 1; i < n; i++) {
620       tr += this.elements[i][i];
621     }
622     return tr;
623   },
624 
625   tr: function() { return this.trace(); },
626 
627   // Returns the rank of the matrix
628   rank: function() {
629     var M = this.toRightTriangular(), rank = 0;
630     var i = this.elements.length, nj = this.elements[0].length, j;
631     while (i--) { j = nj;
632       while (j--) {
633         if (Math.abs(M.elements[i][j]) > Sylvester.precision) { rank++; break; }
634       }
635     }
636     return rank;
637   },
638 
639   rk: function() { return this.rank(); },
640 
641   // Returns the result of attaching the given argument to the right-hand side of the matrix
642   augment: function(matrix) {
643     var M = matrix.elements || matrix;
644     if (typeof(M[0][0]) == 'undefined') { M = Matrix.create(M).elements; }
645     var T = this.dup(), cols = T.elements[0].length;
646     var i = T.elements.length, nj = M[0].length, j;
647     if (i != M.length) { return null; }
648     while (i--) { j = nj;
649       while (j--) {
650         T.elements[i][cols + j] = M[i][j];
651       }
652     }
653     return T;
654   },
655 
656   // Returns the inverse (if one exists) using Gauss-Jordan
657   inverse: function() {
658     if (!this.isSquare() || this.isSingular()) { return null; }
659     var n = this.elements.length, i= n, j;
660     var M = this.augment(Matrix.I(n)).toRightTriangular();
661     var np = M.elements[0].length, p, els, divisor;
662     var inverse_elements = [], new_element;
663     // Matrix is non-singular so there will be no zeros on the diagonal
664     // Cycle through rows from last to first
665     while (i--) {
666       // First, normalise diagonal elements to 1
667       els = [];
668       inverse_elements[i] = [];
669       divisor = M.elements[i][i];
670       for (p = 0; p < np; p++) {
671         new_element = M.elements[i][p] / divisor;
672         els.push(new_element);
673         // Shuffle off the current row of the right hand side into the results
674         // array as it will not be modified by later runs through this loop
675         if (p >= n) { inverse_elements[i].push(new_element); }
676       }
677       M.elements[i] = els;
678       // Then, subtract this row from those above it to
679       // give the identity matrix on the left hand side
680       j = i;
681       while (j--) {
682         els = [];
683         for (p = 0; p < np; p++) {
684           els.push(M.elements[j][p] - M.elements[i][p] * M.elements[j][i]);
685         }
686         M.elements[j] = els;
687       }
688     }
689     return Matrix.create(inverse_elements);
690   },
691 
692   inv: function() { return this.inverse(); },
693 
694   // Returns the result of rounding all the elements
695   round: function() {
696     return this.map(function(x) { return Math.round(x); });
697   },
698 
699   // Returns a copy of the matrix with elements set to the given value if they
700   // differ from it by less than Sylvester.precision
701   snapTo: function(x) {
702     return this.map(function(p) {
703       return (Math.abs(p - x) <= Sylvester.precision) ? x : p;
704     });
705   },
706 
707   // Returns a string representation of the matrix
708   inspect: function() {
709     var matrix_rows = [];
710     var n = this.elements.length;
711     for (var i = 0; i < n; i++) {
712       matrix_rows.push(Vector.create(this.elements[i]).inspect());
713     }
714     return matrix_rows.join('\n');
715   },
716 
717   // Set the matrix's elements from an array. If the argument passed
718   // is a vector, the resulting matrix will be a single column.
719   setElements: function(els) {
720     var i, j, elements = els.elements || els;
721     if (typeof(elements[0][0]) != 'undefined') {
722       i = elements.length;
723       this.elements = [];
724       while (i--) { j = elements[i].length;
725         this.elements[i] = [];
726         while (j--) {
727           this.elements[i][j] = elements[i][j];
728         }
729       }
730       return this;
731     }
732     var n = elements.length;
733     this.elements = [];
734     for (i = 0; i < n; i++) {
735       this.elements.push([elements[i]]);
736     }
737     return this;
738   }
739 };
740 
741 // Constructor function
742 Matrix.create = function(elements) {
743   var M = new Matrix();
744   return M.setElements(elements);
745 };
746 var $M = Matrix.create;
747 
748 // Identity matrix of size n
749 Matrix.I = function(n) {
750   var els = [], i = n, j;
751   while (i--) { j = n;
752     els[i] = [];
753     while (j--) {
754       els[i][j] = (i == j) ? 1 : 0;
755     }
756   }
757   return Matrix.create(els);
758 };
759 
760 // Diagonal matrix - all off-diagonal elements are zero
761 Matrix.Diagonal = function(elements) {
762   var i = elements.length;
763   var M = Matrix.I(i);
764   while (i--) {
765     M.elements[i][i] = elements[i];
766   }
767   return M;
768 };
769 
770 // Rotation matrix about some axis. If no axis is
771 // supplied, assume we're after a 2D transform
772 Matrix.Rotation = function(theta, a) {
773   if (!a) {
774     return Matrix.create([
775       [Math.cos(theta),  -Math.sin(theta)],
776       [Math.sin(theta),   Math.cos(theta)]
777     ]);
778   }
779   var axis = a.dup();
780   if (axis.elements.length != 3) { return null; }
781   var mod = axis.modulus();
782   var x = axis.elements[0]/mod, y = axis.elements[1]/mod, z = axis.elements[2]/mod;
783   var s = Math.sin(theta), c = Math.cos(theta), t = 1 - c;
784   // Formula derived here: http://www.gamedev.net/reference/articles/article1199.asp
785   // That proof rotates the co-ordinate system so theta
786   // becomes -theta and sin becomes -sin here.
787   return Matrix.create([
788     [ t*x*x + c, t*x*y - s*z, t*x*z + s*y ],
789     [ t*x*y + s*z, t*y*y + c, t*y*z - s*x ],
790     [ t*x*z - s*y, t*y*z + s*x, t*z*z + c ]
791   ]);
792 };
793 
794 // Special case rotations
795 Matrix.RotationX = function(t) {
796   var c = Math.cos(t), s = Math.sin(t);
797   return Matrix.create([
798     [  1,  0,  0 ],
799     [  0,  c, -s ],
800     [  0,  s,  c ]
801   ]);
802 };
803 Matrix.RotationY = function(t) {
804   var c = Math.cos(t), s = Math.sin(t);
805   return Matrix.create([
806     [  c,  0,  s ],
807     [  0,  1,  0 ],
808     [ -s,  0,  c ]
809   ]);
810 };
811 Matrix.RotationZ = function(t) {
812   var c = Math.cos(t), s = Math.sin(t);
813   return Matrix.create([
814     [  c, -s,  0 ],
815     [  s,  c,  0 ],
816     [  0,  0,  1 ]
817   ]);
818 };
819 
820 // Random matrix of n rows, m columns
821 Matrix.Random = function(n, m) {
822   return Matrix.Zero(n, m).map(
823     function() { return Math.random(); }
824   );
825 };
826 
827 // Matrix filled with zeros
828 Matrix.Zero = function(n, m) {
829   var els = [], i = n, j;
830   while (i--) { j = m;
831     els[i] = [];
832     while (j--) {
833       els[i][j] = 0;
834     }
835   }
836   return Matrix.create(els);
837 };// Line class - depends on Vector, and some methods require Matrix and Plane.
838 
839 function Line() {}
840 Line.prototype = {
841 
842   // Returns true if the argument occupies the same space as the line
843   eql: function(line) {
844     return (this.isParallelTo(line) && this.contains(line.anchor));
845   },
846 
847   // Returns a copy of the line
848   dup: function() {
849     return Line.create(this.anchor, this.direction);
850   },
851 
852   // Returns the result of translating the line by the given vector/array
853   translate: function(vector) {
854     var V = vector.elements || vector;
855     return Line.create([
856       this.anchor.elements[0] + V[0],
857       this.anchor.elements[1] + V[1],
858       this.anchor.elements[2] + (V[2] || 0)
859     ], this.direction);
860   },
861 
862   // Returns true if the line is parallel to the argument. Here, 'parallel to'
863   // means that the argument's direction is either parallel or antiparallel to
864   // the line's own direction. A line is parallel to a plane if the two do not
865   // have a unique intersection.
866   isParallelTo: function(obj) {
867     if (obj.normal || (obj.start && obj.end)) { return obj.isParallelTo(this); }
868     var theta = this.direction.angleFrom(obj.direction);
869     return (Math.abs(theta) <= Sylvester.precision || Math.abs(theta - Math.PI) <= Sylvester.precision);
870   },
871 
872   // Returns the line's perpendicular distance from the argument,
873   // which can be a point, a line or a plane
874   distanceFrom: function(obj) {
875     if (obj.normal || (obj.start && obj.end)) { return obj.distanceFrom(this); }
876     if (obj.direction) {
877       // obj is a line
878       if (this.isParallelTo(obj)) { return this.distanceFrom(obj.anchor); }
879       var N = this.direction.cross(obj.direction).toUnitVector().elements;
880       var A = this.anchor.elements, B = obj.anchor.elements;
881       return Math.abs((A[0] - B[0]) * N[0] + (A[1] - B[1]) * N[1] + (A[2] - B[2]) * N[2]);
882     } else {
883       // obj is a point
884       var P = obj.elements || obj;
885       var A = this.anchor.elements, D = this.direction.elements;
886       var PA1 = P[0] - A[0], PA2 = P[1] - A[1], PA3 = (P[2] || 0) - A[2];
887       var modPA = Math.sqrt(PA1*PA1 + PA2*PA2 + PA3*PA3);
888       if (modPA === 0) return 0;
889       // Assumes direction vector is normalized
890       var cosTheta = (PA1 * D[0] + PA2 * D[1] + PA3 * D[2]) / modPA;
891       var sin2 = 1 - cosTheta*cosTheta;
892       return Math.abs(modPA * Math.sqrt(sin2 < 0 ? 0 : sin2));
893     }
894   },
895 
896   // Returns true iff the argument is a point on the line, or if the argument
897   // is a line segment lying within the receiver
898   contains: function(obj) {
899     if (obj.start && obj.end) { return this.contains(obj.start) && this.contains(obj.end); }
900     var dist = this.distanceFrom(obj);
901     return (dist !== null && dist <= Sylvester.precision);
902   },
903 
904   // Returns the distance from the anchor of the given point. Negative values are
905   // returned for points that are in the opposite direction to the line's direction from
906   // the line's anchor point.
907   positionOf: function(point) {
908     if (!this.contains(point)) { return null; }
909     var P = point.elements || point;
910     var A = this.anchor.elements, D = this.direction.elements;
911     return (P[0] - A[0]) * D[0] + (P[1] - A[1]) * D[1] + ((P[2] || 0) - A[2]) * D[2];
912   },
913 
914   // Returns true iff the line lies in the given plane
915   liesIn: function(plane) {
916     return plane.contains(this);
917   },
918 
919   // Returns true iff the line has a unique point of intersection with the argument
920   intersects: function(obj) {
921     if (obj.normal) { return obj.intersects(this); }
922     return (!this.isParallelTo(obj) && this.distanceFrom(obj) <= Sylvester.precision);
923   },
924 
925   // Returns the unique intersection point with the argument, if one exists
926   intersectionWith: function(obj) {
927     if (obj.normal || (obj.start && obj.end)) { return obj.intersectionWith(this); }
928     if (!this.intersects(obj)) { return null; }
929     var P = this.anchor.elements, X = this.direction.elements,
930         Q = obj.anchor.elements, Y = obj.direction.elements;
931     var X1 = X[0], X2 = X[1], X3 = X[2], Y1 = Y[0], Y2 = Y[1], Y3 = Y[2];
932     var PsubQ1 = P[0] - Q[0], PsubQ2 = P[1] - Q[1], PsubQ3 = P[2] - Q[2];
933     var XdotQsubP = - X1*PsubQ1 - X2*PsubQ2 - X3*PsubQ3;
934     var YdotPsubQ = Y1*PsubQ1 + Y2*PsubQ2 + Y3*PsubQ3;
935     var XdotX = X1*X1 + X2*X2 + X3*X3;
936     var YdotY = Y1*Y1 + Y2*Y2 + Y3*Y3;
937     var XdotY = X1*Y1 + X2*Y2 + X3*Y3;
938     var k = (XdotQsubP * YdotY / XdotX + XdotY * YdotPsubQ) / (YdotY - XdotY * XdotY);
939     return Vector.create([P[0] + k*X1, P[1] + k*X2, P[2] + k*X3]);
940   },
941 
942   // Returns the point on the line that is closest to the given point or line/line segment
943   pointClosestTo: function(obj) {
944     if (obj.start && obj.end) {
945       // obj is a line segment
946       var P = obj.pointClosestTo(this);
947       return (P === null) ? null : this.pointClosestTo(P);
948     } else if (obj.direction) {
949       // obj is a line
950       if (this.intersects(obj)) { return this.intersectionWith(obj); }
951       if (this.isParallelTo(obj)) { return null; }
952       var D = this.direction.elements, E = obj.direction.elements;
953       var D1 = D[0], D2 = D[1], D3 = D[2], E1 = E[0], E2 = E[1], E3 = E[2];
954       // Create plane containing obj and the shared normal and intersect this with it
955       // Thank you: http://www.cgafaq.info/wiki/Line-line_distance
956       var x = (D3 * E1 - D1 * E3), y = (D1 * E2 - D2 * E1), z = (D2 * E3 - D3 * E2);
957       var N = [x * E3 - y * E2, y * E1 - z * E3, z * E2 - x * E1];
958       var P = Plane.create(obj.anchor, N);
959       return P.intersectionWith(this);
960     } else {
961       // obj is a point
962       var P = obj.elements || obj;
963       if (this.contains(P)) { return Vector.create(P); }
964       var A = this.anchor.elements, D = this.direction.elements;
965       var D1 = D[0], D2 = D[1], D3 = D[2], A1 = A[0], A2 = A[1], A3 = A[2];
966       var x = D1 * (P[1]-A2) - D2 * (P[0]-A1), y = D2 * ((P[2] || 0) - A3) - D3 * (P[1]-A2),
967           z = D3 * (P[0]-A1) - D1 * ((P[2] || 0) - A3);
968       var V = Vector.create([D2 * x - D3 * z, D3 * y - D1 * x, D1 * z - D2 * y]);
969       var k = this.distanceFrom(P) / V.modulus();
970       return Vector.create([
971         P[0] + V.elements[0] * k,
972         P[1] + V.elements[1] * k,
973         (P[2] || 0) + V.elements[2] * k
974       ]);
975     }
976   },
977 
978   // Returns a copy of the line rotated by t radians about the given line. Works by
979   // finding the argument's closest point to this line's anchor point (call this C) and
980   // rotating the anchor about C. Also rotates the line's direction about the argument's.
981   // Be careful with this - the rotation axis' direction affects the outcome!
982   rotate: function(t, line) {
983     // If we're working in 2D
984     if (typeof(line.direction) == 'undefined') { line = Line.create(line.to3D(), Vector.k); }
985     var R = Matrix.Rotation(t, line.direction).elements;
986     var C = line.pointClosestTo(this.anchor).elements;
987     var A = this.anchor.elements, D = this.direction.elements;
988     var C1 = C[0], C2 = C[1], C3 = C[2], A1 = A[0], A2 = A[1], A3 = A[2];
989     var x = A1 - C1, y = A2 - C2, z = A3 - C3;
990     return Line.create([
991       C1 + R[0][0] * x + R[0][1] * y + R[0][2] * z,
992       C2 + R[1][0] * x + R[1][1] * y + R[1][2] * z,
993       C3 + R[2][0] * x + R[2][1] * y + R[2][2] * z
994     ], [
995       R[0][0] * D[0] + R[0][1] * D[1] + R[0][2] * D[2],
996       R[1][0] * D[0] + R[1][1] * D[1] + R[1][2] * D[2],
997       R[2][0] * D[0] + R[2][1] * D[1] + R[2][2] * D[2]
998     ]);
999   },
1000 
1001   // Returns a copy of the line with its direction vector reversed.
1002   // Useful when using lines for rotations.
1003   reverse: function() {
1004     return Line.create(this.anchor, this.direction.x(-1));
1005   },
1006 
1007   // Returns the line's reflection in the given point or line
1008   reflectionIn: function(obj) {
1009     if (obj.normal) {
1010       // obj is a plane
1011       var A = this.anchor.elements, D = this.direction.elements;
1012       var A1 = A[0], A2 = A[1], A3 = A[2], D1 = D[0], D2 = D[1], D3 = D[2];
1013       var newA = this.anchor.reflectionIn(obj).elements;
1014       // Add the line's direction vector to its anchor, then mirror that in the plane
1015       var AD1 = A1 + D1, AD2 = A2 + D2, AD3 = A3 + D3;
1016       var Q = obj.pointClosestTo([AD1, AD2, AD3]).elements;
1017       var newD = [Q[0] + (Q[0] - AD1) - newA[0], Q[1] + (Q[1] - AD2) - newA[1], Q[2] + (Q[2] - AD3) - newA[2]];
1018       return Line.create(newA, newD);
1019     } else if (obj.direction) {
1020       // obj is a line - reflection obtained by rotating PI radians about obj
1021       return this.rotate(Math.PI, obj);
1022     } else {
1023       // obj is a point - just reflect the line's anchor in it
1024       var P = obj.elements || obj;
1025       return Line.create(this.anchor.reflectionIn([P[0], P[1], (P[2] || 0)]), this.direction);
1026     }
1027   },
1028 
1029   // Set the line's anchor point and direction.
1030   setVectors: function(anchor, direction) {
1031     // Need to do this so that line's properties are not
1032     // references to the arguments passed in
1033     anchor = Vector.create(anchor);
1034     direction = Vector.create(direction);
1035     if (anchor.elements.length == 2) {anchor.elements.push(0); }
1036     if (direction.elements.length == 2) { direction.elements.push(0); }
1037     if (anchor.elements.length > 3 || direction.elements.length > 3) { return null; }
1038     var mod = direction.modulus();
1039     if (mod === 0) { return null; }
1040     this.anchor = anchor;
1041     this.direction = Vector.create([
1042       direction.elements[0] / mod,
1043       direction.elements[1] / mod,
1044       direction.elements[2] / mod
1045     ]);
1046     return this;
1047   }
1048 };
1049 
1050 // Constructor function
1051 Line.create = function(anchor, direction) {
1052   var L = new Line();
1053   return L.setVectors(anchor, direction);
1054 };
1055 var $L = Line.create;
1056 
1057 // Axes
1058 Line.X = Line.create(Vector.Zero(3), Vector.i);
1059 Line.Y = Line.create(Vector.Zero(3), Vector.j);
1060 Line.Z = Line.create(Vector.Zero(3), Vector.k);/**
1061  * @namespace
1062  */
1063 geo.linalg = {};
1064 
1065 geo.linalg.Vector = function() {
1066   return Vector.create.apply(null, arguments);
1067 };
1068 geo.linalg.Vector.create = Vector.create;
1069 geo.linalg.Vector.i = Vector.i;
1070 geo.linalg.Vector.j = Vector.j;
1071 geo.linalg.Vector.k = Vector.k;
1072 geo.linalg.Vector.Random = Vector.Random;
1073 geo.linalg.Vector.Zero = Vector.Zero;
1074 
1075 geo.linalg.Matrix = function() {
1076   return Matrix.create.apply(null, arguments);
1077 };
1078 geo.linalg.Matrix.create = Matrix.create;
1079 geo.linalg.Matrix.I = Matrix.I;
1080 geo.linalg.Matrix.Random = Matrix.Random;
1081 geo.linalg.Matrix.Rotation = Matrix.Rotation;
1082 geo.linalg.Matrix.RotationX = Matrix.RotationX;
1083 geo.linalg.Matrix.RotationY = Matrix.RotationY;
1084 geo.linalg.Matrix.RotationZ = Matrix.RotationZ;
1085 geo.linalg.Matrix.Zero = Matrix.Zero;
1086 
1087 geo.linalg.Line = function() {
1088   return Line.create.apply(null, arguments);
1089 };
1090 geo.linalg.Line.create = Line.create;
1091 geo.linalg.Line.X = Line.X;
1092 geo.linalg.Line.Y = Line.Y;
1093 geo.linalg.Line.Z = Line.Z;
1094 /**
1095  * @namespace
1096  */
1097 geo.math = {isnamespace_:true};
1098 /**
1099  * Converts an angle from radians to degrees.
1100  * @type Number
1101  * @return Returns the angle, converted to degrees.
1102  */
1103 if (!('toDegrees' in Number.prototype)) {
1104   Number.prototype.toDegrees = function() {
1105     return this * 180 / Math.PI;
1106   };
1107 }
1108 
1109 /**
1110  * Converts an angle from degrees to radians.
1111  * @type Number
1112  * @return Returns the angle, converted to radians.
1113  */
1114 if (!('toRadians' in Number.prototype)) {
1115   Number.prototype.toRadians = function() {
1116     return this * Math.PI / 180;
1117   };
1118 }
1119 /**
1120  * Normalizes an angle to the [0,2pi) range.
1121  * @param {Number} angleRad The angle to normalize, in radians.
1122  * @type Number
1123  * @return Returns the angle, fit within the [0,2pi) range, in radians.
1124  */
1125 geo.math.normalizeAngle = function(angleRad) {
1126   angleRad = angleRad % (2 * Math.PI);
1127   return angleRad >= 0 ? angleRad : angleRad + 2 * Math.PI;
1128 };
1129 
1130 /**
1131  * Normalizes a latitude to the [-90,90] range. Latitudes above 90 or
1132  * below -90 are capped, not wrapped.
1133  * @param {Number} lat The latitude to normalize, in degrees.
1134  * @type Number
1135  * @return Returns the latitude, fit within the [-90,90] range.
1136  */
1137 geo.math.normalizeLat = function(lat) {
1138   return Math.max(-90, Math.min(90, lat));
1139 };
1140 
1141 /**
1142  * Normalizes a longitude to the [-180,180] range. Longitudes above 180
1143  * or below -180 are wrapped.
1144  * @param {Number} lng The longitude to normalize, in degrees.
1145  * @type Number
1146  * @return Returns the latitude, fit within the [-90,90] range.
1147  */
1148 geo.math.normalizeLng = function(lng) {
1149   if (lng % 360 == 180) {
1150     return 180;
1151   }
1152 
1153   lng = lng % 360;
1154   return lng < -180 ? lng + 360 : lng > 180 ? lng - 360 : lng;
1155 };
1156 
1157 /**
1158  * Reverses an angle.
1159  * @param {Number} angleRad The angle to reverse, in radians.
1160  * @type Number
1161  * @return Returns the reverse angle, in radians.
1162  */
1163 geo.math.reverseAngle = function(angleRad) {
1164   return geo.math.normalizeAngle(angleRad + Math.PI);
1165 };
1166 
1167 /**
1168  * Wraps the given number to the given range. If the wrapped value is exactly
1169  * equal to min or max, favors max, unless favorMin is true.
1170  * @param {Number} value The value to wrap.
1171  * @param {Number[]} range An array of two numbers, specifying the minimum and
1172  *     maximum bounds of the range, respectively.
1173  * @param {Boolean} [favorMin=false] Whether or not to favor min over
1174  *     max in the case of ambiguity.
1175  * @return {Number} Returns the value wrapped to the given range.
1176  */
1177 geo.math.wrapValue = function(value, range, favorMin) {
1178   if (!range || !geo.util.isArray(range) || range.length != 2) {
1179     throw new TypeError('The range parameter must be an array of 2 numbers.');
1180   }
1181   
1182   // Don't wrap min as max.
1183   if (value === range[0]) {
1184     return range[0];
1185   }
1186   
1187   // Normalize to min = 0.
1188   value -= range[0];
1189   
1190   value = value % (range[1] - range[0]);
1191   if (value < 0) {
1192     value += (range[1] - range[0]);
1193   }
1194   
1195   // Reverse normalization.
1196   value += range[0];
1197   
1198   // When ambiguous (min or max), return max unless favorMin is true.
1199   return (value === range[0]) ? (favorMin ? range[0] : range[1]) : value;
1200 };
1201 
1202 /**
1203  * Constrains the given number to the given range.
1204  * @param {Number} value The value to constrain.
1205  * @param {Number[]} range An array of two numbers, specifying the minimum and
1206  *     maximum bounds of the range, respectively.
1207  * @return {Number} Returns the value constrained to the given range.
1208  */
1209 geo.math.constrainValue = function(value, range) {
1210   if (!range || !geo.util.isArray(range) || range.length != 2) {
1211     throw new TypeError('The range parameter must be an array of 2 numbers.');
1212   }
1213   
1214   return Math.max(range[0], Math.min(range[1], value));
1215 };
1216 /**
1217  * The radius of the Earth, in meters, assuming the Earth is a perfect sphere.
1218  * @see http://en.wikipedia.org/wiki/Earth_radius
1219  * @type Number
1220  */
1221 geo.math.EARTH_RADIUS = 6378135;
1222 
1223 /**
1224  * The average radius-of-curvature of the Earth, in meters.
1225  * @see http://en.wikipedia.org/wiki/Radius_of_curvature_(applications)
1226  * @type Number
1227  * @ignore
1228  */
1229 geo.math.EARTH_RADIUS_CURVATURE_AVG = 6372795;
1230 /**
1231  * Returns the approximate sea level great circle (Earth) distance between
1232  * two points using the Haversine formula and assuming an Earth radius of
1233  * geo.math.EARTH_RADIUS.
1234  * @param {geo.Point} point1 The first point.
1235  * @param {geo.Point} point2 The second point.
1236  * @return {Number} The Earth distance between the two points, in meters.
1237  * @see http://www.movable-type.co.uk/scripts/latlong.html
1238  */
1239 geo.math.distance = function(point1, point2) {
1240   return geo.math.EARTH_RADIUS * geo.math.angularDistance(point1, point2);
1241 };
1242 
1243 /*
1244 Vincenty formula:
1245 geo.math.angularDistance = function(point1, point2) {
1246   point1 = new geo.Point(point1);
1247   point2 = new geo.Point(point2);
1248   
1249   var phi1 = point1.lat.toRadians();
1250   var phi2 = point2.lat.toRadians();
1251   
1252   var sin_phi1 = Math.sin(phi1);
1253   var cos_phi1 = Math.cos(phi1);
1254   
1255   var sin_phi2 = Math.sin(phi2);
1256   var cos_phi2 = Math.cos(phi2);
1257   
1258   var sin_d_lmd = Math.sin(
1259       point2.lng.toRadians() - point1.lng.toRadians());
1260   var cos_d_lmd = Math.cos(
1261       point2.lng.toRadians() - point1.lng.toRadians());
1262   
1263   // TODO: options to specify formula
1264   // TODO: compute radius of curvature at given point for more precision
1265   
1266   // Vincenty formula (may replace with Haversine for performance?)
1267   return Math.atan2(
1268       Math.sqrt(
1269         Math.pow(cos_phi2 * sin_d_lmd, 2) +
1270         Math.pow(cos_phi1 * sin_phi2 - sin_phi1 * cos_phi2 * cos_d_lmd, 2)
1271       ), sin_phi1 * sin_phi2 + cos_phi1 * cos_phi2 * cos_d_lmd);
1272 }
1273 */
1274 /**
1275  * Returns the angular distance between two points using the Haversine
1276  * formula.
1277  * @see geo.math.distance
1278  * @ignore
1279  */
1280 geo.math.angularDistance = function(point1, point2) {
1281   var phi1 = point1.lat().toRadians();
1282   var phi2 = point2.lat().toRadians();
1283   
1284   var d_phi = (point2.lat() - point1.lat()).toRadians();
1285   var d_lmd = (point2.lng() - point1.lng()).toRadians();
1286   
1287   var A = Math.pow(Math.sin(d_phi / 2), 2) +
1288           Math.cos(phi1) * Math.cos(phi2) *
1289             Math.pow(Math.sin(d_lmd / 2), 2);
1290   
1291   return 2 * Math.atan2(Math.sqrt(A), Math.sqrt(1 - A));
1292 };
1293 // TODO: add non-sea level distance using Earth API's math3d.js or Sylvester
1294 /*
1295     p1 = V3.latLonAltToCartesian([loc1.lat(), loc1.lng(),
1296       this.ge.getGlobe().getGroundAltitude(loc1.lat(), loc1.lng())]);
1297     p2 = V3.latLonAltToCartesian([loc2.lat(), loc2.lng(),
1298       this.ge.getGlobe().getGroundAltitude(loc2.lat(), loc2.lng())]);
1299     return V3.earthDistance(p1, p2);
1300 */
1301 
1302 /**
1303  * Calculates the initial heading/bearing at which an object at the start
1304  * point will need to travel to get to the destination point.
1305  * @param {geo.Point} start The start point.
1306  * @param {geo.Point} dest The destination point.
1307  * @return {Number} The initial heading required to get to the destination
1308  *     point, in the [0,360) degree range.
1309  * @see http://mathforum.org/library/drmath/view/55417.html
1310  */
1311 geo.math.heading = function(start, dest) {
1312   var phi1 = start.lat().toRadians();
1313   var phi2 = dest.lat().toRadians();
1314   var cos_phi2 = Math.cos(phi2);
1315   
1316   var d_lmd = (dest.lng() - start.lng()).toRadians();
1317   
1318   return geo.math.normalizeAngle(Math.atan2(
1319       Math.sin(d_lmd) * cos_phi2,
1320       Math.cos(phi1) * Math.sin(phi2) - Math.sin(phi1) * cos_phi2 *
1321         Math.cos(d_lmd))).toDegrees();
1322 };
1323 
1324 /**
1325  * @function
1326  * @param {geo.Point} start
1327  * @param {geo.Point} dest
1328  * @return {Number}
1329  * @see geo.math.heading
1330  */
1331 geo.math.bearing = geo.math.heading;
1332 
1333 /**
1334  * Calculates an intermediate point on the geodesic between the two given
1335  * points.
1336  * @param {geo.Point} point1 The first point.
1337  * @param {geo.Point} point2 The second point.
1338  * @param {Number} [fraction] The fraction of distance between the first
1339  *     and second points.
1340  * @return {geo.Point}
1341  * @see http://williams.best.vwh.net/avform.htm#Intermediate
1342  */
1343 geo.math.midpoint = function(point1, point2, fraction) {
1344   // TODO: check for antipodality and fail w/ exception in that case
1345   if (geo.util.isUndefined(fraction) || fraction === null) {
1346     fraction = 0.5;
1347   }
1348   
1349   if (point1.equals(point2)) {
1350     return new geo.Point(point1);
1351   }
1352   
1353   var phi1 = point1.lat().toRadians();
1354   var phi2 = point2.lat().toRadians();
1355   var lmd1 = point1.lng().toRadians();
1356   var lmd2 = point2.lng().toRadians();
1357   
1358   var cos_phi1 = Math.cos(phi1);
1359   var cos_phi2 = Math.cos(phi2);
1360   
1361   var angularDistance = geo.math.angularDistance(point1, point2);
1362   var sin_angularDistance = Math.sin(angularDistance);
1363   
1364   var A = Math.sin((1 - fraction) * angularDistance) / sin_angularDistance;
1365   var B = Math.sin(fraction * angularDistance) / sin_angularDistance;
1366   
1367   var x = A * cos_phi1 * Math.cos(lmd1) +
1368           B * cos_phi2 * Math.cos(lmd2);
1369   
1370   var y = A * cos_phi1 * Math.sin(lmd1) +
1371           B * cos_phi2 * Math.sin(lmd2);
1372   
1373   var z = A * Math.sin(phi1) +
1374           B * Math.sin(phi2);
1375   
1376   return new geo.Point(
1377       Math.atan2(z, Math.sqrt(Math.pow(x, 2) +
1378                               Math.pow(y, 2))).toDegrees(),
1379       Math.atan2(y, x).toDegrees());
1380 };
1381 
1382 /**
1383  * Calculates the destination point along a geodesic, given an initial heading
1384  * and distance, from the given start point.
1385  * @see http://www.movable-type.co.uk/scripts/latlong.html
1386  * @param {geo.Point} start The start point.
1387  * @param {Object} options The heading and distance object literal.
1388  * @param {Number} options.heading The initial heading, in degrees.
1389  * @param {Number} options.distance The distance along the geodesic, in meters.
1390  * @return {geo.Point}
1391  */
1392 geo.math.destination = function(start, options) {
1393   if (!('heading' in options && 'distance' in options)) {
1394     throw new TypeError('destination() requres both heading and ' +
1395                         'distance options.');
1396   }
1397   
1398   var phi1 = start.lat().toRadians();
1399   
1400   var sin_phi1 = Math.sin(phi1);
1401   
1402   var angularDistance = options.distance / geo.math.EARTH_RADIUS;
1403   var heading_rad = options.heading.toRadians();
1404   
1405   var sin_angularDistance = Math.sin(angularDistance);
1406   var cos_angularDistance = Math.cos(angularDistance);
1407   
1408   var phi2 = Math.asin(
1409                sin_phi1 * cos_angularDistance + 
1410                Math.cos(phi1) * sin_angularDistance *
1411                  Math.cos(heading_rad));
1412   
1413   return new geo.Point(
1414       phi2.toDegrees(),
1415       Math.atan2(
1416         Math.sin(heading_rad) *
1417           sin_angularDistance * Math.cos(phi2),
1418         cos_angularDistance - sin_phi1 * Math.sin(phi2)).toDegrees() +
1419         start.lng());
1420 };
1421 /**
1422  * Creates a new point from the given parameters.
1423  * @param {geo.Point|Number[]|KmlPoint|KmlLookAt|KmlCoord|KmlLocation|GLatLng}
1424  *     src The point data.
1425  * @constructor
1426  */
1427 geo.Point = function() {
1428   var pointArraySrc = null;
1429   
1430   // 1 argument constructor
1431   if (arguments.length == 1) {
1432     var point = arguments[0];
1433     
1434     // copy constructor
1435     if (point.constructor === geo.Point) {
1436       this.lat_ = point.lat();
1437       this.lng_ = point.lng();
1438       this.altitude_ = point.altitude();
1439       this.altitudeMode_ = point.altitudeMode();
1440       
1441     // array constructor
1442     } else if (geo.util.isArray(point)) {
1443       pointArraySrc = point;
1444     
1445     // constructor from an Earth API object
1446     } else if (isEarthAPIObject_(point)) {
1447       var type = point.getType();
1448       
1449       // KmlPoint and KmlLookAt constructor
1450       if (type == 'KmlPoint' ||
1451           type == 'KmlLookAt') {
1452         this.lat_ = point.getLatitude();
1453         this.lng_ = point.getLongitude();
1454         this.altitude_ = point.getAltitude();
1455         this.altitudeMode_ = point.getAltitudeMode();
1456       
1457       // KmlCoord and KmlLocation constructor
1458       } else if (type == 'KmlCoord' ||
1459                  type == 'KmlLocation') {
1460         this.lat_ = point.getLatitude();
1461         this.lng_ = point.getLongitude();
1462         this.altitude_ = point.getAltitude();
1463       
1464       // Error, can't create a Point from any other Earth object
1465       } else {
1466         throw new TypeError(
1467             'Could not create a point from the given Earth object');
1468       }
1469     
1470     // GLatLng constructor
1471     } else if (isGLatLng_(point)) {
1472       this.lat_ = point.lat();
1473       this.lng_ = point.lng();
1474 
1475     // Error, can't create a Point from the single argument
1476     } else {
1477       throw new TypeError('Could not create a point from the given arguments');
1478     }
1479   
1480   // Assume each argument is a point coordinate, i.e.
1481   // new Point(0, 1, 2) ==> new Point([0, 1, 2])
1482   } else {
1483     pointArraySrc = arguments;
1484   }
1485   
1486   // construct from an array
1487   if (pointArraySrc) {
1488     for (var i = 0; i < pointArraySrc.length; i++) {
1489       if (typeof pointArraySrc[i] != 'number') {
1490         throw new TypeError('Coordinates must be numerical');
1491       }
1492     }
1493     
1494     this.lat_ = pointArraySrc[0];
1495     this.lng_ = pointArraySrc[1];
1496     if (pointArraySrc.length >= 3) {
1497       this.altitude_ = pointArraySrc[2];
1498       if (pointArraySrc.length >= 4) {
1499         this.altitudeMode_ = pointArraySrc[3];
1500       }
1501     }
1502   }
1503 
1504   // normalize
1505   this.lat_ = geo.math.normalizeLat(this.lat_);
1506   this.lng_ = geo.math.normalizeLng(this.lng_);
1507 };
1508 
1509 /**
1510  * The point's latitude, in degrees.
1511  * @type Number
1512  */
1513 geo.Point.prototype.lat = function() {
1514   return this.lat_;
1515 };
1516 geo.Point.prototype.lat_ = 0;
1517 
1518 /**
1519  * The point's longitude, in degrees.
1520  * @type Number
1521  */
1522 geo.Point.prototype.lng = function() {
1523   return this.lng_;
1524 };
1525 geo.Point.prototype.lng_ = 0;
1526 
1527 /**
1528  * The point's altitude, in meters.
1529  * @type Number
1530  */
1531 geo.Point.prototype.altitude = function() {
1532   return this.altitude_;
1533 };
1534 geo.Point.prototype.altitude_ = 0;
1535 
1536 /**
1537  * The point's altitude mode.
1538  * @type KmlAltitudeModeEnum
1539  */
1540 geo.Point.prototype.altitudeMode = function() {
1541   return this.altitudeMode_;
1542 };
1543 geo.Point.prototype.altitudeMode_ = geo.ALTITUDE_RELATIVE_TO_GROUND;
1544 
1545 /**
1546  * Returns the string representation of the point.
1547  * @type String
1548  */
1549 geo.Point.prototype.toString = function() {
1550   return '(' + this.lat().toString() + ', ' + this.lng().toString() + ', ' +
1551       this.altitude().toString() + ')';
1552 };
1553 
1554 /**
1555  * Returns the 2D (no altitude) version of this point.
1556  * @type geo.Point
1557  */
1558 geo.Point.prototype.flatten = function() {
1559   return new geo.Point(this.lat(), this.lng());
1560 };
1561 
1562 /**
1563  * Determines whether or not this point has an altitude component.
1564  * @type Boolean
1565  */
1566 geo.Point.prototype.is3D = function() {
1567   return this.altitude_ !== 0;
1568 };
1569 
1570 /**
1571  * Determines whether or not the given point is the same as this one.
1572  * @param {geo.Point} otherPoint The other point.
1573  * @type Boolean
1574  */
1575 geo.Point.prototype.equals = function(p2) {
1576   return this.lat() == p2.lat() &&
1577          this.lng() == p2.lng() &&
1578          this.altitude() == p2.altitude() &&
1579          this.altitudeMode() == p2.altitudeMode();
1580 };
1581 
1582 /**
1583  * Returns the angular distance between this point and the destination point.
1584  * @param {geo.Point} dest The destination point.
1585  * @see geo.math.angularDistance
1586  * @ignore
1587  */
1588 geo.Point.prototype.angularDistance = function(dest) {
1589   return geo.math.angularDistance(this, dest);
1590 };
1591 
1592 /**
1593  * Returns the approximate sea level great circle (Earth) distance between
1594  * this point and the destination point using the Haversine formula and
1595  * assuming an Earth radius of geo.math.EARTH_RADIUS.
1596  * @param {geo.Point} dest The destination point.
1597  * @return {Number} The distance, in meters, to the destination point.
1598  * @see geo.math.distance
1599  */
1600 geo.Point.prototype.distance = function(dest) {
1601   return geo.math.distance(this, dest);
1602 };
1603 
1604 /**
1605  * Calculates the initial heading/bearing at which an object at the start
1606  * point will need to travel to get to the destination point.
1607  * @param {geo.Point} dest The destination point.
1608  * @return {Number} The initial heading required to get to the destination
1609  *     point, in the [0,360) degree range.
1610  * @see geo.math.heading
1611  */
1612 geo.Point.prototype.heading = function(dest) {
1613   return geo.math.heading(this, dest);
1614 };
1615 
1616 /**
1617  * Calculates an intermediate point on the geodesic between this point and the
1618  * given destination point.
1619  * @param {geo.Point} dest The destination point.
1620  * @param {Number} [fraction] The fraction of distance between the first
1621  *     and second points.
1622  * @return {geo.Point}
1623  * @see geo.math.midpoint
1624  */
1625 geo.Point.prototype.midpoint = function(dest, fraction) {
1626   return geo.math.midpoint(this, dest, fraction);
1627 };
1628 
1629 /**
1630  * Calculates the destination point along a geodesic, given an initial heading
1631  * and distance, starting at this point.
1632  * @param {Object} options The heading and distance object literal.
1633  * @param {Number} options.heading The initial heading, in degrees.
1634  * @param {Number} options.distance The distance along the geodesic, in meters.
1635  * @return {geo.Point}
1636  * @see geo.math.destination
1637  */
1638 geo.Point.prototype.destination = function(options) {
1639   return geo.math.destination(this, options);
1640 };
1641 
1642 /**
1643  * Returns the cartesian representation of the point, as a 3-vector,
1644  * assuming a spherical Earth of radius geo.math.EARTH_RADIUS.
1645  * @return {geo.linalg.Vector}
1646  */
1647 geo.Point.prototype.toCartesian = function() {
1648   var sin_phi = Math.sin(this.lng().toRadians());
1649   var cos_phi = Math.cos(this.lng().toRadians());
1650   var sin_lmd = Math.sin(this.lat().toRadians());
1651   var cos_lmd = Math.cos(this.lat().toRadians());
1652 
1653   var r = geo.math.EARTH_RADIUS + this.altitude();
1654   return new geo.linalg.Vector([r * cos_phi * cos_lmd,
1655                                 r * sin_lmd,
1656                                 r * -sin_phi * cos_lmd]);
1657 };
1658 
1659 /**
1660  * A static method to create a point from a 3-vector representing the cartesian
1661  * coordinates of a point on the Earth, assuming a spherical Earth of radius
1662  * geo.math.EARTH_RADIUS.
1663  * @param {geo.linalg.Vector} cartesianVector The cartesian representation of
1664  *     the point to create.
1665  * @return {geo.Point} The point, or null if the point doesn't exist.
1666  */
1667 geo.Point.fromCartesian = function(cartesianVector) {
1668   var r = cartesianVector.distanceFrom(geo.linalg.Vector.Zero(3));
1669   var unitVector = cartesianVector.toUnitVector();
1670   
1671   var altitude = r - geo.math.EARTH_RADIUS;
1672   
1673   var lat = Math.asin(unitVector.e(2)).toDegrees();
1674   if (lat > 90) {
1675     lat -= 180;
1676   }
1677   
1678   var lng = 0;
1679   if (Math.abs(lat) < 90) {
1680     lng = -Math.atan2(unitVector.e(3), unitVector.e(1)).toDegrees();
1681   }
1682   
1683   return new geo.Point(lat, lng, altitude);
1684 };
1685 /**
1686  * Create a new bounds object from the given parameters.
1687  * @param {geo.Bounds|geo.Point} [swOrBounds] Either an existing bounds object
1688  *     to copy, or the southwest, bottom coordinate of the new bounds object.
1689  * @param {geo.Point} [ne] The northeast, top coordinate of the new bounds
1690  *     object.
1691  * @constructor
1692  */
1693 geo.Bounds = function() {
1694   // TODO: accept instances of GLatLngBounds
1695 
1696   // 1 argument constructor
1697   if (arguments.length == 1) {
1698     // copy constructor
1699     if (arguments[0].constructor === geo.Bounds) {
1700       var bounds = arguments[0];
1701       this.sw_ = new geo.Point(bounds.southWestBottom());
1702       this.ne_ = new geo.Point(bounds.northEastTop());
1703 
1704     // anything else, treated as the lone coordinate
1705     // TODO: accept array of points, a Path, or a Polygon
1706     } else {
1707       this.sw_ = this.ne_ = new geo.Point(arguments[0]);
1708 
1709     }
1710 
1711   // Two argument constructor -- a northwest and southeast coordinate
1712   } else if (arguments.length == 2) {
1713     var sw = new geo.Point(arguments[0]);
1714     var ne = new geo.Point(arguments[1]);
1715 
1716     // handle degenerate cases
1717     if (!sw && !ne) {
1718       return;
1719     } else if (!sw) {
1720       sw = ne;
1721     } else if (!ne) {
1722       ne = sw;
1723     }
1724 
1725     if (sw.lat() > ne.lat()) {
1726       throw new RangeError('Bounds southwest coordinate cannot be north of ' +
1727                            'the northeast coordinate');
1728     }
1729 
1730     if (sw.altitude() > ne.altitude()) {
1731       throw new RangeError('Bounds southwest coordinate cannot be north of ' +
1732                            'the northeast coordinate');
1733     }
1734 
1735     // TODO: check for incompatible altitude modes
1736 
1737     this.sw_ = sw;
1738     this.ne_ = ne;
1739   }
1740 };
1741 
1742 /**
1743  * The bounds' southwest, bottom coordinate.
1744  * @type geo.Point
1745  */
1746 geo.Bounds.prototype.southWestBottom = function() {
1747   return this.sw_;
1748 };
1749 geo.Bounds.prototype.sw_ = null;
1750 
1751 /**
1752  * The bounds' south coordinate.
1753  * @type Number
1754  */
1755 geo.Bounds.prototype.south = function() {
1756   return !this.isEmpty() ? this.sw_.lat() : null;
1757 };
1758 
1759 /**
1760  * The bounds' west coordinate.
1761  * @type Number
1762  */
1763 geo.Bounds.prototype.west = function() {
1764   return !this.isEmpty() ? this.sw_.lng() : null;
1765 };
1766 
1767 /**
1768  * The bounds' minimum altitude.
1769  * @type Number
1770  */
1771 geo.Bounds.prototype.bottom = function() {
1772   return !this.isEmpty() ? this.sw_.altitude() : null;
1773 };
1774 
1775 /**
1776  * The bounds' northeast, top coordinate.
1777  * @type geo.Point
1778  */
1779 geo.Bounds.prototype.northEastTop = function() {
1780   return this.ne_;
1781 };
1782 geo.Bounds.prototype.ne_ = null;
1783 
1784 /**
1785  * The bounds' north coordinate.
1786  * @type Number
1787  */
1788 geo.Bounds.prototype.north = function() {
1789   return !this.isEmpty() ? this.ne_.lat() : null;
1790 };
1791 
1792 /**
1793  * The bounds' east coordinate.
1794  * @type Number
1795  */
1796 geo.Bounds.prototype.east = function() {
1797   return !this.isEmpty() ? this.ne_.lng() : null;
1798 };
1799 
1800 /**
1801  * The bounds' maximum altitude.
1802  * @type Number
1803  */
1804 geo.Bounds.prototype.top = function() {
1805   return !this.isEmpty() ? this.ne_.altitude() : null;
1806 };
1807 
1808 /**
1809  * Returns whether or not the bounds intersect the antimeridian.
1810  * @type Boolean
1811  */
1812 geo.Bounds.prototype.crossesAntimeridian = function() {
1813   return !this.isEmpty() && (this.sw_.lng() > this.ne_.lng());
1814 };
1815 
1816 /**
1817  * Returns whether or not the bounds have an altitude component.
1818  * @type Boolean
1819  */
1820 geo.Bounds.prototype.is3D = function() {
1821   return !this.isEmpty() && (this.sw_.is3D() || this.ne_.is3D());
1822 };
1823 
1824 /**
1825  * Returns whether or not the given point is inside the bounds.
1826  * @param {geo.Point} point The point to test.
1827  * @type Boolean
1828  */
1829 geo.Bounds.prototype.containsPoint = function(point) {
1830   point = new geo.Point(point);
1831   
1832   if (this.isEmpty()) {
1833     return false;
1834   }
1835 
1836   // check latitude
1837   if (!(this.south() <= point.lat() && point.lat() <= this.north())) {
1838     return false;
1839   }
1840 
1841   // check altitude
1842   if (this.is3D() && !(this.bottom() <= point.altitude() &&
1843                        point.altitude() <= this.top())) {
1844     return false;
1845   }
1846 
1847   // check longitude
1848   return this.containsLng_(point.lng());
1849 };
1850 
1851 /**
1852  * Returns whether or not the given line of longitude is inside the bounds.
1853  * @private
1854  * @param {Number} lng The longitude to test.
1855  * @type Boolean
1856  */
1857 geo.Bounds.prototype.containsLng_ = function(lng) {
1858   if (this.crossesAntimeridian()) {
1859     return (lng <= this.east() || lng >= this.west());
1860   } else {
1861     return (this.west() <= lng && lng <= this.east());
1862   }
1863 };
1864 
1865 /**
1866  * Gets the longitudinal span of the given west and east coordinates.
1867  * @private
1868  * @param {Number} west
1869  * @param {Number} east
1870  */
1871 function lngSpan_(west, east) {
1872   return (west > east) ? (east + 360 - west) : (east - west);
1873 }
1874 
1875 /**
1876  * Extends the bounds object by the given point, if the bounds don't already
1877  * contain the point. Longitudinally, the bounds will be extended either east
1878  * or west, whichever results in a smaller longitudinal span.
1879  * @param {geo.Point} point The point to extend the bounds by.
1880  */
1881 geo.Bounds.prototype.extend = function(point) {
1882   point = new geo.Point(point);
1883   
1884   if (this.containsPoint(point)) {
1885     return;
1886   }
1887 
1888   if (this.isEmpty()) {
1889     this.sw_ = this.ne_ = point;
1890     return;
1891   }
1892 
1893   // extend up or down
1894   var newBottom = this.bottom();
1895   var newTop = this.top();
1896 
1897   if (this.is3D()) {
1898     newBottom = Math.min(newBottom, point.altitude());
1899     newTop = Math.max(newTop, point.altitude());
1900   }
1901 
1902   // extend north or south
1903   var newSouth = Math.min(this.south(), point.lat());
1904   var newNorth = Math.max(this.north(), point.lat());
1905 
1906   var newWest = this.west();
1907   var newEast = this.east();
1908 
1909   if (!this.containsLng_(point.lng())) {
1910     // try extending east and try extending west, and use the one that
1911     // has the smaller longitudinal span
1912     var extendEastLngSpan = lngSpan_(newWest, point.lng());
1913     var extendWestLngSpan = lngSpan_(point.lng(), newEast);
1914 
1915     if (extendEastLngSpan <= extendWestLngSpan) {
1916       newEast = point.lng();
1917     } else {
1918       newWest = point.lng();
1919     }
1920   }
1921 
1922   // update the bounds' coordinates
1923   this.sw_ = new geo.Point(newSouth, newWest, newBottom);
1924   this.ne_ = new geo.Point(newNorth, newEast, newTop);
1925 };
1926 
1927 /**
1928  * Returns the bounds' latitude, longitude, and altitude span as an object
1929  * literal.
1930  * @return {Object} Returns an object literal containing `lat`, `lng`, and
1931  *     `altitude` properties. Altitude will be null in the case that the bounds
1932  *     aren't 3D.
1933  */
1934 geo.Bounds.prototype.span = function() {
1935   if (this.isEmpty()) {
1936     return {lat: 0, lng: 0, altitude: 0};
1937   }
1938   
1939   return {
1940     lat: (this.ne_.lat() - this.sw_.lat()),
1941     lng: lngSpan_(this.sw_.lng(), this.ne_.lng()),
1942     altitude: this.is3D() ? (this.ne_.altitude() - this.sw_.altitude()) : null
1943   };
1944 };
1945 
1946 /**
1947  * Determines whether or not the bounds object is empty, i.e. whether or not it
1948  * has no known associated points.
1949  * @type Boolean
1950  */
1951 geo.Bounds.prototype.isEmpty = function() {
1952   return (this.sw_ === null && this.sw_ === null);
1953 };
1954 
1955 /**
1956  * Gets the center of the bounds.
1957  * @type geo.Point
1958  */
1959 geo.Bounds.prototype.center = function() {
1960   if (this.isEmpty()) {
1961     return null;
1962   }
1963 
1964   return new geo.Point(
1965     (this.sw_.lat() + this.ne_.lat()) / 2,
1966     this.crossesAntimeridian() ?
1967         geo.math.normalizeLng(
1968             this.sw_.lng() +
1969             lngSpan_(this.sw_.lng(), this.ne_.lng()) / 2) :
1970         (this.sw_.lng() + this.ne_.lng()) / 2,
1971     (this.sw_.altitude() + this.ne_.altitude()) / 2);
1972 };
1973 
1974 // backwards compat
1975 geo.Bounds.prototype.getCenter = geo.Bounds.prototype.center;
1976 
1977 /**
1978  * Determines whether or not the bounds occupy the entire latitudinal range.
1979  * @type Boolean
1980  */
1981 geo.Bounds.prototype.isFullLat = function() {
1982   return !this.isEmpty() && (this.south() == -90 && this.north() == 90);
1983 };
1984 
1985 /**
1986  * Determines whether or not the bounds occupy the entire longitudinal range.
1987  * @type Boolean
1988  */
1989 geo.Bounds.prototype.isFullLng = function() {
1990   return !this.isEmpty() && (this.west() == -180 && this.east() == 180);
1991 };
1992 
1993 // TODO: equals(other)
1994 // TODO: intersects(other)
1995 // TODO: containsBounds(other)
1996 /**
1997  * Creates a new path from the given parameters.
1998  * @param {geo.Path|geo.Point[]|PointSrc[]|KmlLineString|GPolyline|GPolygon}
1999  *     path The path data.
2000  * @constructor
2001  */
2002 geo.Path = function() {
2003   this.coords_ = []; // don't use mutable objects in global defs
2004   var coordArraySrc = null;
2005   var i, n;
2006   
2007   // 1 argument constructor
2008   if (arguments.length == 1) {
2009     var path = arguments[0];
2010     
2011     // copy constructor
2012     if (path.constructor === geo.Path) {
2013       for (i = 0; i < path.numCoords(); i++) {
2014         this.coords_.push(new geo.Point(path.coord(i)));
2015       }
2016     
2017     // array constructor
2018     } else if (geo.util.isArray(path)) {
2019       coordArraySrc = path;
2020     
2021     // construct from Earth API object
2022     } else if (isEarthAPIObject_(path)) {
2023       var type = path.getType();
2024       
2025       // contruct from KmlLineString
2026       if (type == 'KmlLineString' ||
2027           type == 'KmlLinearRing') {
2028         n = path.getCoordinates().getLength();
2029         for (i = 0; i < n; i++) {
2030           this.coords_.push(new geo.Point(path.getCoordinates().get(i)));
2031         }
2032       
2033       // can't construct from the passed-in Earth object
2034       } else {
2035         throw new TypeError(
2036             'Could not create a path from the given arguments');
2037       }
2038     
2039     // GPolyline or GPolygon constructor
2040     } else if ('getVertex' in path && 'getVertexCount' in path) {
2041       n = path.getVertexCount();
2042       for (i = 0; i < n; i++) {
2043         this.coords_.push(new geo.Point(path.getVertex(i)));
2044       }
2045     
2046     // can't construct from the given argument
2047     } else {
2048       throw new TypeError('Could not create a path from the given arguments');
2049     }
2050   
2051   // Assume each argument is a PointSrc, i.e.
2052   // new Path(p1, p2, p3) ==>
2053   //    new Path([new Point(p1), new Point(p2), new Point(p3)])
2054   } else {
2055     coordArraySrc = arguments;
2056   }
2057   
2058   // construct from an array (presumably of PointSrcs)
2059   if (coordArraySrc) {
2060     for (i = 0; i < coordArraySrc.length; i++) {
2061       this.coords_.push(new geo.Point(coordArraySrc[i]));
2062     }
2063   }
2064 };
2065 
2066 /**#@+
2067   @field
2068 */
2069 
2070 /**
2071  * The path's coordinates array.
2072  * @type Number
2073  * @private
2074  */
2075 geo.Path.prototype.coords_ = null; // don't use mutable objects here
2076 
2077 /**#@-*/
2078 
2079 /**
2080  * Returns the string representation of the path.
2081  * @type String
2082  */
2083 geo.Path.prototype.toString = function() {
2084   return '[' + geo.util.arrayMap(this.coords_,function(p) {
2085                                   return p.toString();
2086                                 }).join(', ') + ']';
2087 };
2088 
2089 /**
2090  * Determines whether or not the given path is the same as this one.
2091  * @param {geo.Path} otherPath The other path.
2092  * @type Boolean
2093  */
2094 geo.Path.prototype.equals = function(p2) {
2095   for (var i = 0; i < p2.numCoords(); i++) {
2096     if (!this.coord(i).equals(p2.coord(i))) {
2097       return false;
2098     }
2099   }
2100   
2101   return true;
2102 };
2103 
2104 /**
2105  * Returns the number of coords in the path.
2106  */
2107 geo.Path.prototype.numCoords = function() {
2108   return this.coords_.length;
2109 };
2110 
2111 /**
2112  * Returns the coordinate at the given index in the path.
2113  * @param {Number} index The index of the coordinate.
2114  * @type geo.Point
2115  */
2116 geo.Path.prototype.coord = function(i) {
2117   // TODO: bounds check
2118   return this.coords_[i];
2119 };
2120 
2121 /**
2122  * Prepends the given coordinate to the path.
2123  * @param {geo.Point|PointSrc} coord The coordinate to prepend.
2124  */
2125 geo.Path.prototype.prepend = function(coord) {
2126   this.coords_.unshift(new geo.Point(coord));
2127 };
2128 
2129 /**
2130  * Appends the given coordinate to the path.
2131  * @param {geo.Point|PointSrc} coord The coordinate to append.
2132  */
2133 geo.Path.prototype.append = function(coord) {
2134   this.coords_.push(new geo.Point(coord));
2135 };
2136 
2137 /**
2138  * Inserts the given coordinate at the i'th index in the path.
2139  * @param {Number} index The index to insert into.
2140  * @param {geo.Point|PointSrc} coord The coordinate to insert.
2141  */
2142 geo.Path.prototype.insert = function(i, coord) {
2143   // TODO: bounds check
2144   this.coords_.splice(i, 0, new geo.Point(coord));
2145 };
2146 
2147 /**
2148  * Removes the coordinate at the i'th index from the path.
2149  * @param {Number} index The index of the coordinate to remove.
2150  */
2151 geo.Path.prototype.remove = function(i) {
2152   // TODO: bounds check
2153   this.coords_.splice(i, 1);
2154 };
2155 
2156 /**
2157  * Returns a sub path, containing coordinates starting from the
2158  * startIndex position, and up to but not including the endIndex
2159  * position.
2160  * @type geo.Path
2161  */
2162 geo.Path.prototype.subPath = function(startIndex, endIndex) {
2163   return this.coords_.slice(startIndex, endIndex);
2164 };
2165 
2166 /**
2167  * Reverses the order of the path's coordinates.
2168  */
2169 geo.Path.prototype.reverse = function() {
2170   this.coords_.reverse();
2171 };
2172 
2173 /**
2174  * Calculates the total length of the path using great circle distance
2175  * calculations.
2176  * @return {Number} The total length of the path, in meters.
2177  */
2178 geo.Path.prototype.distance = function() {
2179   var dist = 0;
2180   for (var i = 0; i < this.coords_.length - 1; i++) {
2181     dist += this.coords_[i].distance(this.coords_[i + 1]);
2182   }
2183   
2184   return dist;
2185 };
2186 
2187 /**
2188  * Returns whether or not the path, when closed, contains the given point.
2189  * Thanks to Mike Williams of http://econym.googlepages.com/epoly.htm and
2190  * http://alienryderflex.com/polygon/ for this code.
2191  * @param {geo.Point} point The point to test.
2192  */
2193 geo.Path.prototype.containsPoint = function(point) {
2194   var oddNodes = false;
2195   var y = point.lat();
2196   var x = point.lng();
2197   for (var i = 0; i < this.coords_.length; i++) {
2198     var j = (i + 1) % this.coords_.length;
2199     if (((this.coords_[i].lat() < y && this.coords_[j].lat() >= y) ||
2200          (this.coords_[j].lat() < y && this.coords_[i].lat() >= y)) &&
2201         (this.coords_[i].lng() + (y - this.coords_[i].lat()) /
2202             (this.coords_[j].lat() - this.coords_[i].lat()) *
2203             (this.coords_[j].lng() - this.coords_[i].lng()) < x)) {
2204       oddNodes = !oddNodes;
2205     }
2206   }
2207   
2208   return oddNodes;
2209 };
2210 
2211 /**
2212  * Returns the latitude/longitude bounds wholly containing this path.
2213  * @type geo.Bounds
2214  */
2215 geo.Path.prototype.bounds = function() {
2216   if (!this.numCoords()) {
2217     return new geo.Bounds();
2218   }
2219 
2220   var bounds = new geo.Bounds(this.coord(0));
2221 
2222   // TODO: optimize
2223   var numCoords = this.numCoords();
2224   for (var i = 1; i < numCoords; i++) {
2225     bounds.extend(this.coord(i));
2226   }
2227 
2228   return bounds;
2229 };
2230 // TODO: unit test
2231 
2232 /**
2233  * Returns the signed approximate area of the polygon formed by the path when
2234  * the path is closed.
2235  * @see http://econym.org.uk/gmap/epoly.htm
2236  * @private
2237  */
2238 geo.Path.prototype.signedArea_ = function() {
2239   var a = 0;
2240   var b = this.bounds();
2241   var x0 = b.west();
2242   var y0 = b.south();
2243 
2244   var numCoords = this.numCoords();
2245   for (var i = 0; i < numCoords; i++) {
2246     var j = (i + 1) % numCoords;
2247     var x1 = this.coord(i).distance(new geo.Point(this.coord(i).lat(), x0));
2248     var x2 = this.coord(j).distance(new geo.Point(this.coord(j).lat(), x0));
2249     var y1 = this.coord(i).distance(new geo.Point(y0, this.coord(i).lng()));
2250     var y2 = this.coord(j).distance(new geo.Point(y0, this.coord(j).lng()));
2251     a += x1 * y2 - x2 * y1;
2252   }
2253 
2254   return a * 0.5;
2255 };
2256 
2257 /**
2258  * Returns the approximate area of the polygon formed by the path when the path
2259  * is closed.
2260  * @return {Number} The approximate area, in square meters.
2261  * @see http://econym.org.uk/gmap/epoly.htm
2262  * @note This method only works with non-intersecting polygons.
2263  * @note The method is inaccurate for large regions because the Earth's
2264  *     curvature is not accounted for.
2265  */
2266 geo.Path.prototype.area = function() {
2267   return Math.abs(this.signedArea_());
2268 };
2269 // TODO: unit test
2270 
2271 /**
2272  * Returns whether or not the coordinates of the polygon formed by the path when
2273  * the path is closed are in counter clockwise order.
2274  * @type Boolean
2275  */
2276 geo.Path.prototype.isCounterClockwise_ = function() {
2277   return Boolean(this.signedArea_() >= 0);
2278 };
2279 /**
2280  * Creates a new polygon from the given parameters.
2281  * @param {geo.Polygon|geo.Path} outerBoundary
2282  *     The polygon's outer boundary.
2283  * @param {geo.Path[]} [innerBoundaries]
2284  *     The polygon's inner boundaries, if any.
2285  * @constructor
2286  */
2287 geo.Polygon = function() {
2288   this.outerBoundary_ = new geo.Path();
2289   this.innerBoundaries_ = [];
2290   var i;
2291   
2292   // 0 argument constructor
2293   if (arguments.length === 0) {
2294     
2295   // 1 argument constructor
2296   } else if (arguments.length == 1) {
2297     var poly = arguments[0];
2298     
2299     // copy constructor
2300     if (poly.constructor === geo.Polygon) {
2301       this.outerBoundary_ = new geo.Path(poly.outerBoundary());
2302       for (i = 0; i < poly.innerBoundaries().length; i++) {
2303         this.innerBoundaries_.push(new geo.Path(poly.innerBoundaries()[i]));
2304       }
2305     
2306     // construct from Earth API object
2307     } else if (isEarthAPIObject_(poly)) {
2308       var type = poly.getType();
2309 
2310       // construct from KmlLineString
2311       if (type == 'KmlLineString' ||
2312           type == 'KmlLinearRing') {
2313         this.outerBoundary_ = new geo.Path(poly);
2314       
2315       // construct from KmlPolygon
2316       } else if (type == 'KmlPolygon') {
2317         this.outerBoundary_ = new geo.Path(poly.getOuterBoundary());
2318         
2319         var ibChildNodes = poly.getInnerBoundaries().getChildNodes();
2320         var n = ibChildNodes.getLength();
2321         for (i = 0; i < n; i++) {
2322           this.innerBoundaries_.push(new geo.Path(ibChildNodes.item(i)));
2323         }
2324       
2325       // can't construct from the passed-in Earth object
2326       } else {
2327         throw new TypeError(
2328             'Could not create a polygon from the given arguments');
2329       }
2330     
2331     // treat first argument as an outer boundary path
2332     } else {
2333       this.outerBoundary_ = new geo.Path(arguments[0]);
2334     }
2335   
2336   // multiple argument constructor, either:
2337   // - arrays of numbers (outer boundary coords)
2338   // - a path (outer boundary) and an array of paths (inner boundaries)
2339   } else {
2340     if (arguments[0].length && typeof arguments[0][0] == 'number') {
2341       // ...new geo.Polygon([0,0], [1,1], [2,2]...
2342       this.outerBoundary_ = new geo.Path(arguments);
2343     } else if (arguments[1]) {
2344       // ...new geo.Polygon([ [0,0] ... ], [ [ [0,0], ...
2345       this.outerBoundary_ = new geo.Path(arguments[0]);
2346       if (!geo.util.isArray(arguments[1])) {
2347         throw new TypeError('Second argument to geo.Polygon constructor ' +
2348                             'must be an array of paths.');
2349       }
2350       
2351       for (i = 0; i < arguments[1].length; i++) {
2352         this.innerBoundaries_.push(new geo.Path(arguments[1][i]));
2353       }
2354     } else {
2355       throw new TypeError('Cannot create a path from the given arguments.');
2356     }
2357   }
2358 };
2359 
2360 /**#@+
2361   @field
2362 */
2363 
2364 /**
2365  * The polygon's outer boundary (path).
2366  * @type {geo.Path}
2367  * @private
2368  */
2369 geo.Polygon.prototype.outerBoundary_ = null;
2370 
2371 /**
2372  * The polygon's inner boundaries.
2373  * @type {geo.Path[]}
2374  * @private
2375  */
2376 geo.Polygon.prototype.innerBoundaries_ = null; // don't use mutable objects
2377 
2378 /**#@-*/
2379 
2380 /**
2381  * Returns the string representation of the polygon, useful primarily for
2382  * debugging purposes.
2383  * @type String
2384  */
2385 geo.Polygon.prototype.toString = function() {
2386   return 'Polygon: ' + this.outerBoundary().toString() +
2387       (this.innerBoundaries().length ?
2388         ', (' + this.innerBoundaries().length + ' inner boundaries)' : '');
2389 };
2390 
2391 
2392 /**
2393  * Returns the polygon's outer boundary path.
2394  * @type geo.Path
2395  */
2396 geo.Polygon.prototype.outerBoundary = function() {
2397   return this.outerBoundary_;
2398 };
2399 
2400 /**
2401  * Returns an array containing the polygon's inner boundaries.
2402  * You may freely add or remove geo.Path objects to this array.
2403  * @type geo.Path[]
2404  */
2405 geo.Polygon.prototype.innerBoundaries = function() {
2406   return this.innerBoundaries_;
2407 };
2408 // TODO: deprecate writability to this in favor of addInnerBoundary and
2409 // removeInnerBoundary
2410 
2411 /**
2412  * Returns whether or not the polygon contains the given point.
2413  * @see geo.Path.containsPoint
2414  * @see http://econym.googlepages.com/epoly.htm
2415  */
2416 geo.Polygon.prototype.containsPoint = function(point) {
2417   // outer boundary should contain the point
2418   if (!this.outerBoundary_.containsPoint(point)) {
2419     return false;
2420   }
2421   
2422   // none of the inner boundaries should contain the point
2423   for (var i = 0; i < this.innerBoundaries_.length; i++) {
2424     if (this.innerBoundaries_[i].containsPoint(point)) {
2425       return false;
2426     }
2427   }
2428   
2429   return true;
2430 };
2431 
2432 /**
2433  * Returns the latitude/longitude bounds wholly containing this polygon.
2434  * @type geo.Bounds
2435  */
2436 geo.Polygon.prototype.bounds = function() {
2437   return this.outerBoundary_.bounds();
2438 };
2439 
2440 /**
2441  * Returns the approximate area of the polygon.
2442  * @return {Number} The approximate area, in square meters.
2443  * @see geo.Path.area
2444  */
2445 geo.Polygon.prototype.area = function() {
2446   // start with outer boundary area
2447   var area = this.outerBoundary_.area();
2448   
2449   // subtract inner boundary areas
2450   // TODO: handle double counting of intersections
2451   for (var i = 0; i < this.innerBoundaries_.length; i++) {
2452     area -= this.innerBoundaries_[i].area();
2453   }
2454   
2455   return area;
2456 };
2457 
2458 /**
2459  * Returns whether or not the polygon's outer boundary coordinates are
2460  * in counter clockwise order.
2461  * @type Boolean
2462  */
2463 geo.Polygon.prototype.isCounterClockwise = function() {
2464   return this.outerBoundary_.isCounterClockwise_();
2465 };
2466 
2467 /**
2468  * Ensures that the polygon's outer boundary coordinates are in counter
2469  * clockwise order by reversing them if they are counter clockwise.
2470  * @see geo.Polygon.isCounterClockwise
2471  */
2472 geo.Polygon.prototype.makeCounterClockwise = function() {
2473   if (this.isCounterClockwise()) {
2474     this.outerBoundary_.reverse();
2475   }
2476 };
2477 /**
2478  * The geo.util namespace contains generic JavaScript and JS/Geo utility
2479  * functions.
2480  * @namespace
2481  */
2482 geo.util = {isnamespace_:true};
2483 
2484 /**
2485  * Ensures that Array.prototype.map is available
2486  * @param {Array} arr Array target of mapFn
2487  * @param {function(*, number,Array):*} mapFn Function that produces an element of the new Array from an element of the current one.
2488  * @param {Object=} thisp Object to use as this when executing mapFn. (optional).
2489  * @return {Array} A new array with the results of calling mapFn on every element in arr.
2490  */
2491 geo.util.arrayMap = (function() {
2492   function arrayMap(arr, mapFn, thisp) {
2493     return arr.map(mapFn, thisp);
2494   }
2495 
2496   //see https://developer.mozilla.org/En/Core_JavaScript_1.5_Reference:Objects:Array:map
2497   function noArrayMap(arr, mapFn, thisp) {
2498     if (typeof mapFn != 'function') {
2499       throw new TypeError('map() requires a mapping function.');
2500     }
2501 
2502     var len = arr.length,
2503         res = new Array(len);
2504 
2505     for (var i = 0; i < len; i++) {
2506       if (i in arr) {
2507         res[i] = mapFn.call(thisp, arr[i], i, arr);
2508       }
2509     }
2510 
2511     return res;
2512   }
2513   
2514   if (!('map' in Array.prototype)) {
2515     return noArrayMap;
2516     
2517   } else {
2518     return arrayMap;
2519   }
2520 })();
2521 
2522 /**
2523  * Determines whether or not the object is `undefined`.
2524  * @param {Object} object The object to test.
2525  * @note Taken from Prototype JS library
2526  */
2527 geo.util.isUndefined = function(object) {
2528   return typeof object == 'undefined';
2529 };
2530 
2531 /**
2532  * Determines whether or not the object is a JavaScript array.
2533  * @param {Object} object The object to test.
2534  * @note Taken from Prototype JS library
2535  */
2536 geo.util.isArray = function(object) {
2537   return object !== null && typeof object == 'object' &&
2538       'splice' in object && 'join' in object;
2539 };
2540 
2541 /**
2542  * Determines whether or not the object is a JavaScript function.
2543  * @param {Object} object The object to test.
2544  * @note Taken from Prototype JS library
2545  */
2546 geo.util.isFunction = function(object) {
2547   return object !== null && typeof object == 'function' &&
2548       'call' in object && 'apply' in object;
2549 };
2550 
2551 /**
2552  * Determines whether or not the given object is an Earth API object.
2553  * @param {Object} object The object to test.
2554  * @private
2555  */
2556 function isEarthAPIObject_(object) {
2557   return object !== null &&
2558       (typeof object == 'function' || typeof object == 'object') &&
2559       'getType' in object;
2560 }
2561 
2562 /**
2563  * Determines whether or not the object is an object literal (a.k.a. hash).
2564  * @param {Object} object The object to test.
2565  */
2566 geo.util.isObjectLiteral = function(object) {
2567   return object !== null && typeof object == 'object' &&
2568       object.constructor === Object && !isEarthAPIObject_(object);
2569 };
2570 
2571 /**
2572  * Determins whether or not the given object is a google.maps.LatLng object
2573  * (GLatLng).
2574  */
2575 function isGLatLng_(object) {
2576   return (window.google &&
2577           window.google.maps &&
2578           window.google.maps.LatLng &&
2579           object.constructor === window.google.maps.LatLng);
2580 }
2581 window.geo = geo;
2582 })();
2583 /*
2584 Copyright 2009 Google Inc.
2585 
2586 Licensed under the Apache License, Version 2.0 (the "License");
2587 you may not use this file except in compliance with the License.
2588 You may obtain a copy of the License at
2589 
2590      http://www.apache.org/licenses/LICENSE-2.0
2591 
2592 Unless required by applicable law or agreed to in writing, software
2593 distributed under the License is distributed on an "AS IS" BASIS,
2594 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
2595 See the License for the specific language governing permissions and
2596 limitations under the License.
2597 */
2598 (function() {
2599 /**
2600  * @class The root class/namespace hybrid for the Earth API extensions library.
2601  * This class groups functionality into namespaces such as
2602  * {@link GEarthExtensions#dom } and {@link GEarthExtensions#fx }.
2603  * @param {GEPlugin} pluginInstance The Google Earth Plugin instance to
2604  *     associate this GEarthExtensions instance with.
2605  * @example
2606  * var gex = new GEarthExtensions(ge); // ge is an instance of GEPlugin
2607  * gex.dom.clearFeatures(); // gex is an instance of a class, and gex.dom
2608  *                          // is effectively a namespace grouping
2609  *                          // functionality
2610  */
2611 var GEarthExtensions = function(pluginInstance) {
2612   // create class
2613   var me = this;
2614   this.pluginInstance = pluginInstance;
2615   
2616   // bind all functions in namespaces to this GEarthExtensions instance
2617   /** @private */
2618   function bindFunction_(fn_) {
2619     return function() {
2620       return fn_.apply(me, arguments);
2621     };
2622   }
2623 
2624   /** @private */
2625   function bindNamespaceMembers_(nsParent) {
2626     for (var mstr in nsParent) {
2627       var member = nsParent[mstr];
2628       
2629       // bind this namespace's functions to the GEarthExtensions object
2630       if (geo.util.isFunction(member)) {
2631         if (member.isclass_) {
2632           // if it's a class constructor, give it access to this
2633           // GEarthExtensions instance
2634           member.extInstance_ = me;
2635         } else {
2636           // function's not a constructor, just bind it to this
2637           // GEarthExtensions instance
2638           nsParent[mstr] = bindFunction_(member);
2639         }
2640       }
2641       
2642       // duplicate sub-namespace objects (required for multiple instances to
2643       // work) and bind functions of all sub-namespaces
2644       if (isExtensionsNamespace_(member)) {
2645         var nsDuplicate = {};
2646         for (var subMstr in member) {
2647           nsDuplicate[subMstr] = member[subMstr];
2648         }
2649         
2650         bindNamespaceMembers_(nsDuplicate);
2651         
2652         nsParent[mstr] = nsDuplicate;
2653       }
2654     }
2655   }
2656   
2657   bindNamespaceMembers_(this);
2658 };
2659 /** @private */
2660 var AUTO_ = Infinity; // for dom builder (auto property setters)
2661 
2662 /** @private */
2663 var ALLOWED_ = null;
2664 
2665 /** @private */
2666 var REQUIRED_ = undefined;
2667 
2668 /**
2669  * Checks a given parameters object against an parameter spec,
2670  * throwing exceptions as necessary, and returning the resulting options object
2671  * with defaults filled in.
2672  * @param {Object} explicitParams The parameters object to check.
2673  * @param {Boolean} allowAll Whether or not to allow all parameters, or limit
2674  *     allowed parameters to those listed in the parameter spec.
2675  * @param {Object} paramSpec The parameter spec, which should be an object whose
2676  *     properties are the properties expected in the given parameters object and
2677  *     whose property values are REQUIRED_ if the property is
2678  *     required or some other value to set a default value.
2679  * @return Returns a shallow copy of the given parameters object, cleaned up
2680  *     according to the parameters spec and with default values filled in.
2681  * @ignore
2682  */
2683 function checkParameters_(explicitParams, allowAll, paramSpec) {
2684   // shallow copy explicitParameters
2685   var finalParams = {};
2686   
2687   explicitParams = explicitParams || {};
2688   paramSpec = paramSpec || {};
2689   
2690   for (var member in explicitParams) {
2691     // if not allowing all, check that it's in the param spec
2692     if (!allowAll && !(member in paramSpec)) {
2693       var allowed = [];
2694       for (var m in paramSpec) {
2695         allowed.push(m);
2696       }
2697       
2698       throw new Error(
2699           'Unexpected parameter \'' + member + '\'. ' +
2700           'Allowed parameters are: ' + allowed.join(', ') + '.');
2701     }
2702     
2703     finalParams[member] = explicitParams[member];
2704   }
2705   
2706   // copy in defaults
2707   for (member in paramSpec) {
2708     if (!(member in finalParams)) {
2709       // if member was required, throw an exception
2710       if (paramSpec[member] === REQUIRED_) {
2711         throw new Error(
2712             'Required parameter \'' + member + '\' was not passed.');
2713       }
2714       
2715       if (paramSpec[member] != ALLOWED_ &&
2716           paramSpec[member] != AUTO_) {
2717         // ALLOWED_ and AUTO_ are placeholders,
2718         // not default values
2719         finalParams[member] = paramSpec[member];
2720       }
2721     }
2722   }
2723   
2724   return finalParams;
2725 }
2726 
2727 /**
2728  * Creates a new 'class' from the provided constructor function and mixes in
2729  * members of provided mixin classes.
2730  * @private
2731  */
2732 function createClass_() {
2733   var mixins = [];
2734   var constructorFn = null;
2735   
2736   if (geo.util.isArray(arguments[0])) {
2737     mixins = arguments[0];
2738     constructorFn = arguments[1];
2739   } else {
2740     constructorFn = arguments[0];
2741   }
2742   
2743   constructorFn.isclass_ = true;
2744   
2745   for (var i = 0; i < mixins.length; i++) {
2746     for (var k in mixins[i].prototype) {
2747       constructorFn.prototype[k] = mixins[i].prototype[k];
2748     }
2749   }
2750   
2751   return constructorFn;
2752 }
2753 
2754 /**
2755  * Determines whether or not the object is a GEarthExtensions namespace.
2756  * @param {Object} object The object to test.
2757  * @private
2758  */
2759 function isExtensionsNamespace_(object) {
2760   return object !== null && typeof object == 'object' &&
2761       'isnamespace_' in object && object.isnamespace_;
2762 }
2763 
2764 /**
2765  * Determines whether or not the given object is directly an instance
2766  * of the specified Earth API type.
2767  * @param {Object} object The object to test.
2768  * @param {String} type The Earth API type string, i.e. 'KmlPlacemark'
2769  */
2770 GEarthExtensions.isInstanceOfEarthInterface = function(object, type) {
2771   // TODO: double check that all earth interfaces are typeof 'function'
2772   return object !== null &&
2773       (typeof object == 'object' || typeof object == 'function') &&
2774       'getType' in object && object.getType() == type;
2775 };
2776 /**
2777  * Contains DOM builder functions (buildXX) and DOM
2778  * manipulation/traversal functions.
2779  * @namespace
2780  */
2781 GEarthExtensions.prototype.dom = {isnamespace_:true};
2782 
2783 /**
2784  * This is a sort of parametrized decorator around a fundamental constructor
2785  * DOM builder function,
2786  * it calls GEPlugin's buildXX factory functions, allows for a type of
2787  * inheritance, provides extra functionality such as automatic property setters,
2788  * default arguments (i.e. fn('bar', {cat:'dog'}) == fn({foo:'bar', cat:'dog'}))
2789  * and checking if the parameter is an instance of the object we're constructing
2790  * @private
2791  */
2792 function domBuilder_(params) {
2793   if (params.apiInterface && !geo.util.isArray(params.apiInterface)) {
2794     params.apiInterface = [params.apiInterface];
2795   }
2796   
2797   // merge in base builder params
2798   // TODO: detect circular base builders
2799   var base = params.base;
2800   while (base) {
2801     // merge in propertyspec
2802     if ('propertySpec' in base.builderParams) {
2803       if (!('propertySpec' in params)) {
2804         params.propertySpec = [];
2805       }
2806       
2807       for (var member in base.builderParams.propertySpec) {
2808         if (!(member in params.propertySpec)) {
2809           params.propertySpec[member] =
2810               base.builderParams.propertySpec[member];
2811         }
2812       }
2813     }
2814     
2815     // set Earth API interface if none was set for this builder
2816     if (!params.apiInterface) {
2817       params.apiInterface = base.builderParams.apiInterface;
2818     }
2819     
2820     // set Earth API factory fn if none was set for this builder
2821     if (!params.apiFactoryFn) {
2822       params.apiFactoryFn = base.builderParams.apiFactoryFn;
2823     }
2824     
2825     base = base.builderParams.base;
2826   }
2827   
2828   // merge in root dom builder property spec (only id is universal to
2829   // all DOM objects)
2830   var rootPropertySpec = {
2831     id: ''
2832   };
2833   
2834   for (member in rootPropertySpec) {
2835     if (!(member in params.propertySpec)) {
2836       params.propertySpec[member] = rootPropertySpec[member];
2837     }
2838   }
2839   
2840   /** @ignore */
2841   var builderFn = function() {
2842     var options = {};
2843     var i;
2844     
2845     // construct options literal to pass to constructor function
2846     // from arguments
2847     if (arguments.length === 0) {
2848       throw new TypeError('Cannot create object without any arguments!');
2849     } else if (arguments.length == 1) {
2850       // the argument to the function may already be an instance of the
2851       // interface we're trying to create... if so, then simply return the
2852       // instance
2853       
2854       // TODO: maybe clone the object instead of just returning it
2855       for (i = 0; i < params.apiInterface.length; i++) {
2856         if (GEarthExtensions.isInstanceOfEarthInterface(
2857             arguments[0], params.apiInterface[i])) {
2858           return arguments[0];
2859         }
2860       }
2861       
2862       // find out if the first argument is the default property or the
2863       // options literal and construct the final options literal to
2864       // pass to the constructor function
2865       var arg = arguments[0];
2866       if (geo.util.isObjectLiteral(arg)) {
2867         // passed in only the options literal
2868         options = arg;
2869       } else if ('defaultProperty' in params) {
2870         // passed in default property and no options literal
2871         options[params.defaultProperty] = arg;
2872       } else {
2873         throw new TypeError('Expected options object');
2874       }
2875     } else if (arguments.length == 2) {
2876       if ('defaultProperty' in params) {
2877         // first parameter is the value of the default property, and the
2878         // other is the options literal
2879         options = arguments[1];
2880         options[params.defaultProperty] = arguments[0];
2881       } else {
2882         throw new Error('No default property for the DOM builder');
2883       }
2884     }
2885     
2886     // check passed in options against property spec
2887     options = checkParameters_(options,
2888         false, params.propertySpec);
2889     
2890     // call Earth API factory function, i.e. createXX(...)
2891     var newObj = this.pluginInstance[params.apiFactoryFn](options.id);
2892 
2893     // call constructor fn with factory-created object and options literal
2894     if (!geo.util.isUndefined(params.constructor)) {
2895       params.constructor.call(this, newObj, options);
2896     }
2897     
2898     // call base builder constructor functions
2899     base = params.base;
2900     while (base) {
2901       // call ancestor constructor functions
2902       if ('constructor' in base.builderParams) {
2903         base.builderParams.constructor.call(this, newObj, options);
2904       }
2905       
2906       base = base.builderParams.base;
2907     }
2908     
2909     // run automatic property setters as defined in property spec
2910     for (var property in params.propertySpec) {
2911       // TODO: abstract away into isAuto()
2912       if (params.propertySpec[property] === AUTO_ &&
2913           property in options) {
2914         // auto setters calls newObj.setXx(options[xx]) if xx is in options
2915         newObj['set' + property.charAt(0).toUpperCase() + property.substr(1)](options[property]);
2916       }
2917     }
2918     
2919     return newObj;
2920   };
2921   
2922   builderFn.builderParams = params;
2923   return builderFn;
2924 }
2925 /** @ignore */
2926 GEarthExtensions.prototype.dom.buildFeature_ = domBuilder_({
2927   propertySpec: {
2928     name: AUTO_,
2929     visibility: AUTO_,
2930     description: AUTO_,
2931     snippet: AUTO_,
2932     
2933     // allowed properties
2934     region: ALLOWED_
2935   },
2936   constructor: function(featureObj, options) {
2937     if (options.region) {
2938       featureObj.setRegion(this.dom.buildRegion(options.region));
2939     }
2940   }
2941 });
2942 
2943 /**
2944  * Creates a new placemark with the given parameters.
2945  * @function
2946  * @param {Object} options The parameters of the placemark to create.
2947  * @param {String} [options.name] The name of the feature.
2948  * @param {Boolean} [options.visibility] Whether or not the feature should
2949  *     be visible.
2950  * @param {String} [options.description] An HTML description for the feature;
2951  *     may be used as balloon text.
2952  * @param {PointOptions|KmlPoint} [options.point] A point geometry to use in the
2953  *     placemark.
2954  * @param {LineStringOptions|KmlLineString} [options.lineString] A line string
2955  *     geometry to use in the placemark.
2956  * @param {LinearRingOptions|KmlLinearRing} [options.linearRing] A linear ring
2957  *     geometry to use in the placemark.
2958  * @param {PolygonOptions|KmlPolygon} [options.polygon] A polygon geometry to
2959  *     use in the placemark.
2960  * @param {ModelOptions|KmlModel} [options.model] A model geometry to use
2961  *     in the placemark.
2962  * @param {MultiGeometryOptions|KmlMultiGeometry} [options.multiGeometry] A
2963  *     multi-geometry to use in the placemark.
2964  * @param {KmlGeometry[]} [options.geometries] An array of geometries to add
2965  *     to the placemark.
2966  * @param {KmlAltitudeModeEnum} [options.altitudeMode] A convenience property
2967  *     for the placemark geometry's altitude mode.
2968  * @param {String} [options.stockIcon] A convenience property to set the
2969  *     point placemark's icon to a stock icon, e.g. 'paddle/wht-blank'.
2970  *     Stock icons reside under 'http://maps.google.com/mapfiles/kml/...'.
2971  * @param {StyleOptions|KmlStyleSelector} [options.style] The style to use for
2972  *     this placemark. See also GEarthExtensions.dom.buildStyle.
2973  * @param {StyleOptions|KmlStyleSelector} [options.highlightStyle] The
2974  *     highlight style to use for this placemark. If this option is used, the
2975  *     style and highlightStyle form a style map.
2976  * @param {IconStyleOptions} [options.icon] A convenience property to build the
2977  *     point placemark's icon style from the given options.
2978  * @param {String} [options.stockIcon] A convenience property to set the
2979  *     point placemark's icon to a stock icon, e.g. 'paddle/wht-blank'.
2980  *     Stock icons reside under 'http://maps.google.com/mapfiles/kml/...'.
2981  * @type KmlPlacemark
2982  */
2983 GEarthExtensions.prototype.dom.buildPlacemark = domBuilder_({
2984   apiInterface: 'KmlPlacemark',
2985   base: GEarthExtensions.prototype.dom.buildFeature_,
2986   apiFactoryFn: 'createPlacemark',
2987   propertySpec: {
2988     // allowed geometries
2989     point: ALLOWED_,
2990     lineString: ALLOWED_,
2991     linearRing: ALLOWED_,
2992     polygon: ALLOWED_,
2993     model: ALLOWED_,
2994     geometries: ALLOWED_,
2995     
2996     // convenience (pass through to geometry)
2997     altitudeMode: ALLOWED_,
2998     
2999     // styling
3000     stockIcon: ALLOWED_,
3001     icon: ALLOWED_,
3002     style: ALLOWED_,
3003     highlightStyle: ALLOWED_
3004   },
3005   constructor: function(placemarkObj, options) {
3006     // geometries
3007     var geometries = [];
3008     if (options.point) {
3009       geometries.push(this.dom.buildPoint(options.point));
3010     }
3011     if (options.lineString) {
3012       geometries.push(this.dom.buildLineString(options.lineString));
3013     }
3014     if (options.linearRing) {
3015       geometries.push(this.dom.buildLinearRing(options.linearRing));
3016     }
3017     if (options.polygon) {
3018       geometries.push(this.dom.buildPolygon(options.polygon));
3019     }
3020     if (options.model) {
3021       geometries.push(this.dom.buildModel(options.model));
3022     }
3023     if (options.multiGeometry) {
3024       geometries.push(this.dom.buildMultiGeometry(options.multiGeometry));
3025     }
3026     if (options.geometries) {
3027       geometries = geometries.concat(options.geometries);
3028     }
3029   
3030     if (geometries.length > 1) {
3031       placemarkObj.setGeometry(this.dom.buildMultiGeometry(geometries));
3032     } else if (geometries.length == 1) {
3033       placemarkObj.setGeometry(geometries[0]);
3034     }
3035   
3036     // set styles
3037     if (options.stockIcon) {
3038       options.icon = options.icon || {};
3039       options.icon.stockIcon = options.stockIcon;
3040     }
3041   
3042     if (options.icon) {
3043       if (!options.style) {
3044         options.style = {};
3045       }
3046     
3047       options.style.icon = options.icon;
3048     }
3049     
3050     // convenience
3051     if ('altitudeMode' in options) {
3052       placemarkObj.getGeometry().setAltitudeMode(options.altitudeMode);
3053     }
3054   
3055     // NOTE: for this library, allow EITHER a style or a styleUrl, not both..
3056     // if you want both, you'll have to do it manually
3057     if (options.style) {
3058       if (options.highlightStyle) {
3059         // style map
3060         var styleMap = this.pluginInstance.createStyleMap('');
3061       
3062         // set normal style
3063         if (typeof options.style == 'string') {
3064           styleMap.setNormalStyleUrl(options.style);
3065         } else {
3066           styleMap.setNormalStyle(this.dom.buildStyle(options.style));
3067         }
3068       
3069         // set highlight style
3070         if (typeof options.highlightStyle == 'string') {
3071           styleMap.setHighlightStyleUrl(options.highlightStyle);
3072         } else {
3073           styleMap.setHighlightStyle(this.dom.buildStyle(
3074               options.highlightStyle));
3075         }
3076       
3077         // assign style map
3078         placemarkObj.setStyleSelector(styleMap);
3079       } else {
3080         // single style
3081         if (typeof options.style == 'string') {
3082           placemarkObj.setStyleUrl(options.style);
3083         } else {
3084           placemarkObj.setStyleSelector(this.dom.buildStyle(options.style));
3085         }
3086       }
3087     }
3088   }
3089 });
3090 
3091 /**
3092  * Convenience method to build a point placemark.
3093  * @param {PointOptions|KmlPoint} point The point geometry.
3094  * @param {Object} options The parameters of the placemark to create.
3095  * @see GEarthExtensions#dom.buildPlacemark
3096  * @function
3097  */
3098 GEarthExtensions.prototype.dom.buildPointPlacemark = domBuilder_({
3099   base: GEarthExtensions.prototype.dom.buildPlacemark,
3100   defaultProperty: 'point'
3101 });
3102 
3103 /**
3104  * Convenience method to build a linestring placemark.
3105  * @param {LineStringOptions|KmlLineString} lineString The line string geometry.
3106  * @param {Object} options The parameters of the placemark to create.
3107  * @see GEarthExtensions#dom.buildPlacemark
3108  * @function
3109  */
3110 GEarthExtensions.prototype.dom.buildLineStringPlacemark = domBuilder_({
3111   base: GEarthExtensions.prototype.dom.buildPlacemark,
3112   defaultProperty: 'lineString'
3113 });
3114 
3115 /**
3116  * Convenience method to build a polygon placemark.
3117  * @param {PolygonOptions|KmlPolygon} polygon The polygon geometry.
3118  * @param {Object} options The parameters of the placemark to create.
3119  * @see GEarthExtensions#dom.buildPlacemark
3120  * @function
3121  */
3122 GEarthExtensions.prototype.dom.buildPolygonPlacemark = domBuilder_({
3123   base: GEarthExtensions.prototype.dom.buildPlacemark,
3124   defaultProperty: 'polygon'
3125 });
3126 
3127 
3128 /**
3129  * Creates a new network link with the given parameters.
3130  * @function
3131  * @param {LinkOptions} [link] An object describing the link to use for this
3132  *     network link.
3133  * @param {Object} options The parameters of the network link to create.
3134  * @param {String} [options.name] The name of the feature.
3135  * @param {Boolean} [options.visibility] Whether or not the feature should
3136  *     be visible.
3137  * @param {String} [options.description] An HTML description for the feature;
3138  *     may be used as balloon text.
3139  * @param {LinkOptions} [options.link] The link to use.
3140  * @param {Boolean} [options.flyToView] Whether or not to fly to the default
3141  *     view of the network link'd content.
3142  * @param {Boolean} [options.refreshVisibility] Whether or not a refresh should
3143  *     reset the visibility of child features.
3144  * @type KmlNetworkLink
3145  */
3146 GEarthExtensions.prototype.dom.buildNetworkLink = domBuilder_({
3147   apiInterface: 'KmlNetworkLink',
3148   base: GEarthExtensions.prototype.dom.buildFeature_,
3149   apiFactoryFn: 'createNetworkLink',
3150   defaultProperty: 'link',
3151   propertySpec: {
3152     link: ALLOWED_,
3153     
3154     // auto properties
3155     flyToView: AUTO_,
3156     refreshVisibility: AUTO_
3157   },
3158   constructor: function(networkLinkObj, options) {
3159     if (options.link) {
3160       networkLinkObj.setLink(this.dom.buildLink(options.link));
3161     }
3162   }
3163 });
3164 // TODO: unit tests
3165 
3166 /** @ignore */
3167 GEarthExtensions.prototype.dom.buildContainer_ = domBuilder_({
3168   base: GEarthExtensions.prototype.dom.buildFeature_,
3169   propertySpec: {
3170     children: ALLOWED_
3171   },
3172   constructor: function(containerObj, options) {
3173     // children
3174     if (options.children) {
3175       for (var i = 0; i < options.children.length; i++) {
3176         containerObj.getFeatures().appendChild(options.children[i]);
3177       }
3178     }  
3179   }
3180 });
3181 
3182 /**
3183  * Creates a new folder with the given parameters.
3184  * @function
3185  * @param {KmlFeature[]} [children] The children of this folder.
3186  * @param {Object} options The parameters of the folder to create.
3187  * @param {String} [options.name] The name of the feature.
3188  * @param {Boolean} [options.visibility] Whether or not the feature should
3189  *     be visible.
3190  * @param {String} [options.description] An HTML description for the feature;
3191  *     may be used as balloon text.
3192  * @param {KmlFeature[]} [options.children] The children of this folder.
3193  * @type KmlFolder
3194  */
3195 GEarthExtensions.prototype.dom.buildFolder = domBuilder_({
3196   apiInterface: 'KmlFolder',
3197   base: GEarthExtensions.prototype.dom.buildContainer_,
3198   apiFactoryFn: 'createFolder',
3199   defaultProperty: 'children'
3200 });
3201 // TODO: unit tests
3202 
3203 /**
3204  * Creates a new document with the given parameters.
3205  * @function
3206  * @param {KmlFeature[]} [children] The children of this document.
3207  * @param {Object} options The parameters of the document to create.
3208  * @param {String} [options.name] The name of the feature.
3209  * @param {Boolean} [options.visibility] Whether or not the feature should
3210  *     be visible.
3211  * @param {String} [options.description] An HTML description for the feature;
3212  *     may be used as balloon text.
3213  * @param {KmlFeature[]} [options.children] The children of this document.
3214  * @type KmlDocument
3215  */
3216 GEarthExtensions.prototype.dom.buildDocument = domBuilder_({
3217   apiInterface: 'KmlDocument',
3218   base: GEarthExtensions.prototype.dom.buildContainer_,
3219   apiFactoryFn: 'createDocument',
3220   defaultProperty: 'children'
3221 });
3222 // TODO: unit tests
3223 
3224 /** @ignore */
3225 GEarthExtensions.prototype.dom.buildOverlay_ = domBuilder_({
3226   base: GEarthExtensions.prototype.dom.buildFeature_,
3227   propertySpec: {
3228     color: ALLOWED_,
3229     icon: ALLOWED_,
3230     
3231     // auto properties
3232     drawOrder: AUTO_
3233   },
3234   constructor: function(overlayObj, options) {
3235     // color
3236     if (options.color) {
3237       overlayObj.getColor().set(this.util.parseColor(options.color));
3238     }
3239   
3240     // icon
3241     if (options.icon) {
3242       var icon = this.pluginInstance.createIcon('');
3243       overlayObj.setIcon(icon);
3244     
3245       if (typeof options.icon == 'string') {
3246         // default just icon href
3247         icon.setHref(options.icon);
3248       }
3249     }
3250   }
3251 });
3252 
3253 /**
3254  * Creates a new ground overlay with the given parameters.
3255  * @function
3256  * @param {String} [icon] The URL of the overlay image.
3257  * @param {Object} options The parameters of the ground overlay to create.
3258  * @param {String} [options.name] The name of the feature.
3259  * @param {Boolean} [options.visibility] Whether or not the feature should
3260  *     be visible.
3261  * @param {String} [options.description] An HTML description for the feature.
3262  * @param {String} [options.color] A color to apply on the overlay.
3263  * @param {String} [options.icon] The URL of the overlay image.
3264  * @param {Number} [options.drawOrder] The drawing order of the overlay;
3265  *     overlays with higher draw orders appear on top of those with lower
3266  *     draw orders.
3267  * @param {Number} [options.altitude] The altitude of the ground overlay, in
3268  *     meters.
3269  * @param {KmlAltitudeModeEnum} [options.altitudeMode] The altitude mode of the
3270  *     ground overlay.
3271  * @param {Object} options.box The bounding box for the overlay.
3272  * @param {Number} options.box.north The north latitude for the overlay.
3273  * @param {Number} options.box.east The east longitude for the overlay.
3274  * @param {Number} options.box.south The south latitude for the overlay.
3275  * @param {Number} options.box.west The west longitude for the overlay.
3276  * @param {Number} [options.box.rotation] The rotation, in degrees, of the
3277  *     overlay.
3278  * @type KmlGroundOverlay
3279  */
3280 GEarthExtensions.prototype.dom.buildGroundOverlay = domBuilder_({
3281   apiInterface: 'KmlGroundOverlay',
3282   base: GEarthExtensions.prototype.dom.buildOverlay_,
3283   apiFactoryFn: 'createGroundOverlay',
3284   defaultProperty: 'icon',
3285   propertySpec: {
3286     // required properties
3287     box: REQUIRED_,
3288     
3289     // auto properties
3290     altitude: AUTO_,
3291     altitudeMode: AUTO_
3292   },
3293   constructor: function(groundOverlayObj, options) {
3294     if (options.box) {
3295       // TODO: exception if any of the options are missing
3296       var box = this.pluginInstance.createLatLonBox('');
3297       box.setBox(options.box.north, options.box.south,
3298                  options.box.east, options.box.west,
3299                  options.box.rotation ? options.box.rotation : 0);
3300       groundOverlayObj.setLatLonBox(box);
3301     }
3302   }
3303 });
3304 
3305 
3306 
3307 /**
3308  * Creates a new screen overlay with the given parameters.
3309  * @function
3310  * @param {String} [icon] The URL of the overlay image.
3311  * @param {Object} options The parameters of the screen overlay to create.
3312  * @param {String} [options.name] The name of the feature.
3313  * @param {Boolean} [options.visibility] Whether or not the feature should
3314  *     be visible.
3315  * @param {String} [options.description] An HTML description for the feature.
3316  * @param {String} [options.color] A color to apply on the overlay.
3317  * @param {String} [options.icon] The URL of the overlay image.
3318  * @param {Number} [options.drawOrder] The drawing order of the overlay;
3319  *     overlays with higher draw orders appear on top of those with lower
3320  *     draw orders.
3321  * @param {Vec2Src} [options.overlayXY] The registration point in the overlay
3322  *     that will be placed at the given screenXY point and potentially
3323  *     rotated about. This object will be passed to
3324  *     GEarthExtensions#dom.setVec2. The default is the top left of the overlay.
3325  *     Note that the behavior of overlayXY in GEarthExtensions is KML-correct;
3326  *     whereas in the Earth API overlayXY and screenXY are swapped.
3327  * @param {Vec2Src} options.screenXY The position in the plugin window
3328  *     that the screen overlay should appear at. This object will
3329  *     be passed to GEarthExtensions#dom.setVec2.
3330  *     Note that the behavior of overlayXY in GEarthExtensions is KML-correct;
3331  *     whereas in the Earth API overlayXY and screenXY are swapped.
3332  * @param {Vec2Src} options.size The size of the overlay. This object will
3333  *     be passed to GEarthExtensions#dom.setVec2.
3334  * @param {KmlAltitudeModeEnum} [options.altitudeMode] The altitude mode of the
3335  *     ground overlay.
3336  * @param {Number} [options.rotation] The rotation of the overlay, in degrees.
3337  * @type KmlScreenOverlay
3338  */
3339 GEarthExtensions.prototype.dom.buildScreenOverlay = domBuilder_({
3340   apiInterface: 'KmlScreenOverlay',
3341   base: GEarthExtensions.prototype.dom.buildOverlay_,
3342   apiFactoryFn: 'createScreenOverlay',
3343   defaultProperty: 'icon',
3344   propertySpec: {
3345     // required properties
3346     screenXY: REQUIRED_,
3347     size: REQUIRED_,
3348 
3349     // auto properties
3350     rotation: AUTO_,
3351 
3352     // optional properties
3353     overlayXY: { left: 0, top: 0 },
3354     rotationXY: ALLOWED_
3355   },
3356   constructor: function(screenOverlayObj, options) {
3357     // NOTE: un-swapped overlayXY and screenXY.
3358     this.dom.setVec2(screenOverlayObj.getScreenXY(), options.overlayXY);
3359     this.dom.setVec2(screenOverlayObj.getOverlayXY(), options.screenXY);
3360     this.dom.setVec2(screenOverlayObj.getSize(), options.size);
3361 
3362     if ('rotationXY' in options) {
3363       this.dom.setVec2(screenOverlayObj.getRotationXY(), options.rotationXY);
3364     }
3365   }
3366 });
3367 // TODO: unit tests
3368 
3369 /**
3370  * @name GEarthExtensions#dom.addPlacemark
3371  * Convenience method that calls GEarthExtensions#dom.buildPlacemark and adds
3372  * the created placemark to the Google Earth Plugin DOM.
3373  * @function
3374  */
3375 var autoDomAdd_ = ['Placemark', 'PointPlacemark', 'LineStringPlacemark',
3376                    'PolygonPlacemark', 'Folder', 'NetworkLink',
3377                    'GroundOverlay', 'ScreenOverlay', 'Style'];
3378 for (var i = 0; i < autoDomAdd_.length; i++) {
3379   GEarthExtensions.prototype.dom['add' + autoDomAdd_[i]] =
3380     function(shortcutBase) {
3381       return function() {
3382         var obj = this.dom['build' + shortcutBase].apply(null, arguments);
3383         this.pluginInstance.getFeatures().appendChild(obj);
3384         return obj;
3385       };
3386   }(autoDomAdd_[i]); // escape closure
3387 }
3388 /** @ignore */
3389 GEarthExtensions.prototype.dom.buildExtrudableGeometry_ = domBuilder_({
3390   propertySpec: {
3391     altitudeMode: AUTO_,
3392     extrude: AUTO_,
3393     tessellate: AUTO_
3394   }
3395 });
3396 
3397 /**
3398  * Creates a new point geometry with the given parameters.
3399  * @function
3400  * @param {PointOptions|geo.Point|KmlPoint} [point] The point data. Anything
3401  *     that can be passed to the geo.Point constructor.
3402  * @param {Object} options The parameters of the point object to create.
3403  * @param {PointOptions|geo.Point|KmlPoint} options.point The point data.
3404  *     Anything that can be passed to the geo.Point constructor.
3405  * @param {KmlAltitudeModeEnum} [options.altitudeMode] The altitude mode of the
3406  *     geometry.
3407  * @param {Boolean} [options.extrude] Whether or not the geometry should
3408  *     extrude down to the Earth's surface.
3409  * @type KmlPoint
3410  */
3411 GEarthExtensions.prototype.dom.buildPoint = domBuilder_({
3412   apiInterface: 'KmlPoint',
3413   base: GEarthExtensions.prototype.dom.buildExtrudableGeometry_,
3414   apiFactoryFn: 'createPoint',
3415   defaultProperty: 'point',
3416   propertySpec: {
3417     point: REQUIRED_
3418   },
3419   constructor: function(pointObj, options) {
3420     var point = new geo.Point(options.point);
3421     pointObj.set(
3422         point.lat(),
3423         point.lng(),
3424         point.altitude(),
3425         ('altitudeMode' in options) ? options.altitudeMode :
3426                                       point.altitudeMode(),
3427         false,
3428         false);
3429   }
3430 });
3431 // TODO: unit tests
3432 
3433 /**
3434  * Creates a new line string geometry with the given parameters.
3435  * @function
3436  * @param {PathOptions|geo.Path|KmlLineString} [path] The path data.
3437  *     Anything that can be passed to the geo.Path constructor.
3438  * @param {Object} options The parameters of the line string to create.
3439  * @param {PathOptions|geo.Path|KmlLineString} options.path The path data.
3440  *     Anything that can be passed to the geo.Path constructor.
3441  * @param {KmlAltitudeModeEnum} [options.altitudeMode] The altitude mode of the
3442  *     geometry.
3443  * @param {Boolean} [options.extrude] Whether or not the geometry should
3444  *     extrude down to the Earth's surface.
3445  * @param {Boolean} [options.tessellate] Whether or not the geometry should
3446  *     be tessellated (i.e. contour to the terrain).
3447  * @type KmlLineString
3448  */
3449 GEarthExtensions.prototype.dom.buildLineString = domBuilder_({
3450   apiInterface: 'KmlLineString',
3451   base: GEarthExtensions.prototype.dom.buildExtrudableGeometry_,
3452   apiFactoryFn: 'createLineString',
3453   defaultProperty: 'path',
3454   propertySpec: {
3455     path: REQUIRED_
3456   },
3457   constructor: function(lineStringObj, options) {
3458     // TODO: maybe use parseKml instead of pushLatLngAlt for performance
3459     // purposes
3460     var coordsObj = lineStringObj.getCoordinates();
3461   
3462     var path = new geo.Path(options.path);
3463     var numCoords = path.numCoords();
3464     for (var i = 0; i < numCoords; i++) {
3465       coordsObj.pushLatLngAlt(path.coord(i).lat(), path.coord(i).lng(),
3466           path.coord(i).altitude());
3467     }
3468   }
3469 });
3470 // TODO: unit tests
3471 
3472 /**
3473  * Creates a new linear ring geometry with the given parameters.
3474  * @function
3475  * @param {PathOptions|geo.Path|KmlLinearRing} [path] The path data.
3476  *     Anything that can be passed to the geo.Path constructor.
3477  *     The first coordinate doesn't need to be repeated at the end.
3478  * @param {Object} options The parameters of the linear ring to create.
3479  * @param {PathOptions|geo.Path|KmlLinearRing} options.path The path data.
3480  *     Anything that can be passed to the geo.Path constructor.
3481  *     The first coordinate doesn't need to be repeated at the end.
3482  * @param {KmlAltitudeModeEnum} [options.altitudeMode] The altitude mode of the
3483  *     geometry.
3484  * @param {Boolean} [options.extrude] Whether or not the geometry should
3485  *     extrude down to the Earth's surface.
3486  * @param {Boolean} [options.tessellate] Whether or not the geometry should
3487  *     be tessellated (i.e. contour to the terrain).
3488  * @type KmlLinearRing
3489  */
3490 GEarthExtensions.prototype.dom.buildLinearRing = domBuilder_({
3491   apiInterface: 'KmlLinearRing',
3492   base: GEarthExtensions.prototype.dom.buildLineString,
3493   apiFactoryFn: 'createLinearRing',
3494   defaultProperty: 'path',
3495   constructor: function(linearRingObj, options) {
3496     /*
3497     Earth API automatically dups first coordinate at the end to complete
3498     the ring when using createLinearRing, but parseKml won't do that...
3499     so if we switch to parseKml, make sure to duplicate the last point
3500     */
3501   }
3502 });
3503 // TODO: unit tests
3504 
3505 /**
3506  * Creates a new polygon geometry with the given parameters.
3507  * @function
3508  * @param {PolygonOptions|geo.Polygon|KmlPolygon} [polygon] The polygon data.
3509  *     Anything that can be passed to the geo.Polygon constructor.
3510  * @param {Object} options The parameters of the polygon to create.
3511  * @param {PolygonOptions|geo.Polygon|KmlPolygon} options.polygon The polygon
3512  *     data. Anything that can be passed to the geo.Polygon constructor.
3513  * @param {KmlAltitudeModeEnum} [options.altitudeMode] The altitude mode of the
3514  *     geometry.
3515  * @param {Boolean} [options.extrude] Whether or not the geometry should
3516  *     extrude down to the Earth's surface.
3517  * @param {Boolean} [options.tessellate] Whether or not the geometry should
3518  *     be tessellated (i.e. contour to the terrain).
3519  * @type KmlPolygon
3520  */
3521 GEarthExtensions.prototype.dom.buildPolygon = domBuilder_({
3522   apiInterface: 'KmlPolygon',
3523   base: GEarthExtensions.prototype.dom.buildExtrudableGeometry_,
3524   apiFactoryFn: 'createPolygon',
3525   defaultProperty: 'polygon',
3526   propertySpec: {
3527     polygon: REQUIRED_
3528   },
3529   constructor: function(polygonObj, options) {
3530     var polygon = new geo.Polygon(options.polygon);
3531   
3532     polygonObj.setOuterBoundary(
3533         this.dom.buildLinearRing(polygon.outerBoundary()));
3534     if (polygon.innerBoundaries().length) {
3535       var innerBoundaries = polygon.innerBoundaries();
3536       for (var i = 0; i < innerBoundaries.length; i++) {
3537         polygonObj.getInnerBoundaries().appendChild(
3538             this.dom.buildLinearRing(innerBoundaries[i]));
3539       }
3540     }
3541   }
3542 });
3543 // TODO: unit tests
3544 
3545 /**
3546  * Creates a new model geometry with the given parameters.
3547  * @function
3548  * @param {LinkOptions|KmlLink} [link] The remote link this model should use.
3549  * @param {Object} options The parameters of the model to create.
3550  * @param {LinkOptions|KmlLink} [options.link] The remote link this model
3551  *     should use.
3552  * @param {KmlAltitudeModeEnum} [options.altitudeMode] The altitude mode of the
3553  *     geometry.
3554  * @param {PointOptions|geo.Point} [options.location] The location of the model.
3555  * @param {Number|Number[]} [options.scale] The scale factor of the model,
3556  *     either as a constant scale, or a 3-item array for x, y, and z scale.
3557  * @param {Object} [options.orientation] The orientation of the model.
3558  * @param {Number} [options.orientation.heading] The model heading.
3559  * @param {Number} [options.orientation.tilt] The model tilt.
3560  * @param {Number} [options.orientation.roll] The model roll.
3561  * @type KmlModel
3562  */
3563 GEarthExtensions.prototype.dom.buildModel = domBuilder_({
3564   apiInterface: 'KmlModel',
3565   apiFactoryFn: 'createModel',
3566   defaultProperty: 'link',
3567   propertySpec: {
3568     altitudeMode: AUTO_,
3569     
3570     link: ALLOWED_,
3571     location: ALLOWED_,
3572     scale: ALLOWED_,
3573     orientation: ALLOWED_
3574   },
3575   constructor: function(modelObj, options) {
3576     if (options.link) {
3577       modelObj.setLink(this.dom.buildLink(options.link));
3578     }
3579   
3580     if (options.location) {
3581       var pointObj = new geo.Point(options.location);
3582       var locationObj = this.pluginInstance.createLocation('');
3583       locationObj.setLatLngAlt(pointObj.lat(), pointObj.lng(),
3584           pointObj.altitude());
3585       modelObj.setLocation(locationObj);
3586       modelObj.setAltitudeMode(pointObj.altitudeMode());
3587     }
3588   
3589     if (options.scale) {
3590       var scaleObj = this.pluginInstance.createScale('');
3591       if (typeof options.scale == 'number') {
3592         scaleObj.set(options.scale, options.scale, options.scale);
3593       } else if (geo.util.isArray(options.scale)) {
3594         scaleObj.set(options.scale[0], options.scale[1], options.scale[2]);
3595       }
3596     
3597       modelObj.setScale(scaleObj);
3598     }
3599   
3600     if (options.orientation) {
3601       var orientationObj = this.pluginInstance.createOrientation('');
3602       if ('heading' in options.orientation &&
3603           'tilt' in options.orientation &&
3604           'roll' in options.orientation) {
3605         orientationObj.set(options.orientation.heading,
3606                            options.orientation.tilt,
3607                            options.orientation.roll);
3608       }
3609     
3610       modelObj.setOrientation(orientationObj);
3611     }
3612   }
3613 });
3614 
3615 /**
3616  * Creates a new multi-geometry with the given parameters.
3617  * @function
3618  * @param {KmlGeometry[]} [geometries] The child geometries.
3619  * @param {Object} options The parameters of the multi-geometry to create.
3620  * @param {KmlGeometry[]} [options.geometries] The child geometries.
3621  * @type KmlMultiGeometry
3622  */
3623 GEarthExtensions.prototype.dom.buildMultiGeometry = domBuilder_({
3624   apiInterface: 'KmlMultiGeometry',
3625   apiFactoryFn: 'createMultiGeometry',
3626   defaultProperty: 'geometries',
3627   propertySpec: {
3628     geometries: ALLOWED_
3629   },
3630   constructor: function(multiGeometryObj, options) {
3631     var geometriesObj = multiGeometryObj.getGeometries();
3632   
3633     if (geo.util.isArray(options.geometries)) {
3634       for (var i = 0; i < options.geometries.length; i++) {
3635         geometriesObj.appendChild(options.geometries[i]);
3636       }
3637     }
3638   }
3639 });
3640 // TODO: unit tests
3641 /**
3642  * Creates a new link object with the given parameters.
3643  * @function
3644  * @param {String} [href] The link href.
3645  * @param {Object} options The link parameters.
3646  * @param {String} [options.href] The link href.
3647  * @param {KmlRefreshModeEnum} [options.refreshMode] The link refresh mode.
3648  * @param {Number} [options.refreshInterval] The link refresh interval,
3649  *     in seconds.
3650  * @param {KmlViewRefreshModeEnum} [options.viewRefreshMode] The view-based
3651  *     refresh mode.
3652  * @type KmlLink
3653  */
3654 GEarthExtensions.prototype.dom.buildLink = domBuilder_({
3655   apiInterface: 'KmlLink',
3656   apiFactoryFn: 'createLink',
3657   defaultProperty: 'href',
3658   propertySpec: {
3659     // auto properties
3660     href: AUTO_,
3661     refreshMode: AUTO_,
3662     refreshInterval: AUTO_,
3663     viewRefreshMode: AUTO_,
3664     viewBoundScale: AUTO_
3665   }
3666 });
3667 
3668 /**
3669  * Creates a new region with the given parameters.
3670  * @function
3671  * @param {Object} options The parameters of the region to create.
3672  * @param {String} options.box The bounding box of the region, defined by
3673  *     either N/E/S/W, or center+span, and optional altitudes.
3674  * @param {Number} [options.box.north] The north latitude for the region.
3675  * @param {Number} [options.box.east] The east longitude for the region.
3676  * @param {Number} [options.box.south] The south latitude for the region.
3677  * @param {Number} [options.box.west] The west longitude for the region.
3678  * @param {PointOptions|geo.Point} [options.box.center] The center point
3679  *     for the region's bounding box.
3680  * @param {Number|Number[]} [options.box.span] If using center+span region box
3681  *     definition, this is either a number indicating both latitude and
3682  *     longitude span, or a 2-item array defining [latSpan, lngSpan].
3683  * @param {Number} [options.box.minAltitude] The low altitude for the region.
3684  * @param {Number} [options.box.maxAltitude] The high altitude for the region.
3685  * @param {KmlAltitudeModeEnum} [options.box.altitudeMode] The altitude mode
3686  *     of the region, pertaining to min and max altitude.
3687  * @param {Number[]} [options.lod] An array of values indicating the LOD range
3688  *     for the region. The array can either contain 2 values, i.e.
3689  *     [minLodPixels, maxLodPixels], or 4 values to indicate fade extents, i.e.
3690  *     [minLodPixels, minFadeExtent, maxFadeExtent, maxLodPixels].
3691  * @type KmlRegion
3692  */
3693 GEarthExtensions.prototype.dom.buildRegion = domBuilder_({
3694   apiInterface: 'KmlRegion',
3695   apiFactoryFn: 'createRegion',
3696   propertySpec: {
3697     // required properties
3698     box: REQUIRED_,
3699     
3700     // allowed properties
3701     lod: ALLOWED_
3702   },
3703   constructor: function(regionObj, options) {
3704     // TODO: exception if any of the options are missing
3705     var box = this.pluginInstance.createLatLonAltBox('');
3706     
3707     // center +/- span to calculate n/e/s/w
3708     if (options.box.center && options.box.span) {
3709       if (!geo.util.isArray(options.box.span) &&
3710           typeof options.box.span === 'number') {
3711         // use this one number as both the lat and long span
3712         options.box.span = [options.box.span, options.box.span];
3713       }
3714       
3715       var center = new geo.Point(options.box.center);
3716       options.box.north = center.lat() + options.box.span[0] / 2;
3717       options.box.south = center.lat() - options.box.span[0] / 2;
3718       options.box.east = center.lng() + options.box.span[1] / 2;
3719       options.box.west = center.lng() - options.box.span[1] / 2;
3720     }
3721     
3722     box.setAltBox(options.box.north, options.box.south,
3723                   options.box.east, options.box.west,
3724                   options.box.rotation || 0,
3725                   options.box.minAltitude || 0,
3726                   options.box.maxAltitude || 0,
3727                   options.box.altitudeMode ||
3728                       this.pluginInstance.ALTITUDE_CLAMP_TO_GROUND);
3729     
3730     // NOTE: regions MUST be given an Lod due to
3731     // http://code.google.com/p/earth-api-samples/issues/detail?id=190
3732     var lod = this.pluginInstance.createLod('');
3733     lod.set(-1, -1, 0, 0); // default Lod
3734     
3735     if (options.lod && geo.util.isArray(options.lod)) {
3736       // TODO: exception if it's not an array
3737       if (options.lod.length == 2) {
3738         // minpix, maxpix
3739         lod.set(options.lod[0], options.lod[1], 0, 0);
3740       } else if (options.lod.length == 4) {
3741         // minpix, minfade, maxfade, maxpix
3742         lod.set(options.lod[0], options.lod[3],
3743                 options.lod[1], options.lod[2]);
3744       } else {
3745         // TODO: exception
3746       }
3747     }
3748     
3749     regionObj.setLatLonAltBox(box);
3750     regionObj.setLod(lod);
3751   }
3752 });
3753 /**
3754  * Creates a new style with the given parameters.
3755  * @function
3756  * @param {Object} options The style parameters.
3757 
3758  * @param {String|Object} [options.icon] The icon href or an icon
3759  *     object literal.
3760  * @param {String} [options.icon.href] The icon href.
3761  * @param {String} [options.icon.stockIcon] A convenience property to set the
3762  *     icon to a stock icon, e.g. 'paddle/wht-blank'.
3763  *     Stock icons reside under 'http://maps.google.com/mapfiles/kml/...'.
3764  * @param {Number} [options.icon.scale] The icon scaling factor.
3765  * @param {ColorSpec} [options.icon.color] The color of the icon.
3766  * @param {ColorSpec} [options.icon.opacity] The opacity of the icon,
3767  *     between 0.0 and 1.0. This is a convenience property, since opacity can
3768  *     be defined in the color.
3769  * @param {Vec2Options|KmlVec2} [options.icon.hotSpot] The hot sopt of the icon,
3770  *     as a KmlVec2, or as an options literal to pass to3
3771  *     GEarthExtensions.dom.setVec2.
3772 
3773  * @param {ColorSpec|Object} [options.label] The label color or a label
3774  *     object literal.
3775  * @param {Number} [options.label.scale] The label scaling factor.
3776  * @param {ColorSpec} [options.label.color] The color of the label.
3777  * @param {ColorSpec} [options.icon.opacity] The opacity of the label,
3778  *     between 0.0 and 1.0. This is a convenience property, since opacity can
3779  *     be defined in the color.
3780 
3781  * @param {ColorSpec|Object} [options.line] The line color or a line
3782  *     object literal.
3783  * @param {Number} [options.line.width] The line width.
3784  * @param {ColorSpec} [options.line.color] The line color.
3785  * @param {ColorSpec} [options.icon.opacity] The opacity of the line,
3786  *     between 0.0 and 1.0. This is a convenience property, since opacity can
3787  *     be defined in the color.
3788 
3789  * @param {ColorSpec|Object} [options.poly] The polygon color or a polygon style
3790  *     object literal.
3791  * @param {Boolean} [options.poly.fill] Whether or not the polygon will be
3792  *     filled.
3793  * @param {Boolean} [options.poly.outline] Whether or not the polygon will have
3794  *     an outline.
3795  * @param {ColorSpec} [options.poly.color] The color of the polygon fill.
3796  * @param {ColorSpec} [options.icon.opacity] The opacity of the polygon,
3797  *     between 0.0 and 1.0. This is a convenience property, since opacity can
3798  *     be defined in the color.
3799 
3800  * @param {ColorSpec|Object} [options.balloon] The balloon bgColor or a balloon
3801  *     style object literal.
3802  * @param {Boolean} [options.balloon.bgColor] The balloon background color.
3803  * @param {Boolean} [options.balloon.textColor] The balloon text color.
3804  * @param {String} [options.balloon.text] The balloon text template.
3805 
3806  * @type KmlStyle
3807  */
3808 GEarthExtensions.prototype.dom.buildStyle = domBuilder_({
3809   apiInterface: ['KmlStyle', 'KmlStyleMap'],
3810   apiFactoryFn: 'createStyle',
3811   propertySpec: {
3812     icon: ALLOWED_,
3813     label: ALLOWED_,
3814     line: ALLOWED_,
3815     poly: ALLOWED_,
3816     balloon: ALLOWED_
3817   },
3818   constructor: function(styleObj, options) {
3819     // set icon style
3820     var pad2 = function(s) {
3821       return ((s.length < 2) ? '0' : '') + s;
3822     };
3823     
3824     var me = this;
3825     
3826     var mergeColorOpacity_ = function(color, opacity) {
3827       color = color ? me.util.parseColor(color) : 'ffffffff';
3828       if (!geo.util.isUndefined(opacity)) {
3829         color = pad2(Math.floor(255 * opacity).toString(16)) +
3830             color.substring(2);
3831       }
3832       
3833       return color;
3834     };
3835     
3836     if (options.icon) {
3837       var iconStyle = styleObj.getIconStyle();
3838 
3839       if (typeof options.icon == 'string') {
3840         options.icon = { href: options.icon };
3841       }
3842     
3843       var icon = this.pluginInstance.createIcon('');
3844       iconStyle.setIcon(icon);
3845     
3846       // more options
3847       if ('href' in options.icon) {
3848         icon.setHref(options.icon.href);
3849       } else if ('stockIcon' in options.icon) {
3850         icon.setHref('http://maps.google.com/mapfiles/kml/' +
3851             options.icon.stockIcon + '.png');
3852       } else {
3853         // use default icon href
3854         icon.setHref('http://maps.google.com/mapfiles/kml/' +
3855             'paddle/wht-blank.png');
3856         iconStyle.getHotSpot().set(0.5, this.pluginInstance.UNITS_FRACTION,
3857             0, this.pluginInstance.UNITS_FRACTION);
3858       }
3859       if ('scale' in options.icon) {
3860         iconStyle.setScale(options.icon.scale);
3861       }
3862       if ('heading' in options.icon) {
3863         iconStyle.setHeading(options.icon.heading);
3864       }
3865       if ('color' in options.icon || 'opacity' in options.icon) {
3866         options.icon.color = mergeColorOpacity_(options.icon.color,
3867                                                 options.icon.opacity);
3868         iconStyle.getColor().set(options.icon.color);
3869       }
3870       if ('opacity' in options.icon) {
3871         if (!('color' in options.icon)) {
3872           options.icon.color = 'ffffffff';
3873         }
3874         
3875         options.icon.color = pad2(options.icon.opacity.toString(16)) +
3876             options.icon.color.substring(2);
3877       }
3878       if ('hotSpot' in options.icon) {
3879         this.dom.setVec2(iconStyle.getHotSpot(), options.icon.hotSpot);
3880       }
3881       // TODO: colormode
3882     }
3883   
3884     // set label style
3885     if (options.label) {
3886       var labelStyle = styleObj.getLabelStyle();
3887     
3888       if (typeof options.label == 'string') {
3889         options.label = { color: options.label };
3890       }
3891     
3892       // more options
3893       if ('scale' in options.label) {
3894         labelStyle.setScale(options.label.scale);
3895       }
3896       if ('color' in options.label || 'opacity' in options.label) {
3897         options.label.color = mergeColorOpacity_(options.label.color,
3898                                                  options.label.opacity);
3899         labelStyle.getColor().set(options.label.color);
3900       }
3901       // TODO: add colormode
3902     }
3903   
3904     // set line style
3905     if (options.line) {
3906       var lineStyle = styleObj.getLineStyle();
3907     
3908       if (typeof options.line == 'string') {
3909         options.line = { color: options.line };
3910       }
3911   
3912       // more options
3913       if ('width' in options.line) {
3914         lineStyle.setWidth(options.line.width);
3915       }
3916       if ('color' in options.line || 'opacity' in options.line) {
3917         options.line.color = mergeColorOpacity_(options.line.color,
3918                                                 options.line.opacity);
3919         lineStyle.getColor().set(options.line.color);
3920       }
3921       // TODO: add colormode
3922     }
3923   
3924     // set poly style
3925     if (options.poly) {
3926       var polyStyle = styleObj.getPolyStyle();
3927     
3928       if (typeof options.poly == 'string') {
3929         options.poly = { color: options.poly };
3930       }
3931     
3932       // more options
3933       if ('fill' in options.poly) {
3934         polyStyle.setFill(options.poly.fill);
3935       }
3936       if ('outline' in options.poly) {
3937         polyStyle.setOutline(options.poly.outline);
3938       }
3939       if ('color' in options.poly || 'opacity' in options.poly) {
3940         options.poly.color = mergeColorOpacity_(options.poly.color,
3941                                                 options.poly.opacity);
3942         polyStyle.getColor().set(options.poly.color);
3943       }
3944       // TODO: add colormode
3945     }
3946     
3947     // set balloon style
3948     if (options.balloon) {
3949       var balloonStyle = styleObj.getBalloonStyle();
3950     
3951       if (typeof options.balloon == 'string') {
3952         options.balloon = { bgColor: options.balloon };
3953       }
3954     
3955       // more options
3956       if ('bgColor' in options.balloon) {
3957         balloonStyle.getBgColor().set(
3958             me.util.parseColor(options.balloon.bgColor));
3959       }
3960       if ('textColor' in options.balloon) {
3961         balloonStyle.getTextColor().set(
3962             me.util.parseColor(options.balloon.textColor));
3963       }
3964       if ('text' in options.balloon) {
3965         balloonStyle.setText(options.balloon.text);
3966       }
3967     }
3968   }
3969 });
3970 // TODO: unit tests
3971 /**
3972  * Removes all top-level features from the Earth object's DOM.
3973  */
3974 GEarthExtensions.prototype.dom.clearFeatures = function() {
3975   var featureContainer = this.pluginInstance.getFeatures();
3976   var c;
3977   while ((c = featureContainer.getLastChild()) !== null) {
3978     featureContainer.removeChild(c);
3979   }
3980 };
3981 
3982 /**
3983  * Walks a KML object, calling a given visit function for each object in
3984  * the KML DOM. The lone argument must be either a visit function or an
3985  * options literal.
3986  * 
3987  * NOTE: walking the DOM can have pretty poor performance on very large
3988  * hierarchies, as first time accesses to KML objects from JavaScript
3989  * incur some overhead in the API.
3990  * 
3991  * @param {Object} [options] The walk options:
3992  * @param {Function} options.visitCallback The function to call upon visiting
3993  *     a node in the DOM. The 'this' variable in the callback function will be
3994  *     bound to the object being visited. The lone argument passed to this
3995  *     function will be an object literal for the call context. To get the
3996  *     current application-specific call context, use the 'current' property
3997  *     of the context object. To set the context for all child calls, set the
3998  *     'child' property of the context object.To prevent walking the children
3999  *     of the current object, set the 'walkChildren' property of the context
4000  *     object to false. To stop the walking process altogether,
4001  *     return false in the function.
4002  * @param {KmlObject} [options.rootObject] The root of the KML object hierarchy
4003  *     to walk. The default is to walk the entire Earth Plugin DOM.
4004  * @param {Boolean} [options.features=true] Descend into feature containers?
4005  * @param {Boolean} [options.geometries=false] Descend into geometry containers?
4006  * @param {Object} [options.rootContext] The application-specific context to
4007  *     pass to the root item.
4008  */
4009 GEarthExtensions.prototype.dom.walk = function() {
4010   var options;
4011   
4012   // figure out the arguments
4013   if (arguments.length == 1) {
4014     if (geo.util.isObjectLiteral(arguments[0])) {
4015       // object literal only
4016       options = arguments[0];
4017     } else if (geo.util.isFunction(arguments[0])) {
4018       // callback function only
4019       options = { visitCallback: arguments[0] };
4020     } else {
4021       throw new TypeError('walk requires a visit callback function or ' +
4022                           'options literal as a first parameter');
4023     }
4024   } else {
4025     throw new Error('walk takes at most 1 arguments');
4026   }
4027   
4028   options = checkParameters_(options, false, {
4029     visitCallback: REQUIRED_,
4030     features: true,
4031     geometries: false,
4032     rootObject: this.pluginInstance,
4033     rootContext: ALLOWED_
4034   });
4035   
4036   var recurse_ = function(object, currentContext) {
4037     var contextArgument = {
4038       current: currentContext,
4039       child: currentContext,
4040       walkChildren: true
4041     };
4042     
4043     // walk object
4044     var retValue = options.visitCallback.call(object, contextArgument);
4045     if (!retValue && !geo.util.isUndefined(retValue)) {
4046       return false;
4047     }
4048     
4049     if (!contextArgument.walkChildren) {
4050       return true;
4051     }
4052     
4053     var objectContainer = null; // GESchemaObjectContainer
4054     
4055     // check if object is a parent
4056     if ('getFeatures' in object) { // GEFeatureContainer
4057       if (options.features) {
4058         objectContainer = object.getFeatures();
4059       }
4060     } else if ('getGeometry' in object) { // KmlFeature - descend into
4061                                           // contained geometry
4062       if (options.geometries && object.getGeometry()) {
4063         recurse_(object.getGeometry(), contextArgument.child);
4064       }
4065     } else if ('getGeometries' in object) { // GEGeometryContainer
4066       if (options.geometries) {
4067         objectContainer = object.getGeometries();
4068       }
4069     } else if ('getOuterBoundary' in object) { // KmlPolygon - descend into
4070                                                // outer boundary
4071       if (options.geometries && object.getOuterBoundary()) {
4072         recurse_(object.getOuterBoundary(), contextArgument.child);
4073         objectContainer = object.getInnerBoundaries(); // GELinearRingContainer
4074       }
4075     }
4076     
4077     // iterate through children if object is a parent and recurse so they
4078     // can be walked
4079     if (objectContainer && objectContainer.hasChildNodes()) {
4080       var childNodes = objectContainer.getChildNodes();
4081       var numChildNodes = childNodes.getLength();
4082       
4083       for (var i = 0; i < numChildNodes; i++) {
4084         var child = childNodes.item(i);
4085         
4086         if (!recurse_(child, contextArgument.child)) {
4087           return false;
4088         }
4089       }
4090     }
4091     
4092     return true;
4093   };
4094   
4095   if (options.rootObject) {
4096     recurse_(options.rootObject, options.rootContext);
4097   }
4098 };
4099 
4100 /**
4101  * Gets the object in the Earth DOM with the given id.
4102  * @param {String} id The id of the object to retrieve.
4103  * @param {Object} [options] An options literal.
4104  * @param {Boolean} [options.recursive=true] Whether or not to walk the entire
4105  *     object (true) or just its immediate children (false).
4106  * @param {KmlObject} [options.rootObject] The root of the KML object hierarchy
4107  *     to search. The default is to search the entire Earth Plugin DOM.
4108  * @return Returns the object with the given id, or null if it was not found.
4109  */
4110 GEarthExtensions.prototype.dom.getObjectById = function(id, options) {
4111   options = checkParameters_(options, false, {
4112     recursive: true,
4113     rootObject: this.pluginInstance
4114   });
4115   
4116   // check self
4117   if ('getId' in options.rootObject && options.rootObject.getId() == id) {
4118     return options.rootObject;
4119   }
4120   
4121   var returnObject = null;
4122   
4123   this.dom.walk({
4124     rootObject: options.rootObject,
4125     features: true,
4126     geometries: true,
4127     visitCallback: function() {
4128       if ('getId' in this && this.getId() == id) {
4129         returnObject = this;
4130         return false; // stop walk
4131       }
4132     }
4133   });
4134 
4135   return returnObject;
4136 };
4137 // TODO: unit test
4138 
4139 /**
4140  * Removes the given object from the Earth object's DOM.
4141  * @param {KmlObject} object The object to remove.
4142  */
4143 GEarthExtensions.prototype.dom.removeObject = function(object) {
4144   if (!object) {
4145     return;
4146   }
4147 
4148   var parent = object.getParentNode();
4149   if (!parent) {
4150     throw new Error('Cannot remove an object without a parent.');
4151   }
4152 
4153   var objectContainer = null; // GESchemaObjectContainer
4154   
4155   if ('getFeatures' in parent) { // GEFeatureContainer
4156     objectContainer = parent.getFeatures();
4157   } else if ('getGeometries' in parent) { // GEGeometryContainer
4158     objectContainer = parent.getGeometries();
4159   } else if ('getInnerBoundaries' in parent) { // GELinearRingContainer
4160     objectContainer = parent.getInnerBoundaries();
4161   }
4162   
4163   objectContainer.removeChild(object);
4164 };
4165 // TODO: unit test (heavily)
4166 
4167 /**
4168  * Sets the given KmlVec2 object to the point defined in the options.
4169  * @param {KmlVec2} vec2 The object to set, for example a screen overlay's
4170  *     screenXY.
4171  * @param {Object|KmlVec2} options The options literal defining the point, or
4172  *     an existing KmlVec2 object to copy.
4173  * @param {Number|String} [options.left] The left offset, in pixels (i.e. 5),
4174  *     or as a percentage (i.e. '25%').
4175  * @param {Number|String} [options.top] The top offset, in pixels or a string
4176  *     percentage.
4177  * @param {Number|String} [options.right] The right offset, in pixels or a
4178  *     string percentage.
4179  * @param {Number|String} [options.bottom] The bottom offset, in pixels or a
4180  *     string percentage.
4181  * @param {Number|String} [options.width] A convenience parameter specifying
4182  *     width, only useful for screen overlays, in pixels or a string percentage.
4183  * @param {Number|String} [options.height] A convenience parameter specifying
4184  *     height, only useful for screen overlays, in pixels or a string
4185  *     percentage.
4186  */
4187 GEarthExtensions.prototype.dom.setVec2 = function(vec2, options) {
4188   if ('getType' in options && options.getType() == 'KmlVec2') {
4189     vec2.set(options.getX(), options.getXUnits(),
4190              options.getY(), options.getYUnits());
4191     return;
4192   }
4193   
4194   options = checkParameters_(options, false, {
4195     left: ALLOWED_,
4196     top: ALLOWED_,
4197     right: ALLOWED_,
4198     bottom: ALLOWED_,
4199     width: ALLOWED_, // for screen overlay size
4200     height: ALLOWED_ // for screen overlay size
4201   });
4202   
4203   if ('width' in options) {
4204     options.left = options.width;
4205   }
4206   
4207   if ('height' in options) {
4208     options.bottom = options.height;
4209   }
4210   
4211   var x = 0.0;
4212   var xUnits = this.pluginInstance.UNITS_PIXELS;
4213   var y = 0.0;
4214   var yUnits = this.pluginInstance.UNITS_PIXELS;
4215   
4216   // set X (origin = left)
4217   if ('left' in options) {
4218     if (typeof options.left == 'number') {
4219       x = options.left;
4220     } else if (typeof options.left == 'string' &&
4221                options.left.charAt(options.left.length - 1) == '%') {
4222       x = parseFloat(options.left) / 100;
4223       xUnits = this.pluginInstance.UNITS_FRACTION;
4224     } else {
4225       throw new TypeError('left must be a number or string indicating a ' +
4226                           'percentage');
4227     }
4228   } else if ('right' in options) {
4229     if (typeof options.right == 'number') {
4230       x = options.right;
4231       xUnits = this.pluginInstance.UNITS_INSET_PIXELS;
4232     } else if (typeof options.right == 'string' &&
4233                options.right.charAt(options.right.length - 1) == '%') {
4234       x = 1.0 - parseFloat(options.right) / 100;
4235       xUnits = this.pluginInstance.UNITS_FRACTION;
4236     } else {
4237       throw new TypeError('right must be a number or string indicating a ' +
4238                           'percentage');
4239     }
4240   }
4241   
4242   // set Y (origin = bottom)
4243   if ('bottom' in options) {
4244     if (typeof options.bottom == 'number') {
4245       y = options.bottom;
4246     } else if (typeof options.bottom == 'string' &&
4247                options.bottom.charAt(options.bottom.length - 1) == '%') {
4248       y = parseFloat(options.bottom) / 100;
4249       yUnits = this.pluginInstance.UNITS_FRACTION;
4250     } else {
4251       throw new TypeError('bottom must be a number or string indicating a ' +
4252                           'percentage');
4253     }
4254   } else if ('top' in options) {
4255     if (typeof options.top == 'number') {
4256       y = options.top;
4257       yUnits = this.pluginInstance.UNITS_INSET_PIXELS;
4258     } else if (typeof options.top == 'string' &&
4259                options.top.charAt(options.top.length - 1) == '%') {
4260       y = 1.0 - parseFloat(options.top) / 100;
4261       yUnits = this.pluginInstance.UNITS_FRACTION;
4262     } else {
4263       throw new TypeError('top must be a number or string indicating a ' +
4264                           'percentage');
4265     }
4266   }
4267   
4268   vec2.set(x, xUnits, y, yUnits);
4269 };
4270 
4271 /**
4272  * Computes the latitude/longitude bounding box for the given object.
4273  * Note that this method walks the object's DOM, so may have poor performance
4274  * for large objects.
4275  * @param {KmlFeature|KmlGeometry} object The feature or geometry whose bounds
4276  *     should be computed.
4277  * @type geo.Bounds
4278  */
4279 GEarthExtensions.prototype.dom.computeBounds = function(object) {
4280   var bounds = new geo.Bounds();
4281   
4282   // Walk the object's DOM, extending the bounds as coordinates are
4283   // encountered.
4284   this.dom.walk({
4285     rootObject: object,
4286     features: true,
4287     geometries: true,
4288     visitCallback: function() {
4289       if ('getType' in this) {
4290         var type = this.getType();
4291         switch (type) {
4292           case 'KmlGroundOverlay':
4293             var llb = this.getLatLonBox();
4294             if (llb) {
4295               var alt = this.getAltitude();
4296               bounds.extend(new geo.Point(llb.getNorth(), llb.getEast(), alt));
4297               bounds.extend(new geo.Point(llb.getNorth(), llb.getWest(), alt));
4298               bounds.extend(new geo.Point(llb.getSouth(), llb.getEast(), alt));
4299               bounds.extend(new geo.Point(llb.getSouth(), llb.getWest(), alt));
4300               // TODO: factor in rotation
4301             }
4302             break;
4303           
4304           case 'KmlModel':
4305             bounds.extend(new geo.Point(this.getLocation()));
4306             break;
4307         
4308           case 'KmlLinearRing':
4309           case 'KmlLineString':
4310             var coords = this.getCoordinates();
4311             if (coords) {
4312               var n = coords.getLength();
4313               for (var i = 0; i < n; i++) {
4314                 bounds.extend(new geo.Point(coords.get(i)));
4315               }
4316             }
4317             break;
4318 
4319           case 'KmlCoord': // coordinates
4320           case 'KmlLocation': // models
4321           case 'KmlPoint': // points
4322             bounds.extend(new geo.Point(this));
4323             break;
4324         }
4325       }
4326     }
4327   });
4328   
4329   return bounds;
4330 };
4331 /**
4332  * Creates a new lookat object with the given parameters.
4333  * @function
4334  * @param {PointSpec} [point] The point to look at.
4335  * @param {Object} options The parameters of the lookat object to create.
4336  * @param {PointSpec} options.point The point to look at.
4337  * @param {Boolean} [options.copy=false] Whether or not to copy parameters from
4338  *     the existing view if they aren't explicitly specified in the options.
4339  * @param {Number} [options.heading] The lookat heading/direction.
4340  * @param {Number} [options.tilt] The lookat tilt.
4341  * @param {Number} [options.range] The range of the camera (distance from the
4342  *     lookat point).
4343  * @type KmlLookAt
4344  */
4345 GEarthExtensions.prototype.dom.buildLookAt = domBuilder_({
4346   apiInterface: 'KmlLookAt',
4347   apiFactoryFn: 'createLookAt',
4348   defaultProperty: 'point',
4349   propertySpec: {
4350     copy: false,
4351     point: REQUIRED_,
4352     heading: ALLOWED_,
4353     tilt: ALLOWED_,
4354     range: ALLOWED_
4355   },
4356   constructor: function(lookAtObj, options) {
4357     var point = new geo.Point(options.point);
4358   
4359     var defaults = {
4360       heading: 0,
4361       tilt: 0,
4362       range: 1000
4363     };
4364   
4365     if (options.copy) {
4366       var currentLookAt = this.util.getLookAt(defaults.altitudeMode);
4367       defaults.heading = currentLookAt.getHeading();
4368       defaults.tilt = currentLookAt.getTilt();
4369       defaults.range = currentLookAt.getRange();
4370     }
4371   
4372     options = checkParameters_(options, true, defaults);
4373   
4374     lookAtObj.set(
4375         point.lat(),
4376         point.lng(),
4377         point.altitude(),
4378         point.altitudeMode(),
4379         options.heading,
4380         options.tilt,
4381         options.range);
4382   }
4383 });
4384 // TODO: incrementLookAt
4385 
4386 /**
4387  * Creates a new camera object with the given parameters.
4388  * @function
4389  * @param {PointSpec} [point] The point at which to place the camera.
4390  * @param {Object} options The parameters of the camera object to create.
4391  * @param {PointSpec} options.point The point at which to place the camera.
4392  * @param {Boolean} [options.copy=false] Whether or not to copy parameters from
4393  *     the existing view if they aren't explicitly specified in the options.
4394  * @param {Number} [options.heading] The camera heading/direction.
4395  * @param {Number} [options.tilt] The camera tilt.
4396  * @param {Number} [options.range] The camera roll.
4397  * @type KmlCamera
4398  */
4399 GEarthExtensions.prototype.dom.buildCamera = domBuilder_({
4400   apiInterface: 'KmlCamera',
4401   apiFactoryFn: 'createCamera',
4402   defaultProperty: 'point',
4403   propertySpec: {
4404     copy: false,
4405     point: REQUIRED_,
4406     heading: ALLOWED_,
4407     tilt: ALLOWED_,
4408     roll: ALLOWED_
4409   },
4410   constructor: function(cameraObj, options) {
4411     var point = new geo.Point(options.point);
4412   
4413     var defaults = {
4414       heading: 0,
4415       tilt: 0,
4416       roll: 0
4417     };
4418   
4419     if (options.copy) {
4420       var currentCamera = this.util.getCamera(defaults.altitudeMode);
4421       defaults.heading = currentCamera.getHeading();
4422       defaults.tilt = currentCamera.getTilt();
4423       defaults.roll = currentCamera.getRoll();
4424     }
4425   
4426     options = checkParameters_(options, true, defaults);
4427   
4428     cameraObj.set(
4429         point.lat(),
4430         point.lng(),
4431         point.altitude(),
4432         point.altitudeMode(),
4433         options.heading,
4434         options.tilt,
4435         options.roll);
4436   }
4437 });
4438 // TODO: incrementLookAt
4439 /**
4440  * Contains methods for allowing user-interactive editing of features inside
4441  * the Google Earth Plugin.
4442  * @namespace
4443  */
4444 GEarthExtensions.prototype.edit = {isnamespace_:true};
4445 var DRAGDATA_JSDATA_KEY = '_GEarthExtensions_dragData';
4446 
4447 // NOTE: this is shared across all GEarthExtensions instances
4448 var currentDragContext_ = null;
4449 
4450 function beginDragging_(extInstance, placemark) {
4451   // get placemark's drag data
4452   var placemarkDragData = extInstance.util.getJsDataValue(
4453       placemark, DRAGDATA_JSDATA_KEY) || {};
4454 
4455   currentDragContext_ = {
4456     placemark: placemark,
4457     startAltitude: placemark.getGeometry().getAltitude(),
4458     draggableOptions: placemarkDragData.draggableOptions,
4459     dragged: false
4460   };
4461 }
4462 
4463 function makeMouseMoveListener_(extInstance) {
4464   return function(event) {
4465     if (currentDragContext_) {
4466       event.preventDefault();
4467 
4468       if (!event.getDidHitGlobe()) {
4469         return;
4470       }
4471       
4472       if (!currentDragContext_.dragged) {
4473         currentDragContext_.dragged = true;
4474 
4475         // set dragging style
4476         if (currentDragContext_.draggableOptions.draggingStyle) {
4477           currentDragContext_.oldStyle =
4478               currentDragContext_.placemark.getStyleSelector();
4479           currentDragContext_.placemark.setStyleSelector(
4480               extInstance.dom.buildStyle(
4481               currentDragContext_.draggableOptions.draggingStyle));
4482         }
4483 
4484         // animate
4485         if (currentDragContext_.draggableOptions.bounce) {
4486           extInstance.fx.cancel(currentDragContext_.placemark);
4487           extInstance.fx.bounce(currentDragContext_.placemark, {
4488             phase: 1
4489           });
4490         }
4491 
4492         // show 'target' screen overlay (will be correctly positioned
4493         // later)
4494         if (currentDragContext_.draggableOptions.targetScreenOverlay) {
4495           var overlay = extInstance.dom.buildScreenOverlay(
4496               currentDragContext_.draggableOptions.targetScreenOverlay);
4497           extInstance.pluginInstance.getFeatures().appendChild(overlay);
4498           currentDragContext_.activeTargetScreenOverlay = overlay;
4499         }
4500       }
4501 
4502       // move 'target' screen overlay
4503       if (currentDragContext_.activeTargetScreenOverlay) {
4504         // NOTE: overlayXY but we really are setting the screenXY due to
4505         // the two being swapped in the Earth API
4506         extInstance.dom.setVec2(
4507             currentDragContext_.activeTargetScreenOverlay.getOverlayXY(),
4508             { left: event.getClientX(), top: event.getClientY() });
4509       }
4510 
4511       // TODO: allow for non-point dragging (models?)
4512       var point = currentDragContext_.placemark.getGeometry();
4513       point.setLatitude(event.getLatitude());
4514       point.setLongitude(event.getLongitude());
4515       
4516       // show the placemark
4517       currentDragContext_.placemark.setVisibility(true);
4518 
4519       if (currentDragContext_.draggableOptions.dragCallback) {
4520         currentDragContext_.draggableOptions.dragCallback.call(
4521             currentDragContext_.placemark);
4522       }
4523     }
4524   };
4525 }
4526 
4527 function stopDragging_(extInstance, abort) {
4528   if (currentDragContext_) {
4529     if (currentDragContext_.dragged) {
4530       // unset dragging style
4531       if (currentDragContext_.oldStyle) {
4532         currentDragContext_.placemark.setStyleSelector(
4533             currentDragContext_.oldStyle);
4534         delete currentDragContext_.oldStyle;
4535       }
4536 
4537       // remove 'target' screen overlay
4538       if (currentDragContext_.activeTargetScreenOverlay) {
4539         extInstance.pluginInstance.getFeatures().removeChild(
4540             currentDragContext_.activeTargetScreenOverlay);
4541         delete currentDragContext_.activeTargetScreenOverlay;
4542       }
4543 
4544       // animate
4545       if (currentDragContext_.draggableOptions.bounce) {
4546         extInstance.fx.cancel(currentDragContext_.placemark);
4547         extInstance.fx.bounce(currentDragContext_.placemark, {
4548           startAltitude: currentDragContext_.startAltitude,
4549           phase: 2,
4550           repeat: 1,
4551           dampen: 0.3
4552         });
4553       }
4554     }
4555     
4556     // in case the drop callback does something with dragging, don't
4557     // mess with the global currentDragContext_ variable after the drop
4558     // callback returns
4559     var dragContext_ = currentDragContext_;
4560     currentDragContext_ = null;
4561 
4562     if (dragContext_.dragged &&
4563         dragContext_.draggableOptions.dropCallback && !abort) {
4564       dragContext_.draggableOptions.dropCallback.call(
4565           dragContext_.placemark);
4566     }
4567   }
4568 }
4569 
4570 /**
4571  * Turns on draggability for the given point placemark.
4572  * @param {KmlPlacemark} placemark The point placemark to enable dragging on.
4573  * @param {Object} [options] The draggable options.
4574  * @param {Boolean} [options.bounce=true] Whether or not to bounce up upon
4575  *     dragging and bounce back down upon dropping.
4576  * @param {Function} [options.dragCallback] A callback function to fire
4577  *     continuously while dragging occurs.
4578  * @param {Function} [options.dropCallback] A callback function to fire
4579  *     once the placemark is successfully dropped.
4580  * @param {StyleOptions|KmlStyle} [options.draggingStyle] The style options
4581  *     to apply to the placemark while dragging.
4582  * @param {ScreenOverlayOptions|KmlScreenOverlay} [options.targetScreenOverlay]
4583  *     A screen overlay to use as a drop target indicator (i.e. a bullseye)
4584  *     while dragging.
4585  */
4586 GEarthExtensions.prototype.edit.makeDraggable = function(placemark, options) {
4587   this.edit.endDraggable(placemark);
4588 
4589   // TODO: assert this is a point placemark
4590   options = checkParameters_(options, false, {
4591     bounce: true,
4592     dragCallback: ALLOWED_,
4593     dropCallback: ALLOWED_,
4594     draggingStyle: ALLOWED_,
4595     targetScreenOverlay: ALLOWED_
4596   });
4597 
4598   var me = this;
4599 
4600   // create a mouse move listener for use once dragging has begun
4601   var mouseMoveListener = makeMouseMoveListener_(me);
4602 
4603   // create a mouse up listener for use once dragging has begun
4604   var mouseUpListener;
4605   mouseUpListener = function(event) {
4606     if (currentDragContext_ && event.getButton() === 0) {
4607       // remove listener for mousemove on the globe
4608       google.earth.removeEventListener(me.pluginInstance.getWindow(),
4609           'mousemove', mouseMoveListener);
4610 
4611       // remove listener for mouseup on the window
4612       google.earth.removeEventListener(me.pluginInstance.getWindow(),
4613           'mouseup', mouseUpListener);
4614 
4615       if (currentDragContext_.dragged) {
4616         // if the placemark was dragged, prevent balloons from popping up
4617         event.preventDefault();
4618       }
4619 
4620       stopDragging_(me);
4621     }
4622   };
4623 
4624   // create a mouse down listener
4625   var mouseDownListener = function(event) {
4626     if (event.getButton() === 0) {
4627       // TODO: check if getTarget() is draggable and is a placemark
4628       beginDragging_(me, event.getTarget());
4629 
4630       // listen for mousemove on the globe
4631       google.earth.addEventListener(me.pluginInstance.getWindow(),
4632           'mousemove', mouseMoveListener);
4633 
4634       // listen for mouseup on the window
4635       google.earth.addEventListener(me.pluginInstance.getWindow(),
4636           'mouseup', mouseUpListener);
4637     }
4638   };
4639 
4640   // persist drag options for use in listeners
4641   this.util.setJsDataValue(placemark, DRAGDATA_JSDATA_KEY, {
4642     draggableOptions: options,
4643     abortAndEndFn: function() {
4644       if (currentDragContext_ &&
4645           currentDragContext_.placemark.equals(placemark)) {
4646         // remove listener for mousemove on the globe
4647         google.earth.removeEventListener(me.pluginInstance.getWindow(),
4648             'mousemove', mouseMoveListener);
4649 
4650         // remove listener for mouseup on the window
4651         google.earth.removeEventListener(me.pluginInstance.getWindow(),
4652             'mouseup', mouseUpListener);
4653 
4654         stopDragging_(me, true); // abort
4655       }
4656 
4657       google.earth.removeEventListener(placemark, 'mousedown',
4658           mouseDownListener);
4659     }
4660   });
4661 
4662   // listen for mousedown on the placemark
4663   google.earth.addEventListener(placemark, 'mousedown', mouseDownListener);
4664 };
4665 
4666 /**
4667  * Ceases the draggability of the given placemark. If the placemark is in the
4668  * process of being placed via GEarthExtensions#edit.place, the placement
4669  * is cancelled.
4670  */
4671 GEarthExtensions.prototype.edit.endDraggable = function(placemark) {
4672   // get placemark's drag data
4673   var placemarkDragData = this.util.getJsDataValue(
4674       placemark, DRAGDATA_JSDATA_KEY);
4675 
4676   // stop listening for mousedown on the window
4677   if (placemarkDragData) {
4678     placemarkDragData.abortAndEndFn.call(null);
4679 
4680     this.util.clearJsDataValue(placemark, DRAGDATA_JSDATA_KEY);
4681   }
4682 };
4683 
4684 /**
4685  * Enters a mode in which the user can place the given point placemark onto
4686  * the globe by clicking on the globe. To cancel the placement, use
4687  * GEarthExtensions#edit.endDraggable.
4688  * @param {KmlPlacemark} placemark The point placemark for the user to place
4689  *     onto the globe.
4690  * @param {Object} [options] The draggable options. See
4691  *     GEarthExtensions#edit.makeDraggable.
4692  */
4693 GEarthExtensions.prototype.edit.place = function(placemark, options) {
4694   // TODO: assert this is a point placemark
4695   options = checkParameters_(options, false, {
4696     bounce: true,
4697     dragCallback: ALLOWED_,
4698     dropCallback: ALLOWED_,
4699     draggingStyle: ALLOWED_,
4700     targetScreenOverlay: ALLOWED_
4701   });
4702 
4703   var me = this;
4704 
4705   // create a mouse move listener
4706   var mouseMoveListener = makeMouseMoveListener_(me);
4707   
4708   // hide the placemark initially
4709   placemark.setVisibility(false);
4710 
4711   // create a mouse down listener
4712   var mouseDownListener;
4713   mouseDownListener = function(event) {
4714     if (currentDragContext_ && event.getButton() === 0) {
4715       event.preventDefault();
4716       event.stopPropagation();
4717       
4718       // remove listener for mousemove on the globe
4719       google.earth.removeEventListener(me.pluginInstance.getWindow(),
4720           'mousemove', mouseMoveListener);
4721 
4722       // remove listener for mousedown on the window
4723       google.earth.removeEventListener(me.pluginInstance.getWindow(),
4724           'mousedown', mouseDownListener);
4725 
4726       stopDragging_(me);
4727     }
4728   };
4729 
4730   // persist drag options for use in listeners
4731   this.util.setJsDataValue(placemark, DRAGDATA_JSDATA_KEY, {
4732     draggableOptions: options,
4733     abortAndEndFn: function() {
4734       if (currentDragContext_ &&
4735           currentDragContext_.placemark.equals(placemark)) {
4736         // remove listener for mousemove on the globe
4737         google.earth.removeEventListener(me.pluginInstance.getWindow(),
4738             'mousemove', mouseMoveListener);
4739 
4740         // remove listener for mousedown on the window
4741         google.earth.removeEventListener(me.pluginInstance.getWindow(),
4742             'mousedown', mouseDownListener);
4743 
4744         stopDragging_(me, true); // abort
4745       }
4746     }
4747   });
4748 
4749   // enter dragging mode right away to 'place' the placemark on the globe
4750   beginDragging_(me, placemark);
4751 
4752   // listen for mousemove on the window
4753   google.earth.addEventListener(me.pluginInstance.getWindow(),
4754       'mousemove', mouseMoveListener);
4755 
4756   // listen for mousedown on the window
4757   google.earth.addEventListener(me.pluginInstance.getWindow(),
4758       'mousedown', mouseDownListener);
4759 };
4760 var LINESTRINGEDITDATA_JSDATA_KEY = '_GEarthExtensions_lineStringEditData';
4761 var LINESTRING_COORD_ICON = 'http://maps.google.com/mapfiles/kml/' +
4762                             'shapes/placemark_circle.png';
4763 var LINESTRING_COORD_ICON_SCALE = 0.85;
4764 var LINESTRING_MIDPOINT_ICON_SCALE = 0.6;
4765 
4766 function coordsEqual_(coord1, coord2) {
4767   return coord1.getLatitude() ==  coord2.getLatitude() &&
4768          coord1.getLongitude() == coord2.getLongitude() &&
4769          coord1.getAltitude() == coord2.getAltitude();
4770 }
4771 
4772 /**
4773  * Enters a mode in which the user can draw the given line string geometry
4774  * on the globe by clicking on the globe to create coordinates.
4775  * To cancel the placement, use GEarthExtensions#edit.endEditLineString.
4776  * This is similar in intended usage to GEarthExtensions#edit.place.
4777  * @param {KmlLineString|KmlLinearRing} lineString The line string geometry
4778  *     to allow the user to draw (or append points to).
4779  * @param {Object} [options] The edit options.
4780  * @param {Boolean} [options.bounce=true] Whether or not to enable bounce
4781  *     effects while drawing coordinates.
4782  * @param {Function} [options.drawCallback] A callback to fire when new
4783  *     vertices are drawn. The only argument passed will be the index of the
4784  *     new coordinate (it can either be prepended or appended, depending on
4785  *     whether or not ensuring counter-clockwisedness).
4786  * @param {Function} [options.finishCallback] A callback to fire when drawing
4787  *     is successfully completed (via double click or by clicking on the first
4788  *     coordinate again).
4789  * @param {Boolean} [options.ensureCounterClockwise=true] Whether or not to
4790  *     automatically keep polygon coordinates in counter clockwise order.
4791  */
4792 GEarthExtensions.prototype.edit.drawLineString = function(lineString,
4793                                                           options) {
4794   options = checkParameters_(options, false, {
4795     bounce: true,
4796     drawCallback: ALLOWED_,
4797     finishCallback: ALLOWED_,
4798     ensureCounterClockwise: true
4799   });
4800   
4801   var lineStringEditData = this.util.getJsDataValue(
4802       lineString, LINESTRINGEDITDATA_JSDATA_KEY) || {};
4803   if (lineStringEditData) {
4804     this.edit.endEditLineString(lineString);
4805   }
4806   
4807   var me = this;
4808 
4809   // TODO: options: icon for placemarks
4810 
4811   // used to ensure counterclockwise-ness
4812   var isReverse = false;
4813   var tempPoly = new geo.Polygon();
4814   
4815   var done = false;
4816   var placemarks = [];
4817   var altitudeMode = lineString.getAltitudeMode();
4818   var headPlacemark = null;
4819   var isRing = (lineString.getType() == 'KmlLinearRing');
4820   var coords = lineString.getCoordinates();
4821   var innerDoc = this.pluginInstance.parseKml([
4822       '<Document>',
4823       '<Style id="_GEarthExtensions_regularCoordinate"><IconStyle>',
4824       '<Icon><href>', LINESTRING_COORD_ICON, '</href></Icon>',
4825       '<scale>', LINESTRING_COORD_ICON_SCALE, '</scale></IconStyle></Style>',
4826       '<Style id="_GEarthExtensions_firstCoordinateHighlight"><IconStyle>',
4827       '<Icon><href>', LINESTRING_COORD_ICON, '</href></Icon>',
4828       '<scale>', LINESTRING_COORD_ICON_SCALE * 1.3, '</scale>',
4829       '<color>ff00ff00</color></IconStyle></Style>',
4830       '<StyleMap id="_GEarthExtensions_firstCoordinate">',
4831       '<Pair><key>normal</key>',
4832       '<styleUrl>#_GEarthExtensions_regularCoordinate</styleUrl>',
4833       '</Pair><Pair><key>highlight</key>',
4834       '<styleUrl>#_GEarthExtensions_firstCoordinateHighlight</styleUrl>',
4835       '</Pair></StyleMap>',
4836       '</Document>'].join(''));
4837 
4838   var finishListener;
4839   
4840   var endFunction = function(abort) {
4841     google.earth.removeEventListener(me.pluginInstance.getWindow(),
4842         'dblclick', finishListener);
4843     
4844     // duplicate the first coordinate to the end if necessary
4845     var numCoords = coords.getLength();
4846     if (numCoords && isRing) {
4847       var firstCoord = coords.get(0);
4848       var lastCoord = coords.get(numCoords - 1);
4849       if (!coordsEqual_(firstCoord, lastCoord)) {
4850         coords.pushLatLngAlt(firstCoord.getLatitude(),
4851                              firstCoord.getLongitude(),
4852                              firstCoord.getAltitude());
4853       }
4854     }
4855 
4856     me.edit.endDraggable(headPlacemark);
4857     me.dom.removeObject(innerDoc);
4858     me.util.clearJsDataValue(lineString, LINESTRINGEDITDATA_JSDATA_KEY);
4859     placemarks = [];
4860     done = true;
4861 
4862     if (options.finishCallback && !abort) {
4863       options.finishCallback.call(null);
4864     }
4865   };
4866   
4867   finishListener = function(event) {
4868     event.preventDefault();
4869     endFunction.call(null);
4870   };
4871   
4872   var drawNext;
4873   drawNext = function() {
4874     headPlacemark = me.dom.buildPointPlacemark([0, 0], {
4875       altitudeMode: altitudeMode,
4876       style: '#_GEarthExtensions_regularCoordinate',
4877       visibility: false  // start out invisible
4878     });
4879     innerDoc.getFeatures().appendChild(headPlacemark);
4880     if (isReverse) {
4881       placemarks.unshift(headPlacemark);
4882     } else {
4883       placemarks.push(headPlacemark);
4884     }
4885 
4886     me.edit.place(headPlacemark, {
4887       bounce: options.bounce,
4888       dropCallback: function() {
4889         if (!done) {
4890           var coord = [headPlacemark.getGeometry().getLatitude(),
4891                        headPlacemark.getGeometry().getLongitude(),
4892                        0]; // don't use altitude because of bounce
4893           if (isReverse) {
4894             coords.unshiftLatLngAlt(coord[0], coord[1], coord[2]);
4895           } else {
4896             coords.pushLatLngAlt(coord[0], coord[1], coord[2]);
4897           }
4898           
4899           // ensure counterclockwise-ness
4900           if (options.ensureCounterClockwise) {
4901             if (isReverse) {
4902               tempPoly.outerBoundary().prepend(coord);
4903             } else {
4904               tempPoly.outerBoundary().append(coord);
4905             }
4906             
4907             if (!tempPoly.isCounterClockwise()) {
4908               tempPoly.outerBoundary().reverse();
4909               coords.reverse();
4910               isReverse = !isReverse;
4911             }
4912           }
4913               
4914           if (options.drawCallback) {
4915             options.drawCallback.call(null,
4916                 isReverse ? 0 : coords.getLength() - 1);
4917           }
4918 
4919           if (placemarks.length == 1) {
4920             // set up a click listener on the first placemark -- if it gets
4921             // clicked, repeat the first coordinate and stop drawing the
4922             // linestring
4923             placemarks[0].setStyleUrl('#_GEarthExtensions_firstCoordinate');
4924             google.earth.addEventListener(placemarks[0], 'mousedown',
4925                 function(firstCoord) {
4926                   return function(event) {
4927                     if (isReverse) {
4928                       coords.unshiftLatLngAlt(firstCoord[0], firstCoord[1],
4929                                               firstCoord[2]);
4930                     } else {
4931                       coords.pushLatLngAlt(firstCoord[0], firstCoord[1],
4932                                            firstCoord[2]);
4933                     }
4934                   
4935                     finishListener(event);
4936                   };
4937                 }(coord));
4938           }
4939 
4940           setTimeout(drawNext, 0);
4941         }
4942       }
4943     });
4944   };
4945 
4946   drawNext.call(null);
4947   
4948   google.earth.addEventListener(me.pluginInstance.getWindow(), 'dblclick',
4949       finishListener);
4950 
4951   // display the editing UI
4952   this.pluginInstance.getFeatures().appendChild(innerDoc);
4953 
4954   // set up an abort function for use in endEditLineString
4955   this.util.setJsDataValue(lineString, LINESTRINGEDITDATA_JSDATA_KEY, {
4956     abortAndEndFn: function() {
4957       endFunction.call(null, true); // abort
4958     }
4959   });
4960 };
4961 // TODO: interactive test
4962 
4963 /**
4964  * Allows the user to edit the coordinates of the given line string by
4965  * dragging existing points, splitting path segments/creating new points or
4966  * deleting existing points.
4967  * @param {KmlLineString|KmlLinearRing} lineString The line string or lienar
4968  *     ring geometry to edit. For KmlPolygon geometries, pass in an outer
4969  *     or inner boundary.
4970  * @param {Object} [options] The line string edit options.
4971  * @param {Function} [options.editCallback] A callback function to fire
4972  *     when the line string coordinates have changed due to user interaction.
4973  */
4974 GEarthExtensions.prototype.edit.editLineString = function(lineString,
4975                                                           options) {
4976   options = checkParameters_(options, false, {
4977     editCallback: ALLOWED_
4978   });
4979   
4980   var lineStringEditData = this.util.getJsDataValue(
4981       lineString, LINESTRINGEDITDATA_JSDATA_KEY) || {};
4982   if (lineStringEditData) {
4983     this.edit.endEditLineString(lineString);
4984   }
4985 
4986   var me = this;
4987   
4988   var isRing = (lineString.getType() == 'KmlLinearRing');
4989   var altitudeMode = lineString.getAltitudeMode();
4990   var coords = lineString.getCoordinates();
4991   
4992   // number of total coords, including any repeat first coord in the case of
4993   // linear rings
4994   var numCoords = coords.getLength();
4995   
4996   // if the first coordinate isn't repeated at the end and we're editing
4997   // a linear ring, repeat it
4998   if (numCoords && isRing) {
4999     var firstCoord = coords.get(0);
5000     var lastCoord = coords.get(numCoords - 1);
5001     if (!coordsEqual_(firstCoord, lastCoord)) {
5002       coords.pushLatLngAlt(firstCoord.getLatitude(),
5003                            firstCoord.getLongitude(),
5004                            firstCoord.getAltitude());
5005       numCoords++;
5006     }
5007   }
5008   
5009   var innerDoc = this.pluginInstance.parseKml([
5010       '<Document>',
5011       '<Style id="_GEarthExtensions_regularCoordinate"><IconStyle>',
5012       '<Icon><href>', LINESTRING_COORD_ICON, '</href></Icon>',
5013       '<color>ffffffff</color>',
5014       '<scale>', LINESTRING_COORD_ICON_SCALE, '</scale></IconStyle></Style>',
5015       '<StyleMap id="_GEarthExtensions_midCoordinate">',
5016       '<Pair><key>normal</key>',
5017       '<Style><IconStyle>',
5018       '<Icon><href>', LINESTRING_COORD_ICON, '</href></Icon>',
5019       '<color>60ffffff</color><scale>', LINESTRING_MIDPOINT_ICON_SCALE,
5020       '</scale></IconStyle></Style></Pair>',
5021       '<Pair><key>highlight</key>',
5022       '<styleUrl>#_GEarthExtensions_regularCoordinate</styleUrl>',
5023       '</Pair></StyleMap>',
5024       '</Document>'].join(''));
5025 
5026   // TODO: options: icon for placemarks
5027   // TODO: it may be easier to use a linked list for all this
5028 
5029   var coordDataArr = [];
5030   
5031   var checkDupMidpoints_ = function() {
5032     if (!isRing) {
5033       return;
5034     }
5035     
5036     // handle special case for polygons w/ 2 coordinates
5037     if (numCoords == 3) /* including duplicate first coord */ {
5038       coordDataArr[1].rightMidPlacemark.setVisibility(false);
5039     } else if (numCoords >= 4) {
5040       coordDataArr[numCoords - 2].rightMidPlacemark.setVisibility(true);
5041     }
5042   };
5043 
5044   var makeRegularDeleteEventListener_ = function(coordData) {
5045     return function(event) {
5046       event.preventDefault();
5047 
5048       // get the coord info of the left coordinate, as we'll need to
5049       // update its midpoint placemark
5050       var leftCoordData = null;
5051       if (coordData.index > 0 || isRing) {
5052         var leftIndex = coordData.index - 1;
5053         if (leftIndex < 0) {
5054           leftIndex += numCoords; // wrap
5055         }
5056         
5057         if (isRing && coordData.index === 0) {
5058           // skip repeated coord at the end
5059           leftIndex--;
5060         }
5061 
5062         leftCoordData = coordDataArr[leftIndex];
5063       }
5064 
5065       // shift coordinates in the KmlCoordArray up
5066       // TODO: speed this up
5067       for (i = coordData.index; i < numCoords - 1; i++) {
5068         coords.set(i, coords.get(i + 1));
5069       }
5070       
5071       coords.pop();
5072 
5073       // user removed first coord, make the last coord equivalent
5074       // to the new first coord (previously 2nd coord)
5075       if (isRing && coordData.index === 0) {
5076         coords.set(numCoords - 2, coords.get(0));
5077       }
5078       
5079       numCoords--;
5080 
5081       // at the end of the line and there's no right-mid placemark.
5082       // the previous-to-last point's mid point should be removed too.
5083       if (!coordData.rightMidPlacemark && leftCoordData) {
5084         me.edit.endDraggable(leftCoordData.rightMidPlacemark);
5085         me.dom.removeObject(leftCoordData.rightMidPlacemark);
5086         leftCoordData.rightMidPlacemark = null;
5087       }
5088 
5089       // tear down mid placemark
5090       if (coordData.rightMidPlacemark) {
5091         me.edit.endDraggable(coordData.rightMidPlacemark);
5092         me.dom.removeObject(coordData.rightMidPlacemark);
5093       }
5094 
5095       // tear down this placemark
5096       me.edit.endDraggable(coordData.regularPlacemark);
5097       google.earth.removeEventListener(coordData.regularPlacemark,
5098           'dblclick', coordData.deleteEventListener);
5099       me.dom.removeObject(coordData.regularPlacemark);
5100 
5101       coordDataArr.splice(coordData.index, 1);
5102 
5103       // update all coord data indices after this removed
5104       // coordinate, because indices have changed
5105       for (i = 0; i < numCoords; i++) {
5106         coordDataArr[i].index = i;
5107       }
5108 
5109       // call the drag listener for the previous coordinate
5110       // to update the midpoint location
5111       if (leftCoordData) {
5112         leftCoordData.regularDragCallback.call(
5113             leftCoordData.regularPlacemark, leftCoordData);
5114       }
5115       
5116       checkDupMidpoints_();
5117       
5118       if (options.editCallback) {
5119         options.editCallback(null);
5120       }
5121     };
5122   };
5123 
5124   var makeRegularDragCallback_ = function(coordData) {
5125     return function() {
5126       // update this coordinate
5127       coords.setLatLngAlt(coordData.index,
5128           this.getGeometry().getLatitude(),
5129           this.getGeometry().getLongitude(),
5130           this.getGeometry().getAltitude());
5131       
5132       // if we're editing a ring and the first and last coords are the same,
5133       // keep them in sync
5134       if (isRing && numCoords >= 2 && coordData.index === 0) {
5135         var firstCoord = coords.get(0);
5136         var lastCoord = coords.get(numCoords - 1);
5137         
5138         // update both first and last coordinates
5139         coords.setLatLngAlt(0,
5140             this.getGeometry().getLatitude(),
5141             this.getGeometry().getLongitude(),
5142             this.getGeometry().getAltitude());
5143         coords.setLatLngAlt(numCoords - 1,
5144             this.getGeometry().getLatitude(),
5145             this.getGeometry().getLongitude(),
5146             this.getGeometry().getAltitude());
5147       }
5148 
5149       // update midpoint placemarks
5150       var curCoord = coords.get(coordData.index);
5151 
5152       if (coordData.index > 0 || isRing) {
5153         var leftIndex = coordData.index - 1;
5154         if (leftIndex < 0) {
5155           leftIndex += numCoords; // wrap
5156         }
5157         
5158         if (isRing && coordData.index === 0) {
5159           // skip repeated coord at the end
5160           leftIndex--;
5161         }
5162         
5163         var leftMidPt = new geo.Point(coords.get(leftIndex)).midpoint(
5164             new geo.Point(curCoord));
5165         coordDataArr[leftIndex].rightMidPlacemark.getGeometry().setLatitude(
5166             leftMidPt.lat());
5167         coordDataArr[leftIndex].rightMidPlacemark.getGeometry().setLongitude(
5168             leftMidPt.lng());
5169         coordDataArr[leftIndex].rightMidPlacemark.getGeometry().setAltitude(
5170             leftMidPt.altitude());
5171       }
5172 
5173       if (coordData.index < numCoords - 1 || isRing) {
5174         var rightCoord;
5175         if ((isRing && coordData.index == numCoords - 2) ||
5176             (!isRing && coordData.index == numCoords - 1)) {
5177           rightCoord = coords.get(0);
5178         } else {
5179           rightCoord = coords.get(coordData.index + 1);
5180         }
5181         
5182         var rightMidPt = new geo.Point(curCoord).midpoint(
5183             new geo.Point(rightCoord));
5184         coordData.rightMidPlacemark.getGeometry().setLatitude(
5185             rightMidPt.lat());
5186         coordData.rightMidPlacemark.getGeometry().setLongitude(
5187             rightMidPt.lng());
5188         coordData.rightMidPlacemark.getGeometry().setAltitude(
5189             rightMidPt.altitude());
5190       }
5191       
5192       checkDupMidpoints_();
5193       
5194       if (options.editCallback) {
5195         options.editCallback(null);
5196       }
5197     };
5198   };
5199 
5200   var makeMidDragCallback_ = function(coordData) {
5201     // vars for the closure
5202     var convertedToRegular = false;
5203     var newCoordData = null;
5204 
5205     return function() {
5206       if (!convertedToRegular) {
5207         // first time drag... convert this midpoint into a regular point
5208 
5209         convertedToRegular = true;
5210         var i;
5211 
5212         // change style to regular placemark style
5213         this.setStyleUrl('#_GEarthExtensions_regularCoordinate');
5214 
5215         // shift coordinates in the KmlCoordArray down
5216         // TODO: speed this up
5217         coords.push(coords.get(numCoords - 1));
5218         for (i = numCoords - 1; i > coordData.index + 1; i--) {
5219           coords.set(i, coords.get(i - 1));
5220         }
5221 
5222         numCoords++;
5223 
5224         // create a new coordData object for the newly created
5225         // coordinate
5226         newCoordData = {};
5227         newCoordData.index = coordData.index + 1;
5228         newCoordData.regularPlacemark = this; // the converted midpoint
5229 
5230         // replace this to-be-converted midpoint with a new midpoint
5231         // placemark (will be to the left of the new coord)
5232         coordData.rightMidPlacemark = me.dom.buildPointPlacemark({
5233           point: coords.get(coordData.index),
5234           altitudeMode: altitudeMode,
5235           style: '#_GEarthExtensions_midCoordinate'
5236         });
5237         innerDoc.getFeatures().appendChild(coordData.rightMidPlacemark);
5238 
5239         me.edit.makeDraggable(coordData.rightMidPlacemark, {
5240           bounce: false,
5241           dragCallback: makeMidDragCallback_(coordData) // previous coord
5242         });
5243 
5244         // create a new right midpoint
5245         newCoordData.rightMidPlacemark = me.dom.buildPointPlacemark({
5246           point: coords.get(coordData.index),
5247           altitudeMode: altitudeMode,
5248           style: '#_GEarthExtensions_midCoordinate'
5249         });
5250         innerDoc.getFeatures().appendChild(newCoordData.rightMidPlacemark);
5251 
5252         me.edit.makeDraggable(newCoordData.rightMidPlacemark, {
5253           bounce: false,
5254           dragCallback: makeMidDragCallback_(newCoordData)
5255         });
5256 
5257         // create a delete listener
5258         newCoordData.deleteEventListener = makeRegularDeleteEventListener_(
5259             newCoordData);
5260         google.earth.addEventListener(this, 'dblclick',
5261             newCoordData.deleteEventListener);
5262 
5263         newCoordData.regularDragCallback =
5264             makeRegularDragCallback_(newCoordData);
5265 
5266         // insert the new coordData
5267         coordDataArr.splice(newCoordData.index, 0, newCoordData);
5268 
5269         // update all placemark indices after this newly inserted
5270         // coordinate, because indices have changed
5271         for (i = 0; i < numCoords; i++) {
5272           coordDataArr[i].index = i;
5273         }
5274       }
5275 
5276       // do regular dragging stuff
5277       newCoordData.regularDragCallback.call(this, newCoordData);
5278       
5279       // the regular drag callback calls options.editCallback
5280     };
5281   };
5282 
5283   // create the vertex editing (regular and midpoint) placemarks
5284   me.util.batchExecute(function() {
5285     for (var i = 0; i < numCoords; i++) {
5286       var curCoord = coords.get(i);
5287       var nextCoord = coords.get((i + 1) % numCoords);
5288 
5289       var coordData = {};
5290       coordDataArr.push(coordData);
5291       coordData.index = i;
5292 
5293       if (isRing && i == numCoords - 1) {
5294         // this is a repeat of the first coord, don't make placemarks for it
5295         continue;
5296       }
5297       
5298       // create the regular placemark on the point
5299       coordData.regularPlacemark = me.dom.buildPointPlacemark(curCoord, {
5300         altitudeMode: altitudeMode,
5301         style: '#_GEarthExtensions_regularCoordinate'
5302       });
5303       innerDoc.getFeatures().appendChild(coordData.regularPlacemark);
5304 
5305       coordData.regularDragCallback = makeRegularDragCallback_(coordData);
5306 
5307       // set up drag handlers for main placemarks
5308       me.edit.makeDraggable(coordData.regularPlacemark, {
5309         bounce: false,
5310         dragCallback: coordData.regularDragCallback
5311       });
5312 
5313       coordData.deleteEventListener =
5314           makeRegularDeleteEventListener_(coordData);
5315       google.earth.addEventListener(coordData.regularPlacemark, 'dblclick',
5316           coordData.deleteEventListener);
5317 
5318       // create the next midpoint placemark
5319       if (i < numCoords - 1 || isRing) {
5320         coordData.rightMidPlacemark = me.dom.buildPointPlacemark({
5321           point: new geo.Point(curCoord).midpoint(
5322               new geo.Point(nextCoord)),
5323           altitudeMode: altitudeMode,
5324           style: '#_GEarthExtensions_midCoordinate'
5325         });
5326         innerDoc.getFeatures().appendChild(coordData.rightMidPlacemark);
5327 
5328         // set up drag handlers for mid placemarks
5329         me.edit.makeDraggable(coordData.rightMidPlacemark, {
5330           bounce: false,
5331           dragCallback: makeMidDragCallback_(coordData)
5332         });
5333       }
5334     }
5335     
5336     checkDupMidpoints_();
5337 
5338     // display the editing UI
5339     me.pluginInstance.getFeatures().appendChild(innerDoc);
5340   });
5341 
5342   // set up an abort function for use in endEditLineString
5343   me.util.setJsDataValue(lineString, LINESTRINGEDITDATA_JSDATA_KEY, {
5344     innerDoc: innerDoc,
5345     abortAndEndFn: function() {
5346       me.util.batchExecute(function() {
5347         // duplicate the first coordinate to the end if necessary
5348         var numCoords = coords.getLength();
5349         if (numCoords && isRing) {
5350           var firstCoord = coords.get(0);
5351           var lastCoord = coords.get(numCoords - 1);
5352           if (!coordsEqual_(firstCoord, lastCoord)) {
5353             coords.pushLatLngAlt(firstCoord.getLatitude(),
5354                                  firstCoord.getLongitude(),
5355                                  firstCoord.getAltitude());
5356           }
5357         }
5358         
5359         for (var i = 0; i < coordDataArr.length; i++) {
5360           if (!coordDataArr[i].regularPlacemark) {
5361             continue;
5362           }
5363           
5364           // teardown for regular placemark, its delete event listener
5365           // and its right-mid placemark
5366           google.earth.removeEventListener(coordDataArr[i].regularPlacemark,
5367               'dblclick', coordDataArr[i].deleteEventListener);
5368 
5369           me.edit.endDraggable(coordDataArr[i].regularPlacemark);
5370         
5371           if (coordDataArr[i].rightMidPlacemark) {
5372             me.edit.endDraggable(coordDataArr[i].rightMidPlacemark);
5373           }
5374         }
5375 
5376         me.dom.removeObject(innerDoc);
5377       });
5378     }
5379   });
5380 };
5381 
5382 /**
5383  * Ceases the ability for the user to edit or draw the given line string.
5384  */
5385 GEarthExtensions.prototype.edit.endEditLineString = function(lineString) {
5386   // get placemark's drag data
5387   var lineStringEditData = this.util.getJsDataValue(
5388       lineString, LINESTRINGEDITDATA_JSDATA_KEY);
5389 
5390   // stop listening for mousedown on the window
5391   if (lineStringEditData) {
5392     lineStringEditData.abortAndEndFn.call(null);
5393 
5394     this.util.clearJsDataValue(lineString, LINESTRINGEDITDATA_JSDATA_KEY);
5395   }
5396 };
5397 /**
5398  * Contains various animation/effects tools for use in the Google Earth API.
5399  * @namespace
5400  */
5401 GEarthExtensions.prototype.fx = {isnamespace_:true};
5402 /**
5403  * @class Private singleton class for managing GEarthExtensions#fx animations
5404  * in a plugin instance.
5405  * @private
5406  */
5407 GEarthExtensions.prototype.fx.AnimationManager_ = createClass_(function() {
5408   this.extInstance = arguments.callee.extInstance_;
5409   this.animations_ = [];
5410 
5411   this.running_ = false;
5412   this.globalTime_ = 0.0;
5413 });
5414 
5415 /**
5416  * Start an animation (deriving from GEarthExtensions#fx.Animation).
5417  * @ignore
5418  */
5419 GEarthExtensions.prototype.fx.AnimationManager_.prototype.startAnimation =
5420 function(anim) {
5421   this.animations_.push({
5422     obj: anim,
5423     startGlobalTime: this.globalTime_
5424   });
5425   
5426   this.start_();
5427 };
5428 
5429 /**
5430  * Stop an animation (deriving from GEarthExtensions#fx.Animation).
5431  * @ignore
5432  */
5433 GEarthExtensions.prototype.fx.AnimationManager_.prototype.stopAnimation =
5434 function(anim) {
5435   for (var i = 0; i < this.animations_.length; i++) {
5436     if (this.animations_[i].obj == anim) {
5437       // remove the animation from the array
5438       this.animations_.splice(i, 1);
5439       return;
5440     }
5441   }
5442 };
5443 
5444 /**
5445  * Private, internal function to start animating
5446  * @ignore
5447  */
5448 GEarthExtensions.prototype.fx.AnimationManager_.prototype.start_ = function() {
5449   if (this.running_) {
5450     return;
5451   }
5452   
5453   this.startTimeStamp_ = Number(new Date());
5454   this.tick_();
5455   
5456   for (var i = 0; i < this.animations_.length; i++) {
5457     this.animations_[i].obj.renderFrame(0);
5458   }
5459   
5460   var me = this;
5461   this.frameendListener_ = function(){ me.tick_(); };
5462   this.tickInterval_ = window.setInterval(this.frameendListener_, 100);
5463   google.earth.addEventListener(this.extInstance.pluginInstance,
5464       'frameend', this.frameendListener_);
5465   this.running_ = true;
5466 };
5467 
5468 /**
5469  * Private, internal function to stop animating
5470  * @ignore
5471  */
5472 GEarthExtensions.prototype.fx.AnimationManager_.prototype.stop_ = function() {
5473   if (!this.running_) {
5474     return;
5475   }
5476   
5477   google.earth.removeEventListener(this.extInstance.pluginInstance,
5478       'frameend', this.frameendListener_);
5479   this.frameendListener_ = null;
5480   window.clearInterval(this.tickInterval_);
5481   this.tickInterval_ = null;
5482   this.running_ = false;
5483   this.globalTime_ = 0.0;
5484 };
5485 
5486 /**
5487  * Internal tick handler (frameend)
5488  * @ignore
5489  */
5490 GEarthExtensions.prototype.fx.AnimationManager_.prototype.tick_ = function() {
5491   if (!this.running_) {
5492     return;
5493   }
5494   
5495   this.globalTime_ = Number(new Date()) - this.startTimeStamp_;
5496   this.renderCurrentFrame_();
5497 };
5498 
5499 /**
5500  * Private function to render current animation frame state (by calling
5501  * registered Animations' individual frame renderers.
5502  * @ignore
5503  */
5504 GEarthExtensions.prototype.fx.AnimationManager_.prototype.renderCurrentFrame_ =
5505 function() {
5506   for (var i = this.animations_.length - 1; i >= 0; i--) {
5507     var animation = this.animations_[i];
5508     animation.obj.renderFrame(this.globalTime_ - animation.startGlobalTime);
5509   }
5510   
5511   if (this.animations_.length === 0) {
5512     this.stop_();
5513   }
5514 };
5515 
5516 /**
5517  * Returns the singleton animation manager for the plugin instance.
5518  * @private
5519  */
5520 GEarthExtensions.prototype.fx.getAnimationManager_ = function() {
5521   if (!this.fx.animationManager_) {
5522     this.fx.animationManager_ = new this.fx.AnimationManager_();
5523   }
5524   
5525   return this.fx.animationManager_;
5526 };
5527 
5528 /**
5529  * @class Base class for all GEarthExtensions#fx animations. Animations of this
5530  * base class are not bounded by a given time duration and must manually be
5531  * stopped when they are 'complete'.
5532  * @param {Function} renderCallback A method that will be called to render
5533  *     a frame of the animation. Its sole parameter will be the time, in
5534  *     seconds, of the frame to render.
5535  * @param {Function} [completionCallback] A callback method to fire when the
5536  *     animation is completed/stopped. The callback will receive an object
5537  *     literal argument that will contain a 'cancelled' boolean value that will
5538  *     be true if the effect was cancelled.
5539  */
5540 GEarthExtensions.prototype.fx.Animation = createClass_(function(renderFn,
5541                                                                 completionFn) {
5542   this.extInstance = arguments.callee.extInstance_;
5543   this.renderFn = renderFn;
5544   this.completionFn = completionFn || function(){};
5545 });
5546 
5547 /**
5548  * Start this animation.
5549  */
5550 GEarthExtensions.prototype.fx.Animation.prototype.start = function() {
5551   this.extInstance.fx.getAnimationManager_().startAnimation(this);
5552 };
5553 
5554 /**
5555  * Stop this animation.
5556  * @param {Boolean} [completed=true] Whether or not the animation is being
5557  *     stopped due to a successful completion. If not, the stop call is treated
5558  *     as a cancellation of the animation.
5559  */
5560 GEarthExtensions.prototype.fx.Animation.prototype.stop = function(completed) {
5561   this.extInstance.fx.getAnimationManager_().stopAnimation(this);
5562   this.completionFn({
5563     cancelled: !Boolean(completed || geo.util.isUndefined(completed))
5564   });
5565 };
5566 
5567 /**
5568  * Stop and rewind the animation to the frame at time t=0.
5569  */
5570 GEarthExtensions.prototype.fx.Animation.prototype.rewind = function() {
5571   this.renderFrame(0);
5572   this.stop(false);
5573 };
5574 
5575 /**
5576  * Render the frame at the given time after the animation was started.
5577  * @param {Number} time The time in seconds of the frame to render.
5578  */
5579 GEarthExtensions.prototype.fx.Animation.prototype.renderFrame = function(t) {
5580   this.renderFn.call(this, t);
5581 };
5582 
5583 /**
5584  * @class Generic class for animations of a fixed duration.
5585  * @param {Number} duration The length of time for which this animation should
5586  *     run, in seconds.
5587  * @param {Function} renderCallback A method that will be called to render
5588  *     a frame of the animation. Its sole parameter will be the time, in
5589  *     seconds, of the frame to render.
5590  * @param {Function} [completionCallback] A callback method to fire when the
5591  *     animation is completed/stopped. The callback will receive an object
5592  *     literal argument that will contain a 'cancelled' boolean value that will
5593  *     be true if the effect was cancelled.
5594  * @extends GEarthExtensions#fx.Animation
5595  */
5596 GEarthExtensions.prototype.fx.TimedAnimation = createClass_(
5597   [GEarthExtensions.prototype.fx.Animation],
5598 function(duration, renderFn, completionFn) {
5599   this.extInstance = arguments.callee.extInstance_;
5600   this.duration = duration;
5601   this.renderFn = renderFn;
5602   this.complete = false;
5603   this.completionFn = completionFn || function(){};
5604 });
5605 
5606 /**
5607  * Render the frame at the given time after the animation was started.
5608  * @param {Number} time The time of the frame to render, in seconds.
5609  */
5610 GEarthExtensions.prototype.fx.TimedAnimation.prototype.renderFrame =
5611 function(t) {
5612   if (this.complete) {
5613     return;
5614   }
5615   
5616   if (t > this.duration) {
5617     this.renderFn.call(this, this.duration);
5618     this.stop();
5619     this.complete = true;
5620     return;
5621   }
5622   
5623   this.renderFn.call(this, t);
5624 };
5625 /**
5626  * Bounces a point placemark by animating its altitude.
5627  * @param {KmlPlacemark} placemark The point placemark to bounce.
5628  * @param {Object} [options] The bounce options.
5629  * @param {Number} [options.duration=300] The duration of the initial bounce,
5630  *     in milliseconds.
5631  * @param {Number} [options.startAltitude] The altitude at which to start the
5632  *     bounce, in meters. The default is the point's current altitude.
5633  * @param {Number} [options.altitude] The altitude by which the placemark
5634  *     should rise at its peak, in meters. The default is the computed based
5635  *     on the current plugin viewport.
5636  * @param {Number} [options.phase] The bounce phase. If no phase is specified,
5637  *     both ascent and descent are performed. If phase=1, then only the ascent
5638  *     is performed. If phase=2, then only the descent and repeat are performed.
5639  * @param {Number} [options.repeat=0] The number of times to repeat the bounce.
5640  * @param {Number} [options.dampen=0.3] The altitude and duration dampening
5641  *     factor that repeat bounces should be scaled by.
5642  * @param {Function} [options.callback] A callback function to be triggered
5643  *     after the bounce is completed. The callback's 'this' variable will be
5644  *     bound to the placemark object, and it will receive a single boolean
5645  *     argument that will be true if the bounce was cancelled.
5646  *     Note that the callback is not fired if phase=2.
5647  */
5648 GEarthExtensions.prototype.fx.bounce = function(placemark, options) {
5649   options = checkParameters_(options, false, {
5650     duration: 300,
5651     startAltitude: ALLOWED_,
5652     altitude: this.util.getCamera().getAltitude() / 5,
5653     phase: ALLOWED_,
5654     repeat: 0,
5655     dampen: 0.3,
5656     callback: function(){}
5657   });
5658   
5659   var me = this;
5660   this.fx.rewind(placemark);
5661   
5662   // double check that we're given a placemark with a point geometry
5663   if (!'getGeometry' in placemark ||
5664       !placemark.getGeometry() ||
5665       placemark.getGeometry().getType() != 'KmlPoint') {
5666     throw new TypeError('Placemark must be a KmlPoint geometry');
5667   }
5668   
5669   var point = placemark.getGeometry();
5670   var origAltitudeMode = point.getAltitudeMode();
5671 
5672   // changing altitude if the mode is clamp to ground does nothing, so switch
5673   // to relative to ground
5674   if (origAltitudeMode == this.pluginInstance.ALTITUDE_CLAMP_TO_GROUND) {
5675     point.setAltitude(0);
5676     point.setAltitudeMode(this.pluginInstance.ALTITUDE_RELATIVE_TO_GROUND);
5677   }
5678   
5679   if (origAltitudeMode == this.pluginInstance.ALTITUDE_CLAMP_TO_SEA_FLOOR) {
5680     point.setAltitude(0);
5681     point.setAltitudeMode(this.pluginInstance.ALTITUDE_RELATIVE_TO_SEA_FLOOR);
5682   }
5683 
5684   if (typeof options.startAltitude != 'number') {
5685     options.startAltitude = point.getAltitude();
5686   }
5687   
5688   // setup the animation phases
5689   var phase1, phase2;
5690   
5691   // up
5692   phase1 = function() {
5693     me.fx.animateProperty(point, 'altitude', {
5694       duration: options.duration / 2,
5695       end: options.startAltitude + options.altitude,
5696       easing: 'out',
5697       featureProxy: placemark,
5698       callback: phase2 || function(){}
5699     });
5700   };
5701   
5702   // down and repeats
5703   phase2 = function(e) {
5704     if (e && e.cancelled) {
5705       return;
5706     }
5707     
5708     me.fx.animateProperty(point, 'altitude', {
5709       duration: options.duration / 2,
5710       start: options.startAltitude + options.altitude,
5711       end: options.startAltitude,
5712       easing: 'in',
5713       featureProxy: placemark,
5714       callback: function(e2) {
5715         point.setAltitudeMode(origAltitudeMode);
5716 
5717         if (e2.cancelled) {
5718           point.setAltitude(options.startAltitude);
5719           options.callback.call(placemark, e2);
5720           return;
5721         }
5722 
5723         // done with this bounce, should we bounce again?
5724         if (options.repeat >= 1) {
5725           --options.repeat;
5726           options.altitude *= options.dampen;
5727           options.duration *= Math.sqrt(options.dampen);
5728           options.phase = 0; // do all phases
5729           me.fx.bounce(placemark, options);
5730         } else {
5731           options.callback.call(placemark, e2);
5732         }
5733       }
5734     });
5735   };
5736   
5737   // animate the bounce
5738   if (options.phase === 1) {
5739     phase2 = null;
5740     phase1.call();
5741   } else if (options.phase === 2) {
5742     phase2.call();
5743   } else {
5744     phase1.call();
5745   }
5746 };
5747 /**
5748  * Cancel all animations on a given feature, potentially leaving them in an
5749  * intermediate visual state.
5750  */
5751 GEarthExtensions.prototype.fx.cancel = function(feature) {
5752   // TODO: verify that feature is a KmlFeature
5753   var animations = this.util.getJsDataValue(feature,
5754                        '_GEarthExtensions_anim') || [];
5755   for (var i = 0; i < animations.length; i++) {
5756     animations[i].stop(false);
5757   }
5758 };
5759 
5760 /**
5761  * Cancel all animations on a given feature and revert them to their t = 0
5762  * state.
5763  */
5764 GEarthExtensions.prototype.fx.rewind = function(feature) {
5765   // TODO: verify that feature is a KmlFeature
5766   var animations = this.util.getJsDataValue(feature,
5767                        '_GEarthExtensions_anim') || [];
5768   for (var i = 0; i < animations.length; i++) {
5769     animations[i].rewind();
5770   }
5771 };
5772 
5773 /**
5774  * Animate a numeric property on a plugin object.
5775  * @param {KmlObject} object The plugin object whose property to animate.
5776  * @param {String} property The property to animate. This should match 1:1 to
5777  *     the getter/setter methods on the plugin object. For example, to animate
5778  *     a KmlPoint latitude, pass in `latitude`, since the getter/setters are
5779  *     `getLatitude` and `setLatitude`.
5780  * @param {Object} options The property animation options.
5781  * @param {Number} [options.duration=500] The duration, in milliseconds, of the
5782  *     animation.
5783  * @param {Number} [options.start] The value of the property to set at the
5784  *     start of the animation.
5785  * @param {Number} [options.end] The desired end value of the property.
5786  * @param {Number} [options.delta] If end is not specified, you may set this
5787  *     to the desired change in the property value.
5788  * @param {String|Function} [options.easing='none'] The easing function to use
5789  *     during the animation. Valid values are 'none', 'in', 'out', or 'both'.
5790  *     Alternatively, an easy function mapping `[0.0, 1.0] -> [0.0, 1.0]` can
5791  *     be specified. No easing is `f(x) = x`.
5792  * @param {Function} [options.callback] A callback method to fire when the
5793  *     animation is completed/stopped. The callback will receive an object
5794  *     literal argument that will contain a 'cancelled' boolean value that will
5795  *     be true if the effect was cancelled.
5796  * @param {KmlFeature} [options.featureProxy] A feature to associate with this
5797  *     property animation for use with GEarthExtensions#fx.cancel or
5798  *     GEarthExtensions#fx.rewind.
5799  */
5800 GEarthExtensions.prototype.fx.animateProperty =
5801 function(obj, property, options) {
5802   options = checkParameters_(options, false, {
5803     duration: 500,
5804     start: ALLOWED_,
5805     end: ALLOWED_,
5806     delta: ALLOWED_,
5807     easing: 'none',
5808     callback: ALLOWED_,
5809     featureProxy: ALLOWED_
5810   });
5811   
5812   // http://www.timotheegroleau.com/Flash/experiments/easing_function_generator.htm
5813   // TODO: ensure easing function exists
5814   // get the easing function
5815   if (typeof options.easing == 'string') {
5816     options.easing = {
5817       'none': function(t) {
5818         return t;
5819       },
5820       'in': function(t) { // cubic in
5821         return t*t*t;
5822       },
5823       'out': function(t) { // cubic out
5824         var ts = t*t;
5825         var tc = ts*t;
5826         return tc - 3*ts + 3*t;
5827       },
5828       'both': function(t) { // quintic in-out
5829         var ts = t*t;
5830         var tc = ts*t;
5831         return 6*tc*ts - 15*ts*ts + 10*tc;
5832       }
5833     }[options.easing];
5834   }
5835 
5836   var propertyTitleCase = property.charAt(0).toUpperCase() +
5837                           property.substr(1);
5838 
5839   var me = this;
5840   
5841   /** @private */
5842   var doAnimate_;
5843   if (property == 'color') {
5844     // KmlColor blending
5845     if (options.delta) {
5846       throw new Error('Cannot use delta with color animations.');
5847     }
5848     
5849     var colorObj = obj.getColor() || {get: function(){ return ''; }};
5850     
5851     // use start/end
5852     if (!options.start) {
5853       options.start = colorObj.get();
5854     }
5855 
5856     if (!options.end) {
5857       options.end = colorObj.get();
5858     }
5859   
5860     /** @private */
5861     doAnimate_ = function(f) {
5862       colorObj.set(me.util.blendColors(options.start, options.end,
5863           options.easing.call(null, f)));
5864     };
5865   } else {
5866     // numerical property blending
5867     var getter = function() {
5868       return obj['get' + propertyTitleCase]();
5869     };
5870   
5871     var setter = function(val) {
5872       return obj['set' + propertyTitleCase](val);
5873     };
5874     
5875     // use EITHER start/end or delta
5876     if (!isFinite(options.start) && !isFinite(options.end)) {
5877       // use delta
5878       if (!isFinite(options.delta)) {
5879         options.delta = 0.0;
5880       }
5881     
5882       options.start = getter();
5883       options.end = getter() + options.delta;
5884     } else {
5885       // use start/end
5886       if (!isFinite(options.start)) {
5887         options.start = getter();
5888       }
5889 
5890       if (!isFinite(options.end)) {
5891         options.end = getter();
5892       }
5893     }
5894   
5895     /** @private */
5896     doAnimate_ = function(f) {
5897       setter(options.start + (options.end - options.start) *
5898                              options.easing.call(null, f));
5899     };
5900   }
5901   
5902   var anim = new this.fx.TimedAnimation(options.duration,
5903     function(t) {
5904       // render callback
5905       doAnimate_(1.0 * t / options.duration);
5906     },
5907     function(e) {
5908       // completion callback
5909       
5910       // remove this animation from the list of animations on the object
5911       var animations = me.util.getJsDataValue(options.featureProxy || obj,
5912           '_GEarthExtensions_anim');
5913       if (animations) {
5914         for (var i = 0; i < animations.length; i++) {
5915           if (animations[i] == this) {
5916             animations.splice(i, 1);
5917             break;
5918           }
5919         }
5920         
5921         if (!animations.length) {
5922           me.util.clearJsDataValue(options.featureProxy || obj,
5923               '_GEarthExtensions_anim');
5924         }
5925       }
5926 
5927       if (options.callback) {
5928         options.callback.call(obj, e);
5929       }
5930     });
5931   
5932   // add this animation to the list of animations on the object
5933   var animations = this.util.getJsDataValue(options.featureProxy || obj,
5934       '_GEarthExtensions_anim');
5935   if (animations) {
5936     animations.push(anim);
5937   } else {
5938     this.util.setJsDataValue(options.featureProxy || obj,
5939         '_GEarthExtensions_anim', [anim]);
5940   }
5941   
5942   anim.start();
5943   return anim;
5944 };
5945 /**
5946  * Contains methods for 3D math, including linear algebra/geo bindings.
5947  * @namespace
5948  */
5949 GEarthExtensions.prototype.math3d = {isnamespace_:true};
5950 /**
5951  * Converts an array of 3 Euler angle rotations to matrix form.
5952  * NOTE: Adapted from 'Graphics Gems IV', Chapter III.5,
5953  * "Euler Angle Conversion" by Ken Shoemake.
5954  * @see http://vered.rose.utoronto.ca/people/spike/GEMS/GEMS.html
5955  * @param {Number[]} eulerAngles An array of 3 frame-relative Euler rotation
5956  *     angles, each in radians.
5957  * @return {geo.linalg.Matrix} A matrix representing the transformation.
5958  * @private
5959  */
5960 function eulerAnglesToMatrix_(eulerAngles) {
5961   var I = 2; // used for roll, in radians
5962   var J = 0; // heading, in radians
5963   var K = 1; // tilt
5964 
5965   var m = [[0, 0, 0], [0, 0, 0], [0, 0, 0]];
5966 
5967   var cos_ti = Math.cos(eulerAngles[0]);
5968   var cos_tj = Math.cos(eulerAngles[1]);
5969   var cos_th = Math.cos(eulerAngles[2]);
5970 
5971   var sin_ti = Math.sin(eulerAngles[0]);
5972   var sin_tj = Math.sin(eulerAngles[1]);
5973   var sin_th = Math.sin(eulerAngles[2]);
5974 
5975   var cos_c = cos_ti * cos_th;
5976   var cos_s = cos_ti * sin_th;
5977   var sin_c = sin_ti * cos_th;
5978   var sin_s = sin_ti * sin_th;
5979 
5980   m[I][I] = cos_tj * cos_th;
5981   m[I][J] = sin_tj * sin_c - cos_s;
5982   m[I][K] = sin_tj * cos_c + sin_s;
5983 
5984   m[J][I] = cos_tj * sin_th;
5985   m[J][J] = sin_tj * sin_s + cos_c;
5986   m[J][K] = sin_tj * cos_s - sin_c;
5987 
5988   m[K][I] = -sin_tj;
5989   m[K][J] = cos_tj * sin_ti;
5990   m[K][K] = cos_tj * cos_ti;
5991 
5992   return new geo.linalg.Matrix(m);
5993 }
5994 
5995 /**
5996  * Converts a matrix to an array of 3 Euler angle rotations.
5997  * NOTE: Adapted from 'Graphics Gems IV', Chapter III.5,
5998  * "Euler Angle Conversion" by Ken Shoemake.
5999  * @see http://vered.rose.utoronto.ca/people/spike/GEMS/GEMS.html
6000  * @param {geo.linalg.Matrix} matrix A homogenous matrix representing a
6001  *     transformation.
6002  * @return {Number[]} An array of 3 frame-relative Euler rotation angles
6003  *     representing the transformation, each in radians.
6004  * @private
6005  */
6006 function matrixToEulerAngles_(matrix) {
6007   var I = 2 + 1; // + 1 because Sylvester uses 1-based indices.
6008   var J = 0 + 1;
6009   var K = 1 + 1;
6010   var FLT_EPSILON = 1e-6;
6011 
6012   var cy = Math.sqrt(matrix.e(I, I) * matrix.e(I, I) +
6013                      matrix.e(J, I) * matrix.e(J, I));
6014 
6015   if (cy <= 16 * FLT_EPSILON) {
6016     return [Math.atan2(-matrix.e(J, K), matrix.e(J, J)),
6017             Math.atan2(-matrix.e(K, I), cy),
6018             0];
6019   }
6020 
6021   return [Math.atan2( matrix.e(K, J), matrix.e(K, K)),
6022           Math.atan2(-matrix.e(K, I), cy),
6023           Math.atan2( matrix.e(J, I), matrix.e(I, I))];
6024 }
6025 
6026 /**
6027  * Converts heading, tilt, and roll (HTR) to a local orientation matrix
6028  * that transforms global direction vectors to local direction vectors.
6029  * @param {Number[]} htr A heading, tilt, roll array, where each angle is in
6030  *     degrees.
6031  * @return {geo.linalg.Matrix} A local orientation matrix.
6032  */
6033 GEarthExtensions.prototype.math3d.htrToLocalFrame = function(htr) {
6034   return eulerAnglesToMatrix_([
6035       htr[0].toRadians(), htr[1].toRadians(), htr[2].toRadians()]);
6036 };
6037 
6038 /**
6039  * Converts a local orientation matrix (right, dir, up vectors) in local
6040  * cartesian coordinates to heading, tilt, and roll.
6041  * @param {geo.linalg.Matrix} matrix A local orientation matrix.
6042  * @return {Number[]} A heading, tilt, roll array, where each angle is in
6043  *     degrees.
6044  */
6045 GEarthExtensions.prototype.math3d.localFrameToHtr = function(matrix) {
6046   var htr = matrixToEulerAngles_(matrix);
6047   return [htr[0].toDegrees(), htr[1].toDegrees(), htr[2].toDegrees()];
6048 };
6049 /**
6050  * Creates an orthonormal orientation matrix for a given set of object direction
6051  * and up vectors. The matrix rows will each be unit length and orthogonal to
6052  * each other. If the dir and up vectors are collinear, this function will fail
6053  * and return null.
6054  * @param {geo.linalg.Vector} dir The object direction vector.
6055  * @param {geo.linalg.Vector} up The object up vector.
6056  * @return {geo.linalg.Matrix} Returns the orthonormal orientation matrix,
6057  *     or null if none is possible.
6058  */
6059 GEarthExtensions.prototype.math3d.makeOrthonormalFrame = function(dir, up) {
6060   var newRight = dir.cross(up).toUnitVector();
6061   if (newRight.eql(geo.linalg.Vector.Zero(3))) {
6062     // dir and up are collinear.
6063     return null;
6064   }
6065   
6066   var newDir = up.cross(newRight).toUnitVector();
6067   var newUp = newRight.cross(newDir);
6068   return new geo.linalg.Matrix([newRight.elements,
6069                                 newDir.elements,
6070                                 newUp.elements]);
6071 };
6072 
6073 /**
6074  * Creates a local orientation matrix that can transform direction vectors
6075  * local to a given point to global direction vectors. The transpose of the
6076  * returned matrix performs the inverse transformation.
6077  * @param {geo.Point} point The world point at which local coordinates are to
6078  *     be transformed.
6079  * @return {geo.linalg.Matrix} An orientation matrix that can transform local
6080  *     coordinate vectors to global coordinate vectors.
6081  */
6082 GEarthExtensions.prototype.math3d.makeLocalToGlobalFrame = function(point) {
6083   var vertical = point.toCartesian().toUnitVector();
6084   var east = new geo.linalg.Vector([0, 1, 0]).cross(vertical).toUnitVector();
6085   var north = vertical.cross(east).toUnitVector();
6086   return new geo.linalg.Matrix([east.elements,
6087                                 north.elements,
6088                                 vertical.elements]);
6089 };
6090 /**
6091  * This class/namespace hybrid contains miscellaneous
6092  * utility functions and shortcuts for the Earth API.
6093  * @namespace
6094  */
6095 GEarthExtensions.prototype.util = {isnamespace_:true};
6096 GEarthExtensions.NAMED_COLORS = {
6097   'aqua': 'ffffff00',
6098   'black': 'ff000000',
6099   'blue': 'ffff0000',
6100   'fuchsia': 'ffff00ff',
6101   'gray': 'ff808080',
6102   'green': 'ff008000',
6103   'lime': 'ff00ff00',
6104   'maroon': 'ff000080',
6105   'navy': 'ff800000',
6106   'olive': 'ff008080',
6107   'purple': 'ff800080',
6108   'red': 'ff0000ff',
6109   'silver': 'ffc0c0c0',
6110   'teal': 'ff808000',
6111   'white': 'ffffffff',
6112   'yellow': 'ff00ffff'
6113 };
6114 
6115 /**
6116  * Converts between various color formats, i.e. `#rrggbb`, to the KML color
6117  * format (`aabbggrr`)
6118  * @param {String|Number[]} color The source color value.
6119  * @param {Number} [opacity] An optional opacity to go along with CSS/HTML style
6120  *     colors, from 0.0 to 1.0.
6121  * @return {String} A string in KML color format (`aabbggrr`), or null if
6122  *     the color could not be parsed.
6123  */
6124 GEarthExtensions.prototype.util.parseColor = function(arg, opacity) {
6125   // detect #rrggbb and convert to kml color aabbggrr
6126   // TODO: also accept 'rgb(0,0,0)' format using regex, maybe even hsl?
6127   var pad2_ = function(s) {
6128     return ((s.length < 2) ? '0' : '') + s;
6129   };
6130   
6131   if (geo.util.isArray(arg)) {
6132     // expected array as [r,g,b] or [r,g,b,a]
6133 
6134     return pad2_(((arg.length >= 4) ? arg[3].toString(16) : 'ff')) +
6135            pad2_(arg[2].toString(16)) +
6136            pad2_(arg[1].toString(16)) +
6137            pad2_(arg[0].toString(16));
6138   } else if (typeof arg == 'string') {
6139     // parsing a string
6140     if (arg.toLowerCase() in GEarthExtensions.NAMED_COLORS) {
6141       return GEarthExtensions.NAMED_COLORS[arg.toLowerCase()];
6142     } if (arg.length > 7) {
6143       // large than a possible CSS/HTML-style color, maybe it's already a KML
6144       // color
6145       return arg.match(/^[0-9a-f]{8}$/i) ? arg : null;
6146     } else {
6147       // assume it's given as an HTML color
6148       var kmlColor = null;
6149       if (arg.length > 4) {
6150         // try full HTML color
6151         kmlColor = arg.replace(
6152             /#?([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})/i,
6153             'ff$3$2$1').toLowerCase();
6154       } else {
6155         // try shorthand HTML/CSS color (#fff)
6156         kmlColor = arg.replace(
6157             /#?([0-9a-f])([0-9a-f])([0-9a-f])/i,
6158             'ff$3$3$2$2$1$1').toLowerCase();
6159       }
6160       
6161       if (kmlColor == arg) {
6162         return null; // no replacement done, so can't parse
6163       }
6164       
6165       if (!geo.util.isUndefined(opacity)) {
6166         kmlColor = pad2_(Math.floor(255 * opacity).toString(16)) +
6167             kmlColor.substring(2);
6168       }
6169       
6170       return kmlColor;
6171     }
6172   }
6173   
6174   return null; // couldn't parse, not a string or array
6175 };
6176 
6177 
6178 /**
6179  * Calculates a simple composite of the two given colors.
6180  * @param {String|Number[]} color1 The first ('source') color. Anthing that can
6181  *     be parsed with GEarthExtensions#util.parseColor.
6182  * @param {String|Number[]} color2 The second ('destination') color. Anything
6183  *     that can be parsed with GEarthExtensions#util.parseColor.
6184  * @param {Number} [fraction=0.5] The amount of color2 to composite onto/blend
6185  *     with color1, as a fraction from 0.0 to 1.0.
6186  * @type String
6187  */
6188 GEarthExtensions.prototype.util.blendColors = function(color1, color2,
6189                                                        fraction) {
6190   if (geo.util.isUndefined(fraction) || fraction === null) {
6191     fraction = 0.5;
6192   }
6193   
6194   color1 = this.util.parseColor(color1);
6195   color2 = this.util.parseColor(color2);
6196 
6197   var pad2_ = function(s) {
6198     return ((s.length < 2) ? '0' : '') + s;
6199   };
6200 
6201   var blendHexComponent_ = function(c1, c2) {
6202     c1 = parseInt(c1, 16);
6203     c2 = parseInt(c2, 16);
6204 
6205     return pad2_(Math.floor((c2 - c1) * fraction + c1).toString(16));
6206   };
6207 
6208   return blendHexComponent_(color1.substr(0,2), color2.substr(0,2)) +
6209          blendHexComponent_(color1.substr(2,2), color2.substr(2,2)) +
6210          blendHexComponent_(color1.substr(4,2), color2.substr(4,2)) +
6211          blendHexComponent_(color1.substr(6,2), color2.substr(6,2));
6212 };
6213 // TODO: unit test
6214 // NOTE: this is shared across all GEarthExtensions instances
6215 // dictionary mapping objects's jstag (uuid) to an object literal
6216 // { object: <object>, data: <object's js data dictionary> }
6217 var jsData_ = {};
6218 
6219 /* randomUUID.js - Version 1.0
6220 *
6221 * Copyright 2008, Robert Kieffer
6222 *
6223 * This software is made available under the terms of the Open Software License
6224 * v3.0 (available here: http://www.opensource.org/licenses/osl-3.0.php )
6225 *
6226 * The latest version of this file can be found at:
6227 * http://www.broofa.com/Tools/randomUUID.js
6228 *
6229 * For more information, or to comment on this, please go to:
6230 * http://www.broofa.com/blog/?p=151
6231 */
6232 
6233 /**
6234 * Create and return a "version 4" RFC-4122 UUID string.
6235 * @private
6236 */
6237 function randomUUID_() {
6238   var s = [], itoh = '0123456789ABCDEF', i = 0;
6239 
6240   // Make array of random hex digits. The UUID only has 32 digits in it, but we
6241   // allocate an extra items to make room for the '-'s we'll be inserting.
6242   for (i = 0; i < 36; i++) {
6243     s[i] = Math.floor(Math.random()*0x10);
6244   }
6245 
6246   // Conform to RFC-4122, section 4.4
6247   s[14] = 4;  // Set 4 high bits of time_high field to version
6248   s[19] = (s[19] & 0x3) | 0x8;  // Specify 2 high bits of clock sequence
6249 
6250   // Convert to hex chars
6251   for (i = 0; i < 36; i++) {
6252     s[i] = itoh.charAt(s[i]);
6253   }
6254 
6255   // Insert '-'s
6256   s[8] = s[13] = s[18] = s[23] = '-';
6257 
6258   return s.join('');
6259 }
6260 
6261 /** @private */
6262 function getJsTag_(object) {
6263   // TODO: use unique id from Earth API
6264   for (var tag in jsData_) {
6265     if (jsData_[tag].object.equals(object)) {
6266       return tag;
6267     }
6268   }
6269 
6270   return null;
6271 }
6272 
6273 /**
6274  * Returns whether or not the KmlObject has any JS-side data.
6275  * @param {KmlObject} object The plugin object to inquire about.
6276  * @public
6277  */
6278 GEarthExtensions.prototype.util.hasJsData = function(object) {
6279   return getJsTag_(object) ? true : false;
6280 };
6281 
6282 /**
6283  * Clears all JS-side data for the given KmlObject.
6284  * @param {KmlObject} object The plugin object to clear data on.
6285  */
6286 GEarthExtensions.prototype.util.clearAllJsData = function(object) {
6287   var jsTag = getJsTag_(object);
6288   if (jsTag) {
6289     delete jsData_[jsTag];
6290   }
6291 };
6292 
6293 /**
6294  * Gets the JS-side data for the given KmlObject associated with the given
6295  * key.
6296  * @param {KmlObject} object The plugin object to get data for.
6297  * @param {String} key The JS data key to request.
6298  * @public
6299  */
6300 GEarthExtensions.prototype.util.getJsDataValue = function(object, key) {
6301   var jsTag = getJsTag_(object);
6302   if (jsTag && key in jsData_[jsTag].data) {
6303     return jsData_[jsTag].data[key];
6304   }
6305 
6306   // TODO: null or undefined?
6307   return undefined;
6308 };
6309 
6310 /**
6311  * Sets the JS-side data for the given KmlObject associated with the given
6312  * key to the passed in value.
6313  * @param {KmlObject} object The object to get data for.
6314  * @param {String} key The JS data key to set.
6315  * @param {*} value The value to store for this key.
6316  * @public
6317  */
6318 GEarthExtensions.prototype.util.setJsDataValue =
6319 function(object, key, value) {
6320   var jsTag = getJsTag_(object);
6321   if (!jsTag) {
6322     // no current data dictionary, create a jstag for this object
6323     jsTag = null;
6324     while (!jsTag || jsTag in jsData_) {
6325       jsTag = randomUUID_();
6326     }
6327 
6328     // create an empty data dict
6329     jsData_[jsTag] = { object: object, data: {} };
6330   }
6331 
6332   // set the data
6333   jsData_[jsTag].data[key] = value;
6334 };
6335 
6336 /**
6337  * Clears the JS-side data for the given KmlObject associated with the given
6338  * key.
6339  * @param {KmlObject} object The plugin object to clear data on.
6340  * @param {String} key The JS data key whose value should be cleared.
6341  */
6342 GEarthExtensions.prototype.util.clearJsDataValue = function(object, key) {
6343   var jsTag = getJsTag_(object);
6344   if (jsTag &&
6345       key in jsData_[jsTag].data) {
6346     delete jsData_[jsTag].data[key];
6347 
6348     // check if the data dict is empty... if so, cleanly remove it
6349     for (var k in jsData_[jsTag].data) {
6350       return; // not empty
6351     }
6352 
6353     // data dict is empty
6354     this.util.clearAllJsData(object);
6355   }
6356 };
6357 /**
6358  * Loads and shows the given KML URL in the Google Earth Plugin instance.
6359  * @param {String} url The URL of the KML content to show.
6360  * @param {Object} [options] KML display options.
6361  * @param {Boolean} [options.cacheBuster=false] Enforce freshly downloading the
6362  *     KML by introducing a cache-busting query parameter.
6363  * @param {Boolean} [options.flyToView=false] Fly to the document-level abstract
6364  *     view in the loaded KML after loading it. If no explicit view is
6365  *     available, a default bounds view will be calculated and used unless
6366  *     options.flyToBoundsFallback is false.
6367  *     See GEarthExtensions#util.flyToObject for more information.
6368  * @param {Boolean} [options.flyToBoundsFallback=true] If options.flyToView is
6369  *     true and no document-level abstract view is explicitly defined,
6370  *     calculate and fly to a bounds view.
6371  */
6372 GEarthExtensions.prototype.util.displayKml = function(url, options) {
6373   options = checkParameters_(options, false, {
6374     cacheBuster: false,
6375     flyToView: false,
6376     flyToBoundsFallback: true,
6377     aspectRatio: 1.0
6378   });
6379   
6380   if (options.cacheBuster) {
6381     url += (url.match(/\?/) ? '&' : '?') + '_cacheBuster=' +
6382         Number(new Date()).toString();
6383   }
6384 
6385   // TODO: option to choose network link or fetchKml
6386   var me = this;
6387   google.earth.fetchKml(me.pluginInstance, url, function(kmlObject) {
6388     if (kmlObject) {
6389       me.pluginInstance.getFeatures().appendChild(kmlObject);
6390       
6391       if (options.flyToView) {
6392         me.util.flyToObject(kmlObject, {
6393           boundsFallback: options.flyToBoundsFallback,
6394           aspectRatio: options.aspectRatio
6395         });
6396       }
6397     }
6398   });
6399 };
6400 
6401 /**
6402  * Loads and shows the given KML string in the Google Earth Plugin instance.
6403  * @param {String} str The KML string to show.
6404  * @param {Object} [options] KML display options.
6405  * @param {Boolean} [options.flyToView=false] Fly to the document-level abstract
6406  *     view in the parsed KML. If no explicit view is available,
6407  *     a default bounds view will be calculated and used unless
6408  *     options.flyToBoundsFallback is false.
6409  *     See GEarthExtensions#util.flyToObject for more information.
6410  * @param {Boolean} [options.flyToBoundsFallback=true] If options.flyToView is
6411  *     true and no document-level abstract view is explicitly defined,
6412  *     calculate and fly to a bounds view.
6413  * @return Returns the parsed object on success, or null if there was an error.
6414  */
6415 GEarthExtensions.prototype.util.displayKmlString = function(str, options) {
6416   options = checkParameters_(options, false, {
6417     flyToView: false,
6418     flyToBoundsFallback: true,
6419     aspectRatio: 1.0
6420   });
6421   
6422   var kmlObject = this.pluginInstance.parseKml(str);
6423   if (kmlObject) {
6424     this.pluginInstance.getFeatures().appendChild(kmlObject);
6425     
6426     if (options.flyToView) {
6427       this.util.flyToObject(kmlObject, {
6428         boundsFallback: options.flyToBoundsFallback,
6429         aspectRatio: options.aspectRatio
6430       });
6431     }
6432   }
6433   
6434   return kmlObject;
6435 };
6436 /**
6437  * Creates a KmlLookAt and sets it as the Earth plugin's view. This function
6438  * takes the same parameters as GEarthExtensions#dom.buildLookAt.
6439  */
6440 GEarthExtensions.prototype.util.lookAt = function() {
6441   this.pluginInstance.getView().setAbstractView(
6442       this.dom.buildLookAt.apply(null, arguments));
6443 };
6444 
6445 /**
6446  * Gets the current view as a KmlLookAt.
6447  * @param {Number} [altitudeMode=ALTITUDE_ABSOLUTE] The altitude mode
6448  *     that the resulting LookAt should be in.
6449  * @type KmlLookAt
6450  * @return Returns the current view as a KmlLookAt.
6451  */
6452 GEarthExtensions.prototype.util.getLookAt = function(altitudeMode) {
6453   if (geo.util.isUndefined(altitudeMode)) {
6454     altitudeMode = this.pluginInstance.ALTITUDE_ABSOLUTE;
6455   }
6456   
6457   return this.pluginInstance.getView().copyAsLookAt(altitudeMode);
6458 };
6459 
6460 /**
6461  * Gets the current view as a KmlCamera.
6462  * @param {Number} [altitudeMode=ALTITUDE_ABSOLUTE] The altitude mode
6463  *     that the resulting camera should be in.
6464  * @type KmlCamera
6465  * @return Returns the current view as a KmlCamera.
6466  */
6467 GEarthExtensions.prototype.util.getCamera = function(altitudeMode) {
6468   if (geo.util.isUndefined(altitudeMode)) {
6469     altitudeMode = this.pluginInstance.ALTITUDE_ABSOLUTE;
6470   }
6471   
6472   return this.pluginInstance.getView().copyAsCamera(altitudeMode);
6473 };
6474 
6475 /**
6476  * Flies to an object; if the object is a feature and has an explicitly defined
6477  * abstract view, that view is used. Otherwise, attempts to calculate a bounds
6478  * view of the object and flies to that (assuming options.boundsFallback is
6479  * true).
6480  * @param {KmlObject} obj The object to fly to.
6481  * @param {Object} [options] Flyto options.
6482  * @param {Boolean} [options.boundsFallback=true] Whether or not to attempt to
6483  *     calculate a bounding box view of the object if it doesn't have an
6484  *     abstract view.
6485  * @param {Number} [options.aspectRatio=1.0] When calculating a bounding box
6486  *     view, this should be the current aspect ratio of the plugin window.
6487  */
6488 GEarthExtensions.prototype.util.flyToObject = function(obj, options) {
6489   options = checkParameters_(options, false, {
6490     boundsFallback: true,
6491     aspectRatio: 1.0
6492   });
6493   
6494   if (!obj) {
6495     throw new Error('flyToObject was given an invalid object.');
6496   }
6497   
6498   if ('getAbstractView' in obj && obj.getAbstractView()) {
6499     this.pluginInstance.getView().setAbstractView(
6500         obj.getAbstractView());
6501   } else if (options.boundsFallback) {
6502     var bounds = this.dom.computeBounds(obj);
6503     if (bounds && !bounds.isEmpty()) {
6504       this.view.setToBoundsView(bounds, {
6505         aspectRatio: options.aspectRatio
6506       });
6507     }
6508   }
6509 };
6510 
6511 /**
6512  * Executes the given function quickly using a Google Earth API callback
6513  * hack. Future versions of this method may use other methods for batch
6514  * execution.
6515  * @param {Function} batchFn The function containing batch code to execute.
6516  * @param {Object} [context] Optional context parameter to pass to the
6517  *     function.
6518  */
6519 GEarthExtensions.prototype.util.batchExecute = function(batchFn, context) {
6520   var me = this;
6521   google.earth.executeBatch(this.pluginInstance, function() {
6522     batchFn.call(me, context);
6523   });
6524 };
6525 
6526 /**
6527  * Enables or disables full camera ownership mode, which sets fly to speed
6528  * to teleport, disables user mouse interaction, and hides the navigation
6529  * controls.
6530  * @param {Boolean} enable Whether to enable or disable full camera ownership.
6531  */
6532 GEarthExtensions.prototype.util.takeOverCamera = function(enable) {
6533   if (enable || geo.util.isUndefined(enable)) {
6534     if (this.cameraControlOldProps_) {
6535       return;
6536     }
6537     
6538     this.cameraControlOldProps_ = {
6539       flyToSpeed: this.pluginInstance.getOptions().getFlyToSpeed(),
6540       mouseNavEnabled:
6541           this.pluginInstance.getOptions().getMouseNavigationEnabled(),
6542       navControlVis: this.pluginInstance.getNavigationControl().getVisibility()
6543     };
6544     
6545     this.pluginInstance.getOptions().setFlyToSpeed(
6546         this.pluginInstance.SPEED_TELEPORT);
6547     this.pluginInstance.getOptions().setMouseNavigationEnabled(false);
6548     this.pluginInstance.getNavigationControl().setVisibility(
6549         this.pluginInstance.VISIBILITY_HIDE);
6550   } else {
6551     if (!this.cameraControlOldProps_) {
6552       return;
6553     }
6554     
6555     this.pluginInstance.getOptions().setFlyToSpeed(
6556         this.cameraControlOldProps_.flyToSpeed);
6557     this.pluginInstance.getOptions().setMouseNavigationEnabled(
6558         this.cameraControlOldProps_.mouseNavEnabled);
6559     this.pluginInstance.getNavigationControl().setVisibility(
6560         this.cameraControlOldProps_.navControlVis);
6561     
6562     delete this.cameraControlOldProps_;
6563   }
6564 };
6565 // modified base64 for url
6566 // http://en.wikipedia.org/wiki/Base64
6567 var ALPHABET_ =
6568     'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_';
6569 
6570 // These algorithms are based on the Maps aPI polyline encoding algorithm:
6571 // http://code.google.com/apis/maps/documentation/include/polyline.js
6572 
6573 /**
6574  * Encodes an array of signed numbers into a string.
6575  * @param {Number[]} arr An array of signed numbers.
6576  * @type String
6577  * @return An encoded string representing the array of numbers.
6578  */
6579 GEarthExtensions.prototype.util.encodeArray = function(arr) {
6580   var s = '';
6581   for (var i = 0; i < arr.length; i++) {
6582     var sgn_num = arr[i] << 1;
6583     sgn_num = (arr[i] < 0) ? ~sgn_num : sgn_num;
6584 
6585     while (sgn_num >= 0x20) {
6586       s += ALPHABET_.charAt(0x20 | (sgn_num & 0x1f));
6587       sgn_num >>= 5;
6588     }
6589 
6590     s += ALPHABET_.charAt(sgn_num);
6591   }
6592 
6593   return s;
6594 };
6595 
6596 /**
6597  * Decodes a string representing an array of signed numbers encoded with
6598  * GEarthExtensions#util.encodeArray.
6599  * @param {String} str The encoded string.
6600  * @type Number[]
6601  */
6602 GEarthExtensions.prototype.util.decodeArray = function(str) {
6603   var len = str.length;
6604   var index = 0;
6605   var array = [];
6606 
6607   while (index < len) {
6608     var b;
6609     var shift = 0;
6610     var result = 0;
6611     do {
6612       b = ALPHABET_.indexOf(str.charAt(index++));
6613       result |= (b & 0x1f) << shift;
6614       shift += 5;
6615     } while (b >= 0x20);
6616 
6617     array.push(((result & 1) ? ~(result >> 1) : (result >> 1)));
6618   }
6619 
6620   return array;
6621 };
6622 /**
6623  * This class/namespace hybrid contains various camera/view
6624  * related.
6625  * @namespace
6626  */
6627 GEarthExtensions.prototype.view = {isnamespace_:true};
6628 /**
6629  * Creates a KmlAbstractView from a bounding box.
6630  * @param {geo.Bounds} bounds The bounding box for which to create a view.
6631  * @param {Object} options The parameters of the bounds view.
6632  * @param {Number} options.aspectRatio The aspect ratio (width : height)
6633  *     of the plugin viewport.
6634  * @param {Number} [options.defaultRange=1000] The default lookat range to use
6635  *     when creating a view for a degenerate, single-point bounding box.
6636  * @param {Number} [options.scaleRange=1.5] A scaling factor by which
6637  *     to multiple the lookat range.
6638  * @type KmlAbstractView
6639  */
6640 GEarthExtensions.prototype.view.createBoundsView = function(bounds, options) {
6641   options = checkParameters_(options, false, {
6642     aspectRatio: REQUIRED_,
6643     
6644     defaultRange: 1000,
6645     scaleRange: 1.5
6646   });
6647   
6648   var center = bounds.center();
6649   var lookAtRange = options.defaultRange;
6650   
6651   var boundsSpan = bounds.span();
6652   if (boundsSpan.lat || boundsSpan.lng) {
6653     var distEW = new geo.Point(center.lat(), bounds.east())
6654        .distance(new geo.Point(center.lat(), bounds.west()));
6655     var distNS = new geo.Point(bounds.north(), center.lng())
6656        .distance(new geo.Point(bounds.south(), center.lng()));
6657     
6658     var aspectRatio = Math.min(Math.max(options.aspectRatio,
6659                                         distEW / distNS),
6660                                1.0);
6661     
6662     // Create a LookAt using the experimentally derived distance formula.
6663     var alpha = (45.0 / (aspectRatio + 0.4) - 2.0).toRadians();
6664     var expandToDistance = Math.max(distNS, distEW);
6665     var beta = Math.min((90).toRadians(),
6666                         alpha + expandToDistance / (2 * geo.math.EARTH_RADIUS));
6667     
6668     lookAtRange = options.scaleRange * geo.math.EARTH_RADIUS *
6669         (Math.sin(beta) * Math.sqrt(1 + 1 / Math.pow(Math.tan(alpha), 2)) - 1);
6670   }
6671   
6672   return this.dom.buildLookAt(
6673       new geo.Point(center.lat(), center.lng(),
6674                     bounds.top(), bounds.northEastTop().altitudeMode()),
6675       { range: lookAtRange });
6676 };
6677 
6678 /**
6679  * Creates a bounds view and sets it as the Earth plugin's view. This function
6680  * takes the same parameters as GEarthExtensions#view.createBoundsView.
6681  */
6682 GEarthExtensions.prototype.view.setToBoundsView = function() {
6683   this.pluginInstance.getView().setAbstractView(
6684       this.view.createBoundsView.apply(this, arguments));
6685 };
6686 var ENC_OVERFLOW_ = 1073741824;
6687 
6688 function encodeCamera_(extInstance, cam) {
6689   var alt = Math.floor(cam.altitude * 1e1);
6690   return extInstance.util.encodeArray([
6691     Math.floor(geo.math.constrainValue(cam.lat, [-90, 90]) * 1e5),
6692     Math.floor(geo.math.wrapValue(cam.lng, [-180, 180]) * 1e5),
6693     Math.floor(alt / ENC_OVERFLOW_),
6694     (alt >= 0) ? alt % ENC_OVERFLOW_
6695                : (ENC_OVERFLOW_ - Math.abs(alt) % ENC_OVERFLOW_),
6696     Math.floor(geo.math.wrapValue(cam.heading, [0, 360]) * 1e1),
6697     Math.floor(geo.math.wrapValue(cam.tilt, [0, 180]) * 1e1),
6698     Math.floor(geo.math.wrapValue(cam.roll, [-180, 180]) * 1e1)
6699   ]);
6700 }
6701 
6702 function decodeCamera_(extInstance, str) {
6703   var arr = extInstance.util.decodeArray(str);
6704   return {
6705     lat: geo.math.constrainValue(arr[0] * 1e-5, [-90, 90]),
6706     lng: geo.math.wrapValue(arr[1] * 1e-5, [-180, 180]),
6707     altitude: (ENC_OVERFLOW_ * arr[2] + arr[3]) * 1e-1,
6708     heading: geo.math.wrapValue(arr[4] * 1e-1, [0, 360]),
6709     tilt: geo.math.wrapValue(arr[5] * 1e-1, [0, 180]),
6710     roll: geo.math.wrapValue(arr[6] * 1e-1, [-180, 180])
6711   };
6712 }
6713 
6714 /**
6715  * Serializes the current plugin viewport into a modified base64 alphabet
6716  * string. This method is platform and browser agnostic, and is safe to
6717  * store and distribute to others.
6718  * @return {String} A string representing the current viewport.
6719  * @see http://code.google.com/apis/maps/documentation/include/polyline.js
6720  *     for inspiration.
6721  */
6722 GEarthExtensions.prototype.view.serialize = function() {
6723   var camera = this.pluginInstance.getView().copyAsCamera(
6724       this.pluginInstance.ALTITUDE_ABSOLUTE);
6725   return '0' + encodeCamera_(this, {
6726     lat: camera.getLatitude(),
6727     lng: camera.getLongitude(),
6728     altitude: camera.getAltitude(),
6729     heading: camera.getHeading(),
6730     tilt: camera.getTilt(),
6731     roll: camera.getRoll()
6732   });
6733 };
6734 
6735 /**
6736  * Sets the current plugin viewport to the view represented by the given
6737  * string.
6738  * @param {String} viewString The modified base64 alphabet string representing
6739  *     the view to fly to. This string should've previously been calculated
6740  *     using GEarthExtensions#view.serialize.
6741  */
6742 GEarthExtensions.prototype.view.deserialize = function(s) {
6743   if (s.charAt(0) != '0') {  // Magic number.
6744     throw new Error('Invalid serialized view string.');
6745   }
6746 
6747   var cameraProps = decodeCamera_(this, s.substr(1));
6748   var camera = this.pluginInstance.createCamera('');
6749   
6750   // TODO: isFinite checks
6751   camera.set(cameraProps.lat, cameraProps.lng, cameraProps.altitude,
6752       this.pluginInstance.ALTITUDE_ABSOLUTE, cameraProps.heading,
6753       cameraProps.tilt, cameraProps.roll);
6754   this.pluginInstance.getView().setAbstractView(camera);
6755 };
6756 
6757 // Backwards compatibility.
6758 GEarthExtensions.prototype.util.serializeView =
6759     GEarthExtensions.prototype.view.serialize;
6760 GEarthExtensions.prototype.util.deserializeView =
6761     GEarthExtensions.prototype.view.deserialize;
6762 /**
6763  * Creates an abstract view with the viewer at the given camera point, looking
6764  * towards the given look at point. For best results, use ALTITUDE_ABSOLUTE
6765  * camera and look at points.
6766  * @param {PointOptions|geo.Point} cameraPoint The viewer location.
6767  * @param {PointOptions|geo.Point} lookAtPoint The location to look at/towards.
6768  * @type KmlAbstractView
6769  */
6770 GEarthExtensions.prototype.view.createVantageView = function(cameraPoint,
6771                                                              lookAtPoint) {
6772   // TODO: handle case where lookat point is directly below camera.
6773   cameraPoint = new geo.Point(cameraPoint);
6774   lookAtPoint = new geo.Point(lookAtPoint);
6775   
6776   var heading = cameraPoint.heading(lookAtPoint);
6777   var roll = 0;
6778   
6779   // Tilt is the hard part:
6780   // 
6781   // Put the positions in world space and get a local orientation matrix for the
6782   // camera position. The matrix is used to figure out the angle between the
6783   // upside up vector of the local frame and the direction towards the
6784   // placemark. This is used for tilt.
6785   // 
6786   // Tilt is complicated for two reasons:
6787   //   1. tilt = 0 is facing down instead of facing towards horizon. This is 
6788   //      opposite of KML model behavior.
6789   //   2. tilt is relative to the current position of the camera. Not relative
6790   //      to say, the North Pole or some other global axis. Tilt is *relative*.
6791   var cameraCartesian = cameraPoint.toCartesian();
6792   var lookAtCartesian = lookAtPoint.toCartesian();
6793   var frame = this.math3d.makeLocalToGlobalFrame(cameraPoint);
6794 
6795   // Create the unit direction vector from the camera to the look at point.
6796   var lookVec = lookAtCartesian.subtract(cameraCartesian).toUnitVector();
6797 
6798   // Take the angle from the negative upside down vector.
6799   // See tilt complication reason (1).
6800   var downVec = new geo.linalg.Vector(frame.elements[2]).multiply(-1);
6801 
6802   // Figure out the tilt angle in degrees.
6803   var tilt = Math.acos(downVec.dot(lookVec)).toDegrees();
6804 
6805   return this.dom.buildCamera(cameraPoint, {heading: heading, tilt: tilt});
6806 };
6807 window.GEarthExtensions = GEarthExtensions;
6808 })();
6809