1/** 2 * Copyright (c) 2010 The Chromium Authors. All rights reserved. Use of this 3 * source code is governed by a BSD-style license that can be found in the 4 * LICENSE file. 5 */ 6 7/** 8 * PHASES 9 * 1) Load next event from server refresh every 30 minutes or every time 10 * you go to calendar or every time you logout drop in a data object. 11 * 2) Display on screen periodically once per minute or on demand. 12 */ 13 14// Message shown in badge title when no title is given to an event. 15var MSG_NO_TITLE = chrome.i18n.getMessage('noTitle'); 16 17// Time between server polls = 30 minutes. 18var POLL_INTERVAL = 30 * 60 * 1000; 19 20// Redraw interval is 1 min. 21var DRAW_INTERVAL = 60 * 1000; 22 23// The time when we last polled. 24var lastPollTime_ = 0; 25 26// Object for BadgeAnimation 27var badgeAnimation_; 28 29//Object for CanvasAnimation 30var canvasAnimation_; 31 32// Object containing the event. 33var nextEvent_ = null; 34 35// Storing events. 36var eventList = []; 37var nextEvents = []; 38 39// Storing calendars. 40var calendars = []; 41 42var pollUnderProgress = false; 43var defaultAuthor = ''; 44var isMultiCalendar = false; 45 46//URL for getting feed of individual calendar support. 47var SINGLE_CALENDAR_SUPPORT_URL = 'https://www.google.com/calendar/feeds' + 48 '/default/private/embed?toolbar=true&max-results=10'; 49 50//URL for getting feed of multiple calendar support. 51var MULTIPLE_CALENDAR_SUPPORT_URL = 'https://www.google.com/calendar/feeds' + 52 '/default/allcalendars/full'; 53 54//URL for opening Google Calendar in new tab. 55var GOOGLE_CALENDAR_URL = 'http://www.google.com/calendar/render'; 56 57//URL for declining invitation of the event. 58var DECLINED_URL = 'http://schemas.google.com/g/2005#event.declined'; 59 60//This is used to poll only once per second at most, and delay that if 61//we keep hitting pages that would otherwise force a load. 62var pendingLoadId_ = null; 63 64/** 65 * A "loading" animation displayed while we wait for the first response from 66 * Calendar. This animates the badge text with a dot that cycles from left to 67 * right. 68 * @constructor 69 */ 70function BadgeAnimation() { 71 this.timerId_ = 0; 72 this.maxCount_ = 8; // Total number of states in animation 73 this.current_ = 0; // Current state 74 this.maxDot_ = 4; // Max number of dots in animation 75}; 76 77/** 78 * Paints the badge text area while loading the data. 79 */ 80BadgeAnimation.prototype.paintFrame = function() { 81 var text = ''; 82 for (var i = 0; i < this.maxDot_; i++) { 83 text += (i == this.current_) ? '.' : ' '; 84 } 85 86 chrome.browserAction.setBadgeText({text: text}); 87 this.current_++; 88 if (this.current_ == this.maxCount_) { 89 this.current_ = 0; 90 } 91}; 92 93/** 94 * Starts the animation process. 95 */ 96BadgeAnimation.prototype.start = function() { 97 if (this.timerId_) { 98 return; 99 } 100 101 var self = this; 102 this.timerId_ = window.setInterval(function() { 103 self.paintFrame(); 104 }, 100); 105}; 106 107/** 108 * Stops the animation process. 109 */ 110BadgeAnimation.prototype.stop = function() { 111 if (!this.timerId_) { 112 return; 113 } 114 115 window.clearInterval(this.timerId_); 116 this.timerId_ = 0; 117}; 118 119/** 120 * Animates the canvas after loading the data from all the calendars. It 121 * rotates the icon and defines the badge text and title. 122 * @constructor 123 */ 124function CanvasAnimation() { 125 this.animationFrames_ = 36; // The number of animation frames 126 this.animationSpeed_ = 10; // Time between each frame(in ms). 127 this.canvas_ = $('canvas'); // The canvas width + height. 128 this.canvasContext_ = this.canvas_.getContext('2d'); // Canvas context. 129 this.loggedInImage_ = $('logged_in'); 130 this.rotation_ = 0; //Keeps count of rotation angle of extension icon. 131 this.w = this.canvas_.width; // Setting canvas width. 132 this.h = this.canvas_.height; // Setting canvas height. 133 this.RED = [208, 0, 24, 255]; //Badge color of extension icon in RGB format. 134 this.BLUE = [0, 24, 208, 255]; 135 this.currentBadge_ = null; // The text in the current badge. 136}; 137 138/** 139 * Flips the icon around and draws it. 140 */ 141CanvasAnimation.prototype.animate = function() { 142 this.rotation_ += (1 / this.animationFrames_); 143 this.drawIconAtRotation(); 144 var self = this; 145 if (this.rotation_ <= 1) { 146 setTimeout(function() { 147 self.animate(); 148 }, self.animationSpeed_); 149 } else { 150 this.drawFinal(); 151 } 152}; 153 154/** 155 * Renders the icon. 156 */ 157CanvasAnimation.prototype.drawIconAtRotation = function() { 158 this.canvasContext_.save(); 159 this.canvasContext_.clearRect(0, 0, this.w, this.h); 160 this.canvasContext_.translate(Math.ceil(this.w / 2), Math.ceil(this.h / 2)); 161 this.canvasContext_.rotate(2 * Math.PI * this.getSector(this.rotation_)); 162 this.canvasContext_.drawImage(this.loggedInImage_, -Math.ceil(this.w / 2), 163 -Math.ceil(this.h / 2)); 164 this.canvasContext_.restore(); 165 chrome.browserAction.setIcon( 166 {imageData: this.canvasContext_.getImageData(0, 0, this.w, this.h)}); 167}; 168 169/** 170 * Calculates the sector which has to be traversed in a single call of animate 171 * function(360/animationFrames_ = 360/36 = 10 radians). 172 * @param {integer} sector angle to be rotated(in radians). 173 * @return {integer} value in radian of the sector which it has to cover. 174 */ 175CanvasAnimation.prototype.getSector = function(sector) { 176 return (1 - Math.sin(Math.PI / 2 + sector * Math.PI)) / 2; 177}; 178 179/** 180 * Draws the event icon and determines the badge title and icon title. 181 */ 182CanvasAnimation.prototype.drawFinal = function() { 183 badgeAnimation_.stop(); 184 185 if (!nextEvent_) { 186 this.showLoggedOut(); 187 } else { 188 this.drawIconAtRotation(); 189 this.rotation_ = 0; 190 191 var ms = nextEvent_.startTime.getTime() - getCurrentTime(); 192 var nextEventMin = ms / (1000 * 60); 193 var bgColor = (nextEventMin < 60) ? this.RED : this.BLUE; 194 195 chrome.browserAction.setBadgeBackgroundColor({color: bgColor}); 196 currentBadge_ = this.getBadgeText(nextEvent_); 197 chrome.browserAction.setBadgeText({text: currentBadge_}); 198 199 if (nextEvents.length > 0) { 200 var text = ''; 201 for (var i = 0, event; event = nextEvents[i]; i++) { 202 text += event.title; 203 if (event.author || event.location) { 204 text += '\n'; 205 } 206 if (event.location) { 207 text += event.location + ' '; 208 } 209 if (event.author) { 210 text += event.author; 211 } 212 if (i < (nextEvents.length - 1)) { 213 text += '\n----------\n'; 214 } 215 } 216 text = filterSpecialChar(text); 217 chrome.browserAction.setTitle({'title' : text}); 218 } 219 } 220 pollUnderProgress = false; 221 222 chrome.extension.sendRequest({ 223 message: 'enableSave' 224 }, function() { 225 }); 226 227 return; 228}; 229 230/** 231 * Shows the user logged out. 232 */ 233CanvasAnimation.prototype.showLoggedOut = function() { 234 currentBadge_ = '?'; 235 chrome.browserAction.setIcon({path: '../images/icon-16_bw.gif'}); 236 chrome.browserAction.setBadgeBackgroundColor({color: [190, 190, 190, 230]}); 237 chrome.browserAction.setBadgeText({text: '?'}); 238 chrome.browserAction.setTitle({ 'title' : ''}); 239}; 240 241/** 242 * Gets the badge text. 243 * @param {Object} nextEvent_ next event in the calendar. 244 * @return {String} text Badge text to be shown in extension icon. 245 */ 246CanvasAnimation.prototype.getBadgeText = function(nextEvent_) { 247 if (!nextEvent_) { 248 return ''; 249 } 250 251 var ms = nextEvent_.startTime.getTime() - getCurrentTime(); 252 var nextEventMin = Math.ceil(ms / (1000 * 60)); 253 254 var text = ''; 255 if (nextEventMin < 60) { 256 text = chrome.i18n.getMessage('minutes', nextEventMin.toString()); 257 } else if (nextEventMin < 1440) { 258 text = chrome.i18n.getMessage('hours', 259 Math.round(nextEventMin / 60).toString()); 260 } else if (nextEventMin < (1440 * 10)) { 261 text = chrome.i18n.getMessage('days', 262 Math.round(nextEventMin / 60 / 24).toString()); 263 } 264 return text; 265}; 266 267/** 268 * Provides all the calendar related utils. 269 */ 270CalendarManager = {}; 271 272/** 273 * Extracts event from the each entry of the calendar. 274 * @param {Object} elem The XML node to extract the event from. 275 * @return {Object} out An object containing the event properties. 276 */ 277CalendarManager.extractEvent = function(elem) { 278 var out = {}; 279 280 for (var node = elem.firstChild; node != null; node = node.nextSibling) { 281 if (node.nodeName == 'title') { 282 out.title = node.firstChild ? node.firstChild.nodeValue : MSG_NO_TITLE; 283 } else if (node.nodeName == 'link' && 284 node.getAttribute('rel') == 'alternate') { 285 out.url = node.getAttribute('href'); 286 } else if (node.nodeName == 'gd:where') { 287 out.location = node.getAttribute('valueString'); 288 } else if (node.nodeName == 'gd:who') { 289 if (node.firstChild) { 290 out.attendeeStatus = node.firstChild.getAttribute('value'); 291 } 292 } else if (node.nodeName == 'gd:eventStatus') { 293 out.status = node.getAttribute('value'); 294 } else if (node.nodeName == 'gd:when') { 295 var startTimeStr = node.getAttribute('startTime'); 296 var endTimeStr = node.getAttribute('endTime'); 297 298 startTime = rfc3339StringToDate(startTimeStr); 299 endTime = rfc3339StringToDate(endTimeStr); 300 301 if (startTime == null || endTime == null) { 302 continue; 303 } 304 305 out.isAllDay = (startTimeStr.length <= 11); 306 out.startTime = startTime; 307 out.endTime = endTime; 308 } 309 } 310 return out; 311}; 312 313/** 314 * Polls the server to get the feed of the user. 315 */ 316CalendarManager.pollServer = function() { 317 if (! pollUnderProgress) { 318 eventList = []; 319 pollUnderProgress = true; 320 pendingLoadId_ = null; 321 calendars = []; 322 lastPollTime_ = getCurrentTime(); 323 var url; 324 var xhr = new XMLHttpRequest(); 325 try { 326 xhr.onreadystatechange = CalendarManager.genResponseChangeFunc(xhr); 327 xhr.onerror = function(error) { 328 console.log('error: ' + error); 329 nextEvent_ = null; 330 canvasAnimation_.drawFinal(); 331 }; 332 if (isMultiCalendar) { 333 url = MULTIPLE_CALENDAR_SUPPORT_URL; 334 } else { 335 url = SINGLE_CALENDAR_SUPPORT_URL; 336 } 337 338 xhr.open('GET', url); 339 xhr.send(null); 340 } catch (e) { 341 console.log('ex: ' + e); 342 nextEvent_ = null; 343 canvasAnimation_.drawFinal(); 344 } 345 } 346}; 347 348/** 349 * Gathers the list of all calendars of a specific user for multiple calendar 350 * support and event entries in single calendar. 351 * @param {xmlHttpRequest} xhr xmlHttpRequest object containing server response. 352 * @return {Object} anonymous function which returns to onReadyStateChange. 353 */ 354CalendarManager.genResponseChangeFunc = function(xhr) { 355 return function() { 356 if (xhr.readyState != 4) { 357 return; 358 } 359 if (!xhr.responseXML) { 360 console.log('No responseXML'); 361 nextEvent_ = null; 362 canvasAnimation_.drawFinal(); 363 return; 364 } 365 if (isMultiCalendar) { 366 var entry_ = xhr.responseXML.getElementsByTagName('entry'); 367 if (entry_ && entry_.length > 0) { 368 calendars = []; 369 for (var i = 0, entry; entry = entry_[i]; ++i) { 370 if (!i) { 371 defaultAuthor = entry.querySelector('title').textContent; 372 } 373 // Include only those calendars which are not hidden and selected 374 var isHidden = entry.querySelector('hidden'); 375 var isSelected = entry.querySelector('selected'); 376 if (isHidden && isHidden.getAttribute('value') == 'false') { 377 if (isSelected && isSelected.getAttribute('value') == 'true') { 378 var calendar_content = entry.querySelector('content'); 379 var cal_src = calendar_content.getAttribute('src'); 380 cal_src += '?toolbar=true&max-results=10'; 381 calendars.push(cal_src); 382 } 383 } 384 } 385 CalendarManager.getCalendarFeed(0); 386 return; 387 } 388 } else { 389 calendars = []; 390 calendars.push(SINGLE_CALENDAR_SUPPORT_URL); 391 CalendarManager.parseCalendarEntry(xhr.responseXML, 0); 392 return; 393 } 394 395 console.error('Error: feed retrieved, but no event found'); 396 nextEvent_ = null; 397 canvasAnimation_.drawFinal(); 398 }; 399}; 400 401/** 402 * Retrieves feed for a calendar 403 * @param {integer} calendarId Id of the calendar in array of calendars. 404 */ 405CalendarManager.getCalendarFeed = function(calendarId) { 406 var xmlhttp = new XMLHttpRequest(); 407 try { 408 xmlhttp.onreadystatechange = CalendarManager.onCalendarResponse(xmlhttp, 409 calendarId); 410 xmlhttp.onerror = function(error) { 411 console.log('error: ' + error); 412 nextEvent_ = null; 413 canvasAnimation_.drawFinal(); 414 }; 415 416 xmlhttp.open('GET', calendars[calendarId]); 417 xmlhttp.send(null); 418 } 419 catch (e) { 420 console.log('ex: ' + e); 421 nextEvent_ = null; 422 canvasAnimation_.drawFinal(); 423 } 424}; 425 426/** 427 * Gets the event entries of every calendar subscribed in default user calendar. 428 * @param {xmlHttpRequest} xmlhttp xmlHttpRequest containing server response 429 * for the feed of a specific calendar. 430 * @param {integer} calendarId Variable for storing the no of calendars 431 * processed. 432 * @return {Object} anonymous function which returns to onReadyStateChange. 433 */ 434CalendarManager.onCalendarResponse = function(xmlhttp, calendarId) { 435 return function() { 436 if (xmlhttp.readyState != 4) { 437 return; 438 } 439 if (!xmlhttp.responseXML) { 440 console.log('No responseXML'); 441 nextEvent_ = null; 442 canvasAnimation_.drawFinal(); 443 return; 444 } 445 CalendarManager.parseCalendarEntry(xmlhttp.responseXML, calendarId); 446 }; 447}; 448 449/** 450 * Parses events from calendar response XML 451 * @param {string} responseXML Response XML for calendar. 452 * @param {integer} calendarId Id of the calendar in array of calendars. 453 */ 454CalendarManager.parseCalendarEntry = function(responseXML, calendarId) { 455 var entry_ = responseXML.getElementsByTagName('entry'); 456 var author = responseXML.querySelector('author name').textContent; 457 458 if (entry_ && entry_.length > 0) { 459 for (var i = 0, entry; entry = entry_[i]; ++i) { 460 var event_ = CalendarManager.extractEvent(entry); 461 462 // Get the time from then to now 463 if (event_.startTime) { 464 var t = event_.startTime.getTime() - getCurrentTime(); 465 if (t >= 0 && (event_.attendeeStatus != DECLINED_URL)) { 466 if (isMultiCalendar) { 467 event_.author = author; 468 } 469 eventList.push(event_); 470 } 471 } 472 } 473 } 474 475 calendarId++; 476 //get the next calendar 477 if (calendarId < calendars.length) { 478 CalendarManager.getCalendarFeed(calendarId); 479 } else { 480 CalendarManager.populateLatestEvent(eventList); 481 } 482}; 483 484/** 485 * Fills the event list with the events acquired from the calendar(s). 486 * Parses entire event list and prepares an array of upcoming events. 487 * @param {Array} eventList List of all events. 488 */ 489CalendarManager.populateLatestEvent = function(eventList) { 490 nextEvents = []; 491 if (isMultiCalendar) { 492 eventList.sort(sortByDate); 493 } 494 495 //populating next events array. 496 if (eventList.length > 0) { 497 nextEvent_ = eventList[0]; 498 nextEvent_.startTime.setSeconds(0, 0); 499 nextEvents.push(nextEvent_); 500 var startTime = nextEvent_.startTime; 501 for (var i = 1, event; event = eventList[i]; i++) { 502 var time = event.startTime.setSeconds(0, 0); 503 if (time == startTime) { 504 nextEvents.push(event); 505 } else { 506 break; 507 } 508 } 509 if (nextEvents.length > 1) { 510 nextEvents.sort(sortByAuthor); 511 } 512 canvasAnimation_.animate(); 513 return; 514 } else { 515 console.error('Error: feed retrieved, but no event found'); 516 nextEvent_ = null; 517 canvasAnimation_.drawFinal(); 518 } 519}; 520 521var DATE_TIME_REGEX = 522 /^(\d\d\d\d)-(\d\d)-(\d\d)T(\d\d):(\d\d):(\d\d)\.\d+(\+|-)(\d\d):(\d\d)$/; 523var DATE_TIME_REGEX_Z = /^(\d\d\d\d)-(\d\d)-(\d\d)T(\d\d):(\d\d):(\d\d)\.\d+Z$/; 524var DATE_REGEX = /^(\d\d\d\d)-(\d\d)-(\d\d)$/; 525 526/** 527* Convert the incoming date into a javascript date. 528* @param {String} rfc3339 The rfc date in string format as following 529* 2006-04-28T09:00:00.000-07:00 530* 2006-04-28T09:00:00.000Z 531* 2006-04-19. 532* @return {Date} The javascript date format of the incoming date. 533*/ 534function rfc3339StringToDate(rfc3339) { 535 var parts = DATE_TIME_REGEX.exec(rfc3339); 536 537 // Try out the Z version 538 if (!parts) { 539 parts = DATE_TIME_REGEX_Z.exec(rfc3339); 540 } 541 542 if (parts && parts.length > 0) { 543 var d = new Date(); 544 d.setUTCFullYear(parts[1], parseInt(parts[2], 10) - 1, parts[3]); 545 d.setUTCHours(parts[4]); 546 d.setUTCMinutes(parts[5]); 547 d.setUTCSeconds(parts[6]); 548 549 var tzOffsetFeedMin = 0; 550 if (parts.length > 7) { 551 tzOffsetFeedMin = parseInt(parts[8], 10) * 60 + parseInt(parts[9], 10); 552 if (parts[7] != '-') { // This is supposed to be backwards. 553 tzOffsetFeedMin = -tzOffsetFeedMin; 554 } 555 } 556 return new Date(d.getTime() + tzOffsetFeedMin * 60 * 1000); 557 } 558 559 parts = DATE_REGEX.exec(rfc3339); 560 if (parts && parts.length > 0) { 561 return new Date(parts[1], parseInt(parts[2], 10) - 1, parts[3]); 562 } 563 return null; 564}; 565 566/** 567 * Sorts all the events by date and time. 568 * @param {object} event_1 Event object. 569 * @param {object} event_2 Event object. 570 * @return {integer} timeDiff Difference in time. 571 */ 572function sortByDate(event_1, event_2) { 573 return (event_1.startTime.getTime() - event_2.startTime.getTime()); 574}; 575 576/** 577 * Sorts all the events by author name. 578 * @param {object} event_1 Event object. 579 * @param {object} event_2 Event object. 580 * @return {integer} nameDiff Difference in default author and others. 581 */ 582function sortByAuthor(event_1, event_2) { 583 var nameDiff; 584 if (event_2.author == defaultAuthor) { 585 nameDiff = 1; 586 } else { 587 return 0; 588 } 589 return nameDiff; 590}; 591 592/** 593 * Fires once per minute to redraw extension icon. 594 */ 595function redraw() { 596 // If the next event just passed, re-poll. 597 if (nextEvent_) { 598 var t = nextEvent_.startTime.getTime() - getCurrentTime(); 599 if (t <= 0) { 600 CalendarManager.pollServer(); 601 return; 602 } 603 } 604 canvasAnimation_.animate(); 605 606 // if ((we are logged in) && (30 minutes have passed)) re-poll 607 if (nextEvent_ && (getCurrentTime() - lastPollTime_ >= POLL_INTERVAL)) { 608 CalendarManager.pollServer(); 609 } 610}; 611 612/** 613 * Returns the current time in milliseconds. 614 * @return {Number} Current time in milliseconds. 615 */ 616function getCurrentTime() { 617 return (new Date()).getTime(); 618}; 619 620/** 621* Replaces ASCII characters from the title. 622* @param {String} data String containing ASCII code for special characters. 623* @return {String} data ASCII characters replaced with actual characters. 624*/ 625function filterSpecialChar(data) { 626 if (data) { 627 data = data.replace(/</g, '<'); 628 data = data.replace(/>/g, '>'); 629 data = data.replace(/&/g, '&'); 630 data = data.replace(/%7B/g, '{'); 631 data = data.replace(/%7D/g, '}'); 632 data = data.replace(/"/g, '"'); 633 data = data.replace(/'/g, '\''); 634 } 635 return data; 636}; 637 638/** 639 * Called from options.js page on saving the settings 640 */ 641function onSettingsChange() { 642 isMultiCalendar = JSON.parse(localStorage.multiCalendar); 643 badgeAnimation_.start(); 644 CalendarManager.pollServer(); 645}; 646 647/** 648 * Function runs on updating a tab having url of google applications. 649 * @param {integer} tabId Id of the tab which is updated. 650 * @param {String} changeInfo Gives the information of change in url. 651 * @param {String} tab Gives the url of the tab updated. 652 */ 653function onTabUpdated(tabId, changeInfo, tab) { 654 var url = tab.url; 655 if (!url) { 656 return; 657 } 658 659 if ((url.indexOf('www.google.com/calendar/') != -1) || 660 ((url.indexOf('www.google.com/a/') != -1) && 661 (url.lastIndexOf('/acs') == url.length - 4)) || 662 (url.indexOf('www.google.com/accounts/') != -1)) { 663 664 // The login screen isn't helpful 665 if (url.indexOf('https://www.google.com/accounts/ServiceLogin?') == 0) { 666 return; 667 } 668 669 if (pendingLoadId_) { 670 clearTimeout(pendingLoadId_); 671 pendingLoadId_ = null; 672 } 673 674 // try to poll in 2 second [which makes the redirects settle down] 675 pendingLoadId_ = setTimeout(CalendarManager.pollServer, 2000); 676 } 677}; 678 679/** 680 * Called when the user clicks on extension icon and opens calendar page. 681 */ 682function onClickAction() { 683 chrome.tabs.getAllInWindow(null, function(tabs) { 684 for (var i = 0, tab; tab = tabs[i]; i++) { 685 if (tab.url && isCalendarUrl(tab.url)) { 686 chrome.tabs.update(tab.id, {selected: true}); 687 CalendarManager.pollServer(); 688 return; 689 } 690 } 691 chrome.tabs.create({url: GOOGLE_CALENDAR_URL}); 692 CalendarManager.pollServer(); 693 }); 694}; 695 696/** 697 * Checks whether an instance of Google calendar is already open. 698 * @param {String} url Url of the tab visited. 699 * @return {boolean} true if the url is a Google calendar relative url, false 700 * otherwise. 701 */ 702function isCalendarUrl(url) { 703 return url.indexOf('www.google.com/calendar') != -1 ? true : false; 704}; 705 706/** 707 * Initializes everything. 708 */ 709function init() { 710 badgeAnimation_ = new BadgeAnimation(); 711 canvasAnimation_ = new CanvasAnimation(); 712 713 isMultiCalendar = JSON.parse(localStorage.multiCalendar || false); 714 715 chrome.browserAction.setIcon({path: '../images/icon-16.gif'}); 716 badgeAnimation_.start(); 717 CalendarManager.pollServer(); 718 window.setInterval(redraw, DRAW_INTERVAL); 719 720 chrome.tabs.onUpdated.addListener(onTabUpdated); 721 722 chrome.browserAction.onClicked.addListener(function(tab) { 723 onClickAction(); 724 }); 725}; 726 727//Adding listener when body is loaded to call init function. 728window.addEventListener('load', init, false); 729