study_ids = collect(); $this->client = $client ?? new OrthancRestClient($dicomServer); $this->resetQueues(); } public function getDicomServer(): DicomServer { return $this->dicomServer; } public function execute(): void { app(Pipeline::class) ->send($this) ->through([ Pipes\ScanStudies::class, Pipes\FilterStudies::class, Pipes\InsertStudies::class, Pipes\UpdateStudies::class, Pipes\ArchiveStudies::class, ]) ->thenReturn(); } public function getClient(): OrthancRestClient { return $this->client; } public function getStudyIds(): Collection { return $this->study_ids; } public function setStudyIds(array $study_ids): void { $this->study_ids = collect($study_ids); } public function resetQueues(): void { $this->insert_queue = collect(); $this->update_queue = collect(); $this->archive_queue = collect(); } public function getInsertQueue(): Collection { return $this->insert_queue; } public function getUpdateQueue(): Collection { return $this->update_queue; } public function getArchiveQueue(): Collection { return $this->archive_queue; } public function fetchStudyDetails(string $orthanc_uuid): ?array { $study = $this->client->getStudyDetails($orthanc_uuid); if ($study == null) { return null; } $stats = $this->client->getStudyStatistics($orthanc_uuid); $study['Statistics'] = $stats; $series = $this->client->getStudySeries($orthanc_uuid); $study['Series'] = $series; return $study; } public function fetchInstancesList(string $orthanc_uuid): ?array { return $this->client->getStudyInstancesList($orthanc_uuid); } public function fetchInstancesTags(string $orthanc_uuid): ?array { return $this->client->getInstanceDetails($orthanc_uuid, true); } public function getStudyDescription(mixed $orthanc_src): ?string { $result = data_get($orthanc_src, 'MainDicomTags.StudyDescription'); if (blank($result)) { $result = data_get($orthanc_src, 'RequestedTags.AcquisitionDeviceProcessingDescription'); } if (blank($result)) { $result = data_get($orthanc_src, 'MainDicomTags.AcquisitionDeviceProcessingDescription'); } return $result; } public function transformData(mixed $orthanc_src): array { $stable_study = (bool) data_get($orthanc_src, 'IsStable', false); if (! $stable_study) { // do not process unstable studies. // defer till next sync when hopefully the study becomes stable return []; } $orthanc_uuid = strtolower($orthanc_src['ID']); $dicomHeaders = $this->getStudyDicomTags($orthanc_uuid); $routing = DicomStudyRouter::matchStudy($dicomHeaders); $inst_name = data_get($orthanc_src, 'MainDicomTags.InstitutionName'); $patient_name = data_get($orthanc_src, 'PatientMainDicomTags.PatientName'); $descr = $this->getStudyDescription($orthanc_src); $study = [ 'dicom_server_id' => $this->dicomServer->id, 'orthanc_uuid' => $orthanc_uuid, 'institution_name' => $inst_name, 'organization_id' => $routing['organization_id'], 'department_id' => $routing['department_id'], 'patient_uuid' => strtolower($orthanc_src['ParentPatient']), 'patient_id' => data_get($orthanc_src, 'PatientMainDicomTags.PatientID'), 'patient_name' => $patient_name, 'patient_sex' => data_get($orthanc_src, 'PatientMainDicomTags.PatientSex'), 'patient_age' => data_get($orthanc_src, 'RequestedTags.PatientAge'), 'accession_number' => data_get($orthanc_src, 'MainDicomTags.AccessionNumber'), 'referring_physician_name' => data_get($orthanc_src, 'MainDicomTags.ReferringPhysicianName'), 'study_id' => data_get($orthanc_src, 'MainDicomTags.StudyID'), 'study_instance_uid' => data_get($orthanc_src, 'MainDicomTags.StudyInstanceUID'), 'modality' => data_get($orthanc_src, 'RequestedTags.Modality'), 'body_part_examined' => data_get($orthanc_src, 'RequestedTags.BodyPartExamined'), 'study_date' => DicomUtils::dateTimeToCarbon($orthanc_src['MainDicomTags']['StudyDate'], $orthanc_src['MainDicomTags']['StudyTime']) ?? Carbon::createFromTimestamp(0), 'received_at' => Carbon::parse($orthanc_src['LastUpdate'], 'UTC'), 'image_count' => data_get($orthanc_src, 'Statistics.CountInstances'), 'series_count' => data_get($orthanc_src, 'Statistics.CountSeries'), 'disk_size' => data_get($orthanc_src, 'Statistics.DiskSize'), 'study_description' => $descr, ]; $study['workflow_level'] = $stable_study ? WorkflowLevel::Unassigned->value : WorkflowLevel::Pending->value; $study['patient_birthdate'] = null; $dob = data_get($orthanc_src, 'PatientMainDicomTags.PatientBirthDate'); if (filled($dob)) { try { $study['patient_birthdate'] = Carbon::parse($dob); } catch (Exception) { Log::error('Failed to parse PatientMainDicomTags.PatientBirthDate: {dob}', ['dob' => $dob]); } } // check for priority in patient name or description if (preg_match('/\b(urgent|stat)\b/i', implode(' ', [$descr, $patient_name]))) { $this->setValue($study, 'priority', Priority::Stat->value); } $properties = [ 'other_patient_names' => data_get($orthanc_src, 'RequestedTags.OtherPatientNames'), 'other_patient_ids' => data_get($orthanc_src, 'RequestedTags.OtherPatientIDs'), 'software_versions' => data_get($orthanc_src, 'RequestedTags.SoftwareVersions'), 'station_name' => data_get($orthanc_src, 'RequestedTags.StationName'), 'operators_name' => data_get($orthanc_src, 'RequestedTags.OperatorsName'), 'manufacturer' => data_get($orthanc_src, 'RequestedTags.Manufacturer'), 'manufacturer_model_name' => data_get($orthanc_src, 'RequestedTags.ManufacturerModelName'), 'acquisition_date' => DicomUtils::dateTimeToCarbon(data_get($orthanc_src, 'RequestedTags.AcquisitionDate'), data_get($orthanc_src, 'RequestedTags.AcquisitionTime')), ]; $properties = array_purge($properties); if (empty($properties)) { $properties = [ 'other_patient_names' => data_get($orthanc_src, 'MainDicomTags.OtherPatientNames'), 'other_patient_ids' => data_get($orthanc_src, 'MainDicomTags.OtherPatientIDs'), 'software_versions' => data_get($orthanc_src, 'MainDicomTags.SoftwareVersions'), 'station_name' => data_get($orthanc_src, 'MainDicomTags.StationName'), 'operators_name' => data_get($orthanc_src, 'MainDicomTags.OperatorsName'), 'manufacturer' => data_get($orthanc_src, 'MainDicomTags.Manufacturer'), 'manufacturer_model_name' => data_get($orthanc_src, 'MainDicomTags.ManufacturerModelName'), 'acquisition_date' => DicomUtils::dateTimeToCarbon(data_get($orthanc_src, 'MainDicomTags.AcquisitionDate'), data_get($orthanc_src, 'MainDicomTags.AcquisitionTime')), ]; $properties = array_purge($properties); } $series = []; foreach (data_get($orthanc_src, 'Series', []) as $ser) { $params = [ 'orthanc_uuid' => strtolower($ser['ID']), 'series_instance_uid' => data_get($ser, 'MainDicomTags.SeriesInstanceUID'), 'series_date' => DicomUtils::dateTimeToCarbon(data_get($ser, 'MainDicomTags.SeriesDate'), data_get($ser, 'MainDicomTags.SeriesTime')), 'series_number' => data_get($ser, 'MainDicomTags.SeriesNumber'), 'series_description' => data_get($ser, 'MainDicomTags.SeriesDescription'), 'protocol_name' => data_get($ser, 'MainDicomTags.ProtocolName'), 'modality' => data_get($ser, 'MainDicomTags.Modality'), 'body_part_examined' => data_get($ser, 'MainDicomTags.BodyPartExamined'), 'performed_procedure_step_description' => data_get($ser, 'MainDicomTags.PerformedProcedureStepDescription'), 'sequence_name' => data_get($ser, 'MainDicomTags.SequenceName'), ]; $params['num_instances'] = count(data_get($ser, 'Instances', [])); $params = array_purge($params); if (! empty($params)) { $series[] = $params; } } if (empty($series)) { $series = null; } else { // $series = array_multisort(array_column($series, 'series_number'), SORT_ASC, $series); usort($series, fn ($a, $b): int => (int) $a['series_number'] <=> (int) $b['series_number']); } if (empty($properties)) { $properties = null; } $dicom_properties = array_purge([ 'patient_id' => $study['patient_id'], 'patient_name' => $study['patient_name'], 'patient_birthdate' => $study['patient_birthdate'], 'patient_age' => $study['patient_age'], 'patient_sex' => $study['patient_sex'], 'accession_number' => $study['accession_number'], 'referring_physician_name' => $study['referring_physician_name'], 'study_id' => $study['study_id'], 'body_part_examined' => $study['body_part_examined'], 'study_date' => $study['study_date'], 'study_description' => $study['study_description'], ]); $details = array_purge(compact('properties', 'series', 'dicom_properties')); $study = array_purge($study); // todo: handle $routing['radiologists'] return compact('study', 'details'); } private function getStudyDicomTags(string $study_uuid): array { $instances = $this->fetchInstancesList($study_uuid); if (empty($instances)) { return []; } // randomly sample few instances for tags collection $selectedInstances = count($instances) <= $this->maxInstances ? $instances : array_rand(array_flip($instances), $this->maxInstances); $tags = collect(); foreach ($selectedInstances as $instance) { foreach ($this->fetchInstancesTags($instance) as $key => $value) { if ($key == 'MainDicomTags' || $key == 'RequestedTags') { foreach ($value as $tag => $val) { $dcmTag = RawDicomTags::tryFrom($tag); if ($dcmTag != null && (! $tags->has($dcmTag->name) || $tags->get($dcmTag->name) !== $val)) { $tags->put($dcmTag->name, $val); } } } } } return $tags->toArray(); } private function setValue(array &$array, string $key, mixed $value): void { if (filled($value)) { $array[$key] = $value; } } }