// 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) } 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 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( grayscale ) 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( throbber ) $('#throbber').attr('style', 'display:show; position:absolute; left:'+canvas.width/2+'px; top:'+(canvas.height/2-50)+'px' ) else $('#throbber').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( objs[current].faces ) { $('#faces').attr('disabled', false) $('#distance').attr('disabled', false) $('#model').val( Number(objs[current].face_model) ) } 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') && objs[current].faces ) { // draw rect on each face for( i=0; i= fx && x <= fx+fw && y >= fy && y <= fy+fh ) { if( objs[current].faces[i].override ) { item_list['remove_force_match_override']={ 'name': 'Remove override for this face', 'which_face': i, 'id': objs[current].faces[i].id } } else if( objs[current].faces[i].who ) { item_list['match']={ 'name': objs[current].faces[i].who, 'which_face': i, 'id': objs[current].faces[i].id } item_list['match_add_refimg']={ 'name': 'Add this as refimg for ' + objs[current].faces[i].who, 'person_id': objs[current].faces[i].pid, 'who': objs[current].faces[i].who, 'which_face': i, 'id': objs[current].faces[i].id, } item_list['wrong_person']={ 'name': 'wrong person', 'which_face': i, 'id': objs[current].faces[i].id } } else { item_list['no_match_new_person']={ 'name': 'Add as reference image to NEW person', 'which_face': i, 'id': objs[current].faces[i].id } item_list['no_match_new_refimg']={ 'name': 'Add as reference image to EXISTING person', 'which_face': i, 'id': objs[current].faces[i].id } for( var el in NMO ) { item_list['NMO_'+el]={'type_id': NMO[el].type_id, 'name': 'Override: ' + NMO[el].name, 'which_face': i, 'id': objs[current].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 }; } } ) } ); // quick wrapper function to make calling this ajax code simpler in SearchForPerson function OverrideForceMatch( person_id, key ) { // we have type_id passed in, so dig the NMO out, and use that below (its really just for name, but in case we change that in the DB) for( el in NMO ) { if( NMO[el].type_id == item[key].type_id ) { fm_idx=el break } } ofm='&person_id='+person_id+'&face_id='+item[key].id $.ajax({ type: 'POST', data: ofm, url: '/add_force_match_override', success: function(data) { objs[current].faces[item[key].which_face].override={} objs[current].faces[item[key].which_face].override.who=data.person_tag objs[current].faces[item[key].which_face].override.distance='N/A' objs[current].faces[item[key].which_face].override.type_id=NMO[fm_idx].id objs[current].faces[item[key].which_face].override.type_name=NMO[fm_idx].name $('#dbox').modal('hide') $('#faces').prop('checked',true) DrawImg() CheckForJobs() } } ) } 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) { objs[current].faces[item[key].which_face].who=data.who objs[current].faces[item[key].which_face].distance=data.distance $('#dbox').modal('hide') $('#faces').prop('checked',true) DrawImg() CheckForJobs() } }) } 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) { objs[current].faces[item[key].which_face].who=data.who objs[current].faces[item[key].which_face].distance=data.distance $('#dbox').modal('hide') $('#faces').prop('checked',true) DrawImg() CheckForJobs() } }) } // 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]; // NMO_1 is a non-match-override type_id==1 (or force match to existing person) if( key == "NMO_1" ) { 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 } function RemoveOverrideForceMatch(face_pos) { if( objs[current].faces[face_pos].override ) who=objs[current].faces[face_pos].override.who else who=objs[current].faces[face_pos].who d='&face_id='+objs[current].faces[face_pos].id+'&person_tag='+who+'&file_eid='+current $.ajax({ type: 'POST', data: d, url: '/remove_force_match_override', success: function(data) { delete objs[current].faces[face_pos].override $('#dbox').modal('hide') DrawImg() CheckForJobs() return false } } ) return false } function RemoveOverrideNoMatch(face_pos, type_id) { d='&face_id='+objs[current].faces[face_pos].id+'&type_id='+type_id $.ajax({ type: 'POST', data: d, url: '/remove_no_match_override', success: function(data) { delete objs[current].faces[face_pos].override $('#dbox').modal('hide') DrawImg() CheckForJobs() return false } } ) return false } 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) { objs[current].faces[face_pos].override={} objs[current].faces[face_pos].override.who=NMO[type_id].name objs[current].faces[face_pos].override.distance='N/A' objs[current].faces[face_pos].override.type_id=type_id objs[current].faces[face_pos].override.type_name=NMO[type_id].name $('#dbox').modal('hide') $('#faces').prop('checked',true) DrawImg() CheckForJobs() } } ) } 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( objs[current].faces[face_pos].override.type_name == 'Manual match to existing person' ) div+='
remove this override (force match to: ' + objs[current].faces[face_pos].override.who + ')
' else div+='
remove this override (no match)
' div+='
' div+='' div+='' else div+='onClick="RemoveOverrideNoMatch(' +face_pos+','+objs[current].faces[face_pos].override.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+=`` 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="+current $.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') } }) }