From 4c26b8a473fd3d2a945de476e5ebe0856887993a Mon Sep 17 00:00:00 2001 From: Dr Masroor Ehsan Date: Sat, 28 Dec 2024 21:53:37 +0600 Subject: [PATCH] wip --- app/Http/Controllers/PacsController.php | 14 +++- app/Models/Enums/StudyLevelStatus.php | 2 +- app/Models/Study.php | 2 + app/Services/InputMatcher.php | 19 ++++++ app/Services/Pacs/DicomTags.php | 10 +++ app/Services/Pacs/InstituteMapper.php | 35 ++++++++++ app/Services/Pacs/OrthancRestClient.php | 21 +++++- app/Services/Pacs/StudyImporter.php | 65 ++++++++++++++----- ...2024_12_27_060234_create_studies_table.php | 14 +++- routes/web.php | 1 + 10 files changed, 156 insertions(+), 27 deletions(-) create mode 100644 app/Services/InputMatcher.php create mode 100644 app/Services/Pacs/InstituteMapper.php diff --git a/app/Http/Controllers/PacsController.php b/app/Http/Controllers/PacsController.php index 96a380c..70cefb5 100644 --- a/app/Http/Controllers/PacsController.php +++ b/app/Http/Controllers/PacsController.php @@ -3,13 +3,19 @@ namespace App\Http\Controllers; use App\Services\Pacs\OrthancRestClient; +use App\Services\Pacs\StudyImporter; +use Arr; class PacsController extends Controller { public function index() { $studies = (new OrthancRestClient)->getStudies(); - dd($studies[0]); + //dd($studies[0]); + $study = array_pop($studies); + //dd(Arr::get($study, 'MainDicomTags.StudyDate')); + //dd(collect($study)->get('MainDicomTags.StudyDate')); + dd($study); return view('pacs.studies', compact('studies')); } @@ -21,10 +27,12 @@ public function show($id) return view('pacs.study', compact('study')); } - public function import($id) + public function import() { $studies = (new OrthancRestClient)->getStudies(); - return redirect()->route('pacs.index'); + (new StudyImporter)->import($studies); + + return redirect()->route('studies.index'); } } diff --git a/app/Models/Enums/StudyLevelStatus.php b/app/Models/Enums/StudyLevelStatus.php index ad7feb8..61aabe6 100644 --- a/app/Models/Enums/StudyLevelStatus.php +++ b/app/Models/Enums/StudyLevelStatus.php @@ -4,7 +4,7 @@ enum StudyLevelStatus: int { - case None = 0; + case Pending = 0; case StudyArrived = 1 << 1; case StudyLocked = 1 << 2; case StudyUnlocked = 1 << 3; diff --git a/app/Models/Study.php b/app/Models/Study.php index a189d76..e85b57d 100644 --- a/app/Models/Study.php +++ b/app/Models/Study.php @@ -14,4 +14,6 @@ class Study extends Model 'study_status' => StudyLevelStatus::class, 'report_status' => ReportStatus::class, ]; + + protected $guarded = ['id']; } diff --git a/app/Services/InputMatcher.php b/app/Services/InputMatcher.php new file mode 100644 index 0000000..d66fb9d --- /dev/null +++ b/app/Services/InputMatcher.php @@ -0,0 +1,19 @@ + strcasecmp($input, $pattern) === 0, + NameMatchModes::Contains => stripos($input, $pattern) !== false, + NameMatchModes::StartsWith => strncasecmp($input, $pattern, \strlen($pattern)) === 0, + NameMatchModes::EndsWith => str_ends_with(strtolower($input), strtolower($pattern)), + NameMatchModes::Regex => preg_match($pattern, $input) === 1, + }; + } +} diff --git a/app/Services/Pacs/DicomTags.php b/app/Services/Pacs/DicomTags.php index ff41207..32e728e 100644 --- a/app/Services/Pacs/DicomTags.php +++ b/app/Services/Pacs/DicomTags.php @@ -6,10 +6,20 @@ enum DicomTags: string { case AcquisitionDate = 'AcquisitionDate'; case AcquisitionTime = 'AcquisitionTime'; + case ContentDate = 'ContentDate'; + case ContentTime = 'ContentTime'; case AcquisitionDeviceProcessingDescription = 'AcquisitionDeviceProcessingDescription'; case BodyPartExamined = 'BodyPartExamined'; case Modality = 'Modality'; case SoftwareVersions = 'SoftwareVersions'; + case ProtocolName = 'ProtocolName'; case StationName = 'StationName'; + case InstitutionAddress = 'InstitutionAddress'; + case StudyDescription = 'StudyDescription'; + case SeriesDescription = 'SeriesDescription'; + case Manufacturer = 'Manufacturer'; + case OperatorsName = 'OperatorsName'; + case ManufacturerModelName = 'ManufacturerModelName'; case Private10 = '0029,0010'; + case IW_Private = '0009,0010'; } diff --git a/app/Services/Pacs/InstituteMapper.php b/app/Services/Pacs/InstituteMapper.php new file mode 100644 index 0000000..6bd5793 --- /dev/null +++ b/app/Services/Pacs/InstituteMapper.php @@ -0,0 +1,35 @@ +addDay(), function () { + return DB::table('institute_names')->orderBy('priority')->get()->toArray(); + }); + self::$catchAll = DB::table('institutes')->first('id')->id; + } + + $input = strtolower($input); + + foreach (self::$patterns as $pattern) { + if (InputMatcher::match($input, $pattern->name, NameMatchModes::from($pattern->match_mode))) { + return $pattern->institute_id; + } + } + + return self::$catchAll; + } +} diff --git a/app/Services/Pacs/OrthancRestClient.php b/app/Services/Pacs/OrthancRestClient.php index a1320d5..cac5616 100644 --- a/app/Services/Pacs/OrthancRestClient.php +++ b/app/Services/Pacs/OrthancRestClient.php @@ -13,6 +13,23 @@ public function getClient(): Client ]); } + public function getStudyStatistics(string $study_id): array + { + $response = $this->getClient()->get('/studies/'.$study_id.'/statistics'); + + return json_decode($response->getBody()->getContents(), true); + } + + public function getStudyDetails(string $study_id): array + { + $query = [ + 'requested-tags' => implode(';', array_column(DicomTags::cases(), 'value')), + ]; + $response = $this->getClient()->get('/studies/'.$study_id.http_build_query($query)); + + return json_decode($response->getBody()->getContents(), true); + } + public function getStudies(): array { $query = [ @@ -22,8 +39,6 @@ public function getStudies(): array $url = '/studies?'.http_build_query($query); $response = $this->getClient()->get($url); - $studies = json_decode($response->getBody()->getContents(), true); - - return $studies; + return json_decode($response->getBody()->getContents(), true); } } diff --git a/app/Services/Pacs/StudyImporter.php b/app/Services/Pacs/StudyImporter.php index 0834106..2bc607b 100644 --- a/app/Services/Pacs/StudyImporter.php +++ b/app/Services/Pacs/StudyImporter.php @@ -11,38 +11,67 @@ final class StudyImporter public function import(array $studies): void { foreach ($studies as $study) { - $othanc_id = strtolower($study['ID']); - $row = Study::where('othanc_id', $othanc_id)->first(); - if ($row && $row->study_status < StudyLevelStatus::StudyArrived) { - // todo: update study + $orthanc_uid = strtolower($study['ID']); + $row = Study::where(compact('orthanc_uid'))->first(); + if ($row != null) { + if ($row->study_status < StudyLevelStatus::StudyArrived) { + // todo: update study + } + return; } + $inst_name = data_get($study, 'MainDicomTags.InstitutionName'); + $inst_id = InstituteMapper::map($inst_name); + $data = [ - 'othanc_id' => $othanc_id, - 'study_status' => StudyLevelStatus::StudyArrived, - 'study_datetime' => $study['StudyDateTime'], - 'receive_datetime' => $study['ReceiveDateTime'], + 'orthanc_uid' => $orthanc_uid, 'is_locked' => false, 'is_active' => true, - 'patient_id' => $study['PatientMainDicomTags']['PatientID'], - 'patient_name' => $study['PatientMainDicomTags']['PatientName'], - 'patient_sex' => $study['PatientMainDicomTags']['PatientSex'], - 'patient_birthdate' => $study['PatientMainDicomTags']['PatientBirthDate'], + 'institution_name' => $inst_name, + 'institute_id' => $inst_id, - 'institution_name' => $study['MainDicomTags']['InstitutionName'], - 'accession_number' => $study['MainDicomTags']['AccessionNumber'], - 'referring_physician_name' => $study['MainDicomTags']['ReferringPhysicianName'], - 'study_id' => $study['MainDicomTags']['StudyID'], + 'patient_uuid' => $study['ParentPatient'], + 'patient_id' => data_get($study, 'PatientMainDicomTags.PatientID'), + 'patient_name' => data_get($study, 'PatientMainDicomTags.PatientName'), + 'patient_sex' => data_get($study, 'PatientMainDicomTags.PatientSex'), + 'accession_number' => data_get($study, 'MainDicomTags.AccessionNumber'), + 'referring_physician_name' => data_get($study, 'MainDicomTags.ReferringPhysicianName'), + 'study_id' => data_get($study, 'MainDicomTags.StudyID'), + 'study_instance_uid' => data_get($study, 'MainDicomTags.StudyInstanceUID'), 'study_date' => DicomUtils::dateTimeToCarbon($study['MainDicomTags']['StudyDate'], $study['MainDicomTags']['StudyTime']), 'receive_date' => Carbon::parse($study['LastUpdate'], 'UTC'), - 'study_modality' => $study['MainDicomTags']['Modality'], + + 'study_modality' => data_get($study, 'RequestedTags.Modality'), + 'body_part_examined' => data_get($study, 'RequestedTags.BodyPartExamined'), + 'software_versions' => data_get($study, 'RequestedTags.SoftwareVersions'), + 'station_name' => data_get($study, 'RequestedTags.StationName'), + 'operators_name' => data_get($study, 'RequestedTags.OperatorsName'), + 'manufacturer' => data_get($study, 'RequestedTags.Manufacturer'), + 'manufacturer_model_name' => data_get($study, 'RequestedTags.ManufacturerModelName'), 'series_count' => count($study['Series']), ]; - $row = Study::create($data); + + if ($study['IsStable']) { + $data['study_status'] = StudyLevelStatus::StudyArrived->value; + } else { + $data['study_status'] = StudyLevelStatus::Pending->value; + } + + $dob = data_get($study, 'PatientMainDicomTags.PatientBirthDate'); + if (trim($dob) != '') { + $data['patient_birthdate'] = Carbon::parse($dob); + } + + $descr = data_get($study, 'MainDicomTags.StudyDescription'); + if ($descr != null) { + $descr = data_get($study, 'RequestedTags.AcquisitionDeviceProcessingDescription'); + } + $data['study_description'] = trim($descr); + Study::create($data); } } } diff --git a/database/migrations/2024_12_27_060234_create_studies_table.php b/database/migrations/2024_12_27_060234_create_studies_table.php index 7468bff..e00a6e9 100644 --- a/database/migrations/2024_12_27_060234_create_studies_table.php +++ b/database/migrations/2024_12_27_060234_create_studies_table.php @@ -20,14 +20,22 @@ public function up(): void $table->boolean('is_locked')->default(true); $table->unsignedTinyInteger('study_priority')->default(0); $table->string('patient_id')->nullable(); + $table->string('patient_uuid')->nullable(); $table->string('patient_name'); $table->string('patient_sex')->nullable(); $table->date('patient_birthdate')->nullable(); + $table->string('study_instance_uid')->unique(); $table->string('study_id')->nullable(); $table->string('institution_name')->nullable(); $table->string('accession_number')->nullable(); $table->string('study_description')->nullable(); + $table->string('body_part_examined')->nullable(); + $table->string('station_name')->nullable(); + $table->string('operators_name')->nullable(); + $table->string('manufacturer')->nullable(); + $table->string('manufacturer_model_name')->nullable(); + $table->string('referring_physician_name')->nullable(); $table->string('study_modality', 4)->nullable(); $table->dateTime('study_date'); @@ -36,8 +44,10 @@ public function up(): void $table->foreignIdFor(Institute::class)->constrained()->onDelete('cascade'); $table->unsignedTinyInteger('study_status')->default(StudyLevelStatus::None->value); $table->unsignedTinyInteger('report_status')->default(ReportStatus::Pending->value); - $table->unsignedSmallInteger('image_count')->default(0); - $table->unsignedSmallInteger('series_count')->default(0); + + $table->unsignedSmallInteger('image_count')->nullable(0); + $table->unsignedSmallInteger('series_count')->nullable(); + $table->unsignedSmallInteger('disk_size')->nullable(); $table->foreignIdFor(User::class, 'assigned_physician_id')->nullable()->constrained()->onDelete('set null'); $table->foreignIdFor(User::class, 'interpreting_physician_id')->nullable()->constrained()->onDelete('set null'); diff --git a/routes/web.php b/routes/web.php index 99811c5..82bc7b3 100644 --- a/routes/web.php +++ b/routes/web.php @@ -18,5 +18,6 @@ Route::get('/studies', [PacsController::class, 'index'])->name('studies.index'); Route::get('/studies/{id}', [PacsController::class, 'show'])->name('studies.show'); + Route::get('/cron', [PacsController::class, 'import'])->name('studies.import'); });