started working on DICOM support (stored in Media object) (#116)

This commit is contained in:
Jason Kulatunga 2023-04-04 19:25:48 -07:00 committed by GitHub
parent 390cea6108
commit 01b6cc3aee
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 729 additions and 27 deletions

View File

@ -77,6 +77,7 @@ Next we'll start the processes described above:
# In terminal #1, run the following
cd frontend
yarn install
yarn dist -- -c [sandbox|prod]
# eg. yarn dist -- -c prod

View File

@ -537,7 +537,8 @@ func (sr *SqliteRepository) GetFlattenedResourceGraph(ctx context.Context) ([]*m
graph.DFS(g, vertexId, func(relatedVertexId string) bool {
relatedResourceFhir, _ := g.Vertex(relatedVertexId)
//skip the current resource if it's referenced in this list.
if vertexId != resourceVertexId(relatedResourceFhir) {
//also skip the current resource if its a Binary resource (which is a special case)
if vertexId != resourceVertexId(relatedResourceFhir) && relatedResourceFhir.SourceResourceType != "Binary" {
resource.RelatedResourceFhir = append(resource.RelatedResourceFhir, relatedResourceFhir)
}
return false

View File

@ -27,13 +27,19 @@
"aot": true,
"assets": [
"src/favicon.ico",
"src/assets"
"src/assets",
{
"glob": "**/*",
"input": "./node_modules/dwv/decoders/",
"output": "/assets/dwv/decoders/"
},
],
"styles": [
"src/styles.scss"
],
"scripts": [
"node_modules/@panva/oauth4webapi/build/index.js"
"node_modules/@panva/oauth4webapi/build/index.js",
]
},
"configurations": {
@ -55,7 +61,7 @@
{
"type": "initial",
"maximumWarning": "2mb",
"maximumError": "5mb"
"maximumError": "10mb"
},
{
"type": "anyComponentStyle",

View File

@ -36,6 +36,7 @@
"asmcrypto.js": "^2.3.2",
"bootstrap": "^4.4.1",
"chart.js": "2.9.4",
"dwv": "^0.31.0",
"fhirpath": "^3.3.0",
"humanize-duration": "^3.27.3",
"idb": "^7.1.0",

View File

@ -0,0 +1,77 @@
<div class="row">
<div class="col text-center">
<div class="btn-group" role="group" aria-label="Basic example">
<button ngbTooltip="scroll layers" type="button" (click)="onChangeTool('Scroll')" class="btn btn-secondary pd-x-25" [class.active]="selectedTool == 'Scroll'"><i class="fas fa-bars"></i></button>
<button ngbTooltip="zoom and pan" type="button" (click)="onChangeTool('ZoomAndPan')" class="btn btn-secondary pd-x-25" [class.active]="selectedTool == 'ZoomAndPan'"><i class="fas fa-search"></i></button>
<button ngbTooltip="change brightness" type="button" (click)="onChangeTool('WindowLevel')" class="btn btn-secondary pd-x-25" [class.active]="selectedTool == 'WindowLevel'"><i class="fas fa-adjust"></i></button>
<button ngbTooltip="add measurements" type="button" (click)="onChangeTool('Draw')" class="btn btn-secondary pd-x-25" [class.active]="selectedTool == 'Draw'"><i class="fas fa-edit"></i></button>
</div>
<div class="btn-group" role="group" aria-label="Basic example">
<button ngbTooltip="toggle orientation" type="button" (click)="toggleOrientation()" class="btn btn-secondary pd-x-25"><i class="fas fa-compress"></i></button>
</div>
<div class="btn-group" role="group" aria-label="Info">
<button ngbTooltip="show info" type="button" (click)="openTagsModal(tagsModal)" class="btn btn-secondary pd-x-25"><i class="fas fa-info"></i></button>
</div>
</div>
</div>
<div id="dwv">
<!-- <mat-progress-bar mode="determinate" value="{{ loadProgress }}"></mat-progress-bar>-->
<!-- <div class="button-row">-->
<!-- <mat-button-toggle-group name="tool" [disabled]="!dataLoaded">-->
<!-- <mat-button-toggle value="{{ tool }}" color="primary"-->
<!-- *ngFor="let tool of toolNames"-->
<!-- title="{{ tool }}"-->
<!-- (click)="onChangeTool(tool)"-->
<!-- [disabled]="!dataLoaded || !canRunTool(tool)">-->
<!-- <mat-icon>{{ getToolIcon(tool) }}</mat-icon>-->
<!-- </mat-button-toggle>-->
<!-- </mat-button-toggle-group>-->
<div id="layerGroup0" class="layerGroup pd-t-10">
<div id="dropBox"></div>
</div>
</div>
<ng-template #tagsModal let-modal>
<div class="modal-header">
<h4 class="modal-title" id="modal-basic-title">DICOM tags</h4>
<button type="button" class="close" aria-label="Close" (click)="modal.dismiss('Cross click')">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body">
<div class="table-responsive">
<table class="table table-striped mg-b-0">
<thead>
<tr>
<th>ID</th>
<th>Name</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let row of visibleMetaData">
<th scope="row">{{row.id}}</th>
<td>{{row.name}}</td>
<td>{{row.value}}</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="modal-footer">
<ngb-pagination
class="mr-auto"
[collectionSize]="metaData.length"
[(page)]="tagsPage"
[pageSize]="tagsPageSize"
(pageChange)="refreshTags()"
>
</ngb-pagination>
<button type="button" class="btn btn-outline-dark" (click)="modal.close('Close click')">Close</button>
</div>
</ng-template>

View File

@ -0,0 +1,22 @@
/* Layers */
//.layerGroup {
// position: relative;
// padding: 0;
// display: flex;
// justify-content: center;
// height: 90%;
//}
//.layer {
// position: absolute;
// pointer-events: none;
//}
#dwv {
width: 100%;
height: 700px;
}
#layerGroup0 {
//width: 90%;
height: 90%;
}

View File

@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { DicomComponent } from './dicom.component';
describe('DicomComponent', () => {
let component: DicomComponent;
let fixture: ComponentFixture<DicomComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ DicomComponent ]
})
.compileComponents();
fixture = TestBed.createComponent(DicomComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,262 @@
import {Component, Input, OnInit, TemplateRef} from '@angular/core';
import * as dwv from 'dwv';
import {NgbModal} from '@ng-bootstrap/ng-bootstrap';
import { VERSION } from '@angular/core';
import {BinaryModel} from '../../../../../lib/models/resources/binary-model';
// Copied from https://raw.githubusercontent.com/ivmartel/dwv-angular/master/src/app/dwv/dwv.component.ts
// gui overrides
// Image decoders (for web workers)
dwv.image.decoderScripts = {
jpeg2000: 'assets/dwv/decoders/pdfjs/decode-jpeg2000.js',
'jpeg-lossless': 'assets/dwv/decoders/rii-mango/decode-jpegloss.js',
'jpeg-baseline': 'assets/dwv/decoders/pdfjs/decode-jpegbaseline.js',
rle: 'assets/dwv/decoders/dwv/decode-rle.js'
};
@Component({
selector: 'fhir-dicom',
templateUrl: './dicom.component.html',
styleUrls: ['./dicom.component.scss']
})
export class DicomComponent implements OnInit {
@Input() displayModel: BinaryModel
public loadProgress = 0;
public dataLoaded = false;
public tools = {
Scroll: {},
ZoomAndPan: {},
WindowLevel: {},
Draw: {
options: ['Ruler']
}
};
selectedTool = 'Scroll';
private dwvApp: any;
private metaData: any[];
visibleMetaData: any[] = [] //for pagination usage
private orientation: string;
tagsPage = 1;
tagsPageSize = 10;
// tagsCollectionSize = this.metadata.length;
// countries: Country[];
constructor(private modalService: NgbModal) {}
ngOnInit() {
// create app
this.dwvApp = new dwv.App();
// initialise app
this.dwvApp.init({
dataViewConfigs: {'*': [{divId: 'layerGroup0'}]},
tools: this.tools
});
// handle load events
let nLoadItem = null;
let nReceivedLoadError = null;
let nReceivedLoadAbort = null;
let isFirstRender = null;
this.dwvApp.addEventListener('loadstart', (/*event*/) => {
// reset flags
this.dataLoaded = false;
nLoadItem = 0;
nReceivedLoadError = 0;
nReceivedLoadAbort = 0;
isFirstRender = true;
// hide drop box
//TODO:
// this.showDropbox(false);
});
this.dwvApp.addEventListener('loadprogress', (event) => {
this.loadProgress = event.loaded;
});
this.dwvApp.addEventListener('renderend', (/*event*/) => {
if (isFirstRender) {
isFirstRender = false;
// available tools
let selectedTool = 'ZoomAndPan';
if (this.dwvApp.canScroll()) {
selectedTool = 'Scroll';
}
this.onChangeTool(selectedTool);
}
});
this.dwvApp.addEventListener('load', (/*event*/) => {
// set dicom tags
this.metaData = dwv.utils.objectToArray(this.dwvApp.getMetaData(0));
// set data loaded flag
this.dataLoaded = true;
});
this.dwvApp.addEventListener('loadend', (/*event*/) => {
if (nReceivedLoadError) {
this.loadProgress = 0;
alert('Received errors during load. Check log for details.');
// show drop box if nothing has been loaded
if (!nLoadItem) {
//TODO: this.showDropbox(true);
}
}
if (nReceivedLoadAbort) {
this.loadProgress = 0;
alert('Load was aborted.');
//TODO: this.showDropbox(true);
}
});
this.dwvApp.addEventListener('loaditem', (/*event*/) => {
++nLoadItem;
});
this.dwvApp.addEventListener('loaderror', (event) => {
console.error(event.error);
++nReceivedLoadError;
});
this.dwvApp.addEventListener('loadabort', (/*event*/) => {
++nReceivedLoadAbort;
});
// handle key events
this.dwvApp.addEventListener('keydown', (event) => {
this.dwvApp.defaultOnKeydown(event);
});
// handle window resize
window.addEventListener('resize', this.dwvApp.onResize);
// setup drop box
//TODO: this.setupDropbox();
// possible load from location
// dwv.utils.loadFromUri(window.location.href, this.dwvApp);
if(!this.displayModel) {
return;
}
//Load from Input file
let files = [new File([
this.dataBase64toBlob(this.displayModel.data, "application/dicom")
], "dicom.dcm", {type: "application/dicom"})]
this.dwvApp.loadFiles(files);
}
/**
* Handle a change tool event.
* @param tool The new tool name.
*/
onChangeTool = (tool: string) => {
if ( this.dwvApp ) {
this.selectedTool = tool;
this.dwvApp.setTool(tool);
if (tool === 'Draw') {
this.onChangeShape(this.tools.Draw.options[0]);
}
}
}
/**
* Check if a tool can be run.
*
* @param tool The tool name.
* @returns True if the tool can be run.
*/
canRunTool = (tool: string) => {
let res: boolean;
if (tool === 'Scroll') {
res = this.dwvApp.canScroll();
} else if (tool === 'WindowLevel') {
res = this.dwvApp.canWindowLevel();
} else {
res = true;
}
return res;
}
/**
* Toogle the viewer orientation.
*/
toggleOrientation = () => {
if (typeof this.orientation !== 'undefined') {
if (this.orientation === 'axial') {
this.orientation = 'coronal';
} else if (this.orientation === 'coronal') {
this.orientation = 'sagittal';
} else if (this.orientation === 'sagittal') {
this.orientation = 'axial';
}
} else {
// default is most probably axial
this.orientation = 'coronal';
}
// update data view config
const config = {
'*': [
{
divId: 'layerGroup0',
orientation: this.orientation
}
]
};
this.dwvApp.setDataViewConfig(config);
// render data
for (let i = 0; i < this.dwvApp.getNumberOfLoadedData(); ++i) {
this.dwvApp.render(i);
}
}
/**
* Handle a change draw shape event.
* @param shape The new shape name.
*/
private onChangeShape = (shape: string) => {
if ( this.dwvApp && this.selectedTool === 'Draw') {
this.dwvApp.setToolFeatures({shapeName: shape});
}
}
/**
* Handle a reset event.
*/
onReset = () => {
if ( this.dwvApp ) {
this.dwvApp.resetDisplay();
}
}
/**
* Open the DICOM tags dialog.
*/
openTagsModal(template: TemplateRef<any>) {
this.refreshTags();
this.modalService.open(template, { size: 'lg', ariaLabelledBy: 'modal-basic-title' });
}
refreshTags() {
//TODO: if tag.value is a array, content should be json encoded
this.visibleMetaData = this.metaData.map((tag, i) => ({ id: i + 1, ...tag })).slice(
(this.tagsPage - 1) * this.tagsPageSize,
(this.tagsPage - 1) * this.tagsPageSize + this.tagsPageSize,
);
}
//This function is used to convert base64 binary data to a blob
dataBase64toBlob(base64Data, contentType) {
// convert base64 to raw binary data held in a string
// doesn't handle URLEncoded DataURIs - see SO answer #6850276 for code that does this
var byteString = atob(base64Data);
// write the bytes of the string to an ArrayBuffer
var ab = new ArrayBuffer(byteString.length);
var ia = new Uint8Array(ab);
for (var i = 0; i < byteString.length; i++) {
ia[i] = byteString.charCodeAt(i);
}
return new Blob([ab], { type: contentType });
}
}

View File

@ -24,6 +24,7 @@ import {ProcedureComponent} from '../resources/procedure/procedure.component';
import {DiagnosticReportComponent} from '../resources/diagnostic-report/diagnostic-report.component';
import {PractitionerComponent} from '../resources/practitioner/practitioner.component';
import {DocumentReferenceComponent} from '../resources/document-reference/document-reference.component';
import {MediaComponent} from '../resources/media/media.component';
@Component({
selector: 'fhir-resource',
@ -118,6 +119,9 @@ export class FhirResourceComponent implements OnInit, OnChanges {
case "Immunization": {
return ImmunizationComponent;
}
case "Media": {
return MediaComponent;
}
case "Medication": {
return MedicationComponent;
}

View File

@ -1,7 +1,18 @@
<ng-container [ngTemplateOutlet]="
<div *ngIf="loading">
<div class="text-center mg-b-20">
<div class="spinner-border" role="status">
<span class="sr-only">Loading...</span>
</div>
</div>
</div>
<ng-container *ngIf="!loading" [ngTemplateOutlet]="
displayModel?.content_type == 'application/pdf' ? showPdf :
displayModel?.content_type == 'text/markdown' ? showMarkdown :
displayModel?.content_type == 'text/plain' ? showText :
displayModel?.content_type == 'application/dicom' ? showDicom :
(displayModel?.content_type == 'text/html' || displayModel?.content_type == 'application/html') ? showHtml :
(displayModel?.content_type == 'application/xml' || displayModel?.content_type == 'application/json') ? showHighlight :
(displayModel?.content_type == 'image/jpeg' || displayModel?.content_type == 'image/png') ? showImg :
@ -26,6 +37,9 @@
<ng-template #showText>
<fhir-binary-text [displayModel]="displayModel"></fhir-binary-text>
</ng-template>
<ng-template #showDicom>
<fhir-dicom [displayModel]="displayModel"></fhir-dicom>
</ng-template>
<ng-template #showEmpty>
Unknown Binary content type {{displayModel?.content_type}}
</ng-template>

View File

@ -2,16 +2,24 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
import { BinaryComponent } from './binary.component';
import {NgbCollapseModule} from '@ng-bootstrap/ng-bootstrap';
import {FastenApiService} from '../../../../services/fasten-api.service';
import {RouterTestingModule} from '@angular/router/testing';
describe('BinaryComponent', () => {
let component: BinaryComponent;
let fixture: ComponentFixture<BinaryComponent>;
let mockedFastenApiService
beforeEach(async () => {
mockedFastenApiService = jasmine.createSpyObj('FastenApiService', ['getBinaryModel'])
await TestBed.configureTestingModule({
declarations: [ BinaryComponent ],
imports: [NgbCollapseModule]
imports: [NgbCollapseModule, RouterTestingModule],
providers: [{
provide: FastenApiService,
useValue: mockedFastenApiService
}]
})
.compileComponents();

View File

@ -2,6 +2,8 @@ import {ChangeDetectorRef, Component, Input, OnInit} from '@angular/core';
import {BinaryModel} from '../../../../../lib/models/resources/binary-model';
import {FhirResourceComponentInterface} from '../../fhir-resource/fhir-resource-component-interface';
import {Router} from '@angular/router';
import {AttachmentModel} from '../../../../../lib/models/datatypes/attachment-model';
import {FastenApiService} from '../../../../services/fasten-api.service';
@Component({
selector: 'fhir-binary',
@ -11,13 +13,27 @@ import {Router} from '@angular/router';
export class BinaryComponent implements OnInit, FhirResourceComponentInterface {
@Input() displayModel: BinaryModel
@Input() showDetails: boolean = true
@Input() attachmentSourceId: string
@Input() attachmentModel: AttachmentModel //can only have attachmentModel or binaryModel, not both.
constructor(public changeRef: ChangeDetectorRef, public router: Router) {}
loading: boolean = false
constructor(public changeRef: ChangeDetectorRef, public router: Router, public fastenApi: FastenApiService) {}
ngOnInit(): void {
if(!this.displayModel && this.attachmentSourceId && this.attachmentModel){
this.loading = true
this.fastenApi.getBinaryModel(this.attachmentSourceId, this.attachmentModel)
.subscribe((binaryModel: BinaryModel) => {
this.loading = false
this.displayModel = binaryModel
this.markForCheck()
}, (error) => {
this.loading = false
this.markForCheck()
})
}
}
markForCheck(){
this.changeRef.markForCheck()
}
}

View File

@ -16,7 +16,7 @@
<fhir-ui-table [displayModel]="displayModel" [tableData]="tableData"></fhir-ui-table>
<div *ngIf="!showDetails">
<fhir-binary *ngFor="let binaryModel of displayModel.content" [displayModel]="binaryModel"></fhir-binary>
<fhir-binary *ngFor="let attachmentModel of displayModel.content" [attachmentModel]="attachmentModel" [attachmentSourceId]="displayModel?.source_id" ></fhir-binary>
</div>
</div>
<div *ngIf="showDetails" class="card-footer">

View File

@ -0,0 +1,25 @@
<div class="card card-fhir-resource" >
<div class="card-header" (click)="isCollapsed = ! isCollapsed">
<div>
<h6 class="card-title">{{displayModel?.sort_title}}</h6>
<!-- <p class="card-text tx-gray-400" *ngIf="displayModel?.created_at"><strong>Created at</strong> {{displayModel?.created_at | date}}</p>-->
</div>
<fhir-ui-badge class="float-right" [status]="displayModel?.status">{{displayModel?.status}}</fhir-ui-badge>
<!-- <div class="btn-group">-->
<!-- <button class="btn active">Day</button>-->
<!-- <button class="btn">Week</button>-->
<!-- <button class="btn">Month</button>-->
<!-- </div>-->
</div>
<div #collapse="ngbCollapse" [(ngbCollapse)]="isCollapsed" class="card-body">
<p class="az-content-text mg-b-20">An action that is or was performed on or for a patient, practitioner, device, organization, or location. For example, this can be a physical intervention on a patient like an operation, or less invasive like long term services, counseling, or hypnotherapy.</p>
<fhir-ui-table [displayModel]="displayModel" [tableData]="tableData"></fhir-ui-table>
<div *ngIf="!showDetails">
<fhir-binary [attachmentModel]="displayModel.content" [attachmentSourceId]="displayModel?.source_id"></fhir-binary>
</div>
</div>
<div *ngIf="showDetails" class="card-footer">
<a class="float-right" routerLink="/source/{{displayModel?.source_id}}/resource/{{displayModel?.source_resource_id}}">details</a>
</div>
</div>

View File

@ -0,0 +1,26 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MediaComponent } from './media.component';
import {NgbCollapseModule} from '@ng-bootstrap/ng-bootstrap';
import {RouterTestingModule} from '@angular/router/testing';
describe('MediaComponent', () => {
let component: MediaComponent;
let fixture: ComponentFixture<MediaComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ MediaComponent ],
imports: [NgbCollapseModule, RouterTestingModule],
})
.compileComponents();
fixture = TestBed.createComponent(MediaComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,45 @@
import {ChangeDetectorRef, Component, Input, OnInit} from '@angular/core';
import {DocumentReferenceModel} from '../../../../../lib/models/resources/document-reference-model';
import {TableRowItem, TableRowItemDataType} from '../../common/table/table-row-item';
import {Router} from '@angular/router';
import {FhirResourceComponentInterface} from '../../fhir-resource/fhir-resource-component-interface';
import {MediaModel} from '../../../../../lib/models/resources/media-model';
@Component({
selector: 'app-media',
templateUrl: './media.component.html',
styleUrls: ['./media.component.scss']
})
export class MediaComponent implements OnInit, FhirResourceComponentInterface{
@Input() displayModel: MediaModel
@Input() showDetails: boolean = true
isCollapsed: boolean = false
tableData: TableRowItem[] = []
constructor(public changeRef: ChangeDetectorRef, public router: Router) {}
ngOnInit(): void {
this.tableData = [
{
label: 'Description',
data: this.displayModel?.description,
enabled: !!this.displayModel?.description,
},
// {
// label: 'Performer',
// data: this.displayModel?.performer,
// data_type: TableRowItemDataType.Reference,
// enabled: this.displayModel?.has_performer,
// },
// {
// label: 'Conclusion',
// data: this.displayModel?.conclusion,
// enabled: !!this.displayModel?.conclusion,
// },
];
}
markForCheck(){
this.changeRef.markForCheck()
}
}

View File

@ -1,16 +1,30 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { GlossaryLookupComponent } from './glossary-lookup.component';
import {FastenApiService} from '../../services/fasten-api.service';
import {of} from 'rxjs';
describe('GlossaryLookupComponent', () => {
let component: GlossaryLookupComponent;
let fixture: ComponentFixture<GlossaryLookupComponent>;
let mockedFastenApiService
beforeEach(async () => {
mockedFastenApiService = jasmine.createSpyObj('FastenApiService', ['getGlossarySearchByCode'])
await TestBed.configureTestingModule({
declarations: [ GlossaryLookupComponent ]
declarations: [ GlossaryLookupComponent ],
providers: [{
provide: FastenApiService,
useValue: mockedFastenApiService
}]
})
.compileComponents();
mockedFastenApiService.getGlossarySearchByCode.and.returnValue(of({
url: 'http://www.example.com',
publisher: 'test-publisher',
description: 'test description'
}));
fixture = TestBed.createComponent(GlossaryLookupComponent);
component = fixture.componentInstance;

View File

@ -65,6 +65,8 @@ import { PractitionerComponent } from './fhir/resources/practitioner/practitione
import {PipesModule} from '../pipes/pipes.module';
import { NlmTypeaheadComponent } from './nlm-typeahead/nlm-typeahead.component';
import { DocumentReferenceComponent } from './fhir/resources/document-reference/document-reference.component';
import { DicomComponent } from './fhir/datatypes/dicom/dicom.component';
import { MediaComponent } from './fhir/resources/media/media.component';
import { GlossaryLookupComponent } from './glossary-lookup/glossary-lookup.component';
@NgModule({
@ -140,6 +142,8 @@ import { GlossaryLookupComponent } from './glossary-lookup/glossary-lookup.compo
NlmTypeaheadComponent,
DocumentReferenceComponent,
GlossaryLookupComponent,
DicomComponent,
MediaComponent,
],
exports: [
ComponentsSidebarComponent,

View File

@ -12,13 +12,13 @@ import {
BundleEntry,
Bundle,
Organization,
Practitioner, MedicationRequest, Patient, Encounter, DocumentReference, Media, DiagnosticReport, Reference
Practitioner, MedicationRequest, Patient, Encounter, DocumentReference, Media, DiagnosticReport, Reference, Binary
} from 'fhir/r4';
import {uuidV4} from '../../../lib/utils/uuid';
interface ResourceStorage {
[resourceType: string]: {
[resourceId: string]: Condition | Patient | MedicationRequest | Organization | FhirLocation | Practitioner | Procedure | Encounter | DocumentReference | Media | DiagnosticReport
[resourceId: string]: Condition | Patient | MedicationRequest | Organization | FhirLocation | Practitioner | Procedure | Encounter | DocumentReference | Media | DiagnosticReport | Binary
}
}
@ -416,6 +416,15 @@ function resourceCreateMedicationToR4MedicationRequest(resourceStorage: Resource
}
function resourceAttachmentToR4DocumentReference(resourceStorage: ResourceStorage, resourceAttachment: ResourceCreateAttachment): ResourceStorage {
resourceStorage['Binary'] = resourceStorage['Binary'] || {}
let binaryResource = {
id: uuidV4(),
resourceType: 'Binary',
contentType: resourceAttachment.file_type,
data: resourceAttachment.file_content,
} as Binary
resourceStorage['Binary'][binaryResource.id] = binaryResource
resourceStorage['DocumentReference'] = resourceStorage['DocumentReference'] || {}
@ -437,7 +446,7 @@ function resourceAttachmentToR4DocumentReference(resourceStorage: ResourceStorag
{
attachment: {
contentType: resourceAttachment.file_type,
data: resourceAttachment.file_content,
url: `urn:uuid:${binaryResource.id}`, //Binary
title: resourceAttachment.name,
}
}
@ -455,12 +464,19 @@ function resourceAttachmentToR4DocumentReference(resourceStorage: ResourceStorag
} as DocumentReference
resourceStorage['DocumentReference'][documentReferenceResource.id] = documentReferenceResource
//TODO create Binary object?
return resourceStorage
}
function resourceAttachmentToR4DiagnosticReport(resourceStorage: ResourceStorage, resourceAttachment: ResourceCreateAttachment): ResourceStorage {
resourceStorage['Binary'] = resourceStorage['Binary'] || {}
let binaryResource = {
id: uuidV4(),
resourceType: 'Binary',
contentType: resourceAttachment.file_type,
data: resourceAttachment.file_content,
} as Binary
resourceStorage['Binary'][binaryResource.id] = binaryResource
resourceStorage['Media'] = resourceStorage['Media'] || {}
let mediaResource = {
@ -476,7 +492,7 @@ function resourceAttachmentToR4DiagnosticReport(resourceStorage: ResourceStorage
},
content: {
contentType: resourceAttachment.file_type,
data: resourceAttachment.file_content,
url: `urn:uuid:${binaryResource.id}`, //Binary,
title: resourceAttachment.name,
},
} as Media

View File

@ -1,6 +1,6 @@
import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import {Observable} from 'rxjs';
import {Observable, of} from 'rxjs';
import { Router } from '@angular/router';
import {map} from 'rxjs/operators';
import {ResponseWrapper} from '../models/response-wrapper';
@ -15,6 +15,8 @@ import {GetEndpointAbsolutePath} from '../../lib/utils/endpoint_absolute_path';
import {environment} from '../../environments/environment';
import {ResourceAssociation} from '../models/fasten/resource_association';
import {ValueSet} from 'fhir/r4';
import {AttachmentModel} from '../../lib/models/datatypes/attachment-model';
import {BinaryModel} from '../../lib/models/resources/binary-model';
@Injectable({
providedIn: 'root'
@ -180,4 +182,35 @@ export class FastenApiService {
})
);
}
getBinaryModel(sourceId: string, attachmentModel: AttachmentModel): Observable<BinaryModel> {
if(attachmentModel.url && !attachmentModel.data){
//this attachment model is a refernce to a Binary model, we need to download it first.
let binaryResourceId = attachmentModel.url
//strip out the urn prefix (if this is an embedded id, eg. urn:uuid:2a35e080-c5f7-4dde-b0cf-8210505708f1)
let urnPrefix = "urn:uuid:";
if (binaryResourceId.startsWith(urnPrefix)) {
// PREFIX is exactly at the beginning
binaryResourceId = binaryResourceId.slice(urnPrefix.length);
}
//TODO: this is a naiive solution.
//assumes that this is a relative or absolutie url in the following format:
// 'Binary/xxx-xxx-xxx-xxx'
// 'https://www.example.com/R4/path/Binary/xxx-xx-x-xx'
let urlParts = binaryResourceId.split("Binary/");
binaryResourceId = urlParts[urlParts.length - 1];
return this.getResourceBySourceId(sourceId, binaryResourceId).pipe(
map((resourceFhir: ResourceFhir) => {
return new BinaryModel(resourceFhir.resource_raw)
})
)
} else {
return of(new BinaryModel(attachmentModel));
}
}
}

View File

@ -16,6 +16,7 @@ export enum ResourceType {
Goal = "Goal",
Immunization = "Immunization",
Location = "Location",
Media = "Media",
Medication = "Medication",
MedicationDispense = "MedicationDispense",
MedicationRequest = "MedicationRequest",

View File

@ -0,0 +1,24 @@
import fhirpath from 'fhirpath';
export class AttachmentModel {
contentType: string
language: string
data: string
url: string
size: number
hash: string
title: string
creation: string
constructor(fhirData: any) {
this.contentType = fhirData.contentType
this.language = fhirData.language
this.data = fhirData.data
this.url = fhirData.url
this.size = fhirData.size
this.hash = fhirData.hash
this.title = fhirData.title
this.creation = fhirData.creation
}
}

View File

@ -26,6 +26,7 @@ import {FastenOptions} from './fasten/fasten-options';
import {FastenDisplayModel} from './fasten/fasten-display-model';
import {MedicationRequestModel} from './resources/medication-request-model';
import {BinaryModel} from './resources/binary-model';
import {MediaModel} from './resources/media-model';
// import {BinaryModel} from './resources/binary-model';
@ -93,6 +94,9 @@ export function fhirModelFactory(modelResourceType: ResourceType, fhirResourceWr
case "Location":
resourceModel = new LocationModel(fhirResourceWrapper.resource_raw, fhirVersion, fastenOptions)
break
case "Media":
resourceModel = new MediaModel(fhirResourceWrapper.resource_raw, fhirVersion, fastenOptions)
break
case "Medication":
resourceModel = new MedicationModel(fhirResourceWrapper.resource_raw, fhirVersion, fastenOptions)
break

View File

@ -16,6 +16,8 @@ describe('ConditionModel', () => {
it('should parse example1.json', () => {
// let fixture = require("../../fixtures/r4/resources/condition/example1.json")
let expected = new ConditionModel({})
expected.code_id = '39065001'
expected.code_system = 'http://snomed.info/sct'
expected.code_text = 'Burn of ear'
expected.severity_text = 'Severe'
// expected.hasAsserter: boolean | undefined
@ -55,6 +57,8 @@ describe('ConditionModel', () => {
it('should parse example3.json', () => {
let expected = new ConditionModel({})
expected.code_text = 'Fever'
expected.code_id = '386661006'
expected.code_system = 'http://snomed.info/sct'
expected.severity_text = 'Mild'
expected.has_asserter = true
expected.asserter = { reference: 'Practitioner/f201' }

View File

@ -2,7 +2,7 @@ import { DocumentReferenceModel } from './document-reference-model';
import {AdverseEventModel} from './adverse-event-model';
import {CodableConceptModel} from '../datatypes/codable-concept-model';
import * as example1Fixture from "../../fixtures/r4/resources/documentReference/example1.json"
import {BinaryModel} from './binary-model';
import {AttachmentModel} from '../datatypes/attachment-model';
describe('DocumentReferenceModel', () => {
@ -29,7 +29,7 @@ describe('DocumentReferenceModel', () => {
]
})
expected.content = [
new BinaryModel({
new AttachmentModel({
"contentType": "application/hl7-v3+xml",
"language": "en-US",
"url": "http://example.org/xds/mhd/Binary/07a6483f-732b-461e-86b6-edb665c45510",

View File

@ -7,6 +7,7 @@ import {FastenDisplayModel} from '../fasten/fasten-display-model';
import {FastenOptions} from '../fasten/fasten-options';
import {Attachment} from 'fhir/r4';
import {BinaryModel} from './binary-model';
import {AttachmentModel} from '../datatypes/attachment-model';
export class DocumentReferenceModel extends FastenDisplayModel {
@ -18,7 +19,7 @@ export class DocumentReferenceModel extends FastenDisplayModel {
class_coding: CodingModel | undefined
created_at: string | undefined
security_label_coding: CodingModel | undefined
content: BinaryModel[] | undefined
content: AttachmentModel[] | undefined
context: {
eventCoding: CodingModel
facilityTypeCoding: CodingModel
@ -81,8 +82,8 @@ export class DocumentReferenceModel extends FastenDisplayModel {
this.category = new CodableConceptModel(_.get(fhirResource, 'category[0]') || {});
this.content = _.get(fhirResource, 'content', []).map((content: any) => {
const attachment: Attachment = _.get(content, 'attachment');
const binaryModel = new BinaryModel(attachment, fhirVersion);
return binaryModel;
const attachmentModel = new AttachmentModel(attachment);
return attachmentModel;
})
};

View File

@ -0,0 +1,36 @@
import {FastenDisplayModel} from '../fasten/fasten-display-model';
import {CodableConceptModel} from '../datatypes/codable-concept-model';
import {ReferenceModel} from '../datatypes/reference-model';
import {fhirVersions, ResourceType} from '../constants';
import {FastenOptions} from '../fasten/fasten-options';
import * as _ from "lodash";
import {AttachmentModel} from '../datatypes/attachment-model';
export class MediaModel extends FastenDisplayModel {
status: string
type: CodableConceptModel
reasonCode: CodableConceptModel[]
deviceName: string
device: ReferenceModel
height: number
width: number
description: string
content: AttachmentModel
constructor(fhirResource: any, fhirVersion?: fhirVersions, fastenOptions?: FastenOptions) {
super(fastenOptions)
this.source_resource_type = ResourceType.Media
this.status = _.get(fhirResource, 'status');
this.type = new CodableConceptModel(_.get(fhirResource, 'type'));
this.reasonCode = (_.get(fhirResource, 'reasonCode') || []).map((_reasonCode: any) => new CodableConceptModel(_reasonCode));
this.deviceName = _.get(fhirResource, 'deviceName');
this.device = _.get(fhirResource, 'device');
this.height = _.get(fhirResource, 'height');
this.width = _.get(fhirResource, 'width');
this.description = _.get(fhirResource, 'note');
this.content = new AttachmentModel(_.get(fhirResource, 'content'));
}
}

View File

@ -3273,6 +3273,15 @@ domutils@^2.8.0:
domelementtype "^2.2.0"
domhandler "^4.2.0"
dwv@^0.31.0:
version "0.31.0"
resolved "https://registry.npmjs.org/dwv/-/dwv-0.31.0.tgz#c9777291774d49225ba0c1b8020d104f09312ebf"
integrity sha512-c/9cmBdPHtH2l2YEacMLdkqpzXTx0rfcof70ujvR2XhDC/IVgGuXUkM1+ZJKvfFyQowditTciif938ib3SB9vQ==
dependencies:
jszip "~3.7.0"
konva "~8.3.0"
magic-wand-tool "~1.1.7"
ecc-jsbn@~0.1.1:
version "0.1.2"
resolved "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9"
@ -4743,6 +4752,16 @@ jszip@^3.1.3:
readable-stream "~2.3.6"
setimmediate "^1.0.5"
jszip@~3.7.0:
version "3.7.1"
resolved "https://registry.npmjs.org/jszip/-/jszip-3.7.1.tgz#bd63401221c15625a1228c556ca8a68da6fda3d9"
integrity sha512-ghL0tz1XG9ZEmRMcEN2vt7xabrDdqHHeykgARpmZ0BiIctWxM47Vt63ZO2dnp4QYt/xJVLLy5Zv1l/xRdh2byg==
dependencies:
lie "~3.3.0"
pako "~1.0.2"
readable-stream "~2.3.6"
set-immediate-shim "~1.0.1"
karma-chrome-launcher@~3.1.0:
version "3.1.1"
resolved "https://registry.npmjs.org/karma-chrome-launcher/-/karma-chrome-launcher-3.1.1.tgz#baca9cc071b1562a1db241827257bfe5cab597ea"
@ -4832,6 +4851,11 @@ klona@^2.0.4, klona@^2.0.5:
resolved "https://registry.npmjs.org/klona/-/klona-2.0.6.tgz#85bffbf819c03b2f53270412420a4555ef882e22"
integrity sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==
konva@~8.3.0:
version "8.3.14"
resolved "https://registry.npmjs.org/konva/-/konva-8.3.14.tgz#691ecb6f4568c58818359af369f03e7438ea3640"
integrity sha512-6I/TZppgY3Frs//AvZ87YVQLFxLywitb8wLS3qMM+Ih9e4QcB5Yy8br6eq7DdUzxPdbsYTz1FQBHzNxs08M1Tw==
less-loader@11.0.0:
version "11.0.0"
resolved "https://registry.npmjs.org/less-loader/-/less-loader-11.0.0.tgz#a31b2bc5cdfb62f1c7de9b2d01cd944c22b1a024"
@ -4973,6 +4997,11 @@ magic-string@^0.26.0:
dependencies:
sourcemap-codec "^1.4.8"
magic-wand-tool@~1.1.7:
version "1.1.7"
resolved "https://registry.npmjs.org/magic-wand-tool/-/magic-wand-tool-1.1.7.tgz#c1d49c5f26307bec06eb8c6ac51e03fff5b9d61b"
integrity sha512-S4rHzCs/bAp7nhQGKeg+McWuqrdyZKpnu8Ahd8AU7NzuLTm/Hh8tkpv1tW91Kmm59foIrXzip1d+P9NDoyxrZA==
make-dir@^2.1.0:
version "2.1.0"
resolved "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5"
@ -6698,6 +6727,11 @@ set-blocking@^2.0.0:
resolved "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
integrity sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==
set-immediate-shim@~1.0.1:
version "1.0.1"
resolved "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz#4b2b1b27eb808a9f8dcc481a58e5e56f599f3f61"
integrity sha512-Li5AOqrZWCVA2n5kryzEmqai6bKSIvpz5oUJHPVj6+dsbD3X1ixtsY5tEnsaNpH3pFAHmG8eIHUrtEtohrg+UQ==
setimmediate@^1.0.5:
version "1.0.5"
resolved "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285"

4
go.mod
View File

@ -5,7 +5,8 @@ go 1.18
require (
github.com/analogj/go-util v0.0.0-20210417161720-39b497cca03b
github.com/dominikbraun/graph v0.15.0
github.com/fastenhealth/fasten-sources v0.1.2
github.com/fastenhealth/fasten-sources v0.1.7
github.com/fastenhealth/gofhir-models v0.0.4
github.com/gin-gonic/gin v1.8.1
github.com/glebarez/sqlite v1.5.0
github.com/golang-jwt/jwt/v4 v4.4.2
@ -25,7 +26,6 @@ require (
require (
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/fastenhealth/gofhir-models v0.0.4 // indirect
github.com/fatih/color v1.13.0 // indirect
github.com/fsnotify/fsnotify v1.5.4 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect

4
go.sum
View File

@ -74,8 +74,8 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m
github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fastenhealth/fasten-sources v0.1.2 h1:VW+gNggUeX+SrqYsWleaiYlCRNItpu4F3Bml1g5UVWQ=
github.com/fastenhealth/fasten-sources v0.1.2/go.mod h1:OYNf47aUBNP2J0kPkrm1X9Mq+30v92yDBrRy4nqPNEU=
github.com/fastenhealth/fasten-sources v0.1.7 h1:KcLNfsCd/ujldAXHsbJErxXfM+llMv8r9D+o53pP9kg=
github.com/fastenhealth/fasten-sources v0.1.7/go.mod h1:OYNf47aUBNP2J0kPkrm1X9Mq+30v92yDBrRy4nqPNEU=
github.com/fastenhealth/gofhir-models v0.0.4 h1:Q//StwNXGfK+WAS2DckGBHAP1R4cHMRZEF/sLGgmR04=
github.com/fastenhealth/gofhir-models v0.0.4/go.mod h1:xB8ikGxu3bUq2b1JYV+CZpHqBaLXpOizFR0eFBCunis=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=