Skip to content

Commit 9cfbbb5

Browse files
committed
Add Genealogical Tree Editor tool:
* Use OWL/SHACL-based metadata provider with `schema:` + `genealogy:` data namespaces; * Use `dash` spec to define relation shapes (`dash:reifiedBy`); * Serialize to a zip file (`GenealogicalPackage`) with file upload support; * Store and edit package settings as a (hidden) entity withing the data; * Use menu action to re-pin element properties (a workaround); * Add basic `ValidationProvider` to check marriage (>= 2 partners) and person (should have specified gender) entities; * Use workaround for type-based styling to style person elements based on gender;
1 parent 998c616 commit 9cfbbb5

23 files changed

Lines changed: 2199 additions & 1 deletion

docusaurus.config.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,13 @@ const config: Config = {
123123
{to: '/playground/classic-workspace', label: 'Classic Workspace'},
124124
]
125125
},
126+
{
127+
label: 'Tools',
128+
position: 'left',
129+
items: [
130+
{to: '/tools/genealogical-tree-editor', label: 'Genealogical Tree Editor (α ver.)'},
131+
]
132+
},
126133
{
127134
href: 'https://github.com/reactodia/reactodia-workspace',
128135
label: 'GitHub',

package-lock.json

Lines changed: 12 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
"@mdx-js/react": "^3.1.0",
2323
"@reactodia/hashmap": "^0.2.1",
2424
"@reactodia/workspace": "^0.34.1",
25+
"@zip.js/zip.js": "^2.8.23",
2526
"clsx": "^2.1.1",
2627
"n3": "^1.17.2",
2728
"prism-react-renderer": "^2.4.1",

src/css/custom.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
--docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3);
3131
}
3232

33-
/* Add alpha symbol after "Docs" link in the header */
33+
/* Add beta symbol after "Docs" link in the header */
3434
a.navbar__item.navbar__link[href="/docs/"]::after,
3535
a.menu__link[href="/docs/"]::after {
3636
content: 'β';
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import BrowserOnly from '@docusaurus/BrowserOnly';
2+
import Layout from '@theme/Layout';
3+
import { InlineReactodia, InlineReactodiaHead } from '@site/src/components/InlineReactodia';
4+
5+
export default function Tool() {
6+
return (
7+
<>
8+
<InlineReactodiaHead />
9+
<Layout title='Genealogical Tree Editor'
10+
noFooter>
11+
<BrowserOnly>
12+
{() => {
13+
const {ToolGenealogicalTree} = require(
14+
'@site/src/tools/GenealogicalTree/GenealogicalTree'
15+
) as typeof import('@site/src/tools/GenealogicalTree/GenealogicalTree');
16+
return (
17+
<InlineReactodia fullSize>
18+
<ToolGenealogicalTree />
19+
</InlineReactodia>
20+
);
21+
}}
22+
</BrowserOnly>
23+
</Layout>
24+
</>
25+
);
26+
}
Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
import { HashSet } from '@reactodia/hashmap';
2+
import * as Reactodia from '@reactodia/workspace';
3+
4+
type EncodedTerm =
5+
| Reactodia.ElementIri
6+
| Reactodia.ElementTypeIri
7+
| Reactodia.PropertyTypeIri
8+
| Reactodia.LinkTypeIri;
9+
10+
type DecodedTerm = Reactodia.Rdf.NamedNode | Reactodia.Rdf.BlankNode;
11+
12+
// TODO: move into Reactodia
13+
export function applyRdfChanges(params: {
14+
initialDataset: Iterable<Reactodia.Rdf.Quad>;
15+
authoringState: Reactodia.AuthoringState;
16+
dataFactory: Reactodia.Rdf.DataFactory;
17+
decodeTerm: (iri: EncodedTerm) => DecodedTerm;
18+
}): Reactodia.MemoryDataset {
19+
const {initialDataset, authoringState, dataFactory, decodeTerm} = params;
20+
const dataset = Reactodia.indexedDataset(
21+
Reactodia.IndexQuadBy.S |
22+
Reactodia.IndexQuadBy.SP |
23+
Reactodia.IndexQuadBy.O
24+
);
25+
dataset.addAll(initialDataset);
26+
27+
const toDelete = Reactodia.indexedDataset(Reactodia.IndexQuadBy.OnlyQuad);
28+
const toInsert = Reactodia.indexedDataset(Reactodia.IndexQuadBy.OnlyQuad);
29+
const updateDataset = () => {
30+
for (const quad of toDelete) {
31+
dataset.delete(quad);
32+
}
33+
dataset.addAll(toInsert);
34+
toDelete.clear();
35+
toInsert.clear();
36+
};
37+
38+
const context: DatasetChangeContext = {
39+
dataset,
40+
toDelete,
41+
toInsert,
42+
updateDataset,
43+
dataFactory,
44+
decodeTerm,
45+
};
46+
47+
processDeleteEvents(context, authoringState);
48+
processAddChangeEvents(context, authoringState);
49+
processEntityRenames(context, authoringState);
50+
51+
return dataset;
52+
}
53+
54+
interface DatasetChangeContext {
55+
readonly dataset: Reactodia.MemoryDataset;
56+
readonly toDelete: Reactodia.MemoryDataset;
57+
readonly toInsert: Reactodia.MemoryDataset;
58+
readonly updateDataset: () => void;
59+
60+
readonly dataFactory: Reactodia.Rdf.DataFactory;
61+
readonly decodeTerm: (iri: EncodedTerm) => DecodedTerm;
62+
}
63+
64+
function processDeleteEvents(context: DatasetChangeContext, authoringState: Reactodia.AuthoringState): void {
65+
const {dataset, toDelete, updateDataset, decodeTerm} = context;
66+
67+
for (const change of authoringState.elements.values()) {
68+
if (change.type === 'entityDelete') {
69+
const iri = decodeTerm(change.data.id);
70+
toDelete.addAll(dataset.iterateMatches(iri, null, null));
71+
toDelete.addAll(dataset.iterateMatches(null, null, iri));
72+
}
73+
}
74+
updateDataset();
75+
76+
for (const change of authoringState.links.values()) {
77+
if (change.type === 'relationDelete') {
78+
const subject = decodeTerm(change.data.sourceId);
79+
const predicate = decodeTerm(change.data.linkTypeId);
80+
const object = decodeTerm(change.data.targetId);
81+
for (const quad of dataset.iterateMatches(subject, predicate, object)) {
82+
toDelete.add(quad);
83+
toDelete.addAll(dataset.iterateMatches(quad, null, null));
84+
toDelete.addAll(dataset.iterateMatches(null, null, quad));
85+
}
86+
}
87+
}
88+
updateDataset();
89+
}
90+
91+
function processAddChangeEvents(context: DatasetChangeContext, authoringState: Reactodia.AuthoringState): void {
92+
const {dataset, toDelete, toInsert, updateDataset, dataFactory, decodeTerm} = context;
93+
94+
const rdfType = dataFactory.namedNode(Reactodia.rdf.type);
95+
const beforeSet = new HashSet<Reactodia.Rdf.NamedNode | Reactodia.Rdf.Literal>(
96+
Reactodia.Rdf.hashTerm,
97+
Reactodia.Rdf.equalTerms
98+
);
99+
const propertyContext: PropertyChangeContext = {
100+
...context,
101+
beforeSet,
102+
addedSet: beforeSet.clone(),
103+
};
104+
105+
for (const change of authoringState.elements.values()) {
106+
if (change.type === 'entityChange' || change.type === 'entityAdd') {
107+
const before = change.type === 'entityChange' ? change.before : undefined;
108+
const after = change.data;
109+
const iri = decodeTerm(after.id);
110+
111+
if (before) {
112+
for (const type of before.types) {
113+
if (!after.types.includes(type)) {
114+
toDelete.addAll(dataset.iterateMatches(iri, rdfType, decodeTerm(type)));
115+
}
116+
}
117+
}
118+
119+
for (const type of after.types) {
120+
if (!before || !before.types.includes(type)) {
121+
toInsert.add(dataFactory.quad(iri, rdfType, decodeTerm(type)));
122+
}
123+
}
124+
125+
processChangeProperties(propertyContext, iri, before?.properties ?? {}, after.properties);
126+
}
127+
}
128+
updateDataset();
129+
130+
for (const change of authoringState.links.values()) {
131+
if (change.type === 'relationChange' || change.type === 'relationAdd') {
132+
const before = change.type === 'relationChange' ? change.before : undefined;
133+
const subject = decodeTerm(change.data.sourceId);
134+
const predicate = decodeTerm(change.data.linkTypeId);
135+
const object = decodeTerm(change.data.targetId);
136+
if (predicate.termType !== 'NamedNode') {
137+
continue;
138+
}
139+
140+
const quads = before
141+
? Array.from(dataset.iterateMatches(subject, predicate, object))
142+
: [];
143+
if (quads.length === 0) {
144+
quads.push(dataFactory.quad(subject, predicate, object));
145+
}
146+
147+
if (change.type === 'relationAdd') {
148+
toInsert.addAll(quads);
149+
}
150+
151+
for (const quad of quads) {
152+
processChangeProperties(
153+
propertyContext,
154+
quad,
155+
before?.properties ?? {},
156+
change.data.properties
157+
);
158+
}
159+
}
160+
}
161+
updateDataset();
162+
}
163+
164+
interface PropertyChangeContext extends DatasetChangeContext {
165+
readonly beforeSet: HashSet<Reactodia.Rdf.NamedNode | Reactodia.Rdf.Literal>;
166+
readonly addedSet: HashSet<Reactodia.Rdf.NamedNode | Reactodia.Rdf.Literal>;
167+
}
168+
169+
function processChangeProperties(
170+
context: PropertyChangeContext,
171+
subject: Reactodia.Rdf.NamedNode | Reactodia.Rdf.BlankNode | Reactodia.Rdf.Quad,
172+
from: { readonly [id: string]: readonly (Reactodia.Rdf.NamedNode | Reactodia.Rdf.Literal)[] },
173+
to: { readonly [id: string]: readonly (Reactodia.Rdf.NamedNode | Reactodia.Rdf.Literal)[] },
174+
): void {
175+
const {dataset, toInsert, toDelete, dataFactory, decodeTerm, beforeSet, addedSet} = context;
176+
177+
for (const property of Object.keys(from)) {
178+
if (!Object.prototype.hasOwnProperty.call(to, property)) {
179+
const predicate = decodeTerm(property);
180+
toDelete.addAll(dataset.iterateMatches(subject, predicate, null));
181+
}
182+
}
183+
184+
for (const [property, toValues] of Object.entries(to)) {
185+
const predicate = decodeTerm(property);
186+
if (predicate.termType !== 'NamedNode') {
187+
continue;
188+
}
189+
190+
if (Object.prototype.hasOwnProperty.call(from, property)) {
191+
for (const value of from[property]) {
192+
beforeSet.add(value);
193+
}
194+
}
195+
196+
for (const value of toValues) {
197+
addedSet.add(value);
198+
if (!beforeSet.has(value)) {
199+
toInsert.add(dataFactory.quad(subject, predicate, value));
200+
}
201+
}
202+
203+
for (const value of beforeSet) {
204+
if (!addedSet.has(value)) {
205+
toDelete.addAll(dataset.iterateMatches(subject, predicate, value));
206+
}
207+
}
208+
209+
beforeSet.clear();
210+
addedSet.clear();
211+
}
212+
};
213+
214+
function processEntityRenames(context: DatasetChangeContext, authoringState: Reactodia.AuthoringState): void {
215+
const {dataset, toDelete, toInsert, updateDataset, dataFactory, decodeTerm} = context;
216+
217+
for (const change of authoringState.elements.values()) {
218+
if (change.type === 'entityChange' && change.newIri) {
219+
const from = decodeTerm(change.data.id);
220+
const to = decodeTerm(change.newIri);
221+
222+
for (const fromQuad of dataset.iterateMatches(from, null, null)) {
223+
toDelete.add(fromQuad);
224+
const toQuad = dataFactory.quad(to, fromQuad.predicate, fromQuad.object, fromQuad.graph);
225+
toInsert.add(toQuad);
226+
renameIndirectQuads(context, fromQuad, toQuad);
227+
}
228+
229+
for (const fromQuad of dataset.iterateMatches(null, null, from)) {
230+
toDelete.add(fromQuad);
231+
const toQuad = dataFactory.quad(fromQuad.subject, fromQuad.predicate, to, fromQuad.graph);
232+
toInsert.add(toQuad);
233+
renameIndirectQuads(context, fromQuad, toQuad);
234+
}
235+
}
236+
}
237+
updateDataset();
238+
}
239+
240+
function renameIndirectQuads(
241+
context: DatasetChangeContext,
242+
fromQuad: Reactodia.Rdf.Quad,
243+
toQuad: Reactodia.Rdf.Quad
244+
): void {
245+
const {dataset, toDelete, toInsert, dataFactory} = context;
246+
247+
for (const indirect of dataset.iterateMatches(fromQuad, null, null)) {
248+
toDelete.add(indirect);
249+
toInsert.add(dataFactory.quad(toQuad, indirect.predicate, indirect.object, indirect.graph));
250+
}
251+
252+
for (const indirect of dataset.iterateMatches(null, null, fromQuad)) {
253+
toDelete.add(indirect);
254+
toInsert.add(dataFactory.quad(indirect.subject, indirect.predicate, fromQuad, indirect.graph));
255+
}
256+
};

0 commit comments

Comments
 (0)