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