X-Git-Url: http://git.shadowcat.co.uk/gitweb/gitweb.cgi?a=blobdiff_plain;f=root%2Fjs%2Frelationship.js;h=0f5ae0994fe1d59355ba11bea42fb321272c525c;hb=48156ccde369b0fbd72c5efb322e2f6e04f5444b;hp=0b0b959ac32dd89537b6600c088a3637cd1e2a2d;hpb=fdb375813058a9ce84302986d594f4f08cc9f291;p=scpubgit%2Fstemmaweb.git diff --git a/root/js/relationship.js b/root/js/relationship.js index 0b0b959..0f5ae09 100644 --- a/root/js/relationship.js +++ b/root/js/relationship.js @@ -5,6 +5,12 @@ var start_element_height = 0; var reltypes = {}; var readingdata = {}; +jQuery.removeFromArray = function(value, arr) { + return jQuery.grep(arr, function(elem, index) { + return elem !== value; + }); +}; + function arrayUnique(array) { var a = array.concat(); for(var i=0; i' + target_id ); svg.path( relation, path.move( sx, sy ).curveC( sx + (2*rx), sy, ex + (2*rx), ey, ex, ey ), {fill: 'none', stroke: relation_color, strokeWidth: 4}); var relation_element = $('#svgenlargement .relation').filter( ':last' ); @@ -629,6 +663,176 @@ function draw_relation( source_id, target_id, relation_color ) { return relation_element; } +function detach_node( readings ) { + // separate out the deleted relationships, discard for now + if( 'DELETED' in readings ) { + // Remove each of the deleted relationship links. + $.each( readings['DELETED'], function( idx, pair ) { + var relation_id = get_relation_id( pair[0], pair[1] ); + var relation = $( jq( relation_id ) ); + if( relation.size() == 0 ) { + relation_id = get_relation_id( pair[1], pair[0] ); + } + relation_manager.remove( relation_id ); + }); + delete readings['DELETED']; + } + // add new node(s) + $.extend( readingdata, readings ); + // remove from existing readings the witnesses for the new nodes/readings + $.each( readings, function( node_id, reading ) { + $.each( reading.witnesses, function( index, witness ) { + var witnesses = readingdata[ reading.orig_rdg ].witnesses; + readingdata[ reading.orig_rdg ].witnesses = $.removeFromArray( witness, witnesses ); + } ); + } ); + + detached_edges = []; + + // here we detach witnesses from the existing edges accoring to what's being relayed by readings + $.each( readings, function( node_id, reading ) { + var edges = edges_of( get_ellipse( reading.orig_rdg ) ); + incoming_remaining = []; + outgoing_remaining = []; + $.each( reading.witnesses, function( index, witness ) { + incoming_remaining.push( witness ); + outgoing_remaining.push( witness ); + } ); + $.each( edges, function( index, edge ) { + detached_edge = edge.detach_witnesses( reading.witnesses ); + if( detached_edge != null ) { + detached_edges.push( detached_edge ); + $.each( detached_edge.witnesses, function( index, witness ) { + if( detached_edge.is_incoming == true ) { + incoming_remaining = $.removeFromArray( witness, incoming_remaining ); + } else { + outgoing_remaining = $.removeFromArray( witness, outgoing_remaining ); + } + } ); + } + } ); + + // After detaching we still need to check if for *all* readings + // an edge was detached. It may be that a witness was not + // explicitly named on an edge but was part of a 'majority' edge + // in which case we need to duplicate and name that edge after those + // remaining witnesses. + if( outgoing_remaining.length > 0 ) { + $.each( edges, function( index, edge ) { + if( edge.get_label() == 'majority' && !edge.is_incoming ) { + detached_edges.push( edge.clone_for( outgoing_remaining ) ); + } + } ); + } + if( incoming_remaining.length > 0 ) { + $.each( edges, function( index, edge ) { + if( edge.get_label() == 'majority' && edge.is_incoming ) { + detached_edges.push( edge.clone_for( incoming_remaining ) ); + } + } ); + } + + // Finally multiple selected nodes may share edges + var copy_array = []; + $.each( detached_edges, function( index, edge ) { + var do_copy = true; + $.each( copy_array, function( index, copy_edge ) { + if( copy_edge.g_elem.attr( 'id' ) == edge.g_elem.attr( 'id' ) ) { do_copy = false } + } ); + if( do_copy == true ) { + copy_array.push( edge ); + } + } ); + detached_edges = copy_array; + + // Lots of unabstracted knowledge down here :/ + // Clone original node/reading, rename/id it.. + duplicate_node = get_ellipse( reading.orig_rdg ).parent().clone(); + duplicate_node.attr( 'id', node_id ); + duplicate_node.children( 'title' ).text( node_id ); + + // This needs somehow to move to node or even to shapes! #repositioned + duplicate_node_data = get_ellipse( reading.orig_rdg ).parent().data( 'repositioned' ); + if( duplicate_node_data != null ) { + duplicate_node.children( 'ellipse' ).parent().data( 'repositioned', duplicate_node_data ); + } + + // Add the node and all new edges into the graph + var graph_root = $('#svgenlargement svg g.graph'); + graph_root.append( duplicate_node ); + $.each( detached_edges, function( index, edge ) { + id_suffix = node_id.slice( node_id.indexOf( '_' ) ); + edge.g_elem.attr( 'id', ( edge.g_elem.attr( 'id' ) + id_suffix ) ); + edge_title = edge.g_elem.children( 'title' ).text(); + edge_weight = 0.8 + ( 0.2 * edge.witnesses.length ); + edge_title = edge_title.replace( reading.orig_rdg, node_id ); + edge.g_elem.children( 'title' ).text( edge_title ); + edge.g_elem.children( 'path').attr( 'stroke-width', edge_weight ); + // Reg unabstracted knowledge: isn't it more elegant to make + // it edge.append_to( graph_root )? + graph_root.append( edge.g_elem ); + } ); + + // Make the detached node a real node_obj + var ellipse_elem = get_ellipse( node_id ); + var new_node = new node_obj( ellipse_elem ); + ellipse_elem.data( 'node_obj', new_node ); + + // Move the node somewhat up for 'dramatic effect' :-p + new_node.reposition( 0, -70 ); + + } ); + +} + +function merge_nodes( source_node_id, target_node_id, consequences ) { + if( consequences.status != null && consequences.status == 'ok' ) { + merge_node( source_node_id, target_node_id ); + if( consequences.checkalign != null ) { + $.each( consequences.checkalign, function( index, node_ids ) { + var temp_relation = draw_relation( node_ids[0], node_ids[1], "#89a02c" ); + var sy = parseInt( temp_relation.children('path').attr('d').split('C')[0].split(',')[1] ); + var ey = parseInt( temp_relation.children('path').attr('d').split(' ')[2].split(',')[1] ); + var yC = ey + (( sy - ey )/2); + // TODO: compute xC to be always the same distance to the amplitude of the curve + var xC = parseInt( temp_relation.children('path').attr('d').split(' ')[1].split(',')[0] ); + var svg = $('#svgenlargement').children('svg').svg('get'); + parent_g = svg.group( $('#svgenlargement svg g') ); + var ids_text = node_ids[0] + '-' + node_ids[1]; + var merge_id = 'merge-' + ids_text; + svg.image( parent_g, xC, (yC-8), 16, 16, merge_button_yes, { id: merge_id } ); + svg.image( parent_g, (xC+20), (yC-8), 16, 16, merge_button_no, { id: 'no' + merge_id } ); + $( '#' + merge_id ).hover( function(){ $(this).addClass( 'draggable' ) }, function(){ $(this).removeClass( 'draggable' ) } ); + $( '#no' + merge_id ).hover( function(){ $(this).addClass( 'draggable' ) }, function(){ $(this).removeClass( 'draggable' ) } ); + $( '#' + merge_id ).click( function( evt ){ + merge_node( node_ids[0], node_ids[1] ); + temp_relation.remove(); + $( '#' + merge_id ).parent().remove(); + //notify backend + var ncpath = getTextURL( 'merge' ); + var form_values = "source_id=" + node_ids[0] + "&target_id=" + node_ids[1] + "&single=true"; + $.post( ncpath, form_values ); + } ); + $( '#no' + merge_id ).click( function( evt ) { + temp_relation.remove(); + $( '#' + merge_id ).parent().remove(); + } ); + } ); + } + } +} + +function merge_node( source_node_id, target_node_id ) { + $.each( edges_of( get_ellipse( source_node_id ) ), function( index, edge ) { + if( edge.is_incoming == true ) { + edge.attach_endpoint( target_node_id ); + } else { + edge.attach_startpoint( target_node_id ); + } + } ); + $( jq( source_node_id ) ).remove(); +} + function Marquee() { var self = this; @@ -641,8 +845,6 @@ function Marquee() { this.svg_rect = $('#svgenlargement svg').svg('get'); this.show = function( event ) { - // TODO: uncolor possible selected - // TODO: unless SHIFT? self.x = event.clientX; self.y = event.clientY; p = svg_root.createSVGPoint(); @@ -670,6 +872,7 @@ function Marquee() { var rect = $('#marquee'); if( rect.length != 0 ) { //unselect any possible selected first + //TODO: unless SHIFT? if( $('ellipse[fill="#9999ff"]').size() > 0 ) { $('ellipse[fill="#9999ff"]').each( function() { $(this).data( 'node_obj' ).set_draggable( false ); @@ -697,13 +900,22 @@ function Marquee() { $('#svgenlargement ellipse').each( function( index ) { var cx = parseInt( $(this).attr('cx') ); var cy = parseInt( $(this).attr('cy') ); + + // This needs somehow to move to node or even to shapes! #repositioned + // We should ask something more aling the lines of: nodes.each { |item| node.selected? } + var org_translate = $(this).parent().data( 'repositioned' ); + if( org_translate != null ) { + cx = cx + org_translate[0]; + cy = cy + org_translate[1]; + } + if( cx > cx_min && cx < cx_max) { if( cy > cy_min && cy < cy_max) { // we actually heve no real 'selected' state for nodes, except coloring $(this).attr( 'fill', '#9999ff' ); // Take note of the selected reading(s) and applicable witness(es) // so we can populate the multipleselect-form - readings.push( $(this).parent().attr('id') ); + readings.push( $(this).parent().attr('id') ); var this_witnesses = $(this).data( 'node_obj' ).get_witnesses(); witnesses = arrayUnique( witnesses.concat( this_witnesses ) ); } @@ -713,16 +925,16 @@ function Marquee() { //add intersection of witnesses sets to the multi select form and open it $('#detach_collated_form').empty(); $.each( readings, function( index, value ) { - $('#detach_collated_form').append( $('').attr( - "type", "hidden").attr("name", "readings[]").attr( - "value", value ) ); - }); - $.each( witnesses, function( index, value ) { + $('#detach_collated_form').append( $('').attr( + "type", "hidden").attr("name", "readings[]").attr( + "value", value ) ); + }); + $.each( witnesses, function( index, value ) { $('#detach_collated_form').append( - '' + value + '
' ); + '' + value + '
' ); }); - $('#multiple_selected_readings').attr('value', readings.join(',') ); + $('#multiple_selected_readings').attr('value', readings.join(',') ); $('#multipleselect-form').dialog( 'open' ); } self.svg_rect.remove( $('#marquee') ); @@ -735,6 +947,21 @@ function Marquee() { } +function readings_equivalent( source, target ) { + var sourcetext = readingdata[source].text; + var targettext = readingdata[target].text; + if( sourcetext === targettext ) { + return true; + } + // Lowercase and strip punctuation from both and compare again + var stlc = sourcetext.toLowerCase().replace(/[^\w\s]|_/g, ""); + var ttlc = targettext.toLowerCase().replace(/[^\w\s]|_/g, ""); + if( stlc === ttlc ) { + return true; + } + return false; +} + $(document).ready(function () { @@ -742,7 +969,8 @@ $(document).ready(function () { relation_manager = new relation_factory(); $('#update_workspace_button').data('locked', false); - + + // Set up the mouse events on the SVG enlargement $('#enlargement').mousedown(function (event) { $(this) .data('down', true) @@ -807,23 +1035,37 @@ $(document).ready(function () { }); + // Set up the relationship creation dialog. This also functions as the reading + // merge dialog where appropriate. + if( editable ) { - $( "#dialog-form" ).dialog({ + $( '#dialog-form' ).dialog( { autoOpen: false, height: 270, width: 290, modal: true, buttons: { - "Ok": function( evt ) { - $(evt.target).button("disable"); - $('#status').empty(); - form_values = $('#collapse_node_form').serialize(); + 'Merge readings': function( evt ) { + $( evt.target ).button( 'disable' ); + $( '#status' ).empty(); + form_values = $( '#collapse_node_form' ).serialize(); + ncpath = getTextURL( 'merge' ); + var jqjson = $.post( ncpath, form_values, function( data ) { + merge_nodes( $( '#source_node_id' ).val(), $( '#target_node_id' ).val(), data ); + $(evt.target).button( 'enable' ); + $( '#dialog-form' ).dialog( 'close' ); + } ); + }, + OK: function( evt ) { + $( evt.target ).button( 'disable' ); + $( '#status' ).empty(); + form_values = $( '#collapse_node_form' ).serialize(); ncpath = getTextURL( 'relationships' ); - var jqjson = $.post( ncpath, form_values, function(data) { - $.each( data, function(item, source_target) { + var jqjson = $.post( ncpath, form_values, function( data ) { + $.each( data, function( item, source_target ) { var source_found = get_ellipse( source_target[0] ); var target_found = get_ellipse( source_target[1] ); - var relation_found = $.inArray( source_target[2], $('#keymap').data('relations') ); + var relation_found = $.inArray( source_target[2], $( '#keymap' ).data( 'relations' ) ); if( source_found.size() && target_found.size() && relation_found > -1 ) { var relation = relation_manager.create( source_target[0], source_target[1], relation_found ); relation.data( 'type', source_target[2] ); @@ -831,13 +1073,13 @@ $(document).ready(function () { relation.data( 'note', $('#note').val() ); relation_manager.toggle_active( relation.attr('id') ); } - $(evt.target).button("enable"); + $(evt.target).button( 'enable' ); }); - $( "#dialog-form" ).dialog( "close" ); + $( '#dialog-form' ).dialog( 'close' ); }, 'json' ); }, Cancel: function() { - $( this ).dialog( "close" ); + $( this ).dialog( 'close' ); } }, create: function(event, ui) { @@ -859,7 +1101,15 @@ $(document).ready(function () { }); }, open: function() { - relation_manager.create_temporary( $('#source_node_id').val(), $('#target_node_id').val() ); + relation_manager.create_temporary( + $('#source_node_id').val(), $('#target_node_id').val() ); + var buttonset = $(this).parent().find( '.ui-dialog-buttonset' ) + if( readings_equivalent( $('#source_node_id').val(), + $('#target_node_id').val() ) ) { + buttonset.find( "button:contains('Merge readings')" ).show(); + } else { + buttonset.find( "button:contains('Merge readings')" ).hide(); + } $(".ui-widget-overlay").css("background", "none"); $("#dialog_overlay").show(); $("#dialog_overlay").height( $("#enlargement_container").height() ); @@ -892,23 +1142,23 @@ $(document).ready(function () { } ); } - var deletion_buttonset = { - cancel: function() { $( this ).dialog( "close" ); }, - global: function () { delete_relation( true ); }, - delete: function() { delete_relation( false ); } - }; - + // Set up the relationship info display and deletion dialog. $( "#delete-form" ).dialog({ autoOpen: false, height: 135, width: 250, modal: false, + buttons: { + OK: function() { $( this ).dialog( "close" ); }, + "Delete all": function () { delete_relation( true ); }, + Delete: function() { delete_relation( false ); } + }, create: function(event, ui) { // TODO What is this logic doing? // This scales the buttons in the dialog and makes it look proper // Not sure how essential it is, does anything break if it's not here? var buttonset = $(this).parent().find( '.ui-dialog-buttonset' ).css( 'width', '100%' ); - buttonset.find( "button:contains('Cancel')" ).css( 'float', 'right' ); + buttonset.find( "button:contains('OK')" ).css( 'float', 'right' ); // A: This makes sure that the pop up delete relation dialogue for a hovered over // relation auto closes if the user doesn't engage (mouseover) with it. var dialog_aria = $("div[aria-labelledby='ui-dialog-title-delete-form']"); @@ -920,22 +1170,21 @@ $(document).ready(function () { }) }, open: function() { + // Show the appropriate buttons... + var buttonset = $(this).parent().find( '.ui-dialog-buttonset' ) + // If the user can't edit, show only the OK button if( !editable ) { - $( this ).dialog( "option", "buttons", - [{ text: "OK", click: deletion_buttonset['cancel'] }] ); + buttonset.find( "button:contains('Delete')" ).hide(); + // If the relationship scope is local, show only OK and Delete } else if( $('#delete_relation_scope').text() === 'local' ) { $( this ).dialog( "option", "width", 160 ); - $( this ).dialog( "option", "buttons", - [{ text: "Delete", click: deletion_buttonset['delete'] }, - { text: "Cancel", click: deletion_buttonset['cancel'] }] ); + buttonset.find( "button:contains('Delete')" ).show(); + buttonset.find( "button:contains('Delete all')" ).hide(); + // Otherwise, show all three } else { $( this ).dialog( "option", "width", 200 ); - $( this ).dialog( "option", "buttons", - [{ text: "Delete", click: deletion_buttonset['delete'] }, - { text: "Delete all", click: deletion_buttonset['global'] }, - { text: "Cancel", click: deletion_buttonset['cancel'] }] ); + buttonset.find( "button:contains('Delete')" ).show(); } - mouseWait = setTimeout( function() { $("#delete-form").dialog( "close" ) }, 2000 ); }, close: function() {} @@ -947,18 +1196,18 @@ $(document).ready(function () { width: 250, modal: true, buttons: { - Cancel: function() { $( this ).dialog( "close" ); }, - Detach: function ( evt ) { - $(evt.target).button("disable"); - var form_values = $('#detach_collated_form').serialize(); - ncpath = getTextURL( 'duplicate' ); - var jqjson = $.post( ncpath, form_values, function(data) { - $.each( data, function(reading, newreading) { - alert( "Would detach reading " + newreading['id'] + " from " + reading ); - }); - $(evt.target).button("enable"); - }); - } + Cancel: function() { $( this ).dialog( "close" ); }, + Detach: function ( evt ) { + var self = $(this); + $( evt.target ).button( "disable" ); + var form_values = $('#detach_collated_form').serialize(); + ncpath = getTextURL( 'duplicate' ); + var jqjson = $.post( ncpath, form_values, function(data) { + detach_node( data ); + $(evt.target).button("enable"); + self.dialog( "close" ); + } ); + } }, create: function(event, ui) { var buttonset = $(this).parent().find( '.ui-dialog-buttonset' ).css( 'width', '100%' ); @@ -978,23 +1227,23 @@ $(document).ready(function () { $("#dialog_overlay").hide(); } }).ajaxError( function(event, jqXHR, ajaxSettings, thrownError) { - if( ajaxSettings.url == getTextURL('duplicate') - && ajaxSettings.type == 'POST' && jqXHR.status == 403 ) { - var error; - if( jqXHR.responseText.indexOf('do not have permission to modify') > -1 ) { - error = 'You are not authorized to modify this tradition. (Try logging in again?)'; - } else { - try { - var errobj = jQuery.parseJSON( jqXHR.responseText ); - error = errobj.error + '
The relationship cannot be made.

'; - } catch(e) { - error = jqXHR.responseText; - } - } - $('#multipleselect-form-status').append( '

Error: ' + error ); - } - $(event.target).parent().find('.ui-button').button("enable"); - }); + if( ajaxSettings.url == getTextURL('duplicate') + && ajaxSettings.type == 'POST' && jqXHR.status == 403 ) { + var error; + if( jqXHR.responseText.indexOf('do not have permission to modify') > -1 ) { + error = 'You are not authorized to modify this tradition. (Try logging in again?)'; + } else { + try { + var errobj = jQuery.parseJSON( jqXHR.responseText ); + error = errobj.error + '
The relationship cannot be made.

'; + } catch(e) { + error = jqXHR.responseText; + } + } + $('#multipleselect-form-status').append( '

Error: ' + error ); + } + $(event.target).parent().find('.ui-button').button("enable"); + }); // Helpers for relationship deletion