  //-------------------------------------------------------------------------
  // Constants
  //
  var G_MAP_ICONS_DIRECTORY = "/map/images/category-icons/";
  var G_MAP_ICONS_SHADOW = "shadow-icon.png";
  var G_UPLOADS_DIRECTORY = "/map/images/locations/";
  var G_DEFAULT_LAT = 39.329285;
  var G_DEFAULT_LNG = -82.10259;

  var G_MAP_ELEMENT = "googleMap";

  var G_UPDATE_MAP_CONTAINER = "updateMapWrapper";
  var G_UPDATE_MAP_BUTTON = "updateMap";

  var G_CAT_BUTTONS_FORM_ID = "catButtonsForm";
  var G_CAT_BUTTON_CONTAINER_CLASS = "catButtonContainer";
  var G_CAT_CHECKBOX_CLASS = "catCheckbox";
  var G_CAT_BUTTON_IMAGE_CLASS = "catButtonImage";
  var G_CAT_BUTTON_LABEL_CLASS = "catButtonLabel";

  var G_SUBCAT_CONTAINER_CLASS = "subcatContainer";

  var G_LOADING_PANEL_ID = "loading";
  var G_LOCATION_INFO_BAR_ID = "locationInfoBar"
  var G_DIRECTIONS_BAR_ID = "directionsBar";
  var G_DIRECTIONS_LINKS_ID = "directionsLinks";

  // Return codes for the directions
  var gDirCodes = [];
  gDirCodes[G_GEO_SUCCESS]            = "Success";
  gDirCodes[G_GEO_MISSING_ADDRESS]    = "Missing Address: The address was either missing or had no value.";
  gDirCodes[G_GEO_UNKNOWN_ADDRESS]    = "Unknown Address:  No corresponding geographic location could be found for the specified address.";
  gDirCodes[G_GEO_UNAVAILABLE_ADDRESS]= "Unavailable Address:  The geocode for the given address cannot be returned due to legal or contractual reasons.";
  gDirCodes[G_GEO_BAD_KEY]            = "Bad Key: The API key is either invalid or does not match the domain for which it was given";
  gDirCodes[G_GEO_TOO_MANY_QUERIES]   = "Too Many Queries: The daily geocoding quota for this site has been exceeded.";
  gDirCodes[G_GEO_SERVER_ERROR]       = "Server error: The geocoding request could not be successfully processed.";
  gDirCodes[G_GEO_BAD_REQUEST]        = "A directions request could not be successfully parsed.";
  gDirCodes[G_GEO_MISSING_QUERY]      = "No query was specified in the input.";
  gDirCodes[G_GEO_UNKNOWN_DIRECTIONS] = "The gDirectionsections object could not compute directions between the points.";

  //-------------------------------------------------------------------------
  // Globals
  //-------------------------------------------------------------------------

  var gMap;
  var gCategories = new collection();
  var gMapLocations = null;
  var gDirections;
  var gLastZoom = null;
  var gLastCenter = null;

  var gLocationsRequest = null;
  var gLocationsJSON = {};

  //-------------------------------------------------------------------------
  // initializeMap() is the entire "bootstrap"
  //-------------------------------------------------------------------------

  initializeMap();

  //-------------------------------------------------------------------------
  // That's it... execution is done.  Just functions below here
  //-------------------------------------------------------------------------

  //-------------------------------------------------------------------------
  //
  function initializeMap() {

    // Make sure the browser supports what is needed for the Google Maps API
    if (!GBrowserIsCompatible()) {
      alert("Sorry, the Google Maps API is not compatible with this browser");
      return;
    }

    // AJAX request for all the category/subcategory information we need.
    //--!! Has the side effect of creating the UI elements in the left panel.  It's
    //--!! more efficient to do it there as we only have to loop through all the
    //--!! records once, but it's probably prematurely optimized. We're not likely
    //--!! to be dealing with enough records to make an appreciable difference by
    //--!! looping through it all twice, so probably best to have getCategories()
    //--!! just be concerned with parsing the JSON from the server/DB and have a
    //--!! seperate function that creates the UI elements from the resulting
    //--!! collection (gCategories).
    getCategories();

    // Map API initialization
    gMap = new GMap2(document.getElementById(G_MAP_ELEMENT));
    gMap.setCenter(new GLatLng(G_DEFAULT_LAT, G_DEFAULT_LNG), 14, G_NORMAL_MAP);
    gMap.setUIToDefault();
    gDirections = new GDirections(gMap, document.getElementById(G_DIRECTIONS_BAR_ID));

    // catch Directions errors
    GEvent.addListener(gDirections, "error", function() {
      var statusCode = gDirections.getStatus().code;
      var reason = "Code " + statusCode;

      if (gDirCodes[statusCode]) {
        reason = gDirCodes[statusCode];
      }

      alert("Failed to obtain directions, " + reason);
    });

    // Initialize the UI
    setMapClass("largeMap");
    showInfoBar(false);
  }

  //-------------------------------------------------------------------------
  //
  function createIcon(iconPath) {
    newIcon = new GIcon(G_DEFAULT_ICON, iconPath);

    newIcon.iconSize = new GSize(32, 37);
    newIcon.shadow = G_MAP_ICONS_DIRECTORY + G_MAP_ICONS_SHADOW;
    newIcon.shadowSize = new GSize(51, 37);
    newIcon.iconAnchor = new GPoint(15, 36);
    newIcon.infoWindowAnchor = new GPoint(16, 2);
    newIcon.transparent = G_MAP_ICONS_DIRECTORY + "transparent.png";
    newIcon.imageMap = [0,0, 32,0, 32,37, 0,37];

    return newIcon;
  }

  //-------------------------------------------------------------------------
  // Create a marker and set up the event window
  //
  function createMarker(point, customIcon, markerTitle, objLocation) {
    var marker = new GMarker(point, {icon : customIcon, title : markerTitle } );

    /*
    GEvent.addListener(marker, "click", function() {
      marker.openInfoWindowHtml(objLocation.infoWindowText);
    });
    */

    GEvent.addListener(marker, "click", mapMarkerClick(objLocation));


    return marker;
  }

  //-------------------------------------------------------------------------
  //
  function getCategoryList() {
    var catList = "";

    // Build the category list
    for (var key in gCategories.items) {
      if (gCategories.items[key].active) {
        catList += gCategories.items[key].id + ",";
      }
    }
    // Remove the final trailing comma, if we have anything to begin with
    if (catList.length > 0) {
      catList = catList.substring(0, catList.length -1);
    }
    // No categories
    else {
      catList = null;
    }

    return catList;
  }

  /*
  //-------------------------------------------------------------------------
  //
  function getCategoryList() {
    var catList = "";
    var subcatList = "";

    // Build the category list
    for (var key in gCategories.items) {
      objThisCat = gCategories.items[key];

      if (objThisCat.active) {

        // Does this category have subcategories?
        if (objThisCat.subcategories.count() > 0) {

          // Run through the subcats
          for (var key in objThisCat.subcategories.items) {
            var objThisSubcat = objThisCat.subcategories.items[key];
            if (objThisSubcat.active) {
              subcatList += objThisSubcat.id + ","
            }
          }
        }

        // No subcategories, just add this category ID to the list
        else {
          catList += objThisCat.id + ",";
        }
      }
    }

    // Remove the final trailing comma, if we have anything to begin with
    if (catList.length > 0) {
      catList = catList.substring(0, catList.length -1);
    }
    // No categories
    else {
      catList = null;
    }

    if (subcatList.length > 0) {
      subcatList = subcatList.substring(0, subcatList.length -1);
    }
    else {
      subcatList = null;
    }

    return catList + "|" + subcatList;
  }
  */

  //-------------------------------------------------------------------------
  // Process our data in chunks using setTimeout() calls.  Javascript is
  // single-threaded and a long sequence of processing will block pretty
  // much EVERYTHING browser related including updating the animated gif.
  // Without a true sleep() function (CPU idle), we jump through hoops to get
  // some free cycles to update things and attempt to keep the browser
  // responsive. We do this by only processing a few map markers at a time
  // (the call to addOverlay() is the processing hog) and set a timeout to
  // process the next batch some milliseconds later.  This approach gives the
  // CPU a breath every so often.
  //
  function processLocations(startIdx) {
    var thisLocation = null;
    var thisCategory = null;
    var thisHtml = null;
    var lastLocId = null;

    for (var index = startIdx; index < gMapLocations.count() && index < startIdx + 3; index++) {

      thisLocation = gMapLocations.getByIndex(index);
      thisCategory = gCategories.getById(thisLocation.catId);

      // Expand the bounds to include this location and add the marker to the map
      gLocationsJSON.bounds.extend(thisLocation.mapPoint);
      gMap.addOverlay(thisLocation.mapMarker);

      // Put up the category headers when needed (info bar)
      if (gLocationsJSON.lastCatId != thisCategory.id) {
        gLocationsJSON.lastCatId = thisCategory.id;
        gLocationsJSON.locationBarHtml += '<div class="catHeading">' + thisCategory.name + '</div>';
      }
      else {
        // Drop in dividers whenever there isn't a category header directly above
        gLocationsJSON.locationBarHtml += '<hr class="locationInfoRule" />';
      }

      gLocationsJSON.locationBarHtml += getInfoHtml(thisLocation, false);
      thisLocation.infoWindowText = getInfoHtml(thisLocation, true);
      //thisLocation.infoWindowText += getDirectionsLink(thisLocation);
    }

    // More records to add still?
    if (index < gMapLocations.count()) {
      setTimeout(function () { processLocations(index); }, 0);
    }

    // All records added
    else {

      // Were there locations added to the map?
      if (gLocationsJSON.locationBarHtml != "") {
        setMapClass("smallMap");
        showInfoBar(true)

        // Expand the bounds just a smidge to ensure the full marker shows on the map
        var boundsStr = gLocationsJSON.bounds.toString().replace(/[\(\)\s]/g, "");
        boundAr = boundsStr.split(",")
        boundAr[0] = parseFloat(boundAr[0]) - 0.01;
        boundAr[2] = parseFloat(boundAr[2]) + 0.01;
        gLocationsJSON.bounds.extend(new GLatLng(boundAr[0], boundAr[1]));
        gLocationsJSON.bounds.extend(new GLatLng(boundAr[2], boundAr[3]));

        // Set the zoom level and center of the map based on the calculated bounds
        gMap.setZoom(gMap.getBoundsZoomLevel(gLocationsJSON.bounds));
        gMap.setCenter(gLocationsJSON.bounds.getCenter());
      }

      // No locations added to the map
      else {
        setMapClass("largeMap");
        showInfoBar(false);
        //gLocationsJSON.locationBarHtml = '<b>Choose one or more categories from the check-boxes on the left and click the "Update map" button</b>';
      }

      // put the assembled side_bar_html contents into the side_bar div
      document.getElementById(G_LOCATION_INFO_BAR_ID).innerHTML = gLocationsJSON.locationBarHtml;

      showLoadingPanel(false);
      enableUpdateButton(true);
    }
  }

  //-------------------------------------------------------------------------
  //
  function getInfoHtml(objLocation, isPopupWindow) {

    var thisHtml = '<div class="locationInfoDiv" id="locationInfoDiv' + objLocation.id + '">';

    if (objLocation.image && isPopupWindow) {
      thisHtml += '<img src="' + G_UPLOADS_DIRECTORY + objLocation.image + '" alt="" />';
    }

    // The link opens the pop-up window so we obviously don't need it on the pop-up itself
    if (isPopupWindow) {
      thisHtml += objLocation.name;
    }
    else {
      thisHtml +=
      '<a class="locationLink" href="javascript:locationLinkClick(' + objLocation.id + ')">' + objLocation.name + '</a>';
    }

    if (objLocation.subcatList) {
      thisHtml += '<br />' + '(' + objLocation.subcatList + ')' ;
    }

    thisHtml +=
      '<br />' +
      objLocation.address +
      '<br />' +
      objLocation.city + ", " + objLocation.state + " " + objLocation.zip +
      '<br />';

    if (objLocation.phone) {
      thisHtml += objLocation.phone + '<br />';
    }

    if (objLocation.desc) {
      thisHtml +=
          '<div class="locationDesc">'
        + objLocation.desc
        + '</div>';
    }

    if (objLocation.website) {
      thisHtml +=
        '<div class="locationWebLink">' +
        '  <a href="' + objLocation.website + '" target="_blank">Visit Website</a><br />' +
        '</div>';
    }

    // Directions form for the popupwindow only
    if (isPopupWindow) {
      thisHtml += ''
        + '<div style="margin-top: 10px;">'
        +   'Get directions from:<form action="javascript:getDirections()">'
        +   '<input type="text" size=40 maxlength=40 name="saddr" id="saddr" value="" /><br />'
        +   '<input value="Get Directions" type="submit"><br />'
        +   '<input type="hidden" id="daddr" value="' + objLocation.name + "@"+ objLocation.lat + ',' + objLocation.lng + '"/>'
        + '</div>'
      ;
    }

    // Close 'locationInfoDiv'
    thisHtml += "</div>";

    return thisHtml;
  }

  //-------------------------------------------------------------------------
  // AJAX
  //-------------------------------------------------------------------------

  //-------------------------------------------------------------------------
  // Currently synchonous, so category information should be initialized
  // when this function returns.
  //
  function getCategories() {
    var request = GXmlHttp.create();
    var objResponse = null;
    var objNewCat = null;

    request.open("GET", "get-categories.php?random=" + Math.random(), false);
    request.send(null);

    // AJAX callback function
    if (request.readyState == 4) {

      // Response text returned, evaluate the returned JSON information
      var objResponse = eval('(' + request.responseText + ')');

      // Loop through the categories
      for (var i = 0; i < objResponse.category.length; i++) {

        // Create a new category object, initialize it, add it to the collection,
        // and stash a reference to it
        objNewCat = gCategories.add(new objCategory(objResponse.category[i]));

        // Add an icon, checkbox, label, and the appropriate set of
        // subcategory UI elements for each category
        //--!! More efficient to do this here, but probably best to avoid
        //--!! doing UI stuff.  Consider calling from outside this function.
        addCategoryButton(objNewCat);
      }
    }

    // Make the form visible, now that all the controls have been loaded
    document.getElementById(G_CAT_BUTTONS_FORM_ID).style.display = "block";
  }


  /*
  //-------------------------------------------------------------------------
  // Make the asynchronous request for all the locations we need
  //
  function requestLocations() {
    var tempList = getCategoryList().split("|");
    var catList = tempList[0];
    var subcatList = tempList[1];

    gMapLocations = new collection();

    // AJAX request for category info
    gLocationsRequest = GXmlHttp.create();
    gLocationsRequest.open("GET",
      "get-locations.php?catlist=" + catList
      + "&subcatlist=" + subcatList
      + "&random=" + Math.random(), true);
    gLocationsRequest.onreadystatechange = locationsDataCallback;
    gLocationsRequest.send(null);
  }
  */


  //-------------------------------------------------------------------------
  // Make the asynchronous request for all the locations we need
  //
  function requestLocations(nameFilter) {

    if (nameFilter === undefined) {
      var catList = getCategoryList();
      var requestURL = "get-locations.php?catlist=" + catList;
    }
    else {
      var requestURL = "get-locations.php?nameFilter=" + escape(nameFilter);
    }

    gMapLocations = new collection();

    // AJAX request for category info
    gLocationsRequest = GXmlHttp.create();
    gLocationsRequest.open("GET", requestURL + "&random=" + Math.random(), true);
    gLocationsRequest.onreadystatechange = locationsDataCallback;
    gLocationsRequest.send(null);
  }


  //-------------------------------------------------------------------------
  //
  function locationsDataCallback() {

    if (gLocationsRequest.readyState != 4) {
      //--!!
      return;
    }

    gLocationsJSON.locationBarHtml = "";
    setMapClass("smallMap");
    gDirections.clear();
    gMap.getInfoWindow().hide();
    gMap.clearOverlays();
    gMap.setCenter(new GLatLng(G_DEFAULT_LAT, G_DEFAULT_LNG), 14, gMap.getCurrentMapType());

    // Try to parse the returned JSON
    try {
      gLocationsJSON = eval('(' + gLocationsRequest.responseText + ')');
    }
    catch(err) {
      alert(err.name + "\n" + err.message);

      // Reset the UI and exit
      setMapClass("largeMap");
      showInfoBar(false);
      showLoadingPanel(false);
      enableUpdateButton(true);
      return;
    }

    gLocationsJSON.locationBarHtml = "";
    gLocationsJSON.lastCatId = "";
    gLocationsJSON.bounds = new GLatLngBounds();

    var lastLocId = null;
    for (var locIdx = 0; locIdx< gLocationsJSON.locations.length; locIdx++) {

      // Is this a new, unique location record? (we rely on the SQL query to ensure the list order
      // is given to us grouped by location ID)
      if (gLocationsJSON.locations[locIdx].id != lastLocId) {

        // Grab a couple reference we're going to be using a lot
        thisLocation = gMapLocations.add(new objLocation(gLocationsJSON.locations[locIdx]));
        thisCategory = gCategories.getById(thisLocation.catId);
      }

      // Another subcategory record for the previously processed location
      else {

        // This whole hack in the else clause is to put all selected subcategory names together in
        // a single comma separated list
        thisLocation.subcatList += ', ' + gLocationsJSON.locations[locIdx].subcatName;
      }

      lastLocId = thisLocation.id
    }

    setTimeout(function () { processLocations(0); }, 0);
  }

  //-------------------------------------------------------------------------
  // UI
  //-------------------------------------------------------------------------

  //-------------------------------------------------------------------------
  //--!! No longer referenced?
  //
  function showNameSearchPanel(show) {

    var panelElement = document.getElementById('nameSearchPanel');

    if (panelElement == null) return;

    if (show) {
      panelElement.style.display = 'block';
    }
    else {
      panelElement.style.display = 'none';
    }
  }

  //-------------------------------------------------------------------------
  //
  function showLoadingPanel(show) {

    var panelElement = document.getElementById(G_LOADING_PANEL_ID);
    var mapElement = document.getElementById(G_MAP_ELEMENT);

    if (show) {
      mapElement.style.display = 'none';
      panelElement.style.display = 'block';
    }
    else {
      mapElement.style.display = 'block';
      panelElement.style.display = 'none';
    }
  }

  //-------------------------------------------------------------------------
  //
  function showInfoBar(bShow) {
    var infoBar = document.getElementById(G_LOCATION_INFO_BAR_ID);

    infoBar.style.display = bShow ? "block" : "none";
  }

  //-------------------------------------------------------------------------
  //
  function showDirectionsBar(bShow) {
    var dirBar = document.getElementById(G_DIRECTIONS_BAR_ID);
    var dirLinks = document.getElementById(G_DIRECTIONS_LINKS_ID);

    dirBar.style.display = bShow ? "block" : "none";
    dirLinks.style.display = bShow ? "block" : "none";
  }

  //-------------------------------------------------------------------------
  //
  function showSubcatContainer(bShow, subcatId) {

    var subcatContainer = document.getElementById(G_SUBCAT_CONTAINER_CLASS + subcatId);
    subcatContainer.style.display = bShow ? "block" : "none";
  }

  //-------------------------------------------------------------------------
  //
  function setMapClass(className) {
    var googleMap = document.getElementById(G_MAP_ELEMENT);

    googleMap.className = className;
  }

  //-------------------------------------------------------------------------
  //
  function enableUpdateButton(enable) {

    var updateButton = document.getElementById(G_UPDATE_MAP_BUTTON);
    updateButton.disabled = !enable;
  }

  //-------------------------------------------------------------------------
  //
  function addCategoryButton(objNewCat) {

    // Get a reference to the parent form and add the container div
    var parentForm = document.getElementById(G_CAT_BUTTONS_FORM_ID);
    var updateButtonContainer = document.getElementById(G_UPDATE_MAP_CONTAINER);

    var elementId = "cat" + objNewCat.id;

    // The icon for this category
    var iconImage = document.createElement("img");
    iconImage.className = G_CAT_BUTTON_IMAGE_CLASS;
    iconImage.setAttribute("src", G_MAP_ICONS_DIRECTORY + objNewCat.iconFile);
    iconImage.setAttribute("alt", "");

    // The checkbox
    var newCheckbox = document.createElement("input");
    newCheckbox.setAttribute("type", "checkbox");
    newCheckbox.name = elementId;
    newCheckbox.id = elementId;
    newCheckbox.className = G_CAT_CHECKBOX_CLASS;

    // The checkbox's label
    var newLabel = document.createElement("label");
    newLabel.className = G_CAT_BUTTON_LABEL_CLASS;
    //--!! Having this set seems to cause 2 click events on the div
    //newLabel.htmlFor = elementId;
    newLabel.innerHTML = objNewCat.name;

    // Create the enclosing div and add our elements to it
    var catSelectDiv = document.createElement("div");
    catSelectDiv.appendChild(iconImage);
    catSelectDiv.appendChild(newCheckbox);
    catSelectDiv.appendChild(newLabel);
    catSelectDiv.className = G_CAT_BUTTON_CONTAINER_CLASS;
    catSelectDiv.onclick = function() {toggleCategory(objNewCat.id);}

    //parentForm.appendChild(catSelectDiv);
    parentForm.insertBefore(catSelectDiv, updateButtonContainer);

    // Controls for subcategories (if there are any)
    if (objNewCat.subcategories.count() > 0) {
      var subcatContainer = document.createElement("div");
      subcatContainer.id = G_SUBCAT_CONTAINER_CLASS + objNewCat.id;
      subcatContainer.className = G_SUBCAT_CONTAINER_CLASS;
      subcatContainer.style.display = "none";

      for (var key in objNewCat.subcategories.items) {
        var thisSubcat = objNewCat.subcategories.items[key];
        subcatContainer.appendChild(addSubcategoryButton(thisSubcat));
      }
      parentForm.insertBefore(subcatContainer, updateButtonContainer);
    }
  }

  //-------------------------------------------------------------------------
  //
  function addSubcategoryButton(objNewSubcat) {

    var subcatElementId = "subcat" + objNewSubcat.id;

    var subcatCheckbox = document.createElement("input");
    subcatCheckbox.setAttribute("type", "checkbox");
    subcatCheckbox.checked = objNewSubcat.active ? "true" : "false";
    subcatCheckbox.name = subcatElementId;
    subcatCheckbox.id = subcatElementId;
    subcatCheckbox.className = G_CAT_CHECKBOX_CLASS;

    var subcatLabel = document.createElement("label");
    subcatLabel.className = G_CAT_BUTTON_LABEL_CLASS;
    subcatLabel.innerHTML = objNewSubcat.name;

    // Create the enclosing div and add our elements to it
    var subcatSelectDiv = document.createElement("div");
    subcatSelectDiv.appendChild(subcatCheckbox);
    subcatSelectDiv.appendChild(subcatLabel);
    subcatSelectDiv.className = G_CAT_BUTTON_CONTAINER_CLASS;
    subcatSelectDiv.onclick = function() {toggleSubcategory(objNewSubcat.catId, objNewSubcat.id);}

    return subcatSelectDiv;
  }

  //-------------------------------------------------------------------------
  // EVENTS
  //-------------------------------------------------------------------------


  //-------------------------------------------------------------------------
  //
  /*
  function mapMarkerClick(objLocation) {
    this.openInfoWindowHtml(objLocation.infoWindowText);
  }
  */

  var mapMarkerClick = function(objLocation) {
    return function(evt) {
      this.openInfoWindowHtml(objLocation.infoWindowText);

      /*
      var oldX = document.body.scrollLeft;
      var oldY = getScrollTop();

      var targetInfoDiv = document.getElementById('locationInfoDiv' + objLocation.id);
      targetInfoDiv.scrollIntoView(true);

      window.scrollTo(oldX, oldY);
      */
    }
  };

  function getScrollTop(){
    if(typeof pageYOffset!= 'undefined'){
      //most browsers
      return pageYOffset;
    }
    else{
      var B= document.body; //IE 'quirks'
      var D= document.documentElement; //IE with doctype
      D= (D.clientHeight)? D: B;
      return D.scrollTop;
    }
  }


  //-------------------------------------------------------------------------
  //
  function tabClick(activeTab) {
    var inactiveTab;
    var activePanel;
    var inactivePanel;

    switch(activeTab.id) {
      case 'tabName':
        inactiveTab = document.getElementById('tabCategory');
        inactivePanel = document.getElementById('categoryBar');
        activePanel = document.getElementById('nameSearchBar');
        break;

      case 'tabCategory':
        inactiveTab = document.getElementById('tabName');
        inactivePanel = document.getElementById('nameSearchBar');
        activePanel = document.getElementById('categoryBar');
        break;
    }

    activePanel.style.display = 'block';
    inactivePanel.style.display = 'none';

    inactiveTab.style.zIndex = 1;
    inactiveTab.style.backgroundColor = '#DCDEBC';

    activeTab.style.zIndex = 200;
    activeTab.style.backgroundColor = '#F2EFE9';
  }

  //-------------------------------------------------------------------------
  //
  function toggleCategory(targetId) {
    var catButton = null;
    var targetCat = null;

    // Get a reference to the specified category object
    targetCat = gCategories.getById(targetId);

    // Make updates if we found a valid category object
    if (targetCat != null) {

      // Flip the active flag
      targetCat.active = !targetCat.active;

      // Ensure the check-box is marked correctly
      catButton = document.getElementById("cat" + targetId);
      catButton.checked = targetCat.active;

      //--!! Commented out but the functionality is in place
      /*
      if (targetCat.subcategories.count() > 0) {
        showSubcatContainer(targetCat.active, targetCat.id);
      }
      */
    }
  }

  //-------------------------------------------------------------------------
  //
  function toggleSubcategory(catId, subcatId) {
    var subcatButton = null;
    var targetCat = null;
    var targetSubcat = null;

    // Get a reference to the specified category object
    targetCat = gCategories.getById(catId)
    targetSubcat = targetCat.subcategories.getById(subcatId);

    // Make updates if we found a valid category object
    if (targetSubcat  != null) {

      // Flip the active flag
      targetSubcat.active = !targetSubcat.active;

      // Ensure the check-box is marked correctly
      subcatButton = document.getElementById("subcat" + subcatId);
      subcatButton.checked = targetSubcat.active;
    }
  }

  //-------------------------------------------------------------------------
  //
  //
  function updateMapClick() {

    enableUpdateButton(false);
    showLoadingPanel(true);
    showDirectionsBar(false);
    showInfoBar(true);

    setTimeout(function() { requestLocations(); }, 50);
  }

  //-------------------------------------------------------------------------
  //
  function nameFilterSubmitClick() {
    nameFilterElement = document.getElementById("nameFilter");

    showNameSearchPanel(false);
    enableUpdateButton(false);
    showLoadingPanel(true);
    showDirectionsBar(false);
    showInfoBar(true);

    setTimeout(function() { requestLocations(nameFilterElement.value); }, 50);
  }

  //-------------------------------------------------------------------------
  // This function picks up the click and opens the corresponding info window
  //
  function locationLinkClick(locationId) {
    var targetLocation = gMapLocations.getById(locationId);
    targetLocation.mapMarker.openInfoWindowHtml(targetLocation.infoWindowText);
  }

  //-------------------------------------------------------------------------
  //
  function printerFriendlyDirections() {
    var zoomLevel = gMap.getZoom();

    var outUrl =
      "http://maps.google.com/maps?f=q&hl=en&ie=UTF8&pw=2"
      + "&saddr=" + gDirections.saddrText
      + "&daddr=" + gDirections.daddrText
      + "&z=" + zoomLevel;

    window.open(outUrl,'','scrollbars=yes,menubar=no,height=600,width=800,resizable=yes,toolbar=no,location=no,status=no');
  }

  //-------------------------------------------------------------------------
  //
  function emailDirections() {
    var zoomLevel = gMap.getZoom();

    var mapUrl = escape(
      "http://maps.google.com/maps?f=q&hl=en&ie=UTF8&pw=2"
      + "&saddr=" + gDirections.saddrText
      + "&daddr=" + gDirections.daddrText
      + "&z=" + zoomLevel
    );

    var outUrl = "http://www.athensohio.com/map/email-directions.php?link=" + mapUrl;

    window.open(outUrl,'','');
  }

  //-------------------------------------------------------------------------
  // request the directions
  //
  function getDirections() {
    var opts = {};

    showInfoBar(false);
    showDirectionsBar(true);

    // Save the map state
    gLastZoom = gMap.getZoom();
    gLastCenter = gMap.getCenter();

    // Hide all the markers
    for (var key in gMapLocations.items) {
      gMapLocations.items[key].mapMarker.hide();
    }

    // set the start and end locations
    //--!! hard-coded element IDs
    var saddr = document.getElementById("saddr").value;
    var daddr = document.getElementById("daddr").value;
    gDirections.saddrText = escape(saddr);
    gDirections.daddrText = escape(daddr);
    gDirections.load("from: " + saddr + " to: " + daddr, opts);
  }

  //-------------------------------------------------------------------------
  //
  function clearDirections() {

    showInfoBar(true);
    showDirectionsBar(false);


    gDirections.clear();

    // Show all the markers
    for (var key in gMapLocations.items) {
      gMapLocations.items[key].mapMarker.show();
    }

    gMap.setZoom(gLastZoom);
    gMap.setCenter(gLastCenter);
  }

  //-------------------------------------------------------------------------
  //
  function nameSearchClick() {
    showNameSearchPanel(true);
  }

  //-------------------------------------------------------------------------
  // OBJECTS
  //-------------------------------------------------------------------------

  //-------------------------------------------------------------------------
  //
  function objLocation(newObj) {

    // Straight from the database
    this.id = newObj.id;
    this.name = newObj.name;
    this.address = newObj.address;
    this.city = newObj.city;
    this.state =  newObj.state;
    this.zip =  newObj.zip;
    this.phone =  newObj.phone;
    this.website = newObj.website;
    this.desc = newObj.desc;
    this.image = newObj.image;
    this.lat =  parseFloat(newObj.lat);
    this.lng =  parseFloat(newObj.lng);
    this.catId = newObj.catId;
    this.subcatList = newObj.subcatName;

    this.mapPoint = new GLatLng (this.lat, this.lng);

    this.infoWindowText = '<strong>' + this.name + '</strong>' + '<br />';
    this.infoWindowText += this.address + '<br />';
    this.infoWindowText += this.city + ', ' + this.state + ' ' + this.zip + '<br />';
    if (this.website != "") {
      this.infoWindowText += '<br /><a href="' + this.website + '" target="_blank">Visit Website</a><br />';
    }
    this.infoWindowText +='<br /><br />Get directions here from:<form action="javascript:getDirections()">'
      + '<input type="text" size=40 maxlength=40 name="saddr" id="saddr" value="" /><br />'
      + '<input value="Get Directions" type="submit"><br />'
      + '<input type="hidden" id="daddr" value="' + this.name + "@"+ this.lat + ',' + this.lng + '"/>';

    this.mapMarker = createMarker(this.mapPoint, gCategories.getById(this.catId).mapIcon, this.name, this);
  }

  //-------------------------------------------------------------------------
  //
  function objCategory(newObj) {

    // Set these directly from the database
    this.id = newObj.id;
    this.name = newObj.name;
    this.iconFile = newObj.iconFile;

    // Create a new icon object for this category
    this.mapIcon = createIcon(G_MAP_ICONS_DIRECTORY + newObj.iconFile);

    // default to "off" for categories
    this.active = false;

    // Get any associated subcategories
    this.subcategories = new collection();
    for (var i = 0; i < newObj.subcategory.length; i++) {
      var newSubcat = this.subcategories.add(new objSubcategory(newObj.subcategory[i]));
      newSubcat.catId = this.id;
    }
  }

  //-------------------------------------------------------------------------
  //
  function objSubcategory(newObj) {

    // Set these directly from the database
    this.id = newObj.subcat_id;
    this.name = newObj.subcat_name;

    // default to "on" for subcategories
    this.active = true;
  }

  //-------------------------------------------------------------------------
  //
  function collection() {

    // "this.items" is not an array.  Javascript calls our usage an
    // "associative array" but it's really just some syntax magic in JS that
    // lets us manage an object (this.items) with syntax that makes it look like
    // an array.  But this.items IS really an object, our "indices" are really
    // just properties of this.items, and attempts to use the .length property
    // will come up dry (even if you change the {} to [], you're still just
    // adding PROPERTIES to the array in this.add()).  Thus we use a real array
    // to keep a lookup table, facilitating lookups by index OR by a specific
    // location ID (indices are in the order members are added).
    var locIds = [];
    this.items = {};

    //-----------------------------------------------------------------------
    // Add a location object
    //
    this.add = function (newObj) {
      this.items["k" + newObj.id] = newObj;
      locIds[locIds.length] = newObj.id;
      return newObj;
    }

    //-----------------------------------------------------------------------
    // this.items doesn't have a concept of "length", but the lookup array
    // is a true array
    //
    this.count = function () {
      return locIds.length;
    }

    //-----------------------------------------------------------------------
    // Find a location object by its ID
    //
    this.getById = function(targetId) {
      return this.items["k" + targetId];
    }

    //-----------------------------------------------------------------------
    // Find a location object by index (in the order members were added)
    //
    this.getByIndex = function(targetIndex) {
      return this.items["k" + locIds[targetIndex]];
    }
  }

