This commit is contained in:
Dr Masroor Ehsan 2024-12-28 21:53:37 +06:00
parent cf12ab27e4
commit 4c26b8a473
10 changed files with 156 additions and 27 deletions

View File

@ -3,13 +3,19 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Services\Pacs\OrthancRestClient; use App\Services\Pacs\OrthancRestClient;
use App\Services\Pacs\StudyImporter;
use Arr;
class PacsController extends Controller class PacsController extends Controller
{ {
public function index() public function index()
{ {
$studies = (new OrthancRestClient)->getStudies(); $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')); return view('pacs.studies', compact('studies'));
} }
@ -21,10 +27,12 @@ public function show($id)
return view('pacs.study', compact('study')); return view('pacs.study', compact('study'));
} }
public function import($id) public function import()
{ {
$studies = (new OrthancRestClient)->getStudies(); $studies = (new OrthancRestClient)->getStudies();
return redirect()->route('pacs.index'); (new StudyImporter)->import($studies);
return redirect()->route('studies.index');
} }
} }

View File

@ -4,7 +4,7 @@
enum StudyLevelStatus: int enum StudyLevelStatus: int
{ {
case None = 0; case Pending = 0;
case StudyArrived = 1 << 1; case StudyArrived = 1 << 1;
case StudyLocked = 1 << 2; case StudyLocked = 1 << 2;
case StudyUnlocked = 1 << 3; case StudyUnlocked = 1 << 3;

View File

@ -14,4 +14,6 @@ class Study extends Model
'study_status' => StudyLevelStatus::class, 'study_status' => StudyLevelStatus::class,
'report_status' => ReportStatus::class, 'report_status' => ReportStatus::class,
]; ];
protected $guarded = ['id'];
} }

View File

@ -0,0 +1,19 @@
<?php
namespace App\Services;
use App\Models\Enums\NameMatchModes;
final class InputMatcher
{
public static function match(string $input, string $pattern, NameMatchModes $mode): bool
{
return match ($mode) {
NameMatchModes::Exact => 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,
};
}
}

View File

@ -6,10 +6,20 @@ enum DicomTags: string
{ {
case AcquisitionDate = 'AcquisitionDate'; case AcquisitionDate = 'AcquisitionDate';
case AcquisitionTime = 'AcquisitionTime'; case AcquisitionTime = 'AcquisitionTime';
case ContentDate = 'ContentDate';
case ContentTime = 'ContentTime';
case AcquisitionDeviceProcessingDescription = 'AcquisitionDeviceProcessingDescription'; case AcquisitionDeviceProcessingDescription = 'AcquisitionDeviceProcessingDescription';
case BodyPartExamined = 'BodyPartExamined'; case BodyPartExamined = 'BodyPartExamined';
case Modality = 'Modality'; case Modality = 'Modality';
case SoftwareVersions = 'SoftwareVersions'; case SoftwareVersions = 'SoftwareVersions';
case ProtocolName = 'ProtocolName';
case StationName = 'StationName'; 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 Private10 = '0029,0010';
case IW_Private = '0009,0010';
} }

View File

@ -0,0 +1,35 @@
<?php
namespace App\Services\Pacs;
use App\Models\Enums\NameMatchModes;
use App\Services\InputMatcher;
use Cache;
use DB;
final class InstituteMapper
{
private static array $patterns = [];
private static int $catchAll = -1;
public static function map(string $input): int
{
if (empty(self::$patterns)) {
self::$patterns = Cache::remember('institute_names', now()->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;
}
}

View File

@ -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 public function getStudies(): array
{ {
$query = [ $query = [
@ -22,8 +39,6 @@ public function getStudies(): array
$url = '/studies?'.http_build_query($query); $url = '/studies?'.http_build_query($query);
$response = $this->getClient()->get($url); $response = $this->getClient()->get($url);
$studies = json_decode($response->getBody()->getContents(), true); return json_decode($response->getBody()->getContents(), true);
return $studies;
} }
} }

View File

@ -11,38 +11,67 @@ final class StudyImporter
public function import(array $studies): void public function import(array $studies): void
{ {
foreach ($studies as $study) { foreach ($studies as $study) {
$othanc_id = strtolower($study['ID']); $orthanc_uid = strtolower($study['ID']);
$row = Study::where('othanc_id', $othanc_id)->first(); $row = Study::where(compact('orthanc_uid'))->first();
if ($row && $row->study_status < StudyLevelStatus::StudyArrived) { if ($row != null) {
if ($row->study_status < StudyLevelStatus::StudyArrived) {
// todo: update study // todo: update study
}
return; return;
} }
$inst_name = data_get($study, 'MainDicomTags.InstitutionName');
$inst_id = InstituteMapper::map($inst_name);
$data = [ $data = [
'othanc_id' => $othanc_id, 'orthanc_uid' => $orthanc_uid,
'study_status' => StudyLevelStatus::StudyArrived,
'study_datetime' => $study['StudyDateTime'],
'receive_datetime' => $study['ReceiveDateTime'],
'is_locked' => false, 'is_locked' => false,
'is_active' => true, 'is_active' => true,
'patient_id' => $study['PatientMainDicomTags']['PatientID'], 'institution_name' => $inst_name,
'patient_name' => $study['PatientMainDicomTags']['PatientName'], 'institute_id' => $inst_id,
'patient_sex' => $study['PatientMainDicomTags']['PatientSex'],
'patient_birthdate' => $study['PatientMainDicomTags']['PatientBirthDate'],
'institution_name' => $study['MainDicomTags']['InstitutionName'], 'patient_uuid' => $study['ParentPatient'],
'accession_number' => $study['MainDicomTags']['AccessionNumber'], 'patient_id' => data_get($study, 'PatientMainDicomTags.PatientID'),
'referring_physician_name' => $study['MainDicomTags']['ReferringPhysicianName'], 'patient_name' => data_get($study, 'PatientMainDicomTags.PatientName'),
'study_id' => $study['MainDicomTags']['StudyID'], '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']), 'study_date' => DicomUtils::dateTimeToCarbon($study['MainDicomTags']['StudyDate'], $study['MainDicomTags']['StudyTime']),
'receive_date' => Carbon::parse($study['LastUpdate'], 'UTC'), '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']), '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);
} }
} }
} }

View File

@ -20,14 +20,22 @@ public function up(): void
$table->boolean('is_locked')->default(true); $table->boolean('is_locked')->default(true);
$table->unsignedTinyInteger('study_priority')->default(0); $table->unsignedTinyInteger('study_priority')->default(0);
$table->string('patient_id')->nullable(); $table->string('patient_id')->nullable();
$table->string('patient_uuid')->nullable();
$table->string('patient_name'); $table->string('patient_name');
$table->string('patient_sex')->nullable(); $table->string('patient_sex')->nullable();
$table->date('patient_birthdate')->nullable(); $table->date('patient_birthdate')->nullable();
$table->string('study_instance_uid')->unique(); $table->string('study_instance_uid')->unique();
$table->string('study_id')->nullable(); $table->string('study_id')->nullable();
$table->string('institution_name')->nullable(); $table->string('institution_name')->nullable();
$table->string('accession_number')->nullable(); $table->string('accession_number')->nullable();
$table->string('study_description')->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('referring_physician_name')->nullable();
$table->string('study_modality', 4)->nullable(); $table->string('study_modality', 4)->nullable();
$table->dateTime('study_date'); $table->dateTime('study_date');
@ -36,8 +44,10 @@ public function up(): void
$table->foreignIdFor(Institute::class)->constrained()->onDelete('cascade'); $table->foreignIdFor(Institute::class)->constrained()->onDelete('cascade');
$table->unsignedTinyInteger('study_status')->default(StudyLevelStatus::None->value); $table->unsignedTinyInteger('study_status')->default(StudyLevelStatus::None->value);
$table->unsignedTinyInteger('report_status')->default(ReportStatus::Pending->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, 'assigned_physician_id')->nullable()->constrained()->onDelete('set null');
$table->foreignIdFor(User::class, 'interpreting_physician_id')->nullable()->constrained()->onDelete('set null'); $table->foreignIdFor(User::class, 'interpreting_physician_id')->nullable()->constrained()->onDelete('set null');

View File

@ -18,5 +18,6 @@
Route::get('/studies', [PacsController::class, 'index'])->name('studies.index'); Route::get('/studies', [PacsController::class, 'index'])->name('studies.index');
Route::get('/studies/{id}', [PacsController::class, 'show'])->name('studies.show'); Route::get('/studies/{id}', [PacsController::class, 'show'])->name('studies.show');
Route::get('/cron', [PacsController::class, 'import'])->name('studies.import');
}); });