All files / extensions/default/src/DicomWebDataSource/utils StaticWadoClient.ts

59.82% Statements 67/112
39.28% Branches 22/56
57.14% Functions 12/21
60% Lines 63/105

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      34x   34x     34x 34x   34x 5x     5x                               34x                                 68x       68x 68x 68x                                                                                             288x       288x 288x   288x       288x 288x 22176x 46020x 21831x     345x   288x       34x       34x 34x 34x     34x   34x 133x 399x       133x     34x                                 44208x   44208x     44208x 22032x   22176x       22176x                                   22176x 22032x     22032x     22032x     22032x   22032x       22176x                                                     46419x   46419x   46419x       46419x 46419x     46419x 46419x 24243x   22176x 22176x     22176x     22176x   22176x         322x 322x 1474x   322x      
import { api } from 'dicomweb-client';
import fixMultipart from './fixMultipart';
 
const { DICOMwebClient } = api;
 
const anyDicomwebClient = DICOMwebClient as any;
 
// Ugly over-ride, but the internals aren't otherwise accessible.
if (!anyDicomwebClient._orig_buildMultipartAcceptHeaderFieldValue) {
  anyDicomwebClient._orig_buildMultipartAcceptHeaderFieldValue =
    anyDicomwebClient._buildMultipartAcceptHeaderFieldValue;
  anyDicomwebClient._buildMultipartAcceptHeaderFieldValue = function (mediaTypes, acceptableTypes) {
    Iif (mediaTypes.length === 1 && mediaTypes[0].mediaType.endsWith('/*')) {
      return '*/*';
    } else {
      return anyDicomwebClient._orig_buildMultipartAcceptHeaderFieldValue(
        mediaTypes,
        acceptableTypes
      );
    }
  };
}
 
/**
 * An implementation of the static wado client, that fetches data from
 * a static response rather than actually doing real queries.  This allows
 * fast encoding of test data, but because it is static, anything actually
 * performing searches doesn't work.  This version fixes the query issue
 * by manually implementing a query option.
 */
 
export default class StaticWadoClient extends api.DICOMwebClient {
  static studyFilterKeys = {
    studyinstanceuid: '0020000D',
    patientname: '00100010',
    '00100020': 'mrn',
    studydescription: '00081030',
    studydate: '00080020',
    modalitiesinstudy: '00080061',
    accessionnumber: '00080050',
  };
 
  static seriesFilterKeys = {
    seriesinstanceuid: '0020000E',
    seriesnumber: '00200011',
    modality: '00080060',
  };
 
  protected config;
  protected staticWado;
 
  constructor(config) {
    super(config);
    this.staticWado = config.staticWado;
    this.config = config;
  }
 
  /**
   * Handle improperly specified multipart/related return type.
   * Note if the response is SUPPOSED to be multipart encoded already, then this
   * will double-decode it.
   *
   * @param options
   * @returns De-multiparted response data.
   *
   */
  public retrieveBulkData(options): Promise<any[]> {
    const shouldFixMultipart = this.config.fixBulkdataMultipart !== false;
    const useOptions = {
      ...options,
    };
    Iif (this.staticWado) {
      useOptions.mediaTypes = [{ mediaType: 'application/*' }];
    }
    return super
      .retrieveBulkData(useOptions)
      .then(result => (shouldFixMultipart ? fixMultipart(result) : result));
  }
 
  /**
   * Retrieves instance frames using the image/* media type when configured
   * to do so (static wado back end).
   */
  public retrieveInstanceFrames(options) {
    if (this.staticWado) {
      return super.retrieveInstanceFrames({
        ...options,
        mediaTypes: [{ mediaType: 'image/*' }],
      });
    } else {
      return super.retrieveInstanceFrames(options);
    }
  }
 
  /**
   * Replace the search for studies remote query with a local version which
   * retrieves a complete query list and then sub-selects from it locally.
   * @param {*} options
   * @returns
   */
  async searchForStudies(options) {
    Iif (!this.staticWado) {
      return super.searchForStudies(options);
    }
 
    const searchResult = await super.searchForStudies(options);
    const { queryParams } = options;
 
    Iif (!queryParams) {
      return searchResult;
    }
 
    const lowerParams = this.toLowerParams(queryParams);
    const filtered = searchResult.filter(study => {
      for (const key of Object.keys(StaticWadoClient.studyFilterKeys)) {
        if (!this.filterItem(key, lowerParams, study, StaticWadoClient.studyFilterKeys)) {
          return false;
        }
      }
      return true;
    });
    return filtered;
  }
 
  async searchForSeries(options) {
    Iif (!this.staticWado) {
      return super.searchForSeries(options);
    }
 
    const searchResult = await super.searchForSeries(options);
    const { queryParams } = options;
    Iif (!queryParams) {
      return searchResult;
    }
    const lowerParams = this.toLowerParams(queryParams);
 
    const filtered = searchResult.filter(series => {
      for (const key of Object.keys(StaticWadoClient.seriesFilterKeys)) {
        Iif (!this.filterItem(key, lowerParams, series, StaticWadoClient.seriesFilterKeys)) {
          return false;
        }
      }
      return true;
    });
 
    return filtered;
  }
 
  /**
   * Compares values, matching any instance of desired to any instance of
   * actual by recursively go through the paired set of values.  That is,
   * this is O(m*n) where m is how many items in desired and n is the length of actual
   * Then, at the individual item node, compares the Alphabetic name if present,
   * and does a sub-string matching on string values, and otherwise does an
   * exact match comparison.
   *
   * @param {*} desired
   * @param {*} actual
   * @param {*} options - fuzzyMatching: if true, then do a sub-string match
   * @returns true if the values match
   */
  compareValues(desired, actual, options) {
    const { fuzzyMatching } = options;
 
    Iif (Array.isArray(desired)) {
      return desired.find(item => this.compareValues(item, actual, options));
    }
    if (Array.isArray(actual)) {
      return actual.find(actualItem => this.compareValues(desired, actualItem, options));
    }
    Iif (actual?.Alphabetic) {
      actual = actual.Alphabetic;
    }
 
    Iif (fuzzyMatching && typeof actual === 'string' && typeof desired === 'string') {
      const normalizeValue = str => {
        return str.toLowerCase();
      };
 
      const normalizedDesired = normalizeValue(desired);
      const normalizedActual = normalizeValue(actual);
 
      const tokenizeAndNormalize = str => str.split(/[\s^]+/).filter(Boolean);
 
      const desiredTokens = tokenizeAndNormalize(normalizedDesired);
      const actualTokens = tokenizeAndNormalize(normalizedActual);
 
      return desiredTokens.every(desiredToken =>
        actualTokens.some(actualToken => actualToken.startsWith(desiredToken))
      );
    }
 
    if (typeof actual == 'string') {
      Iif (actual.length === 0) {
        return true;
      }
      Iif (desired.length === 0 || desired === '*') {
        return true;
      }
      Iif (desired[0] === '*' && desired[desired.length - 1] === '*') {
        // console.log(`Comparing ${actual} to ${desired.substring(1, desired.length - 1)}`)
        return actual.indexOf(desired.substring(1, desired.length - 1)) != -1;
      } else Iif (desired[desired.length - 1] === '*') {
        return actual.indexOf(desired.substring(0, desired.length - 1)) != -1;
      } else Iif (desired[0] === '*') {
        return actual.indexOf(desired.substring(1)) === actual.length - desired.length + 1;
      }
    }
    return desired === actual;
  }
 
  /** Compares a pair of dates to see if the value is within the range */
  compareDateRange(range, value) {
    Iif (!value) {
      return true;
    }
    const dash = range.indexOf('-');
    Iif (dash === -1) {
      return this.compareValues(range, value, {});
    }
    const start = range.substring(0, dash);
    const end = range.substring(dash + 1);
    return (!start || value >= start) && (!end || value <= end);
  }
 
  /**
   * Filters the return list by the query parameters.
   *
   * @param anyCaseKey - a possible search key
   * @param queryParams -
   * @param {*} study
   * @param {*} sourceFilterMap
   * @returns
   */
  filterItem(key: string, queryParams, study, sourceFilterMap) {
    const isName = (key: string) => key.indexOf('name') !== -1;
 
    const { supportsFuzzyMatching = false } = this.config;
 
    const options = {
      fuzzyMatching: isName(key) && supportsFuzzyMatching,
    };
 
    const altKey = sourceFilterMap[key] || key;
    Iif (!queryParams) {
      return true;
    }
    const testValue = queryParams[key] || queryParams[altKey];
    if (!testValue) {
      return true;
    }
    const valueElem = study[key] || study[altKey];
    Iif (!valueElem) {
      return false;
    }
    Iif (valueElem.vr === 'DA' && valueElem.Value?.[0]) {
      return this.compareDateRange(testValue, valueElem.Value[0]);
    }
    const value = valueElem.Value;
 
    return this.compareValues(testValue, value, options);
  }
 
  /** Converts the query parameters to lower case query parameters */
  toLowerParams(queryParams: Record<string, unknown>): Record<string, unknown> {
    const lowerParams = {};
    Object.entries(queryParams).forEach(([key, value]) => {
      lowerParams[key.toLowerCase()] = value;
    });
    return lowerParams;
  }
}