From 715bb150067d35eb21a8c5c7bb59cda94cc12536 Mon Sep 17 00:00:00 2001 From: Chandrika Singh Jadon Date: Thu, 13 Feb 2025 02:09:20 +0530 Subject: [PATCH 01/34] Enhancements to XML import: Color parsing, polyline handling, independent annotations, and bounding boxes --- apps/port/import.html | 1 + apps/port/xml2geo.js | 54 +++++++++++++++++++++++++++++++------------ 2 files changed, 40 insertions(+), 15 deletions(-) diff --git a/apps/port/import.html b/apps/port/import.html index ecf6114da..8d1eff9a3 100644 --- a/apps/port/import.html +++ b/apps/port/import.html @@ -43,3 +43,4 @@

File Contents

+ diff --git a/apps/port/xml2geo.js b/apps/port/xml2geo.js index 4722df21e..4b8b47fb2 100644 --- a/apps/port/xml2geo.js +++ b/apps/port/xml2geo.js @@ -28,14 +28,16 @@ function xml2geo() { let input = document.getElementById('xml_in').value; xmlDoc = parser.parseFromString(input, 'text/xml'); let regions = xmlDoc.getElementsByTagName('Region'); + for (let i of regions) { - console.log('Looking at Region ID', i.getAttribute('Id')); + let regionId = i.getAttribute('Id'); + let regionType = i.getAttribute('Type') || 'Polygon'; // Default to Polygon if Type is missing + console.log('Processing Region ID:', regionId, 'as', regionType); + let vertices = i.getElementsByTagName('Vertex'); let coordinates = []; - let minX = 99e99; - let maxX = 0; - let minY = 99e99; - let maxY = 0; + let minX = 99e99, maxX = 0, minY = 99e99, maxY = 0; + for (let j of vertices) { let x = parseFloat(j.getAttribute('X')); let y = parseFloat(j.getAttribute('Y')); @@ -45,18 +47,38 @@ function xml2geo() { maxY = Math.max(maxY, y); coordinates.push([x, y]); } - coordinates.push(coordinates[0]); + + // **Detect Polygon vs. Polyline** + if (regionType === 'Polygon') { + coordinates.push(coordinates[0]); // Close the polygon by repeating the first point + } + let boundRect = [[minX, minY], [minX, maxY], [maxX, maxY], [maxX, minY], [minX, minY]]; - let feature = {}; - feature['type'] = 'Feature'; - feature['geometry'] = {}; - feature['geometry']['type'] = 'Polygon'; - feature['geometry']['coordinates'] = [coordinates]; - feature['bound'] = {}; - feature['bound']['type'] = 'Polygon'; - feature['bound']['coordinates'] = [boundRect]; + + // **Detect Color** + let colorValue = i.getAttribute('LineColor'); + let hexColor = colorValue ? `#${parseInt(colorValue).toString(16).padStart(6, '0')}` : '#000000'; + + let feature = { + 'type': 'Feature', + 'geometry': { + 'type': regionType === 'Polyline' ? 'LineString' : 'Polygon', + 'coordinates': [coordinates], + }, + 'properties': { + 'regionId': regionId, + 'lineColor': hexColor, + 'group': i.parentNode.getAttribute('Name') || 'Ungrouped', + }, + 'bound': { + 'type': 'BoundingBox', + 'coordinates': [[minX, minY], [maxX, maxY]], + } + }; + features.push(feature); } + let output = Object.assign({}, template); output['geometries']['features'] = features; output['provenance']['image']['slide'] = document.getElementById('slide_id').value; @@ -64,5 +86,7 @@ function xml2geo() { output['properties']['annotations']['name'] = document.getElementById('annot_name').value; output['provenance']['analysis']['name'] = document.getElementById('annot_name').value; output['provenance']['analysis']['execution_id'] = document.getElementById('annot_name').value; - document.getElementById('output').innerHTML = JSON.stringify(output); + + document.getElementById('output').textContent = JSON.stringify(output); + } From 7b909adfa6ef3f332178143d4cdf2130a45192a2 Mon Sep 17 00:00:00 2001 From: Chandrika Date: Thu, 13 Feb 2025 02:30:52 +0530 Subject: [PATCH 02/34] import.html --- apps/port/import.html | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/port/import.html b/apps/port/import.html index 8d1eff9a3..ecf6114da 100644 --- a/apps/port/import.html +++ b/apps/port/import.html @@ -43,4 +43,3 @@

File Contents

- From 56f1978211a336be3b7052830777a703bd3d38fe Mon Sep 17 00:00:00 2001 From: Chandrika Date: Thu, 13 Feb 2025 02:37:10 +0530 Subject: [PATCH 03/34] xml2geo.js From b20a41eda7ee23f7b1c8ec6e4f3a384a5fba5de6 Mon Sep 17 00:00:00 2001 From: Chandrika Date: Thu, 13 Feb 2025 18:49:32 +0530 Subject: [PATCH 04/34] xml2geo.js --- apps/port/xml2geo.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/apps/port/xml2geo.js b/apps/port/xml2geo.js index 4b8b47fb2..e865fa19a 100644 --- a/apps/port/xml2geo.js +++ b/apps/port/xml2geo.js @@ -36,7 +36,7 @@ function xml2geo() { let vertices = i.getElementsByTagName('Vertex'); let coordinates = []; - let minX = 99e99, maxX = 0, minY = 99e99, maxY = 0; + let minX = 99e99; let maxX = 0; let minY = 99e99; let maxY = 0; for (let j of vertices) { let x = parseFloat(j.getAttribute('X')); @@ -73,7 +73,7 @@ function xml2geo() { 'bound': { 'type': 'BoundingBox', 'coordinates': [[minX, minY], [maxX, maxY]], - } + }, }; features.push(feature); @@ -88,5 +88,4 @@ function xml2geo() { output['provenance']['analysis']['execution_id'] = document.getElementById('annot_name').value; document.getElementById('output').textContent = JSON.stringify(output); - } From 2cb1fb277f76ca0439a9057fd7bee94ad009ae98 Mon Sep 17 00:00:00 2001 From: Chandrika Date: Thu, 13 Feb 2025 19:06:32 +0530 Subject: [PATCH 05/34] Update axe-a11y-check.yml so that ChromeDriver version matches Chrome --- .github/workflows/axe-a11y-check.yml | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/.github/workflows/axe-a11y-check.yml b/.github/workflows/axe-a11y-check.yml index 7c6058049..748014b2e 100644 --- a/.github/workflows/axe-a11y-check.yml +++ b/.github/workflows/axe-a11y-check.yml @@ -20,14 +20,30 @@ jobs: uses: actions/setup-node@v1 with: node-version: ${{ matrix.node-version }} + + # Install Dependencies - run: npm install - run: npm install -g http-server + + # Install Google Chrome + - name: Install Google Chrome + run: | + sudo apt update + sudo apt install -y google-chrome-stable + + # Install the matching ChromeDriver version + - name: Install specific version of ChromeDriver + run: | + CHROME_VERSION=$(google-chrome --version | grep -oP '[0-9]+\.[0-9]+\.[0-9]+') + npm install -g chromedriver@$CHROME_VERSION + + # Start local server - run: npm run build --if-present - run: http-server -s & - - name: Install specific version of ChromeDriver - run: npm install -g chromedriver@125 + + # Run Axe tests with ChromeDriver - name: Run axe run: | npm install -g @axe-core/cli - sleep 90 + sleep 90 # Allow server to start axe http://127.0.0.1:8080 --chromedriver-path $(npm root -g)/chromedriver/bin/chromedriver --exit From be0f02a5a6214a52065e5e8690d4e6f4da087101 Mon Sep 17 00:00:00 2001 From: Chandrika Date: Thu, 13 Feb 2025 19:09:05 +0530 Subject: [PATCH 06/34] axe-a11y-check.yml --- .github/workflows/axe-a11y-check.yml | 22 +++------------------- 1 file changed, 3 insertions(+), 19 deletions(-) diff --git a/.github/workflows/axe-a11y-check.yml b/.github/workflows/axe-a11y-check.yml index 748014b2e..7c6058049 100644 --- a/.github/workflows/axe-a11y-check.yml +++ b/.github/workflows/axe-a11y-check.yml @@ -20,30 +20,14 @@ jobs: uses: actions/setup-node@v1 with: node-version: ${{ matrix.node-version }} - - # Install Dependencies - run: npm install - run: npm install -g http-server - - # Install Google Chrome - - name: Install Google Chrome - run: | - sudo apt update - sudo apt install -y google-chrome-stable - - # Install the matching ChromeDriver version - - name: Install specific version of ChromeDriver - run: | - CHROME_VERSION=$(google-chrome --version | grep -oP '[0-9]+\.[0-9]+\.[0-9]+') - npm install -g chromedriver@$CHROME_VERSION - - # Start local server - run: npm run build --if-present - run: http-server -s & - - # Run Axe tests with ChromeDriver + - name: Install specific version of ChromeDriver + run: npm install -g chromedriver@125 - name: Run axe run: | npm install -g @axe-core/cli - sleep 90 # Allow server to start + sleep 90 axe http://127.0.0.1:8080 --chromedriver-path $(npm root -g)/chromedriver/bin/chromedriver --exit From c44a717f60d76d4c40526fe57aa673ca5a6f3643 Mon Sep 17 00:00:00 2001 From: Birm Date: Mon, 17 Feb 2025 23:28:09 -0500 Subject: [PATCH 07/34] test a11y test as warn not fail --- .github/workflows/axe-a11y-check.yml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/axe-a11y-check.yml b/.github/workflows/axe-a11y-check.yml index 7c6058049..7638cf67e 100644 --- a/.github/workflows/axe-a11y-check.yml +++ b/.github/workflows/axe-a11y-check.yml @@ -1,4 +1,5 @@ name: axe + on: push: branches: @@ -8,6 +9,7 @@ on: branches: - master - develop + jobs: axe: runs-on: ubuntu-latest @@ -27,7 +29,12 @@ jobs: - name: Install specific version of ChromeDriver run: npm install -g chromedriver@125 - name: Run axe + id: axe-test run: | npm install -g @axe-core/cli sleep 90 - axe http://127.0.0.1:8080 --chromedriver-path $(npm root -g)/chromedriver/bin/chromedriver --exit + axe http://127.0.0.1:8080 --chromedriver-path $(npm root -g)/chromedriver/bin/chromedriver --exit || echo "::set-output name=status::warning" + + - name: Set output status to warning (if test fails) + if: steps.axe-test.outputs.status == 'warning' + run: echo "Accessibility tests failed, but this is an aspirational test and does not block the PR." From ec85b22851b4ec79f615de2e37f27f8d65543025 Mon Sep 17 00:00:00 2001 From: Birm Date: Mon, 17 Feb 2025 23:36:36 -0500 Subject: [PATCH 08/34] try to get explicit warning message --- .github/workflows/axe-a11y-check.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/axe-a11y-check.yml b/.github/workflows/axe-a11y-check.yml index 7638cf67e..1e00d27c9 100644 --- a/.github/workflows/axe-a11y-check.yml +++ b/.github/workflows/axe-a11y-check.yml @@ -37,4 +37,4 @@ jobs: - name: Set output status to warning (if test fails) if: steps.axe-test.outputs.status == 'warning' - run: echo "Accessibility tests failed, but this is an aspirational test and does not block the PR." + run: echo "::warning Accessibility tests failed, but this does not block (most) PRs." From 67ced4bdc6a5f51c7849661a6249f290a775b39c Mon Sep 17 00:00:00 2001 From: Birm Date: Mon, 17 Feb 2025 23:56:46 -0500 Subject: [PATCH 09/34] different warning syntax, no set status --- .github/workflows/axe-a11y-check.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/axe-a11y-check.yml b/.github/workflows/axe-a11y-check.yml index 1e00d27c9..5a4d93867 100644 --- a/.github/workflows/axe-a11y-check.yml +++ b/.github/workflows/axe-a11y-check.yml @@ -33,8 +33,8 @@ jobs: run: | npm install -g @axe-core/cli sleep 90 - axe http://127.0.0.1:8080 --chromedriver-path $(npm root -g)/chromedriver/bin/chromedriver --exit || echo "::set-output name=status::warning" + axe http://127.0.0.1:8080 --chromedriver-path $(npm root -g)/chromedriver/bin/chromedriver --exit || echo "status=warning" >> $GITHUB_ENV - name: Set output status to warning (if test fails) - if: steps.axe-test.outputs.status == 'warning' - run: echo "::warning Accessibility tests failed, but this does not block (most) PRs." + if: env.status == 'warning' + run: echo "::warning::Accessibility tests failed, but this does not block (most) PRs." From bb36a1211e7cdd509f69e49d418c9d0481103132 Mon Sep 17 00:00:00 2001 From: Birm Date: Thu, 20 Feb 2025 13:17:13 -0500 Subject: [PATCH 10/34] map aperio types --- apps/port/xml2geo.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/apps/port/xml2geo.js b/apps/port/xml2geo.js index e865fa19a..14e6c40f7 100644 --- a/apps/port/xml2geo.js +++ b/apps/port/xml2geo.js @@ -23,6 +23,13 @@ var template = { }, }; +var aperio_map = { + "0":"Polygon", + "1":"Polygon", + "2":"Polygon", // rectangle but should work?? haven't seen one yet + "4": "LineString" +}; + function xml2geo() { let features = []; let input = document.getElementById('xml_in').value; @@ -31,7 +38,9 @@ function xml2geo() { for (let i of regions) { let regionId = i.getAttribute('Id'); - let regionType = i.getAttribute('Type') || 'Polygon'; // Default to Polygon if Type is missing + let regionType = i.getAttribute('Type') || '0'; + regionType = aperio_map[regionType] || 'Polygon';// Default to Polygon if Type is missing + console.log('Processing Region ID:', regionId, 'as', regionType); let vertices = i.getElementsByTagName('Vertex'); From 79634234eae7fac26929ba9b8d48963488a0deeb Mon Sep 17 00:00:00 2001 From: Birm Date: Thu, 20 Feb 2025 13:24:28 -0500 Subject: [PATCH 11/34] uses polyline not linestring here --- apps/port/xml2geo.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/port/xml2geo.js b/apps/port/xml2geo.js index 14e6c40f7..565e7cf02 100644 --- a/apps/port/xml2geo.js +++ b/apps/port/xml2geo.js @@ -27,7 +27,7 @@ var aperio_map = { "0":"Polygon", "1":"Polygon", "2":"Polygon", // rectangle but should work?? haven't seen one yet - "4": "LineString" + "4": "Polyline" }; function xml2geo() { From 7641106a038ebd2f179668c81b0a93e7e4e6aaa9 Mon Sep 17 00:00:00 2001 From: Birm Date: Thu, 20 Feb 2025 13:54:00 -0500 Subject: [PATCH 12/34] need the polygon not range form for bounding box --- apps/port/xml2geo.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/port/xml2geo.js b/apps/port/xml2geo.js index 565e7cf02..14a1411b2 100644 --- a/apps/port/xml2geo.js +++ b/apps/port/xml2geo.js @@ -81,7 +81,7 @@ function xml2geo() { }, 'bound': { 'type': 'BoundingBox', - 'coordinates': [[minX, minY], [maxX, maxY]], + 'coordinates': [boundRect], }, }; From 44c2476e72685deec269a24f56ef7307622a835e Mon Sep 17 00:00:00 2001 From: Birm Date: Thu, 20 Feb 2025 14:11:39 -0500 Subject: [PATCH 13/34] take color, type from parent annotation for region --- apps/port/xml2geo.js | 95 ++++++++++++++++++++++++-------------------- 1 file changed, 51 insertions(+), 44 deletions(-) diff --git a/apps/port/xml2geo.js b/apps/port/xml2geo.js index 14a1411b2..d903e2e48 100644 --- a/apps/port/xml2geo.js +++ b/apps/port/xml2geo.js @@ -34,58 +34,65 @@ function xml2geo() { let features = []; let input = document.getElementById('xml_in').value; xmlDoc = parser.parseFromString(input, 'text/xml'); - let regions = xmlDoc.getElementsByTagName('Region'); + let annotations = xmlDoc.getElementsByTagName('Annotation'); // Assuming regions are inside 'Annotation' elements + + for (let annotation of annotations) { + let annotationType = annotation.getAttribute('Type') || '0'; // Default to '0' if Type is not provided + let annotationLineColor = annotation.getAttribute('LineColor'); // Get LineColor from the parent annotation + let annotationId = annotation.getAttribute('Id'); + + console.log('Processing Annotation ID:', annotationId, 'with Type:', annotationType); - for (let i of regions) { - let regionId = i.getAttribute('Id'); - let regionType = i.getAttribute('Type') || '0'; - regionType = aperio_map[regionType] || 'Polygon';// Default to Polygon if Type is missing + let regions = annotation.getElementsByTagName('Region'); // Get regions within this annotation + for (let region of regions) { + let regionId = region.getAttribute('Id'); + regionType = annotationType || region.getAttribute('Type'); // parent annotation type if present, else own (odd?) + regionType = aperio_map[regionType] + console.log('Processing Region ID:', regionId, 'as', regionType); - console.log('Processing Region ID:', regionId, 'as', regionType); + let vertices = region.getElementsByTagName('Vertex'); + let coordinates = []; + let minX = 99e99; let maxX = 0; let minY = 99e99; let maxY = 0; - let vertices = i.getElementsByTagName('Vertex'); - let coordinates = []; - let minX = 99e99; let maxX = 0; let minY = 99e99; let maxY = 0; + for (let vertex of vertices) { + let x = parseFloat(vertex.getAttribute('X')); + let y = parseFloat(vertex.getAttribute('Y')); + minX = Math.min(minX, x); + minY = Math.min(minY, y); + maxX = Math.max(maxX, x); + maxY = Math.max(maxY, y); + coordinates.push([x, y]); + } - for (let j of vertices) { - let x = parseFloat(j.getAttribute('X')); - let y = parseFloat(j.getAttribute('Y')); - minX = Math.min(minX, x); - minY = Math.min(minY, y); - maxX = Math.max(maxX, x); - maxY = Math.max(maxY, y); - coordinates.push([x, y]); - } - - // **Detect Polygon vs. Polyline** - if (regionType === 'Polygon') { - coordinates.push(coordinates[0]); // Close the polygon by repeating the first point - } + // **Detect Polygon vs. Polyline** + if (regionType === 'Polygon') { + coordinates.push(coordinates[0]); // Close the polygon by repeating the first point + } - let boundRect = [[minX, minY], [minX, maxY], [maxX, maxY], [maxX, minY], [minX, minY]]; + let boundRect = [[minX, minY], [minX, maxY], [maxX, maxY], [maxX, minY], [minX, minY]]; - // **Detect Color** - let colorValue = i.getAttribute('LineColor'); - let hexColor = colorValue ? `#${parseInt(colorValue).toString(16).padStart(6, '0')}` : '#000000'; + // **Detect Color** + let hexColor = annotationLineColor ? `#${parseInt(annotationLineColor).toString(16).padStart(6, '0')}` : '#000000'; - let feature = { - 'type': 'Feature', - 'geometry': { - 'type': regionType === 'Polyline' ? 'LineString' : 'Polygon', - 'coordinates': [coordinates], - }, - 'properties': { - 'regionId': regionId, - 'lineColor': hexColor, - 'group': i.parentNode.getAttribute('Name') || 'Ungrouped', - }, - 'bound': { - 'type': 'BoundingBox', - 'coordinates': [boundRect], - }, - }; + let feature = { + 'type': 'Feature', + 'geometry': { + 'type': regionType === 'Polyline' ? 'LineString' : 'Polygon', + 'coordinates': [coordinates], + }, + 'properties': { + 'regionId': regionId, + 'lineColor': hexColor, + 'group': region.parentNode.getAttribute('Name') || 'Ungrouped', + }, + 'bound': { + 'type': 'BoundingBox', + 'coordinates': [boundRect], + }, + }; - features.push(feature); + features.push(feature); + } } let output = Object.assign({}, template); From ceabcf671cd6998148ee1ff381f9a86c370d35fc Mon Sep 17 00:00:00 2001 From: Birm Date: Thu, 20 Feb 2025 14:18:36 -0500 Subject: [PATCH 14/34] fix style presentation --- apps/port/xml2geo.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/apps/port/xml2geo.js b/apps/port/xml2geo.js index d903e2e48..c9ee9ce46 100644 --- a/apps/port/xml2geo.js +++ b/apps/port/xml2geo.js @@ -63,10 +63,11 @@ function xml2geo() { maxY = Math.max(maxY, y); coordinates.push([x, y]); } - + let isFill = false; // **Detect Polygon vs. Polyline** if (regionType === 'Polygon') { coordinates.push(coordinates[0]); // Close the polygon by repeating the first point + isFill = true; } let boundRect = [[minX, minY], [minX, maxY], [maxX, maxY], [maxX, minY], [minX, minY]]; @@ -83,6 +84,10 @@ function xml2geo() { 'properties': { 'regionId': regionId, 'lineColor': hexColor, + 'style':{ + 'color': hexColor, + 'isFill': isFill, + }, 'group': region.parentNode.getAttribute('Name') || 'Ungrouped', }, 'bound': { From 9c89e9dda8b4bdefebacd169872f06f6284d6f27 Mon Sep 17 00:00:00 2001 From: Birm Date: Thu, 20 Feb 2025 14:24:08 -0500 Subject: [PATCH 15/34] lint fix xml script --- apps/port/xml2geo.js | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/apps/port/xml2geo.js b/apps/port/xml2geo.js index c9ee9ce46..7e1d99613 100644 --- a/apps/port/xml2geo.js +++ b/apps/port/xml2geo.js @@ -23,11 +23,11 @@ var template = { }, }; -var aperio_map = { - "0":"Polygon", - "1":"Polygon", - "2":"Polygon", // rectangle but should work?? haven't seen one yet - "4": "Polyline" +var aperioMap = { + '0': 'Polygon', + '1': 'Polygon', + '2': 'Polygon', // rectangle but should work?? haven't seen one yet + '4': 'Polyline', }; function xml2geo() { @@ -35,19 +35,19 @@ function xml2geo() { let input = document.getElementById('xml_in').value; xmlDoc = parser.parseFromString(input, 'text/xml'); let annotations = xmlDoc.getElementsByTagName('Annotation'); // Assuming regions are inside 'Annotation' elements - + for (let annotation of annotations) { - let annotationType = annotation.getAttribute('Type') || '0'; // Default to '0' if Type is not provided - let annotationLineColor = annotation.getAttribute('LineColor'); // Get LineColor from the parent annotation + let annotationType = annotation.getAttribute('Type') || '0'; // Default to '0' if Type is not provided + let annotationLineColor = annotation.getAttribute('LineColor'); // Get LineColor from the parent annotation let annotationId = annotation.getAttribute('Id'); - + console.log('Processing Annotation ID:', annotationId, 'with Type:', annotationType); - let regions = annotation.getElementsByTagName('Region'); // Get regions within this annotation + let regions = annotation.getElementsByTagName('Region'); // Get regions within this annotation for (let region of regions) { let regionId = region.getAttribute('Id'); regionType = annotationType || region.getAttribute('Type'); // parent annotation type if present, else own (odd?) - regionType = aperio_map[regionType] + regionType = aperioMap[regionType]; console.log('Processing Region ID:', regionId, 'as', regionType); let vertices = region.getElementsByTagName('Vertex'); @@ -84,7 +84,7 @@ function xml2geo() { 'properties': { 'regionId': regionId, 'lineColor': hexColor, - 'style':{ + 'style': { 'color': hexColor, 'isFill': isFill, }, From 9ba07b955b016181513ef449a7f77b10285486cb Mon Sep 17 00:00:00 2001 From: Birm Date: Thu, 20 Mar 2025 16:19:58 -0400 Subject: [PATCH 16/34] getting the image from dicomweb to render in camicroscope --- apps/viewer/viewer.html | 1 + common/DicomWebMods.js | 142 ++++++++++++++++++++++++++++++++++++ common/dynamicLoadScript.js | 5 ++ 3 files changed, 148 insertions(+) create mode 100644 common/DicomWebMods.js diff --git a/apps/viewer/viewer.html b/apps/viewer/viewer.html index a15b47214..af65c37a2 100644 --- a/apps/viewer/viewer.html +++ b/apps/viewer/viewer.html @@ -380,6 +380,7 @@ + + + + + + + + + + + CaMicroscope Data Table + + + + + + + + + + + + + + + + + +
+ + +
+

caMicroscope

+

Digital pathology image viewer with support for human/machine generated annotations and markups.

+
+ +
+
+
+ + + +
+
+
+
+
+ + + + + + + + + diff --git a/apps/dicom-web/table.js b/apps/dicom-web/table.js new file mode 100644 index 000000000..6f0fc8a12 --- /dev/null +++ b/apps/dicom-web/table.js @@ -0,0 +1,366 @@ +/** + * static variables + */ + +const sources = [{ + 'name': 'j4care', + 'url': 'https://ihe.j4care.com:18443/dcm4chee-arc/aets/DCM4CHEE/rs', + +}, { + 'name': 'google', + 'url': 'https://dicomwebproxy-bqmq3usc3a-uc.a.run.app/dicomWeb', +}]; +// const j4careStudiesUrl = 'https://development.j4care.com:11443/dcm4chee-arc/aets/DCM4CHEE/rs' +// const dicomWebStudiesUrl = 'https://dicomwebproxy-bqmq3usc3a-uc.a.run.app/dicomWeb' + +/** + * global variables + */ +isAllSeriesSynced = false; + +const datatableConfig = { + scrollX: true, + lengthMenu: [ + [15, 25, 50, -1], + [15, 25, 50, 'All'], + ], +}; + + +const pageStates = { + sources: { + data: [{ + 'name': 'j4care', + 'url': 'https://ihe.j4care.com:18443/dcm4chee-arc/aets/DCM4CHEE/rs', + + }, { + 'name': 'google', + 'url': 'https://dicomwebproxy-bqmq3usc3a-uc.a.run.app/dicomWeb', + }], + }, + studies: { + data: null, + }, + series: { + data: null, + }, + instances: { + data: null, + }, + status: 'sources', // 'sources, studies, series, instsances' +}; +var studies = []; + + +function getStudies(baseUrl) { + const url = `${baseUrl}/studies`; + return fetch(url).then((resp)=>resp.json()); +} + +function getSeries(baseUrl, studyId) { + const url = `${baseUrl}/studies/${studyId}/series`; + return fetch(url).then((resp)=>resp.json()); +} + +function getInstances(baseUrl, studyId, seriesId) { + const url = `${baseUrl}/studies/${studyId}/series/${seriesId}/instances`; + return fetch(url).then((resp)=>resp.json()); +} + +function previewSeries(baseUrl, study, series, modality) { + if (modality=='SM') { + baseUrlEncoded = encodeURIComponent(baseUrl); + window.location = `../viewer/viewer.html?mode=dcmweb&study=${study}&series=${series}&slideId=preview&source=${baseUrlEncoded}`; + } else { + alert('Cannot preview annotations yet.'); + } +} + + +function sanitize(string) { + string = string || ''; + const map = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + '\'': ''', + '/': '/', + }; + const reg = /[&<>"'/]/ig; + return string.replace(reg, (match) => (map[match])); +} + + +function initialize() { + const params = getUrlVars(); + console.log('params'); + console.log(params); + // store + const store = new Store('../../data/'); + if (params.status=='studies'&¶ms.source) { + pageStates.status = params.status; + } else if (params.status=='series'&¶ms.source&¶ms.studyId) { // series table + pageStates.status = params.status; + } else if (params.status=='instances'&¶ms.source&¶ms.studyId&¶ms.seriesId) { // isntasnces table + pageStates.status = params.status; + } + + + // + // + // + switch (pageStates.status) { + case 'sources': + $('#breadcrumb').append(``); + function generateLink(data, type, row) { + return `${row.name}`; + } + datatable = $('#datatable').DataTable({ + ...datatableConfig, + 'data': pageStates[pageStates.status].data, + 'columns': [ + {data: 'name', title: 'Name', render: generateLink}, + ], + }); + + break; + case 'studies': + + // get source info + var idx = sources.findIndex((elt)=>elt.name==params.source); + var src = sources[idx]; + + // create breadcrumb for studies + $('#breadcrumb').append(``); + $('#breadcrumb').append(``); + // get all studies + + getStudies(src.url).then(function(data) { + // mapping and merge + data.forEach((elt)=>elt.source=src.name); + pageStates[pageStates.status].data = data; + // ${baseUrl}/studies/${studyId}/series + function generateLink(data, type, row) { + const studyId = row['0020000D']['Value'][0]; + return `${studyId}`; + } + datatable = $('#datatable').DataTable({ + ...datatableConfig, + 'data': pageStates[pageStates.status].data, + 'columns': [ + {data: '0020000D.Value.0', title: 'Study Id', render: generateLink}, + {data: '00100020.Value.0', title: 'Name'}, + {data: 'source', title: 'Source'}, + ], + }); + }); + + break; + case 'series': + // get source info + var idx = sources.findIndex((elt)=>elt.name==params.source); + var src = sources[idx]; + + // create breadcrumb for series + $('#breadcrumb').append(``); + $('#breadcrumb').append(``); + $('#breadcrumb').append(``); + // get all series + + + getSeries(src.url, params.studyId).then(function(data) { + // add source and study id + data.forEach((elt)=>{ + elt.source=src.name; + elt.url=src.url; + elt.studyId=params.studyId; + elt.status='searching'; // 'searching', 'unsync', 'loading', 'done' + }); + pageStates[pageStates.status].data = data; + function generateLink(data, type, row) { + const seriesId = row['0020000E']['Value'][0]; + const modality = row['00080060']['Value'][0]; + if (row.status !='done') return seriesId; + const slideId = row.slideId; + // if (modality=='SM') + return `${seriesId}`; + } + function generateStatus(data, type, row) { + const seriesId = row['0020000E']['Value'][0]; + const modality = row['00080060']['Value'][0]; + let previewBtn = ``; + switch (row.status) { + case 'searching': + // return spin + return ''; + case 'unsync': + // return btn + let syncBtn = ``; + if (modality=='SM') { + return `
` + previewBtn + syncBtn + `
`; + } else { + return `
` + syncBtn + `
`; + } + + case 'loading': + // return downloading + // return '
'; + let progressIcon = `
`; + if (modality=='SM') { + return `
` + previewBtn + progressIcon + `
`; + } else { + return `
` + progressIcon + `
`; + } + + case 'done': + // return url + + return '
' + previewBtn + `
`; + + default: + + return '
'; + } + } + datatable = $('#datatable').DataTable({ + ...datatableConfig, + 'data': pageStates[pageStates.status].data, + 'columns': [ + {data: 'status', title: 'Status', render: generateStatus}, + {data: '0020000E.Value.0', title: 'Series Id', render: generateLink}, + {data: '00080060.Value.0', title: 'Modality'}, + {data: 'source', title: 'Source'}, + {data: 'studyId', title: 'study Id'}, + + ], + }); + + async function checkInterval() { + const query = { + 'dicom-source-url': src.url, + 'study': params.studyId, + }; + + const slides = await store.findSlide(null, null, params.studyId, null, query); + console.log(slides); + + const data = datatable.data(); + + for (let i = 0; i < data.length; i++) { + const d = data[i]; + const modality = d['00080060']['Value'][0]; + const series = d['0020000E']['Value'][0]; + + if (modality === 'SM') { + const idx = slides.findIndex((slide) => series === slide.series); + if (idx !== -1) { + d.status = slides[idx].status; + d.slideId = slides[idx]._id.$oid; + } else { + d.status = 'unsync'; + } + } + + if (modality === 'ANN') { + let annotationQuery = { + 'provenance.image.dicom-source-url': src.url, + 'provenance.image.dicom-study': params.studyId, + 'provenance.image.dicom-series': series, + }; + + let annotationCount = await store.countMarks(annotationQuery); + console.info('Counted ' + annotationCount[0].count + ' mark objects for ' + series); + + if (annotationCount[0].count > 0) { + d.status = 'done'; + d.slideId = slides[0]._id.$oid; + } else { + d.status = 'unsync'; + } + } + } + + datatable.rows().invalidate().draw(); + + const series = pageStates[pageStates.status].data; + + if (series.every((s) => s.status !== 'unsync' && s.status !== 'syncing')) { + console.log('clear'); + clearInterval(updateSeriesStatus); + } + + console.log('running'); + } + + // initialize + checkInterval(); + // update every 10 seconds + var updateSeriesStatus = setInterval(checkInterval, 10000); + }); + break; + case 'instances': + // get source info + var idx = sources.findIndex((elt)=>elt.name==params.source); + var src = sources[idx]; + // create breadcrumb for instances + const backSeriesUrl = `../dicom-web/table.html?source=${params.source}&status=series&studyId=${params.studyId}`; + $('#breadcrumb').append(``); + $('#breadcrumb').append(``); + $('#breadcrumb').append(``); + $('#breadcrumb').append(``); + + + getInstances(src.url, params.studyId, params.seriesId).then(function(data) { + // add status + data.forEach((elt)=>{ + elt.source=params.source; + elt.studyId=params.studyId; + elt.seriesId=params.seriesId; + }); + pageStates[pageStates.status].data = data; + function generateLink(data, type, row) { + const {studyId, seriesId, status}= row; + const instanceId = row['00080018']['Value'][0]; + if (status=='done') return instanceId; + return `${instanceId}`; + } + + datatable = $('#datatable').DataTable({ + ...datatableConfig, + 'data': pageStates[pageStates.status].data, + 'columns': [ + {data: '00080018.Value.0', title: 'Instance Id', render: generateLink}, + {data: 'source', title: 'Source'}, + {data: 'seriesId', title: 'Series Id'}, + {data: 'studyId', title: 'Study Id'}, + + ], + }); + }); + break; + + default: + break; + } +} + + +$(document).ready(function() { + initialize(); +}); + + +async function syncSeries(sourceUrl, study, series, modality) { + console.log(sourceUrl, study, series, modality); + const result = await store.syncSeries('../../', {sourceUrl, study, series, modality}); + console.log('syncSeries:'); + console.log(result); +} + +function checkSeriesStatus() { + const series = pageStates[pageStates.status].data; + series.map(); +} +// table.rows.add( dataset ).draw(). + diff --git a/apps/viewer/init.js b/apps/viewer/init.js index 441b3b7e3..6c091f1a4 100644 --- a/apps/viewer/init.js +++ b/apps/viewer/init.js @@ -524,7 +524,7 @@ async function initUIcomponents() { callback: toggleMeasurement, }); } - if ($D.params.mode != 'dcmweb'){ + if ($D.params.mode != 'dcmweb') { // donwload selection subToolsOpt.push({ name: 'download_selection', @@ -596,7 +596,7 @@ async function initUIcomponents() { callback: toggleViewerMode, }); - if ($D.params.mode != 'dcmweb'){ + if ($D.params.mode != 'dcmweb') { // heatmap subToolsOpt.push({ name: 'heatmap', @@ -672,58 +672,58 @@ async function initUIcomponents() { ariaLabel: 'Load marks', callback: Store.prototype.LoadMarksFromFile, }); - // -- For Nano borb End -- // + // -- For Nano borb End -- // - // -- view btn START -- // - if (!($D.params.data.hasOwnProperty('review') && $D.params.data['review']=='true')) { + // -- view btn START -- // + if (!($D.params.data.hasOwnProperty('review') && $D.params.data['review']=='true')) { + subToolsOpt.push({ + name: 'review', + icon: 'playlist_add_check', + title: 'has reviewed', + type: 'btn', + value: 'review', + ariaLabel: 'Has reviewed', + callback: updateSlideView, + }); + } + + if ($D.params.mode != 'dcmweb') { + // screenshot + subToolsOpt.push({ + name: 'slideCapture', + icon: 'camera_enhance', + title: 'Slide Capture', + type: 'btn', + value: 'slCap', + ariaLabel: 'Slide capture', + callback: captureSlide, + }); + } + + // visualization panel subToolsOpt.push({ - name: 'review', - icon: 'playlist_add_check', - title: 'has reviewed', + name: 'visualization', + icon: 'auto_graph', // material icons' name + title: 'visualization', + value: 'visualization', type: 'btn', - value: 'review', - ariaLabel: 'Has reviewed', - callback: updateSlideView, + callback: visualization, }); - } - if ($D.params.mode != 'dcmweb'){ - // screenshot subToolsOpt.push({ - name: 'slideCapture', - icon: 'camera_enhance', - title: 'Slide Capture', + name: 'tutorial', + icon: 'help', + title: 'Tutorial', + value: 'tutorial', type: 'btn', - value: 'slCap', - ariaLabel: 'Slide capture', - callback: captureSlide, + ariaLabel: 'Tutorial', + callback: function() { + tour.init(); + tour.start(true); + }, }); } - // visualization panel - subToolsOpt.push({ - name: 'visualization', - icon: 'auto_graph', // material icons' name - title: 'visualization', - value: 'visualization', - type: 'btn', - callback: visualization, - }); - - subToolsOpt.push({ - name: 'tutorial', - icon: 'help', - title: 'Tutorial', - value: 'tutorial', - type: 'btn', - ariaLabel: 'Tutorial', - callback: function() { - tour.init(); - tour.start(true); - }, - }); -} - // Additional Links handler function additionalLinksHandler(url, openInNewTab, appendSlide) { if (appendSlide === true) { diff --git a/package.json b/package.json index 2dfca9fa0..299bf1054 100644 --- a/package.json +++ b/package.json @@ -5,8 +5,8 @@ "scripts": { "test" : "jest", "test-mocha": "mocha test --recursive", - "lint": "eslint ./core/*.js ./components/**/*.js ./common/smartpen/*.js ./apps/*.js ./apps/port/*.js ./apps/heatmap/*.js ./apps/multi/*.js ./apps/labeling/*.js ./apps/loader/*.js ./apps/model/*.js ./apps/segment/*.js ./apps/model/**/*.js ./apps/segment/**/*.js ./apps/viewer/*.js --quiet", - "lint-fix": "eslint ./core/*.js ./components/**/*.js ./common/smartpen/*.js ./apps/*.js ./apps/port/*.js ./apps/heatmap/*.js ./apps/multi/*.js ./apps/labeling/*.js ./apps/loader/*.js ./apps/model/*.js ./apps/segment/*.js ./apps/model/**/*.js ./apps/segment/**/*.js ./apps/viewer/*.js --quiet --fix" + "lint": "eslint ./core/*.js ./components/**/*.js ./common/smartpen/*.js ./apps/*.js ./apps/port/*.js ./apps/heatmap/*.js ./apps/multi/*.js ./apps/labeling/*.js ./apps/loader/*.js ./apps/model/*.js ./apps/segment/*.js ./apps/model/**/*.js ./apps/segment/**/*.js ./apps/viewer/*.js ./apps/dicom-web/*.js --quiet", + "lint-fix": "eslint ./core/*.js ./components/**/*.js ./common/smartpen/*.js ./apps/*.js ./apps/port/*.js ./apps/heatmap/*.js ./apps/multi/*.js ./apps/labeling/*.js ./apps/loader/*.js ./apps/model/*.js ./apps/segment/*.js ./apps/model/**/*.js ./apps/segment/**/*.js ./apps/viewer/*.js ./apps/dicom-web/*.js --quiet --fix" }, "author": "", "license": "BSD-3-Clause", From 6b8a5fe3714788d59ada4c83d1149277647f7637 Mon Sep 17 00:00:00 2001 From: Birm Date: Wed, 9 Apr 2025 14:17:44 -0400 Subject: [PATCH 21/34] better fixes for multi img dicom --- apps/dicom-connect/table.css | 260 ------------ apps/dicom-connect/table.html | 118 ------ apps/dicom-connect/table.js | 378 ------------------ common/DicomWebMods.js | 12 +- .../openseadragon-canvas-draw-overlay.js | 10 +- .../openseadragon-overlays-manage.js | 2 + 6 files changed, 20 insertions(+), 760 deletions(-) delete mode 100644 apps/dicom-connect/table.css delete mode 100644 apps/dicom-connect/table.html delete mode 100644 apps/dicom-connect/table.js diff --git a/apps/dicom-connect/table.css b/apps/dicom-connect/table.css deleted file mode 100644 index 786b61235..000000000 --- a/apps/dicom-connect/table.css +++ /dev/null @@ -1,260 +0,0 @@ -html, -body { - width: 100%; - height: 100%; - min-height: 100%; - font-family: Arial, Helvetica, sans-serif; -} - -footer { - /* position: sticky; - /* bottom: -8px; */ - width: 100%; -} -.header { - margin-top: 60px; -} - -.page-container { - height: 100vh; - /* flex-direction: column; */ - /* justify-content: space-between; */ -} - -.link { - letter-spacing: 0.02em; - text-transform: uppercase; - font-size: 0.99em; - font-weight: bold; - color: #5e6875; - text-decoration: none; - padding-right: 0.7rem; - padding-left: 0.7rem; - margin-top: 0.2rem; - font-family: "Open Sans", Helvetica, sans-serif; - /* line-height: 1.8; */ -} -.bg-dark { - background-color: #343a40 !important; -} -.bg-info { - background-color: #17a2b8 !important; -} -#collection-list li.item { - cursor: pointer; -} -#collection-list li.item:hover { - background-color: #deeeff; -} -#entries { - cursor: pointer; -} -.page-item { - cursor: pointer; -} - -/* #collection-list .item:hover { - background-color:#f8f9fa; -} */ - -#collection-list .item { - /* font-weight:bold; */ - color: #007bff; - padding: 0.5rem; -} -#collection-list .item i { - padding: 0.25rem; -} - -nav li { - transition: background 0.5s; - border-radius: 3px; -} -nav li:not(:first-child) { - margin-left: 0.3em !important; -} -nav li:not(.active):hover { - background: white; -} -nav li:not(.active):hover a { - color: black !important; -} -.active { - background: white; -} -.active a { - color: black !important; -} - -nav li:not(.active):hover{ - background: white; -} - -.overall { - display: flex; -} - -.reload { - display: none; - padding: 0 4px; -} - -.btn2{ - display: flex; - align-items: center; -} -@media screen and (min-width: 480px){ - .reload { - display: block; - } -} - -#deleteBtn, -#downloadBtn { - margin-left: 0.6em; -} -#deleteBtn i { - color: white; -} -#open-delete { - display: inline-flex; -} -.custom-file-input, -.sort-btn { - cursor: pointer; -} - -#notification-box { - overflow-y: scroll; - max-height: 40em; -} - -/* Tooltip container */ -.tooltipCustom { - position: relative; - display: inline-block; - /* border-bottom: 1px dotted black; If you want dots under the hoverable text */ -} - -/* Tooltip text */ -.tooltipCustom .tooltiptextCustom { - visibility: hidden; - background-color: black; - color: #fff; - text-align: center; - padding: 5px 0; - border-radius: 6px; - - /* Position the tooltip text */ - position: absolute; - z-index: 100; - width: 200px; - bottom: 100%; - left: 50%; - margin-left: -100px; /* Use half of the width (120/2 = 60), to center the tooltip */ -} - -/* Show the tooltip text when you mouse over the tooltip container */ -.tooltipCustom:hover .tooltiptextCustom { - visibility: visible; - z-index: 1000; -} - -.notification-box { - padding: 10px 0px; - color: black; -} - -#tabs, -#content { - width: 100%; -} - -.bg-gray { - background-color: #eee; -} -@media (max-width: 640px) { - #dropNot { - top: 50px; - left: -16px; - width: 290px; - } - .nav { - display: block; - } - .nav .nav-item, - .nav .nav-item a { - padding-left: 0px; - } - .message { - font-size: 13px; - } -} -#dropNot { - top: 60px; - left: 0px; - right: unset; - width: 460px; - box-shadow: 0px 5px 7px -1px #c1c1c1; - padding-bottom: 0px; - padding: 0px; -} -.dropdown-menu:before { - content: ""; - position: absolute; - top: -20px; - left: 12px; - border: 10px solid #343a40; - border-color: transparent transparent #343a40 transparent; -} - -.has-search .form-control { - padding-left: 2.375rem; -} - -.has-search .form-control-feedback { - position: absolute; - z-index: 2; - display: block; - width: 2.375rem; - height: 2.375rem; - line-height: 2.375rem; - text-align: center; - pointer-events: none; - color: #aaa; -} - -/* .nav-tabs { - display: flex; -} */ - -.collapse.in { - display: inline !important; -} -.p { - margin-bottom: 0; -} - -.icon-center { - text-align: center!important -} -/* .main-container { - max-height: calc(100% - 199px); - overflow-x: hidden; - overflow-y: auto; -} */ - -.loader { - -webkit-animation: spin 2s linear infinite; - animation: spin 2s linear infinite; -} - -@-webkit-keyframes spin { - 0% { -webkit-transform: rotate(0deg); } - 100% { -webkit-transform: rotate(360deg); } -} - -@keyframes spin { - 0% { transform: rotate(0deg); } - 100% { transform: rotate(360deg); } -} \ No newline at end of file diff --git a/apps/dicom-connect/table.html b/apps/dicom-connect/table.html deleted file mode 100644 index 65f0e8118..000000000 --- a/apps/dicom-connect/table.html +++ /dev/null @@ -1,118 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - CaMicroscope Data Table - - - - - - - - - - - - - - - - - -
- - -
-

caMicroscope

-

Digital pathology image viewer with support for human/machine generated annotations and markups.

-
- -
-
-
- - - -
-
-
-
-
- - - - - - - - - diff --git a/apps/dicom-connect/table.js b/apps/dicom-connect/table.js deleted file mode 100644 index 949fd1660..000000000 --- a/apps/dicom-connect/table.js +++ /dev/null @@ -1,378 +0,0 @@ -/** - * static variables - */ - -const sources = [{ - 'name':'j4care', - 'url':'https://ihe.j4care.com:18443/dcm4chee-arc/aets/DCM4CHEE/rs' - -},{ - 'name': 'google', - 'url': 'https://dicomwebproxy-bqmq3usc3a-uc.a.run.app/dicomWeb' -}] -// const j4careStudiesUrl = 'https://development.j4care.com:11443/dcm4chee-arc/aets/DCM4CHEE/rs' -// const dicomWebStudiesUrl = 'https://dicomwebproxy-bqmq3usc3a-uc.a.run.app/dicomWeb' - -/** - * global variables - */ -isAllSeriesSynced = false; - -const datatableConfig = { - scrollX: true, - lengthMenu: [ - [15, 25, 50, -1], - [15, 25, 50, 'All'] - ] -} - - -const page_states = { - sources: { - data: [{ - 'name':'j4care', - 'url':'https://ihe.j4care.com:18443/dcm4chee-arc/aets/DCM4CHEE/rs' - - },{ - 'name': 'google', - 'url': 'https://dicomwebproxy-bqmq3usc3a-uc.a.run.app/dicomWeb' - }], - }, - studies: { - data: null, - }, - series: { - data: null, - }, - instances: { - data: null, - }, - status: 'sources', // 'sources, studies, series, instsances' -} -var studies = [] - - - -function getStudies(baseUrl) { - const url = `${baseUrl}/studies` - return fetch(url).then(resp=>resp.json()); -} - -function getSeries(baseUrl, studyId) { - const url = `${baseUrl}/studies/${studyId}/series` - return fetch(url).then(resp=>resp.json()); -} - -function getInstances(baseUrl, studyId, seriesId) { - const url = `${baseUrl}/studies/${studyId}/series/${seriesId}/instances` - return fetch(url).then(resp=>resp.json()); -} - -function previewSeries(base_url, study, series, modality){ - if (modality=="SM"){ - base_url_encoded = encodeURIComponent(base_url) - window.location = `../viewer/viewer.html?mode=dcmweb&study=${study}&series=${series}&slideId=preview&source=${base_url_encoded}` - } - else{ - alert("Cannot preview annotations yet."); - } -} - - - -function sanitize(string) { - string = string || ''; - const map = { - '&': '&', - '<': '<', - '>': '>', - '"': '"', - '\'': ''', - '/': '/', - }; - const reg = /[&<>"'/]/ig; - return string.replace(reg, (match) => (map[match])); -} - - -function initialize() { - const params = getUrlVars(); - console.log('params') - console.log(params) - // store - const store = new Store('../../data/'); - if(params.status=='studies'&¶ms.source){ - page_states.status = params.status; - }else if(params.status=='series'&¶ms.source&¶ms.studyId) { // series table - page_states.status = params.status; - } else if(params.status=='instances'&¶ms.source&¶ms.studyId&¶ms.seriesId) { // isntasnces table - page_states.status = params.status; - } - - - - // - // - // - switch (page_states.status) { - case 'sources': - $('#breadcrumb').append(``); - function generateLink (data, type, row) { - return `${row.name}`; - } - datatable = $('#datatable').DataTable({ - ... datatableConfig, - 'data': page_states[page_states.status].data, - 'columns': [ - {data: 'name', title: 'Name', render:generateLink} - ] - }); - - break; - case 'studies': - - //get source info - var idx = sources.findIndex(elt=>elt.name==params.source) - var src = sources[idx] - - // create breadcrumb for studies - $('#breadcrumb').append(``); - $('#breadcrumb').append(``); - // get all studies - - getStudies(src.url).then(function(data) { - // mapping and merge - data.forEach(elt=>elt.source=src.name) - page_states[page_states.status].data = data - // ${baseUrl}/studies/${studyId}/series - function generateLink (data, type, row) { - const studyId = row['0020000D']['Value'][0] - return `${studyId}`; - } - datatable = $('#datatable').DataTable({ - ... datatableConfig, - 'data': page_states[page_states.status].data, - 'columns': [ - {data: '0020000D.Value.0', title: 'Study Id', render: generateLink}, - {data: '00100020.Value.0', title: 'Name'}, - {data: 'source', title: 'Source'} - ] - }); - }) - - break; - case 'series': - //get source info - var idx = sources.findIndex(elt=>elt.name==params.source) - var src = sources[idx] - - // create breadcrumb for series - $('#breadcrumb').append(``); - $('#breadcrumb').append(``); - $('#breadcrumb').append(``); - // get all series - - - getSeries(src.url, params.studyId).then(function(data) { - // add source and study id - data.forEach(elt=>{ - elt.source=src.name - elt.url=src.url - elt.studyId=params.studyId - elt.status='searching' // 'searching', 'unsync', 'loading', 'done' - }) - page_states[page_states.status].data = data - function generateLink (data, type, row) { - const seriesId = row['0020000E']['Value'][0] - const modality = row['00080060']['Value'][0] - if (row.status !='done') return seriesId; - const slideId = row.slideId; - //if (modality=='SM') - return `${seriesId}` - - - // return `${seriesId}`; - } - function generateStatus (data, type, row) { - const seriesId = row['0020000E']['Value'][0]; - const modality = row['00080060']['Value'][0]; - let previewBtn = ``; - switch (row.status) { - case 'searching': - // return spin - return ''; - case 'unsync': - // return btn - let syncBtn = ``; - if (modality=='SM'){ - return `
` + previewBtn + syncBtn + `
`; - } else { - return `
` + syncBtn + `
`; - } - - case 'loading': - // return downloading - // return '
'; - let progressIcon = `
` - if (modality=='SM'){ - return `
` + previewBtn + progressIcon + `
`; - } else { - return `
` + progressIcon + `
`; - } - - case 'done': - // return url - - return '
' + previewBtn + `
`; - - default: - - return '
'; - } - } - datatable = $('#datatable').DataTable({ - ... datatableConfig, - 'data': page_states[page_states.status].data, - 'columns': [ - {data: 'status', title: 'Status', render: generateStatus}, - {data: '0020000E.Value.0', title: 'Series Id',render:generateLink }, - {data: '00080060.Value.0', title: 'Modality'}, - {data: 'source', title: 'Source'}, - {data: 'studyId', title: 'study Id'} - - ] - }); - - async function checkInterval() { - const query = { - 'dicom-source-url': src.url, - 'study': params.studyId, - }; - - const slides = await store.findSlide(null, null, params.studyId, null, query); - console.log(slides) - - const data = datatable.data(); - - for (let i = 0; i < data.length; i++) { - const d = data[i]; - const modality = d['00080060']['Value'][0]; - const series = d['0020000E']['Value'][0]; - - if (modality === 'SM') { - const idx = slides.findIndex(slide => series === slide.series); - if (idx !== -1) { - d.status = slides[idx].status; - d.slideId = slides[idx]._id.$oid; - } else { - d.status = 'unsync'; - } - } - - if (modality === 'ANN') { - let annotationQuery = { - 'provenance.image.dicom-source-url': src.url, - 'provenance.image.dicom-study': params.studyId, - 'provenance.image.dicom-series': series - }; - - let annotationCount = await store.countMarks(annotationQuery); - console.info("Counted " + annotationCount[0].count + " mark objects for " + series); - - if (annotationCount[0].count > 0) { - d.status = 'done'; - d.slideId = slides[0]._id.$oid; - } else { - d.status = 'unsync'; - } - } - } - - datatable.rows().invalidate().draw(); - - const series = page_states[page_states.status].data; - - if (series.every(s => s.status !== 'unsync' && s.status !== 'syncing')) { - console.log('clear'); - clearInterval(updateSeriesStatus); - } - - console.log('running'); - } - - // initialize - checkInterval() - // update every 10 seconds - var updateSeriesStatus = setInterval(checkInterval, 10000); - }) - break; - case 'instances': - //get source info - var idx = sources.findIndex(elt=>elt.name==params.source) - var src = sources[idx] - // create breadcrumb for instances - const backSeriesUrl = `../dicom-connect/table.html?source=${params.source}&status=series&studyId=${params.studyId}` - $('#breadcrumb').append(``); - $('#breadcrumb').append(``); - $('#breadcrumb').append(``); - $('#breadcrumb').append(``); - - - - - - getInstances(src.url, params.studyId, params.seriesId).then(function(data) { - // add status - data.forEach(elt=>{ - elt.source=params.source - elt.studyId=params.studyId - elt.seriesId=params.seriesId - - }) - page_states[page_states.status].data = data - function generateLink (data, type, row) { - const {studyId, seriesId, status}= row - const instanceId = row['00080018']['Value'][0] - if (status=='done') return instanceId - return `${instanceId}`; - } - - datatable = $('#datatable').DataTable({ - ... datatableConfig, - 'data': page_states[page_states.status].data, - 'columns': [ - {data: '00080018.Value.0', title: 'Instance Id', render: generateLink}, - {data: 'source', title: 'Source'}, - {data: 'seriesId', title: 'Series Id'}, - {data: 'studyId', title: 'Study Id'}, - - ] - }); - }) - break; - - default: - break; - } -} - - -$(document).ready(function() { - initialize(); -}); - - -async function syncSeries(source_url, study, series, modality) { - console.log(source_url, study, series, modality); - const result = await store.syncSeries('../../', {source_url, study, series, modality}) - console.log('syncSeries:'); - console.log(result); -} - -function checkSeriesStatus() { - const series = page_states[page_states.status].data - series.map() - -} -// table.rows.add( dataset ).draw(). - diff --git a/common/DicomWebMods.js b/common/DicomWebMods.js index d7f6d2d49..e0813da91 100644 --- a/common/DicomWebMods.js +++ b/common/DicomWebMods.js @@ -109,14 +109,22 @@ function DicomWebMods() { } CaMic.prototype.loadImg = function(func) { // override for multi image as single viewport image simulation - this.viewer.viewport.viewportToImageCoordinates = function(x,y){ + OpenSeadragon.Viewport.prototype.viewportToImageCoordinates = function(x,y){ let i = this.viewer.world._items.length - 1 return this.viewer.world.getItemAt(i).viewportToImageCoordinates(x,y) } - this.viewer.viewport.viewportToImageZoom = function(z){ + OpenSeadragon.Viewport.prototype.viewportToImageZoom = function(z){ let i = this.viewer.world._items.length - 1 return this.viewer.world.getItemAt(i).viewportToImageZoom(z) } + OpenSeadragon.Viewport.prototype.imageToViewportZoom = function(z){ + let i = this.viewer.world._items.length - 1 + return this.viewer.world.getItemAt(i).imageToViewportZoom(z) + } + OpenSeadragon.Viewport.prototype.imageToViewportCoordinates = function(x,y){ + let i = this.viewer.world._items.length - 1 + return this.viewer.world.getItemAt(i).imageToViewportCoordinates(x,y) + } var urlParams = new URLSearchParams(window.location.search); let encodedUrl = urlParams.get('source') || "https%3A%2F%2Fihe.j4care.com%3A18443%2Fdcm4chee-arc%2Faets%2FDCM4CHEE%2Frs"; let base_url = decodeURIComponent(encodedUrl); diff --git a/core/extension/openseadragon-canvas-draw-overlay.js b/core/extension/openseadragon-canvas-draw-overlay.js index 8984c6fe6..26a2d9998 100644 --- a/core/extension/openseadragon-canvas-draw-overlay.js +++ b/core/extension/openseadragon-canvas-draw-overlay.js @@ -230,6 +230,7 @@ * @return {[type]} [description] */ drawOnCanvas: function(ctx, drawFuc) { + console.log("drawOnCanvas", ctx, this, drawFuc) var viewportZoom = this._viewer.viewport.getZoom(true); var zoom = this._viewer.viewport.viewportToImageZoom(viewportZoom); var x = @@ -291,7 +292,9 @@ this._viewportWidth = boundsRect.width; this._viewportHeight = boundsRect.height * this.imgAspectRatio; - var image1 = this._viewer.world.getItemAt(0); + + let i = this._viewer.world._items.length - 1 + var image1 = this._viewer.world.getItemAt(i); this.imgWidth = image1.source.dimensions.x; this.imgHeight = image1.source.dimensions.y; @@ -460,13 +463,16 @@ // ml tools try { mltools.initcanvas(this._viewer.drawer.canvas); - } catch (error) {} + } catch (error) { + console.error("ml err, ignored", error) + } if ( 0 > img_point.x || this.imgWidth < img_point.x || 0 > img_point.y || this.imgHeight < img_point.y ) { + console.error("drawing not within image") return; } diff --git a/core/extension/openseadragon-overlays-manage.js b/core/extension/openseadragon-overlays-manage.js index 48162a333..f02e3cef6 100644 --- a/core/extension/openseadragon-overlays-manage.js +++ b/core/extension/openseadragon-overlays-manage.js @@ -643,6 +643,7 @@ * @return {[type]} [description] */ drawOnCanvas:function(drawFuc,args){ + console.log("drawOnCanvas called") var viewportZoom = this._viewer.viewport.getZoom(true); var image1 = this._viewer.world.getItemAt(0); var zoom = image1.viewportToImageZoom(viewportZoom); @@ -729,6 +730,7 @@ * updateView update all canvas according to the current states of the osd'viewer */ updateView:function(){ + console.log("updateView", this) this.resize(); if(this.hasShowOverlay()) { this._div.style.display = 'block'; From 2a698a159035d22c942c2f3010ea2ec2b1a2743d Mon Sep 17 00:00:00 2001 From: Birm Date: Wed, 23 Apr 2025 15:43:53 -0400 Subject: [PATCH 22/34] no label/thumbnail, safer --- common/DicomWebMods.js | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/common/DicomWebMods.js b/common/DicomWebMods.js index e0813da91..cc00f5332 100644 --- a/common/DicomWebMods.js +++ b/common/DicomWebMods.js @@ -59,16 +59,31 @@ function DicomWebMods() { const instance_results = instance_data.map(x => { try { return { - height: x["00480007"]["Value"][0], - width: x["00480006"]["Value"][0], - tile_size: x["00280010"]["Value"][0], // Assuming square tiles - url: x["url"].split("/metadata")[0] + height: x["00480007"]?.["Value"]?.[0] ?? null, + width: x["00480006"]?.["Value"]?.[0] ?? null, + tile_size: x["00280010"]?.["Value"]?.[0] ?? null, + url: x["url"]?.split("/metadata")[0] ?? "", + type: x["00080008"]?.["Value"] ?? [], }; } catch (error) { console.error("Error processing instance metadata:", error); return null; } - }).filter(item => item !== null); + }).filter(x=>{ + if (x == null){ + return false; + } + let types = x['type'] + for (let i=0; i< types.length; i++){ + let v = types[i].toUpperCase(); + if (v.indexOf("LABEL") !== -1 || + v.indexOf("THUMBNAIL") !== -1 || + v.indexOf("OVERVIEW") !== -1) { + return false; + } + } + return true; + }); // Sort instance_results by width in ascending order instance_results.sort((a, b) => a.width - b.width); From 2f91e47cde6a62b45d601ba287dd25f445917cb7 Mon Sep 17 00:00:00 2001 From: Birm Date: Wed, 23 Apr 2025 16:31:03 -0400 Subject: [PATCH 23/34] make drawings show up --- core/extension/openseadragon-canvas-draw-overlay.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/extension/openseadragon-canvas-draw-overlay.js b/core/extension/openseadragon-canvas-draw-overlay.js index 26a2d9998..c66bcf427 100644 --- a/core/extension/openseadragon-canvas-draw-overlay.js +++ b/core/extension/openseadragon-canvas-draw-overlay.js @@ -320,7 +320,8 @@ */ _updateCanvas: function() { var viewportZoom = this._viewer.viewport.getZoom(true); - var image1 = this._viewer.world.getItemAt(0); + var zoomidx = $CAMIC?.viewer?.world?._items?.length || 1; + var image1 = this._viewer.world.getItemAt(zoomidx - 1); var zoom = image1.viewportToImageZoom(viewportZoom); var x = From af4e282de5024d1d0cf63bda9b76d9a9b4448a15 Mon Sep 17 00:00:00 2001 From: Birm Date: Wed, 21 May 2025 22:03:28 -0400 Subject: [PATCH 24/34] table fix, var name in api --- apps/dicom-web/table.js | 2 +- common/DicomWebMods.js | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/apps/dicom-web/table.js b/apps/dicom-web/table.js index 6f0fc8a12..6f966e6ea 100644 --- a/apps/dicom-web/table.js +++ b/apps/dicom-web/table.js @@ -353,7 +353,7 @@ $(document).ready(function() { async function syncSeries(sourceUrl, study, series, modality) { console.log(sourceUrl, study, series, modality); - const result = await store.syncSeries('../../', {sourceUrl, study, series, modality}); + const result = await store.syncSeries('../../', {"source_url": sourceUrl, study, series, modality}); console.log('syncSeries:'); console.log(result); } diff --git a/common/DicomWebMods.js b/common/DicomWebMods.js index cc00f5332..a2e32dee9 100644 --- a/common/DicomWebMods.js +++ b/common/DicomWebMods.js @@ -58,12 +58,19 @@ function DicomWebMods() { // Transform result into OpenSeadragon-compatible format const instance_results = instance_data.map(x => { try { + let tile_order = 1; // default + if (x["00480102"]?.Value && + Array.isArray(x["00480102"].Value) && + x["00480102"].Value.length > 4) { + tile_order = x["00480102"].Value[4]; + } return { height: x["00480007"]?.["Value"]?.[0] ?? null, width: x["00480006"]?.["Value"]?.[0] ?? null, tile_size: x["00280010"]?.["Value"]?.[0] ?? null, url: x["url"]?.split("/metadata")[0] ?? "", type: x["00080008"]?.["Value"] ?? [], + tile_order: tile_order, }; } catch (error) { console.error("Error processing instance metadata:", error); @@ -105,6 +112,9 @@ function DicomWebMods() { getTileUrl: function(level, x_pos, y_pos) { if (level == x['order']){ var frameIndex = y_pos * Math.ceil(x['width'] / x['tile_size']) + x_pos; + if (x['tile_order'] == -1){ + frameIndex = x_pos * Math.ceil(x['height'] / x['tile_size']) + y_pos; + } return `${x["url"]}/frames/${frameIndex + 1}/rendered`; } else { return null; From 57cb7f9d2fc901192ec38c701f60295bdfa6ca14 Mon Sep 17 00:00:00 2001 From: Birm Date: Thu, 22 May 2025 00:02:01 -0400 Subject: [PATCH 25/34] wip, what on earth is tile order --- common/DicomWebMods.js | 64 ++++++++++++++++++++++++++++++++++++------ 1 file changed, 55 insertions(+), 9 deletions(-) diff --git a/common/DicomWebMods.js b/common/DicomWebMods.js index a2e32dee9..c527d1d36 100644 --- a/common/DicomWebMods.js +++ b/common/DicomWebMods.js @@ -16,7 +16,6 @@ function DicomWebMods() { if (!Array.isArray(series_overview)) { throw new Error("Unexpected series metadata format"); } - // Extract instance IDs const instance_ids = series_overview .map(item => item?.["00080018"]?.["Value"]?.[0]) @@ -54,15 +53,38 @@ function DicomWebMods() { if (instance_data.length === 0) { throw new Error("No valid instance metadata retrieved"); } - + console.log(instance_data) // Transform result into OpenSeadragon-compatible format const instance_results = instance_data.map(x => { try { - let tile_order = 1; // default + let tile_order = "r++"; // default if (x["00480102"]?.Value && Array.isArray(x["00480102"].Value) && - x["00480102"].Value.length > 4) { - tile_order = x["00480102"].Value[4]; + x["00480102"].Value.length == 6) { + let [X1, Y1, Z1, X2, Y2, Z2] = x["00480102"].Value; + isRowMajor = Math.abs(X1) < Math.abs(X2); + isColReverse = X1 < 0 || X2 < 0; + isRowReverse = Y1 < 0 || Y2 < 0; + tile_order_proposed = `${isRowMajor ? 'r' : 'c'}${isRowReverse ? '-' : '+'}${isColReverse ? '-' : '+'}`; + + //isColReverse = false; + isRowReverse = !isRowReverse; + //isRowMajor = false; + //isRowMajor = !isRowMajor; + //isColReverse = !isColReverse; + //isRowReverse = !isRowReverse; + let doReverse = true; + doReverse = false; + if (doReverse && !isRowMajor){ + let tmp = isRowReverse; + isRowReverse = isColReverse; + isColReverse = tmp; + } + + tile_order = `${isRowMajor ? 'r' : 'c'}${isRowReverse ? '-' : '+'}${isColReverse ? '-' : '+'}`; + //tile_order = "r++" + console.info(x["00480102"]?.Value, tile_order, tile_order_proposed) + } return { height: x["00480007"]?.["Value"]?.[0] ?? null, @@ -77,7 +99,7 @@ function DicomWebMods() { return null; } }).filter(x=>{ - if (x == null){ + if (x == null || x.height == null || x.width == null){ return false; } let types = x['type'] @@ -85,12 +107,18 @@ function DicomWebMods() { let v = types[i].toUpperCase(); if (v.indexOf("LABEL") !== -1 || v.indexOf("THUMBNAIL") !== -1 || + v.indexOf("MACRO") !==-1 || v.indexOf("OVERVIEW") !== -1) { return false; } } return true; }); + console.log(instance_results) + if (instance_results.length == 0){ + alert("didn't find anything!! Labels only maybe?") + history.back() + } // Sort instance_results by width in ascending order instance_results.sort((a, b) => a.width - b.width); @@ -111,10 +139,28 @@ function DicomWebMods() { maxLevel: x['order'], getTileUrl: function(level, x_pos, y_pos) { if (level == x['order']){ - var frameIndex = y_pos * Math.ceil(x['width'] / x['tile_size']) + x_pos; - if (x['tile_order'] == -1){ - frameIndex = x_pos * Math.ceil(x['height'] / x['tile_size']) + y_pos; + const numRows = Math.ceil(x['height'] / x['tile_size']); + const numCols = Math.ceil(x['width'] / x['tile_size']); + let a = x_pos; + let b = y_pos; + + if (x['tile_order'][1] == "-") { + a = numRows - 1 - a; } + + if (x['tile_order'][2] == "-") { + b = numCols - 1 - b; + } + + if (x['tile_order'][0] == "c") { + let tmp = b; + b = a; + a = tmp; + } + + let frameIndex = b * numCols + a; + + return `${x["url"]}/frames/${frameIndex + 1}/rendered`; } else { return null; From 132237d9b795ed55208e6a82795c7585a1ca427d Mon Sep 17 00:00:00 2001 From: Birm Date: Fri, 23 May 2025 14:12:40 -0400 Subject: [PATCH 26/34] streamline logger --- common/util.js | 69 +++++++++++++++++++++++++++++++++++++------------- core/Store.js | 4 +-- 2 files changed, 54 insertions(+), 19 deletions(-) diff --git a/common/util.js b/common/util.js index d22482bd5..4d281f565 100644 --- a/common/util.js +++ b/common/util.js @@ -1089,57 +1089,92 @@ function getBounds(points) { } class Tracker { - constructor(camic, slide, userId, period = 1) { + constructor(camic, slide, userId, period = 1, batchSize = 10) { this.__camic = camic; this.__viewer = camic.viewer; this.__period = period; this.__userId = userId; this.__slide = slide; this.__viewId = this.generateViewId(); + this.__buffer = []; + this.__batchSize = batchSize; + this.__lastSaved = null; + + // Save buffer when user leaves page + window.addEventListener('beforeunload', this.handleUnload.bind(this)); } generateViewId() { return crypto.randomUUID(); } + start() { if (!this.__time) { this.__time = setInterval(this.record.bind(this), this.__period * 1000); } } + stop() { if (this.__time) clearInterval(this.__time); + this.__time = null; } record() { const viewer = this.__viewer; const center = viewer.viewport.getCenter(); - const {x, y} = viewer.viewport.viewportToImageCoordinates( - center.x, - center.y, - ); - const image_zoom = viewer.viewport.viewportToImageZoom( - viewer.viewport.getZoom(true), - ); + const { x, y } = viewer.viewport.viewportToImageCoordinates(center.x, center.y); + const z = viewer.viewport.viewportToImageZoom(viewer.viewport.getZoom(true)); + + const point = [Math.round(x), Math.round(y), z]; + + // Skip if the buffer is empty and this point equals the last saved + if (this.__buffer.length === 0 && this.__lastSaved && this.pointsEqual(this.__lastSaved, point)) { + return; + } + + // Skip if point is the same as the last one in the buffer + const lastInBuffer = this.__buffer[this.__buffer.length - 1]; + if (lastInBuffer && this.pointsEqual(lastInBuffer, point)) { + return; + } + + this.__buffer.push(point); + //console.info("debug: tracker added new point") + + if (this.__buffer.length >= this.__batchSize) { + this.flush(); + } + } + + flush() { + if (this.__buffer.length === 0) return; this.__camic.store.addLog({ viewId: this.__viewId, slide: this.__slide, user: this.__userId, - x: Math.round(x), - y: Math.round(y), - z: image_zoom, + points: this.__buffer, time: new Date(), }); + //console.info("sent log") - return { - x: Math.round(x), - y: Math.round(y), - z: image_zoom, - }; - }; + this.__lastSaved = this.__buffer[this.__buffer.length - 1]; + this.__buffer = []; + } + + handleUnload(event) { + // does NOT seem to reliably work. + //console.info("logger, unload case") + this.flush(); + } + + pointsEqual(a, b) { + return a[0] === b[0] && a[1] === b[1] && a[2] === b[2]; + } } + function showSuccessPopup(message) { // show pop-up message to user let popups = document.getElementById('popup-container'); diff --git a/core/Store.js b/core/Store.js index 94f83ea5b..6c688fcc4 100644 --- a/core/Store.js +++ b/core/Store.js @@ -833,6 +833,7 @@ class Store { method: 'POST', credentials: 'include', mode: 'cors', + keepalive: true, headers: { 'Content-Type': 'application/json; charset=utf-8', // "Content-Type": "application/x-www-form-urlencoded", @@ -849,9 +850,8 @@ class Store { const suffix = 'Log/find'; const url = this.base + suffix; const query = { - '_id': '65f564d7dd62a90013124d3e', + '_id': id, }; - return fetch(url + '?' + objToParamStr(query), { credentials: 'include', mode: 'cors', From 9d32fb3fd207ec7f13af7d4c4afc341359d8214f Mon Sep 17 00:00:00 2001 From: Birm Date: Wed, 28 May 2025 16:39:34 -0400 Subject: [PATCH 27/34] update code style checks --- .eslintrc.js | 2 +- package.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index b08c42504..72874e116 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -11,7 +11,7 @@ module.exports = { 'SharedArrayBuffer': 'readonly', }, 'parserOptions': { - 'ecmaVersion': 2018, + 'ecmaVersion': 2020, }, 'rules': { "require-jsdoc" : 0, diff --git a/package.json b/package.json index 299bf1054..1f7a5c1f2 100644 --- a/package.json +++ b/package.json @@ -5,8 +5,8 @@ "scripts": { "test" : "jest", "test-mocha": "mocha test --recursive", - "lint": "eslint ./core/*.js ./components/**/*.js ./common/smartpen/*.js ./apps/*.js ./apps/port/*.js ./apps/heatmap/*.js ./apps/multi/*.js ./apps/labeling/*.js ./apps/loader/*.js ./apps/model/*.js ./apps/segment/*.js ./apps/model/**/*.js ./apps/segment/**/*.js ./apps/viewer/*.js ./apps/dicom-web/*.js --quiet", - "lint-fix": "eslint ./core/*.js ./components/**/*.js ./common/smartpen/*.js ./apps/*.js ./apps/port/*.js ./apps/heatmap/*.js ./apps/multi/*.js ./apps/labeling/*.js ./apps/loader/*.js ./apps/model/*.js ./apps/segment/*.js ./apps/model/**/*.js ./apps/segment/**/*.js ./apps/viewer/*.js ./apps/dicom-web/*.js --quiet --fix" + "lint": "eslint ./core/*.js ./components/**/*.js ./common/smartpen/*.js common/DicomWebMods.js ./apps/*.js ./apps/port/*.js ./apps/heatmap/*.js ./apps/multi/*.js ./apps/labeling/*.js ./apps/loader/*.js ./apps/model/*.js ./apps/segment/*.js ./apps/model/**/*.js ./apps/segment/**/*.js ./apps/viewer/*.js ./apps/dicom-web/*.js --quiet", + "lint-fix": "eslint ./core/*.js ./components/**/*.js ./common/DicomWebMods.js ./common/smartpen/*.js ./apps/*.js ./apps/port/*.js ./apps/heatmap/*.js ./apps/multi/*.js ./apps/labeling/*.js ./apps/loader/*.js ./apps/model/*.js ./apps/segment/*.js ./apps/model/**/*.js ./apps/segment/**/*.js ./apps/viewer/*.js ./apps/dicom-web/*.js --quiet --fix" }, "author": "", "license": "BSD-3-Clause", From 95409ab9b37c4803fd88fa1b0cdaefee8b7009b2 Mon Sep 17 00:00:00 2001 From: Birm Date: Wed, 28 May 2025 16:39:56 -0400 Subject: [PATCH 28/34] update dicomweb render capabilities --- apps/dicom-web/table.js | 2 +- common/DicomWebMods.js | 449 ++++++++++++++++++++-------------------- 2 files changed, 229 insertions(+), 222 deletions(-) diff --git a/apps/dicom-web/table.js b/apps/dicom-web/table.js index 6f966e6ea..6dce66240 100644 --- a/apps/dicom-web/table.js +++ b/apps/dicom-web/table.js @@ -353,7 +353,7 @@ $(document).ready(function() { async function syncSeries(sourceUrl, study, series, modality) { console.log(sourceUrl, study, series, modality); - const result = await store.syncSeries('../../', {"source_url": sourceUrl, study, series, modality}); + const result = await store.syncSeries('../../', {'source_url': sourceUrl, study, series, modality}); console.log('syncSeries:'); console.log(result); } diff --git a/common/DicomWebMods.js b/common/DicomWebMods.js index c527d1d36..0722bdff0 100644 --- a/common/DicomWebMods.js +++ b/common/DicomWebMods.js @@ -1,232 +1,239 @@ // overwrites loadImg function to handle dicom sources. function DicomWebMods() { - async function openSeries(base_url, study_id, series_id) { + async function openSeries(baseUrl, studyId, seriesId) { + try { + // Construct series metadata URL + const seriesUrl = `${baseUrl}/studies/${studyId}/series/${seriesId}/metadata`; + + // Fetch series metadata + const seriesOverview = await fetch(seriesUrl, {mode: 'cors'}) + .then((response) => { + if (!response.ok) throw new Error(`HTTP error! Status: ${response.status}`); + return response.json(); + }); + + if (!Array.isArray(seriesOverview)) { + throw new Error('Unexpected series metadata format'); + } + // Extract instance IDs + const instanceIds = seriesOverview + .map((item) => item?.['00080018']?.['Value']?.[0]) + .filter((id) => typeof id === 'string'); + + if (instanceIds.length === 0) { + throw new Error('No valid instance IDs found'); + } + + // Generate list of instance metadata URLs + const instanceUrls = instanceIds.map( + (id) => `${baseUrl}/studies/${studyId}/series/${seriesId}/instances/${id}/metadata`, + ); + + // Fetch metadata for each instance + const instancePromises = instanceUrls.map(async (url) => { try { - // Construct series metadata URL - const series_url = `${base_url}/studies/${study_id}/series/${series_id}/metadata`; - - // Fetch series metadata - const series_overview = await fetch(series_url, { mode: 'cors' }) - .then(response => { - if (!response.ok) throw new Error(`HTTP error! Status: ${response.status}`); - return response.json(); - }); - - if (!Array.isArray(series_overview)) { - throw new Error("Unexpected series metadata format"); - } - // Extract instance IDs - const instance_ids = series_overview - .map(item => item?.["00080018"]?.["Value"]?.[0]) - .filter(id => typeof id === "string"); - - if (instance_ids.length === 0) { - throw new Error("No valid instance IDs found"); - } - - // Generate list of instance metadata URLs - const instance_urls = instance_ids.map( - id => `${base_url}/studies/${study_id}/series/${series_id}/instances/${id}/metadata` - ); - - // Fetch metadata for each instance - const instance_promises = instance_urls.map(async (url) => { - try { - const response = await fetch(url, { mode: 'cors' }); - if (!response.ok) throw new Error(`Failed to fetch: ${url} (Status: ${response.status})`); - const json = await response.json(); - if (!Array.isArray(json) || json.length === 0) throw new Error("Invalid response structure"); - - const metadata = json[0]; - metadata["url"] = url; - return metadata; - } catch (error) { - console.error("Error fetching instance metadata:", error); - return null; // Skip this instance if it fails - } - }); - - // Wait for all instance requests to complete - const instance_data = (await Promise.all(instance_promises)).filter(item => item !== null); - - if (instance_data.length === 0) { - throw new Error("No valid instance metadata retrieved"); - } - console.log(instance_data) - // Transform result into OpenSeadragon-compatible format - const instance_results = instance_data.map(x => { - try { - let tile_order = "r++"; // default - if (x["00480102"]?.Value && - Array.isArray(x["00480102"].Value) && - x["00480102"].Value.length == 6) { - let [X1, Y1, Z1, X2, Y2, Z2] = x["00480102"].Value; - isRowMajor = Math.abs(X1) < Math.abs(X2); - isColReverse = X1 < 0 || X2 < 0; - isRowReverse = Y1 < 0 || Y2 < 0; - tile_order_proposed = `${isRowMajor ? 'r' : 'c'}${isRowReverse ? '-' : '+'}${isColReverse ? '-' : '+'}`; - - //isColReverse = false; - isRowReverse = !isRowReverse; - //isRowMajor = false; - //isRowMajor = !isRowMajor; - //isColReverse = !isColReverse; - //isRowReverse = !isRowReverse; - let doReverse = true; - doReverse = false; - if (doReverse && !isRowMajor){ - let tmp = isRowReverse; - isRowReverse = isColReverse; - isColReverse = tmp; - } - - tile_order = `${isRowMajor ? 'r' : 'c'}${isRowReverse ? '-' : '+'}${isColReverse ? '-' : '+'}`; - //tile_order = "r++" - console.info(x["00480102"]?.Value, tile_order, tile_order_proposed) - - } - return { - height: x["00480007"]?.["Value"]?.[0] ?? null, - width: x["00480006"]?.["Value"]?.[0] ?? null, - tile_size: x["00280010"]?.["Value"]?.[0] ?? null, - url: x["url"]?.split("/metadata")[0] ?? "", - type: x["00080008"]?.["Value"] ?? [], - tile_order: tile_order, - }; - } catch (error) { - console.error("Error processing instance metadata:", error); - return null; - } - }).filter(x=>{ - if (x == null || x.height == null || x.width == null){ - return false; - } - let types = x['type'] - for (let i=0; i< types.length; i++){ - let v = types[i].toUpperCase(); - if (v.indexOf("LABEL") !== -1 || - v.indexOf("THUMBNAIL") !== -1 || - v.indexOf("MACRO") !==-1 || - v.indexOf("OVERVIEW") !== -1) { - return false; - } - } - return true; - }); - console.log(instance_results) - if (instance_results.length == 0){ - alert("didn't find anything!! Labels only maybe?") - history.back() - } - - // Sort instance_results by width in ascending order - instance_results.sort((a, b) => a.width - b.width); - - // Add an `order` field starting with smallest - instance_results.forEach((item, index) => { - item.order = index; - }); - - // prep result for openseadragon - let tilesources = instance_results.map(x=>{ - return { - // Low-res image layer - height: x['height'], - width: x['width'], - tileSize: x['tile_size'], - minLevel: 0, - maxLevel: x['order'], - getTileUrl: function(level, x_pos, y_pos) { - if (level == x['order']){ - const numRows = Math.ceil(x['height'] / x['tile_size']); - const numCols = Math.ceil(x['width'] / x['tile_size']); - let a = x_pos; - let b = y_pos; - - if (x['tile_order'][1] == "-") { - a = numRows - 1 - a; - } - - if (x['tile_order'][2] == "-") { - b = numCols - 1 - b; - } - - if (x['tile_order'][0] == "c") { - let tmp = b; - b = a; - a = tmp; - } - - let frameIndex = b * numCols + a; - - - return `${x["url"]}/frames/${frameIndex + 1}/rendered`; - } else { - return null; - } - } - } - }) - - return tilesources - + const response = await fetch(url, {mode: 'cors'}); + if (!response.ok) throw new Error(`Failed to fetch: ${url} (Status: ${response.status})`); + const json = await response.json(); + if (!Array.isArray(json) || json.length === 0) throw new Error('Invalid response structure'); + + const metadata = json[0]; + metadata['url'] = url; + return metadata; } catch (error) { - console.error("Error in openSeries:", error); - } - } - Store.prototype.default_findSlide = Store.prototype.findSlide; - Store.prototype.findSlide = function(slide, specimen, study, location, q, collection) { - } - CaMic.prototype.loadImg = function(func) { - // override for multi image as single viewport image simulation - OpenSeadragon.Viewport.prototype.viewportToImageCoordinates = function(x,y){ - let i = this.viewer.world._items.length - 1 - return this.viewer.world.getItemAt(i).viewportToImageCoordinates(x,y) + console.error('Error fetching instance metadata:', error); + return null; // Skip this instance if it fails } - OpenSeadragon.Viewport.prototype.viewportToImageZoom = function(z){ - let i = this.viewer.world._items.length - 1 - return this.viewer.world.getItemAt(i).viewportToImageZoom(z) + }); + + // Wait for all instance requests to complete + const instanceData = (await Promise.all(instancePromises)).filter((item) => item !== null); + + if (instanceData.length === 0) { + throw new Error('No valid instance metadata retrieved'); + } + console.log(instanceData); + // Transform result into OpenSeadragon-compatible format + const instanceResults = instanceData.map((x) => { + try { + let instanceHeight = x['00480007']?.['Value']?.[0] ?? null; + let instanceWidth = x['00480006']?.['Value']?.[0] ?? null; + let tileSize = x['00280010']?.['Value']?.[0] ?? null; + // instanceWidth = tileSize*Math.ceil(instanceWidth/tileSize) + instanceHeight = tileSize*Math.ceil(instanceHeight/tileSize); + let tileMap = {}; + if (x['52009230']?.Value && + Array.isArray(x['52009230'].Value) && + x['52009230'].Value.length > 0) { + console.log('sparse!'); + let frames = x['52009230']?.Value || []; + for (let i = 0; i < frames.length; i++) { + const frame = frames[i]; + const item = frame['0048021A']?.Value?.[0]; + const col = item?.['0048021E']?.Value?.[0]; + const row = item?.['0048021F']?.Value?.[0]; + if (col !== undefined && row !== undefined && tileSize) { + const tileX = Math.floor(col / tileSize); + const tileY = Math.floor(row / tileSize); + tileMap[`${tileX}_${tileY}`] = i + 1; + // console.log(row, col, tileX, tileY, i+1) + } + } + console.log(tileMap); + } + + return { + height: instanceHeight, + width: instanceWidth, + tileSize: tileSize, + url: x['url']?.split('/metadata')[0] ?? '', + type: x['00080008']?.['Value'] ?? [], + tileMap: tileMap, + }; + } catch (error) { + console.error('Error processing instance metadata:', error); + return null; } - OpenSeadragon.Viewport.prototype.imageToViewportZoom = function(z){ - let i = this.viewer.world._items.length - 1 - return this.viewer.world.getItemAt(i).imageToViewportZoom(z) + }).filter((x)=>{ + if (x == null || x.height == null || x.width == null || x.height < x.tileSize || x.width < x.tileSize) { + return false; } - OpenSeadragon.Viewport.prototype.imageToViewportCoordinates = function(x,y){ - let i = this.viewer.world._items.length - 1 - return this.viewer.world.getItemAt(i).imageToViewportCoordinates(x,y) + let types = x['type']; + for (let i=0; i< types.length; i++) { + let v = types[i].toUpperCase(); + if (v.indexOf('LABEL') !== -1 || + v.indexOf('THUMBNAIL') !== -1 || + v.indexOf('MACRO') !==-1 || + v.indexOf('OVERVIEW') !== -1) { + return false; + } } - var urlParams = new URLSearchParams(window.location.search); - let encodedUrl = urlParams.get('source') || "https%3A%2F%2Fihe.j4care.com%3A18443%2Fdcm4chee-arc%2Faets%2FDCM4CHEE%2Frs"; - let base_url = decodeURIComponent(encodedUrl); - let study_id = urlParams.get('study'); - let series_id = urlParams.get('series'); - this.slideId = series_id - this.slideName = series_id - img_id = this.slideId - var imagingHelper = new OpenSeadragonImaging.ImagingHelper({ - viewer: this.viewer - }); - imagingHelper.setMaxZoom(1); - openSeries(base_url, study_id, series_id).then(tilesources=>{ - this.viewer.open(tilesources) - let x = {} - x['_id'] = "0" - x.name = this.slideName - x.mpp = this.mpp; - x.mpp_x = this.mpp_x; - x.mpp_y = this.mpp_y; - x.location = img_id; - x.url = tilesources; - if (func && typeof func === 'function'){ - func.call(null, x); + return true; + }); + console.log(instanceResults); + if (instanceResults.length == 0) { + alert('didn\'t find anything!! Labels only maybe?'); + history.back(); + } + + // Sort instanceResults by width in ascending order + instanceResults.sort((a, b) => a.width - b.width); + + // Add an `order` field starting with smallest + instanceResults.forEach((item, index) => { + item.order = index; + }); + + // prep result for openseadragon + let tilesources = instanceResults.map((x)=>{ + return { + // Low-res image layer + height: x['height'], + width: x['width'], + tileSize: x['tileSize'], + minLevel: 0, + maxLevel: x['order'], + getTileBounds: function(level, col, row) { + const tileSize = x['tileSize']; + const fullWidth = x['width']; + const fullHeight = x['height']; + + const xPx = col * tileSize; + const yPx = row * tileSize; + + const tileWidth = Math.min(tileSize, fullWidth - xPx); + const tileHeight = Math.min(tileSize, fullHeight - yPx); + // console.log(xPx, yPx, tileHeight, tileWidth) + return new OpenSeadragon.Rect( + xPx / x['width'], + yPx / x['width'], + tileSize / x['width'], + tileSize / x['width'], + ); + }, + getTileUrl: function(level, xPos, yPos) { + if (level == x['order']) { + const numRows = Math.ceil(x['height'] / x['tileSize']); + const numCols = Math.ceil(x['width'] / x['tileSize']); + let numDir = numCols; + let a = xPos; + let b = yPos; + + if (!x['tileMap'] || Object.keys(x['tileMap']).length === 0) { + let frameIndex = b * numCols + a; + return `${x['url']}/frames/${frameIndex + 1}/rendered`; + } else { + let tileIdx = x['tileMap'][`${xPos}_${yPos}`]; + if (tileIdx !== undefined) { + return `${x['url']}/frames/${tileIdx}/rendered`; + } else { + return null; + } } - Loading.text.textContent = `Loading Slide...`; - }).catch(e=>{ - console.error(e) - Loading.text.textContent = "ERROR - Slide May be Broken or Unsupported" - //if(func && typeof func === 'function') func.call(null,{hasError:true,message:e}); - }) - + } else { + return null; + } + }, + }; + }); + + return tilesources; + } catch (error) { + console.error('Error in openSeries:', error); } -} \ No newline at end of file + } + Store.prototype.default_findSlide = Store.prototype.findSlide; + Store.prototype.findSlide = function(slide, specimen, study, location, q, collection) { + }; + CaMic.prototype.loadImg = function(func) { + // override for multi image as single viewport image simulation + OpenSeadragon.Viewport.prototype.viewportToImageCoordinates = function(x, y) { + let i = this.viewer.world._items.length - 1; + return this.viewer.world.getItemAt(i).viewportToImageCoordinates(x, y); + }; + OpenSeadragon.Viewport.prototype.viewportToImageZoom = function(z) { + let i = this.viewer.world._items.length - 1; + return this.viewer.world.getItemAt(i).viewportToImageZoom(z); + }; + OpenSeadragon.Viewport.prototype.imageToViewportZoom = function(z) { + let i = this.viewer.world._items.length - 1; + return this.viewer.world.getItemAt(i).imageToViewportZoom(z); + }; + OpenSeadragon.Viewport.prototype.imageToViewportCoordinates = function(x, y) { + let i = this.viewer.world._items.length - 1; + return this.viewer.world.getItemAt(i).imageToViewportCoordinates(x, y); + }; + var urlParams = new URLSearchParams(window.location.search); + let encodedUrl = urlParams.get('source') || 'https%3A%2F%2Fihe.j4care.com%3A18443%2Fdcm4chee-arc%2Faets%2FDCM4CHEE%2Frs'; + let baseUrl = decodeURIComponent(encodedUrl); + let studyId = urlParams.get('study'); + let seriesId = urlParams.get('series'); + this.slideId = seriesId; + this.slideName = seriesId; + imgId = this.slideId; + var imagingHelper = new OpenSeadragonImaging.ImagingHelper({ + viewer: this.viewer, + }); + imagingHelper.setMaxZoom(1); + openSeries(baseUrl, studyId, seriesId).then((tilesources)=>{ + this.viewer.open(tilesources); + let x = {}; + x['_id'] = '0'; + x.name = this.slideName; + x.mpp = this.mpp; + x.mpp_x = this.mpp_x; + x.mpp_y = this.mpp_y; + x.location = imgId; + x.url = tilesources; + if (func && typeof func === 'function') { + func.call(null, x); + } + Loading.text.textContent = `Loading Slide...`; + }).catch((e)=>{ + console.error(e); + Loading.text.textContent = 'ERROR - Slide May be Broken or Unsupported'; + // if(func && typeof func === 'function') func.call(null,{hasError:true,message:e}); + }); + }; +} From bcde3dd44830a198633de3be9f67ed0ada58b758 Mon Sep 17 00:00:00 2001 From: Birm Date: Wed, 28 May 2025 18:59:37 -0400 Subject: [PATCH 29/34] optional tile debug mode --- common/DicomWebMods.js | 74 ++++++++++++++++++++++++++++++++++-------- 1 file changed, 60 insertions(+), 14 deletions(-) diff --git a/common/DicomWebMods.js b/common/DicomWebMods.js index 0722bdff0..743885eec 100644 --- a/common/DicomWebMods.js +++ b/common/DicomWebMods.js @@ -1,5 +1,7 @@ - // overwrites loadImg function to handle dicom sources. + +// special: for 'sparse' tiles, show a debug overlay on render. +showDebugTiles = false; function DicomWebMods() { async function openSeries(baseUrl, studyId, seriesId) { try { @@ -55,11 +57,12 @@ function DicomWebMods() { } console.log(instanceData); // Transform result into OpenSeadragon-compatible format - const instanceResults = instanceData.map((x) => { + var instanceResults = instanceData.map((x) => { try { let instanceHeight = x['00480007']?.['Value']?.[0] ?? null; let instanceWidth = x['00480006']?.['Value']?.[0] ?? null; let tileSize = x['00280010']?.['Value']?.[0] ?? null; + console.info('x, y', x['00280010']?.['Value']?.[0], x['00280011']?.['Value']?.[0]); // instanceWidth = tileSize*Math.ceil(instanceWidth/tileSize) instanceHeight = tileSize*Math.ceil(instanceHeight/tileSize); let tileMap = {}; @@ -73,10 +76,12 @@ function DicomWebMods() { const item = frame['0048021A']?.Value?.[0]; const col = item?.['0048021E']?.Value?.[0]; const row = item?.['0048021F']?.Value?.[0]; + const physRow = item?.['0040072A']?.Value?.[0]; + const physCol = item?.['0040073A']?.Value?.[0]; if (col !== undefined && row !== undefined && tileSize) { - const tileX = Math.floor(col / tileSize); - const tileY = Math.floor(row / tileSize); - tileMap[`${tileX}_${tileY}`] = i + 1; + const tileX = Math.floor((col-1) / tileSize); + const tileY = Math.floor((row-1) / tileSize); + tileMap[`${tileX}_${tileY}`] = {'idx': i + 1, 'col': col-1, 'row': row-1, 'physRow': physRow, 'physCol': physCol}; // console.log(row, col, tileX, tileY, i+1) } } @@ -125,6 +130,20 @@ function DicomWebMods() { item.order = index; }); + if (showDebugTiles) { + let newInstanceResults = []; + + for (let item of instanceResults) { + // let item = instanceResults[x]; + newInstanceResults.push(item); + let item2 = JSON.parse(JSON.stringify(item)); + item2.debug = true; + newInstanceResults.push(item2); + } + + instanceResults = newInstanceResults; + } + // prep result for openseadragon let tilesources = instanceResults.map((x)=>{ return { @@ -153,20 +172,46 @@ function DicomWebMods() { ); }, getTileUrl: function(level, xPos, yPos) { - if (level == x['order']) { + const debugTile = x['debug'] || false; // Toggle this to enable/disable debug mode + + if (level == x['order']== 1) { const numRows = Math.ceil(x['height'] / x['tileSize']); const numCols = Math.ceil(x['width'] / x['tileSize']); - let numDir = numCols; - let a = xPos; - let b = yPos; if (!x['tileMap'] || Object.keys(x['tileMap']).length === 0) { - let frameIndex = b * numCols + a; + let frameIndex = yPos * numCols + xPos; return `${x['url']}/frames/${frameIndex + 1}/rendered`; - } else { - let tileIdx = x['tileMap'][`${xPos}_${yPos}`]; + } else if (x['tileMap'].hasOwnProperty(`${xPos}_${yPos}`)) { + let tileIdx = x['tileMap'][`${xPos}_${yPos}`]['idx']; + let tileRow = x['tileMap'][`${xPos}_${yPos}`]['row']; + let tileCol = x['tileMap'][`${xPos}_${yPos}`]['col']; + let physRow = x['tileMap'][`${xPos}_${yPos}`]['physRow']; + let physCol = x['tileMap'][`${xPos}_${yPos}`]['physCol']; if (tileIdx !== undefined) { - return `${x['url']}/frames/${tileIdx}/rendered`; + let tileUrl = `${x['url']}/frames/${tileIdx}/rendered`; + let lgFont = 50 * (x['tileSize']/1024); + let smFont = 30 * (x['tileSize']/1024); + if (debugTile) { + const svg = ` + + + idx: ${tileIdx} + dcm R: ${tileRow} + dcm C: ${tileCol} + phys R: ${physRow} + phys C: ${physCol} + L: ${level} + X: ${xPos} + Y: ${yPos} + + `; + const encoded = encodeURIComponent(svg) + .replace(/'/g, '%27') + .replace(/"/g, '%22'); + return `data:image/svg+xml;charset=UTF-8,${encoded}`; + } else { + return tileUrl; + } } else { return null; } @@ -175,9 +220,10 @@ function DicomWebMods() { return null; } }, + }; }); - + console.log(tilesources); return tilesources; } catch (error) { console.error('Error in openSeries:', error); From b5f790ee541aaf7126b5c1627a334c6e7791b979 Mon Sep 17 00:00:00 2001 From: Birm Date: Fri, 30 May 2025 10:59:12 -0400 Subject: [PATCH 30/34] update google public url --- apps/dicom-web/table.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/dicom-web/table.js b/apps/dicom-web/table.js index 6dce66240..a72d56581 100644 --- a/apps/dicom-web/table.js +++ b/apps/dicom-web/table.js @@ -8,7 +8,7 @@ const sources = [{ }, { 'name': 'google', - 'url': 'https://dicomwebproxy-bqmq3usc3a-uc.a.run.app/dicomWeb', + 'url': 'https://dicomwebproxy.app/dicomWeb', }]; // const j4careStudiesUrl = 'https://development.j4care.com:11443/dcm4chee-arc/aets/DCM4CHEE/rs' // const dicomWebStudiesUrl = 'https://dicomwebproxy-bqmq3usc3a-uc.a.run.app/dicomWeb' From c5bc612d4e929b562f24788bc4a3eda50d5808f8 Mon Sep 17 00:00:00 2001 From: Birm Date: Mon, 2 Jun 2025 15:23:26 -0400 Subject: [PATCH 31/34] add z slice limit where applicable, new source --- apps/dicom-web/table.js | 35 +++++++++++++++++++++++++++++------ common/DicomWebMods.js | 35 ++++++++++++++++++++++++++++------- core/CaMic.js | 1 + 3 files changed, 58 insertions(+), 13 deletions(-) diff --git a/apps/dicom-web/table.js b/apps/dicom-web/table.js index a72d56581..86a3ae490 100644 --- a/apps/dicom-web/table.js +++ b/apps/dicom-web/table.js @@ -9,6 +9,9 @@ const sources = [{ }, { 'name': 'google', 'url': 'https://dicomwebproxy.app/dicomWeb', +},{ + 'name':'BMD', + 'url': 'https://dicom-wg26.bmd-software.com/ext/dicom-web' }]; // const j4careStudiesUrl = 'https://development.j4care.com:11443/dcm4chee-arc/aets/DCM4CHEE/rs' // const dicomWebStudiesUrl = 'https://dicomwebproxy-bqmq3usc3a-uc.a.run.app/dicomWeb' @@ -36,6 +39,9 @@ const pageStates = { }, { 'name': 'google', 'url': 'https://dicomwebproxy-bqmq3usc3a-uc.a.run.app/dicomWeb', + },{ + 'name':'BMD', + 'url': 'https://dicom-wg26.bmd-software.com/ext/dicom-web' }], }, studies: { @@ -147,13 +153,30 @@ function initialize() { } datatable = $('#datatable').DataTable({ ...datatableConfig, - 'data': pageStates[pageStates.status].data, - 'columns': [ - {data: '0020000D.Value.0', title: 'Study Id', render: generateLink}, - {data: '00100020.Value.0', title: 'Name'}, - {data: 'source', title: 'Source'}, - ], + data: pageStates[pageStates.status].data, + columns: [ + { + data: null, + title: 'Study Id', + render: function (data, type, row) { + const value = row?.['0020000D']?.Value?.[0] ?? ''; + return generateLink(value, type, row); + } + }, + { + data: null, + title: 'Name', + render: function (data, type, row) { + return row?.['00100020']?.Value?.[0] ?? ''; + } + }, + { + data: 'source', + title: 'Source' + } + ] }); + }); break; diff --git a/common/DicomWebMods.js b/common/DicomWebMods.js index 743885eec..647052f39 100644 --- a/common/DicomWebMods.js +++ b/common/DicomWebMods.js @@ -2,6 +2,8 @@ // special: for 'sparse' tiles, show a debug overlay on render. showDebugTiles = false; +// special: for z index multi-plane/focal images, pick only one z. +whichZ = 1; // -1 means all no matter what. function DicomWebMods() { async function openSeries(baseUrl, studyId, seriesId) { try { @@ -66,6 +68,7 @@ function DicomWebMods() { // instanceWidth = tileSize*Math.ceil(instanceWidth/tileSize) instanceHeight = tileSize*Math.ceil(instanceHeight/tileSize); let tileMap = {}; + let uniquePhysZ = []; if (x['52009230']?.Value && Array.isArray(x['52009230'].Value) && x['52009230'].Value.length > 0) { @@ -78,14 +81,16 @@ function DicomWebMods() { const row = item?.['0048021F']?.Value?.[0]; const physRow = item?.['0040072A']?.Value?.[0]; const physCol = item?.['0040073A']?.Value?.[0]; + const physZ = item?.['0040074A']?.Value?.[0]; if (col !== undefined && row !== undefined && tileSize) { const tileX = Math.floor((col-1) / tileSize); const tileY = Math.floor((row-1) / tileSize); - tileMap[`${tileX}_${tileY}`] = {'idx': i + 1, 'col': col-1, 'row': row-1, 'physRow': physRow, 'physCol': physCol}; + tileMap[`${tileX}_${tileY}`] = {'idx': i + 1, 'col': col-1, 'row': row-1, 'physRow': physRow, 'physCol': physCol, 'physZ': physZ}; // console.log(row, col, tileX, tileY, i+1) } } console.log(tileMap); + uniquePhysZ = [...new Set(Object.values(tileMap).map(tile => tile.physZ))].sort((a, b) => a - b); } return { @@ -95,20 +100,21 @@ function DicomWebMods() { url: x['url']?.split('/metadata')[0] ?? '', type: x['00080008']?.['Value'] ?? [], tileMap: tileMap, + uniquePhysZ: uniquePhysZ, }; } catch (error) { console.error('Error processing instance metadata:', error); return null; } }).filter((x)=>{ - if (x == null || x.height == null || x.width == null || x.height < x.tileSize || x.width < x.tileSize) { + if (x == null || x.height == null || x.width == null) { return false; } let types = x['type']; for (let i=0; i< types.length; i++) { let v = types[i].toUpperCase(); if (v.indexOf('LABEL') !== -1 || - v.indexOf('THUMBNAIL') !== -1 || + v.indexOf('THUMBNAIL') !==-1 || v.indexOf('MACRO') !==-1 || v.indexOf('OVERVIEW') !== -1) { return false; @@ -129,6 +135,18 @@ function DicomWebMods() { instanceResults.forEach((item, index) => { item.order = index; }); + // get a true list of possible z values + const globalUniquePhysZ = [ + ...new Set(instanceResults.flatMap(inst => inst.uniquePhysZ)) + ].sort((a, b) => a - b); + + // picking a z slice + if (whichZ == -1 || globalUniquePhysZ.length == 0){ + whichZ = false; // sinal no slices to filter between + } else { + whichZ = Math.min(Math.max(whichZ, 0), globalUniquePhysZ.length) + instanceResults = instanceResults.filter(inst => inst.uniquePhysZ.includes(globalUniquePhysZ[whichZ])) + } if (showDebugTiles) { let newInstanceResults = []; @@ -187,7 +205,9 @@ function DicomWebMods() { let tileCol = x['tileMap'][`${xPos}_${yPos}`]['col']; let physRow = x['tileMap'][`${xPos}_${yPos}`]['physRow']; let physCol = x['tileMap'][`${xPos}_${yPos}`]['physCol']; - if (tileIdx !== undefined) { + let physZ = x['tileMap'][`${xPos}_${yPos}`]['physZ']; + + if (tileIdx !== undefined && (whichZ === false || physZ == globalUniquePhysZ[whichZ])) { let tileUrl = `${x['url']}/frames/${tileIdx}/rendered`; let lgFont = 50 * (x['tileSize']/1024); let smFont = 30 * (x['tileSize']/1024); @@ -200,9 +220,10 @@ function DicomWebMods() { dcm C: ${tileCol} phys R: ${physRow} phys C: ${physCol} - L: ${level} - X: ${xPos} - Y: ${yPos} + phys Z: ${physZ} + L: ${level} + X: ${xPos} + Y: ${yPos} `; const encoded = encodeURIComponent(svg) diff --git a/core/CaMic.js b/core/CaMic.js index 1eb1a38f2..647580764 100644 --- a/core/CaMic.js +++ b/core/CaMic.js @@ -44,6 +44,7 @@ class CaMic { hasMeasurementTool: true, hasPatchManager: true, hasHeatmap: false, + crossOriginPolicy: 'Anonymous', }; extend(this.setting, options); From c0d1dc5e380f01be35dd89f9b49c287a465014c2 Mon Sep 17 00:00:00 2001 From: Birm Date: Mon, 2 Jun 2025 15:26:08 -0400 Subject: [PATCH 32/34] code lint style fix --- apps/dicom-web/table.js | 27 +++++++++++++-------------- common/DicomWebMods.js | 12 ++++++------ 2 files changed, 19 insertions(+), 20 deletions(-) diff --git a/apps/dicom-web/table.js b/apps/dicom-web/table.js index 86a3ae490..b4710805f 100644 --- a/apps/dicom-web/table.js +++ b/apps/dicom-web/table.js @@ -9,9 +9,9 @@ const sources = [{ }, { 'name': 'google', 'url': 'https://dicomwebproxy.app/dicomWeb', -},{ - 'name':'BMD', - 'url': 'https://dicom-wg26.bmd-software.com/ext/dicom-web' +}, { + 'name': 'BMD', + 'url': 'https://dicom-wg26.bmd-software.com/ext/dicom-web', }]; // const j4careStudiesUrl = 'https://development.j4care.com:11443/dcm4chee-arc/aets/DCM4CHEE/rs' // const dicomWebStudiesUrl = 'https://dicomwebproxy-bqmq3usc3a-uc.a.run.app/dicomWeb' @@ -39,9 +39,9 @@ const pageStates = { }, { 'name': 'google', 'url': 'https://dicomwebproxy-bqmq3usc3a-uc.a.run.app/dicomWeb', - },{ - 'name':'BMD', - 'url': 'https://dicom-wg26.bmd-software.com/ext/dicom-web' + }, { + 'name': 'BMD', + 'url': 'https://dicom-wg26.bmd-software.com/ext/dicom-web', }], }, studies: { @@ -158,25 +158,24 @@ function initialize() { { data: null, title: 'Study Id', - render: function (data, type, row) { + render: function(data, type, row) { const value = row?.['0020000D']?.Value?.[0] ?? ''; return generateLink(value, type, row); - } + }, }, { data: null, title: 'Name', - render: function (data, type, row) { + render: function(data, type, row) { return row?.['00100020']?.Value?.[0] ?? ''; - } + }, }, { data: 'source', - title: 'Source' - } - ] + title: 'Source', + }, + ], }); - }); break; diff --git a/common/DicomWebMods.js b/common/DicomWebMods.js index 647052f39..422c08a56 100644 --- a/common/DicomWebMods.js +++ b/common/DicomWebMods.js @@ -2,7 +2,7 @@ // special: for 'sparse' tiles, show a debug overlay on render. showDebugTiles = false; -// special: for z index multi-plane/focal images, pick only one z. +// special: for z index multi-plane/focal images, pick only one z. whichZ = 1; // -1 means all no matter what. function DicomWebMods() { async function openSeries(baseUrl, studyId, seriesId) { @@ -90,7 +90,7 @@ function DicomWebMods() { } } console.log(tileMap); - uniquePhysZ = [...new Set(Object.values(tileMap).map(tile => tile.physZ))].sort((a, b) => a - b); + uniquePhysZ = [...new Set(Object.values(tileMap).map((tile) => tile.physZ))].sort((a, b) => a - b); } return { @@ -137,15 +137,15 @@ function DicomWebMods() { }); // get a true list of possible z values const globalUniquePhysZ = [ - ...new Set(instanceResults.flatMap(inst => inst.uniquePhysZ)) + ...new Set(instanceResults.flatMap((inst) => inst.uniquePhysZ)), ].sort((a, b) => a - b); // picking a z slice - if (whichZ == -1 || globalUniquePhysZ.length == 0){ + if (whichZ == -1 || globalUniquePhysZ.length == 0) { whichZ = false; // sinal no slices to filter between } else { - whichZ = Math.min(Math.max(whichZ, 0), globalUniquePhysZ.length) - instanceResults = instanceResults.filter(inst => inst.uniquePhysZ.includes(globalUniquePhysZ[whichZ])) + whichZ = Math.min(Math.max(whichZ, 0), globalUniquePhysZ.length); + instanceResults = instanceResults.filter((inst) => inst.uniquePhysZ.includes(globalUniquePhysZ[whichZ])); } if (showDebugTiles) { From fc9a08b168e66a8f726bd314f4a353e28fa6342e Mon Sep 17 00:00:00 2001 From: Birm Date: Mon, 2 Jun 2025 16:32:51 -0400 Subject: [PATCH 33/34] color split in xml2geo.js --- apps/port/xml2geo.js | 58 ++++++++++++++++++++++++-------------------- 1 file changed, 32 insertions(+), 26 deletions(-) diff --git a/apps/port/xml2geo.js b/apps/port/xml2geo.js index 7e1d99613..dd050d65a 100644 --- a/apps/port/xml2geo.js +++ b/apps/port/xml2geo.js @@ -1,5 +1,9 @@ parser = new DOMParser(); +function generateRandomId(length = 6) { + return Math.random().toString(36).substr(2, length); +} + var template = { 'provenance': { 'image': { @@ -20,39 +24,50 @@ var template = { }, 'geometries': { 'type': 'FeatureCollection', + 'features': [], }, }; var aperioMap = { '0': 'Polygon', '1': 'Polygon', - '2': 'Polygon', // rectangle but should work?? haven't seen one yet + '2': 'Polygon', '4': 'Polyline', }; function xml2geo() { - let features = []; let input = document.getElementById('xml_in').value; xmlDoc = parser.parseFromString(input, 'text/xml'); - let annotations = xmlDoc.getElementsByTagName('Annotation'); // Assuming regions are inside 'Annotation' elements + let annotations = xmlDoc.getElementsByTagName('Annotation'); + let slideId = document.getElementById('slide_id').value; + let annotName = document.getElementById('annot_name').value; + + // Store objects per unique color + let outputMap = {}; for (let annotation of annotations) { - let annotationType = annotation.getAttribute('Type') || '0'; // Default to '0' if Type is not provided - let annotationLineColor = annotation.getAttribute('LineColor'); // Get LineColor from the parent annotation + let annotationType = annotation.getAttribute('Type') || '0'; + let annotationLineColor = annotation.getAttribute('LineColor') || '0'; let annotationId = annotation.getAttribute('Id'); - console.log('Processing Annotation ID:', annotationId, 'with Type:', annotationType); + let hexColor = `#${parseInt(annotationLineColor).toString(16).padStart(6, '0')}`; + if (!outputMap[hexColor]) { + let randomId = generateRandomId(); + outputMap[hexColor] = JSON.parse(JSON.stringify(template)); + outputMap[hexColor]['provenance']['image']['slide'] = slideId; + outputMap[hexColor]['provenance']['analysis']['execution_id'] = randomId; + outputMap[hexColor]['provenance']['analysis']['name'] = `${annotName}_${hexColor}`; + outputMap[hexColor]['properties']['annotations']['name'] = `${annotName}_${hexColor}`; + } - let regions = annotation.getElementsByTagName('Region'); // Get regions within this annotation + let regions = annotation.getElementsByTagName('Region'); for (let region of regions) { let regionId = region.getAttribute('Id'); - regionType = annotationType || region.getAttribute('Type'); // parent annotation type if present, else own (odd?) - regionType = aperioMap[regionType]; - console.log('Processing Region ID:', regionId, 'as', regionType); + let regionType = aperioMap[annotationType || region.getAttribute('Type')] || 'Polygon'; let vertices = region.getElementsByTagName('Vertex'); let coordinates = []; - let minX = 99e99; let maxX = 0; let minY = 99e99; let maxY = 0; + let minX = 99e99, maxX = 0, minY = 99e99, maxY = 0; for (let vertex of vertices) { let x = parseFloat(vertex.getAttribute('X')); @@ -63,18 +78,15 @@ function xml2geo() { maxY = Math.max(maxY, y); coordinates.push([x, y]); } + let isFill = false; - // **Detect Polygon vs. Polyline** if (regionType === 'Polygon') { - coordinates.push(coordinates[0]); // Close the polygon by repeating the first point + coordinates.push(coordinates[0]); isFill = true; } let boundRect = [[minX, minY], [minX, maxY], [maxX, maxY], [maxX, minY], [minX, minY]]; - // **Detect Color** - let hexColor = annotationLineColor ? `#${parseInt(annotationLineColor).toString(16).padStart(6, '0')}` : '#000000'; - let feature = { 'type': 'Feature', 'geometry': { @@ -96,17 +108,11 @@ function xml2geo() { }, }; - features.push(feature); + outputMap[hexColor]['geometries']['features'].push(feature); } } - let output = Object.assign({}, template); - output['geometries']['features'] = features; - output['provenance']['image']['slide'] = document.getElementById('slide_id').value; - output['provenance']['analysis']['execution'] = document.getElementById('annot_name').value; - output['properties']['annotations']['name'] = document.getElementById('annot_name').value; - output['provenance']['analysis']['name'] = document.getElementById('annot_name').value; - output['provenance']['analysis']['execution_id'] = document.getElementById('annot_name').value; - - document.getElementById('output').textContent = JSON.stringify(output); + // Show all color-specific outputs + let finalOutput = Object.values(outputMap); + document.getElementById('output').textContent = JSON.stringify(finalOutput, null, 2); } From 536eec9c1bbdee88d31e4e4925c35a8f26cb25fb Mon Sep 17 00:00:00 2001 From: Birm Date: Fri, 13 Jun 2025 15:19:54 -0400 Subject: [PATCH 34/34] lint fix xml to geojson script --- apps/port/xml2geo.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/port/xml2geo.js b/apps/port/xml2geo.js index dd050d65a..272f491aa 100644 --- a/apps/port/xml2geo.js +++ b/apps/port/xml2geo.js @@ -67,7 +67,7 @@ function xml2geo() { let vertices = region.getElementsByTagName('Vertex'); let coordinates = []; - let minX = 99e99, maxX = 0, minY = 99e99, maxY = 0; + let minX = 99e99; let maxX = 0; let minY = 99e99; let maxY = 0; for (let vertex of vertices) { let x = parseFloat(vertex.getAttribute('X'));