/* License see bottom */
package jpianotrain.gui;
import static jpianotrain.util.ResourceKeys.LABEL_CHORD;
import static jpianotrain.util.ResourceKeys.LABEL_DOM_SUB;
import static jpianotrain.util.ResourceKeys.LABEL_LEFT;
import static jpianotrain.util.ResourceKeys.LABEL_NOTES_ENGLISH;
import static jpianotrain.util.ResourceKeys.LABEL_PENTATONIC;
import static jpianotrain.util.ResourceKeys.LABEL_RENDER;
import static jpianotrain.util.ResourceKeys.LABEL_RIGHT;
import static jpianotrain.util.ResourceKeys.LABEL_SCALE;
import static jpianotrain.util.ResourceKeys.LABEL_SCALE_MAJOR;
import static jpianotrain.util.ResourceKeys.LABEL_SCALE_MINOR;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.Point;
import java.awt.RenderingHints;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.awt.font.FontRenderContext;
import java.awt.font.GlyphVector;
import java.awt.geom.AffineTransform;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.swing.ButtonGroup;
import javax.swing.DefaultComboBoxModel;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JComboBox;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JRadioButton;
import jpianotrain.Constants;
import jpianotrain.font.MusiQwikB;
import jpianotrain.staff.Chord;
import jpianotrain.staff.Note;
import jpianotrain.staff.NoteName;
import jpianotrain.staff.Scale;
import jpianotrain.staff.ScaleName;
import jpianotrain.util.ResourceFactory;
import org.apache.log4j.Logger;
/**
* Multifunctional visualization of notes, scales and chords.
* Instead of rendering chords and scales as a table one
* can use this class to draw common music theory aspects.
*
* @since 0
* @author Alexander Methke
*/
public class CircleOfFifths extends JPanel
implements ActionListener,
ItemListener {
private static final Logger log=Logger.getLogger(CircleOfFifths.class);
public CircleOfFifths() {
createUI();
}
protected void createUI() {
setLayout(new BorderLayout());
circlePane=new CirclePane();
add(circlePane, BorderLayout.CENTER);
JPanel p=new JPanel();
GridBagLayout gbl=new GridBagLayout();
GridBagConstraints gbc=new GridBagConstraints();
gbc.insets=new Insets(0,1,0,1);
gbc.gridy=0;
p.setLayout(gbl);
maJcb=new JComboBox(Note.MAJOR_NAMES);
maJcb.addItemListener(this);
gbc.gridx=0;
p.add(new JLabel(ResourceFactory.getString(LABEL_SCALE_MAJOR)+": "), gbc);
gbc.gridx=1;
p.add(maJcb, gbc);
miJcb=new JComboBox(Note.MINOR_NAMES);
miJcb.addItemListener(this);
gbc.gridx=2;
p.add(new JLabel(ResourceFactory.getString(LABEL_SCALE_MINOR)+": "), gbc);
gbc.gridx=3;
p.add(miJcb, gbc);
scaleGroup=new ButtonGroup();
scaleRadio=new JRadioButton(ResourceFactory.getString(LABEL_SCALE));
scaleRadio.addActionListener(this);
pentRadio=new JRadioButton(ResourceFactory.getString(LABEL_PENTATONIC));
pentRadio.addActionListener(this);
domSubRadio=new JRadioButton(ResourceFactory.getString(LABEL_DOM_SUB));
domSubRadio.addActionListener(this);
scaleGroup.add(scaleRadio);
scaleGroup.add(pentRadio);
scaleGroup.add(domSubRadio);
gbc.gridx=4;
p.add(domSubRadio, gbc);
gbc.gridx=5;
p.add(scaleRadio, gbc);
gbc.gridx=6;
p.add(pentRadio, gbc);
enCheck=new JCheckBox(ResourceFactory.getString(LABEL_NOTES_ENGLISH));
enCheck.addActionListener(this);
gbc.gridwidth=GridBagConstraints.REMAINDER;
gbc.gridx=7;
p.add(enCheck, gbc);
add(p, BorderLayout.NORTH);
p=new JPanel();
gbl=new GridBagLayout();
p.setLayout(gbl);
gbc=new GridBagConstraints();
gbc.insets=new Insets(1,0,1,0);
gbc.gridx=0;
gbc.gridy=0;
gbc.gridwidth=GridBagConstraints.REMAINDER;
p.add(new JLabel(ResourceFactory.getString(LABEL_CHORD)), gbc);
gbc.gridwidth=1;
quadChordCheck=new JCheckBox();
quadChordCheck.addActionListener(this);
gbc.gridx=1;
gbc.gridy=1;
gbc.fill=GridBagConstraints.HORIZONTAL;
c1b=new JComboBox(Note.MAJOR_NAMES);
c2b=new JComboBox(Note.MAJOR_NAMES);
c3b=new JComboBox(Note.MAJOR_NAMES);
c4b=new JComboBox(Note.MAJOR_NAMES);
c4b.setEnabled(false);
p.add(c1b, gbc);
gbc.gridy++;
p.add(c2b, gbc);
gbc.gridy++;
p.add(c3b, gbc);
gbc.gridy++;
p.add(c4b, gbc);
gbc.gridx=0;
p.add(quadChordCheck, gbc);
gbc.gridx=1;
drawButton=new JButton(ResourceFactory.getString(LABEL_RENDER));
drawButton.addActionListener(this);
gbc.gridy++;
p.add(drawButton, gbc);
gbc.gridy++;
lrotateButton=new JButton(ResourceFactory.getString(LABEL_LEFT));
lrotateButton.addActionListener(this);
p.add(lrotateButton, gbc);
gbc.gridy++;
rrotateButton=new JButton(ResourceFactory.getString(LABEL_RIGHT));
rrotateButton.addActionListener(this);
p.add(rrotateButton, gbc);
majMinGroup=new ButtonGroup();
majorRadio=new JRadioButton(ResourceFactory.getString(LABEL_SCALE_MAJOR));
majorRadio.addActionListener(this);
minorRadio=new JRadioButton(ResourceFactory.getString(LABEL_SCALE_MINOR));
minorRadio.addActionListener(this);
majMinGroup.add(majorRadio);
majMinGroup.add(minorRadio);
gbc.gridy++;
p.add(majorRadio, gbc);
gbc.gridy++;
gbc.gridheight=GridBagConstraints.REMAINDER;
p.add(minorRadio, gbc);
add(p, BorderLayout.EAST);
}
/**
* Shows Dominant and Subdominat according
* to the given note. This note is treated
* like a scale's base note. It's <i>value</i>
* must be in 12-tone-range.
*
* @return Signals if the operation
* could be performed.
*/
public boolean showDomSub(Note n) {
circlePane.setCenter(n.getName());
return true;
}
/**
* Shows Dominant and Subdominant of
* the given scale. Take care, that
* this scale is either major or
* minor.
*
* @return Signals if the operation
* could be performed.
*/
public boolean showDomSub(Scale s) {
int len=s.getLength();
if (len<ScaleName.MAJOR.getMapping().length ||
len>ScaleName.MAJOR.getMapping().length) {
// better return instead of
// throwing Exception(s)
return false;
}
return showDomSub(s.getRoot());
}
// ItemListener
public void itemStateChanged(ItemEvent e) {
Object src=e.getSource();
boolean update=false;
if (src==maJcb) {
lastSelection=(NoteName)maJcb.getSelectedItem();
update=true;
} else if (src==miJcb) {
lastSelection=(NoteName)miJcb.getSelectedItem();
update=true;
}
if (update) {
// initial phase, if none is selected,
// select dominant/subdominant
if (!domSubRadio.isSelected() &&
!pentRadio.isSelected() &&
!scaleRadio.isSelected()) {
domSubRadio.setSelected(true);
}
circlePane.setCenter(lastSelection);
}
}
// ActionListener
public void actionPerformed(ActionEvent e) {
Object src=e.getSource();
boolean enState=enCheck.isSelected();
Chord c;
if (quadChordCheck.isSelected()) {
c=new Chord((NoteName)c1b.getSelectedItem(),
(NoteName)c2b.getSelectedItem(),
(NoteName)c3b.getSelectedItem(),
(NoteName)c4b.getSelectedItem());
} else {
c=new Chord((NoteName)c1b.getSelectedItem(),
(NoteName)c2b.getSelectedItem(),
(NoteName)c3b.getSelectedItem());
}
if (lastSelection==null) {
lastSelection=(NoteName)maJcb.getSelectedItem();
}
if (src==drawButton) {
if (!minorRadio.isSelected() &&
!majorRadio.isSelected()) {
majorRadio.setSelected(true);
}
circlePane.renderChord(c);
} else if (src==lrotateButton) {
circlePane.rotateChord(false);
} else if (src==rrotateButton) {
circlePane.rotateChord(true);
} else if (src==majorRadio) {
circlePane.renderMajorChord(true);
circlePane.renderChord(c);
} else if (src==minorRadio) {
circlePane.renderMajorChord(false);
circlePane.renderChord(c);
} else if (src==scaleRadio) {
circlePane.setRenderMode(CirclePane.RENDER_SCALE);
circlePane.setCenter(lastSelection);
} else if (src==domSubRadio) {
circlePane.setRenderMode(CirclePane.RENDER_DOM_SUB);
circlePane.setCenter(lastSelection);
} else if (src==pentRadio) {
circlePane.setRenderMode(CirclePane.RENDER_PENTATONIC);
circlePane.setCenter(lastSelection);
} else if (src==enCheck) {
circlePane.useEnglish(enCheck.isSelected());
// only do something if «classic» is already
// selected
if (enState) {
maJcb.setModel(new DefaultComboBoxModel(Note.MAJOR_NAMES_EN));
miJcb.setModel(new DefaultComboBoxModel(Note.MINOR_NAMES_EN));
} else {
maJcb.setModel(new DefaultComboBoxModel(Note.MAJOR_NAMES));
miJcb.setModel(new DefaultComboBoxModel(Note.MINOR_NAMES));
}
} else if (src==quadChordCheck) {
c4b.setEnabled(quadChordCheck.isSelected());
}
}
public class CirclePane extends JPanel {
public static final int RENDER_DOM_SUB=1;
public static final int RENDER_SCALE=2;
public static final int RENDER_PENTATONIC=4;
public CirclePane() {
setOpaque(false);
majorNameMap=new HashMap<NoteName, Integer>();
minorNameMap=new HashMap<NoteName, Integer>();
int len=Scale.MAJOR_SCALES_CLASSIC.length;
for (int i=0;i<len;i++) {
majorNameMap.put(majArr[i], i);
minorNameMap.put(minArr[i], i);
majPointList.add(p1);
minPointList.add(p2);
}
}
public void useClassicNames(boolean b) {
if (b==classicFlag) {
return;
}
classicFlag=b;
if (enFlag) {
majArr=Scale.MAJOR_SCALES_CLASSIC_EN;
minArr=Scale.MINOR_SCALES_CLASSIC_EN;
} else {
majArr=Scale.MAJOR_SCALES_CLASSIC;
minArr=Scale.MINOR_SCALES_CLASSIC;
}
majorNameMap.clear();
minorNameMap.clear();
for (int i=0;i<majArr.length;i++) {
majorNameMap.put(majArr[i], i);
minorNameMap.put(minArr[i], i);
}
repaint();
}
public void useEnglish(boolean b) {
if (b==enFlag) {
return;
}
enFlag=b;
if (enFlag) {
majArr=Scale.MAJOR_SCALES_CLASSIC_EN;
minArr=Scale.MINOR_SCALES_CLASSIC_EN;
} else {
majArr=Scale.MAJOR_SCALES_CLASSIC;
minArr=Scale.MINOR_SCALES_CLASSIC;
}
majorNameMap.clear();
minorNameMap.clear();
for (int i=0;i<majArr.length;i++) {
majorNameMap.put(majArr[i], i);
minorNameMap.put(minArr[i], i);
}
repaint();
}
/**
* Center the dom/sub rendering around
* note's name.
*
* @param n Note's name, maybe <code>null</code>
* to reset rendering.
* @return If operation was successfull.
*/
public boolean setCenter(NoteName n) {
if (n==null) {
centerIdx=-1;
repaint();
return true;
}
Integer i=-1;
if (n.isMajor()) {
i=majorNameMap.get(n);
} else {
i=minorNameMap.get(n);
}
if (i==null) {
if (n.isMajor()) {
i=majorNameMap.get(Scale.getEnharmonicInScale(enFlag, n));
majorNameMap.put(n, i);
} else {
i=minorNameMap.get(Scale.getEnharmonicInScale(enFlag, n));
minorNameMap.put(n, i);
}
}
centerIdx=i.intValue();
repaint();
return true;
}
/**
* Determines if dominant and sub dominant,
* the full scale or only the pentatonic
* part will be highlited.
*/
public void setRenderMode(int key) {
switch (key) {
case RENDER_SCALE:
case RENDER_PENTATONIC:
case RENDER_DOM_SUB: {
scaleCode=key;
}break;
default: {
scaleCode=RENDER_DOM_SUB;
}
}
repaint();
}
/**
* Determines if the major or minor
* chord should be rendered.
*/
public void renderMajorChord(boolean f) {
majorFlag=f;
}
public void renderChord(Chord c) {
if (c==null) {
cIdx1=-1;
cIdx2=-1;
cIdx3=-1;
cIdx4=-1;
repaint();
return;
}
Map<NoteName, Integer> map;
if (majorFlag) {
map=majorNameMap;
} else {
map=minorNameMap;
}
cIdx1=getIndexOfName(c.getNote1().getName(), map);
cIdx2=getIndexOfName(c.getNote2().getName(), map);
cIdx3=getIndexOfName(c.getNote3().getName(), map);
if (c.getLength()>3) {
cIdx4=getIndexOfName(c.getNote4().getName(), map);
} else {
cIdx4=-1;
}
repaint();
}
public void rotateChord(boolean clockWise) {
int ofs=-1;
if (clockWise) {
ofs=1;
}
cIdx1=(((cIdx1+ofs)%12)+12)%12;
cIdx2=(((cIdx2+ofs)%12)+12)%12;
cIdx3=(((cIdx3+ofs)%12)+12)%12;
if (cIdx4!=-1) {
cIdx4=(((cIdx4+ofs)%12)+12)%12;
}
repaint();
}
public void revalidate() {
// always the same
majPointList=new ArrayList<Point>();
minPointList=new ArrayList<Point>();
double pRad;
int idx=1;
int aIdx=0;
for (int i=0;i<12;i++) {
majPointList.add(p1);
minPointList.add(p2);
}
for (int i=30;i<91;i+=30) {
pRad=radF*(i);
// center weight points of each
// sector
x1=(int)Math.round(Math.sin(pRad)*125);
x2=(int)Math.round(Math.sin(pRad)*175);
y1=(int)Math.round(Math.cos(pRad)*125);
y2=(int)Math.round(Math.cos(pRad)*175);
// maybe buffering 250+ and 250- saves
// another 2 adds per iteration
p1=new Point(250+x1, 250-y1);
p2=new Point(250+x2, 250-y2);
minPointList.set(idx, p1);
majPointList.set(idx, p2);
p1=new Point(250+y1, 250+x1);
p2=new Point(250+y2, 250+x2);
aIdx=idx+3;
minPointList.set(aIdx, p1);
majPointList.set(aIdx, p2);
p1=new Point(250-x1, 250+y1);
p2=new Point(250-x2, 250+y2);
aIdx=idx+6;
minPointList.set(aIdx, p1);
majPointList.set(aIdx, p2);
p1=new Point(250-y1, 250-x1);
p2=new Point(250-y2, 250-x2);
// avoid index out of bounds
// and make sector 12=sector 0
aIdx=idx+9;
minPointList.set(aIdx%12, p1);
majPointList.set(aIdx%12, p2);
idx++;
}
Dimension d=new Dimension(500, 510);
setBounds(0,0, d.width, d.height);
setPreferredSize(d);
setMinimumSize(d);
}
public void paintComponent(Graphics g) {
g=g.create();
Graphics2D g2d=(Graphics2D)g;
g2d.setRenderingHint(
RenderingHints.KEY_TEXT_ANTIALIASING,
RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
g2d.setRenderingHint(
RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
try {
Font sf=ResourceFactory.getNotationFont();
sf=sf.deriveFont(Font.PLAIN, Constants.NOTATION_FONT_SIZE_SMALL);
g.setFont(sf);
g.drawString(MusiQwikB.ONE_SHARP,350,70);
g.drawString(MusiQwikB.TWO_SHARPS,430,150);
g.drawString(MusiQwikB.THREE_SHARPS,460,270);
g.drawString(MusiQwikB.FOUR_SHARPS,430,375);
g.drawString(MusiQwikB.FIVE_SHARPS,350,470);
g.drawString(MusiQwikB.SIX_SHARPS, 257, 495);
g.drawLine(250, 460, 250, 500);
g.drawString(MusiQwikB.ONE_FLAT,120,70);
g.drawString(MusiQwikB.TWO_FLATS,25,150);
g.drawString(MusiQwikB.THREE_FLATS,0,270);
g.drawString(MusiQwikB.FOUR_FLATS,25,375);
g.drawString(MusiQwikB.FIVE_FLATS,120,470);
g.drawString(MusiQwikB.SIX_FLATS, 195, 495);
} catch (Exception ex) {
log.error("error while rendering scale markers", ex);
}
if (centerIdx>-1) {
startArc=75-(centerIdx*30);
g.setColor(lightYellow);
switch (scaleCode) {
case RENDER_DOM_SUB: {
// start at 75° and spin clockwise
g.fillArc(50,50,400,400,startArc,-30);
// start at 75°+30° and spin counter clockwise
g.fillArc(50,50,400,400,startArc+30,30);
}break;
case RENDER_SCALE: {
g.fillArc(50,50,400,400, startArc+60, -210);
g.setColor(getBackground());
g.fillOval(100,100,300,300);
g.setColor(lightYellow);
g.fillArc(100,100,300,300, startArc+150, -210);
g.setColor(getBackground());
g.fillOval(150,150,200,200);
}break;
case RENDER_PENTATONIC: {
g.fillArc(50,50,400,400, startArc+30, -150);
g.setColor(getBackground());
g.fillOval(100,100,300,300);
g.setColor(lightYellow);
g.fillArc(100,100,300,300, startArc+120, -150);
g.setColor(getBackground());
g.fillOval(150,150,200,200);
}break;
default: {
}
}
g.setColor(Color.yellow);
g.fillArc(50,50,400,400,startArc,30);
}
g.setColor(Color.black);
g.drawOval(150,150,200,200);
g.drawOval(100,100,300,300);
g.drawOval(50,50,400,400);
double rad;
for (int i=15;i<90;i+=30) {
rad=radF*i;
x1=(int)Math.round(Math.cos(rad)*100);
y1=(int)Math.round(Math.sin(rad)*100);
x2=x1<<1;
y2=y1<<1;
g.drawLine(250+x1, 250+y1, 250+x2, 250+y2);
g.drawLine(250+x1, 250-y1, 250+x2,250-y2);
g.drawLine(250-x1, 250+y1, 250-x2,250+y2);
g.drawLine(250-x1, 250-y1, 250-x2,250-y2);
}
if (cIdx1!=-1) {
if (majorFlag) {
g.setColor(Color.blue);
p=majPointList.get(cIdx1);
p1=majPointList.get(cIdx2);
p2=majPointList.get(cIdx3);
if (cIdx4!=-1) {
p3=majPointList.get(cIdx4);
} else {
p3=null;
}
} else {
g.setColor(Color.red);
p=minPointList.get(cIdx1);
p1=minPointList.get(cIdx2);
p2=minPointList.get(cIdx3);
if (cIdx4!=-1) {
p3=minPointList.get(cIdx4);
} else {
p3=null;
}
}
g.drawLine(p.x, p.y, p1.x, p1.y);
g.drawLine(p1.x, p1.y, p2.x, p2.y);
if (p3!=null) {
g.drawLine(p2.x, p2.y, p3.x, p3.y);
g.drawLine(p3.x, p3.y, p.x, p.y);
} else {
g.drawLine(p2.x, p2.y, p.x, p.y);
}
}
g.setColor(Color.black);
AffineTransform at=AffineTransform.getTranslateInstance(240,91);
Font font = new Font("SansSerif", Font.PLAIN, 32);
FontRenderContext frc=new FontRenderContext(at, true, true);
// sub-panel's coordinate system is not relative
// to parent by default
//g2d.setTransform(at);
g2d.transform(at);
AffineTransform defStat=g2d.getTransform();
GlyphVector gv=font.createGlyphVector(frc, majArr[0].toString());
g2d.drawGlyphVector(gv, 0, 0);
at=AffineTransform.getTranslateInstance(82,15);
double rot=30*radF;
at.rotate(rot);
int count=majArr.length;
for (int i=1;i<count;i++) {
g2d.transform(at);
gv=font.createGlyphVector(frc, majArr[i].toString());
g2d.drawGlyphVector(gv, 0, 0);
}
// move char's height down
at=AffineTransform.getTranslateInstance(0, 45);
// but stay at default transformation (see above)
g2d.setTransform(defStat);
g2d.transform(at);
gv=font.createGlyphVector(frc, minArr[0].toString());
g2d.drawGlyphVector(gv, 0, 0);
at=AffineTransform.getTranslateInstance(58,10);
rot=30*radF;
at.rotate(rot);
for (int i=1;i<count;i++) {
g2d.transform(at);
gv=font.createGlyphVector(frc, minArr[i].toString());
g2d.drawGlyphVector(gv, 0, 0);
}
}
private int getIndexOfName(NoteName n,
Map<NoteName, Integer> m) {
Integer idx=m.get(n);
if (idx==null) {
idx=m.get(Scale.getEnharmonicInScale(
enFlag, n));
m.put(n, idx);
}
return idx.intValue();
}
private boolean classicFlag;
private boolean enFlag;
private boolean majorFlag=true;
private int scaleCode=RENDER_DOM_SUB;
private int x1;
private int y1;
private int x2;
private int y2;
private int centerIdx=-1;
private int startArc;
private int cIdx1=-1;
private int cIdx2=-1;
private int cIdx3=-1;
private int cIdx4=-1;
private static final double radF=Math.PI/180;
private NoteName[] majArr=Scale.MAJOR_SCALES_CLASSIC;
private NoteName[] minArr=Scale.MINOR_SCALES_CLASSIC;
private Point p=new Point(0,0);
private Point p1=new Point(0,0);
private Point p2=new Point(0,0);
private Point p3=new Point(0,0);
private Color lightYellow=new Color(255,255,150);
private List<Point> majPointList;
private List<Point> minPointList;
private HashMap<NoteName, Integer> majorNameMap;
private HashMap<NoteName, Integer> minorNameMap;
}
private NoteName lastSelection;
private ButtonGroup majMinGroup;
private ButtonGroup scaleGroup;
private JButton drawButton;
private JButton lrotateButton;
private JButton rrotateButton;
private JCheckBox enCheck;
private JCheckBox quadChordCheck;
private JComboBox c1b;
private JComboBox c2b;
private JComboBox c3b;
private JComboBox c4b;
private JComboBox maJcb;
private JComboBox miJcb;
private JRadioButton domSubRadio;
private JRadioButton pentRadio;
private JRadioButton scaleRadio;
private JRadioButton majorRadio;
private JRadioButton minorRadio;
private CirclePane circlePane;
}
/*
Copyright (C) 2008 Alexander Methke
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program (gplv3.txt).
*/