1/*! 2 * GMAP3 Plugin for jQuery 3 * Version : 6.0.0 4 * Date : 2014-04-25 5 * Author : DEMONTE Jean-Baptiste 6 * Contact : [email protected] 7 * Web site : http://gmap3.net 8 * Licence : GPL v3 : http://www.gnu.org/licenses/gpl.html 9 * 10 * Copyright (c) 2010-2014 Jean-Baptiste DEMONTE 11 * All rights reserved. 12 */ 13;(function ($, undef) { 14 15var defaults, gm, 16 gId = 0, 17 isFunction = $.isFunction, 18 isArray = $.isArray; 19 20function isObject(m) { 21 return typeof m === "object"; 22} 23 24function isString(m) { 25 return typeof m === "string"; 26} 27 28function isNumber(m) { 29 return typeof m === "number"; 30} 31 32function isUndefined(m) { 33 return m === undef; 34} 35 36/** 37 * Initialize default values 38 * defaults are defined at first gmap3 call to pass the rails asset pipeline and jasmine while google library is not yet loaded 39 */ 40function initDefaults() { 41 gm = google.maps; 42 if (!defaults) { 43 defaults = { 44 verbose: false, 45 queryLimit: { 46 attempt: 5, 47 delay: 250, // setTimeout(..., delay + random); 48 random: 250 49 }, 50 classes: (function () { 51 var r = {}; 52 $.each("Map Marker InfoWindow Circle Rectangle OverlayView StreetViewPanorama KmlLayer TrafficLayer BicyclingLayer GroundOverlay StyledMapType ImageMapType".split(" "), function (_, k) { 53 r[k] = gm[k]; 54 }); 55 return r; 56 }()), 57 map: { 58 mapTypeId : gm.MapTypeId.ROADMAP, 59 center: [46.578498, 2.457275], 60 zoom: 2 61 }, 62 overlay: { 63 pane: "floatPane", 64 content: "", 65 offset: { 66 x: 0, 67 y: 0 68 } 69 }, 70 geoloc: { 71 getCurrentPosition: { 72 maximumAge: 60000, 73 timeout: 5000 74 } 75 } 76 } 77 } 78} 79 80 81/** 82 * Generate a new ID if not defined 83 * @param id {string} (optional) 84 * @param simulate {boolean} (optional) 85 * @returns {*} 86 */ 87function globalId(id, simulate) { 88 return isUndefined(id) ? "gmap3_" + (simulate ? gId + 1 : ++gId) : id; 89} 90 91 92/** 93 * Return true if current version of Google Maps is equal or above to these in parameter 94 * @param version {string} Minimal version required 95 * @return {Boolean} 96 */ 97function googleVersionMin(version) { 98 var i, 99 gmVersion = gm.version.split("."); 100 version = version.split("."); 101 for (i = 0; i < gmVersion.length; i++) { 102 gmVersion[i] = parseInt(gmVersion[i], 10); 103 } 104 for (i = 0; i < version.length; i++) { 105 version[i] = parseInt(version[i], 10); 106 if (gmVersion.hasOwnProperty(i)) { 107 if (gmVersion[i] < version[i]) { 108 return false; 109 } 110 } else { 111 return false; 112 } 113 } 114 return true; 115} 116 117 118/** 119 * attach events from a container to a sender 120 * td[ 121 * events => { eventName => function, } 122 * onces => { eventName => function, } 123 * data => mixed data 124 * ] 125 **/ 126function attachEvents($container, args, sender, id, senders) { 127 var td = args.td || {}, 128 context = { 129 id: id, 130 data: td.data, 131 tag: td.tag 132 }; 133 function bind(items, handler) { 134 if (items) { 135 $.each(items, function (name, f) { 136 var self = $container, fn = f; 137 if (isArray(f)) { 138 self = f[0]; 139 fn = f[1]; 140 } 141 handler(sender, name, function (event) { 142 fn.apply(self, [senders || sender, event, context]); 143 }); 144 }); 145 } 146 } 147 bind(td.events, gm.event.addListener); 148 bind(td.onces, gm.event.addListenerOnce); 149} 150 151/** 152 * Extract keys from object 153 * @param obj {object} 154 * @returns {Array} 155 */ 156function getKeys(obj) { 157 var k, keys = []; 158 for (k in obj) { 159 if (obj.hasOwnProperty(k)) { 160 keys.push(k); 161 } 162 } 163 return keys; 164} 165 166/** 167 * copy a key content 168 **/ 169function copyKey(target, key) { 170 var i, 171 args = arguments; 172 for (i = 2; i < args.length; i++) { 173 if (key in args[i]) { 174 if (args[i].hasOwnProperty(key)) { 175 target[key] = args[i][key]; 176 return; 177 } 178 } 179 } 180} 181 182/** 183 * Build a tuple 184 * @param args {object} 185 * @param value {object} 186 * @returns {object} 187 */ 188function tuple(args, value) { 189 var k, i, 190 keys = ["data", "tag", "id", "events", "onces"], 191 td = {}; 192 193 // "copy" the common data 194 if (args.td) { 195 for (k in args.td) { 196 if (args.td.hasOwnProperty(k)) { 197 if ((k !== "options") && (k !== "values")) { 198 td[k] = args.td[k]; 199 } 200 } 201 } 202 } 203 // "copy" some specific keys from value first else args.td 204 for (i = 0; i < keys.length; i++) { 205 copyKey(td, keys[i], value, args.td); 206 } 207 208 // create an extended options 209 td.options = $.extend({}, args.opts || {}, value.options || {}); 210 211 return td; 212} 213 214/** 215 * Log error 216 */ 217function error() { 218 if (defaults.verbose) { 219 var i, err = []; 220 if (window.console && (isFunction(console.error))) { 221 for (i = 0; i < arguments.length; i++) { 222 err.push(arguments[i]); 223 } 224 console.error.apply(console, err); 225 } else { 226 err = ""; 227 for (i = 0; i < arguments.length; i++) { 228 err += arguments[i].toString() + " "; 229 } 230 alert(err); 231 } 232 } 233} 234 235/** 236 * return true if mixed is usable as number 237 **/ 238function numeric(mixed) { 239 return (isNumber(mixed) || isString(mixed)) && mixed !== "" && !isNaN(mixed); 240} 241 242/** 243 * convert data to array 244 **/ 245function array(mixed) { 246 var k, a = []; 247 if (!isUndefined(mixed)) { 248 if (isObject(mixed)) { 249 if (isNumber(mixed.length)) { 250 a = mixed; 251 } else { 252 for (k in mixed) { 253 a.push(mixed[k]); 254 } 255 } 256 } else { 257 a.push(mixed); 258 } 259 } 260 return a; 261} 262 263/** 264 * create a function to check a tag 265 */ 266function ftag(tag) { 267 if (tag) { 268 if (isFunction(tag)) { 269 return tag; 270 } 271 tag = array(tag); 272 return function (val) { 273 var i; 274 if (isUndefined(val)) { 275 return false;
276 } 277 if (isObject(val)) { 278 for (i = 0; i < val.length; i++) { 279 if ($.inArray(val[i], tag) >= 0) { 280 return true; 281 } 282 } 283 return false; 284 } 285 return $.inArray(val, tag) >= 0; 286 }; 287 } 288} 289 290 291/** 292 * convert mixed [ lat, lng ] objet to gm.LatLng 293 **/ 294function toLatLng(mixed, emptyReturnMixed, noFlat) { 295 var empty = emptyReturnMixed ? mixed : null; 296 if (!mixed || (isString(mixed))) { 297 return empty; 298 } 299 // defined latLng 300 if (mixed.latLng) { 301 return toLatLng(mixed.latLng); 302 } 303 // gm.LatLng object 304 if (mixed instanceof gm.LatLng) { 305 return mixed; 306 } 307 // {lat:X, lng:Y} object 308 if (numeric(mixed.lat)) { 309 return new gm.LatLng(mixed.lat, mixed.lng); 310 } 311 // [X, Y] object 312 if (!noFlat && isArray(mixed)) { 313 if (!numeric(mixed[0]) || !numeric(mixed[1])) { 314 return empty; 315 } 316 return new gm.LatLng(mixed[0], mixed[1]); 317 } 318 return empty; 319} 320 321/** 322 * convert mixed [ sw, ne ] object by gm.LatLngBounds 323 **/ 324function toLatLngBounds(mixed) { 325 var ne, sw; 326 if (!mixed || mixed instanceof gm.LatLngBounds) { 327 return mixed || null; 328 } 329 if (isArray(mixed)) { 330 if (mixed.length === 2) { 331 ne = toLatLng(mixed[0]); 332 sw = toLatLng(mixed[1]); 333 } else if (mixed.length === 4) { 334 ne = toLatLng([mixed[0], mixed[1]]); 335 sw = toLatLng([mixed[2], mixed[3]]); 336 } 337 } else { 338 if (("ne" in mixed) && ("sw" in mixed)) { 339 ne = toLatLng(mixed.ne); 340 sw = toLatLng(mixed.sw); 341 } else if (("n" in mixed) && ("e" in mixed) && ("s" in mixed) && ("w" in mixed)) { 342 ne = toLatLng([mixed.n, mixed.e]); 343 sw = toLatLng([mixed.s, mixed.w]); 344 } 345 } 346 if (ne && sw) { 347 return new gm.LatLngBounds(sw, ne); 348 } 349 return null; 350} 351 352/** 353 * resolveLatLng 354 **/ 355function resolveLatLng(ctx, method, runLatLng, args, attempt) { 356 var latLng = runLatLng ? toLatLng(args.td, false, true) : false, 357 conf = latLng ? {latLng: latLng} : (args.td.address ? (isString(args.td.address) ? {address: args.td.address} : args.td.address) : false), 358 cache = conf ? geocoderCache.get(conf) : false, 359 self = this; 360 if (conf) { 361 attempt = attempt || 0; // convert undefined to int 362 if (cache) { 363 args.latLng = cache.results[0].geometry.location; 364 args.results = cache.results; 365 args.status = cache.status; 366 method.apply(ctx, [args]); 367 } else { 368 if (conf.location) { 369 conf.location = toLatLng(conf.location); 370 } 371 if (conf.bounds) { 372 conf.bounds = toLatLngBounds(conf.bounds); 373 } 374 geocoder().geocode( 375 conf, 376 function (results, status) { 377 if (status === gm.GeocoderStatus.OK) { 378 geocoderCache.store(conf, {results: results, status: status}); 379 args.latLng = results[0].geometry.location; 380 args.results = results; 381 args.status = status; 382 method.apply(ctx, [args]); 383 } else if ((status === gm.GeocoderStatus.OVER_QUERY_LIMIT) && (attempt < defaults.queryLimit.attempt)) { 384 setTimeout( 385 function () { 386 resolveLatLng.apply(self, [ctx, method, runLatLng, args, attempt + 1]); 387 }, 388 defaults.queryLimit.delay + Math.floor(Math.random() * defaults.queryLimit.random) 389 ); 390 } else { 391 error("geocode failed", status, conf); 392 args.latLng = args.results = false; 393 args.status = status; 394 method.apply(ctx, [args]); 395 } 396 } 397 ); 398 } 399 } else { 400 args.latLng = toLatLng(args.td, false, true); 401 method.apply(ctx, [args]); 402 } 403} 404 405function resolveAllLatLng(list, ctx, method, args) { 406 var self = this, i = -1; 407 408 function resolve() { 409 // look for next address to resolve 410 do { 411 i++; 412 } while ((i < list.length) && !("address" in list[i])); 413 414 // no address found, so run method 415 if (i >= list.length) { 416 method.apply(ctx, [args]); 417 return; 418 } 419 420 resolveLatLng( 421 self, 422 function (args) { 423 delete args.td; 424 $.extend(list[i], args); 425 resolve.apply(self, []); // resolve next (using apply avoid too much recursion) 426 }, 427 true, 428 {td: list[i]} 429 ); 430 } 431 resolve(); 432} 433 434 435 436/** 437 * geolocalise the user and return a LatLng 438 **/ 439function geoloc(ctx, method, args) { 440 var is_echo = false; // sometime, a kind of echo appear, this trick will notice once the first call is run to ignore the next one 441 if (navigator && navigator.geolocation) { 442 navigator.geolocation.getCurrentPosition( 443 function (pos) { 444 if (!is_echo) { 445 is_echo = true; 446 args.latLng = new gm.LatLng(pos.coords.latitude, pos.coords.longitude); 447 method.apply(ctx, [args]); 448 } 449 }, 450 function () { 451 if (!is_echo) { 452 is_echo = true; 453 args.latLng = false;
454 method.apply(ctx, [args]); 455 } 456 }, 457 args.opts.getCurrentPosition 458 ); 459 } else { 460 args.latLng = false; 461 method.apply(ctx, [args]); 462 } 463} 464 465/** 466 * Return true if get is a direct call 467 * it means : 468 * - get is the only key 469 * - get has no callback 470 * @param obj {Object} The request to check 471 * @return {Boolean} 472 */ 473function isDirectGet(obj) { 474 var k, 475 result = false; 476 if (isObject(obj) && obj.hasOwnProperty("get")) { 477 for (k in obj) { 478 if (k !== "get") { 479 return false; 480 } 481 } 482 result = !obj.get.hasOwnProperty("callback"); 483 } 484 return result; 485} 486var services = {}, 487 geocoderCache = new GeocoderCache(); 488 489 490function geocoder(){ 491 if (!services.geocoder) { 492 services.geocoder = new gm.Geocoder(); 493 } 494 return services.geocoder; 495} 496/** 497 * Class GeocoderCache 498 * @constructor 499 */ 500function GeocoderCache() { 501 var cache = []; 502 503 this.get = function (request) { 504 if (cache.length) { 505 var i, j, k, item, eq, 506 keys = getKeys(request); 507 for (i = 0; i < cache.length; i++) { 508 item = cache[i]; 509 eq = keys.length === item.keys.length; 510 for (j = 0; (j < keys.length) && eq; j++) { 511 k = keys[j]; 512 eq = k in item.request; 513 if (eq) { 514 if (isObject(request[k]) && ("equals" in request[k]) && isFunction(request[k])) { 515 eq = request[k].equals(item.request[k]); 516 } else { 517 eq = request[k] === item.request[k]; 518 } 519 } 520 } 521 if (eq) { 522 return item.results; 523 } 524 } 525 } 526 }; 527 528 this.store = function (request, results) { 529 cache.push({request: request, keys: getKeys(request), results: results}); 530 }; 531} 532/** 533 * Class Stack 534 * @constructor 535 */ 536function Stack() { 537 var st = [], 538 self = this; 539 540 self.empty = function () { 541 return !st.length; 542 }; 543 544 self.add = function (v) { 545 st.push(v); 546 }; 547 548 self.get = function () { 549 return st.length ? st[0] : false; 550 }; 551 552 self.ack = function () { 553 st.shift(); 554 }; 555} 556/** 557 * Class Store 558 * @constructor 559 */ 560function Store() { 561 var store = {}, // name => [id, ...] 562 objects = {}, // id => object 563 self = this; 564 565 function normalize(res) { 566 return { 567 id: res.id, 568 name: res.name, 569 object: res.obj, 570 tag: res.tag, 571 data: res.data 572 }; 573 } 574 575 /** 576 * add a mixed to the store 577 **/ 578 self.add = function (args, name, obj, sub) { 579 var td = args.td || {}, 580 id = globalId(td.id); 581 if (!store[name]) { 582 store[name] = []; 583 } 584 if (id in objects) { // object already exists: remove it 585 self.clearById(id); 586 } 587 objects[id] = {obj: obj, sub: sub, name: name, id: id, tag: td.tag, data: td.data}; 588 store[name].push(id); 589 return id; 590 }; 591 592 /** 593 * return a stored object by its id 594 **/ 595 self.getById = function (id, sub, full) { 596 var result = false;
597 if (id in objects) { 598 if (sub) { 599 result = objects[id].sub; 600 } else if (full) { 601 result = normalize(objects[id]); 602 } else { 603 result = objects[id].obj; 604 } 605 } 606 return result; 607 }; 608 609 /** 610 * return a stored value 611 **/ 612 self.get = function (name, last, tag, full) { 613 var n, id, check = ftag(tag); 614 if (!store[name] || !store[name].length) { 615 return null; 616 } 617 n = store[name].length; 618 while (n) { 619 n--; 620 id = store[name][last ? n : store[name].length - n - 1]; 621 if (id && objects[id]) { 622 if (check && !check(objects[id].tag)) { 623 continue; 624 } 625 return full ? normalize(objects[id]) : objects[id].obj; 626 } 627 } 628 return null; 629 }; 630 631 /** 632 * return all stored values 633 **/ 634 self.all = function (name, tag, full) { 635 var result = [], 636 check = ftag(tag), 637 find = function (n) { 638 var i, id; 639 for (i = 0; i < store[n].length; i++) { 640 id = store[n][i]; 641 if (id && objects[id]) { 642 if (check && !check(objects[id].tag)) { 643 continue; 644 } 645 result.push(full ? normalize(objects[id]) : objects[id].obj); 646 } 647 } 648 }; 649 if (name in store) { 650 find(name); 651 } else if (isUndefined(name)) { // internal use only 652 for (name in store) { 653 find(name); 654 } 655 } 656 return result; 657 }; 658 659 /** 660 * hide and remove an object 661 **/ 662 function rm(obj) { 663 // Google maps element 664 if (isFunction(obj.setMap)) { 665 obj.setMap(null); 666 } 667 // jQuery 668 if (isFunction(obj.remove)) { 669 obj.remove(); 670 } 671 // internal (cluster) 672 if (isFunction(obj.free)) { 673 obj.free(); 674 } 675 obj = null; 676 } 677 678 /** 679 * remove one object from the store 680 **/ 681 self.rm = function (name, check, pop) { 682 var idx, id; 683 if (!store[name]) { 684 return false; 685 } 686 if (check) { 687 if (pop) { 688 for (idx = store[name].length - 1; idx >= 0; idx--) { 689 id = store[name][idx]; 690 if (check(objects[id].tag)) { 691 break; 692 } 693 } 694 } else { 695 for (idx = 0; idx < store[name].length; idx++) { 696 id = store[name][idx]; 697 if (check(objects[id].tag)) { 698 break; 699 } 700 } 701 } 702 } else { 703 idx = pop ? store[name].length - 1 : 0; 704 } 705 if (!(idx in store[name])) { 706 return false; 707 } 708 return self.clearById(store[name][idx], idx); 709 }; 710 711 /** 712 * remove object from the store by its id 713 **/ 714 self.clearById = function (id, idx) { 715 if (id in objects) { 716 var i, name = objects[id].name; 717 for (i = 0; isUndefined(idx) && i < store[name].length; i++) { 718 if (id === store[name][i]) { 719 idx = i; 720 } 721 } 722 rm(objects[id].obj); 723 if (objects[id].sub) { 724 rm(objects[id].sub); 725 } 726 delete objects[id]; 727 store[name].splice(idx, 1); 728 return true; 729 } 730 return false; 731 }; 732 733 /** 734 * return an object from a container object in the store by its id 735 * ! for now, only cluster manage this feature 736 **/ 737 self.objGetById = function (id) { 738 var result, idx; 739 if (store.clusterer) { 740 for (idx in store.clusterer) { 741 if ((result = objects[store.clusterer[idx]].obj.getById(id)) !== false) { 742 return result; 743 } 744 } 745 } 746 return false; 747 }; 748 749 /** 750 * remove object from a container object in the store by its id 751 * ! for now, only cluster manage this feature 752 **/ 753 self.objClearById = function (id) { 754 var idx; 755 if (store.clusterer) { 756 for (idx in store.clusterer) { 757 if (objects[store.clusterer[idx]].obj.clearById(id)) { 758 return true; 759 } 760 } 761 } 762 return null; 763 }; 764 765 /** 766 * remove objects from the store 767 **/ 768 self.clear = function (list, last, first, tag) { 769 var k, i, name, 770 check = ftag(tag); 771 if (!list || !list.length) { 772 list = []; 773 for (k in store) { 774 list.push(k); 775 } 776 } else { 777 list = array(list); 778 } 779 for (i = 0; i < list.length; i++) { 780 name = list[i]; 781 if (last) { 782 self.rm(name, check, true); 783 } else if (first) { 784 self.rm(name, check, false); 785 } else { // all 786 while (self.rm(name, check, false)) { 787 } 788 } 789 } 790 }; 791 792 /** 793 * remove object from a container object in the store by its tags 794 * ! for now, only cluster manage this feature 795 **/ 796 self.objClear = function (list, last, first, tag) { 797 var idx; 798 if (store.clusterer && ($.inArray("marker", list) >= 0 || !list.length)) { 799 for (idx in store.clusterer) { 800 objects[store.clusterer[idx]].obj.clear(last, first, tag); 801 } 802 } 803 }; 804} 805/** 806 * Class Task 807 * @param ctx 808 * @param onEnd 809 * @param td 810 * @constructor 811 */ 812function Task(ctx, onEnd, td) { 813 var session = {}, 814 self = this, 815 current, 816 resolve = { 817 latLng: { // function => bool (=> address = latLng) 818 map: false, 819 marker: false, 820 infowindow: false, 821 circle: false,
822 overlay: false, 823 getlatlng: false, 824 getmaxzoom: false, 825 getelevation: false, 826 streetviewpanorama: false, 827 getaddress: true 828 }, 829 geoloc: { 830 getgeoloc: true 831 } 832 }; 833 834 function unify(td) { 835 var result = {}; 836 result[td] = {}; 837 return result; 838 } 839 840 if (isString(td)) { 841 td = unify(td); 842 } 843 844 function next() { 845 var k; 846 for (k in td) { 847 if (td.hasOwnProperty(k) && !session.hasOwnProperty(k)) { 848 return k; 849 } 850 } 851 } 852 853 self.run = function () { 854 var k, opts; 855 while (k = next()) { 856 if (isFunction(ctx[k])) { 857 current = k; 858 opts = $.extend(true, {}, defaults[k] || {}, td[k].options || {}); 859 if (k in resolve.latLng) { 860 if (td[k].values) { 861 resolveAllLatLng(td[k].values, ctx, ctx[k], {td: td[k], opts: opts, session: session}); 862 } else { 863 resolveLatLng(ctx, ctx[k], resolve.latLng[k], {td: td[k], opts: opts, session: session}); 864 } 865 } else if (k in resolve.geoloc) { 866 geoloc(ctx, ctx[k], {td: td[k], opts: opts, session: session}); 867 } else { 868 ctx[k].apply(ctx, [{td: td[k], opts: opts, session: session}]); 869 } 870 return; // wait until ack 871 } else { 872 session[k] = null; 873 } 874 } 875 onEnd.apply(ctx, [td, session]); 876 }; 877 878 self.ack = function(result){ 879 session[current] = result; 880 self.run.apply(self, []); 881 }; 882} 883 884function directionsService(){ 885 if (!services.ds) { 886 services.ds = new gm.DirectionsService(); 887 } 888 return services.ds; 889} 890 891function distanceMatrixService() { 892 if (!services.dms) { 893 services.dms = new gm.DistanceMatrixService(); 894 } 895 return services.dms; 896} 897 898function maxZoomService() { 899 if (!services.mzs) { 900 services.mzs = new gm.MaxZoomService(); 901 } 902 return services.mzs; 903} 904 905function elevationService() { 906 if (!services.es) { 907 services.es = new gm.ElevationService(); 908 } 909 return services.es; 910} 911 912 /** 913 * Usefull to get a projection 914 * => done in a function, to let dead-code analyser works without google library loaded 915 **/ 916 function newEmptyOverlay(map, radius) { 917 function Overlay() { 918 var self = this; 919 self.onAdd = function () {}; 920 self.onRemove = function () {}; 921 self.draw = function () {}; 922 return defaults.classes.OverlayView.apply(self, []); 923 } 924 Overlay.prototype = defaults.classes.OverlayView.prototype; 925 var obj = new Overlay(); 926 obj.setMap(map); 927 return obj; 928 } 929 930/** 931 * Class InternalClusterer 932 * This class manage clusters thanks to "td" objects 933 * 934 * Note: 935 * Individuals marker are created on the fly thanks to the td objects, they are 936 * first set to null to keep the indexes synchronised with the td list 937 * This is the "display" function, set by the gmap3 object, which uses theses data 938 * to create markers when clusters are not required 939 * To remove a marker, the objects are deleted and set not null in arrays 940 * markers[key] 941 * = null : marker exist but has not been displayed yet 942 * = false : marker has been removed 943 **/ 944function InternalClusterer($container, map, raw) { 945 var timer, projection, 946 ffilter, fdisplay, ferror, // callback function 947 updating = false, 948 updated = false, 949 redrawing = false, 950 ready = false, 951 enabled = true, 952 self = this, 953 events = [], 954 store = {}, // combin of index (id1-id2-...) => object 955 ids = {}, // unique id => index 956 idxs = {}, // index => unique id 957 markers = [], // index => marker 958 tds = [], // index => td or null if removed 959 values = [], // index => value 960 overlay = newEmptyOverlay(map, raw.radius); 961 962 main(); 963 964 function prepareMarker(index) { 965 if (!markers[index]) { 966 delete tds[index].options.map; 967 markers[index] = new defaults.classes.Marker(tds[index].options); 968 attachEvents($container, {td: tds[index]}, markers[index], tds[index].id); 969 } 970 } 971 972 /** 973 * return a marker by its id, null if not yet displayed and false if no exist or removed 974 **/ 975 self.getById = function (id) { 976 if (id in ids) { 977 prepareMarker(ids[id]); 978 return markers[ids[id]]; 979 } 980 return false;
981 }; 982 983 /** 984 * remove one object from the store 985 **/ 986 self.rm = function (id) { 987 var index = ids[id]; 988 if (markers[index]) { // can be null 989 markers[index].setMap(null); 990 } 991 delete markers[index]; 992 markers[index] = false; 993 994 delete tds[index]; 995 tds[index] = false; 996 997 delete values[index]; 998 values[index] = false; 999 1000 delete ids[id]; 1001 delete idxs[index]; 1002 updated = true; 1003 }; 1004 1005 /** 1006 * remove a marker by its id 1007 **/ 1008 self.clearById = function (id) { 1009 if (id in ids){ 1010 self.rm(id); 1011 return true; 1012 } 1013 }; 1014 1015 /** 1016 * remove objects from the store 1017 **/ 1018 self.clear = function (last, first, tag) { 1019 var start, stop, step, index, i, 1020 list = [], 1021 check = ftag(tag); 1022 if (last) { 1023 start = tds.length - 1; 1024 stop = -1; 1025 step = -1; 1026 } else { 1027 start = 0; 1028 stop = tds.length; 1029 step = 1; 1030 } 1031 for (index = start; index !== stop; index += step) { 1032 if (tds[index]) { 1033 if (!check || check(tds[index].tag)) { 1034 list.push(idxs[index]); 1035 if (first || last) { 1036 break; 1037 } 1038 } 1039 } 1040 } 1041 for (i = 0; i < list.length; i++) { 1042 self.rm(list[i]); 1043 } 1044 }; 1045 1046 // add a "marker td" to the cluster 1047 self.add = function (td, value) { 1048 td.id = globalId(td.id); 1049 self.clearById(td.id); 1050 ids[td.id] = markers.length; 1051 idxs[markers.length] = td.id; 1052 markers.push(null); // null = marker not yet created / displayed 1053 tds.push(td); 1054 values.push(value); 1055 updated = true; 1056 }; 1057 1058 // add a real marker to the cluster 1059 self.addMarker = function (marker, td) { 1060 td = td || {}; 1061 td.id = globalId(td.id); 1062 self.clearById(td.id); 1063 if (!td.options) { 1064 td.options = {}; 1065 } 1066 td.options.position = marker.getPosition(); 1067 attachEvents($container, {td: td}, marker, td.id); 1068 ids[td.id] = markers.length; 1069 idxs[markers.length] = td.id; 1070 markers.push(marker); 1071 tds.push(td); 1072 values.push(td.data || {}); 1073 updated = true; 1074 }; 1075 1076 // return a "marker td" by its index 1077 self.td = function (index) { 1078 return tds[index]; 1079 }; 1080 1081 // return a "marker value" by its index 1082 self.value = function (index) { 1083 return values[index]; 1084 }; 1085 1086 // return a marker by its index 1087 self.marker = function (index) { 1088 if (index in markers) { 1089 prepareMarker(index); 1090 return markers[index]; 1091 } 1092 return false; 1093 }; 1094 1095 // return a marker by its index 1096 self.markerIsSet = function (index) { 1097 return Boolean(markers[index]); 1098 }; 1099 1100 // store a new marker instead if the default "false" 1101 self.setMarker = function (index, marker) { 1102 markers[index] = marker; 1103 }; 1104 1105 // link the visible overlay to the logical data (to hide overlays later) 1106 self.store = function (cluster, obj, shadow) { 1107 store[cluster.ref] = {obj: obj, shadow: shadow}; 1108 }; 1109 1110 // free all objects 1111 self.free = function () { 1112 var i; 1113 for(i = 0; i < events.length; i++) { 1114 gm.event.removeListener(events[i]); 1115 } 1116 events = []; 1117 1118 $.each(store, function (key) { 1119 flush(key); 1120 }); 1121 store = {}; 1122 1123 $.each(tds, function (i) { 1124 tds[i] = null; 1125 }); 1126 tds = []; 1127 1128 $.each(markers, function (i) { 1129 if (markers[i]) { // false = removed 1130 markers[i].setMap(null); 1131 delete markers[i]; 1132 } 1133 }); 1134 markers = []; 1135 1136 $.each(values, function (i) { 1137 delete values[i]; 1138 }); 1139 values = []; 1140 1141 ids = {}; 1142 idxs = {}; 1143 }; 1144 1145 // link the display function 1146 self.filter = function (f) { 1147 ffilter = f; 1148 redraw(); 1149 }; 1150 1151 // enable/disable the clustering feature 1152 self.enable = function (value) { 1153 if (enabled !== value) { 1154 enabled = value; 1155 redraw(); 1156 } 1157 }; 1158 1159 // link the display function 1160 self.display = function (f) { 1161 fdisplay = f; 1162 }; 1163 1164 // link the errorfunction 1165 self.error = function (f) { 1166 ferror = f; 1167 }; 1168 1169 // lock the redraw 1170 self.beginUpdate = function () { 1171 updating = true; 1172 }; 1173 1174 // unlock the redraw 1175 self.endUpdate = function () { 1176 updating = false; 1177 if (updated) { 1178 redraw(); 1179 } 1180 }; 1181 1182 // extends current bounds with internal markers 1183 self.autofit = function (bounds) { 1184 var i; 1185 for (i = 0; i < tds.length; i++) { 1186 if (tds[i]) { 1187 bounds.extend(tds[i].options.position); 1188 } 1189 } 1190 }; 1191 1192 // bind events 1193 function main() { 1194 projection = overlay.getProjection(); 1195 if (!projection) { 1196 setTimeout(function () { main.apply(self, []); }, 25); 1197 return; 1198 } 1199 ready = true; 1200 events.push(gm.event.addListener(map, "zoom_changed", delayRedraw)); 1201 events.push(gm.event.addListener(map, "bounds_changed", delayRedraw)); 1202 redraw(); 1203 } 1204 1205 // flush overlays 1206 function flush(key) { 1207 if (isObject(store[key])) { // is overlay 1208 if (isFunction(store[key].obj.setMap)) { 1209 store[key].obj.setMap(null); 1210 } 1211 if (isFunction(store[key].obj.remove)) { 1212 store[key].obj.remove(); 1213 } 1214 if (isFunction(store[key].shadow.remove)) { 1215 store[key].obj.remove(); 1216 } 1217 if (isFunction(store[key].shadow.setMap)) { 1218 store[key].shadow.setMap(null); 1219 } 1220 delete store[key].obj; 1221 delete store[key].shadow; 1222 } else if (markers[key]) { // marker not removed 1223 markers[key].setMap(null); 1224 // don't remove the marker object, it may be displayed later 1225 } 1226 delete store[key]; 1227 } 1228 1229 /** 1230 * return the distance between 2 latLng couple into meters 1231 * Params : 1232 * Lat1, Lng1, Lat2, Lng2 1233 * LatLng1, Lat2, Lng2 1234 * Lat1, Lng1, LatLng2 1235 * LatLng1, LatLng2 1236 **/ 1237 function distanceInMeter() { 1238 var lat1, lat2, lng1, lng2, e, f, g, h, 1239 cos = Math.cos, 1240 sin = Math.sin, 1241 args = arguments; 1242 if (args[0] instanceof gm.LatLng) { 1243 lat1 = args[0].lat(); 1244 lng1 = args[0].lng(); 1245 if (args[1] instanceof gm.LatLng) { 1246 lat2 = args[1].lat(); 1247 lng2 = args[1].lng(); 1248 } else { 1249 lat2 = args[1]; 1250 lng2 = args[2]; 1251 } 1252 } else { 1253 lat1 = args[0]; 1254 lng1 = args[1]; 1255 if (args[2] instanceof gm.LatLng) { 1256 lat2 = args[2].lat(); 1257 lng2 = args[2].lng(); 1258 } else { 1259 lat2 = args[2]; 1260 lng2 = args[3]; 1261 } 1262 } 1263 e = Math.PI * lat1 / 180; 1264 f = Math.PI * lng1 / 180; 1265 g = Math.PI * lat2 / 180; 1266 h = Math.PI * lng2 / 180; 1267 return 1000 * 6371 * Math.acos(Math.min(cos(e) * cos(g) * cos(f) * cos(h) + cos(e) * sin(f) * cos(g) * sin(h) + sin(e) * sin(g), 1)); 1268 } 1269 1270 // extend the visible bounds 1271 function extendsMapBounds() { 1272 var radius = distanceInMeter(map.getCenter(), map.getBounds().getNorthEast()),
1273 circle = new gm.Circle({ 1274 center: map.getCenter(), 1275 radius: 1.25 * radius // + 25% 1276 }); 1277 return circle.getBounds(); 1278 } 1279 1280 // return an object where keys are store keys 1281 function getStoreKeys() { 1282 var k, 1283 keys = {}; 1284 for (k in store) { 1285 keys[k] = true; 1286 } 1287 return keys; 1288 } 1289 1290 // async the delay function 1291 function delayRedraw() { 1292 clearTimeout(timer); 1293 timer = setTimeout(redraw, 25); 1294 } 1295 1296 // generate bounds extended by radius 1297 function extendsBounds(latLng) { 1298 var p = projection.fromLatLngToDivPixel(latLng), 1299 ne = projection.fromDivPixelToLatLng(new gm.Point(p.x + raw.radius, p.y - raw.radius)), 1300 sw = projection.fromDivPixelToLatLng(new gm.Point(p.x - raw.radius, p.y + raw.radius)); 1301 return new gm.LatLngBounds(sw, ne); 1302 } 1303 1304 // run the clustering process and call the display function 1305 function redraw() { 1306 if (updating || redrawing || !ready) { 1307 return; 1308 } 1309 1310 var i, j, k, indexes, check = false, bounds, cluster, position, previous, lat, lng, loop, 1311 keys = [], 1312 used = {}, 1313 zoom = map.getZoom(), 1314 forceDisabled = ("maxZoom" in raw) && (zoom > raw.maxZoom), 1315 previousKeys = getStoreKeys(); 1316 1317 // reset flag 1318 updated = false; 1319 1320 if (zoom > 3) { 1321 // extend the bounds of the visible map to manage clusters near the boundaries 1322 bounds = extendsMapBounds(); 1323 1324 // check contain only if boundaries are valid
1325 check = bounds.getSouthWest().lng() < bounds.getNorthEast().lng(); 1326 } 1327 1328 // calculate positions of "visibles" markers (in extended bounds) 1329 for (i = 0; i < tds.length; i++) { 1330 if (tds[i] && (!check || bounds.contains(tds[i].options.position)) && (!ffilter || ffilter(values[i]))) { 1331 keys.push(i); 1332 } 1333 } 1334 1335 // for each "visible" marker, search its neighbors to create a cluster 1336 // we can't do a classical "for" loop, because, analysis can bypass a marker while focusing on cluster 1337 while (1) { 1338 i = 0; 1339 while (used[i] && (i < keys.length)) { // look for the next marker not used 1340 i++; 1341 } 1342 if (i === keys.length) { 1343 break; 1344 } 1345 1346 indexes = []; 1347 1348 if (enabled && !forceDisabled) { 1349 loop = 10; 1350 do { 1351 previous = indexes; 1352 indexes = []; 1353 loop--; 1354 1355 if (previous.length) { 1356 position = bounds.getCenter(); 1357 } else { 1358 position = tds[keys[i]].options.position; 1359 } 1360 bounds = extendsBounds(position); 1361 1362 for (j = i; j < keys.length; j++) { 1363 if (used[j]) { 1364 continue; 1365 } 1366 if (bounds.contains(tds[keys[j]].options.position)) { 1367 indexes.push(j); 1368 } 1369 } 1370 } while ((previous.length < indexes.length) && (indexes.length > 1) && loop); 1371 } else { 1372 for (j = i; j < keys.length; j++) { 1373 if (!used[j]) { 1374 indexes.push(j); 1375 break; 1376 } 1377 } 1378 } 1379 1380 cluster = {indexes: [], ref: []}; 1381 lat = lng = 0; 1382 for (k = 0; k < indexes.length; k++) { 1383 used[indexes[k]] = true; 1384 cluster.indexes.push(keys[indexes[k]]); 1385 cluster.ref.push(keys[indexes[k]]); 1386 lat += tds[keys[indexes[k]]].options.position.lat(); 1387 lng += tds[keys[indexes[k]]].options.position.lng(); 1388 } 1389 lat /= indexes.length; 1390 lng /= indexes.length; 1391 cluster.latLng = new gm.LatLng(lat, lng); 1392 1393 cluster.ref = cluster.ref.join("-"); 1394 1395 if (cluster.ref in previousKeys) { // cluster doesn't change 1396 delete previousKeys[cluster.ref]; // remove this entry, these still in this array will be removed 1397 } else { // cluster is new 1398 if (indexes.length === 1) { // alone markers are not stored, so need to keep the key (else, will be displayed every time and marker will blink) 1399 store[cluster.ref] = true; 1400 } 1401 fdisplay(cluster); 1402 } 1403 } 1404 1405 // flush the previous overlays which are not still used 1406 $.each(previousKeys, function (key) { 1407 flush(key); 1408 }); 1409 redrawing = false; 1410 } 1411} 1412/** 1413 * Class Clusterer 1414 * a facade with limited method for external use 1415 **/ 1416function Clusterer(id, internalClusterer) { 1417 var self = this; 1418 self.id = function () { 1419 return id; 1420 }; 1421 self.filter = function (f) { 1422 internalClusterer.filter(f); 1423 }; 1424 self.enable = function () { 1425 internalClusterer.enable(true); 1426 }; 1427 self.disable = function () { 1428 internalClusterer.enable(false); 1429 }; 1430 self.add = function (marker, td, lock) { 1431 if (!lock) { 1432 internalClusterer.beginUpdate(); 1433 } 1434 internalClusterer.addMarker(marker, td); 1435 if (!lock) { 1436 internalClusterer.endUpdate(); 1437 } 1438 }; 1439 self.getById = function (id) { 1440 return internalClusterer.getById(id); 1441 }; 1442 self.clearById = function (id, lock) { 1443 var result; 1444 if (!lock) { 1445 internalClusterer.beginUpdate(); 1446 } 1447 result = internalClusterer.clearById(id); 1448 if (!lock) { 1449 internalClusterer.endUpdate(); 1450 } 1451 return result; 1452 }; 1453 self.clear = function (last, first, tag, lock) { 1454 if (!lock) { 1455 internalClusterer.beginUpdate(); 1456 } 1457 internalClusterer.clear(last, first, tag); 1458 if (!lock) { 1459 internalClusterer.endUpdate(); 1460 } 1461 }; 1462} 1463 1464/** 1465 * Class OverlayView 1466 * @constructor 1467 */ 1468function OverlayView(map, opts, latLng, $div) { 1469 var self = this, 1470 listeners = []; 1471 1472 defaults.classes.OverlayView.call(self); 1473 self.setMap(map); 1474 1475 self.onAdd = function () { 1476 var panes = self.getPanes(); 1477 if (opts.pane in panes) { 1478 $(panes[opts.pane]).append($div); 1479 } 1480 $.each("dblclick click mouseover mousemove mouseout mouseup mousedown".split(" "), function (i, name) { 1481 listeners.push( 1482 gm.event.addDomListener($div[0], name, function (e) { 1483 $.Event(e).stopPropagation(); 1484 gm.event.trigger(self, name, [e]); 1485 self.draw(); 1486 }) 1487 ); 1488 }); 1489 listeners.push( 1490 gm.event.addDomListener($div[0], "contextmenu", function (e) { 1491 $.Event(e).stopPropagation(); 1492 gm.event.trigger(self, "rightclick", [e]); 1493 self.draw(); 1494 }) 1495 ); 1496 }; 1497 1498 self.getPosition = function () { 1499 return latLng; 1500 }; 1501 1502 self.setPosition = function (newLatLng) { 1503 latLng = newLatLng; 1504 self.draw(); 1505 }; 1506 1507 self.draw = function () { 1508 var ps = self.getProjection().fromLatLngToDivPixel(latLng); 1509 $div 1510 .css("left", (ps.x + opts.offset.x) + "px") 1511 .css("top", (ps.y + opts.offset.y) + "px"); 1512 }; 1513 1514 self.onRemove = function () { 1515 var i; 1516 for (i = 0; i < listeners.length; i++) { 1517 gm.event.removeListener(listeners[i]); 1518 } 1519 $div.remove(); 1520 }; 1521 1522 self.hide = function () { 1523 $div.hide(); 1524 }; 1525 1526 self.show = function () { 1527 $div.show(); 1528 }; 1529 1530 self.toggle = function () { 1531 if ($div) { 1532 if ($div.is(":visible")) { 1533 self.show(); 1534 } else { 1535 self.hide(); 1536 } 1537 } 1538 }; 1539 1540 self.toggleDOM = function () { 1541 self.setMap(self.getMap() ? null : map); 1542 }; 1543 1544 self.getDOMElement = function () { 1545 return $div[0]; 1546 }; 1547} 1548 1549function Gmap3($this) { 1550 var self = this, 1551 stack = new Stack(), 1552 store = new Store(), 1553 map = null, 1554 task; 1555 1556 /** 1557 * if not running, start next action in stack 1558 **/ 1559 function run() { 1560 if (!task && (task = stack.get())) { 1561 task.run(); 1562 } 1563 } 1564 1565 /** 1566 * called when action in finished, to acknoledge the current in stack and start next one 1567 **/ 1568 function end() { 1569 task = null; 1570 stack.ack(); 1571 run.call(self); // restart to high level scope 1572 } 1573 1574//-----------------------------------------------------------------------// 1575// Tools 1576//-----------------------------------------------------------------------// 1577 1578 /** 1579 * execute callback functions 1580 **/ 1581 function callback(args) { 1582 var params, 1583 cb = args.td.callback; 1584 if (cb) { 1585 params = Array.prototype.slice.call(arguments, 1); 1586 if (isFunction(cb)) { 1587 cb.apply($this, params); 1588 } else if (isArray(cb)) { 1589 if (isFunction(cb[1])) { 1590 cb[1].apply(cb[0], params); 1591 } 1592 } 1593 } 1594 } 1595 1596 /** 1597 * execute ending functions 1598 **/ 1599 function manageEnd(args, obj, id) { 1600 if (id) { 1601 attachEvents($this, args, obj, id); 1602 } 1603 callback(args, obj); 1604 task.ack(obj); 1605 } 1606 1607 /** 1608 * initialize the map if not yet initialized 1609 **/
1610 function newMap(latLng, args) { 1611 args = args || {}; 1612 var opts = args.td && args.td.options ? args.td.options : 0; 1613 if (map) { 1614 if (opts) { 1615 if (opts.center) { 1616 opts.center = toLatLng(opts.center); 1617 } 1618 map.setOptions(opts); 1619 } 1620 } else { 1621 opts = args.opts || $.extend(true, {}, defaults.map, opts || {}); 1622 opts.center = latLng || toLatLng(opts.center); 1623 map = new defaults.classes.Map($this.get(0), opts); 1624 } 1625 } 1626 1627 /** 1628 * store actions to execute in a stack manager 1629 **/ 1630 self._plan = function (list) { 1631 var k; 1632 for (k = 0; k < list.length; k++) { 1633 stack.add(new Task(self, end, list[k])); 1634 } 1635 run(); 1636 }; 1637 1638 /** 1639 * Initialize gm.Map object 1640 **/ 1641 self.map = function (args) { 1642 newMap(args.latLng, args); 1643 attachEvents($this, args, map); 1644 manageEnd(args, map); 1645 }; 1646 1647 /** 1648 * destroy an existing instance 1649 **/ 1650 self.destroy = function (args) { 1651 store.clear(); 1652 $this.empty(); 1653 if (map) { 1654 map = null; 1655 } 1656 manageEnd(args, true); 1657 }; 1658 1659 /** 1660 * add an overlay 1661 **/ 1662 self.overlay = function (args, internal) { 1663 var objs = [], 1664 multiple = "values" in args.td; 1665 if (!multiple) { 1666 args.td.values = [{latLng: args.latLng, options: args.opts}]; 1667 } 1668 if (!args.td.values.length) { 1669 manageEnd(args, false); 1670 return; 1671 } 1672 if (!OverlayView.__initialised) { 1673 OverlayView.prototype = new defaults.classes.OverlayView(); 1674 OverlayView.__initialised = true; 1675 } 1676 $.each(args.td.values, function (i, value) { 1677 var id, obj, td = tuple(args, value), 1678 $div = $(document.createElement("div")).css({ 1679 border: "none", 1680 borderWidth: 0, 1681 position: "absolute" 1682 }); 1683 $div.append(td.options.content); 1684 obj = new OverlayView(map, td.options, toLatLng(td) || toLatLng(value), $div); 1685 objs.push(obj); 1686 $div = null; // memory leak 1687 if (!internal) { 1688 id = store.add(args, "overlay", obj); 1689 attachEvents($this, {td: td}, obj, id); 1690 } 1691 }); 1692 if (internal) { 1693 return objs[0]; 1694 } 1695 manageEnd(args, multiple ? objs : objs[0]); 1696 }; 1697 1698 /** 1699 * Create an InternalClusterer object 1700 **/ 1701 function createClusterer(raw) { 1702 var internalClusterer = new InternalClusterer($this, map, raw), 1703 td = {}, 1704 styles = {}, 1705 thresholds = [], 1706 isInt = /^[0-9]+$/, 1707 calculator, 1708 k; 1709 1710 for (k in raw) { 1711 if (isInt.test(k)) { 1712 thresholds.push(1 * k); // cast to int 1713 styles[k] = raw[k]; 1714 styles[k].width = styles[k].width || 0; 1715 styles[k].height = styles[k].height || 0; 1716 } else { 1717 td[k] = raw[k]; 1718 } 1719 } 1720 thresholds.sort(function (a, b) { return a > b; }); 1721 1722 // external calculator 1723 if (td.calculator) { 1724 calculator = function (indexes) { 1725 var data = []; 1726 $.each(indexes, function (i, index) { 1727 data.push(internalClusterer.value(index)); 1728 }); 1729 return td.calculator.apply($this, [data]); 1730 }; 1731 } else { 1732 calculator = function (indexes) { 1733 return indexes.length; 1734 }; 1735 } 1736 1737 // set error function 1738 internalClusterer.error(function () { 1739 error.apply(self, arguments); 1740 }); 1741 1742 // set display function 1743 internalClusterer.display(function (cluster) { 1744 var i, style, atd, obj, offset, shadow, 1745 cnt = calculator(cluster.indexes); 1746 1747 // look for the style to use 1748 if (raw.force || cnt > 1) { 1749 for (i = 0; i < thresholds.length; i++) { 1750 if (thresholds[i] <= cnt) { 1751 style = styles[thresholds[i]]; 1752 } 1753 } 1754 } 1755 1756 if (style) { 1757 offset = style.offset || [-style.width/2, -style.height/2]; 1758 // create a custom overlay command 1759 // nb: 2 extends are faster self a deeper extend 1760 atd = $.extend({}, td); 1761 atd.options = $.extend({ 1762 pane: "overlayLayer", 1763 content: style.content ? style.content.replace("CLUSTER_COUNT", cnt) : "", 1764 offset: { 1765 x: ("x" in offset ? offset.x : offset[0]) || 0, 1766 y: ("y" in offset ? offset.y : offset[1]) || 0 1767 } 1768 }, 1769 td.options || {}); 1770 1771 obj = self.overlay({td: atd, opts: atd.options, latLng: toLatLng(cluster)}, true); 1772 1773 atd.options.pane = "floatShadow"; 1774 atd.options.content = $(document.createElement("div")).width(style.width + "px").height(style.height + "px").css({cursor: "pointer"}); 1775 shadow = self.overlay({td: atd, opts: atd.options, latLng: toLatLng(cluster)}, true); 1776 1777 // store data to the clusterer 1778 td.data = { 1779 latLng: toLatLng(cluster), 1780 markers:[] 1781 }; 1782 $.each(cluster.indexes, function(i, index){ 1783 td.data.markers.push(internalClusterer.value(index)); 1784 if (internalClusterer.markerIsSet(index)){ 1785 internalClusterer.marker(index).setMap(null); 1786 } 1787 }); 1788 attachEvents($this, {td: td}, shadow, undef, {main: obj, shadow: shadow}); 1789 internalClusterer.store(cluster, obj, shadow); 1790 } else { 1791 $.each(cluster.indexes, function (i, index) {
1792 internalClusterer.marker(index).setMap(map); 1793 }); 1794 } 1795 }); 1796 1797 return internalClusterer; 1798 } 1799 1800 /** 1801 * add a marker 1802 **/ 1803 self.marker = function (args) { 1804 var objs, 1805 clusterer, internalClusterer, 1806 multiple = "values" in args.td, 1807 init = !map; 1808 if (!multiple) { 1809 args.opts.position = args.latLng || toLatLng(args.opts.position); 1810 args.td.values = [{options: args.opts}]; 1811 } 1812 if (!args.td.values.length) { 1813 manageEnd(args, false); 1814 return; 1815 } 1816 if (init) { 1817 newMap(); 1818 } 1819 if (args.td.cluster && !map.getBounds()) { // map not initialised => bounds not available : wait for map if clustering feature is required 1820 gm.event.addListenerOnce(map, "bounds_changed", function () { self.marker.apply(self, [args]); }); 1821 return; 1822 } 1823 if (args.td.cluster) { 1824 if (args.td.cluster instanceof Clusterer) { 1825 clusterer = args.td.cluster; 1826 internalClusterer = store.getById(clusterer.id(), true); 1827 } else { 1828 internalClusterer = createClusterer(args.td.cluster); 1829 clusterer = new Clusterer(globalId(args.td.id, true), internalClusterer); 1830 store.add(args, "clusterer", clusterer, internalClusterer); 1831 } 1832 internalClusterer.beginUpdate(); 1833 1834 $.each(args.td.values, function (i, value) { 1835 var td = tuple(args, value); 1836 td.options.position = td.options.position ? toLatLng(td.options.position) : toLatLng(value); 1837 if (td.options.position) { 1838 td.options.map = map; 1839 if (init) { 1840 map.setCenter(td.options.position); 1841 init = false; 1842 } 1843 internalClusterer.add(td, value); 1844 } 1845 }); 1846 1847 internalClusterer.endUpdate(); 1848 manageEnd(args, clusterer); 1849 1850 } else { 1851 objs = []; 1852 $.each(args.td.values, function (i, value) { 1853 var id, obj, 1854 td = tuple(args, value); 1855 td.options.position = td.options.position ? toLatLng(td.options.position) : toLatLng(value); 1856 if (td.options.position) { 1857 td.options.map = map; 1858 if (init) { 1859 map.setCenter(td.options.position); 1860 init = false; 1861 } 1862 obj = new defaults.classes.Marker(td.options); 1863 objs.push(obj); 1864 id = store.add({td: td}, "marker", obj); 1865 attachEvents($this, {td: td}, obj, id); 1866 } 1867 }); 1868 manageEnd(args, multiple ? objs : objs[0]); 1869 } 1870 }; 1871 1872 /** 1873 * return a route 1874 **/ 1875 self.getroute = function (args) { 1876 args.opts.origin = toLatLng(args.opts.origin, true); 1877 args.opts.destination = toLatLng(args.opts.destination, true); 1878 directionsService().route( 1879 args.opts, 1880 function (results, status) { 1881 callback(args, status === gm.DirectionsStatus.OK ? results : false, status); 1882 task.ack(); 1883 } 1884 ); 1885 }; 1886 1887 /** 1888 * return the distance between an origin and a destination 1889 * 1890 **/ 1891 self.getdistance = function (args) { 1892 var i; 1893 args.opts.origins = array(args.opts.origins); 1894 for (i = 0; i < args.opts.origins.length; i++) { 1895 args.opts.origins[i] = toLatLng(args.opts.origins[i], true); 1896 } 1897 args.opts.destinations = array(args.opts.destinations); 1898 for (i = 0; i < args.opts.destinations.length; i++) { 1899 args.opts.destinations[i] = toLatLng(args.opts.destinations[i], true); 1900 } 1901 distanceMatrixService().getDistanceMatrix( 1902 args.opts, 1903 function (results, status) { 1904 callback(args, status === gm.DistanceMatrixStatus.OK ? results : false, status); 1905 task.ack(); 1906 } 1907 ); 1908 }; 1909 1910 /** 1911 * add an infowindow 1912 **/ 1913 self.infowindow = function (args) { 1914 var objs = [], 1915 multiple = "values" in args.td; 1916 if (!multiple) { 1917 if (args.latLng) { 1918 args.opts.position = args.latLng; 1919 } 1920 args.td.values = [{options: args.opts}]; 1921 } 1922 $.each(args.td.values, function (i, value) { 1923 var id, obj, 1924 td = tuple(args, value); 1925 td.options.position = td.options.position ? toLatLng(td.options.position) : toLatLng(value.latLng); 1926 if (!map) { 1927 newMap(td.options.position); 1928 } 1929 obj = new defaults.classes.InfoWindow(td.options);
1930 if (obj && (isUndefined(td.open) || td.open)) { 1931 if (multiple) { 1932 obj.open(map, td.anchor || undef); 1933 } else { 1934 obj.open(map, td.anchor || (args.latLng ? undef : (args.session.marker ? args.session.marker : undef))); 1935 } 1936 } 1937 objs.push(obj); 1938 id = store.add({td: td}, "infowindow", obj); 1939 attachEvents($this, {td: td}, obj, id); 1940 }); 1941 manageEnd(args, multiple ? objs : objs[0]); 1942 }; 1943 1944 /** 1945 * add a circle 1946 **/ 1947 self.circle = function (args) { 1948 var objs = [], 1949 multiple = "values" in args.td; 1950 if (!multiple) { 1951 args.opts.center = args.latLng || toLatLng(args.opts.center); 1952 args.td.values = [{options: args.opts}]; 1953 } 1954 if (!args.td.values.length) { 1955 manageEnd(args, false); 1956 return; 1957 } 1958 $.each(args.td.values, function (i, value) { 1959 var id, obj, 1960 td = tuple(args, value); 1961 td.options.center = td.options.center ? toLatLng(td.options.center) : toLatLng(value); 1962 if (!map) { 1963 newMap(td.options.center); 1964 } 1965 td.options.map = map; 1966 obj = new defaults.classes.Circle(td.options); 1967 objs.push(obj); 1968 id = store.add({td: td}, "circle", obj); 1969 attachEvents($this, {td: td}, obj, id); 1970 }); 1971 manageEnd(args, multiple ? objs : objs[0]); 1972 }; 1973 1974 /** 1975 * returns address structure from latlng 1976 **/ 1977 self.getaddress = function (args) { 1978 callback(args, args.results, args.status); 1979 task.ack(); 1980 }; 1981 1982 /** 1983 * returns latlng from an address 1984 **/ 1985 self.getlatlng = function (args) { 1986 callback(args, args.results, args.status); 1987 task.ack(); 1988 }; 1989 1990 /** 1991 * return the max zoom of a location 1992 **/ 1993 self.getmaxzoom = function (args) { 1994 maxZoomService().getMaxZoomAtLatLng( 1995 args.latLng, 1996 function (result) { 1997 callback(args, result.status === gm.MaxZoomStatus.OK ? result.zoom : false, status); 1998 task.ack(); 1999 } 2000 ); 2001 }; 2002 2003 /** 2004 * return the elevation of a location 2005 **/ 2006 self.getelevation = function (args) { 2007 var i, 2008 locations = [], 2009 f = function (results, status) { 2010 callback(args, status === gm.ElevationStatus.OK ? results : false, status); 2011 task.ack(); 2012 }; 2013 2014 if (args.latLng) { 2015 locations.push(args.latLng); 2016 } else { 2017 locations = array(args.td.locations || []); 2018 for (i = 0; i < locations.length; i++) { 2019 locations[i] = toLatLng(locations[i]); 2020 } 2021 } 2022 if (locations.length) { 2023 elevationService().getElevationForLocations({locations: locations}, f); 2024 } else { 2025 if (args.td.path && args.td.path.length) { 2026 for (i = 0; i < args.td.path.length; i++) {
2027 locations.push(toLatLng(args.td.path[i])); 2028 } 2029 } 2030 if (locations.length) { 2031 elevationService().getElevationAlongPath({path: locations, samples:args.td.samples}, f); 2032 } else { 2033 task.ack(); 2034 } 2035 } 2036 }; 2037 2038 /** 2039 * define defaults values 2040 **/ 2041 self.defaults = function (args) { 2042 $.each(args.td, function(name, value) { 2043 if (isObject(defaults[name])) { 2044 defaults[name] = $.extend({}, defaults[name], value); 2045 } else { 2046 defaults[name] = value; 2047 } 2048 }); 2049 task.ack(true); 2050 }; 2051 2052 /** 2053 * add a rectangle 2054 **/ 2055 self.rectangle = function (args) { 2056 var objs = [], 2057 multiple = "values" in args.td; 2058 if (!multiple) { 2059 args.td.values = [{options: args.opts}]; 2060 } 2061 if (!args.td.values.length) { 2062 manageEnd(args, false); 2063 return; 2064 } 2065 $.each(args.td.values, function (i, value) { 2066 var id, obj, 2067 td = tuple(args, value); 2068 td.options.bounds = td.options.bounds ? toLatLngBounds(td.options.bounds) : toLatLngBounds(value); 2069 if (!map) { 2070 newMap(td.options.bounds.getCenter()); 2071 } 2072 td.options.map = map; 2073 2074 obj = new defaults.classes.Rectangle(td.options); 2075 objs.push(obj); 2076 id = store.add({td: td}, "rectangle", obj); 2077 attachEvents($this, {td: td}, obj, id); 2078 }); 2079 manageEnd(args, multiple ? objs : objs[0]); 2080 }; 2081 2082 /** 2083 * add a polygone / polyline 2084 **/ 2085 function poly(args, poly, path) { 2086 var objs = [], 2087 multiple = "values" in args.td; 2088 if (!multiple) { 2089 args.td.values = [{options: args.opts}]; 2090 } 2091 if (!args.td.values.length) { 2092 manageEnd(args, false); 2093 return; 2094 } 2095 newMap(); 2096 $.each(args.td.values, function (_, value) { 2097 var id, i, j, obj, 2098 td = tuple(args, value); 2099 if (td.options[path]) { 2100 if (td.options[path][0][0] && isArray(td.options[path][0][0])) { 2101 for (i = 0; i < td.options[path].length; i++) { 2102 for (j = 0; j < td.options[path][i].length; j++) { 2103 td.options[path][i][j] = toLatLng(td.options[path][i][j]); 2104 } 2105 } 2106 } else { 2107 for (i = 0; i < td.options[path].length; i++) { 2108 td.options[path][i] = toLatLng(td.options[path][i]); 2109 } 2110 } 2111 } 2112 td.options.map = map; 2113 obj = new gm[poly](td.options); 2114 objs.push(obj); 2115 id = store.add({td: td}, poly.toLowerCase(), obj); 2116 attachEvents($this, {td: td}, obj, id); 2117 }); 2118 manageEnd(args, multiple ? objs : objs[0]); 2119 } 2120 2121 self.polyline = function (args) { 2122 poly(args, "Polyline", "path"); 2123 }; 2124 2125 self.polygon = function (args) { 2126 poly(args, "Polygon", "paths"); 2127 }; 2128 2129 /** 2130 * add a traffic layer 2131 **/ 2132 self.trafficlayer = function (args) { 2133 newMap(); 2134 var obj = store.get("trafficlayer"); 2135 if (!obj) { 2136 obj = new defaults.classes.TrafficLayer(); 2137 obj.setMap(map); 2138 store.add(args, "trafficlayer", obj); 2139 } 2140 manageEnd(args, obj); 2141 }; 2142 2143 /** 2144 * add a bicycling layer 2145 **/ 2146 self.bicyclinglayer = function (args) { 2147 newMap(); 2148 var obj = store.get("bicyclinglayer"); 2149 if (!obj) { 2150 obj = new defaults.classes.BicyclingLayer(); 2151 obj.setMap(map); 2152 store.add(args, "bicyclinglayer", obj); 2153 } 2154 manageEnd(args, obj); 2155 }; 2156 2157 /** 2158 * add a ground overlay 2159 **/ 2160 self.groundoverlay = function (args) { 2161 args.opts.bounds = toLatLngBounds(args.opts.bounds); 2162 if (args.opts.bounds){ 2163 newMap(args.opts.bounds.getCenter()); 2164 } 2165 var id, 2166 obj = new defaults.classes.GroundOverlay(args.opts.url, args.opts.bounds, args.opts.opts); 2167 obj.setMap(map); 2168 id = store.add(args, "groundoverlay", obj); 2169 manageEnd(args, obj, id); 2170 }; 2171 2172 /** 2173 * set a streetview 2174 **/ 2175 self.streetviewpanorama = function (args) { 2176 if (!args.opts.opts) { 2177 args.opts.opts = {}; 2178 } 2179 if (args.latLng) { 2180 args.opts.opts.position = args.latLng; 2181 } else if (args.opts.opts.position) { 2182 args.opts.opts.position = toLatLng(args.opts.opts.position); 2183 } 2184 if (args.td.divId) { 2185 args.opts.container = document.getElementById(args.td.divId); 2186 } else if (args.opts.container) { 2187 args.opts.container = $(args.opts.container).get(0); 2188 } 2189 var id, obj = new defaults.classes.StreetViewPanorama(args.opts.container, args.opts.opts); 2190 if (obj) { 2191 map.setStreetView(obj); 2192 } 2193 id = store.add(args, "streetviewpanorama", obj); 2194 manageEnd(args, obj, id); 2195 }; 2196 2197 self.kmllayer = function (args) { 2198 var objs = [], 2199 multiple = "values" in args.td; 2200 if (!multiple) { 2201 args.td.values = [{options: args.opts}]; 2202 } 2203 if (!args.td.values.length) { 2204 manageEnd(args, false); 2205 return; 2206 } 2207 $.each(args.td.values, function (i, value) { 2208 var id, obj, options, 2209 td = tuple(args, value); 2210 if (!map) { 2211 newMap(); 2212 } 2213 options = td.options; 2214 // compatibility 5.0- 2215 if (td.options.opts) { 2216 options = td.options.opts; 2217 if (td.options.url) { 2218 options.url = td.options.url; 2219 } 2220 } 2221 // -- end -- 2222 options.map = map; 2223 if (googleVersionMin("3.10")) { 2224 obj = new defaults.classes.KmlLayer(options); 2225 } else { 2226 obj = new defaults.classes.KmlLayer(options.url, options); 2227 } 2228 objs.push(obj); 2229 id = store.add({td: td}, "kmllayer", obj); 2230 attachEvents($this, {td: td}, obj, id); 2231 }); 2232 manageEnd(args, multiple ? objs : objs[0]); 2233 }; 2234 2235 /** 2236 * add a fix panel 2237 **/ 2238 self.panel = function (args) { 2239 newMap(); 2240 var id, $content, 2241 x = 0, 2242 y = 0, 2243 $div = $(document.createElement("div")); 2244 2245 $div.css({ 2246 position: "absolute", 2247 zIndex: 1000, 2248 visibility: "hidden" 2249 }); 2250 2251 if (args.opts.content) { 2252 $content = $(args.opts.content); 2253 $div.append($content); 2254 $this.first().prepend($div); 2255 2256 if (!isUndefined(args.opts.left)) { 2257 x = args.opts.left; 2258 } else if (!isUndefined(args.opts.right)) { 2259 x = $this.width() - $content.width() - args.opts.right; 2260 } else if (args.opts.center) { 2261 x = ($this.width() - $content.width()) / 2; 2262 } 2263 2264 if (!isUndefined(args.opts.top)) { 2265 y = args.opts.top; 2266 } else if (!isUndefined(args.opts.bottom)) { 2267 y = $this.height() - $content.height() - args.opts.bottom; 2268 } else if (args.opts.middle) { 2269 y = ($this.height() - $content.height()) / 2 2270 } 2271 2272 $div.css({ 2273 top: y, 2274 left: x, 2275 visibility: "visible" 2276 }); 2277 } 2278 2279 id = store.add(args, "panel", $div); 2280 manageEnd(args, $div, id); 2281 $div = null; // memory leak 2282 }; 2283 2284 /** 2285 * add a direction renderer 2286 **/ 2287 self.directionsrenderer = function (args) { 2288 args.opts.map = map; 2289 var id, 2290 obj = new gm.DirectionsRenderer(args.opts); 2291 if (args.td.divId) { 2292 obj.setPanel(document.getElementById(args.td.divId)); 2293 } else if (args.td.container) { 2294 obj.setPanel($(args.td.container).get(0)); 2295 } 2296 id = store.add(args, "directionsrenderer", obj); 2297 manageEnd(args, obj, id); 2298 }; 2299 2300 /** 2301 * returns latLng of the user 2302 **/ 2303 self.getgeoloc = function (args) { 2304 manageEnd(args, args.latLng); 2305 }; 2306 2307 /** 2308 * add a style 2309 **/ 2310 self.styledmaptype = function (args) { 2311 newMap(); 2312 var obj = new defaults.classes.StyledMapType(args.td.styles, args.opts); 2313 map.mapTypes.set(args.td.id, obj); 2314 manageEnd(args, obj); 2315 }; 2316 2317 /** 2318 * add an imageMapType 2319 **/ 2320 self.imagemaptype = function (args) { 2321 newMap(); 2322 var obj = new defaults.classes.ImageMapType(args.opts); 2323 map.mapTypes.set(args.td.id, obj); 2324 manageEnd(args, obj); 2325 }; 2326 2327 /** 2328 * autofit a map using its overlays (markers, rectangles ...) 2329 **/ 2330 self.autofit = function (args) { 2331 var bounds = new gm.LatLngBounds(); 2332 $.each(store.all(), function (i, obj) { 2333 if (obj.getPosition) { 2334 bounds.extend(obj.getPosition()); 2335 } else if (obj.getBounds) { 2336 bounds.extend(obj.getBounds().getNorthEast());
2337 bounds.extend(obj.getBounds().getSouthWest()); 2338 } else if (obj.getPaths) { 2339 obj.getPaths().forEach(function (path) { 2340 path.forEach(function (latLng) { 2341 bounds.extend(latLng); 2342 }); 2343 }); 2344 } else if (obj.getPath) { 2345 obj.getPath().forEach(function (latLng) { 2346 bounds.extend(latLng); 2347 }); 2348 } else if (obj.getCenter) { 2349 bounds.extend(obj.getCenter()); 2350 } else if (typeof Clusterer === "function" && obj instanceof Clusterer) { 2351 obj = store.getById(obj.id(), true); 2352 if (obj) { 2353 obj.autofit(bounds); 2354 } 2355 } 2356 }); 2357 2358 if (!bounds.isEmpty() && (!map.getBounds() || !map.getBounds().equals(bounds))) { 2359 if ("maxZoom" in args.td) { 2360 // fitBouds Callback event => detect zoom level and check maxZoom 2361 gm.event.addListenerOnce( 2362 map, 2363 "bounds_changed", 2364 function () { 2365 if (this.getZoom() > args.td.maxZoom) { 2366 this.setZoom(args.td.maxZoom); 2367 } 2368 } 2369 ); 2370 } 2371 map.fitBounds(bounds); 2372 } 2373 manageEnd(args, true); 2374 }; 2375 2376 /** 2377 * remove objects from a map 2378 **/ 2379 self.clear = function (args) { 2380 if (isString(args.td)) { 2381 if (store.clearById(args.td) || store.objClearById(args.td)) { 2382 manageEnd(args, true); 2383 return; 2384 } 2385 args.td = {name: args.td}; 2386 } 2387 if (args.td.id) { 2388 $.each(array(args.td.id), function (i, id) { 2389 store.clearById(id) || store.objClearById(id); 2390 }); 2391 } else { 2392 store.clear(array(args.td.name), args.td.last, args.td.first, args.td.tag); 2393 store.objClear(array(args.td.name), args.td.last, args.td.first, args.td.tag); 2394 } 2395 manageEnd(args, true); 2396 }; 2397 2398 /** 2399 * return objects previously created 2400 **/ 2401 self.get = function (args, direct, full) { 2402 var name, res, 2403 td = direct ? args : args.td; 2404 if (!direct) { 2405 full = td.full; 2406 } 2407 if (isString(td)) { 2408 res = store.getById(td, false, full) || store.objGetById(td); 2409 if (res === false) { 2410 name = td; 2411 td = {}; 2412 } 2413 } else { 2414 name = td.name; 2415 } 2416 if (name === "map") { 2417 res = map; 2418 } 2419 if (!res) { 2420 res = []; 2421 if (td.id) { 2422 $.each(array(td.id), function (i, id) { 2423 res.push(store.getById(id, false, full) || store.objGetById(id)); 2424 }); 2425 if (!isArray(td.id)) { 2426 res = res[0]; 2427 } 2428 } else { 2429 $.each(name ? array(name) : [undef], function (i, aName) { 2430 var result; 2431 if (td.first) { 2432 result = store.get(aName, false, td.tag, full); 2433 if (result) { 2434 res.push(result); 2435 } 2436 } else if (td.all) { 2437 $.each(store.all(aName, td.tag, full), function (i, result) { 2438 res.push(result); 2439 }); 2440 } else { 2441 result = store.get(aName, true, td.tag, full); 2442 if (result) { 2443 res.push(result); 2444 } 2445 } 2446 }); 2447 if (!td.all && !isArray(name)) { 2448 res = res[0]; 2449 } 2450 } 2451 } 2452 res = isArray(res) || !td.all ? res : [res]; 2453 if (direct) { 2454 return res; 2455 } else { 2456 manageEnd(args, res); 2457 } 2458 }; 2459 2460 /** 2461 * run a function on each items selected 2462 **/ 2463 self.exec = function (args) { 2464 $.each(array(args.td.func), function (i, func) { 2465 $.each(self.get(args.td, true, args.td.hasOwnProperty("full") ? args.td.full : true), function (j, res) { 2466 func.call($this, res); 2467 }); 2468 }); 2469 manageEnd(args, true); 2470 }; 2471 2472 /** 2473 * trigger events on the map 2474 **/ 2475 self.trigger = function (args) { 2476 if (isString(args.td)) { 2477 gm.event.trigger(map, args.td); 2478 } else { 2479 var options = [map, args.td.eventName]; 2480 if (args.td.var_args) { 2481 $.each(args.td.var_args, function (i, v) { 2482 options.push(v); 2483 }); 2484 } 2485 gm.event.trigger.apply(gm.event, options); 2486 } 2487 callback(args); 2488 task.ack(); 2489 }; 2490} 2491 2492$.fn.gmap3 = function () { 2493 var i, 2494 list = [], 2495 empty = true, 2496 results = []; 2497 2498 // init library 2499 initDefaults(); 2500 2501 // store all arguments in a td list 2502 for (i = 0; i < arguments.length; i++) { 2503 if (arguments[i]) { 2504 list.push(arguments[i]); 2505 } 2506 } 2507 2508 // resolve empty call - run init 2509 if (!list.length) { 2510 list.push("map"); 2511 } 2512 2513 // loop on each jQuery object 2514 $.each(this, function () { 2515 var $this = $(this), 2516 gmap3 = $this.data("gmap3"); 2517 empty = false;
2518 if (!gmap3) { 2519 gmap3 = new Gmap3($this); 2520 $this.data("gmap3", gmap3); 2521 } 2522 if (list.length === 1 && (list[0] === "get" || isDirectGet(list[0]))) { 2523 if (list[0] === "get") { 2524 results.push(gmap3.get("map", true)); 2525 } else { 2526 results.push(gmap3.get(list[0].get, true, list[0].get.full)); 2527 } 2528 } else { 2529 gmap3._plan(list); 2530 } 2531 }); 2532 2533 // return for direct call only 2534 if (results.length) { 2535 if (results.length === 1) { // 1 css selector 2536 return results[0]; 2537 } 2538 return results; 2539 } 2540 2541 return this; 2542}; 2543})(jQuery);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.