I work for a company 'Momentum Technologies' in Australia which provides real time video streaming solutions. For the last couple of days, I have been thinking of making a sophisticated dashboard with live maps (all 3D, 2D, street view), tracking live positions of the camera, streaming video on the side with a whole lot of other information and settings to customize the dashboard view. I started my research and was impressed with bing maps initially but as they revoked their 3D maps feature, I ended up using Google maps for my solution. I also did research on Ajax, Jquery and restful services pattern so that I could interact with server side. After 3 days of work, I ended up creating a successful prototype of what I desired. Our clients are quite impressed with this and I am soon going to develop a full fledged interface. In this article, I will cover the code of my prototype and the explanation side by side. I shall not be able to cover any video streaming part as that will be out of scope for this article and against my company's policies. Below is the link of the video showing what we can achieve.
I am a regular lazy viewer of CodeProject and this is my first article to share the working behind my prototype. English is not my first language so please don't mind any bad grammar here, as long as you get the sense of what I am writing.
The prototype is quite simple and there is no complex logic involved in it. If you are aware of ajax, jquery, Rest Services, Google Map & Earth API and some Java, then it's just a piece of cake for you.
In one line: "We repeatedly pan in Google earth and move the marker in Google map to the next geolocation when available live from the server using Ajax".
I used Google maps for my 2D views and Google earth browser plugin for my 3D views. RestFul services structure is used to obtain the latest geolocation. Jquery is used to make an Ajax call to the specific rest service to get geolocation which is finally used to update maps on the browser. The whole process is repeated desired x number of times per second (depending on the rate of your GPS capture device) giving an effect of animation. The desired 3D model is also placed on the Google earth and moved accordingly. The orientation of the map also changes with the heading direction. Remember what I made is just a prototype and there are large number of enhancements that can be made to make it much more complex and perfect in many ways. I will list a number of future enhancements that are in my mind and some ways to do it at the end of this article.
Now I am going to describe my code line by line and answer as many what's and why's as I can.
<table id="holder" style="height:700px; width:100%; border-collapse:collapse"
cellspacing="0" cellpadding="1" >
<tr>
<td colspan="2" style="height:30px;">
<div style=" background-color:Gray; color:white">
<input type="checkbox" id="buildings" title="Toggle 3-D Buildings"
checked="checked" onclick="enbalebuildings()" />3-D Buildings |
<input type="button" id="show2d" title="Toggle 2-D Maps" onclick="toggle2d()"
value="Hide 2-D Map" />
<input type="button" id="Button1" title="Start tracking GPS"
onclick="startTracking()" value="start" />
<input type="button" id="Button3" title="Stop tracking GPS"
onclick="stopTracking()" value="stop" />
</div>
</td>
</tr>
<tr>
<td id="td3dmap" style="height:100%; width:50%">
<div id="div_map3d" style="height:100%; width:100%;"></div></td>
<td id="td2dmap" style="height:100%; width:50%">
<div id="div_map2d" style="height:100%; width:100%;"></div></td>
</tr>
</table>
The page design has one menu bar at the top that can enable /disable various features on maps followed by 2 div tags for both 3D and 2D maps. The height of the table is resized on resize event of windows. Start and stop button will start and stop reading and updating maps with GPS data. 2D map can be toggled.
div
We will first design our restFul web services, then Google maps interface followed by placing 3D models and other interactions. In the end, we will discuss some future enhancements of our prototype.
Design the restFul web services first to get the GPS readings. Rest Services use simple http request response methods to send and receive data. We will design a rest service that uses a specific http URL and returns the latest latitude and longitude from the database.
To start, create an Interface named "IRESTfulService.cs" as below, remember to include 'System.ServiceModel' in your references. The interface will have one operation contract to declare the function and one data contract to declare the return type. Class 'LatestPosition' is declared as a data contract so that it can be returned as a JSON string when we call the service. Operation contract will define a URI format that we will use to call the service. The WebMessageFormat will be 'json' here, but can also be 'xml' if you feel comfortable with that. The BodyStyle is set to 'Bare' as we don't want the returned json string to get wrapped automatically.
System.ServiceModel
LatestPosition
WebMessageFormat
BodyStyle
[ServiceContract]
public interface IRESTfulService
{
[OperationContract]
[WebInvoke(Method = "GET", ResponseFormat = WebMessageFormat.Json,
BodyStyle = WebMessageBodyStyle.Bare, UriTemplate = "LiveLocation/json")]
LatestPosition GetLatestPosition(); // function to return last updated
// coordinate from database.
}
[DataContract]
public class LatestPosition
{
[DataMember]
public string lat = "";
[DataMember]
public string lon = "";
}
Add a class 'RESTfulService' and implement the above interface to it. Here, you can define your function 'GetLatestPosition'.
RESTfulService
GetLatestPosition
public class RESTfulService : IRESTfulService
{
#region IRESTfulService Members
public LatestPosition GetLatestPosition()
{
LatestPosition pos = new LatestPosition();
// do your stuff here to fetch and assign ps.lat and pos.lon values from database.
return pos;
}
#endregion
}
Set serviceBehaviors and services sections in system.serviceModel section of your web config file accordingly. You can search CodeProject for various articles on rest services if you want to go in details. For now, see below for the reference:
serviceBehaviors
services
system.serviceModel
<services>
<service behaviorConfiguration="RESTfulServiceBehavior" name="RESTfulService">
<endpoint address="" binding="webHttpBinding" contract="IRESTfulService"
behaviorConfiguration="web">
<identity>
<dns value="localhost" />
</identity>
</endpoint>
<endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange" />
</service>
</services>
<behaviors>
<serviceBehaviors>
<behavior name="RESTfulServiceBehavior">
<serviceMetadata httpGetEnabled="true" />
<serviceDebug includeExceptionDetailInFaults="false" />
</behavior>
</serviceBehaviors>
<endpointBehaviors>
<behavior name="web">
<webHttp/>
</behavior>
</endpointBehaviors>
</behaviors>
That's it and our restFul web services are ready for use: To test it, go in the browser and use the http request URL to obtain the result. If you are on local host, try the link below in browser:
It should return the values of latest latitude and longitude from the database in json format as below:
{"lat":"-37.8282611111111","lon":"144.997516666667"}
Once we have finished with our rest services, it's time to move on to the main task, which is to animate Google earth and maps with the GPS data. You have to install the Google earth plugin into your browser if you have not done it yet. The code that we are going to discuss will cover the following features:
<script src=""></script>
<script src=" api key................">
</script>
<script type="text/javascript" src="">
</script>
Before starting, we have to include the scripts for Google earth API, Google map API and minified jquery JavaScript. We need the API Key for Google earth which can be registered from here. We don't need any API key for Google maps as per the latest version of Java API 3.
var ge; //hold instance of google earth plugin
var model; //hold 3D model to be placed on earth
var marker; //hold marker on the google map
var map; //hold instance of google map
google.load("earth", "1.5");
google.setOnLoadCallback(init);
function init() {
google.earth.createInstance('div_map3d', initialise3dMap, failed);
}
function failed(errorCode) { alert('Unable to load google earth ' + errorCode); }
The code above is quite self explanatory. We got four global variables to hold the instances of Google earth, map, model and marker. Google earth is then loaded with the version required. 'setOnLoadCallback' is used to call the desired function ('init' in our case) once the API has finished loading. The 'init()' function will in turn call a function 'createInstance' to create an instance of the Google earth plugin and add it to the div element 'div_map3d'. If successful in creating an instance, it will invoke function 'initialise3dMap', else it will fails with calling function 'failed'. The 'initialise3dMap' function will define whatever you want to do on the first run, example where you want to point the map on first load, what controls to be visible, etc.
setOnLoadCallback
init
init()
createInstance
div
div_map3d
initialise3dMap
failed
function initialise3dMap(instance) {
//initialise google earth plugin instance for the very first time.
ge = instance;
ge.getWindow().setVisibility(true);
ge.getLayerRoot().enableLayerById(ge.LAYER_BUILDINGS, true);
ge.getLayerRoot().enableLayerById(ge.LAYER_TERRAIN, true);
ge.getOptions().setFlyToSpeed(1);
var lookAt = ge.createLookAt(''); // Create a new LookAt (i.e., point where we
// are looking )
lookAt.setLatitude(-37.825307); // Set the coordinates values of the point
// where we looking at.
lookAt.setLongitude(144.980183);
lookAt.setRange(100.0); // Set zoom range i.e., from how far are we
// looking at the point.
lookAt.setAltitude(0); // Set altitude from ground of the point
// where we looking at.
lookAt.setTilt(60); // Set the tilt angle where we looking at
// i.e., how many degrees we need to down
// our head to look at the point.
ge.getView().setAbstractView(lookAt); // update Google earth to new 'lookAt' point
In initialise3dMap function above, we have assigned the Google earth plugin instance to our global variable which can be used now to set any other properties related to the plugin. Click here for more details about the Google earth plugin object. We have to set the visibility to true to make sure that our plugin is visible inside our div tag. Later, we can enable/disable the layers we want, Google earth provides various options for 3D overlay layers, click here for more info. we have enabled 3D buildings and terrain in our case.
initialise3dMap
true
setFlyToSpeed' is set to 1 to slowly move/pan from one location to another in Google earth. You can set the speed from 0.0 to 5.0 according to your needs.
setFlyToSpeed
1
Now, we have to create a new 'lookAt point' or the point where we look at the moment on the earth. This point is actually a combination of lat/long, altitude, tilt, etc. that determines in perspective in which you are looking at this point. We basically move this point ahead once we get new lat/long pair. 'setAbstractView' updates Google earth to the new lookAt point. Continuously doing this can give a real moving effect in 3D. You can also user 'camera' instead of 'lookAt' if you feel more comfortable from the viewer's perspective. Click here to read more about cameras.
lookAt point
setAbstractView
lookAt
camera
lookAt
It's time to place a 3D model or an object on to the map. You can design your own 3D model using Google sketch up software. Click here to read more about Google sketch up and modeling. The files with .skp are generally used in Google sketchup but to place it on Google earth, you need to convert it to collada file format. You can visit Google 3D Warehouse to browse through 3D models and download them in collada file format (which is a zipped directory). I have used US Police Car model from the warehouse that is downloadable in collada format.
The collada file format has an extension '.dae'. Once you download the collada zipped folder, you have to host it somewhere so that you can access the .dae file from your browser. As far as I know, you cannot use the local links to reference the collada files because of some terms in Google earth plugin security. Hence, you need to upload the folder somewhere and get the link to .dae file. In my case, I created a folder 'Model3d' on my server and placed the unzipped folders and files - images, models, doc.kml, textures in it. This gave me a link to .dae file inside the folder 'models'. The link should be accessible as below:
// 3D Model placement
model = ge.createModel(''); // create the model geometry
var loc = ge.createLocation('');
var scale = ge.createScale('');
var orientation = ge.CreateOrientation('');
loc.setLatitude(lookAt.getLatitude()); // fetch location of the current lookat point.
loc.setLongitude(lookAt.getLongitude());
model.setLocation(loc); // set the location of the model.
scale.Set(5, 5, 5);
model.setScale(scale); // set scale for the model along x,y,z axis
orientation.setHeading(-180);
model.setOrientation(orientation); // rotate the model x degrees from north.
var link = ge.createLink(''); // defining link
model.setLink(link); // setting the collada file for the model
link.setHref('');
var modelPlacemark = ge.createPlacemark(''); // define the model placemark
modelPlacemark.setGeometry(model); // set model in to the placemark
ge.getFeatures().appendChild(modelPlacemark); // add placemark to Earth
}
Now as per the code above, you can see we have defined a model geometry and three variables for location, scale and orientation of model. We first set the latitude and longitude of the location and assign it to model. Then we can set the scale along x, y and z axis which when applied to model will scale it accordingly. In our case, we scale the real world car model to 5 times so that it can be clearly visible even if we zoom out our map. I set the orientation in such a way so that front of the car faces north on first load. Finally, we have to set the link of the model to the collada file and then create a 'placemark' on Google earth that can be appended with our model. Click here to read more on placing 3D models.
function initialise2dMap() {
var latlng = new google.maps.LatLng(-37.825307, 144.980183);
var myOptions = {
zoom: 15,
panControl: false,
streetViewControl: false,
center: latlng,
mapTypeId: google.maps.MapTypeId.SATELLITE
};
map = new google.maps.Map(document.getElementById("div_map2d"), myOptions);
marker = new google.maps.Marker({ map: map, draggable: true,
animation: google.maps.Animation.DROP, position: latlng });
}
$(window).load(function() {
initialise2dMap();
});
It's a bit of fun to work with Google earth plugin and 3D models, but don't forget about 2D maps as well. It's very simple and straight forward to implement it side by side. You can load a 2D Google map in window load function by calling function 'initialise2dMap' via using jquery. Create a new lat/long center position and specify other options variable to load the map into the div element names 'div_2dmap'. In options, you can enable/disable various flexible features available in the map API. Example - showing controls, map type, enable/disable street view, etc. Click here to read more on map API v3.
initialise2dMap
div_2dmap
Place the marker on the map with various flexible options available to customize it. While updating Google earth, we will also update the marker position and the center of the map when we get the latest GPS coordinate.
Now it's the time to make it happen...
var plat =0, plon =0; // previous latitude and longitude
function MoveTo(lat, lon) {
if (plat != lat || plon != lon) { // if new location is not same as previous location
var lookAt = ge.getView().copyAsLookAt(ge.ALTITUDE_ABSOLUTE); // get current
// lookAt view
lat1 = lookAt.getLatitude();
lon1 = lookAt.getLongitude();
lat2 = lat;
lon2 = lon;
var br = GetDirection(lat1, lon1, lat2, lon2); // get heading direction
// from previous location to current location (x degrees from north)
lookAt.setHeading(br); // set this as a new heading direction.
lookAt.setLatitude(lat); // set new location
lookAt.setLongitude(lon);
ge.getView().setAbstractView(lookAt); // update Google earth to this
// new location
model.getLocation().setLatLngAlt(lat, lon, 0); // get 3d model location
// and update it to new location
var orientation = ge.CreateOrientation('');
orientation.setHeading(br - 180); // set model orientation to the same
// heading direction (x degrees from north)
model.setOrientation(orientation);
var position = new google.maps.LatLng(lat, lon); // move the 2d map marker
// to the new location
marker.setPosition(position);
map.setCenter(position); // set this new location as the center of the map
}
plat = lat; plon = lon; //make current location as previous location before leaving.
}
function MoveToLatest() {
var currentTime = new Date();
// calling the rest web service
$.getJSON('?' +
currentTime.getTime(), function(data) {
MoveTo(data.lat, data.lon);
});
}
var interval = 0;
function startTracking() {
//MoveTo(-37.824661, 144.979607);
//MoveToLatest();
interval = setInterval("MoveToLatest()", 300); // call moveToLatest in every
// 300 ms
}
function stopTracking() {
clearInterval(interval);
}
The code above is self explanatory, on start and stop button click, the functions 'startTracking' and 'stopTracking' are triggered. They in turn call the function 'MoveToLatest' by setting a time interval of x milliseconds on it. Function 'MoveToLatest' uses an inbuilt jquery function '$.getJSON' to make an Ajax call to the rest service which we designed in the beginning. This rest service returns a latitude and longitude in JSON format which can be retrieved as 'data.lat' and 'data.lon'. To avoid complexity, we further call another function 'MoveTo(lat, lon)' which actually moves our Google earth and map from the previous lat/lon to current lat/lon.
startTracking
stopTracking
MoveToLatest
$.getJSON
data.lat
data.lon
MoveTo(lat, lon)
NOTE: I am using a datetime stamp as a query parameter and appending it to our http URL. This datetime stamp has no use in our function definition though it is used to make our HTTP request different every time and to avoid getting any cached result back. If you find any other way to turn off caching in Ajax call, please go for it. If we don't do this, we will keep getting the very first pair of lat/long as a result of every call.
'MoveTo(lat,lon)' will actually move the 'lookAt' point, 3D model and marker on Google maps to the latitude and longitude passed. To do so, we first check if the lat/lon pair is not the same as the previous one, this will avoid any overhead calculation that we might do every x milliseconds even when GPS antena is not moving. To move ahead, we get current lookAt view and update the lat/lon pair in it and do the same with the location of 3D model. While doing so, we also rotate the map and model in the heading direction from previous lat/lon to current lat/lon. For this, we calculate the angle in x degrees from north between two geolocations. Here I have written a function 'GetDirection(lat1, lon1, lat2, lon2)' to do the task.
MoveTo(lat,lon)
GetDirection(lat1, lon1, lat2, lon2)
function GetDirection(lat1, lon1, lat2, lon2) {
var br = 0;
br = Math.atan2(Math.sin(lon2 - lon1) *
Math.cos(lat2), Math.cos(lat1) * Math.sin(lat2) -
Math.sin(lat1) * Math.cos(lat2) * Math.cos(lon2 - lon1)) % (2 * Math.PI);
br = (br * 180) / Math.PI;
return br;
}
Click here to read more on the mathematical formulas and scripts related to geolocation.
function toggle2d() {
if (show2d.value == "Show 2-D Map") {
$("#td2dmap").animate({ width: "50%" }, 500);
$("#td3dmap").animate({ width: "50%" }, 500);
show2d.value = "Hide 2-D Map";
}
else {
$("#td2dmap").animate({ width: "0%" }, 500);
$("#td3dmap").animate({ width: "100%" }, 500);
show2d.value = "Show 2-D Map";
}
}
function onresize() {
var clientHeight = document.documentElement.clientHeight;
$('#holder').each(function() {
$(this).css({
height: (clientHeight -
$(this).position().top-50).toString() + 'px'
});
});
}
$(window).resize(onresize);
onresize();
function enbalebuildings() {
if(buildings.checked)
ge.getLayerRoot().enableLayerById(ge.LAYER_BUILDINGS, true);
else
ge.getLayerRoot().enableLayerById(ge.LAYER_BUILDINGS, false);
}
There are couple of functions I added to make things more interactive and flexible. We can make 2-d map collapsible so that 3-d map can stretch all the way which looks quite good on the screen (watch the video). On a check box click event, we can make the buildings layer enabled and disabled very easily, you can add many other options similar to this. To resize the maps based on screen re sizing, we add an event 'onresize' which sets the height of the table as per the window height - height of menu bar.
onresize
That's all for this article. This article may interest a restricted audience, but still I hope this information might be helpful to them. Read for the future enhancements that I am planning. I would also love to get some more ideas on how to make it more rocking...
I have uploaded the Visual Studio 2008 solution file which can be downloaded and needs to be tweaked a little bit to get things working. Once you have downloaded the file, you need to perform 3 tasks:
GetLatestPosition()
I have manually assigned and returned the next coordinated in the function GetLatestPosition() for example purpose. IF you run the code without any database by just updating the API key and the link of 3D model collada file, you will see the map moving one point ahead when you click on 'start'.
Ok, so if you have spent time in doing the database yourself, great. It's time to pump some GPS coordinated into your database and see what happens. Once you click on start button on the page, it will keep pinging the database for latest coordinates. Here is the time to run the script below. It is a very simple script to pump the GPS coordinated into the database table 'Location' every 500 ms. You can change the time interval at some places if you want to test the variations in time (like a real GPS scenario).
Location
WAITFOR DELAY '000:00:00:500'
INSERT INTO Location ([latitude],[longitude])
VALUES (-37.823771, 144.978858)
WAITFOR DELAY '000:00:00:500'
INSERT INTO Location ([latitude],[longitude])
VALUES (-37.823324, 144.978523)
WAITFOR DELAY '000:00:00:500'
INSERT INTO Location ([latitude],[longitude])
VALUES (-37.822756, 144.977882)
WAITFOR DELAY '000:00:00:500'
INSERT INTO Location ([latitude],[longitude])
VALUES (-37.822107, 144.977134)
WAITFOR DELAY '000:00:00:500'
INSERT INTO Location ([latitude],[longitude])
VALUES (-37.821599, 144.976611)
WAITFOR DELAY '000:00:00:500'
INSERT INTO Location ([latitude],[longitude])
VALUES (-37.821182, 144.976299)
You can add more entries if you got more coordinate to test. When you run the script above, you will see your maps moving with it. Enjoy!!!.