package org.egonet.gui.wholenet;
import java.awt.Component;
import java.awt.Cursor;
import java.awt.event.ActionEvent;
import java.io.File;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import javax.swing.*;
import javax.swing.table.AbstractTableModel;
import javax.swing.table.DefaultTableCellRenderer;
import javax.swing.table.TableCellRenderer;
import org.egonet.graph.wholenet.WholeNetwork;
import org.egonet.graph.wholenet.WholeNetwork.Settings;
import org.egonet.graph.wholenet.WholeNetworkTie.DiscrepancyStrategy;
import org.egonet.io.InterviewReader;
import org.egonet.io.wholenet.NameMappingReader;
import org.egonet.io.wholenet.NameMappingWriter;
import org.egonet.model.Interview;
import org.egonet.model.Study;
import org.egonet.model.answer.*;
import org.egonet.model.question.AlterQuestion;
import org.egonet.model.question.Question;
import org.egonet.util.CatchingAction;
import org.egonet.util.ExtensionFileFilter;
import org.egonet.util.Name;
import org.egonet.util.SwingWorker;
import org.jdesktop.swingx.JXTable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import uk.ac.shef.wit.simmetrics.similaritymetrics.AbstractStringMetric;
import uk.ac.shef.wit.simmetrics.similaritymetrics.Levenshtein;
import net.miginfocom.swing.MigLayout;
import net.sf.functionalj.Function2;
import net.sf.functionalj.Function2Impl;
import net.sf.functionalj.Functions;
import net.sf.functionalj.tuple.Pair;
import net.sf.functionalj.tuple.Triple;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
public class NameMapperFrame extends JFrame {
final private static Logger logger = LoggerFactory.getLogger(NameMapperFrame.class);
private final Study study;
private final File studyFile;
private final List<Pair<File, Interview>> interviewMap;
private MapperTableModel tableModel;
public NameMapperFrame(Study study, File studyFile, List<File> mappableFiles) {
super("Whole Network - Alter Name Mapping Editor");
this.study = study;
this.studyFile = studyFile;
this.interviewMap = new ArrayList<Pair<File, Interview>>(mappableFiles.size());
this.study.toString();
for(File intFile : mappableFiles) {
try {
InterviewReader ir = new InterviewReader(study, intFile);
Interview intV = ir.getInterview();
Pair<File, Interview> p = new Pair<File, Interview>(intFile, intV);
interviewMap.add(p);
}
catch (Exception ex ) {
logger.info("Couldn't map " + intFile.getName());
logger.error(ex.toString());
}
}
build();
}
/**
* This class is a data member which will keep track of exactly what should
* mapping happen for every alter in every interview. It may point to
* another alter that should be treated as the same thing, or it may be set
* to never map to anything.
*/
public class NameMapping implements Comparable<NameMapping> {
final Study study;
final Interview interview;
final Integer alterNumber;
final String alterName;
private Integer group;
public NameMapping(Study study, Interview interview, Integer alterNumber, Integer group) {
super();
this.interview = interview;
this.study = study;
this.alterNumber = alterNumber;
this.alterName = interview.getAlterList()[alterNumber];
this.group = group;
}
// ego constructor
public NameMapping(Study study, Interview interview, String iName, Integer group) {
super();
this.interview = interview;
this.study = study;
this.alterNumber = -1;
this.alterName = new Name(iName).toString();
this.group = group;
}
public Integer getGroup() {
return group;
}
public void setGroup(Integer group) {
this.group = group;
}
public Study getStudy() {
return study;
}
public Interview getInterview() {
return interview;
}
public Integer getAlterNumber() {
return alterNumber;
}
public String toString() {
return alterName;
}
public int compareTo(NameMapping o) {
return alterNumber.compareTo(o.alterNumber);
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + getOuterType().hashCode();
result = prime * result
+ ((alterNumber == null) ? 0 : alterNumber.hashCode());
result = prime * result
+ ((interview == null) ? 0 : interview.hashCode());
result = prime * result + ((study == null) ? 0 : study.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (!(obj instanceof NameMapping))
return false;
NameMapping other = (NameMapping) obj;
if (!getOuterType().equals(other.getOuterType()))
return false;
if (alterNumber == null) {
if (other.alterNumber != null)
return false;
} else if (!alterNumber.equals(other.alterNumber))
return false;
if (interview == null) {
if (other.interview != null)
return false;
} else if (!interview.equals(other.interview))
return false;
if (study == null) {
if (other.study != null)
return false;
} else if (!study.equals(other.study))
return false;
return true;
}
private NameMapperFrame getOuterType() {
return NameMapperFrame.this;
}
}
public class MapperTableModel extends AbstractTableModel {
public final String [] columns = {"Alter", "Ego", "Mapping Group"};
private final List<NameMapping> mappings;
private final List<Question> alterQuestions;
private final Set<Long> alterQuestionIds;
private final Map<Triple<Long,String,Integer>,Answer> questionInterviewAlterToAnswer;
public Map<String,String>
attributesForInterviewAndAlterId(Interview interview,Integer alterId) {
Map<String,String> attributes = Maps.newTreeMap();
String interviewName = interview.toString();
for(Question question : alterQuestions) {
Answer answer =
questionInterviewAlterToAnswer.get(
new Triple<Long,String,Integer>(
question.UniqueId,interviewName,alterId));
attributes.put(question.title, showAnswer(answer));
}
return attributes;
}
public MapperTableModel() {
mappings = new ArrayList<NameMapping>();
alterQuestions = new ArrayList<Question>();
alterQuestionIds = new TreeSet<Long>();
questionInterviewAlterToAnswer = new TreeMap<Triple<Long,String,Integer>,Answer>();
for(Question question : study.getQuestions().values()) {
if(question instanceof AlterQuestion) {
alterQuestions.add(question);
alterQuestionIds.add(question.UniqueId);
}
}
int group = 1;
for(Pair<File, Interview> entry : interviewMap) {
Interview interview = entry.getSecond();
NameMapping egoMapping = new NameMapping(study, interview, entry.getFirst().getName(), group++);
mappings.add(egoMapping);
String [] alterList = interview.getAlterList();
for(int i = 0; i < alterList.length; i++) {
NameMapping mapping = new NameMapping(study, interview, i, group++);
mappings.add(mapping);
}
for(Answer answer : interview.get_answers()) {
if(alterQuestionIds.contains(answer.getQuestionId())) {
questionInterviewAlterToAnswer.put(
new Triple<Long,String,Integer>(
answer.getQuestionId(),
interview.toString(),
answer.firstAlter()),
answer);
}
}
}
}
public int getColumnCount() {
return columns.length + alterQuestions.size();
}
public String getColumnName(int column) {
return column < columns.length ? columns[column] :
questionForColumn(column).title;
}
private Question questionForColumn(int column) {
return alterQuestions.get(column - columns.length);
}
public int getRowCount() {
return mappings.size();
}
public Object getValueAt(int rowIndex, int columnIndex) {
NameMapping row = mappings.get(rowIndex);
if(columnIndex == 0) {
return row.alterName;
} else if(columnIndex == 1) {
String egoName = row.getInterview().getIntName();
return new Name(egoName).toString();
} else if(columnIndex == 2) {
return row.group;
}
else { // Answer to alter question
Question question = questionForColumn(columnIndex);
Triple<Long,String,Integer> key =
new Triple<Long,String,Integer>(
question.UniqueId,
row.getInterview().toString(),
row.alterNumber);
return showAnswer(questionInterviewAlterToAnswer.get(key));
}
}
private String showAnswer(Answer answer) {
String result = answer+"";
return result.equals("-1") || result.equals("null") ? "" : result;
}
@Override
public boolean isCellEditable(int rowIndex, int columnIndex) {
if(columnIndex == 2)
return true;
return false;
}
@Override
public void setValueAt(Object aValue, int rowIndex, int columnIndex) {
if(columnIndex == 2 && aValue instanceof String) {
Integer i = null;
try {
i = Integer.parseInt(aValue.toString());
}
catch(NumberFormatException ex) {
return;
}
NameMapping row = mappings.get(rowIndex);
row.setGroup(i);
}
}
public List<NameMapping> getMappings() {
return mappings;
}
}
class MappingRenderer implements ListCellRenderer, TableCellRenderer, Serializable {
private final DefaultTableCellRenderer tableDelegate = new DefaultTableCellRenderer();
private final DefaultListCellRenderer listDelegate = new DefaultListCellRenderer();
public Component getTableCellRendererComponent(JTable table, Object value,
boolean isSelected, boolean hasFocus, int row, int column) {
if(value instanceof NameMapping) {
value = convert((NameMapping)value);
}
return tableDelegate.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
}
public Component getListCellRendererComponent(JList list, Object value,
int index, boolean isSelected, boolean cellHasFocus) {
if(value instanceof NameMapping) {
value = convert((NameMapping)value);
}
return listDelegate.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
}
private String convert(NameMapping mapping) {
Interview intv = mapping.interview;
String ego = new Name(intv.getIntName()).toString();
String alter = mapping.alterName;
return alter + " (" + ego + ")";
}
}
private Settings settings = new Settings();
private void editSettings(final Settings settings) {
JPanel panel = new JPanel(new MigLayout());
panel.add(new JLabel("In how many interviews must an alter be mentioned"),"span,grow");
panel.add(new JLabel("in order to be included in the whole network?"),"span,grow");
final JTextField inclusionField = new JTextField(5);
inclusionField.setText(settings.inclusionThreshold+"");
panel.add(inclusionField,"wrap");
panel.add(new JSeparator(),"span,grow");
final JCheckBox egoAlwaysIncludedField =
new JCheckBox(
"Always include ego (otherwise ego not mentioned often " +
"enough is filtered as above)",
settings.alwaysIncludeEgo);
panel.add(egoAlwaysIncludedField,"span,grow");
panel.add(new JSeparator(),"span,grow");
panel.add(new JLabel("Alter tie discrepancies"),"wrap");
final ButtonGroup group = new ButtonGroup();
for(DiscrepancyStrategy strategy : DiscrepancyStrategy.values()) {
JRadioButton button = new JRadioButton(strategy.name()+" - "+strategy.getDescription());
button.setActionCommand(strategy.name());
group.add(button);
panel.add(button,"span,grow");
if(settings.discrepancyStrategy.equals(strategy)) {
button.setSelected(true);
}
}
final JCheckBox egoOverrideField =
new JCheckBox(
"Ego always connected to own alters " +
"(even if all other respondents disagree)",
settings.egoAlwaysTiedToOwnAlters);
panel.add(egoOverrideField,"span,grow");
panel.add(new JSeparator(),"span,grow");
final JFrame frame = new JFrame("Whole Network Analysis");
panel.add(new JButton(new CatchingAction("Save") {
public void safeActionPerformed(ActionEvent e) throws Exception {
try {
settings.inclusionThreshold = Integer.parseInt(inclusionField.getText());
} catch(Exception ex) {
}
settings.alwaysIncludeEgo = egoAlwaysIncludedField.isSelected();
settings.discrepancyStrategy =
DiscrepancyStrategy.valueOf(group.getSelection().getActionCommand());
settings.egoAlwaysTiedToOwnAlters = egoOverrideField.isSelected();
frame.dispose();
}
}));
frame.setContentPane(panel);
frame.pack();
frame.setVisible(true);
}
private void build() {
tableModel = new MapperTableModel();
final JXTable table = new JXTable(tableModel);
table.setSortable(true);
table.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
// column 0 - source ego/alter
table.getColumnModel().getColumn(0).setCellRenderer(new MappingRenderer());
// column 1 - mapping group
; // no changes
MigLayout layout = new MigLayout("fill", "[grow]");
setLayout(layout);
JScrollPane scrollPane =
new JScrollPane(table,
ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS,
ScrollPaneConstants.HORIZONTAL_SCROLLBAR_ALWAYS);
add(new JLabel("Please perform alter/ego name mappings"), "growx, wrap");
add(scrollPane, "grow, span, wrap");
Action cancelAction = new CatchingAction("Cancel") {
@Override
public void safeActionPerformed(ActionEvent e) throws Exception {
dispose();
}
};
JButton cancelButton = new JButton(cancelAction);
add(cancelButton, "split, growx");
Action automatchAction = new CatchingAction("Automatch") {
@Override
public void safeActionPerformed(ActionEvent e) throws Exception {
setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); // Hourglass cursor
try {
doDefaultSimilarity(tableModel, 0.8f);
table.repaint();
} finally {
setCursor(Cursor.getDefaultCursor()); // Finished - back to normal cursor
}
}
};
JButton automatchButton = new JButton(automatchAction);
add(automatchButton, "split, growx");
add(
new JButton(new CatchingAction("Save") {
public void safeActionPerformed(ActionEvent e)
throws Exception {
File studyFile = NameMapperFrame.this.studyFile;
File suggestedOutputFile =
new File(
studyFile.getParent(),
NameMapperFrame.this.study.getStudyName()+".mapping");
JFileChooser fc = new JFileChooser(suggestedOutputFile);
fc.setSelectedFile(suggestedOutputFile);
fc.addChoosableFileFilter(new ExtensionFileFilter("Name Mapping Files","mapping"));
fc.setDialogTitle("Save name mappings");
if(fc.showSaveDialog(NameMapperFrame.this) ==
JFileChooser.APPROVE_OPTION)
{
new NameMappingWriter(tableModel.getMappings())
.writeToFile(fc.getSelectedFile());
}
}
}),
"split, growx");
add(
new JButton(new CatchingAction("Load") {
public void safeActionPerformed(ActionEvent e)
throws Exception {
File studyFile = NameMapperFrame.this.studyFile;
File suggestedOutputFile =
new File(studyFile.getParent());
JFileChooser fc = new JFileChooser(suggestedOutputFile);
fc.setSelectedFile(suggestedOutputFile);
fc.addChoosableFileFilter(new ExtensionFileFilter("Name Mapping Files","mapping"));
fc.setDialogTitle("Load name mappings from file");
if(fc.showOpenDialog(NameMapperFrame.this) ==
JFileChooser.APPROVE_OPTION)
{
new NameMappingReader(fc.getSelectedFile())
.applyTo(tableModel.getMappings());
}
}
}),
"split, growx");
add(new JButton(
new CatchingAction("Settings") {
@Override
public void safeActionPerformed(ActionEvent e) throws Exception {
editSettings(settings);
}
}),
"split, growx");
Action continueAction = new CatchingAction("Continue") {
@Override
public void safeActionPerformed(ActionEvent e) throws Exception {
SwingWorker sw = new SwingWorker() {
WholeNetworkViewer viewer;
@Override
public Object construct() {
List<NameMapping> mappings = tableModel.getMappings();
List<Interview> interviews = Lists.newArrayList(
Functions.map(new Pair<File,Interview>().second, interviewMap));
Function2<Map<String,String>,Interview,Integer> getAlterAttributes = new Function2Impl<Map<String,String>,Interview,Integer>(){
public Map<String, String> call(
Interview interview, Integer alterId) {
return tableModel.attributesForInterviewAndAlterId(interview, alterId);
}
};
// do the whole network combination, and export/show it!
WholeNetwork net =
new WholeNetwork(study, interviews, mappings, settings, getAlterAttributes);
viewer = new WholeNetworkViewer(study, studyFile, net);
return viewer;
}
public void finished() {
viewer.setVisible(true);
}
};
sw.start();
dispose();
}
};
JButton continueButton = new JButton(continueAction);
add(continueButton, "growx");
pack();
}
private void doDefaultSimilarity(MapperTableModel model, float cutoff) {
AbstractStringMetric metric = new Levenshtein();
Map<Integer,List<NameMapping>> groupings = new HashMap<Integer,List<NameMapping>>();
int groupCounter = 1;
ArrayList<NameMapping> names = new ArrayList<NameMapping>(model.getMappings());
while(names.size() > 0) {
NameMapping current = names.remove(0);
if(current == null || current.alterName == null)
continue;
float highest = 0.0f; Integer highestGroup = null;
for(Map.Entry<Integer,List<NameMapping>> entry : groupings.entrySet()) {
float averageScore = 0; int elementCount = 0;
for(NameMapping entryMapping : entry.getValue()) {
if(entryMapping.alterName == null)
continue;
// when figuring a true score, make everything lowercase and trimmed
float thisScore = metric.getSimilarity(current.alterName.toLowerCase().trim(), entryMapping.alterName.toLowerCase().trim());
if(thisScore >= cutoff)
logger.info(current.alterName + " + " + entryMapping.alterName + " = " + thisScore);
averageScore += thisScore;
elementCount++;
}
averageScore /= elementCount;
if(averageScore >= cutoff && averageScore > highest) {
highest = averageScore;
highestGroup = entry.getKey();
}
}
// pick an existing group
if(highestGroup != null) {
List<NameMapping> destGroup = groupings.get(highestGroup);
destGroup.add(current);
}
// create a new group
else {
List<NameMapping> destGroup = new ArrayList<NameMapping>();
destGroup.add(current);
groupings.put(groupCounter++, destGroup);
}
}
for(NameMapping map : model.getMappings()) {
for(Integer group : groupings.keySet()) {
if(groupings.get(group).contains(map)) {
map.setGroup(group);
break;
}
}
}
}
}