Final project: building a live itinerary

Although the natural next step from our previous sample would be just to add an extra feature to our already growing social map (which we have built throughout this chapter), we are taking a direction shift.

In our final recipe, we will build an interactive Google map that will animate with the travel information of a close friend of mine in South America while I was working on this book. To build this application, we will animate the map by adding drawings and moving markers; we will integrate with an external feed of travel information and integrate animations and text snippets that will describe the journey. In the following screenshot, you can see a very small snapshot of the plain path:

Final project: building a live itinerary

Getting ready

Many of the elements we will be working with in this recipe will be based on work we did throughout all of the chapters. As such, it will not be easy to just jump right in if you haven't gone through the journey together with us. There are no prerequisites. We will start from scratch, but we will not focus on things we have learned already.

As the user "travels" around the world map when there is a message for the user in the data source, the map will fade out and the message will be displayed before the user can continue traveling the world:

Getting ready

How to do it...

In this recipe we will be creating two files: an HTML file and a JavaScript file. Let's look into them, starting with the HTML file:

  1. Create the HTML file.
    <!DOCTYPE html>
    <html>
      <head>
        <title>Google Maps Markers and Events</title>
        <meta charset="utf-8" />
        <meta name="viewport" content="initial-scale=1.0, user-scalable=no" />
        <link href='http://fonts.googleapis.com/css?family=Yellowtail' rel='stylesheet' type='text/css'>
        <style>
          html { height: 100% }
          body { height: 100%; margin: 0; padding: 0 }
          #map { height: 100%; width:100%; position:absolute; top:0px; left:0px }
          
          .overlay {
            background: #000000 scroll;
            height: 100%;
            left: 0;
            opacity: 0;
            position: absolute;
            top: 0;
            width: 100%;
            z-index: 50;
        }
        .overlayBox {
            left: -9999em;
            opacity: 0;
            position: absolute;
            z-index: 51;
            text-align:center;
            font-size:32px;
            color:#ffffff;
            font-family: 'Yellowtail', cursive;
        }
        </style>
      <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js"></script>
        <script src="http://maps.googleapis.com/maps/api/js?key=AIzaSyBp8gVrtxUC2Ynjwqox7I0dxrqjtCYim-8&sensor=false"></script>
        <script src="https://www.google.com/jsapi"></script>
        
        <script src="./10.05.travel.js"></script>
      </head>
      <body>
        <div id="map"></div>
      </body>
    </html>
  2. Time to move to the JavaScript file, 10.05.travel.js. We will start by initiating the visualization library and storing a global map variable.
     google.load('visualization', '1.0'),
    google.setOnLoadCallback(init);
  3. var map;
When the init function is triggered, the map is loaded and it triggers the loading of a Google spreadsheet in which we will store all of my friend's travel information.
    function init() {
      var BASE_CENTER = new google.maps.LatLng(48.516817734860105,13.005318750000015 );
    
      map = new google.maps.Map(document.getElementById("map"),{
        center: BASE_CENTER,
        mapTypeId: google.maps.MapTypeId.SATELLITE,
        disableDefaultUI: true,
    
      });
      var query = new google.visualization.Query(
          'https://spreadsheets.google.com/tq?key=0Aldzs55s0XbDdERJVlYyWFJISFN3cjlqU1JnTGpOdHc'),
        query.send(onTripDataReady);
    
    }
  4. When the document is loaded, it will trigger the onTripDataReady listener. When that happens, we will want to create a new GoogleMapTraveler object (a custom class for managing our experience).
    function onTripDataReady(response){
      var gmt = new GoogleMapTraveler(response.g.D,map);	
    }
  5. The constructor method of the GoogleMapTraveler object will prepare our variables, create a new Animator object, a Traveler object and a new google.maps.Polyline object, and will trigger the creation of the first travel point by calling the nextPathPoint method.
    function GoogleMapTraveler(aData,map){
      this.latLong; //will be used to store current location
      this.zoomLevel; //to store current zoom level
      this.currentIndex=0;
      this.data = aData; //locations
      this.map = map;
    
      //this.setPosition(0,2);
      this.animator = new Animator(30);
    
      this.pathPoints = [this.getPosition(0,1)]; //start with two points at same place.
    
      var lineSymbol = {
            path: 'M 0,-1 0,1',
            strokeOpacity: .6,
            scale: 2
          };
    
        this.lines = new google.maps.Polyline({
            path: this.pathPoints,
            strokeOpacity: 0,
            strokeColor: "#FF0000",
            icons: [{
              icon: lineSymbol,
              offset: '0',
              repeat: '20px'
            }],
            map: map
          });
    
      this.traveler = new Traveler(this.map,this.getPosition(0,1));
      this.nextPathPoint(1);
    
    }
  6. The getPosition method is a very smart, small method that enables us to create a new google.maps.LatLng object each time it's called and to create a point based on an average of points or based on one item.
      GoogleMapTraveler.prototype.getPosition = function (index,amount){
      var lat=0;
      var lng=0;
      for(var i=0; i<amount; i++){
        lat+= parseFloat(this.data[index+i].c[0].v);
        lng+= parseFloat(this.data[index+i].c[1].v);
    
      }
      var ll=new google.maps.LatLng(
                lat/amount,
                lng/amount);
      return ll;
    }
  7. We want to be able to set the position of our traveler, and as such we will want to create a setPosition method as well.
    GoogleMapTraveler.prototype.setPosition = function(index,amount){
      this.currentFocus = index;
    
      var lat=0;
      var lng=0;
      for(var i=0; i<amount; i++){
        lat+= parseFloat(this.data[index+i].c[0].v);
        lng+= parseFloat(this.data[index+i].c[1].v);
    
      }
      var ll=new google.maps.LatLng(
                lat/amount,
                lng/amount);
    
      if(this.data[index].c[2])this.map.setZoom(this.data[index].c[2].v);
      this.map.setCenter(ll);
    
    }
  8. In the heart of our application is the ability to automatically move from one step to the next. This logic is applied using our Animator object in combination with the nextPathPoint method:
    GoogleMapTraveler.prototype.nextPathPoint = function(index){
      this.setPosition(index-1,2);
      this.pathPoints.push(this.getPosition(index-1,1)); //add last point again
      var currentPoint = this.pathPoints[this.pathPoints.length-1];
      var point = this.getPosition(index,1);
    
      //console.log(index,currentPoint,point,this.getPosition(index,1));
      this.animator.add(currentPoint,"Za",currentPoint.Za,point.Za,1);
      this.animator.add(currentPoint,"Ya",currentPoint.Ya,point.Ya,1);
      this.animator.add(this.traveler.ll,"Za",this.traveler.ll.Za,point.Za,2,0.75);
      this.animator.add(this.traveler.ll,"Ya",this.traveler.ll.Ya,point.Ya,2,0.75);
    
      this.animator.onUpdate = this.bind(this,this.renderLine);
      this.animator.onComplete = this.bind(this,this.showOverlayCopy);//show copy after getting to destination
    }
  9. There are two callbacks that are triggered through our Animator object (they're highlighted in the preceding code snippet). It is time to create the logic that updates the information on our onUpdate callback. Let's take a peek at the renderLine method.
    GoogleMapTraveler.prototype.renderLine = function(){
      this.lines.setPath(this.pathPoints);
      if(this.traveler.isReady)this.traveler.refreshPosition();
    }
  10. In the next step, when the animation is complete, it triggers our overlay logic. The overlay logic is very simple; if there is text in the Google document, in the fifth column, we will darken the screen and type it out. If there is no text, we will skip this step and go right to the next step that is in the hideOverlayCopy method that triggers the next travel point (the next line in the spreadsheet).
  11. Our previous method of the GoogleMapTraveler object is the bind method. We already created this bind method in the Moving to an OOP perspective recipe in Chapter 6, Bringing Static Things to Life; as such, we will not elaborate on it further.
    GoogleMapTraveler.prototype.bind = function(scope, fun){
       return function () {
            fun.apply(scope, arguments);
        };
    }
  12. Create the Traveler class. The Traveler class will be based on the work we did in the Customizing the look and feel of markers recipe in this chapter, only this time it will be an animating marker.
    function Traveler(map,ll) {
      this.ll = ll;
        this.radius = 15;
        this.innerRadius = 10;
        this.glowDirection = -1;
        this.setMap(map);
        this.isReady = false;
        
      }
    
      Traveler.prototype = new google.maps.OverlayView();
    
      Traveler.prototype.onAdd = function() {
      this.div = document.createElement("DIV");
      this.canvasBG = document.createElement("CANVAS");
        this.canvasBG.width = this.radius*2;
      this.canvasBG.height = this.radius*2;
      this.canvasFG = document.createElement("CANVAS");
        this.canvasFG.width = this.radius*2;
      this.canvasFG.height = this.radius*2;
    
      this.div.style.border = "none";
      this.div.style.borderWidth = "0px";
      this.div.style.position = "absolute";
    
      this.canvasBG.style.position = "absolute";
      this.canvasFG.style.position = "absolute";
    
    
      this.div.appendChild(this.canvasBG);
      this.div.appendChild(this.canvasFG);
    
    
        this.contextBG = this.canvasBG.getContext("2d");
        this.contextFG = this.canvasFG.getContext("2d");
    
      var panes = this.getPanes();
        panes.overlayLayer.appendChild(this.div);
    
      }
    
      Traveler.prototype.draw = function() {
        var radius = this.radius;
        var context = this.contextBG;
    
      context.fillStyle = "rgba(73,154,219,.4)";
      context.beginPath();
        context.arc(radius,radius, radius, 0, Math.PI*2, true);
      context.closePath();
      context.fill();
    
      context = this.contextFG;
      context.fillStyle = "rgb(73,154,219)";
      context.beginPath();
        context.arc(radius,radius, this.innerRadius, 0, Math.PI*2, true);
      context.closePath();
      context.fill();
    
        var projection = this.getProjection();
    
        this.updatePosition(this.ll);
        this.canvasBG.style.opacity = 1;
        this.glowUpdate(this);
        setInterval(this.glowUpdate,100,this);
        this.isReady = true;
        
      };
      
      Traveler.prototype.refreshPosition=function(){
        this.updatePosition(this.ll);	
      }
      
      Traveler.prototype.updatePosition=function(latlng){
        var radius = this.radius;
        var projection = this.getProjection();
      var point = projection.fromLatLngToDivPixel(latlng);
        this.div.style.left = (point.x - radius) + 'px';
        this.div.style.top = (point.y - radius) + 'px';	
      }
      
      Traveler.prototype.glowUpdate=function(scope){ //endless loop
        scope.canvasBG.style.opacity = parseFloat(scope.canvasBG.style.opacity) + scope.glowDirection*.04;
        if(scope.glowDirection==1 && scope.canvasBG.style.opacity>=1) scope.glowDirection=-1;
        if(scope.glowDirection==-1 && scope.canvasBG.style.opacity<=0.1) scope.glowDirection=1;
      }
  13. We will grab the Animator class created in the Animating independent layers recipe in Chapter 6, Bringing Static Things to Life, and tweak it (changes are highlighted in the code snippet).
    function Animator(refreshRate){
      this.onUpdate = function(){};
      this.onComplete = function(){};
      this.animQue = [];
      this.refreshRate = refreshRate || 35; //if nothing set 35 FPS
      this.interval = 0;
    }
    
    Animator.prototype.add = function(obj,property, from,to,time,delay){
      obj[property] = from;
    
      this.animQue.push({obj:obj,
                p:property,
                crt:from,
                to:to,
                stepSize: (to-from)/(time*1000/this.refreshRate),
                delay:delay*1000 || 0});
    
      if(!this.interval){ //only start interval if not running already
        this.interval = setInterval(this._animate,this.refreshRate,this);	
      }
    
    }
    
    
    Animator.prototype._animate = function(scope){
      var obj;
      var data;
    
      for(var i=0; i<scope.animQue.length; i++){
          data = scope.animQue[i];
    
          if(data.delay>0){
            data.delay-=scope.refreshRate;
          }else{
            obj = data.obj;
            if((data.stepSize>0 && data.crt<data.to) ||
               (data.stepSize<0 && data.crt>data.to)){
    
              data.crt = data.crt + data.stepSize;
              obj[data.p] = data.crt;
            }else{
              obj[data.p] = data.to;	
              scope.animQue.splice(i,1);
              --i;
            }
          }
    
      }
      scope.onUpdate();
      if(	scope.animQue.length==0){
        clearInterval(scope.interval);
        scope.interval = 0; //reset interval variable
        scope.onComplete();
      }
    }
    

When you load the HTML file, you will find a fullscreen map that is getting its directions from a spreadsheet. It will animate and show you the paths my friend took as he traveled from Israel to South America and back.

How it works...

There are many components in this example, but we will focus mainly on the new steps that we haven't covered in any other part of this book.

The first new thing we meet is right in our HTML and CSS:

<link href='http://fonts.googleapis.com/css?family=Yellowtail' rel='stylesheet' type='text/css'>

We picked a font from the Google font library at http://www.google.com/webfonts and integrated it into the text overlays.

.overlayBox {
       ...
        font-family: 'Yellowtail', cursive;
    }

It is time to travel into our JavaScript file, which we start by loading in the Google Visualization Library. It's the same library we were working with in Chapter 8, Playing with Google Charts. Once it's loaded, the init function is triggered. The init function starts our map up and starts loading in the spreadsheet.

In the Changing data source to Google spreadsheet recipe in Chapter 8, Playing with Google Charts, we worked with Google spreadsheets for the first time. There you learned all the steps involved with preparing and adding a Google chart into the Google visualization. In our case, we created a chart that contains line by line all the areas through which my friend traveled.

How it works...

The exception in this case is that we don't want to feed our URL into a Google chart, but instead we want to work with it directly. To do that we will use one of Google's API interfaces, the google.visualization.Query object:

var query = new google.visualization.Query(
      'https://spreadsheets.google.com/tq?key=0Aldzs55s0XbDdERJVlYyWFJISFN3cjlqU1JnTGpOdHc'),
    query.send(onTripDataReady);

The next step is to create our GoogleMapTraveler object. The Google map traveler is a new way for us to work with Google Maps. It doesn't extend any built-in feature of Google maps but is instead a hub for all the other ideas we created in the past. It is used as a manager hub for the marker, called Traveler, that we will create soon and the google.maps.Polyline object that enables us to draw lines on the map.

Instead of having a very static line appearance, let's create a reveal effect for new lines that are added to the Google map. To achieve that, we need a way to update the polyline every few milliseconds to create an animation. From the get go, I know the start point and the destination point as I get that information from the Google spreadsheet created earlier.

The idea is very simple even though in a very complex ecosystem. The idea is to have an array that will store all the latitude/longitude points. This would then be fed into the this.line object every time we wanted to update our screen.

The heart of the logic in this application is stored within this line of code:

this.nextPathPoint(1);

It will start a recursive journey throughout all of the points in our chart.

There's more...

Let's take a deeper look at the logic behind the GoogleMapTraveler.prototype.nextPathPoint method. The first thing we do in this function is to set our map view.

this.setPosition(index-1,2);

The setPosition method does a few things that are all related to repositioning our map and our zoom level based on the data in the current index that is sent. It's a bit smarter than that as it takes in a second parameter that enables it to average out two points. As one travels between two points, it would be best if our map is at the center of the two points. That is done by sending in 2 as the second parameter. The internal logic of the setPosition method is simple. It will loop through as many items as it needs to, to average out the right location.

Next, we add a new point to our this.pathPoints array. We start by duplicating the same point that is already in the array, as we want our new second point to start from the starting point. This way, we can update the last value in our array each time, until it reaches the end goal (of the real next point).

this.pathPoints.push(this.getPosition(index-1,1)); //add last point again

We create a few helper variables. One will point to the new object we just created and pushed into our pathPoints array. And the second is the point that we want to reach at the end of our animation.

var currentPoint = this.pathPoints[this.pathPoints.length-1];
var point = this.getPosition(index,1);

Note

The first variable is not a new object but a reference to the last point created, and the second line is a new object.

Our next step will be to start and animate the values of our currentPoint until it reaches the values in the point object and to update our traveler latitude/longitude information until it reaches its destination as well. We give a delay of 0.75 seconds to our second animation to keep things more interesting.

this.animator.add(currentPoint,"Za",currentPoint.Za,point.Za,1);
this.animator.add(currentPoint,"Ya",currentPoint.Ya,point.Ya,1);
this.animator.add(this.traveler.ll,"Za",this.traveler.ll.Za,point.Za,2,0.75);
this.animator.add(this.traveler.ll,"Ya",this.traveler.ll.Ya,point.Ya,2,0.75);

Before we end this method, we want to actually animate our lines. Right now, we are animating two objects that are not visual. To start animating our visual elements, we will listen to updates till the time we complete the animations.

this.animator.onUpdate = this.bind(this,this.renderLine);
  this.animator.onComplete = this.bind(this,this.showOverlayCopy);//show copy after getting to destination

Each time the animation happens, we update the values of our visual elements in the renderLine method.

To avoid getting runtime errors, we added to the traveler marker an isReady Boolean to indicate to us when our element is ready to be drawn into.

this.lines.setPath(this.pathPoints);
if(this.traveler.isReady)this.traveler.refreshPosition();

When the animation completes, we move to the showOverlayCopy method, where we take over the screen and animate the copy in the same strategy as we've done before. This time around, when we are done with this phase, we will trigger our initial function again and start the cycle all over with an updated index.

GoogleMapTraveler.prototype.hideOverlayCopy = function(){
  //update index now that we are done with initial element
  this.currentIndex++;
  ...

  //as long as the slide is not over go to the next.
  if(this.data.length>this.currentIndex+1)this.nextPathPoint(this.currentIndex+1); 
}

That covers the heart of our build. It's time for us to talk briefly about the two other classes that will help create this application.

Understanding the Traveler marker

We will not dig deeply into this class, as for the most part, it's based on the work we did in the previous recipe, Customizing the look and feel of markers. The biggest difference is that we added internal animation to our element and an updatePosition method that enables us to move our marker around whenever we want to move it.

 Traveler.prototype.updatePosition=function(latlng){
    var radius = this.radius;
    var projection = this.getProjection();
  var point = projection.fromLatLngToDivPixel(latlng);
    this.div.style.left = (point.x - radius) + 'px';
    this.div.style.top = (point.y - radius) + 'px';	
  }

This method gets a latitude and longitude and updates the marker's position.

As we are animating the actual ll object of this object in the main class, we added a second method, refreshPosition, which is called each time the animations are updated.

Traveler.prototype.refreshPosition=function(){
    this.updatePosition(this.ll);	
  }

There is more to explore and find in this class, but I'll leave that for you to have some fun.

Updating the Animator object

We made two major updates to our Animator class, which was originally created in the Animating independent layers recipe in Chapter 6, Bringing Static Things to Life. The first change was integrating callback methods. The idea of a callback is very similar to events. Callbacks enable us to call a function when something happens. This way of working enables us to only have one listener at a time. To do this, we start by creating the two following variables that are our callback functions:

function Animator(refreshRate){
  this.onUpdate = function(){};
  this.onComplete = function(){};

We then trigger both functions in the Animator class in their relevant location (on update or on complete). In our GoogleMapTraveler object, we override the default functions with functions that are internal to the GoogleMapTraveler object.

Our second and last major update to the Animator object is that we added smarter, more detailed logic to enable our animator to animate both to positive and negative areas. Our original animation didn't accommodate animating latitude/longitude values, and as such we tweaked the core animation logic.

This covers the major new things we explored in this recipe. This recipe is jam-packed with many other small things we picked up throughout the chapters. I truly hope you've enjoyed this journey with me, as this is the end of our book. Please feel free to share with me your work and insight. You can find me at http://02geek.com and my e-mail is .

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.147.103.227