import { ClassificationResult , ClassificationRequest } from '../../../../core/api/classification.modal' ; import { ConfirmClassificationRequest } from '../../../../core/api/confirm-classification.model' ; import { Settings } from './../../../settings/settings.model' ; import { NotificationsService } from './../../../../core/notifications/notifications.service' ; import { includes , filter , pull , find , orderBy , map , uniq , concat } from 'lodash' ; import { ApiService } from './../../../../core/api/api.service' ; import { Glyph } from './../../shared/workspace/glyph.model' ; import { WorkspaceComponent } from './../../shared/workspace/workspace.component' ; import { Component , ViewChild , OnDestroy , ChangeDetectorRef , ChangeDetectionStrategy } from '@angular/core' ; import { BehaviorSubject , Observable , combineLatest , Subscription } from 'rxjs' ; import { DataService } from './../../../../core/data/data.service' ; import { WorkspaceSeed , WorkspaceData } from '../../shared/workspace/workspace.model' ; import { ProjectAside , ProjectPanel } from '../../project.model' ; import { AnalyseConfig , AnalyseTranslationResult , AnalyseTranslationResults } from './analyse.model' ; import { NgxSmartModalService } from 'ngx-smart-modal' ; import { Sentence } from '../../shared/workspace/sentence.model' ; import { Word } from '../../shared/workspace/word.model' ; import { TranslateService } from '@ngx-translate/core' ; import { debounce } from 'lodash' ; import * as ArrayMove from 'array-move' ; import { TranslationRequest , TranslationResult } from '../../../../core/api/translation.model' ; import { WordStorage } from '../../../../core/storage/storage.model' ; import Sortable from 'sortablejs' ; import { gardinerGlyphs } from '../../shared/gardiner-glyphs.model' ; import { GlyphPickerModalComponent } from '../../shared/glyph-picker-modal/glyph-picker-modal.component' ; interface WordCache { parentId : Sentence [ 'id' ] ; words : Array < Word > ; } interface GlyphCache { parentId : Word [ 'id' ] ; glyphs : Array < Glyph > ; } @ Component ( { selector : 'app-analyse' , templateUrl : './analyse.component.html' , styleUrls : [ './analyse.component.scss' ] , changeDetection : ChangeDetectionStrategy . OnPush , } ) export class AnalyseComponent implements OnDestroy { @ ViewChild ( 'workspace' , { static : true } ) private workspace : WorkspaceComponent ; updateWorkspace : ( ) => any = debounce ( this . updateWorkspaceDebounced , 100 , { trailing : true } ) ; aside : ProjectAside = { active : true , loaded : false , } ; panel : ProjectPanel = { active : false , loaded : false , } ; settings : Settings ; workspaceData : WorkspaceData [ 'analyse' ] ; config : AnalyseConfig = { selected : [ ] , editing : { active : false , object : null , } , ai : { identify : { enabled : false , loading : false , } , confirmation : { enabled : false } , classification : { enabled : false , loading : false , glyphs : [ ] , } , translation : { enabled : false , active : false , sentences : [ ] , } , } , sortable : { instances : [ ] , } , } ; sentencesCache : Array < Sentence > ; wordsCache : Array < WordCache > ; glyphsCache : Array < GlyphCache > ; private componentSubs : Array < Subscription > = [ ] ; private workspaceReadySub : BehaviorSubject < boolean > = new BehaviorSubject ( false ) ; private workspaceReadyObs : Observable < boolean > = this . workspaceReadySub . asObservable ( ) ; enableDragDrop = false ; private enableHighlighting = true ; constructor ( private dataService , private apiService , private notificationService , private ngxSmartModalService , private translate , private changeDetector ) { this . sentencesCache = [ ] ; this . wordsCache = [ ] ; this . glyphsCache = [ ] ; this . componentSubs . push ( this . apiService . servicesEnabledObs . subscribe ( servicesEnabled => { this . config . ai . identify . enabled = includes ( servicesEnabled , 'clusterAnalysis' ) ; this . config . ai . classification . enabled = includes ( servicesEnabled , 'classification' ) ; this . config . ai . translation . enabled = includes ( servicesEnabled , 'translation' ) ; this . config . ai . confirmation . enabled = includes ( servicesEnabled , 'confirmation' ) ; } ) , this . dataService . settingsObs . subscribe ( ( settings ) => { this . settings = settings ; } ) ) ; const loadSub = combineLatest ( [ this . dataService . projectObs , this . dataService . workspaceSeedObs , this . workspaceReadyObs ] ) . subscribe ( ( [ project , workspaceSeed , workspaceReady ] ) => { if ( project && workspaceSeed && workspaceReady ) { this . workspaceInit ( workspaceSeed ) ; loadSub . unsubscribe ( ) ; } } ) ; } ngOnDestroy ( ) { this . componentSubs . forEach ( sub => sub . unsubscribe ( ) ) ; } onWorkspaceReady ( ) { this . workspaceReadySub . next ( true ) ; } workspaceInit ( workspaceSeed ) { this . workspace . init ( 'analyse' , workspaceSeed ) ; } workspaceUpdate ( workspaceData ) { this . workspaceData = workspaceData . analyse ; this . setListCaches ( ) ; this . config . selected = this . getSelectedGlyphs ( ) ; if ( this . enableDragDrop ) { this . dragDropInit ( ) ; } else { setTimeout ( ( ) => { this . aside . loaded = true ; this . changeDetector . detectChanges ( ) ; } , 10 ) ; } } setListCaches ( ) { this . sentencesCache = [ ] ; this . wordsCache = [ ] ; this . glyphsCache = [ ] ; if ( this . workspaceData ) { this . workspaceData . sentences . forEach ( ( s ) => { this . sentencesCache . push ( s ) ; const sentenceWords = s . getWords ( ) ; const wordCache = { parentId : s . id , words : sentenceWords , } ; this . wordsCache . push ( wordCache ) ; sentenceWords . forEach ( ( w ) => { const glyphCache = { parentId : w . id , glyphs : w . getGlyphs ( ) , } ; this . glyphsCache . push ( glyphCache ) ; } ) ; } ) ; } } getSentenceList ( ) { return this . sentencesCache || [ ] ; } getWordList ( sentence ) { let words = [ ] ; if ( sentence ) { const wordsCache = find ( this . wordsCache , { parentId : sentence . id } ) ; if ( wordsCache ) { words = wordsCache . words ; } } return words ; } getGlyphList ( type , object ) { let glyphs = [ ] ; if ( object ) { if ( type === 'sentence' ) { ( < Sentence > object ) . words . forEach ( ( wordId ) => { const glyphsCache = find ( this . glyphsCache , { parentId : wordId } ) ; if ( glyphsCache ) { glyphs = concat ( glyphs , glyphsCache . glyphs ) ; } } ) ; } if ( type === 'word' ) { const glyphsCache = find ( this . glyphsCache , { parentId : object . id } ) ; if ( glyphsCache ) { glyphs = glyphsCache . glyphs ; } } } return glyphs ; } selectGlyph ( glyph ) { this . workspace . selectObjects ( [ glyph ] , true , false ) ; } deleteGlyph ( glyph ) { this . workspace . deleteGlyphs ( [ glyph ] ) ; } toggleExpand ( type , object ) { if ( object ) { if ( type === 'sentence' ) { ( < Sentence > object ) . expanded = ! ( < Sentence > object ) . expanded ; this . changeDetector . detectChanges ( ) ; } if ( type === 'word' ) { ( < Word > object ) . expanded = ! ( < Word > object ) . expanded ; this . changeDetector . detectChanges ( ) ; } } } nudge ( direction , type , object , parent ) { if ( object ) { const newOrder = direction === 'up' ? object . order - 1 : object . order + 1 ; if ( type === 'sentence' ) { this . workspace . reorderSentences ( ) ; const list = orderBy ( this . workspaceData . sentences , 'order' ) ; ArrayMove . mutate ( list , object . order , newOrder ) ; let cnt = 0 ; list . forEach ( ( item ) => { if ( item ) { item . order = cnt ; cnt ++ ; } } ) ; this . workspace . reorderObjects ( ) ; } if ( type === 'word' ) { ( < Sentence > parent ) . reorderWords ( ) ; const list = ( < Sentence > parent ) . getWords ( ) ; ArrayMove . mutate ( list , object . order , newOrder ) ; let cnt = 0 ; list . forEach ( ( item ) => { if ( item ) { item . order = cnt ; cnt ++ ; } } ) ; ( < Sentence > parent ) . reorderWords ( ) ; } if ( type === 'glyph' ) { ( < Word > parent ) . reorderGlyphs ( ) ; const list = ( < Word > parent ) . getGlyphs ( ) ; ArrayMove . mutate ( list , object . order , newOrder ) ; let cnt = 0 ; list . forEach ( ( item ) => { if ( item ) { item . order = cnt ; cnt ++ ; } } ) ; ( < Word > parent ) . reorderGlyphs ( ) ; } this . updateWorkspace ( ) ; } } highlightSentence ( sentence , state ) { if ( this . enableHighlighting ) { this . workspace . highlightObjects ( this . getGlyphList ( 'sentence' , sentence ) , state ) ; } } highlightSentenceRange ( sentence , start , end , state ) { if ( this . enableHighlighting ) { this . getGlyphList ( 'sentence' , sentence ) . forEach ( ( glyph , i ) => { if ( i >= start && i <= end ) { this . highlightGlyph ( glyph , state ) ; this . highlightTranslationPanelGlyph ( glyph , state ) ; } } ) ; } } highlightWord ( word , state ) { if ( this . enableHighlighting ) { this . workspace . highlightObjects ( this . getGlyphList ( 'word' , word ) , state ) ; } } highlightGlyph ( glyph , state ) { if ( this . enableHighlighting ) { this . workspace . highlightObjects ( [ glyph ] , state ) ; } } highlightTranslationPanelGlyph ( glyph , state ) { if ( this . enableHighlighting ) { const glyphEl = document . querySelector ( `.c-translation-glyphs__item[data-glyph-id="${ glyph . id }"]` ) ; if ( glyphEl ) { if ( state ) { glyphEl . classList . add ( 'is-highlighted' ) ; } else { glyphEl . classList . remove ( 'is-highlighted' ) ; } } } } autoClassifyGlyphs ( glyphs ) { if ( this . config . ai . classification . enabled ) { let identifiedGlyphPresent = false ; const unlockedGlyphs = filter ( glyphs , ( g ) => { if ( g . gardinerCode ) { identifiedGlyphPresent = true ; } return ! g . locked ; } ) ; if ( unlockedGlyphs . length > 0 && identifiedGlyphPresent && ! confirm ( this . translate . instant ( 'MODULES.PROJECT.MODULES.ANALYSE.TS.CONFIRM' , { glyphs : unlockedGlyphs . length } ) ) ) { return ; } this . config . ai . classification . loading = true ; let sequence = Promise . resolve ( ) ; unlockedGlyphs . forEach ( ( glyph ) => { sequence = sequence . then ( ( ) => { return new Promise < void > ( ( resolve , reject ) => { this . autoClassifyGlyph ( glyph ) . then ( ( ) => { resolve ( ) ; } ) ; } ) ; } ) ; } ) ; sequence . then ( ( ) => { this . config . ai . classification . loading = false ; this . updateWorkspace ( ) ; } , err => { console . warn ( 'Classification ERROR: ' , err ) ; console . warn ( this . translate . instant ( 'MODULES.PROJECT.MODULES.ANALYSE.TS.WARN.CLASSIFICATION' ) , err ) ; this . notificationService . error ( this . translate . instant ( 'MODULES.PROJECT.MODULES.ANALYSE.TS.ERROR.CLASSIFICATION' ) ) ; this . config . ai . classification . loading = false ; } ) ; } } confirmGlyphClassification ( glyph ) { return new Promise ( ( resolve , reject ) => { this . workspace . getPolyImgOfFacsimile ( glyph ) . then ( ( imgSrc ) => { const data = { image : imgSrc , gardinerCode : glyph . gardinerCode } ; this . apiService . post ( 'confirmation' , data ) . then ( ( ) => { resolve ( ) ; } ) . catch ( ( e ) => { reject ( e ) ; } ) ; } ) ; } ) ; } autoClassifyGlyph ( glyph ) { this . config . ai . classification . glyphs . push ( glyph . id ) ; this . changeDetector . detectChanges ( ) ; return new Promise ( ( resolve , reject ) => { this . workspace . getPolyImgOfFacsimile ( glyph ) . then ( ( imgSrc ) => { const data = { image : imgSrc , limit : 3 , model_name : this . settings . classification . model ? this . settings . classification . model . name : '' , model_version : this . settings . classification . model ? this . settings . classification . model . version : '' , original_height : glyph . height , original_width : glyph . width , threshold : 0 , weighted : false , } ; this . apiService . post ( 'classify' , data ) . then ( ( results ) => { results = filter ( results , ( result ) => { return find ( gardinerGlyphs , { code : result . glyph . toLowerCase ( ) } ) ; } ) ; if ( results && results . length ) { glyph . gardinerCodePredictions = orderBy ( results , [ 'score' ] , [ 'desc' ] ) ; glyph . gardinerCode = glyph . gardinerCodePredictions [ 0 ] . glyph ; } else { glyph . gardinerCodePredictions = [ ] ; glyph . gardinerCode = '' ; } glyph . gardinerCodePredictions . forEach ( p => { p . glyph = p . glyph . toLowerCase ( ) ; } ) ; glyph . gardinerCode = glyph . gardinerCode . toLowerCase ( ) ; pull ( this . config . ai . classification . glyphs , glyph . id ) ; this . updateWorkspace ( ) ; resolve ( ) ; } , err => { console . warn ( this . translate . instant ( 'MODULES.PROJECT.MODULES.ANALYSE.TS.WARN.API' ) , err ) ; pull ( this . config . ai . classification . glyphs , glyph . id ) ; this . changeDetector . detectChanges ( ) ; resolve ( ) ; } ) ; } , err => { pull ( this . config . ai . classification . glyphs , glyph . id ) ; this . changeDetector . detectChanges ( ) ; } ) ; } ) ; } manuallyClassifyGlyph ( glyph ) { const modalId = 'glyphPickerModal' ; this . ngxSmartModalService . getModal ( modalId ) . open ( ) ; this . ngxSmartModalService . setModalData ( { code : glyph . gardinerCode , result : null } , modalId , true ) ; const onDataAddedSub = this . ngxSmartModalService . getModal ( modalId ) . onDataAdded . subscribe ( ( modal ) => { if ( modal && modal . result ) { this . setGlyphCode ( glyph , modal . result ) ; this . ngxSmartModalService . resetModalData ( modalId ) ; onDataAddedSub . unsubscribe ( ) ; } } ) ; const onCloseSub = this . ngxSmartModalService . getModal ( modalId ) . onAnyCloseEvent . subscribe ( ( modal ) => { this . ngxSmartModalService . resetModalData ( modalId ) ; if ( onDataAddedSub ) { onDataAddedSub . unsubscribe ( ) ; } onCloseSub . unsubscribe ( ) ; } ) ; } setGlyphCode ( glyph , code ) { glyph . gardinerCode = code ; this . updateWorkspace ( ) ; } toggleGlyphLock ( glyph ) { glyph . locked = ! glyph . locked ; this . updateWorkspace ( ) ; } toggleSentenceSelection ( sentence ) { if ( sentence . selected ) { this . workspace . unselectSentence ( sentence ) ; } else { this . workspace . selectSentence ( sentence ) ; } } deleteSentence ( sentence ) { this . workspace . deleteSentence ( sentence ) ; } toggleWordSelection ( word ) { if ( word . selected ) { this . workspace . unselectWord ( word ) ; } else { this . workspace . selectWord ( word ) ; } } deleteWord ( word ) { this . workspace . deleteWord ( word ) ; } deleteSelectedGlyphs ( ) { this . workspace . deleteGlyphs ( this . config . selected ) ; } createWord ( glyphs ) { const wordIds = uniq ( map ( glyphs , 'parentId' ) ) ; if ( wordIds . length === 1 ) { if ( this . isSequentialArray ( map ( glyphs , 'order' ) ) ) { this . createWordFromGlyphs ( glyphs ) ; } else { this . notificationService . warn ( this . translate . instant ( 'MODULES.PROJECT.MODULES.ANALYSE.CREATE_WORD.SEQ_WARN' ) ) ; } return ; } this . notificationService . warn ( this . translate . instant ( 'MODULES.PROJECT.MODULES.ANALYSE.CREATE_WORD.SAME_WORD_WARN' ) ) ; } createWordFromGlyphs ( glyphs ) { if ( glyphs . length ) { const beforeWordGlyphs = [ ] ; const newWordGlyphs = [ ] ; const afterWordGlyphs = [ ] ; const parent = glyphs [ 0 ] . getParent ( ) ; const parentGlyphs = this . getGlyphList ( 'word' , parent ) ; const startIndex = glyphs [ 0 ] . order ; const endIndex = glyphs [ glyphs . length - 1 ] . order ; const sentence = parent . getParent ( ) ; parentGlyphs . forEach ( ( glyph ) => { if ( glyph . order < startIndex ) { beforeWordGlyphs . push ( glyph . id ) ; } else if ( glyph . order > endIndex ) { afterWordGlyphs . push ( glyph . id ) ; } else { newWordGlyphs . push ( glyph . id ) ; } } ) ; const currentWord = { add : true , indexCnt : 0 , glyphs : [ ] } ; const newWord1 = { add : true , indexCnt : 0.5 , glyphs : [ ] } ; const newWord2 = { add : true , indexCnt : 0.75 , glyphs : [ ] } ; if ( beforeWordGlyphs . length ) { currentWord . glyphs = beforeWordGlyphs ; if ( newWordGlyphs . length ) { newWord1 . glyphs = newWordGlyphs ; } else { newWord1 . add = false ; } if ( afterWordGlyphs . length ) { newWord2 . glyphs = afterWordGlyphs ; } else { newWord2 . add = false ; } } else { if ( newWordGlyphs . length ) { currentWord . glyphs = newWordGlyphs ; } else { currentWord . add = false ; } if ( afterWordGlyphs . length ) { newWord1 . glyphs = afterWordGlyphs ; } else { newWord1 . add = false ; } newWord2 . add = false ; } const wordCreatePromises = [ ] ; if ( currentWord . add ) { parent . glyphs = currentWord . glyphs ; parent . reorderGlyphs ( ) ; } if ( newWord1 . add ) { const newWordData = { author : parent . author , created : new Date ( ) . getTime ( ) , glyphs : [ ] , isCartouche : parent . isCartouche , order : parent . order + newWord1 . indexCnt , position : { top : parent . top , left : parent . left , width : parent . width , height : parent . height , } , translation : '' , transliteration : '' , } ; wordCreatePromises . push ( this . workspace . createWord ( newWordData , parent . parentId , { glyphs : newWord1 . glyphs , expanded : true , } ) ) ; } if ( newWord2 . add ) { const afterWordData = { author : parent . author , created : new Date ( ) . getTime ( ) , glyphs : [ ] , isCartouche : parent . isCartouche , order : parent . order + newWord2 . indexCnt , position : { top : parent . top , left : parent . left , width : parent . width , height : parent . height , } , translation : '' , transliteration : '' , } ; wordCreatePromises . push ( this . workspace . createWord ( afterWordData , parent . parentId , { glyphs : newWord2 . glyphs , expanded : true , } ) ) ; } Promise . all ( wordCreatePromises ) . then ( ( words ) => { words . forEach ( ( word ) => { word . reorderGlyphs ( ) ; sentence . addWord ( word ) ; } ) ; sentence . reorderWords ( ) ; this . workspace . reorderObjects ( ) ; this . updateWorkspace ( ) ; } ) ; } } updateSentenceTransliteration ( sentence ) { let transliteration = '' ; this . getWordList ( sentence ) . forEach ( ( word ) => { if ( word . transliteration ) { transliteration += ' ' + word . transliteration ; } } ) ; sentence . transliteration = transliteration ; this . updateWorkspace ( ) ; } togglePanel ( ) { this . panel . active = ! this . panel . active ; } autoTranslateSentence ( sentence ) { const sentenceTranslation = { id : sentence . id , loading : true , results : [ ] , } ; this . config . ai . translation . sentences . push ( sentenceTranslation ) ; const data = { sequence : map ( this . getGlyphList ( 'sentence' , sentence ) , 'gardinerCode' ) , } ; this . apiService . post ( 'translate' , data ) . then ( ( results ) => { sentenceTranslation . results = this . parseAutoTranslateResults ( results ) ; this . updateWorkspace ( ) ; sentenceTranslation . loading = false ; } , err => { console . warn ( this . translate . instant ( 'MODULES.PROJECT.MODULES.ANALYSE.TRANSLATION.TS.WARN.API' ) , err ) ; this . notificationService . error ( this . translate . instant ( 'MODULES.PROJECT.MODULES.ANALYSE.TRANSLATION.TS.WARN.API' ) ) ; sentenceTranslation . loading = false ; } ) ; } parseAutoTranslateResults ( results ) { const words = [ ] ; results . forEach ( ( result ) => { const word = { end : result . end , sequence : result . sequence , start : result . start , translation : null , transliteration : result . transliteration , } ; const locale = this . translate . currentLang ; if ( result . translations ) { const translation = find ( result . translations , { locale : locale } ) ; if ( translation ) { word . translation = translation ; words . push ( word ) ; } } } ) ; return words ; } getTranslationData ( id ) { return find ( this . config . ai . translation . sentences , { id : id } ) ; } isLoadingTranslation ( id ) { return ! ! find ( this . config . ai . translation . sentences , { id : id } ) ; } autoFillWord ( sentence , result ) { } editGlyph ( glyph ) { this . config . editing . active = true ; this . config . editing . object = glyph ; this . panel . active = false ; this . selectGlyph ( glyph ) ; this . workspace . panZoomToObject ( glyph ) ; this . workspace . editPolygonObject ( glyph ) ; } saveGlyph ( glyph ) { this . workspace . saveEditedPolygonObject ( glyph ) ; this . config . editing . active = false ; this . config . editing . object = null ; } trackByFn ( index , item ) { if ( item ) { return item . id ; } return index ; } private getSelectedGlyphs ( ) { return this . workspace . getSelectedObjects ( 'glyph' ) as Array < Glyph > ; } private updateWorkspaceDebounced ( ) { this . changeDetector . detectChanges ( ) ; this . workspace . updateHistory ( ) ; this . workspace . updateData ( ) ; } private isSequentialArray ( numbers ) { for ( let i = 0 ; i < numbers . length ; i ++ ) { if ( numbers [ i ] + 1 < numbers [ i + 1 ] || numbers [ i ] - 1 > numbers [ i - 1 ] ) { return false ; } } return true ; } private dragDropInit ( ) { if ( this . config . sortable . instances ) { this . config . sortable . instances . forEach ( instance => { if ( instance ) { instance . destroy ( ) ; } } ) ; this . config . sortable . instances = [ ] ; } setTimeout ( ( ) => { const listTypes = [ ] ; listTypes . forEach ( ( listType ) => { const listSelector = `.js-drag-drop-${ listType }` ; const targetSelector = `.js-drag-drop-target-${ listType }` ; const handleSelector = `.js-drag-drop-handle-${ listType }` ; const listEls = document . querySelectorAll ( listSelector ) ; listEls . forEach ( ( listEl ) => { const ins = Sortable . create ( listEl , { handle : handleSelector , draggable : targetSelector , animation : 100 , direction : 'vertical' , ghostClass : 'is-ghosted' , group : listType , forceFallback : navigator . userAgent . toLowerCase ( ) . indexOf ( 'firefox' ) > - 1 , onStart : ( ) => { this . dragDropStart ( ) ; } , onEnd : ( ) => { this . dragDropEnd ( ) ; } , onAdd : ( e ) => { this . dragDropUpdateData ( e . from . closest ( 'li' ) ? e . from . closest ( 'li' ) . getAttribute ( 'data-id' ) : null , e . to . closest ( 'li' ) ? e . to . closest ( 'li' ) . getAttribute ( 'data-id' ) : null , e . item . getAttribute ( 'data-id' ) , e . oldIndex , e . newIndex ) ; } , onUpdate : ( e ) => { this . dragDropUpdateData ( e . from . closest ( 'li' ) ? e . from . closest ( 'li' ) . getAttribute ( 'data-id' ) : null , e . to . closest ( 'li' ) ? e . to . closest ( 'li' ) . getAttribute ( 'data-id' ) : null , e . item . getAttribute ( 'data-id' ) , e . oldIndex , e . newIndex ) ; } , } ) ; this . config . sortable . instances . push ( ins ) ; } ) ; } ) ; this . aside . loaded = true ; this . changeDetector . detectChanges ( ) ; } , 10 ) ; } private dragDropStart ( ) { this . changeDetector . detectChanges ( ) ; } private dragDropEnd ( ) { this . changeDetector . detectChanges ( ) ; } private dragDropUpdateData ( fromId , toId , objectId , oldIndex , newIndex ) { const reorderObjectInList = ( list , objectOldIndex , objectNewIndex , preOrder = true ) : void => { if ( preOrder ) { list = orderBy ( list , 'order' ) ; } ArrayMove . mutate ( list , objectOldIndex , objectNewIndex ) ; for ( let i = 0 ; i < list . length ; i ++ ) { list [ i ] . order = i ; } } ; const resetObjectInListOrder = ( list ) : void => { list = orderBy ( list , 'order' ) ; for ( let i = 0 ; i < list . length ; i ++ ) { list [ i ] . order = i ; } } ; const object = this . workspace . getObject ( 'id' , objectId ) ; if ( object && object . tid ) { switch ( object . tid ) { case 'sentence' : reorderObjectInList ( this . workspace . getObjects ( 'tid' , [ 'sentence' ] ) , oldIndex , newIndex ) ; break ; case 'word' : if ( fromId === toId ) { reorderObjectInList ( filter ( this . workspace . getObjects ( 'tid' , [ 'word' ] ) , { parentId : fromId } ) , oldIndex , newIndex ) ; } else { const fromSentence = this . workspace . getObject ( 'id' , fromId ) ; const toSentence = this . workspace . getObject ( 'id' , toId ) ; fromSentence . words = pull ( fromSentence . words , objectId ) ; resetObjectInListOrder ( this . getWordList ( fromSentence ) ) ; const toSentenceWords = this . getWordList ( toSentence ) ; ( < Word > object ) . parentId = toId ; toSentence . words . push ( objectId ) ; toSentenceWords . unshift ( object as Word ) ; reorderObjectInList ( toSentenceWords , 0 , newIndex , false ) ; } break ; case 'glyph' : if ( fromId === toId ) { reorderObjectInList ( filter ( this . workspace . getObjects ( 'tid' , [ 'glyph' ] ) , { parentId : fromId } ) , oldIndex , newIndex ) ; } else { const toWord = this . workspace . getObject ( 'id' , toId ) ; const fromWord = this . workspace . getObject ( 'id' , fromId ) ; if ( toWord && fromWord ) { fromWord . glyphs = pull ( fromWord . glyphs , objectId ) ; resetObjectInListOrder ( this . getGlyphList ( 'word' , fromWord ) ) ; const toWordGlyphs = this . getGlyphList ( 'word' , toWord ) ; ( < Glyph > object ) . parentId = toId ; toWord . glyphs . push ( objectId ) ; toWordGlyphs . unshift ( object as Glyph ) ; reorderObjectInList ( toWordGlyphs , 0 , newIndex , false ) ; } } break ; } } this . workspace . reorderObjects ( ) ; this . updateWorkspace ( ) ; } }