import { EngravingRules } from "./EngravingRules" ; import { StaffLine } from "./StaffLine" ; import { PointF2D } from "../../Common/DataObjects/PointF2D" ; import { CanvasVexFlowBackend } from "./VexFlow/CanvasVexFlowBackend" ; import { VexFlowMeasure } from "./VexFlow/VexFlowMeasure" ; import { unitInPixels } from "./VexFlow/VexFlowMusicSheetDrawer" ; import log from "loglevel" ; import { BoundingBox } from "./BoundingBox" ; /**
 * This class calculates and holds the skyline and bottom line information.
 * It also has functions to update areas of the two lines if new elements are
 * added to the staffline (e.g. measure number, annotations, ...)
 */ export class SkyBottomLineCalculator { /** Parent Staffline where the skyline and bottom line is attached */ private mStaffLineParent : StaffLine ; /** Internal array for the skyline */ private mSkyLine : number [ ] ; /** Internal array for the bottomline */ private mBottomLine : number [ ] ; /** Engraving rules for formatting */ private mRules : EngravingRules ; param staffLineParent constructor ( staffLineParent ) { this . mStaffLineParent = staffLineParent ; this . mRules = staffLineParent . ParentMusicSystem . rules ; } /**
     * This method calculates the Sky- and BottomLines for a StaffLine.
     */ public calculateLines ( ) { const arrayLength = Math . max ( Math . ceil ( this . StaffLineParent . PositionAndShape . Size . width * this . SamplingUnit ) , 1 ) ; this . mSkyLine = [ ] ; this . mBottomLine = [ ] ; const tmpCanvas = new CanvasVexFlowBackend ( this . StaffLineParent . ParentMusicSystem . rules ) ; for ( const measure of this . StaffLineParent . Measures as VexFlowMeasure [ ] ) { measure . PositionAndShape . calculateAbsolutePositionsRecursive ( 0 , 0 ) ; const vsStaff = measure . getVFStave ( ) ; let width = vsStaff . getWidth ( ) ; if ( ! ( width > 0 ) && ! measure . IsExtraGraphicalMeasure ) { log . warn ( "SkyBottomLineCalculator: width not > 0 in measure " + measure . MeasureNumber ) ; width = 50 ; } tmpCanvas . initializeHeadless ( width ) ; const ctx = tmpCanvas . getContext ( ) ; const canvas = tmpCanvas . getCanvas ( ) ; width = canvas . width ; const height = canvas . height ; vsStaff . setY ( vsStaff . y + 100 ) ; const oldMeasureWidth = vsStaff . getWidth ( ) ; vsStaff . setWidth ( width ) ; measure . format ( ) ; vsStaff . setWidth ( oldMeasureWidth ) ; try { measure . draw ( ctx ) ; } catch ( ex ) { log . warn ( "SkyBottomLineCalculator.calculateLines.draw" , ex ) ; } const imageData = ctx . getImageData ( 0 , 0 , width , height ) ; const rgbaLength = 4 ; const measureArrayLength = Math . max ( Math . ceil ( measure . PositionAndShape . Size . width * this . mRules . SamplingUnit ) , 1 ) ; const tmpSkyLine = new Array ( measureArrayLength ) ; const tmpBottomLine = new Array ( measureArrayLength ) ; for ( let x = 0 ; x < width ; x ++ ) { for ( let y = 0 ; y < height ; y ++ ) { const yOffset = y * width * rgbaLength ; const bufIndex = yOffset + x * rgbaLength ; const alpha = imageData . data [ bufIndex + 3 ] ; if ( alpha > 0 ) { tmpSkyLine [ x ] = y ; break ; } } for ( let y = height ; y > 0 ; y -- ) { const yOffset = y * width * rgbaLength ; const bufIndex = yOffset + x * rgbaLength ; const alpha = imageData . data [ bufIndex + 3 ] ; if ( alpha > 0 ) { tmpBottomLine [ x ] = y ; break ; } } } for ( let idx = 0 ; idx < tmpSkyLine . length ; idx ++ ) { if ( tmpSkyLine [ idx ] === undefined ) { tmpSkyLine [ idx ] = Math . max ( this . findPreviousValidNumber ( idx , tmpSkyLine ) , this . findNextValidNumber ( idx , tmpSkyLine ) ) ; } } for ( let idx = 0 ; idx < tmpBottomLine . length ; idx ++ ) { if ( tmpBottomLine [ idx ] === undefined ) { tmpBottomLine [ idx ] = Math . max ( this . findPreviousValidNumber ( idx , tmpBottomLine ) , this . findNextValidNumber ( idx , tmpBottomLine ) ) ; } } this . mSkyLine . push ( ... tmpSkyLine ) ; this . mBottomLine . push ( ... tmpBottomLine ) ; const debugTmpCanvas = false ; if ( debugTmpCanvas ) { tmpSkyLine . forEach ( ( y , x ) => this . drawPixel ( new PointF2D ( x , y ) , tmpCanvas ) ) ; tmpBottomLine . forEach ( ( y , x ) => this . drawPixel ( new PointF2D ( x , y ) , tmpCanvas , "blue" ) ) ; const img = canvas . toDataURL ( "image/png" ) ; document . write ( '<img src="' + img + '"/>' ) ; } tmpCanvas . clear ( ) ; } const arrayChunkSize = this . mSkyLine . length / arrayLength ; const subSampledSkyLine = [ ] ; const subSampledBottomLine = [ ] ; for ( let chunkIndex = 0 ; chunkIndex < this . mSkyLine . length ; chunkIndex += arrayChunkSize ) { if ( subSampledSkyLine . length === arrayLength ) { break ; } const endIndex = Math . min ( this . mSkyLine . length , chunkIndex + arrayChunkSize ) ; let chunk = this . mSkyLine . slice ( chunkIndex , endIndex + 1 ) ; subSampledSkyLine . push ( Math . min ( ... chunk ) ) ; chunk = this . mBottomLine . slice ( chunkIndex , endIndex + 1 ) ; subSampledBottomLine . push ( Math . max ( ... chunk ) ) ; } this . mSkyLine = subSampledSkyLine ; this . mBottomLine = subSampledBottomLine ; if ( this . mSkyLine . length !== arrayLength ) { log . debug ( `SkyLine calculation was not correct (${ this . mSkyLine . length } instead of ${ arrayLength })` ) ; } this . mSkyLine = this . mSkyLine . map ( v => ( v - Math . max ( ... this . mSkyLine ) ) / unitInPixels + this . StaffLineParent . TopLineOffset ) ; this . mBottomLine = this . mBottomLine . map ( v => ( v - Math . min ( ... this . mBottomLine ) ) / unitInPixels + this . StaffLineParent . BottomLineOffset ) ; } param start param backend param color private findPreviousValidNumber ( start , tSkyLine ) { for ( let idx = start ; idx >= 0 ; idx -- ) { if ( ! isNaN ( tSkyLine [ idx ] ) ) { return tSkyLine [ idx ] ; } } return 0 ; } param start param backend param color private findNextValidNumber ( start , tSkyLine ) { if ( start >= tSkyLine . length ) { return tSkyLine [ start - 1 ] ; } for ( let idx = start ; idx < tSkyLine . length ; idx ++ ) { if ( ! isNaN ( tSkyLine [ idx ] ) ) { return tSkyLine [ idx ] ; } } return 0 ; } param coord param backend param color private drawPixel ( coord , backend , color = "#FF0000FF" ) { const ctx = backend . getContext ( ) ; const oldStyle = ctx . fillStyle ; ctx . fillStyle = color ; ctx . fillRect ( coord . x , coord . y , 2 , 2 ) ; ctx . fillStyle = oldStyle ; } param start param end public updateSkyLineWithWedge ( start , end ) { let startIndex = Math . floor ( start . x * this . SamplingUnit ) ; let endIndex = Math . ceil ( end . x * this . SamplingUnit ) ; let slope = ( end . y - start . y ) / ( end . x - start . x ) ; if ( endIndex - startIndex <= 1 ) { endIndex ++ ; slope = 0 ; } if ( startIndex < 0 ) { startIndex = 0 ; } if ( startIndex >= this . BottomLine . length ) { startIndex = this . BottomLine . length - 1 ; } if ( endIndex < 0 ) { endIndex = 0 ; } if ( endIndex >= this . BottomLine . length ) { endIndex = this . BottomLine . length ; } this . SkyLine [ startIndex ] = start . y ; for ( let i = startIndex + 1 ; i < Math . min ( endIndex , this . SkyLine . length ) ; i ++ ) { this . SkyLine [ i ] = this . SkyLine [ i - 1 ] + slope / this . SamplingUnit ; } } param start param end public updateBottomLineWithWedge ( start , end ) { let startIndex = Math . floor ( start . x * this . SamplingUnit ) ; let endIndex = Math . ceil ( end . x * this . SamplingUnit ) ; let slope = ( end . y - start . y ) / ( end . x - start . x ) ; if ( endIndex - startIndex <= 1 ) { endIndex ++ ; slope = 0 ; } if ( startIndex < 0 ) { startIndex = 0 ; } if ( startIndex >= this . BottomLine . length ) { startIndex = this . BottomLine . length - 1 ; } if ( endIndex < 0 ) { endIndex = 0 ; } if ( endIndex >= this . BottomLine . length ) { endIndex = this . BottomLine . length ; } this . BottomLine [ startIndex ] = start . y ; for ( let i = startIndex + 1 ; i < endIndex ; i ++ ) { this . BottomLine [ i ] = this . BottomLine [ i - 1 ] + slope / this . SamplingUnit ; } } param start param end param value public updateSkyLineInRange ( startIndex , endIndex , value ) { this . updateInRange ( this . mSkyLine , startIndex , endIndex , value ) ; } param to param start param end param value public updateBottomLineInRange ( startIndex , endIndex , value ) { this . updateInRange ( this . BottomLine , startIndex , endIndex , value ) ; } param to param startIndex param endIndex public resetSkyLineInRange ( startIndex , endIndex ) { this . updateInRange ( this . SkyLine , startIndex , endIndex ) ; } param to param startIndex param endIndex public resetBottomLineInRange ( startIndex , endIndex ) { this . setInRange ( this . BottomLine , startIndex , endIndex ) ; } param value public setSkyLineWithValue ( value ) { this . SkyLine . forEach ( sl => sl = value ) ; } param value public setBottomLineWithValue ( value ) { this . BottomLine . forEach ( bl => bl = value ) ; } public getLeftIndexForPointX ( x , length ) { const index = Math . floor ( x * this . SamplingUnit ) ; if ( index < 0 ) { return 0 ; } if ( index >= length ) { return length - 1 ; } return index ; } public getRightIndexForPointX ( x , length ) { const index = Math . ceil ( x * this . SamplingUnit ) ; if ( index < 0 ) { return 0 ; } if ( index >= length ) { return length - 1 ; } return index ; } /**
     * This method updates the StaffLine Borders with the Sky- and BottomLines Min- and MaxValues.
     */ public updateStaffLineBorders ( ) { this . mStaffLineParent . PositionAndShape . BorderTop = this . getSkyLineMin ( ) ; this . mStaffLineParent . PositionAndShape . BorderMarginTop = this . getSkyLineMin ( ) ; this . mStaffLineParent . PositionAndShape . BorderBottom = this . getBottomLineMax ( ) ; this . mStaffLineParent . PositionAndShape . BorderMarginBottom = this . getBottomLineMax ( ) ; } param staffLine public getSkyLineMin ( ) { return Math . min ( ... this . SkyLine . filter ( s => ! isNaN ( s ) ) ) ; } public getSkyLineMinAtPoint ( point ) { const index = Math . round ( point * this . SamplingUnit ) ; return this . mSkyLine [ index ] ; } param staffLine param startIndex param endIndex public getSkyLineMinInRange ( startIndex , endIndex ) { return this . getMinInRange ( this . SkyLine , startIndex , endIndex ) ; } param staffLine public getBottomLineMax ( ) { return Math . max ( ... this . BottomLine . filter ( s => ! isNaN ( s ) ) ) ; } public getBottomLineMaxAtPoint ( point ) { const index = Math . round ( point * this . SamplingUnit ) ; return this . mBottomLine [ index ] ; } param staffLine param startIndex param endIndex public getBottomLineMaxInRange ( startIndex , endIndex ) { return this . getMaxInRange ( this . BottomLine , startIndex , endIndex ) ; } param boundingBox returns public getBottomLineMaxInBoundingBox ( boundingBox ) { const startPoint = Math . floor ( boundingBox . AbsolutePosition . x + boundingBox . BorderLeft ) ; const endPoint = Math . ceil ( boundingBox . AbsolutePosition . x + boundingBox . BorderRight ) ; return this . getMaxInRange ( this . mBottomLine , startPoint , endPoint ) ; } param boundingBox param topBorder public updateWithBoundingBoxRecursively ( boundingBox ) { if ( boundingBox . ChildElements && boundingBox . ChildElements . length > 0 ) { this . updateWithBoundingBoxRecursively ( boundingBox ) ; } else { const currentTopBorder = boundingBox . BorderTop + boundingBox . AbsolutePosition . y ; const currentBottomBorder = boundingBox . BorderBottom + boundingBox . AbsolutePosition . y ; if ( currentTopBorder < 0 ) { const startPoint = Math . floor ( boundingBox . AbsolutePosition . x + boundingBox . BorderLeft ) ; const endPoint = Math . ceil ( boundingBox . AbsolutePosition . x + boundingBox . BorderRight ) ; this . updateInRange ( this . mSkyLine , startPoint , endPoint , currentTopBorder ) ; } else if ( currentBottomBorder > this . StaffLineParent . StaffHeight ) { const startPoint = Math . floor ( boundingBox . AbsolutePosition . x + boundingBox . BorderLeft ) ; const endPoint = Math . ceil ( boundingBox . AbsolutePosition . x + boundingBox . BorderRight ) ; this . updateInRange ( this . mBottomLine , startPoint , endPoint , currentBottomBorder ) ; } } } param array param startIndex param endIndex param value private updateInRange ( array , startIndex = 0 , endIndex = array . length , value = 0 ) { startIndex = Math . floor ( startIndex * this . SamplingUnit ) ; endIndex = Math . ceil ( endIndex * this . SamplingUnit ) ; if ( endIndex < startIndex ) { throw new Error ( "start index of line is greater than the end index" ) ; } if ( startIndex < 0 ) { startIndex = 0 ; } if ( endIndex > array . length ) { endIndex = array . length ; } for ( let i = startIndex ; i < endIndex ; i ++ ) { array [ i ] = Math . abs ( value ) > Math . abs ( array [ i ] ) ? value : array [ i ] ; } } param array param startIndex param endIndex param value private setInRange ( array , startIndex = 0 , endIndex = array . length , value = 0 ) { startIndex = Math . floor ( startIndex * this . SamplingUnit ) ; endIndex = Math . ceil ( endIndex * this . SamplingUnit ) ; if ( endIndex < startIndex ) { throw new Error ( "start index of line is greater then the end index" ) ; } if ( startIndex < 0 ) { startIndex = 0 ; } if ( endIndex > array . length ) { endIndex = array . length ; } for ( let i = startIndex ; i < endIndex ; i ++ ) { array [ i ] = value ; } } param skyBottomArray param startIndex param endIndex private getMinInRange ( skyBottomArray , startIndex , endIndex ) { startIndex = Math . floor ( startIndex * this . SamplingUnit ) ; endIndex = Math . ceil ( endIndex * this . SamplingUnit ) ; if ( ! skyBottomArray ) { return Number . MAX_VALUE ; } if ( startIndex < 0 ) { startIndex = 0 ; } if ( startIndex >= skyBottomArray . length ) { startIndex = skyBottomArray . length - 1 ; } if ( endIndex < 0 ) { endIndex = 0 ; } if ( endIndex >= skyBottomArray . length ) { endIndex = skyBottomArray . length ; } if ( startIndex >= 0 && endIndex <= skyBottomArray . length ) { return Math . min ( ... skyBottomArray . slice ( startIndex , endIndex + 1 ) ) ; } } param skyBottomArray param startIndex param endIndex private getMaxInRange ( skyBottomArray , startIndex , endIndex ) { startIndex = Math . floor ( startIndex * this . SamplingUnit ) ; endIndex = Math . ceil ( endIndex * this . SamplingUnit ) ; if ( ! skyBottomArray ) { return Number . MIN_VALUE ; } if ( startIndex < 0 ) { startIndex = 0 ; } if ( startIndex >= skyBottomArray . length ) { startIndex = skyBottomArray . length - 1 ; } if ( endIndex < 0 ) { endIndex = 0 ; } if ( endIndex >= skyBottomArray . length ) { endIndex = skyBottomArray . length ; } if ( startIndex >= 0 && endIndex <= skyBottomArray . length ) { return Math . max ( ... skyBottomArray . slice ( startIndex , endIndex + 1 ) ) ; } } /** Sampling units that are used to quantize the sky and bottom line  */ get SamplingUnit ( ) : number { return this . mRules . SamplingUnit ; } /** Parent staffline where the skybottomline calculator is attached to */ get StaffLineParent ( ) : StaffLine { return this . mStaffLineParent ; } /** Get the plain skyline array */ get SkyLine ( ) : number [ ] { return this . mSkyLine ; } /** Get the plain bottomline array */ get BottomLine ( ) : number [ ] { return this . mBottomLine ; } }