Introduction
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.
Click here to watch the demo video.
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.
Summary
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.
Let's Get Started...
Page Design
<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.
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.
RestFul Web Service
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.
[ServiceContract]
public interface IRESTfulService
{
[OperationContract]
[WebInvoke(Method = "GET", ResponseFormat = WebMessageFormat.Json,
BodyStyle = WebMessageBodyStyle.Bare, UriTemplate = "LiveLocation/json")]
LatestPosition GetLatestPosition(); }
[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
'.
public class RESTfulService : IRESTfulService
{
#region IRESTfulService Members
public LatestPosition GetLatestPosition()
{
LatestPosition pos = new LatestPosition();
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:
<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"}
Click here for more information about json.
Google Earth and Maps
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:
- Load Google Earth plugin and Google map side by side
- Initialize the maps for the first time
- Make 2D map as a collapsible feature
- Make sure both the maps resize as per screen
- Some functions to play around with Google earth
- Logic to animate both the maps by continuously moving to the next position
- Manage orientation of map and 3D models automatically
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.5/jquery.min.js"></script>
<script src="https://www.google.com/jsapi?key=.............your api key................">
</script>
<script type="text/javascript" src="http://maps.google.com/maps/api/js?sensor=false">
</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; var model; var marker; var 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.
function initialise3dMap(instance) {
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(''); lookAt.setLatitude(-37.825307); lookAt.setLongitude(144.980183);
lookAt.setRange(100.0); lookAt.setAltitude(0); lookAt.setTilt(60); ge.getView().setAbstractView(lookAt);
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.
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.
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.
Place 3-D Model
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:
model = ge.createModel(''); var loc = ge.createLocation('');
var scale = ge.createScale('');
var orientation = ge.CreateOrientation('');
loc.setLatitude(lookAt.getLatitude()); loc.setLongitude(lookAt.getLongitude());
model.setLocation(loc); scale.Set(5, 5, 5);
model.setScale(scale); orientation.setHeading(-180);
model.setOrientation(orientation); var link = ge.createLink(''); model.setLink(link); link.setHref('http://www.mydomain.com/Model3d/models/us_police_car.dae');
var modelPlacemark = ge.createPlacemark(''); modelPlacemark.setGeometry(model); ge.getFeatures().appendChild(modelPlacemark); }
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.
Don't Forget 2D Maps
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.
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.
Give It Some Life
Now it's the time to make it happen...
var plat =0, plon =0; function MoveTo(lat, lon) {
if (plat != lat || plon != lon) { var lookAt = ge.getView().copyAsLookAt(ge.ALTITUDE_ABSOLUTE); lat1 = lookAt.getLatitude();
lon1 = lookAt.getLongitude();
lat2 = lat;
lon2 = lon;
var br = GetDirection(lat1, lon1, lat2, lon2); lookAt.setHeading(br); lookAt.setLatitude(lat); lookAt.setLongitude(lon);
ge.getView().setAbstractView(lookAt);
model.getLocation().setLatLngAlt(lat, lon, 0); var orientation = ge.CreateOrientation('');
orientation.setHeading(br - 180); model.setOrientation(orientation);
var position = new google.maps.LatLng(lat, lon); marker.setPosition(position);
map.setCenter(position); }
plat = lat; plon = lon; }
function MoveToLatest() {
var currentTime = new Date();
$.getJSON('http://localhost:50315/website/RESTfulService.svc/LiveLocation/json/?' +
currentTime.getTime(), function(data) {
MoveTo(data.lat, data.lon);
});
}
var interval = 0;
function startTracking() {
interval = setInterval("MoveToLatest()", 300); }
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.
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.
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.
Toggle 2D Map, Resize Maps & Enable/Disable 3D Buildings
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.
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...
Using the Code (Updated 11th May, 2011)
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:
- Get your Google Map API key and replace it with the text 'your google map api key' in Default.aspx file.
- Upload the downloaded collada file zip directory to your desired hosting server to obtain the link of .dae file. Set the link where I am setting it for 3D model in Default.aspx.
- Make a basic database table with the columns ID, Lat, Lon and do the code to connect to the database and get the last row (latest lat, lon). This code needs to be done in the class RESTfulService.cs for function
GetLatestPosition()
where commented by me.
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'.
Script to Pump GPS Data
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).
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!!!
Future Enhancements
- Add more checkboxes to enable disable layers, controls, timelines, etc.
- To change the heading direction angle only when the current and previous location is at least 10-15 meters away. As right now if a car stops at some point and we keep getting locations that are varied by .00000x factor, the angle change is irrelevant and makes the map dance.
- To change the panning speed as per the GPS incoming speed automatically or allow the user to select the speed
- To allow the user to select 3D-models on the fly
- To consider altitude and test the prototype while flying above the ground
History
- 3rd May, 2011: Initial version
- 11th May, 2011: Article updated
- Code is now uploaded as a VS 2008 Solution File
- Code testing details included
- Script to pump Dummy GPS data in database
Thank you.