Monday, October 27, 2014

JavaScript and Working With the Google Maps JavaScript API

The Introduction
I am the first to admit that JavaScript is one of my least favorite languages to work with.  It could have been the inability to really debug the code - unless you count a bunch of "alert('i am here')" within the code.  Or if it was my OCD and experience in software development to that point that abhorred loosely coupled data types and JavaScript felt a bit too loose for my tastes.  It could also have been the poor documentation.  Either way I've arrived at a reasonable working relationship with the language.  I've been given (and found) better tools to debug the script, I've learned to accept the loosely typed nature of the language (and even that has gotten better), and the documentation has gotten significantly better.

So recently I had a real opportunity to stretch my JavaScript skills beyond simple $.ajax() calls and form validation.  My current employer is attempting to put together a web site where our business customers can examine the status of their services in real-time, put in trouble tickets, and even follow up on status for trouble tickets submitted either by them or on their behalf.  One the visual aspects of this request is the ability to map where those services exist.  What I mean by services is advanced products called circuits used to carry large amounts of voice or data - think between 10MB to 1GB pipes.  The industry term used for the location of these circuits is "A to Z addresses".  You could likely take a guess at what that means - each circuit has a start location and an end location.  If the circuit is simply a connection between the network provider and the customer's location the circuit will only have an "A" location.  The "Z" location is inferred to be at the local office of the network provider.  On the other hand if the circuit is between two different offices it would have an "A" location and a "Z" location.  Each service the customer has installed can be comprised of one to hundreds of circuits.

The Solution
The data that is stored about these circuits is used a lot and the quality is extremely high and is stored in a network inventory system (NIS).  In our business it is required to know where a circuit is installed, what equipment is located at the end points, and even if portions of the circuit is being leased from another provider.  So getting the services and the A to Z locations for each circuit is relatively straight forward.  The disappointing part here is that the NIS doesn't store the latitude or longitude of the circuit addresses - so those will have to be looked up.   I decided early on that the Google Maps JavaScript API would be used to display the A to Z points on a map.  Also if the circuit had both an A and Z location that a line would be drawn between the two points indicating that those two markers were connected.

Because the customer could have several services it was decided early that a map should appear for each service instance and not include circuits associated with a different service instance.

So first I'll introduce the base classes that were used for the initial prototype.

     
function MarkerAddress() {
    this.address = null;
    this.description = null;
    this.marker = null;
    this.drawLine = false;
    this.geoCodeResult = null;
    this.drawnLine = null;
}

function GoogleMapContainer() {
    this.companyMapInstance = null;
    this.serviceObjectId = null;
    this.googleListener = null;
    this.mapElement = null;
}

MarkerAddress will include the given address, a description that should appear on the Google Map marker, the marker object that appears on the map, an indicator that a line should be drawn to the prior marker in an array that will be stored, and the results from calling the Map API's geoLocation API.

GoogleMapContainer will contain the element which the map will appear, the service instance Id from the NIS, the Google listener handle, and an instance of the object that will be doing most of the work of looking up (and storing) the addresses for the circuits on that service instance.

     
// REQUIRES THE underscore.js library to be loaded!
function CompanyMapInstance() {
    this.googleGeoCodeInstance = new google.maps.Geocoder();
    this.googleMapInstance = null;

    // these contain the addresses we passed 
    // along with the extended properties
    // in the geoCode location in GoogleMaps!
    this.addresses = new Array;

    _.bindAll(this, "callBackGeoCode");
}

Finally there is the CompanyMapInstance object. Here is where an instance of the Google Geolocation object and Google Map object is stored. Along with an array of MarkerAddresses located in the addresses array.  You might notice the call to _.bindAll(this, "callBackGeoCode").  I'll talk more about this later.

I won't go much into the details behind creating the instances of the GoogleMapContainer - I'll just say that a new instance will be created for each service instance the customer has on their account.  There's an array that will contain these so the already created GoogleMapContainer can found later as the customer can display/hide each of the service instances on the main page.

When a new service instance is requested for display an $.ajax() call is made back to the server to obtain all the circuit addresses.  The addresses are hydrated as objects and are placed into the CompanyMapInstance.addresses array.  Here's the initial version of the success callback that is invoked within the $.ajax() call.

     
success: function (circuitPoints) {
    var circuitPointList = JSON.parse(circuitPoints);

    if (circuitPointList.length > 0) {
        var containerId = circuitPointList[0].ServiceObjectId;

        // find the map container for this service instance
        var mapContainer = $.grep(googleApis, function (e) { return e.serviceObjectId == containerId; });

        if (mapContainer.length > 0) {

            var aCompanyMapInstance = mapContainer[0].companyMapInstance;

            for (var i in circuitPointList) {
                // normalize the data a bit - TODO - this could be better?
                var aMarkerAddress = new MarkerAddress();
                aMarkerAddress.address = circuitPointList[i].ALocationAddress;
                aMarkerAddress.description = circuitPointList[i].Description;
                aMarkerAddress.drawLine = false;
                aCompanyMapInstance.addresses.push(aMarkerAddress);

                if (circuitPointList[i].ZLocationAddress != null) {
                    aMarkerAddress = new MarkerAddress();
                    aMarkerAddress.address = circuitPointList[i].ZLocationAddress;
                    aMarkerAddress.description = circuitPointList[i].Description;
                    aMarkerAddress.drawLine = true;
                    aCompanyMapInstance.addresses.push(aMarkerAddress);
                }
            }
            
            // i've populated the addresses!!!!
            // now mark the points...and here's why the _.bindAll() is important!!
            aCompanyMapInstance .setMarkers();
        }
    }
}

Once the addresses are populated the CompanyMapInstance method of setMarkers is invoked.  This is displayed below.

     
CompanyMapInstance.prototype.setMarkers = function () {
    for (var i in this.addresses) {
        var address = this.addresses[i].address;
        this.googleGeoCodeInstance.geocode({ 'address': address }, this.callBackGeoCode);
    }
};

So for each address the Google geocode method is invoked to find the lat/long.  The CompanyMapInstance method "callBackGeoCode" is registered as the call back method when the address is found.  So now you might have guessed why the _.bindAll is necessary.  This allows the callBackGeoCode method to access the addresses array stored in the CompanyMapInstance object that invoked the Google geocode method.  This allows, once the correct MarkerAddress has been found, to pull the description which is then set to the marker object, assign the marker object to the MarkerAddress instance, and store off the results geocode method.  So the callBackGeoCode method is defined below.

     
CompanyMapInstance.prototype.callBackGeoCode = function (results, status) {

    var captionName = "A circuit point";

    if (status == google.maps.GeocoderStatus.OK) {
        if (status != google.maps.GeocoderStatus.ZERO_RESULTS) {

            // pull the results...
            var latLong = results[0].geometry.location;
            var geoCoderObjectResult = results[0];

            // center the map on the last circuit.
            this.googleMapInstance.setCenter(latLong);

            // place the marker on the map.
            var marker = new google.maps.Marker({
                position: latLong,
                map: this.googleMapInstance,
                title: captionName
            });

            // now find this address in the array of this.addresses
            var item = this.findAddress(geoCoderObjectResult);

            if (item>=0) {
                // found the address item...
                // save off the marker!
                this.addresses[item].marker = marker;
                // save off the geoCoderObjectResult!
                this.addresses[item].geoCodeResult = geoCoderObjectResult;

                marker.setTitle(this.addresses[item].description);

                if (this.addresses[item].drawLine) {
                    if (item > 0) { // make sure you aren't the first item in the list!
                        var priorAddress = this.addresses[item - 1];

                        if (priorAddress.geoCodeResult) {
                            // only try and draw that line IF you have a geoCodeResult!
                            var pathItem = [latLong, priorAddress.geoCodeResult.geometry.location];

                            if (priorAddress.geoCodeResult) {
                                this.addresses[item].drawnLine = new google.maps.Polyline({
                                    path: pathItem,
                                    geodesic: false,
                                    strokeColor: '#FF0000',
                                    strokeOpacity: 1.0,
                                    strokeWeight: 2,
                                    map: this.googleMapInstance
                                });
                            }
                        }
                    }
                }
            }
        }
    }
};

Mind you there's still plenty of code / testing that needs to place - however, the initial results were quite exciting. And they provided a great opportunity to flex my JavaScript skills.

No comments:

Post a Comment