<commit_before>/**
 * Attaches CustomEvent 'component-library-analytics' listener to document.body
 * to translate component library actions into analytics dataLayer events.
 */
import _recordEvent from 'platform/monitoring/record-event';
import { getSectionLabel } from 'applications/static-pages/subscription-creators/subscribeAccordionEvents';

const analyticsEvents = {
  Modal: [{ action: 'show', event: 'int-modal-show', prefix: 'modal' }],
  Accordion: [
    { action: 'expand', event: 'int-accordion-expand', prefix: 'accordion' },
    {
      action: 'collapse',
      event: 'int-accordion-collapse',
      prefix: 'accordion',
    },
  ],
  AdditionalInfo: [
    {
      action: 'expand',
      event: 'int-additional-info-expand',
      prefix: 'additional-info',
    },
    {
      action: 'collapse',
      event: 'int-additional-info-collapse',
      prefix: 'additional-info',
    },
  ],
  AlertBox: [
    {
      action: 'linkClick',
      event: 'nav-alert-box-link-click',
      prefix: 'alert-box',
    },
  ],
  Breadcrumbs: [
    {
      action: 'linkClick',
      event: 'nav-breadcrumb-link-click',
      prefix: 'breadcrumbs',
    },
  ],
  Checkbox: [
    {
      action: 'change',
      event: 'int-checkbox-option-click',
      prefix: 'checkbox',
    },
  ],
  CheckboxGroup: [
    {
      action: 'click',
      event: 'int-checkbox-group-option-click',
      prefix: 'checkbox-group',
    },
  ],
  LoadingIndicator: [
    {
      action: 'displayed',
      event: 'loading-indicator-displayed',
      prefix: 'loading-indicator',
    },
  ],
  PromoBanner: [
    {
      action: 'linkClick',
      event: 'nav-promo-banner-link-click',
      prefix: 'promo-banner',
    },
  ],
  RadioButtons: [
    {
      action: 'change',
      event: 'int-radio-button-option-click',
      prefix: 'radio-button',
    },
  ],
  Select: [
    {
      action: 'change',
      event: 'int-select-box-option-click',
      prefix: 'select',
    },
  ],
  TextArea: [
    { action: 'blur', event: 'int-text-area-blur', prefix: 'text-area' },
  ],
  TextInput: [
    { action: 'blur', event: 'int-text-input-blur', prefix: 'text-input' },
  ],
  'va-checkbox': [
    {
      action: 'change',
      event: 'int-checkbox-option-click',
      prefix: 'checkbox',
    },
  ],
  'va-text-input': [
    { action: 'blur', event: 'int-text-input-blur', prefix: 'text-input' },
  ],
  'va-accordion': [
    { action: 'expand', event: 'int-accordion-expand', prefix: 'accordion' },
    {
      action: 'collapse',
      event: 'int-accordion-collapse',
      prefix: 'accordion',
    },
  ],
  'va-additional-info': [
    {
      action: 'expand',
      event: 'int-additional-info-expand',
      prefix: 'additional-info',
    },
    {
      action: 'collapse',
      event: 'int-additional-info-collapse',
      prefix: 'additional-info',
    },
  ],
  'va-alert': [
    {
      action: 'linkClick',
      event: 'nav-alert-box-link-click',
      prefix: 'alert-box',
    },
  ],
  'va-alert-expandable': [
    {
      action: 'expand',
      event: 'int-alert-expandable-expand',
      prefix: 'alert-expandable',
    },
    {
      action: 'collapse',
      event: 'int-alert-expandable-collapse',
      prefix: 'alert-expandable',
    },
  ],
  'va-breadcrumbs': [
    {
      action: 'linkClick',
      event: 'nav-breadcrumb-link-click',
      prefix: 'breadcrumbs',
    },
  ],
  'va-checkbox-group': [
    {
      action: 'change',
      event: 'int-checkbox-group-option-click',
      prefix: 'checkbox-group',
    },
  ],
  'va-loading-indicator': [
    {
      action: 'displayed',
      event: 'loading-indicator-displayed',
      prefix: 'loading-indicator',
    },
  ],
  'va-modal': [
    {
      action: 'show',
      event: 'int-modal-show',
      prefix: 'modal',
    },
  ],
  'va-radio': [
    {
      action: 'change',
      event: 'int-radio-button-option-click',
      prefix: 'radio-button',
    },
  ],
  'va-select': [
    {
      action: 'change',
      event: 'int-select-box-option-click',
      prefix: 'select',
    },
  ],
  'va-progress-bar': [
    {
      action: 'change',
      event: 'nav-progress-bar-change',
      prefix: 'progress-bar',
    },
  ],
  'va-segmented-progress-bar': [
    {
      action: 'change',
      event: 'nav-segmented-progress-bar-change',
      prefix: 'segmented-progress-bar',
    },
  ],
  'va-on-this-page': [
    {
      action: 'click',
      event: 'nav-jumplink-click',
    },
  ],
};

export function subscribeComponentAnalyticsEvents(
  e,
  recordEvent = _recordEvent,
) {
  // Is it a component we are tracking?
  const component = analyticsEvents[e.detail.componentName];

  if (component) {
    const action = component.find(ev => ev.action === e.detail.action);
    const { version } = e.detail;

    if (action) {
      const dataLayer = {
        event: action.event,
        'event-source': 'component-library',
        'component-library-version': version,
      };

      // If the event included additional details / context...
      if (e.detail.details) {
        for (const key of Object.keys(e.detail.details)) {
          const newKey = action.prefix ? `${action.prefix}-${key}` : key;

          dataLayer[newKey] = e.detail.details[key];
        }
      }

      if (
        ['int-accordion-expand', 'int-accordion-collapse'].includes(
          action.event,
        )
      ) {
        dataLayer['accordion-section-label'] = getSectionLabel(e.target);
      }

      recordEvent(dataLayer);

      // Reset dataLayer properties to undefined with a new push
      // so they don't bleed into other event.

      const clearedDataLayer = { ...dataLayer };

      // Remove event property
      if (Object.prototype.hasOwnProperty.call(clearedDataLayer, 'event')) {
        delete clearedDataLayer.event;
      }

      // Remove gtm.uniqueEventId property
      if (
        Object.prototype.hasOwnProperty.call(
          clearedDataLayer,
          'gtm.uniqueEventId',
        )
      ) {
        delete clearedDataLayer['gtm.uniqueEventId'];
      }

      // Set everything else to undefined
      Object.keys(clearedDataLayer).forEach(key => {
        clearedDataLayer[key] = undefined;
      });

      recordEvent(clearedDataLayer);
    }
  }
}

document.body.addEventListener(
  'component-library-analytics',
  subscribeComponentAnalyticsEvents,
);
<commit_msg>Add va-promo-banner to analyticsEvents dictionary (#21923)<commit_after>/**
 * Attaches CustomEvent 'component-library-analytics' listener to document.body
 * to translate component library actions into analytics dataLayer events.
 */
import _recordEvent from 'platform/monitoring/record-event';
import { getSectionLabel } from 'applications/static-pages/subscription-creators/subscribeAccordionEvents';

const analyticsEvents = {
  Modal: [{ action: 'show', event: 'int-modal-show', prefix: 'modal' }],
  Accordion: [
    { action: 'expand', event: 'int-accordion-expand', prefix: 'accordion' },
    {
      action: 'collapse',
      event: 'int-accordion-collapse',
      prefix: 'accordion',
    },
  ],
  AdditionalInfo: [
    {
      action: 'expand',
      event: 'int-additional-info-expand',
      prefix: 'additional-info',
    },
    {
      action: 'collapse',
      event: 'int-additional-info-collapse',
      prefix: 'additional-info',
    },
  ],
  AlertBox: [
    {
      action: 'linkClick',
      event: 'nav-alert-box-link-click',
      prefix: 'alert-box',
    },
  ],
  Breadcrumbs: [
    {
      action: 'linkClick',
      event: 'nav-breadcrumb-link-click',
      prefix: 'breadcrumbs',
    },
  ],
  Checkbox: [
    {
      action: 'change',
      event: 'int-checkbox-option-click',
      prefix: 'checkbox',
    },
  ],
  CheckboxGroup: [
    {
      action: 'click',
      event: 'int-checkbox-group-option-click',
      prefix: 'checkbox-group',
    },
  ],
  LoadingIndicator: [
    {
      action: 'displayed',
      event: 'loading-indicator-displayed',
      prefix: 'loading-indicator',
    },
  ],
  RadioButtons: [
    {
      action: 'change',
      event: 'int-radio-button-option-click',
      prefix: 'radio-button',
    },
  ],
  Select: [
    {
      action: 'change',
      event: 'int-select-box-option-click',
      prefix: 'select',
    },
  ],
  TextArea: [
    { action: 'blur', event: 'int-text-area-blur', prefix: 'text-area' },
  ],
  TextInput: [
    { action: 'blur', event: 'int-text-input-blur', prefix: 'text-input' },
  ],
  'va-checkbox': [
    {
      action: 'change',
      event: 'int-checkbox-option-click',
      prefix: 'checkbox',
    },
  ],
  'va-text-input': [
    { action: 'blur', event: 'int-text-input-blur', prefix: 'text-input' },
  ],
  'va-accordion': [
    { action: 'expand', event: 'int-accordion-expand', prefix: 'accordion' },
    {
      action: 'collapse',
      event: 'int-accordion-collapse',
      prefix: 'accordion',
    },
  ],
  'va-additional-info': [
    {
      action: 'expand',
      event: 'int-additional-info-expand',
      prefix: 'additional-info',
    },
    {
      action: 'collapse',
      event: 'int-additional-info-collapse',
      prefix: 'additional-info',
    },
  ],
  'va-alert': [
    {
      action: 'linkClick',
      event: 'nav-alert-box-link-click',
      prefix: 'alert-box',
    },
  ],
  'va-alert-expandable': [
    {
      action: 'expand',
      event: 'int-alert-expandable-expand',
      prefix: 'alert-expandable',
    },
    {
      action: 'collapse',
      event: 'int-alert-expandable-collapse',
      prefix: 'alert-expandable',
    },
  ],
  'va-breadcrumbs': [
    {
      action: 'linkClick',
      event: 'nav-breadcrumb-link-click',
      prefix: 'breadcrumbs',
    },
  ],
  'va-checkbox-group': [
    {
      action: 'change',
      event: 'int-checkbox-group-option-click',
      prefix: 'checkbox-group',
    },
  ],
  'va-loading-indicator': [
    {
      action: 'displayed',
      event: 'loading-indicator-displayed',
      prefix: 'loading-indicator',
    },
  ],
  'va-modal': [
    {
      action: 'show',
      event: 'int-modal-show',
      prefix: 'modal',
    },
  ],
  'va-promo-banner': [
    {
      action: 'linkClick',
      event: 'nav-promo-banner-link-click',
      prefix: 'promo-banner',
    },
  ],
  'va-radio': [
    {
      action: 'change',
      event: 'int-radio-button-option-click',
      prefix: 'radio-button',
    },
  ],
  'va-select': [
    {
      action: 'change',
      event: 'int-select-box-option-click',
      prefix: 'select',
    },
  ],
  'va-progress-bar': [
    {
      action: 'change',
      event: 'nav-progress-bar-change',
      prefix: 'progress-bar',
    },
  ],
  'va-segmented-progress-bar': [
    {
      action: 'change',
      event: 'nav-segmented-progress-bar-change',
      prefix: 'segmented-progress-bar',
    },
  ],
  'va-on-this-page': [
    {
      action: 'click',
      event: 'nav-jumplink-click',
    },
  ],
};

export function subscribeComponentAnalyticsEvents(
  e,
  recordEvent = _recordEvent,
) {
  // Is it a component we are tracking?
  const component = analyticsEvents[e.detail.componentName];

  if (component) {
    const action = component.find(ev => ev.action === e.detail.action);
    const { version } = e.detail;

    if (action) {
      const dataLayer = {
        event: action.event,
        'event-source': 'component-library',
        'component-library-version': version,
      };

      // If the event included additional details / context...
      if (e.detail.details) {
        for (const key of Object.keys(e.detail.details)) {
          const newKey = action.prefix ? `${action.prefix}-${key}` : key;

          dataLayer[newKey] = e.detail.details[key];
        }
      }

      if (
        ['int-accordion-expand', 'int-accordion-collapse'].includes(
          action.event,
        )
      ) {
        dataLayer['accordion-section-label'] = getSectionLabel(e.target);
      }

      recordEvent(dataLayer);

      // Reset dataLayer properties to undefined with a new push
      // so they don't bleed into other event.

      const clearedDataLayer = { ...dataLayer };

      // Remove event property
      if (Object.prototype.hasOwnProperty.call(clearedDataLayer, 'event')) {
        delete clearedDataLayer.event;
      }

      // Remove gtm.uniqueEventId property
      if (
        Object.prototype.hasOwnProperty.call(
          clearedDataLayer,
          'gtm.uniqueEventId',
        )
      ) {
        delete clearedDataLayer['gtm.uniqueEventId'];
      }

      // Set everything else to undefined
      Object.keys(clearedDataLayer).forEach(key => {
        clearedDataLayer[key] = undefined;
      });

      recordEvent(clearedDataLayer);
    }
  }
}

document.body.addEventListener(
  'component-library-analytics',
  subscribeComponentAnalyticsEvents,
);
<|endoftext|>


<commit_before>var fs = require('fs');
var path = require('path');
var async = require('async');

/**
 * Name of this test file
 * @type String
 */
exports.name = 'check local dependencies and paths';

/**
 * Function which filters out all invalid paths specified in a component. The remaining valid paths
 * will be returned via the provided callback.
 *
 * @param {Object} componentData
 * @param {Object} componentHint
 * @param {Function} cb
 * @returns {undefined}
 */
function getValidPaths(componentData, componentHint, cb) {
	var componentPath = componentData.path;
	var eventChannel = componentData.channel;

	var pathsList = componentData.json.paths || [];
	var validPaths = [];

	async.eachSeries(pathsList, function (pathItem, callback) {
		var checkPath = path.resolve(componentPath, pathItem);
		fs.stat(checkPath, function (error, stats) {
			// Make sure the path exists
			if (error) {
				componentHint.emit(eventChannel, componentPath, 'Path does not exist: ' + pathItem);
				return callback();
			}

			// Make sure the path is a directory
			if (!stats.isDirectory()) {
				componentHint.emit(eventChannel, componentPath, 'Path is not a directory: ' + pathItem);
				return callback();
			}

			// Otherwise push as valid path
			validPaths.push(pathItem);
			return callback();
		});
	}, function () {
		return cb(validPaths);
	});
}

/**
 * Function which resolves the path to a local dependency from a list of lookup paths.
 *
 * @param {String} dependency
 * @param {String} componentPath
 * @param {Array} componentPaths
 * @param {Function} cb
 * @returns {undefined}
 */
function resolveDependencyPaths(dependency, componentPath, componentPaths, cb) {
	var resolvedPaths = [];

	async.eachSeries(componentPaths, function (lookupPath, callback) {
		var depPath = path.resolve(componentPath, lookupPath, dependency);
		var checkPath = path.join(depPath, 'component.json');

		fs.exists(checkPath, function (exists) {
			if (exists) {
				resolvedPaths.push(depPath);
			}

			return callback();
		});
	}, function () {
		return cb(resolvedPaths);
	});
}

/**
 * This test will run on the fly and will:
 *  - Check paths to ensure they are valid
 *  - Resolves dependency paths
 *  - Checks that the dependency doesnt exist in more than one path
 *  - Checks that there aren't any unused paths
 *
 * @param {Object} componentData
 * @param {Object} componentHint
 * @param {Function} cb
 * @returns {undefined}
 */
exports.onTheFlyTest = function (componentData, componentHint, cb) {
	var componentPath = componentData.path;
	var eventChannel = componentData.channel;

	var lookupPaths = componentHint.lookupPaths || [];
	var localDeps = componentData.json.local || [];
	var resolvedDeps = [];

	// Check paths for existence and filter out the invalid ones
	getValidPaths(componentData, componentHint, function (validPaths) {
		var unusedPaths = validPaths.slice(0);

		async.eachSeries(localDeps, function (depName, callback) {
			resolveDependencyPaths(depName, componentPath, validPaths, function (resolvedPaths) {
				// Remove all resolved paths from the unused paths array
				for (var i = 0; i < resolvedPaths.length; i += 1) {
					var usedPathI = unusedPaths.indexOf(resolvedPaths[i]);
					if (usedPathI >= 0) {
						unusedPaths.splice(usedPathI, 1);
					}
				}

				// Check that dependency exists in a path
				if (resolvedPaths.length === 0) {
					var notFoundError = ['Could not find dep "' + depName + '" in any of the given paths'];

					// Check lookup paths to give the user a hint
					resolveDependencyPaths(depName, null, lookupPaths, function (hintPaths) {
						for (var hintI = 0; hintI < hintPaths.length; hintI += 1) {
							var hintPath = hintPaths[hintI];
							notFoundError.push('Found it in Lookup Path "' + path.relative(componentPath, hintPath) + '"');
						}

						componentHint.emit(eventChannel, componentPath, notFoundError.join('\n'));
						return callback();
					});

					return;
				}

				// Check that path doesnt exist in more than 1 path
				if (resolvedPaths.length > 1) {
					var tooManyError = ['Found dependency "' + depName + '" in more than one path:'];
					tooManyError.push.apply(tooManyError, resolvedPaths);

					componentHint.emit(eventChannel, componentPath, tooManyError.join('\n'));
					return callback();
				}

				// Add successfully resolved path to resolvedDeps list
				resolvedDeps.push(resolvedPaths[0]);

				return callback();
			});
		}, function () {
			// Check that there are no unused paths
			if (unusedPaths.length) {
				var unusedPathsError = ['Found unused component paths:'];
				unusedPathsError.push.apply(unusedPathsError, unusedPaths);
				componentHint.emit(eventChannel, componentPath, unusedPathsError.join('\n'));
			}

			return cb({localDeps: resolvedDeps});
		});
	});
};<commit_msg>fixed bug with unused paths<commit_after>var fs = require('fs');
var path = require('path');
var async = require('async');

/**
 * Name of this test file
 * @type String
 */
exports.name = 'check local dependencies and paths';

/**
 * Function which filters out all invalid paths specified in a component. The remaining valid paths
 * will be returned via the provided callback.
 *
 * @param {Object} componentData
 * @param {Object} componentHint
 * @param {Function} cb
 * @returns {undefined}
 */
function getValidPaths(componentData, componentHint, cb) {
	var componentPath = componentData.path;
	var eventChannel = componentData.channel;

	var pathsList = componentData.json.paths || [];
	var validPaths = [];

	async.eachSeries(pathsList, function (pathItem, callback) {
		var checkPath = path.resolve(componentPath, pathItem);
		fs.stat(checkPath, function (error, stats) {
			// Make sure the path exists
			if (error) {
				componentHint.emit(eventChannel, componentPath, 'Path does not exist: ' + pathItem);
				return callback();
			}

			// Make sure the path is a directory
			if (!stats.isDirectory()) {
				componentHint.emit(eventChannel, componentPath, 'Path is not a directory: ' + pathItem);
				return callback();
			}

			// Otherwise push as valid path
			validPaths.push(pathItem);
			return callback();
		});
	}, function () {
		return cb(validPaths);
	});
}

/**
 * Function which resolves the path to a local dependency from a list of lookup paths.
 *
 * @param {String} dependency
 * @param {String} componentPath
 * @param {Array} componentPaths
 * @param {Function} cb
 * @returns {undefined}
 */
function resolveDependencyPaths(dependency, componentPath, componentPaths, cb) {
	var resolvedPaths = [];
	var usedPaths = [];

	async.eachSeries(componentPaths, function (lookupPath, callback) {
		var depPath = path.resolve(componentPath, lookupPath, dependency);
		var checkPath = path.join(depPath, 'component.json');

		fs.exists(checkPath, function (exists) {
			if (exists) {
				resolvedPaths.push(depPath);
				usedPaths.push(lookupPath);
			}

			return callback();
		});
	}, function () {
		return cb(resolvedPaths, usedPaths);
	});
}

/**
 * This test will run on the fly and will:
 *  - Check paths to ensure they are valid
 *  - Resolves dependency paths
 *  - Checks that the dependency doesnt exist in more than one path
 *  - Checks that there aren't any unused paths
 *
 * @param {Object} componentData
 * @param {Object} componentHint
 * @param {Function} cb
 * @returns {undefined}
 */
exports.onTheFlyTest = function (componentData, componentHint, cb) {
	var componentPath = componentData.path;
	var eventChannel = componentData.channel;

	var lookupPaths = componentHint.lookupPaths || [];
	var localDeps = componentData.json.local || [];
	var resolvedDeps = [];

	// Check paths for existence and filter out the invalid ones
	getValidPaths(componentData, componentHint, function (validPaths) {
		var unusedPaths = validPaths.slice(0);

		async.eachSeries(localDeps, function (depName, callback) {
			resolveDependencyPaths(depName, componentPath, validPaths, function (resolvedPaths, usedPaths) {
				// Remove all used paths from the unused paths array
				for (var i = 0; i < usedPaths.length; i += 1) {
					var usedPathI = unusedPaths.indexOf(usedPaths[i]);
					if (usedPathI >= 0) {
						unusedPaths.splice(usedPathI, 1);
					}
				}

				// Check that dependency exists in a path
				if (resolvedPaths.length === 0) {
					var notFoundError = ['Could not find dep "' + depName + '" in any of the given paths'];

					// Check lookup paths to give the user a hint
					resolveDependencyPaths(depName, null, lookupPaths, function (hintPaths) {
						for (var hintI = 0; hintI < hintPaths.length; hintI += 1) {
							var hintPath = hintPaths[hintI];
							notFoundError.push('Found it in Lookup Path "' + path.relative(componentPath, hintPath) + '"');
						}

						componentHint.emit(eventChannel, componentPath, notFoundError.join('\n'));
						return callback();
					});

					return;
				}

				// Check that path doesnt exist in more than 1 path
				if (resolvedPaths.length > 1) {
					var tooManyError = ['Found dependency "' + depName + '" in more than one path:'];
					tooManyError.push.apply(tooManyError, resolvedPaths);

					componentHint.emit(eventChannel, componentPath, tooManyError.join('\n'));
					return callback();
				}

				// Add successfully resolved path to resolvedDeps list
				resolvedDeps.push(resolvedPaths[0]);

				return callback();
			});
		}, function () {
			// Check that there are no unused paths
			if (unusedPaths.length) {
				var unusedPathsError = ['Found unused component paths:'];
				unusedPathsError.push.apply(unusedPathsError, unusedPaths);
				componentHint.emit(eventChannel, componentPath, unusedPathsError.join('\n'));
			}

			return cb({localDeps: resolvedDeps});
		});
	});
};<|endoftext|>


<commit_before>// This is a large file being touched by open PRs. It should be typescripted
// in a separate PR.
/* eslint-disable */
import coreUtils from '@opentripplanner/core-utils'
import React from 'react'
import {
  FormattedMessage,
  FormattedNumber,
  FormattedTime,
  injectIntl
} from 'react-intl'
import { connect } from 'react-redux'
import styled from 'styled-components'
import { AccessibilityRating } from '@opentripplanner/itinerary-body'

import FieldTripGroupSize from '../../admin/field-trip-itinerary-group-size'
import NarrativeItinerary from '../narrative-itinerary'
import ItineraryBody from '../line-itin/connected-itinerary-body'
import SimpleRealtimeAnnotation from '../simple-realtime-annotation'
import FormattedDuration from '../../util/formatted-duration'
import FormattedMode from '../../util/formatted-mode'
import { getTotalFare } from '../../../util/state'
import {
  getAccessibilityScoreForItinerary,
  itineraryHasAccessibilityScores
} from '../../../util/accessibility-routing'
import Icon from '../../util/icon'

import { FlexIndicator } from './flex-indicator'
import ItinerarySummary from './itinerary-summary'

const { isBicycle, isMicromobility, isTransit, isFlex, isOptional, isContinuousDropoff } = coreUtils.itinerary

// Styled components
const LegIconWrapper = styled.div`
  display: inline-block;
  height: 20px;
  padding-bottom: 6px;
  padding-left: 2px;
  width: 20px;

  /* Equivalent of a single space before the leg icon. */
  &::before {
    content: "";
    margin: 0 0.125em;
  }

  &::before {
    content: "";
    margin: 0 0.125em;
  }
`

const DetailsHint = styled.div`
  clear: both;
  color: #685c5c;
  font-size: small;
  text-align: center;
`

/**
 * Obtains the description of an itinerary in the given locale.
 */
function ItineraryDescription({ itinerary }) {
  let primaryTransitDuration = 0
  let accessModeId = 'walk'
  let transitMode
  itinerary.legs.forEach((leg, i) => {
    const { duration, mode, rentedBike, rentedVehicle } = leg
    if (isTransit(mode) && duration > primaryTransitDuration) {
      // TODO: convert OTP's TRAM mode to the correct wording for Portland
      primaryTransitDuration = duration
      transitMode = <FormattedMode mode={mode.toLowerCase()} />
    }
    if (isBicycle(mode)) accessModeId = 'bicycle'
    if (isMicromobility(mode)) accessModeId = 'micromobility'
    if (rentedVehicle) accessModeId = 'micromobility_rent'
    if (rentedBike) accessModeId = 'bicycle_rent'
    if (mode === 'CAR') accessModeId = 'drive'
  })

  const mainMode = <FormattedMode mode={accessModeId} />
  return transitMode
    ? <FormattedMessage
      id='components.DefaultItinerary.multiModeSummary'
      values={{ accessMode: mainMode, transitMode }}
    />
    : mainMode
}

const ITINERARY_ATTRIBUTES = [
  {
    alias: 'best',
    id: 'duration',
    order: 0,
    render: (itinerary, options) => (
      <FormattedDuration duration={itinerary.duration} />
    )
  },
  {
    alias: 'departureTime',
    id: 'arrivalTime',
    order: 1,
    render: (itinerary, options) => {
      const formattedStartTime = itinerary.startTime + options.offset
      const formattedEndTime = itinerary.endTime + options.offset
      if (options.isSelected) {
        if (options.selection === 'ARRIVALTIME') {
          return <FormattedTime value={formattedStartTime} />
        } else {
          return <FormattedTime value={formattedEndTime} />
        }
      }
      return (
        <FormattedMessage
          id='common.time.departureArrivalTimes'
          values={{
            endTime: formattedEndTime,
            startTime: formattedStartTime
          }}
        />
      )
    }
  },
  {
    id: 'cost',
    order: 2,
    render: (itinerary, options, defaultFareKey = 'regular') => {
      return (
        <FormattedNumber
          currency={options.currency}
          currencyDisplay='narrowSymbol'
          style='currency'
          value={
            getTotalFare(itinerary, options.configCosts, defaultFareKey) / 100
          }
        />
      )
    }
  },
  {
    id: 'walkTime',
    order: 3,
    render: (itinerary, options) => {
      const leg = itinerary.legs[0]
      const { LegIcon } = options
      return (
        // FIXME: For CAR mode, walk time considers driving time.
        <>
          <FormattedDuration duration={itinerary.walkTime} />
          <LegIconWrapper>
            <LegIcon leg={leg} size={5} />
          </LegIconWrapper>
        </>
      )
    }
  }
]

class DefaultItinerary extends NarrativeItinerary {
  _onMouseEnter = () => {
    const { active, index, setVisibleItinerary, visibleItinerary } = this.props
    // Set this itinerary as visible if not already visible.
    const visibleNotSet =
      visibleItinerary === null || visibleItinerary === undefined
    const isVisible =
      visibleItinerary === index || (active === index && visibleNotSet)
    if (typeof setVisibleItinerary === 'function' && !isVisible) {
      setVisibleItinerary({ index })
    }
  }

  _onMouseLeave = () => {
    const { index, setVisibleItinerary, visibleItinerary } = this.props
    if (typeof setVisibleItinerary === 'function' && visibleItinerary === index) {
      setVisibleItinerary({ index: null })
    }
  }

  _isSortingOnAttribute = (attribute) => {
    const { sort } = this.props
    if (sort && sort.type) {
      const type = sort.type.toLowerCase()
      return (
        attribute.id.toLowerCase() === type ||
        (attribute.alias && attribute.alias.toLowerCase() === type)
      )
    }
    return false
  }

  render() {
    const {
      accessibilityScoreGradationMap,
      active,
      configCosts,
      currency,
      defaultFareKey,
      expanded,
      itinerary,
      LegIcon,
      setActiveLeg,
      showRealtimeAnnotation,
      timeFormat
    } = this.props
    const timeOptions = {
      format: timeFormat,
      offset: coreUtils.itinerary.getTimeZoneOffset(itinerary)
    }
    const isFlexItinerary = itinerary.legs.some(coreUtils.itinerary.isFlex)
    const isCallAhead = itinerary.legs.some(coreUtils.itinerary.isReservationRequired)
    const isContinuousDropoff = itinerary.legs.some(coreUtils.itinerary.isContinuousDropoff)

    // Use first leg's agency as a fallback
    const agency = itinerary.legs.map(leg => leg?.agencyName).filter(name => !!name)[0]
    let phone = `contact ${agency}`

    if (isCallAhead) {
    // Picking 0 ensures that if multiple flex legs with
    // different phone numbers, the first leg is prioritized
      phone = itinerary.legs
        .map((leg) => leg.pickupBookingInfo?.contactInfo?.phoneNumber)
        .filter((number) => !!number)[0]
    }
    return (

        <button
          className='header'
          // _onHeaderClick comes from super component (NarrativeItinerary).
          onClick={this._onHeaderClick}
        >

            <ItineraryDescription itinerary={itinerary} />
            <ItinerarySummary itinerary={itinerary} LegIcon={LegIcon} />
            {itineraryHasAccessibilityScores(itinerary) && (
              <AccessibilityRating
                gradationMap={accessibilityScoreGradationMap}
                large
                score={getAccessibilityScoreForItinerary(itinerary)}
              />
            )}

          {isFlexItinerary && (
            <FlexIndicator
              isCallAhead={isCallAhead}
              isContinuousDropoff={isContinuousDropoff}
              phoneNumber={phone}
            />
          )}

            {ITINERARY_ATTRIBUTES.sort((a, b) => {
              const aSelected = this._isSortingOnAttribute(a)
              const bSelected = this._isSortingOnAttribute(b)
              if (aSelected) return -1
              if (bSelected) return 1
              return a.order - b.order
            }).map((attribute) => {
              const isSelected = this._isSortingOnAttribute(attribute)
              const options = attribute.id === 'arrivalTime' ? timeOptions : {}
              if (isSelected) {
                options.isSelected = true
                options.selection = this.props.sort.type
              }
              options.LegIcon = LegIcon
              options.configCosts = configCosts
              options.currency = currency
              return (
                <li
                  className={`${attribute.id}${isSelected ? ' main' : ''}`}
                  key={attribute.id}
                >
                  {attribute.render(itinerary, options, defaultFareKey)}

              )
            })}

          <FieldTripGroupSize itinerary={itinerary} />
        </button>
        {active && expanded && (
          <>
            {showRealtimeAnnotation && <SimpleRealtimeAnnotation />}
            <ItineraryBody
              accessibilityScoreGradationMap={accessibilityScoreGradationMap}
              itinerary={itinerary}
              LegIcon={LegIcon}
              setActiveLeg={setActiveLeg}
              timeOptions={timeOptions}
            />
          </>
        )}
        {active && !expanded && (
          <DetailsHint>
            <FormattedMessage id='components.DefaultItinerary.clickDetails' />
          </DetailsHint>
        )}

    )
  }
}

const mapStateToProps = (state, ownProps) => {
  const { intl } = ownProps
  const gradationMap = state.otp.config.accessibilityScore?.gradationMap

  // Generate icons based on fa icon keys in config
  // Override text fields if translation set
  gradationMap &&
    Object.keys(gradationMap).forEach((key) => {
      const { icon } = gradationMap[key]
      if (icon && typeof icon === 'string') {
        gradationMap[key].icon = <Icon type={icon} />
      }

      // As these localization keys are in the config, rather than
      // standard language files, the message ids must be dynamically generated
      const localizationId = `config.acessibilityScore.gradationMap.${key}`
      const localizedText = intl.formatMessage({ id: localizationId })
      // Override the config label if a localized label exists
      if (localizationId !== localizedText) {
        gradationMap[key].text = localizedText
      }
    })

  return {
    accessibilityScoreGradationMap: gradationMap,
    configCosts: state.otp.config.itinerary?.costs,
    // The configured (ambient) currency is needed for rendering the cost
    // of itineraries whether they include a fare or not, in which case
    // we show $0.00 or its equivalent in the configured currency and selected locale.
    currency: state.otp.config.localization?.currency || 'USD',
    defaultFareKey: state.otp.config.itinerary?.defaultFareKey
  }
}

export default injectIntl(connect(mapStateToProps)(DefaultItinerary))
<commit_msg>Revert "fix(default-itinerary): show detailsHint at correct spot"<commit_after>// This is a large file being touched by open PRs. It should be typescripted
// in a separate PR.
/* eslint-disable */
import coreUtils from '@opentripplanner/core-utils'
import React from 'react'
import {
  FormattedMessage,
  FormattedNumber,
  FormattedTime,
  injectIntl
} from 'react-intl'
import { connect } from 'react-redux'
import styled from 'styled-components'
import { AccessibilityRating } from '@opentripplanner/itinerary-body'

import FieldTripGroupSize from '../../admin/field-trip-itinerary-group-size'
import NarrativeItinerary from '../narrative-itinerary'
import ItineraryBody from '../line-itin/connected-itinerary-body'
import SimpleRealtimeAnnotation from '../simple-realtime-annotation'
import FormattedDuration from '../../util/formatted-duration'
import FormattedMode from '../../util/formatted-mode'
import { getTotalFare } from '../../../util/state'
import {
  getAccessibilityScoreForItinerary,
  itineraryHasAccessibilityScores
} from '../../../util/accessibility-routing'
import Icon from '../../util/icon'

import { FlexIndicator } from './flex-indicator'
import ItinerarySummary from './itinerary-summary'

const { isBicycle, isMicromobility, isTransit, isFlex, isOptional, isContinuousDropoff } = coreUtils.itinerary

// Styled components
const LegIconWrapper = styled.div`
  display: inline-block;
  height: 20px;
  padding-bottom: 6px;
  padding-left: 2px;
  width: 20px;

  /* Equivalent of a single space before the leg icon. */
  &::before {
    content: "";
    margin: 0 0.125em;
  }

  &::before {
    content: "";
    margin: 0 0.125em;
  }
`

const DetailsHint = styled.div`
  clear: both;
  color: #685c5c;
  font-size: small;
  text-align: center;
`

/**
 * Obtains the description of an itinerary in the given locale.
 */
function ItineraryDescription({ itinerary }) {
  let primaryTransitDuration = 0
  let accessModeId = 'walk'
  let transitMode
  itinerary.legs.forEach((leg, i) => {
    const { duration, mode, rentedBike, rentedVehicle } = leg
    if (isTransit(mode) && duration > primaryTransitDuration) {
      // TODO: convert OTP's TRAM mode to the correct wording for Portland
      primaryTransitDuration = duration
      transitMode = <FormattedMode mode={mode.toLowerCase()} />
    }
    if (isBicycle(mode)) accessModeId = 'bicycle'
    if (isMicromobility(mode)) accessModeId = 'micromobility'
    if (rentedVehicle) accessModeId = 'micromobility_rent'
    if (rentedBike) accessModeId = 'bicycle_rent'
    if (mode === 'CAR') accessModeId = 'drive'
  })

  const mainMode = <FormattedMode mode={accessModeId} />
  return transitMode
    ? <FormattedMessage
      id='components.DefaultItinerary.multiModeSummary'
      values={{ accessMode: mainMode, transitMode }}
    />
    : mainMode
}

const ITINERARY_ATTRIBUTES = [
  {
    alias: 'best',
    id: 'duration',
    order: 0,
    render: (itinerary, options) => (
      <FormattedDuration duration={itinerary.duration} />
    )
  },
  {
    alias: 'departureTime',
    id: 'arrivalTime',
    order: 1,
    render: (itinerary, options) => {
      const formattedStartTime = itinerary.startTime + options.offset
      const formattedEndTime = itinerary.endTime + options.offset
      if (options.isSelected) {
        if (options.selection === 'ARRIVALTIME') {
          return <FormattedTime value={formattedStartTime} />
        } else {
          return <FormattedTime value={formattedEndTime} />
        }
      }
      return (
        <FormattedMessage
          id='common.time.departureArrivalTimes'
          values={{
            endTime: formattedEndTime,
            startTime: formattedStartTime
          }}
        />
      )
    }
  },
  {
    id: 'cost',
    order: 2,
    render: (itinerary, options, defaultFareKey = 'regular') => {
      return (
        <FormattedNumber
          currency={options.currency}
          currencyDisplay='narrowSymbol'
          style='currency'
          value={
            getTotalFare(itinerary, options.configCosts, defaultFareKey) / 100
          }
        />
      )
    }
  },
  {
    id: 'walkTime',
    order: 3,
    render: (itinerary, options) => {
      const leg = itinerary.legs[0]
      const { LegIcon } = options
      return (
        // FIXME: For CAR mode, walk time considers driving time.
        <>
          <FormattedDuration duration={itinerary.walkTime} />
          <LegIconWrapper>
            <LegIcon leg={leg} size={5} />
          </LegIconWrapper>
        </>
      )
    }
  }
]

class DefaultItinerary extends NarrativeItinerary {
  _onMouseEnter = () => {
    const { active, index, setVisibleItinerary, visibleItinerary } = this.props
    // Set this itinerary as visible if not already visible.
    const visibleNotSet =
      visibleItinerary === null || visibleItinerary === undefined
    const isVisible =
      visibleItinerary === index || (active === index && visibleNotSet)
    if (typeof setVisibleItinerary === 'function' && !isVisible) {
      setVisibleItinerary({ index })
    }
  }

  _onMouseLeave = () => {
    const { index, setVisibleItinerary, visibleItinerary } = this.props
    if (typeof setVisibleItinerary === 'function' && visibleItinerary === index) {
      setVisibleItinerary({ index: null })
    }
  }

  _isSortingOnAttribute = (attribute) => {
    const { sort } = this.props
    if (sort && sort.type) {
      const type = sort.type.toLowerCase()
      return (
        attribute.id.toLowerCase() === type ||
        (attribute.alias && attribute.alias.toLowerCase() === type)
      )
    }
    return false
  }

  render() {
    const {
      accessibilityScoreGradationMap,
      active,
      configCosts,
      currency,
      defaultFareKey,
      expanded,
      itinerary,
      LegIcon,
      setActiveLeg,
      showRealtimeAnnotation,
      timeFormat
    } = this.props
    const timeOptions = {
      format: timeFormat,
      offset: coreUtils.itinerary.getTimeZoneOffset(itinerary)
    }
    const isFlexItinerary = itinerary.legs.some(coreUtils.itinerary.isFlex)
    const isCallAhead = itinerary.legs.some(coreUtils.itinerary.isReservationRequired)
    const isContinuousDropoff = itinerary.legs.some(coreUtils.itinerary.isContinuousDropoff)

    // Use first leg's agency as a fallback
    const agency = itinerary.legs.map(leg => leg?.agencyName).filter(name => !!name)[0]
    let phone = `contact ${agency}`

    if (isCallAhead) {
    // Picking 0 ensures that if multiple flex legs with
    // different phone numbers, the first leg is prioritized
      phone = itinerary.legs
        .map((leg) => leg.pickupBookingInfo?.contactInfo?.phoneNumber)
        .filter((number) => !!number)[0]
    }
    return (

        <button
          className='header'
          // _onHeaderClick comes from super component (NarrativeItinerary).
          onClick={this._onHeaderClick}
        >

            <ItineraryDescription itinerary={itinerary} />
            <ItinerarySummary itinerary={itinerary} LegIcon={LegIcon} />
            {itineraryHasAccessibilityScores(itinerary) && (
              <AccessibilityRating
                gradationMap={accessibilityScoreGradationMap}
                large
                score={getAccessibilityScoreForItinerary(itinerary)}
              />
            )}

          {isFlexItinerary && (
            <FlexIndicator
              isCallAhead={isCallAhead}
              isContinuousDropoff={isContinuousDropoff}
              phoneNumber={phone}
            />
          )}

            {ITINERARY_ATTRIBUTES.sort((a, b) => {
              const aSelected = this._isSortingOnAttribute(a)
              const bSelected = this._isSortingOnAttribute(b)
              if (aSelected) return -1
              if (bSelected) return 1
              return a.order - b.order
            }).map((attribute) => {
              const isSelected = this._isSortingOnAttribute(attribute)
              const options = attribute.id === 'arrivalTime' ? timeOptions : {}
              if (isSelected) {
                options.isSelected = true
                options.selection = this.props.sort.type
              }
              options.LegIcon = LegIcon
              options.configCosts = configCosts
              options.currency = currency
              return (
                <li
                  className={`${attribute.id}${isSelected ? ' main' : ''}`}
                  key={attribute.id}
                >
                  {attribute.render(itinerary, options, defaultFareKey)}

              )
            })}

          <FieldTripGroupSize itinerary={itinerary} />
          {active && !expanded && (
            <DetailsHint>
              <FormattedMessage id='components.DefaultItinerary.clickDetails' />
            </DetailsHint>
          )}
        </button>
        {active && expanded && (
          <>
            {showRealtimeAnnotation && <SimpleRealtimeAnnotation />}
            <ItineraryBody
              accessibilityScoreGradationMap={accessibilityScoreGradationMap}
              itinerary={itinerary}
              LegIcon={LegIcon}
              setActiveLeg={setActiveLeg}
              timeOptions={timeOptions}
            />
          </>
        )}

    )
  }
}

const mapStateToProps = (state, ownProps) => {
  const { intl } = ownProps
  const gradationMap = state.otp.config.accessibilityScore?.gradationMap

  // Generate icons based on fa icon keys in config
  // Override text fields if translation set
  gradationMap &&
    Object.keys(gradationMap).forEach((key) => {
      const { icon } = gradationMap[key]
      if (icon && typeof icon === 'string') {
        gradationMap[key].icon = <Icon type={icon} />
      }

      // As these localization keys are in the config, rather than
      // standard language files, the message ids must be dynamically generated
      const localizationId = `config.acessibilityScore.gradationMap.${key}`
      const localizedText = intl.formatMessage({ id: localizationId })
      // Override the config label if a localized label exists
      if (localizationId !== localizedText) {
        gradationMap[key].text = localizedText
      }
    })

  return {
    accessibilityScoreGradationMap: gradationMap,
    configCosts: state.otp.config.itinerary?.costs,
    // The configured (ambient) currency is needed for rendering the cost
    // of itineraries whether they include a fare or not, in which case
    // we show $0.00 or its equivalent in the configured currency and selected locale.
    currency: state.otp.config.localization?.currency || 'USD',
    defaultFareKey: state.otp.config.itinerary?.defaultFareKey
  }
}

export default injectIntl(connect(mapStateToProps)(DefaultItinerary))
<|endoftext|>


<commit_before>// Copyright (c) 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

'use strict';

/**
 * @fileoverview The event page for Google Now for Chrome implementation.
 * The Google Now event page gets Google Now cards from the server and shows
 * them as Chrome notifications.
 * The service performs periodic updating of Google Now cards.
 * Each updating of the cards includes 3 steps:
 * 1. Obtaining the location of the machine;
 * 2. Making a server request based on that location;
 * 3. Showing the received cards as notifications.
 */

// TODO(vadimt): Use background permission to show notifications even when all
// browser windows are closed.
// TODO(vadimt): Remove the C++ implementation.
// TODO(vadimt): Decide what to do in incognito mode.
// TODO(vadimt): Gather UMAs.
// TODO(vadimt): Honor the flag the enables Google Now integration.
// TODO(vadimt): Figure out the final values of the constants.
// TODO(vadimt): Report errors to the user.

// TODO(vadimt): Figure out the server name. Use it in the manifest and for
// NOTIFICATION_CARDS_URL. Meanwhile, to use the feature, you need to manually
// edit NOTIFICATION_CARDS_URL before building Chrome.
/**
 * URL to retrieve notification cards.
 */
var NOTIFICATION_CARDS_URL = '';

/**
 * Standard response code for successful HTTP requests. This is the only success
 * code the server will send.
 */
var HTTP_OK = 200;

/**
 * Period for polling for Google Now Notifications cards to use when the period
 * from the server is not available.
 */
var DEFAULT_POLLING_PERIOD_SECONDS = 300;  // 5 minutes

var storage = chrome.storage.local;

/**
 * Show a notification and remember information associated with it.
 * @param {Object} card Google Now card represented as a set of parameters for
 *     showing a Chrome notification.
 */
function createNotification(card) {
  var notificationId = card.notificationId;

  // Fill the information about button actions for the notification. This is a
  // map from the clicked area name to URL to open when the area is clicked. Any
  // of the fields may be defined or undefined.
  var actionUrls = {
    message: card.messageUrl,
    button0: card.buttonOneUrl,
    button1: card.buttonTwoUrl
  };

  // TODO(vadimt): Reorganize server response to avoid deleting fields.
  delete card.notificationId;
  delete card.messageUrl;
  delete card.buttonOneUrl;
  delete card.buttonTwoUrl;

  // Create a notification or quietly update if it already exists.
  // TODO(vadimt): Implement deleting and non-quiet updates.
  chrome.experimental.notification.create(
      notificationId,
      card,
      function(assignedNotificationId) {
        // Once the notification is created, store button actions for its ID.
        storage.get('activeNotifications', function(items) {
          // TODO(vadimt): Make sure that activeNotifications map cannot grow
          // infinitely.
          items.activeNotifications[assignedNotificationId] = actionUrls;
          storage.set(
              {activeNotifications: items.activeNotifications},
              function() {
                // TODO(vadimt): Analyze runtime.lastError.
              });
        });
      });
}

/**
 * Parse JSON response from the notification server, show notifications and
 * schedule next update.
 * @param {string} response Server response.
 */
function parseAndShowNotificationCards(response) {
  try {
    var parsedResponse = JSON.parse(response);
  } catch (error) {
    // TODO(vadimt): Report errors to the user.
    return;
  }

  var cards = parsedResponse.cards;

  if (!(cards instanceof Array)) {
    // TODO(vadimt): Report errors to the user.
    return;
  }

  if (typeof parsedResponse.expiration_timestamp_seconds != 'number') {
    // TODO(vadimt): Report errors to the user.
    return;
  }

  for (var i = 0; i < cards.length; ++i) {
    try {
      createNotification(cards[i]);
    } catch (error) {
      // TODO(vadimt): Report errors to the user.
      return;
    }
  }

  scheduleNextUpdate(parsedResponse.expiration_timestamp_seconds);
}

/**
 * Request notification cards from the server.
 * @param {string} requestParameters Query string for the request.
 */
function requestNotificationCards(requestParameters) {
  // TODO(vadimt): Figure out how to send user's identity to the server.
  var request = new XMLHttpRequest();

  request.responseType = 'text';
  request.onload = function(event) {
    if (request.status == HTTP_OK)
      parseAndShowNotificationCards(request.response);
  }

  request.open(
      'GET',
      NOTIFICATION_CARDS_URL + '/notifications' + requestParameters,
      true);
  request.send();
}

/**
 * Request notification cards from the server when we have geolocation.
 * @param {Geoposition} position Location of this computer.
 */
function requestNotificationCardsWithLocation(position) {
  // TODO(vadimt): Should we use 'q' as the parameter name?
  var requestParameters =
      '?q=' + position.coords.latitude +
      ',' + position.coords.longitude +
      ',' + position.coords.accuracy;

  requestNotificationCards(requestParameters);
}

/**
 * Request notification cards from the server when we don't have geolocation.
 * @param {PositionError} positionError Position error.
 */
function requestNotificationCardsWithoutLocation(positionError) {
  requestNotificationCards('');
}

/**
 * Obtain new location; request and show notification cards based on this
 * location.
 */
function updateNotificationsCards() {
  // Immediately schedule the update after the default period. If we
  // successfully retrieve, parse and show the notifications cards, we'll
  // schedule next update based on the expiration timestamp received from the
  // server. At that point scheduled time will be overwritten by the new one
  // based on the expiration timestamp.
  // TODO(vadimt): Implement exponential backoff with randomized jitter.
  scheduleNextUpdate(DEFAULT_POLLING_PERIOD_SECONDS);

  // TODO(vadimt): Use chrome.* geolocation API once it's ready.
  navigator.geolocation.getCurrentPosition(
      requestNotificationCardsWithLocation,
      requestNotificationCardsWithoutLocation);
}

/**
 * Opens URL corresponsing to the clicked part of the notification.
 * @param {string} notificationId Unique identifier of the notification.
 * @param {string} area Name of the notification's clicked area.
 */
function onNotificationClicked(notificationId, area) {
  storage.get('activeNotifications', function(items) {
    var notificationActions = items.activeNotifications[notificationId] || {};
    if (area in notificationActions) {
      // Open URL specified for the clicked area.
      // TODO(vadimt): Figure out whether to open link in a new tab etc.
      chrome.windows.create({url: notificationActions[area]});
    }
  });
}

/**
 * Callback for chrome.experimental.notification.onClosed event.
 * @param {string} notificationId Unique identifier of the notification.
 * @param {boolean} byUser Whether the notification was closed by the user.
 */
function onNotificationClosed(notificationId, byUser) {
  // Remove button actions entry for notificationId.
  storage.get('activeNotifications', function(items) {
    delete items.activeNotifications[notificationId];
    storage.set({activeNotifications: items.activeNotifications}, function() {
      // TODO(vadimt): Analyze runtime.lastError.
    });
  });

  if (byUser) {
    // TODO(vadimt): Analyze possible race conditions between request for cards
    // and dismissal.
    // Send a dismiss request to the server.
    var requestParameters = '?id=' + notificationId;
    var request = new XMLHttpRequest();
    request.responseType = 'text';
    // TODO(vadimt): If the request fails, for example, because there is no
    // internet connection, do retry with exponential backoff.
    request.open(
      'GET',
      NOTIFICATION_CARDS_URL + '/dismiss' + requestParameters,
      true);
    request.send();
  }
}

/**
 * Schedule next update for notification cards.
 * @param {int} delaySeconds Length of time in seconds after which the alarm
 *     event should fire.
 */
function scheduleNextUpdate(delaySeconds) {
  chrome.alarms.create({delayInMinutes: delaySeconds / 60});
}

/**
 * Initialize the event page on install or on browser startup.
 */
function initialize() {
  updateNotificationsCards();

  storage.set({activeNotifications: {}}, function() {
    // TODO(vadimt): Analyze runtime.lastError.
  });
}

chrome.runtime.onInstalled.addListener(function(details) {
  if (details.reason != 'chrome_update')
    initialize();
});

chrome.runtime.onStartup.addListener(function() {
  initialize();
});

chrome.alarms.onAlarm.addListener(function(alarm) {
  updateNotificationsCards();
});

chrome.experimental.notification.onClicked.addListener(
    function(notificationId) {
      onNotificationClicked(notificationId, 'message');
    });

chrome.experimental.notification.onButtonClicked.addListener(
    function(notificationId, buttonIndex) {
      onNotificationClicked(notificationId, 'button' + buttonIndex);
    });

chrome.experimental.notification.onClosed.addListener(onNotificationClosed);
<commit_msg>Retry with exponential backoff upon failing to request notifications.<commit_after>// Copyright (c) 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

'use strict';

/**
 * @fileoverview The event page for Google Now for Chrome implementation.
 * The Google Now event page gets Google Now cards from the server and shows
 * them as Chrome notifications.
 * The service performs periodic updating of Google Now cards.
 * Each updating of the cards includes 3 steps:
 * 1. Obtaining the location of the machine;
 * 2. Making a server request based on that location;
 * 3. Showing the received cards as notifications.
 */

// TODO(vadimt): Use background permission to show notifications even when all
// browser windows are closed.
// TODO(vadimt): Remove the C++ implementation.
// TODO(vadimt): Decide what to do in incognito mode.
// TODO(vadimt): Gather UMAs.
// TODO(vadimt): Honor the flag the enables Google Now integration.
// TODO(vadimt): Figure out the final values of the constants.
// TODO(vadimt): Report errors to the user.

// TODO(vadimt): Figure out the server name. Use it in the manifest and for
// TODO(vadimt): Consider processing errors for all storage.set calls.
// NOTIFICATION_CARDS_URL. Meanwhile, to use the feature, you need to manually
// edit NOTIFICATION_CARDS_URL before building Chrome.
/**
 * URL to retrieve notification cards.
 */
var NOTIFICATION_CARDS_URL = '';

/**
 * Standard response code for successful HTTP requests. This is the only success
 * code the server will send.
 */
var HTTP_OK = 200;

/**
 * Initial period for polling for Google Now Notifications cards to use when the
 * period from the server is not available.
 */
var INITIAL_POLLING_PERIOD_SECONDS = 300;  // 5 minutes

/**
 * Maximal period for polling for Google Now Notifications cards to use when the
 * period from the server is not available.
 */
var MAXIMUM_POLLING_PERIOD_SECONDS = 3600;  // 1 hour

var storage = chrome.storage.local;

/**
 * Show a notification and remember information associated with it.
 * @param {Object} card Google Now card represented as a set of parameters for
 *     showing a Chrome notification.
 */
function createNotification(card) {
  var notificationId = card.notificationId;

  // Fill the information about button actions for the notification. This is a
  // map from the clicked area name to URL to open when the area is clicked. Any
  // of the fields may be defined or undefined.
  var actionUrls = {
    message: card.messageUrl,
    button0: card.buttonOneUrl,
    button1: card.buttonTwoUrl
  };

  // TODO(vadimt): Reorganize server response to avoid deleting fields.
  delete card.notificationId;
  delete card.messageUrl;
  delete card.buttonOneUrl;
  delete card.buttonTwoUrl;

  // Create a notification or quietly update if it already exists.
  // TODO(vadimt): Implement deleting and non-quiet updates.
  chrome.experimental.notification.create(
      notificationId,
      card,
      function(assignedNotificationId) {
        // Once the notification is created, store button actions for its ID.
        storage.get('activeNotifications', function(items) {
          // TODO(vadimt): Make sure that activeNotifications map cannot grow
          // infinitely.
          items.activeNotifications[assignedNotificationId] = actionUrls;
          storage.set({activeNotifications: items.activeNotifications});
        });
      });
}

/**
 * Parse JSON response from the notification server, show notifications and
 * schedule next update.
 * @param {string} response Server response.
 */
function parseAndShowNotificationCards(response) {
  try {
    var parsedResponse = JSON.parse(response);
  } catch (error) {
    // TODO(vadimt): Report errors to the user.
    return;
  }

  var cards = parsedResponse.cards;

  if (!(cards instanceof Array)) {
    // TODO(vadimt): Report errors to the user.
    return;
  }

  if (typeof parsedResponse.expiration_timestamp_seconds != 'number') {
    // TODO(vadimt): Report errors to the user.
    return;
  }

  for (var i = 0; i < cards.length; ++i) {
    try {
      createNotification(cards[i]);
    } catch (error) {
      // TODO(vadimt): Report errors to the user.
      return;
    }
  }

  scheduleNextUpdate(parsedResponse.expiration_timestamp_seconds);

  // Now that we got a valid response from the server, reset the retry period to
  // the initial value. This retry period will be used the next time we fail to
  // get the server-provided period.
  storage.set({retryDelaySeconds: INITIAL_POLLING_PERIOD_SECONDS});
}

/**
 * Request notification cards from the server.
 * @param {string} requestParameters Query string for the request.
 */
function requestNotificationCards(requestParameters) {
  // TODO(vadimt): Figure out how to send user's identity to the server.
  var request = new XMLHttpRequest();

  request.responseType = 'text';
  request.onload = function(event) {
    if (request.status == HTTP_OK)
      parseAndShowNotificationCards(request.response);
  }

  request.open(
      'GET',
      NOTIFICATION_CARDS_URL + '/notifications' + requestParameters,
      true);
  request.send();
}

/**
 * Request notification cards from the server when we have geolocation.
 * @param {Geoposition} position Location of this computer.
 */
function requestNotificationCardsWithLocation(position) {
  // TODO(vadimt): Should we use 'q' as the parameter name?
  var requestParameters =
      '?q=' + position.coords.latitude +
      ',' + position.coords.longitude +
      ',' + position.coords.accuracy;

  requestNotificationCards(requestParameters);
}

/**
 * Request notification cards from the server when we don't have geolocation.
 * @param {PositionError} positionError Position error.
 */
function requestNotificationCardsWithoutLocation(positionError) {
  requestNotificationCards('');
}

/**
 * Obtain new location; request and show notification cards based on this
 * location.
 */
function updateNotificationsCards() {
  storage.get('retryDelaySeconds', function(items) {
    // Immediately schedule the update after the current retry period. Then,
    // we'll use update time from the server if available.
    scheduleNextUpdate(items.retryDelaySeconds);

    // TODO(vadimt): Consider interrupting waiting for the next update if we
    // detect that the network conditions have changed. Also, decide whether the
    // exponential backoff is needed both when we are offline and when there are
    // failures on the server side.
    var newRetryDelaySeconds =
        Math.min(items.retryDelaySeconds * 2 * (1 + 0.2 * Math.random()),
                 MAXIMUM_POLLING_PERIOD_SECONDS);
    storage.set({retryDelaySeconds: newRetryDelaySeconds});

    navigator.geolocation.getCurrentPosition(
        requestNotificationCardsWithLocation,
        requestNotificationCardsWithoutLocation);
  });
}

/**
 * Opens URL corresponding to the clicked part of the notification.
 * @param {string} notificationId Unique identifier of the notification.
 * @param {string} area Name of the notification's clicked area.
 */
function onNotificationClicked(notificationId, area) {
  storage.get('activeNotifications', function(items) {
    var notificationActions = items.activeNotifications[notificationId] || {};
    if (area in notificationActions) {
      // Open URL specified for the clicked area.
      // TODO(vadimt): Figure out whether to open link in a new tab etc.
      chrome.windows.create({url: notificationActions[area]});
    }
  });
}

/**
 * Callback for chrome.experimental.notification.onClosed event.
 * @param {string} notificationId Unique identifier of the notification.
 * @param {boolean} byUser Whether the notification was closed by the user.
 */
function onNotificationClosed(notificationId, byUser) {
  // Remove button actions entry for notificationId.
  storage.get('activeNotifications', function(items) {
    delete items.activeNotifications[notificationId];
    storage.set({activeNotifications: items.activeNotifications});
  });

  if (byUser) {
    // TODO(vadimt): Analyze possible race conditions between request for cards
    // and dismissal.
    // Send a dismiss request to the server.
    var requestParameters = '?id=' + notificationId;
    var request = new XMLHttpRequest();
    request.responseType = 'text';
    // TODO(vadimt): If the request fails, for example, because there is no
    // internet connection, do retry with exponential backoff.
    request.open(
      'GET',
      NOTIFICATION_CARDS_URL + '/dismiss' + requestParameters,
      true);
    request.send();
  }
}

/**
 * Schedule next update for notification cards.
 * @param {int} delaySeconds Length of time in seconds after which the alarm
 *     event should fire.
 */
function scheduleNextUpdate(delaySeconds) {
  // Schedule an alarm after the specified delay. 'periodInMinutes' is for the
  // case when we fail to re-register the alarm.
  chrome.alarms.create({
    delayInMinutes: delaySeconds / 60,
    periodInMinutes: MAXIMUM_POLLING_PERIOD_SECONDS / 60
  });
}

/**
 * Initialize the event page on install or on browser startup.
 */
function initialize() {
  var initialStorage = {
    activeNotifications: {},
    retryDelaySeconds: INITIAL_POLLING_PERIOD_SECONDS
  };
  storage.set(initialStorage, updateNotificationsCards);
}

chrome.runtime.onInstalled.addListener(function(details) {
  if (details.reason != 'chrome_update')
    initialize();
});

chrome.runtime.onStartup.addListener(function() {
  initialize();
});

chrome.alarms.onAlarm.addListener(function(alarm) {
  updateNotificationsCards();
});

chrome.experimental.notification.onClicked.addListener(
    function(notificationId) {
      onNotificationClicked(notificationId, 'message');
    });

chrome.experimental.notification.onButtonClicked.addListener(
    function(notificationId, buttonIndex) {
      onNotificationClicked(notificationId, 'button' + buttonIndex);
    });

chrome.experimental.notification.onClosed.addListener(onNotificationClosed);
<|endoftext|>


<commit_before>var getRandomInt = function(min, max) {
    return Math.floor(Math.random() * (max - min + 1)) + min;
};

var getStepSize = function() {
    return (90*ponies.length*50)/(raceLength*1000);
};

var runPonies = function() {
    if (running) {
        // Figure out which (if any) ponies haven't finished
        var notFinished = [];
        for (var i = 0; i < ponies.length; i++) {
            if (ponies[i].progress < 90) {
                notFinished.push(i);
            }
        }

        // See if any of the ponies still need to finish
        if (notFinished.length > 0) {
            // Move a random (still running) pony forward and record if they
            // made it to the end
            var tmp = getRandomInt(0,notFinished.length-1);
            ponies[notFinished[tmp]].progress += getStepSize();
            if (ponies[notFinished[tmp]].progress >= 90)
            {
                ponies[notFinished[tmp]].finishPos = nextPlace++;
            }

            // Actually move the pony
            d3.selectAll('.pony').data(ponies)
                .transition().ease('linear').duration(50)
                .attr('transform',function(d,i) { return 'translate('+x(d.progress)+',0)';})
            setTimeout(runPonies,50);
        } else {
            d3.select('#winner-list').selectAll('.rank-entry')
               .data(ponies,function(d) { return d.finishPos; })
              .enter()
                .append('p')
                .classed('rank-entry',true)
                .text(function(d,i) { return d.finishPos + ': ' + d.jockey;}) ;

           d3.selectAll('.rank-entry').sort(function(a,b) { return a.finishPos < b.finishPos ? -1 : 1; }).order();

           setTimeout(function() { d3.select('#winner-list').style('display','block'); }, 350);

            d3.selectAll('#controls button').filter(function(d,i) { return i == 0;}).text('Done');
            done = true;
            running = false;
        }
    }
};

var clearWinners = function() {
    d3.selectAll('.rank-entry').remove();
    d3.select('#winner-list').style('display','none');
};

var addEntrant = function() {
    if (allowChanging) {
        var name = $('#name').val(),
            num = $('#num').val(),
            col = $('#color1').val();

        if (name.length > 0 && !isNaN(+num)) {
            // Check to see if a jockey name exists, if so, don't allow it
            // to be reused
            var previouslyUsed = false;
            for (var i=0; i < jockeys.length; i++) {
                previouslyUsed = name == jockeys[i].name ? true : false;
            }
            if (previouslyUsed) {
                window.alert('Sorry, you can not reuse a Jockey')
            } else {

                // Add the individual ponies and the jockey to the list
                for (var i = 0; i < +num; i++) {
                    ponies.push({progress:0,
                            jockey:name,
                            col:col,
                            finishPos:0});
                }

                // Add the jockey to the jockies list
                jockeys.push({name:name,num:num,col:col});

                updateJockeyLineup();
                updatePonyLineup();
            }

            $('#name').val('Jockey');
            $('#num').val('#');
            formDefaulter.add('name');
            formDefaulter.add('num');
        } else {
            window.alert('Please enter a unique jockey name and # of ponies for that jockey!')
        }
    }
};

var removeEntrant = function(i) {
    if (allowChanging) {
        // Figure out which indexes we need to remove
        var toRemove = [];
        for (var j = 0; j < ponies.length; j++) {
            if (ponies[j].jockey == jockeys[i].name) {
                toRemove.push(j);
            }
        }

        // Remove them in reverse order so we don't have
        // to do anything fancy with indexes
        for (var j = toRemove.length-1; j > -1; j--) {
            ponies.splice(toRemove[j],1);
        }

        // Remove jockey's rects and move remaining ponies/fix y-scale
        updatePonyLineup();

        // Remove jockeys name from displayed list list
        jockeys.splice(i,1);
        updateJockeyLineup();
    }
};

var resetPonies = function() {
    // Send the ponies back to the beginning
    for (var i = 0; i < ponies.length; i++) {
        ponies[i].progress = 0;
    }

    done = false;
    running = false;
    nextPlace = 1;

    d3.selectAll('.pony').data(ponies)
        .transition().ease('linear').duration(250)
        .attr('transform',function(d,i) { return 'translate('+x(d.progress)+',0)';})

    d3.selectAll('#controls button').filter(function(d,i) { return i ==
            0;}).text('Go!');

    d3.selectAll('.rank-entry').remove();
};

var updateJockeyLineup = function() {

    // Select current jockey list
    var jockeyList = d3.select('#jockey-list').selectAll('.jockey')
            .data(jockeys);

    // Remove old jockeys
    jockeyList.exit().remove();

    // Update entries for existing jockeys
    jockeyList.select('.jockey-name').attr('value',function(d) { return d.name; })
    jockeyList.select('.jockey-num').attr('value',function(d) { return d.num; })
    jockeyList.select('.jockey-col').style('background-color',function(d) { return d.col; })

    // Add new jockey entries
    var newJockeyList = jockeyList.enter()
            .append('div')
            .classed('jockey',true);

    newJockeyList.append('input')
        .attr('type','text')
        .attr('value',function(d) { return d.name; })
        .classed('jockey-name',true);

    newJockeyList.append('input')
        .attr('type','text')
        .attr('value',function(d) { return d.num; })
        .classed('jockey-num',true);

    newJockeyList.append('input')
        .attr('type','text')
        .style('background-color',function(d) { return d.col; })
        .classed('jockey-col',true);

    newJockeyList.append('span')
        .text('-')
        .classed('jockey-remove',true)
        .on('click',function(d,i) {removeEntrant(i); });

};

var updatePonyLineup = function() {
    // Make sure y domain is correct
    y.domain([0,ponies.length]);

    var ponyGroup = d3.select('#data-region').selectAll('.pony-group')
            .data(ponies);

    // Update position for old ponies
    ponyGroup.transition().duration(100)
            .attr('transform',function(d,i) { return 'translate(0,'+y(i)+')';});

    // Add new group for new ponies
    var newPonyGroup = ponyGroup.enter()
        .append('svg:g')
        .attr('transform',function(d,i) { return 'translate(0,'+y(i)+')';})
        .classed('pony-group',true);

    // Update positions for existing lanes
    ponyGroup.select('.pony-lane')
        .transition().duration(100)
            .attr('y',y(.5-.025))
            .attr('height',y(.05)-y(0))
            .attr('x',x(10))
            .attr('width',x(90)-x(0))
            .style('fill',function(d) { return d.col;})
            .attr('height',y(.050)-y(0));

    // Add new lanes
    newPonyGroup.append('svg:rect')
            .attr('height',y(.05)-y(0))
            .attr('x',x(10))
            .attr('width',x(90)-x(0))
            .attr('y',y(.5-.025))
            .style('stroke','none')
            .classed('pony-lane',true)
            .style('fill',function(d) { return d.col;});

    // Update positions for existing ponies
    ponyGroup.select('.pony').select('.race-pony')
        .transition().duration(100)
            .style('fill',function(d) { return d.col;})
            .attr('height',y(.5)-y(0))
            .attr('width',x(10)-x(0))
            .attr('y',y(.25))
            .attr('height',y(.50)-y(0));

    // Add new pony groups
    var racePonies = newPonyGroup.append('svg:g')
            .classed('pony',true);

    racePonies.append('svg:rect')
            .attr('y',y(.25))
            .style('stroke','none')
            .attr('height',y(.5)-y(0))
            .attr('width',x(10)-x(0))
            .style('stroke','none')
            .classed('race-pony',true)
            .style('fill',function(d) { return d.col;});

    //racePonies.append('svg:text')
    //    .text(function(d) { console.log(d); return d.jockey[0]; })
    //    .attr('x',0)
    //    .attr('y',0);

    ponyGroup.exit().remove();
};

var expandAdvOptions = function() {
    if (advOptionsHidden) {
        d3.select('#adv-options-expand').style('display','block');
        d3.select('#adv-options-title').text('- Hide Advanced Options');
        advOptionsHidden = false;
    } else {
        d3.select('#adv-options-expand').style('display','none');
        d3.select('#adv-options-title').text('+ Show Advanced Options');
        advOptionsHidden = true;
    }
};
<commit_msg>Don't allow changes to jockey list inputs<commit_after>var getRandomInt = function(min, max) {
    return Math.floor(Math.random() * (max - min + 1)) + min;
};

var getStepSize = function() {
    return (90*ponies.length*50)/(raceLength*1000);
};

var runPonies = function() {
    if (running) {
        // Figure out which (if any) ponies haven't finished
        var notFinished = [];
        for (var i = 0; i < ponies.length; i++) {
            if (ponies[i].progress < 90) {
                notFinished.push(i);
            }
        }

        // See if any of the ponies still need to finish
        if (notFinished.length > 0) {
            // Move a random (still running) pony forward and record if they
            // made it to the end
            var tmp = getRandomInt(0,notFinished.length-1);
            ponies[notFinished[tmp]].progress += getStepSize();
            if (ponies[notFinished[tmp]].progress >= 90)
            {
                ponies[notFinished[tmp]].finishPos = nextPlace++;
            }

            // Actually move the pony
            d3.selectAll('.pony').data(ponies)
                .transition().ease('linear').duration(50)
                .attr('transform',function(d,i) { return 'translate('+x(d.progress)+',0)';})
            setTimeout(runPonies,50);
        } else {
            d3.select('#winner-list').selectAll('.rank-entry')
               .data(ponies,function(d) { return d.finishPos; })
              .enter()
                .append('p')
                .classed('rank-entry',true)
                .text(function(d,i) { return d.finishPos + ': ' + d.jockey;}) ;

           d3.selectAll('.rank-entry').sort(function(a,b) { return a.finishPos < b.finishPos ? -1 : 1; }).order();

           setTimeout(function() { d3.select('#winner-list').style('display','block'); }, 350);

            d3.selectAll('#controls button').filter(function(d,i) { return i == 0;}).text('Done');
            done = true;
            running = false;
        }
    }
};

var clearWinners = function() {
    d3.selectAll('.rank-entry').remove();
    d3.select('#winner-list').style('display','none');
};

var addEntrant = function() {
    if (allowChanging) {
        var name = $('#name').val(),
            num = $('#num').val(),
            col = $('#color1').val();

        if (name.length > 0 && !isNaN(+num)) {
            // Check to see if a jockey name exists, if so, don't allow it
            // to be reused
            var previouslyUsed = false;
            for (var i=0; i < jockeys.length; i++) {
                previouslyUsed = name == jockeys[i].name ? true : false;
            }
            if (previouslyUsed) {
                window.alert('Sorry, you can not reuse a Jockey')
            } else {

                // Add the individual ponies and the jockey to the list
                for (var i = 0; i < +num; i++) {
                    ponies.push({progress:0,
                            jockey:name,
                            col:col,
                            finishPos:0});
                }

                // Add the jockey to the jockies list
                jockeys.push({name:name,num:num,col:col});

                updateJockeyLineup();
                updatePonyLineup();
            }

            $('#name').val('Jockey');
            $('#num').val('#');
            formDefaulter.add('name');
            formDefaulter.add('num');
        } else {
            window.alert('Please enter a unique jockey name and # of ponies for that jockey!')
        }
    }
};

var removeEntrant = function(i) {
    if (allowChanging) {
        // Figure out which indexes we need to remove
        var toRemove = [];
        for (var j = 0; j < ponies.length; j++) {
            if (ponies[j].jockey == jockeys[i].name) {
                toRemove.push(j);
            }
        }

        // Remove them in reverse order so we don't have
        // to do anything fancy with indexes
        for (var j = toRemove.length-1; j > -1; j--) {
            ponies.splice(toRemove[j],1);
        }

        // Remove jockey's rects and move remaining ponies/fix y-scale
        updatePonyLineup();

        // Remove jockeys name from displayed list list
        jockeys.splice(i,1);
        updateJockeyLineup();
    }
};

var resetPonies = function() {
    // Send the ponies back to the beginning
    for (var i = 0; i < ponies.length; i++) {
        ponies[i].progress = 0;
    }

    done = false;
    running = false;
    nextPlace = 1;

    d3.selectAll('.pony').data(ponies)
        .transition().ease('linear').duration(250)
        .attr('transform',function(d,i) { return 'translate('+x(d.progress)+',0)';})

    d3.selectAll('#controls button').filter(function(d,i) { return i ==
            0;}).text('Go!');

    d3.selectAll('.rank-entry').remove();
};

var updateJockeyLineup = function() {

    // Select current jockey list
    var jockeyList = d3.select('#jockey-list').selectAll('.jockey')
            .data(jockeys);

    // Remove old jockeys
    jockeyList.exit().remove();

    // Update entries for existing jockeys
    jockeyList.select('.jockey-name').attr('value',function(d) { return d.name; })
    jockeyList.select('.jockey-num').attr('value',function(d) { return d.num; })
    jockeyList.select('.jockey-col').style('background-color',function(d) { return d.col; })

    // Add new jockey entries
    var newJockeyList = jockeyList.enter()
            .append('div')
            .classed('jockey',true);

    newJockeyList.append('input')
        .attr('type','text')
        .attr('disabled','disabled')
        .attr('value',function(d) { return d.name; })
        .classed('jockey-name',true);

    newJockeyList.append('input')
        .attr('type','text')
        .attr('disabled','disabled')
        .attr('value',function(d) { return d.num; })
        .classed('jockey-num',true);

    newJockeyList.append('input')
        .attr('type','text')
        .attr('disabled','disabled')
        .style('background-color',function(d) { return d.col; })
        .classed('jockey-col',true);

    newJockeyList.append('span')
        .text('-')
        .classed('jockey-remove',true)
        .on('click',function(d,i) {removeEntrant(i); });

};

var updatePonyLineup = function() {
    // Make sure y domain is correct
    y.domain([0,ponies.length]);

    var ponyGroup = d3.select('#data-region').selectAll('.pony-group')
            .data(ponies);

    // Update position for old ponies
    ponyGroup.transition().duration(100)
            .attr('transform',function(d,i) { return 'translate(0,'+y(i)+')';});

    // Add new group for new ponies
    var newPonyGroup = ponyGroup.enter()
        .append('svg:g')
        .attr('transform',function(d,i) { return 'translate(0,'+y(i)+')';})
        .classed('pony-group',true);

    // Update positions for existing lanes
    ponyGroup.select('.pony-lane')
        .transition().duration(100)
            .attr('y',y(.5-.025))
            .attr('height',y(.05)-y(0))
            .attr('x',x(10))
            .attr('width',x(90)-x(0))
            .style('fill',function(d) { return d.col;})
            .attr('height',y(.050)-y(0));

    // Add new lanes
    newPonyGroup.append('svg:rect')
            .attr('height',y(.05)-y(0))
            .attr('x',x(10))
            .attr('width',x(90)-x(0))
            .attr('y',y(.5-.025))
            .style('stroke','none')
            .classed('pony-lane',true)
            .style('fill',function(d) { return d.col;});

    // Update positions for existing ponies
    ponyGroup.select('.pony').select('.race-pony')
        .transition().duration(100)
            .style('fill',function(d) { return d.col;})
            .attr('height',y(.5)-y(0))
            .attr('width',x(10)-x(0))
            .attr('y',y(.25))
            .attr('height',y(.50)-y(0));

    // Add new pony groups
    var racePonies = newPonyGroup.append('svg:g')
            .classed('pony',true);

    racePonies.append('svg:rect')
            .attr('y',y(.25))
            .style('stroke','none')
            .attr('height',y(.5)-y(0))
            .attr('width',x(10)-x(0))
            .style('stroke','none')
            .classed('race-pony',true)
            .style('fill',function(d) { return d.col;});

    //racePonies.append('svg:text')
    //    .text(function(d) { console.log(d); return d.jockey[0]; })
    //    .attr('x',0)
    //    .attr('y',0);

    ponyGroup.exit().remove();
};

var expandAdvOptions = function() {
    if (advOptionsHidden) {
        d3.select('#adv-options-expand').style('display','block');
        d3.select('#adv-options-title').text('- Hide Advanced Options');
        advOptionsHidden = false;
    } else {
        d3.select('#adv-options-expand').style('display','none');
        d3.select('#adv-options-title').text('+ Show Advanced Options');
        advOptionsHidden = true;
    }
};
<|endoftext|>


<commit_before>'use strict';
var util = require('util'),
    generators = require('yeoman-generator'),
    chalk = require('chalk'),
    scriptBase = require('../generator-base'),
    packagejs = require('../../package.json'),
    fs = require('fs'),
    shelljs = require('shelljs'),
    exec = require('child_process').exec,
    semver = require('semver'),
    Git = require('git-wrapper');

var UpgradeGenerator = generators.Base.extend({});

util.inherits(UpgradeGenerator, scriptBase);

var configOptions = {};

/* Constants used throughout */
const GENERATOR_JHIPSTER = 'generator-jhipster',
    UPGRADE_BRANCH = 'jhipster_upgrade';

module.exports = UpgradeGenerator.extend({
    constructor: function () {
        generators.Base.apply(this, arguments);

        this.git = new Git();

        this.force = this.options['force'];
    },

    initializing: {
        displayLogo: function () {
            this.log(chalk.white('Welcome to the JHipster Upgrade Sub-Generator '));
            this.log(chalk.white('This will upgrade your current application codebase to the latest JHipster version');
        },

        getCurrentJHVersion: function () {
            this.currentVersion = this.config.get('jhipsterVersion');
        }
    },

    _gitCheckout: function(branch) {
        var done = this.async();
        this.git.exec('checkout', {q: true}, [branch], function(err, msg) {
            if (err != null) this.error('Unable to checkout branch ' + branch + ':\n' + err);
            this.log('Checked out branch \'' + branch + '\'');
            done();
        }.bind(this));
    },

    _gitCommitAll: function(commitMsg, callback) {
        var commit = function() {
            this.git.exec('commit', {q: true}, ['-m \"' + commitMsg + '\"', '-a', '--allow-empty'], function(err, msg) {
                if (err != null) this.error('Unable to commit in git:\n' + err);
                this.log('Committed: ' + commitMsg);
                callback();
            }.bind(this));
        }.bind(this);
        this.git.exec('add', {}, ['-A'], function(err, msg) {
            if (err != null) this.error('Unable to add resources in git:\n' + err);
            commit();
        }.bind(this));
    },

    configuring: {
        assertGitPresent: function() {
            var done = this.async();
            this.isGitInstalled(function () {
                done();
            }, function () {
                this.error('Exiting the process.');
                done();
            });
        },

        checkLatestVersion: function() {
            this.log('Looking for latest ' + GENERATOR_JHIPSTER + ' version...');
            var done = this.async();
            shelljs.exec('npm show ' + GENERATOR_JHIPSTER + ' version', {silent:true}, function (code, stdout, stderr) {
                this.latestVersion = stdout.replace('\n','');
                if (semver.lt(this.currentVersion, this.latestVersion)) {
                    this.log(chalk.green('New ' + GENERATOR_JHIPSTER + ' version found: ' + this.latestVersion));
                } else if (this.force) {
                    this.log(chalk.yellow('Forced re-generation'));
                } else {
                    this.error(chalk.green('No update available.') + ' Application has already been generated with latest version.');
                }
                done();
            }.bind(this));
        },

        assertGitRepository: function() {
            if (! fs.existsSync('.git')) {
                var done = this.async();
                this.git.exec('init', {}, [], function(err, msg) {
                    if (err != null) this.error('Unable to initialize a new git repository:\n' + err);
                    this.log('Initialized a new git repository');
                    this._gitCommitAll('Initial', function() {
                        done();
                    });
                }.bind(this));
            }
        },

        assertNoLocalChanges: function() {
            var done = this.async();
            this.git.exec('status', {}, ['--porcelain'], function(err, msg) {
                if (err != null) this.error('Unable to check for local changes:\n' + err);
                if (msg != null && msg !== '') {
                    this.warning(' local changes found.\n' +
                        '\tPlease commit/stash them before upgrading');
                    this.error('Exiting process');
                }
                done();
            }.bind(this));
        },

        detectCurrentBranch: function() {
            var done = this.async();
            this.git.exec('rev-parse', {q: true}, ['--abbrev-ref', 'HEAD'], function(err, msg) {
                if (err != null) this.error('Unable to detect current git branch:\n' + err);
                this.sourceBranch = msg.replace('\n','');
                done();
            }.bind(this));
        },

        prepareUpgradeBranch: function() {
            var done = this.async();
            var createUpgradeBranch = function() {
                this.git.exec('branch', {q: true}, [UPGRADE_BRANCH], function(err, msg) {
                    if (err != null) this.error('Unable to create ' + UPGRADE_BRANCH + ':\n' + err);
                    this.log('Created branch ' + UPGRADE_BRANCH);
                }.bind(this));
            }.bind(this);
            this.git.exec('rev-parse', {q: true}, ['--verify', UPGRADE_BRANCH], function(err, msg) {
                if (err != null) createUpgradeBranch();
                done();
            }.bind(this));
        },

        checkoutUpgradeBranch: function() {
            this._gitCheckout(UPGRADE_BRANCH);
        }
    },

    default: {
        insight: function () {
            var insight = this.insight();
            insight.trackWithEvent('generator', 'upgrade');
        },

        updateJhipster: function() {
            this.log('Updating ' + GENERATOR_JHIPSTER + '. This might take some time...');
            var done = this.async();
            shelljs.exec('npm install -g ' + GENERATOR_JHIPSTER, {silent:true}, function (code, stdout, stderr) {
                this.log(chalk.green('Updated ' + GENERATOR_JHIPSTER + ' to version ' + this.latestVersion));
                done();
            }.bind(this));
        },

        cleanUp: function() {
            var done = this.async();
            this.git.exec('rm', {q: true}, ['-rf', '--ignore-unmatch', '*'], function(err, msg) {
                if (err != null) return this.error('Unable to cleanup directory:\n' + err);
                shelljs.exec('ls -a1 | grep -E -v \'.yo-rc.json|.git\|node_modules\' | xargs rm -rf +', {silent:true},
                    function (code, stdout, stderr) {
                        this.log('Cleaned up directory');
                        done();
                    }.bind(this));
            }.bind(this));
        },

        generate: function() {
            this.log('Regenerating app with jhipster ' + this.latestVersion + '...');
            var done = this.async();
            shelljs.exec('yo jhipster --force --with-entities', {silent:true}, function (code, stdout, stderr) {
                this.log(chalk.green('Successfully regenerated app with jhipster ' + this.latestVersion));
                done();
            }.bind(this));
        },

        commitChanges: function() {
            var done = this.async();
            this._gitCommitAll('Upgrade to ' + this.latestVersion, function() {done();});
        },

        checkoutSourceBranch: function() {
            this._gitCheckout(this.sourceBranch);
        },

        mergeChangesBack: function() {
            this.log('Merging changes back to ' + this.sourceBranch + '...');
            var done = this.async();
            this.git.exec('merge', {q: true}, [UPGRADE_BRANCH], function(err, msg) {
                if (err != null) return this.error('Unable to merge changes back to ' + this.sourceBranch + ':\n' + err);
                this.log(chalk.green('Merge done !') + '\n\tPlease now fix conflicts if any, and commit !');
                done();
            }.bind(this));
        }
    },

    end: function () {
        if (this.abort) return;
        this.log(chalk.green.bold('\nUpgraded successfully.\n'));
    }

});
<commit_msg>polish #3596<commit_after>'use strict';
var util = require('util'),
    generators = require('yeoman-generator'),
    chalk = require('chalk'),
    scriptBase = require('../generator-base'),
    packagejs = require('../../package.json'),
    fs = require('fs'),
    shelljs = require('shelljs'),
    exec = require('child_process').exec,
    semver = require('semver'),
    Git = require('git-wrapper');

var UpgradeGenerator = generators.Base.extend({});

util.inherits(UpgradeGenerator, scriptBase);

var configOptions = {};

/* Constants used throughout */
const GENERATOR_JHIPSTER = 'generator-jhipster',
    UPGRADE_BRANCH = 'jhipster_upgrade';

module.exports = UpgradeGenerator.extend({
    constructor: function () {
        generators.Base.apply(this, arguments);

        this.git = new Git();

        this.force = this.options['force'];
    },

    initializing: {
        displayLogo: function () {
            this.log(chalk.white('Welcome to the JHipster Upgrade Sub-Generator '));
            this.log(chalk.white('This will upgrade your current application codebase to the latest JHipster version');
        },

        getCurrentJHVersion: function () {
            this.currentVersion = this.config.get('jhipsterVersion');
        }
    },

    _gitCheckout: function(branch) {
        var done = this.async();
        this.git.exec('checkout', {q: true}, [branch], function(err, msg) {
            if (err != null) this.error('Unable to checkout branch ' + branch + ':\n' + err);
            this.log('Checked out branch \'' + branch + '\'');
            done();
        }.bind(this));
    },

    _gitCommitAll: function(commitMsg, callback) {
        var commit = function() {
            this.git.exec('commit', {q: true}, ['-m \"' + commitMsg + '\"', '-a', '--allow-empty'], function(err, msg) {
                if (err != null) this.error('Unable to commit in git:\n' + err);
                this.log('Committed: ' + commitMsg);
                callback();
            }.bind(this));
        }.bind(this);
        this.git.exec('add', {}, ['-A'], function(err, msg) {
            if (err != null) this.error('Unable to add resources in git:\n' + err);
            commit();
        }.bind(this));
    },

    configuring: {
        assertGitPresent: function() {
            var done = this.async();
            this.isGitInstalled(function () {
                done();
            }, function () {
                this.error('Exiting the process.');
                done();
            });
        },

        checkLatestVersion: function() {
            this.log('Looking for latest ' + GENERATOR_JHIPSTER + ' version...');
            var done = this.async();
            shelljs.exec('npm show ' + GENERATOR_JHIPSTER + ' version', {silent:true}, function (code, stdout, stderr) {
                this.latestVersion = stdout.replace('\n','');
                if (semver.lt(this.currentVersion, this.latestVersion)) {
                    this.log(chalk.green('New ' + GENERATOR_JHIPSTER + ' version found: ' + this.latestVersion));
                } else if (this.force) {
                    this.log(chalk.yellow('Forced re-generation'));
                } else {
                    this.error(chalk.green('No update available.') + ' Application has already been generated with latest version.');
                }
                done();
            }.bind(this));
        },

        assertGitRepository: function() {
            if (! fs.existsSync('.git')) {
                var done = this.async();
                this.git.exec('init', {}, [], function(err, msg) {
                    if (err != null) this.error('Unable to initialize a new git repository:\n' + err);
                    this.log('Initialized a new git repository');
                    this._gitCommitAll('Initial', function() {
                        done();
                    });
                }.bind(this));
            }
        },

        assertNoLocalChanges: function() {
            var done = this.async();
            this.git.exec('status', {}, ['--porcelain'], function(err, msg) {
                if (err != null) this.error('Unable to check for local changes:\n' + err);
                if (msg != null && msg !== '') {
                    this.warning(' local changes found.\n' +
                        '\tPlease commit/stash them before upgrading');
                    this.error('Exiting process');
                }
                done();
            }.bind(this));
        },

        detectCurrentBranch: function() {
            var done = this.async();
            this.git.exec('rev-parse', {q: true}, ['--abbrev-ref', 'HEAD'], function(err, msg) {
                if (err != null) this.error('Unable to detect current git branch:\n' + err);
                this.sourceBranch = msg.replace('\n','');
                done();
            }.bind(this));
        },

        prepareUpgradeBranch: function() {
            var done = this.async();
            var createUpgradeBranch = function() {
                this.git.exec('branch', {q: true}, [UPGRADE_BRANCH], function(err, msg) {
                    if (err != null) this.error('Unable to create ' + UPGRADE_BRANCH + ':\n' + err);
                    this.log('Created branch ' + UPGRADE_BRANCH);
                }.bind(this));
            }.bind(this);
            this.git.exec('rev-parse', {q: true}, ['--verify', UPGRADE_BRANCH], function(err, msg) {
                if (err != null) createUpgradeBranch();
                done();
            }.bind(this));
        },

        checkoutUpgradeBranch: function() {
            this._gitCheckout(UPGRADE_BRANCH);
        }
    },

    default: {
        insight: function () {
            var insight = this.insight();
            insight.trackWithEvent('generator', 'upgrade');
        },

        updateJhipster: function() {
            this.log('Updating ' + GENERATOR_JHIPSTER + '. This might take some time...');
            var done = this.async();
            shelljs.exec('npm install -g ' + GENERATOR_JHIPSTER, {silent:true}, function (code, stdout, stderr) {
                this.log(chalk.green('Updated ' + GENERATOR_JHIPSTER + ' to version ' + this.latestVersion));
                done();
            }.bind(this));
        },

        cleanUp: function() {
            var done = this.async();
            this.git.exec('rm', {q: true}, ['-rf', '--ignore-unmatch', '*'], function(err, msg) {
                if (err != null) return this.error('Unable to cleanup directory:\n' + err);
                shelljs.exec('ls -a1 | grep -E -v \'.yo-rc.json|.git\|node_modules\' | xargs rm -rf +', {silent:true},
                    function (code, stdout, stderr) {
                        this.log('Cleaned up directory');
                        done();
                    }.bind(this));
            }.bind(this));
        },

        generate: function() {
            this.log('Regenerating app with jhipster ' + this.latestVersion + '...');
            var done = this.async();
            shelljs.exec('yo jhipster --force --with-entities', {silent:false}, function (code, stdout, stderr) {
                if (!stderr) this.log(chalk.green('Successfully regenerated app with jhipster ' + this.latestVersion));
                else this.error('Something went wrong!')
                done();
            }.bind(this));
        },

        commitChanges: function() {
            var done = this.async();
            this._gitCommitAll('Upgrade to ' + this.latestVersion, function() {
                done();
            });
        },

        checkoutSourceBranch: function() {
            this._gitCheckout(this.sourceBranch);
        },

        mergeChangesBack: function() {
            this.log('Merging changes back to ' + this.sourceBranch + '...');
            var done = this.async();
            this.git.exec('merge', {q: true}, [UPGRADE_BRANCH], function(err, msg) {
                if (err != null) this.error('Unable to merge changes back to ' + this.sourceBranch + ':\n' + err);
                this.log(chalk.green('Merge done !') + '\n\tPlease now fix conflicts if any, and commit !');
                done();
            }.bind(this));
        }
    },

    end: function () {
        this.log(chalk.green.bold('\nUpgraded successfully.\n'));
    }

});
<|endoftext|>