linked-rolls 0.15.0 → 0.17.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.
@@ -13,8 +13,10 @@ export const getAt = (path, obj) => {
13
13
  /** Keys under which an object names others by id, singly or in a list. */
14
14
  const referenceKeys = ['delete', 'comprehends', 'motivation'];
15
15
  const isObject = (v) => v !== null && typeof v === "object";
16
- /** An object that names another by its id and says nothing else, save perhaps a belief about the reference. */
17
- const isReferenceOnly = (keys) => keys.every(key => key === 'id' || key === '@annotation');
16
+ /** What a reference may state besides the id: a belief about it, and the tolerance a derivation was collated at. */
17
+ const referenceOwnKeys = new Set(['id', '@annotation', 'collationTolerance']);
18
+ /** An object that names another by its id and says nothing of its own beyond that. */
19
+ const isReferenceOnly = (keys) => keys.every(key => referenceOwnKeys.has(key));
18
20
  const pathOf = (trail) => {
19
21
  const path = [];
20
22
  for (let link = trail; link !== null; link = link.up)
package/lib/Feature.d.ts CHANGED
@@ -89,6 +89,14 @@ export declare const conditions: {
89
89
  readonly Mark: readonly ["faded"];
90
90
  readonly GluedOn: readonly ["detaching", "ripped"];
91
91
  };
92
+ /** The kinds of condition a feature may be in, whichever kind of feature it is. */
93
+ export type FeatureConditionType = typeof conditions[FeatureType][number];
94
+ /**
95
+ * A condition assigned to a feature, annotatable with a belief about
96
+ * its certainty. Which of the kinds a feature may be in depends on its
97
+ * own kind, which `conditions` states.
98
+ */
99
+ export type FeatureConditionAssignment = ObjectAssumption<ConditionState<FeatureConditionType>>;
92
100
  /**
93
101
  * A hole (perforation) in the roll paper. Holes are the primary
94
102
  * carriers of musical information on piano rolls, as they trigger
@@ -109,7 +117,7 @@ export interface Hole extends RollFeature<'Hole', typeof conditions.Hole[number]
109
117
  * A trace is a visible mark or writing on the roll surface.
110
118
  * Traces may fade over time.
111
119
  */
112
- export interface Trace<T extends FeatureType> extends RollFeature<T, typeof conditions.Mark[number]> {
120
+ export interface Trace<T extends FeatureType> extends RollFeature<T, typeof conditions[T][number]> {
113
121
  }
114
122
  export declare const writingMethods: readonly ['Print', 'Handwriting', 'Stamp'];
115
123
  /**
@@ -165,7 +173,7 @@ export interface GluedOn extends RollFeature<'GluedOn', typeof conditions.GluedO
165
173
  * Nested features do not need to be positioned explicitly.
166
174
  * @see crm:P56 bears feature
167
175
  */
168
- features?: PartialBy<AnyFeature, 'horizontal' | 'vertical'>[];
176
+ features?: NestedFeature[];
169
177
  }
170
178
  /**
171
179
  * The union of all physical feature types that can appear on a roll:
@@ -173,4 +181,14 @@ export interface GluedOn extends RollFeature<'GluedOn', typeof conditions.GluedO
173
181
  * and glued-on patches (paper, tape).
174
182
  */
175
183
  export type AnyFeature = Hole | Writing | Mark | GluedOn;
184
+ /**
185
+ * A feature borne by another feature. It states no place of its own,
186
+ * the feature bearing it standing in one.
187
+ */
188
+ export type NestedFeature = PartialBy<AnyFeature, 'horizontal' | 'vertical'>;
176
189
  export declare const isRollFeature: (obj: object) => obj is AnyFeature;
190
+ export declare const isGluedOn: <T extends NestedFeature>(feature: T) => feature is T & GluedOn;
191
+ /** The features a feature bears: a patch those stated as parts of it, any other feature none. */
192
+ export declare const featuresBorneBy: (feature: NestedFeature) => NestedFeature[];
193
+ /** The feature together with everything it bears, as deep as a patch on a patch goes. */
194
+ export declare const withBorneFeatures: (feature: NestedFeature) => NestedFeature[];
package/lib/Feature.js CHANGED
@@ -9,3 +9,8 @@ export const writingMethods = ['Print', 'Handwriting', 'Stamp'];
9
9
  export const isRollFeature = (obj) => {
10
10
  return 'type' in obj && featureTypes.includes(obj.type);
11
11
  };
12
+ export const isGluedOn = (feature) => feature.type === 'GluedOn';
13
+ /** The features a feature bears: a patch those stated as parts of it, any other feature none. */
14
+ export const featuresBorneBy = (feature) => isGluedOn(feature) ? feature.features ?? [] : [];
15
+ /** The feature together with everything it bears, as deep as a patch on a patch goes. */
16
+ export const withBorneFeatures = (feature) => [feature, ...featuresBorneBy(feature).flatMap(withBorneFeatures)];
package/lib/Quantity.d.ts CHANGED
@@ -7,7 +7,7 @@
7
7
  * The unit names are the ones the records state in their `unit` field.
8
8
  */
9
9
  declare const unit: unique symbol;
10
- export type Unit = 'mm' | 'cm' | 'px' | 'track' | 's' | 'ms' | 'ft/min' | 'm/min';
10
+ export type Unit = 'mm' | 'cm' | 'px' | 'track' | 's' | 'ms' | 'ft/min' | 'm/min' | 'px/in';
11
11
  export type Quantity<U extends Unit> = number & {
12
12
  readonly [unit]: U;
13
13
  };
@@ -22,6 +22,8 @@ export type Seconds = Quantity<'s'>;
22
22
  export type Milliseconds = Quantity<'ms'>;
23
23
  export type FeetPerMinute = Quantity<'ft/min'>;
24
24
  export type MetersPerMinute = Quantity<'m/min'>;
25
+ /** How finely a scan was read: pixels of the image per inch of paper. */
26
+ export type Resolution = Quantity<'px/in'>;
25
27
  export declare const mm: (value: number) => Quantity<"mm">;
26
28
  export declare const cm: (value: number) => Quantity<"cm">;
27
29
  export declare const px: (value: number) => Quantity<"px">;
@@ -30,6 +32,7 @@ export declare const seconds: (value: number) => Quantity<"s">;
30
32
  export declare const milliseconds: (value: number) => Quantity<"ms">;
31
33
  export declare const feetPerMinute: (value: number) => Quantity<"ft/min">;
32
34
  export declare const metersPerMinute: (value: number) => Quantity<"m/min">;
35
+ export declare const pixelsPerInch: (value: number) => Quantity<"px/in">;
33
36
  /**
34
37
  * A value together with the unit it was measured in, as a record
35
38
  * states it.
@@ -56,6 +59,8 @@ export declare const sum: <U extends Unit>(values: readonly Quantity<U>[]) => Qu
56
59
  export declare const mean: <U extends Unit>(values: readonly Quantity<U>[]) => Quantity<U>;
57
60
  /** A place in a scan taken at `dpi` dots per inch, on the paper. */
58
61
  export declare const inMillimeters: (place: Pixels, dpi: number) => Millimeters;
62
+ /** A place on the paper, in a scan taken at `dpi` dots per inch. */
63
+ export declare const inPixels: (place: Millimeters, dpi: number) => Pixels;
59
64
  export declare const inCentimeters: (length: Millimeters) => Centimeters;
60
65
  export declare const inSeconds: (time: Milliseconds) => Seconds;
61
66
  /** A speed as a record states it, in feet or metres per minute. */
package/lib/Quantity.js CHANGED
@@ -7,6 +7,7 @@ export const seconds = quantity;
7
7
  export const milliseconds = quantity;
8
8
  export const feetPerMinute = quantity;
9
9
  export const metersPerMinute = quantity;
10
+ export const pixelsPerInch = quantity;
10
11
  // The second operand takes its unit from the first. Inferred from both,
11
12
  // TypeScript would unite two units rather than reject them.
12
13
  export const add = (a, b) => quantity(a + b);
@@ -21,6 +22,8 @@ export const mean = (values) => quantity(sum(values) / values.length);
21
22
  const MM_PER_INCH = 25.4;
22
23
  /** A place in a scan taken at `dpi` dots per inch, on the paper. */
23
24
  export const inMillimeters = (place, dpi) => mm(place / dpi * MM_PER_INCH);
25
+ /** A place on the paper, in a scan taken at `dpi` dots per inch. */
26
+ export const inPixels = (place, dpi) => px(place / MM_PER_INCH * dpi);
24
27
  export const inCentimeters = (length) => cm(length / 10);
25
28
  export const inSeconds = (time) => seconds(time / 1000);
26
29
  const METERS_PER_FOOT = 0.3048;
package/lib/RollCopy.d.ts CHANGED
@@ -222,6 +222,16 @@ export interface RollCopy extends WithType<'RollCopy'>, WithId {
222
222
  * Not exported to RDF.
223
223
  */
224
224
  scale: number;
225
+ /**
226
+ * The resolution this copy's scan was read at, along the roll.
227
+ * It is what the measurements given in pixels are to be read
228
+ * against, and what every conversion between a place in the
229
+ * scan and one on the paper goes through. A scanner may read
230
+ * across the roll at another resolution, which the analysis
231
+ * files do not state.
232
+ * @see reo:scanResolution
233
+ */
234
+ scanResolution: Measure<'px/in'>;
225
235
  /**
226
236
  * Relates this copy's scan to the tracker bar: how the scanning
227
237
  * software's hole numbering was shifted onto the bar, and where
package/lib/Version.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { Edit } from "./Edit";
2
2
  import { ReferenceAssumption } from "./Assumption";
3
+ import { CollationTolerance } from "./Collation";
3
4
  import { AnySymbol } from "./Symbol";
4
5
  import { WithId, WithNote, WithType } from "./utils";
5
6
  export declare const versionTypes: readonly [
@@ -24,6 +25,23 @@ export type VersionType = typeof versionTypes[number];
24
25
  * @see crm:E33 Linguistic Object
25
26
  */
26
27
  export type Motivation = WithType<'motivation'> & WithId & WithNote;
28
+ /**
29
+ * A derivation names the version another one was derived from, together
30
+ * with the tolerance the two were collated at. How precisely the copies
31
+ * put a symbol depends on what they are and on how their features were
32
+ * obtained, so the tolerance can differ from derivation to derivation.
33
+ * @see lrmoo:R76 is derivative of
34
+ */
35
+ export type Derivation = ReferenceAssumption & {
36
+ /**
37
+ * The tolerance at which the derived version was collated against
38
+ * the one it is based on. A derivation written before the tolerance
39
+ * was held here states none. Not exported to RDF.
40
+ */
41
+ collationTolerance?: CollationTolerance;
42
+ };
43
+ /** The tolerance the derivation was collated at, or the default where it states none. */
44
+ export declare const collationToleranceOf: (derivation: Readonly<Derivation>) => CollationTolerance;
27
45
  /**
28
46
  * A version is defined by the sum of edits applied
29
47
  * to the version it is based on. For simple identification,
@@ -46,7 +64,7 @@ export interface Version extends WithId, WithType<'Version'> {
46
64
  * If no derivation is defined, it is assumed that this version represents the mother roll.
47
65
  * @see lrmoo:R76 is derivative of
48
66
  */
49
- basedOn?: ReferenceAssumption;
67
+ basedOn?: Derivation;
50
68
  /**
51
69
  * The list of edits that, applied to the base version, produce this version.
52
70
  * @see reo:involvedEdit
package/lib/Version.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { defaultCollationTolerance } from "./Collation";
1
2
  export const versionTypes = [
2
3
  /**
3
4
  * The roll is in a state where it is (possibly) used as
@@ -9,6 +10,8 @@ export const versionTypes = [
9
10
  */
10
11
  'unicum'
11
12
  ];
13
+ /** The tolerance the derivation was collated at, or the default where it states none. */
14
+ export const collationToleranceOf = (derivation) => derivation.collationTolerance ?? defaultCollationTolerance;
12
15
  /** The symbols the version's edits insert. */
13
16
  export const insertedBy = (version) => version.edits.flatMap(edit => edit.insert ?? []);
14
17
  /** The ids of the symbols the version's edits delete. */
@@ -4,10 +4,10 @@ import { Edition } from "./Edition";
4
4
  import { AnySymbol, PlacementRelation } from "./Symbol";
5
5
  import { CollationTolerance } from "./Collation";
6
6
  import { Edit } from "./Edit";
7
- import { RollCopy, ScaleReading, Shift } from "./RollCopy";
7
+ import { GeneralRollCondition, RollCopy, ScaleReading, Shift } from "./RollCopy";
8
8
  import { FeatureSource } from "./FeatureSource";
9
- import { AnyArgumentation, Certainty } from "./Assumption";
10
- import { AnyFeature } from "./Feature";
9
+ import { AnyArgumentation, Certainty, ObjectAssumption } from "./Assumption";
10
+ import { AnyFeature, FeatureConditionAssignment } from "./Feature";
11
11
  /**
12
12
  * A change to an edition, written onto an immer draft of it. One
13
13
  * operation is one undo step, so an operation that has to read the
@@ -36,13 +36,31 @@ export declare const unalignCopy: (copyId: string) => EditionOp;
36
36
  export declare const stateSource: (copyId: string, source: FeatureSource) => EditionOp;
37
37
  /** Takes back the statement, leaving the copy silent about its source again. */
38
38
  export declare const clearSource: (copyId: string) => EditionOp;
39
+ /**
40
+ * Adds a general condition to the copy, beside whatever is stated of it
41
+ * already. The other condition a copy may be in, a paper stretch, is
42
+ * read off an alignment and stated by `alignCopy`.
43
+ */
44
+ export declare const addGeneralCondition: (copyId: string, condition: ObjectAssumption<GeneralRollCondition>) => EditionOp;
45
+ /**
46
+ * States the condition of the feature, in place of any earlier
47
+ * statement. Throws where the kind of feature is in no such condition;
48
+ * `conditions` says which conditions each kind of feature may be in.
49
+ */
50
+ export declare const stateFeatureCondition: (copyId: string, featureId: string, condition: FeatureConditionAssignment) => EditionOp;
39
51
  /** The symbols of the versions that no other copy carries. */
40
52
  export declare const symbolsCarriedOnlyBy: (edition: Edition, copyId: string) => AnySymbol[];
41
- /** Takes the features off the copy, and out of the versions with what only they carried. */
53
+ /**
54
+ * Takes the features off the copy, and out of the versions with what
55
+ * only they carried. A feature may be named wherever the copy bears it:
56
+ * a patch goes with everything glued onto it, and a feature of a patch
57
+ * may be taken back on its own, the patch staying where it is.
58
+ */
42
59
  export declare const removeFeatures: (copyId: string, featureIds: readonly string[]) => EditionOp;
43
60
  /**
44
61
  * Takes the copy out of the edition together with the symbols only it
45
- * carries, and with every reference the versions made to those symbols.
62
+ * carries, and with every reference the versions and the argumentations
63
+ * made to those symbols.
46
64
  */
47
65
  export declare const removeCopy: (copyId: string) => EditionOp;
48
66
  /** Why several features cannot be replaced by one. */
@@ -61,7 +79,14 @@ export declare const mergeObstacle: (features: readonly AnyFeature[]) => MergeOb
61
79
  * all, where a scan has split what the editor reads as one feature.
62
80
  * The merged feature takes the place of the first it replaces and
63
81
  * spans from the first to the last, any gap between them included.
64
- * What the replaced features carried is carried by the merged one.
82
+ * What the replaced features carried is carried by the merged one, and
83
+ * whatever else named them, a modification or a comprehension, names
84
+ * the merged one in their stead.
85
+ *
86
+ * Only the features the copy bears itself are merged. A feature of a
87
+ * patch states no place along the roll of its own, so there is nothing
88
+ * for a merge to span, and an id naming one is passed over as an id the
89
+ * copy does not bear is.
65
90
  *
66
91
  * Throws where the features cannot stand for one; `mergeObstacle`
67
92
  * says beforehand whether they can.
@@ -71,12 +96,14 @@ export declare const mergeFeatures: (copyId: string, featureIds: readonly string
71
96
  * Bases the child on the parent. A symbol of the child that collates
72
97
  * with one the parent hands down adds its carriers to that symbol; the
73
98
  * rest become the child's insertions, and what the parent hands down
74
- * and the child lacks becomes its deletions.
99
+ * and the child lacks becomes its deletions. The derivation states the
100
+ * tolerance it was collated at.
75
101
  */
76
102
  export declare const connectVersions: (view: EditionView, childId: string, parentId: string, tolerance?: CollationTolerance) => EditionOp;
77
103
  /**
78
104
  * Folds the version's own symbols into those it inherits and collates
79
- * with: the carriers pass over, and the insertions go.
105
+ * with: the carriers pass over, and the insertions go. Collates at the
106
+ * tolerance of the derivation, where the caller names none.
80
107
  */
81
108
  export declare const collateSymbols: (view: EditionView, versionId: string, symbolIds: readonly string[], tolerance?: CollationTolerance) => EditionOp;
82
109
  /**
package/lib/editionOps.js CHANGED
@@ -3,10 +3,11 @@ import { v4 } from "uuid";
3
3
  import { getAt } from "./EditionView";
4
4
  import { isPerforation, placementRelations } from "./Symbol";
5
5
  import { collationsOf, defaultCollationTolerance } from "./Collation";
6
- import { insertedBy } from "./Version";
6
+ import { collationToleranceOf, insertedBy } from "./Version";
7
7
  import { asSymbols } from "./RollCopy";
8
8
  import { applyShift, applyScale, revertShift, revertScale } from "./alignment";
9
9
  import { assignReference, idOf } from "./Assumption";
10
+ import { conditions as conditionsAllowed, featuresBorneBy, isGluedOn, withBorneFeatures } from "./Feature";
10
11
  import { distance, mm, subtract } from "./Quantity";
11
12
  const noChange = () => undefined;
12
13
  const onCopy = (copyId, op) => draft => {
@@ -19,6 +20,11 @@ const onVersion = (versionId, op) => draft => {
19
20
  if (version)
20
21
  op(version, draft);
21
22
  };
23
+ const onFeature = (copyId, featureId, op) => onCopy(copyId, copy => {
24
+ const feature = copy.features.find(f => f.id === featureId);
25
+ if (feature)
26
+ op(feature);
27
+ });
22
28
  /**
23
29
  * The state a draft stands at, as plain data. Reading a draft proxies
24
30
  * everything it touches, so what is only read is read from this.
@@ -31,15 +37,24 @@ const mapped = (items, change) => {
31
37
  const changed = items.map(change);
32
38
  return changed.every((item, i) => item === items[i]) ? items : changed;
33
39
  };
40
+ /** The items each changed, less those the change emptied; the very same array where it changed none. */
41
+ const pruned = (items, change, emptied) => {
42
+ const changed = mapped(items, change);
43
+ return changed === items ? items : changed.filter((item, i) => item === items[i] || !emptied(item));
44
+ };
45
+ /** The record with the field replaced, or the very same record where that is what stood there. */
46
+ const replacing = (record, key, value) => record[key] === value ? record : { ...record, [key]: value };
47
+ /** The ids of the items the rewriting left out. */
48
+ const droppedIds = (before, after) => {
49
+ const kept = new Set(after.map(item => item.id));
50
+ return before.flatMap(item => kept.has(item.id) ? [] : [item.id]);
51
+ };
34
52
  const insertion = (symbol) => ({ type: 'edit', id: v4(), insert: [symbol] });
35
53
  const deletion = (symbolId) => ({ type: 'edit', id: v4(), delete: [symbolId] });
36
54
  const isEmpty = (edit) => !edit.insert?.length && !edit.delete?.length;
37
55
  const insertedIn = (versions) => versions.flatMap(insertedBy);
38
56
  /** The edits with the change applied, less those it emptied; the very same array where it changed none. */
39
- const edited = (edits, change) => {
40
- const changed = mapped(edits, change);
41
- return changed === edits ? edits : changed.filter((edit, i) => edit === edits[i] || !isEmpty(edit));
42
- };
57
+ const edited = (edits, change) => pruned(edits, change, isEmpty);
43
58
  /** The edit without the symbols among its insertions, or the very same edit where it inserts none of them. */
44
59
  const droppingInsertions = (symbolIds) => (edit) => {
45
60
  const insert = edit.insert && without(edit.insert, symbol => symbolIds.has(symbol.id));
@@ -104,7 +119,36 @@ export const stateSource = (copyId, source) => onCopy(copyId, copy => {
104
119
  export const clearSource = (copyId) => onCopy(copyId, copy => {
105
120
  copy.readFrom = undefined;
106
121
  });
107
- const featureIdsOf = (copy) => new Set(copy.features.map(feature => feature.id));
122
+ /**
123
+ * Adds a general condition to the copy, beside whatever is stated of it
124
+ * already. The other condition a copy may be in, a paper stretch, is
125
+ * read off an alignment and stated by `alignCopy`.
126
+ */
127
+ export const addGeneralCondition = (copyId, condition) => onCopy(copyId, copy => {
128
+ copy.conditions.push(condition);
129
+ });
130
+ /**
131
+ * Whether the feature's own kind allows a condition of this kind. The
132
+ * type narrowed to is the weaker statement, holding of a condition any
133
+ * kind of feature may be in.
134
+ */
135
+ const allows = (feature, condition) => {
136
+ const allowed = conditionsAllowed[feature.type];
137
+ return allowed.includes(condition.conditionType);
138
+ };
139
+ /**
140
+ * States the condition of the feature, in place of any earlier
141
+ * statement. Throws where the kind of feature is in no such condition;
142
+ * `conditions` says which conditions each kind of feature may be in.
143
+ */
144
+ export const stateFeatureCondition = (copyId, featureId, condition) => onFeature(copyId, featureId, feature => {
145
+ if (!allows(feature, condition)) {
146
+ throw new Error(`A ${feature.type} is in no '${condition.conditionType}' condition`);
147
+ }
148
+ feature.condition = condition;
149
+ });
150
+ /** The ids of every feature the copy bears, those a patch bears among them. */
151
+ const featureIdsOf = (copy) => new Set(copy.features.flatMap(withBorneFeatures).map(feature => feature.id));
108
152
  /**
109
153
  * A symbol every carrier of which lies among the features loses its
110
154
  * evidence with them. A symbol without carriers, such as a label,
@@ -146,34 +190,136 @@ const forgettingFeatures = (features, dropped) => {
146
190
  return { ...edit, ...(insert && { insert }), ...(deleted && { delete: deleted }) };
147
191
  };
148
192
  };
193
+ const isRecord = (value) => typeof value === 'object' && value !== null;
194
+ /** The record's values each changed, or the very same record where the change left every one as it was. */
195
+ const withValues = (record, change) => {
196
+ const entries = Object.entries(record);
197
+ const changed = entries.map(([key, value]) => [key, change(value)]);
198
+ return changed.every(([, value], i) => value === entries[i][1]) ? record : Object.fromEntries(changed);
199
+ };
200
+ /** The value with the change applied to every record within it and then to itself, innermost first. */
201
+ const deeply = (change) => {
202
+ const changed = (value) => Array.isArray(value) ? mapped(value, changed)
203
+ : isRecord(value) ? change(withValues(value, changed))
204
+ : value;
205
+ return changed;
206
+ };
207
+ /** Whether the two stand alike, so that what differs within them can be written where it lies. */
208
+ const alike = (before, after) => {
209
+ if (!isRecord(before) || !isRecord(after))
210
+ return false;
211
+ if (Array.isArray(before))
212
+ return Array.isArray(after) && before.length === after.length;
213
+ return !Array.isArray(after);
214
+ };
215
+ /** Writes onto the draft what the rewriting changed, as deep as the change reaches. */
216
+ const writeInto = (draft, before, after) => Object.entries(after).forEach(([key, value]) => {
217
+ if (value === before[key])
218
+ return;
219
+ if (alike(before[key], value))
220
+ writeInto(draft[key], before[key], value);
221
+ else
222
+ draft[key] = value;
223
+ });
224
+ const isCopy = (record) => 'type' in record && record.type === 'RollCopy';
225
+ const isBelief = (record) => 'type' in record && record.type === 'belief';
226
+ const isComprehension = (reason) => reason.type === 'meaningComprehension';
227
+ /** What the modification names, be it as added or as removed. */
228
+ const membersOf = (modification) => modification.type === 'Addition' ? modification.added : modification.removed;
229
+ const namesNothing = (modification) => membersOf(modification).length === 0;
230
+ const comprehendsNothing = (reason) => isComprehension(reason) && reason.comprehends.length === 0;
231
+ /** The modification with what it names rewritten, or the very same one where that leaves it as it was. */
232
+ const renamingMembers = (rename) => (modification) => modification.type === 'Addition'
233
+ ? replacing(modification, 'added', rename(modification.added))
234
+ : replacing(modification, 'removed', rename(modification.removed));
235
+ /** The reason with what a comprehension comprehends rewritten; any other reason names nothing of the kind. */
236
+ const renamingComprehended = (rename) => (reason) => isComprehension(reason) ? replacing(reason, 'comprehends', rename(reason.comprehends)) : reason;
237
+ /**
238
+ * The record with the ids it names rewritten: what a copy's
239
+ * modifications added or removed, and what the comprehensions among a
240
+ * belief's reasons comprehend. A modification or a comprehension the
241
+ * rewriting emptied goes with what it named; one that named nothing
242
+ * before stays, as an edit that was empty before it does.
243
+ */
244
+ const renaming = (rename) => (record) => {
245
+ if (isCopy(record)) {
246
+ return replacing(record, 'modifications', pruned(record.modifications, renamingMembers(rename), namesNothing));
247
+ }
248
+ if (isBelief(record)) {
249
+ return replacing(record, 'reasons', pruned(record.reasons, renamingComprehended(rename), comprehendsNothing));
250
+ }
251
+ return record;
252
+ };
149
253
  /**
150
- * Strikes the features from the versions: their carriers go, a symbol
151
- * that had no other carrier goes with them, and so does every
152
- * reference the versions made to such a symbol.
254
+ * Rewrites the ids by which the edition names features and symbols
255
+ * outside the versions. A belief is annotatable anywhere, so the whole
256
+ * edition is read; what the rewriting leaves alone is left the very
257
+ * object it was.
258
+ */
259
+ const renameReferences = (draft, rename) => {
260
+ const before = stateOf(draft);
261
+ const after = deeply(renaming(rename))(before);
262
+ if (after !== before)
263
+ writeInto(draft, before, after);
264
+ };
265
+ /**
266
+ * Strikes the features from the edition: their carriers go, a symbol
267
+ * that had no other carrier goes with them, an edit left exchanging
268
+ * nothing goes as well, and so does every reference the versions, the
269
+ * modifications of the copies and the comprehensions made to what went.
153
270
  */
154
271
  const forgetFeatures = (draft, features) => {
155
272
  const versions = stateOf(draft.versions);
156
273
  const dropped = new Set(insertedIn(versions).filter(carriedOnlyOn(features)).map(symbol => symbol.id));
157
274
  const forget = forgettingFeatures(features, dropped);
275
+ const edits = versions.map(version => edited(version.edits, forget));
276
+ const gone = new Set([
277
+ ...features,
278
+ ...dropped,
279
+ ...versions.flatMap((version, i) => droppedIds(version.edits, edits[i]))
280
+ ]);
281
+ renameReferences(draft, ids => without(ids, id => gone.has(id)));
158
282
  draft.versions.forEach((version, i) => {
159
- version.edits = edited(versions[i].edits, forget);
283
+ version.edits = edits[i];
160
284
  });
161
285
  };
162
- /** Takes the features off the copy, and out of the versions with what only they carried. */
286
+ /**
287
+ * The ids of the features named and of everything they bear: a feature
288
+ * of a patch stands nowhere once the patch is gone.
289
+ */
290
+ const goneWith = (features, named) => features.flatMap(feature => named.has(feature.id)
291
+ ? withBorneFeatures(feature).map(borne => borne.id)
292
+ : goneWith(featuresBorneBy(feature), named));
293
+ /** The feature with the named ones gone from what it bears, or the very same one where it bears none of them. */
294
+ const withoutBorne = (feature, named) => {
295
+ if (!isGluedOn(feature) || !feature.features)
296
+ return feature;
297
+ const borne = withoutFeatures(feature.features, named);
298
+ return borne === feature.features ? feature : { ...feature, features: borne };
299
+ };
300
+ /** The features without those named, wherever they lie, and without whatever those bore. */
301
+ const withoutFeatures = (features, named) => mapped(without(features, feature => named.has(feature.id)), feature => withoutBorne(feature, named));
302
+ /**
303
+ * Takes the features off the copy, and out of the versions with what
304
+ * only they carried. A feature may be named wherever the copy bears it:
305
+ * a patch goes with everything glued onto it, and a feature of a patch
306
+ * may be taken back on its own, the patch staying where it is.
307
+ */
163
308
  export const removeFeatures = (copyId, featureIds) => onCopy(copyId, (copy, draft) => {
164
- const features = new Set(featureIds);
165
- copy.features = without(copy.features, feature => features.has(feature.id));
166
- forgetFeatures(draft, features);
309
+ const named = new Set(featureIds);
310
+ const features = stateOf(copy.features);
311
+ copy.features = withoutFeatures(features, named);
312
+ forgetFeatures(draft, new Set(goneWith(features, named)));
167
313
  });
168
314
  /**
169
315
  * Takes the copy out of the edition together with the symbols only it
170
- * carries, and with every reference the versions made to those symbols.
316
+ * carries, and with every reference the versions and the argumentations
317
+ * made to those symbols.
171
318
  */
172
319
  export const removeCopy = (copyId) => onCopy(copyId, (copy, draft) => {
173
320
  forgetFeatures(draft, featureIdsOf(copy));
174
321
  draft.copies = draft.copies.filter(c => c.id !== copyId);
175
322
  });
176
- const isRecord = (value) => typeof value === 'object' && value !== null;
177
323
  /**
178
324
  * Whether two records of the edition state the same, the identity of a
179
325
  * statement left out: two transcriptions reading the same word say the
@@ -188,10 +334,17 @@ const sayTheSame = (a, b) => {
188
334
  keys.delete('id');
189
335
  return [...keys].every(key => sayTheSame(a[key], b[key]));
190
336
  };
191
- /** What a feature states beyond its identity, its place along the roll, its depiction and its condition. */
337
+ /**
338
+ * What a feature states beyond its identity, its place along the roll,
339
+ * its depiction and its condition. What a patch bears counts by
340
+ * identity: two patches bear the same only where they bear the very
341
+ * same features, so that no merge takes a feature of a patch away.
342
+ */
192
343
  const nature = (feature) => {
193
344
  const { id, horizontal, depiction, condition, ...rest } = feature;
194
- return rest;
345
+ return isGluedOn(feature) && feature.features
346
+ ? { ...rest, features: feature.features.map(borne => borne.id) }
347
+ : rest;
195
348
  };
196
349
  const conditionsOf = (features) => features.flatMap(feature => feature.condition ? [feature.condition] : []);
197
350
  /**
@@ -266,23 +419,31 @@ const carryOver = (draft, replaced, mergedId) => {
266
419
  * all, where a scan has split what the editor reads as one feature.
267
420
  * The merged feature takes the place of the first it replaces and
268
421
  * spans from the first to the last, any gap between them included.
269
- * What the replaced features carried is carried by the merged one.
422
+ * What the replaced features carried is carried by the merged one, and
423
+ * whatever else named them, a modification or a comprehension, names
424
+ * the merged one in their stead.
425
+ *
426
+ * Only the features the copy bears itself are merged. A feature of a
427
+ * patch states no place along the roll of its own, so there is nothing
428
+ * for a merge to span, and an id naming one is passed over as an id the
429
+ * copy does not bear is.
270
430
  *
271
431
  * Throws where the features cannot stand for one; `mergeObstacle`
272
432
  * says beforehand whether they can.
273
433
  */
274
434
  export const mergeFeatures = (copyId, featureIds) => onCopy(copyId, (copy, draft) => {
275
- const replaced = new Set(featureIds);
276
- const isReplaced = (feature) => replaced.has(feature.id);
435
+ const named = new Set(featureIds);
277
436
  const features = stateOf(copy.features);
278
- const toMerge = features.filter(isReplaced);
437
+ const toMerge = features.filter(feature => named.has(feature.id));
279
438
  const obstacle = mergeObstacle(toMerge);
280
439
  if (obstacle) {
281
440
  throw new Error(`The features of copy ${copyId} cannot be merged: ${obstacle}`);
282
441
  }
442
+ const replaced = new Set(toMerge.map(feature => feature.id));
283
443
  const merged = mergedFrom(toMerge);
284
- copy.features = standingFor(features, isReplaced, merged);
444
+ copy.features = standingFor(features, feature => replaced.has(feature.id), merged);
285
445
  carryOver(draft, replaced, merged.id);
446
+ renameReferences(draft, ids => standingFor(ids, id => replaced.has(id), merged.id));
286
447
  });
287
448
  /** The carriers of each collated symbol pass to its counterpart. */
288
449
  const handOverCarriers = (view, draft, collations) => collations.forEach(({ symbol, counterpart }) => {
@@ -294,7 +455,8 @@ const handOverCarriers = (view, draft, collations) => collations.forEach(({ symb
294
455
  * Bases the child on the parent. A symbol of the child that collates
295
456
  * with one the parent hands down adds its carriers to that symbol; the
296
457
  * rest become the child's insertions, and what the parent hands down
297
- * and the child lacks becomes its deletions.
458
+ * and the child lacks becomes its deletions. The derivation states the
459
+ * tolerance it was collated at.
298
460
  */
299
461
  export const connectVersions = (view, childId, parentId, tolerance = defaultCollationTolerance) => {
300
462
  const inherited = view.snapshot(parentId);
@@ -309,20 +471,21 @@ export const connectVersions = (view, childId, parentId, tolerance = defaultColl
309
471
  return onVersion(childId, (child, draft) => {
310
472
  handOverCarriers(view, draft, collations);
311
473
  child.edits = edits;
312
- child.basedOn = assignReference(parentId);
474
+ child.basedOn = { ...assignReference(parentId), collationTolerance: tolerance };
313
475
  });
314
476
  };
315
477
  /**
316
478
  * Folds the version's own symbols into those it inherits and collates
317
- * with: the carriers pass over, and the insertions go.
479
+ * with: the carriers pass over, and the insertions go. Collates at the
480
+ * tolerance of the derivation, where the caller names none.
318
481
  */
319
- export const collateSymbols = (view, versionId, symbolIds, tolerance = defaultCollationTolerance) => {
482
+ export const collateSymbols = (view, versionId, symbolIds, tolerance) => {
320
483
  const version = view.get(versionId);
321
484
  if (!version?.basedOn)
322
485
  return noChange;
323
486
  const chosen = new Set(symbolIds);
324
487
  const own = insertedIn([version]).filter(symbol => chosen.has(symbol.id));
325
- const collations = collationsOf(own, view.snapshot(idOf(version.basedOn)), symbol => view.dimensionOf(symbol), tolerance);
488
+ const collations = collationsOf(own, view.snapshot(idOf(version.basedOn)), symbol => view.dimensionOf(symbol), tolerance ?? collationToleranceOf(version.basedOn));
326
489
  const collated = new Set(collations.map(({ symbol }) => symbol.id));
327
490
  return onVersion(versionId, (version, draft) => {
328
491
  handOverCarriers(view, draft, collations);
package/lib/migrate.js CHANGED
@@ -107,4 +107,24 @@ const withRollSystem = (edition) => {
107
107
  const withEditors = (edition) => !edition.creation || edition.creation.editors
108
108
  ? edition
109
109
  : { ...edition, creation: { ...edition.creation, editors: [] } };
110
- export const migrate = (edition) => walk(withRollSystem(withEditors(edition)));
110
+ const statesNoTolerance = (version) => version.basedOn && !version.basedOn.collationTolerance;
111
+ /**
112
+ * The collation tolerance was the edition's before it was stated on
113
+ * each derivation. An edition written then collated every version at
114
+ * that one value, so it is written onto every derivation that gives
115
+ * none of its own.
116
+ */
117
+ const withDerivationTolerance = (edition) => {
118
+ const collationTolerance = edition.creation?.collationTolerance;
119
+ const versions = Array.isArray(edition.versions) ? edition.versions : [];
120
+ if (!collationTolerance || !versions.some(statesNoTolerance))
121
+ return edition;
122
+ return {
123
+ ...edition,
124
+ versions: versions.map(version => statesNoTolerance(version)
125
+ ? { ...version, basedOn: { ...version.basedOn, collationTolerance } }
126
+ : version)
127
+ };
128
+ };
129
+ const editionSteps = [withRollSystem, withEditors, withDerivationTolerance];
130
+ export const migrate = (edition) => walk(editionSteps.reduce((result, step) => step(result), edition));
@@ -2,7 +2,7 @@ import { v4 } from "uuid";
2
2
  import { AtonParser } from "./AtonParser";
3
3
  import { systemOf, translationBetween } from "../TrackerBar";
4
4
  import { welteT100 } from "../systems/welteT100/bar";
5
- import { inMillimeters, mean, mm, px, subtract, track } from "../Quantity";
5
+ import { inMillimeters, mean, mm, pixelsPerInch, px, subtract, track } from "../Quantity";
6
6
  /** Values in these files carry their unit as a suffix, e.g. "37.7646px". */
7
7
  const readPx = (value) => px(parseFloat(value));
8
8
  /** The parser writes one record as an object and several as an array. */
@@ -155,6 +155,9 @@ export function readFromStanfordAton(atonString, { trackShift, bar = welteT100,
155
155
  bass: readPx(json.ROLLINFO.HARD_MARGIN_BASS),
156
156
  unit: 'px'
157
157
  },
158
+ ...(Number.isFinite(dpi) && {
159
+ scanResolution: { value: pixelsPerInch(dpi), unit: 'px/in' }
160
+ }),
158
161
  trackCalibration: calibration,
159
162
  ...(measuredBy && { measuredBy })
160
163
  },
package/lib/schema.json CHANGED
@@ -277,6 +277,41 @@
277
277
  ],
278
278
  "type": "object"
279
279
  },
280
+ "Derivation": {
281
+ "description": "A derivation names the version another one was derived from, together with the tolerance the two were collated at. How precisely the copies put a symbol depends on what they are and on how their features were obtained, so the tolerance can differ from derivation to derivation. [ontology: lrmoo:R76 is derivative of]",
282
+ "properties": {
283
+ "@annotation": {
284
+ "description": "An optional annotation expressing a belief about this assumption. Uses the JSON-LD-star `@annotation` mechanism to attach epistemic metadata (certainty and reasons) to any triple.",
285
+ "properties": {
286
+ "belief": {
287
+ "$ref": "#/definitions/Belief",
288
+ "description": "The belief held about the annotated statement. [ontology: crminf:J4i is subject of]"
289
+ },
290
+ "@id": {
291
+ "description": "A unique identifier for this object.",
292
+ "type": "string"
293
+ }
294
+ },
295
+ "required": [
296
+ "belief",
297
+ "@id"
298
+ ],
299
+ "type": "object"
300
+ },
301
+ "collationTolerance": {
302
+ "$ref": "#/definitions/CollationTolerance",
303
+ "description": "The tolerance at which the derived version was collated against the one it is based on. A derivation written before the tolerance was held here states none. Not exported to RDF."
304
+ },
305
+ "@id": {
306
+ "description": "A unique identifier for this object.",
307
+ "type": "string"
308
+ }
309
+ },
310
+ "required": [
311
+ "@id"
312
+ ],
313
+ "type": "object"
314
+ },
280
315
  "Edit": {
281
316
  "description": "A set of edits transforms a version of a roll into another version. Edits insert or delete symbols, or both (= replace). Edits may be motivated by a given set of reasons, e.g. to add an additional accent or to correct an error. If an edit is the interpretation of a metamark, such as a pencil mark, this should be made explicit using a meaning comprehension on the `@annotation` field. [ontology: reo:Edit]",
282
317
  "properties": {
@@ -591,7 +626,7 @@
591
626
  "features": {
592
627
  "description": "A glued-on feature itself may carry other features. Nested features do not need to be positioned explicitly. [ontology: crm:P56 bears feature]",
593
628
  "items": {
594
- "$ref": "#/definitions/PartialBy%3CAnyFeature%2C(%22horizontal%22%7C%22vertical%22)%3E"
629
+ "$ref": "#/definitions/NestedFeature"
595
630
  },
596
631
  "type": "array"
597
632
  },
@@ -882,6 +917,25 @@
882
917
  ],
883
918
  "type": "object"
884
919
  },
920
+ "Measure<\"px/in\">": {
921
+ "description": "A value together with the unit it was measured in, as a record states it. [ontology: crm:E54 Dimension]",
922
+ "properties": {
923
+ "unit": {
924
+ "const": "px/in",
925
+ "description": "The unit of measurement. [ontology: crm:P91 has unit]",
926
+ "type": "string"
927
+ },
928
+ "value": {
929
+ "$ref": "#/definitions/Quantity%3C%22px%2Fin%22%3E",
930
+ "description": "The measured value. [ontology: crm:P90 has value]"
931
+ }
932
+ },
933
+ "required": [
934
+ "value",
935
+ "unit"
936
+ ],
937
+ "type": "object"
938
+ },
885
939
  "Millimeters": {
886
940
  "$ref": "#/definitions/Quantity%3C%22mm%22%3E",
887
941
  "description": "A place along the roll, or any length on the paper."
@@ -995,6 +1049,10 @@
995
1049
  ],
996
1050
  "type": "object"
997
1051
  },
1052
+ "NestedFeature": {
1053
+ "$ref": "#/definitions/PartialBy%3CAnyFeature%2C(%22horizontal%22%7C%22vertical%22)%3E",
1054
+ "description": "A feature borne by another feature. It states no place of its own, the feature bearing it standing in one."
1055
+ },
998
1056
  "Note": {
999
1057
  "description": "A note symbol, representing a single pitched musical event on the roll. The pitch is encoded via the tracker bar position (track number). [ontology: reo:Note]",
1000
1058
  "properties": {
@@ -1227,6 +1285,49 @@
1227
1285
  ],
1228
1286
  "type": "object"
1229
1287
  },
1288
+ "ObjectAssumption<ConditionState<(\"illegible\")>>": {
1289
+ "description": "An object assumption wraps a complex object with an optional annotation. Used for structured values (e.g. persons, conditions) whose properties may be uncertain.",
1290
+ "properties": {
1291
+ "@annotation": {
1292
+ "description": "An optional annotation expressing a belief about this assumption. Uses the JSON-LD-star `@annotation` mechanism to attach epistemic metadata (certainty and reasons) to any triple.",
1293
+ "properties": {
1294
+ "belief": {
1295
+ "$ref": "#/definitions/Belief",
1296
+ "description": "The belief held about the annotated statement. [ontology: crminf:J4i is subject of]"
1297
+ },
1298
+ "@id": {
1299
+ "description": "A unique identifier for this object.",
1300
+ "type": "string"
1301
+ }
1302
+ },
1303
+ "required": [
1304
+ "belief",
1305
+ "@id"
1306
+ ],
1307
+ "type": "object"
1308
+ },
1309
+ "conditionType": {
1310
+ "const": "illegible",
1311
+ "description": "The kind of condition, from the list the roll or the kind of feature allows. [ontology: crm:P2 has type]",
1312
+ "type": "string"
1313
+ },
1314
+ "description": {
1315
+ "description": "A free-text description of the condition, providing details beyond the type classification. [ontology: crm:P3 has note]",
1316
+ "type": "string"
1317
+ },
1318
+ "@type": {
1319
+ "const": "ConditionState",
1320
+ "description": "The type discriminator for this object. [ontology: rdf:type]",
1321
+ "type": "string",
1322
+ "ontology": "rdf:type"
1323
+ }
1324
+ },
1325
+ "required": [
1326
+ "conditionType",
1327
+ "@type"
1328
+ ],
1329
+ "type": "object"
1330
+ },
1230
1331
  "ObjectAssumption<ConditionState<(\"partially-torn\"|\"missing-perforation\")>>": {
1231
1332
  "description": "An object assumption wraps a complex object with an optional annotation. Used for structured values (e.g. persons, conditions) whose properties may be uncertain.",
1232
1333
  "properties": {
@@ -1610,6 +1711,9 @@
1610
1711
  "Quantity<\"px\">": {
1611
1712
  "type": "number"
1612
1713
  },
1714
+ "Quantity<\"px/in\">": {
1715
+ "type": "number"
1716
+ },
1613
1717
  "Quantity<\"track\">": {
1614
1718
  "type": "number"
1615
1719
  },
@@ -1817,6 +1921,10 @@
1817
1921
  "description": "The factor this copy's features were scaled by to align them with the others. What it is put down to is stated apart: a paper-stretch condition, or the speed the copy was cut for. Not exported to RDF.",
1818
1922
  "type": "number"
1819
1923
  },
1924
+ "scanResolution": {
1925
+ "$ref": "#/definitions/Measure%3C%22px%2Fin%22%3E",
1926
+ "description": "The resolution this copy's scan was read at, along the roll. It is what the measurements given in pixels are to be read against, and what every conversion between a place in the scan and one on the paper goes through. A scanner may read across the roll at another resolution, which the analysis files do not state. [ontology: reo:scanResolution]"
1927
+ },
1820
1928
  "shift": {
1821
1929
  "$ref": "#/definitions/Shift",
1822
1930
  "description": "The shift applied to align this copy with the others. Not exported to RDF."
@@ -1978,7 +2086,7 @@
1978
2086
  "description": "A version is defined by the sum of edits applied to the version it is based on. For simple identification, a siglum is given to each version. [ontology: lrmoo:F2 Expression]",
1979
2087
  "properties": {
1980
2088
  "basedOn": {
1981
- "$ref": "#/definitions/ReferenceAssumption",
2089
+ "$ref": "#/definitions/Derivation",
1982
2090
  "description": "If no derivation is defined, it is assumed that this version represents the mother roll. [ontology: lrmoo:R76 is derivative of]"
1983
2091
  },
1984
2092
  "edits": {
@@ -2059,7 +2167,7 @@
2059
2167
  "description": "A piece of writing found on the roll, such as a label, catalogue number, or annotation. Writings have a method of production and a transcription of their content. [ontology: crm:E25 Human-Made Feature]",
2060
2168
  "properties": {
2061
2169
  "condition": {
2062
- "$ref": "#/definitions/ObjectAssumption%3CConditionState%3C(%22faded%22)%3E%3E",
2170
+ "$ref": "#/definitions/ObjectAssumption%3CConditionState%3C(%22illegible%22)%3E%3E",
2063
2171
  "description": "This can be used e.g. to indicate a perforation which is torn out or in any other way damaged. [ontology: crm:P44 has condition]"
2064
2172
  },
2065
2173
  "depiction": {
@@ -131,6 +131,7 @@
131
131
  "height": "reo:height",
132
132
  "punchDiameter": "reo:punchDiameter",
133
133
  "holeSeparation": "reo:holeSeparation",
134
+ "scanResolution": "reo:scanResolution",
134
135
  "value": "crm:P90_has_value",
135
136
  "measuredBy": "crmdig:L23_used_software_or_firmware",
136
137
  "software": "rdfs:label",
package/lib/validate.d.ts CHANGED
@@ -1,3 +1,12 @@
1
+ import { type ErrorObject } from "ajv";
1
2
  import { Edition } from "./Edition";
2
- declare const validate: import("ajv").ValidateFunction<Edition>;
3
+ /**
4
+ * Holds a document against the edition schema. `errors` says what the
5
+ * last call found wrong, and is empty where it found nothing.
6
+ */
7
+ export interface ValidateEdition {
8
+ (document: unknown): document is Edition;
9
+ errors?: ErrorObject[] | null;
10
+ }
11
+ declare const validate: ValidateEdition;
3
12
  export { validate };
package/lib/validate.js CHANGED
@@ -1,10 +1,15 @@
1
1
  import Ajv from "ajv";
2
2
  import * as schema from "./schema.json";
3
- const ajv = new Ajv({
4
- strict: false,
5
- formats: {
6
- "date": true
7
- }
8
- });
9
- const validate = ajv.compile(schema);
3
+ const compileEditionSchema = () => new Ajv({ strict: false, formats: { "date": true } }).compile(schema);
4
+ /**
5
+ * Compiling costs enough that a consumer which never validates should not
6
+ * pay for it, so the compiled form is made on first use and kept.
7
+ */
8
+ let compiled;
9
+ const validate = (document) => {
10
+ compiled ?? (compiled = compileEditionSchema());
11
+ const valid = compiled(document);
12
+ validate.errors = compiled.errors;
13
+ return valid;
14
+ };
10
15
  export { validate };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "linked-rolls",
3
- "version": "0.15.0",
3
+ "version": "0.17.0",
4
4
  "description": "Digital editions of piano rolls: import, collation, editorial assumptions, JSON-LD export, and emulation through a reproducing system",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -8,6 +8,7 @@
8
8
  "url": "git+https://github.com/pfefferniels/linked-rolls.git"
9
9
  },
10
10
  "type": "module",
11
+ "sideEffects": false,
11
12
  "main": "lib/index.js",
12
13
  "types": "lib/index.d.ts",
13
14
  "exports": {