diff --git a/js/menu/filter/filter.js b/js/menu/filter/filter.js index e11d6ba2..93d9c08f 100644 --- a/js/menu/filter/filter.js +++ b/js/menu/filter/filter.js @@ -1,7 +1,8 @@ import { Link } from "../../objects.js"; import { drawAll } from "../../draw.js"; import { parentLinks, childrenLinks, infoBoxes, ctx } from "../../main.js"; -import { Range, Checkbox } from "./parameters.js"; +import { Range, Checkbox, buildCriteriaFunction } from "./parameters.js"; +import { reconnect } from "./reconnect.js"; const filterButton = document.getElementById("filter-button"); const openFilter = document.getElementById("open-filter"); @@ -36,49 +37,18 @@ parametersRange.forEach((parameter) => parameter.render(filters)); let bitsCheckbox = [23, 24, 25, 26, 27, 28, 29, 30]; -bitsCheckbox = bitsCheckbox.map((bit) => new Checkbox(bit)); +bitsCheckbox = bitsCheckbox.map((bit) => new Checkbox("simStatus", bit)); bitsCheckbox.forEach((checkbox) => checkbox.render(filters)); apply.addEventListener("click", () => { - let rangeFunctions = parametersRange.map((parameter) => - parameter.buildCondition() - ); - rangeFunctions = rangeFunctions.filter((fn) => fn); - rangeFunctions = rangeFunctions.reduce((acc, fn) => acc && fn, true); - - let bitsFunction = bitsCheckbox.map((checkbox) => checkbox.buildCondition()); - bitsFunction = bitsFunction.filter((fn) => fn); - - if (bitsFunction.length === 0) { - bitsFunction = true; - } else { - bitsFunction = bitsFunction.reduce( - (acc, fn) => { - return (particle) => acc(particle) || fn(particle); - }, - () => false - ); - } - - function criteriaFunction(particle) { - if ( - typeof bitsFunction === "function" && - typeof rangeFunctions === "function" - ) { - return rangeFunctions(particle) && bitsFunction(particle); - } - - if (typeof bitsFunction === "function") { - return bitsFunction(particle); - } + const rangeFunctions = Range.buildFilter(parametersRange); + const checkboxFunctions = Checkbox.buildFilter(bitsCheckbox); - if (typeof rangeFunctions === "function") { - return rangeFunctions(particle); - } - - return true; - } + const criteriaFunction = buildCriteriaFunction( + rangeFunctions, + checkboxFunctions + ); const [newParentLinks, newChildrenLinks, filteredParticles] = reconnect( criteriaFunction, @@ -106,67 +76,3 @@ reset.addEventListener("click", () => { checkbox.render(filters); }); }); - -function reconnect(criteriaFunction, parentLinks, childrenLinks, particles) { - const newParentLinks = []; - const newChildrenLinks = []; - const filteredParticles = []; - - for (const particle of particles) { - if (!particle) continue; - - if (!criteriaFunction(particle)) { - filteredParticles.push(null); - - const parentParticles = []; - const childrenParticles = []; - - for (const parent of particle.parents) { - if (criteriaFunction(particles[parent])) { - parentParticles.push(parent); - } - } - - for (const child of particle.children) { - if (criteriaFunction(particles[child])) { - childrenParticles.push(child); - } - } - - for (const parent of parentParticles) { - for (const child of childrenParticles) { - const linkToParent = new Link(newParentLinks.length, parent, child); - linkToParent.xShift = 3; - const linkToChild = new Link(newChildrenLinks.length, parent, child); - linkToChild.color = "#0A0"; - linkToChild.xShift = -3; - - newParentLinks.push(linkToParent); - newChildrenLinks.push(linkToChild); - } - } - } else { - filteredParticles.push(particle); - - for (const parentLinkId of particle.parentLinks) { - const parentLink = parentLinks[parentLinkId]; - if (!parentLink) continue; - const parent = particles[parentLink.from]; - if (criteriaFunction(parent)) { - newParentLinks.push(parentLink); - } - } - - for (const childrenLinkId of particle.childrenLinks) { - const childrenLink = childrenLinks[childrenLinkId]; - if (!childrenLink) continue; - const child = particles[childrenLink.to]; - if (criteriaFunction(child)) { - newChildrenLinks.push(childrenLink); - } - } - } - } - - return [newParentLinks, newChildrenLinks, filteredParticles]; -} diff --git a/js/menu/filter/parameters.js b/js/menu/filter/parameters.js index 5ac6d2c8..60f1d6d6 100644 --- a/js/menu/filter/parameters.js +++ b/js/menu/filter/parameters.js @@ -1,21 +1,31 @@ class FilterParameter { - constructor(name) { - this.name = name; + property; + + constructor(property) { + this.property = property; } render(container) {} buildCondition() {} + + static parametersFunctions(parameters) { + const functions = parameters.map((parameter) => parameter.buildCondition()); + return functions.filter((fn) => fn); + } } export class Range extends FilterParameter { - constructor(name) { - super(name); + min; + max; + + constructor(property) { + super(property); } render(container) { const label = document.createElement("label"); - label.textContent = this.name; + label.textContent = this.property; container.appendChild(label); const inputMin = document.createElement("input"); @@ -45,11 +55,11 @@ export class Range extends FilterParameter { return (particle) => { if (particle) { - if (particle[this.name] < this.min) { + if (particle[this.property] < this.min) { return false; } - if (particle[this.name] > this.max) { + if (particle[this.property] > this.max) { return false; } @@ -57,11 +67,27 @@ export class Range extends FilterParameter { } }; } + + static buildFilter(parametersRange) { + const rangeFunctions = Range.parametersFunctions(parametersRange); + + const func = rangeFunctions.reduce( + (acc, fn) => { + return (particle) => acc(particle) && fn(particle); + }, + () => true + ); + + return func; + } } export class Checkbox extends FilterParameter { - constructor(name) { - super(name); + value; + + constructor(property, value) { + super(property); + this.value = value; } render(container) { @@ -69,7 +95,7 @@ export class Checkbox extends FilterParameter { container.appendChild(div); const label = document.createElement("label"); - label.textContent = `Sim Status: ${this.name}`; + label.textContent = `${this.property}: ${this.value}`; div.appendChild(label); const input = document.createElement("input"); @@ -88,6 +114,34 @@ export class Checkbox extends FilterParameter { buildCondition() { if (!this.checked) return null; - return (particle) => particle.simStatus === this.name; + return (particle) => particle[this.property] === this.value; } + + static buildFilter(parametersCheckbox) { + const checkboxFunctions = Checkbox.parametersFunctions(parametersCheckbox); + + if (checkboxFunctions.length === 0) return () => true; + + const func = checkboxFunctions.reduce( + (acc, fn) => { + return (particle) => acc(particle) || fn(particle); + }, + () => false + ); + + return func; + } +} + +export function buildCriteriaFunction(...functions) { + const filterFunctions = functions.filter((fn) => typeof fn === "function"); + + const finalFunction = filterFunctions.reduce( + (acc, fn) => { + return (particle) => acc(particle) && fn(particle); + }, + () => true + ); + + return (particle) => finalFunction(particle); } diff --git a/js/menu/filter/reconnect.js b/js/menu/filter/reconnect.js new file mode 100644 index 00000000..5636d0a8 --- /dev/null +++ b/js/menu/filter/reconnect.js @@ -0,0 +1,68 @@ +export function reconnect( + criteriaFunction, + parentLinks, + childrenLinks, + particles +) { + const newParentLinks = []; + const newChildrenLinks = []; + const filteredParticles = []; + + for (const particle of particles) { + if (!particle) continue; + + if (!criteriaFunction(particle)) { + filteredParticles.push(null); + + const parentParticles = []; + const childrenParticles = []; + + for (const parent of particle.parents) { + if (criteriaFunction(particles[parent])) { + parentParticles.push(parent); + } + } + + for (const child of particle.children) { + if (criteriaFunction(particles[child])) { + childrenParticles.push(child); + } + } + + for (const parent of parentParticles) { + for (const child of childrenParticles) { + const linkToParent = new Link(newParentLinks.length, parent, child); + linkToParent.xShift = 3; + const linkToChild = new Link(newChildrenLinks.length, parent, child); + linkToChild.color = "#0A0"; + linkToChild.xShift = -3; + + newParentLinks.push(linkToParent); + newChildrenLinks.push(linkToChild); + } + } + } else { + filteredParticles.push(particle); + + for (const parentLinkId of particle.parentLinks) { + const parentLink = parentLinks[parentLinkId]; + if (!parentLink) continue; + const parent = particles[parentLink.from]; + if (criteriaFunction(parent)) { + newParentLinks.push(parentLink); + } + } + + for (const childrenLinkId of particle.childrenLinks) { + const childrenLink = childrenLinks[childrenLinkId]; + if (!childrenLink) continue; + const child = particles[childrenLink.to]; + if (criteriaFunction(child)) { + newChildrenLinks.push(childrenLink); + } + } + } + } + + return [newParentLinks, newChildrenLinks, filteredParticles]; +} diff --git a/test/filter.test.js b/test/filter.test.js new file mode 100644 index 00000000..9941c3aa --- /dev/null +++ b/test/filter.test.js @@ -0,0 +1,187 @@ +import { reconnect } from "../js/menu/filter/reconnect.js"; +import { InfoBox, Link } from "../js/objects.js"; +import { + Range, + Checkbox, + buildCriteriaFunction, +} from "../js/menu/filter/parameters.js"; + +const parentLinks = []; +const childrenLinks = []; +const particles = []; + +beforeAll(() => { + for (let i = 0; i < 5; i++) { + const particle = new InfoBox(i); + particle.momentum = i * 100; + particle.charge = i; + particle.mass = i * 10; + particle.simStatus = i + 23; + particles.push(particle); + } + + parentLinks.push(new Link(0, 0, 1)); + parentLinks.push(new Link(1, 0, 2)); + parentLinks.push(new Link(2, 2, 4)); + parentLinks.push(new Link(3, 0, 4)); + + childrenLinks.push(new Link(0, 0, 1)); + childrenLinks.push(new Link(1, 0, 2)); + childrenLinks.push(new Link(2, 1, 3)); + childrenLinks.push(new Link(3, 2, 4)); + childrenLinks.push(new Link(4, 3, 4)); + + particles[0].children = [1, 2, 4]; + particles[0].childrenLinks = [0, 1, 3]; + + particles[1].parents = [0]; + particles[1].children = [3]; + particles[1].parentLinks = [0]; + particles[1].childrenLinks = [2]; + + particles[2].parents = [0]; + particles[2].children = [4]; + particles[2].parentLinks = [1]; + particles[2].childrenLinks = [3]; + + particles[3].parents = [1]; + particles[3].children = [4]; + + particles[4].parents = [0, 2, 3]; + particles[4].parentLinks = [3, 2, 4]; +}); + +describe("filter by ranges", () => { + it("show all particles when no range filter is applied", () => { + const time = new Range("time"); + const rangeFilters = Range.buildFilter([time]); + const criteriaFunction = buildCriteriaFunction(rangeFilters); + + const [newParentLinks, newChildrenLinks, filteredParticles] = reconnect( + criteriaFunction, + parentLinks, + childrenLinks, + particles + ); + }); + + it("filter by a single range parameter", () => { + const momentum = new Range("momentum"); + momentum.min = 10; + momentum.max = 1000; + const rangeFilters = Range.buildFilter([momentum]); + const criteriaFunction = buildCriteriaFunction(rangeFilters); + + const [newParentLinks, newChildrenLinks, filteredParticles] = reconnect( + criteriaFunction, + parentLinks, + childrenLinks, + particles + ); + }); + + it("filter by a combination of ranges", () => { + const charge = new Range("charge"); + charge.min = 0; + const mass = new Range("mass"); + mass.min = 1; + mass.max = 1000; + const rangeFilters = Range.buildFilter([mass, charge]); + const criteriaFunction = buildCriteriaFunction(rangeFilters); + + const [newParentLinks, newChildrenLinks, filteredParticles] = reconnect( + criteriaFunction, + parentLinks, + childrenLinks, + particles + ); + }); +}); + +describe("filter by checkboxes", () => { + it("show all particles when no checkbox filter is applied", () => { + const simulatorStatus = new Checkbox("simStatus", 26); + const checkboxFilters = Checkbox.buildFilter([simulatorStatus]); + const criteriaFunction = buildCriteriaFunction(checkboxFilters); + + const [newParentLinks, newChildrenLinks, filteredParticles] = reconnect( + criteriaFunction, + parentLinks, + childrenLinks, + particles + ); + }); + + it("filter by a single checkbox", () => { + const simulatorStatus = new Checkbox("simStatus", 23); + simulatorStatus.checked = true; + const checkboxFilters = Checkbox.buildFilter([simulatorStatus]); + const criteriaFunction = buildCriteriaFunction(checkboxFilters); + + const [newParentLinks, newChildrenLinks, filteredParticles] = reconnect( + criteriaFunction, + parentLinks, + childrenLinks, + particles + ); + }); + + it("filter by a combination of checkboxes", () => { + const simulatorStatus1 = new Checkbox("simStatus", 24); + simulatorStatus1.checked = true; + const simulatorStatus2 = new Checkbox("simStatus", 25); + simulatorStatus2.checked = true; + const checkboxFilters = Checkbox.buildFilter([ + simulatorStatus1, + simulatorStatus2, + ]); + const criteriaFunction = buildCriteriaFunction(checkboxFilters); + + const [newParentLinks, newChildrenLinks, filteredParticles] = reconnect( + criteriaFunction, + parentLinks, + childrenLinks, + particles + ); + }); +}); + +describe("filter by ranges and checkboxes", () => { + it("show all particles when no kind of filter is applied", () => { + const charge = new Range("charge"); + const simulatorStatus = new Checkbox("simStatus", 26); + const rangeFilters = Range.buildFilter([charge]); + const checkboxFilters = Checkbox.buildFilter([simulatorStatus]); + const criteriaFunction = buildCriteriaFunction( + rangeFilters, + checkboxFilters + ); + + const [newParentLinks, newChildrenLinks, filteredParticles] = reconnect( + criteriaFunction, + parentLinks, + childrenLinks, + particles + ); + }); + + it("filter by a combination of ranges and checkboxes", () => { + const charge = new Range("charge"); + charge.min = 0; + const simulatorStatus = new Checkbox("simStatus", 23); + simulatorStatus.checked = true; + const rangeFilters = Range.buildFilter([charge]); + const checkboxFilters = Checkbox.buildFilter([simulatorStatus]); + const criteriaFunction = buildCriteriaFunction( + rangeFilters, + checkboxFilters + ); + + const [newParentLinks, newChildrenLinks, filteredParticles] = reconnect( + criteriaFunction, + parentLinks, + childrenLinks, + particles + ); + }); +});