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:
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:
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:
<!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>
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);
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); }
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); }
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); }
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; }
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); }
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 }
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(); }
hideOverlayCopy
method that triggers the next travel point (the next line in the spreadsheet).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); }; }
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; }
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.
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.
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.
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);
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.
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.
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 <[email protected]>
.
3.147.103.227