umts/pvta-multiplatform

View on GitHub
src/pages/plan-trip/plan-trip.component.ts

Summary

Maintainability
D
2 days
Test Coverage
import { Component, ViewChild, ElementRef } from '@angular/core';
import { Geolocation } from '@ionic-native/geolocation';
import { NavController, LoadingController, AlertController, NavParams } from 'ionic-angular';
import { StopService } from '../../providers/stop.service';
import { MapService } from '../../providers/map.service';
import { FavoriteTripService } from '../../providers/favorite-trip.service';
import { InfoService } from '../../providers/info.service';
import { StopComponent } from '../stop/stop.component';
import { ToastService } from '../../providers/toast.service';
import * as moment from 'moment';

declare var google, ga;
@Component({
  selector: 'page-plan-trip',
  templateUrl: 'plan-trip.html'
})
export class PlanTripComponent {
  @ViewChild('directionsMap') mapElement: ElementRef;
  @ViewChild('routeScrollArea') routeElement: ElementRef;
  bounds;
  request;
  originPlace;
  originInput: string = '';
  destinationPlace;
  noLocation: boolean;
  destinationInput;
  directionsDisplay;
  map;
  route;
  loader;
  timeOptions = [];
  noLocationToast;
  toastHandler;
  originDestToast;
  isInternetExplorer: boolean = false;

  constructor(public navCtrl: NavController, private stopService: StopService,
    private loadingCtrl: LoadingController, private alertCtrl: AlertController,
    private tripService: FavoriteTripService, private navParams: NavParams,
    private infoSvc: InfoService, private mapSvc: MapService,
    private geolocation: Geolocation, private toastSvc: ToastService) {
    /* List of the different types of times that we can request trips.
     * Each type has a name (for the UI) and a few properties for us:
     * type: whether the user wants a "departure" or "arrival"
     * isASAP: whether we should ignore all other given times and
               request a trip leaving NOW
      */
    this.isInternetExplorer = this.infoSvc.isInternetExplorer();
    this.timeOptions = [
      { title: 'Leaving Now', type: 'departure', isASAP: true, id: 0 },
      { title: 'Departing At...', type: 'departure', isASAP: false, id: 1 },
      { title: 'Arriving By...', type: 'arrival', isASAP: false, id: 2 }
    ];
    ga('set', 'page', '/plan-trip.html');
    ga('send', 'pageview');
  }
  /**
  * Checks whether we're trying to
  * get directions starting at the
  * current location.  If so, get it.
  * Otherwise, clear out the values
  * for origin so the user knows to type something.
 */
 updateOrigin(): void {
   if (this.request.destinationOnly) {
     this.loadLocation();
   } else {
     this.request.origin = {};
   }
 }
 // Loads the user's location and updates the origin
  loadLocation(): void {
    let options = {timeout: 5000, enableHighAccuracy: true};
    this.geolocation.getCurrentPosition(options).then(position => {
      // Geocode current position to retrieve its corresponding Google Maps ID
      new google.maps.Geocoder().geocode(
        {
          'location': new google.maps.LatLng(position.coords.latitude, position.coords.longitude)
        },
        (results, status) => {
          if (status === google.maps.GeocoderStatus.OK) {
            console.log(results);
            if (results[0] && this.bounds.contains(results[0].geometry.location)) {
              let closestGeolocationAddress = results[0];
              this.originPlace = closestGeolocationAddress;
              this.originInput = closestGeolocationAddress.formatted_address;
              this.request.origin = {
                name: closestGeolocationAddress.formatted_address,
                id: closestGeolocationAddress.place_id
              };
              if (this.request.destination.name) {
                this.request.name = this.request.destination.name;
              }
            } else {
              this.presentAlert('Can\'t Use Current Location',
              'Your current location isn\'t in the PVTA\'s service area. Please search for a starting location above.');
            }
          }
        }
      );
      // this.getRoute();
    })
    .catch(err => {
      this.noLocationToast = this.toastSvc.noLocationToast();
      // Tell Google Analytics that a user doesn't have location
      ga('send', 'event', 'LocationFailure',
      'PlanTripComponent.loadLocation()', `location failed on Plan Trip; error: ${err.message}`);
      // When getting location fails, this callback fires
      this.noLocation = true;
      /* When getting location fails immediately, $ionicLoading.hide()
       * is never called (or the page refuses to redraw), so
       * we add a 1 second delay as a workaround.
       *
       * We also set the checkbox state after the delay, but solely
       * for user feedback (it otherwise would never change when clicked on)
       */
      setTimeout(() => {
        this.request.destinationOnly = false;
      }, 1000);
      console.error('unable to get location ' + err.message);
      // this.getRoute();
    });
  }

  /* Called when this page is opened, and either a loaded trip has been queued
   * or there are no current existing parameters. Also called as a result of the
   * newTrip method. Constructs the map, and then sets this.request as either default
   * or loaded parameters */
  reload(loadedTrip): void {
    // @TODO Scroll to input area first
    this.constructMap();
    this.noLocation = false;
    // If we loaded a trip (user came via
    // saved trip on Favorites), pull out
    // its details and display them.
    if (loadedTrip) {
      this.request = loadedTrip;
      loadedTrip = null;
      // If the datetime of loeaded trip is in the past,
      // keep the time and update the date to today. Else do nothing.
      if (this.request.time.datetime < Date.now()) {
        this.request.time.datetime.setDate(new Date().getDate());
        this.request.time.datetime.setMonth(new Date().getMonth());
      }
      // A planned trip from 1.x saves the entire Option object,
      // so if we see this, just change it be the id it already contains
      // so that we can use the timeOptions from above.
      if (typeof(this.request.time.option) !== 'number') {
        this.request.time.option = this.request.time.option.id;
      }
      // If the request has destinationOnly -> true, the user originally used
      // Location Services to plan their trip. We assume they again want to
      // use their current location as the trip's origin.
      // If destinationOnly is false, then we use the origin that
      // was saved with the trip.
      if (this.request.destinationOnly) {
        this.request.origin = {};
        this.loadLocation();
      } else {
        this.getRoute();
      }
      ga('send', 'event', 'TripLoaded', 'PlanTripComponent.reload()',
      'User has navigated to Plan Trip using a saved Trip.');
    } else {
      // There is no loaded trip.  Load the page with default parameters.
      // Attempt to use current location as trip's origin.
      this.request = {
        name: 'Schedule',
        time: {
          datetime: moment().format(),
          option: 0 // The ID of the timeOption the trip will use
        },
        origin: {},
        destination: {},
        destinationOnly: true,
        saved: false
      };
      this.loadLocation();
    }
  }

  mapsLoadedCallback = (loadedTrip) => {
    let swBound = new google.maps.LatLng(41.93335, -72.85809);
    let neBound = new google.maps.LatLng(42.51138, -72.20302);
    this.bounds = new google.maps.LatLngBounds(swBound, neBound);
   console.log(loadedTrip);
   this.reload(loadedTrip);
  }

  ionViewWillEnter() {
    // defaultMapCenter = new google.maps.LatLng(42.3918143, -72.5291417);//Coords for UMass Campus Center
    // These coordinates draw a rectangle around all PVTA-serviced area. Used to restrict requested locations to only PVTALand
    let loadedTrip = this.navParams.get('loadedTrip');
    if (typeof google === 'undefined' || typeof google.maps === 'undefined') {
      this.mapSvc.downloadGoogleMaps(() => this.mapsLoadedCallback(loadedTrip));
    } else {
      this.mapsLoadedCallback(loadedTrip);
    }



  }

  constructMap(): void {
    var mapOptions = {
      zoom: 15,
      mapTypeControl: false,
      mapTypeId: google.maps.MapTypeId.ROADMAP,
      gestureHandling: 'cooperative'
    };
    this.map = new google.maps.Map(this.mapElement.nativeElement, mapOptions);
    this.directionsDisplay = new google.maps.DirectionsRenderer;
    this.directionsDisplay.setMap(this.map);

    let originInput = <HTMLInputElement>document.getElementById('origin-input');
    let destinationInput = <HTMLInputElement>document.getElementById('destination-input');

    let originAutocomplete = new google.maps.places.Autocomplete(originInput);
    let destinationAutocomplete = new google.maps.places.Autocomplete(destinationInput);

    originAutocomplete.setBounds(this.bounds);
    destinationAutocomplete.setBounds(this.bounds);
    // When the user has selected a valid Place from the dropdown
    originAutocomplete.addListener('place_changed', () => {
      let place = originAutocomplete.getPlace();
      if (!place || !place.geometry) {
        this.presentAlert('Invalid Origin',
        'Choose a location from the list of suggestions.');
        ga('send', 'event', 'AutocompleteFailure', 'originAutocomplete.place_changed',
        'autocomplete failure in plan trip: user didnt pick a value from dropdown');
        // If the location chosen is not valid, an error is thrown.
        // request.origin.name still holds the text that the user
        // originally typed into the field. We will set the field's value
        // back to this text.
        this.request.origin.id = null;
        originInput.value = this.request.origin.name;
        console.error('No geometry, invalid input.');
      } else if (!this.bounds.contains(place.geometry.location)) {
        this.presentAlert('Invalid Origin',
        'The PVTA does not service this location.');
        this.request.origin = {};
        console.error(`Location ${place.name} is out of bounds.`);
      } else {
        this.originPlace = place;
        this.request.origin = {
          name: place.name,
          id: place.place_id
        };
        this.request.destinationOnly = false;
      }
    });
    // When a valid destination is chosen:
    destinationAutocomplete.addListener('place_changed', () => {
      let place =  destinationAutocomplete.getPlace();
      if (!place || !place.geometry) {
        this.presentAlert('Invalid Destination',
        'Choose a location from the list of suggestions.');
        console.error('No geometry, invalid input.');
      } else if (!this.bounds.contains(place.geometry.location)) {
        this.request.destination = {};
        this.presentAlert('Invalid Destination',
        'The PVTA does not service this location.');
        console.error(`Location ${place.name} is out of bounds.`);
      } else {
        this.destinationPlace = place;
        this.request.destination = {
          name: place.name,
          id: place.place_id
        };
      }
    }, (err) => {
        ga('send', 'event', 'AutocompleteFailure', 'destinationAutocomplete.place_changed',
        'autocomplete failure in plan trip: user didnt pick a value from dropdown');
        // See comments for originAutocompleteListener method
        this.request.destination.id = null;
        destinationInput.value = this.request.destination.name;
    });
  }

  presentAlert(title: string, body: string): void {
    let alert = this.alertCtrl.create({
      title: title,
      subTitle: body,
      buttons: ['Dismiss']
    });
    alert.present();
  }

  /* Requests a trip from Google using the trip params.
   * This function is the crown jewel of this component.
   */
   getRoute(): void {
     if (this.originDestToast) {
       this.toastSvc.noOriginOrDestinationToast();
     }
    // We need an origin and destination
    if (!this.request.origin.id || !this.request.destination.id) {
      // Clear out the search boxes for either/both of the incorrectly
      // selected fields
      if (!this.request.origin.id) {
        this.request.origin.name = '';
      }
      if (!this.request.destination.id) {
        this.request.destination.name = '';
      }
      this.originDestToast = this.toastSvc.noOriginOrDestinationToast();
      console.error('Missing an origin or destination id');
      return;
    }
    // Google won't return trips for times past.
    // Instead of throwing an error, assume the user wants
    // directions for right now
    if (!this.timeOptions[this.request.time.option].isASAP && moment(this.request.time.datetime).isBefore(moment())) {
      this.request.time.option = this.timeOptions[0].id;
      this.presentAlert('Invalid Trip Date',
      'Trips in the past are not supported. Defaulting to buses leaving now.');
      console.error('Trips in the past are not supported. Defaulting to buses leaving now.');
    }
    this.loader = this.loadingCtrl.create({
      enableBackdropDismiss: true
    });
    this.loader.present();
    let transitOptions = {
      modes: [google.maps.TransitMode.BUS],
      routingPreference: google.maps.TransitRoutePreference.FEWER_TRANSFERS
    };
    if (this.timeOptions[this.request.time.option].isASAP !== true) {
      if (this.timeOptions[this.request.time.option].type === 'departure') {
        // User wants departure in the future? Pass the time.
        transitOptions['departureTime'] = new Date(this.request.time.datetime);
      } else if (this.timeOptions[this.request.time.option].type === 'arrival') {
        // User wants arrival in the future? Pass the time.
        transitOptions['arrivalTime'] = new Date(this.request.time.datetime);
      } else {
        this.presentAlert('Error', 'Received invalid time');
        ga('send', 'event', 'RoutingParamsInvalid', 'PlanTripComponent.getRoute()',
        'Received invalid time params for planning a route');
        this.loader.dismiss();
        return;
      }
    }
    let directionsService = new google.maps.DirectionsService;
    /*
     * Send the official request to Google!
     */
    directionsService.route(
      {
        origin: {'placeId': this.request.origin.id},
        destination: {'placeId': this.request.destination.id},
        travelMode: google.maps.TravelMode.TRANSIT,
        transitOptions: transitOptions
      }, (response, status) => {
      if (status === google.maps.DirectionsStatus.OK ) {
        if (this.noLocationToast) {
          this.toastSvc.noLocationToast();
        }
        // Force a map redraw because it was hidden before.
        // There's an angular bug (with [hidden]) that will cause
        // the map to draw only grey after being hidden
        // unless we force a redraw TWICE after delays -_-
        setTimeout(() => {
          this.loader.dismiss();
          google.maps.event.trigger(this.map, 'resize');
          this.directionsDisplay.setDirections(response);
          this.route = response.routes[0].legs[0];
        }, 500);
        setTimeout(() => {
          google.maps.event.trigger(this.map, 'resize');
          this.directionsDisplay.setDirections(response);
          this.routeElement.nativeElement.scrollIntoView();
        }, 1000);
        ga('send', 'event', 'TripStepsRetrieved', 'PlanTripComponent.getRoute()',
        'Received steps for a planned trip!');
      } else  {
        console.log(status);
        this.presentAlert('Unable to Find Trip', `There are no scheduled buses for your trip. Error: ${status}`);
        this.loader.dismiss();
        ga('send', 'event', 'TripStepsRetrievalFailure',
        'PlanTripComponent.getRoute()', `Unable to get a route; error: ${status}`);

        // In cases of error, we set the route object that
        // otherwise contained all our data to undefined, because, well,
        // the data was bad.
        this.route = null;
      }
    });
  }

  /*
   * Saves the current trip parameters to the db
   * for display on Favorites
  */
  saveTrip(): void {
    console.log('saving trip yo');
     this.alertCtrl.create({
       title: 'Save Trip',
       message: 'Give this trip a name',
       inputs: [
         {
           name: 'name',
           placeholder: 'example: To the mall!'
         },
       ],
       buttons: [
         {
           text: 'Cancel',
           handler: data => {
             console.log('Cancel clicked');
           }
         },
         {
           text: 'Save',
           handler: data => {
             console.log('data', data);
             this.request.name = data.name;
             console.log('Saved clicked');
             this.tripService.saveTrip(this.request);
             ga('send', 'event', 'TripSaveSuccessful', 'PlanTripComponent.saveTrip()',
             'Saved a trip to favorites!');
           }
         }
       ]
     }).present();
  }

  /* Allows for location selection on google
  * typeahead on mobile devices
  */
  disableTap(): void {
    console.log('disable tap');
    // @TODO Figure out if this needs to be a thing
    //  let container = document.getElementsByClassName('pac-container');
    // disable ionic data tap
    //  element(container).attr('data-tap-disabled', 'true');
    //  angular.element(container).attr('id', 'places');
    //  leave input field if google-address-entry is selected
    //  angular.element(container).on('click', function () {
    //    document.getElementById('origin-input').blur();
    //    document.getElementById('destination-input').blur();
    //  });
  }

  goToStop(loc): void {
    // @TODO Show loader
    this.stopService.getNearestStop(loc.lat(), loc.lng()).then(stop => {
      // @TODO Hide loader
      this.navCtrl.push(StopComponent, {stopId: stop.StopId});
    });
  }
}