1"use strict"; 2 3/** 4 * An API to the prefix search tree that maps the names of the tree of life 5 * nodes to the node and subtree indices of those nodes. 6 * 7 * Usage: 8 * function resultCallback(query, resultTrieNode) { ... } 9 * search_model.searchFor('Canis', resultCallback) 10 * 11 * where resultTrieNode is like 12 * 13 * { 14 * v: [ 15 * 123, // id of the node for the node called 'Canis lupus' 16 * 2 // id of the subtree where that node is located 17 * ], 18 * c: { // children 'Canis lupus' in the prefix tree 19 * " familiaris": { ... }, 20 * ... 21 * } 22 * } 23 * 24 * or null if the no name in the tree starts with the given query. 25 * The member v is set only if query corresponds to the exact name of 26 * some node in the tree. The member c can also be empty if query if is not 27 * a proper prefix of any other node in the tree. 28 * 29 */ 30function SearchModel(search_ready_callback) { 31 32 var subtrees = {}; 33 var root = {}; 34 35 var latest_query = ''; 36 var request_counter = 0; 37 38 var fetch = function (id, callback) { 39 if (subtrees[id]) { 40 callback(subtrees[id]); 41 } 42 else { 43 d3.json('data/search-'+id+'.json', function (error, data) { 44 if (error) return console.warn(error); 45 subtrees[id] = data; 46 callback(data); 47 }); 48 } 49 }; 50 51 fetch(0, function (data) { 52 root = data; 53 if (search_ready_callback) search_ready_callback(); 54 }); 55 56 function doSearch(tree, prefix, callback) { 57 58 if (tree.subtree_index) { 59 fetch(tree.subtree_index, function (subtree) { 60 doSearch(subtree, prefix, callback); 61 }); 62 return; 63 } 64 65 if (prefix === '') { 66 callback(tree); 67 return; 68 } 69 70 for (var key in tree.c) { 71 if (prefix.indexOf(key) == 0) { 72 doSearch(tree.c[key], prefix.substring(key.length), callback); 73 return; 74 } 75 if (key.indexOf(prefix) == 0) { 76 var new_tree = { c: {} }; 77 new_tree.c[key.substring(prefix.length)] = tree.c[key]; 78 callback(new_tree); 79 return; 80 } 81 } 82 callback(null); 83 }; 84 85 this.searchFor = function(query, callback) { 86 87 if (latest_query === query) return; 88 latest_query = query; 89 90 request_counter += 1; 91 doSearch(root, latest_query, function (result) { 92 request_counter -= 1; 93 if (latest_query === query) { 94 callback(query, result); 95 } 96 }); 97 } 98 99 this.requestPending = function() { return request_counter > 0; } 100} 101 102function SearchView(result_callback) { 103 var model = new SearchModel(); 104 105 var search_area = d3.select('#search'); 106 var results_area = d3.select('#results'); 107 var go_button = d3.select('#goto-link'); 108 var go_button_action = null; 109 110 this.clear = function () { 111 search_area.property('value', ''); 112 go_button.classed('hidden', true); 113 } 114 115 function openResult(search_result) { 116 result_callback(search_result[0], search_result[1]); 117 } 118 119 function capitalize(str) { 120 if (str == '') return ''; 121 return str.charAt(0).toUpperCase() + str.slice(1); 122 } 123 124 function checkLoader() { 125 d3.select('.loader-container') 126 .classed('loading-search', model.requestPending()); 127 } 128 129 function searchFor(query) { 130 model.searchFor(query, displayResult); 131 setTimeout(checkLoader, 200); 132 } 133 134 function displayResult(query, result) { 135 136 checkLoader(); 137 138 var keys = []; 139 go_button_action = null; 140 141 function onLineClick(l) { 142 var selected = result.c[l]; 143 if (selected) { 144 search_area.property('value', query+l); 145 searchFor(query+l); 146 if (selected.v) openResult(selected.v); 147 } 148 } 149 150 if (result === null) { 151 keys = []; 152 153 } else { 154 keys = d3.keys(result.c); 155 156 if (result.v) { 157 go_button_action = (function () { 158 openResult(result.v); 159 results_area.classed('hidden', true); 160 }); 161 } else if (keys.length == 1) { 162 go_button_action = (function () { 163 onLineClick(keys[0]); 164 }); 165 } else if (query == '') { 166 go_button_action = (function () { 167 result_callback(null); 168 }); 169 } 170 } 171 172 search_area.classed('not-found', result === null); 173 go_button.classed('hidden', go_button_action === null); 174 175 results_area.classed('hidden', 176 (query.length < 2 && keys.length > 10) || 177 (keys.length == 1 && keys[0] == '')); 178 179 var data = []; 180 181 keys.sort(); 182 for (var i in keys) { 183 var key = keys[i]; 184 data.push(key); 185 if (i > 10) { 186 data.push('...'); 187 break; 188 } 189 } 190 191 var lines = results_area 192 .selectAll('a') 193 .data(data); 194 195 lines.enter() 196 .append('a') 197 .attr('class', 'result-line') 198 .attr('href', 'javascript:void(0)'); 199 200 lines.on('click', function (l) { onLineClick(l); }); 201
202 lines.exit().remove(); 203 204 lines.text(function(v) {return capitalize(query+v)}); 205 } 206 207 go_button 208 .on('click', function () { 209 if (go_button_action !== null) { 210 go_button_action(); 211 go_button.classed('hidden', true); 212 } 213 }); 214 215 search_area.on('keyup', function () { 216 if (d3.event.keyCode === 13) { // Enter pressed 217 go_button.on('click')(); 218 } 219 else { 220 searchFor(capitalize(this.value)); 221 } 222 }).on('blur', function () { 223 setTimeout(function() { 224 results_area.classed('hidden', true); 225 }, 200); 226 }); 227} 228 229
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.