//----------------------------------------------------------------------------//
// //
// P a g e P h y s i c a l P a i n t e r //
// //
//----------------------------------------------------------------------------//
// <editor-fold defaultstate="collapsed" desc="hdr"> //
// Copyright © Hervé Bitteur and others 2000-2013. All rights reserved. //
// This software is released under the GNU General Public License. //
// Goto http://kenai.com/projects/audiveris to report bugs or suggestions. //
//----------------------------------------------------------------------------//
// </editor-fold>
package omr.score.ui;
import omr.glyph.Shape;
import static omr.glyph.Shape.*;
import omr.glyph.facets.Glyph;
import omr.grid.LineInfo;
import omr.grid.StaffInfo;
import omr.math.BasicLine;
import omr.math.Line;
import omr.math.Rational;
import omr.score.entity.Barline;
import omr.score.entity.Chord;
import omr.score.entity.Measure;
import omr.score.entity.Note;
import omr.score.entity.Page;
import omr.score.entity.ScoreSystem;
import omr.score.entity.Slot;
import omr.score.entity.Staff;
import omr.score.entity.SystemPart;
import omr.sheet.Ending;
import omr.sheet.Ledger;
import omr.sheet.Sheet;
import omr.sheet.Skew;
import omr.sheet.SystemInfo;
import omr.ui.Colors;
import static omr.ui.symbol.Alignment.*;
import omr.ui.symbol.MusicFont;
import omr.ui.util.UIUtil;
import omr.util.TreeNode;
import omr.util.VerticalSide;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Stroke;
import java.awt.font.TextLayout;
import java.awt.geom.AffineTransform;
import java.awt.geom.Line2D;
import java.awt.geom.Point2D;
import java.util.ConcurrentModificationException;
/**
* Class {@code PagePhysicalPainter} paints the recognized page
* entities at the location of their image counterpart, so that
* discrepancies between them can be easily seen.
*
* <p>TODO:
* - Paint breath marks
*
* @author Hervé Bitteur
*/
public class PagePhysicalPainter
extends PagePainter
{
//~ Static fields/initializers ---------------------------------------------
/** Usual logger utility */
private static final Logger logger = LoggerFactory.getLogger(
PagePhysicalPainter.class);
//~ Constructors -----------------------------------------------------------
//---------------------//
// PagePhysicalPainter //
//---------------------//
/**
* Creates a new PagePhysicalPainter object.
*
* @param graphics Graphic context
* @param color the color to be used for foreground
* @param coloredVoices true for voices with different colors
* @param linePainting true for painting staff lines
* @param annotated true if annotations are to be drawn
*/
public PagePhysicalPainter (Graphics graphics,
Color color,
boolean coloredVoices,
boolean linePainting,
boolean annotated)
{
super(graphics, color, coloredVoices, linePainting, annotated);
}
//~ Methods ----------------------------------------------------------------
//----------//
// drawSlot //
//----------//
/**
* Draw a time slot in the score display.
*
* @param wholeSystem if true, the slot will embrace the whole system,
* otherwise only the part is embraced
* @param slot the slot to draw
* @param color the color to use in drawing
*/
public void drawSlot (boolean wholeSystem,
Slot slot,
Color color)
{
final Measure measure = slot.getMeasure();
final Color oldColor = g.getColor();
g.setColor(color);
final Stroke oldStroke = UIUtil.setAbsoluteStroke(g, 1);
try {
final int x = slot.getX();
if (wholeSystem) {
// Draw for the whole system height
system = measure.getSystem();
int top = system.getFirstPart()
.getFirstStaff()
.getInfo()
.getFirstLine()
.yAt(x);
int bottom = system.getLastPart()
.getLastStaff()
.getInfo()
.getLastLine()
.yAt(x);
g.drawLine(x, top, x, bottom);
} else {
// Draw for just the part height
SystemPart part = measure.getPart();
int top = part.getFirstStaff()
.getInfo()
.getFirstLine()
.yAt(x);
int bottom = part.getLastStaff()
.getInfo()
.getLastLine()
.yAt(x);
g.drawLine(x, top, x, bottom);
// Draw slot start time (with a maximum font size)
Rational slotStartTime = slot.getStartTime();
if (slotStartTime != null) {
TextLayout layout;
double zoom = g.getTransform()
.getScaleX();
if (zoom <= 2) {
layout = basicLayout(slotStartTime.toString(), halfAT);
} else {
AffineTransform at = AffineTransform.getScaleInstance(
1 / zoom,
1 / zoom);
layout = basicLayout(slotStartTime.toString(), at);
}
paint(
layout,
new Point(x, top - annotationDy),
BOTTOM_CENTER);
}
}
} catch (Exception ex) {
logger.warn(
getClass().getSimpleName() + " Error drawing " + slot,
ex);
}
g.setStroke(oldStroke);
g.setColor(oldColor);
}
//---------------//
// highlightSlot //
//---------------//
/**
* Highlight a slot with its related chords (stem / notehead)
*
* @param slot the slot to highlight
*/
public void highlightSlot (Slot slot)
{
Color oldColor = g.getColor();
g.setColor(Colors.SLOT_CURRENT);
// Draw the slot components
for (Chord chord : slot.getChords()) {
visit(chord);
for (TreeNode tn : chord.getNotes()) {
Note note = (Note) tn;
visit(note);
}
}
// Highlight the vertical slot line
drawSlot(false, slot, Colors.SLOT_CURRENT);
g.setColor(oldColor);
}
//---------------//
// visit Barline //
//---------------//
@Override
public boolean visit (Barline barline)
{
if (!barline.getBox()
.intersects(oldClip)
|| systemInfo.getSheet()
.getStaffManager()
.getStaves()
.isEmpty()) {
return false;
}
g.setColor(defaultColor);
try {
Stroke oldStroke = g.getStroke();
// This drawing is driven by the barline shape
Shape shape = barline.getShape();
Rectangle box = barline.getBox();
Point center = barline.getCenter();
SystemPart part = barline.getPart();
// Top and bottom limits of the barline, using staff lines
StaffInfo topStaff = systemInfo.getStaffAt(box.getLocation());
LineInfo topLine = topStaff.getFirstLine();
StaffInfo botStaff = systemInfo.getStaffAt(
new Point(box.x, box.y + box.height));
LineInfo botLine = botStaff.getLastLine();
Skew skew = systemInfo.getSkew();
if (skew == null) { // Safer
return false;
}
double slope = skew.getSlope();
BasicLine bar = new BasicLine();
bar.includePoint(center.x, center.y);
bar.includePoint(center.x - (100 * slope), center.y + 100);
Point2D topCenter = topLine.verticalIntersection(bar);
Point2D botCenter = botLine.verticalIntersection(bar);
if (shape != null) {
BarPainter barPainter = BarPainter.getBarPainter(shape);
barPainter.draw(g, topCenter, botCenter, part);
} else {
barline.addError("Barline with no recognized shape");
}
// This drawing is driven by the underlying glyphs
// for (Glyph glyph : barline.getGlyphs()) {
// Shape shape = glyph.getShape();
//
// if (glyph.isBar()) {
// float thickness = (float) glyph.getWeight() / glyph.
// getLength(
// Orientation.VERTICAL);
// g.setStroke(new BasicStroke(thickness));
//
// // Stroke is now OK for thickness but will draw beyond start
// // and stop points of the bar. So use clipping to fix this.
// final Rectangle box = glyph.getBounds();
// box.y = (int) Math.floor(
// glyph.getStartPoint(Orientation.VERTICAL).getY());
// box.height = (int) Math.ceil(
// glyph.getStopPoint(Orientation.VERTICAL).getY())
// - box.y;
// g.setClip(oldClip.intersection(box));
//
// glyph.renderLine(g);
//
// g.setClip(oldClip);
// } else if ((shape == REPEAT_DOT) || (shape == DOT_set)) {
// paint(DOT_set, glyph.getCentroid());
// }
// }
g.setStroke(oldStroke);
} catch (ConcurrentModificationException ignored) {
return false;
} catch (Exception ex) {
logger.warn(
getClass().getSimpleName() + " Error visiting " + barline,
ex);
}
return true;
}
//-------------//
// visit Chord //
//-------------//
@Override
public boolean visit (Chord chord)
{
try {
// Super: check, voice color, flags
if (!super.visit(chord)) {
return false;
}
// Draw the stem (physical)
if (chord.getStem() != null) {
final Point tail = chord.getTailLocation();
final Point head = chord.getHeadLocation();
if ((tail == null) || (head == null)) {
chord.addError("Missing head or tail for " + chord);
return false;
}
final Point headCopy = new Point(head);
// Slightly correct the ordinate on head side
final int dyFix = scale.getInterline() / 4;
if (tail.y < headCopy.y) {
// Stem up
headCopy.y -= dyFix;
} else {
// Stem down
headCopy.y += dyFix;
}
g.drawLine(headCopy.x, headCopy.y, tail.x, tail.y);
}
} catch (ConcurrentModificationException ignored) {
} catch (Exception ex) {
logger.warn(
getClass().getSimpleName() + " Error visiting " + chord,
ex);
}
return true;
}
//---------------//
// visit Measure //
//---------------//
@Override
public boolean visit (Measure measure)
{
if (annotated) {
if (!measure.isDummy()) {
final SystemPart part = measure.getPart();
final Color oldColor = g.getColor();
// Write the score-based measure id, on first real part only
if (part == measure.getSystem()
.getFirstRealPart()) {
String mid = measure.getScoreId();
if (mid != null) {
g.setColor(Colors.ANNOTATION);
StaffInfo staff = measure.getPart()
.getFirstStaff()
.getInfo();
Point loc = new Point(
measure.getLeftX(),
staff.getFirstLine().yAt(measure.getLeftX())
- annotationDy);
paint(basicLayout(mid, null), loc, BOTTOM_CENTER);
}
}
// Draw slot vertical lines ?
if (parameters.isSlotPainting()
&& (measure.getSlots() != null)) {
for (Slot slot : measure.getSlots()) {
drawSlot(false, slot, Colors.SLOT);
}
}
// // Flag for measure excess duration?
// if (measure.getExcess() != null) {
// g.setColor(Color.red);
// g.drawString(
// "Excess " + Note.quarterValueOf(measure.getExcess()),
// measure.getLeftX() + 10,
// measure.getPart().getFirstStaff().getTopLeft().y - 15);
// }
g.setColor(oldColor);
}
}
// WholeChords are not in the children hierarchy
// Thus, we must explicitly visit them
for (Chord chord : measure.getWholeChords()) {
if (chord.accept(this)) {
chord.acceptChildren(this);
}
}
return true;
}
//------------//
// visit Note //
//------------//
@Override
public boolean visit (Note note)
{
try {
// Paint note head and accidentals
super.visit(note);
// Augmentation dots ?
if (note.getFirstDot() != null) {
paint(DOT_set, note.getFirstDot().getAreaCenter());
}
if (note.getSecondDot() != null) {
paint(DOT_set, note.getSecondDot().getAreaCenter());
}
} catch (ConcurrentModificationException ignored) {
} catch (Exception ex) {
logger.warn(
getClass().getSimpleName() + " Error visiting " + note,
ex);
}
return true;
}
//------------//
// visit Page //
//------------//
@Override
public boolean visit (Page page)
{
try {
score = page.getScore();
scale = page.getScale();
final Sheet sheet = page.getSheet();
if ((sheet == null) || (scale == null)) {
return false;
}
// Set all painting parameters
initParameters();
// Determine beams parameters
if (scale.getMainBeam() != null) {
beamThickness = scale.getMainBeam();
beamHalfThickness = beamThickness / 2;
}
if (!page.getSystems()
.isEmpty()) {
// Normal (full) rendering of the score
page.acceptChildren(this);
} else {
// Render only what we have got so far...
g.setColor(defaultColor);
// Staff lines
sheet.getStaffManager()
.render(g);
if (sheet.getHorizontals() != null) {
// Horizontals
// Ledgers
for (Ledger ledger : sheet.getHorizontals()
.getLedgers()) {
ledger.render(g);
}
// Endings
for (Ending ending : sheet.getHorizontals()
.getEndings()) {
ending.render(g);
}
}
}
} catch (ConcurrentModificationException ignored) {
} catch (Exception ex) {
logger.warn(
getClass().getSimpleName() + " Error visiting " + page,
ex);
}
return false;
}
//-------------------//
// visit ScoreSystem //
//-------------------//
@Override
public boolean visit (ScoreSystem system)
{
this.system = system;
this.systemInfo = system.getInfo();
if (!visit(systemInfo)) {
return false;
}
// System id annotation
if (annotated) {
Color oldColor = g.getColor();
g.setColor(Colors.ANNOTATION);
Point ul = systemInfo.getBoundary()
.getLimit(VerticalSide.TOP)
.getPoint(0);
paint(
basicLayout("S" + system.getId(), null),
new Point(ul.x + annotationDx, ul.y + annotationDy),
TOP_LEFT);
g.setColor(oldColor);
}
return true;
}
//-------------//
// visit Staff //
//-------------//
/**
* This specific version paints the staff lines as closely as
* possible to the physical sheet lines.
*
* @param staff the staff to handle
* @return true if actually painted
*/
@Override
public boolean visit (Staff staff)
{
try {
if (staff.isDummy()) {
return false;
}
staff.getInfo()
.render(g);
} catch (ConcurrentModificationException ignored) {
} catch (Exception ex) {
logger.warn(
getClass().getSimpleName() + " Error visiting " + staff,
ex);
}
return true;
}
//------------------//
// visit SystemInfo //
//------------------//
public boolean visit (SystemInfo systemInfo)
{
try {
// Check that this system is visible
Rectangle bounds = systemInfo.getBounds();
if ((bounds == null) || !bounds.intersects(oldClip)) {
return false;
}
// Determine proper font size for the system
musicFont = MusicFont.getFont(scale.getInterline());
g.setColor(defaultColor);
// Ledgers
for (Glyph glyph : systemInfo.getGlyphs()) {
if ((glyph.getShape() == Shape.LEDGER) && glyph.isActive()) {
// For very short ledgers, glyph line is not reliable
renderLedger(g, glyph);
}
}
// Endings
for (Glyph ending : systemInfo.getEndings()) {
ending.renderLine(g);
}
} catch (ConcurrentModificationException ignored) {
} catch (Exception ex) {
logger.warn(
getClass().getSimpleName() + " Error visiting " + systemInfo,
ex);
}
return true;
}
//--------------------//
// accidentalLocation //
//--------------------//
@Override
protected Point accidentalLocation (Note note,
Glyph accidental)
{
return new Point(accidental.getAreaCenter().x, note.getCenter().y);
}
//----------//
// braceBox //
//----------//
@Override
protected Rectangle braceBox (SystemPart part)
{
Rectangle braceBox = part.getBrace()
.getBounds();
// Cheat a little, so that top and bottom are aligned with part extrema
int leftX = braceBox.x + braceBox.width;
int top = part.getFirstStaff()
.getInfo()
.getFirstLine()
.yAt(leftX);
int bot = part.getLastStaff()
.getInfo()
.getLastLine()
.yAt(leftX);
braceBox.y = top;
braceBox.height = bot - top + 1;
return braceBox;
}
//-------------//
// bracketLine //
//-------------//
@Override
protected Line2D bracketLine (SystemPart part)
{
// Driving line of the brace
final Line line = part.getBrace()
.getLine();
// We use the left points of the embraced staves to adjust ordinates
// This assumes we are close to left side (or the slope is small).
// Another way would be to impose the slope to the bracket line
// as we do with barlines.
Point2D top = part.getFirstStaff()
.getInfo()
.getFirstLine()
.getLeftPoint();
Point2D bot = part.getLastStaff()
.getInfo()
.getLastLine()
.getLeftPoint();
return new Line2D.Double(
new Point2D.Double(line.xAtY(top.getY()), top.getY()),
new Point2D.Double(line.xAtY(bot.getY()), bot.getY()));
}
//--------------//
// noteLocation //
//--------------//
@Override
protected Point noteLocation (Note note)
{
final Point center = note.getCenter();
final Chord chord = note.getChord();
final Glyph stem = chord.getStem();
if (stem != null) {
return location(center, chord);
} else {
return center;
}
}
//--------------//
// renderLedger //
//--------------//
private void renderLedger (Graphics2D g,
Glyph glyph)
{
// We use a horizontal line, going through glyph centroid, within bounds
Rectangle bounds = glyph.getBounds();
if (!bounds.intersects(g.getClipBounds())) {
return;
}
Point centroid = glyph.getCentroid();
Line2D line = new Line2D.Double(
bounds.x,
centroid.y,
(bounds.x + bounds.width) - 1,
centroid.y);
g.draw(line);
}
}