linked-rolls 0.0.1 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/README.md +48 -1
  2. package/lib/Assumption.d.ts +124 -0
  3. package/lib/Assumption.js +34 -0
  4. package/lib/Collation.d.ts +11 -0
  5. package/lib/ConditionState.d.ts +8 -3
  6. package/lib/ConditionState.js +0 -5
  7. package/lib/Edit.d.ts +44 -14
  8. package/lib/Edit.js +2 -95
  9. package/lib/Edition.d.ts +144 -4
  10. package/lib/EditionView.d.ts +51 -0
  11. package/lib/EditionView.js +294 -0
  12. package/lib/Emulation.d.ts +21 -68
  13. package/lib/Emulation.js +94 -386
  14. package/lib/Feature.d.ts +137 -16
  15. package/lib/Feature.js +9 -1
  16. package/lib/Plan.d.ts +190 -0
  17. package/lib/Plan.js +555 -0
  18. package/lib/ReproducingSystem.d.ts +80 -0
  19. package/lib/RollCopy.d.ts +259 -24
  20. package/lib/RollCopy.js +232 -215
  21. package/lib/Symbol.d.ts +70 -37
  22. package/lib/Symbol.js +2 -27
  23. package/lib/TrackCalibration.d.ts +33 -0
  24. package/lib/TrackCalibration.js +14 -0
  25. package/lib/TrackerBar.d.ts +52 -5
  26. package/lib/TrackerBar.js +70 -49
  27. package/lib/Version.d.ts +32 -33
  28. package/lib/Version.js +3 -209
  29. package/lib/alignFeatures.d.ts +3 -2
  30. package/lib/alignFeatures.js +5 -4
  31. package/lib/asJsonLd.d.ts +1 -1
  32. package/lib/asJsonLd.js +13 -26
  33. package/lib/importJsonLd.d.ts +0 -1
  34. package/lib/importJsonLd.js +9 -72
  35. package/lib/index.d.ts +7 -3
  36. package/lib/index.js +7 -3
  37. package/lib/schema.json +1739 -0
  38. package/lib/spec/context.json +125 -144
  39. package/lib/systems/welteT100.d.ts +55 -0
  40. package/lib/systems/welteT100.js +191 -0
  41. package/lib/utils.d.ts +29 -0
  42. package/lib/validate.d.ts +3 -0
  43. package/lib/validate.js +10 -0
  44. package/package.json +41 -8
  45. package/lib/Condition.d.ts +0 -10
  46. package/lib/EditorialAssumption.d.ts +0 -67
  47. package/lib/EditorialAssumption.js +0 -26
  48. package/lib/Measurement.d.ts +0 -9
  49. package/lib/PlaceTimeConversion.d.ts +0 -65
  50. package/lib/PlaceTimeConversion.js +0 -175
  51. package/lib/RollEvent.d.ts +0 -76
  52. package/lib/RollEvent.js +0 -3
  53. package/lib/Stage.d.ts +0 -37
  54. package/lib/Stage.js +0 -165
  55. package/lib/Transcription.d.ts +0 -7
  56. package/lib/Transcription.js +0 -9
  57. package/lib/WithId.d.ts +0 -3
  58. package/lib/WithId.js +0 -1
  59. package/lib/alignRolls.d.ts +0 -7
  60. package/lib/alignRolls.js +0 -49
  61. package/lib/alignSymbols.d.ts +0 -7
  62. package/lib/alignSymbols.js +0 -49
  63. package/lib/aton/AtonParser.test.d.ts +0 -1
  64. package/lib/aton/AtonParser.test.js +0 -16
  65. package/lib/build-schema.cjs +0 -113
  66. /package/lib/{Condition.js → ReproducingSystem.js} +0 -0
  67. /package/lib/{Measurement.js → utils.js} +0 -0
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Relates a scan to the tracker bar it was read with.
3
+ *
4
+ * Scanning software numbers the hole columns it finds by their position
5
+ * in the image, so its numbering is offset by however far the roll lay
6
+ * from the edge of the scanner bed. Calibrating means finding that offset,
7
+ * usually from a landmark such as the rewind perforation.
8
+ *
9
+ * column = offset + scannerTrack * separation
10
+ * trackerBar = scannerTrack + shift
11
+ */
12
+ export interface TrackCalibration {
13
+ unit: 'px';
14
+ /** Image column of the scanning software's track 0. */
15
+ offset: number;
16
+ /** Distance between the centres of adjacent tracks. */
17
+ separation: number;
18
+ /** Added to the scanning software's numbering to reach the tracker bar. */
19
+ shift: number;
20
+ }
21
+ /** Image column at the centre of a tracker bar track. */
22
+ export declare const columnOf: (track: number, calibration: TrackCalibration) => number;
23
+ /** Tracker bar track covering an image column, unrounded. */
24
+ export declare const trackAt: (column: number, calibration: TrackCalibration) => number;
25
+ /**
26
+ * The columns covered by a run of tracks, from the outer edge of the
27
+ * first to the outer edge of the last.
28
+ */
29
+ export declare const columnsOf: (from: number, to: number, calibration: TrackCalibration) => {
30
+ from: number;
31
+ to: number;
32
+ width: number;
33
+ };
@@ -0,0 +1,14 @@
1
+ /** Image column at the centre of a tracker bar track. */
2
+ export const columnOf = (track, calibration) => calibration.offset + (track - calibration.shift) * calibration.separation;
3
+ /** Tracker bar track covering an image column, unrounded. */
4
+ export const trackAt = (column, calibration) => (column - calibration.offset) / calibration.separation + calibration.shift;
5
+ /**
6
+ * The columns covered by a run of tracks, from the outer edge of the
7
+ * first to the outer edge of the last.
8
+ */
9
+ export const columnsOf = (from, to, calibration) => {
10
+ const [lower, upper] = from <= to ? [from, to] : [to, from];
11
+ const start = columnOf(lower, calibration) - calibration.separation / 2;
12
+ const end = columnOf(upper, calibration) + calibration.separation / 2;
13
+ return { from: start, to: end, width: end - start };
14
+ };
@@ -1,8 +1,55 @@
1
1
  import { Expression, Note } from "./Symbol";
2
- export declare abstract class TrackerBar {
3
- abstract meaningOf(track: number): Partial<Note> | Partial<Expression>;
2
+ /**
3
+ * What a tracker bar position does: sound a note, or operate one of
4
+ * the expression valves on the bass or the treble side.
5
+ */
6
+ export type TrackRole = 'bass-expression' | 'note' | 'treble-expression';
7
+ /**
8
+ * A contiguous block of tracker bar positions serving one role.
9
+ * Both bounds are inclusive.
10
+ */
11
+ export interface TrackArea {
12
+ readonly role: TrackRole;
13
+ readonly from: number;
14
+ readonly to: number;
4
15
  }
5
- export declare class WelteT100 {
6
- meaningOf(track: number): Pick<Note, 'type' | 'pitch'> | Pick<Expression, 'type' | 'expressionType' | 'scope'>;
7
- width: number;
16
+ export type NoteMeaning = Pick<Note, 'type' | 'pitch'>;
17
+ export type ExpressionMeaning = Pick<Expression, 'type' | 'expressionType' | 'scope'>;
18
+ export type TrackMeaning = NoteMeaning | ExpressionMeaning;
19
+ /**
20
+ * Describes a tracker bar. Track numbers are 1-based and count from
21
+ * the bass edge of the roll, which is the numbering used throughout
22
+ * the edition: a feature's `vertical.from` is a track in this sense.
23
+ *
24
+ * This is the only place where track numbers are given a meaning.
25
+ * Anything that needs to know where the note block ends, which side
26
+ * an expression belongs to, or how a track maps to a pitch, should
27
+ * ask the tracker bar rather than repeat the boundaries.
28
+ */
29
+ export interface TrackerBar {
30
+ readonly name: string;
31
+ /** Width of the roll the bar reads, in mm. */
32
+ readonly width: number;
33
+ /** Number of positions on the bar. Tracks run from 1 to this. */
34
+ readonly trackCount: number;
35
+ /** The blocks of positions, from the bass edge upwards. */
36
+ readonly areas: readonly TrackArea[];
37
+ /**
38
+ * The position carrying the rewind perforation, which runs at
39
+ * the very end of a roll and is the usual landmark for calibrating
40
+ * a scan against the bar.
41
+ */
42
+ readonly rewindTrack: number;
43
+ /** `undefined` for a position the bar does not read. */
44
+ meaningOf(track: number): TrackMeaning | undefined;
45
+ /** `undefined` for a position the bar does not read. */
46
+ roleOf(track: number): TrackRole | undefined;
8
47
  }
48
+ /**
49
+ * Welte-Mignon T-100 ("red Welte"), cf. Hagmann, pp. 75 and 178.
50
+ *
51
+ * The note block spans 80 positions from C1 to g⁴, i.e. MIDI 24 to 103.
52
+ * The expression valves are duplicated, bass below the note block and
53
+ * treble above it, in mirrored order.
54
+ */
55
+ export declare const welteT100: TrackerBar;
package/lib/TrackerBar.js CHANGED
@@ -1,53 +1,74 @@
1
- export class TrackerBar {
2
- }
3
- export class WelteT100 {
4
- constructor() {
5
- Object.defineProperty(this, "width", {
6
- enumerable: true,
7
- configurable: true,
8
- writable: true,
9
- value: 328
10
- }); // in mm, cf. Hagmann, p. 75
11
- }
12
- meaningOf(track) {
13
- if (track <= 0 || track > 100) {
14
- throw new Error('Track out of range');
15
- }
16
- // Cf. Hagmann, p. 178
17
- const expressionMap = new Map([
18
- [1, 'MezzoforteOff'],
19
- [2, 'MezzoforteOn'],
20
- [3, 'SlowCrescendoOff'],
21
- [4, 'SlowCrescendoOn'],
22
- [5, 'ForzandoOff'],
23
- [6, 'ForzandoOn'],
24
- [7, 'SoftPedalOff'],
25
- [8, 'SoftPedalOn'],
26
- [9, 'MotorOff'],
27
- [10, 'MotorOn'],
28
- [91, 'Rewind'],
29
- [92, 'ElectricCutOff'],
30
- [93, 'SustainPedalOn'],
31
- [94, 'SustainPedalOff'],
32
- [95, 'ForzandoOn'],
33
- [96, 'ForzandoOff'],
34
- [97, 'SlowCrescendoOn'],
35
- [98, 'SlowCrescendoOff'],
36
- [99, 'MezzoforteOn'],
37
- [100, 'MezzoforteOff']
38
- ]);
39
- if (track <= 10 || track >= 91) {
40
- const scope = track <= 10 ? 'bass' : 'treble';
1
+ const areasOf = ({ notes, trackCount }) => [
2
+ { role: 'bass-expression', from: 1, to: notes.from - 1 },
3
+ { role: 'note', from: notes.from, to: notes.to },
4
+ { role: 'treble-expression', from: notes.to + 1, to: trackCount }
5
+ ];
6
+ const scopeOf = (role) => role === 'bass-expression' ? 'bass' : 'treble';
7
+ const describe = (spec) => {
8
+ const areas = areasOf(spec);
9
+ const roleOf = (track) => areas.find(area => track >= area.from && track <= area.to)?.role;
10
+ const meaningOf = (track) => {
11
+ const role = roleOf(track);
12
+ if (!role)
13
+ return undefined;
14
+ if (role === 'note') {
41
15
  return {
42
- expressionType: expressionMap.get(track),
43
- scope,
44
- type: 'expression'
16
+ type: 'note',
17
+ pitch: track - spec.notes.from + spec.notes.lowestPitch
45
18
  };
46
19
  }
47
- // C1-G4 = track 11-90 = 24-103 in MIDI keys
48
- return {
49
- pitch: track + 13,
50
- type: 'note',
51
- };
20
+ const expressionType = spec.expressions.get(track);
21
+ if (!expressionType)
22
+ return undefined;
23
+ return { type: 'expression', expressionType, scope: scopeOf(role) };
24
+ };
25
+ const rewindTrack = [...spec.expressions]
26
+ .find(([, type]) => type === 'Rewind')?.[0];
27
+ if (rewindTrack === undefined) {
28
+ throw new Error(`${spec.name} declares no rewind track`);
52
29
  }
53
- }
30
+ return {
31
+ name: spec.name,
32
+ width: spec.width,
33
+ trackCount: spec.trackCount,
34
+ areas,
35
+ rewindTrack,
36
+ meaningOf,
37
+ roleOf
38
+ };
39
+ };
40
+ /**
41
+ * Welte-Mignon T-100 ("red Welte"), cf. Hagmann, pp. 75 and 178.
42
+ *
43
+ * The note block spans 80 positions from C1 to g⁴, i.e. MIDI 24 to 103.
44
+ * The expression valves are duplicated, bass below the note block and
45
+ * treble above it, in mirrored order.
46
+ */
47
+ export const welteT100 = describe({
48
+ name: 'Welte-Mignon T100',
49
+ width: 328,
50
+ trackCount: 100,
51
+ notes: { from: 11, to: 90, lowestPitch: 24 },
52
+ expressions: new Map([
53
+ [1, 'MezzoforteOff'],
54
+ [2, 'MezzoforteOn'],
55
+ [3, 'SlowCrescendoOff'],
56
+ [4, 'SlowCrescendoOn'],
57
+ [5, 'ForzandoOff'],
58
+ [6, 'ForzandoOn'],
59
+ [7, 'SoftPedalOff'],
60
+ [8, 'SoftPedalOn'],
61
+ [9, 'MotorOff'],
62
+ [10, 'MotorOn'],
63
+ [91, 'Rewind'],
64
+ [92, 'ElectricCutOff'],
65
+ [93, 'SustainPedalOn'],
66
+ [94, 'SustainPedalOff'],
67
+ [95, 'ForzandoOn'],
68
+ [96, 'ForzandoOff'],
69
+ [97, 'SlowCrescendoOn'],
70
+ [98, 'SlowCrescendoOff'],
71
+ [99, 'MezzoforteOn'],
72
+ [100, 'MezzoforteOff']
73
+ ])
74
+ });
package/lib/Version.d.ts CHANGED
@@ -1,42 +1,41 @@
1
- import { ActorAssignment, Edit } from "./Edit";
2
- import { EditorialAssumption, Motivation } from "./EditorialAssumption";
3
- import { AnySymbol } from "./Symbol";
4
- import { CollationTolerance } from "./Collation";
5
- export type Derivation = EditorialAssumption<'derivation', Version>;
6
- export declare const versionTypes: readonly ["edition", "authorised-revision", "unauthorised-revision", "gloss"];
1
+ import { Edit } from "./Edit";
2
+ import { ReferenceAssumption } from "./Assumption";
3
+ import { WithId, WithNote, WithType } from "./utils";
4
+ export declare const versionTypes: readonly ["edition", "unicum"];
5
+ /**
6
+ * The type of a version. An 'edition' version may serve as the
7
+ * master for several roll copies; a 'unicum' version exists only
8
+ * on one specific copy.
9
+ */
7
10
  export type VersionType = typeof versionTypes[number];
8
11
  /**
9
- * Version + Version Creation
12
+ * A motivation provides a reason or rationale for an editorial change.
13
+ * Motivations are defined at the version level and referenced by edits.
14
+ * @see crm:E73 Information Object
15
+ */
16
+ export type Motivation = WithType<'motivation'> & WithId & WithNote;
17
+ /**
18
+ * A version is defined by the sum of edits applied
19
+ * to the version it is based on. For simple identification,
20
+ * a siglum is given to each version.
10
21
  */
11
22
  export interface Version {
12
23
  id: string;
24
+ /**
25
+ * A short siglum to identify the version, e.g. "A", "B1", "B2_rev", etc.
26
+ */
13
27
  siglum: string;
14
- actor?: ActorAssignment;
15
- basedOn?: Derivation;
28
+ /**
29
+ * If no derivation is defined, it is assumed that this version represents the mother roll.
30
+ */
31
+ basedOn?: ReferenceAssumption;
32
+ /**
33
+ * The list of edits that, applied to the base version, produce this version.
34
+ */
16
35
  edits: Edit[];
17
- motivations: Motivation<string>[];
36
+ /**
37
+ * A collection of motivations used in this version's edits.
38
+ */
39
+ motivations: Motivation[];
18
40
  type: VersionType;
19
41
  }
20
- export declare const traverseVersions: (version: Version, callback: (version: Version) => void) => void;
21
- export declare const getSnapshot: (version: Version) => AnySymbol[];
22
- /**
23
- * Walks through the versions. If it finds a symbol that is still
24
- * part of the tradition (i.e. it is included in the current snapshot)
25
- * and is equivalent with the given symbol, it will add the feature
26
- * carrying the symbol to the collated symbol. Otherwise, the
27
- * given symbol will be added to the current version's insertions.
28
- *
29
- * All symbols of the tradition that are not included in the given
30
- * symbols are considered to be deleted.
31
- *
32
- * @param creation
33
- * @param symbols
34
- * @returns
35
- */
36
- export declare function fillEdits(currentVersion: Version, symbols: AnySymbol[], tolerance?: CollationTolerance): void;
37
- /**
38
- * Assigns a generation (depth) to every node.
39
- */
40
- export declare function assignGenerations(versions: Version[]): Array<Version & {
41
- generation: number;
42
- }>;
package/lib/Version.js CHANGED
@@ -1,217 +1,11 @@
1
- import { v4 } from "uuid";
2
- import { flat } from "./EditorialAssumption";
3
- import { dimensionOf } from "./Symbol";
4
1
  export const versionTypes = [
5
2
  /**
6
- * The roll is in a state where it is used as
3
+ * The roll is in a state where it is (possibly) used as
7
4
  * the master roll for several new reproductions.
8
5
  */
9
6
  'edition',
10
7
  /**
11
- * This denotes a version which is specific to (early)
12
- * Welte-Mignon piano rolls, where rolls inteded to
13
- * be pulished are revised by a controller first. These
14
- * rolls typically carry a "controlliert" stamp. The
15
- * revision is done on the same date as the perforation
16
- * and the date is written on the roll towards its end.
8
+ * A version that exists only on one specific copy of a roll.
17
9
  */
18
- 'authorised-revision',
19
- /**
20
- * Unauthorised revisions are those, which cannot be linked
21
- * to a specific controller and are likely done by
22
- * a later, anonymous hand.
23
- */
24
- 'unauthorised-revision',
25
- /**
26
- * In the case of Welte Mignon rolls, glosses are
27
- * typically comments about the roll's condition, added
28
- * e.g. by the collector.
29
- */
30
- 'gloss'
10
+ 'unicum'
31
11
  ];
32
- export const traverseVersions = (version, callback) => {
33
- callback(version);
34
- if (version.basedOn) {
35
- traverseVersions(flat(version.basedOn), callback);
36
- }
37
- };
38
- export const getSnapshot = (version) => {
39
- const snapshot = [];
40
- const toDelete = [];
41
- traverseVersions(version, s => {
42
- snapshot.push(...s.edits.flatMap(edit => edit.insert || []));
43
- // as we travel further up, remove symbols that are
44
- // deleted in the versions further down
45
- const deleted = [];
46
- for (const toRemove of toDelete) {
47
- const index = snapshot.findIndex(s => s === toRemove);
48
- if (index !== -1) {
49
- snapshot.splice(index, 1);
50
- deleted.push(toRemove);
51
- }
52
- }
53
- for (const del of deleted) {
54
- toDelete.splice(toDelete.indexOf(del), 1);
55
- }
56
- // collect symbols that are deleted in the current version
57
- toDelete.push(...s.edits.flatMap(edit => edit.delete || []));
58
- });
59
- return snapshot.sort((a, b) => {
60
- const aDimension = dimensionOf(a);
61
- const bDimension = dimensionOf(b);
62
- return aDimension.horizontal.from - bDimension.horizontal.from;
63
- });
64
- };
65
- const isCollatable = (symbolA, symbolB, tolerance = {
66
- toleranceEnd: 5,
67
- toleranceStart: 5
68
- }) => {
69
- // two symbols are collatible if they share the same
70
- // symbol characteristics (pitch, expression type etc.)
71
- // and occur in the same horizontal position.
72
- if (symbolA.type === 'note' && symbolB.type === 'note') {
73
- if (symbolA.pitch !== symbolB.pitch)
74
- return false;
75
- }
76
- else if (symbolA.type === 'expression' && symbolB.type === 'expression') {
77
- if (symbolA.expressionType !== symbolB.expressionType)
78
- return false;
79
- if (symbolA.scope !== symbolB.scope)
80
- return false;
81
- }
82
- const dimensionA = dimensionOf(symbolA);
83
- const dimensionB = dimensionOf(symbolB);
84
- const distanceStart = Math.abs(dimensionA.horizontal.from - dimensionB.horizontal.from);
85
- const distanceEnd = Math.abs(dimensionA.horizontal.to - dimensionB.horizontal.to);
86
- if (distanceStart > tolerance.toleranceStart
87
- || distanceEnd > tolerance.toleranceEnd) {
88
- // the symbols are too far apart to be collated
89
- return false;
90
- }
91
- return true;
92
- };
93
- const overlaps = (a, b) => {
94
- const overlapsDimension = (a, b) => (a.from ?? 0) < (b.to ?? Infinity) && (b.from ?? 0) < (a.to ?? Infinity);
95
- return overlapsDimension(a.horizontal, b.horizontal) && overlapsDimension(a.vertical, b.vertical);
96
- };
97
- /**
98
- * Walks through the versions. If it finds a symbol that is still
99
- * part of the tradition (i.e. it is included in the current snapshot)
100
- * and is equivalent with the given symbol, it will add the feature
101
- * carrying the symbol to the collated symbol. Otherwise, the
102
- * given symbol will be added to the current version's insertions.
103
- *
104
- * All symbols of the tradition that are not included in the given
105
- * symbols are considered to be deleted.
106
- *
107
- * @param creation
108
- * @param symbols
109
- * @returns
110
- */
111
- export function fillEdits(currentVersion, symbols, tolerance = {
112
- toleranceEnd: 5,
113
- toleranceStart: 5
114
- }) {
115
- const snapshot = getSnapshot(currentVersion);
116
- const treatedSymbols = [];
117
- // can it be collated with any of the symbols of
118
- // included in the current snapshot?
119
- const insertions = [...symbols];
120
- for (const symbol of symbols) {
121
- snapshot
122
- .filter(toCompare => isCollatable(symbol, toCompare, tolerance))
123
- .forEach(corresp => {
124
- corresp.carriers.push(...symbol.carriers);
125
- insertions.splice(insertions.indexOf(symbol), 1);
126
- treatedSymbols.push(corresp);
127
- });
128
- }
129
- // special treatment for covers
130
- const covers = insertions.filter(symbol => symbol.type === 'cover');
131
- for (const cover of covers) {
132
- // find perforations in the snapshot that
133
- // overlap with the cover
134
- snapshot
135
- .filter(symbol => symbol.type === 'note' || symbol.type === 'expression')
136
- .map(dimensionOf)
137
- .filter(dimension => overlaps(dimension, dimensionOf(cover)))
138
- .forEach(dimension => {
139
- const coverDimension = dimensionOf(cover);
140
- // check if the cover partially covers the beginning
141
- if (dimension.horizontal.from >= coverDimension.horizontal.from &&
142
- dimension.horizontal.from <= coverDimension.horizontal.to) {
143
- // the note starts where the cover ends
144
- dimension.horizontal.from = coverDimension.horizontal.to;
145
- }
146
- // check if the cover partially covers the ending
147
- if (dimension.horizontal.to >= coverDimension.horizontal.from &&
148
- dimension.horizontal.to <= coverDimension.horizontal.to) {
149
- // the note ends where the cover starts
150
- dimension.horizontal.to = coverDimension.horizontal.from;
151
- }
152
- });
153
- insertions.splice(insertions.indexOf(cover), 1);
154
- }
155
- currentVersion.edits.push(...insertions.map((symbol) => {
156
- return {
157
- insert: [symbol],
158
- delete: [],
159
- id: v4(),
160
- };
161
- }));
162
- const deletions = snapshot.filter(sym => {
163
- return !treatedSymbols.includes(sym);
164
- });
165
- for (const symbol of deletions) {
166
- currentVersion.edits.push({
167
- insert: [],
168
- delete: [symbol],
169
- id: v4(),
170
- });
171
- }
172
- }
173
- /**
174
- * Assigns a generation (depth) to every node.
175
- */
176
- export function assignGenerations(versions) {
177
- const byId = new Map();
178
- for (const n of versions)
179
- byId.set(n.id, n);
180
- const memo = new Map(); // id -> generation
181
- const inStack = new Set(); // for cycle detection
182
- const computeGen = (id) => {
183
- if (memo.has(id))
184
- return memo.get(id);
185
- if (inStack.has(id)) {
186
- throw new Error(`Cycle detected involving node '${id}'. Check parentId links.`);
187
- }
188
- const node = byId.get(id);
189
- if (!node) {
190
- // If you ever call computeGen on a missing id, treat as root.
191
- memo.set(id, 0);
192
- return 0;
193
- }
194
- inStack.add(id);
195
- let gen;
196
- const p = node.basedOn?.assigned.id;
197
- if (p === undefined) {
198
- gen = 0; // root
199
- }
200
- else if (!byId.has(p)) {
201
- // Orphaned parent reference — treat boundary as root
202
- gen = 0;
203
- }
204
- else {
205
- gen = 1 + computeGen(p);
206
- }
207
- inStack.delete(id);
208
- memo.set(id, gen);
209
- return gen;
210
- };
211
- // Compute for all nodes (order doesn’t matter)
212
- const withGen = versions.map(n => ({
213
- ...n,
214
- generation: computeGen(n.id),
215
- }));
216
- return withGen;
217
- }
@@ -1,4 +1,5 @@
1
- import { RollFeature } from "./Feature";
1
+ import { AnyFeature } from "./Feature";
2
+ import { TrackerBar } from "./TrackerBar";
2
3
  type AlignmentResult = {
3
4
  shift: number;
4
5
  stretch: number;
@@ -7,5 +8,5 @@ type AlignmentResult = {
7
8
  * Align two rolls by computing independent linear fits of each roll's note-onset positions
8
9
  * using only the first and last segments, then deriving a transform x2 = (x1 + shift) * stretch.
9
10
  */
10
- export declare function alignFeatures(rollA: RollFeature[], rollB: RollFeature[]): AlignmentResult;
11
+ export declare function alignFeatures(rollA: AnyFeature[], rollB: AnyFeature[], bar?: TrackerBar): AlignmentResult;
11
12
  export {};
@@ -1,6 +1,6 @@
1
- import { WelteT100 } from "./TrackerBar";
2
- const isNote = (feature) => {
3
- return new WelteT100().meaningOf(feature.vertical.from).type === 'note';
1
+ import { welteT100 } from "./TrackerBar";
2
+ const isNoteOn = (bar) => (feature) => {
3
+ return feature.type === 'Hole' && bar.meaningOf(feature.vertical.from)?.type === 'note';
4
4
  };
5
5
  /**
6
6
  * Fit a line: position = alpha * index + beta via least squares.
@@ -33,8 +33,9 @@ function selectEnds(arr, count) {
33
33
  * Align two rolls by computing independent linear fits of each roll's note-onset positions
34
34
  * using only the first and last segments, then deriving a transform x2 = (x1 + shift) * stretch.
35
35
  */
36
- export function alignFeatures(rollA, rollB) {
36
+ export function alignFeatures(rollA, rollB, bar = welteT100) {
37
37
  // 1. Extract note-onset positions
38
+ const isNote = isNoteOn(bar);
38
39
  const allXA = rollA.filter(isNote).map(f => f.horizontal.from);
39
40
  const allXB = rollB.filter(isNote).map(f => f.horizontal.from);
40
41
  // 2. Determine segment size (e.g. 10% of notes, min 5)
package/lib/asJsonLd.d.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  import { Edition } from "./Edition";
2
- export declare const referenceTypes: string[];
2
+ export declare const exportDate: (date: Date) => string;
3
3
  export declare const asJsonLd: (edition: Edition) => any;
package/lib/asJsonLd.js CHANGED
@@ -1,13 +1,8 @@
1
- import { exportDate } from "./importJsonLd";
2
- // for these keys, references by id will be inserted
3
- // rather than the object itself.
4
- export const referenceTypes = [
5
- 'premises',
6
- 'delete',
7
- 'comprehends'
8
- ];
9
- const asIDArray = (arr) => {
10
- return arr.map(e => e.id);
1
+ export const exportDate = (date) => {
2
+ const year = date.getFullYear();
3
+ const month = String(date.getMonth() + 1).padStart(2, "0");
4
+ const day = String(date.getDate()).padStart(2, "0");
5
+ return `${year}-${month}-${day}`;
11
6
  };
12
7
  const asJsonLdEntity = (obj) => {
13
8
  if (obj instanceof Date) {
@@ -21,19 +16,6 @@ const asJsonLdEntity = (obj) => {
21
16
  if (typeof value === 'function' || typeof value === 'undefined') {
22
17
  // ignore
23
18
  }
24
- else if (referenceTypes.includes(key)) {
25
- if (!Array.isArray(value)) {
26
- console.error(`Expected array for key ${key}, got ${value}`);
27
- }
28
- else {
29
- result[key] = asIDArray(value);
30
- }
31
- }
32
- else if (key === 'assigned'
33
- && (result['@type'] === 'derivation'
34
- || result['@type'] === 'carrierAssignment')) {
35
- result['assigned'] = value.id;
36
- }
37
19
  else if (key === 'type') {
38
20
  result['@type'] = value;
39
21
  }
@@ -53,7 +35,8 @@ const asJsonLdEntity = (obj) => {
53
35
  return result;
54
36
  };
55
37
  export const asJsonLd = (edition) => {
56
- const result = {
38
+ const { base, copies, ...rest } = asJsonLdEntity(edition);
39
+ return {
57
40
  '@context': [
58
41
  'https://linked-rolls.org/rollo/1.0/edition.jsonld',
59
42
  {
@@ -61,7 +44,11 @@ export const asJsonLd = (edition) => {
61
44
  }
62
45
  ],
63
46
  '@type': "Edition",
64
- ...asJsonLdEntity(edition)
47
+ '@id': edition.base,
48
+ ...rest,
49
+ copies: copies?.map(copy => ({
50
+ ...copy,
51
+ '@id': `copy/${copy['@id']}`
52
+ }))
65
53
  };
66
- return result;
67
54
  };
@@ -1,4 +1,3 @@
1
1
  import { Edition } from "./Edition";
2
- export declare const exportDate: (date: Date) => string;
3
2
  export declare const importDate: (str: string) => Date;
4
3
  export declare const importJsonLd: (json: any) => Edition;