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+ 'blurred image' ,
20+ 'technical fault'
21+ ]
22+
23+ // Default probability settings
24+ const DEFAULT_PROBABILITIES = {
25+ viewMissing : 0.05 , // 5% chance of a view being missing
26+ needsRepeat : 0.15 // 15% chance of one view needing a repeat
27+ }
28+
29+ const generateViewKey = ( side , view ) => {
30+ const prefix = side === 'right' ? 'right' : 'left'
31+ const viewName = view === 'mediolateral oblique' ? 'MediolateralOblique' : 'Craniocaudal'
32+ return `${ prefix } ${ viewName } `
33+ }
34+
35+ const generateImageUrl = ( side , view , accessionNumber ) => {
36+ const sideCode = side === 'right' ? 'R' : 'L'
37+ const viewCode = view === 'mediolateral oblique' ? 'MLO' : 'CC'
38+ return `/images/mammograms/${ sideCode } -${ viewCode } -${ accessionNumber . replace ( '/' , '-' ) } .dcm`
39+ }
40+
41+ /**
42+ * Generate images for a single view
43+ * @param {Object } params - Parameters for image generation
44+ * @param {string } params.side - Breast side ('right' or 'left')
45+ * @param {string } params.view - View type ('mediolateral oblique' or 'craniocaudal')
46+ * @param {string } params.accessionBase - Base accession number
47+ * @param {number } params.startIndex - Starting index for image numbering
48+ * @param {string } params.startTime - Start timestamp
49+ * @param {boolean } params.isSeedData - Whether generating seed data
50+ * @param {boolean } [params.needsRepeat] - Force this view to be repeated
51+ * @returns {Object } View data with images
52+ */
53+ const generateViewImages = ( { side, view, accessionBase, startIndex, startTime, isSeedData, needsRepeat = false } ) => {
54+ let currentIndex = startIndex
55+ let currentTime = dayjs ( startTime )
56+ const images = [ ]
57+
58+ // Generate initial image
59+ images . push ( {
60+ timestamp : currentTime . toISOString ( ) ,
61+ accessionNumber : `${ accessionBase } /${ currentIndex } ` ,
62+ url : generateImageUrl ( side , view , `${ accessionBase } /${ currentIndex } ` )
63+ } )
64+
65+ // Generate repeat if needed
66+ if ( needsRepeat ) {
67+ currentIndex ++
68+ currentTime = currentTime . add ( faker . number . int ( { min : 25 , max : 50 } ) , 'seconds' )
69+
70+ images . push ( {
71+ timestamp : currentTime . toISOString ( ) ,
72+ accessionNumber : `${ accessionBase } /${ currentIndex } ` ,
73+ url : generateImageUrl ( side , view , `${ accessionBase } /${ currentIndex } ` )
74+ } )
75+ }
76+
77+ return {
78+ side,
79+ view,
80+ viewShort : view === 'mediolateral oblique' ? 'MLO' : 'CC' ,
81+ viewShortWithSide : `${ side === 'right' ? 'R' : 'L' } ${ view === 'mediolateral oblique' ? 'MLO' : 'CC' } ` ,
82+ images,
83+ isRepeat : needsRepeat && isSeedData ,
84+ repeatReason : needsRepeat && isSeedData ? faker . helpers . arrayElement ( REPEAT_REASONS ) : null
85+ }
86+ }
87+
88+ /**
89+ * Generate a complete set of mammogram images
90+ * @param {Object } options - Generation options
91+ * @param {Date|string } [options.startTime] - Starting timestamp (defaults to now)
92+ * @param {boolean } [options.isSeedData=false] - Whether generating seed data
93+ * @param {Object } [options.config] - Optional configuration for specific scenarios
94+ * @param {string } [options.config.repeatView] - Force a specific view to be repeated (e.g. 'RMLO')
95+ * @param {string[] } [options.config.missingViews] - Array of views to omit (e.g. ['RMLO'])
96+ * @param {Object } [options.probabilities] - Override default probabilities
97+ * @returns {Object } Complete mammogram data
98+ */
99+ const generateMammogramImages = ( {
100+ startTime = new Date ( ) ,
101+ isSeedData = false ,
102+ config = { } ,
103+ probabilities = DEFAULT_PROBABILITIES
104+ } = { } ) => {
105+ const accessionBase = faker . number . int ( { min : 100000000 , max : 999999999 } ) . toString ( )
106+ let currentIndex = 1
107+ let currentTime = dayjs ( startTime )
108+ const views = { }
109+
110+ // Determine which view gets repeated (if any)
111+ let viewToRepeat = null
112+ if ( config . repeatView ) {
113+ viewToRepeat = config . repeatView
114+ } else if ( Math . random ( ) < probabilities . needsRepeat ) {
115+ viewToRepeat = faker . helpers . arrayElement ( [ 'RMLO' , 'RCC' , 'LCC' , 'LMLO' ] )
116+ }
117+
118+ // Generate each standard view
119+ STANDARD_VIEWS . forEach ( ( { side, view } ) => {
120+ const viewKey = generateViewKey ( side , view )
121+ const viewShortWithSide = `${ side === 'right' ? 'R' : 'L' } ${ view === 'mediolateral oblique' ? 'MLO' : 'CC' } `
122+
123+ // Skip if this view is in missingViews config
124+ if ( config . missingViews ?. includes ( viewShortWithSide ) ||
125+ ( ! config . missingViews && Math . random ( ) < probabilities . viewMissing ) ) {
126+ return
127+ }
128+
129+ const viewData = generateViewImages ( {
130+ side,
131+ view,
132+ accessionBase,
133+ startIndex : currentIndex ,
134+ startTime : currentTime . toISOString ( ) ,
135+ isSeedData,
136+ needsRepeat : viewToRepeat === viewShortWithSide
137+ } )
138+
139+ views [ viewKey ] = viewData
140+
141+ // Update counters for next view
142+ currentIndex += viewData . images . length
143+ currentTime = currentTime . add ( faker . number . int ( { min : 45 , max : 70 } ) , 'seconds' )
144+ } )
145+
146+ // Calculate metadata
147+ const totalImages = Object . values ( views ) . reduce ( ( sum , view ) => sum + view . images . length , 0 )
148+ const allTimestamps = Object . values ( views )
149+ . flatMap ( view => view . images . map ( img => img . timestamp ) )
150+ . sort ( )
151+
152+ return {
153+ accessionBase,
154+ views,
155+ metadata : {
156+ totalImages,
157+ standardViewsCompleted : Object . keys ( views ) . length === 4 ,
158+ startTime : allTimestamps [ 0 ] ,
159+ endTime : allTimestamps [ allTimestamps . length - 1 ]
160+ }
161+ }
162+ }
163+
164+ module . exports = {
165+ generateMammogramImages,
166+ STANDARD_VIEWS ,
167+ REPEAT_REASONS
168+ }
0 commit comments