/** * InsideTrip configuration. */ var InsideTrip = { config : { tripResultsPerPage : 15, recentSearchesMax : 5, url : { tripSearch: '../Api/getAirSearch', airportsearch: '../scripts/airportsearch.js.php', airlineImages: '../airLogos' }, timeout : { tripSearch: 120 }, data : { tripFields : [ {name: 'score', type: 'int'}, {name: 'score-depart', type: 'int'}, {name: 'score-return', type: 'int'}, {name: 'score-speed', type: 'int'}, {name: 'score-comfort', type: 'int'}, {name: 'score-ease', type: 'int'}, {name: 'price', type: 'int'}, 'depart-time-depart', 'depart-time-arrive', 'depart-duration', 'return-time-depart', 'return-time-arrive', {name: 'return-duration', type: 'int'}, {name: 'connection-low', type: 'int'}, {name: 'connection-high', type: 'int'}, {name: 'stops', type: 'int'}, 'same-airports', 'airline', 'airlines', 'airports', 'quality-pick', 'json' ], tripFieldKeys : { tripQuality: 'score', price: 'price', depDep: 'depart-time-depart', depArr: 'depart-time-arrive', depDur: 'depart-duration', rtnDep: 'return-time-depart', rtnArr: 'return-time-arrive', rtnDur: 'return-duration' }, tripRecordsRoot : 'itrip_trips', dateFormat : 'Y-m-d', tripRecordsExpiry : 30 * 60, // in seconds tripEvalKeys : { 'comfort-aircraft-age': 'age', 'comfort-aircraft-type': 'atp', 'comfort-filled': 'lod', 'comfort-legroom': 'leg', 'ease-connection': 'cnx', 'ease-gate': 'gat', 'ease-lost-bags': 'lob', 'ease-routing': 'rtq', 'speed-duration': 'dur', 'speed-ontime': 'ons', 'speed-stops': 'nsp', 'speed-wait': 'tsa' } }, input : { dateField : { emptyText: 'mm/dd/yyyy', allowBlank: false, blankText: '', invalidText: '' }, dateFormat : 'n/j/Y', minDate : new Date().clearTime(), maxDate : new Date().clearTime().add(Date.YEAR, 1), maxConnections : 5 }, defaults : { tripQueryForm: { 'journey-type-roundtrip': true, 'journey-type-oneway': false, 'depart-airport': '', 'depart-nearby': false, 'arrive-airport': '', 'arrive-nearby': false, 'depart-date': '', 'depart-time': '0-1440', 'return-date': '', 'return-time': '0-1440', 'travelers': '1', 'cabin': 'EconomyRestricted' }, tripDashboard: { speed: 15, ease: 15, comfort: 15 }, state: 'ui=start', quickTips: { showDelay: 500, hideDelay: 100, autoHide: false } }, // end config.defaults orbitzErrorMessages : { 'PRM10': function(attributes) { if ( /invalid originated point/i.test(attributes.errormessage) ) { return "We're sorry, at this time we are unable to provide prices for tickets originating from this country."; } else { var matches = /(\w+) location code/i.exec( attributes.errormessage ); if (matches[1]) { return "We're sorry, we do not recognize your " + matches[1] + " airport. Please type city/airport then select from list."; } else { return attributes.errormessage; } } } }, // end config.orbitzErrorMessages tooltips: [ { target: 'panel-trip-dashboard-head-link', title: 'TripQuality Dashboard', text: 'Check/uncheck to customize your own TripQuality Score' }, { target: 'label-speed-stops', title: 'Speed: Number of stops', text: 'The number of scheduled stops in a trip' }, { target: 'label-speed-duration', title: 'Speed: Travel Duration', text: 'Total scheduled travel time including layovers' }, { target: 'label-speed-ontime', title: 'Speed: On-time stats', text: 'Flight on-time performance in past 60 days' }, { target: 'label-speed-wait', title: 'Speed: Security wait time', text: 'Average security wait for similar travel days' }, { target: 'label-comfort-legroom', title: 'Comfort: Legroom', text: 'Amount of legroom/pitch between seats' }, { target: 'label-comfort-aircraft-type', title: 'Comfort: Aircraft Type', text: 'Non-Jet, Regional Jet, or Large Jet' }, { target: 'label-comfort-aircraft-age', title: 'Comfort: Aircraft Age', text: "Average aircraft age of an airline's subfleet" }, { target: 'label-comfort-filled', title: 'Comfort: Historical Load Factor', text: 'How full the planes were in this market last year' }, { target: 'label-ease-connection', title: 'Ease: Connection time', text: 'Layover time between connecting flights' }, { target: 'label-ease-routing', title: 'Ease: Routing quality', text: 'How out of the way is the flight path' }, { target: 'label-ease-lost-bags', title: 'Ease: Lost bags rank', text: 'Year-to-date airline lost luggage ranking' }, { target: 'label-ease-gate', title: 'Ease: Gate location', text: 'Ease of getting to/from/between gates' }, { target: 'trip-tripQuality-head', title: 'TripQuality', text: 'Move the slider bar to indicate minimum TripQuality level' }, { target: 'segment-stops-head', title: 'Stops', text: 'Check/uncheck boxes to filter on the number of stops' }, { target: 'segment-time-head', title: 'Flight Times', text: 'Move the slider bars to fine tune departure/return times' }, { target: 'segment-connection-head', title: 'Connection Time', text: 'Move the slider bar to fine tune layover time' } ], // end config.tooltips preloads: [ "img/interstitial_search.gif", "img/interstitial-animation.gif" ], // end config.preloads preloadsInterstitial: [ "img/details-close-box.gif", "img/details-tab-back-left-selected.gif", "img/slider_149px.gif", "img/details-tab-back-left.gif", "img/slider_172px.gif", "img/details-tab-back-right-1-selected.gif", "img/details-tab-back-right-1.gif", "img/details-tab-back-right-2-selected.gif", "img/details-tab-back-right-2.gif", "img/details-tab-back-right-3-selected.gif", "img/details-tab-back-right-3.gif", "img/details-tab-back-right-4-selected.gif", "img/details-tab-back-right-4.gif", "img/details-tab-back-right-5-selected.gif", "img/details-tab-back-right-5.gif", "img/gantt_bar_connection.gif", "img/gantt_bar_flight.gif", "img/harvey-ball-1.gif", "img/harvey-ball-2.gif", "img/harvey-ball-3.gif", "img/harvey-ball-4.gif", "img/harvey-ball-5.gif", "img/harvey-balls-key.gif", "img/slider_thumb.gif", "img/harvey-balls-key.gif", "img/start-back-bottomleft.jpg", "img/start-back-bottomright.jpg", "img/start-back-topleft.jpg", "img/start-back-topright.jpg", "img/trip-dashboard-recalculate.jpg", "img/trip-details-back-bottom.gif", "img/trip-details-direction-head-back.gif", "img/trip-details-leg-head-back.gif", "img/trip-results-buy-back-bottom.jpg", "img/trip-results-buy-back-top.jpg", "img/trip-results-buy-back.jpg", "img/trip-results-details-link-back-hide.jpg", "img/trip-results-details-link-back-show.jpg", "img/trip-results-head-back-bottom.gif", "img/trip-results-quality-pick.gif", "img/trip-results-row-back-bottom.gif", "img/trip-results-row-back-col1.gif", "img/trip-results-row-back-col2.gif", "img/trip-results-row-back-col3.gif", "img/trip-results-row-back-col4.gif", "img/trip-results-row-back-top.gif", "img/trip-results-subhead-back-bottom.gif", "img/trip-results-subhead-back-top.gif" ] // end config.preloads-interstitial }, // end config template : { tripRecentDateFormat : 'M j', tripRecentSearch : '
  • ' + '{depart-airport} to {arrive-airport} ' + '{dates}' + '
  • ' , logoLink : '', tripFilterPanel : '
    ' + '
    ' + '
    ' + '

    TripQuality SM

    ' + '

    ' + '
    ' + '
    ' + '
    ' + '
    ' + '

    Search results for:

    ' + '
    ' + '

     () to

    ' + '

     ()

    ' + '

    ' + '

     for 

    ' + '

    ' + '
    ' + '
    ' + '

    ' + 'Modify your search ' + 'Hide search' + '

    ' + '
    ' + '
    ' + '
    ' + '
    ' + '

    Stops

    ' + '
    ' + '
    ' + '' + '' + '
    ' + '
    ' + '' + '' + '
    ' + '
    ' + '' + '' + '
    ' + '
    ' + '
    ' + '
    ' + '

    Flight Times

    ' + '
    ' + '

    Depart

    ' + '

    ' + '
    ' + '
    ' + '
    ' + '

    Return

    ' + '

    ' + '
    ' + '
    ' + '
    ' + '
    ' + '

    Connection Time

    ' + '

    ' + '
    ' + '
    ' + '
    ' + '
    ' + '
    ' + '

    Airports

    ' + '
    ' + '' + '' + '
    ' + '
    ' + '

    Airports

    ' + '
    ' + '
    ' + '
    ' + '

    Airports

    ' + '
    ' + '
    ' + '
    ' + '

    Connecting Airports

    ' + '
    ' + '
    ' + '
    ' + '
    ' + '

    Airlines

    ' + '
    ' + '

    select all

    ' + '
    ' + '
    ' + '
    ' , // end tripFilterPanel filterAirport : '
    ' + '' + '' + '
    ' , // end filterAirport filterAirline : '
    ' + '' + '' + '
    ' , // end filterAirline tripDashboard : '
    ' + '
    ' + '

    TripQualitySM Dashboard

    ' + '

    What\'s important to you?

    ' + '
    ' + '
    ' + '
    ' + '

    SPEED

    ' + '
    ' + '' + '' + '
    ' + '
    ' + '' + '' + '
    ' + '
    ' + '' + '' + '
    ' + '
    ' + '' + '' + '
    ' + '
    ' + '
    ' + '

    COMFORT

    ' + '
    ' + '' + '' + '
    ' + '
    ' + '' + '' + '
    ' + '
    ' + '' + '' + '
    ' + '
    ' + '' + '' + '
    ' + '
    ' + '
    ' + '

    EASE

    ' + '
    ' + '' + '' + '
    ' + '
    ' + '' + '' + '
    ' + '
    ' + '' + '' + '
    ' + '
    ' + '' + '' + '
    ' + '
    ' + '' + '
    ' + '
    ' , // end tripDashboard tripResultsHead : '
    ' + '
    Price
    ' + '
    ' + '

    DEPARTURE TRIP

    ' + '
    ' + 'Depart' + 'Arrive' + 'Duration' + '
    ' + '
    ' + '
    ' + '

    RETURN TRIP

    ' + '
    ' + 'Depart' + 'Arrive' + 'Duration' + '
    ' + '
    ' + '
    Overall TripQualitySM
    ' + '
    ' , // end tripResultsHead tripResultsPaging : '
    ' + '« ' + '< ' + ' ' + '> ' + '»' + '
    ' , // end tripResultsPaging, tripResultsWaitMsg : 'Loading... Please wait.', tripsAllFilteredOutMsg : '
    ' + "

    Oops! You've filtered out all available trips.

    " + '

    Use the controls to the left to expand your allowable times, airports, airlines, number of stops, or TripQuality Scores.

    ' + '
    ' , // end tripsAllFilteredOutMsg tripResult : '
    ' + '
    ' + '
    ' + '

     buy now

    ' + '

    ' + '' + '
    ' + '
    ' + '
    ' + '

    Duration =

    ' + '

    TripQuality =

    ' + '
    ' + '
    ' + '
    ' + '

    Duration =

    ' + '

    TripQuality =

    ' + '
    ' + '
    ' + '
    ' + '

    Speed

    ' + '

    Comfort

    ' + '

    Ease

    ' + '
    ' + '

    Overall TripQuality

    ' + '
    ' + '
    ' + // , // end tripResult tripResultDetails : '
    ' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '
    ' + '
    ' + 'hide details' + '' + '

     > 

    ' + '

    ' + '

     | 

    ' + '
    ' + '
    ' + '' + '' + '' + '
    SpeedComfortEase
    ' // , // end tripResultDetails tripResultsDetailsFlight : '' + '' + '
    ' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '
    ' + '

    ' + '

    Flight

    ' + '
    Departs from  ()
    Arrives in  ()
    ' + // '' + '' , // end tripResultsDetailsDirection tripResultsDetailsFlightEvaluator : '

    ' , // end tripResultsDetailsFlightEvaluator tripResultsDetailsFlightHeadFirst : '

     Trip: 

    ' , // end tripResultsDetailsFlightHeadFirst tripResultsDetailsFlightHeadSubsequent : '

     layover

    ' , // end tripResultsDetailsFlightHeadFirst tripResultsUi : { head: '
    ', left: '
    ', middle: '
    ' + '
    ' , right: '

    InsideTrip allows you to evaluate all aspects of what makes a trip enjoyable.

    ' }, // end tripResultsUi tripInterstitialSearchUi : { head: '', left: '', middle: '', right: '' }, // end tripInterstitialUi tripInterstitialPurchaseUi : { head: '', left: '', middle: '
    Transferring you to our partner site for purchasing...
    ', right: '' }, // end tripInterstitialUi tripStartUi : { head: '', left: '
    ' + '
    ' + '

    What Makes a Quality Trip?

    ' + '

    Is it Price, Speed, Comfort, Ease?

    ' + '

    What\'s important to you?

    ' + '

    InsideTrip allows YOU to rate your trip according to each of these factors and helps you wade through the hundreds of flight options on the market today.

    ' + '
    ' , middle: '
    ', right: '

    Recent Searches

    ' }, // end tripStartUi unsupportedBrowserUi : { head: '', middle: '
    ' + '

    Sorry, your web browser is not yet supported.

    ' + '

    InsideTrip.com is designed and tested for optimal experience using today' +"'"+ 's best available browser technologies including Safari. So, for now, you will need either to download a new version of Firefox or download a new version of Internet Explorer (Windows only) and then come right back.

    ' + '
    ' }, // end unsupportedBrowserUi tripEvaluators : { // gives both the display names and the display order 'speed': { 'speed-stops': 'Number of Stops', 'speed-duration': 'Travel Duration', 'speed-ontime': 'On-time Stats', 'speed-wait': 'Security Wait' }, 'comfort': { 'comfort-legroom': 'Legroom', 'comfort-aircraft-type': 'Aircraft Type', 'comfort-aircraft-age': 'Aircraft Age', 'comfort-filled': 'Load Factor' }, 'ease': { 'ease-connection': 'Connect Time', 'ease-routing': 'Routing Quality', 'ease-lost-bags': 'Lost Bags Rank', 'ease-gate': 'Gate Location' } }, // end tripEvaluators tripGantt : function(chart, legs, highDur, airportNames) { var ganttMaxWidthPx = 190; var ganttMinWidthPx = 80; // scan legs for times and durations var depTime = [], arrTime = [], dur = [], cnxDur = []; var totalDur = 0; for (var i = 0, len = legs.length; i < len; i++) { var leg = legs[i]; depTime[i] = parseInt(leg.time_dep_str.substr(0,2), 10) * 60 + parseInt(leg.time_dep_str.substr(3,2), 10); arrTime[i] = parseInt(leg.time_arr_str.substr(0,2), 10) * 60 + parseInt(leg.time_arr_str.substr(3,2), 10); totalDur += dur[i] = leg.dur; if (i > 0) { cnxDur[i] = depTime[i] - arrTime[i - 1]; if (cnxDur[i] < 0) { cnxDur[i] += 1440; } // overnight layover totalDur += cnxDur[i]; } } // build segments for legs and connections var wrapper = Ext.DomHelper.append(chart, { tag: 'div', cls: 'gantt-wrapper' }, true); var totalWidth = totalDur / highDur * ganttMaxWidthPx; var segment, width, tooltip; var sumWidth = 0; // because of rounding sumWidth might not exactly equal totalWidth for (i = 0; i < len; i++) { if (cnxDur[i]) { sumWidth += (width = Math.round( cnxDur[i] / totalDur * totalWidth )); segment = Ext.DomHelper.append(wrapper, { tag: 'div', cls: 'gantt-segment-connection', style: 'width:' + width + 'px' }, true); Ext.DomHelper.append(segment, { tag: 'span', cls: 'gantt-connection-airport', html: '' }); Ext.DomHelper.append(segment, { tag: 'span', cls: 'gantt-connection-time', html: InsideTrip.template.minutesToDurationShort(cnxDur[i]) }); var cnxPort = legs[i-1].port_arr; Ext.QuickTips.register({ text: InsideTrip.template.minutesToDurationLong(cnxDur[i]) + ' connection in ' + (airportNames ? ( airportNames[cnxPort] + ' ('+cnxPort+')' ) : cnxPort) , target: segment }); } sumWidth += (width = Math.round( dur[i] / totalDur * totalWidth )); segment = Ext.DomHelper.append(wrapper, { tag: 'div', cls: 'gantt-segment', style: 'width:' + width + 'px' }, true); var depPort = legs[i].port_dep, arrPort = legs[i].port_arr; Ext.QuickTips.register({ text: 'Depart ' + (airportNames ? ( airportNames[depPort] + ' ('+depPort+')' ) : depPort) + ' at ' + InsideTrip.template.minutesToTime(depTime[i]) + '
    ' + 'Arrive ' + (airportNames ? ( airportNames[arrPort] + ' ('+arrPort+')' ) : arrPort) + ' at ' + InsideTrip.template.minutesToTime(arrTime[i]) , target: segment }); if (i == 0) { // first leg Ext.DomHelper.append(segment, { tag: 'span', cls: 'gantt-depart-airport', html: depPort }); Ext.DomHelper.append(segment, { tag: 'span', cls: 'gantt-depart-time', html: InsideTrip.template.minutesToTime(depTime[i]) }); } if (i == len - 1) { // last leg Ext.DomHelper.append(segment, { tag: 'span', cls: 'gantt-arrive-airport', html: arrPort }); Ext.DomHelper.append(segment, { tag: 'span', cls: 'gantt-arrive-time', html: InsideTrip.template.minutesToTime(arrTime[i]) }); } } // set horizontal chart boundaries var wrapperWidth = sumWidth; if (wrapperWidth < ganttMinWidthPx) { wrapperWidth = ganttMinWidthPx; } wrapper.setStyle('width', wrapperWidth+'px'); return chart; }, // end tripGantt() minutesToTime : function(minutes) { var h = Math.floor( minutes / 60 ); var m = minutes % 60; var ampm = ((h % 24) < 12) ? 'a' : 'p'; h = h % 12 || 12; return h + ':' + (m.toString().length==1 ? '0' : '') + m + ampm; }, minutesToTimeLong : function(minutes) { var shortForm = InsideTrip.template.minutesToTime(minutes); return shortForm.replace('a', ' am').replace('p', ' pm'); }, minutesToDuration : function(minutes) { var h = Math.floor( minutes / 60 ); var m = minutes % 60; return (h ? (h + 'h ') : '') + m + 'm'; }, minutesToDurationLong : function(minutes) { var shortForm = InsideTrip.template.minutesToDuration(minutes); return shortForm.replace('h', ' hour, ').replace('m', ' minute'); // var h = Math.floor( minutes / 60 ); // var m = minutes % 60; // return ( // (h ? (h + ((h > 1) ? ' hours, ' : ' hour, ')) : '') + // (m ? (m + ((m > 1) ? ' minutes' : ' minute')) : '') // ); }, minutesToDurationShort : function(minutes) { var h = Math.floor( minutes / 60 ); var m = minutes % 60; return (h ? (h + 'h') : '') + (m ? ((m.toString().length==1 ? '0' : '') + m + "'") : ''); } }, // end template util : { checksum : function(s, bits) { bits = bits || 16; var checksum = 0; for (var i = 0, len = s.length; i < len; i++) { checksum += s.charCodeAt(i); } return (checksum % (1<
    ' ); // register application with History Manager Ext.ux.History.register( this.application, // application ("module") name '', // initial application state this.activateState.createDelegate(this) // state-change handler (in application context) ); // set History Manager to initialize application state when ready Ext.ux.History.on( 'load', this.initState, this // execute in application context ); // app init sequence this.initApplication.defer(200, this); }; Ext.extend(InsideTrip.Application, Object, { application : null, param : null, activated : false, getQueryString : function(url) { // returns URL (before the hash) deserialized var href = url || top.location.href; if (href) { var matches = href.indexOf('#') > 0 ? /\:\/{1,2}.+?\/(.+?)\#/.exec(href) : /\:\/{1,2}.+?\/(.+)/.exec(href) ; var query = ''; if (matches) { var path = matches[1]; var i = path.indexOf('?'); if (i >= 0) { query = path.substr(i+1); path = path.substr(0, i); var namesAndValues = query.split('/'); var namesAndValuesLength = namesAndValues.length; if (namesAndValuesLength > 0) { var pairs = []; if (!namesAndValues[0]) { namesAndValues.shift(); } for (i = 0; i < namesAndValuesLength; i += 2) { pairs.push( namesAndValues[i]+'='+namesAndValues[i+1] ); } query = pairs.join('&'); } } } return query; } return ''; }, // end getQueryString() cache : function(el) { if (!el || Ext.type(el)=='string') { // cache all elements matching either given selector or .autocache var selector = el || '.autocache'; var els = Ext.query(selector); for (var i = 0, len = els.length; i < len; i++) { this.cache( els[i] ); } } else { // cache specific element given el = Ext.get(el); if (el) { this.domCache[el.id] = el; } // remove element from DOM if (el && el.dom.parentNode) { el.dom.parentNode.removeChild(el.dom); } } }, // end cache() retrieve : function(elId) { // look for element of given ID in the DOM and then the DOM cache and return it return Ext.get(elId) || this.domCache[elId]; }, // end retrieve() initApplication : function() { // init trip query results collection this.trips = []; // to contain trip query results by numerical index this.tripQueryIndex = null; // to contain the numerical index of the currently-displayed trip query this.tripQueryStrings = {}; // to contain numerical indeces, indexed by trip query string this.nextTripQueryIndex = 1; // initialize tooltips Ext.QuickTips.init(); var tips = InsideTrip.config.tooltips; for (i = 0, len = tips.length; i < len; i++) { Ext.QuickTips.register( tips[i] ); } // init trip query form this.initTripQueryForm( Ext.get('trip-query') ); // init recent searches this.initRecentSearches(); // activate state if it hasn't been yet if (this.onFirstActivation !== null) { this.activateState(); } }, // end initApplication addTripRecent : function(params) { var searches = this.recentSearches; var tripQueryForm = this.tripQueryForm; var thisSearch = tripQueryForm.translateNames(params); if (!thisSearch['depart-airport']) { return; } // are these airports and dates already in the recent searches? for (var i = 0, len = searches.length; i < len ; i++) { var pastSearch = tripQueryForm.translateNames( searches[i] ); if ( pastSearch['depart-airport'] == thisSearch['depart-airport'] && pastSearch['depart-date'] == thisSearch['depart-date'] && pastSearch['arrive-airport'] == thisSearch['arrive-airport'] && ( (pastSearch['journey-type-oneway'] && thisSearch['journey-type-oneway']) || ( pastSearch['journey-type-roundtrip'] && thisSearch['journey-type-roundtrip'] && pastSearch['return-date'] == thisSearch['return-date'] ) ) ) { // delete the old trip entry, to be replaced with the new one given searches.splice(i, 1); break; } } // add given search to top of recent-searches array searches.unshift(params); // keep list trimmed to maximum number var maxSearches = InsideTrip.config.recentSearchesMax; if (searches.length > maxSearches) { searches.pop(); } }, // end addTripRecent() initRecentSearches : function() { // start recent searches with those given by back end this.recentSearches = []; var recentList = Ext.get('trip-recent-list'); if (!recentList) { return; } var backendSearches = recentList.query('li'); for (var i = 0, len = backendSearches.length; i < len; i++) { var li = Ext.get( backendSearches[i] ); this.addTripRecent( Ext.urlDecode( this.getQueryString( li.child('a').dom.getAttribute('href') ) ) ); li.remove(); } }, // end initRecentSearches() initTripQueryForm : function(formEl) { if (!formEl) { return; } // create Form object to represent the form in DOM var tripQueryForm = this.tripQueryForm = new InsideTrip.Form( formEl, { defaultValues: InsideTrip.config.defaults.tripQueryForm, url: InsideTrip.config.url.tripSearch, timeout: InsideTrip.config.timeout.tripSearch, method: 'POST', baseParams: { 'outputType': 'json', 'maxCnx': InsideTrip.config.input.maxConnections }, onResponse: this.handleTripQueryResponse.createDelegate(this) // execute handler in the application's, not the form's, scope } ); // populate Form object with field objects and tie them into input elements on the page var depAirInputID = Ext.get('input-depart-airport').dom.name; var arrAirInputID = Ext.get('input-arrive-airport').dom.name; var departDate, returnDate, departTime, returnTime; tripQueryForm.add( new Ext.form.Radio({id: 'journey-type-roundtrip'}).applyTo('input-journey-type-roundtrip'), new Ext.form.Radio({id: 'journey-type-oneway'}).applyTo('input-journey-type-oneway'), new InsideTrip.AirportComboBox({id: 'depart-airport', hiddenName: depAirInputID, travelPointName: 'departure airport', app: this}).applyTo('input-depart-airport'), new Ext.form.Checkbox({id: 'depart-nearby'}).applyTo('input-depart-nearby'), new InsideTrip.AirportComboBox({id: 'arrive-airport', hiddenName: arrAirInputID, travelPointName: 'destination airport', app: this}).applyTo('input-arrive-airport'), new Ext.form.Checkbox({id: 'arrive-nearby'}).applyTo('input-arrive-nearby'), departDate = new Ext.form.TextField(InsideTrip.config.input.dateField).applyTo('input-depart-date'), departTime = new Ext.form.TextField({id: 'depart-time'}).applyTo('input-depart-time'), returnDate = new Ext.form.TextField(InsideTrip.config.input.dateField).applyTo('input-return-date'), returnTime = new Ext.form.TextField({id: 'return-time'}).applyTo('input-return-time'), new Ext.form.TextField({id: 'travelers'}).applyTo('input-travelers'), // new Ext.form.TextField({id: 'cabin'}).applyTo('input-cabin') new Ext.form.Field({id: 'cabin'}).applyTo('input-cabin') ); departDate.id = 'depart-date'; returnDate.id = 'return-date'; // hardwire depart/return time names in field objects and blank them in the dom // (rather than submitting with the trip query, these fields become results filter settings) var departTimeName = departTime.getName(); departTime.getName = function() { return departTimeName; } departTime.el.dom.name = ''; var returnTimeName = returnTime.getName(); returnTime.getName = function() { return returnTimeName; } returnTime.el.dom.name = ''; // create DatePickers and TripDateManager for departure and return dates tripQueryForm.tripDateManager = new InsideTrip.TripDateManager({ departText: departDate, returnText: returnDate, roundTripEntry: tripQueryForm.findField('journey-type-roundtrip') }); // set journey-type switch to turn relevant steps off and on var onJourneyChange = function() { var journeyType = Ext.get('input-journey-type-oneway').dom.checked ? 'oneway' : 'roundtrip'; if (journeyType != this.journeyType) { // alter live form this.journeyType = journeyType; if ('oneway' == journeyType) { Ext.get('step-return-when').hide(); this.findField('return-date').disable(); } else if ('roundtrip' == journeyType) { this.findField('return-date').enable(); Ext.get('step-return-when').show(); } } }; // pass reference to this application object to the handler if (Ext.isSafari) { Ext.get('input-journey-type-roundtrip').on('click', onJourneyChange, tripQueryForm, { buffer: 150 }); Ext.get('input-journey-type-oneway').on('click', onJourneyChange, tripQueryForm, { buffer: 150 }); } else { Ext.get('input-journey-type-roundtrip').on('focus', onJourneyChange, tripQueryForm, { buffer: 150 }); Ext.get('input-journey-type-oneway').on('focus', onJourneyChange, tripQueryForm, { buffer: 150 }); } tripQueryForm.on('setValues', onJourneyChange, tripQueryForm); // make sure comboboxes are collapsed before executing a form action (e.g. submission) tripQueryForm.on( 'beforeaction', function(form, action) { var departCombobox = this.findField('depart-airport'); var arriveCombobox = this.findField('arrive-airport'); departCombobox.collapse(); arriveCombobox.collapse(); return departCombobox.queryComplete(this) && arriveCombobox.queryComplete(this); } ); // call application state handler on form submission tripQueryForm.on( 'beforeaction', this.handleTripQueryAction, this // execute in application scope ); }, // end initTripQueryForm() initTripQuality : function(formEl) { // create Form object to represent the form in DOM var tripQualityForm = this.tripQualityForm = new InsideTrip.Form( formEl, { defaultValues: { 'tripQuality': 0 } } ); // add in filter elements/widgets var tripQuality; tripQualityForm.add( tripQuality = new InsideTrip.Slider('field-tripQuality', { id: 'tripQuality', hiddenName: 'tripQuality', readout: Ext.get('tripQuality-low'), readoutContainer: Ext.get('tripQuality-low').findParent('.filter-readout', 2, true) }) ); tripQuality.el.addClass('autocache'); // attach listener to handle filter form change tripQuality.on( 'change', function() { Ext.apply(this.param, this.tripQualityForm.getValues()); this.param.page = 1; this.registerState(); }, this ); }, // end initTripQuality initTripFilters : function(formEl) { // create Form object to represent the form in DOM var tripFilters = this.tripFilters = new InsideTrip.Form( formEl, { defaultValues: { 'time-depart': '0-1440', 'time-return': '0-1440', 'connection': '0-2880' } } ); // prepare slider readout functions var refreshTimeRangeReadout = function(low, high) { this.readout.low.update( InsideTrip.template.minutesToTime(low) ); var highTime = InsideTrip.template.minutesToTime(high); this.readout.high.update( '12:00a'==highTime ? '11:59p' : highTime ); }; var refreshDurationRangeReadout = function(low, high) { this.readout.low.update( InsideTrip.template.minutesToDuration(low) ); this.readout.high.update( InsideTrip.template.minutesToDuration(high) ); }; // add in filter elements/widgets var tripQuality, timeDepart, timeReturn, connection; tripFilters.add( new InsideTrip.Checkbox({id: 'stops-0'}).applyTo('input-stops-0'), new InsideTrip.Checkbox({id: 'stops-1'}).applyTo('input-stops-1'), new InsideTrip.Checkbox({id: 'stops-2'}).applyTo('input-stops-2'), timeDepart = new InsideTrip.DoubleSlider('field-time-depart', { id: 'time-depart', hiddenName: this.tripQueryForm.findField('depart-time').getName() || 'timeDep', min: 0, max: 1440, granularity: 30, start: '0-1440', readout: { low: Ext.get('time-depart-low'), high: Ext.get('time-depart-high') }, readoutContainer: Ext.get('time-depart-low').findParent('.filter-readout', 2, true), refreshReadout: refreshTimeRangeReadout }), timeReturn = new InsideTrip.DoubleSlider('field-time-return', { id: 'time-return', hiddenName: this.tripQueryForm.findField('return-time').getName() || 'timeRtn', min: 0, max: 1440, granularity: 30, start: '0-1440', readout: { low: Ext.get('time-return-low'), high: Ext.get('time-return-high') }, readoutContainer: Ext.get('time-return-low').findParent('.filter-readout', 2, true), refreshReadout: refreshTimeRangeReadout }), connection = new InsideTrip.DoubleSlider('field-connection', { id: 'connection', hiddenName: 'connect', min: 10, max: 1440, granularity: 10, start: '0-1440', readout: { low: Ext.get('connection-low'), high: Ext.get('connection-high') }, readoutContainer: Ext.get('connection-low').findParent('.filter-readout', 2, true), refreshReadout: refreshDurationRangeReadout }) ); timeDepart.el.addClass('autocache'); timeReturn.el.addClass('autocache'); connection.el.addClass('autocache'); // attach listener to handle filter form change tripFilters.on( 'change', function() { Ext.apply(this.param, this.tripFilters.getValues()); this.param.page = 1; this.registerState(); }, this ); }, // end initTripFilters initTripFlags : function(formEl) { // create Form object to represent the form in DOM var tripFlags = this.tripFlags = new InsideTrip.FlagForm(formEl); tripFlags.add( new InsideTrip.Checkbox({category: 'airports', id: 'same-airports'}).applyTo('input-airports') ); // create caches for airport filters tripFlags.airports = { depart: [], connect: [], arrive: [] }; // create cache and methods for airline filters tripFlags.airlines = []; tripFlags.selectAirlinesAll = function(e) { var airlines = tripFlags.airlines; var changed = false; var a; for (var i = 0, len = airlines.length; i < len; i++) { a = tripFlags.findField('airline-' + airlines[i]); if (a) { changed = changed || !a.getValue(); a.setValue(true); } } if (changed) { tripFlags.fireEvent('change'); } if (e) { e.stopEvent(); return false; } }; tripFlags.selectAirlinesOnly = function(only, e) { var airlines = tripFlags.airlines; var changed = false; var a, newValue; for (var i = 0, len = airlines.length; i < len; i++) { a = tripFlags.findField('airline-' + airlines[i]); if (a) { newValue = (airlines[i]==only); changed = changed || (newValue == !a.getValue()); a.setValue(newValue); } } if (changed) { tripFlags.fireEvent('change'); } if (e) { e.stopEvent(); return false; } }; Ext.get('filter-airlines-selectall').on('click', tripFlags.selectAirlinesAll); // attach listener to handle filter form change tripFlags.on( 'change', function() { Ext.apply(this.param, this.tripFlags.getFlags(true)); this.param.page = 1; this.registerState(); }, this ); }, // end initTripFlags initTripDashboard : function(formEl) { // create Form object to represent the form in DOM var tripDashboard = this.tripDashboard = new InsideTrip.FlagForm( formEl, { defaultValues: InsideTrip.config.defaults.tripDashboard } ); // add in checkboxes tripDashboard.add( new InsideTrip.Checkbox({category: 'speed', id: 'speed-stops'} ).applyTo('input-speed-stops'), new InsideTrip.Checkbox({category: 'speed', id: 'speed-duration'} ).applyTo('input-speed-duration'), new InsideTrip.Checkbox({category: 'speed', id: 'speed-ontime'} ).applyTo('input-speed-ontime'), new InsideTrip.Checkbox({category: 'speed', id: 'speed-wait'} ).applyTo('input-speed-wait'), new InsideTrip.Checkbox({category: 'comfort', id: 'comfort-legroom'} ).applyTo('input-comfort-legroom'), new InsideTrip.Checkbox({category: 'comfort', id: 'comfort-aircraft-type'}).applyTo('input-comfort-aircraft-type'), new InsideTrip.Checkbox({category: 'comfort', id: 'comfort-aircraft-age'} ).applyTo('input-comfort-aircraft-age'), new InsideTrip.Checkbox({category: 'comfort', id: 'comfort-filled'} ).applyTo('input-comfort-filled'), new InsideTrip.Checkbox({category: 'ease', id: 'ease-connection'} ).applyTo('input-ease-connection'), new InsideTrip.Checkbox({category: 'ease', id: 'ease-routing'} ).applyTo('input-ease-routing'), new InsideTrip.Checkbox({category: 'ease', id: 'ease-lost-bags'} ).applyTo('input-ease-lost-bags'), new InsideTrip.Checkbox({category: 'ease', id: 'ease-gate'} ).applyTo('input-ease-gate') ); // wire "clear" and "select" links into flagAll() and unflagAll() functions on dashboard object formEl.child('.dashboard-clear-all a').on( 'click', function(e) { e.stopEvent(); this.unflagAll(); }, tripDashboard ); formEl.child('.dashboard-select-all a').on( 'click', function(e) { e.stopEvent(); this.flagAll(); }, tripDashboard ); // activate "recalculate" link Ext.get('trip-dashboard-recalculate').on( 'click', function(e) { e.stopEvent(); Ext.apply(this.param, this.tripDashboard.getFlags()); this.param.page = 1; this.registerState(); }, this // application context ); // activate hide/show link Ext.get('panel-trip-dashboard-head-link').on( 'click', function(e) { e.stopEvent(); var link = Ext.get(e.target).findParent('a', 2, true); if ( link.hasClass('x-dashboard-show') ) { Ext.get('trip-dashboard').setStyle('display', 'block'); link.replaceClass('x-dashboard-show', 'x-dashboard-hide'); } else { Ext.get('trip-dashboard').setStyle('display', 'none'); link.replaceClass('x-dashboard-hide', 'x-dashboard-show'); } } ); }, // end initTripDashboard conscribeTripFilters : function() { var filters = this.tripFilters; var flags = this.tripFlags; var tripQueryIndex = this.tripQueryIndex; var tripSet = this.trips[tripQueryIndex]; // are the filters already conscribed for this trip set? if( filters.el.hasClass('trips-' + tripQueryIndex) ) { // yes return; } // update filters form class filters.el.replaceClassIndexed('trips-', tripQueryIndex); // roundtrip or one-way trip set? var roundtrip = (typeof tripSet.returnTimeHigh == 'number'); // which number-of-stops options are relevant? if (tripSet.stopsLow > 0) { filters.findField('stops-0').disable(); filters.defaultValues['stops-0'] = false; } else { filters.findField('stops-0').enable(); filters.defaultValues['stops-0'] = true; } if (tripSet.stopsLow > 1 || tripSet.stopsHigh < 1) { filters.findField('stops-1').disable(); filters.defaultValues['stops-1'] = false; } else { filters.findField('stops-1').enable(); filters.defaultValues['stops-1'] = true; } if (tripSet.stopsHigh < 2) { filters.findField('stops-2').disable(); filters.defaultValues['stops-2'] = false; } else { filters.findField('stops-2').enable(); filters.defaultValues['stops-2'] = true; } // set departure day and time min and max var timeDepart = filters.findField('time-depart'); var granularity = timeDepart.granularity; var min = Math.floor(tripSet.departTimeLow / granularity) * granularity; var max = Math.ceil(tripSet.departTimeHigh / granularity) * granularity; timeDepart.setRange(min, max); // one-way or roundtrip? if (roundtrip) { // roundtrip // show return time var returnContainer = Ext.get('filter-time-return-container'); if (returnContainer.getStyle('display') != 'block') { Ext.get('filter-time-return-container').setStyle('display', 'block'); Ext.get('field-time-return-widget').setStyle('display', 'block'); // realign connection time widget filters.resynchSliders(); } // set return time min and max var timeReturn = filters.findField('time-return'); var granularity = timeReturn.granularity; var min = Math.floor(tripSet.returnTimeLow / granularity) * granularity; var max = Math.ceil(tripSet.returnTimeHigh / granularity) * granularity; timeReturn.setRange(min, max); } else { // one-way // hide return time var returnContainer = Ext.get('filter-time-return-container'); if (returnContainer.getStyle('display') != 'none') { Ext.get('filter-time-return-container').setStyle('display', 'none'); Ext.get('field-time-return-widget').setStyle('display', 'none'); // realign connection time widget filters.resynchSliders(); } } // set connection time min and max var connection = filters.findField('connection'); var granularity = connection.granularity; var min = Math.floor(tripSet.connectionLow / granularity) * granularity; var max = Math.ceil(tripSet.connectionHigh / granularity) * granularity; connection.setRange(min, max); // can leave/return same airports option be turned off? if (tripSet.airports.arrive.length==1 && tripSet.airports.depart.length==1) { flags.findField('same-airports').disable(); flags.defaultValues['same-airports'] = true; } else { flags.findField('same-airports').enable(); flags.defaultValues['same-airports'] = false; } // insert departure and arrival/return city names Ext.get('filter-airports-depart-city').update('Departure'); Ext.get('filter-airports-arrive-city').update(roundtrip ? 'Arrival/Return' : 'Arrival'); // clear out any old airports var airports = flags.airports; var apCode, apField, apSet, apEl; for (var apSetName in airports) { apSet = airports[apSetName]; while (apSet.length > 0) { apCode = apSet.pop(); // remove from filters form apField = flags.findField('airport-' + apSetName + '-' + apCode); if (apField) { flags.remove(apField); } // // remove from document and cache // apEl = Ext.get('field-filter-airport-' + apSetName + '-' + apCode); // if (apEl) { this.cache(apEl); } // remove from document apEl = Ext.get('field-filter-airport-' + apSetName + '-' + apCode); if (apEl) { apEl.remove(); } } } // clear out any old airlines var airlines = flags.airlines; var alCode, alField, alEl; while (airlines.length > 0) { alCode = airlines.pop(); // remove from flags form alField = flags.findField('airline-' + alCode); if (alField) { flags.remove(alField); } // remove from document and cache alEl = Ext.get('field-filter-airline-' + alCode); if (alEl) { this.cache(alEl); } } // add airport checkbox/entry elements var tripSetPorts = tripSet.airports; var apNames = tripSet.airportNames; var apSort = function(a, b) { return apNames[a] > apNames[b] ? 1 : -1; } var apTemplate = new Ext.Template( InsideTrip.template.filterAirport ); var apSet, apCode, container, apEl, apId; for (var apSetName in airports) { apSet = tripSetPorts[apSetName]; apSet.sort(apSort); container = Ext.get('filter-airports-' + apSetName + '-container'); for (var i = 0, len = apSet.length; i < len; i++) { apCode = apSet[i]; apId = 'airport-' + apSetName + '-' + apCode; apEl = this.retrieve('field-filter-' + apId); if (apEl) { apEl.appendTo(container); } else { apTemplate.append(container, { id: apId, airportCode: apCode, airportName: apNames[apCode] }); } airports[apSetName].push(apCode); flags.add( new InsideTrip.Checkbox({ id: apId, category: 'airports-' + apSetName }).applyTo('input-filter-' + apId) ); } } // add airline checkbox/entry elements to DOM and to filters form airlines = []; var airlineNames = tripSet.airlines; for (alCode in airlineNames) { airlines.push(alCode); } airlines.sort( function(a, b) { return a=='multiple' ? -1 : ( b=='multiple' ? 1 : ( airlineNames[a] > airlineNames[b] ? 1 : -1 ) ); } ); var airlineTemplate = new Ext.Template( InsideTrip.template.filterAirline ); var airlinesContainer = Ext.get('filter-airlines-container'); for (i = 0, len = airlines.length; i < len; i++) { alCode = airlines[i]; airlineTemplate.append(airlinesContainer, { airlineCode: alCode, airlineName: airlineNames[alCode] }); flags.add( new InsideTrip.Checkbox({ category: 'airlines', id: 'airline-' + alCode }).applyTo('input-filter-airline-' + alCode) ); Ext.get('field-filter-airline-' + alCode).child('a').on( 'click', flags.selectAirlinesOnly.createDelegate(this, [ alCode ], 0) ); } flags.airlines = airlines; }, // end conscribeTripFilters conscribeTripQuality : function() { var form = this.tripQualityForm; var trips = this.trips[ this.tripQueryIndex ]; // set TripQuality max Ext.get('tripQuality-high').update( trips.tripQualityHigh ); // set TripQuality min form.findField('tripQuality').setRange(trips.tripQualityLow, trips.tripQualityHigh); }, // end conscribeTripQuality snapTripQueryTimeRanges : function( params ) { // first, make sure we have the time range options var rangeOptions = this.timeRangeOptions; if (typeof rangeOptions != 'object') { // create collection of time range options from trip query form in DOM rangeOptions = this.timeRangeOptions = []; var timeSelectOptions = this.tripQueryForm.findField('depart-time').getEl().dom.options; for (var i = 0, len = timeSelectOptions.length; i < len; i++) { rangeOptions.push( timeSelectOptions[i].value ); } } if (typeof rangeOptions[0] != 'string') { // no range options were found, so return input values unchanged return params; } // copy params and snap time-range values if it has them var newParams = this.tripQueryForm.translateNames( params ); var timeRangeFieldNames = ['depart-time', 'return-time']; for (var i = 0, len = timeRangeFieldNames.length; i < len; i++) { var fieldName = timeRangeFieldNames[i]; var fieldValue = newParams[fieldName]; var valueMatch = false; if (typeof fieldValue == 'string') { for (var j = 0, len = rangeOptions.length; j < len; j++) { if (rangeOptions[j] == fieldValue) { valueMatch = true; break; } } if (!valueMatch) { newParams[fieldName] = rangeOptions[0]; } } } return newParams; }, // end snapTimeRange handleLogoClick : function(e) { e.stopEvent(); this.registerState(''); }, // end handleLogoClick handleRecentLinkClick : function(e) { e.stopEvent(); var href = Ext.get(e.target).findParent('a').getAttribute('href'); var queryStringStart = href.indexOf('#'); if (queryStringStart < 0) { queryStringStart = 0, href = '#' + href; } this.registerState( href.substr(queryStringStart + 1) ); }, // end handleRecentLinkClick handleTripQueryAction : function(form, action) { //alert('action.formQueryString = '+action.formQueryString); if ('submit'==action.type) { if ('client'==action.failureType || !form.isValid()) { //TODO: handle validation error //TODO: pop up context errors in the form //console.log('validation error'); } else { // form validated, last step before submission to back end var formQueryString = form.getQueryString(); action.formQueryString = formQueryString; // (preserving this because the state of the form itself may change) var needToSubmit; // how was this submit action initiated? if (!action.options.stateActivated) { // this submit action was initiated directly by the user, and has not yet passed through activateState() // extract depart and return times (which are set in the query form, but not included in the actual query) var depTimeField = form.findField('depart-time'); var filters = '&' + depTimeField.getName() + '=' + depTimeField.getValue(); var roundtrip = form.findField('journey-type-roundtrip').getValue(); if (roundtrip) { var rtnTimeField = form.findField('return-time'); filters += '&' + rtnTimeField.getName() + '=' + rtnTimeField.getValue(); } // is the submitted trip query the same as currently displayed? if ( ( this.tripQueryStrings[ formQueryString ] === this.tripQueryIndex ) && // check the base trip search ( this.param[ depTimeField.getName() ] == depTimeField.getValue() ) && // and check the depart/return times ( !roundtrip || (this.param[ rtnTimeField.getName() ] == rtnTimeField.getValue()) ) // (filters that can be set from the trip query form) ) { //TODO: behavior for attempt to resubmit current query } else { // register start ui state immediately before submit, so user can return easily if ('start'==this.param.ui) { Ext.apply(this.param, Ext.urlDecode(formQueryString)); //TODO: apply other forms this.registerState(); } // don't use cached results for manual form submission, so delete them if we have them this.deleteTripsByQueryString(formQueryString); } // open a new state for results of this query this.registerState( 'ui=results&' + formQueryString + filters ); // cancel submission (activateState() will take care of this later), but give the query string an index needToSubmit = false; } else { // this submit action is coming from activateState() // has this trip query been submitted by this application instance before? var tripQueryIndex = this.tripQueryStrings[formQueryString]; if (this.trips[tripQueryIndex] === undefined) { // no, it hasn't // prepare to submit this trip query needToSubmit = true; this.trips[tripQueryIndex] = null; // (nothing to put here yet, but null is not undefined!) } } // cancel submission if unnecessary return needToSubmit; } // end if submitted form validates } // end if action is 'submit' }, // end handleTripQueryAction() handleTripQueryResponse : function(form, action) { var json = action.result; var tripQueryIndex = this.tripQueryStrings[ action.formQueryString ]; // is this response for the trip query now displayed? var displayed = ('results'==this.param.ui && tripQueryIndex===this.tripQueryIndex); // check for errors if (!json || !json.success) { // revoke the trip query index for this query string, because we have nothing to store there delete this.tripQueryStrings[ action.formQueryString ]; // return to start ui with error message (unless the user has moved on somewhere else) if (displayed) { this.stateDirective = { err: ( json ? ( ( json.attributes && InsideTrip.config.orbitzErrorMessages[json.attributes.errorcode] ) ? InsideTrip.config.orbitzErrorMessages[json.attributes.errorcode](json.attributes) : ( (json.attributes && json.attributes.errormessage) ? json.attributes.errormessage : json.status ) ) : (action.response.statusText + "\nPlease try again.") ) }; this.registerState( action.formQueryString ); } return; } // parse and store results var drydock = Ext.get('drydock'); var results = new InsideTrip.TripStore({ data: json, queryString: action.formQueryString, domContainer: drydock, domIdRoot: 'trip-' + tripQueryIndex + '-', clickHandler: this.handleTripClick.createDelegate(this) }); if (results) { // load into trips collection this.trips[tripQueryIndex] = results; // cache new elements from the drydock if (drydock) { this.cache('#drydock .trip-result'); } // re-activate state, now with results ready if (displayed) { this.activateState(this.state); } } }, // end handleTripQueryResponse() handleTripSortClick : function(e) { var target = Ext.get(e.target).findParent('a', 2, true); e.stopEvent(); // get clicked sort column name var sortCol = target.id.split('trip-sort-'); sortCol = sortCol[1]; if (!sortCol) { return false; } // ignore clicks on disabled links if (target.hasClass('x-sort-disabled')) { return false; } // determine new sort direction var sortDir; if ( target.hasClass('x-sort-ASC') ) { // toggling from ASC sortDir = 'DESC'; } else if ( target.hasClass('x-sort-DESC') ) { // toggling from DESC sortDir = 'ASC'; } else { // switching to new sort column, so use default direction for the column if (sortCol=='score') { // score defaults to DESC sortDir = 'DESC'; } else { // all others default to ASC sortDir = 'ASC'; } } // translate column name to param format var tripFieldKeys = InsideTrip.config.data.tripFieldKeys; for (var key in tripFieldKeys) { if (tripFieldKeys[key] == sortCol) { sortCol = key; break; } } // set new sort parameters and register new state this.param.sort = sortCol; this.param.dir = sortDir; this.param.page = 1; this.registerState(); return false; }, // end handleTripSortClick handleTripClick : function(e) { var target = Ext.get(e.target); var result = target.findParent('.trip-result'); var tripId = result.id.split('-')[2]; var link; e.stopEvent(); if ( link = target.findParent('.trip-result-purchase') ) { // purchase link // show interstitial ui while browser follows link this.stateDirective = { ui: 'interstitial-purchase', purchaseUrl: target.findParent('a').href }; this.registerState(); return false; } var detailsOpened = target.findParent('.trip-result-with-details') ? true : false; var openDetails = false; var switchToCategory = ''; if ( target.findParent('.trip-result-breakdown') ) { // category score link // details panel needs to be open openDetails = true; // show selected category for (var cat in {speed:1, comfort:1, ease:1}) { if ( target.findParent('.trip-result-breakdown-' + cat) ) { switchToCategory = cat; break; } } } else if ( target.findParent('.trip-result-details-tabs') ) { // category tab link openDetails = true; link = target.findParent('td', 3, true); if (!link.hasClass('x-active-category-tab')) { for (var cat in {speed:1, comfort:1, ease:1}) { if (link.hasClass('trip-result-details-tab-' + cat)) { switchToCategory = cat; break; } } } } else if ( target.findParent('.trip-result-tripQuality', 3, true) ) { // details open link openDetails = true; } else if ( target.findParent('.trip-result-details-link', 3, true) ) { // details toggle link openDetails = !detailsOpened; } else if ( target.findParent('.trip-result-details-close', 2, true) ) { // details close link openDetails = false; } if (detailsOpened != openDetails) { // either open or close details for this trip result = Ext.get(result); link = result.child('.trip-result-details-link a'); var container = result.child('.trip-result-details-container'); if (openDetails) { // open the details // for IE, create new container if (Ext.isIE) { var newContainer = Ext.DomHelper.insertAfter(container, { tag: 'div', cls: 'trip-result-details-container' }, true); container.remove(); container = newContainer; } // generate the details markup for this trip and append to result row var detailsId = 'trip-details-' + this.tripQueryIndex + '-' + tripId; this.trips[ this.tripQueryIndex ].loadDomDetails(tripId, container); link.replaceClass('x-details-show', 'x-details-hide'); link.findParent('.trip-result', 10, true).addClass('trip-result-with-details'); if (Ext.isIE) { container.setStyle('position', 'static'); } } else { // close the details // remove details from result row result.child('.trip-result-details').remove(); link.replaceClass('x-details-hide', 'x-details-show'); link.findParent('.trip-result', 10, true).removeClass('trip-result-with-details'); if (Ext.isIE) { container.setStyle('position', 'absolute'); } } } if (switchToCategory) { // display the specified category score breakdown this.trips[ this.tripQueryIndex ].loadDomDetailsCategory(tripId, cat); } return false; }, // end handleTripClick handleTripPagingClick : function(e) { var target = Ext.get(e.target); var link = target.findParent('a'); var page = link.id.split('trip-results-page-'); page = page[1]; e.stopEvent(); if (!page) { return; } // translate jump-to links into page numbers var currentPage = parseInt(this.param.page, 10) || 1; switch (page) { case 'first': page = 1; break; case 'previous': page = currentPage - 1; break; case 'next': page = currentPage + 1; break; case 'last': page = 99; // there will never be this many pages break; } // navigate to selected page this.param.page = page; this.registerState(); }, // end handleTripPagingClick() initState : function() { // activate the initial state this.activateState( Ext.ux.History.getCurrentState(this.application) ); }, // end initState() activateState : function(state) { // if first activation and non-supported browser, throw up the unsupported-browser ui and stop if ( this.onFirstActivation && !(Ext.isGecko || Ext.isIE7 || Ext.isIE || Ext.isSafari) ) { // this.renderContentBlocks( InsideTrip.template.unsupportedBrowserUi ); // return; } // activate the state given as a query string if (!state) { if (Ext.isIE) { // getBookmarkedState() needs a tiny pause to return accurate results in IE if (this.activateDeferred) { delete this.activateDeferred; } else { this.activateDeferred = true; this.activateState.defer(10, this); return; } } // set default/initial state state = Ext.ux.History.getBookmarkedState(this.application) || // bookmarked hash this.getQueryString() // or URL query string InsideTrip.config.defaults.state || // or default state '' // or nothing ; } // set new state parameters in the application var param = Ext.urlDecode( state ); var directive = this.stateDirective; if (typeof directive == 'object') { Ext.apply(param, directive); this.stateDirective = null; // directives are strictly one-time } var uiChange = (this.param.ui && this.param.ui != param.ui); this.param = param; this.state = state; // ui parameter defaults to 'start' if (!param.ui) { param.ui = 'start'; } // follow application-specific state-setting procedure switch(this.application) { case 'trip-query': // trip search application // has the trip query form been initialized yet? var tripQueryForm = this.tripQueryForm; if (!tripQueryForm) { return; } // load default values and then state parameter values into the trip query form var tripQueryParams = this.snapTripQueryTimeRanges( param ); tripQueryForm.setValues( tripQueryParams ); // set up the UI stage to display switch (param.ui) { case 'results': // validate trip query in current state if (!tripQueryForm.isValid()) { param.ui = 'start'; this.tripQueryIndex = 0; break; } // set the state's tripQueryIndex from new state values, via the just-populated trip query form var tripQueryString = tripQueryForm.getQueryString(); var tripQueryIndex = this.tripQueryStrings[ tripQueryString ]; var tripQueryChange = (tripQueryIndex != this.tripQueryIndex); this.tripQueryIndex = tripQueryIndex; // has this trip query been submitted by this application instance before? if ( tripQueryIndex && this.trips[tripQueryIndex] && (this.trips[tripQueryIndex].secondsSinceLoad() < InsideTrip.config.data.tripRecordsExpiry) ) { // yes, and results have been loaded that are not expired // make sure results ui is shown, including "wait" message this.renderUI('results'); // add trip query to recent searches this.addTripRecent( param ); var query = this.tripQueryForm.translateNames( param ); // this.addTripRecent.defer(100, this, [ // query['depart-airport'], // query['arrive-airport'], // query['depart-date'], // query['journey-type-roundtrip'] ? query['return-date'] : null, // this.state // ]); // pause briefly (for browser to catch its breath) then process and display trip results this.activateTripResults.defer(10, this); } else { // no, either trip query hasn't been submitted or its results aren't back yet if (tripQueryIndex && this.trips[tripQueryIndex]) { // actually, we do have results but they're expired // delete expired results before submitting query anew this.deleteTripsByQueryString(tripQueryString); tripQueryIndex = null; } if ( !tripQueryIndex ) { // query has not in fact been submitted, so submit now // assign an index to the query string this.tripQueryStrings[tripQueryString] = this.tripQueryIndex = this.nextTripQueryIndex++;; // submit query to back end Ext.Ajax.timeout = InsideTrip.config.timeout.tripSearch * 1000; tripQueryForm.submit({ success: tripQueryForm.onResponse, failure: tripQueryForm.onResponse, params: tripQueryString, stateActivated: true // (handleTripQueryAction uses this to distinguish between automatic and user-initiated submit actions) }); this.preloadImageArray.defer(1500, this, [InsideTrip.config.preloadsInterstitial]); } // show interstitial for the wait this.renderUI('interstitial-search'); } break; // end case 'results' case 'start': // blank tripQueryIndex this.tripQueryIndex = 0; // render the start UI this.renderUI('start'); // initiate trip query entry by focusing the form's first input element tripQueryForm.findField('journey-type-roundtrip').focus(); break; case 'interstitial-purchase': this.renderUI('interstitial-purchase'); Ext.get('trip-purchase-transfer').on( 'click', function() { window.location = this.url; }, { url: param.purchaseUrl } ); break; } if (uiChange && Ext.isIE) { // re-apply trip query form values for IE tripQueryForm.setValues(tripQueryParams || param); } break; // end trip search application } // end switch(this.application) // are we activating state for the first time? if (this.onFirstActivation) { this.onFirstActivation(); this.onFirstActivation = null; // obviously, we only want to do this once } // track new state with Google Analytics pageTracker._trackPageview('?' + state); }, // end activateState() activateTripResults : function() { // make sure dashboard and filters forms are initialized if (!this.tripQualityForm) { this.initTripQuality( Ext.get('trip-tripQuality') ); } if (!this.tripFilters) { this.initTripFilters( Ext.get('trip-filters') ); } if (!this.tripFlags) { this.initTripFlags( Ext.get('trip-flags') ); } if (!this.tripDashboard) { this.initTripDashboard( Ext.get('trip-dashboard') ); } // (re)conscribe filters for this trips set this.conscribeTripFilters(); // load default+state parameter values into the dashboard and filters forms this.tripQualityForm.setValues( this.param ); this.tripFilters.setValues( this.param ); this.tripFlags.setValues( this.param ); this.tripDashboard.setValues( this.param ); // (re)score trips and (re)conscribe tripQuality var tripQueryIndex = this.tripQueryIndex; var trips = this.trips[tripQueryIndex]; trips.scoreTrips( this.tripDashboard.getValues(true) ); this.conscribeTripQuality(); // (re)filter trips trips.filterTrips( Ext.apply( this.tripFlags.getValues(true), this.tripFilters.translateNames(), this.tripQualityForm.getValues() ), this.tripFlags.getFlags() ); // sort trips and set sort links var oneway = (trips.returnTimeLow==null); if (oneway) { Ext.get('trip-results-head-return').select('a').addClass('x-sort-disabled'); } else { Ext.get('trip-results-head-return').select('a').removeClass('x-sort-disabled'); } this.param.sort = this.param.sort || 'price'; // sort field defaults to overall tripQuality this.param.dir = this.param.dir || 'ASC'; // sort order defaults to ascending Ext.get('trip-results-head').select('a').removeClass(['x-sort-ASC', 'x-sort-DESC']); var sortField = InsideTrip.config.data.tripFieldKeys[ this.param.sort ]; Ext.get( 'trip-sort-' + sortField ).addClass('x-sort-' + this.param.dir); trips.sort(sortField, this.param.dir); // set paging var tripCount = trips.getCount(); var startIndex, endIndex, pageCount; if (tripCount > 0) { var tripsPerPage = InsideTrip.config.tripResultsPerPage; pageCount = Math.ceil( tripCount / tripsPerPage ); var pagesContainer = Ext.get('trip-results-pages'); var pages = []; for (var i = 1; i <= pageCount; i++) { pages.push( '' + i + '' ); } pagesContainer.update( pages.join(' | ') ).select('a').on('click', this.handleTripPagingClick, this); var page = this.param.page || 1; if (page > pageCount) { this.param.page = page = pageCount; } Ext.get('trip-results-page-' + page).addClass('trip-results-page-current'); startIndex = tripsPerPage * (page - 1); endIndex = startIndex + tripsPerPage; if (page == 1) { Ext.get('trip-results-page-first').addClass('trip-results-page-disabled'); Ext.get('trip-results-page-previous').addClass('trip-results-page-disabled'); } else { Ext.get('trip-results-page-first').removeClass('trip-results-page-disabled'); Ext.get('trip-results-page-previous').removeClass('trip-results-page-disabled'); } if (page == pageCount) { endIndex = tripCount - 1; Ext.get('trip-results-page-next').addClass('trip-results-page-disabled'); Ext.get('trip-results-page-last').addClass('trip-results-page-disabled'); } else { Ext.get('trip-results-page-next').removeClass('trip-results-page-disabled'); Ext.get('trip-results-page-last').removeClass('trip-results-page-disabled'); } } else { // zero results to display this.param.page = pageCount = startIndex = endIndex = 0; Ext.get('trip-results-pages').update(''); } if (pageCount > 1) { Ext.get('trip-results-paging').show(); } else { Ext.get('trip-results-paging').hide(); } // remove "wait" message Ext.get('trip-results-wait').hide(); // cache any trip results currently inside the results container this.cache('#trip-results-list .trip-result'); // generate new results list from cache var results = Ext.get('trip-results-list'); results.update(''); var trips = this.trips[ this.tripQueryIndex ]; var records = trips.getRange(startIndex, endIndex); for (var i = 0, len = records.length; i < len; i++) { var tripId = records[i].id; var tripDomId = 'trip-' + this.tripQueryIndex + '-' + tripId; var tripRow = this.retrieve(tripDomId) || trips.loadDom(tripId); if (tripRow) { results.appendChild(tripRow); } } if (len == 0) { // all results have been filtered out results.update( InsideTrip.template.tripsAllFilteredOutMsg ); } }, // end activateTripResults() preloadImageArray : function(imgArray) { dh = Ext.DomHelper; for(var i = 0; i < imgArray.length; i++) { var newImgObj = new Object(); newImgObj.tag = 'img'; newImgObj.cls = 'preload'; newImgObj.src = imgArray[i]; var preloadImg = dh.append('drydock',newImgObj); } this.cache('#drydock .preload'); }, renderUI : function(ui) { var t = InsideTrip.template; // process application+ui state for new content switch(this.application) { case 'trip-query': // trip search application // interstitials don't show footer; other UIs do if ( ui.match('interstitial') ) { Ext.get('content-foot').hide(); } else { Ext.get('content-foot').show(); } switch(ui) { case 'start': // is basic markup for start ui in place? if ( 'trip-query-start' != document.body.id ) { // build trip query start ui in the document this.renderContentBlocks( t.tripStartUi ); // check trip query form into new container and make sure it's visible Ext.get('trip-query-container').appendChild( this.tripQueryForm.el ); Ext.get('trip-query').setStyle('display', 'block'); // check recent queries list into new container, decide whether to display, and fill in trips var recentContainer = Ext.get('trip-recent-container'); var recentList = this.retrieve('trip-recent-list'); var recentDateFormat = InsideTrip.template.tripRecentDateFormat; var recentTemplate = new Ext.Template( InsideTrip.template.tripRecentSearch ) var recentSearches = this.recentSearches; recentContainer.appendChild(recentList); this.cache('#trip-recent-list li'); if ( recentSearches.length > 0 ) { for (var i = 0, len = recentSearches.length; i < len; i++) { var recentParams = this.tripQueryForm.translateNames( recentSearches[i] ); var departPort = recentParams['depart-airport']; var departDate = recentParams['depart-date']; var arrivePort = recentParams['arrive-airport']; var returnDate = (recentParams['journey-type-roundtrip'] ? recentParams['return-date'] : 'oneway'); var recentId = 'trip-recent-' + departPort + '-' + departDate + '-' + arrivePort + '-' + returnDate; var recentEl = this.retrieve(recentId); if (recentEl) { recentEl.appendTo(recentList); } else { departDate = Date.parseDate(departDate, 'Y-m-d'); if (!departDate) { continue; } returnDate = Date.parseDate(returnDate, 'Y-m-d'); recentEl = recentTemplate.append(recentList, { id: recentId, dates: departDate.format(recentDateFormat) + (returnDate ? ('-' + returnDate.format(recentDateFormat)) : ''), 'depart-airport': departPort, 'arrive-airport': arrivePort, queryString: Ext.urlEncode( recentSearches[i] ) }, true); var a = recentEl.child('a'); a.on('click', this.handleRecentLinkClick, this); } } recentContainer.show(); } else { recentContainer.hide(); } // id document body document.body.id = 'trip-query-start'; //preload images if ( this.onFirstActivation ) { this.preloadImageArray.defer(3000, this, [InsideTrip.config.preloads]); } } break; case 'interstitial-search': if ( 'trip-query-interstitial-search' != document.body.id ) { this.renderContentBlocks( t.tripInterstitialSearchUi ); document.body.id = 'trip-query-interstitial-search'; } break; case 'interstitial-purchase': if ( 'trip-query-interstitial-purchase' != document.body.id ) { this.renderContentBlocks( t.tripInterstitialPurchaseUi ); document.body.id = 'trip-query-interstitial-purchase'; } break; case 'results': // make sure basic markup for results ui is in place if ( 'trip-query-results' != document.body.id ) { // need to build trip query results ui in the document this.renderContentBlocks( t.tripResultsUi ); // retrieve or generate head logo link var logoLink = this.retrieve('logo-link'); if (logoLink) { Ext.get('logo-container').appendChild(logoLink); } else { Ext.get('logo-container').update( t.logoLink ); Ext.get('logo-link').on('click', this.handleLogoClick, this); } // retrieve or generate filters and dashboard panels into new containers var filtersPanel = this.retrieve('panel-trip-filters'); if (filtersPanel) { Ext.get('filters-container').appendChild(filtersPanel); } else { Ext.get('filters-container').update( t.tripFilterPanel ); } var dashboardPanel = this.retrieve('panel-trip-dashboard'); if (dashboardPanel) { Ext.get('dashboard-container').appendChild(dashboardPanel); } else { Ext.get('dashboard-container').update( t.tripDashboard ); } // check trip query form into new container Ext.get('trip-query-container').appendChild( Ext.get(this.tripQueryForm.el) ); // attach listeners to "modify" and "hide" links Ext.get('trip-query-show').on('click', function(e) { Ext.get('trip-query-show').setStyle('display', 'none'); Ext.get('trip-query-hide').setStyle('display', 'block'); Ext.get('trip-query').setStyle('display', 'block'); this.tripFilters.resynchSliders(); e.stopEvent(); }, this); Ext.get('trip-query-hide').on('click', function(e) { Ext.get('trip-query-hide').setStyle('display', 'none'); Ext.get('trip-query-show').setStyle('display', 'block'); Ext.get('trip-query').setStyle('display', 'none'); this.tripFilters.resynchSliders(); e.stopEvent(); }, this); // retrieve or generate results list head into new container, and attach sort link listeners if needed var resultsHead = this.retrieve('trip-results-head'); var resultsContainer = Ext.get('trip-results-container'); if (resultsHead) { resultsContainer.appendChild(resultsHead); } else { resultsContainer.update( t.tripResultsHead ); Ext.get('trip-results-head').select('a').on('click', this.handleTripSortClick, this); } // create new results list container and paging container Ext.DomHelper.append(resultsContainer, { tag: 'div', id: 'trip-results-list' }, true); var pagingContainer = Ext.DomHelper.append(resultsContainer, { tag: 'div', id: 'trip-results-paging-container' }, true); pagingContainer.hide(); // we'll show this when we're ready for the user to see it // retrieve or generate results list paging, and attach link listeners if needed var resultsPaging = this.retrieve('trip-results-paging'); if (resultsPaging) { pagingContainer.appendChild(resultsPaging); } else { Ext.DomHelper.append(pagingContainer, t.tripResultsPaging); Ext.get('trip-results-paging').select('a').on('click', this.handleTripPagingClick, this); } // create "wait" message var waitMsg = new Ext.Layer({ id: 'trip-results-wait', constrain: true, shim: true }); waitMsg.hide().update(InsideTrip.template.tripResultsWaitMsg); // id document body document.body.id = 'trip-query-results'; } // show "wait" message for now this.retrieve('trip-results-wait').center().show(); // snap trip query form closed (hidden) Ext.get('trip-query').setStyle('display', 'none'); Ext.get('trip-query-hide').setStyle('display', 'none'); Ext.get('trip-query-show').setStyle('display', 'block'); // make sure slider widgets are checked into the document and slider positions are synched var docBody = Ext.get(document.body); var widget; if (this.tripQualityForm) { if ( (widget = this.retrieve('field-tripQuality-widget')) && !docBody.contains(widget) ) { widget.appendTo(docBody); } this.tripQualityForm.resynchSliders(); } if (this.tripFilters) { if ( (widget = this.retrieve('field-time-depart-widget')) && !docBody.contains(widget) ) { widget.appendTo(docBody); } if ( (widget = this.retrieve('field-time-return-widget')) && !docBody.contains(widget) ) { widget.appendTo(docBody); } if ( (widget = this.retrieve('field-connection-widget')) && !docBody.contains(widget) ) { widget.appendTo(docBody); } this.tripFilters.resynchSliders(); } // get current trips store var trips = this.trips[ this.tripQueryIndex ]; // update search synopsis if necessary var tripQuerySegment = Ext.get('segment-trip-query'); if ( !tripQuerySegment.hasClass('trips-' + this.tripQueryIndex) ) { var tripQuery = this.tripQueryForm.translateNames( this.tripQueryForm.translateValues(trips.queryString) ); Ext.get('synopsis-location-depart-name').update(trips.airportNames[ tripQuery['depart-airport'] ]); Ext.get('synopsis-location-depart-code').update(tripQuery['depart-airport']); Ext.get('synopsis-location-arrive-name').update(trips.airportNames[ tripQuery['arrive-airport'] ]); Ext.get('synopsis-location-arrive-code').update(tripQuery['arrive-airport']); var departDate = Date.parseDate( tripQuery['depart-date'], 'Y-m-d' ); var synopsisDates = departDate.format('D, n/d/y'); if ( tripQuery['journey-type-roundtrip'] ) { var returnDate = Date.parseDate( tripQuery['return-date'], 'Y-m-d' ); synopsisDates += ' - ' + returnDate.format('D, n/d/y'); } Ext.get('synopsis-dates').update(synopsisDates); Ext.get('synopsis-journey-type').update(tripQuery['journey-type-roundtrip'] ? 'Roundtrip' : 'One way'); Ext.get('synopsis-travelers').update(tripQuery['travelers'] + ' adult' + (tripQuery['travelers'] > 1 ? 's' : '')); Ext.get('synopsis-cabin').update(tripQuery['cabin']); tripQuerySegment.replaceClassIndexed('trips-', this.tripQueryIndex); } break; // end case results } // end switch ui if (this.param.err) { // display error message this.renderError(this.param.err, this.param.errField); } break; } // end switch application }, // end renderState() renderContentBlocks : function(blocks) { // cache autocache elements for safe keeping this.cache(); // write given markup into each content area ("block") var key, el; for (key in blocks) { el = Ext.get('content-' + key); if (el) { el.update( blocks[key] ); } } }, // end renderContentBlocks renderError : function(message, field) { //TODO: make a proper dialog and attach to field if given alert(message); }, // end renderError() registerState : function(state) { if (typeof state != 'string') { // state not given, so generate new state from current parameters state = Ext.urlEncode(this.param); } // is the new state the same as the current state (and there is no state directive) var isRedundant = (state==this.state); var hasStateDirective = (typeof this.stateDirective == 'object'); if (isRedundant && !hasStateDirective) { // requested state is already the current state, so do nothing return false; } // record state in application, extract application state parameters, and record state in history this.state = state; this.param = Ext.urlDecode(state); Ext.ux.History.navigate( this.application, this.state ); // is the new state redundant with a state directive? if (isRedundant && hasStateDirective) { // manually invoke activateState() this.activateState(); } }, // end registerState() deleteTripsByQueryString : function(queryString) { var queryIndex = this.tripQueryStrings[ queryString ]; if (queryIndex) { delete this.trips[ queryIndex ]; delete this.tripQueryStrings[ queryString ]; } } // end deleteTripsByQueryString() }); /** * Customized JsonStore for InsideTrip trip query results */ InsideTrip.TripStore = function(config) { Ext.apply(this, config, { airports: { depart: [], connect: [], arrive: [] }, airlines: {} }); var dummyData = {}; dummyData[ this.root ] = []; InsideTrip.TripStore.superclass.constructor.call(this, { data: this.data, fields: this.fields }); if (config.data) { this.loadTripsJson(config.data); } this.roundtrip = (this.returnTimeLow !== null); } Ext.extend(InsideTrip.TripStore, Ext.data.JsonStore, { fields: InsideTrip.config.data.tripFields, root: InsideTrip.config.data.tripRecordsRoot, scoreInfo: null, filterInfo: null, resultTemplate: new Ext.Template( InsideTrip.template.tripResult ), detailsTemplate: { Main: new Ext.Template( InsideTrip.template.tripResultDetails ), Flight: new Ext.Template( InsideTrip.template.tripResultsDetailsFlight ), Evaluator: new Ext.Template( InsideTrip.template.tripResultsDetailsFlightEvaluator ), FlightHeadFirst: new Ext.Template( InsideTrip.template.tripResultsDetailsFlightHeadFirst ), FlightHeadSub: new Ext.Template( InsideTrip.template.tripResultsDetailsFlightHeadSubsequent ) }, secondsSinceLoad : function() { if (this.loadTimestamp) { return Math.floor( this.loadTimestamp.getElapsed( new Date() ) / 1000 ); } else { return null; } }, loadTripsJson : function(json) { // overall trip-set attributes this.connectionLow = null; this.connectionHigh = null; this.departTimeLow = null; this.departTimeHigh = null; this.returnTimeLow = null; this.returnTimeHigh = null; this.stopsLow = null; this.stopsHigh = null; this.departDurationHigh = null; this.departDurationLow = null; this.returnDurationHigh = null; this.returnDurationLow = null; var allAirports = this.airports = { depart: [], connect: [], arrive: [] }; var allDepartPortsHash = {}, allConnectPortsHash = {}, allArrivePortsHash = {}; var allAirportNames = this.airportNames = {}; var allAirlines = this.airlines = {}; // timestamp this.loadTimestamp = new Date(); // load trip records from json var trips = []; var t = json[this.root]; var departTimeDepart, departTimeArrive, returnTimeDepart, returnTimeArrive; var connectionLow, connectionHigh, airline, airlines; var airports, departPorts, connectPorts, arrivePorts, departPortsHash, connectPortsHash, arrivePortsHash; var r, departDir, returnDir, connections, j, clen, stops, aName, aCode, a, airlinesHash, legs, llen, returnLegs, legAttr; for (var i = 0, len = t.length; i < len; i++) { r = t[i]; departDir = r.itrip_dirs[0]; returnDir = r.itrip_dirs[1]; departTimeDepart = this.dateStringToMinutes( departDir.legs[0].time_dep_str ); departTimeArrive = this.dateStringToMinutes( departDir.legs[departDir.legs.length - 1].time_arr_str ); returnTimeDepart = returnDir ? this.dateStringToMinutes( returnDir.legs[0].time_dep_str ) : null; returnTimeArrive = returnDir ? this.dateStringToMinutes( returnDir.legs[returnDir.legs.length - 1].time_arr_str ) : null; if (this.departTimeLow===null || this.departTimeLow > departTimeDepart) { this.departTimeLow = departTimeDepart; } if (this.departTimeHigh===null || this.departTimeHigh < departTimeDepart) { this.departTimeHigh = departTimeDepart; } if (returnDir) { if (this.returnTimeLow===null || this.returnTimeLow > returnTimeDepart) { this.returnTimeLow = returnTimeDepart; } if (this.returnTimeHigh===null || this.returnTimeHigh < returnTimeDepart) { this.returnTimeHigh = returnTimeDepart; } } connectionLow = 1440; connectionHigh = 0; connections = departDir.cnx; stops = connections.length; if (returnDir) { connections = connections.concat(returnDir.cnx); stops = (returnDir.cnx.length > stops) ? returnDir.cnx.length : stops; } departPortsHash = {}, connectPortsHash = {}, arrivePortsHash = {}; for (j = 0, clen = connections.length; j < clen; j++) { connectPortsHash[ connections[j].port ] = true; if (connectionLow > connections[j].dur) { connectionLow = connections[j].dur; } if (connectionHigh < connections[j].dur) { connectionHigh = connections[j].dur; } } if (connectionHigh == 0) { connectionHigh = connectionLow = null; } // no connections if (this.connectionLow===null || (connectionLow !== null && this.connectionLow > connectionLow)) { this.connectionLow = connectionLow; } if (this.connectionHigh===null || (connectionHigh !== null && this.connectionHigh < connectionHigh)) { this.connectionHigh = connectionHigh; } if (this.stopsLow===null || this.stopsLow > stops) { this.stopsLow = stops; } if (this.stopsHigh===null || this.stopsHigh < stops) { this.stopsHigh = stops; } airline = null; airlinesHash = {}; legs = departDir.legs; departPortsHash[ legs[0].port_dep ] = true; arrivePortsHash[ legs[legs.length - 1].port_arr ] = true; if (returnDir) { returnLegs = returnDir.legs; sameAirports = ( legs[legs.length - 1].port_arr == returnLegs[0].port_dep && legs[0].port_dep == returnLegs[returnLegs.length - 1].port_arr ); legs = legs.concat(returnLegs); llen = legs.length; arrivePortsHash[ returnLegs[0].port_dep ] = true; // note: "departure" and "arrival" cities/airports are defined in terms of the departure direction's origination and destination departPortsHash[ legs[llen - 1].port_arr ] = true; } else { llen = legs.length; sameAirports = true; } for (j = 0; j < llen; j++) { legAttr = legs[j].attributes; aName = legAttr.airline; aCode = legAttr.airline_code; if (aCode) { airline = (airline && airline != aCode) ? 'multiple' : aCode; airlinesHash[aCode] = true; if (aName) { allAirlines[aCode] = aName; } } } if ('multiple'==airline) { airlinesHash['multiple'] = true; allAirlines['multiple'] = 'Multiple carriers'; } airports = { depart: departPorts = [], connect: connectPorts = [], arrive: arrivePorts = [] }; for (a in departPortsHash) { departPorts.push(a); allDepartPortsHash[a] = true; } for (a in connectPortsHash) { connectPorts.push(a); allConnectPortsHash[a] = true; } for (a in arrivePortsHash) { arrivePorts.push(a); allArrivePortsHash[a] = true; } airlines = []; for (a in airlinesHash) { airlines.push(a); } if (this.departDurationLow===null || this.departDurationLow > departDir.dur) { this.departDurationLow = departDir.dur; } if (this.departDurationHigh===null || this.departDurationHigh < departDir.dur) { this.departDurationHigh = departDir.dur; } if (returnDir) { if (this.returnDurationLow===null || this.returnDurationLow > returnDir.dur) { this.returnDurationLow = returnDir.dur; } if (this.returnDurationHigh===null || this.returnDurationHigh < returnDir.dur) { this.returnDurationHigh = returnDir.dur; } } trips.push({ 'price': r.price, 'depart-time-depart': departTimeDepart, 'depart-time-arrive': departTimeArrive, 'depart-duration': departDir.dur, 'return-time-depart': returnTimeDepart, 'return-time-arrive': returnTimeArrive, 'return-duration': returnDir ? returnDir.dur : null, 'connection-low': connectionLow, 'connection-high': connectionHigh, 'stops': stops, 'airline': airline, 'airlines': airlines, 'airports': airports, 'same-airports': sameAirports, 'quality-pick': (r.attributes.qualityPick), json: r }); } this.loadData(trips); // aggregate trip-set airports if (json.airports) { var a = json.airports; for (i = 0, len = a.length; i < len; i++) { var code = a[i].attributes.code; var name = a[i].value; allAirportNames[code] = name; } } for (a in allDepartPortsHash) { allAirports.depart.push(a); } for (a in allConnectPortsHash) { allAirports.connect.push(a); } for (a in allArrivePortsHash) { allAirports.arrive.push(a); } }, // end loadTripsJson() dateStringToMinutes : function(string) { return parseInt(string.substr(0,2), 10) * 60 + parseInt(string.substr(3,2), 10); }, // end dateStringToMinutes() loadDom : function(tripId, container) { var template = this.resultTemplate; container = Ext.get(container || this.domContainer); if (!container || !template) { return false; } var idRoot = this.domIdRoot || 'trip-'; var elId = idRoot + tripId; var dom = template.append(container, { id: elId }, true); var r = this.getById(tripId); if (this.clickHandler) { var purchaseLink = dom.child('.trip-result-purchase a'); purchaseLink.on('click', this.clickHandler); purchaseLink.dom.setAttribute('href', r.get('json').buy); dom.child('.trip-result-details-link a').on('click', this.clickHandler); dom.child('.trip-result-tripQuality a').on('click', this.clickHandler); dom.select('.trip-result-breakdown a').on('click', this.clickHandler); } if (r.get('quality-pick')) { dom.addClass('trip-result-qualityPick'); } dom.child('.trip-result-price').update( '$' + (r.get('price') / 100) ); dom.child('.trip-result-airline').update( this.airlines[r.get('airline')] ); var maxLegDur = Math.max(this.departDurationHigh, this.returnDurationHigh); InsideTrip.template.tripGantt( dom.child('.trip-result-departure .trip-result-gantt'), r.get('json').itrip_dirs[0].legs, maxLegDur, this.airportNames ); dom.child('.trip-result-departure .trip-result-duration span').update( InsideTrip.template.minutesToDuration( r.get('depart-duration') ) ); if (this.roundtrip) { InsideTrip.template.tripGantt( dom.child('.trip-result-return .trip-result-gantt'), r.get('json').itrip_dirs[1].legs, maxLegDur, this.airportNames ); dom.child('.trip-result-return .trip-result-duration span').update( InsideTrip.template.minutesToDuration( r.get('return-duration') ) ); } else { // for oneway trips var returnEl = dom.child('.trip-result-return'); returnEl.addClass('trip-result-noreturn'); returnEl.child('.trip-result-gantt').update('No return flight.').replaceClass('trip-result-gantt', 'trip-result-nogantt'); } dom.select('.gantt-segment').addClassOnOver('x-gantt-highlight'); dom.select('.gantt-segment-connection').addClassOnOver('x-gantt-highlight'); // insert scoring data this.loadDomScores(dom, r); return dom; }, // end loadDom() loadDomScores : function(dom, record) { var scoringFlags = this.scoringFlags; if (typeof scoringFlags != 'object') { return; } var detailsEl = dom.child('.trip-result-details'); var score, bracket; for (var category in scoringFlags) { score = record.get('score-' + category); bracket = ( score === null ? '0' : Math.min(Math.floor(score/20)+1, 5) ); dom.child('.trip-result-breakdown-' + category + ' span').update(score).replaceClassIndexed('score-level-', bracket); if (detailsEl) { detailsEl.child('.trip-result-details-tab-' + category).replaceClassIndexed('score-level-', bracket); } } dom.child('.trip-result-departure .trip-result-direction-score span').update( record.get('score-depart') ); dom.child('.trip-result-return .trip-result-direction-score span').update( record.get('score-return') ); dom.child('.trip-result-tripQuality-score').update( record.get('score') ); }, // end loadDomScores loadDomDetails : function(id, container) { container = Ext.get(container || this.domContainer); var r = this.getById(id); if (!r || !container) { return false; } id = this.domIdRoot + 'details-' + r.id; var dom = this.detailsTemplate.Main.append(container, { id: id }, true); // links if (this.clickHandler) { dom.child('.trip-result-details-close').on('click', this.clickHandler); dom.child('.trip-result-details-tab-speed a').on('click', this.clickHandler); dom.child('.trip-result-details-tab-comfort a').on('click', this.clickHandler); dom.child('.trip-result-details-tab-ease a').on('click', this.clickHandler); } // trip summary var tripQuery = app.tripQueryForm.translateNames(this.queryString); var roundtrip = !tripQuery['journey-type-oneway']; var dirs = r.get('json').itrip_dirs; var imgUrl = InsideTrip.config.url.airlineImages + '/' + r.get('airline') + '_logo.gif'; dom.child('.trip-result-details-summary img').dom.setAttribute('src', imgUrl); dom.child('.trip-result-details-departure-city').update(this.airportNames[ dirs[0].legs[0].port_dep ]); dom.child('.trip-result-details-arrival-city').update(this.airportNames[ dirs[0].legs[ dirs[0].legs.length - 1 ].port_arr ]); var dates = Date.parseDate(dirs[0].legs[0].date_dep_str.substr(0,10), 'Y-m-d').format('D, M j, Y'); if (roundtrip) { dates += ' - ' + Date.parseDate(dirs[1].legs[0].date_dep_str.substr(0,10), 'Y-m-d').format('D, M j, Y'); } dom.child('.trip-result-details-summary-dates').update(dates); dom.child('.trip-result-details-journey-type').update(roundtrip ? 'Roundtrip' : 'One Way'); var travelers = tripQuery.travelers; dom.child('.trip-result-details-travelers').update( travelers + ' adult' + (travelers > 1 ? 's' : '') ); // category tabs for (var category in {speed:1, comfort:1, ease:1}) { score = r.get('score-' + category); dom.child('.trip-result-details-tab-' + category).replaceClassIndexed( 'score-level-', score === null ? '0' : Math.min(Math.floor(score/20)+1, 5) ); } // trip legs var tbody = dom.child('.trip-result-details-table-body'); var legs, connections, flight, head, layoverLength, time; for (var d = 0, dlen = dirs.length; d < dlen; d++) { legs = dirs[d].legs; connections = dirs[d].cnx; for (var i = 0, len = legs.length; i < len; i++) { flight = this.detailsTemplate.Flight.append(tbody, {}, true); head = flight.child('.trip-result-details-flight-head'); if (i==0) { // first leg of direction flight.addClass('trip-result-details-flight-first'); head = this.detailsTemplate.FlightHeadFirst.append(head, {}, true); head.child('.trip-result-details-flight-direction').update( d==0 ? 'Departure' : 'Return' ); head.child('.trip-result-details-flight-date').update( Date.parseDate(legs[i].date_dep_str.substr(0,10), 'Y-m-d').format('m/d/y') ); } else { // subsequent leg head = this.detailsTemplate.FlightHeadSub.append(head, {}, true); layoverLength = InsideTrip.template.minutesToDurationLong( connections[i - 1].dur ); head.child('.trip-result-details-flight-layover-length').update(layoverLength); } flight.child('.trip-result-details-flight-itinerary-airline').update( legs[i].attributes.airline ); flight.child('.trip-result-details-flight-itinerary-number').update( legs[i].attributes.fnum ); time = legs[i].time_dep_str; time = InsideTrip.template.minutesToTimeLong( parseInt(time.substr(0,2), 10) * 60 + parseInt(time.substr(3,2), 10) ); flight.child('.trip-result-details-flight-itinerary-depart-time').update(time); flight.child('.trip-result-details-flight-itinerary-depart-code').update( legs[i].port_dep ); flight.child('.trip-result-details-flight-itinerary-depart-city').update( this.airportNames[legs[i].port_dep] ); time = legs[i].time_arr_str; time = InsideTrip.template.minutesToTimeLong( parseInt(time.substr(0,2), 10) * 60 + parseInt(time.substr(3,2), 10) ); flight.child('.trip-result-details-flight-itinerary-arrive-time').update(time); flight.child('.trip-result-details-flight-itinerary-arrive-code').update( legs[i].port_arr ); flight.child('.trip-result-details-flight-itinerary-arrive-city').update( this.airportNames[legs[i].port_arr] ); } } // activate the Speed tab by default this.loadDomDetailsCategory(r.id, 'speed'); return dom; }, // end loadDomDetails() loadDomDetailsCategory : function(id, cat) { var r = this.getById(id); if (!r) { return false; } var domId = this.domIdRoot + 'details-' + r.id; var dom = Ext.get(domId); if (!dom) { return false; } // switch active category tab dom.select('.x-active-category-tab').removeClass('x-active-category-tab'); dom.child('.trip-result-details-tab-' + cat).addClass('x-active-category-tab'); // evaluator headings var heads = dom.query('.trip-result-details-evaluator-head'); var evalNames = InsideTrip.template.tripEvaluators[cat]; var i = 0; for (var evaluator in evalNames) { Ext.get(heads[i]).update( evalNames[evaluator] ); i++; } // clear out any old evaluators dom.select('.trip-result-details-evaluator').remove(); // flight rows var flights = dom.query('.trip-result-details-flight'); var dirs = r.get('json').itrip_dirs; var findEval = function(key, collection) { var rtnCollection = null; for (var i = 0, len = collection.length; i < len; i++) { if (collection[i].attributes.key == key) { if ( rtnCollection == null ) rtnCollection = Array(); var ord = 0; if ( collection[i].attributes.ord ) ord = collection[i].attributes.ord; rtnCollection [ord]= collection[i].attributes; } } return rtnCollection; }; var tripEvalKeys = InsideTrip.config.data.tripEvalKeys; var dirEvals, legEvals, row, col, rowspan, data, cell; var r = 0; for (var d = 0, dlen = dirs.length; d < dlen; d++) { legs = dirs[d].legs; dirEvals = dirs[d].evals; for (var i = 0, len = legs.length; i < len; i++, r++) { legEvals = legs[i].evals; row = Ext.get(flights[r]); col = 0; for (evaluator in evalNames) { col++; if ( dataCollection = findEval(tripEvalKeys[evaluator], dirEvals) ) { // per-direction evaluator data if (i >= dataCollection.length) { continue; } // no additional cells in this column if (dataCollection.length > 1 ) rowspan = 1; else rowspan = len; data = dataCollection[i]; } else { // per-leg evaluator data rowspan = 1; dataCollection = findEval(tripEvalKeys[evaluator], legEvals); if ( dataCollection ) data = dataCollection[0]; else data = { val: 0, max: 0, text: 'N/A' }; } cell = this.detailsTemplate.Evaluator.append(row, {}, true); cell.dom.setAttribute(Ext.isIE ? 'rowSpan' : 'rowspan', rowspan); cell.addClass( 'trip-result-details-evaluator-col-' + col ); var scoreLevel = ( data.max == 0 ? '0' : Math.min(Math.floor(data.val / data.max * 5)+1, 5) ); cell.addClass( 'score-level-' + scoreLevel ); if ( (rowspan == len || i == (len - 1)) && d == dlen - 1 ) { cell.addClass('trip-result-details-evaluator-bottom'); } cell.child('.trip-result-details-evaluator-class').update(data.text); cell.child('.trip-result-details-evaluator-description').update(data.raw); } // end for each evaluator } // end for each leg/flight } // end for each direction }, // end loadDomDetailsCategory sort : function(fieldName, dir) { // check that this isn't a redundant call, then call superclass sort var sortInfo = this.sortInfo; if ( typeof sortInfo != 'object' || sortInfo.field != fieldName || sortInfo.direction != dir ) { return InsideTrip.TripStore.superclass.sort.call(this, fieldName, dir); } }, // end sort() scoreTrips : function(flagsByCategory) { // update scoring flags this.scoringFlags = flagsByCategory; // prepare scoring criteria var flagsAll = {}; for (var cat in flagsByCategory) { Ext.apply(flagsAll, flagsByCategory[cat]); } if ( this.scoreInfo && Ext.urlEncode(this.scoreInfo)==Ext.urlEncode(flagsAll) ) { // trips are already scored to these criteria return; } this.scoreInfo = flagsAll; // score all trips this.tripQualityLow = this.tripQualityHigh = null; var mean = function(a) { var sum = 0; for (var count = 0, len = a.length; count < len; count++) { sum += a[count]; } return count ? (sum/count) : null; }; var data = this.snapshot || this.data; data.each( function(record) { // score complete unfiltered set (stored in snapshot) var json = record.get('json'); if (!json) { return; } // direction scores var directions = json.itrip_dirs; var directionQuality = []; var evalScores = {}; for (var i = 0, lena = directions.length; i < lena; i++) { // for each direction var d = directions[i]; var dirScore_points = 0; var dirScore_possible = 0; for (var evalName in flagsAll) { // for each evaluator score // ignore dashboard-excluded evaluators if (!flagsAll[evalName]) { continue; } // translate evaluator name to json key var key = InsideTrip.config.data.tripEvalKeys[evalName]; // add evaluator into direction quality dirScore_points += d.ts[key]; dirScore_possible += d.ts_pos[key]; // add this evaluator score into total evaluator score if (!evalScores[evalName]) { evalScores[evalName] = { points: 0, possible: 0 }; } evalScores[evalName].points += d.ts[key]; evalScores[evalName].possible += d.ts_pos[key]; } var dirScore = ( dirScore_possible ? (dirScore_points / dirScore_possible * 100) : null ); directionQuality[i] = dirScore ? Math.round(dirScore) : 0; } record.set('score-depart', directionQuality[0]); record.set('score-return', directionQuality[1] || null); // overall score var tripQuality = mean(directionQuality); tripQuality = tripQuality ? Math.round(tripQuality) : null; record.set('score', tripQuality); if (this.tripQualityLow === null || tripQuality < this.tripQualityLow) { this.tripQualityLow = tripQuality; } if (this.tripQualityHigh === null || tripQuality > this.tripQualityHigh) { this.tripQualityHigh = tripQuality; } // category scores for (var category in flagsByCategory) { var catScore_points = 0, catScore_possible = 0; for (var evalName in flagsByCategory[category]) { var evalScore = evalScores[evalName]; if (evalScore) { catScore_points += evalScore.points; catScore_possible += evalScore.possible; } } var catScore = (catScore_possible ? (catScore_points / catScore_possible * 100) : null); record.set( ('score-' + category), catScore_possible ? (Math.round(catScore) || 0) : null ); } // update scores in dom var dom = ( app ? app.retrieve(this.domIdRoot + record.id) : Ext.get(this.domIdRoot + record.id) ); if (dom) { this.loadDomScores(dom, record); } }, this ); // end each trip record (execute in this TripStore scope) }, // end scoreTrips filterTrips : function(f, flags) { // separate out departure time range low and high var timeDepartRange = f['time-depart'].split('-'); f['time-depart-low'] = timeDepartRange[0]; f['time-depart-high'] = timeDepartRange[1]; // separate out return time range low and high var timeReturnRange = f['time-return'].split('-'); f['time-return-low'] = timeReturnRange[0]; f['time-return-high'] = timeReturnRange[1]; // separate out return time range low and high var connectionRange = f['connection'].split('-'); f['connection-low'] = connectionRange[0]; f['connection-high'] = connectionRange[1]; // are these trips already filtered on these criteria? if ( this.filterInfo && Ext.urlEncode(this.filterInfo)==Ext.urlEncode(f) && this.flagInfo && Ext.urlEncode(this.flagInfo)==Ext.urlEncode(flags) ) { // trips are already scored to these criteria return; } this.filterInfo = f; this.flagInfo = flags; // filter all trips this.clearFilter(true); this.filterBy( this._filterTrips ); }, // end filterTrips _filterTrips : function(r) { var f = this.filterInfo; // filter on tripQuality score var tripQuality = r.get('score'); if (typeof tripQuality == 'number' && tripQuality < f.tripQuality) { return false; } // filter on stops var stops = r.get('stops'); if (!f['stops-0'] && stops===0) { return false; } if (!f['stops-1'] && stops===1) { return false; } if (!f['stops-2'] && stops >= 2) { return false; } // filter on departure time var departTimeDepart = r.get('depart-time-depart'); if ( departTimeDepart < f['time-depart-low'] ) { return false; } if ( departTimeDepart > f['time-depart-high'] ) { return false; } // filter on return time var returnTimeDepart = r.get('return-time-depart'); if ( typeof returnTimeDepart == 'number' && ( returnTimeDepart < f['time-return-low'] || returnTimeDepart > f['time-return-high'] ) ) { return false; } // filter on connection length var connectionLow = r.get('connection-low'); var connectionHigh = r.get('connection-high'); if ( connectionLow && ( connectionLow < f['connection-low'] ) ) { return false; } if ( connectionHigh && (connectionHigh > f['connection-high']) ) { return false; } // filter on arrive/depart same airport if ( f['airports']['same-airports'] && !r.get('same-airports') ) { return false; } // filter on airports var airports = r.get('airports'); for (var apSetName in airports) { var apSet = airports[apSetName]; var apSetFilter = f['airports-' + apSetName]; for (var i = 0, len = apSet.length; i < len; i++) { if (!apSetFilter[ 'airport-' + apSetName + '-' + apSet[i] ]) { return false; }