5

I am trying to build an application using Ionic2 that allows a user to load a StreetViewPanorama object via the Google Street View Static API. Once the view is loaded, a user should be able to manipulate the street view in any way they choose (move away from the original position, zoom, etc.). Upon completion of this task, a user will capture a static image of the final street view.

My difficulty is arising when I attempt to capture the photo of the new street view location. I am trying to use Google's documentation on static image generation to achieve this. Unfortunately, I am unable to get reference the attributes of a Panorama object after the object is created. I am relatively new to Javascript, so bear with me.

To generate the street view panorama, I run the following functions (starting at the bottom with initMap()):

 /**
  * Creates the map options for panorama generation. This includes adjusting the coordinate
  * position of a user to the nearest available street view. Following creation of the settings,
  * it generates the street view on a user's device.
  *
  * @param userLocation a JSON object whose keys are 'lat' and 'lng' and whose values are
  *                     the corresponding latitude and longitude respectively
  */
  generatePanorama(userLocation): void {
    var streetviewService = new google.maps.StreetViewService;
    streetviewService.getPanorama({
      location: userLocation,
      preference: google.maps.StreetViewPreference.NEAREST,
      radius: 100},
      function(result, status) {
        console.log("Adjusted latitude: ", result.location.latLng.lat(),
                    "\nAdjusted longitude: ", result.location.latLng.lng());
        new google.maps.StreetViewPanorama(document.getElementById('street-view'), {
          position: result.location.latLng,
          pov: {heading: 165, pitch: 0},
          zoom: 1
        });
      });
  }


  /**
  * Uses a device's native geolocation capabilities to get the user's current position
  *
  * @return a JSON object whose keys are 'lat' and 'lng' and whose calues are the corresponding
  *         latitude and longitude respectively
  */
  getLocation(callback): void {
    Geolocation.getCurrentPosition().then((position) => {
      console.log("Latitude: ", position.coords.latitude, "\nLongitude: ", position.coords.longitude);
      callback({lat: position.coords.latitude, lng: position.coords.longitude});
    }).catch((error) => {
      console.log('Error getting location', error);
    });
  }

 /**
  * Initialize a Google Street View Panorama image
  */
  initMap(): void {
    this.getLocation(this.generatePanorama);
  }

I am creating a panorama, as shown above, with the code,

new google.maps.StreetViewPanorama(document.getElementById('street-view'), {
      position: result.location.latLng,
      pov: {heading: 165, pitch: 0},
      zoom: 1
    });

I am unable to assign this object to an instance variable for use in the following two functions:

 /**
  * Generates a URL to query the Google Maps API for a static image of a location
  *
  * @param lat the latitude of the static image to query
  * @param lng the longitude of the static image to query
  * @param heading indicates the compass heading of the camera
  * @param pitch specifies the up or down angle of the camera relative to the street
  * @return a string that is the URL of a statically generated image of a location
  */
  generateStaticMapsURL(lat, lng, heading, pitch): string {
    var url = "https://maps.googleapis.com/maps/api/streetview?size=600x300&location=";
    url += lat + "," + lng;
    url += "&heading=" + heading;
    url += "&pitch=" + pitch;
    url += "&key=SECRET_KEY";
    return url;
  }

  openShareModal() {
    console.log("Final Latitude: ", this.panorama.getPosition().lat());
    console.log("Final Longitude: ", this.panorama.getPosition().lng());
    console.log("Final Heading:", this.panorama.getPov().heading);
    console.log("Final Heading:", this.panorama.getPov().pitch);
    let myModal = this.modalCtrl.create(ShareModalPage);
    myModal.present();
  }

When I try to assign the object to an instance variable either directly or through a helper function I get an UnhandledPromiseRejectionWarning and nothing works. So how exactly can I extract things like location, heading, and pitch from the street view object after it is created?

Thank you for your help!

Update 1: The program is currently building fine. I assign an instance variable of panorama: any; and then proceed to try and update the variable using the following function and assignment.

/**
  * Creates the map options for panorama generation. This includes adjusting the coordinate
  * position of a user to the nearest available street view. Following creation of the settings,
  * it generates the street view on a user's device.
  *
  * @param userLocation a JSON object whose keys are 'lat' and 'lng' and whose values are
  *                     the corresponding latitude and longitude respectively
  */
  generatePanorama(userLocation): void {
    var streetviewService = new google.maps.StreetViewService;
    streetviewService.getPanorama({
      location: userLocation,
      preference: google.maps.StreetViewPreference.NEAREST,
      radius: 100},
      function(result, status) {
        console.log("Adjusted latitude: ", result.location.latLng.lat(),
                    "\nAdjusted longitude: ", result.location.latLng.lng());
        this.panorama = new google.maps.StreetViewPanorama(document.getElementById('street-view'), {
          position: result.location.latLng,
          pov: {heading: 165, pitch: 0},
          zoom: 1
        });
      });
  }

When I do this and then subsequently try to use the panorama variable in another function, it seems to think panorama is an empty variable. Additionally, the panorama map doesn't load at all! Here is the second function I try to use the panorama variable in.

openShareModal() {
    console.log("Final Latitude: ", this.panorama.getPosition().lat());
    console.log("Final Longitude: ", this.panorama.getPosition().lng());
    console.log("Final Heading:", this.panorama.getPov().heading);
    console.log("Final Heading:", this.panorama.getPov().pitch);
    let myModal = this.modalCtrl.create(ShareModalPage);
    myModal.present();
  }

UPDATE 2: Posting the entire chunk of my code for assistance.

import { Component, ViewChild } from '@angular/core';
import { IonicPage, NavController, NavParams, ViewController, ModalController } from 'ionic-angular';
import { ShareModalPage } from '../share-modal/share-modal';
import { Geolocation } from 'ionic-native';
declare var google;

/**
 * Generated class for the StreetViewModalPage page.
 *
 * See https://ionicframework.com/docs/components/#navigation for more info on
 * Ionic pages and navigation.
 */

@IonicPage()
@Component({
  selector: 'page-street-view-modal',
  templateUrl: 'street-view-modal.html',
})

export class StreetViewModalPage {

  @ViewChild('map') mapElement;
  map: any;
  panorama: any;

  constructor(public navCtrl: NavController, public navParams: NavParams,
              public viewCtrl: ViewController, public modalCtrl: ModalController) {}

  ionViewDidLoad() {
    console.log('ionViewDidLoad StreetViewModalPage');
    this.initMap();
  }
  

  /**
  * Creates the map options for panorama generation. This includes adjusting the coordinate
  * position of a user to the nearest available street view. Following creation of the settings,
  * it generates the street view on a user's device.
  *
  * @param userLocation a JSON object whose keys are 'lat' and 'lng' and whose values are
  *                     the corresponding latitude and longitude respectively
  */
  generatePanorama(userLocation): void {
    var streetviewService = new google.maps.StreetViewService;
    streetviewService.getPanorama({
      location: userLocation,
      preference: google.maps.StreetViewPreference.NEAREST,
      radius: 100},
      function(result, status) {
        console.log("Adjusted latitude: ", result.location.latLng.lat(),
                    "\nAdjusted longitude: ", result.location.latLng.lng());
        this.panorama = new google.maps.StreetViewPanorama(document.getElementById('street-view'), {
          position: result.location.latLng,
          pov: {heading: 165, pitch: 0},
          zoom: 1
        });
      });
  }


  /**
  * Uses a device's native geolocation capabilities to get the user's current position
  *
  * @return a JSON object whose keys are 'lat' and 'lng' and whose calues are the corresponding
  *         latitude and longitude respectively
  */
  getLocation(callback): void {
    Geolocation.getCurrentPosition().then((position) => {
      console.log("Latitude: ", position.coords.latitude, "\nLongitude: ", position.coords.longitude);
      callback({lat: position.coords.latitude, lng: position.coords.longitude});
    }).catch((error) => {
      console.log('Error getting location', error);
    });
  }

  /**
  * Initialize a Google Street View Panorama image
  */
  initMap(): void {
    this.getLocation(this.generatePanorama);
  }

  /**
  * Generates a URL to query the Google Maps API for a static image of a location
  *
  * @param lat the latitude of the static image to query
  * @param lng the longitude of the static image to query
  * @param heading indicates the compass heading of the camera
  * @param pitch specifies the up or down angle of the camera relative to the street
  * @return a string that is the URL of a statically generated image of a location
  */
  generateStaticMapsURL(lat, lng, heading, pitch): string {
    var url = "https://maps.googleapis.com/maps/api/streetview?size=600x300&location=";
    url += lat + "," + lng;
    url += "&heading=" + heading;
    url += "&pitch=" + pitch;
    url += "&key=XXXXXXXXXXXX"; // TODO : Make private
    return url;
  }

  openShareModal() {
    console.log("Final Latitude: ", this.panorama.getPosition().lat());
    console.log("Final Longitude: ", this.panorama.getPosition().lng());
    console.log("Final Heading:", this.panorama.getPov().heading);
    console.log("Final Heading:", this.panorama.getPov().pitch);
    let myModal = this.modalCtrl.create(ShareModalPage);
    myModal.present();
  }

}

And the corresponding HTML...

<ion-content>
   <div #map id="street-view" style="height:100%; width:100%;"></div>
   <button ion-button style="position: absolute; top: 5px; right: 5px; z-index: 1;" (click)="openShareModal()" large><ion-icon name="camera"></ion-icon></button>
</ion-content>
peachykeen
  • 4,143
  • 4
  • 30
  • 49
  • I don't know much about the google api, but `UnhandledPromiseRejectionWarning` tells us there is a promise there that threw an error that was not handled. We can go further with the help if you tell us exactly what line throws the error and what the whole error is – Adelin Jan 10 '18 at 10:39
  • So actually the code as is (the code I entered above) doesn't throw any error. My issue is that after I make a new StreetViewPanorama object (`new google.maps.StreetViewPanorama()`) I am unable to access it's fields elsewhere. When I attempt to assign it to a variable and pass the variable into a callback which will then assign it to an instance variable, things start breaking. – peachykeen Jan 10 '18 at 13:00
  • I don't see any relation between the panorama and Promises in [the docs](https://developers.google.com/maps/documentation/javascript/streetview#StreetViewPanoramas). Please be more specific. Assign the panorama to a variable, pass it into the callback that does the assignment, show **that** code to us and tell us what do you mean by *"start breaking"* – Adelin Jan 10 '18 at 13:25
  • @Adelin I updated my post. Please see the edits for further clarification on my problem and where I am looking for help. Thanks! – peachykeen Jan 12 '18 at 09:31
  • To complete this question do you want a fix solely solely for the UnhandledPromiseRejectionWarning or do you want the streetview image picture too? – Jonathan Chaplin Jan 12 '18 at 15:29
  • @JonathanChaplin I am looking to get the street view image picture. The UnhandledPromiseRejectionWarning isn’t happening anymore with the changes I made above. – peachykeen Jan 12 '18 at 15:37
  • Before I start on this, you've listed this under android as well as javascript and google maps. I'm guessing the solution you want should be in Google Maps JavaScript API (based on your code) and not in the Google Maps Android API correct? – Jonathan Chaplin Jan 12 '18 at 15:56
  • @JonathanChaplin that is correct. I am looking for a solution based in the Google Maps JavaScript API. Thank you for your help! – peachykeen Jan 15 '18 at 09:37
  • I'm able to get a local copy of this working (and it's similar to your code). I think it might have something to do with this line: this.panorama = new google.maps.StreetViewPanorama How are you invoking generatePanorama(userLocation)? "this" in JS changes based on how you invoke the function. Can you either post your entire code (either to this post or github) ? – Jonathan Chaplin Jan 15 '18 at 19:06
  • @JonathanChaplin I added my full code to the bottom of the post in "update 2" (sorry, my GitHub repo is private). – peachykeen Jan 15 '18 at 20:20

1 Answers1

2

I think the reason why this.panorama is empty is because of the scope where you are creating it.

I'm on the phone so I can't really type code, but try on the generatePanorama to add const self = this; just at the beginning of the method and when you assign this.panorama please replace with self.panorama = ....

Please let me know if that worked. I'll try it out soon to see if that's all you need.

Here's what I'm talking about

/**
  * Creates the map options for panorama generation. This includes adjusting the coordinate
  * position of a user to the nearest available street view. Following creation of the settings,
  * it generates the street view on a user's device.
  *
  * @param userLocation a JSON object whose keys are 'lat' and 'lng' and whose values are
  *                     the corresponding latitude and longitude respectively
  */
  generatePanorama(userLocation): void {
    var self = this;
    var streetviewService = new google.maps.StreetViewService;
    streetviewService.getPanorama({
      location: userLocation,
      preference: google.maps.StreetViewPreference.NEAREST,
      radius: 100},
      function(result, status) {
        console.log("Adjusted latitude: ", result.location.latLng.lat(),
                    "\nAdjusted longitude: ", result.location.latLng.lng());
        self.panorama = new google.maps.StreetViewPanorama(document.getElementById('street-view'), {
          position: result.location.latLng,
          pov: {heading: 165, pitch: 0},
          zoom: 1
        });
      });
  }

UPDATE - Working Example

class StackTest {
 constructor() {
    this.initMap();
    // This next line is very important to keep the scope.
    this.openShareModal = this.openShareModal.bind(this);
    // This next line should be defined by ionic itself so it won't be needed
    document.getElementById('open').addEventListener('click', this.openShareModal);
  }
  
  generatePanorama(userLocation) {
    var self = this;
    var streetviewService = new google.maps.StreetViewService;
    streetviewService.getPanorama({
      location: userLocation,
      preference: google.maps.StreetViewPreference.NEAREST,
      radius: 100},
      function(result, status) {
        console.log("Adjusted latitude: ", result.location.latLng.lat(),
                    "\nAdjusted longitude: ", result.location.latLng.lng());
        self.panorama = new google.maps.StreetViewPanorama(document.getElementById('pano'), {
          position: result.location.latLng,
          pov: {heading: 165, pitch: 0},
          zoom: 1
        });
        self.bindEvents();
      });
  }
  
  bindEvents() {
   var self = this;
    this.panorama.addListener('pano_changed', function() {
        var panoCell = document.getElementById('pano-cell');
        panoCell.innerHTML = self.panorama.getPano();
    });

    this.panorama.addListener('links_changed', function() {
        var linksTable = document.getElementById('links_table');
        while (linksTable.hasChildNodes()) {
          linksTable.removeChild(linksTable.lastChild);
        }
        var links = self.panorama.getLinks();
        for (var i in links) {
          var row = document.createElement('tr');
          linksTable.appendChild(row);
          var labelCell = document.createElement('td');
          labelCell.innerHTML = '<b>Link: ' + i + '</b>';
          var valueCell = document.createElement('td');
          valueCell.innerHTML = links[i].description;
          linksTable.appendChild(labelCell);
          linksTable.appendChild(valueCell);
        }
    });

    this.panorama.addListener('position_changed', function() {
        var positionCell = document.getElementById('position-cell');
        positionCell.firstChild.nodeValue = self.panorama.getPosition() + '';
    });

    this.panorama.addListener('pov_changed', function() {
        var headingCell = document.getElementById('heading-cell');
        var pitchCell = document.getElementById('pitch-cell');
        headingCell.firstChild.nodeValue = self.panorama.getPov().heading + '';
        pitchCell.firstChild.nodeValue = self.panorama.getPov().pitch + '';
    });
  }

  getLocation(callback) {
   callback({lat: 37.869, lng: -122.255});
  }

  initMap() {
    this.getLocation(this.generatePanorama.bind(this));
  }

  openShareModal() {
    console.log("Final Latitude: ", this.panorama.getPosition().lat());
    console.log("Final Longitude: ", this.panorama.getPosition().lng());
    console.log("Final Heading:", this.panorama.getPov().heading);
    console.log("Final Heading:", this.panorama.getPov().pitch);
    alert('If you see this alert this.panorama was defined :)');
    /* let myModal = this.modalCtrl.create(ShareModalPage); */
    /* myModal.present() */;
  }
}

function instantiateTheClass() {
 new StackTest();
}
/* Always set the map height explicitly to define the size of the div
 * element that contains the map. */
#map {
  height: 100%;
}
/* Optional: Makes the sample page fill the window. */
html, body {
  height: 100%;
  margin: 0;
  padding: 0;
}
#open {
  position: absolute;
  top: 0;
  right: 0;
}
#floating-panel {
  position: absolute;
  top: 10px;
  left: 25%;
  z-index: 5;
  background-color: #fff;
  padding: 5px;
  border: 1px solid #999;
  text-align: center;
  font-family: 'Roboto','sans-serif';
  line-height: 30px;
  padding-left: 10px;
}
#pano {
  width: 50%;
  height: 100%;
  float: left;
}
#floating-panel {
  width: 45%;
  height: 100%;
  float: right;
  text-align: left;
  overflow: auto;
  position: static;
  border: 0px solid #999;
}
<div id="pano"></div>
<button id="open">Open modal</button>
<div id="floating-panel">
<table>
  <tr>
    <td><b>Position</b></td><td id="position-cell">&nbsp;</td>
  </tr>
  <tr>
    <td><b>POV Heading</b></td><td id="heading-cell">270</td>
  </tr>
  <tr>
    <td><b>POV Pitch</b></td><td id="pitch-cell">0.0</td>
  </tr>
  <tr>
    <td><b>Pano ID</b></td><td id="pano-cell">&nbsp;</td>
  </tr>
  <table id="links_table"></table>
</table>
</div>
<!-- Replace the value of the key parameter with your own API key. -->
<script async defer
src="https://maps.googleapis.com/maps/api/js?key=AIzaSyAH1G2yf7g4br8sQehvB7C1IfBh8EIaAVE&callback=instantiateTheClass">
</script>
Joao Lopes
  • 936
  • 5
  • 9
  • Thank you!!! This seems to work. I am still getting some issues. It says that `undefined is not an object (evaluating 'this.panorama.getPosition')`. Any idea what might be causing this? It happens when I try to print the updated position of the panorama object using `console.log("Final Latitude: ", this.panorama.getPosition().lat());` – peachykeen Jan 16 '18 at 20:40
  • Is the state of my `panorama` object reset when I call `(click)="openShareModal()` from the HTML code? – peachykeen Jan 16 '18 at 20:47
  • I can't test it myself so I'll guide you through what I would do. I would `console.log('The panorama was set');` when you do `self.panorama = ...` Then I would check if you get the error before or after the panorama was set. Second off, I would store the panorama on the global scope, so `window.panorama = self.panorama = ...` and log `window.panorama` on the `openShareModal()` instead of `this.panorama` and see if that would make a difference. If it does, then it's a scoping or instancing problem. – Joao Lopes Jan 16 '18 at 20:57
  • I changed it to `window.panorama = self.panorama = new google.maps.StreetView...` and the same thing happens. I am able to generate the panorama just fine, but nothing after the call to create the `StreeViewPanorama` object gets executed. Then when I change `this.panorama` to `window.panorama` in `openShareModal()` I get the same error but with the undefined object being `window.panorama.getPosition`. – peachykeen Jan 16 '18 at 21:07
  • I think I got it. On the `constructor` please add `this.openShareModal = this.openShareModal.bind(this);` I've built an example https://jsfiddle.net/m83wwevt/ Make sure you replace with your key on the html. I'm not familiar with the lifecycle of ionic, but basically what seems to be happening is that the context is not bound to the callback of the button. – Joao Lopes Jan 16 '18 at 21:31
  • So it looks like none of the bind events or anything are getting called after the panorama object is generated? I.e. I don't know if `self.bindEvents();` is ever getting called. It seems that the phone is like held in a panorama state while it is active. I am getting a split screen like you have on jsfiddle, but where you have text I have a blank screen and where you have an error message I have the panorama view. – peachykeen Jan 16 '18 at 22:03
  • I'm not getting an error message. Did you change the `key` attribute on the script html? The `self.bindEvents` is definitely getting called if everything goes right. I didn't specify but you need to change the key and click run on the top left. – Joao Lopes Jan 16 '18 at 22:05
  • I don't even get to see the button in the top left. When I try to print to the console after the panorama object is created, nothing is printed. There is also a problem now that when I try to build for ios using `ionic cordova` I get an error that `** EXPORT FAILED ** (node:31400) UnhandledPromiseRejectionWarning: Error code 70 for command: xcodebuild with args: -exportArchive,-archivePath,MyApp.xcarchive,-exportOptionsPlist....` – peachykeen Jan 16 '18 at 22:35
  • Hmm it seemed to work when I ran it on my laptop's web browser rather than my phone. However, your numbers still do not appear to the side. – peachykeen Jan 16 '18 at 22:41
  • try this one https://jsfiddle.net/m83wwevt/1/ I'll invalidate the key later. Haven't tested it on the phone. but it should work – Joao Lopes Jan 16 '18 at 22:44
  • Yay!!! It is working!!! :) But my question is, will I need to keep all those listeners in order for it to work successfully? I tried removing some of the binding code and it started to break. Thank you so so much for all the time you have put into helping me figure this problem out :) – peachykeen Jan 16 '18 at 22:55
  • No worries. Glad we're figuring it out along the way. Here's an example with only the position listener https://jsfiddle.net/m83wwevt/2/ and here's an example without any events https://jsfiddle.net/m83wwevt/4/ – Joao Lopes Jan 16 '18 at 23:02
  • BTW, I didn't address the error with `ionic cordova` because I've never tried ionic myself. I would suggest that you open a new stack overflow question about that. – Joao Lopes Jan 16 '18 at 23:05
  • Thank you! You are such a savior!! :) I am indebted to you :) So if I wanted to also get heading changes I would need to add another listener to heading? – peachykeen Jan 16 '18 at 23:06
  • Glad I could help. :) – Joao Lopes Jan 16 '18 at 23:08
  • Just stumbled upon this https://stackoverflow.com/questions/48145380/getting-error-on-ionic-cordova-build-android Pretty sure it's the same error that you're getting. So hopefully you can use that board to solve your problem. – Joao Lopes Jan 16 '18 at 23:21
  • "So if I wanted to also get heading changes I would need to add another listener to heading?" Missed that sorry. Hope you got it. but yes, you would need to add another listener, not sure what you mean about the heading though. – Joao Lopes Jan 17 '18 at 15:47
  • It would be nice if you update you answer for other people what eventuelly came along this question. – Jozef Dochan Jan 18 '18 at 10:13
  • Will surely do that. – Joao Lopes Jan 18 '18 at 11:24