Introduction
The primary goal is to show both Google Earth and Google Maps synchronized with each other. That is, when one side is either panned or zoomed, the other side is automatically synced in terms of center view position and altitude/zoom level. Unwanted behavior, such as jitter or oscillations, needs to be avoided for both panning and zooming. Zooming also needs to be symmetrical when zooming in versus out from either Goggle Earth or Google Maps.
Another goal is to allow a variable number of markers to be simultaneously loaded in both Google Earth and Google Maps. The set of markers are obtained from the web server using an Ajax jQuery call. New sets of markers can be dynamically loaded any time while the client jQuery code is executing. The viewing can be adjusted to automatically display all markers at the same time in an optimized manner (i.e., the lowest zoom level that can show all loaded markers).
The following image shows how we'd like the display to appear:
At the top-left, we have a drop-down list of locations. When a location is selected, the Google Earth and Google Maps are positioned at that location. In addition, a button to the right shows all locations at an optimum zoom level/altitude. In the top-middle, a link button allows the Google Earth and Google Maps to be optionally automatically synchronized. In the top-right, buttons allow the Google Earth, Google Maps, and driving directions to each be shown or hidden.
Below the top locations and button controls are shown the Google Earth on the left and the Google Maps on the right. The driving directions are optionally shown to the right of the Google Maps. Just above both the Google Earth and Google Maps are displayed the center point's location and a set of checkboxes representing the applicable layers.
Background
Google Earth is a virtual globe, which is a 3D software model of the Earth. Google Earth uses the notion of a camera, the camera's location, and where the camera is looking to derive and show the current view of the Earth. As such, the user can look in directions other than nadir (i.e., a point on the earth directly below the camera).
Google Maps is a web mapping application that uses 2D satellite imagery along with 3D earth views. Google Maps shows a nadir view at discrete mapping zoom levels, which may vary in number based upon the geographical location. In addition, Google Maps has many useful layers for traffic, transit, bicycle, and can give directions between locations.
Both Google Earth and Google Maps have some similar features, such as geographic markers that utilize a latitude/longitude coordinate pair. However, both have their respective advantages that, when used together, can have benefits above what just Google Earth and Google Maps offer alone.
Using the Code
To help mitigate jitter problems when synchronizing Google Earth with Maps or visa-versa, we'll implement a timer. This will also allow for one side to be periodically synchronized with the other during a longer panning action. With the timer, we can position either the Google Earth or Google Maps, and not have to be concerned with synchronization. We'll also perform rounding of the respective Google Earth and Google Maps coordinates to mitigate viewing issues when the same exact precision is not utilized.
Furthermore, we'll define an array of altitudes that correspond to the map zoom levels so we can have symmetrical zooming in and out between the two sides. The number of zoom levels varies based upon the geographical location. Consequently, our array needs to contain all possible values, which will be subsequently adjusted by Google Maps when appropriate.
For this implementation, we'll write a jQuery-based script that is completely separate from the markup. We'll also utilize Google Maps API v3 and the current Google Earth API v1. The application itself is ASP.NET MVC 4, but the jQuery and relevant markup is separate from the application framework, which allows an easy and straightforward port to other web frameworks. For the most part, this article discusses portions of the jQuery and markup that are agnostic to the web framework being utilized.
The GoogleEarthMap
implementation has been successfully tested with Internet Explorer 11, Firefox 27, and Chrome 33. Safari was not tested, but there's no reason why it should not work with the GoogleEarthMap
implementation as well.
Common Declarations (GoogleEarthMaps.js)
The locations
variable will hold an array where each element is a geographical location that contains information we use to populate the markers for both displays. The timerDelay
variable determines the number of milliseconds between calls to the interval timer function of the window. The precision
variable is the number if decimal places in the latitude and longitude coordinates that we consider relevant, but should be less than or equal to the smaller of the Google Earth or Map precision. The remaining variables are toggles for various views and features.
var locations = null;
var timerDelay = 2000;
var precision = 6;
var autoSync = true;
var showEarth = true;
var showMap = true;
var showDirections = false;
Next, we'll declare two images for our link button (i.e., to auto-synchronize the Google Earth and Google Maps or not). The cross-hair image is used on both Goggle Earth and Maps to identify the center point of the views.
var linkImage = '../Images/link.png';
var unlinkImage = '../Images/unlink.png';
var crosshairImage = 'http://maps.google.com/mapfiles/kml/shapes/cross-hairs.png';
The altZoomList
array declares HAE (Height Above Ellipsoid) altitudes in meters for each possible zoom level.
var altZoomList = [
30000000, 24000000, 18000000, 10000000, 4000000, 1900000, 1100000, 550000, 280000,
170000, 82000, 38000, 19000, 9200, 4300, 2000, 990, 570, 280, 100, 36, 12, 0 ];
The function AltToZoom
converts an altitude to a zoom level. Since the array begins with the highest altitude values first, return the first array index (i.e., zoom level) where the given altitude is greater than the array's altitude minus half the distance between the array's altitude and the next. For locations that have less zoom levels, Google Maps will subsequently adjust this value. The function ZoomToAlt
reverses the previous conversion based upon the same common array of altitude values.
function AltToZoom(alt) {
for (var i = 0; i < 22; ++i) {
if (alt > altZoomList[i] - ((altZoomList[i] - altZoomList[i+1]) / 2)) return i;
}
return 10;
}
function ZoomToAlt(zoom) {
return altZoomList[zoom < 0 ? 0 : zoom > 21 ? 21 : zoom];
}
The DecRound
function rounds a given value to a given number of decimal places.
function DecRound(val, n) {
var factor;
factor = Math.pow(10, n);
return (Math.round(val * factor) / factor);
}
The LatDecToDegMin
and LngDecToDegMin
functions convert latitude and longitude values respectively. The values are given in decimal degrees and are converted into a string
representing degrees and minutes notation.
function LatDecToDegMin(decLatitude) {
var intDegree;
var decMinute;
var strLatitude = 'N';
if (decLatitude < 0) {
strLatitude = 'S';
decLatitude = decLatitude * -1;
}
intDegree = Math.floor(decLatitude);
decMinute = (decLatitude - intDegree) * 60;
decMinute = DecRound(decMinute, 3);
strLatitude = String(intDegree) + '\u00B0 ' + String(decMinute) + '\u2032 ' + strLatitude;
return strLatitude;
}
function LngDecToDegMin(decLongitude) {
var intDegree;
var decMinute;
var strLongitude = 'E';
if (decLongitude < 0) {
strLongitude = 'W';
decLongitude = decLongitude * -1;
}
intDegree = Math.floor(decLongitude);
decMinute = (decLongitude - intDegree) * 60;
decMinute = DecRound(decMinute, 3);
strLongitude = String(intDegree) + '\u00B0 ' + String(decMinute) + '\u2032 ' + strLongitude;
return strLongitude;
}
The SetAutoSyncImage
function changes the link button's image and the borders around the Google Earth and Google Maps views based upon the autoSync
toggle.
function SetAutoSyncImage() {
$('#syncAuto').attr('src', (autoSync ? linkImage : unlinkImage));
$('#googleEarth').css('border-color', (autoSync ? 'blue' : 'transparent'));
$('#googleMaps').css('border-color', (autoSync ? 'blue' : 'transparent'));
}
Google Earth (GoogleEarthMaps.js)
The following code declares the main Google Earth instance ge
and the center placemark cp
. A placemarks
array is declared to contain the other placemarks that represent elements of the locations
array. The latest major version 1 of the Google Earth is loaded and the InitGoogleEarth
function is called to create our Google Earth instance.
var ge = null;
var cp = null;
var placemarks = [];
google.load('earth', '1');
function InitGoogleEarth() {
google.earth.createInstance('googleEarth', InitGECallback);
}
The InitGECallback
function initializes our Google Earth instance, sets up navigation controls, and enables the street view. The center point placemark is then created from crosshairImage
and the UpdateGEStatus
event listener is wired up to detect view changes. Since this is an initialization callback function and the locations might have already been loaded, the other placemarks are created by calling InitGELocations
. Finally, various Google Earth layer handlers are wired up to their respective checkboxes.
function InitGECallback(object) {
ge = object;
ge.getWindow().setVisibility(true);
ge.getNavigationControl().setVisibility(ge.VISIBILITY_SHOW);
ge.getNavigationControl().setStreetViewEnabled(true);
var icon = ge.createIcon('');
icon.setHref(crosshairImage);
var style = ge.createStyle('');
style.getIconStyle().setIcon(icon);
cp = ge.createPlacemark('');
cp.setStyleSelector(style);
var pt = ge.createPoint('');
pt.setLatitude(0);
pt.setLongitude(0);
cp.setGeometry(pt);
ge.getFeatures().appendChild(cp);
google.earth.addEventListener(ge.getView(), 'viewchange', function () {
UpdateGEStatus();
});
InitGELocations();
$('#chkGETerrain').change(function (event) {
ge.getLayerRoot().enableLayerById(ge.LAYER_TERRAIN, this.checked);
});
$('#chkGEBorders').change(function () {
ge.getLayerRoot().enableLayerById(ge.LAYER_BORDERS, this.checked);
});
$('#chkGERoads').change(function () {
ge.getLayerRoot().enableLayerById(ge.LAYER_ROADS, this.checked);
});
}
The InitGELocations
function clears any previously defined location placemarks and creates new placemarks for all elements in the locations
array.
function InitGELocations() {
if (locations == null || ge == null) return;
var features = ge.getFeatures();
for (var i = 0; i < placemarks.length; ++i) {
features.removeChild(placemarks[i]);
}
placemarks = [];
$.each(locations, function(index, loc) {
var styleMap = ge.createStyleMap('');
var mapHighlight = ge.createStyle('');
mapHighlight.getBalloonStyle().setText(loc.Name + '\n' + loc.Address);
styleMap.setHighlightStyle(mapHighlight);
var pt = ge.createPoint('');
pt.setLatitude(loc.Latitude);
pt.setLongitude(loc.Longitude);
var pm = ge.createPlacemark('');
pm.setStyleSelector(styleMap);
pm.setGeometry(pt);
features.appendChild(pm);
placemarks.push(pm);
});
}
The UpdateGEStatus
function updates the status display for the Google Earth. The status display includes the latitude, longitude, and altitude of the center point placemark.
function UpdateGEStatus() {
var camera = ge.getView().copyAsCamera(ge.ALTITUDE_RELATIVE_TO_GROUND);
var pt = cp.getGeometry();
pt.setLatitude(camera.getLatitude());
pt.setLongitude(camera.getLongitude());
cp.setGeometry(pt);
var status = $('#googleEarthStatus');
status.text('Lat: ' + LatDecToDegMin(camera.getLatitude()) + ', Lng: ' +
LngDecToDegMin(camera.getLongitude()) + ', Alt: ' + String(DecRound(camera.getAltitude(), 1)) + ' m');
}
Google Maps (GoogleEarthMaps.js)
The following code declares the main Google Maps instance gm
and variables for the directions service dirService
and directions renderer dirDisplay
. A markers
array is declared to contain the markers that represent elements of the locations
array, and toggle variables are declared that correspond to the various free map layers. Finally, variables for the last center position and last zoom level are declared so that our timer function can determine which side changed.
var gm = null;
var dirService = null;
var dirDisplay = null;
var markers = [];
var trafficLayer = null;
var transitLayer = null;
var bicycleLayer = null;
var weatherLayer = null;
var cloudLayer = null;
var lastCenter = null;
var lastZoom = 0;
The InitGoogleMaps
function initializes our Google Maps instance, and sets up the directions service and renderer. The center point marker is then created from crosshairImage
and the UpdateGMStatus
event listener is wired up to detect center point and zoom changes. Finally, various Google Maps layer handlers are wired up to their respective checkboxes.
function InitGoogleMaps() {
var mapOptions = {
mapTypeId: google.maps.MapTypeId.ROADMAP,
center: new google.maps.LatLng(0, 0),
zoom: 8
};
gm = new google.maps.Map(document.getElementById('googleMaps'), mapOptions);
dirService = new google.maps.DirectionsService();
dirDisplay = new google.maps.DirectionsRenderer();
var marker = new google.maps.Marker({
map: gm,
icon: crosshairImage,
shape: { coords: [0, 0, 0, 0], type: 'rect' }
});
marker.bindTo('position', gm, 'center');
google.maps.event.addListener(gm, 'center_changed', function () {
UpdateGMStatus();
});
google.maps.event.addListener(gm, 'zoom_changed', function () {
UpdateGMStatus();
});
$('#chkGMTraffic').change(function () {
if (this.checked) {
trafficLayer = new google.maps.TrafficLayer();
trafficLayer.setMap(gm);
} else {
trafficLayer.setMap(null);
trafficLayer = null;
}
});
$('#chkGMTransit').change(function () {
if (this.checked) {
transitLayer = new google.maps.TransitLayer();
transitLayer.setMap(gm);
} else {
transitLayer.setMap(null);
transitLayer = null;
}
});
$('#chkGMBicycle').change(function () {
if (this.checked) {
bicycleLayer = new google.maps.BicyclingLayer();
bicycleLayer.setMap(gm);
} else {
bicycleLayer.setMap(null);
bicycleLayer = null;
}
});
$('#chkGMWeather').change(function () {
if (this.checked) {
weatherLayer = new google.maps.weather.WeatherLayer({
temperatureUnits: google.maps.weather.TemperatureUnit.FAHRENHEIT
});
weatherLayer.setMap(gm);
} else {
weatherLayer.setMap(null);
weatherLayer = null;
}
});
$('#chkGMClouds').change(function () {
if (this.checked) {
cloudLayer = new google.maps.weather.CloudLayer();
cloudLayer.setMap(gm);
} else {
cloudLayer.setMap(null);
cloudLayer = null;
}
});
}
The InitGMLocations
function clears any previously defined location markers and creates new markers for all elements in the locations
array.
function InitGMLocations() {
if (locations == null || gm == null) return;
for (var i = 0; i < markers.length; ++i) {
markers[i].setMap(null);
}
markers = [];
$.each(locations, function(index, loc) {
var marker = new google.maps.Marker({
position: new google.maps.LatLng(loc.Latitude, loc.Longitude),
map: gm,
title: (loc.Name + '\n' + loc.Address)
});
markers.push(marker);
});
}
The UpdateGEStatus
function updates the status display for Google Maps. The status display includes the center point marker's latitude and longitude, and the map zoom level.
function UpdateGMStatus() {
var center = gm.getCenter();
var status = $('#googleMapsStatus');
status.text('Lat: ' + LatDecToDegMin(center.lat()) + ', Lng: ' +
LngDecToDegMin(center.lng()) + ', Zoom: ' + String(gm.zoom));
}
Synchronization Functions (GoogleEarthMaps.js)
The RequestLocationsAjax
function uses jQuery to issue an asynchronous postback requesting an array of locations in JSON. We set the JSON response to the locations
array and then reinitialize our drop-down list of locations. Finally, the Google Earth placemarks and Google Maps markers are reinitialized with the new locations
array by calling the InitGELocations
and InitGMLocations
functions respectively.
function RequestLocationsAjax() {
$.getJSON('/Location/GetLocations', { region: '' }, function (json) {
locations = json;
var locationList = $('#locationList');
locationList.empty();
var docfrag = document.createDocumentFragment();
$.each(locations, function (index, loc) {
var option = document.createElement("option");
option.innerHTML = loc.Name;
docfrag.appendChild(option);
});
locationList.append(docfrag);
InitGELocations();
InitGMLocations();
});
}
The SyncEarth
function sets the Google Earth view's camera to the values of the latitude, longitude, and altitude parameters.
function SyncEarth(lat, lng, alt) {
var camera = ge.getView().copyAsCamera(ge.ALTITUDE_RELATIVE_TO_GROUND);
camera.setLatitude(lat);
camera.setLongitude(lng);
camera.setAltitude(alt);
ge.getView().setAbstractView(camera);
return camera;
}
The SyncMap
function sets the Google Maps center position and zoom level to the values of the Google Earth's camera. In particular, the zoom level is derived from the altitude by calling the AltToZoom
function.
function SyncMap(camera) {
gm.setCenter(new google.maps.LatLng(camera.getLatitude(), camera.getLongitude()));
gm.setZoom(AltToZoom(camera.getAltitude()));
}
The SyncLocation
function initially synchronizes the Google Earth to the currently selected location. It then synchronizes the Google Maps to the Google Earth.
function SyncToLocation() {
var index = $("#locationList")[0].selectedIndex;
if (index < 0) return;
var camera = SyncEarth(locations[index].Latitude, locations[index].Longitude, locations[index].Altitude);
SyncMap(camera);
}
The SyncEarthToMap
function synchronizes the Google Earth to the Google Maps. In particular, the altitude is derived from the zoom level by calling the ZoomToAlt
function
function SyncEarthToMap() {
var center = gm.getCenter();
SyncEarth(center.lat(), center.lng(), ZoomToAlt(gm.zoom));
}
The SyncMapToEarth
function synchronizes the Google Maps to the Google Earth.
function SyncMapToEarth() {
SyncMap(ge.getView().copyAsCamera(ge.ALTITUDE_RELATIVE_TO_GROUND));
}
The SyncAllLocations
function determines the boundary that encloses all currently defined Google Maps markers and then positions the map to show all markers. The SyncEarthToMap
function is then called to synchronize the Google Earth to the Google Maps.
function SyncAllLocations() {
var bound = new google.maps.LatLngBounds();
for (var i = 0; i < markers.length; ++i) {
bound.extend(markers[i].getPosition());
}
gm.fitBounds(bound);
SyncEarthToMap();
}
Event Handlers (GoogleEarthMaps.js)
The GetDirections
function creates a driving route request using the currently selected location. It then calls the dirService
directions service route
function and sets the dirDisplay
directions renderer to the driving route response.
function GetDirections(obj) {
var index = $("#locationList")[0].selectedIndex;
if (index < 0) return;
dirDisplay.setMap(gm);
dirDisplay.setPanel(document.getElementById('divDirections'));
var request = {
origin: document.getElementById('fromAddress').value,
destination: locations[index].Address,
travelMode: google.maps.DirectionsTravelMode.DRIVING
};
dirService.route(request, function (response, status) {
if (status == google.maps.DirectionsStatus.OK) {
dirDisplay.setDirections(response);
}
});
}
The function ShowEarth
toggles the showing/hiding of the Google Earth view. The Google Maps resize
event is triggered to adjust the display. The Google Earth display automatically adjusts itself.
function ShowEarth(obj) {
showEarth = !showEarth;
if (showEarth) {
$(obj).val('Hide Earth');
$('#tdEarth').css('display', 'table-cell');
$('#tdEarthExt').css('display', 'table-cell');
} else {
$(obj).val('Show Earth');
$('#tdEarth').css('display', 'none');
$('#tdEarthExt').css('display', 'none');
}
google.maps.event.trigger(gm, "resize");
}
The function ShowMap
toggles the showing/hiding of the Google Maps view. The Google Maps resize
event is triggered to adjust the display. The Google Earth display automatically adjusts itself.
function ShowMap(obj) {
showMap = !showMap;
if (showMap) {
$(obj).val('Hide Map');
$('#tdMap').css('display', 'table-cell');
$('#tdMapExt').css('display', 'table-cell');
} else {
$(obj).val('Show Map');
$('#tdMap').css('display', 'none');
$('#tdMapExt').css('display', 'none');
}
google.maps.event.trigger(gm, "resize");
}
The function ShowDirections
toggles the showing/hiding of the Google Maps directions.
function ShowDirections(obj) {
showDirections = !showDirections;
if (showDirections) {
$(obj).val('Hide Directions');
$('#tdDirections').css('display', 'table-cell');
} else {
$(obj).val('Show Directions');
$('#tdDirections').css('display', 'none');
dirDisplay.setMap(null);
dirDisplay.setPanel(null);
}
}
When the toogle autoSync
is true
, the TimerFunction
initially checks if the last center position of the map has been set and, if not set, calls the SyncAllLocations
function to position both Google Earth and Google Maps to show the currently known locations and returns from the function. All relevant activity is done in a try
/catch
to avoid aborts of the timer thread.
The function then checks if the rounded values of the Google Earth's camera and the Google Maps center position are different or if the calculated zoom level of the camera is different from the Google Maps zoom level. If so, the Google Maps is synced to Google Earth if its center position and zoom level has not changed, otherwise the Google Earth is synced to the Google Maps. Finally, the last Google Maps center position and zoom level are recorded in preparation for the next call to this function.
function TimerFunction() {
if (!autoSync) return;
try {
var camera = ge.getView().copyAsCamera(ge.ALTITUDE_RELATIVE_TO_GROUND);
if (lastCenter == null) {
SyncAllLocations();
lastCenter = gm.center;
lastZoom = gm.zoom;
return;
}
var center = gm.getCenter();
if (DecRound(camera.getLatitude(), precision) != DecRound(center.lat(), precision) ||
DecRound(camera.getLongitude(), precision) != DecRound(center.lng(), precision) ||
AltToZoom(camera.getAltitude()) != gm.zoom) {
if (center == lastCenter && gm.zoom == lastZoom)
SyncMapToEarth();
else
SyncEarthToMap();
}
lastCenter = gm.center;
lastZoom = gm.zoom;
}
catch (err) {
}
}
Initialization (GoogleEarthMaps.js)
The document ready
function initially sets the link button's image and then initializes both Google Earth and Google Maps by calling the InitGoogleEarth
and InitGoogleMaps
functions. The RequestLocationsAjax
is called to load the initial locations, but subsequent calls to the RequestLocationsAjax
function can also be made to dynamically load new locations.
The various handlers are then wired up to their respective controls and buttons. Finally, the window's interval is set to the TimerFunction
with a delay between calls determined by the variable timerDelay
(in milliseconds).
$(document).ready(function () {
SetAutoSyncImage();
InitGoogleEarth();
InitGoogleMaps();
RequestLocationsAjax();
$('#locationList').change(function () {
SyncToLocation();
});
$('#syncLocation').click(function () {
SyncToLocation();
});
$('#syncAllLocations').click(function () {
SyncAllLocations();
});
$('#syncAuto').click(function () {
autoSync = !autoSync;
SetAutoSyncImage();
});
$('#btnShowEarth').click(function () {
ShowEarth(this);
});
$('#btnShowMap').click(function () {
ShowMap(this);
});
$('#btnShowDirections').click(function () {
ShowDirections(this);
});
$('#btnGetDirections').click(function () {
GetDirections(this);
});
window.setInterval(function () {
TimerFunction();
}, timerDelay);
});
Markup (GoogleEarthMaps.cshtml)
All of the jQuery presented has been generic, and function virtually the same in different environments and using a variety of browsers (i.e., Internet Explorer 11, Firefox 27, and Chrome 33). Apart from one line that is identified later, the following markup is also generic and should work in other environments as well.
Because of an issue between the Google Earth plug-in and Internet Explorer with emulation modes greater than 9 (i.e., the plug-in is always re-downloaded), the following line is included in the head
tag of the \Views\Shared\_Layout.cshtml
file:
<meta http-equiv="X-UA-Compatible" content="IE=9">
The lines below include the Google Earth and Google Maps scripts. For those that remember, note that the license keys are no longer required by Google to utilize the free version of either Google Earth or Google Maps.
<!--
<script src="http://www.google.com/jsapi"></script>
<!--
<script src="https://maps.googleapis.com/maps/api/js?libraries=weather&sensor=false"></script>
The line below is not generic HTML and includes the GoogleEarthMaps.js script we previously defined. In a generic implementation, the src
attribute's value would be replaced with an applicable reference to the GoogleEarthMaps.js script.
<!--
<script src="@Url.Content("~/Scripts/GoogleEarthMaps.js")"></script>
The following table represents a header area with a selection list of the currently loaded locations. In addition, a number of buttons assist to synchronize and show/hide the various views.
<!--
<table cellpadding="0" cellspacing="0" style="width:100%">
<tr>
<td style="width:45%">
<table cellpadding="0" cellspacing="0" style="width:100%">
<tr>
<td style="width:50%">
<select id="locationList"
class="GEM_Select" style="width:100%" />
</td>
<td style="width:1%">
</td>
<td style="width:24%">
<input id="syncLocation" class="GEM_Input"
type="button" value="Go To Location" />
</td>
<td style="width:25%">
<input id="syncAllLocations" class="GEM_Input"
type="button" value="View All Locations" />
</td>
</tr>
</table>
</td>
<td style="width:10%; text-align:center">
<input id="syncAuto" type="image"
alt="Auto Sync" width="32" height="32">
</td>
<td style="width:45%; text-align:right">
<input id="btnShowEarth" class="GEM_Input"
type="button" value="Hide Earth" />
<input id="btnShowMap" class="GEM_Input"
type="button" value="Hide Map" />
<input id="btnShowDirections" class="GEM_Input"
type="button" value="Show Directions" />
</td>
</tr>
</table>
The following outer table has a left cell that represents the combined Google Earth/Google Maps views and the right cell representing the Google Maps driving directions. The driving directions table cell is not shown by default until requested.
The left cell's inner table has a left cell that represents the Google Earth view and a right cell that represents the Google Maps view. Both table cells can be shown or hidden. The first row of each left and right cell contains a status line for Google Earth and Google Maps respectively, whereas the second row of each left and right cell holds the containers for the Google Earth and Google Maps controls.
The markup below is free of any JavaScript and instead uses ids that are assigned to applicable elements. When combined with the separated GoogleEarthMaps
script, behavior is associated with the markup elements. Since the markup and jQuery are more loosely coupled, they are each naturally more adaptable to change.
<!--
<table cellpadding="0" cellspacing="0" style="width:100%">
<tr>
<td>
<table cellpadding="0" cellspacing="0" style="width:100%">
<tr>
<td id="tdEarthExt" style="width:50%">
<!--
<table cellpadding="0" cellspacing="0"
style="margin-top:5px; width:100%">
<tr>
<td style="width:50%">
<span id="googleEarthStatus" />
</td>
<td style="width:50%">
<span style="white-space:nowrap">
<input id="chkGETerrain"
type="checkbox" checked="checked" />
<label for="chkGETerrain">Terrain</label>
</span>
<span style="white-space:nowrap">
<input id="chkGEBorders" type="checkbox" />
<label for="chkGEBorders">Borders</label>
</span>
<span style="white-space:nowrap">
<input id="chkGERoads" type="checkbox" />
<label for="chkGERoads">Roads</label>
</span>
</td>
</tr>
</table>
</td>
<td id="tdMapExt" style="width:50%">
<!--
<table cellpadding="0"
cellspacing="0" style="margin-top:5px; width:100%">
<tr>
<td style="width:50%">
<span id="googleMapsStatus" />
</td>
<td style="width:50%">
<span style="white-space:nowrap">
<input id="chkGMTraffic" type="checkbox" />
<label for="chkGMTraffic">Traffic</label>
</span>
<span style="white-space:nowrap">
<input id="chkGMTransit" type="checkbox" />
<label for="chkGMTransit">Transit</label>
</span>
<span style="white-space:nowrap">
<input id="chkGMBicycle" type="checkbox" />
<label for="chkGMBicycle">Bicycle</label>
</span>
<span style="white-space:nowrap">
<input id="chkGMWeather" type="checkbox" />
<label for="chkGMWeather">Weather</label>
</span>
<span style="white-space:nowrap">
<input id="chkGMClouds" type="checkbox" />
<label for="chkGMClouds">Clouds</label>
</span>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td id="tdEarth" style="width:50%">
<!--
<div id="googleEarth"
style="height:8in; border:2px solid transparent" />
</td>
<td id="tdMap" style="width:50%">
<!--
<div id="googleMaps"
style="height:8in; border:2px solid transparent" />
</td>
</tr>
</table>
</td>
<td id="tdDirections" style="display:none; width:300px">
<!--
<div style="margin-left:6px">
<div>From: <input type="text"
id="fromAddress" value="Escondido,
CA" style="width:80%"/></div><br />
<div><input id="btnGetDirections" class="GEM_Input"
type="button" value="Get Directions!" /></div><br />
<div id="divDirections"
style="border-style:solid; border-width:1px; padding:2px"></div>
</div>
</td>
</tr>
</table>
Handling the Asynchronous Postback for Locations
As mentioned earlier, the RequestLocationsAjax
function uses jQuery to issue an asynchronous postback
requesting an array of locations in JSON. The project file Models/Location.cs defines a single
location as follows:
namespace GoogleEarthMVC.Models
{
public class Location
{
public double Latitude { get; set; }
public double Longitude { get; set; }
public int Altitude { get; set; }
public string Name { get; set; }
public string Address { get; set; }
}
}
The project file Controllers/LocationController.cs returns the sample array of three locations in
Json format as follows:
namespace GoogleEarthMVC.Controllers
{
public class LocationController : Controller
{
public ActionResult Index()
{
return View();
}
public JsonResult GetLocations(string region)
{
List<location> locList = new List<location>();
locList.Add(new Location()
{
Latitude = 32.715329,
Longitude = -117.157255,
Altitude = 10000,
Name = "San Diego Office",
Address = "San Diego, CA"
});
locList.Add(new Location()
{
Latitude = 33.032002840975736,
Longitude = -117.28510326833907,
Altitude = 10000,
Name = "Encinitas Office",
Address = "Encinitas, CA"
});
locList.Add(new Location()
{
Latitude = 33.15165880513573,
Longitude = -117.14846081228425,
Altitude = 10000,
Name = "San Marcos Office",
Address = "San Marcos, CA"
});
return Json(locList, JsonRequestBehavior.AllowGet);
}
}
}
Compiling and Running GoogleEarthMaps
GoogleEarthMap
is an ASP.NET MVC 4 web application project that can be built and run with either Visual Studio 2012
or Visual Studio 2013. However, the NuGet packages will need to be initially restored. Fortunately with the built-in
NuGet Package Manager, this can be quickly and easily done with the push of a single button. From within Visual Studio 2012,
start the NuGet Package Manager as follows:
From the NuGet Package Manager, restore the NuGet packages by pressing the Restore
button:
After restoring the NuGet packages that are utilized by the GoogleEarthMaps project, you should see:
You can now press F5 to compile and run GoogleEarthMaps. The first time that GoogleEarthMaps
is run in
your browser, you may need to accept the Google Earth Plug-in installation.
Points of Interest
The synchronized Google Earth and Google Maps, along with the support for locations that can be dynamically re-loaded, facilitates some interesting ways of using these two facilities in concert. Hopefully, you'll find the implementation mostly generic, such that it can be easily ported to other environments if desired.
History
- Added sections "Handling the Asynchronous Postback for Locations" and "Compiling and Running GoogleEarthMaps"