diff --git a/examples/web/StaticLinkExamples.html b/examples/web/StaticLinkExamples.html
index 2a8ef314b3..025db83757 100644
--- a/examples/web/StaticLinkExamples.html
+++ b/examples/web/StaticLinkExamples.html
@@ -7,6 +7,12 @@
IGV desktop links
+
+ Load Cram (hg38)
+
+
+ Goto MYC
+
Load multiple files, new session, specify genome and locaus
diff --git a/src/main/java/org/igv/batch/CommandListener.java b/src/main/java/org/igv/batch/CommandListener.java
index b7a39cc53d..5f1165aeb8 100755
--- a/src/main/java/org/igv/batch/CommandListener.java
+++ b/src/main/java/org/igv/batch/CommandListener.java
@@ -39,6 +39,7 @@ public class CommandListener implements Runnable {
private static final String NO_CACHE = "Cache-Control: no-cache, no-store";
private static final String ACCESS_CONTROL_ALLOW_ORIGIN = "Access-Control-Allow-Origin: *";
private static final String ACCESS_CONTROL_ALLOW_HEADERS = "Access-Control-Allow-Headers: access-control-allow-origin";
+ private static final String ACCESS_CONTROL_PRIVATE_NETWORK = "Access-Control-Allow-Private-Network: true";
private int port = -1;
@@ -299,6 +300,8 @@ private void sendHTTPResponse(PrintWriter out, String result, String contentType
out.print(CRLF);
out.print(ACCESS_CONTROL_ALLOW_ORIGIN);
out.print(CRLF);
+ out.print(ACCESS_CONTROL_PRIVATE_NETWORK);
+ out.print(CRLF);
if (result != null) {
out.print("Content-Type: " + contentType);
out.print(CRLF);
@@ -328,7 +331,10 @@ private void sendHTTPOptionsResponse(PrintWriter out) {
out.print(CRLF);
out.print(ACCESS_CONTROL_ALLOW_HEADERS);
out.print(CRLF);
- out.println("Access-Control-Allow-Methods: HEAD, GET, OPTIONS");
+ out.print(ACCESS_CONTROL_PRIVATE_NETWORK);
+ out.print(CRLF);
+ out.print("Access-Control-Allow-Methods: HEAD, GET, OPTIONS");
+ out.print(CRLF);
out.print(CRLF);
out.close();
diff --git a/src/main/java/org/igv/bedpe/InteractionTrack.java b/src/main/java/org/igv/bedpe/InteractionTrack.java
index 9a363fe714..4c8ed89450 100644
--- a/src/main/java/org/igv/bedpe/InteractionTrack.java
+++ b/src/main/java/org/igv/bedpe/InteractionTrack.java
@@ -3,7 +3,7 @@
import org.igv.Globals;
import org.igv.event.IGVEvent;
import org.igv.event.IGVEventObserver;
-import org.igv.jbrowse.CircularViewUtilities;
+import org.igv.circview.CircularViewUtilities;
import org.igv.logging.LogManager;
import org.igv.logging.Logger;
import org.igv.prefs.Constants;
@@ -414,21 +414,18 @@ public List getPopupMenuItems(TrackClickEvent te) {
items.add(TrackMenuUtils.getChangeFeatureWindow(Collections.singletonList(this)));
- // Experimental JBrowse.
- if (PreferencesManager.getPreferences().getAsBoolean(Constants.CIRC_VIEW_ENABLED) &&
- CircularViewUtilities.ping()) {
- items.add(new JPopupMenu.Separator());
- JMenuItem circViewItem = new JMenuItem("Add Features to Circular View");
- circViewItem.addActionListener(e -> {
- List frames = te.getFrame() != null ?
- Collections.singletonList(te.getFrame()) :
- FrameManager.getFrames();
- List extends BedPE> visibleFeatures = getVisibleFeatures(frames);
- CircularViewUtilities.sendBedpeToJBrowse(visibleFeatures, InteractionTrack.this.getName(), InteractionTrack.this.getColor());
- });
- items.add(circViewItem);
- items.add(new JPopupMenu.Separator());
- }
+ // Circular view
+ items.add(new JPopupMenu.Separator());
+ JMenuItem circViewItem = new JMenuItem("Add Features to Circular View");
+ circViewItem.addActionListener(e -> {
+ List frames = te.getFrame() != null ?
+ Collections.singletonList(te.getFrame()) :
+ FrameManager.getFrames();
+ List extends BedPE> visibleFeatures = getVisibleFeatures(frames);
+ CircularViewUtilities.addBedPE(visibleFeatures, InteractionTrack.this.getName(), InteractionTrack.this.getColor());
+ });
+ items.add(circViewItem);
+ items.add(new JPopupMenu.Separator());
}
return items;
diff --git a/src/main/java/org/igv/circview/CircularViewUtilities.java b/src/main/java/org/igv/circview/CircularViewUtilities.java
new file mode 100644
index 0000000000..6ce7913756
--- /dev/null
+++ b/src/main/java/org/igv/circview/CircularViewUtilities.java
@@ -0,0 +1,246 @@
+package org.igv.circview;
+
+import htsjdk.samtools.SAMTag;
+import htsjdk.tribble.Feature;
+import org.igv.bedpe.BedPE;
+import org.igv.circview.model.Assembly;
+import org.igv.circview.model.Chord;
+import org.igv.circview.model.Chromosome;
+import org.igv.circview.model.Mate;
+import org.igv.circview.ui.CircularView;
+import org.igv.circview.ui.CircularViewConfig;
+import org.igv.circview.ui.CircularViewPanel;
+import org.igv.circview.util.ChrColors;
+import org.igv.circview.util.ColorUtils;
+import org.igv.feature.genome.Genome;
+import org.igv.feature.genome.GenomeManager;
+import org.igv.sam.Alignment;
+import org.igv.ui.IGV;
+import org.igv.util.Downsampler;
+import org.igv.variant.Variant;
+import org.igv.variant.vcf.MateVariant;
+
+import javax.swing.JFrame;
+import javax.swing.SwingUtilities;
+import java.awt.Color;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Bridge between IGV and the in-process {@link CircularView} widget.
+ *
+ * This replaces the JBrowse/Electron based {@code org.igv.jbrowse.CircularViewUtilities},
+ * which spoke to an external app over a socket. The view is now a singleton Swing
+ * {@link JFrame} owned by this class; tracks add chords through the static
+ * {@code add*} methods, and the window is created and shown lazily on the first add.
+ *
+ *
Chord features are built from IGV model objects via the {@code Chord.from*}
+ * factory methods (the same conversions the old code used), then downsampled to
+ * {@link #MAX_CHORDS} to keep rendering responsive.
+ */
+public class CircularViewUtilities {
+
+ /**
+ * Maximum number of chords to render in a single set.
+ */
+ static int MAX_CHORDS = 10000;
+
+ /**
+ * Flanking bases added on each side when navigating to a clicked chord's regions.
+ */
+ private static final int CLICK_FLANKING = 2000;
+
+ // Singleton view + window, lazily created on the EDT.
+ private static CircularView view;
+ private static CircularViewPanel panel;
+ private static JFrame frame;
+
+ /**
+ * Id of the genome currently set as the assembly, to avoid needless resets.
+ */
+ private static String currentGenomeId;
+
+ private CircularViewUtilities() {
+ }
+
+ // ---- Public API ---------------------------------------------------------
+
+ /**
+ * True if the circular view window currently exists and is showing.
+ */
+ public static boolean isOpen() {
+ return frame != null && frame.isVisible();
+ }
+
+ public static void addBedPE(List extends BedPE> features, String trackName, Color color) {
+ List chords = new ArrayList<>(features.size());
+ for (BedPE f : features) {
+ chords.add(Chord.fromBedPE(f));
+ }
+ addChords(chords, trackName, color, 0.5f);
+ }
+
+ public static void addAlignments(List alignments, String trackName, Color color) {
+ List chords = new ArrayList<>();
+ for (Alignment a : alignments) {
+ if (a.isPaired() && a.getMate().isMapped()) {
+ chords.add(Chord.fromPEAlignment(a));
+ }
+ if (a.getAttribute(SAMTag.SA.name()) != null) {
+ chords.addAll(Chord.fromSAString(a));
+ }
+ }
+ addChords(chords, trackName, color, 0.1f);
+ }
+
+ public static void addVariants(List variants, String trackName, Color color) {
+ List chords = new ArrayList<>(variants.size());
+ for (Feature f : variants) {
+ if (f instanceof Variant) {
+ Variant v = f instanceof MateVariant ? ((MateVariant) f).mate : (Variant) f;
+ chords.add(Chord.fromVariant(v));
+ }
+ }
+ addChords(chords, trackName, color, 0.5f);
+ }
+
+ /**
+ * Add a set of chords to the view, opening (and creating) the window if needed.
+ *
+ * @param chords the chord features
+ * @param trackName name for this chord set / track row
+ * @param color base color; {@code alpha} is applied to it
+ * @param alpha opacity fraction in [0, 1]
+ */
+ public static void addChords(List chords, String trackName, Color color, float alpha) {
+ Chord[] arr = chords.toArray(new Chord[0]);
+ if (arr.length > MAX_CHORDS) {
+ arr = new Downsampler().sample(arr, MAX_CHORDS);
+ }
+ final List sampled = new ArrayList<>(Arrays.asList(arr));
+ final Color c = ColorUtils.setAlpha(color, alpha);
+ runOnEdt(() -> {
+ CircularView v = getInstance();
+ ensureAssembly(v);
+ v.addChords(sampled, trackName, c);
+ open();
+ });
+ }
+
+ /**
+ * Update the assembly to the given genome. No-op unless the view already
+ * exists; on a genome switch the previously added chords no longer apply, so
+ * this resets the view (matching {@link CircularView#setAssembly}).
+ */
+ public static void changeGenome(Genome genome) {
+ if (view == null || genome == null) {
+ return;
+ }
+ runOnEdt(() -> {
+ view.setAssembly(toAssembly(genome));
+ currentGenomeId = genome.getId();
+ });
+ }
+
+ public static void clearChords() {
+ if (view != null) {
+ runOnEdt(view::clearChords);
+ }
+ }
+
+ // ---- Internals ----------------------------------------------------------
+
+ /**
+ * Lazily create the singleton view and its window. Must run on the EDT.
+ */
+ private static CircularView getInstance() {
+ if (view == null) {
+ CircularViewConfig config = new CircularViewConfig();
+ config.onChordClick = CircularViewUtilities::onChordClick;
+
+ view = new CircularView(config);
+ panel = new CircularViewPanel(view);
+
+ frame = new JFrame("Circular View");
+ frame.setDefaultCloseOperation(JFrame.HIDE_ON_CLOSE);
+ frame.setContentPane(panel);
+ frame.pack();
+ frame.setLocationRelativeTo(IGV.hasInstance() ? IGV.getInstance().getMainFrame() : null);
+ }
+ return view;
+ }
+
+ /**
+ * Must run on the event thread
+ */
+ public static void open() {
+ if (frame == null) {
+ getInstance();
+ }
+ if (!frame.isVisible()) {
+ frame.setVisible(true);
+ }
+ frame.toFront();
+
+ }
+
+ /**
+ * Set the assembly from the current genome if it differs from what's shown.
+ */
+ private static void ensureAssembly(CircularView v) {
+ Genome genome = GenomeManager.getInstance().getCurrentGenome();
+ if (genome != null && !genome.getId().equals(currentGenomeId)) {
+ v.setAssembly(toAssembly(genome));
+ currentGenomeId = genome.getId();
+ }
+ }
+
+ /**
+ * Convert an IGV genome to a circular-view {@link Assembly}. Only the "long"
+ * (whole-genome) chromosomes are drawn; names are shortened so they match the
+ * shortened {@code refName}s on the chords.
+ */
+ private static Assembly toAssembly(Genome genome) {
+ List chromosomes = new ArrayList<>();
+ for (String chr : genome.getLongChromosomeNames()) {
+ org.igv.feature.Chromosome c = genome.getChromosome(chr);
+ String shortName = Chromosome.shortChrName(chr);
+ chromosomes.add(new Chromosome(shortName, c.getLength(), ChrColors.getChrColor(shortName)));
+ }
+ return new Assembly(genome.getDisplayName(), genome.getId(), chromosomes);
+ }
+
+ /**
+ * Navigate IGV to a clicked chord's two regions (with flanking), shown
+ * side-by-side. Port of the onChordClick callback in circularView.js.
+ */
+ private static void onChordClick(Chord feature) {
+ if (!IGV.hasInstance()) {
+ return;
+ }
+ Mate mate = feature.getMate();
+ if (mate == null) {
+ return;
+ }
+ Genome genome = GenomeManager.getInstance().getCurrentGenome();
+ String locus1 = locusString(genome, feature.getRefName(), feature.getStart(), feature.getEnd());
+ String locus2 = locusString(genome, mate.getRefName(), mate.getStart(), mate.getEnd());
+ IGV.getInstance().goToLocus(locus1 + " " + locus2);
+ }
+
+ private static String locusString(Genome genome, String refName, long start, long end) {
+ String chr = (genome != null) ? genome.getCanonicalChrName(refName) : refName;
+ long s = Math.max(0, start - CLICK_FLANKING);
+ long e = end + CLICK_FLANKING;
+ return chr + ":" + s + "-" + e;
+ }
+
+ private static void runOnEdt(Runnable r) {
+ if (SwingUtilities.isEventDispatchThread()) {
+ r.run();
+ } else {
+ SwingUtilities.invokeLater(r);
+ }
+ }
+}
diff --git a/src/main/java/org/igv/circview/demo/CircularViewDemo.java b/src/main/java/org/igv/circview/demo/CircularViewDemo.java
new file mode 100644
index 0000000000..10bb2c3a3c
--- /dev/null
+++ b/src/main/java/org/igv/circview/demo/CircularViewDemo.java
@@ -0,0 +1,99 @@
+package org.igv.circview.demo;
+
+import org.igv.circview.model.Assembly;
+import org.igv.circview.model.Chord;
+import org.igv.circview.model.Chromosome;
+import org.igv.circview.model.Mate;
+import org.igv.circview.ui.CircularView;
+import org.igv.circview.ui.CircularViewConfig;
+import org.igv.circview.ui.CircularViewPanel;
+import org.igv.circview.util.ChrColors;
+import org.igv.circview.util.ColorUtils;
+
+import javax.swing.JFrame;
+import javax.swing.SwingUtilities;
+import java.awt.Color;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Standalone demo: shows the circular view for hg19 with a few sample chords.
+ * Clicking a chord prints its feature to the console.
+ */
+public final class CircularViewDemo {
+
+ public static void main(String[] args) {
+ SwingUtilities.invokeLater(CircularViewDemo::createAndShow);
+ }
+
+ private static void createAndShow() {
+ CircularViewConfig config = new CircularViewConfig();
+ config.onChordClick = feature -> System.out.println("Chord clicked: " + feature);
+
+ CircularView view = new CircularView(config);
+ view.setAssembly(hg19());
+ view.addChords(sampleChords(), "Structural variants",
+ ColorUtils.parseColor("rgba(0, 0, 255, 0.35)"));
+ view.addChords(moreChords(), "Translocations",
+ ColorUtils.parseColor("rgba(220, 0, 0, 0.45)"));
+
+ CircularViewPanel panel = new CircularViewPanel(view);
+
+ JFrame frame = new JFrame("Circular View (Java) — hg19 demo");
+ frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
+ frame.setContentPane(panel);
+ frame.pack();
+ frame.setLocationRelativeTo(null);
+ frame.setVisible(true);
+ }
+
+ /** hg19 assembly, ported from test/hg19.js (names already short). */
+ private static Assembly hg19() {
+ long[][] data = {
+ {1, 248956422}, {2, 242193529}, {3, 198295559}, {4, 190214555},
+ {5, 181538259}, {6, 170805979}, {7, 159345973}, {8, 145138636},
+ {9, 138394717}, {10, 133797422}, {11, 135086622}, {12, 133275309},
+ {13, 114364328}, {14, 107043718}, {15, 101991189}, {16, 90338345},
+ {17, 83257441}, {18, 80373285}, {19, 58617616}, {20, 64444167},
+ {21, 46709983}, {22, 50818468},
+ };
+ List chromosomes = new ArrayList<>();
+ for (long[] d : data) {
+ String name = String.valueOf(d[0]);
+ chromosomes.add(new Chromosome(name, d[1], ChrColors.getChrColor(name)));
+ }
+ chromosomes.add(new Chromosome("X", 156040895, ChrColors.getChrColor("X")));
+ chromosomes.add(new Chromosome("Y", 57227415, ChrColors.getChrColor("Y")));
+ return new Assembly("Human hg19", "hg19", chromosomes);
+ }
+
+ /** A handful of intra- and inter-chromosomal chords. */
+ private static List sampleChords() {
+ List chords = new ArrayList<>();
+ chords.add(chord("1", 30_000_000, 30_200_000, "8", 95_000_000, 95_200_000));
+ chords.add(chord("1", 130_000_000, 130_050_000, "1", 200_000_000, 200_050_000));
+ chords.add(chord("3", 50_000_000, 50_100_000, "17", 40_000_000, 40_100_000));
+ chords.add(chord("5", 12_000_000, 12_100_000, "12", 60_000_000, 60_100_000));
+ chords.add(chord("X", 70_000_000, 70_300_000, "7", 100_000_000, 100_300_000));
+ chords.add(chord("22", 20_000_000, 20_100_000, "9", 21_000_000, 21_100_000));
+ chords.add(chord("2", 90_000_000, 90_200_000, "2", 180_000_000, 180_200_000));
+ return chords;
+ }
+
+ /** A second chord set, so the controls show more than one row. */
+ private static List moreChords() {
+ List chords = new ArrayList<>();
+ chords.add(chord("4", 60_000_000, 60_100_000, "11", 70_000_000, 70_100_000));
+ chords.add(chord("6", 40_000_000, 40_100_000, "20", 30_000_000, 30_100_000));
+ chords.add(chord("10", 50_000_000, 50_100_000, "13", 80_000_000, 80_100_000));
+ chords.add(chord("X", 20_000_000, 20_100_000, "16", 10_000_000, 10_100_000));
+ return chords;
+ }
+
+ private static Chord chord(String ref, long start, long end,
+ String mateRef, long mateStart, long mateEnd) {
+ String id = ref + ":" + start + "-" + end + "_" + mateRef + ":" + mateStart + "-" + mateEnd;
+ Color color = null; // inherit the chord set color
+ return new Chord(id, ref, start, end, new Mate(mateRef, mateStart, mateEnd), color);
+ }
+}
diff --git a/src/main/java/org/igv/circview/model/Assembly.java b/src/main/java/org/igv/circview/model/Assembly.java
new file mode 100644
index 0000000000..8cec60d481
--- /dev/null
+++ b/src/main/java/org/igv/circview/model/Assembly.java
@@ -0,0 +1,54 @@
+package org.igv.circview.model;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A genome assembly: an ordered list of chromosomes drawn around the circle.
+ *
+ * Mirrors the {name, id, chromosomes} input to setAssembly() in
+ * circularView.js. Chromosome names are shortened (leading "chr" stripped) and
+ * a color is assigned from {@link org.igv.circview.util.ChrColors} when absent.
+ */
+public final class Assembly {
+
+ private final String name;
+ private final String id;
+ private final List chromosomes;
+
+ public Assembly(String name, String id, List chromosomes) {
+ this.name = name;
+ this.id = id;
+ this.chromosomes = new ArrayList<>(chromosomes);
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public List getChromosomes() {
+ return chromosomes;
+ }
+
+ /** Total length in base pairs across all chromosomes. */
+ public long totalBp() {
+ long total = 0;
+ for (Chromosome c : chromosomes) {
+ total += c.getBpLength();
+ }
+ return total;
+ }
+
+ public Chromosome getChromosome(String name) {
+ for (Chromosome c : chromosomes) {
+ if (c.getName().equals(name)) {
+ return c;
+ }
+ }
+ return null;
+ }
+}
diff --git a/src/main/java/org/igv/jbrowse/Chord.java b/src/main/java/org/igv/circview/model/Chord.java
similarity index 65%
rename from src/main/java/org/igv/jbrowse/Chord.java
rename to src/main/java/org/igv/circview/model/Chord.java
index 105ec0925c..78b4a7174e 100644
--- a/src/main/java/org/igv/jbrowse/Chord.java
+++ b/src/main/java/org/igv/circview/model/Chord.java
@@ -1,27 +1,50 @@
-package org.igv.jbrowse;
+package org.igv.circview.model;
import htsjdk.samtools.SAMTag;
import org.igv.Globals;
import org.igv.bedpe.BedPE;
-import org.igv.bedpe.BedPEFeature;
import org.igv.sam.Alignment;
import org.igv.sam.ReadMate;
import org.igv.sam.SupplementaryAlignment;
import org.igv.variant.Variant;
+import java.awt.Color;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
-class Chord {
- String uniqueId;
- String color;
- String refName;
- int start;
- int end;
- Mate mate;
+/**
+ * A chord (arc) connecting one genomic region to its {@link Mate} region.
+ *
+ * Mirrors the chord feature objects passed to addChords() in circularView.js:
+ *
+ * {
+ * uniqueId, color, refName, start, end,
+ * mate: { refName, start, end }
+ * }
+ *
+ * The per-feature {@code color} is optional; when null the owning
+ * {@link ChordSet}'s color is used.
+ */
+public final class Chord {
+
+ private String uniqueId;
+ private String refName;
+ private long start;
+ private long end;
+ private Mate mate;
+ private Color color;
+
+ public Chord() {
+ }
- private Chord() {
+ public Chord(String uniqueId, String refName, long start, long end, Mate mate, Color color) {
+ this.uniqueId = uniqueId;
+ this.refName = refName;
+ this.start = start;
+ this.end = end;
+ this.mate = mate;
+ this.color = color;
}
public static Chord fromBedPE(BedPE f) {
@@ -87,52 +110,39 @@ public static Chord fromVariant(Variant v) {
return c;
}
- public String toJson() {
- StringBuffer buf = new StringBuffer();
- buf.append("{");
- buf.append(JsonUtils.toJson("uniqueId", uniqueId));
- buf.append(",");
- buf.append(JsonUtils.toJson("color", color));
- buf.append(",");
- buf.append(JsonUtils.toJson("refName", refName));
- buf.append(",");
- buf.append(JsonUtils.toJson("start", start));
- buf.append(",");
- buf.append(JsonUtils.toJson("end", end));
- buf.append(",");
- buf.append("\"mate\":");
- buf.append(mate.toJson());
- buf.append("}");
- return buf.toString();
- }
-
static String shortName(String chr) {
return chr.startsWith("chr") ? chr.substring(3) : chr;
}
-}
+
+ public String getUniqueId() {
+ return uniqueId;
+ }
+
+ public String getRefName() {
+ return refName;
+ }
+ public long getStart() {
+ return start;
+ }
-class Mate {
- String refName;
- int start;
- int end;
+ public long getEnd() {
+ return end;
+ }
- public Mate(String refName, int start, int end) {
- this.refName = refName;
- this.start = start;
- this.end = end;
+ public Mate getMate() {
+ return mate;
}
- public String toJson() {
- StringBuffer buf = new StringBuffer();
- buf.append("{");
- buf.append(JsonUtils.toJson("refName", refName));
- buf.append(",");
- buf.append(JsonUtils.toJson("start", start));
- buf.append(",");
- buf.append(JsonUtils.toJson("end", end));
- buf.append("}");
- return buf.toString();
+ /** Optional per-feature color; may be null (fall back to the chord set color). */
+ public Color getColor() {
+ return color;
}
-}
+ @Override
+ public String toString() {
+ return "ChordFeature{" + refName + ":" + start + "-" + end
+ + " -> " + mate.getRefName() + ":" + mate.getStart() + "-" + mate.getEnd()
+ + (uniqueId != null ? " id=" + uniqueId : "") + "}";
+ }
+}
diff --git a/src/main/java/org/igv/circview/model/ChordCollection.java b/src/main/java/org/igv/circview/model/ChordCollection.java
new file mode 100644
index 0000000000..c76c2916c0
--- /dev/null
+++ b/src/main/java/org/igv/circview/model/ChordCollection.java
@@ -0,0 +1,25 @@
+package org.igv.circview.model;
+
+import java.awt.Color;
+import java.util.List;
+
+/**
+ * A named, colorable, hideable collection of chords. Implemented by both
+ * {@link ChordSet} (the flat view) and {@link Track} (the grouped view) so the
+ * controls can drive either without caring which is active.
+ */
+public interface ChordCollection {
+
+ String getName();
+
+ Color getColor();
+
+ void setColor(Color color);
+
+ boolean isVisible();
+
+ void setVisible(boolean visible);
+
+ /** All chords in this collection. */
+ List getChords();
+}
diff --git a/src/main/java/org/igv/circview/model/ChordSet.java b/src/main/java/org/igv/circview/model/ChordSet.java
new file mode 100644
index 0000000000..37e31acba2
--- /dev/null
+++ b/src/main/java/org/igv/circview/model/ChordSet.java
@@ -0,0 +1,79 @@
+package org.igv.circview.model;
+
+import java.awt.Color;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+
+/**
+ * A named set of chords with a shared color and visibility, belonging to a track.
+ *
+ * Mirrors the chordSet object built by addChords() in circularView.js:
+ * {name, trackName, chords, color, trackColor, visible, id}.
+ */
+public final class ChordSet implements ChordCollection {
+
+ private final String name;
+ private final String trackName;
+ private final List chords;
+ private Color color;
+ private Color trackColor;
+ private boolean visible;
+ private final String id;
+
+ public ChordSet(String name, String trackName, List chords,
+ Color color, Color trackColor) {
+ this.name = name;
+ this.trackName = trackName;
+ this.chords = new ArrayList<>(chords);
+ this.color = color;
+ this.trackColor = trackColor;
+ this.visible = true;
+ this.id = shortId();
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public String getTrackName() {
+ return trackName;
+ }
+
+ public List getChords() {
+ return chords;
+ }
+
+ public Color getColor() {
+ return color;
+ }
+
+ public void setColor(Color color) {
+ this.color = color;
+ }
+
+ public Color getTrackColor() {
+ return trackColor;
+ }
+
+ public void setTrackColor(Color trackColor) {
+ this.trackColor = trackColor;
+ }
+
+ public boolean isVisible() {
+ return visible;
+ }
+
+ public void setVisible(boolean visible) {
+ this.visible = visible;
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ /** Short random id, the Java analogue of guid() in the JS source. */
+ static String shortId() {
+ return UUID.randomUUID().toString().substring(0, 8);
+ }
+}
diff --git a/src/main/java/org/igv/circview/model/ChordSetManager.java b/src/main/java/org/igv/circview/model/ChordSetManager.java
new file mode 100644
index 0000000000..0932c7b15e
--- /dev/null
+++ b/src/main/java/org/igv/circview/model/ChordSetManager.java
@@ -0,0 +1,68 @@
+package org.igv.circview.model;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Maintains the set of chords as both a flat list of {@link ChordSet}s and a
+ * grouped list of {@link Track}s. The view chooses which list to render based on
+ * its "group by track" flag.
+ *
+ * Direct port of chordSetManager.js.
+ */
+public final class ChordSetManager {
+
+ private final List