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      66x   66x     66x 66x   66x 20x     20x                               66x                                 132x       132x 132x 132x                                                                                             127x       127x 127x   127x       127x 127x 9779x 20665x 9553x     226x   127x       66x       66x 66x 66x     66x   66x 281x 843x       281x     66x                                 19341x   19341x     19341x 9639x   9702x       9702x                                   9702x 9639x     9639x     9639x     9639x   9639x       9702x                                                     21508x   21508x   21508x       21508x 21508x     21508x 21508x 11806x   9702x 9702x     9702x     9702x   9702x         193x 193x 700x   193x      
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;
  }
}