'use strict' ; import { Canvas } from './canvas' ; import { Tensor } from './tensor' ; const _D2R = Math . PI / 180 ; const _EPSILON = 1e-16 ; const _MERGE_THRESHOLD = 0.02 ; const _SPLIT_THRESHOLD = 85 * _D2R ; let _0To2Pi , _axisCache , _getOption ; param value { Number } return { Number } _0To2Pi = function ( value ) { const twoPi = 2 * Math . PI ; while ( value < 0 ) { value += twoPi ; } while ( value >= twoPi ) { value -= twoPi ; } return value ; } ; param axis { Vector3 } return { Object } _axisCache = function ( axis ) { let azimuth , plunge ; azimuth = Math . PI / 2 - axis . vector . azimuth ( ) ; plunge = axis . vector . plunge ( ) ; if ( plunge < 0 ) { plunge *= - 1 ; azimuth += Math . PI ; } azimuth = _0To2Pi ( azimuth , 0 , Math . PI * 2 ) ; return { a : azimuth , ca : Math . cos ( azimuth ) , cp : Math . cos ( plunge ) , p : plunge , sa : Math . sin ( azimuth ) , sp : Math . sin ( plunge ) , v : axis . eigenvalue } ; } ; _getOption = function ( options , name , defaultValue ) : any { if ( options && name in options ) { return options [ name ] ; } return defaultValue ; } ; export class Beachball { static zeroToTwoPi = _0To2Pi ; param tensor param el param options static render ( tensor , el , options ? ) { new Beachball ( tensor , el , options ) . render ( ) ; } axisSize : number = null ; bgColor = '#fff' ; fillColor = '#ddd' ; height : number = null ; labelAxes = true ; labelAxesFont = '24px Arial' ; labelPlanes = true ; labelPlanesFont = '14px Arial' ; lineColor = '#000' ; lineWidth = 0.25 ; plotAxes = false ; plotPlanes = true ; radius : number = null ; size = 200 ; width : number = null ; x0 : number = null ; y0 : number = null ; constructor ( public tensor , public el , options ? ) { this . bgColor = _getOption ( options , 'bgColor' , this . bgColor ) ; this . fillColor = _getOption ( options , 'fillColor' , this . fillColor ) ; this . labelAxes = _getOption ( options , 'labelAxes' , this . labelAxes ) ; this . labelAxesFont = _getOption ( options , 'labelAxesFont' , this . labelAxesFont ) ; this . labelPlanes = _getOption ( options , 'labelPlanes' , this . labelPlanes ) ; this . labelPlanesFont = _getOption ( options , 'labelPlanesFont' , this . labelPlanesFont ) ; this . lineColor = _getOption ( options , 'lineColor' , this . lineColor ) ; this . lineWidth = _getOption ( options , 'lineWidth' , this . lineWidth ) ; this . plotAxes = _getOption ( options , 'plotAxes' , this . plotAxes ) ; this . plotPlanes = _getOption ( options , 'plotPlanes' , this . plotPlanes ) ; this . size = _getOption ( options , 'size' , this . size ) ; this . radius = _getOption ( options , 'radius' , Math . floor ( ( this . size - 2 ) / 2 ) ) ; this . axisSize = _getOption ( options , 'axisSize' , Math . floor ( this . radius / 12.5 ) ) ; this . height = _getOption ( options , 'height' , this . size ) ; this . width = _getOption ( options , 'width' , this . size ) ; this . x0 = _getOption ( options , 'x0' , this . width / 2 ) ; this . y0 = _getOption ( options , 'y0' , this . height / 2 ) ; } param poly { Object } return { Object } completePolygon ( polygon ) { let az , az1 , az2 , x , y ; if ( polygon . x . length === 360 ) { return polygon ; } az1 = polygon . startAz . az ; az2 = polygon . endAz . az ; x = polygon . x ; y = polygon . y ; if ( az1 - az2 > Math . PI ) { az1 -= 2 * Math . PI ; } if ( az2 - az1 > Math . PI ) { az1 += 2 * Math . PI ; } if ( az1 < az2 ) { for ( az = az2 - _D2R ; az > az1 ; az -= _D2R ) { x . push ( Math . sin ( az ) ) ; y . push ( Math . cos ( az ) ) ; } } else { for ( az = az2 + _D2R ; az < az1 ; az += _D2R ) { x . push ( Math . sin ( az ) ) ; y . push ( Math . cos ( az ) ) ; } } return polygon ; } param label { Object } return { Object } computeAzimuthLabel ( label ) { let align , point , labelOffset , size , tickLength , x , y ; point = this . getPoint ( label . azimuth , 0 ) ; x = point . x ; y = point . y ; align = x < 0 ? 'right' : 'left' ; size = this . measureText ( label . text , label . font ) ; labelOffset = ( this . radius + 10 ) / this . radius ; tickLength = ( this . radius + 5 ) / this . radius ; label . align = align ; label . size = size ; label . tick = { x : [ x , x * tickLength ] , y : [ y , y * tickLength ] } ; label . x = x * labelOffset ; label . y = y * labelOffset ; if ( y < 0 ) { label . y = ( y * ( this . radius + 10 + ( Math . abs ( y ) * size . height ) / 2 ) ) / this . radius ; } return label ; } /**
     * Create a Canvas object for plotting.
     * Mainly here for testing.
     */ createCanvas ( ) { return new Canvas ( this . el , this . height , this . width ) ; } param np { Object } return { Object } getPlaneLine ( np ) { let dip , j , point , strike , tanDip , vertical , x , y ; strike = np . strike * _D2R ; dip = np . dip * _D2R ; x = [ ] ; y = [ ] ; vertical = Math . abs ( dip - Math . PI / 2 ) < _EPSILON ; if ( vertical ) { x . push ( Math . sin ( strike ) , Math . sin ( strike + Math . PI ) ) ; y . push ( Math . cos ( strike ) , Math . cos ( strike + Math . PI ) ) ; } else { tanDip = Math . tan ( dip ) ; for ( j = 0 ; j <= Math . PI ; j += _D2R ) { dip = Math . atan ( tanDip * Math . sin ( j ) ) ; point = this . getPoint ( strike + j , dip ) ; x . push ( point . x ) ; y . push ( point . y ) ; } } return { x : x , y : y } ; } param vector { Vector } return { Object } getPoint ( azimuth , plunge ) { let r , x , y ; if ( plunge < 0 ) { plunge *= - 1 ; azimuth += Math . PI ; } azimuth = _0To2Pi ( azimuth ) ; r = Math . sqrt ( 1 - Math . sin ( plunge ) ) ; x = r * Math . sin ( azimuth ) ; y = r * Math . cos ( azimuth ) ; return { x : x , y : y } ; } param tensor { Tensor } return { Array < Object > } getPolygons ( tensor ) { let alphan , az , azes , azp , c , cfi , f , fir , i , iso , n , p , polygon , polygons , r , s , s2alphan , sfi , swapColors , t , tmp , takeoff , vi , x , xe , xn , xz , y ; t = _axisCache ( tensor . T ) ; n = _axisCache ( tensor . N ) ; p = _axisCache ( tensor . P ) ; azes = [ ] ; polygons = [ ] ; vi = ( t . v + n . v + p . v ) / 3 ; t . v -= vi ; n . v -= vi ; p . v -= vi ; f = - n . v / t . v || _EPSILON ; iso = vi / t . v || _EPSILON ; swapColors = false ; for ( i = 0 ; i < 360 ; i ++ ) { fir = i * _D2R ; sfi = Math . sin ( fir ) ; cfi = Math . cos ( fir ) ; s2alphan = ( 2 + 2 * iso ) / ( 3 + ( 1 - 2 * f ) * Math . cos ( 2 * fir ) ) ; if ( Math . abs ( 1 - s2alphan ) <= _EPSILON ) { s2alphan = 1 ; } if ( s2alphan > 1 ) { tmp = t ; t = p ; p = tmp ; swapColors = ! swapColors ; f = - n . v / t . v || _EPSILON ; iso = vi / t . v || _EPSILON ; s2alphan = ( 2 + 2 * iso ) / ( 3 + ( 1 - 2 * f ) * Math . cos ( 2 * fir ) ) ; } alphan = Math . asin ( Math . sqrt ( s2alphan ) ) ; s = Math . sin ( alphan ) ; c = Math . cos ( alphan ) ; xz = c * t . sp + s * sfi * n . sp + s * cfi * p . sp ; xn = c * t . cp * t . ca + s * sfi * n . cp * n . ca + s * cfi * p . cp * p . ca ; xe = c * t . cp * t . sa + s * sfi * n . cp * n . sa + s * cfi * p . cp * p . sa ; if ( Math . abs ( xn ) < _EPSILON && Math . abs ( xe ) < _EPSILON ) { az = 0 ; takeoff = 0 ; } else { az = _0To2Pi ( Math . atan2 ( xe , xn ) ) ; takeoff = Math . acos ( xz / Math . sqrt ( xz * xz + xn * xn + xe * xe ) ) ; if ( takeoff > Math . PI / 2 ) { az = _0To2Pi ( az + Math . PI ) ; takeoff = Math . PI - takeoff ; } } azes . push ( { az : az , takeoff : takeoff } ) ; } polygon = null ; for ( i = 0 ; i < azes . length ; i ++ ) { az = azes [ i ] ; r = Math . SQRT2 * Math . sin ( az . takeoff / 2 ) ; x = r * Math . sin ( az . az ) ; y = r * Math . cos ( az . az ) ; if ( polygon !== null ) { azp = azes [ i === 0 ? azes . length - 1 : i - 1 ] ; if ( Math . abs ( Math . abs ( az . az - azp . az ) - Math . PI ) < 10 * _D2R ) { if ( az . takeoff > _SPLIT_THRESHOLD && azp . takeoff > _SPLIT_THRESHOLD ) { if ( polygon !== null ) { polygon . endAz = azp ; polygons . push ( polygon ) ; polygon = null ; } } } } if ( polygon === null ) { polygon = { endAz : null , startAz : az , x : [ ] , y : [ ] } ; } polygon . x . push ( x ) ; polygon . y . push ( y ) ; } polygon . endAz = azes [ azes . length - 1 ] ; polygons . push ( polygon ) ; polygons = this . mergePolygons ( polygons ) ; polygons = polygons . map ( poly => this . completePolygon ( poly ) ) ; polygons . swapColors = swapColors ; return polygons ; } param vector { Vector } return { Object } getVectorPoint ( vector ) { return this . getPoint ( Math . PI / 2 - vector . azimuth ( ) , vector . plunge ( ) ) ; } param { string } size param { Object } point param { 'left' | 'right' | 'center' } align makeRoomForLabel ( size , point , align ) { let bottom , left , right , top , x , y ; x = this . projectX ( point . x ) ; y = this . projectY ( point . y ) ; bottom = 0 ; left = 0 ; right = 0 ; top = 0 ; bottom = y - size . height ; top = y + size . height ; if ( align === 'left' ) { left = x ; right = x + size . width ; } else if ( align === 'right' ) { left = x - size . width ; right = x ; } else { const halfWidth = Math . ceil ( size . width / 2 ) ; left = x - halfWidth ; right = x + halfWidth ; } if ( bottom < 0 ) { bottom = Math . abs ( bottom ) ; } else { bottom = 0 ; } if ( top > this . height ) { top = top - this . height ; } else { top = 0 ; } if ( left < 0 ) { left = Math . abs ( left ) ; } else { left = 0 ; } if ( right > this . width ) { right = right - this . width ; } else { right = 0 ; } this . width = this . width + left + right ; this . x0 = this . x0 + left ; this . height = this . height + top + bottom ; this . y0 = this . y0 + top ; } param text { String } param font { String } return { Object } measureText ( text , font ) { let el , size ; el = document . createElement ( 'div' ) ; el . setAttribute ( 'style' , 'height:auto;' + 'position:absolute;' + 'visibility:hidden;' + 'white-space:nowrap;' + 'width:auto;' + 'font:' + font + ';' ) ; el . innerText = text ; document . body . appendChild ( el ) ; size = { height : el . scrollHeight , width : el . scrollWidth } ; el . parentNode . removeChild ( el ) ; el = null ; return size ; } param polygons { Array < Object > } return { Array < Object > } mergePolygons ( polygons ) { let i , nextI , p1 , p1x , p1y , p2 , p2x , p2y ; if ( polygons . length === 1 ) { return polygons ; } for ( i = 0 ; i < polygons . length ; i ++ ) { nextI = i === polygons . length - 1 ? 0 : i + 1 ; p1 = polygons [ i ] ; p1x = p1 . x ; p1y = p1 . y ; p2 = polygons [ nextI ] ; p2x = p2 . x ; p2y = p2 . y ; if ( Math . abs ( p1x [ p1x . length - 1 ] - p2x [ 0 ] ) < _MERGE_THRESHOLD && Math . abs ( p1y [ p1y . length - 1 ] - p2y [ 0 ] ) < _MERGE_THRESHOLD ) { p1x . push . apply ( p1x , p2x ) ; p1y . push . apply ( p1y , p2y ) ; p1 . endAz = p2 . endAz ; polygons . splice ( nextI , 1 ) ; } } return polygons ; } param x { Number } return { Number } projectX ( x ) { return this . x0 + this . radius * x ; } param y { Number } return { Number } projectY ( y ) { return this . height - ( this . y0 + this . radius * y ) ; } /**
     * Render view based on current model settings.
     */ render ( ) { let azimuthLabels , canvas , point , polygons , tmp , x , y ; azimuthLabels = [ ] ; if ( this . labelPlanes ) { [ this . tensor . NP1 , this . tensor . NP2 ] . forEach ( np => { let azimuth , text ; azimuth = np . strike * _D2R ; text = '(' + np . strike . toFixed ( 0 ) + ', ' + np . dip . toFixed ( 0 ) + ', ' + np . rake . toFixed ( 0 ) + ')' ; const label = this . computeAzimuthLabel ( { azimuth : azimuth , font : this . labelPlanesFont , text : text } ) ; this . makeRoomForLabel ( label . size , { x : label . x , y : label . y } , label . align ) ; azimuthLabels . push ( label ) ; } ) ; } if ( this . labelAxes ) { [ this . tensor . T , this . tensor . P ] . forEach ( axis => { this . makeRoomForLabel ( this . measureText ( axis . name , this . labelAxesFont ) , this . getVectorPoint ( axis . vector ) , 'center' ) ; } ) ; } canvas = this . createCanvas ( ) ; canvas . context . lineWidth = this . lineWidth ; polygons = this . getPolygons ( this . tensor ) ; if ( polygons . swapColors ) { tmp = this . bgColor ; this . bgColor = this . fillColor ; this . fillColor = tmp ; } x = this . projectX ( 0 ) ; y = this . projectY ( 0 ) ; canvas . circle ( x , y , this . radius * 2 , this . lineColor , this . bgColor ) ; polygons . forEach ( p => { canvas . polygon ( p . x . map ( x0 => this . projectX ( x0 ) ) , p . y . map ( y0 => this . projectY ( y0 ) ) , this . lineColor , this . fillColor ) ; } ) ; if ( this . plotPlanes ) { [ this . tensor . NP1 , this . tensor . NP2 ] . forEach ( np => { let line ; line = this . getPlaneLine ( np ) ; canvas . line ( line . x . map ( x0 => this . projectX ( x0 ) ) , line . y . map ( y0 => this . projectY ( y0 ) ) , this . lineColor , null ) ; } ) ; } canvas . circle ( x , y , this . radius * 2 , this . lineColor , null ) ; if ( this . labelAxes ) { [ this . tensor . P , this . tensor . T ] . forEach ( axis => { point = this . getVectorPoint ( axis . vector ) ; canvas . text ( axis . name , this . labelAxesFont , this . projectX ( point . x ) , this . projectY ( point . y ) , null , 'black' , 'center' ) ; } ) ; } else if ( this . plotAxes ) { point = this . getVectorPoint ( this . tensor . P . vector ) ; canvas . circle ( point . x , point . y , this . axisSize , 'white' , 'black' ) ; point = this . getVectorPoint ( this . tensor . T . vector ) ; canvas . circle ( point . x , point . y , this . axisSize , 'black' , 'white' ) ; } azimuthLabels . forEach ( label => { let tick ; tick = label . tick ; canvas . line ( tick . x . map ( tickX => this . projectX ( tickX ) ) , tick . y . map ( tickY => this . projectY ( tickY ) ) , 'black' , null ) ; canvas . text ( label . text , label . font , this . projectX ( label . x ) , this . projectY ( label . y ) , null , 'black' , label . align ) ; } ) ; canvas = null ; } }