Vitals Interpretation for FHIR (#4528)
[openemr.git] / src / Services / FHIR / Observation / FhirObservationVitalsService.php
blob358fc8f3e4f9f19819f588864a58a485ad98d743
1 <?php
3 /**
4 * FhirObservationVitalsService.php
5 * @package openemr
6 * @link http://www.open-emr.org
7 * @author Stephen Nielson <stephen@nielson.org>
8 * @copyright Copyright (c) 2021 Stephen Nielson <stephen@nielson.org>
9 * @license https://github.com/openemr/openemr/blob/master/LICENSE GNU General Public License 3
12 namespace OpenEMR\Services\FHIR\Observation;
14 use OpenEMR\Common\Logging\SystemLogger;
15 use OpenEMR\Common\Uuid\UuidMapping;
16 use OpenEMR\Common\Uuid\UuidRegistry;
17 use OpenEMR\FHIR\R4\FHIRDomainResource\FHIRObservation;
18 use OpenEMR\FHIR\R4\FHIRElement\FHIRCodeableConcept;
19 use OpenEMR\FHIR\R4\FHIRElement\FHIRCoding;
20 use OpenEMR\FHIR\R4\FHIRElement\FHIRId;
21 use OpenEMR\FHIR\R4\FHIRElement\FHIRMeta;
22 use OpenEMR\FHIR\R4\FHIRElement\FHIRQuantity;
23 use OpenEMR\FHIR\R4\FHIRResource\FHIRObservation\FHIRObservationComponent;
24 use OpenEMR\Services\FHIR\FhirCodeSystemConstants;
25 use OpenEMR\Services\FHIR\FhirProvenanceService;
26 use OpenEMR\Services\FHIR\FhirServiceBase;
27 use OpenEMR\Services\FHIR\IPatientCompartmentResourceService;
28 use OpenEMR\Services\FHIR\UtilsService;
29 use OpenEMR\Services\Search\FhirSearchParameterDefinition;
30 use OpenEMR\Services\Search\SearchFieldException;
31 use OpenEMR\Services\Search\SearchFieldType;
32 use OpenEMR\Services\Search\ServiceField;
33 use OpenEMR\Services\Search\TokenSearchField;
34 use OpenEMR\Services\VitalsService;
35 use OpenEMR\Validators\ProcessingResult;
37 class FhirObservationVitalsService extends FhirServiceBase implements IPatientCompartmentResourceService
39 // we set this to be 'Final' which has the follow interpretation
40 // 'The observation is complete and there are no further actions needed.'
41 // @see http://hl7.org/fhir/R4/valueset-observation-status.html
42 const VITALS_DEFAULT_OBSERVATION_STATUS = "final";
44 const VITALS_PANEL_LOINC_CODE = '85353-1';
45 /**
46 * @var VitalsService
48 private $service;
50 const CATEGORY = "vital-signs";
52 const COLUMN_MAPPINGS = [
53 // @see http://hl7.org/fhir/R4/observation-vitalsigns.html
54 self::VITALS_PANEL_LOINC_CODE => [
55 // this code contains a lot of the other vital sign codes and is treated specially in this service.
56 'fullcode' => 'LOINC:' . self::VITALS_PANEL_LOINC_CODE
57 ,'code' => self::VITALS_PANEL_LOINC_CODE
58 ,'description' => 'Vital signs, weight, height, head circumference, oxygen saturation and BMI panel'
59 ,'column' => ''
60 ,'in_vitals_panel' => false
62 '9279-1' => [
63 'fullcode' => 'LOINC:9279-1'
64 ,'code' => '9279-1'
65 ,'description' => 'Respiratory Rate'
66 ,'column' => ['respiration', 'respiration_unit', 'respiration_interpretation_code', 'respiration_interpretation_title']
67 ,'in_vitals_panel' => true
69 ,'8867-4' => [
70 'fullcode' => 'LOINC:8867-4'
71 ,'code' => '8867-4'
72 ,'description' => 'Heart rate'
73 ,'column' => ['pulse', 'pulse_unit', 'pulse_interpretation_code', 'pulse_interpretation_title']
74 ,'in_vitals_panel' => true
76 ,'2708-6' => [
77 'fullcode' => 'LOINC:2708-6'
78 ,'code' => '2708-6'
79 ,'description' => 'Oxygen saturation in Arterial blood'
80 ,'column' => ['oxygen_saturation', 'oxygen_saturation_unit', 'oxygen_saturation_interpretation_code', 'oxygen_saturation_interpretation_title']
81 ,'in_vitals_panel' => true
83 ,'59408-5' => [
84 'fullcode' => 'LOINC:59408-5',
85 'code' => '59408-5',
86 'description' => 'Oxygen saturation in Arterial blood by Pulse oximetry',
87 'column' => ['oxygen_saturation', 'oxygen_saturation_unit', 'oxygen_flow_rate', 'oxygen_flow_rate_unit', '_interpretation_code', '_interpretation_title'],
88 'in_vitals_panel' => true
90 ,'3151-8' => [
91 'fullcode' => 'LOINC:3151-8'
92 ,'code' => '3151-8',
93 'description' => 'Inhaled oxygen flow rate',
94 'column' => ['oxygen_flow_rate', 'oxygen_flow_rate_unit', 'oxygen_flow_rate_interpretation_code', 'oxygen_flow_rate_interpretation_title'],
95 'in_vitals_panel' => true
97 ,'8310-5' => [
98 'fullcode' => 'LOINC:8310-5',
99 'code' => '8310-5',
100 'description' => 'Body Temperature',
101 'column' => ['temperature', 'temperature_unit', 'temperature_interpretation_code', 'temperature_interpretation_title'],
102 'in_vitals_panel' => true
104 ,'8327-9' => [
105 'fullcode' => 'LOINC:8327-9',
106 'code' => '8327-9',
107 'description' => 'Temperature Location',
108 'column' => 'temp_method',
109 'in_vitals_panel' => true
111 ,'8302-2' => [
112 'fullcode' => 'LOINC:8302-2',
113 'code' => '8302-2',
114 'description' => 'Body height',
115 'column' => ['height', 'height_unit', 'height_interpretation_code', 'height_interpretation_title'],
116 'in_vitals_panel' => true
118 ,'9843-4' => [
119 'fullcode' => 'LOINC:9843-4'
120 ,'code' => '9843-4'
121 ,'description' => 'Head Occipital-frontal circumference'
122 ,'column' => ['head_circ', 'head_circ_unit', 'head_circ_interpretation_code', 'head_circ_interpretation_title']
123 ,'in_vitals_panel' => true
125 ,'29463-7' => [
126 'fullcode' => 'LOINC:29463-7'
127 ,'code' => '29463-7'
128 ,'description' => 'Body weight'
129 ,'column' => ['weight', 'weight_unit', 'weight_interpretation_code', 'weight_interpretation_title']
130 ,'in_vitals_panel' => true
132 ,'39156-5' => [
133 'fullcode' => 'LOINC:39156-5'
134 ,'code' => '39156-5'
135 ,'description' => 'Body mass index (BMI) [Ratio]'
136 ,'column' => ['BMI', 'BMI_status', 'BMI_unit', 'BMI_interpretation_code', 'BMI_interpretation_title']
137 ,'in_vitals_panel' => true
139 ,'85354-9' => [
140 'fullcode' => 'LOINC:85354-9'
141 ,'code' => '85354-9'
142 ,'description' => 'Blood pressure systolic and diastolic'
143 // we hack this a bit to make it work by having our systolic and diastolic together
144 ,'column' => ['bps', 'bps_unit', 'bpd', 'bpd_unit', 'bps_interpretation_code'
145 , 'bps_interpretation_title', 'bpd_interpretation_code', 'bpd_interpretation_title']
146 ,'in_vitals_panel' => true
148 ,'8480-6' => [
149 'fullcode' => 'LOINC:8480-6'
150 ,'code' => '8480-6'
151 ,'description' => 'Systolic blood pressure'
152 // we hack this a bit to make it work by having our systolic and diastolic together
153 ,'column' => ['bps', 'bps_unit', 'bps_interpretation_code', 'bps_interpretation_title']
154 ,'in_vitals_panel' => true
156 ,'8462-4' => [
157 'fullcode' => 'LOINC:8462-4'
158 ,'code' => '8462-4'
159 ,'description' => 'Diastolic blood pressure'
160 // we hack this a bit to make it work by having our systolic and diastolic together
161 ,'column' => ['bpd', 'bpd_unit', 'bpd_interpretation_code', 'bpd_interpretation_title']
162 ,'in_vitals_panel' => true
167 // pediatric profiles are different...
168 // need pediatric BMI
169 // need pediatric head-occipetal
170 // TODO: @adunsulag figure out where these values come from...
172 // Birth - 36 months @see https://www.cdc.gov/growthcharts/html_charts/hcageinf.htm
173 // @see
174 ,'8289-1' => [
175 'fullcode' => 'LOINC:8289-1'
176 ,'code' => '8289-1'
177 ,'description' => 'Head Occipital-frontal circumference Percentile'
178 ,'column' => ['ped_head_circ', 'ped_head_circ_unit', 'ped_head_circ_interpretation_code', 'ped_head_circ_interpretation_title']
179 ,'in_vitals_panel' => false
181 // 2-20yr @see https://www.cdc.gov/growthcharts/html_charts/bmiagerev.htm
182 ,'59576-9' => [
183 'fullcode' => 'LOINC:59576-9'
184 ,'code' => '59576-9'
185 ,'description' => 'Body mass index (BMI) [Percentile] Per age and sex'
186 ,'column' => ['ped_bmi', 'ped_bmi_unit', 'ped_bmi_interpretation_code', 'ped_bmi_interpretation_title']
187 ,'in_vitals_panel' => false
189 // @see https://www.cdc.gov/growthcharts/html_charts/wtstat.htm
190 // grab min(height) and find where weight <= 50
191 // could do this with 3 columns representing height, weight & %
192 // height, weight, %
193 // select % WHERE height <= usrheight & weight <= usrweight ORDER BY height DESC, weight DESC LIMIT 1
194 ,'77606-2' => [
195 'fullcode' => 'LOINC:77606-2'
196 ,'code' => '77606-2'
197 ,'description' => 'Weight-for-length Per age and sex'
198 ,'column' => ['ped_weight_height', 'ped_weight_height_unit', 'ped_weight_height_interpretation_code', 'ped_weight_height_interpretation_title']
199 ,'in_vitals_panel' => false
201 // need pediatric weight for height observations...
204 public function __construct($fhirApiURL = null)
206 parent::__construct($fhirApiURL);
207 $this->service = new VitalsService();
208 $this->populateResourceMappingUuidsForAllVitals();
211 public function getResourcePathForCode($code)
213 return "category=" . self::CATEGORY . "&code=" . $code;
215 public function getCodeFromResourcePath($resourcePath)
217 $query_vars = [];
218 parse_str($resourcePath, $query_vars);
219 return $query_vars['code'] ?? null;
222 public function populateResourceMappingUuidsForAllVitals()
224 $resourcePathList = [];
225 foreach (self::COLUMN_MAPPINGS as $column => $mapping) {
226 // TODO: @adunsulag make this a single function call so we can be more effecient
227 // $resourcePathList[] = "category=vital-signs&code=" . $mapping['code'];
228 $resourcePath = $this->getResourcePathForCode($mapping['code']);
229 UuidMapping::createMissingResourceUuids('Observation', 'form_vitals', $resourcePath);
233 public function supportsCategory($category)
235 return ($category === self::CATEGORY);
238 public function supportsCode($code)
240 return array_search($code, array_keys(self::COLUMN_MAPPINGS)) !== false;
245 * Returns an array mapping FHIR Resource search parameters to OpenEMR search parameters
247 protected function loadSearchParameters()
249 return [
250 'patient' => $this->getPatientContextSearchField(),
251 'code' => new FhirSearchParameterDefinition('code', SearchFieldType::TOKEN, ['code']),
252 'category' => new FhirSearchParameterDefinition('category', SearchFieldType::TOKEN, ['category']),
253 'date' => new FhirSearchParameterDefinition('date', SearchFieldType::DATETIME, ['date']),
254 '_id' => new FhirSearchParameterDefinition('_id', SearchFieldType::TOKEN, [new ServiceField('uuid', ServiceField::TYPE_UUID)]),
260 * Inserts an OpenEMR record into the sytem.
261 * @return The OpenEMR processing result.
263 protected function insertOpenEMRRecord($openEmrRecord)
265 // TODO: Implement insertOpenEMRRecord() method.
269 * Updates an existing OpenEMR record.
270 * @param $fhirResourceId The OpenEMR record's FHIR Resource ID.
271 * @param $updatedOpenEMRRecord The "updated" OpenEMR record.
272 * @return The OpenEMR Service Result
274 protected function updateOpenEMRRecord($fhirResourceId, $updatedOpenEMRRecord)
276 // TODO: Implement updateOpenEMRRecord() method.
280 * Searches for OpenEMR records using OpenEMR search parameters
281 * @param openEMRSearchParameters OpenEMR search fields
282 * @param $puuidBind - Optional variable to only allow visibility of the patient with this puuid.
283 * @return OpenEMR records
285 protected function searchForOpenEMRRecords($openEMRSearchParameters): ProcessingResult
287 $processingResult = new ProcessingResult();
289 try {
290 $observationCodesToReturn = [];
292 if (isset($openEMRSearchParameters['category']) && $openEMRSearchParameters['category'] instanceof TokenSearchField) {
293 if (!$openEMRSearchParameters['category']->hasCodeValue(self::CATEGORY)) {
294 throw new SearchFieldException("category", "invalid value");
296 // we only support one category and then we remove it.
297 unset($openEMRSearchParameters['category']);
300 if (isset($openEMRSearchParameters['code'])) {
302 * @var TokenSearchField
304 $code = $openEMRSearchParameters['code'];
305 if (!($code instanceof TokenSearchField)) {
306 throw new SearchFieldException('code', "Invalid code");
308 foreach ($code->getValues() as $value) {
309 $code = $value->getCode();
310 $observationCodesToReturn[$code] = $code;
312 unset($openEMRSearchParameters['code']);
316 if (empty($observationCodesToReturn)) {
317 // grab everything
318 $observationCodesToReturn = array_keys(self::COLUMN_MAPPINGS);
319 $observationCodesToReturn = array_combine($observationCodesToReturn, $observationCodesToReturn);
322 // convert vital sign records from 1:many
324 $result = $this->service->search($openEMRSearchParameters, true);
325 $data = $result->getData() ?? [];
327 // need to transform these into something we can consume
328 foreach ($data as $record) {
329 // each vital record becomes a 1 -> many record for our observations
330 $this->parseVitalsIntoObservationRecords($processingResult, $record, $observationCodesToReturn);
332 } catch (SearchFieldException $exception) {
333 $processingResult->setValidationMessages([$exception->getField() => $exception->getMessage()]);
336 return $processingResult;
339 private function parseVitalsIntoObservationRecords(ProcessingResult $processingResult, $record, $observationCodesToReturn)
341 $uuidMappings = $this->getVitalSignsUuidMappings(UuidRegistry::uuidToBytes($record['uuid']));
342 // convert each record into it's own openEMR record array
344 if (!empty($observationCodesToReturn[self::VITALS_PANEL_LOINC_CODE])) {
345 if (!empty($uuidMappings[self::VITALS_PANEL_LOINC_CODE])) {
346 $vitalsRecord = [
347 "code" => self::VITALS_PANEL_LOINC_CODE
348 , "description" => $this->getDescriptionForCode(self::VITALS_PANEL_LOINC_CODE)
349 , "category" => self::CATEGORY
350 , "puuid" => $record['puuid']
351 , "euuid" => $record['euuid']
352 , "members" => []
353 , "uuid" => UuidRegistry::uuidToString($uuidMappings[self::VITALS_PANEL_LOINC_CODE])
354 , "user_uuid" => $record['user_uuid']
355 , "date" => $record['date']
357 foreach ($uuidMappings as $code => $uuid) {
358 if (!$this->isVitalSignPanelCodes($code)) { // we will skip over our vital signs code, and any pediatric stuff
359 continue;
361 $vitalsRecord["members"][$code] = UuidRegistry::uuidToString($uuid);
363 $processingResult->addData($vitalsRecord);
364 unset($observationCodesToReturn[self::VITALS_PANEL_LOINC_CODE]);
365 } else {
366 (new SystemLogger())->error("FhirVitalsService->parseVitalsIntoObservationRecords() Cannot return vitals panel as mapping uuid is missing for code " . self::VITALS_PANEL_LOINC_CODE);
369 foreach ($observationCodesToReturn as $code) {
370 $vitalsRecord = [
371 "code" => $code
372 ,"description" => $this->getDescriptionForCode($code)
373 ,"category" => self::CATEGORY
374 , "puuid" => $record['puuid']
375 , "euuid" => $record['euuid']
376 , "user_uuid" => $record['user_uuid']
377 ,"uuid" => UuidRegistry::uuidToString($uuidMappings[$code])
378 ,"date" => $record['date']
381 $columns = $this->getColumnsForCode($code);
382 $columns[] = 'details'; // make sure to grab our detail columns
383 // if any value of the column is populated we will return that the record has a value.
384 foreach ($columns as $column) {
385 if (isset($record[$column]) && $record[$column] != "") {
386 $vitalsRecord[$column] = $record[$column];
389 $processingResult->addData($vitalsRecord);
393 private function getVitalSignsUuidMappings($uuid)
395 $mappedRecords = UuidMapping::getMappedRecordsForTableUUID($uuid);
396 $codeMappings = [];
397 if (!empty($mappedRecords)) {
398 foreach ($mappedRecords as $record) {
399 $resourcePath = $record['resource_path'] ?? '';
400 $code = $this->getCodeFromResourcePath($resourcePath);
401 if (empty($code)) {
402 // TODO: @adunsulag handle this exception
403 continue;
405 $codeMappings[$code] = $record['uuid'];
408 return $codeMappings;
413 * Parses a FHIR Resource, returning the equivalent OpenEMR record.
415 * @param $fhirResource The source FHIR resource
416 * @return a mapped OpenEMR data record (array)
418 public function parseFhirResource($fhirResource = array())
424 * Parses an OpenEMR data record, returning the equivalent FHIR Resource
426 * @param $dataRecord The source OpenEMR data record
427 * @param $encode Indicates if the returned resource is encoded into a string. Defaults to True.
428 * @return the FHIR Resource. Returned format is defined using $encode parameter.
430 public function parseOpenEMRRecord($dataRecord = array(), $encode = false)
432 $observation = new FHIRObservation();
433 $meta = new FHIRMeta();
434 $meta->setVersionId('1');
435 $meta->setLastUpdated(gmdate('c'));
436 $observation->setMeta($meta);
438 $id = new FHIRId();
439 $id->setValue($dataRecord['uuid']);
440 $observation->setId($id);
442 if (!empty($dataRecord['date'])) {
443 $observation->setEffectiveDateTime(gmdate('c', strtotime($dataRecord['date'])));
444 } else {
445 $observation->setEffectiveDateTime(UtilsService::createDataMissingExtension());
448 $code = $dataRecord['code'];
449 $description = $this->getDescriptionForCode($code);
451 $categoryCoding = new FHIRCoding();
452 $categoryCode = new FHIRCodeableConcept();
453 if (!empty($dataRecord['code'])) {
454 $categoryCoding->setCode($dataRecord['code']);
455 $categoryCoding->setDisplay($description);
456 $categoryCoding->setSystem(FhirCodeSystemConstants::LOINC);
457 $categoryCode->addCoding($categoryCoding);
458 $observation->setCode($categoryCode);
462 $observation->setStatus(self::VITALS_DEFAULT_OBSERVATION_STATUS);
464 // TODO: @adunsulag if our provenance needs to be more detailed we can use performer to set the user
466 $obsConcept = new FHIRCodeableConcept();
467 $obsCategoryCoding = new FhirCoding();
468 $obsCategoryCoding->setSystem(FhirCodeSystemConstants::HL7_OBSERVATION_CATEGORY);
469 $obsCategoryCoding->setCode($dataRecord['category']);
470 $obsConcept->addCoding($obsCategoryCoding);
471 $observation->addCategory($obsConcept);
473 $observation->setSubject(UtilsService::createRelativeReference("Patient", $dataRecord['puuid']));
475 if (!empty($dataRecord['notes'])) {
476 $observation->addNote(['text' => $dataRecord['notes']]);
479 $basic_codes = [
480 "9279-1" => 'respiration', "8867-4" => 'pulse', '2708-6' => 'oxygen_saturation', '8310-5' => 'temperature'
481 ,'8302-2' => 'height', '9843-4' => 'head_circ', '29463-7' => 'weight', '39156-5' => 'BMI'
482 ,'59576-9' => 'ped_bmi', '8289-1' => 'ped_head_circ', '77606-2' => 'ped_weight_height'
485 if (isset($basic_codes[$code])) {
486 $this->populateBasicQuantityObservation($basic_codes[$code], $observation, $dataRecord);
488 // more complicated codes
489 switch ($code) {
490 case self::VITALS_PANEL_LOINC_CODE: // vital-signs panel
491 $this->populateVitalSignsPanelObservation($observation, $dataRecord);
492 break;
493 case '8327-9':
494 $this->populateBodyTemperatureLocation($observation, $dataRecord);
495 break;
496 case '85354-9': // blood pressure panel that includes systolic & diastolic pressure
497 $this->populateBloodPressurePanel($observation, $dataRecord);
498 break;
499 case '8480-6':
500 $this->populateComponentColumn(
501 $observation,
502 $dataRecord,
503 'bps',
504 '8480-6',
505 $this->getDescriptionForCode('8480-6')
507 break;
508 case '8462-4':
509 $this->populateComponentColumn(
510 $observation,
511 $dataRecord,
512 'bpd',
513 '8462-4',
514 $this->getDescriptionForCode('8462-4')
516 break;
517 case '2708-6':
518 $this->populateCoding($observation, '59408-5');
519 break;
520 case '59408-5':
521 $this->populatePulseOximetryObservation($observation, $dataRecord);
522 break;
524 return $observation;
527 private function getColumnsForCode($code)
529 $codeMapping = self::COLUMN_MAPPINGS[$code] ?? null;
530 if (isset($codeMapping)) {
531 return is_array($codeMapping['column']) ? $codeMapping['column'] : [$codeMapping['column']];
533 return [];
536 private function getDescriptionForCode($code)
538 $codeMapping = self::COLUMN_MAPPINGS[$code] ?? null;
539 if (isset($codeMapping)) {
540 return $codeMapping['description'];
542 return "";
545 private function populateCoding(FHIRObservation $observation, $code)
547 // add additional oxygen-saturation coding
548 $oxSaturation = new FHIRCoding();
549 $oxSaturation->setCode($code);
550 $oxSaturation->setDisplay($this->getDescriptionForCode($code));
551 $oxSaturation->setSystem(FhirCodeSystemConstants::LOINC);
553 $observation->getCode()->addCoding($oxSaturation);
556 private function populatePulseOximetryObservation(FHIRObservation $observation, $dataRecord)
558 $this->populateCoding($observation, '2708-6');
559 if (
560 $this->columnHasPositiveFloatValue('oxygen_flow_rate', $dataRecord)
561 || $this->columnHasPositiveFloatValue('oxygen_saturation', $dataRecord)
563 $this->populateComponentColumn(
564 $observation,
565 $dataRecord,
566 'oxygen_flow_rate',
567 '3151-8',
568 $this->getDescriptionForCode('3151-8')
570 $this->populateComponentColumn(
571 $observation,
572 $dataRecord,
573 'oxygen_saturation',
574 '3150-0',
575 // only place this is used.
576 'Oxygen saturation in Arterial blood'
578 } else {
579 $observation->setDataAbsentReason(UtilsService::createDataAbsentUnknownCodeableConcept());
583 private function populateVitalSignsPanelObservation(FHIRObservation $observation, $record)
585 if (!empty($record['members'])) {
586 foreach ($record['members'] as $code => $uuid) {
587 $reference = UtilsService::createRelativeReference("Observation", $uuid);
588 $reference->setDisplay($this->getDescriptionForCode($code));
589 $observation->addHasMember($reference);
594 private function isVitalSignPanelCodes($code)
596 $codeMapping = self::COLUMN_MAPPINGS[$code] ?? null;
597 if (isset($codeMapping)) {
598 return $codeMapping['in_vitals_panel'];
600 return false;
603 private function populateBasicQuantityObservation($column, FHIRObservation $observation, $record)
605 $quantity = $this->getFHIRQuantityForColumn($column, $record);
606 if ($quantity != null) {
607 $observation->setValueQuantity($quantity);
608 } else {
609 $observation->setDataAbsentReason(UtilsService::createDataAbsentUnknownCodeableConcept());
612 if (isset($record['details'][$column])) {
613 $observation->addInterpretation($this->getInterpretationForColumn($record, $column));
617 private function getInterpretationForColumn($record, $column): ?FHIRCodeableConcept
619 if (isset($record['details'][$column])) {
620 $code = $record['details'][$column]['interpretation_codes'];
621 $text = $record['details'][$column]['interpretation_title'];
622 return UtilsService::createCodeableConcept([$code => $text], FhirCodeSystemConstants::HL7_V3_OBSERVATION_INTERPRETATION);
624 return null;
627 private function getFHIRQuantityForColumn($column, $record)
629 if ($this->columnHasPositiveFloatValue($column, $record)) {
630 $quantity = new FHIRQuantity();
631 $quantity->setValue(floatval($record[$column]));
632 $quantity->setSystem(FhirCodeSystemConstants::UNITS_OF_MEASURE);
633 $unit = $record[$column . '_unit'] ?? null;
634 $code = $unit;
635 // @see http://hl7.org/fhir/R4/observation-vitalsigns.html for the codes on this
636 if ($unit === 'in') {
637 $unit = 'in_i';
638 $code = "[" . $unit . "]";
639 } else if ($unit === 'lb') {
640 $unit = 'lb_av';
641 $code = "[" . $unit . "]";
642 } else if ($unit === 'degF') {
643 $code = "[" . $unit . "]";
645 $quantity->setCode($code);
646 $quantity->setUnit($unit);
647 return $quantity;
649 return null;
652 private function columnHasPositiveFloatValue($column, $record)
654 return (isset($record[$column]) && floatval($record[$column]) > 0.00);
657 private function populateBloodPressurePanel(FHIRObservation $observation, $dataRecord)
659 $this->populateComponentColumn(
660 $observation,
661 $dataRecord,
662 'bps',
663 '8480-6',
664 $this->getDescriptionForCode('8480-6')
666 $this->populateComponentColumn(
667 $observation,
668 $dataRecord,
669 'bpd',
670 '8462-4',
671 $this->getDescriptionForCode('8462-4')
675 private function populateComponentColumn(FHIRObservation $observation, $dataRecord, $column, $code, $description)
677 $component = new FHIRObservationComponent();
678 $coding = UtilsService::createCodeableConcept([$code => xlt($description)], FhirCodeSystemConstants::LOINC);
679 $component->setCode($coding);
680 $quantity = $this->getFHIRQuantityForColumn($column, $dataRecord);
681 if ($quantity != null) {
682 $component->setValueQuantity($quantity);
683 } else {
684 $component->setDataAbsentReason(UtilsService::createDataAbsentUnknownCodeableConcept());
686 if (isset($dataRecord['details'][$column])) {
687 $component->addInterpretation($this->getInterpretationForColumn($dataRecord, $column));
689 $observation->addComponent($component);
692 private function populateBodyTemperatureLocation(FHIRObservation $observation, $record)
694 // no guidance on how to pass this on, so we are using the value string to pass this on.
695 $observation->setValueString($record['temp_method']);
699 * Creates the Provenance resource for the equivalent FHIR Resource
701 * @param $dataRecord The source OpenEMR data record
702 * @param $encode Indicates if the returned resource is encoded into a string. Defaults to True.
703 * @return the FHIR Resource. Returned format is defined using $encode parameter.
705 public function createProvenanceResource($dataRecord, $encode = false)
707 if (!($dataRecord instanceof FHIRObservation)) {
708 throw new \BadMethodCallException("Data record should be correct instance class");
710 $fhirProvenanceService = new FhirProvenanceService();
711 $fhirProvenance = $fhirProvenanceService->createProvenanceForDomainResource($dataRecord);
712 if ($encode) {
713 return json_encode($fhirProvenance);
714 } else {
715 return $fhirProvenance;
719 public function getPatientContextSearchField(): FhirSearchParameterDefinition
721 return new FhirSearchParameterDefinition('patient', SearchFieldType::REFERENCE, [new ServiceField('puuid', ServiceField::TYPE_UUID)]);