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