All files / platform/core/src/utils updateNewInstanceMetadata.ts

10% Statements 2/20
0% Branches 0/11
0% Functions 0/4
10.52% Lines 2/19

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126                                                                                              216x                     216x                                                                                                                                      
import { DicomMetadataStore } from '../services/DicomMetadataStore/DicomMetadataStore';
import { parseUTCOffset } from './latestInstanceDateTime';
 
/**
 * The current date and time as DICOM DA and TM values.
 *
 * DICOM DA and TM are wall clock values, read in the timezone the object
 * declares in `TimezoneOffsetFromUTC` and otherwise in the local one.  They are
 * displayed as they are stored, so a value in any other zone is simply the
 * wrong date/time to show - and around midnight the wrong day as well.
 *
 * @param now - the instant to express, defaulting to the current one
 * @param timezoneOffsetFromUTC - the object's `TimezoneOffsetFromUTC`, if it
 *   has one; local time is used when it does not
 */
export function getCurrentDicomDateTime(
  now: Date = new Date(),
  timezoneOffsetFromUTC?: string
): { date: string; time: string } {
  // The wall clock reading is the instant shifted by the zone's offset and then
  // read in UTC, which for the local zone is what the local getters return.
  const offsetMinutes = parseUTCOffset(timezoneOffsetFromUTC) ?? -now.getTimezoneOffset();
  const at = new Date(now.getTime() + offsetMinutes * 60_000);
 
  const pad = (value: number, length = 2) => `${value}`.padStart(length, '0');
  const date = `${at.getUTCFullYear()}${pad(at.getUTCMonth() + 1)}${pad(at.getUTCDate())}`;
  const time =
    `${pad(at.getUTCHours())}${pad(at.getUTCMinutes())}${pad(at.getUTCSeconds())}` +
    `.${pad(at.getUTCMilliseconds(), 3)}000`;
 
  return { date, time };
}
 
/**
 * The date/time pair an IOD defines for "when this object was created", for the
 * modalities whose IOD does not define `ContentDate`/`ContentTime`.
 *
 * An RT Structure Set is built from the Structure Set module, which carries
 * `StructureSetDate`/`StructureSetTime` (both type 2) and no content date/time
 * at all - the RTSTRUCT IOD has no General Image module and no Multi-frame
 * Functional Groups module to bring one in.  A softcopy presentation state is
 * the same story with the Presentation State Identification module, whose
 * `PresentationCreationDate`/`PresentationCreationTime` are type 1.
 *
 * Both pairs are read back by `getLatestInstanceDateTime`, so an instance stamped
 * through this map orders exactly as one stamped with a content date/time.
 */
const modalityDateTimeAttributes: Record<string, [string, string]> = {
  RTSTRUCT: ['StructureSetDate', 'StructureSetTime'],
  PR: ['PresentationCreationDate', 'PresentationCreationTime'],
};
 
/**
 * The pair used by every other modality.  A segmentation gets it from the
 * Multi-frame Functional Groups module and a report from the SR Document
 * General module, both as type 1, and an ordinary image series gets it from the
 * General Image module as type 2C.
 */
const defaultDateTimeAttributes: [string, string] = ['ContentDate', 'ContentTime'];
 
/**
 * Stamps a newly created instance - a report, segmentation or structure set
 * about to be stored - as the most recent instance of its series.
 *
 * Two things are needed for that, and neither can be left to the object
 * generation:
 *
 * When the instance is added to an existing series, the series level
 * `SeriesDate`/`SeriesTime` belong to the original series and must stay as they
 * are, so only the instance level creation date/time say that the series has
 * just been added to.  Those are what the display set date/time is chosen from
 * (see `getLatestInstanceDateTime`), so they have to be set on every save.  They are
 * stamped in the dataset's own timezone - `TimezoneOffsetFromUTC` when it has
 * one, the local zone otherwise - because that is the wall clock reading a
 * viewer displays them as.
 *
 * `InstanceCreationDate`/`InstanceCreationTime` are in the SOP Common module,
 * so every IOD has them.  The creation date/time of the object itself is not
 * shared in that way: the attributes that hold it depend on the modality, and
 * writing an attribute the IOD does not define is what a strict validator or
 * archive rejects the instance for.  See {@link modalityDateTimeAttributes}.
 *
 * The series level date/time of a series being *created* cannot be stamped here
 * for the same reason - this function cannot tell the two cases apart - so the
 * store commands pass it to the object generation instead, from
 * {@link getCurrentDicomDateTime} and in the same zone.  Without that the
 * generated `SeriesDate`/`SeriesTime` are dcmjs's UTC ones, which around
 * midnight name the wrong day and then win the latest date this reads.
 *
 * The instance number has to be higher than every instance already in the
 * series.  Deriving it from a single predecessor instance is not enough: the
 * most recently created instance of a series is not necessarily the one with
 * the highest instance number, and then `1 +` its number collides with an
 * instance that already exists.
 *
 * @param dataset - naturalized dataset, modified in place
 * @param priorInstances - the instances already in the series; read from the
 *   metadata store for the dataset's series when not supplied
 * @returns the same dataset
 */
export function updateNewInstanceMetadata(dataset, priorInstances?: Array<{ InstanceNumber }>) {
  const instances =
    priorInstances ??
    DicomMetadataStore.getSeries(dataset.StudyInstanceUID, dataset.SeriesInstanceUID)?.instances ??
    [];
 
  const priorInstanceNumber = instances.reduce(
    (highest, instance) => Math.max(highest, Number(instance?.InstanceNumber) || 0),
    0
  );
  dataset.InstanceNumber = priorInstanceNumber + 1;
 
  const { date, time } = getCurrentDicomDateTime(new Date(), dataset.TimezoneOffsetFromUTC);
  dataset.InstanceCreationDate = date;
  dataset.InstanceCreationTime = time;
 
  const [dateAttribute, timeAttribute] =
    modalityDateTimeAttributes[dataset.Modality] ?? defaultDateTimeAttributes;
  dataset[dateAttribute] = date;
  dataset[timeAttribute] = time;
 
  return dataset;
}
 
export default updateNewInstanceMetadata;