Skip to content

Commit 0f7a18a

Browse files
Generate image data
1 parent 1ffedaa commit 0f7a18a

File tree

2 files changed

+152
-3
lines changed

2 files changed

+152
-3
lines changed

app/lib/generators/event-generator.js

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ const { faker } = require('@faker-js/faker')
55
const weighted = require('weighted')
66
const dayjs = require('dayjs')
77
const config = require('../../config')
8+
const { generateMammogramImages } = require('./mammogram-generator')
89

910
const NOT_SCREENED_REASONS = [
1011
'Recent mammogram at different facility',
@@ -114,9 +115,6 @@ const generateEvent = ({ slot, participant, clinic, outcomeWeights, forceStatus
114115
...eventBase,
115116
details: {
116117
...eventBase.details,
117-
imagesTaken: eventStatus === 'event_complete'
118-
? ['RCC', 'LCC', 'RMLO', 'LMLO']
119-
: null,
120118
notScreenedReason: eventStatus === 'event_attended_not_screened'
121119
? faker.helpers.arrayElement(NOT_SCREENED_REASONS)
122120
: null,
@@ -139,6 +137,12 @@ const generateEvent = ({ slot, participant, clinic, outcomeWeights, forceStatus
139137
actualEndTime: actualEndTime.toISOString(),
140138
actualDuration: actualEndTime.diff(actualStartTime, 'minute'),
141139
}
140+
141+
// Add mammogram images for completed events
142+
event.mammogramData = generateMammogramImages({
143+
startTime: actualStartTime,
144+
isSeedData: true
145+
})
142146
}
143147

144148
return event
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
// app/lib/generators/mammogram-generator.js
2+
3+
const { faker } = require('@faker-js/faker')
4+
const generateId = require('../utils/id-generator')
5+
const dayjs = require('dayjs')
6+
const weighted = require('weighted')
7+
8+
const STANDARD_VIEWS = [
9+
{ side: 'right', view: 'mediolateral oblique' },
10+
{ side: 'right', view: 'craniocaudal' },
11+
{ side: 'left', view: 'craniocaudal' },
12+
{ side: 'left', view: 'mediolateral oblique' }
13+
]
14+
15+
const REPEAT_REASONS = [
16+
'patient movement',
17+
'positioning issue',
18+
'exposure issue',
19+
'breast not fully imaged',
20+
'blurred image',
21+
'technical fault'
22+
]
23+
24+
// Probability settings for repeats
25+
const REPEAT_PROBABILITIES = {
26+
needsRepeat: 0.15, // 15% chance of needing any repeat
27+
multipleRepeats: 0.2 // 20% chance of needing more than one repeat if already repeating
28+
}
29+
30+
const generateViewKey = (side, view) => {
31+
const prefix = side === 'right' ? 'right' : 'left'
32+
const viewName = view === 'mediolateral oblique' ? 'MediolateralOblique' : 'Craniocaudal'
33+
return `${prefix}${viewName}`
34+
}
35+
36+
const generateImageUrl = (side, view, accessionNumber) => {
37+
const sideCode = side === 'right' ? 'R' : 'L'
38+
const viewCode = view === 'mediolateral oblique' ? 'MLO' : 'CC'
39+
return `/images/mammograms/${sideCode}-${viewCode}-${accessionNumber.replace('/', '-')}.dcm`
40+
}
41+
42+
/**
43+
* Generate images for a single view
44+
* @param {Object} params - Parameters for image generation
45+
* @param {string} params.side - Breast side ('right' or 'left')
46+
* @param {string} params.view - View type ('mediolateral oblique' or 'craniocaudal')
47+
* @param {string} params.accessionBase - Base accession number
48+
* @param {number} params.startIndex - Starting index for image numbering
49+
* @param {string} params.startTime - Start timestamp
50+
* @param {boolean} params.isSeedData - Whether generating seed data or live data
51+
* @returns {Object} View data with images
52+
*/
53+
const generateViewImages = ({ side, view, accessionBase, startIndex, startTime, isSeedData }) => {
54+
let currentIndex = startIndex
55+
let currentTime = dayjs(startTime)
56+
const images = []
57+
const needsRepeat = Math.random() < REPEAT_PROBABILITIES.needsRepeat
58+
59+
// Generate initial image
60+
images.push({
61+
timestamp: currentTime.toISOString(),
62+
accessionNumber: `${accessionBase}/${currentIndex}`,
63+
url: generateImageUrl(side, view, `${accessionBase}/${currentIndex}`)
64+
})
65+
66+
// Generate repeats if needed (and if we're in seed mode or randomly need them)
67+
if (needsRepeat && isSeedData) {
68+
const repeatCount = Math.random() < REPEAT_PROBABILITIES.multipleRepeats ? 2 : 1
69+
70+
for (let i = 0; i < repeatCount; i++) {
71+
currentIndex++
72+
currentTime = currentTime.add(faker.number.int({ min: 25, max: 50 }), 'seconds')
73+
74+
images.push({
75+
timestamp: currentTime.toISOString(),
76+
accessionNumber: `${accessionBase}/${currentIndex}`,
77+
url: generateImageUrl(side, view, `${accessionBase}/${currentIndex}`)
78+
})
79+
}
80+
}
81+
82+
return {
83+
side,
84+
view,
85+
images,
86+
isRepeat: needsRepeat && isSeedData,
87+
repeatReason: needsRepeat && isSeedData ? faker.helpers.arrayElement(REPEAT_REASONS) : null
88+
}
89+
}
90+
91+
/**
92+
* Generate a complete set of mammogram images
93+
* @param {Object} options - Generation options
94+
* @param {Date|string} [options.startTime] - Starting timestamp (defaults to now)
95+
* @param {boolean} [options.isSeedData=false] - Whether generating seed data
96+
* @returns {Object} Complete mammogram data
97+
*/
98+
const generateMammogramImages = ({ startTime = new Date(), isSeedData = false } = {}) => {
99+
const accessionBase = faker.number.int({ min: 100000000, max: 999999999 }).toString()
100+
let currentIndex = 1
101+
let currentTime = dayjs(startTime)
102+
const views = {}
103+
104+
// Generate each standard view
105+
STANDARD_VIEWS.forEach(({ side, view }) => {
106+
const viewKey = generateViewKey(side, view)
107+
const viewData = generateViewImages({
108+
side,
109+
view,
110+
accessionBase,
111+
startIndex: currentIndex,
112+
startTime: currentTime.toISOString(),
113+
isSeedData
114+
})
115+
116+
views[viewKey] = viewData
117+
118+
// Update counters for next view
119+
currentIndex += viewData.images.length
120+
currentTime = currentTime.add(faker.number.int({ min: 45, max: 70 }), 'seconds')
121+
})
122+
123+
// Calculate metadata
124+
const totalImages = Object.values(views).reduce((sum, view) => sum + view.images.length, 0)
125+
const allTimestamps = Object.values(views)
126+
.flatMap(view => view.images.map(img => img.timestamp))
127+
.sort()
128+
129+
return {
130+
accessionBase,
131+
views,
132+
metadata: {
133+
totalImages,
134+
standardViewsCompleted: true,
135+
startTime: allTimestamps[0],
136+
endTime: allTimestamps[allTimestamps.length - 1]
137+
}
138+
}
139+
}
140+
141+
module.exports = {
142+
generateMammogramImages,
143+
STANDARD_VIEWS,
144+
REPEAT_REASONS
145+
}

0 commit comments

Comments
 (0)