4
\$\begingroup\$

My Google Maps Script v2


I re-factored my code to be more OO.

Does it have any more room for improvement (naming, readability, oop, etc.)?

I'm importing Gmap.js in the head tags and the main.js just before the closing body tag of the HTML document.

Gmap.js

'use strict';

function Gmap(element, options) {
    if (!(typeof window.google === 'object' && typeof window.google.maps === 'object')) {
        throw Error('The Google Maps JavaScript API v3 library is required.');
    }

    this.googleMap = new google.maps.Map(element, options), //TODO: make private
    this.currentLocation = options.center,
    this.markers = []; //TODO: make private
}

Gmap.prototype = {
    addMarker: function (location, animateDrop, bounceOnClick) {
        animateDrop = (typeof animateDrop === 'undefined') ? true : animateDrop;
        bounceOnClick = (typeof bounceOnClick === 'undefined') ? true : bounceOnClick;

        var marker = new google.maps.Marker({
            map: this.googleMap,
            position: location
        });

        if (animateDrop) {
            marker.setAnimation(google.maps.Animation.DROP);
        }

        if (bounceOnClick) {
            google.maps.event.addListener(marker, 'click', function () {
                if (marker.getAnimation() !== null) {
                    marker.setAnimation(null);
                } else {
                    marker.setAnimation(google.maps.Animation.BOUNCE);
                }
            });
        }

        this.markers.push(marker);
    },

    deleteAllMarkers: function () {
        for (var i = 0; i < this.markers.length; i++) {
            this.markers[i].setMap(null);
        }
    },

    getCenter: function () {
        return this.googleMap.getCenter();
    },

    setCenter: function (latLng) {
        this.googleMap.setCenter(latLng);
    },

    fitBounds: function (latLngBounds) {
        this.googleMap.fitBounds(latLngBounds);
    },

    triggerEvent: function (event) {
        google.maps.event.trigger(this.googleMap, event);
    }
}

Gmap.geocode = function (geocodeRequest, callback) {
    var googleGeocoder = new google.maps.Geocoder();

    googleGeocoder.geocode(geocodeRequest, function (results, status) {
        callback(results, status);
    });
};

Gmap.geocodeStatus = {
    OK: google.maps.GeocoderStatus.OK,
    ZERO_RESULTS: google.maps.GeocoderStatus.ZERO_RESULTS,
    OVER_QUERY_LIMIT: google.maps.GeocoderStatus.OVER_QUERY_LIMIT,
    REQUEST_DENIED: google.maps.GeocoderStatus.REQUEST_DENIED,
    UNKNOWN_ERROR: google.maps.GeocoderStatus.UNKNOWN_ERROR
};

main.js

'use strict';

(function (window, document, Gmap) {
    var MAP_CANVAS_ID = 'map-canvas',
        ADDRESS_INPUT_ID = 'address-input',
        SEARCH_BUTTON_ID = 'search-button';

    var gMap,
        mapCanvas = document.getElementById(MAP_CANVAS_ID),
        addressInput = document.getElementById(ADDRESS_INPUT_ID);

    if (!mapCanvas.hasAttribute('data-default-address')) {
        throw new Error('The default address attribute must be present and not empty.');
    }

    if (!mapCanvas.getAttribute('data-default-address').trim()) {
        throw new Error('The default address attribute must not be empty.');
    }

    Gmap.geocode({ 'address': mapCanvas.getAttribute('data-default-address') }, function (results, status) {
        if (status !== Gmap.geocodeStatus.OK) {
            if (status === Gmap.geocodeStatus.ZERO_RESULTS) {
                throw new Error('The address could not be located.');
            }

            throw new Error('Geocode was unsuccessful: ' + status);
        }

        gMap = new Gmap(mapCanvas, {
            // required
            center: results[0].geometry.location,
            zoom: 10,
            // disable direct GUI interaction
            disableDefaultUI: true,
            navigationControl: false,
            mapTypeControl: false,
            scaleControl: false,
            scrollwheel: false,
            draggable: false,
            zoomControl: false,
            disableDoubleClickZoom: true,
            suppressInfoWindows: true
        });

        addressInput.value = results[0].formatted_address;

        gMap.addMarker(results[0].geometry.location);
    });

    // center map responsively
    window.addEventListener('resize', function () {
        var center = gMap.getCenter();

        gMap.triggerEvent('resize');
        gMap.setCenter(center);
    });

    addressInput.onkeydown = function (e) {
        if (e.keyCode === 13) {
            addressInput.blur();

            processAddressInput();
        }
    };

    document.getElementById(SEARCH_BUTTON_ID).onclick = function () {
        processAddressInput();
    };

    function processAddressInput() {
        Gmap.geocode({ 'address': addressInput.value }, function (results, status) {
            if (status !== Gmap.geocodeStatus.OK) {
                if (status === Gmap.geocodeStatus.ZERO_RESULTS) {
                    return;
                }

                throw new Error('Geocode was unsuccessful: ' + status);
            }

            if (results[0].geometry.location.equals(gMap.currentLocation)) {
                addressInput.value = results[0].formatted_address;

                return;
            }

            gMap.deleteAllMarkers();

            gMap.fitBounds(results[0].geometry.viewport);
            gMap.setCenter(results[0].geometry.location);
            gMap.addMarker(results[0].geometry.location);

            gMap.currentLocation = results[0].geometry.location;

            addressInput.value = results[0].formatted_address;
        });
    }
}(window, document, Gmap));
\$\endgroup\$
3
\$\begingroup\$

Some quick thoughts:

  1. Gmap constructor: I'd prefer semicolons instead of commas when you're setting properties on this.

  2. addMarker: I think moving animateDrop & bounceOnClick into an options object would be good, especially if you ever add additional options. In ES6, the syntax makes this all the more apparent:

    function addMarker(location, {animateDrop = true, bounceOnClick = true} = {}) { ... }
    
  3. typeof animateDrop === 'undefined' is fine, but you can also use animateDrop === undefined as there's no risk that animateDrop isn't declared.

  4. deleteMarkers: Cache the length:

    for (var i=0, l=this.markers.length; i<l; i++) { ... }
    
  5. Consider using promises for geocode.

  6. If you're just aliasing all the status constants, why not this:

    Gmap.geocodeStatus = google.maps.GeocoderStatus;
    
  7. 'address' as an object key doesn't need to be quoted.

\$\endgroup\$
  • \$\begingroup\$ Can you elaborate point 5? Promises? \$\endgroup\$ – Kid Diamond Mar 11 '15 at 20:47
  • \$\begingroup\$ Although the callback pattern works, it's easy to get into callback hell where callbacks are nested within callbacks, etc. Promises alleviate this by allowing you to write geocode({address:...}).then(function(results){}).catch(function(err){}); Come ES7, this becomes even nicer with the await keyword: let result = await geocode({address:....});. Granted; you're not in danger of callback hell yet, but thought I would bring it up, especially with the future direction of ES. \$\endgroup\$ – Kerri Shotts Mar 11 '15 at 20:59

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.