From 0424536ce190c86039482c304fca84a59613ed3a Mon Sep 17 00:00:00 2001 From: Lucas Fernandes de Oliveira <lfo14@inf.ufpr.br> Date: Thu, 6 Jul 2017 15:56:25 -0300 Subject: [PATCH] Issue #22: Change cover algorithm to use graph Signed-off-by: Lucas Fernandes de Oliveira <lfo14@inf.ufpr.br> --- src/core/engine.spec.ts | 4 +- src/core/engine.ts | 125 +++------- src/util/graph.spec.ts | 336 +++++++++++++++++++++++++++ src/util/graph.ts | 494 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 862 insertions(+), 97 deletions(-) create mode 100644 src/util/graph.spec.ts create mode 100644 src/util/graph.ts diff --git a/src/core/engine.spec.ts b/src/core/engine.spec.ts index 69e1d4d3..2bdbb749 100644 --- a/src/core/engine.spec.ts +++ b/src/core/engine.spec.ts @@ -140,7 +140,7 @@ describe("engine class", () => { return optView.childViews[0].dimensions.some((item) => item.name === subdim[0].name); }); expect(optimalView).satisfy((optView: View) => { - return optView.childViews[0].dimensions.some((item) => item.name === subdim[0].name); + return optView.childViews[0].dimensions.some((item) => item.name === subdim[1].name); }); }); @@ -177,7 +177,7 @@ describe("engine class", () => { } catch (e){ error = true; - expect(e.message).to.be.equal("Engine sub-dimention " + subdim[3].name + " with no parent"); + expect(e.message).to.be.equal("Engine views cannot cover the query"); } expect(error).to.be.true; diff --git a/src/core/engine.ts b/src/core/engine.ts index af1640a8..39fbfbb5 100644 --- a/src/core/engine.ts +++ b/src/core/engine.ts @@ -20,29 +20,43 @@ import { Dimension } from "./dimension"; import { Metric } from "./metric"; -import { View, ChildView } from "./view"; +import { View } from "./view"; import { Query } from "../common/query"; -import { RelationType } from "../common/types"; +import { Graph } from "../util/graph"; export class Engine { private views: View[] = []; private metrics: Metric[] = []; private dimensions: Dimension[] = []; + private graph: Graph; - constructor () { } + constructor () { + this.views = []; + this.metrics = []; + this.dimensions = []; + this.graph = new Graph(); + } public getViews() { return this.views; } public addView(view: View) { - this.views.push(view); - return view; + if (this.graph.addView(view)) { + this.views.push(view); + return view; + } + + return null; } public addMetric(metric: Metric) { - this.metrics.push(metric); - return metric; + if (this.graph.addMetric(metric)) { + this.metrics.push(metric); + return metric; + } + + return null; } public getMetricByName(name: string) { @@ -56,8 +70,12 @@ export class Engine { } public addDimension(dimension: Dimension) { - this.dimensions.push(dimension); - return dimension; + if (this.graph.addDimension(dimension)) { + this.dimensions.push(dimension); + return dimension; + } + + return null; } public getDimensionByName(name: string) { @@ -75,92 +93,9 @@ export class Engine { } private selectOptimalView (q: Query) { - let metUncovered = q.metrics.map((met) => met); - let dimUncovered = q.dimensions.map((dim) => dim); - let optimalViews: ChildView[] = []; - let activeViews = this.getViews(); - - // run this block until all metrics and dimmensions are covered - while (metUncovered.length > 0 || dimUncovered.length > 0) { - let bestView: ChildView; - let coverLength = metUncovered.length + dimUncovered.length; - let shortestDistance = coverLength + 1; - - // remove views from the activeViews if they don't intersect - // with the objective - activeViews = activeViews.filter((view: View) => { - let metIntersection = metUncovered.filter((met: Metric) => { - return view.metrics.some((item) => item.name === met.name); - }); - - let dimIntersection = dimUncovered.filter((dim: Dimension) => { - let r: boolean = view.dimensions.some((item) => item.name === dim.name); - while (!r && dim.relation !== RelationType.NONE) { - if (dim.parent === null) { - throw new Error("Engine sub-dimention " + dim.name + " with no parent"); - } - r = view.dimensions.some((item) => item.name === dim.parent.name); - dim = dim.parent; - } - return r; - }); - - let intersection = metIntersection.length + dimIntersection.length; - - if (intersection > 0) { - let distance = coverLength - intersection; - - if (distance < shortestDistance) { - bestView = { - view: view, - metrics: metIntersection, - dimensions: dimIntersection - }; - shortestDistance = distance; - } - - else if (distance === shortestDistance && - view.dimensions.length < bestView.dimensions.length) { - // priorizes views with less dimensions - bestView = { - view: view, - metrics: metIntersection, - dimensions: dimIntersection - }; - } - - return true; - } - - /*If the intersection is empty, - remove this element from future searches*/ - return false; - }); - - if (shortestDistance === coverLength + 1) { - throw new Error("Engine views cannot cover the query"); - } - - optimalViews.push(bestView); - - // remove metrics and dimensions corvered by the bestView from the - // objective (i.e. the object is already met for those metrics/dimensions) - - metUncovered = metUncovered.filter((met: Metric) => { - return !bestView.metrics.some((item) => item.name === met.name); - }); - - dimUncovered = dimUncovered.filter((dim: Dimension) => { - let r: boolean = bestView.dimensions.some((item) => item.name === dim.name); - while (!r && dim.relation !== RelationType.NONE) { - if (dim.parent === null) { - throw new Error("Engine sub-dimention " + dim.name + " with no parent"); - } - r = bestView.dimensions.some((item) => item.name === dim.parent.name); - dim = dim.parent; - } - return !r; - }); + let optimalViews = this.graph.cover(q.metrics, q.dimensions); + if (optimalViews.length === 0) { + throw new Error ("Engine views cannot cover the query"); } // If all the metrics and dimensions are the same and only exist one child view diff --git a/src/util/graph.spec.ts b/src/util/graph.spec.ts new file mode 100644 index 00000000..17de06d0 --- /dev/null +++ b/src/util/graph.spec.ts @@ -0,0 +1,336 @@ +/* + * Copyright (C) 2017 Centro de Computacao Cientifica e Software Livre + * Departamento de Informatica - Universidade Federal do Parana + * + * This file is part of blendb. + * + * blendb is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * blendb is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with blendb. If not, see <http://www.gnu.org/licenses/>. + */ + +import { expect } from "chai"; + +import { Metric } from "../core/metric"; +import { Dimension } from "../core/dimension"; +import { View } from "../core/view"; +import { Graph } from "./graph"; +import { AggregationType, RelationType } from "../common/types"; + +describe("graph class", () => { + + it("should not create 2 vertices with the same dimension", () => { + let g = new Graph(); + let dim = new Dimension({name: "dim:test", dataType: "string"}); + + expect(g.addDimension(dim)).to.be.true; + expect(g.addDimension(dim)).to.be.false; + }); + + it("should not create 2 vertices with same metric", () => { + let g = new Graph(); + let met = new Metric({ + name: "met:test", + aggregation: AggregationType.SUM, + dataType: "string" + }); + + expect(g.addMetric(met)).to.be.true; + expect(g.addMetric(met)).to.be.false; + }); + + it("should not create a vertex with a invalid dimension", () => { + let g = new Graph(); + let dim: Dimension; + + expect(g.addDimension(dim)).to.be.false; + }); + + it("should not create a vertex with a invalid metric", () => { + let g = new Graph(); + let met: Metric; + + expect(g.addMetric(met)).to.be.false; + }); + + it("should not create a vertex with a subdimension when parent is not a vertex", () => { + let g = new Graph(); + let dim = new Dimension({name: "dim:test", dataType: "string"}); + let subdim = new Dimension({ + name: "dim:sub_test", + dataType: "string", + relation: RelationType.MONTH, + parent: dim + }); + + expect(g.addDimension(subdim)).to.be.false; + }); + + it("should add a set of views", () => { + let dims = [ + new Dimension({name: "dim:0", dataType: "string"}), + new Dimension({name: "dim:1", dataType: "string"}), + new Dimension({name: "dim:2", dataType: "string"}), + new Dimension({name: "dim:3", dataType: "string"}) + ]; + + let g = new Graph(); + + for (let i = 0; i < dims.length; ++i) { + expect(g.addDimension(dims[i])).to.be.true; + } + + let views = [ + new View({ + metrics: [], + dimensions: [dims[0], dims[1]], + materialized: true + }), + new View({ + metrics: [], + dimensions: [dims[2], dims[3]], + materialized: true + }), + new View({ + metrics: [], + dimensions: dims, + materialized: true + }) + ]; + + for (let i = 0; i < views.length; ++i) { + expect(g.addView(views[i])).to.be.true; + } + }); + + it("should not add a view twice", () => { + let dims = [ + new Dimension({name: "dim:0", dataType: "string"}), + new Dimension({name: "dim:1", dataType: "string"}), + ]; + + let g = new Graph(); + + for (let i = 0; i < dims.length; ++i) { + expect(g.addDimension(dims[i])).to.be.true; + } + + let view = new View({ + metrics: [], + dimensions: [dims[0], dims[1]], + materialized: true + }); + + expect(g.addView(view)).to.be.true; + expect(g.addView(view)).to.be.false; + }); + + it("should not add views when metrics and dimensions are not vertices", () => { + let dims = [ + new Dimension({name: "dim:0", dataType: "string"}), + new Dimension({name: "dim:1", dataType: "string"}), + new Dimension({name: "dim:2", dataType: "string"}) + ]; + + let g = new Graph(); + + expect(g.addDimension(dims[0])).to.be.true; + + let view = new View({ + metrics: [], + dimensions: dims, + materialized: true + }); + + expect(g.addView(view)).to.be.false; + }); + + it("should not add invalid views", () => { + let g = new Graph(); + expect(g.addView(null)).to.be.false; + }); + + it("should add only once a view with only one vertex", () => { + let dim = new Dimension({name: "dim:0", dataType: "string"}); + + let g = new Graph(); + + expect(g.addDimension(dim)).to.be.true; + + let view = new View({ + metrics: [], + dimensions: [dim], + materialized: true + }); + + expect(g.addView(view)).to.be.true; + expect(g.addView(view)).to.be.false; + }); + + it("should create a cover for a single vertex", () => { + let dim = new Dimension({name: "dim:0", dataType: "string"}); + + let g = new Graph(); + + expect(g.addDimension(dim)).to.be.true; + + let view = new View({ + metrics: [], + dimensions: [dim], + materialized: true + }); + + expect(g.addView(view)).to.be.true; + let children = g.cover([], [dim]); + expect(children).to.be.an("array"); + expect(children).to.have.length(1); + expect(children[0].view.id).to.be.equal(view.id); + }); + + it("should create a cover for several vertices", () => { + let dims = [ + new Dimension({name: "dim:0", dataType: "string"}), + new Dimension({name: "dim:1", dataType: "string"}), + new Dimension({name: "dim:2", dataType: "string"}) + ]; + + let mets = [ + new Metric({name: "met:0", dataType: "integer", aggregation: AggregationType.SUM}), + new Metric({name: "met:1", dataType: "integer", aggregation: AggregationType.AVG}), + new Metric({name: "met:2", dataType: "integer", aggregation: AggregationType.AVG}) + ]; + + let g = new Graph(); + + for (let i = 0; i < 3; ++i) { + expect(g.addDimension(dims[i])).to.be.true; + expect(g.addMetric(mets[i])).to.be.true; + } + + let views = [ + new View({ + metrics: [mets[0]], + dimensions: [dims[0]], + materialized: true + }), + new View({ + metrics: [mets[1]], + dimensions: [dims[1]], + materialized: true + }), + new View({ + metrics: [mets[2]], + dimensions: [dims[2]], + materialized: true + }), + new View({ + metrics: [], + dimensions: dims, + materialized: true + }), + new View({ + metrics: mets, + dimensions: dims, + materialized: true + }), + new View({ + metrics: [mets[0], mets[1]], + dimensions: [dims[0], dims[1]], + materialized: true + }), + ]; + + for (let i = 0; i < views.length; ++i) { + expect(g.addView(views[i])).to.be.true; + } + + let children = g.cover([mets[0], mets[1]], [dims[0], dims[1]]); + expect(children).to.be.an("array"); + expect(children).to.have.length(1); + expect(children[0].view.id).to.be.equal(views[views.length - 1].id); + }); + + it("should create a cover with sub dimensions", () => { + let dim = new Dimension({name: "dim:0", dataType: "date"}); + let dims = [ + dim, + new Dimension({ + name: "subdim:0", + dataType: "string", + parent: dim, + relation: RelationType.MONTH + }), + new Dimension({ + name: "subdim:1", + dataType: "string", + parent: dim, + relation: RelationType.DAY + }), + ]; + + let g = new Graph(); + + for (let i = 0; i < 3; ++i) { + expect(g.addDimension(dims[i])).to.be.true; + } + + let view = new View({ + metrics: [], + dimensions: [dims[0]], + materialized: true + }); + + expect(g.addView(view)).to.be.true; + + let children = g.cover([], [dims[1], dims[2]]); + expect(children).to.be.an("array"); + expect(children).to.have.length(1); + expect(children[0].view.id).to.be.equal(view.id); + }); + + it("should return empty when try to cover a empty list", () => { + let dim = new Dimension({name: "dim:0", dataType: "date"}); + let dims = [ + dim, + new Dimension({ + name: "subdim:0", + dataType: "string", + parent: dim, + relation: RelationType.MONTH + }), + new Dimension({ + name: "subdim:1", + dataType: "string", + parent: dim, + relation: RelationType.DAY + }), + ]; + + let g = new Graph(); + + for (let i = 0; i < 3; ++i) { + expect(g.addDimension(dims[i])).to.be.true; + } + + let view = new View({ + metrics: [], + dimensions: [dims[0]], + materialized: true + }); + + expect(g.addView(view)).to.be.true; + + let children = g.cover([], []); + expect(children).to.be.an("array"); + expect(children).to.be.empty; + }); +}); diff --git a/src/util/graph.ts b/src/util/graph.ts new file mode 100644 index 00000000..7ee9c094 --- /dev/null +++ b/src/util/graph.ts @@ -0,0 +1,494 @@ +/* + * Copyright (C) 2017 Centro de Computacao Cientifica e Software Livre + * Departamento de Informatica - Universidade Federal do Parana + * + * This file is part of blendb. + * + * blendb is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * blendb is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with blendb. If not, see <http://www.gnu.org/licenses/>. + */ + +import { View, ChildView } from "../core/view"; +import { Metric } from "../core/metric"; +import { Dimension } from "../core/dimension"; + +enum State { + UNVISITED, + VISITED +} + +/* + Class (interface) for a vertex in the graph + Created using a dimension or metric + Several of the attributes are used just for + search proposes like parent and state. +*/ + +interface Vertex { + id: string; // Name of vertex + neighbors: { [key: string]: Edge[] }; + state: State; // During the search the vertex has a state + parent: Vertex; // During the search, what vertex "find" this + isSubDimension: boolean; // During insertion, was subdimension + dimensionParent: Vertex; // If is a subdimension, the vertex for that + dimension: Dimension; // If is a dimension, tha dimension that the vertex represents + metric: Metric; // If is a metric, the metric that the vertexrepresents +} + +/* + Class (interface) dor a edge of the Graph + Normaly is possible to use a list of vertices + to represent the edges, however this edges have some + information different from the vertex, so a class + is needed. + + Edges represent views or parent relationship with + dimensions and subdimensions. +*/ +interface Edge { + isView: boolean; // True if edge represent view + view: View; // The view if isView is True + subDimension: Dimension; // The subDimension if isView is False +} +/* + Graph Class + Used to convert the Views Representation (Tables/Relations) + into a graph representation of the database schema. + Is not generic Graph, is not possible create vertex and + edges at will, only with metrics, dimensions and views. + + Struct: + There is a vertex for each Metric and Dimension inserted + in the graph. + The graph is directed, but mostly vertices has edges in + both directions. + There is a edge between 2 vertices i and j in 3 cases: + I) if i and j are in the same view, there is a edge (i, j) + and (j, i) + II) if i is parent of the subdimention j , there is a + edge (i, j) + III) If a view has only one vertex, there is a loop (i, i) + + So there is a edge if vertices are in the same view or + there is a parent relationship between vertices. +*/ +export class Graph { + private vertices: Vertex[]; // vertices of the graph + private verticeMap: { [key: string]: Vertex }; + // Map where key is the vertex id and the value is the vertex + + public constructor () { + this.vertices = []; + this.verticeMap = {}; + } + + /* + Adds a metric + returns true if the metric was inserted in the graph + */ + public addMetric(met: Metric): boolean { + if (!met) { + return false; + } + + if (typeof this.verticeMap[met.name] !== "undefined") { + // If the metric was already added, not add again + return false; + } + else { + let v: Vertex = { + id: met.name, + neighbors: {}, + state: State.UNVISITED, + parent: null, + isSubDimension: false, + dimensionParent: null, + dimension: null, + metric: met + }; + this.vertices.push(v); + this.verticeMap[met.name] = v; + return true; + } + } + + /* + Adds a dimension + returns true if the dimension was inserted in the graph + */ + public addDimension(dim: Dimension): boolean { + if (!dim) { + return false; + } + + if (typeof this.verticeMap[dim.name] !== "undefined") { + // If the dimension was already added, not add again + return false; + } + else { + + if (dim.parent === null) { + // If is not a subdimension, insert normaly + let v: Vertex = { + id: dim.name, + neighbors: {}, + state: State.UNVISITED, + parent: null, + isSubDimension: false, + dimensionParent: null, + dimension: dim, + metric: null + }; + this.vertices.push(v); + this.verticeMap[dim.name] = v; + return true; + } + + else { + // If is a sub dimension get the parent + let p = this.verticeMap[dim.parent.name]; + if (typeof p === "undefined") { + // If the parent was not inserted, not insert + return false; + } + + // Otherwise create a vertex and a edge for the parent + // relationship. + let v: Vertex = { + id: dim.name, + neighbors: {}, + state: State.UNVISITED, + parent: null, + isSubDimension: true, + dimensionParent: p, + dimension: dim, + metric: null + }; + this.vertices.push(v); + this.verticeMap[dim.name] = v; + p.neighbors[dim.name] = [{ + isView: false, + view: null, + subDimension: dim + }]; + return true; + + } + } + } + + /* + Add a View + Get the metrics and dimensions of this view + and create edges between then + returns true when the view in inserted (create all the + edges needed) and false otherwise + If returns false any edge was created, if true all edges + were created. + */ + public addView (view: View): boolean { + if (!view) { + return false; + } + + let vertices = view.dimensions.map((dim) => dim.name); + vertices = vertices.concat(view.metrics.map((met) => met.name)); + // Concat metrics and diemnsions to get all vertices ids + if (vertices.length === 1) { + // If the view has only one vertex, create a loop + if (this.checkEdge(vertices[0], vertices[0], view)) { + this.edge(vertices[0], vertices[0], view); + return true; + } + + return false; + } + else { + let r = true; + // Check if all the edges can be created + for (let i = 0; i < vertices.length; ++i) { + for (let j = (i + 1); j < vertices.length; ++j) { + if (r) { + r = this.checkEdge(vertices[i], vertices[j], view); + } + } + } + + // If at least one can not, not create any one + if (!r) { + return false; + } + + // Otherwise create then all + for (let i = 0; i < vertices.length; ++i) { + for (let j = (i + 1); j < vertices.length; ++j) { + this.edge(vertices[i], vertices[j], view); + } + } + + return true; + } + + } + + /* + Check if a edge can be create + return false when at least one vertex does not exist in the graph + or the edge already exists + returns true otherwise + */ + private checkEdge(idV: string, idU: string, value: View): boolean { + let v: Vertex = this.verticeMap[idV]; + let u: Vertex = this.verticeMap[idU]; + + if (typeof v !== "undefined" && typeof u !== "undefined") { + // Check if the vertices exist + if (typeof v.neighbors[idU] !== "undefined") { + // Check if a edge exist + if (v.neighbors[idU].some((item) => item.view.id === value.id)) { + // Check the id of the edge + return false; + } + } + return true; + } + else { + return false; + } + } + + /* + Create a edge between 2 vertices + + Edges are a kind of special, when say create a edge is + in fact put a index on the array of a edge. + + The edges have a array of ids, when you try to create a edge + and the edge doen not exist, create the edge and a array with + a lonely index in it. If the edge exist but the id is not in + the array, adds the id and checkEdge would return true, + if the id already exists return false + */ + private edge(idV: string, idU: string, value: View) { + // Assuming that checkEdge is called first + let v: Vertex = this.verticeMap[idV]; + let u: Vertex = this.verticeMap[idU]; + + if (typeof v.neighbors[idU] === "undefined") { + v.neighbors[idU] = [{ isView: true, view: value, subDimension: null }]; + } + else { + v.neighbors[idU].push({ isView: true, view: value, subDimension: null }); + } + if (typeof u.neighbors[idV] === "undefined") { + u.neighbors[idV] = [{ isView: true, view: value, subDimension: null }]; + } + else { + u.neighbors[idV].push({ isView: true, view: value, subDimension: null }); + } + } + + /* + Given a list of metrics and dimensions returns a set + of ChildViews that can be used to create a query that + returns the data asked. + + If this set cannot be created, throws a error + */ + public cover(metrics: Metric[], dimensions: Dimension[]): ChildView[] { + let output: ChildView[] = []; + let verticesIds = metrics.map((met) => met.name); + verticesIds = verticesIds.concat(dimensions.map((dim) => dim.name)); + for (let i = 0; i < this.vertices.length; ++i) { + // Get the vertices and set their status and parent + this.vertices[i].state = State.UNVISITED; + this.vertices[i].parent = null; + } + + // Check if dimension/metric is in the graph + if (verticesIds.some((item) => typeof this.verticeMap[item] === "undefined")) { + return []; + } + + // Choose one vertex as root of a bfs + // (Breadth-First-Search) + let root: Vertex = this.verticeMap[verticesIds.shift()]; + if (!root) { + return []; + } + while (root.isSubDimension) { + root = root.dimensionParent; + } + root.state = State.VISITED; + let queue: Vertex[] = [root]; + while (queue.length > 0) { + let v: Vertex = queue.shift(); + for (let key in v.neighbors) { + let u: Vertex = this.verticeMap[key]; + if (u.state === State.UNVISITED) { + // Mark all vertices visited by the search + u.state = State.VISITED; + u.parent = v; + queue.push(u); + } + } + } + + while (verticesIds.length > 0 && + this.verticeMap[verticesIds[0]].state === State.VISITED) { + verticesIds.shift(); + } + + // Removes from the list all the vertices marked + // If at least one vertex in not marked, the graph is + // no connect and a query cannot be create + if (verticesIds.length > 0) { + return []; + } + + // Recover the list of vertices + let dimToCover = dimensions.map((dim) => dim); + let metToCover = metrics.map((met) => met); + verticesIds = metrics.map((met) => met.name); + verticesIds = verticesIds.concat(dimensions.map((dim) => dim.name)); + while (verticesIds.length > 0) { + // Choose a vertex and walks to his ancestors until + // reach the root, when walking this path choose + // the views based on the edges of the path and + // the views already picked. + let v = this.verticeMap[verticesIds.shift()]; + while (v.parent !== null) { + let options = v.parent.neighbors[v.id].filter((edge) => { + // Filther the edges ids to get only the ones that + // represent views + return edge.isView; + }).map((edge) => edge.view); + // Check if there is a intersection between output and options + if (output.some((child) => options.some((view) => child.view === view))) { + // If there is a intersection, does not pick any new view + v = v.parent; + continue; + } + + if (options.length === 0) { + // If there are no view options this means that a + // subdimension edge was taken, so do not pick any + // new view. + v = v.parent; + continue; + } + + // Selects the best edge + let bestEdge = this.pickEdge (options, metToCover, dimToCover); + output.push(bestEdge); + + // Remove from the list of metrics and dimensions the + // covered by the selected view + metToCover = metToCover.filter((item) => { + return !bestEdge.metrics.some((met) => met === item); + }); + dimToCover = dimToCover.filter((item) => { + return !bestEdge.dimensions.some((dim) => dim === item); + }); + // walk back on the path + v = v.parent; + } + } + + let views: View[] = []; + // Pick all views that contain the root vertex + for (let i in root.neighbors) { + views = views.concat(root.neighbors[i].filter((item) => { + return item.isView; + }).map((item) => item.view)); + } + + // Check if some of its views were picked + if (output.some((child) => views.some((view) => child.view === view))) { + // If yes, do nothing and return the actual set + return output; + } + + // If any were picked, choose one + let bestEdge = this.pickEdge (views, metToCover, dimToCover); + output.push(bestEdge); + + // This is required for the case that only one vertex is required + // Or only subdimensions edges are taken + + return output; + } + + /* + From a edge, coohse the best view, based on the metric and dimensions + that are not cover yet, return a childView. + The algorithm chooses the view that covers more metrics and dimensions + that are not covered yet, if there is a tie chooses the one with + less dimensions, if tie again, the earliest in the list. + */ + private pickEdge (views: View[], metToCover: Metric[], dimToCover: Dimension[]): ChildView { + // Picks the first option as the best one until now + let bestView = views[0]; + let bestCoverMet = metToCover.filter((met) => { + return bestView.metrics.some((item) => item === met); + }); + let bestCoverDim = dimToCover.filter((dim) => { + let r = bestView.dimensions.some((item) => item === dim); + // If is a subdimension that must be cover, check if + // any ancestor can cover it + while (!r && dim.parent !== null) { + dim = dim.parent; + r = bestView.dimensions.some((item) => item === dim); + } + return r; + }); + + // Get the score + let bestCover = bestCoverMet.length + bestCoverDim.length; + for (let i = 1; i < views.length; ++i) { + let actual = views[i]; + let actualCoverMet = metToCover.filter((met) => { + return actual.metrics.some((item) => item === met); + }); + let actualCoverDim = dimToCover.filter((dim) => { + let r = actual.dimensions.some((item) => item === dim); + while (!r && dim.parent !== null) { + // If is a subdimension that must be cover, check if + // any ancestor can cover it + dim = dim.parent; + r = actual.dimensions.some((item) => item === dim); + } + return r; + }); + + // Checks if the new option is better than the best until now + let actualCover = actualCoverMet.length + actualCoverDim.length; + if (actualCover < bestCover || + (actualCover === bestCover && + bestView.dimensions.length > actual.dimensions.length)) { + bestCover = actualCover; + bestCoverMet = actualCoverMet; + bestCoverDim = actualCoverDim; + bestView = actual; + } + } + + return { + view: bestView, + metrics: bestCoverMet, + dimensions: bestCoverDim + }; + + } + +} -- GitLab