Skip to content

Commit fa75f60

Browse files
authored
Merge pull request #50 from the-commons-project/develop
Develop to Main
2 parents 72dbb7b + ce026b6 commit fa75f60

10 files changed

+221
-14
lines changed

README.md

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,3 @@ You can accomplish this by using the following commands. Note that this should b
100100
```
101101
git tag v0.4.4-dev2
102102
```
103-
104-
# TO DO:
105-
* Add github workflows for production deployment

public/captureQR.html

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,30 +4,39 @@
44

55
<title>SHC Reader Capture Window</title>
66
<script src='qr-scanner.umd.min.js'></script>
7+
<script src="switchCamera.js"></script>
78

89
</head>
910
<body>
1011

1112
<video id='video' style='width: 400px; height: 225px;'></video>
13+
<div id='switchCamera' style='display: none; margin-top: 8px;'>
14+
<button onclick='sc.switchCameraClick()'>Switch Camera</button>
15+
</div>
1216

1317
<script>
1418

1519
const handleQR = (result) => {
1620
window.opener.openCameraResult(result.data);
1721
window.close();
1822
}
23+
24+
const hash = window.location.hash;
25+
const defaultMode = (hash ? hash.substring(1) : 'environment');
1926

2027
const qrScanner = new QrScanner(
2128
document.getElementById('video'),
2229
handleQR,
2330
{
24-
preferredCamera: 'user',
31+
preferredCamera: sc.getSelectedCamera(defaultMode),
2532
highlightScanRegion: true,
2633
highlightCodeOutline: true,
2734
returnDetailedScanResult: true
2835
});
2936

30-
qrScanner.start();
37+
qrScanner.start().then(() => {
38+
sc.maybeShowSwitchCamera(qrScanner, 'switchCamera');
39+
});
3140

3241
</script>
3342

public/index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
66
<meta name="viewport" content="width=device-width, initial-scale=1" />
77
<meta name="description" content="Web site created using create-react-app" />
8+
<script type="text/javascript" src="%PUBLIC_URL%/switchCamera.js"></script>
89
<!--
910
Notice the use of %PUBLIC_URL% in the tags above.
1011
It will be replaced with the URL of the `public` folder during the build.

public/switchCamera.js

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
2+
// +--------------------------+
3+
// | sc.maybeShowSwitchCamera |
4+
// +--------------------------+
5+
6+
async function maybeShowSwitchCamera(qrScanner, divId) {
7+
8+
const camIds = await listCameraIds();
9+
if (camIds.length === 1) return;
10+
11+
window.sc._scanner = qrScanner;
12+
document.getElementById(divId).style.display = 'block';
13+
}
14+
15+
// +--------------------+
16+
// | getSelectedCamera |
17+
// | saveSelectedCamera |
18+
// +--------------------+
19+
20+
function getSelectedCamera(defaultIdMode) {
21+
const cached = localStorage.getItem('cameraIdMode');
22+
return(cached || defaultIdMode);
23+
}
24+
25+
function saveSelectedCamera(cameraIdMode) {
26+
try { localStorage.setItem('cameraIdMode', cameraIdMode); }
27+
catch (err) { console.error(err.toString()); }
28+
}
29+
30+
// +---------------------------+
31+
// | switchCameraClick |
32+
// | switchCameraClickInternal |
33+
// +---------------------------+
34+
35+
// deal with double-clickness and defer actual work to switchCameraAction
36+
37+
let switchTimer = undefined;
38+
const dblClickMillis = 250;
39+
40+
async function switchCameraClick() {
41+
switchCameraClickInternal(false);
42+
}
43+
44+
async function switchCameraClickInternal(isTimer) {
45+
46+
if (isTimer) {
47+
// double-click timeout --- single
48+
switchTimer = undefined;
49+
await switchCameraAction(false);
50+
}
51+
else if (switchTimer) {
52+
// second click within period --- double
53+
clearTimeout(switchTimer);
54+
switchTimer = undefined;
55+
await switchCameraAction(true);
56+
}
57+
else {
58+
// first click --- set timer
59+
switchTimer = setTimeout(() => {
60+
switchCameraClickInternal(true);
61+
}, dblClickMillis);
62+
}
63+
}
64+
65+
// +-------------------+
66+
// | switchCamerAction |
67+
// +-------------------+
68+
69+
async function switchCameraAction(isDouble) {
70+
71+
let currentCam = getSelectedCamera();
72+
let newCam = undefined;
73+
74+
if (isDouble) {
75+
// switch by id
76+
if (isFacingMode(currentCam)) currentCam = findCurrentCam(true);
77+
newCam = await findNextCameraId(currentCam);
78+
}
79+
else {
80+
// switch by mode
81+
if (!isFacingMode(currentCam)) currentCam = findCurrentCam(false);
82+
newCam = (currentCam === 'user' ? 'environment' : 'user');
83+
}
84+
85+
console.log(`Switching camera from ${currentCam} to ${newCam}`);
86+
87+
saveSelectedCamera(newCam);
88+
window.sc._scanner.setCamera(newCam);
89+
}
90+
91+
// +---------+
92+
// | Helpers |
93+
// +---------+
94+
95+
async function listCameraIds() {
96+
97+
const devs = await navigator.mediaDevices.enumerateDevices();
98+
return(devs.filter((d) => (d.kind === 'videoinput')).map((c,i) => c.deviceId));
99+
}
100+
101+
function findCurrentCam(getId) {
102+
103+
const vid = document.getElementById('video');
104+
const track = vid.srcObject.getTracks().find(t => t.kind === 'video');
105+
const settings = track.getSettings();
106+
return(getId ? settings.deviceId : settings.facingMode);
107+
}
108+
109+
async function findNextCameraId(currentId) {
110+
111+
const camIds = await listCameraIds();
112+
113+
let i = 0;
114+
while (i < camIds.length) {
115+
if (camIds[i] === currentId) break;
116+
++i;
117+
}
118+
119+
return(camIds[(i >= (camIds.length - 1)) ? 0 : i + 1]);
120+
}
121+
122+
function isFacingMode(s) {
123+
return(s === 'user' || s === 'environment');
124+
}
125+
126+
// +-----------+
127+
// | "Exports" |
128+
// +-----------+
129+
130+
window.sc = {
131+
'maybeShowSwitchCamera': maybeShowSwitchCamera,
132+
'getSelectedCamera': getSelectedCamera,
133+
'switchCameraClick': switchCameraClick
134+
};
135+

src/PatientSummary.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export default function PatientSummary({ organized, dcr }) {
2727
const comp = organized.byType.Composition[0];
2828
const rmap = organized.byId;
2929

30-
const authors = comp.author.map((a) => futil.renderPerson(a, rmap));
30+
const authors = comp.author.map((a) => futil.renderOrgOrPerson(a, rmap));
3131

3232
return(
3333
<div className={styles.container}>

src/Photo.js

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@ export default function Photo({ viewData }) {
99
const [paused, setPaused] = useState(false);
1010

1111
const openCameraClick = () => {
12+
const cameraIdMode = window.sc.getSelectedCamera(config("cameraIdMode"));
13+
const url = 'captureQR.html#' + escape(cameraIdMode);
1214
window.openCameraResult = openCameraResult;
13-
window.open('captureQR.html', 'captureQR', 'width=500,height=300');
15+
window.open(url, 'captureQR', 'width=500,height=300');
1416
}
1517

1618
const unPauseCameraClick = () => { setPaused(false); }
@@ -22,6 +24,10 @@ export default function Photo({ viewData }) {
2224
viewData(shx);
2325
}
2426

27+
// +-----------+
28+
// | useEffect |
29+
// +-----------+
30+
2531
useEffect(() => {
2632

2733
if (!haveCamera || paused) return;
@@ -30,13 +36,16 @@ export default function Photo({ viewData }) {
3036
document.getElementById('video'),
3137
result => viewData(result.data),
3238
{
33-
preferredCamera: 'user',
39+
preferredCamera: window.sc.getSelectedCamera(config("cameraIdMode")),
3440
highlightScanRegion: true,
3541
highlightCodeOutline: true,
3642
returnDetailedScanResult: true
3743
});
3844

39-
qrScanner.start().catch((err) => {
45+
qrScanner.start().then(() => {
46+
window.sc.maybeShowSwitchCamera(qrScanner, 'switchCamera');
47+
})
48+
.catch((err) => {
4049
console.error(err);
4150
setHaveCamera(false);
4251
});
@@ -52,6 +61,10 @@ export default function Photo({ viewData }) {
5261

5362
}, [haveCamera, setHaveCamera, paused, viewData]);
5463

64+
// +--------+
65+
// | render |
66+
// +--------+
67+
5568
return (
5669
<div>
5770

@@ -64,8 +77,16 @@ export default function Photo({ viewData }) {
6477
</div> }
6578

6679
{ haveCamera &&
67-
<video id='video' style={{ width: '400px', height: '225px' }}></video> }
68-
80+
<>
81+
<video id='video' style={{ width: '400px', height: '225px' }}></video>
82+
<div id='switchCamera' style={{ display: 'none' }}>
83+
<Button variant='text' onClick={ window.sc.switchCameraClick }>
84+
Change Camera
85+
</Button>
86+
</div>
87+
</>
88+
}
89+
6990
{ !haveCamera &&
7091
<Button variant='contained' onClick={openCameraClick}>Open Camera</Button> }
7192

src/lib/SHX.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,8 +138,10 @@ async function _verifySHX(shx, passcode) {
138138

139139
const dir = await getDirectory();
140140

141+
const permissive = config("permissive");
141142
for (const i in resolved.verifiableCredentials) {
142143
const vres = await verify(resolved.verifiableCredentials[i], dir);
144+
if (permissive) bePermissive(vres);
143145
addVerifiableBundle(statusObj, vres);
144146
}
145147
}
@@ -172,6 +174,23 @@ async function getDirectory() {
172174
return(_verifyDir);
173175
}
174176

177+
function bePermissive(vres) {
178+
179+
if (vres.verified) return;
180+
if (!vres.data || !vres.data.errors) return;
181+
182+
let anyFatal = false;
183+
for (const i in vres.data.errors) {
184+
if (vres.data.errors[i].fatal) {
185+
anyFatal = true;
186+
break;
187+
}
188+
}
189+
190+
if (anyFatal) return;
191+
vres.verified = true;
192+
}
193+
175194
// +------------+
176195
// | resolveSHX |
177196
// +------------+

src/lib/defaults.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ export const DEFAULT_CONFIG = {
1616
'https://raw.githubusercontent.com/seanno/shc-demo-data/main/keystore/directory.json'
1717
],
1818

19+
// default camera mode ('environment' or 'user') or ID
20+
"cameraIdMode": 'environment',
21+
1922
// stop camera scanning after this many millis (default 120 seconds).
2023
// this is to work around what appear to be memory leaks in the
2124
// camera module
@@ -28,6 +31,10 @@ export const DEFAULT_CONFIG = {
2831
// in local storage (default 500k characters of serialized JSON)
2932
"terminologyCacheItemCeiling": (1024 * 500),
3033

34+
// allow display of SHCs that have validation errors as long as
35+
// they are not "fatal" ... e.g., fullUrl values not in resource:# format
36+
"permissive": false,
37+
3138
// true = show TCP privacy, disclaimer, etc.
3239
"tcpFooter": true
3340
};

src/lib/fhirTables.js

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -217,8 +217,15 @@ function renderMedXNameJSX(r, rmap, dcr) {
217217
let nameJSX = "Unknown";
218218

219219
if (r.medicationReference) {
220-
const m = rmap[r.medicationReference.reference];
221-
nameJSX = futil.renderCodeableJSX(m.code, dcr);
220+
const ref = r.medicationReference;
221+
const m = rmap[ref.reference];
222+
if (m) {
223+
nameJSX = futil.renderCodeableJSX(m.code, dcr);
224+
}
225+
else {
226+
if (ref.display) nameJSX = ref.display;
227+
console.error(`medicationReference.reference not found: ${ref.reference}`);
228+
}
222229
}
223230
else if (r.medicationCodeableConcept) {
224231
nameJSX = futil.renderCodeableJSX(r.medicationCodeableConcept, dcr);

src/lib/fhirUtil.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ const NA = "Unknown";
33

44
// +--------------------+
55
// | renderOrganization |
6+
// | renderOrgOrPerson | |
67
// +--------------------+
78

89
export function renderOrganization(org, resources) {
@@ -13,6 +14,16 @@ function renderOrganizationResource(org) {
1314
return(<div>{org.name}</div>);
1415
}
1516

17+
export function renderOrgOrPerson(oop, resources) {
18+
19+
const renderMap = {
20+
"Organization": renderOrganization,
21+
"any": renderPerson
22+
};
23+
24+
return(renderReferenceMap(oop, resources, renderMap));
25+
}
26+
1627
// +---------------+
1728
// | renderContact |
1829
// +---------------+
@@ -687,7 +698,7 @@ export function renderReferenceMap(o, resources, refRenderFuncMap) {
687698
return(renderReferenceMapThrow(o, resources, refRenderFuncMap));
688699
}
689700
catch (err) {
690-
return(<div>{o.display ? o.display : NA}</div>);
701+
return(<div>{o && o.display ? o.display : NA}</div>);
691702
}
692703
}
693704

0 commit comments

Comments
 (0)