catalog, query UI: fix race condition and stabilize UI

add autocomplete, sidebar, deselect, and paging to UI
stability improvements to catalog

Change-Id: I2e873a41eaa8c10d9f9a1d605f33fb5217112d64
refs: #2634, 2638
diff --git a/client/query/query.html b/client/query/query.html
index b8f0e61..9e68518 100644
--- a/client/query/query.html
+++ b/client/query/query.html
@@ -6,122 +6,258 @@
     <meta charset="UTF-8" />
 
 
-    <script src="http://code.jquery.com/jquery-latest.min.js" type="text/javascript"></script>
-    <script src="http://code.jquery.com/ui/1.10.1/jquery-ui.js"></script>
-
+    <script type="text/javascript" src="../jquery/jquery-latest.min.js"></script>
+    <script type="text/javascript" src="../jquery/ui/1.10.1/jquery-ui.js"></script>
+    <script type="text/javascript" src="../ndn-js/build/ndn.js"></script>
 
 
     <link rel="stylesheet" href="query.css">
+    <link rel="stylesheet" href="../jquery/ui/1.11.4/themes/smoothness/jquery-ui.css">
 
     <script>
-        var searchMenuOptions = {};
-        var results = {};
+        // {@ @todo: this need to be configured before the document load
+        var catalog = "/catalog/myUniqueName";
+        var face = new Face({
+            host: "localhost",
+            port: 9696
+        });
+
+        // @}
+
+        var searchMenuOptions = {}
+        var results = [];
         var resultCount = 0;
         var page = 1;
         var totalPages = 1;
+        var selectedSearch = {};
+        var dropdown = [];
 
         $(document).ready(function () {
             var searchMenu = $(".cssmenu");
             var currentPage = $(".page");
             var resultTable = $(".resultTable");
-            var data = $.getJSON("sample.json", function () {
-            })
-            .done(function( data ) {
+            var data = $.getJSON("search_catagories.json", function () {}).done(function (data) {
                 $.each(data, function (pageSection, contents) {
                     if (pageSection == "SearchCatagories") {
-
                         $.each(contents, function (search, searchOptions) {
                             search = search.replace(/\_/g, " ");
 
                             searchMenu.append('<li id="' + search + '" onclick="getDropDown(this.id)"><a href="#">' + search + '</a></li>');
-                            searchMenuOptions[search] = searchOptions;
+                            searchMenuOptions[String(search)] = searchOptions;
                         });
-                    } else if (pageSection == "QueryResults") {
-                        results = {};
-                        resultCount = 0;
-                        var view = [0, 0];
-                        $.each(contents, function (queryResult, field) {
-                            if (queryResult == "Length") {
-                                resultCount = field;
-                            } else if (queryResult == "View") {
-                                view = field;
-                            } else if (queryResult == "Results") {
-                                resultTable.empty();
-                                resultTable.append('<tr><td>Results</td></tr>');
-
-                                $.each(field, function (entryCount, fullResult) {
-
-                                    $.each(fullResult, function (name, metadata) {
-                                        resultTable.append('<tr><td onclick="getDetails(this.id)" id="' + name + '">' + name + '</td></tr>');
-                                        results[name] = metadata;
-                                    });
-
-                                });
-                            } else {
-                                console.error("Unknown field " + queryResult);
-                            }
-                        });
-
-                        // Calculating the current page and the view
-                        var diff = view[1] - view[0] + 1;
-                        if (diff > 0) {
-                            totalPages = Math.ceil(resultCount / diff);
-                            page = Math.ceil(view[0] / diff) + 1;
-                        } else {
-                            totalPages = 1;
-                            page = 1;
-                        }
-
-                        if (page != 1) {
-                            console.log(view[0]);
-                            currentPage.append('<span id="' + (view[0] - 1) + '" onclick="getPage(this.id)"><a href="#"><</a>');
-                        }
-
-                        // This section of code creates the paging for the results.
-                        // To prevent it from having a 1000+ pages, it will only show the 5 pages before/after
-                        // the current page and the total pages (expect users not to really jump around a lot).
-                        for (var i = 0; i < totalPages; ++i) {
-                            if (i + 1 == 1 || i + 1 == page || i + 1 == totalPages || (i + 1 < page && i + 4 > page) || (i + 1 > page && i - 4 < page)) { // in our current page range
-                                currentPage.append(' ');
-                                currentPage.append('<span id="' + (i + 1) + '" onclick="getPage(this.id)">');
-                                if (i + 1 != page) {
-                                    currentPage.append('<a href="#">' + (i + 1) + '</a>')
-                                } else {
-                                    currentPage.append(i + 1);
-                                }
-                                currentPage.append('</span>');
-                            } else { // Need to skip ahead
-                                currentPage.append(' ...');
-                                if (i == page + 5) {
-                                    i = totalPages - 2;
-                                } else if (i < page - 7) {
-                                    i = page - 6;
-                                }
-                            }
-                        }
-                        if (page != totalPages) {
-                            currentPage.append(' <span id="' + (page + diff) + '" onclick="getPage(this.id)"><a href="#">></a>');
-                        }
                     }
                 });
-            })();
+            });
         });
 
-        var state = "";
+        function onData(data) {
+            var payloadStr = data.content.toString().split("\n")[0];
+
+            var queryResults = JSON.parse(payloadStr);
+
+            var resultTable = $(".resultTable");
+            $.each(queryResults, function (queryResult, field) {
+
+                if (queryResult == "next") {
+                    populateAutocomplete(field);
+                }
+
+                $.each(field, function (entryCount, name) {
+                    results.push(name);
+                });
+            });
+
+            // Calculating the current page and the view
+            totalPages = Math.ceil(resultCount / 20);
+            populateResults(0);
+        }
+
+        var state = {};
+
+        function query(prefix, parameters, callback, pipeline) {
+            results = [];
+            dropdown = [];
+
+            var resultTable = $(".resultTable");
+            resultTable.empty();
+            resultTable.append('<tr><td>Results</td></tr>');
+
+            var queryPrefix = new Name(prefix);
+            queryPrefix.append("query");
+
+            queryPrefix.append(JSON.stringify(parameters));
+
+            state = {
+                prefix: new Name(prefix),
+                userOnData: callback,
+                outstanding: {},
+                nextSegment: 0,
+            };
+
+            /*if (state.hasOwnProperty("version")) {
+                console.log("state already has version");
+            }*/
+
+            var queryInterest = new Interest(queryPrefix);
+            queryInterest.setInterestLifetimeMilliseconds(10000);
+
+            face.expressInterest(queryInterest,
+                onQueryData,
+                onQueryTimeout);
+
+            state["outstanding"][queryInterest.getName().toUri()] = 0;
+        }
+
+        function expressNextInterest() {
+            // @todo pipelines
+            var nextName = new Name(state["results"]);
+            nextName.appendSegment(state["nextSegment"]);
+
+            var nextInterest = new Interest(nextName);
+            nextInterest.setInterestLifetimeMilliseconds(10000);
+
+            face.expressInterest(nextInterest,
+                onQueryResultsData,
+                onQueryResultsTimeout);
+
+            state["nextSegment"] ++;
+            state["outstanding"][nextName.toUri()] = 0;
+        }
+
+        function onQueryData(interest, data) {
+            var name = data.getName();
+
+            delete state["outstanding"][interest.getName().toUri()];
+
+            state["version"] = name.get(state["prefix"].size() + 2).toVersion();
+
+            state["results"] = new Name(state["prefix"]).append("query-results").appendVersion(state["version"]);
+
+            expressNextInterest();
+        }
+
+        function onQueryResultsData(interest, data) {
+            var name = data.getName();
+            delete state["outstanding"][interest.getName().toUri()];
+
+            if (!name.get(-1).equals(new Name.Component("END"))) {
+                expressNextInterest();
+            } else {
+                alert("found final block");
+            }
+
+            state["userOnData"](data);
+        }
+
+        function onQueryTimeout(interest) {
+            var uri = interest.getName().toUri();
+            if (state["outstanding"][uri] < 1) {
+                state["outstanding"][uri] ++;
+                face.expressInterest(interest,
+                    onQueryData,
+                    onQueryTimeout);
+            } else {
+                delete state["outstanding"][uri];
+
+                // We modify the autocomplete box here because we need to know
+                // we have all of the entries first. Fairly hacky.
+                var autocompleteFullName = autocompleteText.value;
+                for (var i = 0; i < dropdown.length; ++i) {
+                    if (dropdown[i].substr(0, dropdown[i].length - 1).toUpperCase === autocompleteText.value.toUpperCase || dropdown.length == 1) {
+                        autocompleteText.value = dropdown[i];
+                    }
+                }
+            }
+        }
+
+        function onQueryResultsTimeout(interest) {
+            var uri = interest.getName().toUri();
+            if (state["outstanding"][uri] < 1) {
+                state["outstanding"][uri] ++;
+                face.expressInterest(interest,
+                    onQueryResultsData,
+                    onQueryResultsTimeout);
+            } else {
+                delete state["outstanding"][uri];
+                // We modify the autocomplete box here because we need to know
+                // we have all of the entries first. Fairly hacky.
+                var autocompleteFullName = autocompleteText.value;
+                for (var i = 0; i < dropdown.length; ++i) {
+                    if (dropdown[i].substr(0, dropdown[i].length - 1).toUpperCase === autocompleteText.value.toUpperCase || dropdown.length == 1) {
+                        autocompleteText.value = dropdown[i];
+                    }
+                }
+            }
+        }
+
+
+        var currentViewIndex = 0;
+
+        function populateResults(startIndex) {
+            var resultTable = $(".resultTable");
+            resultTable.empty();
+            resultTable.append('<tr><td>Results</td></tr>');
+
+
+            for (var i = startIndex; i < startIndex + 20 && i < results.length; ++i) {
+                resultTable.append('<tr><td>' + results[i] + '</td></tr>');
+            }
+
+            if (results.length <= 20) {
+                page = 1;
+            } else {
+                page = startIndex / 20 + 1;
+            }
+
+            totalPages = Math.ceil(results.length / 20);
+
+            var currentPage = $(".page");
+            currentPage.empty();
+            if (page != 1) {
+                currentPage.append('<a href="#" onclick="getPage(this.id);" id="<"><</a>');
+            }
+            // This section of code creates the paging for the results.
+            // To prevent it from having a 1000+ pages, it will only show the 5 pages before/after
+            // the current page and the total pages (expect users not to really jump around a lot).
+            for (var i = 1; i <= totalPages; ++i) {
+                if (i == 1 || i == totalPages     // Min or max
+                    || (i <= page && i + 5 >= page)    // in our current page range
+                    || (i >= page && i - 5 <= page)) { // in our current page range
+                       if (i != page) {
+                         currentPage.append(' <a href="#" onclick="getPage(' + i + ');">' + i + '</a>')
+                         if (i == 1 && page > i + 5) {
+                             currentPage.append(' ... ');
+                         }
+                       } else {
+                           currentPage.append(' ' + i);
+                       }
+                } else { // Need to skip ahead
+                    if (i == page + 6) {
+                        currentPage.append(' ... ');
+
+                        currentPage.append(' <a href="#" onclick="getPage(this.id);" id=">">></a>')
+                        i = totalPages - 1;
+                    }
+                }
+            }
+            currentPage.append('  ' + results.length + ' results');
+        }
+
+        var dropState = "";
 
         function getDropDown(str) {
             var searchMenu = $(".cssmenu");
-            if (str == state) {
-                state = "";
+            if (str == dropState) {
+                dropState = "";
                 searchMenu.find("#" + str).find("#options_" + str).empty();
             } else {
-                state = str;
+                dropState = str;
 
                 $.each(searchMenuOptions, function (search, fields) {
                     if (search === str) {
                         searchMenu.find("#" + search).append('<ul id="options_' + search + '" class="sub-menu">');
                         for (var i = 0; i < fields.length; ++i) {
-                            searchMenu.find("#options_" + search).append('<li><a href="#">' + fields[i] + '</a></li>');
+                            searchMenu.find("#options_" + search).append('<li id="' + fields[i] + '"onclick="submitCatalogSearch(this.id)"><a href="#">' + fields[i] + '</a></li>');
                         }
                         searchMenu.append('</ul>');
                     } else {
@@ -133,45 +269,143 @@
             }
         }
 
-        function getPage(str) {
-            // @todo
-        }
+        function getPage(clickedPage) {
+            console.log(clickedPage);
 
-        function getDetails(str) {
-            // @todo: Identify the correct way to integrate the download backend so that we know who
-            // we should be pointing at.
+            var nextPage = clickedPage;
+            if (clickedPage === "<") {
+                nextPage = page - 5;
+            } else if (clickedPage === ">") {
+                console.log("> enabled");
 
-            var details = "<h2>" + str + "<h2>";
-
-            details += '<form action="getDownload()"><input type="submit" value="Download"></form>';
-            details += "<table>";
-            $.each(results[str], function (fieldName, field) {
-                console.log(fieldName);
-
-                details += "<tr>"
-                details += "<td>" + fieldName + "</td>";
-                if (typeof field === 'object') {
-                    details += "<td><table>";
-                    $.each(field, function (name, fields) {
-                        details += '<tr><td>' + name + '</td><td>' + fields + '<td></tr>';
-                    });
-                    details += "</table></td>";
-                } else {
-                    details += "<td>" + field + "</td>"
-                }
-                details += "</tr>"
-
-            });
-            details += "</table>";
-            details += '<form action="getDownload()"><input type="submit" value="Download"></form>';
-
-            var detailsWindow = window.open("", str, "width=400, height=600, toolbar=no, menubar=no");
-            detailsWindow.document.write(details);
-            detailsWindow.onload = function () { // of course you can use other onload-techniques
-                jQuery(detailsWindow.document.head).append('<script>function getDownload() {alert("Yay";)}</' + "script>"); // or any other dom manipulation
+                nextPage = page + 5;
             }
 
-            //window.open("details.html", "_blank", "toolbar=no, scrollbars=yes, resizable=yes, top=500, left=500, width=400, height=400");*/
+            nextPage--; // Need to adjust for starting at 0
+
+            if (nextPage < 0 ) {
+                nextPage = 0;
+                console.log("0 enabled");
+            } else if (nextPage > totalPages - 1) {
+                nextPage = totalPages - 1;
+                console.log("total enabled");
+            }
+
+            populateResults(nextPage * 20);
+            return false;
+        }
+
+        function submitAutoComplete() {
+            if (autocompleteText.value.length > 0) {
+                var selection = autocompleteText.value;
+                $.each(dropdown, function (i, dropdownEntry) {
+                    if (dropdownEntry.substr(0, dropdownEntry.length - 1) == selection) {
+                        selection = dropdownEntry;
+                    }
+                });
+
+                selectedSearch["?"] = selection;
+                query(catalog, selectedSearch, onData, 1);
+                delete selectedSearch["?"];
+            }
+        }
+
+        function submitCatalogSearch(field) {
+            console.log("Sumbit Catalog Search: " + field);
+            // @todo: this logic isn't quite right
+            var remove = false;
+            $.each(selectedSearch, function (search, f) {
+                if (field == f) {
+                    delete selectedSearch[field];
+                    remove = true;
+                }
+            });
+            if (!remove) {
+                $.each(searchMenuOptions, function (search, fields) {
+                    $.each(fields, function (index, f) {
+                        if (f == field) {
+                            selectedSearch[search] = field;
+                        }
+                    });
+                });
+            }
+            query(catalog, selectedSearch, onData, 1);
+            populateCurrentSelections();
+            return false;
+        }
+
+        function populateAutocomplete(fields) {
+            var isAutocompleteFullName = (autocompleteText.value.charAt(autocompleteText.value.length - 1) === "/");
+            var autocompleteFullName = autocompleteText.value;
+            for (var i = 0; i < fields.length; ++i) {
+                var fieldFullName = fields[i];
+                var entry = autocompleteFullName;
+                var skipahead = "";
+
+                if (isAutocompleteFullName) {
+                    skipahead = fieldFullName.substr(autocompleteText.value.length, fieldFullName.length);
+                } else {
+                    if (fieldFullName.charAt(autocompleteText.value.length) === "/") {
+                        entry += "/";
+                        skipahead = fieldFullName.substr(autocompleteText.value.length + 1, fieldFullName.length);
+                    } else {
+                        skipahead = fieldFullName.substr(autocompleteText.value.length, fieldFullName.length);
+                    }
+                }
+                if (skipahead.indexOf("/") != -1) {
+                    entry += skipahead.substr(0, skipahead.indexOf("/") + 1);
+                } else {
+                    entry += skipahead;
+                }
+
+                var added = false;
+                for (var j = 0; j < dropdown.length && !added; ++j) {
+                    if (dropdown[j] === entry) {
+                        added = true;
+                    } else if (dropdown[j] > entry) {
+                        dropdown.splice(j, 0, entry);
+                        added = true;
+                    }
+                }
+                if (!added) {
+                    dropdown.push(entry);
+                }
+
+            }
+            $("#autocompleteText").autocomplete({
+                source: dropdown
+            });
+        }
+
+        function populateCurrentSelections() {
+            var currentSelection = $(".currentSelections");
+            currentSelection.empty();
+
+            currentSelection.append("<p>Filtering on:");
+
+            $.each(selectedSearch, function (searchMenuCatagory, selection) {
+                currentSelection.append('  <a href="#" onclick="removeFilter(this.id);" id="' + searchMenuCatagory + ':' + selection + '">[X] ' + searchMenuCatagory + ":" + selection + '</a>');
+            });
+
+            currentSelection.append("</p>");
+        }
+
+
+        function removeFilter(filter) {
+            console.log("Remove filter" + filter);
+            var searchFilter = filter.split(":");
+
+            var search = "";
+            for (var j = 0; j < searchFilter.length; ++j) {
+                search += searchFilter[j] + " ";
+            }
+            console.log("Split values: '" + search + "'");
+
+            delete selectedSearch[searchFilter[0]];
+            query(catalog, selectedSearch, onData, 1);
+            populateCurrentSelections();
+
+            return false;
         }
     </script>
 </head>
@@ -185,18 +419,25 @@
 
     </ul>
 
+    <div class="currentSelections"></div>
+
+
     <div class="autocomplete">
-        <form action="submit()">
-            <input type="text" name="autocomplete" class="textbox" id="autocomplete" placeholder="cmap" width="800px">
-            <input type="submit" value="Search">
-        </form>
+        <div class="ui-widget">
+            <label for="tags"> Search: </label>
+            <input id="autocompleteText" placeholder="/cmip" class="textbox" onkeydown="if (event.keyCode == 13) submitAutoComplete(); ">
+            <button id="autoButton" value="Search" onclick="submitAutoComplete()" id="autocompleteButton">Search</button>
+        </div>
+
+        <div class="page"></div>
+
+        <table class="resultTable">
+
+
+        </table>
     </div>
-    <table class="resultTable">
 
 
-    </table>
-    <div class="page"></div>
-
 </body>
 
 </html>