My Google Maps Script v2

Posted on

Problem

My Google Maps Script v1


Using the Google Maps JavaScript API v3.

Does my script have any room for improvement (naming, readability, oop, etc.)?

I’m going to pair it with some live time functionality, so that whenever the map its location changes the time would be updated accordingly.

I’ve added this script to my HTML code right after the last DOM node it needs to work.

google-maps.js

(function (window, document) {
    'use strict';

    var MAP_DEFAULT_ADDRESS = 'Paris, France',
        MAP_CANVAS_ID = 'map-canvas',
        ADDRESS_INPUT_ID = 'address-input',
        SEARCH_BUTTON_ID = 'search-button';

    var googleMap,
        currentLocation,
        googleGeocoder = new google.maps.Geocoder(),
        addressInput = document.getElementById(ADDRESS_INPUT_ID),
        markers = [];

    googleGeocoder.geocode({ 'address': MAP_DEFAULT_ADDRESS }, function (results, status) {
        if (status !== google.maps.GeocoderStatus.OK) {
            throw new Error('Geocode was unsuccessful: ' + status);
        }

        googleMap = new google.maps.Map(document.getElementById(MAP_CANVAS_ID), {
            // 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
        });

        currentLocation = results[0].geometry.location;

        addressInput.value = results[0].formatted_address;

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

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

        google.maps.event.trigger(googleMap, 'resize');

        googleMap.setCenter(center);
    });

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

            processAddressInput();
        }
    };

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

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

        marker.setAnimation(google.maps.Animation.DROP);

        markers.push(marker);

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

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

    function processAddressInput() {
        googleGeocoder.geocode({ 'address': addressInput.value }, function (results, status) {
            if (status !== google.maps.GeocoderStatus.OK) {
                if (status === google.maps.GeocoderStatus.ZERO_RESULTS) {
                    return;
                }

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

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

                return;
            }

            deleteAllMarkers();

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

            currentLocation = results[0].geometry.location;

            addressInput.value = results[0].formatted_address;

            addMarker(results[0].geometry.location);
        });
    }
}(window, document));

Solution

On the surface, this looks really good. The only room for improvement I can see is to truly embrace object oriented programming here, and allow this code to be used multiple times in the same page. The code below isn’t a total rework, because it was mostly a copy and paste of the original code, but this gives you a LOT of flexibility and reusability:

/*jslint browser: true, plusplus: true, vars: true, white: true */

(function(google) {

'use strict';

function MyMap(canvas, options) {
    this.options = Object.create(MyMap.prototype.options);
    this.setCanvas(canvas);
    this.mergeOptions(options);
    this.geocoder = new google.maps.Geocoder();
    this.markers = [];
    this.handleWindowResize = this.handleWindowResize.bind(this);
    this.handleAddressKeydown = this.handleAddressKeydown.bind(this);
    this.handleSearchButtonClick = this.handleSearchButtonClick.bind(this);
}

MyMap.prototype = {

    addressInput: null,
    canvas: null,
    container: null,
    currentLocation: null,
    document: null,
    geocoder: null,
    map: null,
    markers: null,
    options: {
        addressSelector: "input[type=text]",
        defaultAddress: "Paris, France",
        searchButtonSelector: "button[type=button]"
    },
    searchButton: null,
    window: null,

    constructor: MyMap,

    init: function() {
        this.addressInput = this.container.querySelector(this.options.addressSelector);
        this.searchButton = this.container.querySelector(this.options.searchButtonSelector);

        this.map = new google.maps.Map(this.canvas, {
            // required
            center: new google.maps.LatLng(0, 0),
            zoom: 10,
            // disable direct GUI interaction
            disableDefaultUI: true,
            navigationControl: false,
            mapTypeControl: false,
            scaleControl: false,
            scrollwheel: false,
            draggable: false,
            zoomControl: false,
            disableDoubleClickZoom: true,
            suppressInfoWindows: true
        });

        this.window.addEventListener("resize", this.handleWindowResize);
        this.addressInput.addEventListener("keydown", this.handleAddressKeydown, false);
        this.searchButton.addressInput("click", this.handleSearchButtonClick, false);
        this.findAddress(this.options.defaultAddress);

        return this;
    },

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

        marker.setAnimation(google.maps.Animation.DROP);

        this.markers.push(marker);

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

    deleteAllMarkers: function() {
        var i = 0;

        for (i; i < this.markers.length; i++) {
            this.markers[i].setMap(null);
        }

        this.markers = [];
    },

    handleAddressKeydown: function(e) {
        if (e.keyCode === 13) {
            this.addressInput.blur();

            this.findAddress(this.addressInput.value);
        }
    },

    handleSearchButtonClick: function() {
        this.findAddress(this.addressInput.value);
    },

    handleWindowResize: function() {
        var center = this.map.getCenter();

        google.maps.event.trigger(this.map, 'resize');

        this.map.setCenter(center);
    },

    mergeOptions: function(overrides) {
        var key;

        for (key in overrides) {
            if (overrides.hasOwnProperty(key)) {
                this.options[key] = overrides[key];
            }
        }
    },

    findAddress: function(address) {
        this.geocoder.geocode({ address: address }, function (results, status) {
            if (status !== google.maps.GeocoderStatus.OK) {
                if (status === google.maps.GeocoderStatus.ZERO_RESULTS) {
                    return;
                }

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

            var firstResult = results[0];

            if (firstResult.geometry.location.equals(this.currentLocation)) {
                this.addressInput.value = firstResult.formatted_address;

                return;
            }

            this.deleteAllMarkers();

            this.map.fitBounds(firstResult.geometry.viewport);
            this.map.setCenter(firstResult.geometry.location);

            this.currentLocation = firstResult.geometry.location;

            this.addressInput.value = firstResult.formatted_address;

            this.addMarker(firstResult.geometry.location);
        }.bind(this));
    },

    setCanvas: function(canvas) {
        this.canvas = canvas;
        this.container = canvas.parentElement;
        this.document = canvas.ownerDocument;
        this.window = this.document.defaultView;
    }

};

MyMap.maps = [];

MyMap.createAll = function(container, canvasSelector) {
    var canvases = container.querySelectorAll(canvasSelector || "div.map-canvas"),
        options = null, i = 0, canvas = null, map;

    for (i; i < canvases.length; i++) {
        canvas = canvases[i];
        options = JSON.parse(canvas.getAttribute("data-map-options") || "{}");
        map = new MyMap(canvas, options).init();

        this.maps.push(map);
    }
};

})(this.google);

First, the MyMap class requires a map canvas to get started. From there we can get the document, window and a container element from which to find all other interesting elements, such as the address input, search button, etc. The second argument to the constructor allows you to configure each instance.

One notable change is adding a function called findAddress which takes an address to geocode. You have repetitive code that geocodes the address in the textbox as well as the default address. Moving the geocoding and processing of the results into its own method removes this repetition, requiring a little change to the initialization (centering the map at 0 lat, 0 lng).

Lastly, the MyMap.createAll(...) function takes any DOM node and finds all map canvases inside it, and creates maps for them assuming a default HTML structure:

<div class="map">
    <input type="text"> <button type="button">Search</button>
    <div class="map-canvas" data-map-options='{ "defaultAddress": "London, England" }'></div>
</div>

Now let’s pull this all together and create a bunch of maps in just a few lines of code, including a map configured they way your original code was configured:

<div class="map">
    <input type="text"> <button type="button">Search</button>
    <div class="map-canvas" data-map-options='{ "defaultAddress": "London, England" }'></div>
</div>

<div class="map">
    <input type="text"> <button type="button">Search</button>
    <div class="map-canvas" data-map-options='{ "defaultAddress": "Los Angeles, United States of America" }'></div>
</div>

<!-- Using the Ids from your original code -->
<div class="map">
    <input type="text" id="address-input">
    <button type="button" id="search-button">Search</button>
    <div id="map-canvas"></div>
</div>

<script type="text/javascript">
    MyMaps.createAll(document.body);

    var map = new MyMap(document.getElementById("map-canvas"), {
        addressSelector: "#address-input",
        searchButtonSelector: "#search-button"
    });
</script>

Why this is important

The MyMap class is (mostly) encapsulated. It reaches out to the global context for Google maps related stuff, but that is not much of break in encapsulation. Furthermore, you have the option of creating multiple maps. Why write code that can only be used once per page when it’s just as easy to make it modular? The MyMap.createAll(...) function provides a means to create and uniquely configure multiple maps on a page using data-* attributes and a single line of JavaScript: MyMap.createAll(document.body);

This is almost spotless, congratulations.

When processing the results, you refer to results[0] many times.
I would put that in a local variable to clarify, for example firstResult.
(An IDE can give a warning if you mistype firstResult,
but it cannot give a warning if you mistype the array index (for example results[2]))

You missed a semicolon at the end of this:

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

Leave a Reply

Your email address will not be published. Required fields are marked *