Skip to content

Commit 326f8a3

Browse files
authored
Update Genealogical Tree: (#10)
* Add `isDeceased` and `fromExMarriage` properties, display corresponding state in the templates; * Add migration from `deathPlace` to `burialPlace`; * Add migration for relatedTo label from "link state" to proper `rdfs:label` property; * Auto-migrate `diagram.json` to `diagrams/main.json`;
1 parent df32601 commit 326f8a3

11 files changed

Lines changed: 423 additions & 99 deletions
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
.inputCheck {
2+
vertical-align: center;
3+
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import * as React from 'react';
2+
import * as Reactodia from '@reactodia/workspace';
3+
import * as Forms from '@reactodia/workspace/forms';
4+
5+
import { findGenealogicalProvider } from './GenealogicalDataProvider';
6+
import { genealogy, rdfs, schema, xsd } from './Vocabularies';
7+
8+
import styles from './FormInputs.module.css';
9+
10+
export function GenealogicalPropertyEditor(props: {
11+
options: Reactodia.PropertyEditorOptions;
12+
}) {
13+
const {options} = props;
14+
return (
15+
<Reactodia.DefaultPropertyEditor options={options}
16+
resolveInput={(property, inputProps) => {
17+
if (property === rdfs.comment) {
18+
return <Forms.InputList {...inputProps} valueInput={MultilineTextInput} />;
19+
} else if (property === schema.gender) {
20+
return <Forms.InputList {...inputProps} valueInput={InputGender} />;
21+
} else if (property === Reactodia.schema.thumbnailUrl) {
22+
return <InputImage {...inputProps} />;
23+
} else if (
24+
property === genealogy.fromExMarriage ||
25+
property === genealogy.isDeceased
26+
) {
27+
return <InputCheck {...inputProps} />;
28+
}
29+
return <Forms.InputList {...inputProps} valueInput={Forms.InputText} />;
30+
}}
31+
/>
32+
);
33+
}
34+
35+
function InputGender(props: Forms.InputSingleProps) {
36+
const {factory} = props;
37+
const {model} = Reactodia.useWorkspace();
38+
39+
let provider: Reactodia.DataProvider | undefined;
40+
if (model.dataProvider instanceof Reactodia.CompositeDataProvider) {
41+
provider = model.dataProvider.providers
42+
.find(p => p.origin?.value === genealogy.SchemaOrigin)?.provider;
43+
}
44+
45+
const {data: entities} = Reactodia.useProvidedEntities(provider, [schema.Male, schema.Female]);
46+
const language = Reactodia.useObservedProperty(
47+
model.events, 'changeLanguage', () => model.language
48+
);
49+
const variants = React.useMemo(
50+
() => Array.from(entities.values(), (item): Forms.InputSelectVariant => ({
51+
value: factory.namedNode(item.id),
52+
label: model.locale.formatEntityLabel(item, language),
53+
})),
54+
[entities, language, factory]
55+
);
56+
57+
return (
58+
<Forms.InputSelect {...props} variants={variants} />
59+
);
60+
}
61+
62+
function InputImage(props: Forms.InputMultiProps) {
63+
const {values} = props;
64+
const {model} = Reactodia.useWorkspace();
65+
const mainProvider = findGenealogicalProvider(model.dataProvider);
66+
if (!mainProvider) {
67+
throw new Error('Failed to find main provider');
68+
}
69+
const {data: fileMetadata} = Reactodia.useProvidedEntities(
70+
mainProvider,
71+
values.filter(v => v.termType === 'NamedNode').map(v => v.value)
72+
);
73+
return (
74+
<Forms.InputFile {...props}
75+
uploader={mainProvider.uploader}
76+
fileAccept='.jpg,.jpeg,.png,.svg,.gif'
77+
fileMetadata={fileMetadata}
78+
allowDrop={item => /^image\//.test(item.type)}
79+
/>
80+
);
81+
}
82+
83+
function InputCheck(props: Forms.InputMultiProps) {
84+
const {readonly, factory, values, updateValues} = props;
85+
const unchecked = values.every(v => (
86+
v.termType === 'Literal' &&
87+
v.datatype.value === xsd.boolean &&
88+
v.value === 'false'
89+
));
90+
return (
91+
<input className={styles.inputCheck}
92+
type='checkbox'
93+
disabled={readonly}
94+
checked={!unchecked}
95+
onChange={e => {
96+
const nextChecked = e.currentTarget.checked;
97+
updateValues(() =>
98+
nextChecked
99+
? [factory.literal('true', factory.namedNode(xsd.boolean))]
100+
: []
101+
);
102+
}}
103+
/>
104+
);
105+
}
106+
107+
function MultilineTextInput(props: Forms.InputSingleProps) {
108+
return <Forms.InputText {...props} multiline />;
109+
}

‎src/tools/GenealogicalTree/GenealogicalPackage.tsx‎

Lines changed: 33 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,15 @@ import { applyRdfChanges } from './ApplyRdfChanges';
88
import { genealogy, rdfs, schema, xsd } from './Vocabularies';
99

1010
export class GenealogicalPackage {
11+
private static readonly DIAGRAM_IRI_PREFIX = 'urn:reactodia:genealogical-package:diagram:';
1112
private static readonly FILE_IRI_PREFIX = 'urn:reactodia:genealogical-package:file:';
1213

1314
static readonly DEFAULT_NAMESPACE_BASE = 'http://reactodia.github.io/genealogy-graph/';
1415

1516
private readonly imageNameToUrl = new Map<string, string>();
1617

1718
private constructor(
18-
readonly diagram: Reactodia.SerializedDiagram | undefined,
19+
readonly diagrams: ReadonlyMap<Reactodia.ElementIri, Reactodia.SerializedDiagram>,
1920
readonly graph: readonly Reactodia.Rdf.Quad[],
2021
private readonly prefixes: Readonly<Record<string, string>>,
2122
private readonly entries: zip.Entry[],
@@ -44,7 +45,7 @@ export class GenealogicalPackage {
4445
factory.literal(GenealogicalPackage.DEFAULT_NAMESPACE_BASE),
4546
),
4647
];
47-
return new GenealogicalPackage(undefined, graph, {}, [], controller.signal);
48+
return new GenealogicalPackage(new Map(), graph, {}, [], controller.signal);
4849
}
4950

5051
static async loadFromBytes(bytes: Uint8Array, options: { signal: AbortSignal }): Promise<GenealogicalPackage> {
@@ -55,16 +56,30 @@ export class GenealogicalPackage {
5556
signal.addEventListener('abort', () => reader.close());
5657

5758
const entries = await reader.getEntries();
58-
const diagramEntry = entries.find((e): e is zip.FileEntry => !e.directory && e.filename === 'diagram.json');
5959
const graphEntry = entries.find((e): e is zip.FileEntry => !e.directory && e.filename === 'graph.ttl');
6060

61-
let diagram: Reactodia.SerializedDiagram | undefined;
62-
if (diagramEntry) {
63-
const diagramJson = await diagramEntry.getData(new zip.TextWriter());
64-
try {
65-
diagram = JSON.parse(diagramJson);
66-
} catch (err) {
67-
throw new Error('Failed to parse serialized diagram "diagram.json"', {cause: err});
61+
const diagrams = new Map<Reactodia.ElementIri, Reactodia.SerializedDiagram>();
62+
for (const entry of entries) {
63+
if (entry.directory) {
64+
continue;
65+
}
66+
67+
const match = /^diagrams\/(.+).json$/.exec(entry.filename);
68+
let diagramName = match ? match[1] : undefined;
69+
70+
// Temporary compatibility with initial version
71+
if (entry.filename === 'diagram.json') {
72+
diagramName = 'main';
73+
}
74+
75+
if (diagramName) {
76+
const diagramJson = await (entry as zip.FileEntry).getData(new zip.TextWriter());
77+
try {
78+
const diagram = JSON.parse(diagramJson);
79+
diagrams.set(GenealogicalPackage.DIAGRAM_IRI_PREFIX + diagramName, diagram);
80+
} catch (err) {
81+
throw new Error(`Failed to parse serialized diagram "${entry.filename}"`, {cause: err});
82+
}
6883
}
6984
}
7085

@@ -81,7 +96,7 @@ export class GenealogicalPackage {
8196
}
8297
}
8398

84-
return new GenealogicalPackage(diagram, graph, prefixes, entries, signal);
99+
return new GenealogicalPackage(diagrams, graph, prefixes, entries, signal);
85100
}
86101

87102
async resolveFileUrl(iri: string, options: { signal?: AbortSignal }): Promise<string | undefined> {
@@ -107,10 +122,10 @@ export class GenealogicalPackage {
107122
async exportWith(params: {
108123
dataProvider: Reactodia.RdfDataProvider;
109124
authoringState: Reactodia.AuthoringState;
110-
diagram: Reactodia.SerializedDiagram | undefined;
125+
diagrams: ReadonlyMap<string, Reactodia.SerializedDiagram>;
111126
uploader?: Forms.MemoryFileUploader;
112127
}): Promise<Blob> {
113-
const {dataProvider, authoringState: baseState, diagram, uploader} = params;
128+
const {dataProvider, authoringState: baseState, diagrams, uploader} = params;
114129
const factory = Reactodia.Rdf.DefaultDataFactory;
115130

116131
let authoringState = baseState;
@@ -191,8 +206,11 @@ export class GenealogicalPackage {
191206

192207
const zipWriter = new zip.ZipWriter(new zip.BlobWriter("application/zip"));
193208

194-
if (diagram) {
195-
await zipWriter.add('diagram.json', new zip.TextReader(JSON.stringify(diagram)));
209+
for (const [slug, diagram] of diagrams) {
210+
await zipWriter.add(
211+
`diagrams/${slug}.json`,
212+
new zip.TextReader(JSON.stringify(diagram))
213+
);
196214
}
197215

198216
await zipWriter.add('graph.ttl', new zip.TextReader(graphTurtle));

‎src/tools/GenealogicalTree/GenealogicalSchema.ttl‎

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,21 @@
1515
:Marriage a owl:Class;
1616
rdfs:label "Marriage"@en, "Брак"@ru .
1717

18+
:burialPlace a owl:ObjectProperty;
19+
rdfs:label "burial place"@en, "похоронен(а) в"@ru .
20+
21+
:fromExMarriage a owl:DatatypeProperty;
22+
rdfs:label "from ex-marriage"@en, "из бывшего брака"@ru .
23+
1824
:hasGodparent a owl:ObjectProperty;
1925
rdfs:label "has godparent"@en, "имеет крёстного(ую)"@ru .
2026

2127
:hasPartner a owl:ObjectProperty;
2228
rdfs:label "has marriage partner"@en, "супруг(а)"@ru .
2329

30+
:isDeceased a owl:DatatypeProperty;
31+
rdfs:label "is deceased"@en, "скончался(ась)"@ru .
32+
2433
rdfs:comment a owl:DatatypeProperty;
2534
rdfs:label "comment"@en, "комментарий"@ru .
2635

@@ -134,7 +143,7 @@ reactodia:sourceProvider a owl:DatatypeProperty;
134143
sh:class schema:Person ;
135144
sh:nodeKind sh:IRI ;
136145
sh:minCount 2 ;
137-
dash:reifiableBy :commonPropertyShape ;
146+
dash:reifiableBy :hasPartnerShape ;
138147
] .
139148

140149
:PersonShape a sh:NodeShape ;
@@ -163,14 +172,20 @@ reactodia:sourceProvider a owl:DatatypeProperty;
163172
sh:order 4 ;
164173
] ;
165174
sh:property [
166-
sh:path schema:thumbnailUrl ;
175+
sh:path :isDeceased ;
167176
sh:maxCount 1 ;
177+
sh:datatype xsd:boolean ;
168178
sh:order 5 ;
169179
] ;
170180
sh:property [
171-
sh:path rdfs:comment ;
181+
sh:path schema:thumbnailUrl ;
182+
sh:maxCount 1 ;
172183
sh:order 6 ;
173184
] ;
185+
sh:property [
186+
sh:path rdfs:comment ;
187+
sh:order 7 ;
188+
] ;
174189
sh:property [
175190
sh:path schema:parent ;
176191
sh:class schema:Person ;
@@ -187,7 +202,7 @@ reactodia:sourceProvider a owl:DatatypeProperty;
187202
sh:path schema:relatedTo ;
188203
sh:class schema:Person ;
189204
sh:nodeKind sh:IRI ;
190-
dash:reifiableBy :commonPropertyShape ;
205+
dash:reifiableBy :relatedToShape ;
191206
] ;
192207
sh:property [
193208
sh:path schema:birthPlace ;
@@ -203,6 +218,13 @@ reactodia:sourceProvider a owl:DatatypeProperty;
203218
sh:maxCount 1 ;
204219
dash:reifiableBy :commonPropertyShape ;
205220
] ;
221+
sh:property [
222+
sh:path :burialPlace ;
223+
sh:class schema:Place ;
224+
sh:nodeKind sh:IRI ;
225+
sh:maxCount 1 ;
226+
dash:reifiableBy :commonPropertyShape ;
227+
] ;
206228
sh:property [
207229
sh:path schema:homeLocation ;
208230
sh:class schema:Place ;
@@ -275,6 +297,16 @@ reactodia:sourceProvider a owl:DatatypeProperty;
275297
sh:path rdfs:comment ;
276298
] .
277299

300+
:relatedToShape a sh:NodeShape ;
301+
sh:property [
302+
sh:path rdfs:comment ;
303+
] ;
304+
sh:property [
305+
sh:path rdfs:label ;
306+
sh:uniqueLang true ;
307+
sh:order 1 ;
308+
] .
309+
278310
:homeLocationShape a sh:NodeShape ;
279311
sh:property [
280312
sh:path rdfs:comment ;
@@ -291,3 +323,14 @@ reactodia:sourceProvider a owl:DatatypeProperty;
291323
sh:datatype xsd:date ;
292324
sh:order 2 ;
293325
] .
326+
327+
:hasPartnerShape a sh:NodeShape ;
328+
sh:property [
329+
sh:path rdfs:comment ;
330+
] ;
331+
sh:property [
332+
sh:path :fromExMarriage ;
333+
sh:maxCount 1 ;
334+
sh:datatype xsd:boolean ;
335+
sh:order 1 ;
336+
] .

0 commit comments

Comments
 (0)