6
\$\begingroup\$

I'm trying to learn JavaScript, and as a practice tool I recently wrote a simple web app that uses the Google Maps API Geocoder to get the latitude and longitude of a location (user input), manipulate them, then query those against a Google Fusion Table listing the world's climate zone codes according to the Köppen-Geiger climate classification. Basically people can type in a location and find out what climate zone it is, and see a map with the zones overlaid if they want it (might be of interest to geography buffs).

All of the logic is in a JavaScript file copied below. It was my goal to write as much of it without jQuery as possible, so I only use jQuery to handle the Fusion Tables request and to add/remove classes.

I've run it through JShint, and fixed most of the things mentioned there. I had the hardest time handling the list of alternative locations that Google returns, and did it with some funky global variables, a function within a for loop, and some other certainly coding practices. Those are changeMapLocation() and buildAltLocationList().

Everything works like I want it to, but this is a learning project I'd appreciate any suggestions for refactoring, design factors, maintainability, etc.

/* Google Address to coordinates code from:
http://krasimirtsonev.com/blog/article/GoogleMaps-JS-API-address-to-coordinates-transformation-text-to-LatLng
*/
(function () { // Begin scoping function
    "use strict";
    var map = null;
    var returnedLocation;
    var altLocationsList;
    var isAlt = false;
    var markersArray = [];


    /*Initializing the map and input elements*/

    window.onload = function () {
        // initialize the map and set options
        var mapHolder = document.getElementById("map-holder");
        map = new google.maps.Map(
            mapHolder,
            {
                zoom: 3,
                mapTypeId: google.maps.MapTypeId.ROADMAP, 
                zoomControl: true,
                zoomControlOptions: {
                    style: google.maps.ZoomControlStyle.SMALL,
                    position: google.maps.ControlPosition.RIGHT_TOP
                },
                panControl: true,
                panControlOptions: {
                    position: google.maps.ControlPosition.TOP_RIGHT
                },
                streetViewControl: false
            }
        );

        // centering the map
        map.setCenter(new google.maps.LatLng(20.2, 0.1));

        // Add event listeners

        //Search field - button click

        document.getElementById("search").onclick = function () {
            var address = document.getElementById("searchtext").value;
            addressToLocation(address, changeMapLocation);
            isAlt = false;
            clearResultsLaunchSpinner();
        };

        //Search field - hit enter

         document.getElementById("searchtext").onkeydown = function (e) {
            if (e.keyCode === 13) {
                var address = document.getElementById("searchtext").value;
                addressToLocation(address, changeMapLocation);
                isAlt = false;
                clearResultsLaunchSpinner();
            }
        };

        document.getElementById("layer-toggle").onclick = function () {
            toggleLayer(0);
        };
    };


    function clearResultsLaunchSpinner () {
        $(document.body).removeClass("showing-results");
        document.getElementById("locationName").innerHTML="";
        document.getElementById("zoneDescription").innerHTML="";
        document.getElementById("zoneOutput").innerHTML="";
        document.getElementById("numOfAlts").innerHTML="";
        document.getElementById("altIds").innerHTML="";
        clearOverlays();
        $(document.body).addClass("active");
    }

    function clearOverlays() {
        for (var i = 0; i < markersArray.length; i++ ) {
            markersArray[i].setMap(null);
        }
            markersArray = [];
    }

    /*Zooming to location and passing reults to climate zone query*/

    function changeMapLocation(locations) {
        clearResultsLaunchSpinner();

        // isAlt determine if this is inputed through the search field or clicked on in the list of alternates and called from buildAltLocationsList()

        if(locations && locations.length && isAlt === false) {


            //Set up a marker and pan map to our best location
            var marker = new google.maps.Marker({
                map: map,
                position: locations[0].location
            });
            markersArray.push(marker);

            returnedLocation = locations[0].text + "<br>(" + parseFloat(locations[0].location.k).toFixed(2) + ", " + parseFloat(locations[0].location.B).toFixed(2) + ")";
            map.panTo(locations[0].location);
            map.setZoom(6);
            //send lat/lng pair to rounder
            rounder([locations[0].location.k, locations[0].location.B]);  

            //Redfine global var so it can be used at the end of Climate Zone query to to call buildAltLocationsList()
            altLocationsList = locations;

        } 

        else if (isAlt === true) {
            marker = new google.maps.Marker({
                map: map,
                position: locations.location
            });
            markersArray.push(marker);

            returnedLocation = locations.text + "<br>(" + parseFloat(locations.location.k).toFixed(2) + ", " + parseFloat(locations.location.B).toFixed(2) + ")";
            map.panTo(locations);
            map.setZoom(6);
            //send lat/lng pair to rounder
            rounder([locations.location.k, locations.location.B]); 

        }
        else {
            describeClimatezone (null);
        }

    }

    function buildAltLocationsList (locations) {
        //Determine whether or not to include the "best result" in altLocationsList or not

        var arrIndex;
        if (isAlt === false ) {
            arrIndex = 1;
        }
        else {
            arrIndex = 0;
        }

        document.getElementById("altIds").innerHTML = "";

        if (locations.length > 1) {
            document.getElementById("numOfAlts").innerHTML = locations.length-arrIndex + " alternate locations found. Click to search:";

            for(var i=arrIndex; i<locations.length; i++) {  
                document.getElementById("altIds").innerHTML += "<li id = alt" + i + "><a>" + locations[i].text + "</a></li>";
            }

            //Separate for loop to build the click handler for each li
            for(var z=arrIndex; z<locations.length; z++) {  
                (function (z) {
                    document.getElementById("alt" + z).onclick = function () {
                        if (window.console.firebug !== undefined) {
                            console.log(locations[z]);
                        }
                        else {
                            isAlt = true;
                            changeMapLocation(locations[z]);
                        }
                    };
                })(z);
            } //End 2nd for loop
        }           
    }


    // Prepping data for Fusion Table Query. 
    // Everything has to be rounded to nearest .25 or .75 to match the data

    function rounder (x) {
        for (var i = 0; i < x.length; i++) {
        var xDecimal = (x[i] % 1);
            if (xDecimal >= 0 && xDecimal <= 0.5) {
                x[i] += (0.25 - xDecimal);
            }
             if (xDecimal > 0.5 && xDecimal < 1) {
                x[i] += (0.75 - xDecimal);
            }
            if (xDecimal >= -0.5 && xDecimal  < 0) {
                 x[i] += (-0.25 - xDecimal);
             }
            if (xDecimal > -1.00 && xDecimal < -0.5) {
                x[i] += (-0.75 - xDecimal);
           }
        }
       determineClimateZone(x[0], x[1]);
    }

    // Fusion Table Query of list of table of worldwide lat/lng pairs and their climate zone
    // https://www.google.com/fusiontables/DataSource?docid=1GQfBT-PXojUbIZP7_tkILYKNjHaQjYqop9gkosho

      function determineClimateZone(lat, lng) {
       var query =    "SELECT 'Cls' FROM " +
                       "1GQfBT-PXojUbIZP7_tkILYKNjHaQjYqop9gkosho" +
                       " WHERE 'Lat' = '" + lat + "' AND Lon = '" + lng + "'";
        var encodedQuery = encodeURIComponent(query);

        // Construct the URL
        var url = ["https://www.googleapis.com/fusiontables/v1/query"];
        url.push("?sql=" + encodedQuery);
        url.push("&key=AIzaSyAm9yWCV7JPCTHCJut8whOjARd7pwROFDQ");
        url.push("&callback=?");

        // Send the JSONP request using jQuery
        $.ajax({
          url: url.join(""),
          dataType: "jsonp",
          success: function (data) {
            describeClimatezone(data.rows[0]);
          }
        });
      }

    function describeClimatezone (climateZone) {
        var czArray = [
            ["Af","Tropical rainforest"],
            ["Am","Tropical monsoon"],
            ["Aw","Tropical wet and dry or savanna"],
            ["As","Tropical wet and dry or savanna ('summer' dry season)"],
            ["BWh","Subtropical desert"],
            ["BSh","Subtropical steppe"],
            ["BWk","Mid-latitude desert"],
            ["BSk","Mid-latitude steppe"],         
            ["Csa","Mediterranean, hot summer"], 
            ["Csb","Mediterranean, warm summer"],             
            ["Cfa","Humid subtropical, no dry season"],
            ["Cwa","Humid subtropical, dry winter"],
            ["Cwb","Temperate highland tropical climate with dry winters"],
            ["Cwc","Temperate highland tropical climate with dry winters"],
            ["Cfb","Marine west coast, warm summer"],
            ["Cfc","Marine west coast, cool summer"],
            ["Dfa","Humid continental, no dry season, hot summer"],
            ["Dfb","Humid continental, no dry season, warm summer"],
            ["Dwa","Humid continental, severe dry winter, hot summer"],
            ["Dwb","Humid continental, severe dry winter, warm summer"],
            ["Dsb","Humid continental, dry warm summer"],                 
            ["Dfc","Subartic, severe winter, no dry season, cool summer"], 
            ["Dfd","Subartic, severe very cold winter, no dry season, cool summer"],             
            ["Dwc","Subartic, dry winter, cool summer"],
            ["Dsc","Subartic, subalpine"],
            ["Dwd","Subartic, very cold and dry winter, cool summer"],
            ["ET","Tundra"],
            ["EF","Ice Cap"]
    ];
        var zoneDescriptionText;

        for (var i = 0; i < czArray.length; i++) {
            if (climateZone == czArray[i][0]) {
                zoneDescriptionText = czArray[i][1];
            }
            else if (climateZone === null) {
                zoneDescriptionText = "Location Not Found! Look at an atlas!";
            }
        }

        document.getElementById("locationName").innerHTML = "Showing Results for: <br>" + returnedLocation;
        document.getElementById("zoneOutput").innerHTML = document.getElementById("zoneOutput").innerHTML + climateZone;
        document.getElementById("zoneDescription").innerHTML = document.getElementById("zoneDescription").innerHTML + zoneDescriptionText;  

        //Change classes in DOM to hide spinner
        $(document.body).removeClass("active");
        $(document.body).addClass("showing-results");

        //Now that the climate zone has been returned, call function to build a list of alternate locations returned by Google's Geocode
        buildAltLocationsList(altLocationsList);

    }

    // converting the address's string to a google.maps.LatLng object
    function addressToLocation(address, callback) {
        var isAlt = false;
        var geocoder = new google.maps.Geocoder();
        geocoder.geocode(
            {
                address: address
            }, 
            function(results, status) {
                var resultLocations = [];

                if(status == google.maps.GeocoderStatus.OK) {
                    if(results) {
                        var numOfResults = results.length;
                        for(var i=0; i<numOfResults; i++) {
                            var result = results[i];
                            resultLocations.push(
                                {
                                    text:result.formatted_address,
                                    addressStr:result.formatted_address,
                                    location:result.geometry.location,
                                    lat:result.geometry.location.lat(),
                                    lng:result.geometry.location.lng(),
                                }
                            );
                        }
                    }
                } else if(status == google.maps.GeocoderStatus.ZERO_RESULTS) {
                    // address not found
                }

                if(resultLocations.length > 0) {
                    callback(resultLocations, false);
                } else {
                    callback(null);
                }
            }
        );
    }

    //define layer

    var layer = new google.maps.KmlLayer("https://sco-tt.github.io/What-s-My-Climate-Zone/Koeppen-Geiger-GE.kmz",
    {preserveViewport: true});

    //toggle kmz layer function

    function toggleLayer() {
        if(layer.getMap()===null) {
            layer.setMap(map);
            google.maps.event.trigger(map, "resize");
            $("#layer-toggle").addClass("pure-button-active");
        }
        else {
            layer.setMap(null);
            $("#layer-toggle").removeClass("pure-button-active");

        }
    }    
})();         // End scoping function
\$\endgroup\$
  • \$\begingroup\$ It returns Humid subtropical, no dry season for New York City. That sounds wrong. The climate for Belgium seems correct. \$\endgroup\$ – konijn Dec 8 '14 at 13:11
  • 2
    \$\begingroup\$ It does sound wrong, as the mid-Atlantic US doesn't seem subtropical, but that's how the classification system is set up. From Wikipedia: Although it is not in the subtropics and its winters are cold, New York has a humid subtropical climate according to the Köppen climate classification because the coldest months' average temperatures are not low enough for persistent snow cover, between 32.5 and 33.0 °F (0.3 and 0.6 °C) at all three reporting stations within the city. \$\endgroup\$ – sco-tt Dec 8 '14 at 15:06
3
\$\begingroup\$

From a once over:

  • I like 'use strict';
  • You have a few magic constants
    • zoom: 3, <- What does 3 mean ? Well named constants would help the reader
    • new google.maps.LatLng(20.2, 0.1) <- Curious..
  • Consider addEventListener or jQueryevent handling instead of assigning listener directly like here:
    document.getElementById("search").onclick <- This does not play nice
  • In my mind, if you use jQuery at all you should it for looking up elements. document.getElementById("search") <> $("#search")
  • Double newlines to separate code might be over the top, I would stick to single newlines
  • I would write the following with a ternary:

    var arrIndex;
    if (isAlt === false ) {
        arrIndex = 1;
    }
    else {
        arrIndex = 0;
    }
    

    could be

    var arrIndex = isAlt?0:1;
    
  • Your rounder calls determineClimateZone, that should not be the job of rounder
  • I would have called rounder roundCoordinates
  • I would play around with having 1 function to set locationName, zoneDescription instead of 2, you are now mixing UI and model in describeClimatezone
  • From a much higher level, if ever your lookup table changes from rounding to .25 and .75 (which is such a weird choice, so it seems likely to happen), then your code is broken. I would spend some time to analyze the fusion table and find the correct entry independent of rounding choices.
\$\endgroup\$
  • \$\begingroup\$ Thanks for the great tips! How do you recommend that I call determineClimateZone? \$\endgroup\$ – sco-tt Dec 16 '14 at 12:08

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.