//----------------------------------------------------------------------------//
// //
// G l y p h s B u i l d 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.glyph;
import omr.constant.ConstantSet;
import omr.glyph.facets.BasicGlyph;
import omr.glyph.facets.Glyph;
import omr.lag.Section;
import omr.score.entity.Staff;
import omr.sheet.Scale;
import omr.sheet.Sheet;
import omr.sheet.SystemInfo;
import omr.util.HorizontalSide;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.awt.Point;
import java.awt.Rectangle;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
/**
* Class {@code GlyphsBuilder} is, at a system level, in charge of
* building (and removing) glyphs and of updating accordingly the
* containing entities (Nest and SystemInfo).
*
* <p>It does not handle the shape of a glyph (this higher-level task is
* handled by {@link GlyphInspector} among others).
* But it does handle all the physical characteristics of a glyph via {@link
* #computeGlyphFeatures} (moments, plus additional data such as ledger, stem).
*
* <p>It typically handles via {@link #retrieveGlyphs} the building of glyphs
* out of the remaining sections of a sheet (since this is done using the
* physical edges between the sections).
*
* <p>It provides provisioning methods to actually insert or remove a glyph:
* <ul>
*
* <li>A given newly built glyph can be inserted via {@link #addGlyph}</li>
*
* <li>Similarly {@link #removeGlyph} allows the removal of an existing glyph.
* <B>Nota:</B> Remember that the sections that compose a glyph are not removed,
* only the glyph is removed. The link from the contained sections back to the
* containing glyph is set to null.</li>
* </ul>
*
* @author Hervé Bitteur
*/
public class GlyphsBuilder
{
//~ Static fields/initializers ---------------------------------------------
/** Specific application parameters */
private static final Constants constants = new Constants();
/** Usual logger utility */
private static final Logger logger = LoggerFactory.getLogger(GlyphsBuilder.class);
//~ Instance fields --------------------------------------------------------
/** The dedicated system */
private final SystemInfo system;
/** The global sheet scale */
private final Scale scale;
/** Global hosting nest for glyphs */
private final Nest nest;
/** Margins for a stem */
private final int stemXMargin;
private final int stemYMargin;
//~ Constructors -----------------------------------------------------------
//---------------//
// GlyphsBuilder //
//---------------//
/**
* Creates a system-dedicated builder of glyphs.
*
* @param system the dedicated system
*/
public GlyphsBuilder (SystemInfo system)
{
this.system = system;
Sheet sheet = system.getSheet();
scale = sheet.getScale();
nest = sheet.getNest();
// Cache parameters
stemXMargin = scale.toPixels(constants.stemXMargin);
stemYMargin = scale.toPixels(constants.stemYMargin);
}
//~ Methods ----------------------------------------------------------------
//------------//
// buildGlyph //
//------------//
/**
* Build a glyph from a collection of sections, with a link back
* from the sections to the glyph.
*
* @param scale the context scale
* @param sections the provided members of the future glyph
* @return the newly built glyph
*/
public static Glyph buildGlyph (Scale scale,
Collection<Section> sections)
{
Glyph glyph = new BasicGlyph(scale.getInterline());
for (Section section : sections) {
glyph.addSection(section, Glyph.Linking.LINK_BACK);
}
return glyph;
}
//----------------//
// retrieveGlyphs //
//----------------//
/**
* Browse through the provided sections not assigned to known
* glyphs, and build new glyphs out of connected sections.
*
* @param sections the sections to browse
* @param nest the nest to host glyphs
* @param scale the sheet scale
*/
public static List<Glyph> retrieveGlyphs (List<Section> sections,
Nest nest,
Scale scale)
{
List<Glyph> created = new ArrayList<>();
// Reset section processed flag
for (Section section : sections) {
if (!section.isKnown()) {
section.setProcessed(false);
} else {
section.setProcessed(true);
}
}
// Browse the various unrecognized sections
for (Section section : sections) {
// Not already visited ?
if (!section.isProcessed()) {
// Let's build a new glyph around this starting section
Glyph glyph = new BasicGlyph(scale.getInterline());
considerConnection(glyph, section);
// Insert this newly built glyph into nest (no system invloved)
glyph = nest.addGlyph(glyph);
created.add(glyph);
}
}
return created;
}
//----------//
// addGlyph //
//----------//
/**
* Add a brand new glyph as an active glyph in proper system and nest.
* 'Active' means that all member sections are set to point back to the
* containing glyph.
*
* @param glyph the brand new glyph
* @return the original glyph as inserted in the glyph nest
*/
public Glyph addGlyph (Glyph glyph)
{
glyph = nest.addGlyph(glyph);
system.addToGlyphsCollection(glyph);
return glyph;
}
//------------//
// buildGlyph //
//------------//
/**
* Build a glyph from a collection of sections, with a link back
* from the sections to the glyph, using the system scale.
*
* @param sections the provided members of the future glyph
* @return the newly built glyph
*/
public Glyph buildGlyph (Collection<Section> sections)
{
return buildGlyph(scale, sections);
}
//------------------------//
// buildTransientCompound //
//------------------------//
/**
* Make a new transient glyph out of a collection of (sub) glyphs,
* by merging all their member sections.
*
* @param parts the collection of (sub) glyphs
* @return the brand new (compound) glyph
*/
public Glyph buildTransientCompound (Collection<Glyph> parts)
{
// Gather all the sections involved
Collection<Section> sections = new HashSet<>();
for (Glyph part : parts) {
sections.addAll(part.getMembers());
}
return buildTransientGlyph(sections);
}
//---------------------//
// buildTransientGlyph //
//---------------------//
/**
* Make a new transient glyph out of a collection of sections.
*
* @param sections the collection of sections
* @return the brand new transientglyph
*/
public Glyph buildTransientGlyph (Collection<Section> sections)
{
// Build a glyph from all sections
Glyph compound = new BasicGlyph(scale.getInterline());
for (Section section : sections) {
compound.addSection(section, Glyph.Linking.NO_LINK_BACK);
}
// Make sure we get access to original forbidden shapes if any
Glyph original = nest.getOriginal(compound);
if (original != null) {
compound = original;
}
// Compute glyph parameters
computeGlyphFeatures(compound);
return compound;
}
//----------------------//
// computeGlyphFeatures //
//----------------------//
/**
* Compute all the features that will be used to recognize the
* glyph at hand.
* (it's a mix of moments plus a few other characteristics).
*
* @param glyph the glyph at hand
*/
public void computeGlyphFeatures (Glyph glyph)
{
if (glyph.isVip()) {
logger.debug("computeGlyphFeatures for {}", glyph.idString());
}
// Mass center (which makes sure moments are available)
glyph.getCentroid();
Point center = glyph.getAreaCenter();
Staff staff = system.getScoreSystem()
.getStaffAt(center);
// Connected stems
int stemNb = 0;
for (HorizontalSide side : HorizontalSide.values()) {
Glyph stem = lookupStem(side, system.getGlyphs(), glyph);
glyph.setStem(stem, side);
if (stem != null) {
stemNb++;
}
}
glyph.setStemNumber(stemNb);
// Has a related ledger ?
glyph.setWithLedger(
checkDashIntersect(
system.getGlyphs(),
ledgerBox(glyph.getBounds())));
// Vertical position wrt staff
glyph.setPitchPosition(staff.pitchPositionOf(center));
}
//---------------//
// registerGlyph //
//---------------//
/**
* Just register this glyph (as inactive) in order to persist glyph
* info such as TextInfo.
* Use {@link #addGlyph} instead to fully add the glyph as active.
*
* @param glyph the glyph to just register
* @return the proper (original) glyph
* @see #addGlyph
*/
public Glyph registerGlyph (Glyph glyph)
{
// Insert in nest, which assigns an id to the glyph
Glyph oldGlyph = nest.registerGlyph(glyph);
system.addToGlyphsCollection(oldGlyph);
return oldGlyph;
}
//-------------//
// removeGlyph //
//-------------//
/**
* Remove a glyph from the containing system glyph list.
*
* @param glyph the glyph to remove
*/
public void removeGlyph (Glyph glyph)
{
system.removeFromGlyphsCollection(glyph);
// Cut link from its member sections, if pointing to this glyph
glyph.cutSections();
}
//----------------//
// retrieveGlyphs //
//----------------//
/**
* In a given system area, browse through all sections not assigned
* to known glyphs, and build new glyphs out of connected sections.
*
* @param compute if true, compute the characteristics of the created glyphs
*/
public void retrieveGlyphs (boolean compute)
{
// Consider all unknown vertical & horizontal sections
List<Section> allSections = new ArrayList<>();
allSections.addAll(system.getVerticalSections());
allSections.addAll(system.getHorizontalSections());
List<Glyph> glyphs = retrieveGlyphs(allSections, nest, scale);
// Record them into the system
for (Glyph glyph : glyphs) {
system.addToGlyphsCollection(glyph);
// Make sure all aggregated sections belong to the same system
SystemInfo alienSystem = glyph.getAlienSystem(system);
if (alienSystem != null) {
removeGlyph(glyph);
// Publish the error on north side only of the boundary
SystemInfo north = (system.getId() < alienSystem.getId())
? system : alienSystem;
north.getScoreSystem()
.addError(glyph, "Glyph crosses system south boundary");
}
}
if (compute) {
// Force update for features of ALL system glyphs, since the mere
// existence of new glyphs may impact the characteristics of others
// (example of stems nearby)
for (Glyph glyph : system.getGlyphs()) {
computeGlyphFeatures(glyph);
}
}
}
//-----------//
// stemBoxOf //
//-----------//
/**
* Report an enlarged box of a given (stem) glyph.
*
* @param stem the stem
* @return the enlarged stem box
*/
public Rectangle stemBoxOf (Glyph stem)
{
Rectangle box = new Rectangle(stem.getBounds());
box.grow(stemXMargin, stemYMargin);
return box;
}
//-----------//
// stemBoxOf //
//-----------//
/**
* Report the stem lookup box on the specified side only
*
* @param stem the stem glyph
* @param side the desired side for the box
* @return the proper stem side box
*/
public Rectangle stemBoxOf (Glyph stem,
HorizontalSide side)
{
Rectangle box = stem.getBounds();
int width = box.width;
box.grow(stemXMargin, stemYMargin);
box.width = 2 * stemXMargin;
if (side == HorizontalSide.RIGHT) {
box.x += width;
}
return box;
}
//--------------------//
// considerConnection //
//--------------------//
/**
* Consider all sections transitively connected to the provided
* section in order to populate the provided glyph.
*
* @param glyph the provided glyph
* @param section the section to consider
*/
private static void considerConnection (Glyph glyph,
Section section)
{
// Check whether this section is suitable to expand the glyph
if (!section.isProcessed()) {
section.setProcessed(true);
glyph.addSection(section, Glyph.Linking.NO_LINK_BACK);
// Add recursively all linked sections in the lag
// Incoming ones
for (Section source : section.getSources()) {
considerConnection(glyph, source);
}
// Outgoing ones
for (Section target : section.getTargets()) {
considerConnection(glyph, target);
}
// Sections from other orientation
for (Section other : section.getOppositeSections()) {
considerConnection(glyph, other);
}
}
}
//--------------------//
// checkDashIntersect //
//--------------------//
private boolean checkDashIntersect (Iterable<Glyph> items,
Rectangle box)
{
for (Glyph item : items) {
if (item.getShape() == Shape.LEDGER
&& item.getBounds().intersects(box)) {
return true;
}
}
return false;
}
//-----------//
// ledgerBox //
//-----------//
private Rectangle ledgerBox (Rectangle rect)
{
Rectangle box = new Rectangle(rect);
box.grow(0, stemYMargin);
return box;
}
//------------//
// lookupStem //
//------------//
private Glyph lookupStem (HorizontalSide side,
Collection<Glyph> glyphs,
Glyph glyph)
{
if (glyph.isStem()) {
return null;
}
// Box for stem(s) lookup
final Rectangle box = stemBoxOf(glyph, side);
final List<Glyph> stems = new ArrayList<>();
for (Glyph s : glyphs) {
// Check bounding box intersection
if (s.isStem() && s.isActive() && s.getBounds().intersects(box)) {
// Use section intersection for confirmation
Rectangle b = stemBoxOf(s);
for (Section section : glyph.getMembers()) {
if (section.intersects(b)) {
stems.add(s);
break;
}
}
}
}
// Pick best stem found, if any
if (stems.isEmpty()) {
return null;
} else {
if (stems.size() > 1) {
Collections.sort(stems, new Comparator<Glyph>()
{
@Override
public int compare (Glyph g1,
Glyph g2)
{
// Use ordinate overlap
int overlap1 = box.intersection(g1.getBounds()).height;
int overlap2 = box.intersection(g2.getBounds()).height;
return Integer.compare(overlap1, overlap2);
}
});
}
return stems.get(0);
}
}
//~ Inner Classes ----------------------------------------------------------
//-----------//
// Constants //
//-----------//
private static final class Constants
extends ConstantSet
{
//~ Instance fields ----------------------------------------------------
Scale.Fraction ledgerHeighten = new Scale.Fraction(
0.1,
"Box heightening to check intersection with ledger");
//
Scale.Fraction stemXMargin = new Scale.Fraction(
0.1d, //0.05,
"Box widening to check intersection with stem");
//
Scale.Fraction stemYMargin = new Scale.Fraction(
0.2d, //0.1,
"Box heightening to check intersection with stem");
}
}