diff --git a/.gitignore b/.gitignore index dd64606..8e09288 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,8 @@ DomainIds.txt VocabularyIds.txt vocabularyVersion.txt +local_files/ + ### Intellij ### # Created by https://www.gitignore.io/api/intellij # Edit at https://www.gitignore.io/?templates=intellij diff --git a/src/org/ohdsi/usagi/CodeMapping.java b/src/org/ohdsi/usagi/CodeMapping.java index c556d12..e7c8f77 100644 --- a/src/org/ohdsi/usagi/CodeMapping.java +++ b/src/org/ohdsi/usagi/CodeMapping.java @@ -1,12 +1,12 @@ /******************************************************************************* - * Copyright 2019 Observational Health Data Sciences and Informatics - * + * Copyright 2020 Observational Health Data Sciences and Informatics & The Hyve + * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -15,28 +15,132 @@ ******************************************************************************/ package org.ohdsi.usagi; +import org.ohdsi.usagi.ui.Global; + import java.util.ArrayList; import java.util.List; /** * Data class for holding information on a single source code and its mapping - * + * * @author MSCHUEMI - * */ public class CodeMapping { - public static enum MappingStatus { - APPROVED, UNCHECKED, AUTO_MAPPED, AUTO_MAPPED_TO_1, INVALID_TARGET - }; + public enum MappingStatus { + // Includes IGNORED for backwards compatibility + APPROVED, UNCHECKED, AUTO_MAPPED, AUTO_MAPPED_TO_1, INVALID_TARGET, FLAGGED, IGNORED + } + + public enum Equivalence { + EQUAL, EQUIVALENT, WIDER, NARROWER, INEXACT, UNMATCHED, UNREVIEWED + } + + private SourceCode sourceCode; + private double matchScore; + private MappingStatus mappingStatus; + private Equivalence equivalence; + private List targetConcepts = new ArrayList<>(1); + private String comment; + private String statusSetBy; + private long statusSetOn; + private String assignedReviewer; + + public CodeMapping(SourceCode sourceCode) { + this.setSourceCode(sourceCode); + } + + public void approve(Equivalence equivalence) { + setStatus(MappingStatus.APPROVED); + this.setEquivalence(equivalence); + } + + public void flag(Equivalence equivalence) { + setStatus(MappingStatus.FLAGGED); + this.setEquivalence(equivalence); + } + + public void setStatus(MappingStatus mappingStatus) { + this.setMappingStatus(mappingStatus); + this.setStatusSetOn(System.currentTimeMillis()); + this.setStatusSetBy(Global.author); + } + + public void setUnchecked() { + this.setMappingStatus(MappingStatus.UNCHECKED); + this.setEquivalence(Equivalence.UNREVIEWED); + this.setStatusSetOn(0); + this.setStatusSetBy(""); + } + + public SourceCode getSourceCode() { + return sourceCode; + } + + public void setSourceCode(SourceCode sourceCode) { + this.sourceCode = sourceCode; + } + + public double getMatchScore() { + return matchScore; + } + + public void setMatchScore(double matchScore) { + this.matchScore = matchScore; + } + + public MappingStatus getMappingStatus() { + return mappingStatus; + } + + public void setMappingStatus(MappingStatus mappingStatus) { + this.mappingStatus = mappingStatus; + } + + public Equivalence getEquivalence() { + return equivalence; + } + + public void setEquivalence(Equivalence equivalence) { + this.equivalence = equivalence; + } + + public List getTargetConcepts() { + return targetConcepts; + } + + public void setTargetConcepts(List targetConcepts) { + this.targetConcepts = targetConcepts; + } + + public String getComment() { + return comment; + } + + public void setComment(String comment) { + this.comment = comment; + } + + public String getStatusSetBy() { + return statusSetBy; + } + + public void setStatusSetBy(String statusSetBy) { + this.statusSetBy = statusSetBy; + } + + public long getStatusSetOn() { + return statusSetOn; + } - public SourceCode sourceCode; - public double matchScore; - public MappingStatus mappingStatus; - public List targetConcepts = new ArrayList(1); - public String comment; + public void setStatusSetOn(long statusSetOn) { + this.statusSetOn = statusSetOn; + } - public CodeMapping(SourceCode sourceCode) { - this.sourceCode = sourceCode; - } + public String getAssignedReviewer() { + return assignedReviewer; + } + public void setAssignedReviewer(String assignedReviewer) { + this.assignedReviewer = assignedReviewer; + } } diff --git a/src/org/ohdsi/usagi/MappingTarget.java b/src/org/ohdsi/usagi/MappingTarget.java new file mode 100644 index 0000000..5341dc6 --- /dev/null +++ b/src/org/ohdsi/usagi/MappingTarget.java @@ -0,0 +1,75 @@ +/******************************************************************************* + * Copyright 2020 Observational Health Data Sciences and Informatics & The Hyve + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + ******************************************************************************/ +package org.ohdsi.usagi; + +/** + * Class for holding information about a single (target) concept in the Vocabulary + */ +public class MappingTarget{ + public enum Type { + MAPS_TO, MAPS_TO_VALUE, MAPS_TO_UNIT + } + + private final Concept concept; + private final String createdBy; + private final long createdTime; + private Type mappingType; + + public MappingTarget() { + this.concept = Concept.createEmptyConcept(); + this.mappingType = Type.MAPS_TO; + this.createdBy = ""; + this.createdTime = 0; + } + + public MappingTarget(Concept concept, String createdBy) { + this(concept, Type.MAPS_TO, createdBy); + } + + public MappingTarget(Concept concept, Type mappingType, String createdBy) { + this.concept = concept; + this.mappingType = mappingType; + this.createdBy = createdBy; + this.createdTime = System.currentTimeMillis(); + } + + public MappingTarget(Concept concept, Type mappingType, String createdBy, long createdTime) { + this.concept = concept; + this.mappingType = mappingType; + this.createdBy = createdBy; + this.createdTime = createdTime; + } + + public Concept getConcept() { + return concept; + } + + public String getCreatedBy() { + return createdBy; + } + + public long getCreatedTime() { + return createdTime; + } + + public Type getMappingType() { + return mappingType; + } + + public void setMappingType(Type mappingType) { + this.mappingType = mappingType; + } +} diff --git a/src/org/ohdsi/usagi/ReadCodeMappingsFromFile.java b/src/org/ohdsi/usagi/ReadCodeMappingsFromFile.java index 0092f41..f514fdd 100644 --- a/src/org/ohdsi/usagi/ReadCodeMappingsFromFile.java +++ b/src/org/ohdsi/usagi/ReadCodeMappingsFromFile.java @@ -1,12 +1,12 @@ /******************************************************************************* * Copyright 2019 Observational Health Data Sciences and Informatics - * + * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -18,12 +18,13 @@ import java.util.Iterator; import org.ohdsi.usagi.CodeMapping.MappingStatus; +import org.ohdsi.usagi.CodeMapping.Equivalence; import org.ohdsi.usagi.ui.Global; import org.ohdsi.utilities.files.ReadCSVFileWithHeader; import org.ohdsi.utilities.files.Row; public class ReadCodeMappingsFromFile implements Iterable { - private String filename; + private final String filename; public ReadCodeMappingsFromFile(String filename) { this.filename = filename; @@ -36,9 +37,9 @@ public Iterator iterator() { public class RowIterator implements Iterator { - private Iterator iterator; - private CodeMapping buffer; - private Row row; + private Iterator iterator; + private CodeMapping buffer; + private Row row; public RowIterator() { iterator = new ReadCSVFileWithHeader(filename).iterator(); @@ -56,22 +57,38 @@ private void readNext() { buffer = null; } else { buffer = new CodeMapping(new SourceCode(row)); - buffer.matchScore = row.getDouble("matchScore"); - buffer.mappingStatus = MappingStatus.valueOf(row.get("mappingStatus")); - try { - buffer.comment = row.get("comment"); - } catch (Exception e) { - buffer.comment = ""; - } - while (row != null && new SourceCode(row).sourceCode.equals(buffer.sourceCode.sourceCode) - && new SourceCode(row).sourceName.equals(buffer.sourceCode.sourceName)) { + buffer.setMatchScore(row.getDouble("matchScore")); + buffer.setMappingStatus(MappingStatus.valueOf(row.get("mappingStatus"))); + + // Status provenance and review need a default as these fields might not be available in older Usagi files + buffer.setStatusSetBy(row.get("statusSetBy", "")); + buffer.setStatusSetOn(row.getLong("statusSetOn", "0")); + buffer.setEquivalence(Equivalence.valueOf(row.get("equivalence", "UNREVIEWED"))); + buffer.setAssignedReviewer(row.get("assignedReviewer", "")); + buffer.setComment(row.get("comment", "")); + + while (row != null + && new SourceCode(row).sourceCode.equals(buffer.getSourceCode().sourceCode) + && new SourceCode(row).sourceName.equals(buffer.getSourceCode().sourceName)) { if (row.getInt("conceptId") != 0) { Concept concept = Global.dbEngine.getConcept(row.getInt("conceptId")); + if (concept == null) { - buffer.mappingStatus = MappingStatus.INVALID_TARGET; - buffer.comment = "Invalid existing target: " + row.get("conceptId"); + buffer.setMappingStatus(MappingStatus.INVALID_TARGET); + buffer.setComment("Invalid existing target: " + row.get("conceptId")); } else { - buffer.targetConcepts.add(concept); + // Type and provenance might not be available in older Usagi files + MappingTarget mappingTarget = new MappingTarget( + concept, + MappingTarget.Type.valueOf(row.get("mappingType", "MAPS_TO") + .replace("EVENT", "MAPS_TO") + .replace("VALUE", "MAPS_TO_VALUE") + .replace("UNIT", "MAPS_TO_UNIT") + ), + row.get("createdBy", ""), + row.getLong("createdOn", "0") + ); + buffer.getTargetConcepts().add(mappingTarget); } } if (iterator.hasNext()) @@ -98,7 +115,5 @@ public CodeMapping next() { public void remove() { throw new RuntimeException("Remove not supported"); } - } - } diff --git a/src/org/ohdsi/usagi/SourceCode.java b/src/org/ohdsi/usagi/SourceCode.java index 3da47fc..92ba068 100644 --- a/src/org/ohdsi/usagi/SourceCode.java +++ b/src/org/ohdsi/usagi/SourceCode.java @@ -36,7 +36,7 @@ public class SourceCode { public Set sourceAutoAssignedConceptIds = new HashSet(); public List> sourceAdditionalInfo = new ArrayList>(); - private static String ADDITIONAL_INFO_PREFIX = "ADD_INFO:"; + private final static String ADDITIONAL_INFO_PREFIX = "ADD_INFO:"; public Row toRow() { Row row = new Row(); diff --git a/src/org/ohdsi/usagi/WriteCodeMappingsToFile.java b/src/org/ohdsi/usagi/WriteCodeMappingsToFile.java index 7eb1867..2e62d92 100644 --- a/src/org/ohdsi/usagi/WriteCodeMappingsToFile.java +++ b/src/org/ohdsi/usagi/WriteCodeMappingsToFile.java @@ -15,8 +15,11 @@ ******************************************************************************/ package org.ohdsi.usagi; +import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; import java.util.ArrayList; import java.util.List; +import java.util.Locale; import org.ohdsi.utilities.files.Row; import org.ohdsi.utilities.files.WriteCSVFileWithHeader; @@ -26,24 +29,34 @@ */ public class WriteCodeMappingsToFile { private WriteCSVFileWithHeader out; + private DecimalFormat scoreFormat = new DecimalFormat("####0.00", new DecimalFormatSymbols(Locale.US)); public WriteCodeMappingsToFile(String filename) { out = new WriteCSVFileWithHeader(filename); } public void write(CodeMapping codeMapping) { - List targetConcepts; - if (codeMapping.targetConcepts.size() == 0) { - targetConcepts = new ArrayList(1); - targetConcepts.add(Concept.EMPTY_CONCEPT); - } else - targetConcepts = codeMapping.targetConcepts; - for (Concept targetConcept : targetConcepts) { - Row row = codeMapping.sourceCode.toRow(); - row.add("matchScore", codeMapping.matchScore); - row.add("mappingStatus", codeMapping.mappingStatus.toString()); - row.add("conceptId", targetConcept.conceptId); - row.add("comment", codeMapping.comment); + List mappingTargets; + if (codeMapping.getTargetConcepts().size() == 0) { + mappingTargets = new ArrayList<>(1); + mappingTargets.add(new MappingTarget()); + } else { + mappingTargets = codeMapping.getTargetConcepts(); + } + for (MappingTarget targetConcept : mappingTargets) { + Row row = codeMapping.getSourceCode().toRow(); + row.add("matchScore", scoreFormat.format(codeMapping.getMatchScore())); + row.add("mappingStatus", codeMapping.getMappingStatus().toString()); + row.add("equivalence", codeMapping.getEquivalence().toString()); + row.add("statusSetBy", codeMapping.getStatusSetBy()); + row.add("statusSetOn", codeMapping.getStatusSetOn()); + row.add("conceptId", targetConcept.getConcept().conceptId); + row.add("conceptName", targetConcept.getConcept().conceptName); // Never read in. + row.add("mappingType", targetConcept.getMappingType().toString()); + row.add("comment", codeMapping.getComment()); + row.add("createdBy", targetConcept.getCreatedBy()); + row.add("createdOn", targetConcept.getCreatedTime()); + row.add("assignedReviewer", codeMapping.getAssignedReviewer()); out.write(row); } } diff --git a/src/org/ohdsi/usagi/dataImport/ImportData.java b/src/org/ohdsi/usagi/dataImport/ImportData.java index ea7f426..2d0e43c 100644 --- a/src/org/ohdsi/usagi/dataImport/ImportData.java +++ b/src/org/ohdsi/usagi/dataImport/ImportData.java @@ -19,13 +19,9 @@ import java.util.List; import java.util.Vector; -import org.ohdsi.usagi.CodeMapping; +import org.ohdsi.usagi.*; import org.ohdsi.usagi.CodeMapping.MappingStatus; -import org.ohdsi.usagi.SourceCode; -import org.ohdsi.usagi.Concept; -import org.ohdsi.usagi.UsagiSearchEngine; import org.ohdsi.usagi.UsagiSearchEngine.ScoredConcept; -import org.ohdsi.usagi.WriteCodeMappingsToFile; import org.ohdsi.utilities.collections.Pair; import org.ohdsi.utilities.files.ReadCSVFileWithHeader; import org.ohdsi.utilities.files.Row; @@ -78,18 +74,19 @@ private void createInitialMapping(List sourceCodes, ImportSettings s List concepts = usagiSearchEngine.search(sourceCode.sourceName, true, sourceCode.sourceAutoAssignedConceptIds, settings.filterDomains, settings.filterConceptClasses, settings.filterVocabularies, settings.filterStandard, settings.includeSourceTerms); if (concepts.size() > 0) { - codeMapping.targetConcepts.add(concepts.get(0).concept); - codeMapping.matchScore = concepts.get(0).matchScore; + codeMapping.getTargetConcepts().add(new MappingTarget(concepts.get(0).concept, "")); + codeMapping.setMatchScore(concepts.get(0).matchScore); } else { - codeMapping.targetConcepts.add(Concept.EMPTY_CONCEPT); - codeMapping.matchScore = 0; + codeMapping.getTargetConcepts().add(new MappingTarget(Concept.EMPTY_CONCEPT, "")); + codeMapping.setMatchScore(0); } - codeMapping.mappingStatus = MappingStatus.UNCHECKED; + codeMapping.setMappingStatus(MappingStatus.UNCHECKED); if (sourceCode.sourceAutoAssignedConceptIds.size() == 1 && concepts.size() > 0) { - codeMapping.mappingStatus = MappingStatus.AUTO_MAPPED_TO_1; + codeMapping.setMappingStatus(MappingStatus.AUTO_MAPPED_TO_1); } else if (sourceCode.sourceAutoAssignedConceptIds.size() > 1 && concepts.size() > 0) { - codeMapping.mappingStatus = MappingStatus.AUTO_MAPPED; + codeMapping.setMappingStatus(MappingStatus.AUTO_MAPPED); } + codeMapping.setEquivalence(CodeMapping.Equivalence.UNREVIEWED); out.write(codeMapping); } out.close(); diff --git a/src/org/ohdsi/usagi/ui/AboutDialog.java b/src/org/ohdsi/usagi/ui/AboutDialog.java index 8a5dd84..575c2d9 100644 --- a/src/org/ohdsi/usagi/ui/AboutDialog.java +++ b/src/org/ohdsi/usagi/ui/AboutDialog.java @@ -63,7 +63,17 @@ public AboutDialog() { "text/html", "Usagi is maintained by The Hyve (www.thehyve.nl), and originally developed by Martijn Schuemie" + "
in Observational Health Data Sciences and Informatics (OHDSI)." + - "

For help, please review the Usagi Wiki."); + "

For help, please review the Usagi Wiki." + + "

Equivalence definitions based on HL7 concept-map-quivalence:" + + "
    " + + "
  • Equal = The concepts are exactly the same (i.e. intentionally identical).
  • " + + "
  • Equivalent = The concepts mean the same thing (i.e. extensionally identical).
  • " + + "
  • Wider = The target contains more information than to the source.
  • " + + "
  • Narrower = The target contains less information than the source.
  • " + + "
  • Inexact = The target overlaps with the source, but both source and target cover additional meaning.
  • " + + "
  • Unmatched = There is no match for this concept in the target code system.
  • " + + "
" + ); text.setEditable(false); text.setOpaque(false); diff --git a/src/org/ohdsi/usagi/ui/AuthorDialog.java b/src/org/ohdsi/usagi/ui/AuthorDialog.java new file mode 100644 index 0000000..6b83d35 --- /dev/null +++ b/src/org/ohdsi/usagi/ui/AuthorDialog.java @@ -0,0 +1,63 @@ +/******************************************************************************* + * Copyright 2020 Observational Health Data Sciences and Informatics & The Hyve + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + ******************************************************************************/ +package org.ohdsi.usagi.ui; + +import javax.swing.*; +import java.awt.*; + +public class AuthorDialog extends JDialog { + + private static final long serialVersionUID = 8239922540117895957L; + + public AuthorDialog() { + setTitle("Author"); + setLayout(new GridBagLayout()); + GridBagConstraints g = new GridBagConstraints(); + g.fill = GridBagConstraints.BOTH; + g.ipadx = 5; + g.ipady = 5; + + g.gridx = 0; + g.gridy = 0; + add(new JLabel("Author:"), g); + + g.gridx = 1; + g.gridy = 0; + JTextField authorField = new JTextField(20); + authorField.setToolTipText("Please enter your name"); + authorField.setPreferredSize(new Dimension(100, 10)); + add(authorField, g); + + g.gridx = 0; + g.gridy = 2; + g.gridwidth = 2; + JPanel buttonPanel = new JPanel(); + buttonPanel.setLayout(new BoxLayout(buttonPanel, BoxLayout.X_AXIS)); + buttonPanel.add(Box.createHorizontalGlue()); + JButton saveButton = new JButton("Save"); + saveButton.setToolTipText("Save your name"); + saveButton.addActionListener(event -> { + Global.author = authorField.getText(); + setVisible(false); + }); + buttonPanel.add(saveButton); + add(buttonPanel, g); + + pack(); + setModal(true); + setLocationRelativeTo(Global.frame); + } +} diff --git a/src/org/ohdsi/usagi/ui/ExportSourceToConceptMapDialog.java b/src/org/ohdsi/usagi/ui/ExportSourceToConceptMapDialog.java index 4942455..2dcc2e0 100644 --- a/src/org/ohdsi/usagi/ui/ExportSourceToConceptMapDialog.java +++ b/src/org/ohdsi/usagi/ui/ExportSourceToConceptMapDialog.java @@ -18,11 +18,10 @@ import java.awt.Dimension; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; import java.io.File; import java.util.ArrayList; import java.util.List; +import java.util.stream.Collectors; import javax.swing.Box; import javax.swing.BoxLayout; @@ -38,6 +37,7 @@ import org.ohdsi.usagi.CodeMapping; import org.ohdsi.usagi.CodeMapping.MappingStatus; import org.ohdsi.usagi.Concept; +import org.ohdsi.usagi.MappingTarget; import org.ohdsi.utilities.files.Row; import org.ohdsi.utilities.files.WriteCSVFileWithHeader; @@ -110,20 +110,21 @@ private void export() { private void writeToCsvFile(String filename) { WriteCSVFileWithHeader out = new WriteCSVFileWithHeader(filename); for (CodeMapping mapping : Global.mapping) - if (exportUnapproved || mapping.mappingStatus == MappingStatus.APPROVED) { + if (exportUnapproved || mapping.getMappingStatus() == MappingStatus.APPROVED) { List targetConcepts; - if (mapping.targetConcepts.size() == 0) { + if (mapping.getTargetConcepts().size() == 0) { targetConcepts = new ArrayList(1); targetConcepts.add(Concept.EMPTY_CONCEPT); - } else - targetConcepts = mapping.targetConcepts; + } else { + targetConcepts = mapping.getTargetConcepts().stream().map(MappingTarget::getConcept).collect(Collectors.toList()); + } for (Concept targetConcept : targetConcepts) { Row row = new Row(); - row.add("source_code", mapping.sourceCode.sourceCode); + row.add("source_code", mapping.getSourceCode().sourceCode); row.add("source_concept_id", "0"); row.add("source_vocabulary_id", sourceVocabularyIdField.getText()); - row.add("source_code_description", mapping.sourceCode.sourceName); + row.add("source_code_description", mapping.getSourceCode().sourceName); row.add("target_concept_id", targetConcept.conceptId); row.add("target_vocabulary_id", targetConcept.conceptId == 0 ? "None" : targetConcept.vocabularyId ); row.add("valid_start_date", "1970-01-01"); diff --git a/src/org/ohdsi/usagi/ui/Global.java b/src/org/ohdsi/usagi/ui/Global.java index 299d41b..be488b8 100644 --- a/src/org/ohdsi/usagi/ui/Global.java +++ b/src/org/ohdsi/usagi/ui/Global.java @@ -42,8 +42,9 @@ public class Global { public static SaveAction saveAction; public static SaveAsAction saveAsAction; public static ApproveAction approveAction; - public static ApproveAllAction approveAllAction; - public static ClearAllAction clearAllAction; + public static FlagAction flagAction; + public static ReviewerAssignmentAction reviewerAssignmentAction; + public static ClearSelectedAction clearSelectedAction; public static ConceptInformationAction conceptInfoAction; public static AthenaAction athenaAction; public static GoogleSearchAction googleSearchAction; @@ -57,4 +58,6 @@ public class Global { public static Vector vocabularyIds; public static Vector domainIds; public static ShowStatsAction showStatsAction; + + public static String author; } diff --git a/src/org/ohdsi/usagi/ui/ImportDialog.java b/src/org/ohdsi/usagi/ui/ImportDialog.java index 5829223..054dc81 100644 --- a/src/org/ohdsi/usagi/ui/ImportDialog.java +++ b/src/org/ohdsi/usagi/ui/ImportDialog.java @@ -15,12 +15,7 @@ ******************************************************************************/ package org.ohdsi.usagi.ui; -import java.awt.BorderLayout; -import java.awt.Color; -import java.awt.Component; -import java.awt.Dimension; -import java.awt.GridBagConstraints; -import java.awt.GridBagLayout; +import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.io.File; @@ -50,6 +45,7 @@ import org.ohdsi.usagi.CodeMapping; import org.ohdsi.usagi.CodeMapping.MappingStatus; +import org.ohdsi.usagi.MappingTarget; import org.ohdsi.usagi.SourceCode; import org.ohdsi.usagi.UsagiSearchEngine.ScoredConcept; import org.ohdsi.utilities.ReadXlsxFile; @@ -190,64 +186,49 @@ private Component createColumnMappingPanel() { columnMappingPanel = new JPanel(); columnMappingPanel.setLayout(new GridBagLayout()); - GridBagConstraints c = new GridBagConstraints(); - c.fill = GridBagConstraints.BOTH; - - c.gridx = 0; - c.gridy = 0; - c.anchor = GridBagConstraints.WEST; - c.weightx = 1; - columnMappingPanel.add(new JLabel("Source code column"), c); - c.gridx = 1; - c.gridy = 0; - c.anchor = GridBagConstraints.EAST; - c.weightx = 0.1; + GridBagConstraints cLabel = new GridBagConstraints(); + cLabel.fill = GridBagConstraints.BOTH; + cLabel.gridx = 0; + cLabel.gridy = 1; + cLabel.anchor = GridBagConstraints.WEST; + cLabel.weightx = 1; + + GridBagConstraints cBox = new GridBagConstraints(); + cBox.fill = GridBagConstraints.BOTH; + cBox.gridx = 1; + cBox.gridy = 1; + cBox.anchor = GridBagConstraints.EAST; + cBox.weightx = 0.1; + + columnMappingPanel.add(new JLabel("Source code column"), cLabel); sourceCodeColumn = new JComboBox(comboBoxOptions); sourceCodeColumn.setToolTipText("The column containing the source code"); - columnMappingPanel.add(sourceCodeColumn, c); + columnMappingPanel.add(sourceCodeColumn, cBox); - c.gridx = 0; - c.gridy = 1; - c.anchor = GridBagConstraints.WEST; - c.weightx = 1; - columnMappingPanel.add(new JLabel("Source name column"), c); - c.gridx = 1; - c.gridy = 1; - c.anchor = GridBagConstraints.EAST; - c.weightx = 0.1; - sourceNameColumn = new JComboBox(comboBoxOptions); + cLabel.gridy++; + cBox.gridy++; + columnMappingPanel.add(new JLabel("Source name column"), cLabel); + sourceNameColumn = new JComboBox<>(comboBoxOptions); sourceNameColumn.setToolTipText("The column containing the name or description of the source code, which will be used for matching"); - columnMappingPanel.add(sourceNameColumn, c); + columnMappingPanel.add(sourceNameColumn, cBox); - c.gridx = 0; - c.gridy = 2; - c.anchor = GridBagConstraints.WEST; - c.weightx = 1; - columnMappingPanel.add(new JLabel("Source frequency column"), c); - c.gridx = 1; - c.gridy = 2; - c.anchor = GridBagConstraints.EAST; - c.weightx = 0.1; - sourceFrequencyColumn = new JComboBox(comboBoxOptions); + cLabel.gridy++; + cBox.gridy++; + columnMappingPanel.add(new JLabel("Source frequency column"), cLabel); + sourceFrequencyColumn = new JComboBox<>(comboBoxOptions); sourceFrequencyColumn.setToolTipText("The column containing the frequency of the code in the source database"); - columnMappingPanel.add(sourceFrequencyColumn, c); + columnMappingPanel.add(sourceFrequencyColumn, cBox); - c.gridx = 0; - c.gridy = 3; - c.anchor = GridBagConstraints.WEST; - c.weightx = 1; - conceptIdsOrAtc = new JComboBox(new String[] { CONCEPT_IDS, ATC }); - columnMappingPanel.add(conceptIdsOrAtc, c); - c.gridx = 1; - c.gridy = 3; - c.anchor = GridBagConstraints.EAST; - c.weightx = 0.1; - autoConceptIdColumn = new JComboBox(comboBoxOptions); + cLabel.gridy++; + cBox.gridy++; + conceptIdsOrAtc = new JComboBox<>(new String[] { CONCEPT_IDS, ATC }); + columnMappingPanel.add(conceptIdsOrAtc, cLabel); + autoConceptIdColumn = new JComboBox<>(comboBoxOptions); autoConceptIdColumn.setToolTipText("The column containing a (semicolon-delimited) list of concept IDs to which the search will be restricted"); - columnMappingPanel.add(autoConceptIdColumn, c); + columnMappingPanel.add(autoConceptIdColumn, cBox); - gridY = 4; + gridY = cLabel.gridy + 1; addExtraColumnMapping(); columnMappingScrollPane = new JScrollPane(columnMappingPanel); @@ -356,7 +337,7 @@ private List createSourceCodes() { int sourceNameIndex = columnNames.indexOf(sourceNameColumn.getSelectedItem().toString()); int sourceFrequencyIndex = columnNames.indexOf(sourceFrequencyColumn.getSelectedItem().toString()); int sourceAutoIndex = columnNames.indexOf(autoConceptIdColumn.getSelectedItem().toString()); - List additionalInfoIndexes = new ArrayList(); + List additionalInfoIndexes = new ArrayList<>(); for (JComboBox additionalInfoColumn : additionalInfoColumns) { int index = columnNames.indexOf(additionalInfoColumn.getSelectedItem().toString()); if (index != -1) @@ -441,18 +422,19 @@ public void run() { List concepts = Global.usagiSearchEngine.search(sourceCode.sourceName, true, filterConceptIds, filterDomainsFinal, filterConceptClassesFinal, filterVocabulariesFinal, filterStandard, includeSourceConcepts); if (concepts.size() > 0) { - codeMapping.targetConcepts.add(concepts.get(0).concept); - codeMapping.matchScore = concepts.get(0).matchScore; + codeMapping.getTargetConcepts().add(new MappingTarget(concepts.get(0).concept, "")); + codeMapping.setMatchScore(concepts.get(0).matchScore); } else { - codeMapping.matchScore = 0; + codeMapping.setMatchScore(0); } - codeMapping.comment = ""; - codeMapping.mappingStatus = MappingStatus.UNCHECKED; + codeMapping.setComment(""); + codeMapping.setMappingStatus(MappingStatus.UNCHECKED); if (sourceCode.sourceAutoAssignedConceptIds.size() == 1 && concepts.size() > 0) { - codeMapping.mappingStatus = MappingStatus.AUTO_MAPPED_TO_1; + codeMapping.setMappingStatus(MappingStatus.AUTO_MAPPED_TO_1); } else if (sourceCode.sourceAutoAssignedConceptIds.size() > 1 && concepts.size() > 0) { - codeMapping.mappingStatus = MappingStatus.AUTO_MAPPED; + codeMapping.setMappingStatus(MappingStatus.AUTO_MAPPED); } + codeMapping.setEquivalence(CodeMapping.Equivalence.UNREVIEWED); synchronized (globalMappingList) { globalMappingList.add(codeMapping); progressBar.setValue(Math.round(100 * globalMappingList.size() / sourceCodes.size())); diff --git a/src/org/ohdsi/usagi/ui/Mapping.java b/src/org/ohdsi/usagi/ui/Mapping.java index d2c6305..d760ce0 100644 --- a/src/org/ohdsi/usagi/ui/Mapping.java +++ b/src/org/ohdsi/usagi/ui/Mapping.java @@ -37,11 +37,12 @@ public void loadFromFile(String filename) { try { for (CodeMapping codeMapping : new ReadCodeMappingsFromFile(filename)) { add(codeMapping); - if (codeMapping.mappingStatus == CodeMapping.MappingStatus.INVALID_TARGET) { + if (codeMapping.getMappingStatus() == CodeMapping.MappingStatus.INVALID_TARGET) { nInvalidTargets += 1; } } } catch (Exception e) { + e.printStackTrace(); JOptionPane.showMessageDialog( Global.frame, "Invalid File Format: '" + e.getMessage() + "'", @@ -84,7 +85,7 @@ public void saveToFile(String filename) { public List getSourceCodes() { List sourceCodes = new ArrayList(size()); for (CodeMapping codeMapping : this) - sourceCodes.add(codeMapping.sourceCode); + sourceCodes.add(codeMapping.getSourceCode()); return sourceCodes; } } diff --git a/src/org/ohdsi/usagi/ui/MappingDetailPanel.java b/src/org/ohdsi/usagi/ui/MappingDetailPanel.java index 508d46a..c42b06f 100644 --- a/src/org/ohdsi/usagi/ui/MappingDetailPanel.java +++ b/src/org/ohdsi/usagi/ui/MappingDetailPanel.java @@ -1,581 +1,657 @@ -/******************************************************************************* - * Copyright 2019 Observational Health Data Sciences and Informatics - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - ******************************************************************************/ -package org.ohdsi.usagi.ui; - -import java.awt.Color; -import java.awt.Component; -import java.awt.Dimension; -import java.awt.GridBagConstraints; -import java.awt.GridBagLayout; -import java.awt.Rectangle; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.List; -import java.util.Set; -import java.util.Timer; -import java.util.TimerTask; -import java.util.Vector; - -import javax.swing.Action; -import javax.swing.BorderFactory; -import javax.swing.Box; -import javax.swing.BoxLayout; -import javax.swing.ButtonGroup; -import javax.swing.JButton; -import javax.swing.JLabel; -import javax.swing.JPanel; -import javax.swing.JRadioButton; -import javax.swing.JScrollPane; -import javax.swing.JTable; -import javax.swing.JTextField; -import javax.swing.ListSelectionModel; -import javax.swing.event.DocumentEvent; -import javax.swing.event.DocumentListener; -import javax.swing.table.AbstractTableModel; -import javax.swing.table.TableRowSorter; - -import org.ohdsi.usagi.CodeMapping; -import org.ohdsi.usagi.CodeMapping.MappingStatus; -import org.ohdsi.usagi.Concept; -import org.ohdsi.usagi.UsagiSearchEngine.ScoredConcept; - -import static org.ohdsi.usagi.ui.DataChangeEvent.*; - -public class MappingDetailPanel extends JPanel implements CodeSelectedListener, FilterChangeListener { - - private static final long serialVersionUID = 2127318722005512776L; - private UsagiTable sourceCodeTable; - private SourceCodeTableModel sourceCodeTableModel; - private UsagiTable targetConceptTable; - private ConceptTableModel targetConceptTableModel; - private UsagiTable searchTable; - private TableRowSorter sorter; - private ConceptTableModel searchTableModel; - private JButton approveButton; - private JTextField commentField; - private JButton removeButton; - private JButton replaceButton; - private JButton addButton; - private JRadioButton autoQueryButton; - private JRadioButton manualQueryButton; - private JTextField manualQueryField; - private CodeMapping codeMapping; - private List codeMappingsFromMulti; - private FilterPanel filterPanel; - private Timer timer; - - public MappingDetailPanel() { - super(); - setLayout(new BoxLayout(this, BoxLayout.Y_AXIS)); - add(createSourceCodePanel()); - add(createTargetConceptsPanel()); - add(createSearchPanel()); - add(createApprovePanel()); - codeMappingsFromMulti = new ArrayList<>(); - } - - private Component createSearchPanel() { - JPanel panel = new JPanel(); - panel.setBorder(BorderFactory.createTitledBorder("Search")); - panel.setLayout(new GridBagLayout()); - GridBagConstraints c = new GridBagConstraints(); - c.fill = GridBagConstraints.BOTH; - - c.gridx = 0; - c.gridy = 0; - c.weightx = 1; - c.weighty = 0.1; - panel.add(createQueryPanel(), c); - - c.gridx = 1; - c.gridy = 0; - c.weightx = 0.1; - c.weighty = 0.1; - filterPanel = new FilterPanel(); - filterPanel.addListener(this); - panel.add(filterPanel, c); - - c.gridx = 0; - c.gridy = 1; - c.weightx = 1; - c.weighty = 1; - c.gridwidth = 2; - panel.add(createSearchResultsPanel(), c); - return panel; - } - - private Component createQueryPanel() { - JPanel panel = new JPanel(); - panel.setBorder(BorderFactory.createTitledBorder("Query")); - panel.setLayout(new GridBagLayout()); - GridBagConstraints c = new GridBagConstraints(); - c.fill = GridBagConstraints.BOTH; - c.anchor = GridBagConstraints.WEST; - c.gridx = 0; - c.gridy = 0; - c.weightx = 0.1; - c.gridwidth = 2; - - autoQueryButton = new JRadioButton("Use source term as query", true); - autoQueryButton.addActionListener(new ActionListener() { - - @Override - public void actionPerformed(ActionEvent arg0) { - doSearch(); - } - }); - panel.add(autoQueryButton, c); - - c.gridx = 0; - c.gridy = 1; - c.weightx = 0.1; - c.gridwidth = 1; - manualQueryButton = new JRadioButton("Query:", false); - manualQueryButton.addActionListener(new ActionListener() { - - @Override - public void actionPerformed(ActionEvent arg0) { - doSearch(); - } - }); - panel.add(manualQueryButton, c); - - ButtonGroup buttonGroup = new ButtonGroup(); - buttonGroup.add(autoQueryButton); - buttonGroup.add(manualQueryButton); - - c.gridx = 1; - c.gridy = 1; - c.weightx = 1; - c.gridwidth = 1; - manualQueryField = new JTextField(""); - // manualQueryField.setPreferredSize(new Dimension(200, 5)); - manualQueryField.getDocument().addDocumentListener(new DocumentListener() { - - @Override - public void removeUpdate(DocumentEvent arg0) { - manualQueryButton.setSelected(true); - doSearch(); - } - - @Override - public void insertUpdate(DocumentEvent arg0) { - manualQueryButton.setSelected(true); - doSearch(); - } - - @Override - public void changedUpdate(DocumentEvent arg0) { - manualQueryButton.setSelected(true); - doSearch(); - } - }); - panel.add(manualQueryField, c); - return panel; - } - - private Component createSearchResultsPanel() { - JPanel panel = new JPanel(); - panel.setBorder(BorderFactory.createTitledBorder("Results")); - panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); - searchTableModel = new ConceptTableModel(true); - searchTable = new UsagiTable(searchTableModel); - sorter = new TableRowSorter(searchTableModel); - searchTable.setRowSorter(sorter); - searchTable.setPreferredScrollableViewportSize(new Dimension(100, 100)); - searchTable.setAutoResizeMode(JTable.AUTO_RESIZE_ALL_COLUMNS); - searchTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); - searchTable.getSelectionModel().addListSelectionListener(event -> { - int viewRow = searchTable.getSelectedRow(); - if (viewRow == -1) { - addButton.setEnabled(false); - replaceButton.setEnabled(false); - } else { - addButton.setEnabled(true); - replaceButton.setEnabled(true); - int modelRow = searchTable.convertRowIndexToModel(viewRow); - Global.conceptInfoAction.setEnabled(true); - Global.conceptInformationDialog.setConcept(searchTableModel.getConcept(modelRow)); - Global.athenaAction.setEnabled(true); - Global.athenaAction.setConcept(searchTableModel.getConcept(modelRow)); - Global.googleSearchAction.setEnabled(false); - } - }); - // searchTable.hideColumn("Synonym"); - searchTable.hideColumn("Valid start date"); - searchTable.hideColumn("Valid end date"); - searchTable.hideColumn("Invalid reason"); - panel.add(new JScrollPane(searchTable)); - - JPanel buttonPanel = new JPanel(); - buttonPanel.setLayout(new BoxLayout(buttonPanel, BoxLayout.X_AXIS)); - buttonPanel.add(Box.createHorizontalGlue()); - - replaceButton = new JButton("Replace concept"); - replaceButton.setToolTipText("Replace selected concept"); - replaceButton.addActionListener(new ActionListener() { - public void actionPerformed(ActionEvent e) { - int viewRow = searchTable.getSelectedRow(); - int modelRow = searchTable.convertRowIndexToModel(viewRow); - replaceConcepts(searchTableModel.getConcept(modelRow)); - } - - }); - replaceButton.setEnabled(false); - buttonPanel.add(replaceButton); - addButton = new JButton("Add concept"); - addButton.setToolTipText("Add selected concept"); - addButton.addActionListener(new ActionListener() { - public void actionPerformed(ActionEvent e) { - int viewRow = searchTable.getSelectedRow(); - int modelRow = searchTable.convertRowIndexToModel(viewRow); - addConcept(searchTableModel.getConcept(modelRow)); - } - - }); - addButton.setEnabled(false); - buttonPanel.add(addButton); - panel.add(buttonPanel); - - return panel; - } - - private Component createApprovePanel() { - JPanel panel = new JPanel(); - panel.setLayout(new BoxLayout(panel, BoxLayout.X_AXIS)); - panel.add(new JLabel("Comment:")); - - panel.add(Box.createHorizontalStrut(5)); - - commentField = new JTextField(); - commentField.setMaximumSize(new Dimension(Integer.MAX_VALUE, commentField.getPreferredSize().height)); - commentField.getDocument().addDocumentListener(new DocumentListener() { - - @Override - public void removeUpdate(DocumentEvent arg0) { - codeMapping.comment = commentField.getText(); - Global.mapping.fireDataChanged(SIMPLE_UPDATE_EVENT); - } - - @Override - public void insertUpdate(DocumentEvent arg0) { - codeMapping.comment = commentField.getText(); - Global.mapping.fireDataChanged(SIMPLE_UPDATE_EVENT); - } - - @Override - public void changedUpdate(DocumentEvent arg0) { - codeMapping.comment = commentField.getText(); - Global.mapping.fireDataChanged(SIMPLE_UPDATE_EVENT); - } - }); - commentField.setToolTipText("Comments about the code mapping can be written here"); - panel.add(commentField); - - panel.add(Box.createHorizontalStrut(5)); - - approveButton = new JButton(Global.approveAction); - approveButton.setBackground(new Color(151, 220, 141)); - panel.add(approveButton); - return panel; - } - - private JPanel createSourceCodePanel() { - JPanel panel = new JPanel(); - panel.setBorder(BorderFactory.createTitledBorder("Source code")); - panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); - sourceCodeTableModel = new SourceCodeTableModel(); - sourceCodeTable = new UsagiTable(sourceCodeTableModel); - sourceCodeTable.setPreferredScrollableViewportSize(new Dimension(500, 35)); - sourceCodeTable.setAutoResizeMode(JTable.AUTO_RESIZE_ALL_COLUMNS); - sourceCodeTable.setRowSelectionAllowed(false); - sourceCodeTable.setCellSelectionEnabled(false); - JScrollPane pane = new JScrollPane(sourceCodeTable); - pane.setBorder(BorderFactory.createEmptyBorder()); - pane.setMinimumSize(new Dimension(500, 40)); - pane.setPreferredSize(new Dimension(500, 40)); - panel.add(pane); - return panel; - } - - private JPanel createTargetConceptsPanel() { - JPanel panel = new JPanel(); - panel.setBorder(BorderFactory.createTitledBorder("Target concepts")); - panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); - targetConceptTableModel = new ConceptTableModel(false); - targetConceptTable = new UsagiTable(targetConceptTableModel); - targetConceptTable.setPreferredScrollableViewportSize(new Dimension(500, 45)); - targetConceptTable.setAutoResizeMode(JTable.AUTO_RESIZE_ALL_COLUMNS); - targetConceptTable.setRowSelectionAllowed(true); - targetConceptTable.getSelectionModel().addListSelectionListener(event -> { - int viewRow = targetConceptTable.getSelectedRow(); - if (viewRow == -1) { - removeButton.setEnabled(false); - } else { - removeButton.setEnabled(true); - int modelRow = targetConceptTable.convertRowIndexToModel(viewRow); - Global.conceptInfoAction.setEnabled(true); - Global.conceptInformationDialog.setConcept(targetConceptTableModel.getConcept(modelRow)); - Global.athenaAction.setEnabled(true); - Global.athenaAction.setConcept(targetConceptTableModel.getConcept(modelRow)); - Global.googleSearchAction.setEnabled(false); - } - }); - targetConceptTable.hideColumn("Valid start date"); - targetConceptTable.hideColumn("Valid end date"); - targetConceptTable.hideColumn("Invalid reason"); - - JScrollPane pane = new JScrollPane(targetConceptTable); - pane.setBorder(BorderFactory.createEmptyBorder()); - pane.setMinimumSize(new Dimension(500, 50)); - pane.setPreferredSize(new Dimension(500, 50)); - panel.add(pane); - - JPanel buttonPanel = new JPanel(); - buttonPanel.setLayout(new BoxLayout(buttonPanel, BoxLayout.X_AXIS)); - buttonPanel.add(Box.createHorizontalGlue()); - - removeButton = new JButton("Remove concept"); - removeButton.setToolTipText("Add selected concept"); - removeButton.addActionListener(new ActionListener() { - public void actionPerformed(ActionEvent e) { - remove(); - } - - }); - removeButton.setEnabled(false); - buttonPanel.add(removeButton); - panel.add(buttonPanel); - return panel; - } - - @Override - public void codeSelected(CodeMapping codeMapping) { - this.codeMapping = codeMapping; - setApproveButton(); - sourceCodeTableModel.setMapping(codeMapping); - targetConceptTableModel.setConcepts(codeMapping.targetConcepts); - commentField.setText(codeMapping.comment); - doSearch(); - } - - @Override - public void addCodeMultiSelected(CodeMapping codeMapping) { - this.codeMappingsFromMulti.add(codeMapping); - } - - @Override - public void clearCodeMultiSelected() { - this.codeMappingsFromMulti = new ArrayList<>(); - } - - public void approve() { - if (codeMapping.mappingStatus != CodeMapping.MappingStatus.APPROVED) { - codeMapping.mappingStatus = CodeMapping.MappingStatus.APPROVED; - Global.mapping.fireDataChanged(APPROVE_EVENT); - } else { - codeMapping.mappingStatus = CodeMapping.MappingStatus.UNCHECKED; - Global.mapping.fireDataChanged(SIMPLE_UPDATE_EVENT); - setApproveButton(); - } - } - - private void setApproveButton() { - if (codeMapping.mappingStatus == MappingStatus.APPROVED) { - Global.approveAction.putValue(Action.NAME, "Unapprove"); - Global.approveAction.putValue(Action.SHORT_DESCRIPTION, "Unapprove this mapping"); - approveButton.setBackground(new Color(220, 151, 141)); - } else { - Global.approveAction.putValue(Action.NAME, "Approve"); - Global.approveAction.putValue(Action.SHORT_DESCRIPTION, "Approve this mapping"); - approveButton.setBackground(new Color(151, 220, 141)); - } - } - - public void addConcept(Concept concept) { - codeMapping.targetConcepts.add(concept); - for (CodeMapping codeMappingMulti : codeMappingsFromMulti) { - codeMappingMulti.targetConcepts.add(concept); - } - targetConceptTableModel.fireTableDataChanged(); - - if (codeMappingsFromMulti.size() > 0) { - Global.mapping.fireDataChanged(MULTI_UPDATE_EVENT); - } else { - Global.mapping.fireDataChanged(SIMPLE_UPDATE_EVENT); - } - } - - public void replaceConcepts(Concept concept) { - codeMapping.targetConcepts.clear(); - for (CodeMapping codeMappingMulti : codeMappingsFromMulti) { - codeMappingMulti.targetConcepts.clear(); - } - addConcept(concept); - } - - private void remove() { - List rows = new ArrayList(); - for (int row : targetConceptTable.getSelectedRows()) - rows.add(targetConceptTable.convertRowIndexToModel(row)); - - Collections.sort(rows, new Comparator() { - - @Override - public int compare(Integer o1, Integer o2) { - return o2.compareTo(o1); - } - }); - for (int row : rows) - codeMapping.targetConcepts.remove(row); - - targetConceptTableModel.fireTableDataChanged(); - Global.mapping.fireDataChanged(SIMPLE_UPDATE_EVENT); - } - - private class SearchTask extends TimerTask { - - @Override - public void run() { - Set filterConceptIds = null; - if (filterPanel.getFilterByAuto()) - filterConceptIds = codeMapping.sourceCode.sourceAutoAssignedConceptIds; - - boolean filterStandard = filterPanel.getFilterStandard(); - Vector filterConceptClasses = null; - if (filterPanel.getFilterByConceptClasses()) - filterConceptClasses = filterPanel.getConceptClass(); - Vector filterVocabularies = null; - if (filterPanel.getFilterByVocabularies()) - filterVocabularies = filterPanel.getVocabulary(); - Vector filterDomains = null; - if (filterPanel.getFilterByDomains()) - filterDomains = filterPanel.getDomain(); - String query = manualQueryField.getText(); - if (autoQueryButton.isSelected()) - query = codeMapping.sourceCode.sourceName; - boolean includeSourceConcepts = filterPanel.getIncludeSourceTerms(); - - if (Global.usagiSearchEngine.isOpenForSearching()) { - List searchResults = Global.usagiSearchEngine.search(query, true, filterConceptIds, filterDomains, filterConceptClasses, - filterVocabularies, filterStandard, includeSourceConcepts); - - searchTableModel.setScoredConcepts(searchResults); - searchTable.scrollRectToVisible(new Rectangle(searchTable.getCellRect(0, 0, true))); - } - Global.statusBar.setSearching(false); - } - } - - public void doSearch() { - Global.statusBar.setSearching(true); - if (timer != null) - timer.cancel(); - timer = new Timer(); - timer.schedule(new SearchTask(), 500); - } - - class SourceCodeTableModel extends AbstractTableModel { - private static final long serialVersionUID = 169286268154988911L; - - private String[] defaultColumnNames = { "Source code", "Source term", "Frequency" }; - private String[] columnNames = defaultColumnNames; - private CodeMapping codeMapping; - private int addInfoColCount = 0; - private int ADD_INFO_START_COL = 3; - - public int getColumnCount() { - return columnNames.length; - } - - public void setMapping(CodeMapping codeMapping) { - this.codeMapping = codeMapping; - - columnNames = defaultColumnNames; - addInfoColCount = codeMapping.sourceCode.sourceAdditionalInfo.size(); - columnNames = new String[defaultColumnNames.length + addInfoColCount]; - for (int i = 0; i < ADD_INFO_START_COL; i++) - columnNames[i] = defaultColumnNames[i]; - - for (int i = 0; i < addInfoColCount; i++) - columnNames[i + ADD_INFO_START_COL] = codeMapping.sourceCode.sourceAdditionalInfo.get(i).getItem1(); - - fireTableStructureChanged(); - } - - public int getRowCount() { - return 1; - } - - public String getColumnName(int col) { - return columnNames[col]; - } - - public Object getValueAt(int row, int col) { - if (codeMapping == null) - return ""; - if (col >= ADD_INFO_START_COL) { - return codeMapping.sourceCode.sourceAdditionalInfo.get(col - ADD_INFO_START_COL).getItem2(); - } else { - switch (col) { - case 0: - return codeMapping.sourceCode.sourceCode; - case 1: - return codeMapping.sourceCode.sourceName; - case 2: - return codeMapping.sourceCode.sourceFrequency; - default: - return ""; - } - } - - } - - public Class getColumnClass(int col) { - if (col >= ADD_INFO_START_COL) { - return String.class; - } else { - switch (col) { - case 0: - return String.class; - case 1: - return String.class; - case 2: - return Integer.class; - default: - return String.class; - } - } - } - - public boolean isCellEditable(int row, int col) { - return true; - } - - public void setValueAt(Object value, int row, int col) { - - } - } - - @Override - public void filterChanged() { - doSearch(); - } - -} +/******************************************************************************* + * Copyright 2019 Observational Health Data Sciences and Informatics + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + ******************************************************************************/ +package org.ohdsi.usagi.ui; + +import java.awt.Color; +import java.awt.Component; +import java.awt.Dimension; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.Rectangle; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.util.*; +import java.util.Timer; + +import javax.swing.*; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; +import javax.swing.table.AbstractTableModel; +import javax.swing.table.TableRowSorter; + +import org.ohdsi.usagi.CodeMapping; +import org.ohdsi.usagi.CodeMapping.MappingStatus; +import org.ohdsi.usagi.Concept; +import org.ohdsi.usagi.MappingTarget; +import org.ohdsi.usagi.UsagiSearchEngine.ScoredConcept; + +import static org.ohdsi.usagi.ui.DataChangeEvent.*; + +public class MappingDetailPanel extends JPanel implements CodeSelectedListener, FilterChangeListener { + + private static final long serialVersionUID = 2127318722005512776L; + private UsagiTable sourceCodeTable; + private SourceCodeTableModel sourceCodeTableModel; + private UsagiTable targetConceptTable; + private TargetConceptTableModel targetConceptTableModel; + private UsagiTable searchTable; + private TableRowSorter sorter; + private ConceptTableModel searchTableModel; + private JButton approveButton; + private JButton flagButton; + private JComboBox equivalenceOptionChooser; + private JTextField commentField; + private JButton removeButton; + private JComboBox typesChooser; + private JButton replaceButton; + private List addButtons; + private JRadioButton autoQueryCodeButton; + private JRadioButton manualQueryButton; + private JTextField manualQueryField; + private CodeMapping codeMapping; + private List codeMappingsFromMulti; + private FilterPanel filterPanel; + private Timer timer; + + public MappingDetailPanel() { + super(); + setLayout(new BoxLayout(this, BoxLayout.Y_AXIS)); + add(createSourceCodePanel()); + add(createTargetConceptsPanel()); + add(createSearchPanel()); + add(createApprovePanel()); + codeMappingsFromMulti = new ArrayList<>(); + } + + private Component createSearchPanel() { + JPanel panel = new JPanel(); + panel.setBorder(BorderFactory.createTitledBorder("Search")); + panel.setLayout(new GridBagLayout()); + GridBagConstraints c = new GridBagConstraints(); + c.fill = GridBagConstraints.BOTH; + + c.gridx = 0; + c.gridy = 0; + c.weightx = 1; + c.weighty = 0.1; + panel.add(createQueryPanel(), c); + + c.gridx = 1; + c.gridy = 0; + c.weightx = 0.1; + c.weighty = 0.1; + filterPanel = new FilterPanel(); + filterPanel.addListener(this); + panel.add(filterPanel, c); + + c.gridx = 0; + c.gridy = 1; + c.weightx = 1; + c.weighty = 1; + c.gridwidth = 2; + panel.add(createSearchResultsPanel(), c); + return panel; + } + + private Component createQueryPanel() { + JPanel panel = new JPanel(); + panel.setBorder(BorderFactory.createTitledBorder("Query")); + panel.setLayout(new GridBagLayout()); + GridBagConstraints c = new GridBagConstraints(); + c.fill = GridBagConstraints.BOTH; + c.anchor = GridBagConstraints.WEST; + c.gridx = GridBagConstraints.RELATIVE; + c.gridy = 0; + c.weightx = 0.1; + c.gridwidth = 2; + + autoQueryCodeButton = new JRadioButton("Use source term", true); + autoQueryCodeButton.addActionListener(x -> doSearch()); + panel.add(autoQueryCodeButton, c); + + c.gridx = 0; + c.gridy = 1; + c.weightx = 0.1; + c.gridwidth = 1; + manualQueryButton = new JRadioButton("Query:", false); + manualQueryButton.addActionListener(x -> doSearch()); + panel.add(manualQueryButton, c); + + ButtonGroup buttonGroup = new ButtonGroup(); + buttonGroup.add(autoQueryCodeButton); + buttonGroup.add(manualQueryButton); + + c.gridx = 1; + c.gridy = 1; + c.weightx = 1; + c.gridwidth = GridBagConstraints.REMAINDER; + manualQueryField = new JTextField(""); + // manualQueryField.setPreferredSize(new Dimension(200, 5)); + manualQueryField.getDocument().addDocumentListener(new DocumentListener() { + + @Override + public void removeUpdate(DocumentEvent arg0) { + manualQueryButton.setSelected(true); + doSearch(); + } + + @Override + public void insertUpdate(DocumentEvent arg0) { + manualQueryButton.setSelected(true); + doSearch(); + } + + @Override + public void changedUpdate(DocumentEvent arg0) { + manualQueryButton.setSelected(true); + doSearch(); + } + }); + panel.add(manualQueryField, c); + return panel; + } + + private Component createSearchResultsPanel() { + JPanel panel = new JPanel(); + panel.setBorder(BorderFactory.createTitledBorder("Results")); + panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); + searchTableModel = new ConceptTableModel(true); + searchTable = new UsagiTable(searchTableModel); + sorter = new TableRowSorter(searchTableModel); + searchTable.setRowSorter(sorter); + searchTable.setPreferredScrollableViewportSize(new Dimension(100, 100)); + searchTable.setAutoResizeMode(JTable.AUTO_RESIZE_ALL_COLUMNS); + searchTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + searchTable.getSelectionModel().addListSelectionListener(event -> { + int viewRow = searchTable.getSelectedRow(); + // Don't enable the buttons if no row selected or status is approved + if (viewRow == -1 || codeMapping.getMappingStatus() == MappingStatus.APPROVED) { + addButtons.forEach(x -> x.setEnabled(false)); + replaceButton.setEnabled(false); + } else { + addButtons.forEach(x -> x.setEnabled(true)); + replaceButton.setEnabled(true); + int modelRow = searchTable.convertRowIndexToModel(viewRow); + Global.conceptInfoAction.setEnabled(true); + Global.conceptInformationDialog.setConcept(searchTableModel.getConcept(modelRow)); + Global.athenaAction.setEnabled(true); + Global.athenaAction.setConcept(searchTableModel.getConcept(modelRow)); + Global.googleSearchAction.setEnabled(false); + } + }); + // searchTable.hideColumn("Synonym"); + searchTable.hideColumn("Valid start date"); + searchTable.hideColumn("Valid end date"); + searchTable.hideColumn("Invalid reason"); + panel.add(new JScrollPane(searchTable)); + + JPanel buttonPanel = new JPanel(); + buttonPanel.setLayout(new BoxLayout(buttonPanel, BoxLayout.X_AXIS)); + buttonPanel.add(Box.createHorizontalGlue()); + + replaceButton = new JButton("Replace concept"); + replaceButton.setToolTipText("Replace selected concept"); + replaceButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + int viewRow = searchTable.getSelectedRow(); + int modelRow = searchTable.convertRowIndexToModel(viewRow); + replaceConcepts(searchTableModel.getConcept(modelRow)); + } + + }); + replaceButton.setEnabled(false); + buttonPanel.add(replaceButton); + + JButton button; + addButtons = new ArrayList<>(); + for (MappingTarget.Type mappingType : MappingTarget.Type.values()) { + if (mappingType.equals(MappingTarget.Type.MAPS_TO)) { + button = new JButton("Add concept"); + } else { + button = new JButton(String.format("Add %s", mappingType)); + } + button.setToolTipText(String.format("Add selected concept as %s", mappingType)); + button.addActionListener(e -> { + int viewRow = searchTable.getSelectedRow(); + int modelRow = searchTable.convertRowIndexToModel(viewRow); + addConcept(searchTableModel.getConcept(modelRow), mappingType); + }); + button.setEnabled(false); + addButtons.add(button); + buttonPanel.add(button); + } + + panel.add(buttonPanel); + + return panel; + } + + private Component createApprovePanel() { + JPanel panel = new JPanel(); + panel.setLayout(new BoxLayout(panel, BoxLayout.X_AXIS)); + + panel.add(new JLabel("Comment:")); + + panel.add(Box.createHorizontalStrut(5)); + + commentField = new JTextField(); + commentField.setMaximumSize(new Dimension(Integer.MAX_VALUE, commentField.getPreferredSize().height)); + commentField.getDocument().addDocumentListener(new DocumentListener() { + + @Override + public void removeUpdate(DocumentEvent arg0) { + codeMapping.setComment(commentField.getText()); + Global.mapping.fireDataChanged(SIMPLE_UPDATE_EVENT); + } + + @Override + public void insertUpdate(DocumentEvent arg0) { + codeMapping.setComment(commentField.getText()); + Global.mapping.fireDataChanged(SIMPLE_UPDATE_EVENT); + } + + @Override + public void changedUpdate(DocumentEvent arg0) { + codeMapping.setComment(commentField.getText()); + Global.mapping.fireDataChanged(SIMPLE_UPDATE_EVENT); + } + }); + commentField.setToolTipText("Comments about the code mapping can be written here"); + panel.add(commentField); + + panel.add(Box.createHorizontalStrut(5)); + + flagButton = new JButton(Global.flagAction); + flagButton.setBackground(new Color(151, 220, 141)); + panel.add(flagButton); + + equivalenceOptionChooser = new JComboBox<>(CodeMapping.Equivalence.values()); + equivalenceOptionChooser.setToolTipText("Choose mapping equivalence"); + equivalenceOptionChooser.setMaximumSize(equivalenceOptionChooser.getPreferredSize()); + panel.add(equivalenceOptionChooser); + + approveButton = new JButton(Global.approveAction); + approveButton.setBackground(new Color(151, 220, 141)); + panel.add(approveButton); + + return panel; + } + + private JPanel createSourceCodePanel() { + JPanel panel = new JPanel(); + panel.setBorder(BorderFactory.createTitledBorder("Source code")); + panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); + sourceCodeTableModel = new SourceCodeTableModel(); + sourceCodeTable = new UsagiTable(sourceCodeTableModel); + sourceCodeTable.setPreferredScrollableViewportSize(new Dimension(500, 35)); + sourceCodeTable.setAutoResizeMode(JTable.AUTO_RESIZE_ALL_COLUMNS); + sourceCodeTable.setRowSelectionAllowed(false); + sourceCodeTable.setCellSelectionEnabled(false); + JScrollPane pane = new JScrollPane(sourceCodeTable); + pane.setBorder(BorderFactory.createEmptyBorder()); + pane.setMinimumSize(new Dimension(500, 40)); + pane.setPreferredSize(new Dimension(500, 40)); + panel.add(pane); + + return panel; + } + + private JPanel createTargetConceptsPanel() { + JPanel panel = new JPanel(); + panel.setBorder(BorderFactory.createTitledBorder("Target concepts")); + panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); + targetConceptTableModel = new TargetConceptTableModel(); + targetConceptTable = new UsagiTable(targetConceptTableModel); + targetConceptTable.setPreferredScrollableViewportSize(new Dimension(500, 45)); + targetConceptTable.setAutoResizeMode(JTable.AUTO_RESIZE_ALL_COLUMNS); + targetConceptTable.setRowSelectionAllowed(true); + targetConceptTable.getSelectionModel().addListSelectionListener(event -> { + int viewRow = targetConceptTable.getSelectedRow(); + if (viewRow == -1 || codeMapping.getMappingStatus() == MappingStatus.APPROVED) { + removeButton.setEnabled(false); + typesChooser.setEnabled(false); + } else { + removeButton.setEnabled(true); + typesChooser.setEnabled(true); + int modelRow = targetConceptTable.convertRowIndexToModel(viewRow); + MappingTarget mappingTarget = targetConceptTableModel.getMappingTarget(modelRow); + typesChooser.setSelectedItem(mappingTarget.getMappingType()); + Global.conceptInfoAction.setEnabled(true); + Global.conceptInformationDialog.setConcept(mappingTarget.getConcept()); + Global.athenaAction.setEnabled(true); + Global.athenaAction.setConcept(mappingTarget.getConcept()); + Global.googleSearchAction.setEnabled(false); + } + }); + targetConceptTable.hideColumn("Valid start date"); + targetConceptTable.hideColumn("Valid end date"); + targetConceptTable.hideColumn("Invalid reason"); + + JScrollPane pane = new JScrollPane(targetConceptTable); + pane.setBorder(BorderFactory.createEmptyBorder()); + pane.setMinimumSize(new Dimension(500, 50)); + pane.setPreferredSize(new Dimension(500, 50)); + panel.add(pane); + + JPanel buttonPanel = new JPanel(); + buttonPanel.setLayout(new BoxLayout(buttonPanel, BoxLayout.X_AXIS)); + buttonPanel.add(Box.createHorizontalGlue()); + + typesChooser = new JComboBox<>(MappingTarget.Type.values()); + typesChooser.setToolTipText("Set type of the mapping"); + typesChooser.addActionListener(e -> { + if (((JComboBox)e.getSource()).hasFocus()) + changeTargetType(); + }); + typesChooser.setMaximumSize(typesChooser.getPreferredSize()); + typesChooser.setEnabled(false); + buttonPanel.add(typesChooser); + + removeButton = new JButton("Remove concept"); + removeButton.setToolTipText("Remove selected concept"); + removeButton.addActionListener(e -> remove()); + removeButton.setEnabled(false); + buttonPanel.add(removeButton); + panel.add(buttonPanel); + return panel; + } + + @Override + public void codeSelected(CodeMapping codeMapping) { + this.codeMapping = codeMapping; + toggleStatusButtons(); + sourceCodeTableModel.setMapping(codeMapping); + targetConceptTableModel.setConcepts(codeMapping.getTargetConcepts()); + commentField.setText(codeMapping.getComment()); + doSearch(); + } + + @Override + public void addCodeMultiSelected(CodeMapping codeMapping) { + this.codeMappingsFromMulti.add(codeMapping); + } + + @Override + public void clearCodeMultiSelected() { + this.codeMappingsFromMulti = new ArrayList<>(); + } + + public void approveOrUnapprove() { + if (codeMapping.getMappingStatus() == MappingStatus.APPROVED) { + uncheckSelected(); + toggleStatusButtons(); + } else { + approveSelected(); + } + } + + public void flagOrUnflag() { + if (codeMapping.getMappingStatus() == MappingStatus.FLAGGED) { + uncheckSelected(); + toggleStatusButtons(); + } else { + flagSelected(); + } + } + + public void approveSelected() { + CodeMapping.Equivalence equivalenceToApply = (CodeMapping.Equivalence) equivalenceOptionChooser.getSelectedItem(); + codeMapping.approve(equivalenceToApply); + for (CodeMapping codeMappingMulti : codeMappingsFromMulti) { + codeMappingMulti.approve(equivalenceToApply); + } + if (codeMappingsFromMulti.isEmpty()) { + Global.mapping.fireDataChanged(APPROVE_EVENT); + } else { + Global.mapping.fireDataChanged(MULTI_UPDATE_EVENT); + } + } + + public void flagSelected() { + CodeMapping.Equivalence equivalenceToApply = (CodeMapping.Equivalence) equivalenceOptionChooser.getSelectedItem(); + codeMapping.flag(equivalenceToApply); + for (CodeMapping codeMappingMulti : codeMappingsFromMulti) { + codeMappingMulti.flag(equivalenceToApply); + } + if (codeMappingsFromMulti.isEmpty()) { + Global.mapping.fireDataChanged(APPROVE_EVENT); + } else { + Global.mapping.fireDataChanged(MULTI_UPDATE_EVENT); + } + } + + public void uncheckSelected() { + codeMapping.setUnchecked(); + for (CodeMapping codeMappingMulti : codeMappingsFromMulti) { + codeMappingMulti.setUnchecked(); + } + if (codeMappingsFromMulti.isEmpty()) { + Global.mapping.fireDataChanged(SIMPLE_UPDATE_EVENT); + } else { + Global.mapping.fireDataChanged(MULTI_UPDATE_EVENT); + } + } + + private void toggleStatusButtons() { + Global.approveAction.setToApprove(); + Global.flagAction.setToFlag(); + flagButton.setEnabled(false); + approveButton.setEnabled(false); + equivalenceOptionChooser.setEnabled(false); + + switch(codeMapping.getMappingStatus()) { + case APPROVED: + Global.approveAction.setToUnapprove(); + approveButton.setEnabled(true); + break; + case FLAGGED: + Global.flagAction.setToUnflag(); + flagButton.setEnabled(true); + break; + default: // unchecked, invalid or auto-mapped + flagButton.setEnabled(true); + approveButton.setEnabled(true); + equivalenceOptionChooser.setEnabled(true); + } + } + + public void addConcept(Concept concept) { + codeMapping.getTargetConcepts().add(new MappingTarget(concept, Global.author)); + for (CodeMapping codeMappingMulti : codeMappingsFromMulti) { + codeMappingMulti.getTargetConcepts().add(new MappingTarget(concept, Global.author)); + } + targetConceptTableModel.fireTableDataChanged(); + + if (codeMappingsFromMulti.isEmpty()) { + Global.mapping.fireDataChanged(SIMPLE_UPDATE_EVENT); + } else { + Global.mapping.fireDataChanged(MULTI_UPDATE_EVENT); + } + } + + public void addConcept(Concept concept, MappingTarget.Type mappingType) { + codeMapping.getTargetConcepts().add(new MappingTarget(concept, mappingType, Global.author)); + for (CodeMapping codeMappingMulti : codeMappingsFromMulti) { + codeMappingMulti.getTargetConcepts().add(new MappingTarget(concept, mappingType, Global.author)); + } + targetConceptTableModel.fireTableDataChanged(); + + if (codeMappingsFromMulti.isEmpty()) { + Global.mapping.fireDataChanged(SIMPLE_UPDATE_EVENT); + } else { + Global.mapping.fireDataChanged(MULTI_UPDATE_EVENT); + } + } + + public void replaceConcepts(Concept concept) { + codeMapping.getTargetConcepts().clear(); + for (CodeMapping codeMappingMulti : codeMappingsFromMulti) { + codeMappingMulti.getTargetConcepts().clear(); + } + addConcept(concept); + } + + private void remove() { + Arrays.stream(targetConceptTable.getSelectedRows()) + .map(r -> targetConceptTable.convertRowIndexToModel(r)) + .boxed().sorted(Comparator.reverseOrder()).mapToInt(Integer::intValue) // sorting for array integrity, remove last first. + .forEach(r -> codeMapping.getTargetConcepts().remove(r)); + + targetConceptTableModel.fireTableDataChanged(); + Global.mapping.fireDataChanged(SIMPLE_UPDATE_EVENT); + } + + private void changeTargetType() { + for (int row : targetConceptTable.getSelectedRows()) { + MappingTarget mappingTarget = codeMapping.getTargetConcepts().get(row); + mappingTarget.setMappingType((MappingTarget.Type) typesChooser.getSelectedItem()); + } + + targetConceptTableModel.fireTableDataChanged(); + Global.mapping.fireDataChanged(SIMPLE_UPDATE_EVENT); + } + + private class SearchTask extends TimerTask { + + @Override + public void run() { + Set filterConceptIds = null; + if (filterPanel.getFilterByAuto()) + filterConceptIds = codeMapping.getSourceCode().sourceAutoAssignedConceptIds; + + boolean filterStandard = filterPanel.getFilterStandard(); + Vector filterConceptClasses = null; + if (filterPanel.getFilterByConceptClasses()) + filterConceptClasses = filterPanel.getConceptClass(); + Vector filterVocabularies = null; + if (filterPanel.getFilterByVocabularies()) + filterVocabularies = filterPanel.getVocabulary(); + Vector filterDomains = null; + if (filterPanel.getFilterByDomains()) + filterDomains = filterPanel.getDomain(); + + String query; + if (autoQueryCodeButton.isSelected()) { + query = codeMapping.getSourceCode().sourceName; + } else { + query = manualQueryField.getText(); + } + + boolean includeSourceConcepts = filterPanel.getIncludeSourceTerms(); + + if (Global.usagiSearchEngine.isOpenForSearching()) { + List searchResults = Global.usagiSearchEngine.search(query, true, filterConceptIds, filterDomains, filterConceptClasses, + filterVocabularies, filterStandard, includeSourceConcepts); + + searchTableModel.setScoredConcepts(searchResults); + searchTable.scrollRectToVisible(new Rectangle(searchTable.getCellRect(0, 0, true))); + } + Global.statusBar.setSearching(false); + } + } + + public void doSearch() { + Global.statusBar.setSearching(true); + if (timer != null) + timer.cancel(); + timer = new Timer(); + timer.schedule(new SearchTask(), 500); + } + + class SourceCodeTableModel extends AbstractTableModel { + private static final long serialVersionUID = 169286268154988911L; + + private String[] defaultColumnNames = { "Source code", "Source term", "Frequency" }; + private String[] columnNames = defaultColumnNames; + private CodeMapping codeMapping; + private int addInfoColCount = 0; + private int ADD_INFO_START_COL = 3; + + public int getColumnCount() { + return columnNames.length; + } + + public void setMapping(CodeMapping codeMapping) { + this.codeMapping = codeMapping; + + columnNames = defaultColumnNames; + addInfoColCount = codeMapping.getSourceCode().sourceAdditionalInfo.size(); + columnNames = new String[defaultColumnNames.length + addInfoColCount]; + for (int i = 0; i < ADD_INFO_START_COL; i++) + columnNames[i] = defaultColumnNames[i]; + + for (int i = 0; i < addInfoColCount; i++) + columnNames[i + ADD_INFO_START_COL] = codeMapping.getSourceCode().sourceAdditionalInfo.get(i).getItem1(); + + fireTableStructureChanged(); + } + + public int getRowCount() { + return 1; + } + + public String getColumnName(int col) { + return columnNames[col]; + } + + public Object getValueAt(int row, int col) { + if (codeMapping == null) + return ""; + if (col >= ADD_INFO_START_COL) { + return codeMapping.getSourceCode().sourceAdditionalInfo.get(col - ADD_INFO_START_COL).getItem2(); + } else { + switch (col) { + case 0: + return codeMapping.getSourceCode().sourceCode; + case 1: + return codeMapping.getSourceCode().sourceName; + case 2: + return codeMapping.getSourceCode().sourceFrequency; + default: + return ""; + } + } + + } + + public Class getColumnClass(int col) { + if (col >= ADD_INFO_START_COL) { + return String.class; + } else { + switch (col) { + case 2: + return Integer.class; + default: + return String.class; + } + } + } + + public boolean isCellEditable(int row, int col) { + return true; + } + + public void setValueAt(Object value, int row, int col) { + + } + } + + @Override + public void filterChanged() { + doSearch(); + } + +} diff --git a/src/org/ohdsi/usagi/ui/MappingTablePanel.java b/src/org/ohdsi/usagi/ui/MappingTablePanel.java index 79230b9..d69c331 100644 --- a/src/org/ohdsi/usagi/ui/MappingTablePanel.java +++ b/src/org/ohdsi/usagi/ui/MappingTablePanel.java @@ -18,7 +18,11 @@ import java.awt.Dimension; import java.awt.Rectangle; import java.util.ArrayList; +import java.util.Collections; import java.util.List; +import java.util.concurrent.ThreadLocalRandom; +import java.util.stream.Collectors; +import java.util.stream.IntStream; import javax.swing.BoxLayout; import javax.swing.JPanel; @@ -27,6 +31,7 @@ import javax.swing.table.AbstractTableModel; import javax.swing.table.TableRowSorter; +import org.apache.xmlbeans.impl.xb.ltgfmt.Code; import org.ohdsi.usagi.CodeMapping; import org.ohdsi.usagi.CodeMapping.MappingStatus; import org.ohdsi.usagi.Concept; @@ -61,13 +66,12 @@ public MappingTablePanel() { } Global.googleSearchAction.setEnabled(true); - Global.googleSearchAction.setSourceTerm(tableModel.getCodeMapping(primaryModelRow).sourceCode.sourceName); + Global.googleSearchAction.setSourceTerm(tableModel.getCodeMapping(primaryModelRow).getSourceCode().sourceName); Global.approveAction.setEnabled(true); - Global.approveAllAction.setEnabled(true); - Global.clearAllAction.setEnabled(true); - if (tableModel.getCodeMapping(primaryModelRow).targetConcepts.size() > 0) { - Concept firstConcept = tableModel.getCodeMapping(primaryModelRow).targetConcepts.get(0); + Global.clearSelectedAction.setEnabled(true); + if (tableModel.getCodeMapping(primaryModelRow).getTargetConcepts().size() > 0) { + Concept firstConcept = tableModel.getCodeMapping(primaryModelRow).getTargetConcepts().get(0).getConcept(); Global.conceptInfoAction.setEnabled(true); Global.conceptInformationDialog.setConcept(firstConcept); Global.athenaAction.setEnabled(true); @@ -84,9 +88,8 @@ public MappingTablePanel() { } } } else { - Global.approveAllAction.setEnabled(false); Global.approveAction.setEnabled(false); - Global.clearAllAction.setEnabled(false); + Global.clearSelectedAction.setEnabled(false); } } }); @@ -107,10 +110,11 @@ class CodeMapTableModel extends AbstractTableModel { private String[] defaultColumnNames = { "Status", "Source code", "Source term", "Frequency", "Match score", "Concept ID", "Concept name", "Domain", "Concept class", "Vocabulary", "Concept code", "Valid start date", "Valid end date", "Invalid reason", "Standard concept", "Parents", - "Children", "Comment" }; + "Children", "Assigned To", "Equivalence", "Comment", "Status Provenance" }; private String[] columnNames = defaultColumnNames; private int addInfoColCount = 0; - private int ADD_INFO_START_COL = 4; + private final int ADD_INFO_START_COL = 4; + private static final int ASSIGNED_REVIEWER_COL = 17; // special meaning, as public int getColumnCount() { return columnNames.length; @@ -125,13 +129,13 @@ public void restructure() { addInfoColCount = 0; if (Global.mapping.size() != 0) { CodeMapping codeMapping = Global.mapping.get(0); - addInfoColCount = codeMapping.sourceCode.sourceAdditionalInfo.size(); + addInfoColCount = codeMapping.getSourceCode().sourceAdditionalInfo.size(); columnNames = new String[defaultColumnNames.length + addInfoColCount]; for (int i = 0; i < ADD_INFO_START_COL; i++) columnNames[i] = defaultColumnNames[i]; for (int i = 0; i < addInfoColCount; i++) - columnNames[i + ADD_INFO_START_COL] = codeMapping.sourceCode.sourceAdditionalInfo.get(i).getItem1(); + columnNames[i + ADD_INFO_START_COL] = codeMapping.getSourceCode().sourceAdditionalInfo.get(i).getItem1(); for (int i = ADD_INFO_START_COL; i < defaultColumnNames.length; i++) columnNames[i + addInfoColCount] = defaultColumnNames[i]; @@ -152,27 +156,25 @@ public Object getValueAt(int row, int col) { CodeMapping codeMapping = Global.mapping.get(row); if (col >= ADD_INFO_START_COL && col < ADD_INFO_START_COL + addInfoColCount) { - return codeMapping.sourceCode.sourceAdditionalInfo.get(col - ADD_INFO_START_COL).getItem2(); + return codeMapping.getSourceCode().sourceAdditionalInfo.get(col - ADD_INFO_START_COL).getItem2(); } else { - if (col >= ADD_INFO_START_COL) { - col = col - addInfoColCount; - } + col = resolveColumnIndex(col); Concept targetConcept; - if (codeMapping.targetConcepts.size() > 0) - targetConcept = codeMapping.targetConcepts.get(0); + if (codeMapping.getTargetConcepts().size() > 0) + targetConcept = codeMapping.getTargetConcepts().get(0).getConcept(); else targetConcept = Concept.EMPTY_CONCEPT; switch (col) { case 0: - return codeMapping.mappingStatus; + return codeMapping.getMappingStatus(); case 1: - return codeMapping.sourceCode.sourceCode; + return codeMapping.getSourceCode().sourceCode; case 2: - return codeMapping.sourceCode.sourceName; + return codeMapping.getSourceCode().sourceName; case 3: - return codeMapping.sourceCode.sourceFrequency == -1 ? "" : codeMapping.sourceCode.sourceFrequency; + return codeMapping.getSourceCode().sourceFrequency == -1 ? "" : codeMapping.getSourceCode().sourceFrequency; case 4: - return codeMapping.matchScore; + return codeMapping.getMatchScore(); case 5: return targetConcept.conceptId; case 6: @@ -197,8 +199,20 @@ public Object getValueAt(int row, int col) { return targetConcept.parentCount; case 16: return targetConcept.childCount; - case 17: - return codeMapping.comment; + case ASSIGNED_REVIEWER_COL: + return codeMapping.getAssignedReviewer(); + case 18: + if (codeMapping.getEquivalence() != CodeMapping.Equivalence.UNREVIEWED) { + return codeMapping.getEquivalence(); + } else { + return null; + } + case 19: + return codeMapping.getComment(); + case 20: + if (codeMapping.getStatusSetOn() != 0L) { + return String.format("%s (%tF)", codeMapping.getStatusSetBy(), codeMapping.getStatusSetOn()); + } default: return ""; } @@ -209,9 +223,7 @@ public Class getColumnClass(int col) { if (col >= ADD_INFO_START_COL && col < ADD_INFO_START_COL + addInfoColCount) { return String.class; } else { - if (col >= ADD_INFO_START_COL) { - col = col - addInfoColCount; - } + col = resolveColumnIndex(col); switch (col) { case 0: return MappingStatus.class; @@ -234,11 +246,26 @@ public Class getColumnClass(int col) { } public boolean isCellEditable(int row, int col) { + col = resolveColumnIndex(col); + if (col == ASSIGNED_REVIEWER_COL) { + return true; + } return false; } public void setValueAt(Object value, int row, int col) { + col = resolveColumnIndex(col); + if (col == ASSIGNED_REVIEWER_COL) { + CodeMapping codeMapping = Global.mapping.get(row); + codeMapping.setAssignedReviewer((String) value); + } + } + private int resolveColumnIndex(int col) { + if (col >= ADD_INFO_START_COL) { + return col - addInfoColCount; + } + return col; } } @@ -272,33 +299,63 @@ public void dataChanged(DataChangeEvent event) { } } - public void approveAll() { - for (int viewRow : table.getSelectedRows()) { - int modelRow = table.convertRowIndexToModel(viewRow); - tableModel.getCodeMapping(modelRow).mappingStatus = MappingStatus.APPROVED; - - } - Global.mapping.fireDataChanged(SIMPLE_UPDATE_EVENT); + private void fireUpdateEventAll(DataChangeEvent event) { + Global.mapping.fireDataChanged(event); int viewRow = table.getSelectedRow(); if (viewRow != -1) { int modelRow = table.convertRowIndexToModel(viewRow); - for (CodeSelectedListener listener : listeners) + for (CodeSelectedListener listener : listeners) { listener.codeSelected(tableModel.getCodeMapping(modelRow)); + } } } - public void clearAll() { + public void clearSelected() { for (int viewRow : table.getSelectedRows()) { int modelRow = table.convertRowIndexToModel(viewRow); - tableModel.getCodeMapping(modelRow).targetConcepts.clear(); + tableModel.getCodeMapping(modelRow).getTargetConcepts().clear(); + tableModel.getCodeMapping(modelRow).setUnchecked(); } - Global.mapping.fireDataChanged(SIMPLE_UPDATE_EVENT); - int viewRow = table.getSelectedRow(); - if (viewRow != -1) { - int modelRow = table.convertRowIndexToModel(viewRow); - for (CodeSelectedListener listener : listeners) - listener.codeSelected(tableModel.getCodeMapping(modelRow)); + fireUpdateEventAll(MULTI_UPDATE_EVENT); + } + + public void assignReviewersRandomly(String[] reviewers) { + // Randomly assign code mappings to given reviewers + ThreadLocalRandom randomGenerator = ThreadLocalRandom.current(); + for (CodeMapping codeMapping : Global.mapping) { + int random = randomGenerator.nextInt(reviewers.length); + codeMapping.setAssignedReviewer(reviewers[random]); } + fireUpdateEventAll(APPROVE_EVENT); + } + public void assignReviewersEqually(String[] reviewers) { + // Shuffle the code mapping array, then assign reviewers one by one, + // dividing the code mappings equally between reviewers. + // If the number of code mappings is not a multiple of the number of reviewers, + // then the first, second, etc. reviewer get one mapping more assigned. + int nReviewers = reviewers.length; + + List codesToAssign = new ArrayList<>(); + if (table.getSelectedRows().length == 1) { + // If only one row selected, assign all rows to the given reviewers + codesToAssign = Global.mapping; + } else { + // If a multi selection is made, only assign the select rows. + for (int viewRow : table.getSelectedRows()) { + int modelRow = table.convertRowIndexToModel(viewRow); + codesToAssign.add(tableModel.getCodeMapping(modelRow)); + } + } + + List codeMappingIndex = IntStream.range(0, codesToAssign.size()) + .boxed().collect(Collectors.toList()); + + Collections.shuffle(codeMappingIndex); + for (int i = 0; i < codeMappingIndex.size(); i++) { + CodeMapping codeMapping = codesToAssign.get(codeMappingIndex.get(i)); + codeMapping.setAssignedReviewer(reviewers[i % nReviewers]); + } + fireUpdateEventAll(APPROVE_EVENT); } } diff --git a/src/org/ohdsi/usagi/ui/ReviewerAssignmentDialog.java b/src/org/ohdsi/usagi/ui/ReviewerAssignmentDialog.java new file mode 100644 index 0000000..409a30a --- /dev/null +++ b/src/org/ohdsi/usagi/ui/ReviewerAssignmentDialog.java @@ -0,0 +1,70 @@ +/******************************************************************************* + * Copyright 2020 Observational Health Data Sciences and Informatics & The Hyve + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + ******************************************************************************/ +package org.ohdsi.usagi.ui; + +import javax.swing.*; +import java.awt.*; +import java.util.Arrays; + +public class ReviewerAssignmentDialog extends JDialog { + + private static final long serialVersionUID = 4349740743952812807L; + + public ReviewerAssignmentDialog() { + setTitle("Reviewer"); + setLayout(new GridBagLayout()); + GridBagConstraints g = new GridBagConstraints(); + g.fill = GridBagConstraints.BOTH; + g.ipadx = 10; + g.ipady = 10; + + g.gridx = 0; + g.gridy = 0; + add(new JLabel("Reviewers:"), g); + + g.gridx = 1; + g.gridy = 0; + JTextField reviewersField = new JTextField(); + reviewersField.setToolTipText("Please enter the reviewers as comma separated list"); + reviewersField.setPreferredSize(new Dimension(300, 10)); + reviewersField.setText("A,B,C,D"); + add(reviewersField, g); + + g.gridx = 0; + g.gridy = 2; + g.gridwidth = 2; + JPanel buttonPanel = new JPanel(); + buttonPanel.setLayout(new BoxLayout(buttonPanel, BoxLayout.X_AXIS)); + buttonPanel.add(Box.createHorizontalGlue()); + JButton saveButton = new JButton("Assign"); + saveButton.setToolTipText("Assign reviewers"); + saveButton.addActionListener(event -> { + String[] reviewers = Arrays.stream(reviewersField.getText().split(",")) + .map(String::trim) + .toArray(String[]::new); + Global.mappingTablePanel.assignReviewersEqually( + reviewers + ); + setVisible(false); + }); + buttonPanel.add(saveButton); + add(buttonPanel, g); + + pack(); + setModal(true); + setLocationRelativeTo(Global.frame); + } +} diff --git a/src/org/ohdsi/usagi/ui/TargetConceptTableModel.java b/src/org/ohdsi/usagi/ui/TargetConceptTableModel.java new file mode 100644 index 0000000..edbd5c4 --- /dev/null +++ b/src/org/ohdsi/usagi/ui/TargetConceptTableModel.java @@ -0,0 +1,106 @@ +package org.ohdsi.usagi.ui; + +import org.ohdsi.usagi.Concept; +import org.ohdsi.usagi.MappingTarget; + +import javax.swing.table.AbstractTableModel; +import java.util.ArrayList; +import java.util.List; + +class TargetConceptTableModel extends AbstractTableModel { + private static final long serialVersionUID = -4978479688021056281L; + + private final String[] columnNames = {"Concept ID", "Concept name", "Domain", "Concept class", "Vocabulary", "Concept code", + "Valid start date", "Valid end date", "Invalid reason", "Standard concept", "Parents", "Children", "Mapping Type", + "Creation Provenance"}; + private List targetConcepts = new ArrayList<>(); + + public TargetConceptTableModel() { + } + + public MappingTarget getMappingTarget(int row) { + return targetConcepts.get(row); + } + + public Concept getConcept(int row) { + return getMappingTarget(row).getConcept(); + } + + public int getColumnCount() { + return columnNames.length; + } + + public void setConcepts(List mappingTargets) { + this.targetConcepts = mappingTargets; + fireTableDataChanged(); + } + + public int getRowCount() { + return targetConcepts.size(); + } + + public String getColumnName(int col) { + return columnNames[col]; + } + + public Object getValueAt(int row, int col) { + if (row > targetConcepts.size()) { + return ""; + } + MappingTarget mappingTarget = targetConcepts.get(row); + Concept targetConcept = mappingTarget.getConcept(); + switch (col) { + case 0: + return targetConcept.conceptId; + case 1: + return targetConcept.conceptName; + case 2: + return targetConcept.domainId; + case 3: + return targetConcept.conceptClassId; + case 4: + return targetConcept.vocabularyId; + case 5: + return targetConcept.conceptCode; + case 6: + return targetConcept.validStartDate; + case 7: + return targetConcept.validEndDate; + case 8: + return targetConcept.invalidReason; + case 9: + return targetConcept.standardConcept; + case 10: + return targetConcept.parentCount; + case 11: + return targetConcept.childCount; + case 12: + return mappingTarget.getMappingType(); + case 13: + if (mappingTarget.getCreatedTime() != 0L) { + return String.format("%s (%tF)", mappingTarget.getCreatedBy(), mappingTarget.getCreatedTime()); + } + default: + return ""; + } + } + + public Class getColumnClass(int col) { + switch (col) { + case 0: + case 10: + case 11: + return Integer.class; + default: + return String.class; + } + } + + public boolean isCellEditable(int row, int col) { + return false; + } + + public void setValueAt(Object value, int row, int col) { + + } +} diff --git a/src/org/ohdsi/usagi/ui/UsagiCellRenderer.java b/src/org/ohdsi/usagi/ui/UsagiCellRenderer.java index a477f1b..a6d4d69 100644 --- a/src/org/ohdsi/usagi/ui/UsagiCellRenderer.java +++ b/src/org/ohdsi/usagi/ui/UsagiCellRenderer.java @@ -26,14 +26,15 @@ public class UsagiCellRenderer extends DefaultTableCellRenderer { - public static int MAX_TOOLTIP_WIDTH_IN_CHARS = 150; - private static final long serialVersionUID = -4732586716304918837L; - private static Color oddColor = new Color(0.95f, 0.95f, 1f); - private static Color checkedColor = new Color(0.75f, 1f, 0.75f); - private static Color checkedOddColor = new Color(0.85f, 1f, 0.85f); - private static Color errorColor = new Color(1f, 0.75f, 0.75f); - private static Color errorOddColor = new Color(1f, 0.85f, 0.85f); - private static DecimalFormat doubleFormatter = new DecimalFormat("###,###,###,##0.00"); + public static int MAX_TOOLTIP_WIDTH_IN_CHARS = 150; + private static final long serialVersionUID = -4732586716304918837L; + private static final Color evenColor = Color.white; + private static final Color oddColor = new Color(0.95f, 0.95f, 1.00f); + private static final Color checkedColor = new Color(0.75f, 1.00f, 0.75f); + private static final Color checkedOddColor = new Color(0.85f, 1.00f, 0.85f); + private static final Color errorColor = new Color(1.00f, 0.75f, 0.75f); + private static final Color errorOddColor = new Color(1.00f, 0.85f, 0.85f); + private static final DecimalFormat doubleFormatter = new DecimalFormat("###,###,###,##0.00"); @Override public void setValue(Object aValue) { @@ -77,6 +78,7 @@ else if (value == MappingStatus.INVALID_TARGET) if (!isSelected) { int modelRow = aTable.convertRowIndexToModel(row); + component.setForeground(Color.black); if (aTable.getModel().getValueAt(modelRow, 0) == MappingStatus.APPROVED) { if (row % 2 == 1) component.setBackground(checkedColor); @@ -90,8 +92,12 @@ else if (value == MappingStatus.INVALID_TARGET) } else { if (row % 2 == 1) { component.setBackground(oddColor); - } else - component.setBackground(Color.white); + } else { + component.setBackground(evenColor); + } + } + if (aTable.getModel().getValueAt(modelRow, 0) == MappingStatus.FLAGGED) { + component.setForeground(Color.red); } } return component; diff --git a/src/org/ohdsi/usagi/ui/UsagiMain.java b/src/org/ohdsi/usagi/ui/UsagiMain.java index f59ef28..7f4f94d 100644 --- a/src/org/ohdsi/usagi/ui/UsagiMain.java +++ b/src/org/ohdsi/usagi/ui/UsagiMain.java @@ -41,7 +41,7 @@ */ public class UsagiMain implements ActionListener { - public static String version = "1.3.0"; + public static String version = "1.4.1"; public static void main(String[] args) { new UsagiMain(args); @@ -63,6 +63,7 @@ public UsagiMain(String[] args) { Global.usagiSearchEngine.openIndexForSearching(false); Global.dbEngine.openForReading(); } + Global.vocabularyVersion = loadVocabularyVersion(Global.folder); Global.conceptClassIds = loadVectorFromFile(Global.folder + "/ConceptClassIds.txt"); Global.vocabularyIds = loadVectorFromFile(Global.folder + "/VocabularyIds.txt"); @@ -78,12 +79,13 @@ public UsagiMain(String[] args) { Global.saveAction = new SaveAction(); Global.saveAsAction = new SaveAsAction(); Global.approveAction = new ApproveAction(); + Global.flagAction = new FlagAction(); + Global.reviewerAssignmentAction = new ReviewerAssignmentAction(); Global.conceptInfoAction = new ConceptInformationAction(); Global.athenaAction = new AthenaAction(); Global.googleSearchAction = new GoogleSearchAction(); Global.showStatsAction = new ShowStatsAction(); Global.aboutAction = new AboutAction(); - Global.approveAllAction = new ApproveAllAction(); Global.rebuildIndexAction = new RebuildIndexAction(); Global.exitAction = new ExitAction(); @@ -93,9 +95,9 @@ public UsagiMain(String[] args) { Global.exportAction.setEnabled(false); Global.exportForReviewAction.setEnabled(false); Global.approveAction.setEnabled(false); - Global.approveAllAction.setEnabled(false); - Global.clearAllAction = new ClearAllAction(); - Global.clearAllAction.setEnabled(false); + Global.flagAction.setEnabled(false); + Global.clearSelectedAction = new ClearSelectedAction(); + Global.clearSelectedAction.setEnabled(false); Global.conceptInfoAction.setEnabled(false); Global.athenaAction.setEnabled(false); Global.googleSearchAction.setEnabled(false); @@ -139,6 +141,9 @@ public void windowClosing(WindowEvent e) { if (args.length > 1 && args[0].equals("--file")) { OpenAction.open(new File(args[1])); } + + AuthorDialog authorDialog = new AuthorDialog(); + authorDialog.setVisible(true); } private String loadVocabularyVersion(String folder) { diff --git a/src/org/ohdsi/usagi/ui/UsagiMenubar.java b/src/org/ohdsi/usagi/ui/UsagiMenubar.java index f2af971..f36ce04 100644 --- a/src/org/ohdsi/usagi/ui/UsagiMenubar.java +++ b/src/org/ohdsi/usagi/ui/UsagiMenubar.java @@ -42,8 +42,8 @@ public UsagiMenubar() { add(editMenu); editMenu.add(Global.approveAction); - editMenu.add(Global.approveAllAction); - editMenu.add(Global.clearAllAction); + editMenu.add(Global.clearSelectedAction); + editMenu.add(Global.reviewerAssignmentAction); JMenu viewMenu = new JMenu("View"); viewMenu.setMnemonic(KeyEvent.VK_V); diff --git a/src/org/ohdsi/usagi/ui/UsagiStatusBar.java b/src/org/ohdsi/usagi/ui/UsagiStatusBar.java index 69a619e..6d0ec04 100644 --- a/src/org/ohdsi/usagi/ui/UsagiStatusBar.java +++ b/src/org/ohdsi/usagi/ui/UsagiStatusBar.java @@ -32,6 +32,7 @@ public class UsagiStatusBar extends JPanel implements DataChangeListener { private static final long serialVersionUID = 4406343348570974587L; private JLabel countLabel; private JLabel percentLabel; + private JLabel reviewPercentLabel; private JLabel searchLabel; private DecimalFormat percentFormatter = new DecimalFormat("##0.0"); @@ -39,25 +40,33 @@ public UsagiStatusBar() { super(); setBorder(BorderFactory.createEmptyBorder()); setLayout(new BoxLayout(this, BoxLayout.X_AXIS)); + JLabel description = new JLabel("Approved / total:"); description.setForeground(Color.gray); add(description); add(Box.createHorizontalStrut(5)); + countLabel = new JLabel("0/0"); countLabel.setForeground(Color.black); add(countLabel); + add(Box.createHorizontalStrut(15)); + percentLabel = new JLabel("0%"); percentLabel.setForeground(Color.black); add(percentLabel); description = new JLabel(" of total frequency"); description.setForeground(Color.gray); add(description); + add(Box.createHorizontalGlue()); + searchLabel = new JLabel("Searching..."); searchLabel.setVisible(false); add(searchLabel); + add(Box.createHorizontalGlue()); + JLabel versionLabel = new JLabel("Vocabulary version: " + Global.vocabularyVersion); add(versionLabel); Global.mapping.addListener(this); @@ -72,24 +81,24 @@ private void update() { long totalFreq = 0; long approvedFreq = 0; for (CodeMapping codeMapping : Global.mapping) { - if (codeMapping.mappingStatus == MappingStatus.APPROVED) { + if (codeMapping.getMappingStatus() == MappingStatus.APPROVED) { approved++; - if (codeMapping.sourceCode.sourceFrequency == -1) + if (codeMapping.getSourceCode().sourceFrequency == -1) approvedFreq++; else - approvedFreq += codeMapping.sourceCode.sourceFrequency; + approvedFreq += codeMapping.getSourceCode().sourceFrequency; } - if (codeMapping.sourceCode.sourceFrequency == -1) + if (codeMapping.getSourceCode().sourceFrequency == -1) { totalFreq++; - else - totalFreq += codeMapping.sourceCode.sourceFrequency; + } else { + totalFreq += codeMapping.getSourceCode().sourceFrequency; + } } countLabel.setText(approved + " / " + Global.mapping.size()); countLabel.setToolTipText(approved + " of the " + Global.mapping.size() + " source codes now has an approved mapping"); String percent = percentFormatter.format(100 * approvedFreq / (double) totalFreq) + "%"; percentLabel.setText(percent); percentLabel.setToolTipText(percent + " of all entries in the source data now has an approved mapping"); - } @Override diff --git a/src/org/ohdsi/usagi/ui/actions/ApplyPreviousMappingAction.java b/src/org/ohdsi/usagi/ui/actions/ApplyPreviousMappingAction.java index b2099fd..9c72b83 100644 --- a/src/org/ohdsi/usagi/ui/actions/ApplyPreviousMappingAction.java +++ b/src/org/ohdsi/usagi/ui/actions/ApplyPreviousMappingAction.java @@ -54,7 +54,7 @@ public void actionPerformed(ActionEvent arg0) { // Existing code lookup Map codeToMapping = new HashMap<>(); for (CodeMapping codeMapping: Global.mapping) { - codeToMapping.put(codeMapping.sourceCode.sourceCode, codeMapping); + codeToMapping.put(codeMapping.getSourceCode().sourceCode, codeMapping); } // Open mapping file to be applied @@ -64,12 +64,12 @@ public void actionPerformed(ActionEvent arg0) { // Apply mapping. Add mappings not currently present for (CodeMapping codeMappingToBeApplied : mappingToBeApplied) { - CodeMapping existingMapping = codeToMapping.get(codeMappingToBeApplied.sourceCode.sourceCode); + CodeMapping existingMapping = codeToMapping.get(codeMappingToBeApplied.getSourceCode().sourceCode); if (existingMapping != null) { - existingMapping.sourceCode.sourceName = codeMappingToBeApplied.sourceCode.sourceName; - existingMapping.targetConcepts = codeMappingToBeApplied.targetConcepts; - existingMapping.mappingStatus = codeMappingToBeApplied.mappingStatus; - existingMapping.comment = codeMappingToBeApplied.comment; + existingMapping.getSourceCode().sourceName = codeMappingToBeApplied.getSourceCode().sourceName; + existingMapping.setTargetConcepts(codeMappingToBeApplied.getTargetConcepts()); + existingMapping.setMappingStatus(codeMappingToBeApplied.getMappingStatus()); + existingMapping.setComment(codeMappingToBeApplied.getComment()); mappingsApplied++; } else { Global.mapping.add(codeMappingToBeApplied); diff --git a/src/org/ohdsi/usagi/ui/actions/ApproveAction.java b/src/org/ohdsi/usagi/ui/actions/ApproveAction.java index cd3aa45..5dd0e46 100644 --- a/src/org/ohdsi/usagi/ui/actions/ApproveAction.java +++ b/src/org/ohdsi/usagi/ui/actions/ApproveAction.java @@ -16,6 +16,7 @@ package org.ohdsi.usagi.ui.actions; import java.awt.event.ActionEvent; +import java.awt.event.InputEvent; import java.awt.event.KeyEvent; import javax.swing.AbstractAction; @@ -29,14 +30,23 @@ public class ApproveAction extends AbstractAction { private static final long serialVersionUID = -6399524936473823131L; public ApproveAction() { - putValue(Action.NAME, "Approve"); - putValue(Action.SHORT_DESCRIPTION, "Approve the selected single mapping"); + setToApprove(); putValue(Action.MNEMONIC_KEY, KeyEvent.VK_A); - putValue(Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_A, ActionEvent.ALT_MASK)); + putValue(Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_A, InputEvent.ALT_MASK)); } @Override public void actionPerformed(ActionEvent arg0) { - Global.mappingDetailPanel.approve(); + Global.mappingDetailPanel.approveOrUnapprove(); + } + + public void setToApprove() { + putValue(Action.NAME, "Approve"); + putValue(Action.SHORT_DESCRIPTION, "Approve the selected single mapping"); + } + + public void setToUnapprove() { + Global.approveAction.putValue(Action.NAME, "Unapprove"); + Global.approveAction.putValue(Action.SHORT_DESCRIPTION, "Unapprove this mapping"); } } diff --git a/src/org/ohdsi/usagi/ui/actions/ClearAllAction.java b/src/org/ohdsi/usagi/ui/actions/ClearSelectedAction.java similarity index 90% rename from src/org/ohdsi/usagi/ui/actions/ClearAllAction.java rename to src/org/ohdsi/usagi/ui/actions/ClearSelectedAction.java index 7e3c225..d081280 100644 --- a/src/org/ohdsi/usagi/ui/actions/ClearAllAction.java +++ b/src/org/ohdsi/usagi/ui/actions/ClearSelectedAction.java @@ -22,18 +22,18 @@ import org.ohdsi.usagi.ui.Global; -public class ClearAllAction extends AbstractAction { +public class ClearSelectedAction extends AbstractAction { private static final long serialVersionUID = 3420357922150237898L; - public ClearAllAction() { + public ClearSelectedAction() { putValue(Action.NAME, "Clear selected"); putValue(Action.SHORT_DESCRIPTION, "Clear all selected mappings (set target to 0)"); } @Override public void actionPerformed(ActionEvent arg0) { - Global.mappingTablePanel.clearAll(); + Global.mappingTablePanel.clearSelected(); } } diff --git a/src/org/ohdsi/usagi/ui/actions/ExportForReviewAction.java b/src/org/ohdsi/usagi/ui/actions/ExportForReviewAction.java index 0a26db7..65b6329 100644 --- a/src/org/ohdsi/usagi/ui/actions/ExportForReviewAction.java +++ b/src/org/ohdsi/usagi/ui/actions/ExportForReviewAction.java @@ -21,6 +21,7 @@ import java.io.File; import java.util.ArrayList; import java.util.List; +import java.util.stream.Collectors; import javax.swing.*; import javax.swing.filechooser.FileFilter; @@ -29,6 +30,7 @@ import org.ohdsi.usagi.CodeMapping; import org.ohdsi.usagi.CodeMapping.MappingStatus; import org.ohdsi.usagi.Concept; +import org.ohdsi.usagi.MappingTarget; import org.ohdsi.usagi.ui.Global; import org.ohdsi.utilities.files.Row; import org.ohdsi.utilities.files.WriteCSVFileWithHeader; @@ -55,7 +57,7 @@ public void actionPerformed(ActionEvent arg0) { boolean hasApprovedMappings = false; for (CodeMapping mapping : Global.mapping) { - if (mapping.mappingStatus == MappingStatus.APPROVED) { + if (mapping.getMappingStatus() == MappingStatus.APPROVED) { hasApprovedMappings = true; break; } @@ -77,18 +79,20 @@ public void actionPerformed(ActionEvent arg0) { Global.frame.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); WriteCSVFileWithHeader out = new WriteCSVFileWithHeader(file.getAbsolutePath()); for (CodeMapping mapping : Global.mapping) - if (exportUnapproved || mapping.mappingStatus == MappingStatus.APPROVED) { + if (exportUnapproved || mapping.getMappingStatus() == MappingStatus.APPROVED) { List targetConcepts; - if (mapping.targetConcepts.size() == 0) { + if (mapping.getTargetConcepts().size() == 0) { targetConcepts = new ArrayList(1); targetConcepts.add(Concept.EMPTY_CONCEPT); } else - targetConcepts = mapping.targetConcepts; + targetConcepts = mapping.getTargetConcepts().stream() + .map(MappingTarget::getConcept) + .collect(Collectors.toList()); for (Concept targetConcept : targetConcepts) { - Row row = mapping.sourceCode.toRow(); - row.add("matchScore", mapping.matchScore); - if (exportUnapproved) row.add("mappingStatus", mapping.mappingStatus.toString()); + Row row = mapping.getSourceCode().toRow(); + row.add("matchScore", mapping.getMatchScore()); + if (exportUnapproved) row.add("mappingStatus", mapping.getMappingStatus().toString()); row.add("targetConceptId", targetConcept.conceptId); row.add("targetConceptName", targetConcept.conceptName); row.add("targetVocabularyId", targetConcept.vocabularyId); diff --git a/src/org/ohdsi/usagi/ui/actions/ExportSourceToConceptMapAction.java b/src/org/ohdsi/usagi/ui/actions/ExportSourceToConceptMapAction.java index 8878eb8..5a9f6ee 100644 --- a/src/org/ohdsi/usagi/ui/actions/ExportSourceToConceptMapAction.java +++ b/src/org/ohdsi/usagi/ui/actions/ExportSourceToConceptMapAction.java @@ -48,7 +48,7 @@ public void actionPerformed(ActionEvent arg0) { boolean hasApprovedMappings = false; for (CodeMapping mapping : Global.mapping) { - if (mapping.mappingStatus == MappingStatus.APPROVED) { + if (mapping.getMappingStatus() == MappingStatus.APPROVED) { hasApprovedMappings = true; break; } diff --git a/src/org/ohdsi/usagi/ui/actions/FlagAction.java b/src/org/ohdsi/usagi/ui/actions/FlagAction.java new file mode 100644 index 0000000..fbfda41 --- /dev/null +++ b/src/org/ohdsi/usagi/ui/actions/FlagAction.java @@ -0,0 +1,49 @@ +/******************************************************************************* + * Copyright 2020 Observational Health Data Sciences and Informatics & The Hyve + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + ******************************************************************************/ +package org.ohdsi.usagi.ui.actions; + +import org.ohdsi.usagi.ui.Global; + +import javax.swing.*; +import java.awt.event.ActionEvent; +import java.awt.event.InputEvent; +import java.awt.event.KeyEvent; + +public class FlagAction extends AbstractAction { + + private static final long serialVersionUID = -395107404415936659L; + + public FlagAction() { + setToFlag(); + putValue(Action.MNEMONIC_KEY, KeyEvent.VK_F); + putValue(Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_F, InputEvent.ALT_MASK)); + } + + @Override + public void actionPerformed(ActionEvent arg0) { + Global.mappingDetailPanel.flagOrUnflag(); + } + + public void setToFlag() { + putValue(Action.NAME, "Flag"); + putValue(Action.SHORT_DESCRIPTION, "Flag this source code for further review"); + } + + public void setToUnflag() { + putValue(Action.NAME, "Unflag"); + putValue(Action.SHORT_DESCRIPTION, "Unflag this code"); + } +} diff --git a/src/org/ohdsi/usagi/ui/actions/ApproveAllAction.java b/src/org/ohdsi/usagi/ui/actions/ReviewerAssignmentAction.java similarity index 66% rename from src/org/ohdsi/usagi/ui/actions/ApproveAllAction.java rename to src/org/ohdsi/usagi/ui/actions/ReviewerAssignmentAction.java index 6c211c5..b313001 100644 --- a/src/org/ohdsi/usagi/ui/actions/ApproveAllAction.java +++ b/src/org/ohdsi/usagi/ui/actions/ReviewerAssignmentAction.java @@ -15,27 +15,29 @@ ******************************************************************************/ package org.ohdsi.usagi.ui.actions; +import org.ohdsi.usagi.ui.Global; +import org.ohdsi.usagi.ui.ReviewerAssignmentDialog; +import org.ohdsi.usagi.ui.ShowStatsDialog; + +import javax.swing.*; import java.awt.event.ActionEvent; import java.awt.event.InputEvent; import java.awt.event.KeyEvent; -import javax.swing.*; - -import org.ohdsi.usagi.ui.Global; - -public class ApproveAllAction extends AbstractAction { +public class ReviewerAssignmentAction extends AbstractAction { - private static final long serialVersionUID = 3420357922150237898L; + private static final long serialVersionUID = -6399524936473823131L; - public ApproveAllAction() { - putValue(Action.NAME, "Approve selected"); - putValue(Action.SHORT_DESCRIPTION, "Approve all selected mappings"); - putValue(Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_A, InputEvent.ALT_MASK | InputEvent.SHIFT_DOWN_MASK)); + public ReviewerAssignmentAction() { + putValue(Action.NAME, "Assign Reviewers"); + putValue(Action.SHORT_DESCRIPTION, "Assign reviewer to the mappings"); } @Override public void actionPerformed(ActionEvent arg0) { - Global.mappingTablePanel.approveAll(); + ReviewerAssignmentDialog dialog = new ReviewerAssignmentDialog(); + dialog.setLocationRelativeTo(Global.frame); + dialog.setVisible(true); } } diff --git a/src/org/ohdsi/utilities/files/ReadCSVFile.java b/src/org/ohdsi/utilities/files/ReadCSVFile.java index f60713b..f768db0 100644 --- a/src/org/ohdsi/utilities/files/ReadCSVFile.java +++ b/src/org/ohdsi/utilities/files/ReadCSVFile.java @@ -146,6 +146,7 @@ private List line2columns(String line) { String column = columns.get(i); if (column.startsWith("\"") && column.endsWith("\"") && column.length() > 1) column = column.substring(1, column.length() - 1); + column = column.replace("\"\"", "\""); column = column.replace("\\\"", "\""); column = column.replaceAll("\\\\\\\\", "\\\\"); columns.set(i, column); diff --git a/src/org/ohdsi/utilities/files/Row.java b/src/org/ohdsi/utilities/files/Row.java index 94323f9..5cd29d6 100644 --- a/src/org/ohdsi/utilities/files/Row.java +++ b/src/org/ohdsi/utilities/files/Row.java @@ -42,18 +42,31 @@ public Row(Row row) { } public String get(String fieldName) { + return get(fieldName, null); + } + + public String get(String fieldName, String defaultValue) { int index; - try { - index = fieldName2ColumnIndex.get(fieldName); - } catch (NullPointerException e) { - throw new RuntimeException("Field \"" + fieldName + "\" not found"); + if (!fieldName2ColumnIndex.containsKey(fieldName)) { + if (defaultValue != null) { + return defaultValue; + } else { + throw new RuntimeException("Field \"" + fieldName + "\" not found"); + } } - if (cells.size() <= index) + + index = fieldName2ColumnIndex.get(fieldName); + if (cells.size() <= index) { return ""; - else - return cells.get(index); + } + + String value = cells.get(index); + if (value.isEmpty() && defaultValue != null) { + return defaultValue; + } + return value; } - + public List getFieldNames() { List names = new ArrayList(fieldName2ColumnIndex.size()); for (int i = 0; i < fieldName2ColumnIndex.size(); i++) @@ -64,15 +77,26 @@ public List getFieldNames() { } public int getInt(String fieldName) { - return Integer.parseInt(get(fieldName).trim()); + return Integer.parseInt(get(fieldName, null).trim()); + } + + public int getInt(String fieldName, String defaultValue) { + return Integer.parseInt(get(fieldName, defaultValue).trim()); } public long getLong(String fieldName) { - return Long.parseLong(get(fieldName)); + return Long.parseLong(get(fieldName, null)); + } + public long getLong(String fieldName, String defaultValue) { + return Long.parseLong(get(fieldName, defaultValue)); } public double getDouble(String fieldName) { - return Double.parseDouble(get(fieldName)); + return Double.parseDouble(get(fieldName, null)); + } + + public double getDouble(String fieldName, String defaultValue) { + return Double.parseDouble(get(fieldName, defaultValue)); } public void add(String fieldName, String value) { diff --git a/src/org/ohdsi/utilities/files/WriteCSVFile.java b/src/org/ohdsi/utilities/files/WriteCSVFile.java index f16b7d9..b1c5150 100644 --- a/src/org/ohdsi/utilities/files/WriteCSVFile.java +++ b/src/org/ohdsi/utilities/files/WriteCSVFile.java @@ -90,7 +90,7 @@ public String columns2line(List columns) { boolean hasQuotes = column.contains("\""); column = column.replaceAll("\\\\", "\\\\\\\\"); if (hasQuotes) - column = column.replaceAll("\"", "\\\\\""); + column = column.replaceAll("\"", "\"\""); column = column.replaceAll("\r", ""); column = column.replaceAll("\n", "\\\\n"); if (hasQuotes || column.contains(Character.toString(delimiter)))