Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/tools/GenealogicalTree/FormInputs.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.inputCheck {
vertical-align: center;
}
109 changes: 109 additions & 0 deletions src/tools/GenealogicalTree/FormInputs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import * as React from 'react';
import * as Reactodia from '@reactodia/workspace';
import * as Forms from '@reactodia/workspace/forms';

import { findGenealogicalProvider } from './GenealogicalDataProvider';
import { genealogy, rdfs, schema, xsd } from './Vocabularies';

import styles from './FormInputs.module.css';

export function GenealogicalPropertyEditor(props: {
options: Reactodia.PropertyEditorOptions;
}) {
const {options} = props;
return (
<Reactodia.DefaultPropertyEditor options={options}
resolveInput={(property, inputProps) => {
if (property === rdfs.comment) {
return <Forms.InputList {...inputProps} valueInput={MultilineTextInput} />;
} else if (property === schema.gender) {
return <Forms.InputList {...inputProps} valueInput={InputGender} />;
} else if (property === Reactodia.schema.thumbnailUrl) {
return <InputImage {...inputProps} />;
} else if (
property === genealogy.fromExMarriage ||
property === genealogy.isDeceased
) {
return <InputCheck {...inputProps} />;
}
return <Forms.InputList {...inputProps} valueInput={Forms.InputText} />;
}}
/>
);
}

function InputGender(props: Forms.InputSingleProps) {
const {factory} = props;
const {model} = Reactodia.useWorkspace();

let provider: Reactodia.DataProvider | undefined;
if (model.dataProvider instanceof Reactodia.CompositeDataProvider) {
provider = model.dataProvider.providers
.find(p => p.origin?.value === genealogy.SchemaOrigin)?.provider;
}

const {data: entities} = Reactodia.useProvidedEntities(provider, [schema.Male, schema.Female]);
const language = Reactodia.useObservedProperty(
model.events, 'changeLanguage', () => model.language
);
const variants = React.useMemo(
() => Array.from(entities.values(), (item): Forms.InputSelectVariant => ({
value: factory.namedNode(item.id),
label: model.locale.formatEntityLabel(item, language),
})),
[entities, language, factory]
);

return (
<Forms.InputSelect {...props} variants={variants} />
);
}

function InputImage(props: Forms.InputMultiProps) {
const {values} = props;
const {model} = Reactodia.useWorkspace();
const mainProvider = findGenealogicalProvider(model.dataProvider);
if (!mainProvider) {
throw new Error('Failed to find main provider');
}
const {data: fileMetadata} = Reactodia.useProvidedEntities(
mainProvider,
values.filter(v => v.termType === 'NamedNode').map(v => v.value)
);
return (
<Forms.InputFile {...props}
uploader={mainProvider.uploader}
fileAccept='.jpg,.jpeg,.png,.svg,.gif'
fileMetadata={fileMetadata}
allowDrop={item => /^image\//.test(item.type)}
/>
);
}

function InputCheck(props: Forms.InputMultiProps) {
const {readonly, factory, values, updateValues} = props;
const unchecked = values.every(v => (
v.termType === 'Literal' &&
v.datatype.value === xsd.boolean &&
v.value === 'false'
));
return (
<input className={styles.inputCheck}
type='checkbox'
disabled={readonly}
checked={!unchecked}
onChange={e => {
const nextChecked = e.currentTarget.checked;
updateValues(() =>
nextChecked
? [factory.literal('true', factory.namedNode(xsd.boolean))]
: []
);
}}
/>
);
}

function MultilineTextInput(props: Forms.InputSingleProps) {
return <Forms.InputText {...props} multiline />;
}
48 changes: 33 additions & 15 deletions src/tools/GenealogicalTree/GenealogicalPackage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,15 @@ import { applyRdfChanges } from './ApplyRdfChanges';
import { genealogy, rdfs, schema, xsd } from './Vocabularies';

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

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

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

private constructor(
readonly diagram: Reactodia.SerializedDiagram | undefined,
readonly diagrams: ReadonlyMap<Reactodia.ElementIri, Reactodia.SerializedDiagram>,
readonly graph: readonly Reactodia.Rdf.Quad[],
private readonly prefixes: Readonly<Record<string, string>>,
private readonly entries: zip.Entry[],
Expand Down Expand Up @@ -44,7 +45,7 @@ export class GenealogicalPackage {
factory.literal(GenealogicalPackage.DEFAULT_NAMESPACE_BASE),
),
];
return new GenealogicalPackage(undefined, graph, {}, [], controller.signal);
return new GenealogicalPackage(new Map(), graph, {}, [], controller.signal);
}

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

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

let diagram: Reactodia.SerializedDiagram | undefined;
if (diagramEntry) {
const diagramJson = await diagramEntry.getData(new zip.TextWriter());
try {
diagram = JSON.parse(diagramJson);
} catch (err) {
throw new Error('Failed to parse serialized diagram "diagram.json"', {cause: err});
const diagrams = new Map<Reactodia.ElementIri, Reactodia.SerializedDiagram>();
for (const entry of entries) {
if (entry.directory) {
continue;
}

const match = /^diagrams\/(.+).json$/.exec(entry.filename);
let diagramName = match ? match[1] : undefined;

// Temporary compatibility with initial version
if (entry.filename === 'diagram.json') {
diagramName = 'main';
}

if (diagramName) {
const diagramJson = await (entry as zip.FileEntry).getData(new zip.TextWriter());
try {
const diagram = JSON.parse(diagramJson);
diagrams.set(GenealogicalPackage.DIAGRAM_IRI_PREFIX + diagramName, diagram);
} catch (err) {
throw new Error(`Failed to parse serialized diagram "${entry.filename}"`, {cause: err});
}
}
}

Expand All @@ -81,7 +96,7 @@ export class GenealogicalPackage {
}
}

return new GenealogicalPackage(diagram, graph, prefixes, entries, signal);
return new GenealogicalPackage(diagrams, graph, prefixes, entries, signal);
}

async resolveFileUrl(iri: string, options: { signal?: AbortSignal }): Promise<string | undefined> {
Expand All @@ -107,10 +122,10 @@ export class GenealogicalPackage {
async exportWith(params: {
dataProvider: Reactodia.RdfDataProvider;
authoringState: Reactodia.AuthoringState;
diagram: Reactodia.SerializedDiagram | undefined;
diagrams: ReadonlyMap<string, Reactodia.SerializedDiagram>;
uploader?: Forms.MemoryFileUploader;
}): Promise<Blob> {
const {dataProvider, authoringState: baseState, diagram, uploader} = params;
const {dataProvider, authoringState: baseState, diagrams, uploader} = params;
const factory = Reactodia.Rdf.DefaultDataFactory;

let authoringState = baseState;
Expand Down Expand Up @@ -191,8 +206,11 @@ export class GenealogicalPackage {

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

if (diagram) {
await zipWriter.add('diagram.json', new zip.TextReader(JSON.stringify(diagram)));
for (const [slug, diagram] of diagrams) {
await zipWriter.add(
`diagrams/${slug}.json`,
new zip.TextReader(JSON.stringify(diagram))
);
}

await zipWriter.add('graph.ttl', new zip.TextReader(graphTurtle));
Expand Down
51 changes: 47 additions & 4 deletions src/tools/GenealogicalTree/GenealogicalSchema.ttl
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,21 @@
:Marriage a owl:Class;
rdfs:label "Marriage"@en, "Брак"@ru .

:burialPlace a owl:ObjectProperty;
rdfs:label "burial place"@en, "похоронен(а) в"@ru .

:fromExMarriage a owl:DatatypeProperty;
rdfs:label "from ex-marriage"@en, "из бывшего брака"@ru .

:hasGodparent a owl:ObjectProperty;
rdfs:label "has godparent"@en, "имеет крёстного(ую)"@ru .

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

:isDeceased a owl:DatatypeProperty;
rdfs:label "is deceased"@en, "скончался(ась)"@ru .

rdfs:comment a owl:DatatypeProperty;
rdfs:label "comment"@en, "комментарий"@ru .

Expand Down Expand Up @@ -134,7 +143,7 @@ reactodia:sourceProvider a owl:DatatypeProperty;
sh:class schema:Person ;
sh:nodeKind sh:IRI ;
sh:minCount 2 ;
dash:reifiableBy :commonPropertyShape ;
dash:reifiableBy :hasPartnerShape ;
] .

:PersonShape a sh:NodeShape ;
Expand Down Expand Up @@ -163,14 +172,20 @@ reactodia:sourceProvider a owl:DatatypeProperty;
sh:order 4 ;
] ;
sh:property [
sh:path schema:thumbnailUrl ;
sh:path :isDeceased ;
sh:maxCount 1 ;
sh:datatype xsd:boolean ;
sh:order 5 ;
] ;
sh:property [
sh:path rdfs:comment ;
sh:path schema:thumbnailUrl ;
sh:maxCount 1 ;
sh:order 6 ;
] ;
sh:property [
sh:path rdfs:comment ;
sh:order 7 ;
] ;
sh:property [
sh:path schema:parent ;
sh:class schema:Person ;
Expand All @@ -187,7 +202,7 @@ reactodia:sourceProvider a owl:DatatypeProperty;
sh:path schema:relatedTo ;
sh:class schema:Person ;
sh:nodeKind sh:IRI ;
dash:reifiableBy :commonPropertyShape ;
dash:reifiableBy :relatedToShape ;
] ;
sh:property [
sh:path schema:birthPlace ;
Expand All @@ -203,6 +218,13 @@ reactodia:sourceProvider a owl:DatatypeProperty;
sh:maxCount 1 ;
dash:reifiableBy :commonPropertyShape ;
] ;
sh:property [
sh:path :burialPlace ;
sh:class schema:Place ;
sh:nodeKind sh:IRI ;
sh:maxCount 1 ;
dash:reifiableBy :commonPropertyShape ;
] ;
sh:property [
sh:path schema:homeLocation ;
sh:class schema:Place ;
Expand Down Expand Up @@ -275,6 +297,16 @@ reactodia:sourceProvider a owl:DatatypeProperty;
sh:path rdfs:comment ;
] .

:relatedToShape a sh:NodeShape ;
sh:property [
sh:path rdfs:comment ;
] ;
sh:property [
sh:path rdfs:label ;
sh:uniqueLang true ;
sh:order 1 ;
] .

:homeLocationShape a sh:NodeShape ;
sh:property [
sh:path rdfs:comment ;
Expand All @@ -291,3 +323,14 @@ reactodia:sourceProvider a owl:DatatypeProperty;
sh:datatype xsd:date ;
sh:order 2 ;
] .

:hasPartnerShape a sh:NodeShape ;
sh:property [
sh:path rdfs:comment ;
] ;
sh:property [
sh:path :fromExMarriage ;
sh:maxCount 1 ;
sh:datatype xsd:boolean ;
sh:order 1 ;
] .
Loading
Loading