comparison src/layout/Layout.ts @ 106:2468f2227d22

make src/layout/ and src/render/ separation
author drewp@bigasterisk.com
date Sun, 13 Mar 2022 22:00:30 -0700
parents src/Layout.ts@1aea03d306af
children 5e6840229a05
comparison
equal deleted inserted replaced
105:4bb8c7775c83 106:2468f2227d22
1 // Organize graph data into tables (column orders, etc) for the view layer.
2
3 import Immutable from "immutable"; // mostly using this for the builtin equals() testing, since NamedNode(x)!=NamedNode(x)
4 import { NamedNode, Quad, Store, Term } from "n3";
5 import { rdf } from "./namespaces";
6 import { TableDesc, ViewConfig } from "./ViewConfig";
7
8 type UriSet = Immutable.Set<NamedNode>;
9 export type TypeToSubjs = Immutable.Map<NamedNode, UriSet>;
10
11 // https://github.com/rdfjs/N3.js/issues/265
12 (NamedNode.prototype as any).hashCode = () => 0;
13
14 interface ColumnHeader {
15 rdfType: NamedNode;
16 pred: NamedNode;
17 }
18 export interface AlignedTable {
19 columnHeaders: ColumnHeader[];
20 rows: (Term | null)[][]; // each row is 1 wider than columnHeaders since the 1st element is the subject for that row
21 }
22 interface FreeStatements {
23 statements: Quad[];
24 }
25 export interface LayoutResult {
26 sections: (AlignedTable | FreeStatements)[];
27 }
28
29 class AlignedTableBuilder {
30 columnPreds = Immutable.List<NamedNode>();
31 subjRowIndices = Immutable.Map<NamedNode, number>();
32 rows: (Term | null)[][] = [];
33 constructor(
34 public rdfType: NamedNode /* plus join types, sort instructions */
35 ) {}
36
37 addQuad(q: Quad) {
38 const pred = q.predicate as NamedNode;
39 const omittedColumn = pred.equals(rdf.type);
40 if (!this.columnPreds.contains(pred) && !omittedColumn) {
41 this.columnPreds = this.columnPreds.push(pred); // this is putting cols in random order
42 this.rows.forEach((r) => r.push(null));
43 }
44
45 const predIndex = omittedColumn ? null : this.columnPreds.indexOf(pred);
46 let rowIndex = this.subjRowIndices.get(q.subject as NamedNode);
47 if (rowIndex === undefined) {
48 const newRow = new Array(1 + this.columnPreds.size).fill(null);
49 newRow[0] = q.subject;
50 this.rows.push(newRow);
51 rowIndex = this.rows.length - 1;
52 this.subjRowIndices = this.subjRowIndices.set(
53 q.subject as NamedNode,
54 rowIndex
55 );
56 }
57 if (predIndex !== null) {
58 this.rows[rowIndex][1 + predIndex] = q.object;
59 }
60 }
61
62 value(): AlignedTable {
63 this.rows.sort((a, b) => {
64 const uriA = (a[0] as NamedNode).value,
65 uriB = (b[0] as NamedNode).value;
66 return uriA.localeCompare(uriB);
67 });
68 const headers = this.columnPreds
69 .map((pred) => {
70 return { rdfType: this.rdfType, pred: pred };
71 })
72 .toArray();
73 return { columnHeaders: headers, rows: this.rows };
74 }
75 }
76
77 function findTypesNeededForTables(viewConfig?: ViewConfig): UriSet {
78 const typesToGather: NamedNode[] = [];
79 if (viewConfig) {
80 viewConfig.tables.forEach((t: TableDesc) => {
81 typesToGather.push(t.primary);
82 });
83 }
84 return Immutable.Set(typesToGather);
85 }
86
87 function findSubjectsWithTypes(graph: Store, typesToGatherSet: UriSet): UriSet {
88 const subjectsToGather: NamedNode[] = [];
89 graph.forEach(
90 (q: Quad) => {
91 if (typesToGatherSet.contains(q.object as NamedNode)) {
92 subjectsToGather.push(q.subject as NamedNode);
93 }
94 },
95 null,
96 rdf.type,
97 null,
98 null
99 );
100 return Immutable.Set(subjectsToGather);
101 }
102
103 export class Layout {
104 constructor(public viewConfig?: ViewConfig) {}
105 plan(graph: Store): LayoutResult {
106 const typesToGatherSet = findTypesNeededForTables(this.viewConfig);
107
108 const subjectsToGatherSet = findSubjectsWithTypes(graph, typesToGatherSet);
109 const ungrouped: Quad[] = [];
110 const vc = this.viewConfig;
111 const table =
112 vc && vc.tables.length > 0
113 ? new AlignedTableBuilder(vc.tables[0].primary)
114 : null;
115
116 graph.forEach(
117 (q: Quad) => {
118 if (!subjectsToGatherSet.contains(q.subject as NamedNode) || !table) {
119 ungrouped.push(q);
120 } else {
121 table.addQuad(q);
122 }
123 },
124 null,
125 null,
126 null,
127 null
128 );
129 const res: LayoutResult = { sections: [] };
130 if (table) {
131 res.sections.push(table.value());
132 }
133 res.sections.push({ statements: ungrouped });
134 return res;
135 }
136 }
137
138 // interface ISP {
139 // subj: NamedNode;
140 // pred: NamedNode;
141 // }
142 // const SP = Immutable.Record<ISP>({
143 // subj: new NamedNode(""),
144 // pred: new NamedNode(""),
145 // });
146
147 // // One table of rows with a common rdf:type.
148 // export class MultiSubjsTypeBlockLayout {
149 // subjs: NamedNode[];
150 // preds: NamedNode[];
151 // graphCells: Immutable.Map<ISP, Immutable.Set<Term>>;
152 // constructor(graph: Store, byType: TypeToSubjs, table: TableDesc) {
153 // const subjSet = byType.get(table.primary);
154 // this.subjs = subjSet ? Array.from(subjSet) : [];
155 // this.subjs.sort();
156
157 // let preds = Immutable.Set<NamedNode>();
158
159 // this.graphCells = Immutable.Map<ISP, Immutable.Set<Term>>().withMutations(
160 // (mutGraphCells) => {
161 // this.subjs.forEach((subj: NamedNode) => {
162 // graph.forEach(
163 // (q: Quad) => {
164 // if (!Util.isNamedNode(q.predicate)) {
165 // throw new Error();
166 // }
167
168 // const pred = q.predicate as NamedNode;
169 // if (pred.equals(rdf.type)) {
170 // // the whole block is labeled with the type
171 // return;
172 // }
173 // preds = preds.add(pred);
174 // const cellKey = this.makeCellKey(subj, pred);
175 // mutGraphCells.set(
176 // cellKey,
177 // mutGraphCells.get(cellKey, Immutable.Set<Term>()).add(q.object)
178 // );
179 // },
180 // subj,
181 // null,
182 // null,
183 // null
184 // );
185 // });
186 // }
187 // );
188 // this.preds = Array.from(preds);
189 // this.preds.splice(this.preds.indexOf(rdf.type), 1);
190 // // also pull out label, which should be used on 1st column
191 // this.preds.sort();
192 // }
193
194 // makeCellKey(subj: NamedNode, pred: NamedNode): ISP {
195 // return SP({
196 // subj: subj,
197 // pred: pred,
198 // });
199 // }
200 // }