All files / metadata/src metaData.ts

72.17% Statements 83/115
64.91% Branches 37/57
82.6% Functions 19/23
71.29% Lines 77/108

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 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390            128x   128x   128x                                                               8832x 512x           1280x 1920x 1024x         1280x           11699x               17664x 32896x   17664x     17664x 24064x 9344x         17664x     17664x 17664x 50560x 50560x 50560x     17664x                                                     36480x         36480x 36480x 4096x 4096x   66560x 18816x   17664x 17664x     17664x                           1920x             145853x 145853x 19095x   126758x                                           128x                                                               269610x     269610x 1778116x 1778116x 235833x                                               40535x                   10791x                             10791x   10791x                       128x 16x   16x 32x 32x 16x   16x 128x                 128x 256x   256x 512x 512x 256x   256x 768x         128x 182910x     6684x 6684x                                                                             128x                               128x 15616x 256x   15360x 128x   15232x 384x   15232x    
// This module defines a way to access various metadata about an imageId.  This layer of abstraction exists
// So metadata can be provided in different ways (e.g. by parsing DICOM P10 or by a WADO-RS document)
 
import type { MetadataModuleType } from './types';
import { getAddModuleType } from './enums';
 
const providers = [];
 
const typedProviderValueMap = new Map<string, TypedProviderValue[]>();
 
const typedProviderMap = new Map<string, TypedProviderBound>();
 
export type TypedProviderValue = {
  provider: TypedProvider;
  priority: number;
  isDefault: boolean;
  /** Clears any data from this instance */
  clear?: () => void;
  /** Clears just this type/query pair */
  clearQuery?: (query: string) => void;
};
 
export type TypedProvider = (
  next: TypedProviderBound,
  query: string,
  data?,
  options?
) => unknown;
 
export type TypedProviderBound = (query: string, data?, options?) => unknown;
 
/**
 * Adds a metadata provider with the specified priority
 * @param provider - Metadata provider function
 * @param priority - 0 is default/normal, > 0 is high, < 0 is low
 *
 * @category MetaData
 */
export function addProvider(
  provider: (type: string, ...query: string[]) => unknown,
  priority = 0
): void {
  if (providers.some((p) => p.provider === provider)) {
    return;
  }
 
  let i;
 
  // Find the right spot to insert this provider based on priority
  for (i = 0; i < providers.length; i++) {
    if (providers[i].priority <= priority) {
      break;
    }
  }
 
  // Insert the decode task at position i
  providers.splice(i, 0, {
    priority,
    provider,
  });
}
 
const nullProvider = (_query, _data, options) => options?.defaultValue;
 
function insertPriority(
  type: string,
  list,
  provider,
  options
): TypedProviderBound {
  const providerValue = { type, ...options, provider };
  Eif (!list.find((it) => it.provider === provider)) {
    let i;
    const { priority = 0 } = options;
 
    // Find the right spot to insert this provider based on priority
    for (i = 0; i < list.length; i++) {
      if (list[i].priority <= priority) {
        break;
      }
    }
 
    // Insert the decode task at position i
    list.splice(i, 0, providerValue);
  }
 
  let currentProvider = nullProvider;
  for (let i = list.length - 1; i >= 0; i--) {
    const p = list[i].provider;
    Eif (p) {
      currentProvider = p.bind(null, currentProvider);
    }
  }
  return currentProvider;
}
 
export interface TypedProviderOptions {
  priority?: number;
  requires?: string[];
  isDefault?: boolean;
  clear?: () => void;
  clearQuery?: (query: string) => void;
}
 
/**
 * Adds a typed provider at the given priority level
 *
 * Typed providers all run as part of the standard  provider framework at
 * priority -1000.  They differ from regular providers in that each provider
 * function handles exactly one type
 *
 * Note: All typed providers are included overall at priority "-1000" with the
 * global priority - that is, at the last item so that the existing non-typed
 * providers all run first.
 */
export function addTypedProvider(
  type: string,
  provider: TypedProvider,
  options: TypedProviderOptions = { priority: 0, isDefault: true }
) {
  Iif (!provider) {
    throw new Error(
      `addTypedProvider: cannot register undefined provider for type "${type}"`
    );
  }
  let list = typedProviderValueMap.get(type);
  if (!list) {
    list = new Array<TypedProviderValue>();
    typedProviderValueMap.set(type, list);
  }
  if (list.find((it) => it.provider === provider)) {
    return;
  }
  const newProvider = insertPriority(type, list, provider, options);
  Iif (!newProvider) {
    throw new Error(`newProvider is empty for ${type}`);
  }
  typedProviderMap.set(type, newProvider);
}
 
/**
 * Adds an add-path typed provider at the given priority level.
 *
 * The add-path provider chain is keyed by appending "Add" to the type and is
 * resolved via add()/addTyped().
 */
export function addAddProvider(
  type: string,
  provider: TypedProvider,
  options: TypedProviderOptions = { priority: 0, isDefault: true }
) {
  addTypedProvider(getAddModuleType(type), provider, options);
}
 
/**
 * A provider bridge for typed metadata modules.
 */
export function metadataModuleProvider(type: string, query: string, options) {
  const typedProvider = typedProviderMap.get(type);
  if (!typedProvider) {
    return;
  }
  return typedProvider(query, null, options);
}
 
/**
 * Removes the specified provider
 *
 * @param provider - Metadata provider function
 *
 * @category MetaData
 */
export function removeProvider(
  provider: (type: string, query: unknown) => unknown
): void {
  for (let i = 0; i < providers.length; i++) {
    if (providers[i].provider === provider) {
      providers.splice(i, 1);
 
      break;
    }
  }
}
 
const TYPED_PROVIDER_BRIDGE_PRIORITY = -1000;
 
/**
 * Removes all providers, clears all typed providers, and re-adds the typed
 * provider bridge at the end so getMetaData can still resolve typed types
 * after registerDefaultProviders() is called again.
 *
 * @category MetaData
 */
export function removeAllProviders(): void {
  while (providers.length > 0) {
    providers.pop();
  }
  typedProviderValueMap.clear();
  typedProviderMap.clear();
}
 
/**
 * Gets metadata from the registered metadata providers.  Will call each one from highest priority to lowest
 * until one responds
 *
 * @param type -  The type of metadata requested from the metadata store
 * @param query - The query for the metadata store, often imageId
 *        Some metadata providers support multi-valued strings, which are interpreted
 *        as the provider chooses.
 *
 * @returns The metadata retrieved from the metadata store
 * @category MetaData
 */
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function getMetaData(type: string, query: string, options?): any {
  // Invoke each provider in priority order until one returns something
  Iif (query === undefined) {
    return;
  }
  for (let i = 0; i < providers.length; i++) {
    const result = providers[i].provider(type, query, options);
    if (result !== undefined) {
      return result;
    }
  }
}
 
/**
 * Gets metadata with the return type inferred from the module type.
 * Pass a name from MetadataModules (e.g. MetadataModules.COMPRESSED_FRAME_DATA) or the constant
 * string (e.g. 'compressedFrameData'); T is inferred from MetadataModuleType.
 * For module types not in that map, the return type is undefined (never in union).
 *
 * @param type - The metadata module type (MetadataModules constant or literal string)
 * @param query - The query (e.g. imageId)
 * @param options - Optional options (e.g. { frameIndex })
 * @returns The result, typed per module, or undefined
 * @category MetaData
 */
export function getTyped<K extends keyof MetadataModuleType | string>(
  type: K,
  query: string,
  options?: unknown
):
  | (K extends keyof MetadataModuleType ? MetadataModuleType[K] : never)
  | undefined {
  return getMetaData(type as string, query, options) as
    | (K extends keyof MetadataModuleType ? MetadataModuleType[K] : never)
    | undefined;
}
 
/**
 * Performs metadata ingestion on the add-path provider chain.
 * Uses the type key `${type}Add`.
 */
export function addMetaData(type: string, query: string, options?): unknown {
  return getMetaData(getAddModuleType(type), query, options) as unknown;
}
 
/**
 * Adds metadata with return type inferred from module type, same mapping as
 * getTyped() but allowing async ingestion handlers.
 */
export function addTyped<K extends keyof MetadataModuleType | string>(
  type: K,
  query: string,
  options?: unknown
):
  | (K extends keyof MetadataModuleType ? MetadataModuleType[K] : never)
  | Promise<K extends keyof MetadataModuleType ? MetadataModuleType[K] : never>
  | undefined {
  const result = addMetaData(type as string, query, options);
 
  return result as
    | (K extends keyof MetadataModuleType ? MetadataModuleType[K] : never)
    | Promise<
        K extends keyof MetadataModuleType ? MetadataModuleType[K] : never
      >
    | undefined;
}
 
/**
 * Clears cached data on the specific type
 * and query key
 */
export const clearQuery = (type: string, query?: string) => {
  const typesToClear = [type, getAddModuleType(type)];
 
  for (const currentType of typesToClear) {
    const typedProviders = typedProviderValueMap.get(currentType);
    if (!typedProviders) {
      continue;
    }
    for (const providerInfo of typedProviders) {
      providerInfo?.clearQuery?.(query);
    }
  }
};
 
/**
 * Clears cached data on the specific type
 * and query key
 */
export const clear = (type: string) => {
  const typesToClear = Array.from(new Set([type, getAddModuleType(type)]));
 
  for (const currentType of typesToClear) {
    const typedProviders = typedProviderValueMap.get(currentType);
    if (!typedProviders) {
      continue;
    }
    for (const providerInfo of typedProviders) {
      providerInfo?.clear?.();
    }
  }
};
 
export const get = (type: string, ...queries: string[]) =>
  queries.length === 1
    ? getMetaData(type, queries[0])
    : queries
        .map((query) => query && getMetaData(type, query))
        .find((it) => it !== undefined);
 
/**
 * Retrieves metadata from a DICOM image and returns it as an object with capitalized keys.
 * @param imageId - the imageId
 * @param metaDataProvider - The metadata provider either wadors or wadouri
 * @param types - An array of metadata types to retrieve.
 * @returns An object containing the retrieved metadata with capitalized keys.
 */
export function getNormalized(
  imageId: string,
  types: string[],
  metaDataProvider = getMetaData
) {
  const result = {};
  for (const t of types) {
    try {
      const data = metaDataProvider(t, imageId);
      if (data) {
        const capitalizedData = {};
        for (const key in data) {
          if (key in data) {
            const capitalizedKey = toUpperCamelTag(key);
            capitalizedData[capitalizedKey] = data[key];
          }
        }
        Object.assign(result, capitalizedData);
      }
    } catch (error) {
      console.error(`Error retrieving ${t} data:`, error);
    }
  }
 
  return result;
}
 
/**
 * Converts a tag name to UpperCamelCase
 */
export const toUpperCamelTag = (tag: string) => {
  if (tag.startsWith('sop')) {
    return `SOP${tag.substring(3)}`;
  }
  if (tag.startsWith('voi')) {
    return `VOI${tag.substring(3)}`;
  }
  if (tag.endsWith('Id')) {
    tag = `${tag.substring(0, tag.length - 2)}ID`;
  }
  return tag.charAt(0).toUpperCase() + tag.slice(1);
};
 
/**
 * Converts a tag name to lowerCamelCase
 */
export const toLowerCamelTag = (tag: string) => {
  if (tag.startsWith('SOP')) {
    return `sop${tag.substring(3)}`;
  }
  if (tag.startsWith('VOI')) {
    return `voi${tag.substring(3)}`;
  }
  if (tag.endsWith('ID') && !tag.endsWith('UID')) {
    tag = `${tag.substring(0, tag.length - 2)}Id`;
  }
  return tag.charAt(0).toLowerCase() + tag.slice(1);
};