"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.getActiveSearch = getActiveSearch;
exports.getTimestamp = getTimestamp;
exports.getFeedWideRentalErrors = getFeedWideRentalErrors;
exports.getResponsesWithErrors = getResponsesWithErrors;
exports.getActiveError = getActiveError;
exports.getErrorMessage = getErrorMessage;
exports.getTotalFare = getTotalFare;
exports.getActiveItinerary = getActiveItinerary;
exports.getVisibleItineraryIndex = getVisibleItineraryIndex;
exports.hasValidLocation = hasValidLocation;
exports.queryIsValid = queryIsValid;
exports.getRealtimeEffects = getRealtimeEffects;
exports.getShowUserSettings = getShowUserSettings;
exports.getStopViewerConfig = getStopViewerConfig;
exports.getUiUrlParams = getUiUrlParams;
exports.getTitle = getTitle;
exports.getTransitiveData = exports.getAgenciesFromRoutes = exports.getModesForActiveAgencyFilter = exports.getSortedFilteredRoutes = exports.getFilteredRoutes = exports.getActiveItineraries = void 0;

require("core-js/modules/web.dom.iterable");

var _coreUtils = _interopRequireDefault(require("@opentripplanner/core-utils"));

var _lodash = _interopRequireDefault(require("lodash.isequal"));

var _lodash2 = _interopRequireDefault(require("lodash.memoize"));

var _moment = _interopRequireDefault(require("moment"));

var _objectHash = _interopRequireDefault(require("object-hash"));

var _qs = _interopRequireDefault(require("qs"));

var _reselect = require("reselect");

var _ui = require("../actions/ui");

var _callTaker = require("./call-taker");

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }

const {
  calculateFares
} = _coreUtils.default.itinerary;
/**
 * Get the active search object
 * @param {Object} state the redux state object
 * @returns {Object} an search object, or null if there is no active search
 */

function getActiveSearch(state) {
  return state.otp.searches[state.otp.activeSearchId];
}
/**
 * Get timestamp in the expected format used by otp-datastore (call taker
 * back end). Defaults to now.
 * TODO: move to OTP-UI?
 */


function getTimestamp(time = (0, _moment.default)()) {
  return time.format('YYYY-MM-DDTHH:mm:ss');
}
/**
 * Analyzes all rental networks provided in the response and returns a list of
 * those that have a FEED_WIDE error.
 */


function getFeedWideRentalErrors(response) {
  // return empty array if the plan key is not available in the response
  if (!response) return []; // get first available rental info (this assumes that there won't be
  // 2 rental info types (for example car rental access and vehicle
  // rental egress))

  const rentalInfo = response.bikeRentalInfo || response.carRentalInfo || response.vehicleRentalInfo;
  if (!rentalInfo) return []; // iterate through all networks and collect a list of feeds that have a feed-
  // wide error

  return Object.entries(rentalInfo).filter(([network, networkRentalInfo]) => networkRentalInfo.errors && networkRentalInfo.errors.some(error => error.severity === 'FEED_WIDE')).map(([network, networkRentalInfo]) => ({
    network,
    networkRentalInfo
  }));
}
/**
 * Converts a comma-separated list of OTP mode values into a human-readable
 * string
 */


function humanReadableMode(modeStr) {
  if (!modeStr) return 'N/A';
  const arr = modeStr.toLowerCase().replace(/_/g, ' ').split(',');

  if (arr.length > 2) {
    const last = arr.pop();
    return arr.join(', ') + ' and ' + last;
  } else {
    return arr.join(' and ');
  }
}
/**
 * Generates a list of issues/errors from a list of OTP responses.
 *
 * @param {Object} state the redux state object
 * @return {Array} An array of objects describing the errors seen. These will at
 *   a minimum contain the `msg` object describing the error, but could also
 *   include an `id` and `modes` key for trip planning errors or `network` for
 *   errors with vehicle rentals
 */


function getResponsesWithErrors(state) {
  const search = getActiveSearch(state);
  const rentalFeedErrors = [];
  const networksWithFeedWideErrors = {};
  const tripPlanningErrors = [];
  const response = !search ? null : search.response;

  if (response) {
    const showModes = response.length > 1;
    response.forEach(res => {
      if (res) {
        if (res.error) {
          var _res$requestParameter;

          const params = _qs.default.parse(res.url);

          const modeStr = ((_res$requestParameter = res.requestParameters) === null || _res$requestParameter === void 0 ? void 0 : _res$requestParameter.mode) || params.mode || '';
          let msg = res.error.msg || 'An error occurred while planning a trip'; // include the modes if doing batch routing

          if (showModes && modeStr) {
            const mode = humanReadableMode(modeStr);
            msg = `No trip found for ${mode}. ${msg.replace(/^No trip found. /, '')}`;
          }

          tripPlanningErrors.push({
            id: res.error.id,
            modes: modeStr.split(','),
            msg,
            url: res.url
          });
        }

        const feedWideRentalErrors = getFeedWideRentalErrors(res);
        feedWideRentalErrors.forEach(networkWithFeedWideError => {
          const {
            network
          } = networkWithFeedWideError;

          if (!networksWithFeedWideErrors[network]) {
            rentalFeedErrors.push({
              msg: `The ${network} network is unavailable at this moment.`,
              network
            });
            networksWithFeedWideErrors[network] = true;
          }
        });
      }
    });
  } // generate list so rental errors show up first


  return [...rentalFeedErrors, ...tripPlanningErrors];
}
/**
 * Gets the active error (either a planning error or a rental feed error). This
 * should only be used with itinerary routing, not batch routing.
 */


function getActiveError(state) {
  const errors = getResponsesWithErrors(state);
  return errors[errors.length - 1];
}
/**
 * Get the appropriate message for a trip planning error given possibly pre-
 * configured message overrides from the config.
 */


function getErrorMessage(error, errorMessages) {
  if (!error) return null; // use the message provided by OTP by default

  let message = error.msg; // check for configuration-defined message override

  if (errorMessages) {
    const msgConfig = errorMessages.find(m => m.id === error.id);

    if (msgConfig) {
      if (error.modes && msgConfig.modes) {
        for (const mode of msgConfig.modes) {
          if (error.modes.includes(mode)) {
            message = msgConfig.msg;
            break;
          }
        }
      } else message = msgConfig.msg;
    }
  }

  return message;
}
/**
 * Helper to get the active search's non-realtime response
 */


function getActiveSearchNonRealtimeResponse(state) {
  const search = getActiveSearch(state);
  return search && search.nonRealtimeResponse;
}
/**
 * Helper to get the active search's realtime response
 */


function getActiveSearchRealtimeResponse(state) {
  const search = getActiveSearch(state);
  return search && search.response;
} // Ignore certain keys that could add significant calculation time to hashing.
// The alerts are irrelevant, but the intermediateStops, interStopGeometry and
// steps could have the legGeometry substitute as an equivalent hash value


const blackListedKeys = ['alerts', 'intermediateStops', 'interStopGeometry', 'steps']; // make blackListedKeys into an object due to superior lookup performance

const blackListedKeyLookup = {};
blackListedKeys.forEach(key => {
  blackListedKeyLookup[key] = true;
});
/**
 * A memoized function to hash the itinerary.
 * NOTE: It can take a while (>30ms) for the object-hash library to calculate
 * an itinerary's hash for some lengthy itineraries. If better performance is
 * desired, additional values to blackListedKeys should be added to avoid
 * spending extra time hashing values that wouldn't result in different
 * itineraries.
 */

const hashItinerary = (0, _lodash2.default)(itinerary => (0, _objectHash.default)(itinerary, {
  excludeKeys: key => blackListedKeyLookup[key]
}));
/**
 * Get the active itineraries for the active search, which is dependent on
 * whether realtime or non-realtime results should be displayed
 * @param {Object} state the redux state object
 * @return {Array}      array of itinerary objects from the OTP plan response,
 *                      or null if there is no active search
 */

const getActiveItineraries = (0, _reselect.createSelector)(state => state.otp.config, getActiveSearchNonRealtimeResponse, state => state.otp.filter, getActiveSearchRealtimeResponse, state => state.otp.useRealtime, _callTaker.getActiveFieldTripRequest, (config, nonRealtimeResponse, itinerarySortSettings, realtimeResponse, useRealtime, activeFieldTripRequest) => {
  // set response to use depending on useRealtime
  const response = !nonRealtimeResponse && !realtimeResponse ? null : useRealtime ? realtimeResponse : nonRealtimeResponse;
  const itineraries = []; // keep track of itinerary hashes in order to not include duplicate
  // itineraries. Duplicate itineraries can occur in batch routing where a walk
  // to transit trip can sometimes still be the most optimal trip even when
  // additional modes such as bike rental were also requested

  const seenItineraryHashes = {};

  if (response) {
    response.forEach(res => {
      if (res && res.plan) {
        res.plan.itineraries.forEach(itinerary => {
          // hashing takes a while on itineraries
          const itineraryHash = hashItinerary(itinerary);

          if (!seenItineraryHashes[itineraryHash]) {
            itineraries.push(itinerary);
            seenItineraryHashes[itineraryHash] = true;
          }
        });
      }
    });
  }

  const {
    sort
  } = itinerarySortSettings;
  const {
    direction,
    type
  } = sort; // If no sort type is provided (e.g., because batch routing is not enabled),
  // do not sort itineraries (default sort from API response is used).
  // Also, do not sort itineraries if a field trip request is active

  return !type || Boolean(activeFieldTripRequest) ? itineraries : itineraries.sort((a, b) => sortItineraries(type, direction, a, b, config));
});
/**
 * Array sort function for itineraries (in batch routing context) that attempts
 * to sort based on the type/direction specified.
 */

exports.getActiveItineraries = getActiveItineraries;

function sortItineraries(type, direction, a, b, config) {
  var _config$itinerary;

  switch (type) {
    case 'WALKTIME':
      if (direction === 'ASC') return a.walkTime - b.walkTime;else return b.walkTime - a.walkTime;

    case 'ARRIVALTIME':
      if (direction === 'ASC') return a.endTime - b.endTime;else return b.endTime - a.endTime;

    case 'DEPARTURETIME':
      if (direction === 'ASC') return a.startTime - b.startTime;else return b.startTime - a.startTime;

    case 'DURATION':
      if (direction === 'ASC') return a.duration - b.duration;else return b.duration - a.duration;

    case 'COST':
      const configCosts = (_config$itinerary = config.itinerary) === null || _config$itinerary === void 0 ? void 0 : _config$itinerary.costs;
      const aTotal = getTotalFare(a, configCosts);
      const bTotal = getTotalFare(b, configCosts);
      if (direction === 'ASC') return aTotal - bTotal;else return bTotal - aTotal;

    default:
      if (type !== 'BEST') console.warn(`Sort (${type}) not supported. Defaulting to BEST.`); // FIXME: Fully implement default sort algorithm.

      const aCost = calculateItineraryCost(a, config);
      const bCost = calculateItineraryCost(b, config);
      if (direction === 'ASC') return aCost - bCost;else return bCost - aCost;
  }
}
/**
 * Default constants for calculating itinerary "cost", i.e., how preferential a
 * particular itinerary is based on factors like wait time, total fare, drive
 * time, etc.
 */


const DEFAULT_WEIGHTS = {
  driveReluctance: 2,
  durationFactor: 0.25,
  fareFactor: 0.5,
  transferReluctance: 0.9,
  waitReluctance: 0.1,
  walkReluctance: 0.1
};
/**
 * This calculates the "cost" (not the monetary cost, but the cost according to
 * multiple factors like duration, total fare, and walking distance) for a
 * particular itinerary, for use in sorting itineraries.
 * FIXME: Do major testing to get this right.
 */

function calculateItineraryCost(itinerary, config = {}) {
  var _config$itinerary2;

  // Initialize weights to default values.
  const weights = DEFAULT_WEIGHTS; // If config contains values to override defaults, apply those.

  const configWeights = config.itinerary && config.itinerary.weights;
  if (configWeights) Object.assign(weights, configWeights);
  return getTotalFare(itinerary, (_config$itinerary2 = config.itinerary) === null || _config$itinerary2 === void 0 ? void 0 : _config$itinerary2.costs) * weights.fareFactor + itinerary.duration * weights.durationFactor + itinerary.walkDistance * weights.walkReluctance + getDriveTime(itinerary) * weights.driveReluctance + itinerary.waitingTime * weights.waitReluctance + itinerary.transfers * weights.transferReluctance;
}
/**
 * Get total drive time (i.e., total duration for legs with mode=CAR) for an
 * itinerary.
 */


function getDriveTime(itinerary) {
  if (!itinerary) return 0;
  let driveTime = 0;
  itinerary.legs.forEach(leg => {
    if (leg.mode === 'CAR') driveTime += leg.duration;
  });
  return driveTime;
}
/**
 * Default costs for modes that currently have no costs evaluated in
 * OpenTripPlanner.
 */


const DEFAULT_COSTS = {
  // $2 per trip? This is a made up number.
  bikeshareTripCostCents: 2 * 100,
  // $2 for 3 hours of parking?
  carParkingCostCents: 3 * 2.00 * 100,
  // FL per diem rate: https://www.flcourts.org/content/download/219314/1981830/TravelInformation.pdf
  drivingCentsPerMile: 0.445 * 100
};
/**
 * Returns total fare for itinerary (in cents)
 * FIXME: Move to otp-ui?
 * TODO: Add GBFS fares
 */

function getTotalFare(itinerary, configCosts = {}) {
  // Get transit/TNC fares.
  const {
    maxTNCFare,
    transitFare
  } = calculateFares(itinerary); // Start with default cost values.

  const costs = DEFAULT_COSTS; // If config contains values to override defaults, apply those.

  if (configCosts) Object.assign(costs, configCosts); // Calculate total cost from itinerary legs.

  let drivingCost = 0;
  let hasBikeshare = false;
  itinerary.legs.forEach(leg => {
    if (leg.mode === 'CAR') {
      // Convert meters to miles and multiple by cost per mile.
      drivingCost += leg.distance * 0.000621371 * costs.drivingCentsPerMile;
    }

    if (leg.mode === 'BICYCLE_RENT' || leg.mode === 'MICROMOBILITY' || leg.rentedBike) {
      hasBikeshare = true;
    }
  });
  const bikeshareCost = hasBikeshare ? costs.bikeshareTripCostCents : 0; // If some leg uses driving, add parking cost to the total.

  if (drivingCost > 0) drivingCost += costs.carParkingCostCents;
  return bikeshareCost + drivingCost + transitFare + maxTNCFare * 100;
}
/**
 * Get the active itinerary/profile for the active search object
 * @param {Object} state the redux state object
 * @returns {Object} an itinerary object from the OTP plan response, or null if
 *   there is no active search or itinerary
 */


function getActiveItinerary(state) {
  const search = getActiveSearch(state);
  const itineraries = getActiveItineraries(state);
  if (!itineraries || !search) return null;
  return itineraries.length > search.activeItinerary && search.activeItinerary >= 0 ? itineraries[search.activeItinerary] : null;
}
/**
 * Returns true if the OTP state contains a response within the current active search.
 * @param {*} state The entire redux state used to obtain the response.
 */


function activeSearchHasResponse(state) {
  const activeSearch = getActiveSearch(state);
  return !!(activeSearch && activeSearch.response);
}
/**
 * Returns the raw OTP response for the current active search.
 * @param {*} state The entire redux state used to obtain the response.
 */


function getOtpResponse(state) {
  var _getActiveSearch, _getActiveSearch$resp;

  return (_getActiveSearch = getActiveSearch(state)) === null || _getActiveSearch === void 0 ? void 0 : (_getActiveSearch$resp = _getActiveSearch.response) === null || _getActiveSearch$resp === void 0 ? void 0 : _getActiveSearch$resp.otp;
}
/**
 * Returns true if the query routing type specifies 'ITINERARY' and an OTP response is available.
 * @param {*} state The entire redux state in which the query is stored.
 */


function itineraryResponseExists(state) {
  var _activeSearch$respons, _activeSearch$query;

  const activeSearch = getActiveSearch(state);
  return (activeSearch === null || activeSearch === void 0 ? void 0 : (_activeSearch$respons = activeSearch.response) === null || _activeSearch$respons === void 0 ? void 0 : _activeSearch$respons.length) > 0 && (activeSearch === null || activeSearch === void 0 ? void 0 : (_activeSearch$query = activeSearch.query) === null || _activeSearch$query === void 0 ? void 0 : _activeSearch$query.routingType) === 'ITINERARY';
}
/**
 * Returns the visible itinerary index to render on the map and in narrative components.
 * @param {*} state The entire redux state from which to retrieve the itinerary.
 */


function getVisibleItineraryIndex(state) {
  var _getActiveSearch2;

  return (_getActiveSearch2 = getActiveSearch(state)) === null || _getActiveSearch2 === void 0 ? void 0 : _getActiveSearch2.visibleItinerary;
}
/**
 * Returns the itinerary to be passed to the transitive overlay for rendering.
 * @param {*} state The entire redux state from which to derive the itinerary.
 */


function getItineraryToRender(state) {
  const visibleItineraryIndex = getVisibleItineraryIndex(state);
  const activeItinerary = getActiveItinerary(state);
  const itins = getActiveItineraries(state);
  return itins[visibleItineraryIndex] || activeItinerary;
}

const routeSelector = state => Object.values(state.otp.transitIndex.routes);

const routeViewerFilterSelector = state => state.otp.ui.routeViewer.filter;
/**
 * Returns all routes that match the route viewer filters
 */


const getFilteredRoutes = (0, _reselect.createSelector)(routeSelector, routeViewerFilterSelector, (routes, filter) => routes.filter(route => // If the filter isn't defined, don't check.
(!filter.agency || filter.agency === route.agencyName) && (!filter.mode || filter.mode === route.mode) && ( // If user search is active, filter by either the long or short name
!filter.search || route.longName && route.longName.toLowerCase().includes(filter.search.toLowerCase()) || route.shortName && route.shortName.toLowerCase().includes(filter.search.toLowerCase()))));
/**
 * Sorts routes filtered by the selector which filters routes
 */

exports.getFilteredRoutes = getFilteredRoutes;
const getSortedFilteredRoutes = (0, _reselect.createSelector)(getFilteredRoutes, state => state.otp.config.transitOperators, (routes, transitOperators) => routes.sort(_coreUtils.default.route.makeRouteComparator(transitOperators)));
/**
 * Get the modes available for the current agency filter. First filters only by agency,
 * then extracts each mode
 */

exports.getSortedFilteredRoutes = getSortedFilteredRoutes;
const getModesForActiveAgencyFilter = (0, _reselect.createSelector)(routeSelector, routeViewerFilterSelector, (routes, filter) => Array.from(new Set(routes.filter(route => route.mode && (!filter.agency || filter.agency === route.agencyName)).map(route => route.mode).filter(mode => mode !== undefined))).sort());
/**
 * Returns list of agencies present within all routes
 */

exports.getModesForActiveAgencyFilter = getModesForActiveAgencyFilter;
const getAgenciesFromRoutes = (0, _reselect.createSelector)(routeSelector, routes => Array.from(new Set(routes.map(route => route.agencyName || route.agency.name))).filter(agency => agency !== undefined).sort());
/**
 * Converts an OTP itinerary to the transitive.js format,
 * using a selector to prevent unnecessary re-renderings of the transitive overlay.
 */

exports.getAgenciesFromRoutes = getAgenciesFromRoutes;
const getTransitiveData = (0, _reselect.createSelector)(activeSearchHasResponse, getOtpResponse, itineraryResponseExists, getItineraryToRender, state => state.otp.config.companies, (state, props) => props.getTransitiveRouteLabel, (hasResponse, otpResponse, hasItineraryResponse, itineraryToRender, companies, getTransitiveRouteLabel) => {
  if (hasResponse) {
    if (hasItineraryResponse) {
      return itineraryToRender ? _coreUtils.default.map.itineraryToTransitive(itineraryToRender, companies, getTransitiveRouteLabel) : null;
    } else if (otpResponse) {
      return otpResponse;
    }
  }
});
/**
 * Determine if the current query has a valid location, including lat/lon
 * @param {Object} query an OTP query object
 * @param {string} locationKey the location key ('from' or 'to')
 * @returns {boolean}
 */

exports.getTransitiveData = getTransitiveData;

function hasValidLocation(query, locationKey) {
  return query[locationKey] && query[locationKey].lat && query[locationKey].lon;
}
/**
 * Determine if the current query is valid
 * @param {Object} state the redux state object
 * @returns {boolean}
 */


function queryIsValid(state) {
  const {
    currentQuery
  } = state.otp;
  return hasValidLocation(currentQuery, 'from') && hasValidLocation(currentQuery, 'to'); // TODO: add mode validation
  // TODO: add date/time validation
}

function getRealtimeEffects(state) {
  const search = getActiveSearch(state);
  const realtimeItineraries = search && search.response && search.response.plan ? search.response.plan.itineraries : null;
  const hasNonRealtimeItineraries = search && search.nonRealtimeResponse && search.nonRealtimeResponse.plan;
  const nonRealtimeItineraries = hasNonRealtimeItineraries ? search.nonRealtimeResponse.plan.itineraries : null;
  const isAffectedByRealtimeData = !!(realtimeItineraries && hasNonRealtimeItineraries && // FIXME: Are realtime impacts only indicated by a change in the duration
  // of the first itinerary
  realtimeItineraries[0].duration !== nonRealtimeItineraries[0].duration);
  const normalRoutes = isAffectedByRealtimeData && nonRealtimeItineraries ? nonRealtimeItineraries[0].legs.filter(leg => !!leg.route).map(leg => leg.route) : [];
  const realtimeRoutes = isAffectedByRealtimeData && realtimeItineraries ? realtimeItineraries[0].legs.filter(leg => !!leg.route).map(leg => leg.route) : [];
  const normalDuration = isAffectedByRealtimeData && nonRealtimeItineraries ? nonRealtimeItineraries[0].duration : 0;
  const realtimeDuration = isAffectedByRealtimeData && realtimeItineraries ? realtimeItineraries[0].duration : 0;
  return {
    exceedsThreshold: Math.abs(normalDuration - realtimeDuration) >= state.otp.config.realtimeEffectsDisplayThreshold,
    isAffectedByRealtimeData,
    normalDuration,
    normalRoutes,
    realtimeDuration,
    realtimeRoutes,
    routesDiffer: !(0, _lodash.default)(normalRoutes, realtimeRoutes)
  }; // // TESTING: Return this instead to simulate a realtime-affected itinerary.
  // return {
  //   isAffectedByRealtimeData: true,
  //   normalRoutes: ['10', '2', '10'],
  //   realtimeRoutes: ['1', '2'],
  //   routesDiffer: true,
  //   normalDuration: 1000,
  //   realtimeDuration: 800,
  //   exceedsThreshold: true
  // }
}
/**
 * Determine whether user settings panel is enabled.
 */


function getShowUserSettings(state) {
  var _state$otp$config$per;

  return (_state$otp$config$per = state.otp.config.persistence) === null || _state$otp$config$per === void 0 ? void 0 : _state$otp$config$per.enabled;
}

function getStopViewerConfig(state) {
  return state.otp.config.stopViewer;
}
/**
 * Assemble any UI-state properties to be tracked via URL into a single object
 * TODO: Expand to include additional UI properties
 */


function getUiUrlParams(state) {
  const activeSearch = getActiveSearch(state);
  const uiParams = {
    ui_activeItinerary: activeSearch ? activeSearch.activeItinerary : 0,
    ui_activeSearch: state.otp.activeSearchId
  };
  return uiParams;
} // Set default title to the original document title (on load) set in index.html


const DEFAULT_TITLE = document.title;

function getTitle(state) {
  // Override title can optionally be provided in config.yml
  const {
    config,
    ui,
    user
  } = state.otp;
  let title = config.title || DEFAULT_TITLE;
  const {
    mainPanelContent,
    viewedRoute,
    viewedStop
  } = ui;

  switch (mainPanelContent) {
    case _ui.MainPanelContent.ROUTE_VIEWER:
      title += ' | Route';
      if (viewedRoute && viewedRoute.routeId) title += ` ${viewedRoute.routeId}`;
      break;

    case _ui.MainPanelContent.STOP_VIEWER:
      title += ' | Stop';
      if (viewedStop && viewedStop.stopId) title += ` ${viewedStop.stopId}`;
      break;

    default:
      const activeSearch = getActiveSearch(state);

      if (activeSearch) {
        title += ` | ${_coreUtils.default.query.summarizeQuery(activeSearch.query, user.locations)}`;
      }

      break;
  } // if (printView) title += ' | Print'


  return title;
}

//# sourceMappingURL=state.js