﻿/*  Anchorage Live 
 *  Google Maps/jQuery functionality
 *  This file powers the AnchorageLive Google map,
 *  including map navigation, custom overlays and parcel queries
 *  History
 *  Created:     January 2011    Cory Smith  corys@resdat.com
 */

// ****************************
// Configurable settings:
// ****************************

var bannerHeight = 60;                             // height of the page banner
var sideBarWidth = 475;                            // width of the sidebar with all the text
var searchBoxMaxWidth = 550;                       // maximum width of the search box
var searchBoxMinWidth = 100;                        // minimum width of the search box
var searchMinChar = 3;                             // minumum number of characters necessary to initiate a search
var searchDelay = 500;                             // milliseconds to wait after a keystroke to initiate a search
var searchTimout = 10000;                          // the amount of time (ms) the search will wait before throwing a timeout error
var resetBtnText = 'Reset';                        // text for the reset button on the map
var resetBtnToolTip = 'Go to full Anchorage view'; // tooltip for the reset button on the map
var exitStrtViewBtnText = 'Exit Street View';      // text for the reset button on the map
var clickParcelZoomLevel = 15;                          // zoom level to enable clickable parcels
var clickParksZoomLevel = 12;                          // zoom level to enable viewable parks
var viewParcelZoomLevel = 17;                      // the level to zoom to when viewing a parcel
var mapPrintNotice = "[NOTE: The map will only print properly when using FireFox or Internet Explorer 9 at this time.]";       // notice to users when printing from a browser other than FF
var zoomFailureMessage = "Could not zoom to this address.  It has been highlighted on the map and you can zoom in manually.";   // message to display when map is unable to geocode an address

// parcel styles
var backgroundParcelPolygonStyle = {
    fillColor: "#FFFFFF",
    fillOpacity: 0,
    strokeColor: "#999999",
    strokeOpacity: 0.7,
    strokeWeight: 2
};
var highlightParcelPolygonStyle = {
    fillColor: "#00FFFF",
    fillOpacity: 0.3,
    strokeColor: "#00FFFF",
    strokeOpacity: 0.9,
    strokeWeight: 2
};

// Map Initial Extent - the lat/lng boundaries of the area to show when the map loads
var nExtentLat = 61.2394;                          // The northern-most latitude
var sExtentLat = 61.0516;                          // The southern-most latitude
var eExtentLng = -149.538;                         // The eastern-most longitude
var wExtentLng = -150.136;                         // The western-most longitude
var extentZoom = 5;                                // initial zoom level

// Google Geocoder Extents - the lat/lng boundaries of the area that the geocoder should search for an address
var nSearchLat = 61.5;                             // The northern-most latitude
var sSearchLat = 60.75;                            // The southern-most latitude
var eSearchLng = -148.5;                           // The eastern-most longitude
var wSearchLng = -150.2;                           // The western-most longitude

// fusion tables
var ftParcel = 826339;                              // the id of the fusion table used for the parcel data
var fcParcelGeometry = 'geometry';                  // the table column that holds the polygon geospatial data
var fcParcelId = 'id';                              // the table column that holds the parcel identifier
var fcParcelCentroid = 'geometry_pos';                        // the table column that holds the lat/lng coordinates for the center of the parcel

// ****************************
// end configurable setting
// ****************************

// create page variables
var map;
var parcelBoundariesLayer;
var parcelSearchRequest;
var initialBounds = new google.maps.LatLngBounds(new google.maps.LatLng(sExtentLat, wExtentLng), new google.maps.LatLng(nExtentLat, eExtentLng));
var mapOptions = {
    zoom: extentZoom,
    center: new google.maps.LatLng(sExtentLat, wExtentLng), 
    mapTypeControlOptions: {
        mapTypeIds: [google.maps.MapTypeId.ROADMAP, google.maps.MapTypeId.SATELLITE, google.maps.MapTypeId.HYBRID]
    },
    mapTypeId: google.maps.MapTypeId.HYBRID,
    streetViewControl: true
};
var geocoder;
var swSearchLatlng = new google.maps.LatLng(sSearchLat, wSearchLng);
var neSearchLatlng = new google.maps.LatLng(nSearchLat, eSearchLng);
var searchBounds = new google.maps.LatLngBounds(swSearchLatlng, neSearchLatlng);
var parcelSearchRequest;        
var waitDialog;
var optOutDialog;
var currentLocation;
var currentParcelId;

// note tab height is not adjustable here.  this is a value that approximates the height set by jQuery
var tabHeight = 40;

// wire up the page on load
$().ready(function () {

    // resize divs based on window size
    adjustPageDivSizes();
    jQuery.event.add(window, "resize", adjustPageDivSizes);

    // create map
    map = new google.maps.Map(document.getElementById("divMap"), mapOptions);
    map.fitBounds(initialBounds);
    geocoder = new google.maps.Geocoder();

    // create the property lines layer
    parcelBoundariesLayer = new google.maps.FusionTablesLayer({
        suppressInfoWindows: true,
        query: {
            select: fcParcelGeometry,
            from: ftParcel
        }
    });

    // add map listeners for showing parcel boundaries layer
    google.maps.event.addListener(map, 'bounds_changed', function () {
        showparcelBoundaries();
    });
    google.maps.event.addListener(map, 'maptypeid_changed', function () {
        showparcelBoundaries();
    });

    // Create the div to hold the "Reset" control
    var resetControlDiv = document.createElement('DIV');
    // Call the ResetControl() constructor
    var resetControl = new ResetControl(resetControlDiv, map);
    // add reset control to the map
    map.controls[google.maps.ControlPosition.TOP_RIGHT].push(resetControlDiv);

    // add "Close streetview" button
    var closeStreetViewDiv = document.createElement('DIV');
    // Call the CloseStreetViewControl() constructor
    var closeStreetView = new CloseStreetViewControl(closeStreetViewDiv, map);
    // add closeStreetView to map
    map.getStreetView().controls[google.maps.ControlPosition.TOP_RIGHT].insertAt(0, closeStreetViewDiv);

    // add autocomplete to search box
    $("#txtSearch").autocomplete({
        source: function (request, response) {
            // if there are any outstanding search requests, do not process them because we have a new search
            if (parcelSearchRequest != null && parcelSearchRequest.status == null) {
                parcelSearchRequest.abort();
            }

            parcelSearchRequest = $.ajax({
                url: parcelSearchUrl,
                type: "POST",
                dataType: "text json",
                timeout: searchTimout,
                data: {
                    q: request.term
                },
                success: function (data, textStatus, jqXHR) {
                    response($.map(data, function (item) {
                        return {
                            label: item['SiteAddress'] + " - " + item['OwnerName'],
                            value: item['SiteAddress'] + " - " + item['OwnerName'],
                            key: item['MapKey'],
                            parcelId: item['ParcelId'],
                            address: item['SiteAddress']
                        };
                    }));
                },
                error: function (jqXHR, textStatus, errorThrown) {
                    // don't show error message if search was purposely aborted
                    if (textStatus != "abort") {
                        alert("Search failed: " + textStatus + ". Please reload this page and try your search again.");
                    }
                    // need to manually remove "loading" image
                    response(null);
                }
            });
        },
        selectFirst: true,    // may be able to switch to built-in autoFocus rather than this plug-in if deleted character bug is fixed in a future release
        minLength: searchMinChar,
        delay: searchDelay,
        select: function (event, ui) {
            goToSearchedParcel(ui.item);
        },
        open: function () {
            $(this).removeClass("ui-corner-all").addClass("ui-corner-top");
            $("ul.ui-autocomplete").children("li.ui-menu-item:odd").addClass("ui-menu-item-alternate");
        },
        close: function () {
            $(this).removeClass("ui-corner-top").addClass("ui-corner-all");
        }
    });

    // create the "Please Wait" animated dialog
    waitDialog = $("#wait").dialog({
        minWidth: 132,
        maxWidth: 135,
        width: 135,
        autoOpen: false,
        closeOnEscape: false,
        resizable: false,
        modal: true,
        open: function (event, ui) {
            $(".ui-dialog-titlebar").hide();
        },
        close: function (event, ui) {
            $(".ui-dialog-titlebar").show();
        }
    });

    // create the Opt Out dialog
    optOutDialog = $("#optOut").dialog({
        minWidth: 400,
        maxWidth: 600,
        width: 600,
        autoOpen: false,
        closeOnEscape: true,
        resizable: true,
        modal: true
    });

    // create tabs for sidebar content
    $("#divTabs").tabs({
        fx: { opacity: 'toggle' },
        ajaxOptions: {
            error: function (xhr, status, index, anchor) {
                $(anchor.hash).html(
					    "Couldn't load this tab.");
            }
        }
    });
    // make the help tab visible
    $("#divTabs").tabs("select", 1);

    // stylize buttons
    $("button, input:submit, input:button").button();
});

/// resizes the map, search and sidebar divs based on window size
function adjustPageDivSizes() {
    $("#banner").css('height', bannerHeight);
    var windowHeight = $(window).height();
    var windowWidth = $(window).width();
    $("#divMap").css('height', windowHeight - bannerHeight);
    $("#divMap").css('width', windowWidth - sideBarWidth);
    $("#divSidebar").css('width', sideBarWidth);
    $(".ui-tabs-panel").css('height', windowHeight - bannerHeight - tabHeight);

    // get width of banner elements
    var lblSearchWidth = $("#lblSearch").width();
    var txtSearchWidth = $("#txtSearch").width();
    var imgLogoWidth = $("#imgLogo").width();
    var extraBannerSpace = windowWidth - (lblSearchWidth + txtSearchWidth + imgLogoWidth);
    //set search box to maximum possible width, including a small buffer
    searchBoxWidth = txtSearchWidth + extraBannerSpace - 75;
    //adjust based on max and min settings
    if (searchBoxWidth < searchBoxMinWidth) {
        searchBoxWidth = searchBoxMinWidth;
    }
    if (searchBoxWidth > searchBoxMaxWidth) {
        searchBoxWidth = searchBoxMaxWidth;
    }
    $("#txtSearch").width(searchBoxWidth);
}

// takes the parcel info returned by the search query and loads the map and parcel details
function goToSearchedParcel(parcel) {
    var getLocation;
    var getDetails;

    currentParcelId = parcel.key;

    try {
        // get a location from geospatial data
        getLocation = getParcelGeometry(parcel.key);

        // then go to it
        getLocation.success(function (data) {
            // get the parcel coordinates array from the fusion tables data object
            var parcelArray = data.table.rows[0];
            var latLngXML = parcelArray[2];
            var centroid = parseCentroid(latLngXML);
            setMap(centroid, viewParcelZoomLevel);
        });
        getLocation.error(function (jqXHR, textStatus, errorThrown) {
            // if something goes wrong, try to geocode the address instead
            goToAddress(parcel.address);
        });
    }
    catch (err) {
        // if something goes wrong, try to geocode the address instead
        goToAddress(parcel.address);
    }

    // show parcel details
    showParcelDetailsById(parcel.parcelId);

}

// takes the parcel info returned by a click event and loads the map and parcel details
function goToClickedParcel(ftEvent) {

    currentParcelId = ftEvent.row[fcParcelId].value;

    // get centroid of clicked parcel
    var centroid = parseCentroid(ftEvent.row[fcParcelCentroid].value);

    // recenter on clicked point
    setMap(centroid, null);

    // get address information to display in search bar
    getParcelsByMapKey(ftEvent.row[fcParcelId].value);

    // display info
    showParcelDetailsByMapKey(ftEvent.row[fcParcelId].value);

}

// get address information to display in search bar
function getParcelsByMapKey(mapKey) {
    $.ajax({
        url: parcelLocationUrl,
        type: "POST",
        dataType: "text json",
        data: {
            //maxRows: 1,
            mapKey: mapKey
        },
        success: function (data) {
            var txtSearchVal = "";
            // if we have more than one parcel at this address, only show address
            if (data.length > 1) {
                if (data[0]['SiteAddress']) {
                    txtSearchVal = data[0]['SiteAddress'];
                }
            }
            // if only one parcel, display name and address
            else if (data.length == 1) 
            {
                if (data[0]['SiteAddress'] && data[0]['OwnerName']) {
                    txtSearchVal = data[0]['SiteAddress'] + " - " + data[0]['OwnerName'];
                }
                else if (data[0]['SiteAddress']) {
                    txtSearchVal = data[0]['SiteAddress'];
                }
                else if (data[0]['OwnerName']) {
                    txtSearchVal = data[0]['OwnerName'];
                }
            }
            $("#txtSearch").val(txtSearchVal);
        },
        error: function (jqXHR, textStatus, errorThrown) {
            waitDialog.dialog('close');
        }
    });

}

// get the parcel details for a single parcel
function showParcelDetailsById(parcelId) {
    waitDialog.dialog('open');

    var parcelIdStr = pad(parcelId, 8);
    return $.ajax({
        url: parcelDetailsUrl,
        dataType: "html",
        data: fcParcelId + "=" + parcelIdStr,
        success: function(html) {
            $("#divParcelDetails").html(html);
            $("#divTabs").tabs("select", "#divParcelDetails");
            waitDialog.dialog('close');
        },
        error: function (jqXHR, textStatus, errorThrown) {
            waitDialog.dialog('close');
            alert("Unable to retrieve details of this parcel: " + textStatus);
        }
    });
}

// get the parcel details for a map location
function showParcelDetailsByMapKey(mapKey) {
    waitDialog.dialog('open');

    return $.ajax({
        url: parcelDetailsUrl,
        dataType: "html",
        data: {
            mapKey: mapKey
        },
        success: function (html) {
            $("#divTabs").tabs("select", "#divParcelDetails");
            $("#divParcelDetails").html(html);
            // need to (re)set the height of the tabs panel in order to see scrollbar (?)
            adjustPageDivSizes();
            waitDialog.dialog('close');
        },
        error: function (jqXHR, textStatus, errorThrown) {
            waitDialog.dialog('close');
            alert("Unable to retrieve details of this parcel: " + textStatus);
        }
    });
}

// a google geocoding function to find a location based on an address (used as a backup)
function goToAddress(parcelAddress) {

    parcelAddress = jQuery.trim(parcelAddress);
    if (parcelAddress) {
        geocoder.geocode({ 'address': parcelAddress, 'bounds': searchBounds }, function (results, status) {
            if (status == google.maps.GeocoderStatus.OK) {
                if (results[0][fcParcelGeometry].location) {
                    setMap(results[0][fcParcelGeometry].location, viewParcelZoomLevel);
                }
            } else {
                map.fitBounds(initialBounds);
                alert(zoomFailureMessage);
            }
        });
    }
}

// parses a parcel's geometry to find a center lat/lng point the map can pan to
function getParcelGeometry(mapKey) {
        
    // for some reason this only works when parameters are specified here in the url, not in the ajax data attribute
    var ftUrl = "https://www.google.com/fusiontables/api/query?sql=SELECT+" + fcParcelId + "," + fcParcelGeometry + "," + fcParcelCentroid + "+FROM+" + ftParcel + "+WHERE+" + fcParcelId + "='" + mapKey + "'&jsonCallback=?";

    return $.ajax({
        type: 'GET',
        url: ftUrl,
        dataType: "json",
        data: {}
    });
}

function parseCentroid(geometryXML) {

    // for ie, the geometry xml string must have an xml doc type in order to be parsed correctly
    if ($.browser.msie) {
        var xmlDoc = new ActiveXObject("Microsoft.XMLDOM");
        xmlDoc.loadXML(geometryXML);
        geometryXML = xmlDoc;
    }

    var coordinates = $(geometryXML).find("coordinates").text();	
    var lngLatArray = coordinates.split(",");
    var thisLng = parseFloat(lngLatArray[0]);
    var thisLat = parseFloat(lngLatArray[1]);
    var centroid = new google.maps.LatLng(thisLat, thisLng);
    return centroid;
}


function setMap(point, zoom) {
    if (zoom != null) {
        map.setZoom(zoom);

        //// make sure entire parcel is visible
        //var parcelBounds = new google.maps.LatLngBounds(new google.maps.LatLng(minLat, minLng), new google.maps.LatLng(maxLat, maxLng));
        //var mapBounds = map.getBounds();
        ////mapBounds.union(parcelBounds);
        //map.fitBounds(mapBounds);
    }
    if (point != null) {
        // if point is within our maximum seach extents
        if (point.lng().between(wSearchLng, eSearchLng) && point.lat().between(sSearchLat, nSearchLat)) {

            // pan and zoom to location
            map.panTo(point);
            currentLocation = point;
        }
        // if not, show whole map
        else {
            map.fitBounds(initialBounds);
        }
    }
}

// determines whether or not to show the parcel boundaries layer based on zoom level and map type
function showparcelBoundaries() {
    var zoomLevel = map.getZoom();
    var backgroundStyle = {
        polygonOptions: backgroundParcelPolygonStyle
    };
    var highlightStyle = { where: fcParcelId + " < 0" };   //default to not showing highlight boundaries

    // if there is a parcel to highlight
    if (currentParcelId != null)
    {
        highlightStyle = {
            where: fcParcelId + " = " + currentParcelId,
            polygonOptions: highlightParcelPolygonStyle
        };
    }

    // if we are zoomed in enough to show parcel boundaries
    if (zoomLevel > clickParcelZoomLevel) {

        parcelBoundariesLayer.setOptions({ styles: [backgroundStyle, highlightStyle] });
        google.maps.event.addListener(parcelBoundariesLayer, 'click', goToClickedParcel);

        // if parcel layer hasn't been set yet, set it
        if (parcelBoundariesLayer.getMap() != map) {
            parcelBoundariesLayer.setMap(map);
        }
    }
    // remove parcel boundary layer
    else {
        parcelBoundariesLayer.setMap();
    }
}

// pads a number to a given length with leading zeros
function pad(number, length) {

    var str = '' + number;
    while (str.length < length) {
        str = '0' + str;
    }

    return str;

}

// post-submit callback 
function showResponse(responseText, statusText, xhr, $form) {
    // for normal html responses, the first argument to the success callback 
    // is the XMLHttpRequest object's responseText property 

    // if the ajaxForm method was passed an Options Object with the dataType 
    // property set to 'xml' then the first argument to the success callback 
    // is the XMLHttpRequest object's responseXML property 

    // if the ajaxForm method was passed an Options Object with the dataType 
    // property set to 'json' then the first argument to the success callback 
    // is the json data object returned by the server 

    optOutDialog.dialog('open');
}

// creates the reset control to show on the map
function ResetControl(controlDiv, map) {

    $(controlDiv).addClass('divResetMapControl');

    var controlUI = document.createElement('DIV');
    $(controlUI).addClass('divResetMapBorder');
    controlUI.title = resetBtnToolTip;
    controlDiv.appendChild(controlUI);

    var controlText = document.createElement('DIV');
    $(controlText).addClass('divResetMapText');
    controlText.innerHTML = resetBtnText;
    controlUI.appendChild(controlText);

    // Setup the click event listeners
    google.maps.event.addDomListener(controlUI, 'click', function () {
        map.fitBounds(initialBounds);
        map.getStreetView().setVisible(false);
    });
}

function CloseStreetViewControl(controlDiv, map) {

    $(controlDiv).addClass('divCloseStreetViewControl');

    var controlUI = document.createElement('DIV');
    $(controlUI).addClass('divCloseStreetViewBorder');
    controlDiv.appendChild(controlUI);

    var controlText = document.createElement('DIV');
    $(controlText).addClass('divCloseStreetViewText');
    controlText.innerHTML = exitStrtViewBtnText;
    controlUI.appendChild(controlText);

    // Setup the click event listeners: simply set the map to Chicago
    google.maps.event.addDomListener(controlUI, 'click', function () {
        map.getStreetView().setVisible(false);
    });
}

// prototype to check if a number is between two values
Number.prototype.between = function (first, last) {
    return (first < last ? this >= first && this <= last : this >= last && this <= first);
}

