spatial-navigation-polyfill
Version:
A polyfill for the spatial navigation
312 lines (277 loc) • 9.66 kB
JavaScript
/* graph.js contains codes which is necessary for generating graph data structures and SCC elements
which are necessary for detectors. */
function graph() {
this.focusable = [];
this.adj_list = [];
this.rev_adj_list = [];
this.node_num;
this.rev_scc = [];
this.scc = [];
this.visited;
this.stack = [];
this.scc_visited = [];
this.valid = true;
this.result = [];
}
/* A fuction that makes a data structure of Graph.
*
* Argument
* 0 : all directions
* 1 : up
* 2 : down
* 3 : left
* 4 : right
*
* Two dimensional array
*
* Row
* Length of the number of focusble element.
*
* Column
* idx | 0 | 1 | 2 | 3 | 4 |
* Nodes | origin | up | down | left | right |
*
*/
graph.prototype.make_adj_array = function(direction) {
const DIR = ["up", "down", "left", "right"];
const option = {mode: 'all'}; // {mode: 'visible'}; //
let targetGraph = [];
this.focusable = document.body.focusableAreas(option);
this.node_num = this.focusable.length;
this.visited = new Array(this.node_num).fill(0);
window.__spatialNavigation__.useMemoizationForIsVisible(true);
if (direction == "all") { // direction == 0 means all directions
for (var i = 0; i < this.node_num; i++) {
this.focusable[i].node_id = i // assign node_id to focusable elements.
targetGraph[i] = [this.focusable[i]];
for (dir of DIR) {
try {
targetGraph[i].push(window.__spatialNavigation__.findNextTarget(targetGraph[i][0], dir, option));
} catch (e) {
console.log(targetGraph[i][0]);
console.log(e);
targetGraph[i].push(undefined);
}
}
}
} else if (DIR.includes(direction)) {
for (var i = 0; i < this.node_num; i++) {
this.focusable[i].node_id = i
targetGraph[i] = new Array(2);
targetGraph[i][0] = this.focusable[i];
targetGraph[i][1] = window.__spatialNavigation__.findNextTarget(targetGraph[i][0], direction, option);
}
}
window.__spatialNavigation__.useMemoizationForIsVisible(false);
return targetGraph;
}
/* A fuction that removes empty and redundant element.
*
*/
graph.prototype.remove_redundancy_in_array = function(input_array) {
var output_array = input_array.filter(function(item, pos) {
return input_array.indexOf(item) == pos;
})
var filtered_output_array = output_array.filter(function(el) {
return el != null;
});
return filtered_output_array;
}
/* A fuction that makes a directed graph in the form of two dimensional list
*
* Row
* Length of the number of focusble element.
*
* Column
* Nodes | origin | [destinations]
*
*/
// Here, directed graph means a grpah only having directed edge
// that can be represented with a pair of two nodes (from, to)
// which is not related to arrow keys direction.
graph.prototype.make_adj_list = function(arrow_graph) {
var directed_graph = [];
for (var i = 0; i < arrow_graph.length; i++) {
var tmp = new Array();
var origin = arrow_graph[i][0];
var destinations_with_each_arrow_keys = arrow_graph[i].slice(1, arrow_graph[i].length);
var destinations_without_redundancy = this.remove_redundancy_in_array(destinations_with_each_arrow_keys);
tmp.push(origin);
Array.prototype.push.apply(tmp, destinations_without_redundancy);
directed_graph.push(tmp);
}
return directed_graph;
}
/* A fuction that makes a reversed adj_list with directed graph
*
* Row
* Length of the number of focusble element.
* Same order of directed graph.
*
* Column
* Nodes | destination | [origins] (in view point of directed graph)
*
*/
graph.prototype.make_rev_adj_list = function(directed_graph) {
var reversed_graph_list = [];
for (var i = 0; i < directed_graph.length; i++) {
reversed_graph_list.push([directed_graph[i][0]]); // make a array of rev_graph in same order with directed_graph
}
for (var i = 0; i < directed_graph.length; i++) {
for (var j = 1; j < directed_graph[i].length; j++) {
if (directed_graph[i][j].node_id == undefined) {
this.valid = false;
return;
}
reversed_graph_list[directed_graph[i][j].node_id].push(directed_graph[i][0]); //push elements into rev_graph.
}
}
return reversed_graph_list;
}
/* A fuction that makes a reversed scc with scc.
*
* has same algorithm with 'make_rev_adj_list'.
*
*/
graph.prototype.make_rev_scc = function(scc) {
var rev_scc = [];
for (var i = 0; i < scc.length; i++) {
rev_scc.push([]);
}
for (var i = 0; i < scc.length; i++) {
for (var j = 0; j < scc[i][0].length; j++) {
rev_scc[scc[i][0][j]].push(i);
}
}
this.rev_scc = rev_scc;
}
/* SCC(Strong Conected Component) with DFS
* scc[i][0] contains edges.
* (e.g : scc[0][0] == [1,2] means scc[0] has directed edges to scc[1], scc[2])
* scc[i][j] (j >= 1) is a node of scc.
* scc[i][1], scc[i][2], scc[i][3] ... are nodes that belong to scc[i]
*
* reference : https://en.wikipedia.org/wiki/Kosaraju%27s_algorithm
*
*/
graph.prototype.make_scc = function() {
for (var node_id = 0; node_id < this.node_num; node_id++) {
if (!this.visited[node_id]) {
this.front_dfs(node_id);
}
}
this.visited.fill(0);
while (this.stack.length) {
var node_id = this.stack.pop();
if (!this.visited[node_id]) {
this.scc.push([]);
this.scc[this.scc.length - 1].push([]);
this.rev_dfs(node_id)
}
}
this.condensation();
}
/* A fuction that traverse graph in dfs (for recursive method).
*
*/
graph.prototype.front_dfs = function(node_id) {
this.visited[node_id] = 1;
for (var i = 1; i < this.adj_list[node_id].length; i++) {
var next_node_id = this.adj_list[node_id][i].node_id;
if (!this.visited[next_node_id]) {
this.front_dfs(next_node_id);
}
}
this.stack.push(node_id);
}
/* A fuction that traverse graph in dfs (for recursive method).
* To make scc, it uses rev_adj_list.
*
*/
graph.prototype.rev_dfs = function(node_id) {
this.visited[node_id] = 1;
this.adj_list[node_id].scc_id = this.scc.length - 1;
this.scc[this.scc.length - 1].push(this.rev_adj_list[node_id][0]);
for (var i = 1; i < this.rev_adj_list[node_id].length; i++) {
var next_node_id = this.rev_adj_list[node_id][i].node_id;
if (!this.visited[next_node_id]) {
this.rev_dfs(next_node_id)
}
}
}
/* A fuction that condenses redundant edges of scc.
*
*/
graph.prototype.condensation = function() {
var tmp = [];
for (var i = 0; i < this.scc.length; i++) {
tmp.length = 0;
for (var j = 1; j < this.scc[i].length; j++) {
var node = this.scc[i][j];
var o_arrow_num = this.adj_list[node.node_id].length;
for (var k = 1; k < o_arrow_num; k++) {
var o_node = this.adj_list[node.node_id][k].node_id
if (this.adj_list[o_node].scc_id != this.adj_list[node.node_id].scc_id) tmp.push(this.adj_list[o_node].scc_id);
}
}
this.scc[i][0] = tmp.filter(function(item, pos) {
return tmp.indexOf(item) == pos;
})
}
}
graph.prototype.detect_trap = function() {
var result = [];
for (var i = 0; i < this.scc.length; i++) {
if (this.scc.length != 1 && !this.scc[i][0].length) {
for (var j = 1; j < this.scc[i].length; j++) {
result.push(this.scc[i][j]);
}
}
}
return result;
}
graph.prototype.detect_loop = function() {
var result = [];
for (var i = 0; i < this.scc.length; i++) {
if (this.scc[i][0].length >= 2) {
for (var j = 1; j < this.scc[i].length; j++) {
result.push(this.scc[i][j]);
}
}
}
return result;
}
/* A fuction that detect unreachable elements
* Assume that someone push "down" button at current screen.
*
*/
graph.prototype.detect_unreachable = function() {
this.scc_visited = new Array(this.scc.length);
var index = this.adj_list[this.focusable.indexOf(document.body.spatialNavigationSearch("down"))].scc_id;
for (var i = 0; i < this.rev_scc[index].length; i++) {
this.unreachable_dfs(this.rev_scc[index][i]);
}
return this.result;
}
graph.prototype.unreachable_dfs = function(scc_id) {
this.scc_visited[scc_id] = 1;
for (var j = 1; j < this.scc[scc_id].length; j++) {
this.result.push(this.scc[scc_id][j]);
}
for (var i = 0; i < this.rev_scc[scc_id].length; i++) {
if (!this.scc_visited[scc_id]) {
this.unreachable_dfs(this.rev_scc[scc_id][i])
}
}
}
graph.prototype.detect_isolation = function() {
var result = [];
for (var i = 0; i < this.scc.length; i++) {
if (!this.scc[i][0].length && !this.rev_scc[i].length) {
for (var j = 1; j < this.scc[i].length; j++) {
result.push(this.scc[i][j]);
}
}
}
return result;
}