Click here to Skip to main content
Click here to Skip to main content

Synchronized Google Earth and Map Views

, 3 Mar 2014 CPOL
Rate this:
Please Sign up or sign in to vote.
Synchronized Google Earth and Map Views using jQuery

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;      // Milliseconds
var precision = 6;          // Significant decimal places
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'; // Standard image

The altZoomList array declares HAE (Height Above Ellipsoid) altitudes in meters for each possible zoom level.

var altZoomList = [ // Altitude <-> Zoom level
    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) {
    /// <summary>Converts an altitude to a zoom level
    /// <param name="alt" />Altitude in meters
    /// <returns>Zoom level
    for (var i = 0; i < 22; ++i) {
        if (alt > altZoomList[i] - ((altZoomList[i] - altZoomList[i+1]) / 2)) return i;
    }
    return 10;
}

function ZoomToAlt(zoom) {
    /// <summary>Converts a zoom level to an altitude
    /// <param name="zoom" />Zoom level
    /// <returns>Altitude in meters
    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) {
    /// <summary>Rounds a number to a given decimal places
    /// <param name="val" />Number to round
    /// <param name="n" />Significant decimal places
    /// <returns>Rounded number
    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) {
    /// <summary>Format a degree minute notation from a latitude
    /// <param name="decLatitude" />Decimal latitude
    /// <returns>Degree minute notation string
    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) {
    /// <summary>Format a degree minute notation from a longitude
    /// <param name="decLongitude" />Decimal longitude
    /// <returns>Degree minute notation string
    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() {
    /// <summary>Set the auto sync image and borders
    $('#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() {
    /// <summary>Creates an instance of Google Earth
    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) {
    /// <summary>Callback after Google Earth instance has been created
    /// <param name="object" />Google Earth instance
    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(''); // Center point placemark
    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() {
    /// <summary>Clear placemarks and initialize new placemarks from the locations array
    if (locations == null || ge == null) return;

    var features = ge.getFeatures();

    // Clear any placemarks
    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() {
    /// <summary>Update the Google Earth status display
    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() {
    /// <summary>Creates an instance of Google Maps
    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({ // Center point 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() {
    /// <summary>Clear markers and initialize new markers from the locations array
    if (locations == null || gm == null) return;

    // Clear any markers
    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() {
    /// <summary>Update the Google Maps status display
    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() {
    /// <summary>Creates an Ajax request and sets the locations array to the JSON response
    $.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) {
    /// <summary>Sync the Google Earth to a geographical location
    /// <param name="lat" />Latitude
    /// <param name="lng" />Longitude
    /// <param name="alt" />Altitude
    /// <returns>The view's camera
    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) {
    /// <summary>Sync the Google Maps to the geographical location of a camera
    /// <param name="camera" />The view's 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() {
    /// <summary>Sync the Google Earth and Google Maps to the currently selected location
    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() {
    /// <summary>Sync the Google Earth to the Google Maps
    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() {
    /// <summary>Sync the Google Maps to the Google Earth
    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() {
    /// <summary>Sync the Google Earth and Google Maps to a bounds that shows all locations
    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) {
    /// <summary>Get the driving directions from a starting address to the selection location
    /// <param name="obj" />Source object instance
    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) {
    /// <summary>Show or hide the Google Earth
    /// <param name="obj" />Source object instance
    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) {
    /// <summary>Show or hide the Google Maps
    /// <param name="obj" />Source object instance
    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) {
    /// <summary>Show or hide the driving directions
    /// <param name="obj" />Source object instance
    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() {
    /// <summary>Periodically syncs the Google Earth and Google Maps
    if (!autoSync) return;

    try {
        var camera = ge.getView().copyAsCamera(ge.ALTITUDE_RELATIVE_TO_GROUND);

        if (lastCenter == null) { // Never set
            SyncAllLocations();
            lastCenter = gm.center;
            lastZoom = gm.zoom;
            return;
        }

        var center = gm.getCenter();
        // Determine if syncing is needed
        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) // Determine what needs to be synced
                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(); // Request initial locations

    $('#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.

<!-- Google Earth Script -->
<script src="http://www.google.com/jsapi"></script>

<!-- Google Maps 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.

<!-- Synchronized GoogleEarthMaps 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.

<!-- Header Area -->
<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.

<!-- Body Area -->
<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%">
                        <!-- Google Earth status and associated layer controls -->
                        <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%">
                        <!-- Google Maps status and associated layer controls -->
                        <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%">
                        <!-- Google Earth container -->
                        <div id="googleEarth" 
                        style="height:8in; border:2px solid transparent" />
                    </td>
                    <td id="tdMap" style="width:50%">
                        <!-- Google Maps container -->
                        <div id="googleMaps" 
                        style="height:8in; border:2px solid transparent" />
                    </td>
                </tr>
            </table>
        </td>
        <td id="tdDirections" style="display:none; width:300px">
            <!-- Google Maps driving directions -->
            <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
{
    /// <summary>
    /// Represents a location entity.
    /// 
    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
{
    /// <summary>
    /// Location controller.
    /// 
    public class LocationController : Controller
    {
        // GET: /Location/
        public ActionResult Index()
        {
            return View();
        }

        // GET: /Location/GetLocations
        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"
            });

            // Add 3 test locations to the location list and return the JSON
            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"

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Scott Kelley
Software Developer
United States United States
No Biography provided

Comments and Discussions

 
Questiongood but no use Pinmembersaxenaabhi624-Nov-14 18:46 
Generalexecution PinmemberMember 104676727-Mar-14 5:41 
AnswerRe: execution (added new sections) [modified] PinprofessionalScott Kelley11-Mar-14 15:42 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Terms of Use | Mobile
Web02 | 2.8.141223.1 | Last Updated 3 Mar 2014
Article Copyright 2014 by Scott Kelley
Everything else Copyright © CodeProject, 1999-2014
Layout: fixed | fluid