import { UtilsService } from './../../../../core/utils/utils.service' ; import { Drawing , DrawingBuilder , DrawData } from './drawing.model' ; import { ApiService } from './../../../../core/api/api.service' ; import { Outlines } from './outlines.effect' ; import { Trace } from './trace.effect' ; import { AreaSeed } from './../../../../core/data/data.model' ; import { AreaBuilder , Area , areaStyles } from './area.model' ; import { Settings } from './../../../settings/settings.model' ; import { Source } from './source.model' ; import { Facsimile } from './facsimile.model' ; import { AnnotationStorage , SourceStorage , FacsimileStorage , DrawingStorage , SentenceStorage , WordStorage } from './../../../../core/storage/storage.model' ; import { NotificationsService } from './../../../../core/notifications/notifications.service' ; import { Component , OnDestroy , ViewChild , ElementRef , HostListener , OnInit , Output , ChangeDetectorRef , EventEmitter } from '@angular/core' ; import { Subscription } from 'rxjs' ; import { DataService } from './../../../../core/data/data.service' ; import { Project } from '../../project.model' ; import { find , filter , includes , concat , unionBy , uniq , take , orderBy } from 'lodash' ; import { fabric } from 'fabric' ; import * as Hammer from 'hammerjs' ; import { WorkspaceConfig , WorkspaceHistory , customKeys , WorkspaceData , WorkspacePosition , WorkspaceSeed , BuilderData , WorkspaceObjectPosition , WorkspacePoint } from './workspace.model' ; import * as Mousetrap from 'mousetrap' ; import * as cloneDeep from 'clone-deep' ; import { AnnotationBuilder , Annotation , annotationStyles } from './annotation.model' ; import { Glyph , GlyphBuilder , glyphStyles } from './glyph.model' ; import { AutoIdentifyRectBuilder , AutoIdentifyRect } from './auto-identify-rect.model' ; import { IdentificationResponse , IdentificationRequest , IdentificationWord } from '../../../../core/api/identification.model' ; import { Sentence } from './sentence.model' ; import { Word } from './word.model' ; import { TranslateService } from '@ngx-translate/core' ; @ Component ( { selector : 'app-workspace' , templateUrl : './workspace.component.html' , styleUrls : [ './workspace.component.scss' ] , } ) export class WorkspaceComponent implements OnInit , OnDestroy { @ ViewChild ( 'canvas' , { static : true } ) private canvasRef : ElementRef ; @ ViewChild ( 'panZoomLayer' , { static : true } ) private panZoomLayerRef : ElementRef ; @ ViewChild ( 'workspace' , { static : true } ) private workspaceRef : ElementRef ; @ HostListener ( 'window:resize' , [ '$event' ] ) private onWindowResize = this . resize ; @ Output ( ) workspaceSetup = new EventEmitter < void > ( ) ; @ Output ( ) workspaceData = new EventEmitter < WorkspaceData > ( ) ; private componentSubs : Array < Subscription > = [ ] ; private workspaceEl : HTMLElement ; private canvasEl : HTMLCanvasElement ; private panZoomEl : HTMLElement ; private history : WorkspaceHistory = { current : { fabric : null , misc : { effects : null , } , } , undo : { stack : [ ] , } , redo : { stack : [ ] , } , } ; private builderData : BuilderData = { type : null , instance : null , eventHandlers : [ ] , selectedGlyphParent : null , } ; private settings : Settings ; private canvas : fabric . Canvas ; project : Project ; config : WorkspaceConfig = { ai : { identify : { enabled : false , } , classification : { enabled : false , } , } , type : null , states : { loaded : false , panning : false , } , tools : { selected : 'select' , visible : { select : false , pan : false , zoomIn : false , zoomZero : false , zoomOut : false , undo : false , redo : false , marquee : false , polygon : false , draw : false , erase : false , sources : false , } , enabled : { select : false , pan : false , zoomIn : false , zoomZero : false , zoomOut : false , undo : false , redo : false , marquee : false , polygon : false , draw : false , erase : false , sources : false , } , } , panels : { active : { sources : false , } } , selectedGlyphParent : null , } ; data : WorkspaceData = { sources : [ ] , facsimile : { processed : null , generated : null , } , process : { areas : [ ] , } , generate : { drawings : [ ] , effects : null , } , analyse : { sentences : [ ] , } , annotate : { annotations : [ ] , } , } ; constructor ( private dataService , private notificationsService , private apiService , private changeDetectorRef , private translate , private utils ) { } ngOnInit ( ) { this . workspaceEl = this . workspaceRef . nativeElement ; this . canvasEl = this . canvasRef . nativeElement ; this . panZoomEl = this . panZoomLayerRef . nativeElement ; this . componentSubs . push ( this . dataService . projectObs . subscribe ( ( project ) => { this . project = project ; if ( project && ! this . config . states . loaded ) { this . setup ( ) ; } } ) , this . dataService . settingsObs . subscribe ( ( settings ) => { this . settings = settings ; } ) , this . apiService . servicesEnabledObs . subscribe ( servicesEnabled => { this . config . ai . identify . enabled = includes ( servicesEnabled , 'clusterAnalysis' ) ; this . config . ai . classification . enabled = includes ( servicesEnabled , 'classification' ) ; this . updateAPIStates ( ) ; } ) ) ; } ngOnDestroy ( ) { this . componentSubs . forEach ( sub => sub . unsubscribe ( ) ) ; } toolClick ( toolId ) { const oneTimeTools = [ 'zoomZero' , 'undo' , 'redo' , ] ; if ( includes ( oneTimeTools , toolId ) ) { switch ( toolId ) { case 'zoomZero' : this . reset ( ) ; break ; case 'undo' : this . undo ( ) ; break ; case 'redo' : this . redo ( ) ; break ; } return ; } let builderToolId = null ; if ( this . config . type === 'process' && toolId === 'marquee' ) { builderToolId = 'area' ; } else if ( this . config . type === 'generate' && toolId === 'draw' ) { builderToolId = 'drawing' ; } else if ( this . config . type === 'generate' && toolId === 'erase' ) { builderToolId = 'drawing' ; } else if ( this . config . type === 'analyse' && toolId === 'marquee' ) { builderToolId = 'auto-identify-rect' ; } else if ( this . config . type === 'analyse' && toolId === 'polygon' ) { builderToolId = 'glyph' ; } else if ( this . config . type === 'annotate' && toolId === 'polygon' ) { builderToolId = 'annotation' ; } this . config . tools . selected = toolId ; if ( builderToolId ) { if ( ( builderToolId === 'drawing' || builderToolId === 'glyph' || builderToolId === 'auto-identify-rect' ) && this . builderData . type ) { this . disableBuilderTool ( this . builderData . type ) ; } this . enableBuilderTool ( builderToolId ) ; } else { if ( this . builderData . type ) { this . disableBuilderTool ( this . builderData . type ) ; } } this . updateDrawEraseClasses ( ) ; this . changeDetectorRef . detectChanges ( ) ; } render ( ) { this . canvas . requestRenderAll ( ) ; } init ( type , workspaceSeed ) { return new Promise ( ( resolve , reject ) => { this . config . type = type ; switch ( this . config . type ) { case 'process' : this . config . tools . visible . marquee = true ; this . config . tools . enabled . marquee = true ; this . canvas . selection = false ; break ; case 'generate' : this . config . tools . visible . draw = true ; this . config . tools . visible . erase = true ; this . config . tools . enabled . draw = true ; this . config . tools . enabled . erase = true ; this . canvas . selection = false ; break ; case 'analyse' : this . config . tools . visible . marquee = true ; this . config . tools . visible . polygon = true ; this . config . tools . enabled . marquee = true ; this . config . tools . enabled . polygon = true ; this . canvas . selection = true ; break ; case 'annotate' : this . config . tools . visible . polygon = true ; this . config . tools . enabled . polygon = true ; this . canvas . selection = false ; break ; } this . loadSeedData ( workspaceSeed ) . then ( ( ) => { this . reorderObjects ( ) ; this . updateData ( this . config . type === 'generate' ? true : false ) . then ( ( ) => { this . updateHistory ( ) ; this . loadPosition ( workspaceSeed . position ) ; this . updateAreas ( ) . then ( ( ) => { this . config . tools . enabled . sources = true ; this . config . states . loaded = true ; } , console . warn ) ; } , console . warn ) ; } , console . warn ) ; } ) ; } updateData ( save = true , updateObjects = true ) { const updatePromises = [ ] ; this . cursorLoading ( true ) ; return new Promise ( ( resolve , reject ) => { if ( this . config . type === 'generate' ) { this . config . tools . enabled . draw = this . data . generate . effects . drawing . active ; this . config . tools . enabled . erase = this . data . generate . effects . drawing . active ; const newWidth = this . data . generate . effects . drawing . width || 3 ; if ( newWidth !== ( < any > this . canvas ) . freeDrawingBrush . width ) { ( < any > this . canvas ) . freeDrawingBrush . width = newWidth ; this . updateDrawEraseClasses ( ) ; } if ( ( this . config . tools . selected === 'draw' && ! this . config . tools . enabled . draw ) || ( this . config . tools . selected === 'erase' && ! this . config . tools . enabled . erase ) ) { this . toolClick ( 'select' ) ; } } if ( updateObjects ) { switch ( this . config . type ) { case 'process' : this . data . process . areas = this . getObjects ( 'tid' , [ 'area' ] ) ; if ( save ) { updatePromises . push ( this . updateProcessedFacsimile ( ) ) ; } break ; case 'generate' : this . data . generate . drawings = this . getObjects ( 'tid' , [ 'drawing' ] ) ; if ( save ) { updatePromises . push ( this . updateGeneratedFacsimile ( ) ) ; } break ; case 'analyse' : this . data . analyse . sentences = this . getObjects ( 'tid' , [ 'sentence' ] ) ; break ; case 'annotate' : this . data . annotate . annotations = this . getObjects ( 'tid' , [ 'annotation' ] ) ; break ; } } Promise . all ( updatePromises ) . then ( ( ) => { if ( updateObjects ) { this . data . sources = this . getObjects ( 'tid' , [ 'source' ] ) ; this . data . facsimile . processed = this . getObject ( 'tid' , 'processedFacsimile' ) ; this . data . facsimile . generated = this . getObject ( 'tid' , 'generatedFacsimile' ) ; } this . workspaceData . emit ( this . data ) ; this . cursorLoading ( false ) ; if ( save ) { this . save ( ) . then ( ( ) => { resolve ( ) ; } ) ; } else { resolve ( ) ; } } ) ; } ) ; } updateHistory ( ) { this . history . redo . stack = [ ] ; this . config . tools . enabled . redo = false ; if ( this . history . current . fabric ) { this . history . undo . stack . push ( this . history . current ) ; this . config . tools . enabled . undo = true ; } this . history . current = cloneDeep ( { fabric : ( < any > this . canvas ) . toJSON ( customKeys ) as string , misc : { effects : this . data . generate . effects , } } ) ; } selectObjects ( objects , updateActive = true , deselect = true ) { if ( deselect ) { this . setObjectsDeselected ( ) ; } this . setObjectsSelected ( this . getDeepObjects ( objects ) ) ; if ( updateActive ) { if ( objects && objects . length && objects [ 0 ] && objects [ 0 ] . tid ) { this . updateActiveSelection ( objects [ 0 ] . tid ) ; } } this . updateData ( false , false ) ; } deleteObjects ( objects , force ? , updateData = true ) { if ( ! force && ! confirm ( objects . length === 1 ? this . translate . instant ( 'MODULES.PROJECT.SHARED.WORKSPACE.TS.DELETE' , { objects : objects . length } ) : this . translate . instant ( 'MODULES.PROJECT.SHARED.WORKSPACE.TS.DELETE_PL' , { objects : objects . length } ) ) ) { return false ; } objects . forEach ( ( o ) => { this . canvas . remove ( o ) ; } ) ; if ( objects . length ) { this . canvas . requestRenderAll ( ) ; if ( updateData ) { this . updateData ( ) ; this . updateHistory ( ) ; } } return true ; } selectSentence ( sentence ) { this . setObjectsDeselected ( ) ; this . setObjectsSelected ( [ sentence ] ) ; this . updateData ( false ) ; } unselectSentence ( sentence ) { this . setObjectsDeselected ( [ sentence ] ) ; this . updateData ( false ) ; } deleteSentence ( sentence ) { const wordCount = sentence . wordCount ( ) ; const glyphCount = sentence . glyphCount ( ) ; if ( ( wordCount || glyphCount ) && ! confirm ( wordCount === 1 ? this . translate . instant ( 'MODULES.PROJECT.SHARED.WORKSPACE.TS.DELETE_SEN' , { words : wordCount } ) : this . translate . instant ( 'MODULES.PROJECT.SHARED.WORKSPACE.TS.DELETE_SEN_PL' , { words : wordCount } ) + glyphCount === 1 ? this . translate . instant ( 'MODULES.PROJECT.SHARED.WORKSPACE.TS.DELETE_SEN_GLY' , { glyphs : glyphCount } ) : this . translate . instant ( 'MODULES.PROJECT.SHARED.WORKSPACE.TS.DELETE_SEN_GLY_PL' , { glyphs : glyphCount } ) ) ) { return false ; } sentence . getGlyphs ( ) . forEach ( glyph => { this . canvas . remove ( glyph ) ; } ) ; const sentenceWords = sentence . getWords ( ) ; this . setObjectsDeselected ( sentenceWords ) ; sentenceWords . forEach ( word => this . canvas . remove ( word ) ) ; this . setObjectsDeselected ( [ sentence ] ) ; this . canvas . remove ( sentence ) ; this . canvas . requestRenderAll ( ) ; this . updateData ( ) ; this . updateHistory ( ) ; return true ; } selectWord ( word ) { this . setObjectsDeselected ( ) ; this . setObjectsSelected ( [ word ] ) ; this . updateData ( false ) ; } unselectWord ( word ) { this . setObjectsDeselected ( [ word ] ) ; this . updateData ( false ) ; } deleteWord ( word ) { const glyphCount = word . glyphCount ( ) ; if ( glyphCount && ! confirm ( glyphCount === 1 ? this . translate . instant ( 'MODULES.PROJECT.SHARED.WORKSPACE.TS.DELETE_WORD' , { glyphs : glyphCount } ) : this . translate . instant ( 'MODULES.PROJECT.SHARED.WORKSPACE.TS.DELETE_WORD_PL' , { glyphs : glyphCount } ) ) ) { return false ; } const sentence = word . getParent ( ) ; if ( sentence ) { this . setObjectsDeselected ( [ word ] ) ; sentence . removeWord ( word ) ; this . canvas . requestRenderAll ( ) ; this . updateData ( ) ; this . updateHistory ( ) ; } return true ; } deleteGlyphs ( glyphs , force ? ) { if ( ! force && glyphs . length > 0 && ! confirm ( glyphs . length === 1 ? this . translate . instant ( 'MODULES.PROJECT.SHARED.WORKSPACE.TS.DELETE_WORD' , { glyphs : glyphs . length } ) : this . translate . instant ( 'MODULES.PROJECT.SHARED.WORKSPACE.TS.DELETE_WORD_PL' , { glyphs : glyphs . length } ) ) ) { return false ; } if ( glyphs . length ) { glyphs . forEach ( ( glyph ) => { const word = glyph . getParent ( ) ; if ( ! word ) { this . canvas . remove ( glyph ) ; } else { if ( word . glyphCount ( ) === 1 ) { word . removeGlyph ( glyph ) ; this . setObjectsDeselected ( [ word ] ) ; const sentence = word . getParent ( ) ; if ( ! sentence ) { this . canvas . remove ( word ) ; } else { if ( sentence . wordCount ( ) === 1 ) { sentence . removeWord ( word ) ; this . setObjectsDeselected ( [ sentence ] ) ; this . canvas . remove ( sentence ) ; } else { sentence . removeWord ( word ) ; } } } else { word . removeGlyph ( glyph ) ; } } } ) ; this . canvas . requestRenderAll ( ) ; this . updateData ( ) ; this . updateHistory ( ) ; } return true ; } highlightObjects ( objects , state ) { objects . forEach ( ( object ) => { if ( state ) { object . set ( this . getObjectStyles ( object . tid , 'highlight' ) ) ; } else { if ( object . selected ) { object . set ( this . getObjectStyles ( object . tid , 'active' ) ) ; } else { object . set ( this . getObjectStyles ( object . tid , 'inactive' ) ) ; } } } ) ; this . canvas . requestRenderAll ( ) ; } getSelectedObjects ( tid ? ) { return filter ( this . getBuilderObjects ( ) , o => { if ( tid ) { return o . selected && o . tid && o . tid === tid ; } return o . selected ; } ) ; } getPolyImgOfFacsimile ( object ) { return new Promise ( ( resolve , reject ) => { const objectPosition = ( < Glyph > object ) . absolutePosition ( ) ; this . getRectImgOfFacsimile ( objectPosition ) . then ( ( rectClipImg ) => { const polyClipImg = new Image ( ) ; polyClipImg . onload = ( ) => { const clipCanvas = document . createElement ( 'canvas' ) ; clipCanvas . width = objectPosition . width ; clipCanvas . height = objectPosition . height ; const clipCanvasCtx = clipCanvas . getContext ( '2d' ) ; clipCanvasCtx . imageSmoothingEnabled = false ; clipCanvasCtx . fillStyle = 'rgba(0, 0, 0, 0)' ; clipCanvasCtx . beginPath ( ) ; const x0 = object . points [ 0 ] . x - objectPosition . left ; const y0 = object . points [ 0 ] . y - objectPosition . top ; clipCanvasCtx . moveTo ( x0 , y0 ) ; object . points . forEach ( point => { const x = point . x - objectPosition . left ; const y = point . y - objectPosition . top ; clipCanvasCtx . lineTo ( x , y ) ; } ) ; clipCanvasCtx . lineTo ( x0 , y0 ) ; clipCanvasCtx . closePath ( ) ; clipCanvasCtx . fill ( ) ; clipCanvasCtx . clip ( ) ; clipCanvasCtx . drawImage ( rectClipImg , 0 , 0 ) ; const resultConfig = { width : 320 , height : 340 , padding : 50 , } ; const polyCanvas = document . createElement ( 'canvas' ) ; polyCanvas . width = resultConfig . width ; polyCanvas . height = resultConfig . height ; const polyCtx = polyCanvas . getContext ( '2d' ) ; polyCtx . imageSmoothingEnabled = true ; polyCtx . imageSmoothingQuality = 'high' ; polyCtx . fillStyle = '#FFFFFF' ; polyCtx . fillRect ( 0 , 0 , polyCanvas . width , polyCanvas . height ) ; const ployImg = new Image ( ) ; ployImg . onload = ( ) => { const ratio = ployImg . width / ployImg . height ; let newWidth = polyCanvas . width - ( resultConfig . padding * 2 ) ; let newHeight = newWidth / ratio ; if ( newHeight > polyCanvas . height - ( resultConfig . padding * 2 ) ) { newHeight = polyCanvas . height - ( resultConfig . padding * 2 ) ; newWidth = newHeight * ratio ; } polyCtx . drawImage ( ployImg , polyCanvas . width / 2 - newWidth / 2 , polyCanvas . height / 2 - newHeight / 2 , newWidth , newHeight ) ; resolve ( polyCanvas . toDataURL ( 'image/png' ) ) ; } ; ployImg . onerror = ( err ) => { this . notificationsService . error ( this . translate . instant ( 'MODULES.PROJECT.SHARED.WORKSPACE.TS.ERROR.GLYPH' ) ) ; console . warn ( this . translate . instant ( 'MODULES.PROJECT.SHARED.WORKSPACE.TS.WARN.GLYPH' ) , err ) ; reject ( ) ; } ; ployImg . src = clipCanvas . toDataURL ( ) ; } ; polyClipImg . src = rectClipImg . src ; } ) ; } ) ; } getObject ( key = 'id' , id ) { if ( this . canvas ) { const objects = this . canvas . getObjects ( ) ; return find ( objects , ( o ) => { return o [ key ] && o [ key ] === id ; } ) ; } return undefined ; } getObjects ( key = 'id' , ids ) { if ( this . canvas ) { const objects = this . canvas . getObjects ( ) ; return filter ( objects , ( o ) => { return o [ key ] && includes ( ids , o [ key ] ) ; } ) ; } return [ ] ; } editPolygonObject ( object ) { object . selectable = false ; object . objectCaching = false ; const reinitialiseHandles = ( ) => { const handles = this . getObjects ( 'tid' , [ 'poly-handle' ] ) ; handles . forEach ( h => this . canvas . remove ( h ) ) ; object . points . forEach ( ( point , index ) => { const handle = new fabric . Circle ( { fill : '#fbe085' , hasBorders : false , hasControls : false , left : point . x , originX : 'center' , originY : 'center' , radius : 5 , stroke : '#373529' , strokeWidth : 0.5 , top : point . y , } ) ; fabric . util . object . extend ( handle , { name : index , tid : 'poly-handle' , } ) ; this . canvas . add ( handle ) ; this . canvas . bringToFront ( handle ) ; this . canvas . setActiveObject ( handle ) ; handle . on ( 'moving' , ( opt ) => { object . points [ handle . name ] . x = handle . getCenterPoint ( ) . x ; object . points [ handle . name ] . y = handle . getCenterPoint ( ) . y ; handle . setCoords ( ) ; object . setCoords ( ) ; this . canvas . requestRenderAll ( ) ; } ) ; } ) ; } ; const distanceBetweenPoints = ( point , origin ) : number => { return Math . sqrt ( Math . pow ( origin . x - point . x , 2 ) + Math . pow ( origin . y - point . y , 2 ) ) ; } ; const addHandle = ( point ) => { const pointDistances = [ ] ; object . points . forEach ( ( p , i ) => { pointDistances . push ( { i : i , d : distanceBetweenPoints ( p , point ) , point : p , } ) ; } ) ; const nearestNeighbours = take ( orderBy ( pointDistances , [ 'd' ] , [ 'asc' ] ) , 2 ) ; const iDiff = Math . abs ( nearestNeighbours [ 0 ] . i - nearestNeighbours [ 1 ] . i ) ; const iMax = Math . max ( nearestNeighbours [ 0 ] . i , nearestNeighbours [ 1 ] . i ) ; const iMin = Math . min ( nearestNeighbours [ 0 ] . i , nearestNeighbours [ 1 ] . i ) ; if ( iDiff <= 1 ) { object . points . splice ( iMax , 0 , point ) ; } else if ( iDiff === ( object . points . length - 1 ) ) { object . points . splice ( iMax + 1 , 0 , point ) ; } else { object . points . splice ( iMin + 1 , 0 , point ) ; } } ; object . on ( 'mousedown' , ( opt ) => { addHandle ( this . canvas . getPointer ( opt . e ) as fabric . Point ) ; reinitialiseHandles ( ) ; object . setCoords ( ) ; this . canvas . requestRenderAll ( ) ; } ) ; reinitialiseHandles ( ) ; } saveEditedPolygonObject ( object ) { const handles = this . getObjects ( 'tid' , [ 'poly-handle' ] ) ; handles . forEach ( h => this . canvas . remove ( h ) ) ; if ( object && object . tid ) { const promises = [ ] ; switch ( object . tid ) { case 'glyph' : this . canvas . remove ( object ) ; promises . push ( this . reloadGlyph ( object as Glyph ) ) ; break ; case 'annotation' : this . canvas . remove ( object ) ; promises . push ( this . reloadAnnotation ( object as Annotation ) ) ; break ; } Promise . all ( promises ) . then ( ( newObjects ) => { this . reorderObjects ( ) ; const newObject = newObjects [ 0 ] ; if ( newObject ) { if ( newObject . tid && newObject . tid === 'glyph' ) { const word = ( < Glyph > newObject ) . getParent ( ) ; if ( word ) { word . reorderGlyphs ( ) ; } } this . selectObjects ( [ newObject ] ) ; } this . canvas . requestRenderAll ( ) ; this . updateHistory ( ) ; this . updateData ( ) ; } ) ; } } panZoomToObject ( object ) { let objectPosition = { top : object . top , left : object . left , width : object . width , height : object . height , } ; if ( object . tid && object . tid === 'glyph' ) { objectPosition = ( < Glyph > object ) . absolutePosition ( ) ; } const animationDuration = 500 ; const maxWidth = 0.5 ; const maxHeight = 0.5 ; let zoom = 1 ; if ( objectPosition . width >= objectPosition . height ) { const widthZoom = ( this . canvas . getWidth ( ) * maxWidth ) / objectPosition . width ; const heightZoom = ( this . canvas . getHeight ( ) * maxHeight ) / objectPosition . height ; zoom = Math . min ( heightZoom , widthZoom ) ; } else { zoom = ( this . canvas . getHeight ( ) * maxHeight ) / objectPosition . height ; } zoom = Math . min ( 5 , Math . max ( 0.1 , zoom ) ) ; fabric . util . animate ( { startValue : this . canvas . getZoom ( ) , endValue : zoom , duration : animationDuration , easing : fabric . util . ease . easeInOutQuad , byValue : null , onChange : value => { this . canvas . setZoom ( value ) ; this . canvas . requestRenderAll ( ) ; } , onComplete : ( ) => { this . setCoords ( ) ; } , } ) ; const objectCenter = { x : objectPosition . left + ( objectPosition . width / 2 ) , y : objectPosition . top + ( objectPosition . height / 2 ) , } ; const viewportCenter = { x : this . canvas . getWidth ( ) / 2 , y : this . canvas . getHeight ( ) / 2 , } ; objectCenter . x -= objectCenter . x * 2 ; objectCenter . y -= objectCenter . y * 2 ; fabric . util . animate ( { startValue : this . canvas . viewportTransform [ 4 ] , endValue : ( objectCenter . x * zoom ) + viewportCenter . x , duration : animationDuration , easing : fabric . util . ease . easeInOutQuad , byValue : null , onChange : value => { this . canvas . viewportTransform [ 4 ] = value ; this . canvas . requestRenderAll ( ) ; } , onComplete : ( ) => { this . setCoords ( ) ; } , } ) ; fabric . util . animate ( { startValue : this . canvas . viewportTransform [ 5 ] , endValue : ( objectCenter . y * zoom ) + viewportCenter . y , duration : animationDuration , easing : fabric . util . ease . easeInOutQuad , byValue : null , onChange : value => { this . canvas . viewportTransform [ 5 ] = value ; this . canvas . requestRenderAll ( ) ; } , onComplete : ( ) => { this . setCoords ( ) ; } , } ) ; } reorderObjects ( ) { let cnt = 0 ; this . getObjects ( 'tid' , [ 'source' ] ) . forEach ( ( source ) => { source . moveTo ( cnt ) ; cnt ++ ; } ) ; this . getObjects ( 'tid' , [ 'processedFacsimile' ] ) . forEach ( ( facsimile ) => { facsimile . moveTo ( cnt ) ; cnt ++ ; } ) ; this . getObjects ( 'tid' , [ 'generatedFacsimile' ] ) . forEach ( ( facsimile ) => { facsimile . moveTo ( cnt ) ; cnt ++ ; } ) ; this . getObjects ( 'tid' , [ 'area' ] ) . forEach ( ( area ) => { area . moveTo ( cnt ) ; cnt ++ ; } ) ; this . getObjects ( 'tid' , [ 'drawing' ] ) . forEach ( ( drawing ) => { drawing . moveTo ( cnt ) ; cnt ++ ; } ) ; orderBy ( this . getObjects ( 'tid' , [ 'sentence' ] ) , 'order' ) . forEach ( ( sentence ) => { sentence . moveTo ( cnt ) ; cnt ++ ; } ) ; orderBy ( this . getObjects ( 'tid' , [ 'word' ] ) , 'order' ) . forEach ( ( word ) => { word . moveTo ( cnt ) ; cnt ++ ; } ) ; orderBy ( this . getObjects ( 'tid' , [ 'glyph' ] ) , 'order' ) . forEach ( ( glyph ) => { glyph . moveTo ( cnt ) ; cnt ++ ; } ) ; this . getObjects ( 'tid' , [ 'annotation' ] ) . forEach ( ( annotation ) => { annotation . moveTo ( cnt ) ; cnt ++ ; } ) ; this . setCoords ( ) ; this . canvas . requestRenderAll ( ) ; } reorderSentences ( ) { this . getObjects ( 'tid' , [ 'sentence' ] ) . forEach ( ( s , i ) => s . order = i ) ; } createWord ( wordData , parentId , overrideData ? ) { return new Promise ( resolve => { const wordStorage = { author : wordData . author , created : wordData . created , glyphs : [ ] , isCartouche : wordData . isCartouche , order : wordData . order , position : { top : wordData . position . top , left : wordData . position . left , width : wordData . position . width , height : wordData . position . height , } , translation : wordData . translation , transliteration : wordData . transliteration , } ; const word = new Word ( wordStorage , parentId , this . canvas ) ; if ( overrideData ) { for ( const key in overrideData ) { if ( overrideData [ key ] ) { word [ key ] = overrideData [ key ] ; } } } resolve ( word ) ; } ) ; } private updateAPIStates ( ) { switch ( this . config . type ) { case 'process' : break ; case 'generate' : break ; case 'analyse' : this . config . tools . visible . marquee = this . config . ai . identify . enabled ; this . config . tools . enabled . marquee = this . config . ai . identify . enabled ; break ; case 'annotate' : break ; } } private resetRectObjectPosition ( object ) { object . width = Math . floor ( object . width * object . scaleX ) ; object . height = Math . floor ( object . height * object . scaleY ) ; object . top = Math . floor ( object . top ) ; object . left = Math . floor ( object . left ) ; object . scaleX = 1 ; object . scaleY = 1 ; object . setCoords ( ) ; } private getRectImgOfFacsimile ( position , asDataUrl ? ) { return new Promise ( resolve => { const newCanvasData = this . canvas . toObject ( customKeys ) ; const newCanvas = new fabric . Canvas ( document . createElement ( 'canvas' ) ) ; newCanvas . imageSmoothingEnabled = false ; newCanvas . loadFromJSON ( newCanvasData , ( ) => { newCanvas . backgroundColor = 'white' ; newCanvas . renderOnAddRemove = false ; let facsimileObject = this . getObject ( 'tid' , 'generatedFacsimile' ) ; if ( ! facsimileObject ) { facsimileObject = this . getObject ( 'tid' , 'processedFacsimile' ) ; } facsimileObject . opacity = 1 ; const nonFacsimileObjects = filter ( newCanvas . getObjects ( ) , o => o . tid && o . tid !== facsimileObject . tid ) ; nonFacsimileObjects . forEach ( o => o . opacity = 0 ) ; newCanvas . requestRenderAll ( ) ; newCanvas . viewportTransform [ 4 ] = 0 ; newCanvas . viewportTransform [ 5 ] = 0 ; newCanvas . setZoom ( 1 ) ; const imageUrl = newCanvas . toDataURL ( { left : position . left , top : position . top , width : position . width , height : position . height , } ) ; if ( asDataUrl ) { resolve ( imageUrl ) ; newCanvas . dispose ( ) ; } else { const img = new Image ( ) ; img . onload = ( ) => { resolve ( img ) ; newCanvas . dispose ( ) ; } ; img . src = imageUrl ; } } ) ; } ) ; } private calcObjectPosition ( object ) { const position = { top : object . top , left : object . left , width : object . width , height : object . height , } ; if ( ( < any > object ) . group ) { const groupMatrix = ( < any > object ) . group . calcTransformMatrix ( ) ; position . left = groupMatrix [ 4 ] + object . left ; position . top = groupMatrix [ 5 ] + object . top ; } return position ; } private setup ( ) { this . canvas = new fabric . Canvas ( this . canvasEl ) ; this . canvas . imageSmoothingEnabled = false ; this . canvas . backgroundColor = '#3b3b3b' ; this . canvas . uniScaleTransform = true ; this . canvas . setDimensions ( { width : this . workspaceEl . clientWidth , height : this . workspaceEl . clientHeight , } ) ; this . config . tools . visible . select = true ; this . config . tools . visible . pan = true ; this . config . tools . visible . zoomIn = true ; this . config . tools . visible . zoomZero = true ; this . config . tools . visible . zoomOut = true ; this . config . tools . visible . undo = true ; this . config . tools . visible . redo = true ; this . config . tools . visible . sources = true ; this . config . tools . enabled . select = true ; this . config . tools . enabled . pan = true ; this . config . tools . enabled . zoomIn = true ; this . config . tools . enabled . zoomOut = true ; this . bindEvents ( ) ; this . workspaceSetup . emit ( ) ; } private bindEvents ( ) { this . bindPanZoomEvents ( ) ; this . bindObjectEvents ( ) ; this . bindSelectionEvents ( ) ; this . bindKeyboardShortcuts ( ) ; } private bindPanZoomEvents ( ) { const currDelta = { x : 0 , y : 0 , scale : 0 , } ; const pinchHammer = new Hammer ( this . workspaceEl ) ; pinchHammer . get ( 'pinch' ) . set ( { enable : true , } ) ; const panHammer = new Hammer ( this . panZoomEl ) ; panHammer . get ( 'pan' ) . set ( { enable : true , } ) ; pinchHammer . on ( 'pinchstart' , ( evt ) => { currDelta . x = evt . deltaX ; currDelta . y = evt . deltaY ; currDelta . scale = evt . scale ; } ) ; pinchHammer . on ( 'pinchmove' , ( evt ) => { this . pan ( evt . deltaX - currDelta . x , evt . deltaY - currDelta . y ) ; currDelta . x = evt . deltaX ; currDelta . y = evt . deltaY ; let zoomDelta = evt . scale - currDelta . scale ; zoomDelta -= zoomDelta * 2 ; this . zoomByDelta ( zoomDelta , new fabric . Point ( evt . center . x , evt . center . y ) ) ; currDelta . scale = evt . scale ; } ) ; panHammer . on ( 'panstart' , ( evt ) => { if ( evt . pointerType === 'touch' && this . config . tools . selected === 'pan' ) { currDelta . x = evt . deltaX ; currDelta . y = evt . deltaY ; currDelta . scale = evt . scale ; } } ) ; panHammer . on ( 'panmove' , ( evt ) => { if ( evt . pointerType === 'touch' && this . config . tools . selected === 'pan' ) { this . pan ( evt . deltaX - currDelta . x , evt . deltaY - currDelta . y ) ; currDelta . x = evt . deltaX ; currDelta . y = evt . deltaY ; } } ) ; const onWheelEvent = ( evt ) => { const delta = evt . deltaY / 50 ; this . zoomByDelta ( delta , new fabric . Point ( evt . offsetX , evt . offsetY ) ) ; evt . preventDefault ( ) ; evt . stopPropagation ( ) ; } ; this . canvas . on ( 'mouse:wheel' , opt => onWheelEvent ( opt . e as WheelEvent ) ) ; this . panZoomEl . addEventListener ( 'wheel' , onWheelEvent ) ; this . panZoomEl . addEventListener ( 'mousedown' , ( evt ) => { if ( this . config . tools . selected === 'pan' ) { this . config . states . panning = true ; currDelta . x = evt . clientX ; currDelta . y = evt . clientY ; } if ( this . config . tools . selected === 'zoomIn' ) { this . zoomByDelta ( - 0.5 , new fabric . Point ( evt . offsetX , evt . offsetY ) ) ; } if ( this . config . tools . selected === 'zoomOut' ) { this . zoomByDelta ( 0.5 , new fabric . Point ( evt . offsetX , evt . offsetY ) ) ; } } ) ; this . panZoomEl . addEventListener ( 'mousemove' , ( evt ) => { if ( this . config . states . panning ) { this . pan ( evt . clientX - currDelta . x , evt . clientY - currDelta . y ) ; currDelta . x = evt . clientX ; currDelta . y = evt . clientY ; } } ) ; this . panZoomEl . addEventListener ( 'mouseup' , ( evt ) => { this . config . states . panning = false ; } ) ; } private bindObjectEvents ( ) { const updateObject = ( opt ) => { const object = opt . target ; const promises = [ ] ; switch ( object . type ) { case 'image' : case 'rect' : this . resetRectObjectPosition ( object ) ; break ; } switch ( object . tid ) { case 'area' : promises . push ( object . updateImage ( this . getObject ( 'id' , object . source ) ) ) ; break ; } Promise . all ( promises ) . then ( ( ) => { this . canvas . requestRenderAll ( ) ; this . updateData ( ) ; this . updateHistory ( ) ; } ) ; } ; this . canvas . on ( 'object:moved' , updateObject ) ; this . canvas . on ( 'object:scaled' , updateObject ) ; } private bindSelectionEvents ( ) { const onSelectionChange = ( opt ) => { if ( opt . selected ) { let objects = [ ] ; const selection = this . canvas . getActiveObject ( ) as any ; if ( ! selection . tid ) { selection . hasControls = false ; selection . hasBorders = false ; selection . selectable = false ; selection . evented = false ; selection . getObjects ( ) . forEach ( ( sO ) => { if ( sO . tid === 'sentence' || sO . tid === 'word' ) { const glyphs = sO . getGlyphs ( this . canvas ) ; objects = concat ( objects , glyphs ) ; } else { objects . push ( sO ) ; } } ) ; } else { if ( selection . tid === 'sentence' || selection . tid === 'word' ) { const glyphs = selection . getGlyphs ( this . canvas ) ; objects = glyphs ; } else { objects = [ selection ] ; } } this . selectObjects ( objects , false ) ; } } ; this . canvas . on ( 'selection:created' , onSelectionChange ) ; this . canvas . on ( 'selection:updated' , onSelectionChange ) ; this . canvas . on ( 'selection:cleared' , ( ) => { this . setObjectsDeselected ( ) ; this . updateData ( false , false ) ; } ) ; } private bindKeyboardShortcuts ( ) { const activeModifiers = { space : { active : false , prevMode : 'select' as WorkspaceConfig [ 'tools' ] [ 'selected' ] , } , } ; Mousetrap . bind ( 's' , ( ) => { if ( this . config . tools . enabled . select && this . config . tools . selected !== 'select' ) { this . toolClick ( 'select' ) ; } } ) ; Mousetrap . bind ( 'p' , ( ) => { if ( this . config . tools . enabled . pan && this . config . tools . selected !== 'pan' ) { this . toolClick ( 'pan' ) ; } } ) ; Mousetrap . bind ( 'd' , ( ) => { if ( this . config . tools . enabled . draw && this . config . tools . selected !== 'draw' ) { this . toolClick ( 'draw' ) ; } } ) ; Mousetrap . bind ( 'e' , ( ) => { if ( this . config . tools . enabled . erase && this . config . tools . selected !== 'erase' ) { this . toolClick ( 'erase' ) ; } } ) ; Mousetrap . bind ( 'm' , ( ) => { if ( this . config . tools . enabled . marquee && this . config . tools . selected !== 'marquee' ) { this . toolClick ( 'marquee' ) ; } } ) ; Mousetrap . bind ( 'g' , ( ) => { if ( this . config . tools . enabled . polygon && this . config . tools . selected !== 'polygon' ) { this . toolClick ( 'polygon' ) ; } } ) ; Mousetrap . bind ( 'mod+=' , ( ) => { this . zoomByDelta ( - 0.5 ) ; return false ; } ) ; Mousetrap . bind ( 'mod+0' , ( ) => { this . toolClick ( 'zoomZero' ) ; return false ; } ) ; Mousetrap . bind ( 'mod+-' , ( ) => { this . zoomByDelta ( 0.5 ) ; return false ; } ) ; Mousetrap . bind ( 'mod+z' , ( ) => { if ( this . config . tools . enabled . undo ) { this . toolClick ( 'undo' ) ; return false ; } } ) ; Mousetrap . bind ( [ 'mod+shift+z' , 'mod+y' ] , ( ) => { if ( this . config . tools . enabled . redo ) { this . toolClick ( 'redo' ) ; return false ; } } ) ; Mousetrap . bind ( 'space' , ( ) => { if ( ! activeModifiers . space . active && this . config . tools . selected !== 'pan' ) { activeModifiers . space . prevMode = this . config . tools . selected ; activeModifiers . space . active = true ; this . toolClick ( 'pan' ) ; } } , 'keydown' ) ; Mousetrap . bind ( 'space' , ( ) => { if ( activeModifiers . space . active && this . config . tools . selected === 'pan' ) { this . toolClick ( activeModifiers . space . prevMode ) ; activeModifiers . space . active = false ; } } , 'keyup' ) ; } private bindBuilderEvents ( type ) { const saveObject = ( object ) => { this . toolClick ( 'select' ) ; this . canvas . discardActiveObject ( ) ; this . canvas . setActiveObject ( object ) ; this . canvas . requestRenderAll ( ) ; this . updateData ( ) ; this . updateHistory ( ) ; } ; const mouseMoveHandler = ( opt ) => { const evt = opt . e ; evt . preventDefault ( ) ; evt . stopPropagation ( ) ; this . builderData . instance . onMouseMove ( opt ) ; } ; this . canvas . on ( 'mouse:move' , mouseMoveHandler ) ; this . builderData . eventHandlers . push ( { event : 'mouse:move' , handler : mouseMoveHandler , } ) ; switch ( type ) { case 'area' : const mouseDownHandlerArea = ( opt ) => { const evt = opt . e ; evt . preventDefault ( ) ; evt . stopPropagation ( ) ; const object = this . builderData . instance . onMouseDown ( opt ) ; if ( object ) { saveObject ( object ) ; } } ; this . canvas . on ( 'mouse:down' , mouseDownHandlerArea ) ; this . builderData . eventHandlers . push ( { event : 'mouse:down' , handler : mouseDownHandlerArea , } ) ; const mouseUpHandlerArea = ( opt ) => { const evt = opt . e ; evt . preventDefault ( ) ; evt . stopPropagation ( ) ; saveObject ( ( < AreaBuilder > this . builderData . instance ) . generate ( ) as Area ) ; } ; this . canvas . on ( 'mouse:up' , mouseUpHandlerArea ) ; this . builderData . eventHandlers . push ( { event : 'mouse:up' , handler : mouseUpHandlerArea , } ) ; break ; case 'annotation' : case 'glyph' : const savePolyObject = ( object ) => { if ( object ) { switch ( object . tid ) { case 'annotation' : saveObject ( object as Annotation ) ; break ; case 'glyph' : this . addGlyphToSequence ( object as Glyph , ( < GlyphBuilder > this . builderData . instance ) . parent ) . then ( ( glyph ) => { saveObject ( glyph as Glyph ) ; } ) ; break ; } } } ; const mouseDownHandlerPoly = ( opt ) => { const evt = opt . e ; evt . preventDefault ( ) ; evt . stopPropagation ( ) ; savePolyObject ( this . builderData . instance . onMouseDown ( opt ) ) ; } ; this . canvas . on ( 'mouse:down' , mouseDownHandlerPoly ) ; this . builderData . eventHandlers . push ( { event : 'mouse:down' , handler : mouseDownHandlerPoly , } ) ; Mousetrap . bind ( 'esc' , ( ) => { this . toolClick ( 'select' ) ; return false ; } ) ; Mousetrap . bind ( 'enter' , e => { savePolyObject ( ( < GlyphBuilder | AnnotationBuilder > this . builderData . instance ) . generate ( ) ) ; return false ; } ) ; break ; case 'auto-identify-rect' : const mouseDownHandlerAI = ( opt ) => { const evt = opt . e ; evt . preventDefault ( ) ; evt . stopPropagation ( ) ; const object = this . builderData . instance . onMouseDown ( opt ) ; if ( object ) { saveObject ( object ) ; } } ; this . canvas . on ( 'mouse:down' , mouseDownHandlerAI ) ; this . builderData . eventHandlers . push ( { event : 'mouse:down' , handler : mouseDownHandlerAI , } ) ; const mouseUpHandlerAI = ( opt ) => { const evt = opt . e ; evt . preventDefault ( ) ; evt . stopPropagation ( ) ; const rect = ( < AutoIdentifyRectBuilder > this . builderData . instance ) . generate ( opt ) ; this . fetchAutoIdentifyRectSentence ( rect , ( < AutoIdentifyRectBuilder > this . builderData . instance ) . parent ) . then ( ( glyphs ) => { this . deleteObjects ( [ rect ] , true , false ) ; this . toolClick ( 'select' ) ; this . canvas . requestRenderAll ( ) ; this . selectObjects ( glyphs ) ; this . updateData ( ) ; this . updateHistory ( ) ; } ) ; } ; this . canvas . on ( 'mouse:up' , mouseUpHandlerAI ) ; this . builderData . eventHandlers . push ( { event : 'mouse:up' , handler : mouseUpHandlerAI , } ) ; break ; case 'drawing' : const mouseDownHandlerDrawing = ( opt ) => { const evt = opt . e ; evt . preventDefault ( ) ; evt . stopPropagation ( ) ; const object = this . builderData . instance . onMouseDown ( opt ) ; if ( object ) { saveObject ( object ) ; } } ; this . canvas . on ( 'mouse:down' , mouseDownHandlerDrawing ) ; this . builderData . eventHandlers . push ( { event : 'mouse:down' , handler : mouseDownHandlerDrawing , } ) ; const pathCreatedHandlerDrawing = ( opt ) => { const path = ( < any > opt ) . path ; ( < DrawingBuilder > this . builderData . instance ) . build ( path ) . then ( ( object ) => { this . canvas . remove ( path ) ; this . canvas . requestRenderAll ( ) ; this . updateData ( ) ; this . updateHistory ( ) ; } ) ; } ; this . canvas . on ( 'path:created' , pathCreatedHandlerDrawing ) ; this . builderData . eventHandlers . push ( { event : 'path:created' , handler : pathCreatedHandlerDrawing , } ) ; break ; } } private unbindBuilderEvents ( type ) { this . builderData . eventHandlers . forEach ( handler => this . canvas . off ( handler . event , handler . handler ) ) ; switch ( type ) { case 'area' : case 'auto-identify-rect' : case 'drawing' : break ; case 'annotation' : case 'glyph' : Mousetrap . unbind ( 'esc' ) ; Mousetrap . unbind ( 'enter' ) ; break ; } } private resize ( e , width = this . workspaceEl . clientWidth , height = this . workspaceEl . clientHeight ) { if ( this . canvas ) { this . canvas . setDimensions ( { width : width , height : height , } ) ; this . canvas . requestRenderAll ( ) ; } } private reset ( ) { if ( this . canvas ) { const defaultSources = this . getObjects ( 'tid' , [ 'source' ] ) ; let width = this . canvas . getWidth ( ) ; let height = this . canvas . getHeight ( ) ; if ( defaultSources [ 0 ] ) { width = defaultSources [ 0 ] . width ; height = defaultSources [ 0 ] . height ; } this . zoom ( 1 ) ; this . canvas . viewportTransform [ 4 ] = ( this . canvas . getWidth ( ) - width ) / 2 ; this . canvas . viewportTransform [ 5 ] = ( this . canvas . getHeight ( ) - height ) / 2 ; this . setCoords ( [ ] ) ; } } private setObjectsSelected ( objects ) { const parents = [ ] ; objects . forEach ( ( o ) => { o . set ( this . getObjectStyles ( o . tid , 'active' ) ) ; o . set ( { selected : true } ) ; if ( o . parentId ) { parents . push ( o . parentId ) ; } } ) ; this . setObjectParentSelections ( uniq ( parents ) ) ; } private setObjectsDeselected ( objects = this . getBuilderObjects ( ) ) { const parentWords = [ ] ; objects . forEach ( ( o ) => { o . set ( this . getObjectStyles ( o . tid , 'inactive' ) ) ; o . set ( { selected : false } ) ; if ( o . tid && o . tid === 'glyph' && o . parentId ) { parentWords . push ( o . parentId ) ; } } ) ; this . setObjectParentSelections ( parentWords ) ; } private setObjectParentSelections ( parentWords ) { let parentSentences = [ ] ; parentWords . forEach ( parentId => { const word = this . getObject ( 'id' , parentId ) ; if ( word ) { word . selected = filter ( word . getGlyphs ( ) , glyph => glyph . selected ) . length ; if ( word . selected && word . parentId ) { parentSentences . push ( word . parentId ) ; } } } ) ; parentSentences = uniq ( parentSentences ) ; let sentenceSelectedCnt = 0 ; parentSentences . forEach ( parentId => { const sentence = this . getObject ( 'id' , parentId ) ; if ( sentence ) { sentence . selected = filter ( sentence . getWords ( ) , word => word . selected ) . length > 1 ; if ( sentence . selected ) { sentence . getWords ( ) . forEach ( word => word . selected = false ) ; sentenceSelectedCnt ++ ; } } } ) ; if ( sentenceSelectedCnt > 1 ) { parentSentences . forEach ( parentId => { const sentence = this . getObject ( 'id' , parentId ) ; if ( sentence ) { sentence . selected = false ; } } ) ; } this . config . selectedGlyphParent = this . builderData . selectedGlyphParent = this . getSelectedParent ( ) ; } private getSelectedParent ( ) { const selectedSentence = find ( this . data . analyse . sentences , s => s . selected ) ; if ( selectedSentence ) { return selectedSentence ; } let selectedWord = null ; find ( this . data . analyse . sentences , s => { selectedWord = find ( s . getWords ( ) , w => w . selected ) ; return ! ! selectedWord ; } ) ; return selectedWord ; } private updateActiveSelection ( tid ) { const selectedObjects = filter ( this . getBuilderObjects ( ) , o => typeof o . selected !== undefined && o . selected && o . tid === tid ) ; this . canvas . discardActiveObject ( ) ; let activeSelection = null ; if ( selectedObjects . length > 1 ) { activeSelection = new fabric . ActiveSelection ( selectedObjects , { canvas : this . canvas , } as fabric . IObjectOptions ) ; } else if ( selectedObjects . length === 1 ) { activeSelection = selectedObjects [ 0 ] ; } if ( activeSelection ) { this . canvas . setActiveObject ( activeSelection ) ; this . canvas . requestRenderAll ( ) ; } } private loadHistory ( ) { this . config . states . loaded = false ; this . canvas . clear ( ) ; this . canvas . loadFromJSON ( this . history . current . fabric , ( ) => { this . reloadObjects ( ) . then ( ( ) => { this . data . generate . effects = cloneDeep ( this . history . current . misc . effects ) ; this . config . tools . enabled . redo = this . history . redo . stack . length > 0 ; this . config . tools . enabled . undo = this . history . undo . stack . length > 0 ; this . canvas . requestRenderAll ( ) ; this . updateData ( ) ; this . config . states . loaded = true ; } ) ; } ) ; } private save ( ) { return new Promise ( ( resolve , reject ) => { if ( this . project ) { this . dataService . saveProject ( this . project , this . data , this . config . type ) . then ( resolve ) ; } } ) ; } private undo ( ) { this . history . redo . stack . push ( this . history . current ) ; this . history . current = this . history . undo . stack . pop ( ) ; this . loadHistory ( ) ; } private redo ( ) { this . history . undo . stack . push ( this . history . current ) ; this . history . current = this . history . redo . stack . pop ( ) ; this . loadHistory ( ) ; } private loadSeedData ( workspaceSeed ) { const promises = [ ] ; this . data . generate . effects = workspaceSeed . effects ; promises . push ( this . loadProcessedFacsimileFromStorage ( workspaceSeed . facsimile . processed ) ) ; workspaceSeed . sources . forEach ( ( sourceStorage ) => { promises . push ( this . loadSourceFromStorage ( sourceStorage ) ) ; } ) ; switch ( this . config . type ) { case 'process' : promises . push ( this . loadGeneratedFacsimileFromStorage ( workspaceSeed . facsimile . generated , true ) ) ; workspaceSeed . areas . forEach ( ( areaSeed ) => { promises . push ( this . loadAreaFromStorage ( areaSeed ) ) ; } ) ; break ; case 'generate' : promises . push ( this . loadGeneratedFacsimileFromStorage ( workspaceSeed . facsimile . generated ) ) ; workspaceSeed . drawings . forEach ( ( drawingStorage ) => { promises . push ( this . loadDrawingFromStorage ( drawingStorage ) ) ; } ) ; break ; case 'analyse' : promises . push ( this . loadGeneratedFacsimileFromStorage ( workspaceSeed . facsimile . generated ) ) ; workspaceSeed . sentences . forEach ( ( sentenceStorage ) => { promises . push ( this . loadSentenceFromStorage ( sentenceStorage ) ) ; } ) ; break ; case 'annotate' : promises . push ( this . loadGeneratedFacsimileFromStorage ( workspaceSeed . facsimile . generated ) ) ; workspaceSeed . annotations . forEach ( ( annotationStorage ) => { promises . push ( this . loadAnnotationFromStorage ( annotationStorage ) ) ; } ) ; break ; } return Promise . all ( promises ) ; } private reloadObjects ( ) { return new Promise ( resolve => { const promises = [ ] ; const objects = this . canvas . getObjects ( ) ; objects . forEach ( ( o ) => { switch ( o . tid ) { case 'source' : this . canvas . remove ( o ) ; promises . push ( this . reloadSource ( o ) ) ; break ; case 'processedFacsimile' : this . canvas . remove ( o ) ; promises . push ( this . reloadProcessedFacsimile ( o ) ) ; break ; case 'generatedFacsimile' : this . canvas . remove ( o ) ; promises . push ( this . reloadGeneratedFacsimile ( o ) ) ; break ; case 'area' : this . canvas . remove ( o ) ; promises . push ( this . reloadArea ( o ) ) ; break ; case 'annotation' : this . canvas . remove ( o ) ; promises . push ( this . reloadAnnotation ( o ) ) ; break ; case 'sentence' : this . canvas . remove ( o ) ; promises . push ( this . reloadSentence ( o ) ) ; break ; case 'word' : this . canvas . remove ( o ) ; promises . push ( this . reloadWord ( o ) ) ; break ; case 'glyph' : this . canvas . remove ( o ) ; promises . push ( this . reloadGlyph ( o ) ) ; break ; case 'drawing' : this . canvas . remove ( o ) ; promises . push ( this . reloadDrawing ( o ) ) ; break ; } } ) ; Promise . all ( promises ) . then ( ( ) => { const morePromises = [ ] ; this . reorderObjects ( ) ; this . updateData ( this . config . type === 'generate' ? true : false ) . then ( ( ) => { switch ( this . config . type ) { case 'process' : morePromises . push ( this . updateAreas ( ) ) ; break ; case 'generate' : break ; case 'analyse' : break ; case 'annotate' : break ; } Promise . all ( morePromises ) . then ( ( ) => { resolve ( ) ; } ) ; } ) ; } ) ; } ) ; } private loadSourceFromStorage ( sourceStorage ) { return new Promise ( ( resolve , reject ) => { fabric . Image . fromURL ( sourceStorage . dataURL , ( object ) => { if ( object ) { const source = new Source ( object . getElement ( ) , { } ) ; source . set ( { id : sourceStorage . id , title : sourceStorage . title , } ) ; this . canvas . add ( source ) ; resolve ( ) ; } else { reject ( this . translate . instant ( 'MODULES.PROJECT.SHARED.WORKSPACE.TS.ERROR.SOURCE.LOAD' , { source : sourceStorage . id } ) ) ; } } ) ; } ) ; } private reloadSource ( object ) { return new Promise ( ( resolve , reject ) => { fabric . Image . fromURL ( object . src , ( o ) => { if ( o ) { const source = new Source ( o . getElement ( ) , { } ) ; source . set ( { id : object . id , title : object . title , } ) ; this . canvas . add ( source ) ; resolve ( ) ; } else { reject ( this . translate . instant ( 'MODULES.PROJECT.SHARED.WORKSPACE.TS.ERROR.SOURCE.LOAD' , { source : object . id } ) ) ; } } ) ; } ) ; } private loadProcessedFacsimileFromStorage ( facsimileStorage ? ) { return new Promise ( ( resolve , reject ) => { if ( facsimileStorage ) { fabric . Image . fromURL ( facsimileStorage . dataURL , ( object ) => { if ( object ) { const facsimile = new Facsimile ( object . getElement ( ) , { } ) ; facsimile . set ( { id : facsimileStorage . id , tid : 'processedFacsimile' , opacity : 0 , } ) ; this . canvas . add ( facsimile ) ; resolve ( ) ; } else { reject ( this . translate . instant ( 'MODULES.PROJECT.SHARED.WORKSPACE.TS.ERROR.FACSIMILIE.LOAD' , { fax : facsimileStorage . id } ) ) ; } } ) ; } else { fabric . Image . fromURL ( 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=' , ( object ) => { if ( object ) { const facsimile = new Facsimile ( object . getElement ( ) , { } ) ; facsimile . set ( { tid : 'processedFacsimile' , opacity : 0 , } ) ; this . canvas . add ( facsimile ) ; resolve ( ) ; } else { reject ( this . translate . instant ( 'MODULES.PROJECT.SHARED.WORKSPACE.TS.ERROR.FACSIMILIE.EMPTY' ) ) ; } } ) ; } } ) ; } private reloadProcessedFacsimile ( object ) { return new Promise ( ( resolve , reject ) => { fabric . Image . fromURL ( object . src , ( o ) => { if ( o ) { const facsimile = new Facsimile ( o . getElement ( ) , { } ) ; facsimile . set ( { id : object . id , tid : 'processedFacsimile' , opacity : object . opacity , } ) ; this . canvas . add ( facsimile ) ; resolve ( ) ; } else { reject ( this . translate . instant ( 'MODULES.PROJECT.SHARED.WORKSPACE.TS.ERROR.FACSIMILIE.PROCESSED' , { fax : object . id } ) ) ; } } ) ; } ) ; } private updateProcessedFacsimile ( ) { return new Promise ( ( resolve , reject ) => { const sources = this . getObjects ( 'tid' , [ 'source' ] ) ; if ( sources . length ) { const source = sources [ 0 ] ; const width = source . width ; const height = source . height ; const canvas = document . createElement ( 'canvas' ) ; const ctx = canvas . getContext ( '2d' ) ; ctx . imageSmoothingEnabled = false ; canvas . width = width ; canvas . height = height ; const areas = this . getObjects ( 'tid' , [ 'area' ] ) ; areas . forEach ( ( area ) => { const elem = area . getElement ( ) ; ctx . drawImage ( ( < any > area ) . _element , area . left , area . top ) ; } ) ; const facsimileDataURL = canvas . toDataURL ( ) ; fabric . Image . fromURL ( facsimileDataURL , ( object ) => { if ( object ) { const previousFacsimile = this . getObject ( 'tid' , 'processedFacsimile' ) ; let facsimileId = null ; let facsimileOpacity = 1 ; if ( previousFacsimile ) { facsimileId = previousFacsimile . id ; facsimileOpacity = previousFacsimile . opacity ; this . canvas . remove ( previousFacsimile ) ; } const newFacsimile = new Facsimile ( object . getElement ( ) , { } ) ; newFacsimile . set ( { tid : 'processedFacsimile' , opacity : facsimileOpacity , } ) ; if ( facsimileId ) { newFacsimile . set ( { id : facsimileId , } ) ; } this . canvas . add ( newFacsimile ) ; this . updateGeneratedFacsimile ( true ) . then ( ( ) => { this . reorderObjects ( ) ; resolve ( ) ; } , resolve ) ; } else { reject ( this . translate . instant ( 'MODULES.PROJECT.SHARED.WORKSPACE.TS.ERROR.FACSIMILIE.UPDATE' ) ) ; } } ) ; } else { reject ( ) ; } } ) ; } private loadGeneratedFacsimileFromStorage ( facsimileStorage ? , hide = false ) { return new Promise ( ( resolve , reject ) => { if ( facsimileStorage ) { fabric . Image . fromURL ( facsimileStorage . dataURL , ( object ) => { if ( object ) { const facsimile = new Facsimile ( object . getElement ( ) , { } ) ; facsimile . set ( { id : facsimileStorage . id , tid : 'generatedFacsimile' , opacity : hide ? 0 : 1 , } ) ; this . canvas . add ( facsimile ) ; resolve ( ) ; } else { reject ( this . translate . instant ( 'MODULES.PROJECT.SHARED.WORKSPACE.TS.ERROR.FACSIMILIE.LOAD' , { fax : facsimileStorage . id } ) ) ; } } ) ; } else { fabric . Image . fromURL ( 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=' , ( object ) => { if ( object ) { const facsimile = new Facsimile ( object . getElement ( ) , { } ) ; facsimile . set ( { tid : 'generatedFacsimile' , opacity : hide ? 0 : 1 , } ) ; this . canvas . add ( facsimile ) ; resolve ( ) ; } else { reject ( this . translate . instant ( 'MODULES.PROJECT.SHARED.WORKSPACE.TS.ERROR.FACSIMILIE.EMPTY' ) ) ; } } ) ; } } ) ; } private reloadGeneratedFacsimile ( object ) { return new Promise ( ( resolve , reject ) => { fabric . Image . fromURL ( object . src , ( o ) => { if ( o ) { const facsimile = new Facsimile ( o . getElement ( ) , { } ) ; facsimile . set ( { id : object . id , tid : 'generatedFacsimile' , opacity : object . opacity , } ) ; this . canvas . add ( facsimile ) ; resolve ( ) ; } else { reject ( this . translate . instant ( 'MODULES.PROJECT.SHARED.WORKSPACE.TS.ERROR.FACSIMILIE.GENERATED' , { fax : object . id } ) ) ; } } ) ; } ) ; } private updateGeneratedFacsimile ( hide = false ) { return new Promise ( ( resolve , reject ) => { const processedFacsimile = this . getObject ( 'tid' , 'processedFacsimile' ) ; if ( processedFacsimile ) { const width = processedFacsimile . width ; const height = processedFacsimile . height ; const canvas = document . createElement ( 'canvas' ) ; const ctx = canvas . getContext ( '2d' ) ; ctx . imageSmoothingEnabled = false ; canvas . width = width ; canvas . height = height ; const oldOpacity = processedFacsimile . opacity ; processedFacsimile . opacity = 1 ; ctx . drawImage ( ( < any > processedFacsimile ) . _element , processedFacsimile . left , processedFacsimile . top , processedFacsimile . width , processedFacsimile . height ) ; processedFacsimile . opacity = oldOpacity ; let imageData = ctx . getImageData ( 0 , 0 , canvas . width , canvas . height ) ; if ( this . data . generate . effects . drawing . active ) { this . data . generate . drawings . forEach ( ( drawing ) => { imageData = drawing . getCompositeImageData ( imageData ) ; } ) ; } if ( this . data . generate . effects . trace . active ) { const trace = new Trace ( imageData ) ; imageData = trace . update ( { level : this . data . generate . effects . trace . level } ) ; } if ( this . data . generate . effects . outlines . active ) { const outlines = new Outlines ( imageData ) ; imageData = outlines . update ( { level : this . data . generate . effects . outlines . level } ) ; } ctx . putImageData ( imageData , 0 , 0 ) ; const facsimileDataURL = canvas . toDataURL ( ) ; fabric . Image . fromURL ( facsimileDataURL , ( object ) => { if ( object ) { const previousFacsimile = this . getObject ( 'tid' , 'generatedFacsimile' ) ; let facsimileId = null ; let facsimileOpacity = hide ? 0 : 1 ; if ( previousFacsimile ) { facsimileId = previousFacsimile . id ; facsimileOpacity = previousFacsimile . opacity ; } const newFacsimile = new Facsimile ( object . getElement ( ) , { } ) ; newFacsimile . set ( { tid : 'generatedFacsimile' , opacity : facsimileOpacity , } ) ; if ( facsimileId ) { newFacsimile . set ( { id : facsimileId , } ) ; } this . canvas . add ( newFacsimile ) ; if ( previousFacsimile ) { this . canvas . remove ( previousFacsimile ) ; } this . data . generate . drawings . forEach ( ( drawing ) => { drawing . set ( { opacity : 0 } ) ; } ) ; this . reorderObjects ( ) ; resolve ( ) ; } else { reject ( `Cannot update facsimile` ) ; } } ) ; } else { reject ( ) ; } } ) ; } private loadAreaFromStorage ( areaSeed ) { return new Promise ( resolve => { const area = new Area ( areaSeed . position ) ; area . set ( { author : areaSeed . author , created : areaSeed . created , effects : areaSeed . effects , source : areaSeed . source , } ) ; const elem = area . getElement ( ) ; elem . width = area . width ; elem . height = area . height ; this . canvas . add ( area ) ; resolve ( ) ; } ) ; } private reloadArea ( object ) { return new Promise ( ( resolve , reject ) => { const position = { top : object . top , left : object . left , width : object . width , height : object . height , } ; const area = new Area ( position ) ; area . set ( { author : object . author , created : object . created , effects : object . effects , source : object . source , } ) ; this . canvas . add ( area ) ; resolve ( ) ; } ) ; } private updateAreas ( ) { const promises = [ ] ; this . data . process . areas . forEach ( area => { promises . push ( area . updateImage ( this . getObject ( 'id' , area . source ) ) ) ; } ) ; return Promise . all ( promises ) . then ( ( ) => { this . canvas . requestRenderAll ( ) ; return null ; } ) ; } private loadDrawingFromStorage ( drawingStorage ) { return new Promise ( resolve => { this . utils . imgFromDataURL ( drawingStorage . dataURL ) . then ( ( img ) => { const drawing = new Drawing ( drawingStorage . position , img ) ; drawing . set ( { author : drawingStorage . author , created : drawingStorage . created , } ) ; this . canvas . add ( drawing ) ; resolve ( ) ; } ) ; } ) ; } private reloadDrawing ( object ) { return new Promise ( ( resolve , reject ) => { this . utils . imgFromDataURL ( object . src ) . then ( ( img ) => { const position = { top : object . top , left : object . left , width : object . width , height : object . height , } ; const drawing = new Drawing ( position , img ) ; drawing . set ( { author : object . author , created : object . created , } ) ; this . canvas . add ( drawing ) ; resolve ( ) ; } ) ; } ) ; } private loadAnnotationFromStorage ( annotationStorage ) { return new Promise ( resolve => { const annotation = new Annotation ( annotationStorage . points ) ; annotation . set ( { author : annotationStorage . author , created : annotationStorage . created , comments : annotationStorage . comments , } ) ; this . canvas . add ( annotation ) ; resolve ( ) ; } ) ; } private reloadAnnotation ( object ) { return new Promise ( resolve => { const annotation = new Annotation ( object . points ) ; annotation . set ( { author : object . author , created : object . created , comments : object . comments , } ) ; this . canvas . add ( annotation ) ; resolve ( annotation ) ; } ) ; } private createGlyph ( points , order , parent ? ) { const glyph = new Glyph ( points , parent ? parent . id : null , this . canvas ) ; glyph . set ( { author : this . settings . userFullName , order : order , } ) ; return glyph ; } private reloadGlyph ( object ) { return new Promise ( resolve => { const glyph = new Glyph ( object . points , object . parentId , this . canvas ) ; glyph . set ( { author : object . author , created : object . created , gardinerCode : object . gardinerCode , gardinerCodePredictions : object . gardinerCodePredictions , id : object . id , locked : object . locked , order : object . order , } ) ; this . canvas . add ( glyph ) ; resolve ( glyph ) ; } ) ; } private addGlyphToSequence ( glyph , parent ) { return new Promise ( resolve => { if ( glyph ) { if ( parent && parent . tid ) { switch ( parent . tid ) { case 'sentence' : const word = new Word ( null , parent . id , this . canvas ) ; glyph . parentId = word . id ; word . addGlyph ( glyph ) ; ( < Sentence > parent ) . addWord ( word ) ; break ; case 'word' : glyph . parentId = parent . id ; ( < Word > parent ) . addGlyph ( glyph ) ; break ; } } else { const sentence = new Sentence ( null , this . canvas ) ; const word = new Word ( null , sentence . id , this . canvas ) ; glyph . parentId = word . id ; word . addGlyph ( glyph ) ; sentence . order = this . getObjects ( 'tid' , [ 'sentence' ] ) . length ; sentence . addWord ( word ) ; this . canvas . add ( sentence ) ; } } resolve ( glyph ) ; } ) ; } private loadSentenceFromStorage ( sentenceStorage ) { return new Promise ( resolve => { const sentence = new Sentence ( sentenceStorage , this . canvas ) ; this . canvas . add ( sentence ) ; resolve ( ) ; } ) ; } private reloadSentence ( object ) { return new Promise ( resolve => { const sentenceStorage = { author : object . author , created : object . created , words : [ ] , order : object . order , position : { top : object . top , left : object . left , width : object . width , height : object . height , } , interpretation : object . interpretation , transliteration : object . transliteration , } ; const sentence = new Sentence ( sentenceStorage , this . canvas ) ; sentence . set ( { id : object . id , words : object . words , expanded : object . expanded , } ) ; this . canvas . add ( sentence ) ; resolve ( ) ; } ) ; } private reloadWord ( object ) { return new Promise ( resolve => { const wordStorage = { author : object . author , created : object . created , glyphs : [ ] , isCartouche : object . isCartouche , order : object . order , position : { top : object . top , left : object . left , width : object . width , height : object . height , } , translation : object . translation , transliteration : object . transliteration , } ; const word = new Word ( wordStorage , object . parentId , this . canvas ) ; word . set ( { id : object . id , glyphs : object . glyphs , expanded : object . expanded , } ) ; this . canvas . add ( word ) ; resolve ( ) ; } ) ; } private fetchAutoIdentifyRectSentence ( object , targetParent ? ) { return new Promise ( ( resolve , reject ) => { const presentGlyphs = this . findGlyphsInRect ( object ) ; if ( presentGlyphs . length > 0 && ! confirm ( this . translate . instant ( 'MODULES.PROJECT.SHARED.WORKSPACE.TS.CONFIRM' , { glyphs : presentGlyphs . length } ) ) ) { resolve ( [ ] ) ; return ; } this . deleteGlyphs ( presentGlyphs , true ) ; const objectPosition = this . calcObjectPosition ( object ) ; this . getRectImgOfFacsimile ( objectPosition , true ) . then ( ( dataUrl ) => { const data = { image : dataUrl , direction : ( < AutoIdentifyRect > object ) . direction , threshold : this . settings . clusterAnalysis . threshold , } ; this . apiService . post ( 'identify' , data ) . then ( ( identificationResponse ) => { let sentence = null ; let word = null ; let isNewSentence = false ; let isSameWord = false ; const totalGlyphs = [ ] ; if ( targetParent && targetParent . tid ) { if ( targetParent . tid === 'sentence' ) { sentence = ( < Sentence > targetParent ) ; } else if ( targetParent . tid === 'word' ) { sentence = ( < Word > targetParent ) . getParent ( ) ; word = ( < Word > targetParent ) ; isSameWord = true ; } } else { sentence = new Sentence ( null , this . canvas ) ; sentence . order = this . getObjects ( 'tid' , [ 'sentence' ] ) . length ; isNewSentence = true ; } const identificationWords = this . parseAutoIdentifyRectGlyphs ( identificationResponse ) ; identificationWords . forEach ( ( identificationWord , i ) => { if ( identificationWord . clusters . length ) { const glyphs = [ ] ; if ( ! isSameWord || isSameWord && i > 0 ) { word = new Word ( null , sentence . id , this . canvas ) ; word . isCartouche = identificationWord . isCartouche ; word . order = i ; } identificationWord . clusters . forEach ( cluster => { if ( cluster . bounds && cluster . hull && cluster . hull . length ) { const points = [ ] ; cluster . hull . forEach ( hullPoint => { points . push ( { x : hullPoint . x + objectPosition . left , y : hullPoint . y + objectPosition . top , } ) ; } ) ; const glyph = this . createGlyph ( points , cluster . bounds . order , word ) ; glyphs . push ( glyph ) ; totalGlyphs . push ( glyph ) ; } } ) ; glyphs . forEach ( g => word . addGlyph ( g ) ) ; if ( ! isSameWord || isSameWord && i > 0 ) { sentence . addWord ( word ) ; } } else { this . notificationsService . error ( this . translate . instant ( 'MODULES.PROJECT.SHARED.WORKSPACE.TS.ERROR.API_MAL' ) ) ; } } ) ; if ( isNewSentence ) { this . canvas . add ( sentence ) ; } resolve ( totalGlyphs ) ; } , ( err ) => { this . notificationsService . error ( this . translate . instant ( 'MODULES.PROJECT.SHARED.WORKSPACE.TS.ERROR.API_ID' ) ) ; console . warn ( this . translate . instant ( 'MODULES.PROJECT.SHARED.WORKSPACE.TS.WARN.API_ID' ) , err ) ; resolve ( null ) ; } ) ; } ) ; } ) ; } private parseAutoIdentifyRectGlyphs ( identificationResponse ) { let identificationWords = [ ] ; if ( identificationResponse ) { if ( identificationResponse . cartouches ) { const clustersLookup = { } ; identificationResponse . clusters . forEach ( ( cluster , i ) => { clustersLookup [ 'cluster_' + cluster . cluster_id ] = cluster ; } ) ; identificationResponse . cartouches . forEach ( ( cartouche , i ) => { cartouche . inner . forEach ( ( inner , j ) => { if ( j === 0 ) { if ( identificationResponse . direction === 'ltr' ) { clustersLookup [ 'cluster_' + inner ] . isCartoucheStart = true ; } else if ( identificationResponse . direction === 'rtl' ) { clustersLookup [ 'cluster_' + inner ] . isCartoucheEnd = true ; } } else if ( j === cartouche . inner . length - 1 ) { if ( identificationResponse . direction === 'ltr' ) { clustersLookup [ 'cluster_' + inner ] . isCartoucheEnd = true ; } else if ( identificationResponse . direction === 'rtl' ) { clustersLookup [ 'cluster_' + inner ] . isCartoucheStart = true ; } } else { clustersLookup [ 'cluster_' + inner ] . isCartoucheInner = true ; } } ) ; cartouche . frames . forEach ( ( frame , j ) => { if ( clustersLookup [ 'cluster_' + frame ] ) { delete clustersLookup [ 'cluster_' + frame ] ; } } ) ; cartouche . wrapper . forEach ( ( wrapper , j ) => { if ( clustersLookup [ 'cluster_' + wrapper ] ) { delete clustersLookup [ 'cluster_' + wrapper ] ; } } ) ; } ) ; let wordCnt = 0 ; for ( const key in clustersLookup ) { if ( clustersLookup . hasOwnProperty ( key ) ) { const cluster = clustersLookup [ key ] ; if ( cluster . isCartoucheStart ) { wordCnt ++ ; } if ( ! identificationWords [ wordCnt ] ) { identificationWords [ wordCnt ] = { isCartouche : cluster . isCartoucheStart || false , clusters : [ ] , } ; } identificationWords [ wordCnt ] . clusters . push ( cluster ) ; if ( cluster . isCartoucheEnd ) { wordCnt ++ ; } } } } else { identificationWords = [ { isCartouche : false , clusters : identificationResponse . clusters , } ] ; } } return identificationWords ; } private findGlyphsInRect ( rectObject ) { const glyphs = [ ] ; this . getObjects ( 'tid' , [ 'glyph' ] ) . forEach ( glyph => { if ( rectObject . intersectsWithObject ( glyph ) ) { glyphs . push ( glyph ) ; } } ) ; return glyphs ; } private setCoords ( types = [ ] ) { if ( this . canvas ) { let objects = [ ] ; if ( types . length ) { objects = this . getObjects ( 'tid' , types ) ; } else { objects = this . canvas . getObjects ( ) ; } objects . forEach ( o => o . setCoords ( ) ) ; this . canvas . requestRenderAll ( ) ; } } private getBuilderObjects ( ) { const objects = filter ( this . canvas . getObjects ( ) , o => { return o . tid && o . tid !== 'source' && o . tid !== 'processedFacsimile' && o . tid !== 'generatedFacsimile' ; } ) ; return this . getDeepObjects ( objects ) ; } private getDeepObjects ( objects ) { let deepObjects = [ ] ; if ( objects ) { objects . forEach ( o => { if ( o . id ) { if ( o . tid === 'sentence' ) { deepObjects = unionBy ( deepObjects , ( < Sentence > o ) . getWords ( ( < Sentence > o ) . wordCount ( ) > 0 ) ) ; deepObjects = unionBy ( deepObjects , ( < Sentence > o ) . getGlyphs ( ( < Sentence > o ) . glyphCount ( ) > 0 ) ) ; } else if ( o . tid === 'word' ) { deepObjects = unionBy ( deepObjects , ( < Word > o ) . getGlyphs ( ( < Word > o ) . glyphCount ( ) > 0 ) ) ; } deepObjects = unionBy ( deepObjects , [ o ] ) ; } } ) ; } return deepObjects ; } private zoom ( zoom ) { if ( this . canvas ) { this . config . tools . enabled . zoomIn = true ; this . config . tools . enabled . zoomZero = true ; this . config . tools . enabled . zoomOut = true ; if ( zoom > 5 ) { zoom = 5 ; this . config . tools . enabled . zoomIn = false ; } if ( zoom === 1 ) { this . config . tools . enabled . zoomZero = false ; } if ( zoom < 0.1 ) { zoom = 0.1 ; this . config . tools . enabled . zoomOut = false ; } this . canvas . setZoom ( zoom ) ; this . updateDrawEraseClasses ( ) ; this . savePosition ( ) ; } } private zoomByDelta ( delta , point ? ) { if ( this . canvas ) { this . config . tools . enabled . zoomIn = true ; this . config . tools . enabled . zoomZero = true ; this . config . tools . enabled . zoomOut = true ; point = point || new fabric . Point ( this . canvas . getWidth ( ) / 2 , this . canvas . getHeight ( ) / 2 ) ; let zoom = this . canvas . getZoom ( ) ; zoom = zoom * ( 1 - ( 100 / zoom ) * ( delta / 500 ) ) ; if ( zoom > 5 ) { zoom = 5 ; this . config . tools . enabled . zoomIn = false ; } if ( zoom === 1 ) { this . config . tools . enabled . zoomZero = false ; } if ( zoom < 0.1 ) { zoom = 0.1 ; this . config . tools . enabled . zoomOut = false ; } this . canvas . zoomToPoint ( point , zoom ) ; this . updateDrawEraseClasses ( ) ; this . savePosition ( ) ; } } private pan ( deltaX , deltaY ) { if ( this . canvas ) { this . canvas . viewportTransform [ 4 ] += deltaX ; this . canvas . viewportTransform [ 5 ] += deltaY ; this . setCoords ( [ ] ) ; this . savePosition ( ) ; } } private loadPosition ( position ) { if ( position && ( position . x !== 0 && position . y !== 0 && position . zoom !== 1 ) ) { this . zoom ( position . zoom ) ; this . pan ( position . x - this . canvas . viewportTransform [ 4 ] , position . y - this . canvas . viewportTransform [ 5 ] ) ; } else { this . reset ( ) ; } } private savePosition ( ) { if ( this . config . states . loaded ) { const position = { x : this . canvas . viewportTransform [ 4 ] , y : this . canvas . viewportTransform [ 5 ] , zoom : this . canvas . getZoom ( ) , } ; this . dataService . updateWorkspacePosition ( position ) ; } } private getObjectStyles ( objectType , state ) { switch ( objectType ) { case 'area' : return areaStyles [ state ] ; case 'annotation' : return annotationStyles [ state ] ; case 'glyph' : return glyphStyles [ state ] ; } } private enableBuilderTool ( type ) { switch ( type ) { case 'area' : this . canvas . defaultCursor = 'crosshair' ; this . builderData . instance = new AreaBuilder ( this . canvas , this . settings . userFullName , this . data . sources [ 0 ] . id ) ; break ; case 'annotation' : this . canvas . defaultCursor = 'crosshair' ; this . builderData . instance = new AnnotationBuilder ( this . canvas , this . settings . userFullName ) ; break ; case 'glyph' : this . canvas . defaultCursor = 'crosshair' ; this . canvas . selection = false ; this . builderData . instance = new GlyphBuilder ( this . canvas , this . settings . userFullName , this . builderData . selectedGlyphParent ) ; break ; case 'auto-identify-rect' : this . canvas . defaultCursor = 'crosshair' ; this . canvas . selection = false ; this . builderData . instance = new AutoIdentifyRectBuilder ( this . canvas , this . builderData . selectedGlyphParent ) ; break ; case 'drawing' : this . canvas . defaultCursor = 'crosshair' ; this . canvas . selection = false ; this . canvas . isDrawingMode = true ; this . builderData . instance = new DrawingBuilder ( this . canvas , this . settings . userFullName , { type : this . config . tools . selected === 'erase' ? 'erase' : 'draw' , width : this . data . generate . effects . drawing . width , } as DrawData ) ; break ; } this . builderData . type = type ; this . bindBuilderEvents ( type ) ; } private disableBuilderTool ( type ) { switch ( type ) { case 'area' : case 'annotation' : this . canvas . defaultCursor = 'default' ; break ; case 'glyph' : case 'auto-identify-rect' : this . canvas . defaultCursor = 'default' ; this . canvas . selection = true ; break ; case 'drawing' : this . canvas . defaultCursor = 'default' ; this . canvas . isDrawingMode = false ; break ; } this . builderData . type = null ; this . builderData . instance . clear ( ) ; this . builderData . instance = null ; this . unbindBuilderEvents ( type ) ; } private updateDrawEraseClasses ( ) { const bodyEl = document . body ; bodyEl . classList . remove ( 'draw-tool' ) ; bodyEl . classList . remove ( 'erase-tool' ) ; if ( this . data . generate . effects . drawing . active && ( this . config . tools . selected === 'draw' || this . config . tools . selected === 'erase' ) ) { bodyEl . classList . add ( this . config . tools . selected + '-tool' ) ; this . removeClassIfStartsWith ( bodyEl , 'cursor-width-' ) ; const cursorWidth = Math . round ( this . data . generate . effects . drawing . width * this . canvas . getZoom ( ) ) ; bodyEl . classList . add ( 'cursor-width-' + Math . min ( cursorWidth , 128 ) ) ; } } private removeClassIfStartsWith ( el , startsWith ) { if ( el && startsWith ) { el . classList . forEach ( className => { if ( className . startsWith ( startsWith ) ) { el . classList . remove ( className ) ; } } ) ; } } private cursorLoading ( state ) { if ( state ) { document . body . classList . add ( 'workspace-loading' ) ; } else { document . body . classList . remove ( 'workspace-loading' ) ; } } private errorHandler ( err ) { this . notificationsService . error ( this . translate . instant ( 'MODULES.PROJECT.SHARED.WORKSPACE.TS.ERROR.WORKSPACE' , { err : err . message } ) ) ; throw new Error ( err . message ) ; } }