pierrebalian
9/25/2017 - 5:44 PM

Google Maps API Integrations for Geolocation, Directions, & Redrawing Pins based on User Search

Google Maps API Integrations for Geolocation, Directions, & Redrawing Pins based on User Search

/**
 * Get HTML5 Geolocation values for Latitude, Longitude & State values
 */
var userLat = bridgeChildGetCookie( 'bridgeChildLatitude' ),
    userLng = bridgeChildGetCookie( 'bridgeChildLongitude' ),
    userState = bridgeChildGetCookie( 'bridgeChildState' );

/**
 * Get Retailers Object from localized variable
 */
var retailerPins = [];

/**
 * Map initialization function.
 */
var geocoder;
var map;
function initMap() {

    geocoder = new google.maps.Geocoder();

    var mapOptions = {
        center: {
            lat: parseFloat( userLat ),
            lng: parseFloat( userLng )
        },
        scrollwheel: false,
        mapTypeControl: false,
        streetViewControl: false,
        zoom: 7,
        styles: [
            {
                "featureType": "landscape.natural",
                "elementType": "geometry.fill",
                "stylers": [
                    {
                        "visibility": "on"
                    },
                    {
                        "color": "#e0efef"
                    }
                ]
            },
            {
                "featureType": "poi",
                "elementType": "geometry.fill",
                "stylers": [
                    {
                        "visibility": "on"
                    },
                    {
                        "hue": "#1900ff"
                    },
                    {
                        "color": "#c0e8e8"
                    }
                ]
            },
            {
                "featureType": "poi",
                "elementType": "labels.text",
                "stylers": [
                    {
                        "visibility": "off"
                    }
                ]
            },
            {
                "featureType": "poi",
                "elementType": "labels.icon",
                "stylers": [
                    {
                        "visibility": "off"
                    }
                ]
            },
            {
                "featureType": "road",
                "elementType": "geometry",
                "stylers": [
                    {
                        "lightness": 100
                    },
                    {
                        "visibility": "simplified"
                    }
                ]
            },
            {
                "featureType": "road",
                "elementType": "labels",
                "stylers": [
                    {
                        "visibility": "off"
                    }
                ]
            },
            {
                "featureType": "transit.line",
                "elementType": "geometry",
                "stylers": [
                    {
                        "visibility": "on"
                    },
                    {
                        "lightness": 700
                    }
                ]
            },
            {
                "featureType": "transit.line",
                "elementType": "labels.text",
                "stylers": [
                    {
                        "visibility": "off"
                    }
                ]
            },
            {
                "featureType": "transit.line",
                "elementType": "labels.text.fill",
                "stylers": [
                    {
                        "visibility": "off"
                    }
                ]
            },
            {
                "featureType": "transit.line",
                "elementType": "labels.text.stroke",
                "stylers": [
                    {
                        "visibility": "off"
                    }
                ]
            },
            {
                "featureType": "transit.line",
                "elementType": "labels.icon",
                "stylers": [
                    {
                        "visibility": "off"
                    }
                ]
            },
            {
                "featureType": "transit.station",
                "elementType": "labels.text",
                "stylers": [
                    {
                        "visibility": "off"
                    }
                ]
            },
            {
                "featureType": "transit.station",
                "elementType": "labels.icon",
                "stylers": [
                    {
                        "visibility": "off"
                    }
                ]
            },
            {
                "featureType": "water",
                "elementType": "all",
                "stylers": [
                    {
                        "color": "#7dcdcd"
                    }
                ]
            }
        ]
    };

    map = new google.maps.Map(document.getElementById('map-container'), mapOptions);

    /**
     * Set the map marker pins if we have retailerPins available.
     */
    if ( typeof retailerPins !== 'undefined' ) {
        setMarkers(map);
    }

    directionsDisplay = new google.maps.DirectionsRenderer();
    directionsDisplay.setMap(map);

    /**
     * Add an event listener for geocode search if submitGeocode element is present.
     */
    if( document.getElementById( 'submitGeocode' ) ) {
        document.getElementById('submitGeocode').addEventListener('click', function() {
            geocodeAddress();
        });
    }

}

/**
 * Set markers on the map function.
 *
 * @param map
 * @param retailerList
 */
function setMarkers(map, retailerList) {

    if ( typeof retailerList === 'undefined' ) {
        retailerList = bridgeChildGeoVars['bridgeChildRetailers'];
    }

    /**
     * URI of the active theme.
     */
    var iconURLPrefix = bridgeChildGeoVars['bridgeChildThemeURI'];

    var icons = {
        'bedbreakfastlodging': iconURLPrefix + '/assets/images/icon-lodging.png',
        'restaurantcoffee-shopbakery': iconURLPrefix + '/assets/images/icon-coffee.png',
        'other-retailer': iconURLPrefix + '/assets/images/icon-retailer.png',
        'college-bookstoreeducational': iconURLPrefix + '/assets/images/icon-education.png',
        'golf-courses': iconURLPrefix + '/assets/images/icon-golf.png',
        'military': iconURLPrefix + '/assets/images/icon-military.png',
        'museum': iconURLPrefix + '/assets/images/icon-museum.png',
        'state-or-national-parks': iconURLPrefix + '/assets/images/icon-park.png'
    };

    jQuery('.retailer-list-container').html('');

    for (var i = 0; i < retailerList.length; i++) {

        var retailer = retailerList[i];

        var retailerSlug;

        if ( typeof retailer.industry !== 'boolean' ) {
            retailerSlug = retailer.industry[0]['slug'];
        } else {
            retailerSlug = 'other-retailer';
        }

        // Add Markers to Map
        var marker = new google.maps.Marker({
            position: {
                lat: parseFloat(retailer['lat']),
                lng: parseFloat(retailer['lng'])
            },
            map: map,
            title: retailer['post_title'],
            content: retailer['address']['address'],
            lat: retailer['lat'],
            lng: retailer['lng'],
            address: retailer['a_address'],
            city: retailer['a_city'],
            state: retailer['a_state'],
            icon: icons[retailerSlug]
        });

        retailerPins.push(marker);

        var infowindow = new google.maps.InfoWindow();

        google.maps.event.addListener(marker, 'click', function() {

            infowindow.setContent(
                '<div class="map-info-wrap">' +
                    '<div class="map-info-title">' + this.title + '</div>' +
                    '<div class="map-info-content">' + this.content + '</div>' +
                    '<div class="map-links">' +
                        '<a data-address="' + this.address + ', ' + this.city + ', ' + this.state + '" onclick="setDirectionEnd(this);">Directions To</a>' +
                        '<a data-lat="' + this.lat + '" data-lng="' + this.lng + '" onclick="zoomToPin(this)">Zoom</a>' +
                    '</div>' +
                '</div>'
            );

            infowindow.open(map, this);

        });

        (function($){

            var listContainer = $('.retailer-list-container');

            listContainer.append(
                '<div class="retailer">' +
                    '<div class="retailer-icon"><img src="' + icons[retailerSlug] + '"/></div>' +
                    '<div class="retailer-title">' + retailer.post_title + '</div>' +
                    '<div class="retailer-location">' + retailer.a_city + ', ' + retailer.a_state + '</div>' +
                    '<a href="' + retailer.a_website + '">Website</a>' +
                '</div>'
            );

        })(jQuery);


    }

}


/**
 *  Set map boundaries to contain all pins
 */
function setMapBoundaries() {
    var bounds = new google.maps.LatLngBounds();
    for (var i = 0; i < retailerPins.length; i++) {
        bounds.extend(retailerPins[i].getPosition());
    }
    map.fitBounds(bounds);
    if ( retailerPins.length <= 1 ) {
        var listener = google.maps.event.addListener(map, 'idle', function(){
            if (map.getZoom() > 11 ) {
                map.setZoom(11);
            }
            google.maps.event.removeListener(listener);
        });
    }
}


/**
 * Function to clear all the markers on the map
 */
function clearMarkers() {

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

}


/**
 * Retrieve retailers by the "state" taxonomy.
 *
 * @param state
 */
function getRetailersByState( state ) {

    var get_retailers_by_state = {
        "action" :   "get_retailers_by_state",
        "state" :    state
    };

    return get_retailers_by_state;

}

/**
 * Retrieve retailers by the "industry" taxonomy.
 *
 * @param industry
 */
function getRetailersByIndustry( industry ) {

    var get_retailers_by_industry = {
        "action" :   "get_retailers_by_industry",
        "industry" : industry
    };

    return get_retailers_by_industry;

}

/**
 * Retrieve retailers by both the "industry" and "state" taxonomy.
 *
 * @param {string} industry
 * @param {string} state
 */
function getRetailersByIndustryAndState( industry, state ) {

    var get_retailers_by_industry_and_state = {
        "action" :   "get_retailers_by_industry_and_state",
        "industry" : industry,
        "state" :    state
    };

    return get_retailers_by_industry_and_state;

}

/**
 * Get initial set of pins based on users location
 */
function getInitialLocationPins() {

    var userStateCode = userState.toLowerCase();

    getRetailersByState( userStateCode );

}

/**
 * Geocode address for dealer search function.
 */
var stateSearch,
    stateSearchValue,
    industrySearch,
    industrySearchValue,
    searchRadius,
    radiusCircle,
    mileRadius;

function geocodeAddress() {

    stateSearch = document.getElementById('search-by-state');
    stateSearchValue = stateSearch[stateSearch.selectedIndex].value;
    industrySearch = document.getElementById('search-by-industry');
    industrySearchValue = industrySearch[industrySearch.selectedIndex].value;
    searchRadius = document.getElementById('search-radius').value;
    
    var searchParameters;

    if ( stateSearchValue !== 'all' && industrySearchValue !== 'all' ) {

        searchParameters = getRetailersByIndustryAndState( industrySearchValue, stateSearchValue );

    } else if ( stateSearchValue !== 'all' && industrySearchValue === 'all' ) {

        searchParameters = getRetailersByState( stateSearchValue );

    } else if ( industrySearchValue !== 'all' && stateSearchValue === 'all' ) {

        searchParameters = getRetailersByIndustry( industrySearchValue );

    }
    
    jQuery.ajax(
        {
            url : bridgeChildRetailerEndpoint + JSON.stringify( searchParameters ),
            beforeSend: function() {
                // loading gif while search function is being run
                jQuery('#loading-animation').fadeIn();
            },
            complete: function() {
                // stop the loading gif
                jQuery('#loading-animation').fadeOut();
            },
            type : 'GET'
        }
    )
        .done(
            function( data, textStatus, jqXHR ) {

                console.log( "HTTP Request Succeded: " + jqXHR.status );
                console.log( data );

                if ( data.success === true ) {
                    clearMarkers();
                    var retailerList = data['data'];
                    setMarkers(map, retailerList);
                    setMapBoundaries();
                    jQuery('.error-alert').fadeOut();
                } else {
                    jQuery('#search-error-alert').fadeIn();
                }

            }
        )
        .fail(
            function( jqXHR, textStatus, errorThrown ) {
                console.log( "HTTP Request Failed" );
            }
        );

    /**
     * Geocode map on search function
     */
    geocoder.geocode( {'address': stateSearchValue}, function(results, status) {

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

            clearMarkers();

            getRetailersByState( stateSearchValue );

            radiusCircle = new google.maps.Circle();

            if ( searchRadius === '' || searchRadius === null || searchRadius === undefined || searchRadius === 0 ) {
                // If search radius isn't set, it defaults to 200 miles
                mileRadius = 200 * 1069;
            } else {
                mileRadius = searchRadius * 1069;
            }

            radiusCircle.setRadius(mileRadius);
            radiusCircle.setCenter(results[0].geometry.location);
            map.setCenter(results[0].geometry.location);
            map.fitBounds(radiusCircle.getBounds());

        } else {
            alert( status );
        }

    });

}

/**
 * Set end address as destination in directions form
 *
 * @param address
 */
function setDirectionEnd( address ) {

    var endAddress = jQuery(address).attr('data-address'),
        endValue = jQuery('#endDirection');

    endValue.attr('value', endAddress);
    jQuery('#directions-form').fadeIn();

}

/**
 * Zoom to Pin
 *
 * @param location
 */
function zoomToPin( location ) {

    var pinLat = jQuery(location).attr('data-lat'),
        pinLng = jQuery(location).attr('data-lng');

    console.log('Lat: ' + pinLat + ' Lng: ' + pinLng);

    map.setZoom(15);
    map.setCenter({lat: parseFloat(pinLat), lng: parseFloat(pinLng) });

}

/**
 * Set active Travel Mode for calculating directions
 */
(function($){

    $('#directions-form .select-travel-mode').click(function(e) {

        e.preventDefault();
        $(this).addClass('active');
        $(this).siblings().removeClass('active');

        var directionsList = $('#directions-list-container');
        var clickedItem = jQuery(this).attr('id');
        directionsList.find('#' + clickedItem).addClass('active');
        directionsList.find('#' + clickedItem).siblings().removeClass('active');

    });

    $('#directions-list-container .select-travel-mode').click(function(e) {

        e.preventDefault();
        $(this).addClass('active');
        $(this).siblings().removeClass('active');

        var directionsForm = $('#directions-form');
        var clickedItem = jQuery(this).attr('id');
        directionsForm.find('#' + clickedItem).addClass('active');
        directionsForm.find('#' + clickedItem).siblings().removeClass('active');

        calcRoute();

    });

})(jQuery);

/**
 * Directions initialization function.
 */
var directionsDisplay = new google.maps.DirectionsRenderer();
var directionsService = new google.maps.DirectionsService();

function calcRoute() {

    // Get user selected travel mode
    var activeMode = jQuery('#travel-modes').find('.active').attr('id');

    var selectedTravelMode;

    if ( activeMode === 'travel-mode-transit' ) {
        selectedTravelMode = 'TRANSIT';
    }
    else if ( activeMode === 'travel-mode-walking' ) {
        selectedTravelMode = 'WALKING';
    }
    else if ( activeMode === 'travel-mode-bicycle' ) {
        selectedTravelMode = 'BICYCLING';
    }
    else {
        selectedTravelMode = 'DRIVING';
    }

    var start = document.getElementById('startDirection').value;
    var end = document.getElementById('endDirection').value;
    var request = {
        origin: start,
        destination: end,
        provideRouteAlternatives: true,
        travelMode: selectedTravelMode
    };

    directionsService.route(request, function(result, status) {

        if( status == 'OK' ) {

            jQuery('#directions-list-container').fadeIn();
            clearMarkers();

            directionsDisplay.setDirections(result);
            
            jQuery('.error-alert').fadeOut();

        } else {

            alert('Directions Request Error ' + status);
            
            jQuery('#directions-error-alert').fadeIn();

        }

    });
    directionsDisplay.setPanel(document.getElementById('directions-list'));
}

/**
 * Close Directions Panel, Clear Directions Route, & Redraw Pins
 */
function closeDirections() {

    jQuery('#directions-form').fadeOut();
    jQuery('#directions-list-container').fadeOut();

    directionsDisplay.setDirections({routes: []});

    map.setZoom(7);
    map.setCenter({
        lat: parseFloat( userLat ),
        lng: parseFloat( userLng )
    });
    
    jQuery('.error-alert').fadeOut();

    setMarkers(map);

}

/**
 * If the map-canvas element exists, initialize the map.
 */
if( document.getElementById( 'map-container' ) ) {
    google.maps.event.addDomListener(window, 'load', initMap);
}
/**
 * JS and functions related to geolocation and retailer map search.
 *
 * This file is intended to be our own geo init, will likely use Geolocator.js V2 to provide user coords.
 * There are 2 functions, bridgeChildSetCookie, and bridgeChildGetCookie, for setting a cookie for the location coords.
 * If these functions are not flexible enough, consider using a library like https://github.com/js-cookie/js-cookie.
 * If you have an array or object that you need to store in the cookie (like lat/lng coords) JSON encode before setting.
 * If the value for the cookie contains disallowed chars, URI encode before setting.
 *
 * @see bridgeChildSetCookie
 *
 * @link https://github.com/onury/geolocator
 * @link https://onury.github.io/geolocator/?api=geolocator
 */

/**
 * The endpoint for geolocation and retailer map search.
 *
 * Note: bridgeChildGeoVars.bridgeChildGeoEndPoint is provided by WordPress PHP as a variable on the page,
 * do not hardcode, as it could change.
 *
 * API endpoint allowed action parameters documented in bridge_child_geo_endpoint.
 * Required inputs for each action available by inspecting the function signatures of the functions in the case statements of bridge_child_geo_endpoint.
 *
 * Example usage :
 * 	   var get_retailers_by_state = {
 * 	       "action" :   "get_retailers_by_state",
 * 	       "state" :    "mn"
 * 	   };
 *     jQuery.ajax(
 * 	       {
 * 	           url :  bridgeChildRetailerEndpoint + JSON.stringify( get_retailers_by_state ),
 * 	           type : 'GET'
 * 	       }
 * 	   )
 * 	   .done(
 * 	       function ( data, textStatus, jqXHR ) {
 * 	           console.log( "HTTP Request Succeeded: " + jqXHR.status );
 * 	           console.log( data );
 * 	       }
 * 	   )
 * 	   .fail(
 * 	       function ( jqXHR, textStatus, errorThrown ) {
 * 	           console.log( "HTTP Request Failed" );
 * 	       }
 * 	   );
 *
 * @type {string}
 */
var bridgeChildRetailerEndpoint = bridgeChildGeoVars.bridgeChildGeoEndPoint;

/**
 * The Google API Key to use.
 *
 * Note: bridgeChildGeoVars.bridgeChildGoogleKey is provided by WordPress PHP as a variable on the page,
 * do not hardcode, as it could change.
 *
 * @type {string}
 */
var bridgeChildGoogleAPIKey = bridgeChildGeoVars.bridgeChildGoogleKey;

/**
 * Configuration object for Geolocator.
 */
geolocator.config(
    {
        "language" : "en",
        "google" : {
            "version" : "3",
            "key" : bridgeChildGoogleAPIKey
        }
    }
);

/**
 * Function(s) to run on window load.
 */
window.onload = function () {

    /**
     * Options to pass to geolocator.locate function.
     *
     * @link https://onury.github.io/geolocator/?api=geolocator#geolocator.locate
     *
     * @type {{enableHighAccuracy: boolean, timeout: number, maximumAge: number, desiredAccuracy: number, fallbackToIP: boolean, addressLookup: boolean, timezone: boolean}}
     */
    var geolocatorOptions = {
        enableHighAccuracy : false,
        timeout : 6000,
        maximumAge : 60000,
        desiredAccuracy : 1609,
        fallbackToIP : true,
        addressLookup : false,
        timezone : false
    };

    /**
     * Perform location lookup.
     * This can return an error if user does not allow HTML5 geolocation, and IP fallback fails.
     * If that happens, we may need a plan to deal with such a situation.
     * This is asynchronous, so plan accordingly.
     */
    geolocator.locate(
        geolocatorOptions,
        function ( locateError, location ) {
            if ( locateError ) {
                /**
                 * Error or no response. Use fallback behavior.
                 */
                return console.log( locateError );
            }
            /**
             * Success, we can glean lat/lng from this.
             * We can set a cookie so we don't need to perform the lookup on every page load.
             * We can pass the response along as args to PHP requests that require lat/lng data.
             * Reminder : Cookies can store maximum 4KB.
             */
            console.log( location );

            /**
             * Set cookie for users latitude value
             */
            bridgeChildSetCookie(
                'bridgeChildLatitude',
                encodeURI(
                    JSON.stringify(
                        location.coords.latitude
                    )
                ),
                1
            );

            /**
             * Set cookie for users longitude value
             */
            bridgeChildSetCookie(
                'bridgeChildLongitude',
                encodeURI(
                    JSON.stringify(
                        location.coords.longitude
                    )
                ),
                1
            );

            /**
             * Set cookie for users state code
             */
            bridgeChildSetCookie(
                'bridgeChildState',
                encodeURI(
                    location.address.stateCode
                ),
                1
            );

        }
    );
};

/**
 * Set a cookie.
 *
 * Example usage :
 *     bridgeChildSetCookie(
 *         'bridgeChildPosition',
 *             encodeURI(
 *                 JSON.stringify(
 *                     bridgeChildCoordinates
 *                 )
 *             ),
 *         1
 *     );
 *
 * @param {string} cookieName The name for the cookie.
 * @param {string} cookieValue The value for the cookie.
 * @param {number} cookieExpiration The expiration value for the cookie in days.
 */
function bridgeChildSetCookie( cookieName, cookieValue, cookieExpiration ) {

    /**
     * @type {Date}
     */
    var d;

    /**
     * @type {string}
     */
    var expires;

    d = new Date();

    d.setTime(
        d.getTime() + (
            cookieExpiration * 24 * 60 * 60 * 1000
        )
    );

    expires = 'expires=' + d.toUTCString();

    document.cookie = cookieName + '=' + cookieValue + '; ' + expires + '; path=/';
}

/**
 * Get the value of a cookie.
 *
 * Example usage:
 *     var positionCookie = bridgeChildGetCookie( 'bridgeChildPosition' );
 *
 * @param {string} cookieName The name of the cookie.
 *
 * @returns {string} The value of the cookie or empty string.
 */
function bridgeChildGetCookie( cookieName ) {

    /**
     * The name of the cookie we are looking for, appended with an equals character
     * because cookies are actually strings of key=value pairs.
     *
     * @type {string}
     */
    var name = cookieName + '=';

    /**
     * Array of cookie key=value strings.
     *
     * document.cookie is a string containing a semicolon-separated list of all cookies (i.e. key=value pairs).
     * We split that string into an array on the semicolon.
     *
     * @type {Array}
     */
    var cookieArray = document.cookie.split( ';' );

    /**
     * A loop counter.
     *
     * @type {number}
     */
    var counter = 0;

    /**
     * The current cookie string in the loop.
     *
     * @type {string}
     */
    var currentCookie = '';

    while ( counter < cookieArray.length ) {

        currentCookie = cookieArray[ counter ];

        while ( ' ' === currentCookie.charAt( 0 ) ) {
            currentCookie = currentCookie.substring( 1 );
        }

        if ( 0 === currentCookie.indexOf( name ) ) {
            return currentCookie.substring( name.length, currentCookie.length );
        }

        counter++;
    }

    /**
     * Cookie not found, empty, or mangled.
     */
    return '';
}