// image, then use the width of the image (with the specified gap) otherwise // the height > width, so scale the new width based on height ratio of // image to window function NewWidth() { w_r=im.width/(window.innerWidth*gap) h_r=im.height/(window.innerHeight*gap) if( w_r > h_r ) return window.innerWidth*gap else return im.width*gap / (im.height/window.innerHeight) } // work out new height for canvas. depending on whether height > width of the // image, then use the height of the image (with the specified gap) otherwise // the width > height, so scale the new height based on width ratio of // image to window function NewHeight() { w_r=im.width/(window.innerWidth*gap) h_r=im.height/(window.innerHeight*gap) if( h_r > w_r ) return window.innerHeight*gap else return im.height*gap / (im.width/window.innerWidth) } // draw 'str' as a label above the bounding box of the face (with a white // transparent background to enhance readability of str) function DrawLabelOnFace(str) { // finish face box, need to clear out new settings for // transparent backed-name tag context.stroke(); context.beginPath() context.lineWidth = 0.1 context.font = "30px Arial" context.globalAlpha = 0.6 bbox = context.measureText(str); f_h=bbox.fontBoundingBoxAscent if( bbox.fontBoundingBoxDescent ) f_h += bbox.fontBoundingBoxDescent f_h -= 8 context.rect( x+w/2-bbox.width/2, y-f_h, bbox.width, f_h ) context.fillStyle="white" context.fill() context.stroke(); context.beginPath() context.globalAlpha = 1.0 context.font = "30px Arial" context.textAlign = "center" context.fillStyle = context.strokeStyle context.fillText(str, x+w/2, y-2) } // This draws the image, it can be called on resize events, img.src finishing // loading or explicitly on page load. Will also deal with all state/toggles // for items like name, grayscale, etc. function DrawImg() { // another call to this func will occur on load, so skip this one if( im.width == 0 ) return // find any matching ammendment am=document.amendments.filter(obj => obj.eid === document.viewing.id) if( am.length ) am=am[0] canvas.width=NewWidth(im) canvas.height=NewHeight(im) // dont let caption be wider than image $('#img-cap').width(canvas.width) // actually draw the pixel images to the canvas at the right size if (!Array.isArray(am)) context.filter='grayscale(1)' context.drawImage(im, 0, 0, canvas.width, canvas.height ) // -50 is a straight up hack, no idea why this works, but its good enough for me if (!Array.isArray(am)) { const style = 'position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%);'; $('#throbber').attr('style', style + ' height: 96px;'); $('#white-circle').attr('style', style + ' height: 72px;'); if(am.type.which == 'img' ) $('#inside-img').attr('style', style + ' height: 64px;').attr('src', '/internal/'+am.type.what ); else { $('#inside-icon').attr('style', `${style} color:${am.type.colour}; height: 64px;`) $('#inside-icon').attr('fill', am.type.colour ) $('#inside-icon use').attr('xlink:href', `/internal/icons.svg#${am.type.what}`); } } else { $('#throbber').hide() $('#white-circle').hide() $('#inside-img').hide() $('#inside-icon').hide() } // show (or not) the whole figcaption with fname in it - based on state of fname_toggle if( $('#fname_toggle').prop('checked' ) ) { // reset fname for new image (if navigated left/right to get here) $('.figcaption').show() } else $('.figcaption').hide() // if we have faces, the enable the toggles, otherwise disable them and reset model select too if( document.viewing.file_details.faces.length ) { $('#faces').attr('disabled', false) $('#distance').attr('disabled', false) // first face is good enough as whole file has to have used same model $('#model').val( document.viewing.file_details.faces[0].facefile_lnk.model_used ) } else { $('#faces').attr('disabled', true) $('#distance').attr('disabled', true) // if no faces, then model is N/A (always 1st element - or 0 in select) $('#model').val(0) } // okay, we want faces drawn so lets do it if( $('#faces').prop('checked') && document.viewing.file_details.faces ) { faces=document.viewing.file_details.faces // draw rect on each face for( i=0; i= fx && x <= fx+fw && y >= fy && y <= fy+fh ) { if( faces[i].ffmo.length || faces[i].fnmo.length ) { item_list['remove_force_match_override']={ 'name': 'Remove override for this face', 'which_face': i, 'id': faces[i].id } } else if( faces[i].refimg ) { item_list['match']={ 'name': faces[i].refimg.person.tag, 'which_face': i, 'id': faces[i].id } item_list['match_add_refimg']={ 'name': 'Add this as refimg for ' + faces[i].refimg.person.tag, 'person_id': faces[i].refimg.person.id, 'who': faces[i].refimg.person.tag, 'which_face': i, 'id': faces[i].id, } item_list['wrong_person']={ 'name': 'wrong person', 'which_face': i, 'id': faces[i].id } } else { item_list['no_match_new_person']={ 'name': 'Add as reference image to NEW person', 'which_face': i, 'id': faces[i].id } item_list['no_match_new_refimg']={ 'name': 'Add as reference image to EXISTING person', 'which_face': i, 'id': faces[i].id } for( var el in NMO ) { item_list['NMO_'+el]={'type_id': NMO[el].id, 'name': 'Override: ' + NMO[el].name, 'which_face': i, 'id': faces[i].id } } } delete item_list['not_a_face'] $('#canvas').prop('menu_item', item_list ) break } } return { callback: function( key, options) { if( key == 'not_a_face' ) { return true } item=$('#canvas').prop( 'menu_item' ); FaceDBox( key, item ) }, items: item_list }; } } ) } ); // POST to the server to force a match for this face to person_id // FIXME: could I not pass person_id, and use // ...[item[key].which_face].refimg.person.id function OverrideForceMatch( person_id, key ) { ofm='&person_id='+person_id+'&face_id='+item[key].id $.ajax({ type: 'POST', data: ofm, url: '/add_force_match_override', success: function(data) { document.viewing.file_details.faces[item[key].which_face].ffmo=[] document.viewing.file_details.faces[item[key].which_face].ffmo[0]={} document.viewing.file_details.faces[item[key].which_face].ffmo[0].person=data.person $('#dbox').modal('hide') $('#faces').prop('checked',true) DrawImg() CheckForJobs() } } ) } // function that handles the POSTed data that comes back when we add a // reference image to a new or existing person (right-click on a face) // used in success callbacks from CreatePersonAndRefimg() and AddRefimgTo() function handleAddRefimgData(key, data) { document.viewing.file_details.faces[item[key].which_face].refimg=data.refimg document.viewing.file_details.faces[item[key].which_face].refimg_lnk={} // if we used this img, for now set distance to 0 - it is an exact match! document.viewing.file_details.faces[item[key].which_face].refimg_lnk.face_distance=0.0 $('#dbox').modal('hide') $('#faces').prop('checked',true) DrawImg() CheckForJobs() } // when we right-click a face and make a new person, this code creates and // associates the face function CreatePersonAndRefimg( key ) { d='&face_id='+item[key].id +'&tag='+$('#tag').val() +'&firstname='+$('#firstname').val() +'&surname='+$('#surname').val() +'&refimg_data='+item[key].refimg_data $.ajax({ type: 'POST', data: d, url: '/match_with_create_person', success: function(data) { handleAddRefimgData(key, data ) }, }) } // when we right-click a face and connect to an existing person, this connects // the refimg and associates the face function AddRefimgTo( person_id, key, search ) { d='&face_id='+item[key].id+'&person_id='+person_id+'&refimg_data='+item[key].refimg_data+'&search='+search $.ajax({ type: 'POST', data: d, url: '/add_refimg_to_person', success: function(data) { handleAddRefimgData(key, data ) }, }) } // function to facilitate adding a face match override to this "found" person // uses Ajax to the f/e to get any person matching #stext's content (via any name/tag) // and displays results in #search_person_results function SearchForPerson(content, key, face_id, face_pos, type_id) { // make URI safe who = encodeURIComponent( $('#stext').val() ) // call ajax to find ppl $.ajax({ type: 'POST', data: null, url: '/find_persons/'+ who, success: function(data) { for( var el in data ) { content+='
' var person = data[el]; if( item[key].name == "Override: Manual match to existing person" ) { func='OverrideForceMatch('+person.id+',\''+key+'\' )' content+= '
' + person.tag + ' (' + person.firstname+' '+person.surname+ ')
' content+= '' } if( key == 'no_match_new_refimg' ) { func='AddRefimgTo('+person.id+',\''+key+'\'' func_sn=func+ ', true )' func_ao=func+ ', false )' content+= '
' + person.tag + ' (' + person.firstname+' '+person.surname+ ')
' content+= ' ' content+= '
' } content+='
' } $('#search_person_results').html( content ) } } ) return false } // if we force a match, this func allows us to POST to the server to remove the override function RemoveOverrideForceMatch(face_pos) { if( document.viewing.file_details.faces[face_pos].ffmo.length ) who=document.viewing.file_details.faces[face_pos].ffmo[0].person.tag else who=document.viewing.file_details.faces[face_pos].refimg.person.tag d='&face_id='+document.viewing.file_details.faces[face_pos].id+'&person_tag='+who+'&file_eid='+document.viewing.id $.ajax({ type: 'POST', data: d, url: '/remove_force_match_override', success: function(data) { // force/delete the ffmo cleanly document.viewing.file_details.faces[face_pos].ffmo.length=0 $('#dbox').modal('hide') DrawImg() CheckForJobs() return false } } ) return false } // if we force NO match, this func allows us to POST to the server to remove the override function RemoveOverrideNoMatch(face_pos, type_id) { d='&face_id='+document.viewing.file_details.faces[face_pos].id+'&type_id='+type_id $.ajax({ type: 'POST', data: d, url: '/remove_no_match_override', success: function(data) { document.viewing.file_details.faces[face_pos].fnmo.length=0 $('#dbox').modal('hide') DrawImg() CheckForJobs() return false } } ) return false } // POST to the server to force NO match for this face function AddNoMatchOverride(type_id, face_id, face_pos, type_id) { d='&type_id='+type_id+'&face_id='+face_id $.ajax({ type: 'POST', data: d, url: '/add_no_match_override', success: function(data) { document.viewing.file_details.faces[face_pos].fnmo[0]=data $('#dbox').modal('hide') $('#faces').prop('checked',true) DrawImg() CheckForJobs() } } ) } // generate html for the appropriate content to search for a person when adding // override DBox. has a button that when clicked calls SeachForPerson() which // POSTs to the server, and fills in the 'search_person_results' div with content function AddSearch( content, key, face_pos ) { html='
search for existing person:
' html+=`
` return html } // function that is called when we click on a face in the viewer and we want to // potentially override the non-match / match... it shows the face, and then // based on which menu item got us here, shows appropriate text to do next action function FaceDBox(key, item) { face_pos=item[key]['which_face'] div ='

' div+='Face position #' + face_pos // use w-100 to force width to not make face image wider than half the dbox div+='

' $.ajax({ type: 'POST', data: null, url: '/get_face_from_image/'+item[key].id, success: function(img_data) { item[key].refimg_data=img_data $('#face_img').prop('src', 'data:image/jpeg;base64,' + img_data ) // used for create_new_person only, so this will do nothing for option menu items $('#refimg_data').val(img_data) } } ) div+='
' if ( key == 'remove_force_match_override' ) { if( document.viewing.file_details.faces[face_pos].ffmo.length ) div+='
remove this override (force match to: ' + document.viewing.file_details.faces[face_pos].ffmo[0].person.tag + ')
' else div+='
remove this override (' + document.viewing.file_details.faces[face_pos].fnmo[0].type.name + ')
' div+='
' div+='' div+='' else div+='onClick="RemoveOverrideNoMatch(' +face_pos+','+document.viewing.file_details.faces[face_pos].fnmo[0].type.id+ ')">Remove' div+='
' } if ( key == 'no_match_new_person' ) { div+='' div+=`
` div+='' div+='
' } if ( key == 'no_match_new_refimg' ) { div+=AddSearch( 'Click one of the link(s) below to add this face as a reference image to the person:

', key, face_pos ); } if ( key == 'match_add_refimg' ) { func='AddRefimgTo('+item[key]['person_id']+',\''+key+'\'' func_sn=func+ ', true )' func_ao=func+ ', false )' div+="Confirm you wish to add this face as a reference image for " + item[key]['who'] div+= '
' + item[key]['who'] + '
' div+= ' ' div+= '
' } if ( key == 'wrong_person' ) { div+='
wrong person, so mark this as the wrong person/refimg connection, for face#' + item[key]['which_face'] div+='
face db id: ' + item[key]['id'] div += '
not yet' } if( /NMO_/.test(key) ) { if( item[key].name == 'Override: Manual match to existing person' ) { div+=AddSearch( 'Click one of the link(s) below to manually connect this face as once-off connection to the person:

', key, face_pos ); } else { type_id=item[key].type_id face_id=item[key].id div+='
' div+='' div+='' div+='
' } } div+='
' $('#dbox-title').html(item[key]['name']) $('#dbox-content').html(div) $('#dbox').modal('show') } // func called to show logs relating to this filename from viewer // pops results up in a dbox function JoblogSearch() { data="eid="+document.viewing.id $.ajax({ type: 'POST', data: data, url: '/joblog_search', success: function(res) { data = JSON.parse(res) div ='
' div+='' for( i=0; i' } div+='
LogWhenJob
' + data[i].log_date + '' div+='' + data[i].name + ' #:'+data[i].id+'
' $('#dbox-title').html("Logs relating to this filename") $('#dbox-content').html(div) $('#dbox').modal('show') } }) } // helper func to resert the src on the video div function setVideoSource(newSrc) { $('#videoSource').attr('src', newSrc); $('#video')[0].load(); } // function called when we get another page from inside the viewer function getPageViewer(res, viewingIdx) { document.viewing=document.entries[viewingIdx] // update viewing, arrows and image/video too ViewImageOrVideo() } // handler used when we double click an entry to show it in the viewer function dblClickToViewEntry(id) { $('#files_div').addClass('d-none') $('#viewer_div').removeClass('d-none') setEntryById( id ) ViewImageOrVideo() } // quick function that allows us to go out of the viewer and back, the viewercomes from files_ip/sp // so just redraw the page with drawPageOfFigures() as we have all the data function goOutOfViewer() { // if this returns -1, we have used arrows to go onto a new page(s) if( getPageNumberForId( $('#figures').find('.figure').first().prop('id') ) == -1 ) drawPageOfFigures() // hide viewer div, then show files_div $('#viewer_div').addClass('d-none') $('#files_div').removeClass('d-none') // no longer viewing an image too document.viewing=null } // change the viewer to the previous entry (handle page change too) function getPreviousEntry() { var currentIndex = entryList.indexOf(document.viewing.id); oldPageOffset=Math.floor(currentIndex / OPT.how_many) if (currentIndex > 0) { currentIndex--; pageOffset=Math.floor(currentIndex / OPT.how_many) currentIndex=currentIndex-(pageOffset*OPT.how_many) // pref page, load it if( oldPageOffset != pageOffset ) // pref page is pageOffset+1 now getPage(pageOffset+1,getPageViewer,currentIndex) else document.viewing=document.entries[currentIndex] } } // change the viewer to the next entry (handle page change too) function getNextEntry() { var currentIndex = entryList.indexOf(document.viewing.id); oldPageOffset=Math.floor(currentIndex / OPT.how_many) if (currentIndex < entryList.length - 1) { currentIndex++ pageOffset=Math.floor(currentIndex / OPT.how_many) currentIndex=currentIndex-(pageOffset*OPT.how_many) // next page, load it if( oldPageOffset != pageOffset ) // next page is pageOffset+1 now getPage(pageOffset+1,getPageViewer,currentIndex) else document.viewing=document.entries[currentIndex] } } // check if we are viewing the very first entry (helps to disable la) function entryIsAtStart() { return document.viewing.id === entryList[0]; } // check if we are viewing the very last entry (helps to disable ra) function entryIsAtEnd() { return document.viewing.id === entryList[entryList.length - 1]; } // helper func to ensure document.viewing is the right entry from document.entries array function setEntryById(id) { var currentIndex = entryList.indexOf(parseInt(id)); // if we are on a different page, adjust as document.entries only has <= how_many pageOffset=Math.floor(currentIndex / OPT.how_many) currentIndex = currentIndex-(pageOffset*OPT.how_many) document.viewing=document.entries[currentIndex] } // disable la button if we are viewing first entry and/or ra button if we are viewing last entry function setDisabledForViewingNextPrevBttons() { $('#la').attr('disabled', entryIsAtStart()); $('#ra').attr('disabled', entryIsAtEnd()); } // when we go into the view, the keybindings are set here for items like 'f' for face box/name function addViewerKeyHandler() { // allow a keypress on the viewer_div $(document).keydown(function(event) { // if dbox is visible, dont process this hot-key, we are inputting text into inputs instead if( $("#dbox").is(':visible') ) return switch (event.key) { case "Left": // IE/Edge specific value case "ArrowLeft": $('#la').click() break; case "Right": // IE/Edge specific value case "ArrowRight": $('#ra').click() break; case "d": $('#distance').click() break; case "f": $('#faces').click() break; case "n": $('#fname_toggle').click() break; case "F": fullscreen=!document.fullscreen ViewImageOrVideo() break; case "l": JoblogSearch() break; case "Delete": $('#del').click() default: return; // Quit when this doesn't handle the key event. } }); } // left arrow onclick handler to go to prev image from inside the viewer function prevImageInViewer() { getPreviousEntry() setDisabledForViewingNextPrevBttons() ViewImageOrVideo() } // right arrow onclick handler to go to next image from inside the viewer function nextImageInViewer() { getNextEntry() setDisabledForViewingNextPrevBttons() ViewImageOrVideo() } // wrapper func to start the viewer - needed as we have a dbl-click & View file // to start the viewer function startViewing(eid) { dblClickToViewEntry( eid ); setDisabledForViewingNextPrevBttons(); addViewerKeyHandler() }