commit 363d5b506d17674607a8686db917e104164960a1 Author: mario Date: Wed Mar 19 14:43:06 2025 +0700 using installed dcmtk diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..397b4a7 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.log diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..608d3c6 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,15 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Launch Package", + "type": "go", + "request": "launch", + "mode": "auto", + "program": "${fileDirname}" + } + ] +} \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..72c27f0 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module devone.aplikasi.web.id/gitea/mario/go-dicom-mwl + +go 1.22.2 diff --git a/main.go b/main.go new file mode 100644 index 0000000..e112400 --- /dev/null +++ b/main.go @@ -0,0 +1,240 @@ +package main + +import ( + "encoding/json" + "fmt" + "log" + "net/http" + "os" + "os/exec" + "regexp" + "strings" + "time" +) + +// WorklistItem represents a single item in the worklist +type WorklistItem struct { + PatientName string `json:"PatientName"` + PatientID string `json:"PatientID"` + PatientBirthDate string `json:"PatientBirthDate"` + PatientSex string `json:"PatientSex"` + AccessionNumber string `json:"AccessionNumber"` + StudyInstanceUID string `json:"StudyInstanceUID"` + RequestingPhysician string `json:"RequestingPhysician"` + ScheduledProcedureStepDescription string `json:"ScheduledProcedureStepDescription"` + ScheduledProcedureStepStartDate string `json:"ScheduledProcedureStepStartDate"` + ScheduledProcedureStepStatus string `json:"ScheduledProcedureStepStatus"` + Modality string `json:"Modality"` +} + +// WorklistResponse is the structure of the JSON response +type WorklistResponse struct { + Count int `json:"count"` + Records []WorklistItem `json:"records"` +} + +func main() { + http.HandleFunc("/worklists", handleWorklists) + + port := 5200 + fmt.Printf("Server started on port %d\n", port) + log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", port), nil)) +} + +func handleWorklists(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + date := r.URL.Query().Get("date") + modality := r.URL.Query().Get("modality") + patientName := r.URL.Query().Get("patientName") + patientID := r.URL.Query().Get("patientId") + status := r.URL.Query().Get("status") + accessionNumber := r.URL.Query().Get("accessionNumber") + + if date == "" { + date = time.Now().Format("20060102") + } + + worklists, err := queryDICOMWorklist(date, modality, patientName, patientID, status, accessionNumber) + if err != nil { + http.Error(w, fmt.Sprintf("Error querying worklist: %v", err), http.StatusInternalServerError) + return + } + + response := WorklistResponse{ + Records: worklists, + Count: len(worklists), + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) +} + +func queryDICOMWorklist(date, modality, patientName, patientID, status, accessionNumber string) ([]WorklistItem, error) { + tempFile := "temp_dicom_output.txt" + + args := []string{ + "-v", "-W", + "-aec", "ABPACS", + "-aet", "DICOMWEB_PROXY", + "128.199.154.150", "11112", + "-k", "QueryRetrieveLevel=WORKLIST", + "-k", fmt.Sprintf("ScheduledProcedureStepSequence[0].ScheduledProcedureStepStartDate=%s", date), + "-k", "AccessionNumber", + "-k", "StudyInstanceUID", + "-k", "ScheduledProcedureStepSequence[0].Modality", + "-k", "ScheduledProcedureStepSequence[0].ScheduledProcedureStepDescription", + "-k", "ScheduledProcedureStepSequence[0].ScheduledProcedureStepStatus", + "-k", "PatientID", + "-k", "PatientName", + "-k", "PatientSex", + "-k", "PatientBirthDate", + "-k", "RequestingPhysician", + } + + if modality != "" { + args = append(args, "-k", fmt.Sprintf("ScheduledProcedureStepSequence[0].Modality=%s", modality)) + } + if patientName != "" { + args = append(args, "-k", fmt.Sprintf("PatientName=%s", patientName)) + } + if patientID != "" { + args = append(args, "-k", fmt.Sprintf("PatientID=%s", patientID)) + } + if status != "" { + args = append(args, "-k", fmt.Sprintf("ScheduledProcedureStepSequence[0].ScheduledProcedureStepStatus=%s", status)) + } + if accessionNumber != "" { + args = append(args, "-k", fmt.Sprintf("AccessionNumber=%s", accessionNumber)) + } + + cmdString := fmt.Sprintf("findscu %s > %s 2>&1", strings.Join(args, " "), tempFile) + + cmd := exec.Command("sh", "-c", cmdString) + err := cmd.Run() + if err != nil { + return nil, fmt.Errorf("error executing findscu: %v", err) + } + + output, err := os.ReadFile(tempFile) + if err != nil { + return nil, fmt.Errorf("error reading output file: %v", err) + } + + defer os.Remove(tempFile) + + return parseDICOMOutput(string(output)), nil +} + +func parseDICOMOutput(output string) []WorklistItem { + var worklists []WorklistItem + var currentItem *WorklistItem + + lines := strings.Split(output, "\n") + inDataset := false + + for i, line := range lines { + trimmedLine := strings.TrimSpace(line) + + // Check if we're starting a new dataset response + if strings.Contains(trimmedLine, "Find Response:") && strings.Contains(trimmedLine, "(Pending)") { + if currentItem != nil && !isEmptyWorklistItem(*currentItem) { + worklists = append(worklists, *currentItem) + } + currentItem = &WorklistItem{} + inDataset = true + continue + } + + // Skip lines that don't contain tag information + if !inDataset || !strings.Contains(trimmedLine, ") ") { + continue + } + + // Extract tag information + extractTagInfo(trimmedLine, currentItem) + + // Look ahead for sequence items (especially for Modality which is nested) + if i+1 < len(lines) && strings.Contains(lines[i], "ScheduledProcedureStepSequence") { + // Start looking for nested sequence items + for j := i + 1; j < len(lines) && !strings.Contains(lines[j], "ItemDelimitationItem"); j++ { + nestedLine := strings.TrimSpace(lines[j]) + if strings.Contains(nestedLine, "0008,0060") && strings.Contains(nestedLine, "Modality") { + re := regexp.MustCompile(`\[([^\]]*)\]`) + matches := re.FindStringSubmatch(nestedLine) + if len(matches) >= 2 { + currentItem.Modality = strings.TrimSpace(matches[1]) + } + } else if strings.Contains(nestedLine, "0040,0002") && strings.Contains(nestedLine, "ScheduledProcedureStepStartDate") { + re := regexp.MustCompile(`\[([^\]]*)\]`) + matches := re.FindStringSubmatch(nestedLine) + if len(matches) >= 2 { + currentItem.ScheduledProcedureStepStartDate = strings.TrimSpace(matches[1]) + } + } else if strings.Contains(nestedLine, "0040,0007") && strings.Contains(nestedLine, "ScheduledProcedureStepDescription") { + re := regexp.MustCompile(`\[([^\]]*)\]`) + matches := re.FindStringSubmatch(nestedLine) + if len(matches) >= 2 { + currentItem.ScheduledProcedureStepDescription = strings.TrimSpace(matches[1]) + } + } else if strings.Contains(nestedLine, "0040,0020") && strings.Contains(nestedLine, "ScheduledProcedureStepStatus") { + re := regexp.MustCompile(`\[([^\]]*)\]`) + matches := re.FindStringSubmatch(nestedLine) + if len(matches) >= 2 { + currentItem.ScheduledProcedureStepStatus = strings.TrimSpace(matches[1]) + } + } + } + } + } + + // Add the last item if it exists + if currentItem != nil && !isEmptyWorklistItem(*currentItem) { + worklists = append(worklists, *currentItem) + } + + return worklists +} + +func extractTagInfo(line string, item *WorklistItem) { + // Pattern for parsing DICOM tags from the format: (0008,0050) SH [CR.250319.4554.01] + re := regexp.MustCompile(`\(([0-9a-fA-F]{4}),([0-9a-fA-F]{4})\) \w+ \[([^\]]*)\]`) + matches := re.FindStringSubmatch(line) + + if len(matches) < 4 { + return + } + + group := matches[1] + element := matches[2] + value := strings.TrimSpace(matches[3]) + + // Map DICOM tags to struct fields + switch group + "," + element { + case "0010,0010": // PatientName + item.PatientName = value + case "0010,0020": // PatientID + item.PatientID = value + case "0010,0030": // PatientBirthDate + item.PatientBirthDate = value + case "0010,0040": // PatientSex + item.PatientSex = value + case "0008,0050": // AccessionNumber + item.AccessionNumber = value + case "0020,000d": // StudyInstanceUID + item.StudyInstanceUID = value + case "0032,1032": // RequestingPhysician + item.RequestingPhysician = value + } +} + +func isEmptyWorklistItem(item WorklistItem) bool { + return item.PatientName == "" && + item.PatientID == "" && + item.AccessionNumber == "" && + item.StudyInstanceUID == "" && + item.Modality == "" +} diff --git a/test.http b/test.http new file mode 100644 index 0000000..27babc9 --- /dev/null +++ b/test.http @@ -0,0 +1,32 @@ + +### === * No Param * === +GET http://192.168.0.116:5200/worklists + +### === * Modality Filter * === +GET http://192.168.0.116:5200/worklists?modality=CR + +### === * Date Filter * === +GET http://192.168.0.116:5200/worklists?date=20250305 + +### === * Date Filter (in range)* === +GET http://192.168.0.116:5200/worklists?date=20250317-20250318 + +### === * Status Filter * === +### Options: SCHEDULED, STARTED, COMPLETED, +GET http://192.168.0.116:5200/worklists?status=SCHEDULED + +### === * Patient Name Filter * === +### Tips: Use * for wildcard +GET http://192.168.0.116:5200/worklists?patientName=Palguno*&date=20250319 + +### === * Patient ID Filter * === +GET http://192.168.0.116:5200/worklists?patientId=00409303&date=20250318 + +### === * Accession Number Filter * === +GET http://192.168.0.116:5200/worklists?accessionNumber=ECG.250318.002&date=20250318 + +### === * Combine Filter * === +GET http://192.168.0.116:5200/worklists?modality=CT&date=20250305&status=SCHEDULED + +### +GET http://192.168.0.116:5200/worklists?modality=CR&status=STARTED&patientName=Palguno*&date=20250319 \ No newline at end of file